Источник: DZone Это руководство включает в себя лучшие практические и справочные материалы по Java для повышения читабельности и надежности вашего кода. Разработчики несут большую ответственность за то, чтобы каждый день принимать правильные решения, и лучшее, что может им помочь в принятии правильных решений, — это опыт. И хотя не все из них обладают большим опытом в разработке программного обеспечения, каждый может использовать чужой опыт. Я подготовил для вас несколько рекомендаций, которые получил благодаря своему опыту работы с Java. Я надеюсь, что они помогут вам улучшить читаемость и надежность вашего кода Java.Кофе-брейк #56. Краткий справочник по лучшим практикам в Java - 1

Принципы программирования

Не пишите код, который только работает. Стремитесь написать код, который можно поддерживать — не только вами, но и кем-либо еще, кто сможет в конечном итоге работать над этим программным обеспечением в будущем. 80% своего времени разработчик читает код, а 20% — пишет и тестирует код. Значит, сосредоточьтесь на написании читаемого кода. Ваш код не должен нуждаться в комментариях, чтобы кто-то мог понять, что он делает. Чтобы писать хороший код, существует множество принципов программирования, которые мы можем использовать в качестве руководящих указаний. Ниже я перечислю самые важные.
  • • KISS — Расшифровывается как «Будь проще, глупец» (Keep It Simple, Stupid). Вы можете заметить, что разработчики в начале своего пути пытаются реализовать сложный, неоднозначный дизайн.
  • • DRY — «Не повторяйся» (Don’t Repeat Yourself). Старайтесь избегать любых дубликатов, вместо этого помещайте их в единую часть системы или метода.
  • YAGNI — «Тебе это не понадобится» (You Ain’t Gonna Need It). Если вы вдруг начнете себя спрашивать: «А как насчет добавления дополнительных (функций, кода и т.д.)?», тогда вам, вероятно, нужно подумать, стоит ли их действительно добавлять.
  • Чистый код вместо умного — Говоря проще, оставьте свое эго за дверью и забудьте о написании умного кода. Вам нужен чистый код, а не умный.
  • Избегайте преждевременной оптимизации — Проблема с преждевременной оптимизацией заключается в том, что вы никогда не узнаете, где в программе будут узкие места, до тех пор, пока они себя не проявят.
  • Единая ответственность — каждый класс или модуль в программе должен заботиться только о предоставлении одного бита определенной функциональности.
  • Композиция вместо наследования реализаций — Объекты со сложным поведением должны содержать экземпляры объектов с индивидуальным поведением, а не наследовать класс и добавлять новые поведения.
  • Объектная гимнастика — это упражнения по программированию , оформленные в виде набора из 9 правил.
  • Быстрая неудача, быстрая остановка — Этот принцип означает остановку текущей операции при возникновении какой-либо непредвиденной ошибки. Соблюдение этого принципа приводит к более стабильной работе.

Пакеты

  1. Отдавайте предпочтение структурированию пакетов по предметным областям, а не по техническим уровням.
  2. Отдавайте предпочтение макетам, которые способствуют инкапсуляции и сокрытию информации для защиты от неправильного использования, а не организации классов по техническим причинам.
  3. Просматривайте пакеты, как будто они имеют неизменяемый API, — не раскрывайте внутренние механизмы (классы), предназначенные только для внутренней обработки.
  4. Не открывайте доступ к классам, которые предполагается использовать только внутри пакета.

Классы

Статические

  1. Не разрешайте создание статического класса. Всегда создавайте частный конструктор.
  2. Статические классы должны оставаться неизменяемыми, не допускайте создания подклассов и многопоточных классов.
  3. Статические классы должны быть защищены от изменения ориентации и должны предоставляться в виде утилит, таких как фильтрация списка.

Наследование

  1. Выбирайте композицию, а не наследование.
  2. Не выставляйте защищенные поля. Вместо этого укажите защищенный метод доступа.
  3. Если переменную класса можно пометить как окончательную, сделайте это.
  4. Если наследования не ожидается, сделайте класс окончательным.
  5. Отметьте метод как окончательный, если не ожидается, что подклассам будет разрешено его переопределить.
  6. Если конструктор не требуется, не создавайте конструктор по умолчанию без логики реализации. Java автоматически предоставит конструктор по умолчанию, если он не указан.

Интерфейсы

  1. Не используйте шаблон константного интерфейса (an interface of constants), поскольку он позволяет классам реализовывать и загрязнять API. Вместо этого используйте статический класс. Это дает дополнительное преимущество, позволяя вам выполнять более сложную инициализацию объекта в статическом блоке (например, заполнение коллекции).
  2. Избегайте чрезмерного использования интерфейса.
  3. Наличие одного и только одного класса, реализующего интерфейс, скорее всего, приведет к чрезмерному использованию интерфейсов и принесет больше вреда, чем пользы.
  4. «Программа для интерфейса, а не для реализации» не означает, что вы должны объединить каждый из ваших классов домена с более или менее идентичным интерфейсом, делая это, вы нарушаете YAGNI.
  5. Всегда делайте интерфейсы небольшими и конкретными, чтобы клиенты знали только о методах, которые им интересны. Проверьте ISP от SOLID.

Финализаторы

  1. Объект # finalize () следует использовать разумно и только как средство защиты от сбоев при очистке ресурсов (например, закрытии файла). Всегда предоставляйте явный метод очистки (например, close()).
  2. В иерархии наследования всегда вызывайте родительский finalize() в блоке try. Очистка класса должна быть в блоке finally.
  3. Если явный метод очистки не был вызван и финализатор закрыл ресурсы, запишите эту ошибку.
  4. Если средство ведения журнала недоступно, используйте обработчик исключений потока (который в конечном итоге передает стандартную ошибку, которая фиксируется в журналах).

Общие правила

Утверждения

Утверждение, обычно в форме проверки предварительного условия, обеспечивает выполнение контракта типа «быстрая неудача, быстрая остановка». Их следует использовать широко, чтобы выявлять ошибки программирования как можно ближе к причине. Состояние объекта:
  • • Объект никогда не должен создаваться или переходить в недопустимое состояние.
  • • В конструкторах и методах всегда описывайте и применяйте контракт с помощью проверок.
  • • Следует избегать использования ключевого Java-термина assert, так как он может быть отключен и обычно является хрупкой конструкцией.
  • • Используйте служебный класс Assertions, чтобы избежать подробных условий if-else для проверок предварительного условия.

Дженерики

Полное, чрезвычайно подробное объяснение доступно в FAQ по Java Generics. Ниже приведены распространенные сценарии, о которых следует знать разработчикам.
  1. По возможности лучше использовать вывод типа, а не возвращать базовый класс / интерфейс:

    
    // MySpecialObject o = MyObjectFactory.getMyObject();
    public  T getMyObject(int type) {
    return (T) factory.create(type);
    }

  2. Если тип не может быть определен автоматически, встройте его.

    
    public class MySpecialObject extends MyObject {
     public MySpecialObject() {
      super(Collections.emptyList());   // This is ugly, as we loose type
      super(Collections.EMPTY_LIST();    // This is just dumb
      // But this is beauty
      super(new ArrayList());    
      super(Collections.emptyList());
     }
    }

  3. Подстановочные знаки:

    Используйте расширенный подстановочный знак, когда вы получаете только значения из структуры, используйте подстановочный знак super, когда вы помещаете только значения в структуру, и не используйте подстановочный знак, когда вы делаете и то, и другое.

    1. Все любят PECS! (Producer-extends, Consumer-super)
    2. Используйте Foo для производителя T.
    3. Используйте Foo для потребителя T.

Синглтоны

Синглтон никогда не следует писать в классическом стиле шаблонов проектирования, который вполне допустим для C ++, но не подходит для Java. Несмотря на то, что он правильно ориентирован на многопоточность, никогда не реализуйте следующее (это было бы узким местом в производительности!):

public final class MySingleton {
  private static MySingleton instance;
  private MySingleton() {
    // singleton
  }
  public static synchronized MySingleton getInstance() {
    if (instance == null) {
      instance = new MySingleton();
    }
    return instance;
  }
}
Если отложенная инициализация действительно желательна, то комбинация этих двух подходов сработает.

public final class MySingleton {
  private MySingleton() {
   // singleton
  }
  private static final class MySingletonHolder {
    static final MySingleton instance = new MySingleton();
  }  
  public static MySingleton getInstance() {
    return MySingletonHolder.instance;
  }
}
Spring: по умолчанию bean-компонент регистрируется с одноэлементной областью видимости, что означает, что только один экземпляр будет создан контейнером и подключен ко всем потребителям. Это обеспечивает ту же семантику, что и обычный синглтон, без ограничений производительности или связывания.

Исключения

  1. Используйте отмеченные исключения для исправимых условий и исключения времени выполнения для ошибок программирования. Пример: получение целого числа из строки.

    Плохо: NumberFormatException расширяет RuntimeException, поэтому оно предназначено для обозначения ошибок программирования.

  2. Не делайте следующего:

    
    // String str = input string
    Integer value = null;
    try {
       value = Integer.valueOf(str);
    } catch (NumberFormatException e) {
    // non-numeric string
    }
    if (value == null) {
    // handle bad string
    } else {
    // business logic
    }
    

    Правильное использование:

    
    // String str = input string
    // Numeric string with at least one digit and optional leading negative sign
    if ( (str != null) && str.matches("-?\\d++") ) {  
       Integer value = Integer.valueOf(str);
      // business logic
    } else {
      // handle bad string
    }
    
  3. Вы должны обрабатывать исключения в нужном месте, в нужном месте на уровне домена.

    НЕПРАВИЛЬНЫЙ СПОСОБ — слой объекта данных не знает, что делать при возникновении исключения базы данных.

    
    class UserDAO{
        public List getUsers(){
            try{
                ps = conn.prepareStatement("SELECT * from users");
                rs = ps.executeQuery();
                //return result
            }catch(Exception e){
                log.error("exception")
                return null
            }finally{
                //release resources
            }
        }}
    

    РЕКОМЕНДУЕМЫЙ СПОСОБ — уровень данных просто должен повторно вызвать исключение и передать ответственность за обработку исключения или не на правильный уровень.

    
    === RECOMMENDED WAY ===
    Data layer should just retrow the exception and transfer the responsability to handle the exception or not to the right layer.
    class UserDAO{
       public List getUsers(){
          try{
             ps = conn.prepareStatement("SELECT * from users");
             rs = ps.executeQuery();
             //return result
          }catch(Exception e){
           throw new DataLayerException(e);
          }finally{
             //release resources
          }
      }
    }

  4. Исключения обычно НЕ должны регистрироваться в момент их выдачи, а скорее в момент их фактической обработки. Исключения журналирования, когда они генерируются или повторно генерируются, имеют тенденцию заполнять файлы журнала шумом. Также обратите внимание, что трассировка стека исключений все равно фиксирует, где было сгенерировано исключение.

  5. Поддерживайте использование стандартных исключений.

  6. Используйте исключения, а не коды возврата.

Equals и HashCode

При написании надлежащих методов эквивалентности объектов и хэш-кода необходимо учитывать ряд проблем. Чтобы упростить использование, используйте java.util.Objects' equals и hash.

public final class User {
 private final String firstName;
 private final String lastName;
 private final int age;
 ...
 public boolean equals(Object o) {
   if (this == o) {
     return true;
   } else if (!(o instanceof User)) {
     return false;
   }
   User user = (User) o;
   return Objects.equals(getFirstName(), user.getFirstName()) &&                                
    Objects.equals(getLastName(),user.getLastName()) &&
    Objects.equals(getAge(), user.getAge());
 }
 public int hashCode() {
   return Objects.hash(getFirstName(),getLastName(),getAge());
 }
}

Управление ресурсами

Способы безопасного высвобождения ресурсов: Оператор try-with-resources гарантирует, что каждый ресурс будет закрыт в конце оператора. Любой объект, который реализует java.lang.AutoCloseable, который включает в себя все объекты, реализующие java.io.Closeable, может быть использован в качестве ресурса.

private doSomething() {
try (BufferedReader br = new BufferedReader(new FileReader(path))) 
 try {
   // business logic
 }
}

Применяйте Shutdown Hooks

Применяйте shutdown hook, который вызывается, если JVM будет корректно завершена. (Но он не сможет обработать внезапные прерывания, например, из-за отключения электроэнергии) Это рекомендуемая альтернатива вместо объявления метода finalize(), который будет запускаться только в том случае, если System.runFinalizersOnExit() имеет значение true (по умолчанию — false).

public final class SomeObject {
 var distributedLock = new ExpiringGeneralLock ("SomeObject", "shared");
 public SomeObject() {
   Runtime
     .getRuntime()
     .addShutdownHook(new Thread(new LockShutdown(distributedLock)));
 }
 /** Code may have acquired lock across servers */
 ...
 /** Safely releases the distributed lock. */
 private static final class LockShutdown implements Runnable {
   private final ExpiringGeneralLock distributedLock;
   public LockShutdown(ExpiringGeneralLock distributedLock) {
     if (distributedLock == null) {
       throw new IllegalArgumentException("ExpiringGeneralLock is null");
     }
     this.distributedLock = distributedLock;
   }
   public void run() {
     if (isLockAlive()) {
       distributedLock.release();
     }
   }
   /** @return True if the lock is acquired and has not expired yet. */
   private boolean isLockAlive() {
     return distributedLock.getExpirationTimeMillis() > System.currentTimeMillis();
   }
 }
}
Разрешите ресурсам стать завершенными (а также возобновляемыми) распределив их между серверами. (Это позволит восстановиться после внезапного прерывания, такого как отключение электроэнергии). См. Пример кода выше, в котором используется ExpiringGeneralLock (блокировка, общая для всех систем).

Date-Time

Java 8 представляет новый API Date-Time в пакете java.time. В Java 8 представлен новый API Date-Time для устранения следующих недостатков старого API Date-Time: не многопоточность, плохой дизайн, сложная обработка часовых поясов и т.д.

Параллелизм

Общие правила

  1. Остерегайтесь following библиотек, которые не ориентированы на многопоточность. Всегда выполняйте синхронизацию с объектами, если они используются несколькими потоками.
  2. Date (не неизменяемая) — используйте новый API Date-Time, который является потокобезопасным.
  3. SimpleDateFormat — используйте новый API Date-Time, который является потокобезопасным.
  4. Предпочитайте использовать классы java.util.concurrent.atomic, а не делайте переменные volatile.
  5. Поведение атомарных классов более очевидно для среднего разработчика, тогда как volatile требует понимания модели памяти Java.
  6. Атомарные классы заключают в себе переменные volatile в более удобный интерфейс.
  7. Разберитесь в примерах использования, в которых подходит volatile. (см. статью)
  8. Используйте Callable , когда требуется проверенное исключение, но нет возвращаемого типа. Поскольку Void не может быть создан, он сообщает о намерении и может безопасно вернуть null.

Потоки

  1. java.lang.Thread следует считать устаревшим. Хотя официально это не так, почти во всех случаях пакет java.util.concurrent предоставляет более четкое решение проблемы.
  2. Расширение java.lang.Thread считается плохой практикой — вместо этого реализуйте Runnable и создайте новый поток с экземпляром в конструкторе (правило композиции важнее наследования).
  3. Предпочитайте исполнителей и потоки, когда требуется параллельная обработка.
  4. Всегда рекомендуется указать собственную фабрику настраиваемых потоков для управления конфигурацией создаваемых потоков (тут подробнее).
  5. Используйте DaemonThreadFactory в Executors для некритических потоков, чтобы пул потоков мог быть отключен немедленно при завершении работы сервера (тут подробнее).

this.executor = Executors.newCachedThreadPool((Runnable runnable) -> {
   Thread thread = Executors.defaultThreadFactory().newThread(runnable);
   thread.setDaemon(true);
   return thread;
});
  1. Синхронизация Java уже не такая медленная (55–110 нс). Не избегайте ее, используя уловки, такие как double-checked locking.
  2. Предпочитайте синхронизацию с внутренним объектом, а не с классом, поскольку пользователи могут синхронизироваться с вашим классом / экземпляром.
  3. Всегда синхронизируйте несколько объектов в одном порядке, чтобы избежать взаимоблокировок.
  4. Синхронизация с классом по сути не блокирует доступ к его внутренним объектам. Всегда используйте одни и те же блокировки при доступе к ресурсу.
  5. Помните, что ключевое слово synchronized не считается частью сигнатуры метода и, следовательно, не будет унаследовано.
  6. Избегайте чрезмерной синхронизации, это может привести к снижению производительности и тупиковой ситуации. Используйте ключевое слово synchronized строго для той части кода, которая требует синхронизации.

Коллекции

  1. По возможности используйте параллельные коллекции Java-5 в многопоточном коде. Они безопасны и обладают превосходными характеристиками.
  2. При необходимости используйте CopyOnWriteArrayList вместо synchronizedList.
  3. Используйте Collections.unmodifiable list (…) или скопируйте коллекцию при ее получении в качестве параметра new ArrayList (list). Избегайте изменения локальных коллекций извне вашего класса.
  4. Всегда возвращайте копию своей коллекции, избегая изменения вашего списка извне new ArrayList (list).
  5. Каждая коллекция должна быть завернута в отдельный класс, поэтому теперь у поведения, связанного с коллекцией, есть дом (например, методы фильтрации, применение правила к каждому элементу).

Разное

  1. Выбирайте лямбды, а не анонимные классы.
  2. Выбирайте ссылки на методы, а не лямбды.
  3. Используйте enums вместо констант типа int.
  4. Избегайте использования float и double, если требуются точные ответы, вместо этого используйте BigDecimal, например Money.
  5. Выбирайте примитивные типы, а не boxed примитивы.
  6. Следует избегать использования магических чисел в коде. Использовать константы.
  7. Не возвращайте Null. Общайтесь со своим клиентом метода с помощью `Optional`. То же самое для коллекций — возвращайте пустые массивы или коллекции, а не nulls.
  8. Избегайте создания ненужных объектов, повторно используйте объекты и избегайте ненужной очистки GC.

Отложенная инициализация

Ленивая инициализация — это оптимизация производительности. Она используется, когда данные по какой-то причине считаются «дорогими». В Java 8 мы должны использовать для этого функциональный интерфейс поставщика.

== Thread safe Lazy initialization ===
public final class Lazy {
   private volatile T value;
   public T getOrCompute(Supplier supplier) {
       final T result = value; // Just one volatile read
       return result == null ? maybeCompute(supplier) : result;
   }
   private synchronized T maybeCompute(Supplier supplier) {
       if (value == null) {
           value = supplier.get();
       }
       return value;
   }
}
Lazy lazyToString= new Lazy<>()
return lazyToString.getOrCompute( () -> "(" + x + ", " + y + ")");
На этом пока все, надеюсь, это было полезно!