Наличие другой пары глаз, сканирующих ваш код, всегда полезно и помогает вам выявлять ошибки до того, как вы остановите производство. Вам не нужно быть экспертом, чтобы просмотреть чей-то код. Некоторый опыт работы с языком программирования и контрольный список обзора должны помочь вам начать работу. Мы составили список вещей, которые вы должны иметь в виду при просмотре кода Java. Читайте дальше!
1. Следуйте соглашениям о коде Java
Соблюдение языковых соглашений помогает быстро просмотреть код и разобраться в нем, тем самым улучшая читабельность. Например, все имена пакетов в Java пишутся в нижнем регистре, константы во всех заглавных буквах, имена переменных в camelCase и т. Д. Найдите полный список конвенций здесь .
Некоторые команды разрабатывают свои собственные соглашения, так что будьте гибки в таких случаях!
2. Замените императивный код лямбдами и потоками
Если вы используете Java 8+, замена циклов и чрезвычайно подробных методов потоками и лямбдами делает код более чистым. Лямбды и потоки позволяют писать функциональный код на Java. Следующий фрагмент фильтрует нечетные числа традиционным императивным способом:
ListoddNumbers = new ArrayList<>(); for (Integer number : Arrays.asList(1, 2, 3, 4, 5, 6)) { if (number % 2 != 0) { oddNumbers.add(number); } }
Это функциональный способ фильтрации нечетных чисел:
ListoddNumbers = Stream.of(1, 2, 3, 4, 5, 6) .filter(number -> number % 2 != 0) .collect(Collectors.toList());
3. Остерегайтесь исключения NullPointerException
При написании новых методов старайтесь по возможности избегать возврата нулей. Это может привести к исключениям нулевого указателя. В приведенном ниже фрагменте кода метод highest возвращает значение null, если в списке нет целых чисел.
class Items { private final Listitems; public Items(List items) { this.items = items; } public Integer highest() { if (items.isEmpty()) return null; Integer highest = null; for (Integer item : items) { if (items.indexOf(item) == 0) highest = item; else highest = highest > item ? highest : item; } return highest; } }
Перед непосредственным вызовом метода для объекта я рекомендую проверить наличие нулей, как показано ниже.
Items items = new Items(Collections.emptyList()); Integer item = items.highest(); boolean isEven = item % 2 == 0; // throws NullPointerException ❌ boolean isEven = item != null && item % 2 == 0 // ✅
Однако наличие нулевых проверок повсюду в вашем коде может быть довольно громоздким. Если вы используете Java 8+, рассмотрите возможность использования Необязательного
класса для представления значений, которые могут не иметь допустимых состояний. Это позволяет легко определять альтернативное поведение и полезно для методов цепочки.
В приведенном ниже фрагменте мы используем Java Stream API для поиска наибольшего числа с помощью метода, который возвращает Необязательно
. Обратите внимание, что мы используем Stream.reduce
, который возвращает Необязательное
значение.
public Optionalhighest() { return items .stream() .reduce((integer, integer2) -> integer > integer2 ? integer : integer2); } Items items = new Items(Collections.emptyList()); items.highest().ifPresent(integer -> { // ✅ boolean isEven = integer % 2 == 0; });
В качестве альтернативы вы также можете использовать аннотации, такие как @Nullable
или @NonNull
, что приведет к появлению предупреждений, если при создании кода возникнет конфликт с нулем. Например, передача аргумента @Nullable
методу, который принимает параметры @NonNull
.
4. Прямое назначение ссылок из клиентского кода полю
Ссылками, отображаемыми в клиентском коде, можно манипулировать, даже если поле является окончательным. Давайте лучше поймем это на примере.
private final Listitems; public Items(List items) { this.items = items; }
В приведенном выше фрагменте мы напрямую назначаем ссылку из клиентского кода полю. Клиент может легко изменять содержимое списка и манипулировать нашим кодом, как показано ниже.
Listnumbers = new ArrayList<>(); Items items = new Items(numbers); numbers.add(1); // This will change how items behaves as well
Вместо этого рассмотрите возможность клонирования ссылки или создания новой ссылки, а затем присвоения ее полю, как показано ниже:
private final Listitems; public Items(List items) { this.items = new ArrayList<>(items); } }
То же правило применяется и при возврате ссылок. Вам нужно быть осторожным, чтобы не выдать внутреннее изменчивое состояние.
5. Обращайтесь с исключениями осторожно
При перехвате исключений, если у вас несколько блоков перехвата, убедитесь, что последовательность блоков перехвата наиболее специфична для наименьшего. В приведенном ниже фрагменте исключение никогда не будет поймано во втором блоке, поскольку класс Exception
является матерью всех исключений.
try { stack.pop(); } catch (Exception exception) { // handle exception } catch (StackEmptyException exception) { // handle exception }
Если ситуация исправима и может быть обработана клиентом (потребителем вашей библиотеки или кода), то полезно использовать проверенные исключения. например. Исключение IOException
– это проверенное исключение, которое заставляет клиента обрабатывать сценарий, и в случае, если клиент решит повторно создать исключение, это должен быть сознательный призыв игнорировать исключение.
6. Поразмышляйте над выбором структур данных
Коллекции Java предоставляют Список массивов
, Список ссылок
, Вектор
, Стек
, Набор хэшей
, Хэш-карта
, Хэш-таблица
. Важно понимать плюсы и минусы каждого из них, чтобы использовать их в правильном контексте. Несколько советов, которые помогут вам сделать правильный выбор:
Карта
: Полезно, если у вас есть неупорядоченные элементы в виде пар ключ-значение и требуются эффективные операции поиска, вставки и удаления.Хэш-карта
,Хэш-таблица
,LinkedHashMap
– все это реализации интерфейсаКарта
.Список
: Очень часто используется для создания упорядоченного списка элементов. Этот список может содержать дубликаты.ArrayList
является реализацией интерфейсаСписок
. Список можно сделать потокобезопасным с помощьюКоллекции.synchronizedList
таким образом устраняется необходимость использованияВектора
. Здесь еще немного информации о том, почемуВектор
по сути устарел.Набор
: Аналогично списку, но не допускает дубликатов.HashSet
реализует интерфейсSet
.
7. Подумайте дважды, прежде чем разоблачать
В Java есть довольно много модификаторов доступа на выбор — общедоступный
, защищенный
, закрытый
. Если вы не хотите предоставлять доступ к методу в клиентском коде, возможно, вам захочется сохранить все закрытым
по умолчанию. Как только вы откроете API, пути назад уже не будет.
Например, у вас есть класс Библиотека
у этого есть следующий способ проверить книгу по названию:
public checkout(String bookName) { Book book = searchByTitle(availableBooks, bookName); availableBooks.remove(book); checkedOutBooks.add(book); } private searchByTitle(ListavailableBooks, String bookName) { ... }
Если вы не сохраняете метод searchByTitle
закрытым по умолчанию, и он становится открытым, другие классы могут начать использовать его и создавать логику поверх него, которую вы, возможно, хотели быть частью класса Library
. Это может нарушить инкапсуляцию класса Библиотеки
или может оказаться невозможным отменить/изменить позже, не нарушая чужой код. Разоблачайте сознательно!
8. Код для интерфейсов
Если у вас есть конкретные реализации определенных интерфейсов (например, Список массивов
или Список ссылок
) и если вы используете их непосредственно в своем коде, то это может привести к высокой сцепке. Использование интерфейса List
позволяет вам переключаться на реализацию в любое время в будущем, не нарушая никакого кода.
public Bill(Printer printer) { this.printer = printer; } new Bill(new ConsolePrinter()); new Bill(new HTMLPrinter());
В приведенном выше фрагменте кода использование интерфейса Принтер
позволяет разработчику перейти к другому конкретному классу HTML Принтер
.
9. Не заставляйте подгонять интерфейсы
Взгляните на следующий интерфейс:
interface BookService { ListfetchBooks(); void saveBooks(List books); void order(OrderDetails orderDetails) throws BookNotFoundException, BookUnavailableException; } class BookServiceImpl implements BookService { ...
Есть ли польза от создания такого интерфейса? Есть ли область применения для этого интерфейса, реализуемого другим классом? Является ли этот интерфейс достаточно универсальным, чтобы быть реализованным другим классом? Если ответ на все эти вопросы отрицательный, то я бы определенно рекомендовал избегать этого ненужного интерфейса, который вам придется поддерживать в будущем. Мартин Фаулер очень хорошо объясняет это в своем блоге .
Хорошо, тогда каков хороший вариант использования интерфейса? Допустим, у нас есть прямоугольник класса и
круг класса
, который имеет поведение для вычисления периметра. Если есть требование, подводя итог, периметр всех фигур — вариант использования полиморфизма, то наличие интерфейса имело бы больше смысла, как показано ниже.
interface Shape { Double perimeter(); } class Rectangle implements Shape { //data members and constructors @Override public Double perimeter() { return 2 * (this.length + this.breadth); } } class Circle implements Shape { //data members and constructors @Override public Double perimeter() { return 2 * Math.PI * (this.radius); } } public double totalPerimeter(Listshapes) { return shapes.stream() .map(Shape::perimeter) .reduce((a, b) -> Double.sum(a, b)) .orElseGet(() -> (double) 0); }
10. Переопределение хэш-кода когда переопределение равно
Объекты , которые равны по своим значениям , называются объектами значений . например, деньги, время. Такие классы должны переопределять метод equals
, чтобы возвращать значение true, если значения одинаковы. Метод equals
обычно используется другими библиотеками для сравнения и проверки равенства; следовательно, необходимо переопределить equals
. Каждый объект Java также имеет значение хэш-кода, которое отличает его от другого объекта.
class Coin { private final int value; Coin(int value) { this.value = value; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Coin coin = (Coin) o; return value == coin.value; } }
В приведенном выше примере мы переопределили только метод равно
объекта .
HashMapcoinCount = new HashMap () {{ put(new Coin(1), 5); put(new Coin(5), 2); }}; //update count for 1 rupee coin coinCount.put(new Coin(1), 7); coinCount.size(); // 3 🤯 why?
Мы ожидаем, что coin Count
обновит количество монет в 1 рупию до 7, так как мы переопределяем equals. Но HashMap
внутренне проверяет, равен ли хэш-код для 2 объектов, и только затем переходит к проверке равенства с помощью метода equals
. Два разных объекта могут иметь или не иметь один и тот же хэш-код, но два одинаковых объекта всегда должны иметь один и тот же хэш-код, как определено контрактом метода Хэш-код
. Таким образом, проверка хэш-кода в первую очередь является условием раннего выхода. Это означает, что оба метода равно
и Хэш-код
должны быть переопределены для выражения равенства.
Если вы пишете или просматриваете код Java, Deep Source может помочь вам автоматизировать обзоры кода и сэкономить массу времени. Просто добавьте файл .deep source.toml
в корень репозитория, и Deep Source сразу же подберет его для сканирования. Сканирование позволит найти возможности для улучшений в вашем коде и поможет вам исправить их с помощью полезных описаний.
Зарегистрируйтесь и убедитесь сами!
Оригинал: “https://dev.to/deepsource/guidelines-for-java-code-reviews-3096”