«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 =\u003e 3 0 =\u003e 1 =\u003e 3 0 =\u003e 2 =\u003e 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 жалуется на разыменование нулевого указателя, чтобы избежать его создания.
Вот самый простой пример создания NPE:
Car car = null;
car.doSomething();
Самый простой способ избежать NPE — выполнить проверку на ноль:
Car car = null;
if (car != null) {
car.doSomething();
}
Нулевые проверки могут избегать NPE, но при активном использовании они безусловно, влияют на читаемость кода.
Итак, вот некоторые приемы, используемые для избегания NPE без проверки null:
-
Избегайте ключевого слова null при кодировании: Это простое правило, избегайте использования ключевого слова null при инициализации переменных или возврате значений Используйте аннотации @NotNull и @Nullable Используйте аннотации @NotNull и @Nullable java.util.Optional Реализация шаблона нулевого объекта
6. Заключение
В этой статье мы сделали общий обзор некоторых критических дефектов, обнаруженных инструментами статического анализа, с основными рекомендациями по надлежащему устранению обнаруженных проблем. .
Вы можете просмотреть полный набор правил для каждого из них, перейдя по следующим ссылкам: FindBugs, PMD.