JavaRush/Java блог/Random/Кофе-брейк #222. 10 идиом Java, которые должен знать кажд...

Кофе-брейк #222. 10 идиом Java, которые должен знать каждый разработчик

Статья из группы Random
участников
Источник: Gitconnected Автор этой статьи решил поделиться популярными идиомами программирования на Java, которые могут улучшить ваши профессиональные навыки разработчика. Кофе-брейк #222. 10 идиом Java, которые должен знать каждый разработчик - 1Идиомы программирования — это проверенный способ написания кода для определенного сценария. Они протестированы, поэтому в них нет ошибок. Используя такие идиомы, вы исключаете возможность появления проблем, которые могут возникнуть, если вы пишете свой собственный код. Идиомы очень похожи на шаблоны и библиотеки, но работать с ними намного проще. Термин “идиома” следует переводить как “стандартная практика”, а наиболее распространенное решение считается “идиоматическим”. Одним из примеров идиомы можно назвать распространенный способ написания бесконечного цикла: с использованием while(true) вместо for(;;). Вот список моих любимых идиом программирования на Java, которые вы можете использовать для написания лучшего, более чистого и надежного кода на Java:

1. Вызов equals() для строкового литерала или известного объекта

Долгое время при написании Java-кода я использовал метод equals, как показано ниже:
if (givenString. equals ( "YES" )){
// что-то делаем
}
Этот код хорошо читается, но небезопасен. Вы можете предотвратить потенциальную ошибку NullPointerException, вызвав equals() для литерала String (если один объект оказался литералом String) или для известного объекта:
"TRUE".equals(givenBoolean)
"YES".equals(givenString)
Если вы сделаете наоборот, например, givenBoolean.equals("YES"), тогда появится исключение NullPointerException, если givenBoolean равен null. Но если вы будете следовать этой идиоме, то он просто вернет false, не выбрасывая никакого NPE. Это намного лучший код, он безопасен и надежен. И это один из популярных способов избежать исключения NullPointerException в Java.

2. Использование entrySet для перебора HashMap

Раньше я перебирал HashMap, используя keyset, примерно так:
Set keySet = map.keyset();
for(Key k : keySet){
value v = map.get(k);
print(k, v)
}
Это приводит к выполнению еще одного поиска для получения значения из Map, которое в худшем случае может быть O (n). Если вам нужны и ключ (key), и значение (value), то лучше перебирать набор записей (entry set), а не набор ключей (key set).
Entry entrySet = map.entrySet();
for(Entry e : entrySet){
Key k = e.getKey();
Value v = e.getValue();
}
Это более эффективно, потому что вы получаете значение непосредственно от объекта, который всегда равен O(1).

3. Использование Enum в качестве синглтона

Было бы неплохо писать Singleton всего в одну строку на Java:
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.of() и Set.of() для создания List и Set со значениями. Это намного лучший вариант, потому что они возвращают неизменяемый List и Set.

5. Проверка условия wait() в цикле

Когда я впервые начал писать код межпотокового взаимодействия с использованием методов wait(), notify() и notifyAll(), то я использовал блок if для проверки истинности условия ожидания перед вызовом wait() и notify(), как показано ниже:
synchronized(queue) {
  if(queue.isFull()){
  queue.wait();
  }
}
Да, я не сталкивался тогда с какими-либо проблемами, но затем я понял свою ошибку, когда узнал, что нужно проверять условие ожидания в цикле, потому что потоки могут получать ложные уведомления. Поэтому правильная идиома для вызова 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. Возврат подтипа (Returning Subtype) известен как переопределение ковариантного метода и доступен в 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;
}
Этот код нормальный, но не гибкий. Вы не можете передать в свои методы другой List, хоть он и лучше, чем 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> этому методу. Кофе-брейк #222. 10 идиом Java, которые должен знать каждый разработчик - 2

8. Использование Iterator для обхода списка

Существует несколько способов обхода или цикла по списку в Java, например for loop с индексом, расширенный for loop и Iterator. Раньше я использовал for loop с методом get(), как показано ниже:
for(int i =0; i<list.size; i++){
  String name = list.get(i)
}
Это прекрасно работает, если вы перебираете ArrayList, но, учитывая, что вы перебираете List, то возможно, что List может быть LinkedList или любой другой реализацией, которая может не поддерживать функциональность произвольного доступа. В этом случае временная сложность такого цикла увеличится до , потому что get() для LinkedList это O (n). Использование цикла для обхода списка также имеет недостаток с точки зрения многопоточности, например, CopyOnWriteArrayList — один поток изменяет список, в то время как другой поток перебирает его с помощью size() или get(), что приводит к исключению IndexOutOfBoundsException. Поэтому Iterator — это стандартный или идиоматический способ обхода списка, как показано ниже:
Iterator itr = list.iterator();
while(itr.hasNext()){
String name = itr.next();
}
Такой код безопасен, а также защищает от непредсказуемого поведения.

9. Написание кода с учетом внедрения зависимостей

Не так давно я еще писал такой код:
public Game {
private HighScoreService service = HighScoreService.getInstance();
   public showLeaderBoeard(){
      List listOfTopPlayers = service.getLeaderBoard();
     System.out.println(listOfTopPlayers);
   }
}
Этот код выглядит довольно знакомым, и многие из нас пропустят его при проверке, но это не то, как вы должны писать современный код Java. Такой код имеет две основные проблемы:
  1. Класс Game тесно связан с классом HighScoreService, поэтому невозможно протестировать класс Game изолированно. Вам нужен класс HighScoreService.
  2. Даже если вы создадите класс HighScoreService, вы не сможете надежно протестировать Game, если ваш HighScoreService устанавливает сетевое соединение, загружает данные с серверов и так далее. Проблема в том, что вы не можете здесь использовать тест вместо реального объекта.
Но вы можете избавиться от этих проблем, написав свой класс Java как POJO и используя внедрение зависимостей , как показано ниже:
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()
}
Тут могут быть проблемы. Если первый поток вызовет исключение, то закрытие второго никогда не будет вызвано. Лучше использовать try-with-resources для написания аналогичного кода, так станет немного проще:
try (InputStream is = new FileInputStream("application.json");
     OutputStream os = new FileOutputStream("application.log")) {
// код для чтения из входного потока и записи в выходной поток
} catch (IOException e) {
// код обработки исключений
}
В этой версии мы объявляем потоки ввода и вывода в операторе try-with-resources, разделяя их точкой с запятой. За блоком try следует блок catch для обработки всех возможных IOExceptions. Прелесть этого подхода в том, что нам больше не нужно беспокоиться о закрытии потоков в блоке finally, поскольку они будут автоматически закрыты после завершения блока try.

Заключение

Это все о идиомах Java, которые могут помочь вам писать более качественный и надежный код. Если вы программируете на Java в течение нескольких лет, то, скорее всего, вы уже знакомы с этими шаблонами, но если вы только начинаете с Java или ваш опыт работы невелик, то эти идиомы могут помочь и открыть вам глаза на специфические проблемы при написании кода.
Комментарии
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны авторизоваться
У этой страницы еще нет ни одного комментария