1. Обзор
В этой статье мы расскажем о некоторых важных правилах, используемых в инструментах анализа кода, таких как FindBugs, PMD и CheckStyle.
2. Цикломатическая сложность
2.1. Что такое цикломатическая сложность?
Сложность кода — важная, но трудная для измерения метрика. PMD предлагает надежный набор правил в разделе «Правила размера кода» . Эти правила предназначены для обнаружения нарушений, касающихся размера методов и сложности структуры.
CheckStyle известен своей способностью анализировать код в соответствии со стандартами кодирования и правилами форматирования. Однако он также может обнаруживать проблемы в разработке классов/методов, вычисляя некоторые метрики сложности .
Одним из наиболее важных измерений сложности, представленных в обоих инструментах, является CC (Cyclomatic Complexity).
Значение CC можно рассчитать, измерив количество независимых путей выполнения программы.
Например, следующий метод даст цикломатическую сложность 3:
public void callInsurance(Vehicle vehicle) {
if (vehicle.isValid()) {
if (vehicle instanceof Car) {
callCarInsurance();
} else {
delegateInsurance();
}
}
}
CC учитывает вложенность условных операторов и логических выражений, состоящих из нескольких частей.
Вообще говоря, код со значением CC выше 11 считается очень сложным, его трудно тестировать и поддерживать.
Некоторые общие значения, используемые инструментами статического анализа, показаны ниже:
- 1-4: низкая сложность — легко тестировать
- 5-7: умеренная сложность – терпимо
- 8-10: высокая сложность — следует рассмотреть возможность рефакторинга для облегчения тестирования.
- 11 + очень высокая сложность — очень сложно протестировать
Уровень сложности также влияет на тестируемость кода: чем выше CC, тем сложнее реализовать соответствующие тесты . На самом деле значение цикломатической сложности точно показывает количество тестовых случаев, необходимых для достижения 100%-го показателя покрытия ветвей.
Блок-схема, связанная с методом callInsurance()
:
Возможные пути выполнения:
- 0 => 3
- 0 => 1 => 3
- 0 => 2 => 3
С математической точки зрения CC можно рассчитать по следующей простой формуле:
CC = E - N + 2P
- E: Общее количество ребер
- N: общее количество узлов
- P: Общее количество точек выхода
2.2. Как уменьшить цикломатическую сложность?
Чтобы написать значительно менее сложный код, разработчики могут использовать разные подходы, в зависимости от ситуации:
- Избегайте написания длинных операторов
switch
, используя шаблоны проектирования, например, шаблоны построителя и стратегии могут быть хорошими кандидатами для решения проблем размера и сложности кода. - Напишите многократно используемые и расширяемые методы, разделив структуру кода на модули и реализуя принцип единой ответственности .
- Соблюдение других правил размера кода PMD может иметь прямое влияние на CC , например, правило чрезмерной длины метода, слишком много полей в одном классе, список чрезмерных параметров в одном методе и т. д.
Вы также можете рассмотреть следующие принципы и шаблоны, касающиеся размера и сложности кода, например, принцип KISS (Keep It Simple and Stupid) и DRY (Don't Repeat Yourself) .
3. Правила обработки исключений
Дефекты, связанные с исключениями, могут быть обычным явлением, но некоторые из них сильно недооценены и должны быть исправлены, чтобы избежать критических сбоев в рабочем коде.
PMD и FindBugs предлагают несколько наборов правил, касающихся исключений. Вот наш выбор того, что может считаться критическим в программе Java при обработке исключений.
3.1. Не выбрасывать исключение в finally
Как вы, возможно, уже знаете, блок finally{}
в Java обычно используется для закрытия файлов и освобождения ресурсов, его использование для других целей можно рассматривать как запах кода .
Типичная подверженная ошибкам процедура выдает исключение внутри блока finally{}
:
String content = null;
try {
String lowerCaseString = content.toLowerCase();
} finally {
throw new IOException();
}
Этот метод должен генерировать NullPointerException
, но, как ни странно, он генерирует IOException
, что может ввести вызывающий метод в заблуждение и заставить его обрабатывать неправильное исключение.
3.2. Возвращение в блок finally
Использование оператора return внутри блока finally{}
может сбивать с толку. Причина, по которой это правило так важно, заключается в том, что всякий раз, когда код генерирует исключение, оно отбрасывается оператором return
.
Например, следующий код выполняется без каких-либо ошибок:
String content = null;
try {
String lowerCaseString = content.toLowerCase();
} finally {
return;
}
Исключение NullPointerException
не было перехвачено, но все же отброшено оператором return в блоке finally .
3.3. Невозможно закрыть поток при исключении
Закрытие потоков — одна из основных причин, почему мы используем блок finally
, но это не тривиальная задача, как кажется.
Следующий код пытается закрыть два потока в блоке finally :
OutputStream outStream = null;
OutputStream outStream2 = null;
try {
outStream = new FileOutputStream("test1.txt");
outStream2 = new FileOutputStream("test2.txt");
outStream.write(bytes);
outStream2.write(bytes);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
outStream.close();
outStream2.close();
} catch (IOException e) {
// Handling IOException
}
}
Если инструкция outStream.close()
выдает исключение IOException
, то outStream2.close()
будет пропущен.
Быстрым решением будет использование отдельного блока try/catch для закрытия второго потока:
finally {
try {
outStream.close();
} catch (IOException e) {
// Handling IOException
}
try {
outStream2.close();
} catch (IOException e) {
// Handling IOException
}
}
Если вам нужен хороший способ избежать последовательных блоков try/catch , проверьте метод
IOUtils.closeQuiety из Apache commons, он упрощает обработку закрытия потоков без создания IOException
.
5. Плохая практика
5.1. Класс определяет compareto() и использует Object.equals()
Всякий раз, когда вы реализуете метод compareTo()
, не забудьте сделать то же самое с методом equals()
, иначе результаты, возвращаемые этим кодом, могут сбивать с толку:
Car car = new Car();
Car car2 = new Car();
if(car.equals(car2)) {
logger.info("They're equal");
} else {
logger.info("They're not equal");
}
if(car.compareTo(car2) == 0) {
logger.info("They're equal");
} else {
logger.info("They're not equal");
}
Результат:
They're not equal
They're equal
Чтобы устранить путаницу, рекомендуется убедиться, что Object.equals()
никогда не вызывается при реализации Comparable,
вместо этого вы должны попытаться переопределить его примерно так:
boolean equals(Object o) {
return compareTo(o) == 0;
}
5.2. Возможное разыменование нулевого указателя
NullPointerException
(NPE) считается наиболее часто встречающимся исключением
в программировании на Java, и FindBugs жалуется на разыменование Null PointeD, чтобы избежать его выбрасывания.
Вот самый простой пример броска NPE:
Car car = null;
car.doSomething();
Самый простой способ избежать NPE — выполнить нулевую проверку:
Car car = null;
if (car != null) {
car.doSomething();
}
Нулевые проверки могут избежать NPE, но при широком использовании они, безусловно, влияют на читабельность кода.
Итак, вот некоторые методы, используемые для избежания NPE без нулевых проверок:
- Избегайте ключевого слова
null
при кодировании:
это простое правило, избегайте использования ключевого словаnull
при инициализации переменных или возврате значений . - Используйте аннотации
@NotNull
и@Nullable
- Используйте
java.util.Необязательно
- Реализуйте шаблон нулевого объекта
6. Заключение
В этой статье мы сделали общий обзор некоторых критических дефектов, обнаруженных инструментами статического анализа, с основными рекомендациями по устранению обнаруженных проблем.
Вы можете просмотреть полный набор правил для каждого из них, перейдя по следующим ссылкам: FindBugs , PMD .