Как стать автором
Обновить
75.94
Axiom JDK
на страже безопасности Java

В центре внимания Java: Local Variable Type Inference, или var

Уровень сложностиСредний
Время на прочтение14 мин
Количество просмотров820
Автор оригинала: Brian Goetz

Команда Axiom JDK подготовила перевод статьи про var, или Local Variable Type Inference (LVTI). Из этой статьи вы узнаете как работает var, когда эту фичу лучше использовать в коде, а когда — воздержаться. Всё это с примерами кода и комментариями от нашей команды.

Комментарий от команды Axiom JDK: материал статьи 2019 года актуален и на 2025 год. var (Local Variable Type Inference) уже давно является частью LTS-релизов и ключевой особенностью современного Java-кода, но по-прежнему является предметом споров даже между опытными разработчиками и вызывает у них вопросы. Статья Брайана Гётца — отличный материал с разбором принципов, которые не устарели. С 2019 года появилось больше практики, но база осталась неизменной. Мы публикуем перевод как удобный справочник по механике var, его  подводным камням, а также плюсам и минусам его использования.


Ключевые моменты

  • В Java SE 10 (март 2018) был представлен var — одна из самых часто запрашиваемых фичей Java за последние годы.

  • var — это механизм, при котором типы переменных могут быть выведены компилятором на основе контекста.

  • В Java var работает локально — область, в которой собираются и разрешаются ограничения, ограничена узкой частью программы, например, одним выражением или оператором.

  • Стюарт Маркс из команды Java Libraries подготовил полезный стайлгайд и FAQ, которые помогают понять плюсы и минусы при использовании var.

  • При правильном применении var может сделать код как более лаконичным, так и более читаемым.

На конференции Java Futures в QCon New York архитектор языка Java Брайан Гётц провёл блиц-обзор некоторых недавних и будущих возможностей Java. В этой статье он подробно рассматривает var.

Java SE 10 (март 2018) представила var. Ранее при объявлении локальной переменной требовалось явно указывать её тип. Теперь компилятор может сам определить статический тип переменной на основе её инициализатора:

var names = new ArrayList<String>();

В этом простом примере переменная names получает тип ArrayList<String>.

Несмотря на синтаксическое сходство с аналогичной возможностью в JavaScript, это не динамическая типизация — все переменные в Java по-прежнему имеют статический тип. var просто позволяет поручить компилятору определить тип за нас, вместо того чтобы указывать его явно.

Что такое var в Java

var — это инструмент, применяемый в языках со статической типизацией, при котором компилятор может определить тип переменной на основе контекста. В разных языках var может работать по-разному. В большинстве случаев это опциональная возможность: разработчик может сам решать, использовать var или явно указывать тип. Главное — применять var осознанно: там, где он улучшает читаемость, и избегать там, где может вносить путаницу.

Имена типов в Java могут быть громоздкими — из-за длинных названий классов, сложных дженериков или и того и другого. Общий факт о языках программирования — чем интереснее становится тип, тем менее приятно писать его имя. По этой причине языки с продвинутой системой типов нередко всё больше полагаются на var.

Java начала движение в сторону var ещё в Java 5, когда появились дженерики. Тогда появилась возможность опускать параметры типов у методов, позволяя компилятору выводить их самостоятельно:

List<String> list = Collection.emptyList();

вместо более громоздкого:

List list = Collection.emptyList();

На практике второй вариант почти не встречается — настолько var-подход стал привычным для Java-разработчиков.

В Java 7 добавили возможность вывода типов у конструкторов — так называемый diamond (<>):

List<String> list = new ArrayList<>();

вместо явного:

List<String> list = new ArrayList<String>();

В Java 8 появились лямбда-выражения, а с ними — возможность выводить типы параметров лямбды:

list.forEach(s -> System.out.println(s))

вместо явного:

list.forEach((String s) -> System.out.println(s))

И вот, наконец, в Java 10 var дошёл до объявления локальных переменных.

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

Хотя за годы область применения var в Java расширилась, один принцип дизайна фичи оставался неизменным: использовать var только в деталях реализации, а не в API. Типы полей, параметров и возвращаемых значений методов всегда должны быть явно указаны, потому что мы не хотим, чтобы контракты API изменялись в зависимости от реализации. Но в реализации тел методов можно позволить себе больше гибкости — ради читаемости.

Как работает var в Java

Многие разработчики воспринимают var как что-то близкое к волшебству или даже телепатии, олицетворяя компилятор и задаваясь вопросом: «Почему компилятор не смог догадаться, чего я хочу?». На деле всё гораздо проще: var — это не магия, а механизм разрешения ограничений (constraint solving).

Разные языки реализуют var по-своему, но базовая идея одна: cобрать ограничения на неизвестные типы и в определённый момент — разрешить эти ограничения и получить конкретные типы. Разработчики языков программирования выбирают: где можно применять var, какие ограничения учитывать, в какой области видимости эти ограничения решаются.

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

В некоторых других языках применяется глобальный вывод типа — например, они анализируют все места использования переменной, прежде чем вывести её тип. На первый взгляд, такой подход кажется более точным, но на практике он часто приводит к трудностям:

  • Ошибки становятся сложнее и менее очевидными.

  • Сообщения об ошибках могут появляться далеко от места проблемы — это путает и усложняет отладку.

  • Поведение становится менее предсказуемым.

Эти различия иллюстрируют один из фундаментальных компромиссов, с которыми сталкиваются дизайнеры языков при проектировании var: точность и выразительность против простоты и предсказуемости.

Мы можем подкрутить алгоритм так, чтобы он чаще “угадывал” правильный тип (например, расширить область анализа или собрать больше ограничений), но это почти всегда делает отладку хуже.

Рассмотрим простой пример с diamond:

List<String> list = new ArrayList<>();

Здесь list имеет явный тип List<String>. Компилятор должен вывести параметр типа x у ArrayList<x>, чтобы правая часть подходила под левую.

Здесь мы знаем, что list имеет явный тип List<String>. Нам нужно вывести параметр типа у конструктора ArrayList, который мы обозначим как x. То есть правая часть имеет тип ArrayList<x>.

Так как мы присваиваем правую часть левой, тип справа должен быть подтипом левого:

ArrayList<x> <: List<String>

(здесь <: означает «подтип»). Также мы знаем, что x по умолчанию ограничен типом Object, т.е. `x <: String`. Кроме того, по сигнатуре ArrayList мы знаем, что List<x> — супертип ArrayList<x>. Значит, из вышеуказанного мы получаем: x <: String (подробнее см. JLS 18.2.3). Так как это единственное ограничение на x, компилятор делает вывод: x = String.

Вот пример посложнее:

List<String> list = ...

Set<String> set = ...

var v = List.of(list, set);

Метод List.of(...) имеет следующую сигнатуру:

public static <X> List<X> of(X... values)

Теперь у нас больше информации, чем в предыдущем примере. Типы аргументов — List<String> и Set<String>.

Значит, мы можем собрать следующие ограничения:

List<String> <: x

Set<String> <: x

Для их решения мы вычисляем наименьшую общую верхнюю границу (least upper bound, LUB) — наиболее точный тип, который является суперклассом обоих типов (см. JLS 4.10.4 — Least Upper Bounds). В данном случае это Collection<String>. Так, итоговый тип v будет List<Collection<String>>.

Какие ограничения мы собираем?

Когда мы проектируем алгоритм var, важным решением становится — откуда именно собирать ограничения на типы в программе. В некоторых конструкциях всё очевидно. Например, в операциях присваивания тип выражения справа должен быть совместим с типом слева, поэтому ограничения собираются обязательно.

При вызове обобщённых методов (generic methods) можно собрать ограничения на основании их типов. Но в других случаях источники информации могут быть проигнорированы сознательно.

На первый взгляд, может показаться, что собирать как можно больше ограничений — это хорошо, ведь это ведёт к более точному выводу типа. Но точность — не всегда главное. Сбор дополнительных ограничений может:

  • Привести к перенасыщенности (overconstrained types), что делает вывод типа невозможным и компилятор вынужден либо провалить вывод, либо выбрать запасной вариант (например, Object).

  • Повысить нестабильность поведения кода — небольшие изменения в одном месте могут повлиять на типизацию или выбор перегруженного метода в другом месте.

Как и в вопросе «в какой области собирать ограничения», здесь тоже приходится искать баланс между точностью и предсказуемостью. Это уже вопрос не техники, а инженерного чутья.

Рассмотрим перегрузку методов, когда в качестве аргумента передаётся лямбда. Казалось бы, можно использовать исключения, выбрасываемые в теле лямбды, чтобы уточнить, какой перегруженный метод должен быть выбран (что повышает точность). Но это бы означало, что любое изменение в теле лямбды (например, выбрасываемое исключение) могло бы изменить выбор метода, что сделало бы поведение непредсказуемым и хрупким.

В итоге, было решено не учитывать такие ограничения при выборе перегрузки в пользу предсказуемости.

Тонкости (The Fine Print)

Теперь, когда мы поняли, как работает var в целом, давайте разберёмся с тонкостями его поведения при объявлении локальных переменных.

Когда мы объявляем переменную с var, компилятор сначала вычисляет так называемый "standalone" тип инициализатора. Это означает, что компилятор анализирует выражение снизу вверх, без учёта типа назначения (то есть он не смотрит на то, чему мы присваиваем результат). Некоторые выражения не имеют standalone-типа — например, лямбды и ссылки на методы. Поэтому они не могут быть инициализаторами переменных с var.

Для большинства выражений тип просто выводится из выражения. Но есть исключения: если тип невозможно записать в синтаксисе языка или тип небезопасен, тогда компилятор может уточнить или отклонить тип.

Тип, который невозможно записать в синтаксисе языка, — это тип, который невозможно записать вручную в коде на Java. Примеры:

  • Пересечения типов (например, Runnable & Serializable).

  • Capture-типы — типы, возникающие при захвате wildcard-типов.

  • Анонимные типы — типы объектов, созданных через new Interface() { ... }.

  • Тип null — это отдельный тип в спецификации Java, соответствующий значению null.

Изначально планировалось запретить использование var с такими типами, предполагая, что var — просто синтаксический сахар для явного типа.

Но на практике оказалось, что эти типы повсеместны, и такой запрет сильно ограничил бы полезность var. Поэтому программы, использующие var, не всегда можно переписать, заменив var на явный тип — потому что не все типы можно выразить явно.

Вот пример такой программы:

var v = new Runnable() {

    void run() { … }

    void runTwice() { run(); run(); }

};

v.runTwice();

Если бы мы попытались явно указать тип Runnable, то метод runTwice() оказался бы недоступным — ведь он не входит в интерфейс Runnable. Только использование var, позволяющее компилятору «увидеть» фактический анонимный тип, даёт доступ к runTwice().

Тип Null (var x = null) — это тип с единственным возможным значением, и это почти никогда не то, что вы хотели. Компилятор не будет гадать, какой тип вы имели в виду — он требует явного указания типа.

var с анонимными или пересечёнными типами — разрешён. Эти типы могут быть “странными”, но они неопасны. Раньше они редко всплывали на поверхность, но с появлением var вы теперь можете увидеть их “вживую”.

Например, у нас есть:

var list = List.of(1, 3.14d);

Мы уже видели похожий пример ранее и знаем, что произойдёт — компилятор будет искать наименьшую общую верхнюю границу (LUB) для Integer и Double. Им окажется не просто Number, а Number & Comparable<? extends Number & Comparable<?>>. Так что тип list будет List<Number & Comparable<? extends Number & Comparable<?>>>.

Да, это ужасно выглядит, но это корректный и безопасный тип. Просто var делает такие случаи видимыми.

Самый сложный случай — capture-типы. Capture-типы — это результат wildcard-захвата. Они появляются, когда мы работаем с типами вроде List<?>. Каждый такой ? соответствует своему типу. Рассмотрим такой пример объявления метода:

void m(List<?> a, List<?> b)

Кажется, что a и b идентичны по типу, но это не так. У нас нет оснований полагать, что оба списка содержат элементы одного вида. Если бы мы хотели, чтобы списки были одного типа, мы бы сделали метод m() обобщённым с параметром T и использовали List<T> для обоих случаев.

Так, компилятор создаёт плейсхолдер (placeholder), называемый capture (захватом), для каждого использования ? в программе — чтобы разделять разные применения wildcard-типов. До сих пор capture-типы оставались в тени, где им и положено быть. Если бы мы позволили им просочиться в код, это могло бы внести путаницу.

Например, предположим в классе MyClass есть такой код:

var c = getClass();

Можно подумать, что c имеет тип Class<?>. Но на самом деле правая часть имеет тип Class<capture<?>>. Если бы мы позволили этому типу "разгуляться" в нашей программе, это никому не помогло бы.

Изначально запрет на вывод capture-типов казался привлекательным решением, но на практике такие типы возникали слишком часто. Вместо этого мы решили "санировать" их с помощью преобразования, известного как upward projection (JLS 4.10.5). В процессе преобразования берётся тип, который может содержать capture-типы, и возвращается его супертип — уже без capture-типов.

В примере выше upward projection преобразует тип c в Class<?> — более предсказуемый и "воспитанный" тип.

Такой подход — это прагматичное решение, но не идеальное. Поскольку мы выводим не "естественный" тип выражения, а его обработанную версию, это может повлиять на вывод типов при рефакторинге и выбор перегруженных методов. Например, если заменить сложное выражение f(e) на:

var x = e;

f(x);

То это может изменить логику вывода типов ниже по коду. Обычно проблем не возникает, но мы осознанно идём на этот риск, чтобы избежать больших сложностей с capture-типами. В данном случае "побочные эффекты лечения оказались лучше самой болезни".

Мнения разделились

В сравнении с такими крупными фичами, как лямбды или дженерики (generics), var может показаться мелочью. Хотя, как вы уже видели, детали var сложнее, чем кажется. Однако споры вокруг неё были отнюдь не маленькими.

В течение нескольких лет var был одной из самых часто запрашиваемых фич. Разработчики, работавшие с другими языками вроде C#, Scala или Kotlin, остро ощущали отсутствие var при возвращении к Java и активно об этом говорили. Мы (разработчики языка) решили двигаться вперёд, основываясь на популярности фичи, успешном опыте в Java-подобных языках, ограниченной области её действия и слабом влиянии на другие особенности языка.

Как ни странно, как только мы анонсировали работу над этой фичей, появилась и другая группа разработчиков — ярые противники, считавшие это худшей идеей всех времён. Они называли это «погоней за модой», «поощрением лени» (и похуже), а также пророчили мрачное будущее с нечитаемым кодом. При этом и сторонники, и противники апеллировали к одному и тому же принципу — читаемости кода.

После релиза оказалось, что всё не так страшно. Да, сначала разработчикам пришлось осваивать новые правила (как и с любой другой фичей), но в итоге они нашли разумные стили использования и быстро усвоили разумные границы применения var — где он полезен, а где только вредит.

Рекомендации по стилю

Стюарт Маркс (Stuart Marks) из команды Java Libraries в Oracle подготовил рекомендации по стилю, чтобы помочь разработчикам правильно использовать var и понимать плюсы и минусы, связанные с ним.

Как и в большинстве хороших гайдлайнов, акцент здесь сделан на ясном обозначении плюсов и минусов. Явное указание типов — это всегда баланс: с одной стороны, явный тип даёт чёткое и точное описание типа переменной, но с другой — иногда тип очевиден или неважен, а его указание может отвлекать внимание от более значимой информации.

Основные принципы руководства:

  • Выбирайте осмысленные имена переменных. Если имя переменной “говорящее”, то явное указание типа часто становится избыточным или даже мешает. Если мы используем такие имена переменных, как a3 или x, тогда отсутствие явного указания типа сделает код менее понятным.

  • Минимизируйте область видимости переменных. Чем больше расстояние между объявлением переменной и её использованием, тем выше риск ошибочной трактовки. Использование var для переменных с большой областью видимости повышает риск ошибок по сравнению с переменными c небольшой областью видимости или переменными с явно указанным типом.

  • Используйте var, когда инициализатор говорит сам за себя. Во многих случаях инициализирующее выражение делает тип очевидным (например, var names = new ArrayList<String>()), и явное указание типа становится излишним.

  • Не переживайте насчёт "программирования через интерфейсы". Хотя традиционно рекомендуется использовать абстрактные типы (например, List) вместо конкретных реализаций (вроде ArrayList), при выводе типов компилятор выбирает конкретный тип. Для локальных переменных с небольшой областью видимости это некритично — важнее соблюдать это правило для API (например, для типов возвращаемых методов).

  • Будьте осторожны с сочетанием var и diamond (<>). И var, и <> перекладывают вывод типов на компилятор. Их можно использовать вместе, но только если контекст предоставляет достаточно информации (например, в типах аргументов конструктора).

  • Берегитесь комбо var + числовые литералы. Числовые литералы — это poly expressions: их тип зависит от контекста (например, short x = 0 совместим с int, long, short и byte). Но без целевого типа отдельно стоящий тип числового литерала по умолчанию — int. Замена short s = 0 на var s = 0 изменит тип переменной.

  • Используйте var, чтобы выделять подвыражения. Если разбиение вложенных или выражений-цепочек на отдельные переменные кажется громоздким, это подталкивает к написанию сложного (и менее читаемого) кода. var снижает "стоимость" создания промежуточных переменных, уменьшая соблазн писать запутанные однострочники.

Последний пункт иллюстрирует важный нюанс, часто упускаемый в спорах.

При оценке новой фичи обычно рассматривают только поверхностные варианты использования (например, механическую замену явных типов на var). Однако на стиль программирования влияют и другие факторы, включая "стоимость" тех или иных конструкций. Упрощение объявления переменных может сместить баланс в сторону большего числа локальных переменных — и в итоге улучшить читаемость кода. Такие косвенные эффекты редко учитываются в горячих спорах о пользе или вреде фичи.

Многие из этих правил — просто хорошие практики: например, выбор удачных имён переменных улучшает читаемость независимо от использования вывода типов.

Выводы

Как мы увидели, var — это не такая уж и простая фича, как кажется на первый взгляд из-за её лаконичного синтаксиса. Вопреки ожиданиям некоторых разработчиков, она не позволяет просто "игнорировать типы", а напротив — требует глубокого понимания системы типов Java. Но если вы понимаете, как работает var, и следуете разумным рекомендациям по стилю, то var действительно может сделать код более лаконичным и читаемым.

Об авторе

сам Брайан Гётц
сам Брайан Гётц

Брайан Гётц (Brian Goetz) — архитектор языка Java в Oracle и руководитель разработки спецификации JSR-335 (Lambda Expressions for the Java Programming Language). Он — автор бестселлера «Java Concurrency in Practice», увлекается программированием ещё со времён президентства Джимми Картера.


Дополнения от команды Axiom JDK:

var — это не магия, а строгое правило вывода типа по контексту. В публичных API его лучше избегать — читаемость важнее. Механизм предсказуем даже в edge-кейсах (capture-типы, LUB), что говорит о качестве архитектуры.

При этом есть моменты, которые, на наш взгляд, стоит уточнить. Во-первых, var — не просто синтаксический сахар. Он действительно влияет на стиль кода, особенно в лямбда-выражениях. Начиная с Java 11, var можно использовать в параметрах лямбд — и это не просто про синтаксическую единообразность. Это открывает возможность применять аннотации прямо в параметрах без явного указания типа:

list.stream()

    .map((@Nonnull var item) -> item.toUpperCase())

    .toList();

Такой подход полезен, например, при валидации, логировании, проверках nullability и других сценариях, где аннотация важнее самого типа.

Но есть ограничения:

  • Нельзя смешивать var и неуказанные типы: (var a, b) -> ... — ошибка.

  • Нельзя комбинировать var и явно заданные типы: (var a, String b) -> ... — тоже ошибка.

  • И, в отличие от обычных однопараметрных лямбд, с var скобки обязательны: var s -> ... не скомпилируется.

Так что var в лямбдах — не просто "удобство", а мощный инструмент, особенно если вы активно используете аннотации.

Ещё один момент: var в Java остаётся строго статически типизированным, несмотря на внешнее сходство с JavaScript или Kotlin. И при рефакторинге замена var на явный тип (и обратно) может неожиданно повлиять на выбор перегруженного метода — это стоит держать в голове.

Если посмотреть на современный Java-код разных команд — всё становится понятно. Были холивары, "нужно ли вообще этот var", были советы и гайды: "только если тип очевиден", "не трогай в публичных API", "не порти читаемость". Всё это обсуждали, спорили, кто-то писал регламенты, кто-то игнорировал.

Прошло более 5 лет. В Java 21+ var используется уже и в pattern matching, и в record-классах. Возможностей стало больше, стиль кода стал эволюционировать:

// Records + var

record User(String name, int age) {}

var user = new User("Alice", 30); // всё ясно по правой части
System.out.println(user.name());
// Stream API + var (иногда упрощает, особенно с лямбдами)

list.stream()

    .map((var item) -> process(item)) // var в лямбда-параметре

    .forEach(System.out::println);

Всё это стало частью стиля: var — это не про "ленивое программирование", а про читаемость и уместность. И сейчас статья Гётца читается не как спорный манифест, а как спокойное резюме: вот как всё работает под капотом, вот где можно ошибиться, а вот где фича действительно в тему. И за это — спасибо.


Итоговая рекомендация, которую мы сами придерживаемся: используйте var, когда тип очевиден и код выигрывает в читаемости. В остальном — явное лучше неявного.

Теги:
Хабы:
+8
Комментарии1

Публикации

Информация

Сайт
axiomjdk.ru
Дата регистрации
Численность
51–100 человек