Урок 2 из 11 старт около 20 мин
var: выведение типов без потери ясности
Где локальное выведение типов делает код чище, где вредит читаемости и какие простые правила помогают не спорить об этом в команде.
Что такое var
С Java 10 тип локальной переменной можно не писать: компилятор выведет его из выражения справа от знака равенства. Слово var при этом не делает Java динамически типизированной. Тип определяется один раз при компиляции и не меняется: переменной, объявленной как var n = 1, нельзя потом присвоить 2.5.
var count = 10; // int
var title = "Открытые уроки"; // String
var names = new ArrayList<String>(); // ArrayList<String>
var now = java.time.LocalDate.now(); // LocalDate
Формально var — не ключевое слово, а зарезервированное имя типа, поэтому старый код с переменной по имени var продолжает компилироваться. Использовать это имя для своих переменных всё же не стоит.
Где var разрешён, а где нет
Выведение работает только там, где у компилятора есть выражение-инициализатор:
- локальные переменные с инициализатором:
var x = ...; - переменные цикла:
for (var item : items)иfor (var i = 0; i < 10; i++); - ресурсы в
try (var in = Files.newInputStream(path)); - параметры лямбда-выражений, если нужны аннотации:
(var a, var b) -> a + b.
Нельзя объявить через var поле класса, параметр метода, возвращаемый тип, переменную без инициализатора и переменную, инициализированную литералом null. Компилятор в каждом из этих случаев выдаст понятную ошибку.
var later; // ошибка: нет инициализатора
var nothing = null; // ошибка: тип null вывести нельзя
var lambda = x -> x + 1; // ошибка: у лямбды нет собственного типа
Три ловушки выведения
1. Ромб без аргументов
Если справа стоит конструктор с пустым ромбом, компилятору не от чего оттолкнуться, и он выберет самый общий тип:
var list = new ArrayList<>(); // ArrayList<Object>, а не ArrayList<String>
list.add("текст");
list.add(42); // компилируется, хотя вы этого не хотели
Правило простое: с var ромб заполняют явно — new ArrayList<String>().
2. Числовые литералы
Литерал 1 имеет тип int, 1L — long, 1.0 — double. Если переменная задумана как long, а инициализирована var id = 1, в неё не поместится большое значение. Пишите суффикс или тип явно.
3. Конкретный тип вместо интерфейса
При явном объявлении принято писать List<String> names = new ArrayList<>(), чтобы код зависел от интерфейса. С var переменная получит тип ArrayList<String>. Для локальной переменной это почти всегда безобидно, но помните, что вы связали себя с реализацией.
Когда var делает код лучше
Выведение уместно, когда тип виден из правой части: конструктор, литерал, фабричный метод, в имени которого есть тип. Тогда повторять его слева — шум.
// Было
Map<String, List<LocalDate>> visitsByUser = new HashMap<String, List<LocalDate>>();
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
// Стало
var visitsByUser = new HashMap<String, List<LocalDate>>();
var reader = new BufferedReader(new InputStreamReader(System.in));
Выведение мешает, когда тип не виден и читателю приходится идти в определение метода:
var result = repository.find(id); // что вернулось: сущность, Optional, список?
var data = client.call(request); // неизвестно, что с этим делать дальше
Здесь либо оставьте явный тип, либо назовите переменную так, чтобы имя отвечало на вопрос: var foundUser = repository.find(id) читается уже лучше, но полностью не спасает.
Правило для команды
Договоритесь о двух вещах: var разрешён, когда тип очевиден из правой части, и запрещён для результатов методов с непрозрачным именем. Этого достаточно, чтобы спор о стиле не возвращался в каждом обзоре кода.
var и цепочки вызовов
Особенно полезен var для промежуточных результатов в длинных цепочках, где явный тип занял бы полстроки:
var wordsByLength = java.util.Arrays.stream("один два три четыре".split(" "))
.collect(java.util.stream.Collectors.groupingBy(String::length));
// wordsByLength имеет тип Map<Integer, List<String>>
System.out.println(wordsByLength); // {3=[два, три], 4=[один], 6=[четыре]}
Если сомневаетесь, какой тип вывел компилятор, введите выражение в JShell: оболочка покажет тип в ответ на команду /vars.
Попробуйте сами
- Перепишите объявления ниже с помощью
varтам, где тип остаётся очевидным, и оставьте явный тип там, где он нужен читателю:Scanner scanner = new Scanner(System.in); Set<Integer> seen = new HashSet<>(); Object payload = parser.parse(input); long total = 0; - В JShell объявите
var list = new ArrayList<>();, добавьте строку и число и командой/varsпосмотрите, какой тип получила переменная. Исправьте объявление. - Объясните словами, почему
varнельзя использовать для поля класса, хотя инициализатор у поля есть. Подсказка: подумайте о читателях, которые видят только сигнатуру класса.