Улучшения языка Java, которые вам следует знать

Последнее обновление 30.03.2021, включающее изменения до JDK 16.

Когда в Java 8 были представлены Streams и Lambdas, это было большим изменением, позволившим использовать функциональный стиль программирования с гораздо меньшим количеством шаблонного кода.

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

Усовершенствования языка после Java 8

Java 16

  • Классы записей (Record)

  • Сопоставление с образцом для instanceof

Java 15

  • Текстовые блоки

  • Полезные исключения NullPointerExceptions

Java 14

  • Switch выражения

Java 11

  • Вывод типа локальной переменной

Java 9

  • Разрешены private методы в интерфейсах

  • Оператор Diamond для анонимных внутренних классов

  • Разрешено использовать final переменные в качестве ресурсов в операторах try-with-resources

  • Подчеркивание больше не является допустимым в имени идентификатора

  • Улучшенные предупреждения

Что дальше: Превью функции в Java 16

  • Запечатанные (Sealed) классы

Детальный обзор всех JEP, формирующих новую платформу, включая API, улучшения производительности и безопасности, представлен в тщательно сформированном списке новых функций языка начиная с Java 8.

Классы записей (Record)

Доступно с: JDK 16 (предварительная версия в JDK 14 JDK 15 )

Классы записей вводят в язык декларации нового типа для определения неизменяемых классов данных. Вместо обычной церемонии с private полями, геттерами и конструкторами это позволяет нам использовать компактный синтаксис:

public record Point(int x, int y) { }

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

  • два private final поля, int x и int y

  • конструктор, который принимает x и y в качестве параметров

  • методы x() и y(), которые работают как геттеры для полей

  • hashCodeequals и toString, каждый из которых принимает в качестве параметров x и y

Их можно использовать так же, как обычные классы:

var point = new Point(1, 2);
point.x(); // возвращает 1
point.y(); // возвращает 2

Класс записи предназначен для прозрачного хранения своих неизменяемых данных. Для реализации этой концепции у класса записи ряд ограничений.

Поля класса записи являются не только final по умолчанию, но также невозможно в классе записи иметь какие-либо поля, не являющиеся final.

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

Наконец, классы записи не могут расширять другие классы, они не могут объявлять собственные методы, они неявно являются final и не могут быть abstract.

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

public record Point(int x, int y) {
  public Point {
    if (x < 0) {
      throw new IllegalArgumentException("x не может быть отрицательным");
    }
    if (y < 0) {
      y = 0;
    }
  }
}

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

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

public record Point(int x, int y) {
  public Point(int x) {
    this(x, 0);
  }
}

Получить данные из записи можно с помощью ее методов доступа. Для каждого поля x классы записи имеют сгенерированный общедоступный метод получения в форме x().

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

public record Point(int x, int y) {
  static Point zero() {
    return new Point(0, 0);
  }
  
  boolean isZero() {
    return x == 0 && y == 0;
  }
}

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

Благодаря этой особой конструкции сериализация для записей намного проще и безопаснее, чем для обычных классов. Как написано в JEP:

Экземпляры классов записей можно сериализовать и десериализовать. Однако процесс нельзя настроить, предоставив методы writeObject, readObject, readObjectNoData, writeExternal или readExternal. Компоненты класса записи управляют сериализацией, в то время как канонический конструктор класса записи управляет десериализацией.

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

С точки зрения пользователя включение и использование сериализации может быть выполнено как обычно:

public record Point(int x, int y) implements Serializable { }

public static void recordSerializationExample() throws Exception {
  Point point = new Point(1, 2);

  // Serialize
  var oos = new ObjectOutputStream(new FileOutputStream("tmp"));
  oos.writeObject(point);

  // Deserialize
  var ois = new ObjectInputStream(new FileInputStream("tmp"));
  Point deserialized = (Point) ois.readObject();
}

Обратите внимание, что больше не требуется определять a serialVersionUID, поскольку требование сопоставления serialVersionUIDзначений отменяется для классов записи.

Ресурсы:

? ?? Совет: используйте локальные записи для моделирования промежуточных преобразований.

Сложные преобразования данных требуют от нас моделирования промежуточных значений. До Java 16 типичным решением было полагаться на Pair или аналогичные библиотечные классы-хранители или определять свой собственный (возможно, внутренний статический) класс для хранения этих данных.

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

В Java 16 это улучшено и теперь также можно определять локальные записи в теле метода:

public List<Product> findProductsWithMostSaving(List<Product> products) {
  record ProductWithSaving(Product product, double savingInEur) {}

  products.stream()
    .map(p -> new ProductWithSaving(p, p.basePriceInEur * p.discountPercentage))
    .sorted((p1, p2) -> Double.compare(p2.savingInEur, p1.savingInEur))
    .map(ProductWithSaving::product)
    .limit(5)
    .collect(Collectors.toList());
}

Компактный синтаксис классов записи хорошо сочетается с компактным синтаксисом Streams API.

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

? ??Совет: проверьте свои библиотеки

Классы записи не придерживаются соглашений JavaBeans:

  • У них нет конструктора по умолчанию.

  • У них нет set методов.

  • Методы доступа не соответствуют форме getX().

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

Одним из таких случаев является то, что записи нельзя использовать как объекты JPA (например, Hibernate). В списке рассылки jpa-dev обсуждается согласование спецификации с записями Java, но пока я не нашел новостей о состоянии процесса разработки. Однако стоит отметить, что Records можно использовать для проекций без проблем.

Большинство других инструментов, которые я проверил (включая JacksonApache Commons LangJSON-P и Guava ), поддерживают записи, но, поскольку он довольно новый, есть и некоторые острые углы. Например, Jackson, популярная библиотека JSON, была одной из первых, кто начал использовать записи. Большинство его функций, включая сериализацию и десериализацию, одинаково хорошо работают для классов записи и JavaBeans, но некоторые функции для управления объектами еще предстоит адаптировать.

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

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

Посмотрите мои эксперименты с интеграцией инструментов для классов записи на GitHub.

Сопоставление с образцом для instanceof

Доступно с: JDK 16 (предварительная версия в JDK 14 JDK 15 )

В большинстве случаев за instanceofобычно следует приведение типа:

if (obj instanceof String) {
    String s = (String) obj;
    // use s
}

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

if (obj instanceof String s) {
    // use s
}

Шаблон представляет собой комбинацию теста ( obj instanceof String) и переменной шаблона ( s).

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

// "старый" instanceof без переменной шаблона: 
// компилируется с условием, которое всегда истинно
Integer i = 1;
if (i instanceof Object) { ... } // работает

// "новый" instanceof, с переменной шаблона:
// дает ошибку компиляции в этом случае
if (i instanceof Object o) { ... } // ошибка

Обратите внимание, что противоположный случай, когда сопоставление с образцом всегда дает false, уже является ошибкой времени компиляции даже для старого instanceof.

Переменная шаблона создается, только если тест успешен. Она работает почти как обычная не final переменная:

  • она может быть изменена

  • она может переопределять объявления полей

  • если есть локальная переменная с тем же именем, это приведет к ошибке компиляции

Однако к таким переменным применяются особые правила области действия: переменная шаблона находится в области действия, где она точно совпала, что определяется анализом области действия потока выполнения.

Самый простой случай - это то, что можно увидеть в приведенном выше примере: если тест проходит, переменную s можно использовать внутри блока if.

Но правило «точно совпадают» также применимо и к частям более сложных условий:

if (obj instanceof String s && s.length() > 5) {
  // use s
}

s может использоваться во второй части условия, потому что она оценивается только тогда, когда первая выполняется успешно и instanceofоператор имеет совпадение.

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

private static int getLength(Object obj) {
  if (!(obj instanceof String s)) {
    throw new IllegalArgumentException();
  }

  // s находится в области видимости - если instanceof не совпадет 
  // выполнение не достигнет этого оператора
  return s.length();
}

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

private static int getDoubleLength(String s) {
  int a; // 'a' declared but unassigned
  if (s == null) {
    return 0; // return early
  } else {
    a = s.length(); // assign 'a'
  }

  // 'a' is definitely assigned
  // so we can use it
  a = a * 2;
  return a;
}

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

Например, в Kotlin вам не нужно определять переменную шаблона:

if (obj is String) {
    print(obj.length)
}

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

? Совет: следите за обновлениями

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

JEP 405 предлагает добавить функции декомпозиции, чтобы также соответствовать содержимому класса записи или массива:

if (o instanceof Point(int x, int y)) {
  System.out.println(x + y);
}

if (o instanceof String[] { String s1, String s2, ... }){
  System.out.println("Первые два элемента этого массива: " + s1 + ", " + s2);

Кроме того, JEP 406 посвящен добавлению функций сопоставления с образцом для операторов и switch выражений:

return switch (o) {
  case Integer i -> String.format("int %d", i);
  case Long l    -> String.format("long %d", l);
  case Double d  -> String.format("double %f", d);
  case String s  -> String.format("String %s", s);
  default        -> o.toString();
};

В настоящее время оба JEP находятся в статусе кандидата и не имеют конкретной целевой версии, но я надеюсь, что мы скоро увидим их предварительные версии.

Текстовые блоки

Доступно с: JDK 15 (предварительная версия в JDK 13 JDK 14 )

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

String html = "";
html += "<html>\n";
html += "  <body>\n";
html += "    <p>Hello, world</p>\n";
html += "  </body>\n";
html += "</html>\n";

System.out.println(html);

Чтобы сделать это более удобным для программистов, в Java 15 были введены многострочные строковые литералы, называемые текстовыми блоками:

String html = """
          <html>
            <body>
              <p>Hello, world</p>
            </body>
          </html>
          """;

System.out.println(html);

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

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

Их можно использовать везде, где можно использовать старый строковый литерал, и они оба создают похожие строковые объекты.

Для каждого разрыва строки в исходном коде в результате будет символ \n.

String twoLines = """
          Hello
          World
          """;

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

String singleLine = """
          Hello           World
          """;

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

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

String noIndentation = """
          First line
          Second line
          """;

String indentedByToSpaces = """
          First line 
          Second line
        """;

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

String indentedByToSpaces = """
         First line 
         Second line
       """;

String indentedByToSpaces = """
                              First line 
                              Second line
                            """;

Класс String также предоставляет некоторые программные способы обращения с отступом. Метод indentпринимает целое число и возвращает новую строку с заданными уровнями дополнительного отступа, а stripIndent возвращает содержимое исходной строки без всех несущественных отступов.

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

var greeting = """
    hello
    %s
    """.formatted("world");

Ресурсы:

? Совет: сохраняйте конечные пробелы

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

В этом случае помните о них, и если строка заканчивается пробелом, добавьте \sили \t вместо последнего пробела или табуляции в конец строки.

? Совет: создавайте правильные символы новой строки для Windows

Окончания строк представлены разными управляющими символами в Unix и Windows. В первом случае используется одинарный перевод строки ( \n), а во втором - возврат каретки, за которым следует перевод строки ( \r\n).

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

Files.writeString(Paths.get("<PATH_TO_FILE>"), """
    first line
    second line
    """);

Если для открытия такого файла используется инструмент, совместимый только с форматом окончания строки Windows (например, Блокнот), он будет отображать только одну строку. Убедитесь, что вы используете правильные управляющие символы, если вы также ориентируетесь на Windows, например, путем вызова String::replace для замены каждого "\n" на "\r\n".

? Совет: обратите внимание на однотипный отступ

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

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

Полезные исключения NullPointerExceptions

Доступно с: JDK 15 (Включено -XX:+ShowCodeDetailsInExceptionMessagesв JDK 14 )

Эта маленькая жемчужина - не совсем языковая функция, но она настолько хороша, что я захотел включить ее в этот список.

Традиционно исключение NullPointerException дает такой вывод:

node.getElementsByTagName("name").item(0).getChildNodes().item(0).getNodeValue();

Exception in thread "main" java.lang.NullPointerException
        at Unlucky.method(Unlucky.java:83)

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

Начиная с Java 15, в этом нет необходимости, потому что NPE описывает, какая часть в операторе имеет значение null. (Кроме того, в Java 14 вы можете включить его с помощью -XX:+ShowCodeDetailsInExceptionMessagesфлага.)

Exception in thread "main" java.lang.NullPointerException:
  Cannot invoke "org.w3c.dom.Node.getChildNodes()" because
  the return value of "org.w3c.dom.NodeList.item(int)" is null
        at Unlucky.method(Unlucky.java:83)

Посмотрите пример проекта на GitHub )

Подробное сообщение содержит действие, которое не удалось выполнить (невозможно вызвать getChildNodes), и причину сбоя (item(int)есть null), что значительно упрощает поиск точного источника проблемы.

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

Расширение Helpful NullPointerExceptions реализовано в JVM, поэтому вы получаете те же преимущества для кода, скомпилированного с более старыми версиями Java, и при использовании других языков JVM, таких как Scala или Kotlin.

Обратите внимание, что не все NPE получают эту дополнительную информацию, а только те, которые создаются и генерируются JVM при:

  • чтении или записи null в поля

  • вызов null метода

  • доступ или присвоение элемента массива (индексы не являются частью сообщения об ошибке)

  • unboxing null

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

В настоящее время Helpful NullPointerExceptions отключены по умолчанию и должны быть включены с помощью -XX:+ShowCodeDetailsInExceptionMessagesфлага.

? Совет: проверьте свои инструменты

При обновлении до Java 15 обязательно проверьте свое приложение и инфраструктуру, чтобы убедиться:

  • имена конфиденциальных переменных не попадают в файлы журналов и ответы веб-сервера

  • инструменты анализа журнала могут обрабатывать новый формат сообщений

  • накладные расходы, необходимые для создания дополнительных деталей, приемлемы

Switch выражения

Доступно с: JDK 14 (предварительная версия в JDK 12 JDK 13 )

Старый добрый switch получил обновленную версию в Java 14. Хотя Java продолжает поддерживать старый оператор switch, он добавляет в язык новое switch выражение:

int numLetters = switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> 6;
    case TUESDAY                -> 7;
    default      -> {
        String s = day.toString();
        int result = s.length();
        yield result;
    }
};

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

int k = 3;
System.out.println(
    switch (k) {
        case  1 -> "one";
        case  2 -> "two";
        default -> "many";
    }
);

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

Во-первых, для switch выражений варианты (case) не выполняются все. Поэтому нет больше никаких мелких ошибок, вызванных отсутствием breakМожно указать несколько констант для каждого случая.

У каждого варианта своя область видимости.

String s = switch (k) {
    case  1 -> {
        String temp = "one";
        yield temp;
    }
    case  2 -> {
        String temp = "two";
        yield temp;
    }
    default -> "many";
}

Ветвь - это либо одно выражение, либо, если оно состоит из нескольких операторов, оно должно быть заключено в блок.

В-третьих, варианты switch выражения являются исчерпывающими. Это означает, что для String, примитивных типов и их оболочек defaultвсегда должен быть определен регистр.

int k = 3;
String s = switch (k) {
    case  1 -> "one";
    case  2 -> "two";
    default -> "many";
}

Для enums либо случай default должен присутствовать, или все варианты должны быть охвачены явно. Довольно приятно полагаться на последнее, чтобы убедиться, что все значения учтены. Добавление дополнительного значения к enum переменной приведет к ошибке компиляции для всех switch выражений, в которых она используется.

enum Day {
   MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

Day day = Day.TUESDAY;
switch (day) {
    case  MONDAY -> ":(";
    case  TUESDAY, WEDNESDAY, THURSDAY -> ":|";
    case  FRIDAY -> ":)";
    case  SATURDAY, SUNDAY -> ":D";
}

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

? Совет: используйте синтаксис стрелок

Switch выражение можно использовать не только с лямбда-подобными случаями формы стрелки. Старый оператор switch с его вариантами с двоеточиями также можно использовать как выражение, используя yield:

int result = switch (s) {
    case "foo":
    case "bar":
        yield 2;
    default:
        yield 3;
};

Эта версия также может использоваться как выражение, но она больше похожа на старый оператор switch, потому что

  • варианты выполняются все

  • варианты имеют одинаковую область видимости

Мой совет? Не используйте эту форму, вместо этого используйте switch выражения с синтаксисом стрелки, чтобы получить все преимущества.

Вывод типа локальной переменной

Доступно с: JDK 11 (без поддержки лямбда в JDK 10 )

Вероятно, наиболее значительным улучшением языка со времен Java 8 является добавление ключевого слова var. Первоначально оно было представлено в Java 10 и было дополнительно улучшено в Java 11.

Эта функция позволяет нам упростить объявления локальной переменной, опуская явную спецификацию типа:

var greetingMessage = "Hello!";

Хотя это похоже на ключевое слово var вJavascript, речь не идет о динамической типизации.

Прочитайте эту цитату из JEP:

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

Тип объявленных переменных определяется во время компиляции. В приведенном выше примере предполагаемый тип - String. Использование var вместо явного типа делает этот фрагмент кода менее избыточным и, следовательно, более удобным для чтения.

Вот еще один хороший кандидат для вывода типов:

MyAwesomeClass awesome = new MyAwesomeClass();

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

? Совет: помните о удобочитаемости

В первом случае удаление явной информации о типе из исходного кода делает его менее читаемым.

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

Вот небольшая загадка. Следующий фрагмент кода использует API даты и времени Java 8. Угадайте типы переменных в следующем фрагменте:

var date = LocalDate.parse("2019-08-13");
var dayOfWeek = date.getDayOfWeek();
var dayOfMonth = date.getDayOfMonth();

Сделали? Вот решение:

Первый довольно интуитивно понятен, parse метод возвращает LocalDateобъект. Однако в следующих двух вы должны быть немного лучше знакомы с API: dayOfWeekвозвращает a java.time.DayOfWeek, а dayOfMonthпросто возвращает int.

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

private void longerMethod() {
    // ...
    // ...
    // ...

    var dayOfWeek = date.getDayOfWeek();

    // ...
    // ...
    // ...
}

Бьюсь об заклад, основываясь на предыдущем примере, вы догадались, что это файл java.time.DayOfWeek. Но на этот раз это целое число, потому что date в этом примере используется время  Joda. Это другой API, который ведет себя немного иначе, но вы этого не видите, потому что это более длинный метод, и вы не прочитали все строки. (JavaDoc: Joda time / Java 8 Date / Time API )

Если бы явное объявление типа присутствовало, выяснить, какой у него тип dayOfWeek, было бы тривиально. Теперь, с var, читатель сначала должен узнать тип date переменной и проверить, что getDayOfWeek делает. Это просто с IDE, не так просто при визуальном сканировании кода.

? Совет: обратите внимание на сохранение важной информации о типе

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

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

Map<String, String> myMap = new HashMap<String, String>(); // До Java 7
Map<String, String> myMap = new HashMap<>(); // Используя Diamond оператор

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

var myMap = new HashMap<>();

Этот пример корректный, и в Java 11 он даже не выдает в компиляторе предупреждений об этом. Однако со всем этим выводом типов мы пришли к тому, что вообще не указали универсальные типы, и тип будет Map<Object, Object>.

Конечно, эта проблема может быть легко решена удалением Diamond оператора:

var myMap = new HashMap<String, String>();

Другой набор проблем может возникнуть при использовании var с примитивными типами данных:

byte   b = 1;
short  s = 1;
int    i = 1;
long   l = 1;
float  f = 1;
double d = 1;

Без явного объявления типа будет выведен тип всех этих переменных int. Используйте литералы типа (например, 1L) при работе с примитивными типами данных или не используйте var в этом случае вообще.

? Совет: обязательно прочтите официальные руководства по стилю программирования.

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

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

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

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

Ресурсы:

Разрешены private методы в интерфейсах

Доступно с: JDK 9 (Milling Project Coin )

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

Хотя это не так уж важно, но это логическое дополнение, которое позволяет привести код в порядок в методах по умолчанию.

Оператор Diamond для анонимных внутренних классов

Доступно с: JDK 9 (Milling Project Coin )

Java 7 представила Diamond оператор (<>) для уменьшения многословности, позволяя компилятору определять типы параметров для конструкторов:

List<Integer> numbers = new ArrayList<>();

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

В Java 9 эта небольшая шероховатость устранена, что делает оператор более универсальным:

List<Integer> numbers = new ArrayList<>() {
    // ...
}

Разрешено использовать final переменные в качестве ресурсов в операторах try-with-resources

Доступно с: JDK 9 (Milling Project Coin)

Еще одно усовершенствование, представленное в Java 7, - это расширение try-with-resources, которое освобождает разработчика от необходимости беспокоиться об освобождении ресурсов.

Чтобы проиллюстрировать его возможности, сначала рассмотрим усилия, предпринятые для правильного закрытия ресурса в этом типичном примере до Java 7:

BufferedReader br = new BufferedReader(...);
try {
    return br.readLine();
} finally {
    if (br != null) {
        br.close();
    }
}

С try-with-resourcesресурсы могут быть автоматически освобождены, с гораздо меньшим объемом кода:

try (BufferedReader br = new BufferedReader(...)) {
    return br.readLine();
}

Несмотря на свою мощь, try-with-resources в Java 9 было несколько недостатков.

Хотя эта конструкция может обрабатывать несколько ресурсов, она может легко усложнить чтение кода. Объявление таких переменных в списке после try ключевого слова немного нетрадиционно по сравнению с обычным кодом Java:

try (BufferedReader br1 = new BufferedReader(...);
    BufferedReader br2 = new BufferedReader(...)) {
    System.out.println(br1.readLine() + br2.readLine());
}

Кроме того, в версии Java 7, если у вас уже есть переменная, которую вы хотите обрабатывать с помощью этой конструкции, вам нужно было ввести фиктивную переменную. (Для примера см. JDK-8068948.)

Чтобы смягчить эту критику, try-with-resources была улучшена обработка final или фактически final локальных переменных в дополнение к вновь созданным:

BufferedReader br1 = new BufferedReader(...);
BufferedReader br2 = new BufferedReader(...);
try (br1; br2) {
    System.out.println(br1.readLine() + br2.readLine());
}

В этом примере инициализация переменных отделена от их регистрации в конструкции try-with-resources.

Совет: следите за освобожденными ресурсами

Следует иметь в виду одно предостережение: теперь можно ссылаться на переменные, которые уже освобождены, с помощью try-with-resources, что в большинстве случаев завершается ошибкой:

BufferedReader br = new BufferedReader(...);
try (br) {
    System.out.println(br.readLine());
}
br.readLine(); // Boom!

Подчеркивание больше не является допустимым именем идентификатора

Доступно с: JDK 9 (Milling Project Coin)

В Java 8 компилятор выдает предупреждение, когда "_" используется в качестве идентификатора. Java 9 пошла дальше, сделав единственный символ подчеркивания недопустимым в качестве идентификатора, сохранив за этим именем особую семантику в будущем:

int _ = 10; // Ошибка компиляции

Улучшенные предупреждения

Доступно с: JDK 9

Наконец, позвольте сказать несколько слов об изменениях, связанных с предупреждениями компилятора в новых версиях Java.

Теперь можно аннотировать частный метод, @SafeVarargs чтобы пометить Type safety: Potential heap pollution via varargs parameterпредупреждение как ложное срабатывание. (Фактически, это изменение является частью ранее обсуждавшегося JEP 213: Milling Project Coin ). Узнайте больше о VarargsGenerics и потенциальных проблемах, которые могут возникнуть при объединении этих функций в официальной документации.

Также, начиная с Java 9, компилятор не выдает предупреждения для операторов импорта при импорте устаревшего типа. Эти предупреждения были неинформативными и избыточными, поскольку при фактическом использовании устаревших элементов всегда отображается отдельное предупреждение.

Что дальше: превью функции в Java 16

В Java 16 есть превью функция языка, которую можно включить с помощью --enable-preview -source 16 флагов. Скорее всего, мы скоро увидим это как готовую функцию языка Java. Вот небольшой тизер.

Sealed (запечатанные) классы

JEP 360 улучшает добавленные в язык sealed классы и интерфейсы, которые можно использовать для ограничения того, какие другие классы или интерфейсы могут их расширять или реализовывать.

public abstract sealed class Shape
    permits Circle, Rectangle {...}

public class Circle extends Shape {...} // OK
public class Rectangle extends Shape {...} // OK
public class Triangle extends Shape {...} // Ошибка компиляции

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

double area = switch (shape) {
    case Circle c    -> Math.pow(c.radius(), 2) * Math.PI
    case Rectangle r -> r.a() * r.b()
};

Резюме

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