Статьи

Новый вывод типа локальной переменной Java A

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

В настоящее время существует JEP 286 для вывода типа локальной переменной со статусом «Кандидат». И запрос обратной связи от Брайана Гетца, в котором я хотел бы пригласить вас принять участие: http://mail.openjdk.java.net/pipermail/platform-jep-discuss/2016-March/000037.html

Пожалуйста, сделайте это, опрос остается открытым только с 9 по 16 марта!

Это не та функция, которая будет реализована. Это может быть реализовано. Следовательно, конкретной версии Java пока нет, поэтому я называю версию Java «A» (для Awesome).

Что такое вывод типа локальной переменной и почему это хорошо?

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

В Java, как и в некоторых других языках, типы всегда объявляются явно и подробно. Например, вы пишете такие вещи, как:

1
2
3
4
5
// Java 5 and 6
List<String> list = new ArrayList<String>();
 
// Java 7
List<String> list = new ArrayList<>();

Обратите внимание, как в Java 7 добавлен некоторый синтаксический сахар с помощью полезного оператора diamond <> . Это помогает устранить ненужную избыточность способом Java, т.е. применяя «типизацию цели», что означает, что тип определяется «целью». Возможные цели:

  • Объявления локальных переменных
  • Аргументы метода (как снаружи, так и внутри метода)
  • Члены класса

Поскольку во многих случаях целевой тип ДОЛЖЕН быть объявлен явно (аргументы метода, члены класса), подход Java имеет большой смысл. В случае локальных переменных, однако, целевой тип действительно не нужно объявлять. Поскольку определение типа связано с очень локальной областью действия, из которой оно не может выйти, оно вполне может быть выведено компилятором, если исходный код никогда не будет явным об этом, из «исходного типа» . Это означает, что мы сможем делать такие вещи, как:

1
2
3
4
5
6
7
// Java A as suggested in the JEP
 
// infers ArrayList<String>
var list = new ArrayList<String>();
 
// infers Stream<String>
val stream = list.stream();

В приведенном выше примере var обозначает изменяемую (не окончательную) локальную переменную, тогда как val обозначает неизменяемую (окончательную) локальную переменную. Обратите внимание, что тип списка действительно никогда не был нужен, как когда мы пишем следующее, где тип уже выведен сегодня:

1
stream = new ArrayList<String>().stream();

Это будет работать ничем не отличается от лямбда-выражений, где у нас уже есть такой тип вывода в Java 8:

1
2
3
4
5
6
List<String> list = new ArrayList<>();
 
// infers String
list.forEach(s -> {
    System.out.println(s);
};

Думайте о лямбда-аргументах как о локальных переменных. Альтернативный синтаксис для такого лямбда-выражения мог бы быть:

1
2
3
4
5
6
List<String> list = new ArrayList<>();
 
// infers String
list.forEach((val s) -> {
    System.out.println(s);
};

У других языков есть это, но хорошо ли это?

Среди этих других языков: C # и Scala и JavaScript, если хотите;). YAGNI, вероятно, является обычной реакцией на эту особенность. Для большинства людей просто удобство — не набирать все типы постоянно. Некоторые люди могут предпочесть видеть тип, записанный явно, при чтении кода. Особенно, когда у вас есть сложный конвейер обработки Java 8 Stream, может быть сложно отслеживать все типы, которые выводятся по пути. Пример этого можно увидеть в нашей статье о поддержке оконной функции jOOλ :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
BigDecimal currentBalance = new BigDecimal("19985.81");
  
Seq.of(
    tuple(9997, "2014-03-18", new BigDecimal("99.17")),
    tuple(9981, "2014-03-16", new BigDecimal("71.44")),
    tuple(9979, "2014-03-16", new BigDecimal("-94.60")),
    tuple(9977, "2014-03-16", new BigDecimal("-6.96")),
    tuple(9971, "2014-03-15", new BigDecimal("-65.95")))
.window(Comparator
    .comparing((Tuple3<Integer, String, BigDecimal> t)
        -> t.v1, reverseOrder())
    .thenComparing(t -> t.v2), Long.MIN_VALUE, -1)
.map(w -> w.value().concat(
     currentBalance.subtract(w.sum(t -> t.v3)
                              .orElse(BigDecimal.ZERO))
));

Выше реализован расчет промежуточного итога, который дает:

1
2
3
4
5
6
7
8
9
+------+------------+--------+----------+
|   v0 | v1         |     v2 |       v3 |
+------+------------+--------+----------+
| 9997 | 2014-03-18 99.17 | 19985.81 |
| 9981 | 2014-03-16 71.44 | 19886.64 |
| 9979 | 2014-03-16 | -94.60 | 19815.20 |
| 9977 | 2014-03-16 |  -6.96 | 19909.80 |
| 9971 | 2014-03-15 | -65.95 | 19916.76 |
+------+------------+--------+----------+

В то время Tuple3 тип Tuple3 должен быть объявлен из-за ограниченных возможностей вывода типов в Java 8 ( см. Также эту статью об обобщенном выводе целевого типа ), вы можете отслеживать все другие типы? Вы можете легко предсказать результат? Некоторые люди предпочитают короткий стиль, другие утверждают:

С другой стороны, хотите ли вы вручную записать тип, такой как Tuple3<Integer, String, BigDecimal> ? Или, работая с jOOQ , какую из следующих версий одного и того же кода вы предпочитаете?

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
// Explicit typing
// ----------------------------------------
for (Record3<String, Integer, Date> record : ctx
    .select(BOOK.TITLE, BOOK.ID, BOOK.MODIFIED_AT)
    .from(BOOK)
    .where(TITLE.like("A%"))
) {
    // Do things with record
    String title = record.value1();
}
 
// "Don't care" typing
// ----------------------------------------
for (Record record : ctx
    .select(BOOK.TITLE, BOOK.ID, BOOK.MODIFIED_AT)
    .from(BOOK)
    .where(TITLE.like("A%"))
) {
    // Do things with record
    String title = record.getValue(0, String.class);
}
 
// Implicit typing
// ----------------------------------------
for (val record : ctx
    .select(BOOK.TITLE, BOOK.ID, BOOK.MODIFIED_AT)
    .from(BOOK)
    .where(TITLE.like("A%"))
) {
    // Do things with record
    String title = record.value1();
}

Я уверен, что немногие из вас действительно хотели бы явно записать весь универсальный тип, но если ваш компилятор все еще может запомнить это, это было бы здорово, не так ли? И это дополнительная функция. Вы всегда можете вернуться к явным объявлениям типов.

Крайние случаи с дисперсией сайта использования

Есть некоторые вещи, которые невозможны без такого вывода типа, и они связаны с дисперсией сайта использования и спецификой обобщений, реализованных в Java. С помощью дисперсии сайта использования и подстановочных знаков можно создавать «опасные» типы, которые ни к чему не могут быть отнесены, поскольку они неразрешимы. Для получения дополнительной информации, пожалуйста, прочитайте статью Росса Тэйта о «Приручении подстановочных знаков» в системе типов Java .

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

  • Не заботился об этой боли, которую они причиняют своим пользователям
  • Не нашел лучшего решения, так как у Java нет различий между сайтами объявлений
  • Не обращали внимания на эту проблему

Пример:

1
2
3
4
interface Node {
    void add(List<? extends Node> children);
    List<? extends Node> children();
}

Представьте библиотеку древовидной структуры данных, где узлы дерева возвращают списки своих дочерних элементов. Технически правильный тип потомков будет List<? extends Node> List<? extends Node> потому что потомки являются подтипами Node, и вполне нормально использовать список подтипов Node.

Принятие этого типа в методе add() прекрасно с точки зрения разработки API. Это позволяет людям добавлять List<LeafNode> , например. Однако возвращать его у children() ужасно, потому что теперь доступны только следующие варианты:

1
2
3
4
5
6
7
8
// Raw type. meh
List children = parent.children();
 
// Wild card. meh
List<?> children = parent.children();
 
// Full type declaration. Yuk
List<? extends Node> children = parent.children();

С JEP 286 мы могли бы обойти все это и иметь этот хороший четвертый вариант:

1
2
3
// Awesome. The compiler knows it's
// List<? extends Node>
val children = parent.children();

Вывод

Вывод типа локальной переменной является горячей темой. Это совершенно необязательно, нам это не нужно . Но это делает многое намного проще, особенно при работе с тоннами дженериков. Мы видели, что вывод типов является убийственной функцией при работе с лямбда-выражениями и сложными преобразованиями Java 8 Stream. Конечно, будет сложнее отслеживать все типы в длинном операторе, но в то же время, если эти типы были прописаны, это сделало бы оператор очень нечитаемым (и часто также очень трудным для написания).

Вывод типа помогает сделать разработчиков более продуктивными, не отказываясь от безопасности типов. На самом деле это поощряет безопасность типов, потому что разработчики API теперь неохотно предоставляют своим пользователям сложные универсальные типы, поскольку пользователи могут использовать эти типы более легко ( см. Снова пример jOOQ ).

Фактически, эта функция уже присутствует в Java в различных ситуациях, но не при назначении значения локальной переменной, присвоении ей имени.

Каково бы ни было ваше мнение: не забудьте поделиться им с сообществом и ответить на этот опрос: http://mail.openjdk.java.net/pipermail/platform-jep-discuss/2016-March/000037.html

С нетерпением жду Java A, где A означает Awesome.

Ссылка: Новый вывод типа локальной переменной Java A от нашего партнера по JCG Лукаса Эдера из блога JAVA, SQL и JOOQ .