Рубрики
Без рубрики

Эффективная Java — Главы с 10 по 12

Введение Это заключительная часть краткого изложения этой книги. В первой части я кратко изложил ча… Помеченный java.

Это заключительная часть краткого изложения этой книги. В первой части я кратко изложил главы 2-5, а во второй части – главы 6-9. В этой заключительной части у нас будет краткое изложение остальной части книги, глав с 10 по 12. Если вы обнаружили проблемы с какой-либо частью резюме или если у вас есть предложения, чтобы сделать их лучше или понятнее, пожалуйста, дайте мне знать, и я буду обновлять их.

Пункт 69

  • Используйте исключения только для исключительных условий
  • мы не должны использовать их для обычного условного потока, подобного этому:
try {
    int i = 0;
    while (true) {
        range[i++].climb();
    }
} catch (ArrayIndexOutOfBoundsException e) {}
  • такой подход к использованию исключений неверен на разных уровнях:
    • это сбивает с толку, так как это не входит в намерения исключений
    • JVM не будет оптимизировать их (по сравнению с тем, когда мы используем массивы.длина(), например)
    • мы можем поймать здесь фактическое исключение непреднамеренно, не осознавая этого
  • хорошо разработанный API не должен заставлять своих клиентов использовать исключения для обычного рабочего процесса
    • например, итератор имеет hasNext() который вы можете использовать перед вызовом next() , так что вам не нужно пытаться/ловить здесь
    • в общем случае, когда метод зависит от состояния, у нас должен быть метод проверки состояния для вызова перед вызовом вашего метода, чтобы убедиться, что мы можем безопасно его вызвать.

Пункт 70

  • Используйте проверенные исключения для исправляемых условий и исключения во время выполнения для ошибок программирования
  • Java предоставляет три вида метаемых объектов:
    1. проверенные исключения
    2. когда мы объявляем метод, возникает исключение
    3. вызывающий абонент должен поймать его и разрешить или распространить вверх
    4. они предназначены для использования в условиях, от которых можно разумно ожидать, что вызывающий абонент восстановится
    5. это плохая идея – просто ловить эти исключения и игнорировать их.
    6. предоставьте методы для проверенных исключений, чтобы помочь в восстановлении. например, у нас есть способ оплаты с помощью кредитной карты, который создает исключение, если средств недостаточно. Мы можем предоставить метод доступа для запроса суммы недостачи. Это позволяет вызывающему абоненту передать сумму покупателю
    7. исключения во время выполнения
    8. эти исключения обычно являются результатом нарушения предварительных условий, которое заключается в несоблюдении клиентом контракта, установленного спецификациями API
    9. они являются результатом ошибок программирования
    10. например, контракт на доступ к массиву определяет индексы в диапазоне от 0 до длины массива минус 1, использование чего-либо за пределами этого диапазона приведет к Исключение ArrayIndexOutOfBoundsException
    11. ошибки
    12. существует строгое соглашение о том, что ошибки зарезервированы для использования JVM
    13. такие, как OutOfMemoryError , Смерть потока и т.д.
    14. все непроверенные исключения, которые вы реализуете, должны относиться к подклассу Исключение времени выполнения

Пункт 71

  • Избегайте ненужного использования проверенных исключений
  • при экономном использовании они повышают надежность программ, а при чрезмерном использовании делают использование API болезненным
  • нам нужно использовать их, когда мы считаем, что пользователь может восстановить ситуацию при получении исключения
    • если они просто регистрируют трассировку стека или игнорируют ее (как будто они больше ничего не могут сделать), не имеет смысла делать это проверенным исключением. Это просто усложняет его использование, так как вам нужно использовать попытку/улов или распространять его наружу!

Пункт 72

  • Одобряйте использование стандартных исключений
  • Библиотеки Java предоставляют набор исключений, который удовлетворяет большинству потребностей большинства API в создании исключений
  • мы должны стараться использовать стандартные и соответствующие типы исключений, которые делают наш код более чистым и легким для чтения и понимания
  • наиболее часто используемыми исключениями являются:
    • Исключение IllegalArgumentException : ненулевое значение параметра неуместно
    • например, передача отрицательного числа в аргумент, ожидающий количества элементов
    • Исключение IllegalStateException : Состояние объекта не подходит для вызова метода
    • подобный объект не инициализирован
    • Исключение NullPointerException : значение параметра равно нулю там, где запрещено
    • Исключение Indexoutofboundsexception : значение параметра индекса находится вне диапазона
    • Исключение ConcurrentModificationException : обнаружено одновременное изменение объекта там, где это запрещено
    • Исключение UnsupportedOperationException : объект не поддерживает метод

Пункт 73

  • Создавать исключения, соответствующие абстракции
  • когда исключения распространяются вовне, мы можем получить исключение, которое не имеет очевидной связи с выполняемой им задачей.
  • чтобы избежать этой проблемы, мы можем выполнить перевод исключений , который в основном улавливает исключение (при необходимости) и вместо этого выдает более релевантное исключение на более высокий уровень
  • это пример из AbstractSequentialList :
public E get(int index) {
    ListIterator i = listIterator(index);
    try {
        return i.next();
    } catch (NoSuchElementException e) {
        throw new IndexOutOfBoundsException("Index: " + index);
    }
}
  • специальная форма трансляции исключений называется цепочка исключений , когда исключение более низкого уровня может быть полезно для тех, кто отлаживает исключение более высокого уровня. в этом случае мы передаем исключение более низкого уровня исключению более высокого уровня:
try {
    ...
} catch (LowerLevelException cause) {
    throw new HigherLevelException(cause);
}
  • большинство стандартных исключений имеют конструкторы с поддержкой цепочки, которые передают “причину” конструктору более высокого уровня, к которому можно получить доступ позже.
  • для исключений мы можем использовать метод Throwable ‘s initCause , к которому позже можно получить доступ с помощью Получить причину

Записи

  • хотя перевод исключений превосходит бездумное распространение исключений с более низких уровней, им не следует злоупотреблять

Пункт 74

  • Документ все исключения, создаваемые каждым методом
  • всегда объявляйте проверенные исключения по отдельности и точно документируйте условия, при которых создается каждое из них
  • используйте Javadoc @бросает метка
  • также рекомендуется документировать непроверенные исключения, которые может выдавать метод. Эти непроверенные исключения обычно являются ошибкой программиста, и, делая это, они будут знать, чего ожидать и как правильно использовать этот метод или интерфейс.

Пункт 75

  • Включать информацию о сбое в подробные сообщения
  • чтобы зафиксировать сбой, подробное сообщение об исключении должно содержать все значения для всех параметров и полей, которые способствовали возникновению исключения
    • например, если мы получим Исключение Indexoutofboundsexception , было бы очень полезно иметь значение индекса, а также нижнюю границу и верхнюю связанный
    • исключением из этого правила (в целях безопасности) являются пароли, ключи шифрования и тому подобное, которые мы не хотим включать в подробное сообщение

Пункт 76

  • Стремитесь к атомарности отказа
  • это означает, что неудачный вызов метода должен оставить объект в том состоянии, в котором он находился до вызова
  • существуют разные способы достижения этой цели. Например, в предыдущей реализации стека:
public Object pop() {
    if (size == 0) {
        throw new EmptyStackException();
    }
    Object result = elements[--size];
    ...
}

итак, здесь мы сначала проверяем размер и создаем исключение вместо создания размера -1 а затем дождитесь возникновения исключения при доступе к элементам[-1]

  • другой способ добиться атомарности сбоев – по возможности изменить порядок вычислений, чтобы на первом месте стояла та часть, которая может вызвать исключение.
  • Другой подход заключается в выполнении операций над копией объекта, а затем, если все прошло успешно, применить их обратно к оригиналу
  • следует отметить, что атомарность сбоев не всегда достижима (например, потоки без надлежащей синхронизации что-то изменяют), или иногда создание чего-то атомарного сильно усложнит код. В этих случаях мы должны упомянуть об этом в документации API, что такое нарушенное состояние, которое может произойти, например, при вызове метода

Пункт 77

  • Не игнорируйте исключения
  • пустой блок catch сводит на нет цель исключений. Мы всегда должны принимать надлежащие меры для устранения исключений
  • в некоторых редких случаях может быть нормально игнорировать их, и в этом случае мы должны поместить комментарий в блок catch с объяснениями:
int numColors = 4; // Default
try {
    numColors = getNumColors();
} catch (ExecutionException | TimedoutException ignored) {
    // in this case just use default value
}

Пункт 78

  • Синхронизировать доступ к общим изменяемым данным
  • лучше не делиться изменяемыми данными между несколькими потоками и делиться только неизменяемыми данными.
  • если у нас есть изменяемый объект, мы можем поделиться им только с одним потоком
  • если нам нужно поделиться изменяемыми данными, мы должны использовать синхронизацию, которая гарантирует, что ни один метод никогда не будет наблюдать объект в несогласованном состоянии.
    • спецификация языка гарантирует, что чтение/запись являются атомарными, если переменная не является длинной или двойной (есть Атомный длинный которые мы можем использовать)
  • один из примеров использования синхронизации:
public class StopThread() {
    private static boolean stopRequested;

    private static synchronized void requestStop() {
        stopRequested = true;
    }

    private static synchronized boolean stopRequested() {
        return stopRequested;
    }

    public static void main(String[] args) {
        Thread backgroundThread = new Thread(() -> {
            int i = 0;
            while ( !stopRequested() ) {
                i++;
            }
        });
        backgroundThread.start();

        TimeUnit.SECONDS.sleep(1);
        requestStop();
    }
}
  • синхронизация может быть использована для:
    • “связь”: если мы удалим код синхронизации выше, ваш поток не будет иметь доступа к обновленному значению stopRequested , поэтому мы должны использовать его, чтобы убедиться, что он действительно получает обновленное значение , как мы ожидаем оно
    • “взаимное исключение”: таким образом, у нас не будет состояния гонки или того, что один поток считывает значение, в то время как другой записывает в него новое значение.
  • в приведенном выше примере, поскольку синхронизация используется только для “связи”, мы можем удалить синхронизированные методы для чтения и записи и вместо этого объявить переменную следующим образом: private static volatile boolean stopRequested; это заставит поток проверять обновленное значение каждый раз, когда он его читает.
  • синхронизация не гарантируется, если не синхронизированы оба метода чтения/записи.

Пункт 79

  • Избегайте чрезмерной синхронизации
  • внутри синхронизированного метода или блока никогда не передавайте контроль клиенту
    • например, вызов метода, который предназначен для переопределения
    • или вызов метода, предоставленного клиентом
    • с точки зрения класса с синхронизированной областью эти методы называются чужеродными
  • мы можем получить нежелательные исключения или тупики
  • как правило, мы должны выполнять как можно меньше работы внутри синхронизированных регионов
  • чрезмерная синхронизация может иметь свои собственные проблемы

Пункт 80

  • Предпочитайте исполнителей, задачи и потоки потокам
  • пакет java.util.concurrent содержит Платформу исполнителя , которая представляет собой гибкое средство выполнения задач на основе интерфейса. Вместо создания рабочей очереди для асинхронного выполнения задач и обработки этой очереди вручную лучше использовать эту платформу:
ExecutorService exec = Executors.newSingleThreadExecutor();
// submitting a runnable for execution:
exec.execute(runnable);
// terminating executor gracefully:
exec.shutdown();
  • вы можете сделать гораздо больше с помощью этой услуги. Вы можете дождаться завершения всех задач или конкретной задачи, вы можете получать результаты задач по мере их выполнения, вы можете запланировать их выполнение в определенное время или периодически и так далее.
  • по сути, Фреймворк исполнителя делает для выполнения то, что Фреймворк коллекций предназначен для агрегирования.

Пункт 81

  • Предпочитаю утилиты параллелизма подождите и уведомлять
  • утилиты более высокого уровня в java.util.concurrent делятся на три категории:

    1. Структура исполнителя
    2. параллельные коллекции
    3. синхронизаторы
  • параллельные коллекции – это высокопроизводительная параллельная реализация стандартных интерфейсов коллекций, таких как список, очередь и карта (например, ConcurrentHashMap )
  • Синхронизаторы – это объекты, которые позволяют потокам ждать друг друга, позволяя им координировать свои действия. наиболее распространенными из них являются Обратный отсчет и Семафор и самый мощный из них – Фазер .
  • краткое объяснение CountDownLatch : у него есть конструктор, который получает значение int, равное количеству раз, когда метод Countdown должен быть вызван для него, прежде чем все ожидающие потоки смогут продолжить:
CountDownLatch latch = new CountDownLatch(2);
latch.await();
System.out.println("done!");

итак, здесь, если, например, в другом потоке мы вызовем latch.countDown два раза, приведенный выше код будет выполнен и выведет “готово!”

  • таким образом, этот пункт (и предыдущий) познакомил нас с тремя основными утилитами java.util.concurrent . Для каждого из них существуют старые альтернативы, требующие большого количества кода и обслуживания (для № 2 выше есть Синхронизированные коллекции , которые работают медленнее. Для № 3 есть подождите и уведомлять / уведомлять всех , что является беспорядочным и требует дополнительной обработки). Эти последние два пункта представляют эти новые альтернативы и побуждают нас использовать их вместо этого. Для каждого из них есть много деталей, и если они нам нужны, мы должны изучить их более подробно.

Пункт 82

  • Безопасность потока документов
  • Поведение класса при одновременном вызове его методов является важной частью его контракта с API
  • чтобы обеспечить безопасное одновременное использование, класс должен четко документировать свой уровень потокобезопасности:
    • Неизменяемый : Экземпляры постоянны, внешняя синхронизация не требуется
    • Безоговорочно потокобезопасный : экземпляры этого класса изменяемы, но класс имеет достаточную внутреннюю синхронизацию
    • Условно потокобезопасный : как и предыдущий, но некоторые из его методов нуждаются во внешней синхронизации
    • Не потокобезопасный : для их безопасного использования клиент должен окружать каждый вызов метода внешней синхронизацией
    • Поток враждебный : небезопасно для одновременного использования, даже если все окружено синхронизацией. Обычно это происходит в результате изменения статических данных без внутренней синхронизации, внешняя синхронизация здесь не поможет!
  • поля блокировки всегда должны быть объявлены окончательными:
    private final Object lock = new Object();

    public void foo() {
        synchronized(lock) {
            ...
        }
    }

Пункт 83

  • Разумно используйте ленивую инициализацию
  • Ленивая инициализация : акт задержки инициализации поля до тех пор, пока его ценность необходима
  • как и в случае большинства оптимизаций, не делайте этого, если вам не нужно к
  • если доступ к полю осуществляется только в небольшом количестве экземпляров и его инициализация обходится дорого, лучше лениво инициализировать его

  • обычная ленивая инициализация, которая должна быть синхронизирована:

private FieldType field;
private synchronized FieldType getField() {
    if (field == null) {
        field = computeFieldValue();
    }
    return field;
}
  • идиома класса держателя ленивой инициализации для использования в статическом поле:
private static class FieldHolder {
    static final FieldType field = computeFieldValue(); // this will be ignored until accessor is called
}
private static FieldType getField() {
    return FieldHolder.field;
}
  • чтобы использовать отложенную инициализацию для повышения производительности поля экземпляра, используйте идиому перепроверьте :
    private volatile FieldType field;

    private FieldType getField() {
        FieldType result = field;
        if (result == null) { // first check, no locking
            synchronized(this) {
                if (field == null) { // second check with locking
                    field = result = computeFieldValue();
                }
            }
        }
    }

Пункт 84

  • Не зависите от потока планировщик
  • когда одновременно выполняется много потоков, планировщик потоков определяет, какой из них будет выполняться как долго, и это может отличаться в разных системах. Любая программа, которая полагается на планировщик потоков для корректности и производительности, скорее всего, будет непереносимой.
  • потоки не должны запускаться, если они не выполняют полезную работу (например, чего-то ждут)
  • не поддавайтесь искушению использовать Поток.уступать или приоритеты потоков, которые относятся к числу наименее переносимых функций Java

Пункт 85

  • Предпочитаете альтернативы сериализации Java
  • При сериализации существует множество рисков для безопасности. лучший способ избежать эксплойтов сериализации – никогда ничего не десериализовывать
  • нет причин использовать сериализацию Java в любом новом коде ты пишешь
  • две хорошие альтернативы – использовать JSON или protobuf в качестве кроссплатформенного структурированного представления данных

Пункт 86

  • Реализовать Сериализуемый с большой осторожностью
  • вы можете просто сделать любой класс для реализации Сериализуемым но есть несколько вещей, о которых следует знать

    • это снижает гибкость класса при изменении после его выпуска. по сути, поток байтов его внутренних компонентов теперь является частью экспортируемого API
    • это увеличивает вероятность ошибок и дыр в безопасности
    • это увеличивает нагрузку на тестирование. Должна быть возможность сериализовать старую версию класса и десериализовать его, используя его новую версию, и наоборот. Все они должны быть проверены
    • Классы, предназначенные для наследования, редко должны реализовывать Сериализуемый и интерфейсы редко должны расширять его
    • внутренние классы не должны реализовывать Сериализуемый
  • когда вы создаете сериализуемый класс, если вы не объявляете статическое конечное длинное поле, называемое serialVersionUID , система автоматически генерирует его во время выполнения

Пункт 87

  • Рассмотрите возможность использования сериализованной формы клиента
  • Не принимайте сериализованную форму по умолчанию, не рассмотрев, подходит она или нет.
  • сериализация по умолчанию – это эффективное кодирование физического представления графа объектов
  • если это физическое представление идентично его логическому содержимому, то целесообразно использовать сериализацию по умолчанию. Следующий класс является примером:
public class Name implements Serialization {
    /**
    * Last name, must be non-Null
    * @serial
    */
    private final String lastName;

    /**
    * First name, must be non-Null
    * @serial
    */
    private final String firstName;

    /**
    * Middle name, or null if there is none
    * @serial
    */
    private final String middleName;
}
  • даже если вы решите, что сериализованная форма по умолчанию подходит, вам часто приходится предоставлять метод readObject
  • На другом конце спектра, вот ужасный кандидат для сериализации по умолчанию:
public final class StringList implements Serializable {
    private int size = p;
    private Entry head = null;

    private static class Entry implements Serializable {
        String data;
        Entry next;
        Entry previous;
    }
    //...
}

логически говоря, этот класс представляет собой последовательность строк, но физически это последовательность двусвязного списка. Разумной формой сериализации этого класса является просто число, представляющее количество строк в списке, за которым следуют сами строки:

public final class StringList implements Serializable {
    private transient int size = 0;
    private transient Entry head = null;

    private static class Entry {
        String data;
        Entry next;
        Entry previous;
    }

    // appends the specified string to the list
    public final void add(String s) {...}

    /**
    * Serialize this {@code StringList} instance
    *
    * @serialData The size of the list (the number of strings it contains) is
    * emitted ({@code int}), followed by all of its elements (each a {@code String}
    * ), in the proper sequence
    */
    private void writeObject(ObjectOutputStream s) throws IOException {
        s.defaultWriteObject();
        s.writeInt(size);

        // write out all elements in the proper order
        for (Entry e = head; e != null; e = e.next) {
            s.writeObject(e.data);
        }
    }

    private void readObject(ObjectInputStream s) throws IOException {
        s.defaultReadObject();
        int numElements = s.readInt();

        // read in all elements and insert them in list
        for (int i = 0; i < numElements; i++) {
            add((String) s.readObject());
        }
    }

    // remainder omitted
}

Пункт 88

  • Писать методы readObject в защитной форме
  • сериализацию и десериализацию классов по умолчанию можно переопределить с помощью Объект чтения и Методы writeObject .
  • их можно использовать для различных сценариев, например, для инициализации переходных (несериализуемых) полей после десериализации объекта
  • другим вариантом использования, который является темой этого пункта, является защита неизменяемых классов при их десериализации
  • в качестве напоминания, для классов, которые имеют изменяемые объекты, мы делаем их неизменяемыми, делая их закрытыми, а также защищая их копирование повсюду:
public final class Period {
    private final Date start;
    private final Date end;

    public Period(Date start, Date end) {
        this.start = new Date(start.getTime());
        this.end = new Date(end.getTime());
        if (this.start.compareTo(this.end) > 0) {
            throw new IllegalArgumentException();
        }
    }

    // getters and no setters
}

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

  • Этот пункт объясняет, что readObject фактически является еще одним общедоступным конструктором, который требует такой же осторожности, как и любой другой конструктор

    • в книге есть несколько примеров, которые изменяют сериализованный поток байтов, чтобы нарушать то, что обычно невозможно в вышеупомянутом классе Period . (примеры приведены, когда мы делаем Период для реализации сериализуемого, и мы не предоставляем надлежащий readObject )
    • короче говоря, правильный readObject должен защитно копировать поля и проверять наличие подтверждений
    • вот его правильная реализация:
  private void readObject(ObjectInputStream is) throws IOException, ClassNotFoundException {
      is.defaultReadObject();

      start = new Date(start.getTime());
      end = new Date(end.getTime());

      if (start.compareTo(end) > 0) {
          throw new InvalidObjectException();
      }
  }

обратите внимание, что здесь мы должны сделать поля не окончательными, чтобы иметь возможность сделать защитную копию сразу после десериализации.

  • так же, как и неизменяемые классы, эти методы readObject не должны вызывать переопределяемые методы

Пункт 89

  • Например, управление, предпочитайте типы перечислений для разрешения чтения
  • еще один риск, связанный с использованием сериализации, – это одноэлементные классы. Они выглядят вот так:
public class Elvis {
    public static final Elvis INSTANCE = new Elvis();
    private Elvis() {...}
}

если мы сделаем его сериализуемым, он больше не будет одноэлементным.

  • В качестве исправления мы можем использовать метод readResolve . Этот метод принимает объект после десериализации, и объект, возвращаемый этим методом, является конечным десериализованным объектом (поэтому мы можем заменить сериализованный объект или изменить его так, как мы хотим, прежде чем он попадет к клиенту). Исправление с использованием readResolve будет выглядеть примерно так:
private Object readResolve() {
    // return the only instance of Elvis and garbage collector will take care of
    // the deserialized Elvis impersonator which will be ignored here:
    return INSTANCE;
}
  • если мы зависим от readResolve для управления экземпляром, все поля экземпляра с типами ссылок на объекты должны быть объявлены переходными, в противном случае может возникнуть вероятность атаки.
  • Мы можем предотвратить эту атаку с помощью более предпочтительного подхода, который использует перечисление:
public enum Elvis {
    INSTANCE;

    private String[] favoriteSongs = {"a", "b"};

    public void printFavourites() {
        System.out.println(Arrays.toString(favoriteSongs));
    }
}

Пункт 90

  • Рассмотрите прокси-серверы сериализации вместо сериализованных экземпляров
  • учитывайте этот шаблон всякий раз, когда вам приходится писать readObject или writeObject в классе, который не может быть расширен его клиентами
  • как упоминалось в предыдущих пунктах, использование сериализации увеличивает вероятность ошибок и проблем с безопасностью. Шаблон прокси-сервера сериализации значительно снижает эти риски
  • во-первых, создайте частный статический вложенный класс, который кратко представляет логическое состояние экземпляра заключающего класса (этот вложенный класс является прокси-сервером сериализации заключающего класса)
  • этот класс должен иметь один конструктор, тип параметра которого является заключающим классом.
  • как заключающий класс, так и прокси-сервер сериализации должны объявлять Сериализацию
  • это прокси-сервер сериализации для класса Период , описанного в пункте 50:
private static class SerializationProxy implements Serializable {
    private final Date start;
    private final Date end;

    SerializationProxy(Period period) {
        this.start = period.start;
        this.end = period.end;
    }

    private static final long serialVersionUID = 23413487423847742L; // any number will do
}
  • теперь нам нужно добавить метод writeReplace во вложенный класс:
private Object writeReplace() {
    return new SerializationProxy(this);
}
  • с помощью этого шаблона мы не должны создавать сериализованную версию заключающего класса, чтобы защитить ее от злоумышленников, добавьте это в заключающий класс:
private void readObject(ObjectInputStream stream) throws InvalidObjectException {
    throw new InvalidObjectException("Proxy required!");
}
  • наконец, предоставьте метод readResolve для прокси-класса сериализации, который возвращает логически эквивалентный экземпляр заключающего класса:
private Object readResolve() {
    return new Period(start, end);
}
  • ключевым моментом здесь является то, что в конце этот шаблон использует открытый конструктор в readResolve метод, который делает его безопасным (обычно сериализация использует экстралингвистический механизм вместо обычных конструкторов)

Оригинал: “https://dev.to/ashkanent/effective-java-chapters-10-to-12-41hk”