Статьи

Java: необязательная реализация необязательного

Класс 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, являются их собственными.