Класс java.util.Optional
реализован как один неизменный конкретный класс, который внутренне обрабатывает два случая; один с элементом и один без. Не лучше ли было бы позволить Optional
быть интерфейсом и иметь вместо него две разные реализации? В конце концов, это то, что нас обычно учат делать на объектно-ориентированном языке.
В этой статье мы узнаем о некоторых потенциальных аргументах для текущей реализации Optional
. Мы также узнаем, почему потоки реализованы по-другому, позволяя получать потоки из файлов или даже таблиц базы данных.
Реальная дополнительная реализация
Реальный java.util.Optional::get
реализован, как показано ниже:
1
2
3
4
5
6
|
public T get() { if (value == null ) { throw new NoSuchElementException( "No value present" ); } return value; } |
Как видно, есть два пути кода; один, где значение равно нулю (нет элемента и выбрасывается исключение) и одно, когда значение является чем-то другим (возвращается значение).
Необязательная Необязательная реализация
Давайте представим, что мы вернемся к машине времени, и перед нами была поставлена задача внедрить Optional
еще раз. Я думаю, что вполне вероятно, что многие из нас придумали бы исходное решение, очень похожее на приведенное ниже (я назвал Option
гипотетического интерфейса, чтобы мы могли отличить его от «реального») с двумя различными реализациями (здесь EmptyOption
и PresentOption
):
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
public interface Option<T> { T get(); boolean isPresent(); public <U> Option<U> map(Function<? super T, ? extends U> mapper); static <T> Option<T> empty() { return (Option<T>) EmptyOption.EMPTY; } static <T> Option<T> of(T value) { return new PresentOption<>(value); } static <T> Option<T> ofNullable(T value) { return value == null ? empty() : of(value); } } final class EmptyOption<T> implements Option<T> { static final EmptyOption<?> EMPTY = new EmptyOption<>(); private EmptyOption() {} @Override public T get() { throw new NoSuchElementException(); } @Override public boolean isPresent() { return false ; } @Override public <U> Option<U> map(Function<? super T, ? extends U> mapper) { requireNonNull(mapper); return (Option<U>) EMPTY; } } final class PresentOption<T> implements Option<T> { private final T value; PresentOption(T value) { this .value = requireNonNull(value); } @Override public T get() { return value; } @Override public boolean isPresent() { return true ; } @Override public <U> Option<U> map(Function<? super T, ? extends U> mapper) { requireNonNull(mapper); return Option.ofNullable(mapper.apply(value)); } } |
Для краткости показаны только несколько методов, но принцип остается тем же: различные реализации для случая, когда элемент присутствует, а когда его нет. Это дает гораздо более четкий код, а также открывает возможность для каждого реализовать дополнительные возможности.
Анализ
Я уверен, что этот тип решения был оценен командой JDK в то время, когда Optional
был задуман, и я думаю, что это было хорошо обоснованное решение не выбирать это решение. Optional
изначально предназначался для «переноса» возвращаемых значений для защиты от NPE и других недостатков возврата необработанных нулевых значений. Я также думаю, что цель проекта состояла в том, чтобы при использовании Optional
было незначительное или незначительное влияние на производительность.
Далее я рассуждаю о некоторых аргументах, чтобы избрать настоящую опциональную реализацию вместо той, что была приведена выше.
Загрязнение профиля
JIT-компилятор компилирует байт-код Java по требованию для повышения производительности по сравнению с интерпретацией байт-кода.
Чтобы сделать это эффективно, JIT-компилятор может собирать статистику для каждого известного метода. Каждый метод может иметь объект MethodData
который содержит метрики о том, как метод используется, и такой объект создается, когда JVM считает, что метод достаточно «теплый» (то есть вызван в некотором смысле достаточно).
Процесс создания и поддержки MethodData
называется «профилированием».
«Загрязнение профиля» происходит, когда метод используется существенно по-разному между вызовами, включая, но не ограничиваясь, предоставлением чередующихся ненулевых / нулевых элементов и вызовом различных полиморфных методов (например, параметр является общим для типа T
и вызываемый метод вызывает T::equals
). Краеугольным камнем Java является ее способность динамически вызывать методы. Таким образом, когда вызывается Option::get
, либо EmptyOption::get
либо
PresentOption::get
в конечном счете вызывается в зависимости от того, какая реализация присутствует во время вызова.
После того, как метод был вызван около 10000 раз, JIT-компилятор использует MethodData
для создания эффективного фрагмента скомпилированного кода, который выполняется наилучшим образом, учитывая собранную статистику.
Таким образом, если элементы присутствуют постоянно (с использованием PresentOption
) и код компилируется с учетом этого, но затем внезапно появляется EmptyOption
, код должен «отступить» и пойти по гораздо более медленному пути кода.
При использовании Optional
только в одном последнем классе никогда не может быть никакой другой реализации методов Optional
и, следовательно, не будет загрязнения профиля из-за различных реализаций. JIT может сделать детерминированное и достаточно быстрое определение скомпилированного кода.
Но подождите, разве JVM не сможет проверить все классы при запуске и определить, что на самом деле существует только два реализующих класса
Option
а потом он мог все это выяснить? Ну нет. Мы можем добавлять классы в любое время, поэтому невозможно было бы безопасно перечислить все возможные реализации конкретного интерфейса. По крайней мере, пока у нас не появятся настоящие запечатанные классы на Java.
Загрязнение API
Если бы люди могли свободно писать пользовательские реализации Optional
, то эти реализации, скорее всего, страдали бы недостатками / отклонениями проекта по сравнению со встроенным Optional
. Кроме того, люди, скорее всего, позволят своим собственным типам реализовать интерфейс, Optional
добавляющий к бремени компилятора / профилировщика JIT, и, таким образом, соблазнят людей использовать составные типы (например, Foo implements Bar, Optional<Bazz>)
который не был предназначен.
Кроме того, Optional
теперь является неотъемлемой частью Java и, как таковой, его можно эффективно развивать с помощью самого JDK, включая, возможно, встроенные классы и другие новые будущие функции Java.
Необязательно против потоков
В отличие от Optional
, java.util.stream.Stream
и специализированные версии, такие как IntStream
, действительно являются интерфейсами. Почему Stream
не является конкретным единственным финальным классом, как Optional
?
Ну, у Streams совершенно другой набор требований. Потоки могут быть получены из Collection
или массива, но есть гораздо более мощные способы получения Stream
. Можно получить Stream
из файла, сокета, генератора случайных чисел и даже из таблиц в базе данных. Эти функции было бы невозможно реализовать, если бы Stream был запечатан.
Speedment Stream — это пример библиотеки, которая позволяет получать стандартные потоки Java практически из любой базы данных. Узнайте больше о Speedment Stream здесь.
Вывод
Optional
опечатан и есть веские причины, почему. Внутренняя реализация Optional
менее ясна, но это цена, которую стоит заплатить с преимуществами лучшей производительности и более четкого пользовательского кода.
Потоки — это незапечатанные интерфейсы, которые могут быть реализованы любым пользователем и могут использоваться для получения элементов из различных источников, включая файлы и таблицы базы данных. Speedment Stream ORM можно использовать для получения потоков из таблиц базы данных.
Скачайте Speedment Stream здесь .
См. Оригинальную статью здесь: Java: необязательная реализация необязательного Мнения, высказанные участниками Java Code Geeks, являются их собственными. |