Эффективное программирование Java
1. Введение
2. Создание и уничтожение объектов
- Замена конструктора статическим фабричным методом
- Полезно в случае:
- Не понятное имя конструктора;
- Возножность создавать конструкторы с одинаковыми параметрами но разными именами;
- Использование созданных членов класса;
- Возможность вернуть тип наследника. Пимер: не модифицируемые Collections;
- Возвращаемый тип может меняться от типа значения параметра;
- Можно улучшать реализацию от версии к версии;
- Код эеземпляра не обязан существовать намемент вызова фабричного метода.
- Недостатки фабричного метода:
- Классы только с статическими фабричными методами не могут порождать подклассы;
- Не всегда понятные имена ФМ. Потому нужно использовать принятое именование (pages: 33-34).
- Полезно в случае:
- Шаблон Bilder вместо множества параметров (в том числе пример рекурсивного Generic(pages: 40-41)
- Плюсы:
- Легче расширять;
- Лучше чем подход с JavaBeans (pages: 36-37)
- Удобнее расширять в библиотеке.
- Минусы:
- Время на нписание;
- Громоздкость при малом числе параметров.
- Синглтон
- Рассмотрен вопрос сериализации (pages: 43-45).
- Особенности реализации:
- Как поле класса;
- Метод getInstance();
- Enum с одним значением;
- Передача параметра лучше чем жестко зашитый ресурс (pages: 47-49):
- Поддержка разных версий/языков, а также при тестировании;
- Проблема dependencyInjection;
- Разные классы могут использовать один/разные версии этого класса даже не подозревая об этом;
- Избегайте создания лишних объектов:
- String s = new String("bad");
- long better than Long - не пользуем Bound Boxing где не нужно (яркий пример с циклом pages: 52-53).
- Избегайте устаревших ссылок на объекты:
- Обнулять все подряд не нужно;
- Лучший вариант: ограничение области видимости переменной и автоматическое ее удаление;
- Случай с массивом объектов, которые не используются(удаленные элементы из стека, листа. page: 54);
- Кеш;
- Обратные вызовы - лучше использовать слабые ссылки.
- Финализаторы и очестители:
- Не гарантируются;
- Устаревшие методы: System.runFinalizersOnExit(), Runtime.runFinalizersOnExit();
- Игнор исключений;
- Проблемы с производительностью;
- ДЕЛАЙТЕ КЛАСС AutoCloseable (pages: 60-62);
- Зачем же тогда они нужны?
- Контроль вызова close для подстраховки;
- Удаление платформозавсисемых узлов(С++ и пр.)
- TRY с ресурсами используем вместо try-finaly.
- Плюсы:
3. Методы общие для всех объектов
- equals()
- условия:
- Рефлексивнсть x.equals(x) = true;
- Симметричность x.equals(y) , y.equals(x);
- Транзитивность x.equals(y), y.equals(z), z.equals(x);
- Непротиворечивость for(0~100000) x.equals(x)=true/false (always);
- x.equals(null) = false.
- Правила написания своего equals(Object o):
- this==o;
- if(!(o instanceof MyType)) return fasle;
- Сравниваем значемые поля (начиная с более простых и более изменяемых);
- hashCode!!!!! нужно перекрыть при перекрытии equals (pages: 81-87).
- Правила написания своего hash:
- Для примитивных полей Type.hashCode(f);
- Сложные типы: поступайте как поступили для поля в equals, например просто вызовите hash();
- Arrays.hashCode();
- null - 0;
- result = 31 * result + fieldHashValue.
- Низко скоросной, но полезный метод: Objects.hash(value1, object2, value3);
- Рекомендации по hash функции:
- Используйте все значимые поля, иначе функция может вызвать неэффективность hashMap;
- Избегаем деталей реализации в описании, чтобы можно было поменять реализацию в будущем.
- Для примитивных полей Type.hashCode(f);
- clone & Clonable (see example on pages: 93-94):
- Переопределить интерфейс Clonable достаточно, если нет сложных переменных, иначе нужно реализовать метод clone;
- Если сложные переменные final, то их не получится склонировать, но можно рекурсивно вызывать clone (это доделывается вручную в clone);
- В случае если сложные элементы содержат ссылки друг на друга, то их тоже прийдется обновить или скопировать "по умному"(see example on pages: 95-97);
- Можно просто не поддерживать клонирование: throw new cloneNotSupportedException() (see page: 98);
- Возможно, требуется синхронизация;
- Лучший вариант: написать копирующий конструктор - это понятнее и безопаснее.;
- Массивы лучше копировать все же через clone.
- Comparable.
- Метод выполняет сравнение бъекта с указанным:
- Взвращает целое отрицательное, 0, целое положительное, если обект меньше, равен или больше указанного;
- Генерирует ClassCastException, если передан не тот тип обьекта.
- Свойства и советы:
- Позволяет быстро сортировать: Arrays.sort(arr);
- Позволяет взаимодействовать с: TreeSet;
- Используйте стандартные static методы Comparator.comparingInt().thenComparingInt(). Они на 10% медлиннее рукописного компаратора, но очень удобны (See for example page: 105).
- условия:
4 Классы и интерфейсы
- 1. Минимизируйте доступ к реализации:
- Даем всегда минимальный уровень доступа;
- Исключения - static final поля:
- Примитивные типы - норм;
- Сложные обекты очень окуратно;
- Массивы (see page: 113):
- Преобразуем в неизменяемый список;
- Возвращаем клон массива в паблик методе а сам масив закрытым.
- Есть новых 2 типа доступа на уровне пакета, но они не очень полезные(see pages: 113-114).
- 2. В открытых классах используйте метод доступа а не открытые поля:
- можно открытые, то только с пакетной видемостью;
- даже final не желательно но допустимо.
- 3. Минимизируйте изменяемость классов:
- Как создать:
- Все поля final;
- Все закрытое;
- Доступ только по методам;
- ЕСЛИ ЧТО создаем и возвращаем новый экземпляр. Это называется ФУНКЦИОНАЛЬНЫЙ подход(for example see pages: 118-119).
- Как создать:
- Приимущетва:
- Потокобезопасность;
- public static final для часто используемых экземпляров;
- Использование фабоик для кеширования часто используемых объектов;
- Не требуется создавать защищенные копии;
- Не нужно предоставить копирующий конструктор или метод clone();
- Удобный строительный блок для других объектов;
- Кеширование долгоиграющих изменений в не final полях для ускорения.
- Поблемы:
- Потеря ПРОИЗВОДИТЕЛЬНСОТИ при создании нового эеземпляра при каждой операции (примеры решения: StringBuilder, StringBuffeer).
- Особенности создания неизменяемых классов:
- Все поля-объекты тоже долдны быть неизменяемые;
- нужно запретить наследование изменяемого класса, чтобы предупредить изменяемость в наследниках. Это можно сделать с помощью фабричных методов и приватных конструкторов;
- нужно делать копию каждого изменяемого объекта, который входит в изменяемый класс.
- ВЫВОДЫ:
- Классы должны быть неизменяемыми, если нет серьезной причины для их изменяемости;
- Изменяемые классы при возможности должны иметь ограниченное число состояний;
- Все параметры по возможности private final;
- Конструкторы должны создавать полностью инициализированные обекты, все инварианты которых должны быть установлены.
- 4. Предпочитайте композицию наследодванию:
- Можно использовать наследование после композиции (for example see pages: 128-129);
- Решает проблемы изменения унаследованого класса;
- Недостатки:
- проблемы с callBack-ами;
- расход памяти и производительнсоть (на практике не оправдываются).
- Нвследование уместно только когда один обект всегда является подклассом другого;
- Наследование раскрывает детали реализации.
- 5. Проектируйте и документируйте наследование либо запрещайте его:
- Класс должен быть спроектирован для наследования:
- Конструкторы не должны вызывать public, protected методы(for example see page: 135);
- Открытые к наследованию методы не должны вызываться в классе напрямую, а через приватные функции-обертки(pages: 137-138).
- Документация содержать информацию об наследовании(for example see pages: 132-133);
- Протестировать наследуемость можно написав несколько наследников, при этом:
- Не используемые методы закрыть;
- Открыть те, которые необходимы.
- Класс должен быть спроектирован для наследования:
- 6. Предпочитайте интерфейсы абстрактным классам:
- Позволяют создавать mixin-ы - добавочную функциональность (see page: 139);
- Для множественного наследования;
- Возможно реализовать на интерфейс еще абстрактный класс, который перекроет часть методов и упростит использование интерфейса.
- 7. Внимательно проектируйте интерфейсы:
- Невозможно изменить;
- Методы по умолчанию помогают плохо. (пример с перекрытием removeIf see on pages: 144-146).
- 8. Используйте интерфейсы только для опеределения типов;
- НЕ используем тлько константные классы:
- Необходимость поддерживать их, даже если они уже не нужны;
- Запутывание пользователей.
- 9. Предпочитайте иерархию классов дескрипторам классов (когда один клас выполняет разные функции в зависемости от параметров типа TAG)(pages: 149-152)
- Запутанная реализация и нечитаемость;
- Слишком много кода;
- Много switch;
- Необходимость каждый раз запутывать класс еще больше.
- НЕ используем тлько константные классы:
- 10. Предпочитайте статические классы-члены нестатическим
- static inner ckasses:
- Не содержат ссылку на родителя;
- Можно создать без родителя.
- Анонимные классы имеют ряд ограничений:
- Нелья применить instanceof;
- Нельзя операться на имя класса;
- Нельзя неследоваь и имплиментировать интерфейс одновременно.
- static inner ckasses:
- 11. Ограничивайтесь одним классом верхнего уровня на исходный файл.
5. Обобщенное программирование
- 1. Не используйте не сформированные типы: List data = new ArrayList<String>; (правильно так: List<String> data = ...):
- Проблема: отсутствие проверки типов при компиляции;
- Используйте List<?> data. В этом случае типы БУДУТ ПРОВЕРЯТЬСЯ, а там где они не важны, они не будут использоваться (for example see page: 163);
- Исключения, где без не сформированных типов не обойтись:
- List.class (List<String>.class и List<?>.class недопустимы) (page: 164);
- instanceof Set (<String>и <?> являются просто шумом и игнорируются) (page: 164);
- Простой пример как приводить типы <?> (page: 164).
- Резюме:
- Set<Object> - может содержать обьекты любого типа;
- Set<?> - может содержать ТОЛЬКО обьекты некого неизвестного типа;
- Set - не бузопасный способ записи, которы не позволяет компилятору проверять приведение типов.
- Термины:
- List<String> - Параметризированный тип с фактическим параметром String;
- List<E> - Обобщенный тип с формальным параметром E;
- List<?> - Неограниченный тип с символом подстановки;
- List - Несформированный тип;
- <E extends Number> - Ограниченный параметр типа;
- <T extends Comparable<T>> - Рекурсивно граниченный тип;
- <? extends Number> - Ограниченный тип с символом подстановки;
- <? super Number> - Ограниченный тип с символом подстаноEeвки;
- <T extends Enum<T> & Operation> (page: 228 или пункт 6.5);
- static <E> List<E> asList(E[] a) - Обобщенный метод;
- String.class - Токен типа.
- Проблема: отсутствие проверки типов при компиляции;
- 2. Устраняйте предупреждения о непроверяемом коде:
- Анотация @SuppressWarnings("unchecked"):
- Добавляем, только когда нельзя написать по другому и проверили, что точно работает;
- Используем как можно реже и сужаем область видемости насколько можно (for example see pages: 167-168);
- Добавляем комментарий: почему мы добавили анотацию.
- Добавляем, только когда нельзя написать по другому и проверили, что точно работает;
- Анотация @SuppressWarnings("unchecked"):
- 3. Предпочитайте списки массивам:
- Массивы кованиантны(if Sub instanceof Super => Sub[ ] instanceof Super[ ]), а списки инвариантны (List<Type1> =!= List<type2>) (for example see pages: 169-170);
- Изза этих и друих (page: 169) различий массивы и обопщенные типы нельзя смешивать: List<E>[ ], new List<String>[ ], new E[ ] => ОШИБОЧНЫ;
- При сообщениях о проблемах постарайтесь заменть массивы списками (for example see pages: 171-173).
- 4-5. Предпочитайте обобщенные типы, обобщенные методы:
- Пример с добавлением обобщенных типов в код: (pages: 178-180);
- Иногда, для обобщения возможных операций, нужно создать "обобщенную фабрику синглтонов" (page: 180);
- Рекурсивное ограничение типа применяется для Comparable: <E extends Comparable E> и можно прочитать как: "Любой тип Е, который можно сравнивать с самим собой".
- 6 используйте ограниченные символы подстановки для повышения гибкости АПИ (pages: 183-188):
- Используем как параметры: <? extends Number>, <? super Number>;
- Правило: производитель extends, потребитель super (page: 185);
- Как созвращаемое значение только <E>;
- Символы подстановки должны быть "не видны" пользователям класса. Если пользователь класса должен думать о типах с символьной подстановкой, вероятно что-то не так с АПИ этого клласса.
- 7. Аккуратно сочитайте обопщенные типы и переменное число аргументов:
- Нельзя использовать массив с переменным числом аргументов вне функции, так как это тот самый массив обобщенных типов, тип которого известен лишь при компиляции (page: 192);
- Аннотацию SafeVarargs следует применять с осторожностью, но все же модно в методах с переменным числом аргументов;
- Применение List более предпочтительно, чем переменное число аргументов (for example see page: 195).
- 8. Пименяйте безопасные с точки зрени типов гетерогенные контейнеры (for example see pages: 196-198).
6. Перечисления и анотации
- 1. Используйте перечисления вместо констант int (page: 203):
- Использовани подкласса для каждого элемента enum, объединение стратегии для групп enum значений (for example see page: 214).
- 2. Используйте поля экземпляров вместо порядковых значений;
- 5. Имитируйте расширяеміе перечисления с помощью интерфейсов (pages: 225-229):
- Достаточно переопределить недастающие значения в другом enum, реализующим тот же интерфейс (pages: 225-227);
- Недостаток тут - возможное копирование похожей реализации. Для исючения копирования нужно выносить этот код в отдельный класс;
- 6. Предпочитайте анотации схемам именования:
- Пример описания анотации (page: 230), применения (page: 231) и использования (page: 232);
- Пример вызова анотируемых методов по рефлексии (page: 232);
- Анотация с параметром, пример (pages: 233-234);
- Анотация с параметром-массивом (pages: 235-236);
- Повторяющиеся однотипные анотации (pages: 236-238).
- 7. Последовательно используйте анотацию @Override;
- 8. Используйте интерфейсы-маркеры для определения типов (pages: 242-244):
- Стоит использовать интерфейсы-маркеры, если мы хотим определить тип, и анотацию, елси мы не хотим определять тип.
- 8. Используйте интерфейсы-маркеры для определения типов (pages: 242-244):
7. Лямбда выражения и потоки
- 1. Предпочитайте лямбда выражения анонимным классам.
- Если лямбда выражение длиннее 3 строк или слоджное для чтения, то либо найтдите способ его упростить, либо выполните рефакторинг для его устранения.
- 2. Предпочитайте ссылки на методы лямбда выражениям.
- Типы ссылок:
- Статическая - Intager::parseInt - str -> Intager.parseInt(str);
- Ограниченная - Instant::now()::isAfter - Instant then = Instant.now(); t -> then.isAfter(t);
- Неограниченная - String::toLowerCase - str -> str.toLowerCase();
- Конструктор класса - TreeMap<K,V>::new - () -> new TreeMap<K,V>;
- Конструктор массива - int[ ]::new - len -> new int[len].
- 3. Предпочитайте использовать стандартные функциональные интерфейсы:
- Типы ссылок:
- Есть 3 варианта каждого из этих интерфейсов для int, long, double (eg: IntPredicate page: 254);
- Стоит задуматься о написании своего интерфейса даже при существовании функционального аналога в случаях(пример: Comporator):
- Он будет часто использоваться и его имя даст описание действиям;
- С ним связан строгий контракт;
- Он выигрывает от пользовательских методов по умолчанию.
- Анотируйте свои функцональные интерфейсы @FunctionalInterface, что не даст возможность их использования более широко чем для лямбда выражений.
- 4. Разумно используйте потоки (good example see on page:264):
- Имя для метода возвращающего элементы потока рекомендуется называть как множественное число имени существительного (пример: primes, words... page: 263).
- 5. Предпочитайте в потоках функции без побочных эффектов (more discription about threads development see on pages: 265-271)
- forEach() должна использоватся только для вывода результатов потоковых вычислений, но не для выполнения вычислений, так как она не может быть разпоралелена;
- collect из класса Colectors. Результат раоты методов этого класса обычно является собранной коллекцией.
- 6. Предпочитайте коллекции потокам в качестве возвращаемых типов:
- Collection или подтип есть наилучшим типом возвращаемого значения, так как он является подтипом Iterable и имеет метод stream
- Массив можно сделать Arrays.asList или Stream.of, при желании, потому он тоже подходит;
- Не храните в памяти большую последовательность, только чтобы вернуть ее как колекцию;
- Подумайте о сокращении, например замены листа подобием адаптера;
- Есть обходные пути для итерации потоков (see code on pages: 272-273).
- 7. Будте внимательны при параллелизации потоков:
- Не рабтает паралелизация с(see code on page: 278):
- Stream.iterable;
- limit.
- Выигрыш паралелизма оказывается наибольшим в случае потоков над экземплярами:
- ArrayList;
- HashMap;
- HashSet;
- ConcorrentHashmap;
- Массивами;
- Диапазонами int и long.
- Абстаркция используемая библиотекой потоков для паралелизации есть Stream.splititerator и Iterable.splititerator;
- Пример эффективного параллельного вычисления (see code on page: 281);
- ДАЖЕ НЕ ПЫТАЙТЕСЬ распаралелить конвейер потока, если не уверены, что что это сохранит правильность вычислений и улучшит их скорость.
- Не рабтает паралелизация с(see code on page: 278):
8. Методы
- 1. Проверяйте корректность параметров:
- Чем раньше мы обнаружим ошибку, тем мение трудоемким будет ее нахождение;
- Не соблюджение этого может привести к нарушению принципа атомарности сбоев (смотри раздел 10.8);
- Метод Objects.requireNonNull() добавлен в Java7 и теперь нет никакой пичины для проверки значения null вручную;
- Также могут быть полезны методы: checkFromIndexSize, checkFromIndex, checkIndex;
- Также можно использовать assert, предварительно задав в опциях флаг командной строки: java -ea (или -enableassertions)(see code and details on page: 285);
- Особенно важно проверять параметры, которые прямо сразу не используются.
- 2. При необходимости создавайте защитные копии:
- Вы должны писать программу оборонительно, исходя из предположения, что клиенты будут пытаться разрушить его инварианты;
- Обдумайте необходимость копирования входных параметров классов, чтобы избежать их изменения после передачи;
- Избегайте возврата из класса ссылок на изменяемые обекты.
- 3. Тщательно проектируйте сигнатуры методов:
- Тщательно выберайте имена методов;
- Не заходите слишком далеко в погоне за удобством методов;
- Избегайте длинный списков параметров;
- Предпочитайте в качестве типов параметров интерфейсы, а не классы;
- Предпочитайте 2-х элементные типы перечислений вместо параметров boolean.
- 4. Перегружайте методы разумно:
- Пример с перегрузкой методов, который не работает (see example on pages: 294-296);
- Не перегружайте и функциональные интерфейсы тоже. Включение проверки этого факта компилятором: -Xlint:overloads (see details on pages: 299-300).
- 5. Используйте методы с переменным колличесвом аргументов с осторожностью;
- 6. Возвращайте пустые коллекции и массивы а не null. Аргументацию смотри в книге (see details on page: 305):
- Можно использовать вместо создания пустых объектов: Colections.emptyList(), Colections.emptySet(), Colections.emptyMap();
- Выделение массива для возврата зачений заранее вредит производительности(see details on page: 307).
- 7. Возвращайте Optional с осторожностью (see details on pages: 307-312);
- 8. Пишите документирующие коментарии для открытых элементов API (see details on pages: 312-320):
- Например, если метод запускает фоновый поток - это должно быть отражено в документации;
- Документацию к пакету нужно помещать в файл packege-info.java, к модулю к module-info.java (see details on page: 319);
- Документируйте вопросы потоковой безопасности и возможность сериализации (see details on page: 319);
- Документирование можно наследовать используя дескриптор {@inheritDoc} (see details on page: 319);
- Если существует документ описываюий архитектуру класса/пакета/модуля, то документация должна содержать ссылку на него;
- Более детальная информация про документирования смотри "Haw to write doc comments[23]".
9. Общие вопросы программирования
- 1. Минимизируйте область видемости локальных переменных (see details on pages: 321-324):
- Обьявляйте переменную там, где ее в первый раз используюте
- Цикл for предпочтительней чем while, так как:
- Сужает область видемости;
- Допускает копипаст с теми же переменными;
- Допускает ополнительные переменные, в том числе и переменную size;
- Сохраняйте малый размер и точную направленность методов, это способствует минимизации видемости.
- 2. Предпочитайте циклы for для колекции традиционным циклам for(see details on pages: 324-328):
- Подумайте про реализацию интерфейса Iterable(даже не реализовую Colection), чобы ваш класс мог быть подвержен обходом for как для колекции.
- 3. Изучите и используйте возможности библеотек:
- Random:
- Используйте Random.nextInt() (see details on pages: 328-329);
- Для большенства случаев используем ThresdLocalRandom (он быстрее Random в 3,6 раза);
- Для пулов и паралельных потоков используйте SplittableRandom.
- Скорость библиотек увеличивается. Некоторые библиотеки переписываются годами;
- Библиотеки расширяются;
- Другие разработчики тоже используют библиотеки. Это делает код более простым;
- Библиотеки достаточно велики, но стоит просмотреть хотя-бы:
- java.lang
- java.util
- java.io
- Random:
- 4. Если вам не нужны точные ответы, избегайте float и double (see details on pages: 331-334):
- Используйте BigDecimal. Он имеет 8 режиов округления.;
- Используйте int или long в зависемости от размерности вычислений. Вместо дробных частей можно вести подсчет в центах, например;
- До 9 цифр годится int, до 18 long, иначе только BigDecimal (see details on pages: 334).
- 5. Предпочитайте примитивные типы упакованным примитивным:
- Различия между типами:
- Два упакованных типа могут иметь одинаковое значение но разную идентичность;
- Упакованный тип может быть null;
- Примитивный типы более эффективны с точки зрения быстродействия и потребления памяти.
- "==" к упакованным типам просто сравнивает значение ссылок;
- Учитывайте, что упакованные типы нужно инициализировать;
- Учитывайте, что упакованные типы при сравнении и любых действиях система сначала распаковывает и на это уходит время.
- Различия между типами:
- 6. Избегайте применения строк там, где уместнее другой тип (see details on pages: 338-341):
- Строки - плохая замена другим типам значений (особенно, что касается хранения вводимых значений);
- Строки - плохая замена для перечислений;
- Строки - плохая замена для агрегатных типов;
- Строки - плохая замена надежным (устойчивым к подделке) ключам.
- 7. Помните о проблемах производительности при конкотинации строк (see details on pages: 341-342);
- 8. Для ссылки на обьекты используйте их интерфейсы (see details on pages: 344-348);
- 9. Предпочитайте интерфейсы рефлексии (see details on page: 346):
- Вы теряете преимущества проверки типов времени компиляции;
- Код необходимый для рейлексивного доступа к класам неуклюжий и многословный;
- Проблемы с производительностью (в 7 раз медленнее);
- Даже если рефлексия необходима, то постарайтесь ограничеться созданием экземпляра а потом все-же обращайтесь к нему без рефлексии.
- 10. Пользуйтесь машинно-зависемыми методами с осторожностью;
- 11. Оптимизируйте осторожно.
- Старайтесь писать хорошие а не быстрые программы;
- Старайтесь избегать проэктных решений, которые ограничевают производительность;
- Измеряйте производительность до и после каждой попытки оптимизайии;
- Используйте средства профилирования, например jmh (see details on page: 352);
- Существуеи много мифов о производительности, которые на поверку оказываються полуправдой а то и просто ложью;
- Не старайтесь писать быстрые программы - старайтесь писать хорошие программы, - скорость прийдет сама;
- Не забывайте о производительности, когда проектируете систему, особенно, когда проэетируете АПИ, протоколы физического уровня и форматы данных;
- Первый шаг оптимизации заключается в выборе алгоритма: никакая низкоуровневая оптимизация не сможет скомпенсировать плохой выбор алгоритма.
- 12. Придержуйтесь общепринятых соглашений по именованию (see details on pages: 353-357).
- Кроме всего прочего руководствуйтесь сдравым смыслом.
10. Исключения
- 1. Используйте исключения только в исключительных ситуациях (see details on pages: 359-362);
- 2. Используйте для восстановления проверяемые значения, а для програмных ошибок исключения времени выполнения (see details on pages: 362-364):
- Если у вас возникает сомнения, возмодно ли восстановить состояние и какое исключение генерировать, то генерируйте непроверяемые исключения;
- По-возможности используйте стандартные исключения;
- Добавляйте в объект/класс исключения дополнительную информацию, чтобы не заставлять пользователя АПИ анализировать строковую информацию в коде.
- 3. Избегайте ненужных проверяемых исключений:
- Если программист все равно ничего не сможет сделать в обработке исключения, кроме как проигнорировать его, то лучше использовать непроверяемое исключение (see details on page: 365);
- Помним о проблемах работы в многопоточности. Кто будет ловить исключение? (see details on page: 366)
- Альтернатива проверяемому исключению:
- вернуть Optional(см раздел 8.7), но в этом случае есть недостаток: пользователь не получит информацию о ситуации;
- разбить на 2 метода, один из которых проверяет можно ли выполнить действие, а второй выполняет (с оговоркой о потокобезопасности) (see details on pages: 366-367);
- только если эти 2 подхода не подходят рассматриваем вариант генерации исключения.
- 4. Предпочитайте использовать стандартные исключения.
- Чаще всего используемые исключения: (see details on pages: 368-369)
- IllegalArgumentException - неверное ненулевое значение;
- IllegalStateException - неверное состояние объекта для вызова метода;
- NullPointerException - неразрешеное нулевое значение параметра;
- IndexOutOfBoundsException - индексний параметр за границей допустимого диапазона;
- ConcurrentModificationException - обнаружено запрешеное паралельное использование объекта;
- UnsupportedOperationException - объект не поддерживает метод.
- Никогда не используйте напрямую приведенные ниже исключения. Рассматривайте их как абстрактные. Это поможет отличить ваше ситуацию от общего случая: (see details on pages: 368-369)
- Exception
- RuntimeException
- Throwable
- Error
- При наследовании помним, что исключения сериалезуемы(глава 12).
- 5. Генерируйте исключения, соответствующие абстракции:
- При перехвате и передаче исключения выше на уровень следует использовать не то же исключение а исключение, понятное по контексту данного уровня (трансляция исключений see details on pages: 370-371);
- Иногда полезна цепочка исключений (склеиваниеисключений see details on page: 371).
- 6. Документируйте все исключения, которые может генерировать метод:
- Индификатор @throws используется для описани только проверяемых исключений;
- Документирвать все непроверяемые исключения - недостижимый идеал;
- Документацию общих исключений для разных методов класса можно переместить в описание класса.
- 7. Включайте в сообщение информацию о сбое:
- Добавляем все параметры, приведше к сбою;
- НЕ включаем пароли и прочую приватную информцию.
- 8. Добивайтесь атомарности сбоев (see details on pages: 376-378):
- Метод, вызов которого завершился сбоем должен оставить должен оставить проверяемый обект в том же состоянии, в каком тот был перед вызовом. Метод, обладающий таким свойством называют атамарным по отношению к сбою (failure anatomic, see details on page: 376);
- Способы достижения:
- Неизменяемость объекта;
- Проверка критических значений перед изменением объекта;
- Произведение изменений с копией объекта;
- Использование специального кода восстановления.
- Иногда соблюдение этого правила имеет слишком высокую цену, в этом случае желательно описать состояние объекта после исключения в документации.
- 9. Не игнорируйте исключения
- Если вы игнорируете блок catch, по крайней мере оставте коментарий.
11. Паралельные вычисления
- 1. Синхронизируйте доступ к совместно используемым изменяемым данным:
- Рассмотрены примеры ошибок в синхрогизации:
- Использовать атомарность вместо синхронизации - это опасный совет (see details on page: 382);
- Спецификация языка java гарантирует, что чтение и запись в переменную атамарная операция, если только это не long или double (see details on page: 382).
- Не используйте Thread.stop(). Лучше иметь boolean переменную с синхронизированным доступом (see details on pages: 382-384);
- Доступ к переменной boolean должен быть синхронизирован (see details on pages: 382-384).
- Используйте volatile с осторожностью (see details on page: 385);
- ++ не является атомарным.
- Хроший вариант - использовать java.concurent.anatomic.AnatomicLong, это лучше и быстрее чем volatile или syncronized (see details on pages: 385-386);
- Наилучший вариант - ограничивать изменяемые данные одним потоком;
- Будте окуратны: некоторые методы могут использовать потоки без Вашего ведома.
- Вполне допустимо чобы один поток изменял обьект данных, а затем совместно использовал его с другими потоками. Такие обьекты называютяс фактически неизменяемыми, а передача ссылки называется безопасной публикацией (see details on page: 386).
- Использовать атомарность вместо синхронизации - это опасный совет (see details on page: 382);
- 2. Избегайте излишней синхронизации.(see details on pages: 386-394):
- Никогда не передавайте управление клиенту из синхронизированного метода или блока;
- Можно использовать для динамически удаляемых элементов CopyOnWriteArrayList. Для большенства применений производительнсоть будет ужастной, но он будет идеален, если мы часто обходим лист, и редко его меняем (see details on pages: 391-392);
- Как правило нужно выполнять как можно меньше действий внутри синхронизируемой области;
- Если требуются какие-либо длительные действия - выносите их за пределы области блокировки.
- Есть 2 способа синхронизации:
- Внутренняя синхронизация в обекте (StringBuffer);
- И отсутствие синхронизации внутри, и оставление этого на совесть клиента (StringBuilder).
- Доступ к статическим полям должен быть синхронизирован внутренне, так как это поле может меняться даже из разных клиентов.(see details on page: 393).
- 3. Предпочитайте исполнителей заданиям, потоки данных потокам исполнения:
- Краткий обзор Executors (see details on pages: 394-396);
- Воздержитесь от написания собственных рабочих очередейб, и от прямой работы с потоком.(see details on page: 395):
- Используйте задачу как абстракцию;
- Полезные классы: задание с ветвлением ForkJoinTask, может быть разделено на подзадачи ForkJoinPool.
- 4. Предпочитайте утилиты параллельности методам wait & notify (see details on pages: 396-402):
- Высокоуровневые утилиты паралельности:
- Каркаc исполнителей (see details on page: 395);
- Паралельные коллекции:
- BlockingQueue;
- ConcurentHashMap.
- Синхронизаторы(cynchronizer) (see details on page: 398):
- CountDownLatch (see example on page: 399);
- Semaphore;
- CyclicBarier;
- Exchanger;
- Phaser.
- Блокировка голодания потока (thread starvation deadlock) (see details on page: 400):
- Для определения продолжительности интервалов всегда используйте System.nanoTime a не System.currentTimeMillis, так как метод System.nanoTime более окуратен и точен, на него не влияюют коректировки системных часов;
- Точное измерение времени - трудная задача. Для этго рекомендуется использовать один из спец каркасов таких как jmh.
- Как пользоваться wait (see example on page: 400):
- используем wait только в цикле while.
- Случаи ложного пробуждения потока (see details on page: 401):
- notify или notifyAll? Лучше вызывать notifyAll, так как препядствует залипанию программы (see details on page: 401).
- Высокоуровневые утилиты паралельности:
- 5. Докуменируйте безопасность с точки зрения потоков(see details on pages: 402-406):
- Уровни безопаснисти с точки зрения потоков: (see details on page: 403):
- Неизменяемый (immutable);
- unconditionally thread-safe;
- conditionally thread-safe;
- not thread-safe;
- Не совместимый с многопоточностью: thread-hesitate.
- Пример документирования (see example on page: 404);
- denial-of-service DoS атака - последствия неправильного использования блокировки с открытым доступом (see example on page: 404):
- Для предотвращения такой блокировки нужно использовать закрытый обьект блокировки(private lock object) (see example on pages: 404-405):
- Поля внутренних блокировок должны быть обявленны как final (see details on page: 405).
- Для предотвращения такой блокировки нужно использовать закрытый обьект блокировки(private lock object) (see example on pages: 404-405):
- Уровни безопаснисти с точки зрения потоков: (see details on page: 403):
- 6. Аккуратно применяйте отложенную инициализацию (see examples on pages: 406-409):
- Есть значительные накладные расходы для этого подхода, в особенности при многопоточном использовании;
- Делайте отложенную инициализацию так:
- Идиома класса отложенгой инициализации (see details on page: 407);
- Идиома двойной првоерки (see details on page: 408).
- Рассмотрены примеры ошибок в синхрогизации:
- 7. Избегайте зависимости от планировщика потоков:
- Колличество работающих потоков должжно быть не больше колличества ядер;
- Потоки не долны запускаться, если они не выполняют полезную работу:
- Правельный размер пула потоков и небольшие (но и не слишком маленькие) задания (see details on page: 410).
- Поток не должен быть в активном ожидании, постоянно опрашивая состояние занятости обекта (see examples on pages: 410-411);
- Столкнувшись с программой, которая едва работает изза недостатка времени для потоков:(see details on pages: 411-412)
- НЕ поддавайтесь соблазну поставить Thread.yeald() - такие решения могут работать по разному на разных java машинах;
- НЕ попробуйте настроить приоритеты потоков. Ситуация с ними та же;
- Ищите фундоментальные проблемы в работе с потока.
12. Сериализация
- 1. Предпочитайте альтернативы сериализации java:
- Примеры атаки через сериализацию (see example on page: 415);
- Альтернативы: (see details on page: 416)
- JSON;
- protobuf;
- Если вы все же используете сериализацию, то:
- Никогда не сериализуййте не проверенные данные;
- Если это невозможно, применяйте фильтр входящих классов (see details on page: 417).
- 2. Реализуйте интерфейс Serialazible крайне осторожно(see details on pages: 418-421):
- Сериализация накладывает ограничения на изменение класса, а иногда даже на переименование методов (see details on pageы: 418-419);
- Повышает вероятность появления ошибок и брешей в системе безопасности;
- Выпуск новой версии класса сопряжен с большей работой по тестированию;
- Реализация интерфейса Serialazible должна быть тщательно продумана;
- Классы, предназначенные для наследования, редко должны реализовывать Serialazible, а интерфейсы - редко его расширят;
- Внутренние классы не должны реализовывать Serialazible (из-за ссылки на парент класс).
- 3. Подумайте о применении пользовательской сериализованной формы:
- Нельзя принимать сереализизованную форму предлагаемую по умолчанию, не обдумав сперва насколько она вас устоавивает;
- Даже если вы решите, что сеиализованная форма по умолчанию для вас пригодна, имейте ввиду, что зачастую для обеспечения инвариантов и безопасности требуется предоставление метода readObject;
- Подводные камни сериализации:(see details and example on pages: 424-425)
- Связывание внутреннего представления;
- Потребление черезмерной памяти;
- Слишком много времени;
- Возможно переполнение стека.
- Хороший пример сериализации:
- Простой (see example on page: 422);
- Усложненный (see example on pages: 425-426):
- transient - пометка для не сереализуемых полей;
- Помните про синхронизацию (see example on pages: 427-428);
- Явным образом обьявляйте индефикатор версии сериализации (see example on page: 428).
- 4. Создавайте защищенные методы readObject:
- Метод readObject во многом похож на конструктор и к его написанию следует подойти очень осторожно (see details and example on pages: 429-430);
- Глава содержит примеры проблем реализацмм Serialazible и примеры возможных атак (see details and example on pages: 429-435).
- 5. Для управления экземпляром предпочитайте типы перечислений метду readResolve:
- Синглтон перестает таковым буть, если он Serialazible;
- Используйте enum.INSTANCE.
- 6. Подумайте о применении прокси агента сеиализации вместо сериализованных экземпляров (see example on pages: 440-444):
- Это лучшее реение для сериализации;
- Проблемы решения:
- Циклические ссылки;
- На 14% медлиннее.