В течение второй недели ноября состоялась Devoxx Belgium , крупнейшая в Европе конференция по Java, и, как и каждый год, сообщество «кто есть кто» появилось. Одним из них был Брайан Гетц, архитектор языка Java в Oracle, и он выступил с тем, что я бы назвал самым волнующим докладом конференции: «Язык Java и будущее платформы: подглядывание» . В нем он представил идеи, которые команда JDK в настоящее время обсуждает. И, парень, этот трубопровод полон отличных вещей! Ява не будет выглядеть так же, как только все это в дикой природе.
Содержание
Когда это случится? Никто не знает. И это не никто, как ни у кого за пределами Oracle , это никто, поскольку никто не знает , существуют ли счастливые окончания для произвольного n
. Брайан сделал все возможное, чтобы подчеркнуть, насколько очень, очень умозрительно все следующее и сколько вещей может эволюционировать или просто упасть. Он зашел так далеко, что позволил всем в аудитории подписать признание (только мысленно, но все же) и явно запретил любые твиты сенсаций.
Ну … во-первых, это не твит, а во-вторых, я не был в этой аудитории. Итак, поехали! (Серьезно, хотя, примите это как то, что это: проблеск в одно из многих, многих возможных вариантов будущего.)
Ускоренный курс
Прежде чем мы рассмотрим идеи по одной, давайте сразу же рассмотрим, как может выглядеть код, использующий все предусмотренные функции. Следующий класс представляет собой простой связанный список, который использует два типа узлов:
-
InnerNode
-
EndNode
Одной из особенно интересных операций является reduce
, которая принимает начальное значение и BinaryOperator
и применяет его к начальному и всем значениям узлов. Вот как это может выглядеть в один прекрасный день:
public class LinkedList<any T> {
private Optional<Node<T>> head;
// [constructors]
// [list mutation by replacing nodes with new ones]
public T reduce(T seed, BinaryOperator<T> operator) {
var currentValue = seed;
var currentNode = head;
while (currentNode.isPresent()) {
currentValue = operator.apply(currentValue, currentNode.get().getValue());
currentNode = switch (currentNode.get()) {
case InnerNode(_, var nextNode) -> Optional.of(nextNode);
case EndNode(_) -> Optional.empty();
default: throw new IllegalArgumentException();
}
}
return currentValue;
}
private interface Node<any T> {
T getValue();
}
private static class InnerNode<any T>(T value, Node<T> next) implements Node<T> { }
private static class EndNode<any T>(T value) implements Node<T> { }
}
Вот это да! Вряд ли Java больше, правда ?! Помимо пропущенных конструкторов есть только код, который на самом деле что- то делает — я имею в виду, где весь шаблон? А что если я скажу вам, что на вершине этого спектакля будет намного лучше, чем сегодня? Звучит как бесплатный обед, черт возьми, как целый бесплатный шведский стол!
Вот что нового:
- Аргумент общего типа помечается
any
- Где информация о типах для
currentValue
currentNode
reduce
- Этот
switch
- Классы
InnerNode
EndNode
Давайте посмотрим на все идеи, которые вошли в этот пример.
Объекты данных
Когда вы в последний раз создавали доменный объект, который по сути представлял собой тупой держатель данных, возможно, с одним или двумя нетривиальными методами, для которого все еще требовалось сто строк для конструкторов, статических методов фабрики, методов доступа, equals
, hashCode
и toString
(Прямо сейчас, вы говорите? Не волнуйтесь, я не осуждаю.) И хотя IDE с радостью генерируют все это, что делает его ввод ненужным даже сегодня, это все же код, который нужно понимать (делает ли конструктор какой-либо валидация?) и поддерживается (лучше не забывать добавлять это новое поле в equals
В агрессивном шаге по сокращению шаблонов, компилятор может генерировать все эти вещи на лету без нас, чтобы согнуть палец!
Вот как может выглядеть пользователь:
public class User(String firstName, String lastName, DateTime birthday) { }
Мы можем получить все остальное, что я упомянул выше, бесплатно, и нам нужно только реализовать то, что является нестандартным (возможно, у пользователей есть идентификатор, который сам определяет равенство, поэтому мы бы хотели, чтобы была реализована equals
Избавление от всего этого кода было бы большим стимулом для удобства обслуживания!
Глядя на пример связанного списка, мы видим, что InnerNode
EndNode
Типы значений
Когда Java создавалась, арифметическая операция и загрузка из основной памяти занимали примерно одинаковое количество циклов (здесь речь идет о величинах). За последние 20 и более лет ситуация значительно изменилась, и доступ к памяти стал на три порядка медленнее.
То, что все абстрактные типы Java являются объектами, связанными друг с другом посредством ссылок, требует поиска указателей и еще более усугубляет проблему. Преимущества состоят в том, что такие типы имеют идентичность, допускают изменчивость, наследование и пару других вещей… которые нам на самом деле не всегда нужны. Это очень неудовлетворительно и нужно что-то делать!
Приходит проект Valhalla , в рамках которого, как мы говорим, разрабатываются типы ценностей. Их можно обобщить как самоопределенные примитивы. Вот простой пример:
value class ComplexNumber {
double real;
double imaginary;
// constructors, getters, setters, equals, hashCode, toString
}
Выглядит как обычный класс — единственное отличие — это value
Как и примитивы, типы значений не несут ни накладных расходов памяти, ни косвенного обращения. ComplexNumber
double
real
imaginary
Как и примитивы, такие числа не имеют идентичности — хотя может быть два разных объекта Double
Это исключает некоторые вещи, которые мы хотели бы сделать с объектами: установка их на нуль, наследование, изменение и блокировка. В свою очередь, это потребует только памяти, необходимой для этих двух двойных чисел, и массив комплексных чисел по существу будет массивом вещественных / мнимых пар.
Как и классы, типы значений могут иметь методы и поля, инкапсулировать внутренние компоненты, использовать универсальные шаблоны и реализовывать интерфейсы (но не расширять другие классы). Таким образом, слоган: «Коды, как класс, работает как int». Это позволит нам больше не взвешивать абстракцию, которую мы бы предпочли, против производительности (мы представляем), в которой мы нуждаемся.
Если говорить о производительности, то преимущества значительны и могут ускорить практически любой код. Например, в HashMap
Но это не низкоуровневая функция, которую захотят использовать только разработчики хардкорных библиотек! Это позволяет всем нам выбрать правильную абстракцию и сообщить компилятору, а также нашим коллегам, что некоторые из наших объектов на самом деле являются не объектами, а значениями.
Кстати, мое личное предположение состоит в том, что компилятор будет так же полезен, как и с объектами данных и чипом в конструкторах, геттерах, сеттерах и т. Д .:
value class ComplexNumber(double real, double imaginary) { }
В случае, если это не было совершенно очевидно: это глубокое изменение и взаимодействует практически со всем:
- язык (дженерики, шаблоны, необработанные типы, …)
- основные библиотеки (коллекции, потоки)
- JVM (тип подписи, байт-коды, …)
Итак … где именно в примере со связанным списком входят типы значений? Правда, они не играют большой роли. Если бы я был достаточно умен, чтобы написать постоянную структуру данных , узлы могли бы быть типами значений (помните, они должны быть неизменными), что могло бы быть довольно интересным.
Но есть один возможный тип значения: Optional
В Java 8 он уже помечен как основанный на значениях класс , который может однажды стать типом значения или его оболочкой. Это делает его плоским и устраняет косвенное обращение к памяти и возможные пропуски кеша, которые он налагает в настоящее время.
Специализированные Дженерики
Когда каждый и его собака создают примитивно-подобные типы значений, становится необходимым посмотреть, как они взаимодействуют с параметрическим полиморфизмом. Как вы знаете, дженерики не работают для примитивов — не может быть ArrayList<int>
Это уже болезненно с восемью примитивами (см. Примитивные специализации Stream или библиотек вроде Trove ), но становится невыносимым, когда разработчики могут определить больше. Если бы типы значений должны были быть помещены в коробку для взаимодействия с обобщениями (как примитивы сегодня), их использование было бы довольно ограниченным, и они не были бы начальными.
Поэтому мы хотим иметь возможность использовать дженерики с типами значений — и примитивы могут прийти вместе. В конце мы не только хотим создать экземпляр ArrayList<int>
ArrayList<ComplexNumber>
int[]
ComplexNumber[]
Это называется специализация и открывает новую банку червей. (Чтобы хорошенько взглянуть на этих червей, посмотрите доклад «Приключения в параметрическом полиморфизме» , который Брайан дал на JVMLS 2016. В этой статье также содержится список выступлений, которые вы можете посмотреть, если хотите углубиться.)
Код, который хочет генерировать не только ссылочные типы, но и типы значений, должен пометить соответствующие параметры типа any
Вы можете видеть, что LinkedList
Node
Это означает, что в LinkedList<int>
int
Object
Integer
LinkedList<Integer>
Более вывод типа
Java сделала вывод типов начиная с Java 5 (для свидетелей типов в универсальных методах), и механизм был расширен в Java 7 (оператор diamond), 8 (типы параметров лямбда) и 9 (diamond в анонимных классах). В Java X это вполне может охватывать объявления переменных. Вот пример Брайана:
// now
URL url = new URL("...")
URLConnectoin conn = url.openConnection();
Reader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
// maybe in the future
var url = new URL("...")
var conn = url.openConnection();
var reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
Здесь типы url
conn
reader
Как следствие, компилятор может вывести их, что делает ненужным их указание. В общем, вывод типа может уменьшить шаблон, но также и скрыть важную информацию. Если вы считаете имена переменных более важными, чем их типы, вам понравится это, поскольку они идеально выравнивают имена, выбрасывая избыточную информацию.
Обратите внимание, что вывод типа не является динамической типизацией — это все еще сильная типизация, просто с меньшим набором текста (каламбур Брайана — предположительно предназначенный). Информация о типе будет по-прежнему попадать в байт-код, и IDE также смогут их показывать — просто нам больше не нужно ее выписывать.
Автоматическое определение типов процессов подразумевает, что изменения кода изменят результат этих вычислений. Хотя обычно локальная переменная может менять свой тип (например, на супертип), этого нельзя сказать о полях, параметрах метода или возвращаемых значениях и т. Д. Напротив, любое изменение здесь может привести к двоичной несовместимости, что привести к коду, скомпилированному со старой версией, не способной связать во время выполнения. Не хорошо и, следовательно, запрещено.
Таким образом, вывод только типов локальных переменных — это больше защита экосистемы от нестабильного кода, чем защита разработчиков от нечитаемого кода.
Сопоставление с образцом
Текущий оператор switch
довольно слабый. Вы можете использовать его для примитивов, перечислений и строк, но это все. Если вы хотите сделать что-то более сложное, вы должны либо прибегнуть к цепочкам if-else-if, либо, если вы не можете выбросить из головы книгу «Банды четырех», шаблон посетителей .
Но подумайте об этом, на самом деле не существует внутренней причины этих ограничений. На более высоком уровне можно описать, как переключатель использует переменную для оценки некоторых условий и выбора подходящей ветви, оценки того, что он там находит — почему тип переменной должен быть настолько ограничен, а условия проверяют только равенство? Если подумать, почему switch
делает что-то, а не становится чем-то. Следуя по этому пути, мы получаем сопоставление с образцом, которое не имеет ни одного из этих ограничений.
Прежде всего, все виды переменных могут быть разрешены. Во-вторых, условия могут быть гораздо шире. Они могут, например, проверять типы или даже деконструировать целые объекты данных. И наконец, что не менее важно, весь switch
Вот примеры Брайана:
// matching types
String formatted;
switch (constant) {
case Integer i: formatted = String.format("int %d", i); break;
case Byte b: //...
case Long l: // ...
// ...
default: formatted = "unknown"
}
// used as an expression
String formatted = switch (constant) {
case Integer i -> String.format("int %d", i);
case Byte b: //...
case Long l: // ...
// ...
default: formatted = "unknown"
}
// deconstructing objects
int eval(ExprNode node) {
return switch (node) {
case ConstantNode(var i) -> i;
case NegNode(var node) -> -eval(node);
case PlusNode(var left, var right) -> eval(left) + eval(right);
case MulNode(var left, var right) -> eval(left) * eval(right);
// ...
}
}
Для связанного списка я также использовал его как выражение и для деконструкции узлов:
currentNode = switch (currentNode.get()) {
case InnerNode(_, var nextNode) -> Optional.of(nextNode);
case EndNode(_) -> Optional.empty();
default: throw new IllegalArgumentException();
}
Гораздо приятнее, чем то, на что это было бы похоже сейчас:
if (currentNode.get() instanceof InnerNode) {
currentNode = Optional.of(((InnerNode) currentNode.get()).getNext());
} else if (currentNode.get() instanceof EndNode) {
currentNode = Optional.empty();
} else {
throw new IllegalArgumentException();
}
(Да, я знаю, этот конкретный пример можно решить с помощью полиморфизма.)
Резюме
Опять вау! Объекты данных, типы значений, общая специализация, больше выводов типов и сопоставление с образцом — это набор огромных возможностей, над которыми работает команда JDK. Я не могу дождаться их выхода! (Кстати, в то время как я представил все функции здесь, Брайан предоставляет гораздо более интересную информацию — вам определенно следует проверить весь доклад .)
Как вы думаете? Вы хотите написать код на Java?
Понравился этот пост? Хотите узнать больше о настоящем и будущем Java? Подпишитесь на нашу ленту новостей или рассылку .