Вопрос 1. Для чего используется var в Java?
Локальные переменные — это "рабочие лошадки" Java. Они используются для хранения промежуточных результатов вычислений. И, в отличие от полей, объявляются, инициализируются и используются в одном блоке. Для понимания кода часто более важны имя и инициализатор, чем тип локальной переменной. В имени и инициализаторе столько же информации, сколько и в типе:
Person person = new Person();
Вместо явного указания типа можно использовать var
:
var person = new Person();
Компилятор определяет тип локальной переменной по инициализатору. Это особенно важно, когда тип параметризован wildcard
, или упоминается в инициализаторе. Использование var
может сделать код более кратким без ущерба для удобочитаемости, а в некоторых случаях может улучшить читаемость благодаря устранению избыточности.
Вопрос 2. Делает ли это Java динамически типизированным языком? Это то же самое, что и var в JavaScript?
Нет и нет. Java остается языком со статической типизацией, и появление var
не изменяет этого. var
используется в объявлении локальной переменной без явного указания типа. При использовании var
компилятор Java определяет тип переменной во время компиляции, используя информацию о типе, полученную от инициализатора переменной. Далее выведенный тип используется как статический тип этой переменной. Как правило, будет тот же тип, который вы бы указали явно, поэтому переменная, объявленная с помощью var
, ведет себя точно так же, как при явном указании типа.
В компиляторе Java выведение типов присутствует уже много лет. Например, параметры лямбда-выражения в Java 8 не нуждаются в явном указании типа, так как компилятор определяет их по тому, как используется лямбда-выражение:
List<Person> list = ...
list.stream().filter(p -> p.getAge() > 18) ...
В приведенном выше фрагменте кода тип параметра p
в лямбде выводится как Person
. Если класс Person
будет изменен так, что в нем больше не будет метода getAge
, или в списке будет храниться что-то отличное от Person
, то выведение типа завершится ошибкой при компиляции.
Вопрос 3. Переменная var неявно final?
Нет. Локальные переменные, объявленные с помощью var
, по умолчанию не являются final
. Однако к var
можно добавить модификатор final
:
final var person = new Person();
Для final var
в Java нет никакого сокращения. Например, в Scala для объявления неизменяемых переменных используется val
. В Scala это хорошо работает, потому что все переменные (и локальные, и поля) объявляются с помощью синтаксиса следующего вида.
val name : type
или
var name : type
Указывать ": type
" в объявлении необязательно — это зависит от того, хотите ли вы использовать выведение типа или нет. В Scala выбор между изменяемостью и неизменяемостью ортогонален к выведению типов.
В Java var
можно использовать только там, где требуется выведение типа. Его нельзя использовать, если тип объявлен явно. Если в Java добавить val
, то его тоже можно было бы использовать только там, где используется выведение типов. В Java нельзя было бы использовать var
или val
для управления иммутабельностью при явном объявлении типа.
Кроме того, Java позволяет использовать var
только для локальных переменных, но не для полей. Для полей иммутабельность более значима. Неизменяемые локальные переменные используются сравнительно редко.
Использование ключевых слов var/val
для управления иммутабельностью — это то, что действительно стоит позаимствовать из Scala в Java. Однако в Java это было бы гораздо менее полезно, чем в Scala.
Вопрос 4. Не будут ли плохие разработчики злоупотреблять var, чтобы писать ужасный код?
Да, плохие разработчики будут писать ужасный код, что бы мы ни делали. Отказ от var
не помешает им это сделать. Но при правильном применении выведение типов позволит писать более качественный код.
Один из моментов, благодаря которому var
может побудить разработчиков писать более качественный код, заключается в том, что var
снижает накладные расходы на объявление новой переменной. Разработчики часто избегают объявления новой переменной, если это требует больших затрат, и создают сложные конструкции, ухудшающие читаемость, только из-за того, чтобы избежать объявления дополнительных переменных. С var накладные расходы на извлечение части кода в именованную переменную уменьшаются, поэтому разработчики с большей вероятностью будут это делать, что приведет к более чистому коду.
Часто программисты начинают очень активно использовать новые возможности языка, возможно, злоупотребляя ими, и требуется некоторое время, чтобы сообщество выработало рекомендации по разумному использованию.
Начиная с выведения типа локальной переменной (Local Variable Type Inference, LVTI), мы публикуем материалы о назначении и рекомендации по использованию (например, данный FAQ и LVTI Style Guidelines) почти одновременно с появлением этой функциональности. Мы надеемся, что это ускорит понимание в сообществе, когда разумно использовать var
, и поможет избежать злоупотребления.
Вопрос 5. Где можно использовать var?
var
может использоваться при объявлении локальных переменных, включая индексные переменные цикла for
и ресурсные переменные оператора try-with-resources
.
var нельзя использовать для полей, параметров методов и возвращаемых типов методов. Причина заключается в том, что типы в этих местах явно присутствуют в class-файлах и в спецификациях Javadoc. При выведении типа изменение инициализатора может легко привести к изменению предполагаемого типа переменной. Для локальных переменных это не проблема, так как область видимости локальных переменных ограничена, и их типы напрямую не записываются в class-файлы. Однако для полей, параметров методов и типов, возвращаемых методом, выведение типа может легко вызвать проблему.
Например, возвращаемый тип метода был выведен из выражения в операторе return
. Изменение реализации метода может привести к изменению типа выражения в return
. Это, в свою очередь, может изменить тип возвращаемого значения. Что может привести к бинарной несовместимости или несовместимости в исходном коде. Такие несовместимые изменения не должны возникать из-за безобидных на вид изменений в реализации.
Предположим, что также будет выводиться тип поля. Тогда изменение инициализатора поля может привести к изменению типа поля, что может неожиданно нарушить рефлексивный код.
Выведение типа допустимо в реализации, но не в API. API-контракты должны быть объявлены явно.
Как насчет private-полей и методов, которые не являются частью API? Теоретически мы могли бы реализовать поддержку var для private-полей и возвращаемых типов для private-методов, не опасаясь, что это вызовет несовместимость из-за раздельной компиляции и динамической линковки. Но для простоты мы решили ограничить область выведения типов. Если расширить границы и включить отдельные виды полей и типы, возвращаемые некоторыми методами, то это сделает все значительно более сложным и трудным для понимания, и лишь незначительно более полезным.
Вопрос 6. Почему для var всегда нужен инициализатор в правой части?
Тип переменной вычисляется на основе инициализатора. Это означает, что var
можно использовать только при наличии инициализатора. Можно было бы сделать выведение типа из присвоений переменной, но это значительно усложнило бы реализацию и потенциально могло привести к вводящим в заблуждение или трудно диагностируемым ошибкам. Для простоты мы определили var
так, чтобы для выведения типа использовалась только локальная информация.
Допустим, мы разрешили выведение типа на основе присваивания в нескольких местах, отдельно от объявления переменной. Тогда рассмотрим следующий пример:
var order;
...
order = "first";
...
order = 2;
Если тип был выбран на основе, например, первого присваивания, то это может привести к ошибке в другом операторе, весьма удаленном от местоположения настоящей ошибки. (Иногда это называют проблемой "дальнодействия".)
В качестве альтернативы можно выбрать тип, совместимый со всеми присваиваниями. И в этом случае можно ожидать, что выведенный тип будет Object
, как общий суперкласс String
и Integer
. К сожалению, ситуация сложнее. Поскольку и String
, и Integer
являются Serializable
и Comparable
, общий супертип будет пересечением типов, что-то вроде
Serializable & Comparable<? extends Serializable & Comparable<...>>
Обратите внимание, что невозможно явно объявить переменную этого типа. Также это приводит к упаковыванию (boxing), когда order присваивается 2, что может быть неожиданным и нежелательным.
Чтобы избежать подобных проблем, проще потребовать вывод типа с использованием явного инициализатора.
Вопрос 7. Почему нельзя использовать var с null?
Посмотрите на такое объявление (оно некорректно):
var person = null; // ОШИБКА
Литерал null
обозначает значение специального типа null
(JLS 4.1), который является подтипом всех ссылочных типов в Java. Единственным значением типа null
является сам null
, поэтому единственное значение, которое может быть присвоено переменной типа null
— это null
. Это не очень полезно.
Можно ввести специальное правило, чтобы объявление var
, инициализированное значением null
, имело тип Object
. Но возникает вопрос о намерениях программиста. Предположим, что переменная инициализируется значением null
, чтобы позже ей присвоить какое-либо другое значение. Но в этом случае маловероятно, что определение типа переменной как Object
будет правильным.
Вместо того чтобы создавать какие-то специальные правила для обработки этого случая, мы его запретили. Если вам нужна переменная типа Object
, объявите ее явно.
Вопрос 8. Можно ли использовать var с diamond-оператором справа?
Да, это работает, но, скорее всего, не так, как вы ожидаете. Пример:
var list = new ArrayList<>();
Тип списка будет ArrayList<Object>
. В общем, лучше указывать явный тип слева с diamond-оператором справа или использовать var слева с явным типом справа. См. особенности использования var с diamond-оператором и дженериками в LVTI Style Guidelines (Руководство по стилю LVTI).
Приглашаем всех желающих на открытое занятие «Реляционные базы данных для начинающих Java-разработчиков». Поговорим о месте реляционных баз данных в архитектуре информационных систем. Рассмотрим основные компоненты и возможности РСУБД на примере PostgreSQL. Сделаем обзор основных технологий по работе с реляционными БД в Java (JDBC, JPA/Hibernate, Spring Data и др.) Регистрация по ссылке.
Комментарии (6)
valery1707
16.09.2022 18:18+1Вопрос 6. Почему для
var
всегда нужен инициализатор в правой части?
Тип переменной вычисляется на основе инициализатора.
Это означает, чтоvar
можно использовать только при наличии инициализатора.Начиная с
Java 11
(вышла 4 года назад) ключевое словоvar
можно использовать и не только при наличии инициализатора, а например и у аргументов в лямбдах:Function<String, String> append = (@NotNull var string) -> string + " ";
ultrinfaern
16.09.2022 18:38+3Можно var инициализировать null:
var test = (AnyType) null;
Зачем? Ну, напрмер, если у вас много инциализаций через var, и в нем затесается определениечерез явный тип, то на таком определении ломается "чтение" кода.
IgorPie
16.09.2022 22:49-1var person = null; // ОШИБКА
и это в языке, где в случае любой непонятной ситуации отсыл к Object
Sap_ru
17.09.2022 02:53+2А к иначе, если тип переменной выводит компилятор в момент объявления? Какой у вас тим переменной получился?
Kotofay
17.09.2022 18:41+2var это не более чем синтаксический сахар.
Никакого автоматического приведения типов нет и быть не может.
panzerfaust
Как-то спорно. По-моему реальная проблема это придумать название для промежуточной переменной посреди длинного метода, а не объявлять ее тип. От var тут ни холодно, ни жарко. Красивое решение этой проблемы - это контекстные функции в Kotlin. Там если повезет, то можно весь метод выразить как цепочку из функций высшего порядка без промежуточных переменных.