26 рекомендаций по использованию типа var в Java

Автор оригинала: Anghel Leonard
  • Перевод

The Java Local Variable Type Inference (LVTI) или кратко — тип var (идентификатор var — это не ключевое слово, а зарезервированное имя типа) был добавлен в Java 10 с помощью JEP 286: Local-Variable Type Inference. Являясь 100% функцией компилятора, она не влияет на байт-код, время выполнения или производительность. В основном компилятор проверяет правую часть от оператора присваивания и, исходя из нее, определяет конкретный тип переменной, а затем заменяет им var.


Кроме того, это полезно для сокращения многословности шаблонного кода, а так же позволяет ускорить сам процесс программирования. Например, очень удобно писать var evenAndOdd =... вместо Map<Boolean, List<Integer>> evenAndOdd =... .


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


В этой статье мы рассмотрим 26 ситуаций, с примерами того, когда можно использовать var, а когда этого делать не стоит.


Пункт 1: старайтесь давать осмысленные имена локальным переменным


Обычно мы фокусируемся на том, чтобы давать правильные имена полям классов, но мы не уделяем такого же внимания именам локальных переменных. Когда наши методы отлично реализованы, содержат мало кода и имеют хорошие имена, то очень часто мы не обращаем внимание на локальные переменные, а то и вовсе сокращаем их имена.


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


Пример 1:


Наверно, многие согласятся, что в примере ниже имена локальных переменных слишком короткие:


// HAVING
public boolean callDocumentationTask() {
    DocumentationTool dtl = ToolProvider.getSystemDocumentationTool();
    DocumentationTask dtt = dtl.getTask(...);
    return dtt.call();
}

При использовании коротких имен, совместно с var, код становится еще менее понятным:


// AVOID
public boolean callDocumentationTask() {
    var dtl = ToolProvider.getSystemDocumentationTool();
    var dtt = dtl.getTask(...);
    return dtt.call();
}

Более предпочтительный вариант:


// PREFER
public boolean callDocumentationTask() {
    var documentationTool = ToolProvider.getSystemDocumentationTool();
    var documentationTask = documentationTool.getTask(...);
  return documentationTask.call();
}

Пример 2:


Избегайте подобного именования переменных:


// AVOID
public List<Product> fetchProducts(long userId) {
    var u = userRepository.findById(userId);
    var p = u.getCart();
    return p;
}

Используйте более осмысленные имена:


// PREFER
public List<Product> fetchProducts(long userId) {
    var user = userRepository.findById(userId);
    var productList = user.getCart();
    return productList;
}

Пример 3:


В стремлении давать более понятные имена локальным переменным, не впадайте в крайности:


// AVOID
var byteArrayOutputStream = new ByteArrayOutputStream();

Вместо этого можно использовать более краткий, но не менее понятный вариант:


// PREFER
var outputStream = new ByteArrayOutputStream();
// or
var outputStreamOfFoo = new ByteArrayOutputStream();

Знаете ли вы, что у Java есть внутренний класс с именем:
InternalFrameInternalFrameTitlePaneInternalFrameTitlePaneMaximizeButtonWindowNotFocusedState


Что же, именование переменных с таким типом может быть непростым делом :)


Пункт 2: используйте литералы чтобы помочь var точно определить тип примитива (int, long, float, double)


Без использования литералов для примитивных типов мы можем обнаружить, что ожидаемые и предполагаемые типы могут различаться. Это вызвано неявным приведением типов, который используется var-переменными.


Например, следующие два фрагмента кода ведут себя, как и ожидалось. Тут мы явно объявляем типы boolean и char:


boolean flag = true;     // this is of type boolean
char a = 'a';            // this is of type char

Теперь используем var, вместо явного объявления типов:


var flag = true;         // this is inferred as boolean
var a = 'a';             // this is inferred as char

Пока все хорошо. А теперь сделаем то же самое для типов int, long, float и double:


int intNumber = 20;       // this is of type int
long longNumber = 20;     // this is of type long
float floatNumber = 20;   // this is of type float, 20.0
double doubleNumber = 20; // this is of type double, 20.0

Хотя приведенный выше фрагмент кода прост и понятен, теперь давайте воспользуемся var, вместо явного указания типов.


Избегайте:


// AVOID
var intNumber = 20;       // this is inferred as int
var longNumber = 20;      // this is inferred as int
var floatNumber = 20;     // this is inferred as int
var doubleNumber = 20;    // this is inferred as int

Все четыре переменные будут выведены, как int. Чтобы исправить это поведение, нам нужно использовать литералы Java:


// PREFER
var intNumber = 20;       // this is inferred as int
var longNumber = 20L;     // this is inferred as long
var floatNumber = 20F;    // this is inferred as float, 20.0
var doubleNumber = 20D;   // this is inferred as double, 20.0

Но что случится, если мы объявим число с десятичной частью?


Избегайте этого, если ожидаете получить переменную типа float:


// AVOID, IF THIS IS A FLOAT
var floatNumber = 20.5;   // this is inferred as double

Что бы избежать неожиданности, используйте соответствующий литерал:


// PREFER, IF THIS IS A FLOAT
var floatNumber = 20.5F;  // this is inferred as float

Пункт 3: в некоторых случаях var и неявные приведения типов могут упростить поддержку кода


Например, давайте предположим, что наш код находится между двумя методами. Один метод получает корзину покупок с разными товарами и вычисляет лучшую цену. Для этого он сравнивает различные цены на рынке и возвращает общую цену в виде типа float. Другой метод просто списывает эту цену с карты.


Во-первых, давайте посмотрим на метод, который вычисляет лучшую цену:


public float computeBestPrice(String[] items) {
   ...
   float price = ...;
   return price;
}

Во-вторых, давайте взглянем на метод, который работает с картой:


public boolean debitCard(float amount, ...) {
    ...
}

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


// AVOID
public void purchaseCart(long customerId) {
    ...
    float price = computeBestPrice(...);
    debitCard(price, ...);
}

Через некоторое время компания, которая владеет API, решает отказаться от вещественного представления цен в пользу десятичного (вместо float теперь используется int). Итак, они модифицировали код API следующим образом:


public int computeBestPrice(String[] items) {
   ...
   float realprice = ...;
   ...
   int price = (int) realprice;
   return price;
}

public boolean debitCard(int amount, ...) {
    ...
}

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


// PREFER
public void purchaseCart(long customerId) {
    ...
    var price = computeBestPrice(...);
    debitCard(price, ...);
}

Пункт 4: когда литералы не являются подходящим решением, то используйте явное приведение типов или откажитесь от var


Некоторые примитивные типы в Java не имеют специальных литералов, например, типы byte и short. В этом случае, используя явное обозначение типов, мы можем создавать переменные без каких-либо проблем.


Используйте это вместо var:


// PREFER THIS INSTEAD OF USING VAR
byte byteNumber = 45;     // this is of type byte
short shortNumber = 4533; // this is of type short

Но зачем в данной ситуации отдавать предпочтение явному обозначению типов вместо того, чтобы просто использовать var? Что же, давайте напишем этот код, используя var. Обратите внимание, что в обоих случаях компилятор предположит, что вам нужны переменные типа int.


Избегайте этой ошибки:


// AVOID
var byteNumber = 45;     // this is inferred as int
var shortNumber = 4533;  // this is inferred as int

Здесь нет литералов, которые пришли бы нам на помощь, потому мы вынуждены использовать явное нисходящее приведение типов. Лично я буду избегать таких ситуаций, поскольку не вижу здесь никаких преимуществ.


Прибегайте к подобной записи только, если вы действительно хотите использовать var:


// PREFER THIS ONLY IF YOU WANT TO USE VAR
var byteNumber = (byte) 45;     // this is inferred as byte
var shortNumber = (short) 4533; // this is inferred as short

Пункт 5: избегайте использования var, если названия переменных не содержат достаточной информации о типе для понимания кода


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


Избегайте следующего:


// AVOID
MemoryCacheImageInputStream inputStream = new MemoryCacheImageInputStream(...);

Вместо этого используйте:


// PREFER
var inputStream = new MemoryCacheImageInputStream(...);

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


Избегайте:


// AVOID
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fm = compiler.getStandardFileManager(...);

Используйте следующий код:


// PREFER
var compiler = ToolProvider.getSystemJavaCompiler();
var fileManager = compiler.getStandardFileManager(...);

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


Избегайте:


// AVOID
public File fetchCartContent() {
    return new File(...);
}
// As a human, is hard to infer the "cart" type without 
// inspecting the fetchCartContent() method
var cart = fetchCartContent();

Используйте:


// PREFER
public File fetchCartContent() {
    return new File(...);
}
File cart = fetchCartContent();

Подумаем, к примеру, над использованием класса java.nio.channels.Selector. Этот класс имеет статический метод open(), который возвращает new Selector и открывает его. Но здесь запросто можно подумать, что метод Selector.open() может возвращать тип boolean, в зависимости от успешности открытия имеющегося селектора, или даже возвращать void. Использование здесь var приведет к потери информации и путанице в коде.


Пункт 6: тип var гарантирует безопасность во время компиляции


Это означает, что мы не можем скомпилировать приложение, которое попытается выполнить неправильное присваивание. Например, код ниже не скомпилируется:


// IT DOESN'T COMPILE
var items = 10;
items = "10 items"; // incompatible types: String cannot be converted to int

А вот этот скомпилируется:


var items = 10;
items = 20;

И этот код успешно скомпилируется:


var items = "10";
items = "10 items";

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


Пункт 7: var не может использоваться для создания экземпляра конкретного типа и назначения его переменной типа интерфейса


В Java мы используем подход "программирование с помощью интерфейсов". Например, мы создаем экземпляр класса ArrayList, связывая его с абстракцией (интерфейсом):


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

И мы избегаем таких вещей, как привязка объекта к переменной того же типа:


ArrayList<String> products = new ArrayList<>();

Это наиболее распространенная и желательная практика, так как мы можем легко заменить реализацию интерфейса на любую другую. Для этого лишь необходимо объявить переменную типа интерфейса.


Мы не сможем следовать этой концепции, используя var переменные, т.к. для них всегда выводится конкретный тип. Например, в следующем фрагменте кода компилятор определит тип переменной, как ArrayList<String>:


var productList = new ArrayList<String>(); // inferred as ArrayList<String>

Есть несколько аргументов в защиту var, которые объясняют подобное поведение:


  • var используется для локальных переменных, где, в большинстве случаев, программирование с помощью интерфейсов используется меньше, чем в случаях с параметрами методов, возвращаемыми значениями или полями


  • Область действия локальных переменных должна быть небольшой, поэтому решение проблем, вызванных переключением на другую реализацию, не должно составить больших трудностей


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



Пункт 8: вероятность вывода неожидаемого типа


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


До Java 7 для коллекций использовалось явное указание типов:


// explicitly specifying generic class's instantiation parameter type
List<String> products = new ArrayList<String>();

Начиная с Java 7 был введен diamond operator. В таком случае компилятор самостоятельно выведет необходимый тип:


// inferring generic class's instantiation parameter type 
List<String> products = new ArrayList<>();

Какой же тип будет выведен в коде ниже?


Вы должны избегать подобных конструкций:


// AVOID
var productList = new ArrayList<>(); // is inferred as ArrayList<Object>

Тип будет определен, как ArrayList<Object>. Это происходит потому, что информация, необходимая для корректного определения типа, не представлена. Это приводит к тому, что будет выбран ближайший тип, который может быть совместим с контекстом происходящего. В данном случае — Object.


Таким образом, var можно использовать только, если мы предоставим необходимую информацию для определения ожидаемого типа. Тип может быть указан непосредственно или передан в качестве аргумента.


Непосредственно указывайте тип:


// PREFER
var productList = new ArrayList<String>(); // inferred as ArrayList<String>

Передавайте аргументы необходимого типа:


var productStack = new ArrayDeque<String>(); 
var productList = new ArrayList<>(productStack); // inferred as ArrayList<String>

Product p1 = new Product();
Product p2 = new Product();
var listOfProduct = List.of(p1, p2); // inferred as List<Product>
// DON'T DO THIS
var listofProduct = List.of(); // inferred as List<Object>
listofProduct.add(p1);
listofProduct.add(p2);

Пункт 9: присвоение массива к var-переменной не требует скобок [ ]


Все мы знаем как объявлять массивы в Java:


int[] numbers = new int[5];
// or, less preferred
int numbers[] = new int[5];

Как насчет использования var при работе с массивами? В этом случае нет необходимости использовать скобки с левой стороны.


Избегайте следующего (это даже не скомпилируется):


// IT DOESN'T COMPILE
var[] numbers = new int[5];
// or
var numbers[] = new int[5];

Используйте :


// PREFER
var numbers = new int[5]; // inferred as array of int
numbers[0] = 2;   // work
numbers[0] = 2.2; // doesn't work
numbers[0] = "2"; // doesn't work

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


// explicit type work as expected
int[] numbers = {1, 2, 3};
// IT DOESN'T COMPILE
var numbers = {1, 2, 3};
var numbers[] = {1, 2, 3};
var[] numbers = {1, 2, 3};

Пункт 10: var нельзя использовать при объявлении нескольких переменных в одной строке


Если вам нравится объявлять переменные одного типа разом, то вам нужно знать что var не подходит для этого. Следующий код не скомпилируется:


// IT DOESN'T COMPILE
// error: 'var' is not allowed in a compound declaration
var hello = "hello", bye = "bye", welcome = "welcome";

Вместо этого используйте:


// PREFER
String hello = "hello", bye = "bye", welcome = "welcome";

Или это:


// PREFER
var hello = "hello";
var bye = "bye";
var welcome = "welcome";

Пункт 11: локальные переменные должны стремиться к минимизации своей области видимости. Тип var усиливает это утверждение


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


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


Избегайте этого:


// AVOID
...
var stack = new Stack<String>();
stack.push("George");
stack.push("Tyllen");
stack.push("Martin");
stack.push("Kelly");
...
// 50 lines of code that doesn't use stack
// George, Tyllen, Martin, Kelly  
stack.forEach(...);
...

Обратите внимание, что мы вызываем метод forEach(), который унаследован от java.util.Vector. Этот метод будет проходить по стеку, как любой другой вектор и это то, что нам нужно. Но теперь мы решили использовать вместо StackArrayDeque. Когда мы сделаем это, метод forEach() получит реализацию от ArrayDeque, который будет проходить по стеку, как стандартный стек (LIFO)


// AVOID
...
var stack = new ArrayDeque<String>();
stack.push("George");
stack.push("Tyllen");
stack.push("Martin");
stack.push("Kelly");
...
// 50 lines of code that doesn't use stack
// Kelly, Martin, Tyllen, George
stack.forEach(...);
...

Это не то что мы хотим. Тут слишком сложно отследить ошибку, поскольку код, содержащий часть forEach(), не находиться рядом с кодом, в который были внесены изменения. Чтобы увеличить скорость поиска и исправления ошибок, гораздо лучше писать код, использующий переменную stack, как можно ближе к объявлению этой переменной.


Лучше всего это делать так:


// PREFER
...
var stack = new Stack<String>();
stack.push("George");
stack.push("Tyllen");
stack.push("Martin");
stack.push("Kelly");
...
// George, Tyllen, Martin, Kelly  
stack.forEach(...);
...
// 50 lines of code that doesn't use stack

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


Пункт 12: тип var упрощает использование различных типов в тернарных операторах


Мы можем использовать разные типы операндов в правой части тернарного оператора.


При явном указании типов следующий код не скомпилируется:


// IT DOESN'T COMPILE
List code = containsDuplicates ? List.of(12, 1, 12) : Set.of(12, 1, 10);
// or
Set code = containsDuplicates ? List.of(12, 1, 12) : Set.of(12, 1, 10);

Тем не менее мы можем поступить так:


Collection code = containsDuplicates ? List.of(12, 1, 12) : Set.of(12, 1, 10);
Object code = containsDuplicates ? List.of(12, 1, 12) : Set.of(12, 1, 10);

Код ниже, также не скомпилируется:


// IT DOESN'T COMPILE
int code = intOrString ? 12112 : "12112";
String code = intOrString ? 12112 : "12112";

Но можно использовать более общие типы:


Serializable code = intOrString ? 12112 : "12112";
Object code = intOrString ? 12112 : "12112";

Во всех таких случаях лучше предпочесть var:


// PREFER
// inferred as Collection<Integer>
var code = containsDuplicates ? List.of(12, 1, 12) : Set.of(12, 1, 10);
// inferred as Serializable
var code = intOrString ? 12112 : "12112";

Из этих примеров не следует, что тип var определяет типы объектов во время выполнения. Это не так!


И, конечно, тип var будет корректно работать при одинаковых типах обоих операндов:


// inferred as float
var code = oneOrTwoDigits ? 1211.2f : 1211.25f;

Пункт 13: тип var может быть использован внутри циклов


Мы легко можем заменить явное объявление типов в циклах for на тип var.


Изменение явного типа int на var:


// explicit type
for (int i = 0; i < 5; i++) {
     ...
}
// using var
for (var i = 0; i < 5; i++) { // i is inferred of type int
     ...
}

Изменение явного типа Order на var:


List<Order> orderList = ...;
// explicit type
for (Order order : orderList) {
    ...
}
// using var
for (var order : orderList) { // order type is inferred as Order
    ...
}

Пункт 14: var отлично работает с потоками (stream) в Java 8


Очень просто использовать var из Java 10 с потоками (stream), которые появились в Java 8.


Вы можете просто заменить явное объявление типа Stream на var:


Пример 1:


// explicit type
Stream<Integer> numbers = Stream.of(1, 2, 3, 4, 5);                
numbers.filter(t -> t % 2 == 0).forEach(System.out::println);
// using var
var numbers = Stream.of(1, 2, 3, 4, 5); // inferred as Stream<Integer>               
numbers.filter(t -> t % 2 == 0).forEach(System.out::println);

Пример 2:



// explicit types
Stream<String> paths = Files.lines(Path.of("..."));
List<File> files = paths.map(p -> new File(p)).collect(toList());
// using var
var paths = Files.lines(Path.of("...")); // inferred as Stream<String>
var files = paths.map(p -> new File(p)).collect(toList()); // inferred as List<File>

Пункт 15: var можно использовать при объявлении локальных переменных, предназначенных для разбиения больших цепочек выражений на части


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


Пример большого выражения:


List<Integer> intList = List.of(1, 1, 2, 3, 4, 4, 6, 2, 1, 5, 4, 5);
// AVOID
int result = intList.stream()
    .collect(Collectors.partitioningBy(i -> i % 2 == 0))
    .values()
    .stream()
    .max(Comparator.comparing(List::size))
    .orElse(Collections.emptyList())
    .stream()
    .mapToInt(Integer::intValue)
    .sum();

Лучше разбейте код на составные части:


List<Integer> intList = List.of(1, 1, 2, 3, 4, 4, 6, 2, 1, 5, 4, 5);
// PREFER
Map<Boolean, List<Integer>> evenAndOdd = intList.stream()
    .collect(Collectors.partitioningBy(i -> i % 2 == 0));
Optional<List<Integer>> evenOrOdd = evenAndOdd.values()
    .stream()
    .max(Comparator.comparing(List::size));
int sumEvenOrOdd = evenOrOdd.orElse(Collections.emptyList())
    .stream()
    .mapToInt(Integer::intValue)
    .sum();

Второй вариант кода выглядит более читабельнее и проще, но первый вариант также имеет право на существование. Для нашего разума абсолютно нормально адаптироваться к пониманию таких больших выражений и предпочесть их локальным переменным. Тем не менее, использование типа var может помочь при разбиении больших конструкций за счет сокращения усилий на объявление локальных переменных:


var intList = List.of(1, 1, 2, 3, 4, 4, 6, 2, 1, 5, 4, 5);
// PREFER
var evenAndOdd = intList.stream()
    .collect(Collectors.partitioningBy(i -> i % 2 == 0));
var evenOrOdd = evenAndOdd.values()
    .stream()
    .max(Comparator.comparing(List::size));
var sumEvenOrOdd = evenOrOdd.orElse(Collections.emptyList())
    .stream()
    .mapToInt(Integer::intValue)
    .sum();

Пункт 16: var не может быть использован, как тип возвращаемого значения или как тип аргумента метода


Показанные ниже два фрагмента кода не скомпилируются.


Использование var, как тип возвращаемого значения:


// IT DOESN'T COMPILE
public var countItems(Order order, long timestamp) {
    ...        
}

Использование var, как тип аргумента метода:


// IT DOESN'T COMPILE
public int countItems(var order, var timestamp) {
    ...  
}

Пункт 17: локальные переменные типа var могут быть переданы, как параметры метода или могут принимать возвращаемое методом значение


Приведенные ниже фрагменты кода скомпилируются и будут исправно работать:


public int countItems(Order order, long timestamp) {
    ...
}
public boolean checkOrder() {
    var order = ...;     // an Order instance
    var timestamp = ...; // a long representing a timestamp
    var itemsNr = countItems(order, timestamp); // inferred as int type
    ...
}

с дженериками все так же будет работать отлично:


public <A, B> B contains(A container, B tocontain) {
    ...
}
var order = ...;   // Order instance
var product = ...; // Product instance
var resultProduct = contains(order, product); // inferred as Product type

Пункт 18: переменные var могут быть использованы с анонимными классами


Вместо явного указания типов:


public interface Weighter {
    int getWeight(Product product);
}
// AVOID
Weighter weighter = new Weighter() {
    @Override
    public int getWeight(Product product) {
        ...
    }
};
Product product = ...; // a Product instance
int weight = weighter.getWeight(product);

Используйте var:


public interface Weighter {
    int getWeight(Product product);
}
// PREFER
var weighter = new Weighter() {
    @Override
    public int getWeight(Product product) {
        ...
    }
};
var product = ...; // a Product instance
var weight = weighter.getWeight(product);

Пункт 19: переменные типа var могут использоваться в качестве effectively final переменных


Вспомним, что:


… начиная с Java SE 8, локальный класс может обращаться к локальным переменным и параметрам заключающего блока, которые являются final или effectively final. Переменная или параметр, значение которых никогда не изменяется после их инициализации, являются effectively final.

Что ж, переменные типа var могут быть effectively final. Это можно увидеть в следующем примере.


Избегайте:


public interface Weighter {
    int getWeight(Product product);
}
// AVOID
int ratio = 5; // this is effectively final
Weighter weighter = new Weighter() {
    @Override
    public int getWeight(Product product) {
        return ratio * ...;
    }
};
ratio = 3; // this reassignment will cause error

Используйте:


public interface Weighter {
    int getWeight(Product product);
}
// PREFER
var ratio = 5; // this is effectively final
var weighter = new Weighter() {
    @Override
    public int getWeight(Product product) {
        return ratio * ...;
    }
};
ratio = 3; // this reassignment will cause error

Пункт 20: var-переменные могут быть final-переменными


Изначально значение var переменной может быть изменено (за исключением, когда она объявлена как effectively final). Но мы можем объявить переменную, как final.


Избегайте:


// AVOID
// IT DOESN'T COMPILE
public void discount(int price) {
    final int limit = 2000;
    final int discount = 5;
    if (price > limit) {
        discount++; // this reassignment will cause error, which is ok
    }
}

Предпочитайте:


// PREFER
// IT DOESN'T COMPILE
public void discount(int price) {
    final var limit = 2000;
    final var discount = 5;
    if (price > limit) {
        discount++; // this reassignment will cause error, which is ok
    }
}

Пункт 21: лямбда выражениям и ссылкам на методы нужны явные типы


Тип var не может использоваться, если невозможно определить конечные типы. Таким образом инициализация через лямбда выражения и ссылки на методы, с использованием var, не скомпилируется:


// IT DOESN'T COMPILE
// lambda expression needs an explicit target-type
var f = x -> x + 1;
// method reference needs an explicit target-type
var exception = IllegalArgumentException::new;

Вместо этого используйте:


// PREFER
Function<Integer, Integer> f = x -> x + 1;
Supplier<IllegalArgumentException> exception = IllegalArgumentException::new;

Но в Java 11 разрешено использовать var-переменные в контексте лямбда выражений. Следующий пример кода заработает в Java 11:


// Java 11
(var x, var y) -> x + y
// or 
(@Nonnull var x, @Nonnull var y) -> x + y

Пункт 22: инициализировать var null'ем запрещено


Запрещено объявлять var-переменные без инициализации.


Этот код не скомпилируется (попытка присвоить null):


// IT DOESN'T COMPILE
var message = null; // result in an error of type: variable initializer is 'null'

И этот тоже не скомпилируется (отсутствует инициализатор):


// IT DOESN'T COMPILE
var message; // result in: cannot use 'var' on variable without initializer
...
message = "hello";

А этот код скомпилируется и будет исправно работать:


// PREFER
String message = null;
// or
String message;
...
message = "hello";

Пункт 23: тип var нельзя использовать в полях класса


Вы можете использовать var для локальных переменных, но не в качестве полей классов.


Это ограничение приведет к ошибкам компиляции:


// IT DOESN'T COMPILE
public class Product {
    private var price; // error: 'var' is not allowed here
    private var name;  // error: 'var' is not allowed here
    ...
}

Используйте такой способ:


// PREFER
public class Product {
    private int price; 
    private String name;
    ...
}

Пункт 24: var нельзя использовать в блоке catch


Тем не менее, это разрешено в try-with-resources


Блок catch


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


Следующий код вызовет ошибку компиляции:


// IT DOESN'T COMPILE
try {
    TimeUnit.NANOSECONDS.sleep(5000);
} catch (var ex) {
    ...
}

В таком случае необходимо использовать явный тип исключения:


// PREFER
try {
    TimeUnit.NANOSECONDS.sleep(5000);
} catch (InterruptedException ex) {
    ...
}

Try-with-resources


Однако, var отлично работает в блоке try-with-resources.


Например, этот код:


// explicit type
try (PrintWriter writer = new PrintWriter(new File("welcome.txt"))) {
    writer.println("Welcome message");
}

Можно заменить кодом с var:


// using var
try (var writer = new PrintWriter(new File("welcome.txt"))) {
    writer.println("Welcome message");
}

Пункт 25: тип var можно использовать с дженериками


Например, у нас есть следующий код:


public <T extends Number> T add(T t) {
     T temp = t;
     ...
     return temp;   
}

В этом случае, использование var работает как и ожидалось, так что мы просто можем заменить T на var:


public <T extends Number> T add(T t) {
     var temp = t;
     ...
     return temp;   
}

Давайте взглянем на другой пример, где мы можем успешно использовать var:


codepublic <T extends Number> T add(T t) {
     List<T> numbers = new ArrayList<>();
     numbers.add((T) Integer.valueOf(3));
     numbers.add((T) Double.valueOf(3.9));
     numbers.add(t);
     numbers.add("5"); // error: incompatible types: String cannot be converted to T
     ...     
}

Тут можно безопасно заменить List<T> на var:


public <T extends Number> T add(T t) {
     var numbers = new ArrayList<T>();
     // DON'T DO THIS, DON'T FORGET THE, T
     var numbers = new ArrayList<>();
     numbers.add((T) Integer.valueOf(3));
     numbers.add((T) Double.valueOf(3.9));
     numbers.add(t);
     numbers.add("5"); // error: incompatible types: String cannot be converted to T
     ...     
}

Пункт 26: будьте внимательны с типом var при использовании Wildcards (?), ковариантов и контрвариантов


Использование? Wildcards


Можно безопасно использовать var таким образом:


// explicit type
Class<?> clazz = Integer.class;
// use var
var clazz = Integer.class;

Но не заменяйте Foo<?> на var только потому, что вы имеете ошибки в коде, а с использованием var они чудесным образом исчезают.


Давайте рассмотрим следующий пример кода, он не захватывающий, но, думаю, основную идею вы поймете. Исходя из первой строки можно сделать предположение, что вы пытались определить ArrayList из строк, а в итоге получили Collection<?>:


// explicit type
Collection<?> stuff = new ArrayList<>();
stuff.add("hello"); // compile time error
stuff.add("world"); // compile time error
// use var, this will remove the error, but I don't think that this is
// what you had in mind when you wrote the above code
var stuff = new ArrayList<>();
strings.add("hello"); // no error
strings.add("world"); // no error

Использование ковариантов (Foo <? extends T>) и контрвариантов (Foo <? super T>)


Мы знаем, что можно сделать следующее:


// explicit type
Class<? extends Number> intNumber = Integer.class;
Class<? super FilterReader> fileReader = Reader.class;

Если мы ошибочно присвоим неверный тип и получим ошибки во время компиляции, это будет именно то, чего мы ожидаем:


// IT DOESN'T COMPILE
// error: Class<Reader> cannot be converted to Class<? extends Number>
Class<? extends Number> intNumber = Reader.class;
// error: Class<Integer> cannot be converted to Class<? super FilterReader>
Class<? super FilterReader> fileReader = Integer.class;

Но при использовании var:


// using var
var intNumber = Integer.class;
var fileReader = Reader.class;

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


// this will compile just fine
var intNumber = Reader.class;
var fileReader = Integer.class;

Заключение


В этой статье мы рассмотрели тип «var», который появился в Java 10. Также разобрали множество примеров, которые демонстрируют преимущества и недостатки при использовании динамического выведения типа переменных. И наконец узнали, что проверка типов при применении var осуществляется во время компиляции, что позволяет отлавливать множество ошибок.


Используйте var и да прибудет с вами Java!

Поделиться публикацией

Комментарии 26

    +2
    Перевод заглавия статьи неудачный. Это не список рекомендаций.
      0
      А как бы вы перевели заголовок? Заранее спасибо за ответ.
        +2
        Расчлененка: 26 кусков var и Тайная Комната
          +1
          Мы решили, что такой громкий и не информативный заголовок лучше не использовать
      +1
      Пункт 15 какой-то сомнительный, цепочка читается и понимается куда легче, чем разрозненные части.
        0
        Автор потом резюмирует
        Второй вариант кода выглядит более читабельнее и проще, но первый вариант также имеет право на существование.
          +1
          Так его резюмирование и сомнительно — для меня второй вариант выглядит менее читабельно. Поспрашивал окружающих, так они со мной согласны.
        +2
        В чём сакральный смысл замены int на var?
          +2
          Модно, стильно, молодёжно!
            0
            Ну, можно при чтении кода вскипятить мозги пытаясь понять, что же там на самом деле из функции-то возвращается…
              0
              Так тут применяем п.5 и наша SomeAwesomeFunction становится intSomeAwesomeFunction и никаких проблем.
                0
                С примитивными типами сработает, а с чем-то кастомным уже нет. Квик-док уже не вызовешь, не почитаешь. Сплошное неудобство.
                0
                Придется инфу о возвращаемом типе отражать в имени переменной. Это автор и говорит в статье
                … В большинстве случаев это происходит потому, что мы склонны смотреть на тип переменной, как на первичную информацию, а на ее имя, как на вторичную. Хотя должно быть как раз наоборот.
                  0
                  Автор исходит из ошибочного предположения, что информация о типе ограничена его написанием, а это не всегда так. Если String можно безболезненно сократить до str..., то с типом, например, Employee так сделать уже не получится. Сотрудник, это понятно, а конкретно что это значит? Как используется? Какие нюансы? Какой интерфейс реализуется? Контрал+клик, или вызов квикдока на этот и другие вопросы ответит влет, а вот имея перед глазами var employee = getEmployee(accountName) уже ничем не поможет. Придется лезть в тело вызываемой фанкции, смотреть, что конкретно она возвращает, и так далее.

                    0
                    var employee = getEmployee(accountName)
                    если следовать логике, то тут возвращается объект типа Employe, т.к. эта информация зашита в двух местах: в названии и в геттере

                    Если нужны нюансы, то автор предлагает максимум информации помещать в название переменной/метода. Это кстати не противоречит рекомендациям из книги Clean Code — давайте именам переменных/класса/метода говорящие имена, чтобы код был самодокументируемым.
                      0
                      если следовать логике, то тут возвращается объект типа Employe, т.к. эта информация зашита в двух местах: в названии и в геттере

                      Это-то понятно. Не понятно что такое этот Employe, из какого он пакета, что умеет и как с ним жить. Эту информацию невозможно поместить в имя переменной, ее можно получить только из документации. А var лишает нас возможности до нее быстро добраться.
                        0
                        Я думаю, что этот вопрос должен как-то решаться на уровне сред разработки. Ничего не мешает ide автоматом, во время написания кода, уже определять тип переменной employee и давать доступ к соответствующей доке. Поменялся тип, значит и должна автоматом поменяться дока
                      0
                      честно — для меня не понятна претензия.
                      Я программирую на: JS, TS, GoLang, Python, Ruby, Java (последние 2 давно и мало) — только в TS(опционально) и в GoLang есть статические типы и объявление с типом и без него(на последних Java не работал, где появился var).
                      Код очень хорошо читается и нет никаких проблем с поиском описания функций. Почему так сильно бомбят с этого в Java мире? вон котлин при своем старте имел это как одна из главных фич и (вроде как) ему за это аплодировали
                  0
                  Для примитивных типов использовать тип var, наверно, особо смысла нет. А в статье это разбирается, как некая возможность, которой можно пользоваться. Из статьи можно сделать вывод, что суть var — это синтаксический сахар над типами, который позволяет писать меньше кода. А это значит, что при использовании var инфу о типе придется переносить в название переменной, что, наверно, не всем будет по душе.
                  +3
                  Пункт 0: не используйте var вообще…
                    0
                    ППКС! В lombok такую возможность завезли уже очень дано, поигрался я с ней, и понял, что проблем больше, чем профита.
                      0
                      Иногда может быть полезен, когда имя класса бешеное по длине или статический класс 10 уровней вложенности. Например вместо

                      ПетяПетяПетушокЗолотойГребешокКоКоКоКукареку петя = new ПетяПетяПетушокЗолотойГребешокКоКоКоКукареку();

                      можно написать

                      var петя = new ПетяПетяПетушокЗолотойГребешокКоКоКоКукареку();
                      +1
                      Вроде как этот идентификатор призван упрощать\ускорять, но тут появляется список из рекомендаций типа «как делать не стоит ибо чревато ...». Плюс с этой штукой в эпоху продвинутых IDE приходится нажимать больше клавиш на написание одной строки выражения. Например возьмем две строки:
                      DocumentationTool dtl = ToolProvider.getSystemDocumentationTool();
                      var documentationTool = ToolProvider.getSystemDocumentationTool();
                      

                      При наборе первых двух-трех символов IntelliSense в любой нормальной IDE предложит нужный тип и нам останется нажать только пробел, и следующим пробелом выбрать название переменной которое в 99% будет documentationTool, дальше зная тип технология с большей релевантностью подскажет правую часть выражения.
                      Для того чтобы получилась вторая строка, нам нужно полностью напечатать var + имя переменной и плюс ко всему IntelliSense будет работать на половину своих возможностей, т.е. когда мы будем печатать правую часть выражения — он просто не даст нам ошибиться в названии класса и метода, т.е. без какой либо фильтрации.
                      Кстати, такие же проблемы есть в kotlin и swift или может я в чем-то не прав и это действительно нужно?
                        +3
                        В нормальной IDE я сперва набираю правую часть с помощью пары символов и IntelliSense, а затем жму ctrl+shift+v, чтоб сгенерировалась левая часть с выбором имени переменной.
                          0
                          тоже подумал о таком юзкейсе, но ни когда так не пробовал делать. Надо попробовать)
                            0
                            в idea Это делается через ctrl + alt + v

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое