«1. Что такое Project Amber

Project Amber — это текущая инициатива разработчиков Java и OpenJDK, направленная на внесение небольших, но существенных изменений в JDK, чтобы сделать процесс разработки более приятным. Это продолжается с 2017 года и уже внесло некоторые изменения в Java 10 и 11, другие запланированы для включения в Java 12, а еще больше появится в будущих выпусках.

Все эти обновления упакованы в виде JEP — схемы JDK Enhancement Proposal.

2. Доставленные обновления

На сегодняшний день Project Amber успешно доставил некоторые изменения в выпущенные версии JDK — JEP-286 и JEP-323.

2.1. Определение типа локальной переменной

В Java 7 оператор Diamond был представлен как способ упростить работу с дженериками. Эта функция означает, что нам больше не нужно записывать общую информацию несколько раз в одном операторе, когда мы определяем переменные:

List<String> strings = new ArrayList<String>(); // Java 6
List<String> strings = new ArrayList<>(); // Java 7

Java 10 включает завершенную работу над JEP-286, что позволяет нашему коду Java определять переменные без необходимости дублировать информацию о типе там, где она уже доступна компилятору. Это упоминается в более широком сообществе как ключевое слово var и обеспечивает аналогичную функциональность для Java, которая доступна во многих других языках.

С этой работой всякий раз, когда мы определяем локальную переменную, мы можем использовать ключевое слово var вместо полного определения типа, и компилятор автоматически выработает правильную информацию о типе для использования:

var strings = new ArrayList<String>();

In выше, определено, что переменные strings имеют тип ArrayList\u003cString\u003e(), но без необходимости дублировать информацию в той же строке.

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

Слово var — это особый случай, поскольку это не зарезервированное слово. Вместо этого это имя специального типа. Это означает, что это слово можно использовать для других частей кода, включая имена переменных. Настоятельно не рекомендуется этого делать во избежание путаницы.

Мы можем использовать вывод локального типа только тогда, когда мы предоставляем фактический тип как часть объявления. Он намеренно разработан таким образом, чтобы не работать, когда значение явно равно null, когда значение вообще не указано или когда предоставленное значение не может определить точный тип — например, определение Lambda:

var unknownType; // No value provided to infer type from
var nullType = null; // Explicit value provided but it's null
var lambdaType = () -> System.out.println("Lambda"); // Lambda without defining the interface

Однако , значение может быть нулевым, если это возвращаемое значение из какого-либо другого вызова, поскольку сам вызов предоставляет информацию о типе: ) является.

Optional<String> name = Optional.empty();
var nullName = name.orElse(null);

Переменные, определенные таким образом, могут иметь любые другие модификаторы точно так же, как и любая другая переменная, например, транзитивные, синхронизированные и окончательные.

2.2. Определение типа локальной переменной для лямбда-выражений

Приведенная выше работа позволяет нам объявлять локальные переменные без необходимости дублировать информацию о типе. Однако это не работает со списками параметров и, в частности, не работает с параметрами лямбда-функций, что может показаться удивительным.

В Java 10 мы можем определить лямбда-функции одним из двух способов — либо явно объявив типы, либо полностью опустив их:

Здесь вторая строка содержит явное объявление типа — «String», тогда как в третьей строке он полностью отсутствует, и компилятор определяет правильный тип. Чего мы не можем сделать, так это использовать здесь тип var.

names.stream()
  .filter(String name -> name.length() > 5)
  .map(name -> name.toUpperCase());

Java 11 позволяет это сделать, поэтому вместо этого мы можем написать:

Тогда это согласуется с использованием типа var в другом месте нашего кода.

names.stream()
  .filter(var name -> name.length() > 5)
  .map(var name -> name.toUpperCase());

Lambdas всегда ограничивали нас использованием полных имен типов либо для каждого параметра, либо ни для одного из них. Это не изменилось, и использование var должно быть либо для каждого параметра, либо ни для одного из них:

«

numbers.stream()
    .reduce(0, (var a, var b) -> a + b); // Valid

numbers.stream()
    .reduce(0, (var a, b) -> a + b); // Invalid

numbers.stream()
    .reduce(0, (var a, int b) -> a + b); // Invalid

«Здесь первый пример вполне корректен — потому что два лямбда-параметра используют var. Однако второй и третий недопустимы, потому что только один параметр использует var, хотя в третьем случае у нас также есть явное имя типа.

3. Грядущие обновления

В дополнение к обновлениям, которые уже доступны в выпущенных JDK, предстоящий выпуск JDK 12 включает одно обновление — JEP-325.

3.1. Выражения Switch

JEP-325 поддерживает упрощение работы операторов switch и возможность их использования в качестве выражений для еще большего упрощения кода, в котором они используются.

В настоящее время оператор switch работает так же, как в таких языках, как C или C++. Эти изменения делают его более похожим на оператор when в Kotlin или на оператор match в Scala.

С этими изменениями синтаксис определения оператора switch похож на синтаксис лямбда-выражений с использованием символа -\u003e. Это находится между совпадением регистра и кодом, который должен быть выполнен:

switch (month) {
    case FEBRUARY -> System.out.println(28);
    case APRIL -> System.out.println(30);
    case JUNE -> System.out.println(30);
    case SEPTEMBER -> System.out.println(30);
    case NOVEMBER -> System.out.println(30);
    default -> System.out.println(31);
}

Обратите внимание, что ключевое слово break не требуется, и более того, мы не можем использовать его здесь. Автоматически подразумевается, что каждое совпадение уникально, и отказ невозможен. Вместо этого мы можем продолжать использовать старый стиль, когда он нам понадобится.

Правая сторона стрелки должна быть либо выражением, либо блоком, либо оператором throws. Все остальное — ошибка. Это также решает проблему определения переменных внутри операторов switch — это может произойти только внутри блока, что означает, что они автоматически привязаны к этому блоку:

switch (month) {
    case FEBRUARY -> {
        int days = 28;
    }
    case APRIL -> {
        int days = 30;
    }
    ....
}

В операторе switch старого стиля это было бы ошибка из-за повторяющейся переменной days. Требование использовать блок позволяет избежать этого.

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

switch (month) {
    case FEBRUARY -> System.out.println(28);
    case APRIL, JUNE, SEPTEMBER, NOVEMBER -> System.out.println(30);
    default -> System.out.println(31);
}

Пока что все это возможно с текущим способом работы операторов switch и заставляет аккуратнее. Однако в этом обновлении также появилась возможность использовать оператор switch в качестве выражения. Это значительное изменение для Java, но оно согласуется с тем, сколько других языков, включая другие языки JVM, начинают работать.

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

final var days = switch (month) {
    case FEBRUARY -> 28;
    case APRIL, JUNE, SEPTEMBER, NOVEMBER -> 30;
    default -> 31;
}

Здесь мы используем выражение переключения для генерации число, а затем мы присваиваем это число непосредственно переменной.

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

4. Предстоящие изменения

На данный момент все эти изменения либо уже доступны, либо будут в следующем релизе. Есть некоторые предлагаемые изменения в рамках Project Amber, которые еще не запланированы к выпуску.

4.1. Необработанные строковые литералы

В настоящее время в Java есть только один способ определить строковый литерал — заключая содержимое в двойные кавычки. Это просто в использовании, но в более сложных случаях возникают проблемы.

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

JEP-326 представляет новый тип строкового литерала, который называется Raw String Literals. Они заключаются в обратные кавычки вместо двойных кавычек и могут содержать любые символы внутри них.

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

Например:

// File system path
"C:\\Dev\\file.txt"
`C:\Dev\file.txt`

// Regex
"\\d+\\.\\d\\d"
`\d+\.\d\d`

// Multi-Line
"Hello\nWorld"
`Hello
World`

«

«Во всех трех случаях легче увидеть, что происходит в версии с обратными кавычками, которая также гораздо менее подвержена ошибкам при вводе.

``This string allows a single "`" because it's wrapped in two backticks``

Новые необработанные строковые литералы также позволяют нам включать сами обратные кавычки без осложнений. Количество обратных кавычек, используемых для начала и конца строки, может быть сколь угодно длинным — это не обязательно должна быть только одна обратная кавычка. Строка заканчивается только тогда, когда мы достигаем равной длины обратных кавычек. Так, например:

Они позволяют нам вводить строки точно такими, какие они есть, вместо того, чтобы когда-либо нуждаться в специальных последовательностях для работы определенных символов.

4.2. Lambda Leftovers

JEP-302 вносит небольшие улучшения в работу лямбда-выражений.

Основные изменения коснулись обработки параметров. Во-первых, это изменение дает возможность использовать символ подчеркивания для неиспользуемого параметра, чтобы мы не генерировали ненужные имена. Раньше это было возможно, но только для одного параметра, поскольку подчеркивание было допустимым именем.

jdbcTemplate.queryForObject("SELECT * FROM users WHERE user_id = 1", (rs, _) -> parseUser(rs))

В Java 8 было введено изменение, поэтому использование символа подчеркивания в качестве имени является предупреждением. Затем в Java 9 это превратилось в ошибку, что не позволило нам вообще их использовать. Это предстоящее изменение позволяет использовать лямбда-параметры без каких-либо конфликтов. Это позволило бы, например, использовать следующий код:

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

String key = computeSomeKey();
map.computeIfAbsent(key, key2 -> key2.length());

Другим важным изменением в этом улучшении является возможность лямбда-параметров скрывать имена из текущего контекста. В настоящее время это не разрешено, что может привести к тому, что мы напишем далеко не идеальный код. Например:

Нет никакой реальной необходимости, кроме компилятора, почему key и key2 не могут иметь общее имя. Лямбде никогда не нужно ссылаться на переменный ключ, и принуждение нас к этому делает код более уродливым.

String key = computeSomeKey();
map.computeIfAbsent(key, key -> key.length());

Вместо этого это усовершенствование позволяет нам записать его более очевидным и простым способом:

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

m(Predicate<String> ps) { ... }
m(Function<String, String> fss) { ... }

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

Оба этих метода принимают лямбда-выражение с одним строковым параметром и возвращаемым типом, отличным от void. Для разработчика очевидно, что они разные — один возвращает строку, а другой — логическое значение, но компилятор сочтет их неоднозначными.

Этот JEP может устранить этот недостаток и разрешить явную обработку этой перегрузки.

4.3. Сопоставление с образцом

JEP-305 вносит усовершенствования в то, как мы можем работать с оператором instanceof и автоматическим приведением типов.

if (obj instanceof String) {
    String s = (String) obj;
    // use s
}

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

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

if (obj instanceof String s) {
    // use s
}

Это усовершенствование вносит аналогичную корректировку в оператор instanceof, как это было ранее в попытке с ресурсами в Java 7. это изменение, сравнение, приведение и объявление переменной вместо этого становятся одним оператором:

Это дает нам один оператор, без дублирования и без риска появления ошибок, и при этом выполняет то же, что и выше.

if (obj instanceof String s) {
    // can use s here
} else {
    // can't use s here
}

Это также будет правильно работать в разных ветках, позволяя работать следующим образом:

String s = "Hello";
if (obj instanceof String s) {
    // s refers to obj
} else {
    // s refers to the variable defined before the if statement
}

«

if (obj instanceof String s && s.length() > 5) {
    // s is a String of greater than 5 characters
}

«Улучшение также будет корректно работать в разных границах области действия. Переменная, объявленная в предложении instanceof, будет корректно скрывать переменные, определенные за ее пределами, как и ожидалось. Однако это произойдет только в соответствующем блоке:

Это также работает в том же предложении if, точно так же, как мы полагаемся на проверку null:

В настоящее время это планируется только для операторов if, но в будущем он, вероятно, будет расширен для работы с выражениями switch.

4.4. Краткие тела методов

ToIntFunction<String> lenFn = (String s) -> { return s.length(); };
ToIntFunction<String> lenFn = (String s) -> s.length();
ToIntFunction<String> lenFn = String::length;

Проект JEP 8209434 представляет собой предложение по поддержке упрощенных определений методов, аналогично тому, как работают определения лямбда.

Прямо сейчас мы можем определить Lambda тремя различными способами: с телом, как отдельное выражение или как ссылка на метод:

Однако, когда дело доходит до написания реальных тел методов класса, мы в настоящее время должны выписать их полностью.

String getName() -> name;

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

int length(String s) = String::length

Например, для метода-получателя не требуется полное тело метода, но его можно заменить одним выражением:

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

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

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

5. Усовершенствованные перечисления

enum Primitive<X> {
    INT<Integer>(Integer.class, 0) {
       int mod(int x, int y) { return x % y; }
       int add(int x, int y) { return x + y; }
    },
    FLOAT<Float>(Float.class, 0f)  {
       long add(long x, long y) { return x + y; }
    }, ... ;

    final Class<X> boxClass;
    final X defaultValue;

    Primitive(Class<X> boxClass, X defaultValue) {
       this.boxClass = boxClass;
       this.defaultValue = defaultValue;
    }
}

Ранее планировалось, что JEP-301 станет частью проекта Amber. Это внесло бы некоторые улучшения в перечисления, явно позволив отдельным элементам перечисления иметь различную информацию об универсальном типе.

Например, это позволило бы:

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

Таким образом, это улучшение в настоящее время приостановлено до тех пор, пока эти детали не будут проработаны.