Источник: Javarevisited
Это руководство поможет вам лучше понять, как определенные сценарии или идиомы позволяют писать код более надежным и лаконичным.
Рост опыта в кодировании, рефакторинге и тестировании — это то, что отличает вас от ваших конкурентов. Вполне возможно, что ваш опыт растет, но вы до сих пор тратите лишнее время на исправление ошибок, изменение конфигурации, развертывание и поддержку, вместо того чтобы писать код или модульные тесты. Сегодня я хочу поделиться несколькими сценариями кодирования на Java, которые могут улучшить ваши навыки написания кода. Их также называют идиомы.
Идиомы или сценарии кодирования — это проверенный способ написания кода для определенных случаев. Они протестированы, поэтому в них нет ошибок. Используя такие идиомы, вы исключаете риск возникновения ошибок, когда пишете свой собственный код. Идиомы очень похожи на шаблоны и библиотеки, которые также можно повторно использовать, но с намного более низким уровнем сложности.
Одним из примеров идиомы можно назвать самый распространенный способ записи бесконечного цикла (с использованием where(true), а не for(;;)). Ниже я подобрал список моих любимых идиом программирования, которые вы можете использовать для написания лучшего, более чистого и надежного кода на Java.
1. Вызов equals() для строкового литерала или известного объекта
Долгое время при написании кода Java я использовал метод equals():
if(givenString.equals("YES")){
// do something.
}
Это читается хорошо, но есть некоторые риски. Вы можете предотвратить потенциальную ошибку NPE (NullPointerException), вызвав equals() для литерала String, если один объект оказался литералом Sring или для известного объекта, например:
"TRUE" .equals (givenBoolean)
"YES" .equals (givenString)
Если вы сделаете наоборот, например, GivenBoolean.equals("YES"), то тогда появится исключение NullPointerException, если givenBoolean равно нулю. Но если вы будете следовать этой идиоме, то код просто вернет false, не показывая NPE. Этот вариант намного лучше, безопаснее и надежней. И к тому же это один из популярных способов избежать исключения NullPointerException в Java.
2. Использование entrySet для перебора HashMap
Раньше я перебирал HashMap, используя набор ключей (key set), как показано ниже:
Set keySet = map.keyset();
for(Key k : keySet){
value v = map.get(k);
print(k, v)
}
Этот код выполняет еще один поиск для получения значения из Map, которое в худшем случае может быть O(n). Если вам нужны и ключ (key), и значение (value), то лучше перебирать набор записей, а не набор ключей.
Entry entrySet = map.entrySet();
for(Entry e : entrySet){
Key k = e.getKey();
Value v = e.getValue();
}
Это более эффективно, потому что вы получаете значение непосредственно от объекта, который всегда равен O(1).
3. Использование Enum в качестве синглтона
Было бы неплохо писать Singleton всего в одну строку, как здесь:
public enum Singleton{
INSTANCE;
}
Это потокобезопасный и надежный способ. Здесь Java гарантирует только один экземпляр даже в случае сериализации и десериализации.
4. Использование Arrays.asList() для инициализации Collection или List.of(), Set.of()
Раньше, если я заранее знал элементы, я инициализировал коллекцию следующим образом:
List listOfCurrencies = new ArrayList();
listOfCurrencies.add("USD/AUD");
listOfCurrencies.add("USD/JPY");
listOfCurrencies.add("USD/INR");
Это довольно многословно, и к счастью, вы можете написать все это всего в одной строке, используя сценарий кода, где применяются преимущества Arrays.asList() и конструктора копирования Collection:
List listOfPairs = new ArrayList(Arrays.asList( "USD/AUD" , "USD/JPY" , "USD/INR" );
Несмотря на то, что Arrays.asList возвращает List, нам нужно передать его вывод конструктору ArrayList, потому что List, возвращаемый Arrays.asList(), имеет фиксированный размер, вы не можете добавлять или удалять элементы оттуда. Кстати, это не ограничивается только List, вы также можете создать Set или любую другую коллекцию, например:
Set primes = new HashSet(Arrays.asList(2, 3, 5, 7);
Начиная с Java 9, для создания List и Set со значениями (values) вы также можете использовать такие методы, как List.of() и Set.of(). Это даже лучший вариант, потому что они возвращают неизменяемые List и Set.
5. Проверка условия wait() в цикле
Когда я впервые начал писать код межпотокового взаимодействия (inter-thread communication code) с использованием методов wait(), notify() и notifyAll(), я использовал блок if для проверки true/false в ожидании перед вызовом wait() и notify(). Вот пример:
synchronized(queue) {
if(queue.isFull()){
queue.wait();
}
}
Хотя я не сталкивался здесь с какими-либо проблемами, потом я понял свою ошибку, когда прочитал раздел книги Effective Java для wait() and notify(), в котором говорится, что вы должны проверять условие ожидания в цикле, потому что потоки могут получать ложное уведомление. Также возможно, что перед тем, как вы что-либо сделаете, то у вас снова появится условие ожидания. Итак, правильная идиома для вызова wait() и notify() следующая:
synchronized(queue) {
while(queue.isFull()){
queue.wait();
}
}
6. Перехват CloneNotSupportedException и возврат экземпляра SubClass
Функциональность клонирования объектов в Java сильно критикуется за свою плохую реализацию. Но если вам все же нужно реализовать clone(), то для этого есть несколько хороших сценариев:
public Course clone() {
Course c = null;
try {
c = (Course)super.clone();
} catch (CloneNotSupportedException e) {} // Не произойдет
return c;
}
Эта идиома использует тот факт, что clone() никогда не вызовет ошибку CloneNotSupportedException, если класс реализует интерфейс Cloneable. Возврат подтипа, известный как переопределение ковариантного метода, появился в Java 5. Он помогает уменьшить кастинг на стороне клиента, например, теперь ваш клиент может клонировать объект без кастинга:
Course javaBeginners = new Course("Java", 100, 10);
Course clone = javaBeginners.clone();
Ранее, и даже сейчас с классом Date, вы должны явно привести вывод метода клонирования, как показано ниже:
Date d = new Date(); Date clone = (Date) d.clone();
7. Использование интерфейсов везде, где это возможно
Несмотря на то, что я давно занимаюсь программированием, мне еще только предстоит реализовать весь потенциал интерфейсов. Когда я начал писать код, я использовал конкретные классы, например, ArrayList, Vector и HashMap, чтобы определить возвращаемый тип метода, типы переменных или типы аргументов метода. Примерно вот так:
ArrayList<Integer> listOfNumbers = new ArrayList();
public ArrayList<Integer> getNumbers(){
return listOfNumbers;
}
public void setNumbers(ArrayList<Integer> numbers){
listOfNumbers = numbers;
}
Это нормальный вариант, но не гибкий. Вы не можете передать в свои методы другой список, даже если он и лучше, чем ArrayList. А если завтра вам нужно будет перейти на другую реализацию, то придется менять все местами.
Вместо этого вы должны указать тип интерфейса, например, если вам нужен List, то есть упорядоченная коллекция с дубликатами, используйте java.util.List. Если вам нужен Set, то есть неупорядоченная коллекция без дубликатов, используйте java.util.Set. А если вам просто нужен контейнер, применяйте Collection. Это дает гибкость при создании альтернативной реализации.
List<Integer> listOfNumbers;
public List<Integer> getNumberS(){
return listOfNumbers;
}
public void setNumbers(List<Integer> listOfNumbers){
this.listOfNumbers = listOfNumbers;
}
Если хотите, можно пойти еще дальше и использовать ключевое слово extends в Generics. Например, вы можете определить List как List<? extends Number>, а затем вы можете передать List<Integer> или List<Short> этому методу.
8. Использование итератора для обхода списка
В Java существует несколько способов цикла или обхода по списку, например for loop с индексом, расширенный for loop и итератор. Раньше я использовал for loop с методом get(), как показано ниже:
for(int i =0; i<list.size; i++){
String name = list.get(i)
}
Все это прекрасно работает, если вы перебираете ArrayList, но, учитывая, что вы перебираете List, то возможно, что List может быть LinkedList или любой другой реализацией. А она может не поддерживать функциональность произвольного доступа, например, LinkedList. В этом случае временная сложность этого цикла увеличится до N^2, потому что get() равно O(n) для LinkedList.
Использование цикла для обхода списка также имеет недостаток с точки зрения многопоточности. Например, CopyOnWriteArrayList — один поток изменяет список, в то время как другой поток перебирает его с помощью size() или get(). А это приводит к исключению IndexOutOfBoundsException.
С другой стороны, Iterator — это стандартный или идиоматический способ обхода списка:
Iterator itr = list.iterator();
while(itr.hasNext()){
String name = itr.next();
}
Он безопасен, а также защищает от непредсказуемого поведения.
9. Написание кода с использованием Dependency Injection
Не так давно я написал такой код:
public Game {
private HighScoreService service = HighScoreService.getInstance();
public showLeaderBoeard(){ List listOfTopPlayers = service.getLeaderBoard(); System.out.println(listOfTopPlayers);
}
}
Этот код выглядит довольно знакомым, и многие из нас пропустят его при проверке. Но это не то, как вы должны писать свой современный код Java. Этот код имеет три основные проблемы:Класс Game тесно связан с классом HighScoreService, поэтому его невозможно протестировать изолированно. По этой причине вам нужен класс HighScoreService.
Даже если вы создадите класс HighScoreService, вы не сможете надежно протестировать Game, если ваш HighScoreService устанавливает сетевое соединение, загружает данные с серверов и так далее. Проблема в том, что вы не можете использовать Mock здесь вместо фактического объекта.
public Game {
private HighScoreService service;
public Game(HighScoreService svc){
this.service = svc;
}
public showLeaderBoeard(){
List listOfTopPlayers = service.getLeaderBoard();
System.out.println(listOfTopPlayers);
}
}
10. Закрытие потоков в собственном блоке try
Раньше я закрывал потоки InputStream и OutputStream следующим образом:
InputStream is = null;
OutputStream os = null;
try {
is = new FileInputStream("application.json")
os = new FileOutPutStream("application.log")
}catch (IOException io) {
}finally {
is.close();
os.close()
}
Тут есть проблемы: если первый поток вызовет исключение, то закрытие второго никогда не будет вызвано. Что делать в таких ситуациях, вы можете прочитать в моей предыдущей статье, как правильно закрыть Stream в Java.
Это все о идиомах Java, которые могут помочь вам писать более качественный и надежный код. Если вы программируете в течение нескольких лет, то, скорее всего, вы уже знакомы с этими шаблонами, но если вы только начинаете с Java или имеете один или два года опыта, эти идиомы могут помочь решить много конкретных проблем при написании кода.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ