Статьи

Java 8 Пятница: темная сторона Java 8

В Data Geekery мы любим Java. И так как мы действительно входим в свободный API jOOQ и запросы DSL , мы абсолютно взволнованы тем, что Java 8 принесет в нашу экосистему.

Ява 8 Пятница

Каждую пятницу мы показываем вам пару замечательных новых функций Java 8 в виде учебника, в которых используются лямбда-выражения, методы расширения и другие замечательные вещи. Вы найдете исходный код на GitHub .

Темная сторона Java 8

До сих пор мы демонстрировали захватывающие части этого нового основного выпуска . Но есть и предостережения. Многие из них. То, что

  • … Сбивают с толку
  • … не правы
  • … Опущены (на данный момент)
  • … Опущены (надолго)

У основных выпусков Java всегда есть две стороны. С другой стороны, мы получаем много новой функциональности, которая, по мнению большинства людей, просрочена . В других языках платформы были дженерики задолго до Java 5. В других языках платформы были лямбды задолго до Java 8. Но теперь мы наконец-то получили эти возможности. В обычном изворотливом Java-стиле.

Лямбда-выражения были введены довольно элегантно. Идея возможности записать каждый анонимный экземпляр SAM в качестве лямбда-выражения очень убедительна с точки зрения обратной совместимости. Так каковы темные стороны Java 8?

Перегрузка становится еще хуже

Перегрузка, дженерики и varargs не дружат. Мы объяснили это в предыдущей статье , а также в этом вопросе переполнения стека . Это может быть не повседневные проблемы в вашем странном приложении, но они очень важные проблемы для разработчиков и сопровождающих API.

С лямбда-выражениями все становится «хуже». Таким образом, вы думаете, что можете предоставить некоторый удобный API, перегружающий существующий метод run() который принимает Callable чтобы также принять новый тип Supplier :

1
2
3
4
5
6
7
static <T> T run(Callable<T> c) throws Exception {
    return c.call();
}
 
static <T> T run(Supplier<T> s) throws Exception {
    return s.get();
}

То, что выглядит как совершенно полезный код Java 7, сейчас является основной проблемой в Java 8. Потому что вы не можете просто вызвать эти методы с лямбда-аргументом:

1
2
3
4
5
public static void main(String[] args)
throws Exception {
    run(() -> null);
    //  ^^^^^^^^^^ ambiguous method call
}

Везет, как утопленнику. Вам придется прибегнуть к любому из этих «классических» решений:

1
2
3
4
5
6
7
run((Callable<Object>) (() -> null));
    run(new Callable<Object>() {
        @Override
        public Object call() throws Exception {
            return null;
        }
    });

Таким образом, хотя всегда есть обходной путь, эти обходные пути всегда «отстой». Это довольно обидно, даже если вещи не ломаются с точки зрения обратной совместимости.

Не все ключевые слова поддерживаются по умолчанию

Методы по умолчанию являются хорошим дополнением. Некоторые могут утверждать, что у Java наконец есть черты . Другие явно отмежевываются от этого термина, например, Брайан Гетц:

Основной целью добавления методов по умолчанию в Java была «эволюция интерфейса», а не «черты бедняка».

Как найдено в списке рассылки lambda-dev.

На самом деле методы по умолчанию — это немного ортогональная и нерегулярная особенность для всего остального в Java. Вот несколько критических замечаний:

Они не могут быть окончательными

Учитывая, что методы по умолчанию также можно использовать как удобные методы в API:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
public interface NoTrait {
 
    // Run the Runnable exactly once
    default final void run(Runnable r) {
        //  ^^^^^ modifier final not allowed
        run(r, 1);
    }
 
    // Run the Runnable "times" times
    default void run(Runnable r, int times) {
        for (int i = 0; i < times; i++)
            r.run();
    }
}

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

Они не могут быть синхронизированы

Облом! Это было бы трудно реализовать на языке?

1
2
3
4
5
6
7
public interface NoTrait {
    default synchronized void noSynchronized() {
        //  ^^^^^^^^^^^^ modifier synchronized
        //  not allowed
        System.out.println("noSynchronized");
    }
}

Да, synchronized используется редко, как финал. Но когда у вас есть этот вариант использования, почему бы просто не позволить это? Что делает тела метода интерфейса такими особенными?

Ключевое слово по умолчанию

Это, пожалуй, самый странный и нерегулярный из всех функций. Само ключевое слово по default . Давайте сравним интерфейсы и абстрактные классы:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Interfaces are always abstract
public /* abstract */ interface NoTrait {
 
    // Abstract methods have no bodies
    // The abstract keyword is optional
    /* abstract */ void run1();
 
    // Concrete methods have bodies
    // The default keyword is mandatory
    default void run2() {}
}
 
// Classes can optionally be abstract
public abstract class NoInterface {
 
    // Abstract methods have no bodies
    // The abstract keyword is mandatory
    abstract void run1();
 
    // Concrete methods have bodies
    // The default keyword mustn't be used
    void run2() {}
}

Если бы язык был переработан с нуля, он, вероятно, обойдется без abstract или default ключевых слов. Оба не нужны. Сам факт наличия или отсутствия тела является достаточной информацией для компилятора, чтобы оценить, является ли метод абстрактным. Т.е. как все должно быть:

1
2
3
4
5
6
7
8
9
public interface NoTrait {
    void run1();
    void run2() {}
}
 
public abstract class NoInterface {
    void run1();
    void run2() {}
}

Выше было бы гораздо скуднее и регулярнее. Жаль, что полезность default никогда не обсуждалась EG. Ну, это обсуждалось, но ЭГ никогда не хотела принимать это как вариант. Я попытал счастья с этим ответом :

Я не думаю, что # 3 — вариант, потому что интерфейсы с телами методов изначально неестественны. По крайней мере, указание ключевого слова «по умолчанию» дает читателю некоторый контекст, почему язык допускает тело метода. Лично я хотел бы, чтобы интерфейсы оставались чистыми контрактами (без реализации), но я не знаю лучшего варианта развития интерфейсов.

Опять же, это четкое обязательство со стороны EG не придерживаться концепции «черт» в Java. Методы по умолчанию были чисто необходимыми средствами для реализации 1-2 других функций. Они не были хорошо спроектированы с самого начала.

Другие модификаторы

К счастью, static модификатор попал в спецификации в конце проекта. Таким образом, теперь возможно указывать статические методы в интерфейсах. По некоторым причинам, однако, эти методы не нуждаются (и не допускают!) В ключевом слове по default , которое должно быть абсолютно случайным решением EG, точно так же, как вы, очевидно, не можете определить static final методы в интерфейсах.

Хотя модификаторы видимости обсуждались в списке рассылки lambda-dev , но они выходили за рамки этого выпуска. Возможно, мы можем получить их в будущем выпуске.

Несколько методов по умолчанию были реализованы

Некоторые методы имеют разумные реализации по умолчанию на интерфейсе — можно догадаться. Интуитивно понятно, что интерфейсы коллекций, такие как List или Set будут иметь их в своих методах equals() и hashCode() , потому что контракт для этих методов четко определен для интерфейсов. Он также реализован в AbstractList с использованием listIterator() , который является разумной реализацией по умолчанию для большинства индивидуальных списков.

Было бы замечательно, если бы эти API были модифицированы, чтобы упростить реализацию пользовательских коллекций с помощью Java 8. Я мог бы сделать все свои бизнес-объекты, например, реализующими List , не тратя при этом одно наследование базового класса на AbstractList .

Вероятно, однако, была веская причина, связанная с обратной совместимостью, которая помешала команде Java 8 в Oracle реализовать эти методы по умолчанию. Кто бы ни отправил нам причину, по которой это было пропущено, получит бесплатную наклейку jOOQ !

Не был изобретен здесь — менталитет

Это тоже несколько раз подвергалось критике в списке рассылки lambda-dev EG. И пока я пишу эту серию блогов , я могу только подтвердить, что новые функциональные интерфейсы очень запутанны для запоминания. Они сбивают с толку по этим причинам:

Некоторые примитивные типы более равны, чем другие

Типы int , long , double примитивы являются предпочтительными по сравнению со всеми остальными, поскольку они имеют функциональный интерфейс в пакете java.util.function и во всем API-интерфейсе Streams. boolean является гражданином второго сорта, поскольку он все еще превращен в пакет в форме BooleanSupplier или Predicate , или еще хуже: IntPredicate .

Все остальные примитивные типы на самом деле не существуют в этой области. Т.е. нет специальных типов для byte , short , float и char . Хотя аргумент о соблюдении сроков, безусловно, является верным, этот причудливый статус-кво еще больше усложнит изучение языка для новичков.

Типы не просто называются Function

Будем откровенны. Все эти типы просто «функции». Никто на самом деле не заботится о неявной разнице между Consumer , Predicate , UnaryOperator и т. Д.

На самом деле, когда вы ищете тип с не void возвращаемым значением и двумя аргументами, как бы вы его назвали? Function2 ? Ну, ты был не прав. Это называется BiFunction .

Вот дерево решений, чтобы узнать, как называется искомый тип:

  • Ваша функция возвращает void ? Это называется Consumer
  • Ваша функция возвращает boolean ? Это называется Predicate
  • Ваша функция возвращает int , long , double ? Это называется XXToIntYY , XXToLongYY , XXToDoubleYY что-то
  • Ваша функция не принимает аргументов? Это называется Supplier
  • Ваша функция принимает один int , long , double аргумент? Это называется IntXX , LongXX , DoubleXX что-то
  • Ваша функция принимает два аргумента? Это называется BiXX
  • Ваша функция принимает два аргумента одного типа? Это называется BinaryOperator
  • Ваша функция возвращает тот же тип, что и один аргумент? Это называется UnaryOperator
  • Ваша функция принимает два аргумента, первый из которых является ссылочным типом, а второй — примитивным? Он называется ObjXXConsumer (только пользователи существуют с этой конфигурацией)
  • Остальное: это называется Function

О Боже! Мы, безусловно, должны перейти к Oracle Education, чтобы проверить, резко ли выросла цена на курсы Oracle Certified Java Programmer в последнее время … К счастью, с помощью лямбда-выражений нам вряд ли когда-нибудь придется запоминать все эти типы!

Подробнее о Java 8

Обобщения Java 5 принесли много замечательных новых возможностей в язык Java. Но было также немало предостережений, связанных с стиранием типов. Методы по умолчанию в Java 8, Streams API и лямбда-выражения снова принесут множество замечательных новых возможностей в язык и платформу Java. Но мы уверены, что переполнение стека скоро наполнится вопросами запутанных программистов, которые теряются в джунглях Java 8.

Изучить все новые функции будет непросто, но новые функции (и предостережения) останутся здесь. Если вы Java-разработчик, вам лучше начать практиковать сейчас, когда у вас есть такая возможность. Потому что нам предстоит долгий путь.

Ссылка: Java 8 Пятница: темная сторона Java 8 от нашего партнера по JCG Лукаса Эдера из блога JAVA, SQL и JOOQ .