Урок 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, 1Llong, 1.0double. Если переменная задумана как 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.

Попробуйте сами

  1. Перепишите объявления ниже с помощью var там, где тип остаётся очевидным, и оставьте явный тип там, где он нужен читателю:
    Scanner scanner = new Scanner(System.in);
    Set<Integer> seen = new HashSet<>();
    Object payload = parser.parse(input);
    long total = 0;
  2. В JShell объявите var list = new ArrayList<>();, добавьте строку и число и командой /vars посмотрите, какой тип получила переменная. Исправьте объявление.
  3. Объясните словами, почему var нельзя использовать для поля класса, хотя инициализатор у поля есть. Подсказка: подумайте о читателях, которые видят только сигнатуру класса.