Теория и практика Java: Избавьтесь от ошибок

Такие инструменты контроля, как FindBugs, обеспечивают второй уровень защиты от стандартных ошибок кодирования

Большинство советов по стилю программирования нацелены на создание высококачественного и удобного в сопровождении кода. И это правильно, так как проще всего исправить ошибку до того как она сделана (предотвращение...). К сожалению, простого предотвращения не всегда достаточно, и в то время как существует достаточно много хороших инструментальных средств, помогающих создать качественный код, гораздо меньше инструментальных средств помогают анализировать, сопровождать и улучшать качество уже существующего кода. В этом месяце обозреватель Брайан Гетц на основе публикации Криса Гриндстаффа Введение в FindBugs показывает, каким образом данное средство статического анализа поможет вам проанализировать код с точки зрения соответствия принципам проектирования, описанным в предыдущих выпусках данной рубрики.

Брайан Гетц, главный консультант, Quiotix

Брайан Гетц (Brian Goetz) - консультант по ПО и последние 15 лет работал профессиональными разработчиком ПО. Сейчас он является главным консультантом в фирме Quiotix, занимающейся разработкой ПО и консалтингом и находящейся в Лос-Альтос, Калифорния. Следите за публикациями Брайана в популярных промышленных изданиях. Вы можете связаться с Брайаном по адресу brian@quiotix.com



23.01.2007

Достаточно трудно написать поточно-ориентированный класс, однако проанализировать уже существующий класс на поточно-ориентированность еще труднее, так же как и усовершенствовать его таким образом, чтобы он остался поточно-ориентированным. Большая часть знаний о том, как работает класс (или должен работать) забывается вскоре после того, как он написан, а подразумеваемые допущения, инварианты и предполагаемые случаи использования, которые понятны разработчику, никогда не записываются в примечаниях проекта, комментариях или документации. Всегда сложнее иметь дело с уже существующим кодом, чем с новым.

Потребность: более совершенные инструментальные средства анализа кода

Бесспорно самым лучшим моментом для обеспечения высокого качества кода - это непосредственно написание кода, поскольку именно в это время вы лучше всего понимаете каким образом все собрано. Большинство советов посвящено (почитайте данную рубрику!) тому, как написать качественный код, но не всегда выпадает шанс создавать все с чистого листа, или тратить на написание столько времени, сколько хочется. Итак, что делать в этом случае? Зачастую разработчики отдают предпочтение переписыванию (в конце концов, создание нового кода гораздо интереснее, чем исправление чьего-то или даже своего собственного, кода с ошибками), но даже это является роскошью и нередко обменивает известные проблемы сегодняшнего дня на завтрашние неизвестные. Все что необходимо - это лучшие инструментальные средства для анализа и проверки существующих оснований кода, которые помогут разработчикам проверять код и находить ошибки.

Я рад сообщить, что положение инструментальных средств автоматического анализа и проверки кода значительно улучшилось с появлением FindBugs. До настоящего момента большинство инструментальных средств контроля пыталось либо взять на себя трудную задачу доказать, что программа корректна, либо концентрировали свое внимание на таких несущественных вопросах, как, например, форматирование кода и соглашения об именовании, или, в лучшем случае, шаблоны простых ошибок, например, само-присвоение, неиспользуемые поля или потенциальные ошибки, например, неиспользуемые параметры метода или методы общего пользования, которые могли быть описаны как приватные или защищенные. Он может помочь и указать в каком месте ваш код отступает, намеренно или ненамеренно, от принципов корректного проектирования. (Введение в FindBugs представлено в статьях Криса Гриндстаффа, "FindBugs, часть 1: Улучшение качества кода (FindBugs, Part 1: Improve the quality of your code)" и "FindBugs, часть 2: Разработка пользовательских детекторов (FindBugs, Part 2: Writing custom detectors).")


Рекомендации по проектированию и шаблоны ошибок

Для каждого шаблона ошибок существует соответствующий запретительный элемент рекомендации по проектированию призывающий нас избегать определенных шаблонов ошибок. Следовательно, если FindBugs является детектором шаблона ошибок, то, само собой разумеется, что он также должен выступать в роли инструментальных средств для измерения уровня соответствия набору принципов проектирования. Большинство выпусков Теории и практики Java посвящено отдельным элементам рекомендаций по проектированию (или соответствующему шаблону ошибки). В данном выпуске я объясню, каким образом FindBugs может быть полезен для обеспечения уверенности в том, что рекомендации по проектированию выполняются в существующем основании кода. Давайте перефразируем некоторые из прошлых рекомендаций и посмотрим, как FindBugs может помочь обнаружить то, что вы пропустили.

Дебаты об исключениях

В "Дебатах об исключениях," одним из аргументов против проверенных исключений было то, что они могут достаточно легко "запутать" - отловить исключение и ни предпринять корректирующее действие, ни отправить другого исключения, как показано в листинге 1. Такое неумелое обращение зачастую происходит когда, при создании прототипа, кодируется пустой блок catch исключительно для того, чтобы скомпилировать программу, и с намерением вернуться к нему впоследствии и наполнить его какой-либо стратегией по обработке ошибок. В то время как некоторые приводят этот сценарий в качестве примера по непригодности данного подхода для обработки исключений, взятого проектом языка Java, я считаю, что здесь дело только в неумении использовать верные инструментальные средства. FindBugs может легко найти и пометить эти пустые блоки catch. Если вы не хотите обращать внимание на исключения, то достаточно легко можно добавить комментарий для того, чтобы читатели знали, что вы игнорируете его сознательного, а не просто забыли его обработать.

Листинг 1. Неумелое обращение с исключением
try {
  mumbleFoo();
}
catch (MumbleFooException e) { 
}

Устранение

В статье "Устранение (Hashing it out)," я обрисовал основные правила для корректной подмены Object.equals() и Object.hashCode(), а именно, что у одинаковых объектов (в соответствии с equals()) должны быть одинаковые значения hashCode(). Несмотря на то, что, узнав данное правило однажды, его достаточно просто выполнять (в некоторых интегрированных средах разработки есть функции-мастера для последовательного выполнения описания обоих объектов вместо вас), если вы замените один из этих двух методов и забудете заменить другой, то отследить просмотром такую ошибку достаточно сложно - так как ошибка находится не в том коде, который присутствует, а в том, которого нет.

В FindBugs есть детектор для нахождения экземпляров данной проблемы, например, замена только equals(), но не hashCode(), или замена только hashCode(), но не equals(). Такие детекторы FindBugs являются простейшими, так как им нужно проверить только набор характеристик методов в классе и определить были ли заменены и equals() и hashCode(), или только один из них. Так же есть вероятность ошибочного определения equals() с аргументом иного типа, чем Object; несмотря на то, что в целом конструкция верна, она будет работать не так, как вы думаете. Детектор Covariant Equals будет обнаруживать такие спорные замены, как:

  public void boolean equals(Foo other) { ... }

К таким детекторам относится и детектор Confusing Method Names (запутывающих имен методов), который срабатывает для методов с такими названиями, как hashcode() и tostring(), классов, содержащих методы, имена которых отличаются только использованием заглавных букв, или методы, которые называются также, как и конструктор суперкласса. Несмотря на то, что названия этих методов корректны с точки зрения спецификации языка, есть вероятность, что они не являются тем, для чего предназначены. Точно также детектор Serialization (сериализации) будет срабатывать, если поле serialVersionUID не является final или long или static.

Финализаторы - враги

В статье "Сборка мусора и производительность (Garbage collection and performance)," я постарался отговорить вас от использования финализаторов. Финализаторам свойственны значительные затраты с точки зрения производительности и отсутствие гарантий, что они будут выполнены в прогнозируемое количество времени (или вообще). Однако существуют определенные ситуации, когда Вам приходится использовать финализаторы и, в этом случае, вы можете допустить определенные ошибки. Если вы вынуждены использовать финализатор, то в общем виде его структура должна выглядеть так, как показано в листинге 2:

Листинг 2. Корректное описание финализатора
  protected void finalize() { 
    try {
      doStuff();
    }
    finally { 
      super.finalize();
    }
  }

FindBugs определяет множество подозрительных конструкций финализаторов, например:

  • Пустой финализатор (который отрицает действие финализатора суперкласса)
  • Фиктивные финализаторы (которые только вызывают super.finalize(), но могут блокировать определенные оптимизации выполнения)
  • Явное инициирование финализаторов (вызывая finalize() из кода пользователя)
  • Общедоступные финализаторы (финализаторы должны объявляться как protected)
  • Финализаторы, которые не вызывают super.finalize()

Примеры с шаблонами таких ошибок приведены в листинге 3:

Листинг 3. Типичные ошибки финализаторов
  // negates effect of superclass finalizer
  protected void finalize() { }

  // fails to call superclass finalize method
  protected void finalize() { doSomething(); }

  // useless (or worse) finalizer
  protected void finalize() { super.finalize(); }

  // public finalizer
  public void finalize() { try { doSomething(); } finally { super.finalize() } }

Также в статье "Сборка мусора и производительность," я упоминал о еще одной проблеме сборки мусора: явные вызовы System.gc(). Такие явные обращения практически всегда являются некорректными попытками "помочь" или "обмануть" сборщик мусора, и зачастую они больно бьют по производительности. FindBugs способен обнаруживать явные вызовы System.gc() и помечать их (в вируальных машинах Java Sun, у вас есть возможность отключить явную сборку мусора с помощью параметра запуска -XX:+DisableExplicitGC).

Безопасные методы построения

В статье "Безопасные методы построения (Safe construction techniques)," я показал, каким образом позволение ссылке ускользнуть от конструктора может привести к серьезным проблемам. С тех пор, риск позволить указателю this ускользнуть от построения стал еще серьезнее - новая модель памяти Java (описанная JSR 133 и реализованная в инструментальном пакете для разработки программ на Java1.5) сводит к нулю все гарантии безопасности инициализации, если ссылке на объект разрешено ускользнуть от конструктора.

Ссылка на объект может ускользать от своего конструктора различными способами, и прямо и косвенно. Хранение указателя this в статической переменной или структуре данных недозволенно, но есть и более утонченные способы, чтобы позволить указателю ускользнуть от конструкции, например, объявить ссылку на нестатический внутренний класс или начать поток из конструктора (что практически всегда приводит к объявлению ссылки на новый поток). В FindBugs представлен детектор для обнаружения случаев, когда поток начат из конструктора. Хотя сейчас он не способен обнаруживать все угрозы такого типа, но вероятно, что в будущих версиях будут присутствовать детекторы и для других таких безопасных шаблонов инициализации.

Забота о модели памяти

В статье "Исправление модели памяти Java, часть 1 (Fixing the Java Memory Model, Part 1)," я прокомментировал основное правило синхронизации – всякий раз, когда происходит считывание переменной, которая возможно была записана другим потоком, или запись переменной, которая возможно будет считываться другим потоком, необходимо выполнять синхронизацию. Об этом правиле очень легко "забыть", особенно при выполнении считывания, что приведет к возникновению риска с точки зрения безопасности ориентирования вашей программы на потоки. Чаще всего ошибки такого типа возникают при обслуживании класса, который поначалу синхронизировался надлежащим образом, но требования поточного ориентирования не были поняты обслуживающим персоналом до конца.

К счастью, в FindBugs есть набор детекторов, которые могут помочь определить те классы, которые некорректно синхронизируются. Детектор Inconsistent Synchronization (противоречивой синхронизации) – это, пожалуй, самый сложный детектор в FindBugs - он анализирует всю программу, а не отдельные методы, и использует анализ потока данных, чтобы определить момент, когда возникает блокировка, и эвристические процедуры, чтобы сделать вывод о том, что класс гарантирует безопасность потоков. В своей основе, для каждого поля детектор просматривает шаблон доступов к этому полю, и если большинство обращений выполнено с применением синхронизации, то обращения без синхронизации помечаются как потенциальные ошибки. Аналогичным образом, детектор Inconsistent Synchronization (противоречивой синхронизации) выдаст предупреждение, если схема настройки свойства синхронизирована, а получатель – нет.

Кроме противоречивой синхронизации, есть еще набор других детекторов стандартных ошибок потока, например, ожидание монитора с удержанием двух блокировок (которое, хотя и необязательно является ошибкой, может привести к блокировке), использование повторно проверенного блокирующего словосочетания, некорректная ленивая инициализация не изменяющихся полей, активизация run() на потоке вместо того, чтобы начать его, активизация Thread.start() из конструктора или вызов wait() без его инкапсуляции в цикле.

Видоизменять или не видоизменять

В статье "Видоизменять или не видоизменять (To mutate, or not to mutate)" (и в других статьях), я описал преимущества неизменности - неизменные объекты не могут попасть в противоречивое состояние. По своей природе они являются поточно-ориентированными (предполагая, что их неизменность гарантируется использованием ключевого слова final), и вы можете легко распределять и кэшировать ссылки на неизменные объекты, даже не выполняя операции их копирования или клонирования.

Ключевое слово final было добавлено в язык Java, чтобы помочь разработчикам в создании неизменных классов, и предоставить компиляторам и средам выполнения возможность отыскивать оптимальное решение на основе заявленной неизменности. Однако, в то время как поля могут быть результирующими, у элементов массивов такой возможности просто нет. Объект можно сделать неизменным посредством правильного использования результирующих и приватных полей, однако, если состояние объекта включает массивы, то важно предупредить появление ссылок на эти внутренние массивы от ускользающих методов классов. Класс, показанный в листинге 4, претендует на неизменность, но таковым не является, так как вызывающий оператор может изменить массив состояний после вызова getStates(). (Похожая ожидаемая ошибка происходит, если изменяемый класс возвращает ссылку на изменяемый объект, и к тому времени, когда вызывающий оператор будет использовать массив, его содержимое уже может быть изменено.) Хотя и считается уязвимостью для "вредоносного кода" (а большинство разработчиков не задумывается о "вредоносном коде" только потому, что их система не подгружает никаких "непроверенных" классов), данное словосочетание может привести к серьезным проблемам даже в отсутствие вредоносного кода. Было бы лучше либо вернуть не поддающийся изменению List, либо клонировать массив до того, как он будет возвращен. FindBugs способен обнаруживать ошибки, подобные приведенной в getStates() (смотрите листинг 4) - хотя ему может быть неизвестно, что класс States должен быть неизменным, он знает, что данный получатель возвращает маркер изменяемому приватному массиву и помечает его соответствующим образом.

Листинг 4. Ложный возврат ссылки на изменяемый массив
  public class States {
    private final String[] states = { "AL", "AR", "AZ", ... };
    public boolean isState(String stateCandidate) { ... }
    public String[] getStates() { return states; }
  }

Простых ошибок не бывает

FindBugs - это действительно удивительное инструментальное средство - ему удается находить настоящие ошибки практически постоянно. Вам может прийти мысль, что шаблоны некоторых ошибок, которые он ищет, например изменяемая самооценка, являются слишком простыми, для того чтобы тратить время на их поиск, но вы ошибаетесь - каждый из набора детекторов FindBugs обнаружил ошибки в протестированном и профессионально разработанном коде. В вашем коде скрываются глупые ошибки? Загрузите копию FindBugs и проверьте его на своем коде. Результаты просветят и встревожат вас.

Ресурсы

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=191775
ArticleTitle=Теория и практика Java: Избавьтесь от ошибок
publish-date=01232007