Java 10 представляет новую блестящую функцию, называемую выводом типа локальной переменной. Звучит здорово, не так ли? Что это?В следующих двух сценариях мы, Java-разработчики, находим, что Java сложна в использовании.
Контекст: клише и читабельность кода
Может быть, изо дня в день вы надеетесь, что вам не придется делать что-то снова и снова. Например, в приведенном ниже коде (с использованием фабрики коллекций Java 9) тип слева может показаться избыточным и пресным.
import static java.util.Map.entry;
List<String> cities = List.of("Brussels", "Cardiff", "Cambridge")
Map<String, Integer> citiesPopulation
= Map.ofEntries(entry("Brussels", 1_139_000),
entry("Cardiff", 341_000));
Это очень простой пример, но он также подтверждает традиционную философию Java: вам необходимо определить статические типы для всех содержащихся простых выражений. Рассмотрим более сложные примеры. Например, приведенный ниже код строит гистограмму из строк в слова. Он объединяет поток в карту с помощью сборщика groupingBy. Сборщик groupingBy также может подсчитывать количество ассоциаций с помощью функции сортировки, которая строит ключи карты для первого аргумента и ключей (counting()) второго сборщика. Вот пример:
String sentence = "A simple Java example that explores what Java
10 has to offer";
Collector<String, ?, Map<String, Long>> byOccurrence
= groupingBy(Function.identity(), counting());
Map<String, Long> wordFrequency
= Arrays.stream(sentence.split(" "))
.collect(byOccurrence);
Имеет смысл извлекать сложные выражения в переменную или метод, чтобы улучшить читаемость кода и возможность его повторного использования. В этом примере логика построения гистограммы использует коллекторы. К сожалению, тип результата от groupingBy практически нечитаем! Вы ничего не можете с этим поделать, все, что вы можете делать, это наблюдать.
Наиболее важным моментом является то, что по мере добавления новых библиотек в Java они разрабатывают все больше и больше дженериков, что оказывает дополнительное давление на разработчиков, чтобы они добавляли больше стандартного кода. Приведенный выше пример не означает, что писать типы плохо. Очевидно, что выполнение операций, определяющих типы сигнатур переменных и методов, в качестве протокола, который необходимо соблюдать, будет полезно поддерживать и понимать. Однако объявление типов для промежуточных выражений может показаться бесполезным и избыточным.
История вывода типов
В истории Java мы много раз видели, как разработчики языков добавляли «вывод типов», чтобы помочь нам писать более чистый код. Вывод типа - это идея: компилятор может вывести за вас статические типы, вам не нужно указывать их самостоятельно.
Универсальные методы были представлены начиная с Java 5, и параметры универсальных методов можно вывести из контекста. Например
этот код:
List<String> cs = Collections.<String>emptyList();
Можно упростить до:
List<String> cs = Collections.emptyList();
Затем в Java 7 параметры типа могут быть опущены в выражениях, если эти параметры могут быть определены контекстом. Например:
Map<String, List<String>> myMap = new HashMap<String,List<String>>();
Это можно упростить, используя оператор угловой скобки , чтобы:
Map<User, List<String>> userChannels = new HashMap<>();
Как правило, компилятор может вывести тип на основе окружающего контекста. В этом примере слева можно сделать вывод, что HashMap содержит список строк.
Начиная с Java 8, лямбда-выражение, подобное следующему
Predicate<String> nameValidation = (String x) -> x.length() > 0;
Тип можно опустить и записать как
Predicate<String> nameValidation = x -> x.length() > 0;
вывод типа локальной переменной
Поскольку типов становится все больше и больше, универсальный параметр может быть другим универсальным типом, и в этом случае вывод типа может улучшить читаемость. Языки Scala и C# позволяют объявлять тип локальных переменных как var, а компилятор заполняет соответствующий тип в соответствии с оператором инициализации. Например, предыдущее объявление userChannels можно было бы записать так:
var userChannels = new HashMap<User, List<String>>();
Это также можно сделать из возвращаемого значения метода (здесь возвращается список):
var channels = lookupUserChannels("Tom");
channels.forEach(System.out::println);
Эта идея называется выводом типа локальной переменной, и она была представлена в Java 10!
Например следующий код:
Path path = Paths.get("src/web.log");
try (Stream<String> lines = Files.lines(path)){
long warningCount
= lines
.filter(line -> line.contains("WARNING"))
.count();
System.out.println("Found " + warningCount + " warnings in the
log file");
} catch (IOException e) {
e.printStackTrace();
}
В Java 10 его можно реорганизовать следующим образом:
var path = Paths.get("src/web.log");
try (var lines = Files.lines(path)){
var warningCount
= lines
.filter(line -> line.contains("WARNING"))
.count();
System.out.println("Found " + warningCount + " warnings in the
log file");
} catch (IOException e) {
e.printStackTrace();
}
Каждое выражение в приведенном выше коде по-прежнему имеет статический тип (то есть тип значения):
-
Тип локальной переменной path — Path
-
Тип строк переменных — Stream
. -
Переменная warningCount имеет тип long
То есть произойдет сбой, если вы присвоите этим переменным разные значения. Например, двойное присваивание, подобное следующему, вызовет ошибку компиляции:
var warningCount = 5;
warningCount = "6";
| Error:
| incompatible types: java.lang.String cannot be converted to int
| warningCount = "6"
Однако есть некоторые незначительные проблемы с выводом типа; если оба класса Car и Bike являются подклассами Vehicle, то объявить
var v = new Car();
Здесь объявлен тип v Car или Vehicle? Этот случай хорошо объяснен, потому что тип инициализатора (в данном случае Car) очень специфичен. Вы не можете использовать var без инициализатора. Назначить позже, как это
v = new Bike();
пойдет не так. Другими словами, var не идеален для полиморфного кода.
Так где же следует использовать вывод типа локальной переменной?
Когда происходит сбой локального вывода типов? Вы не можете использовать его в сигнатурах полей и методов. Его можно использовать только для локальных переменных, например, следующий код неверен:
public long process(var list) { }
Вы не можете использовать var для объявления локальной переменной без явной инициализации переменной. То есть вы не можете использовать синтаксис var для объявления переменной без присваивания. Следующий код
var x;
Это вызывает ошибку компиляции:
| Error:
| cannot infer type for local variable x
| (cannot use 'var' on variable without initializer)
| var x;
| ^----^
Также переменная, объявленная var, не может быть инициализирована значением null. На самом деле, до поздней инициализации неясно, какого типа он на самом деле.
| Error:
| cannot infer type for local variable x
| (variable initializer is 'null')
| var x = null;
| ^-----------^
Вы не можете использовать var в лямбда-выражении, потому что для этого требуется явный целевой тип. Следующее задание неверно:
var x = () -> {}
| Error:
| cannot infer type for local variable x
| (lambda expression needs an explicit target-type)
| var x = () -> {};
| ^---------------^
Однако следующее присваивание допустимо, поскольку правая часть уравнения имеет явную инициализацию.
var list = new ArrayList<>();
Каков статический тип этого списка? Тип переменной выводится как ArrayList
для непредставимых типов (Non-Denotable Types) сделать вывод
В Java существует множество непредставимых типов — типов, которые существуют в программах, но имена которых нельзя записать точно. Например, анонимный класс — это типичный тип, который не может быть представлен, в анонимный класс можно добавлять поля и методы, но нельзя написать имя анонимного класса в коде Java. Оператор угловой скобки нельзя использовать для анонимных классов, а var немного менее ограничен, он может поддерживать некоторые непредставимые типы, в частности, анонимные классы и перекрестные типы.
Ключевое слово var также позволяет нам более эффективно использовать анонимные классы, которые могут ссылаться на типы, которые невозможно описать. Как правило, можно добавлять поля в анонимные классы, но вы не можете ссылаться на эти поля в другом месте, поскольку для этого требуется, чтобы переменной было присвоено имя типа. Например, следующий код не будет компилироваться, поскольку тип productInfo — Object, и вы не можете получить доступ к полям имени и итогов через тип Object.
Object productInfo = new Object() {
String name = "Apple";
int total = 30;
};
System.out.println("name = " + productInfo.name + ", total = " +
productInfo.total);
Использование var нарушает это ограничение. При присвоении объекта анонимного класса локальной переменной, объявленной с помощью var, он выводит тип анонимного класса вместо того, чтобы рассматривать его как тип своего суперкласса. Поэтому на поля, объявленные в анонимных классах, можно ссылаться.
var productInfo = new Object() {
String name = "Apple";
int total = 30;
};
System.out.println("name = " + productInfo.name + ", total = " +
productInfo.total);
На первый взгляд, это просто самое интересное в языке, и оно не будет очень полезным. Но в некоторых случаях это работает. Например, когда вы хотите вернуть какое-то значение в качестве промежуточного результата. Как правило, для этого вы создаете и поддерживаете новый класс, но используете его только в одном методе. По этой причине реализация Collectors.averagingDouble() использует небольшой массив типа double.
С var у нас есть лучшее решение — использовать анонимные классы для хранения промежуточных значений. Теперь рассмотрим пример, где есть продукты, каждый из которых имеет имя, инвентарь и денежную стоимость или стоимость. Мы хотим рассчитать общую цену (количество * стоимость) каждого товара. Это информация, необходимая для сопоставления каждого Продукта с его общей ценой, но чтобы сделать информацию более значимой, нам также необходимо включить название продукта. В следующем примере показано, как использовать var в Java 10 для достижения этой функциональности:
var products = List.of(
new Product(10, 3, "Apple"),
new Product(5, 2, "Banana"),
new Product(17, 5, "Pear"));
var productInfos = products
.stream()
.map(product -> new Object() {
String name = product.getName();
int total = product.getStock() * product.getValue();
})
.collect(toList());
productInfos.forEach(prod ->
System.out.println("name = " + prod.name + ", total = " +
prod.total));
This outputs:
name = Apple, total = 30
name = Banana, total = 10
name = Pear, total = 85