Статьи

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

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

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

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

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

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

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

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

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

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. Потому что вы не можете просто вызвать эти методы с лямбда-аргументом:

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

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

run((Callable<Object>) (() -> null));
run(new Callable<Object>() {
    @Override
    public Object call() throws Exception {
        return null;
    }
});

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

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

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

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

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

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

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

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

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.

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

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

public interface NoTrait {
    default synchronized void noSynchronized() {
        //  ^^^^^^^^^^^^ modifier synchronized
        //  not allowed
        System.out.println("noSynchronized");
    }
}

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

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

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

// 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 ключевых слов. Оба не нужны. Сам факт наличия или отсутствия тела является достаточной информацией для компилятора, чтобы оценить, является ли метод абстрактным. Т.е. как все должно быть:

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. И пока я пишу эту  серию блогов , я могу только подтвердить, что новые функциональные интерфейсы очень запутанны для запоминания. Они сбивают с толку по этим причинам:

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

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

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

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

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

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

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

  • Ваша функция возвращает  void? Это называется Consumer
  • Ваша функция возвращает  boolean? Это называется Predicate
  • Есть ли вернуть вашу функцию  intlongdouble? Это называется XXToIntYYXXToLongYYXXToDoubleYY что — то
  • Ваша функция не принимает аргументов? Это называется Supplier
  • Несет ли ваша функция один  intlongdouble аргумент? Это называется  IntXXLongXXDoubleXX что — то
  • Ваша функция принимает два аргумента? Это называется 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, опубликованными в этой  серии блогов .

Are you in for another critique about Java 8? Read “New Parallelism APIs in Java 8: Behind the Glitz and Glamour” by the guys over