Java 15 глазами Scala-программиста | OTUS

Java 15 глазами Scala-программиста

Время летит и уже скоро нас ждет очередной релиз Java. Согласно полугодовому расписанию релизов, сейчас время для Java 15, который прокладывает путь к грядущей Java 17 LTS (через год).

В Java наблюдается постоянный поток улучшений, на многие из которых повлияли другие JVM-языки и идеи функционального программирования. Сюда входят такие фичи, как лямбды, ограниченный локальный вывод типов или switch-выражения. Scala — особенно богатый источник идей, благодаря инновационному сочетанию объектно-ориентированного и функционального программирования.

Давайте посмотрим, как фичи, доступные в Java 15, соотносятся с конструкциями, известными в Scala. Мы сосредоточимся на языковых фичах, пропуская улучшения JVM или очистку стандартной библиотеки. Также обратите внимание, что некоторые из описанных компонентов уже доступны в более ранних версиях Java.

1_b1vEl6J6TkeXOYwfc_5A6A_1-1801-80fa4a.jpeg

Records

Начнем с рекордов (records), которые доступны в качестве превью предстоящей финальной версии. Объем кода, который был необходим для создания простого класса данных, был легкой мишенью, когда речь шла о многословности Java.

При создании класса данных мы обычно пишем: • закрытые финальные поля, содержащие данные; • конструктор, устанавливающий поля для заданных значений; • аксессоры для получения данных; • equals, hashCode и toString.

class Person {
   private final String name;
   private final int age;

   Person(String name, int age) {
       this.name = name;
       this.age = get;
   }

   String name() { return name; }
   int age() { return age; }

   public boolean equals(Object o) {
       if (!(o instanceof Person)) return false;
       Person other = (Person) o;
       return other.name == name && other.age = age;
   }

   public int hashCode() {
       return Objects.hash(name, age);
   }

   public String toString() {
       return String.format("Person[name=%s, age=%d]", name, age);
   }
}

Существовали обходные пути, начиная от автоматического создания кода с помощью IDE до использования аннотаций и манипулирования байт-кодом во время выполнения (см. Проект Lombok). Но это всегда выглядело как воркэраунд, а не как правильный способ решить эту проблему. Что ж, больше нет!

С помощью рекордов приведенное выше сокращается до:

record Person(String name, int age) { }

Уменьшение кода в 21 раз! Байт-код, с которым оба они компилируются, будет похож. Экземпляры рекордов могут быть созданы так же, как и класс:

var john = new Person("john", 76);

В Scala есть очень похожая функциональность — case-классы. Приведенный выше пример будет записан как:

case class Person(name: String, age: Int)

В чем сходство между рекордами и кейс-классами?

  • методы equals, hashCode и toString генерируются автоматически (если явно не переопределены);
  • поля данных неизменяемы и общедоступны: private final поля + открытые методы доступа без параметров в Java и public val в Scala;
  • доступен конструктор со всеми полями данных;
  • методы могут быть определены в теле рекорда/кейс класса;
  • рекорды могут реализовывать интерфейсы, а кейс-классы могут реализовывать трейты (trait) (которые являются более мощным эквивалентом интерфейса Java в Scala);
  • все рекорды расширяют java.lang.Record, а все кейс классы реализуют scala.Product.

Однако есть и некоторые заметные отличия:

  1. Рекорды не могут иметь дополнительного состояния: private или public полей. Это означает, что рекорды не могут иметь вычисленное внутреннее состояние; все, что доступно, является частью основной сигнатуры рекорда. В Scala кейс-классы могут иметь private или public поля экземпляров, как и любой другой класс.
  2. Рекорды не могут расширять классы, поскольку они уже неявно расширяют java.lang.Record. В Scala кейс-классы могут расширять любой другой класс, поскольку они только неявно реализуют трейт (за одним исключением: кейс-класс не может расширять другой класс case).
  3. Рекорды всегда являются final (не могут быть расширены), в то время как кейс-классы могут (хотя это имеет ограниченную полезность).
  4. Конструкторы рекордов очень ограничены, поскольку они не могут иметь вычисленное состояние, их можно использовать в основном для проверки, например:
record Person(String name, int age) {
 Person {
   if (age < 0)
     throw new IllegalArgumentException("Too young");
 }
}

В Scala же конструкторы не ограничены.

Также важной особенностью кейс-классов Scala, которая отсутствует в рекордах, является метод копирования (copy). Он позволяет создать копию экземпляра (мы не можем изменять поля из-за неизменности) с некоторыми полями, установленными на новые значения. Копирование действительно является одной из самых полезных функций Scala и настолько широко распространено, что о нем легко забыть!

Подводя итог, можно сказать, что в Scala case действительно ведет себя как модификатор класса: почти все, что разрешено в обычном классе, также разрешено в кейс-классе; модификатор генерирует для нас несколько методов и полей. В Java, с другой стороны, рекорды представляют собой отдельный «тип вещей», который компилируется в класс, но имеет свои собственные ограничения и синтаксис (как и перечисления).

Sealed classes

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

Например, следующее определяет интерфейс Animal с закрытым набором реализаций:

public sealed interface Animal permits Cat, Dog, Elephant {...}

Затем реализация определяется как обычно:

public class Cat implements Animal { ... }
public class Dog implements Animal { ... }
public class Elephant implements Animal { ... }

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

Scala использует то же ключевое слово sealed и механизм очень похож. Однако ключевого слова permit нет. Предполагается, что реализации всегда должны находиться в том же исходном файле, что и базовый трейт/класс. Это менее гибко, чем Java, но классы Scala также имеют тенденцию быть короче, и несколько общедоступных классов могут быть определены в одном исходном файле (часто названном в честь базового трейта/класса):

sealed trait Animal
class Cat extends Animal { ... }
class Dog extends Animal { ... }
class Elephant extends Animal { ... }

(обратите внимание, что в Scala public является модификатором доступа по умолчанию, поэтому он здесь опущен, в отличие от Java package-private).

И в Java, и в Scala модификатор sealed хорошо работает с рекордами/кейс-классами. Используя эту комбинацию, мы получаем реализацию алгебраических типов данных, одного из основных инструментов функционального программирования. Рекорд/кейс класс — это тип произведения, а sealed интерфейс/трейт — это тип суммы.

Что насчет расширения реализаций sealed-типов? В Java у нас есть три возможности: • реализация может быть final, что означает невозможность дальнейшего расширения; • он может быть объявлен sealed сам, снова перечисляя возможные реализации с использованием разрешений; • или он может быть non-sealed, что делает эту конкретную реализацию открытой для расширения.

Любая реализация sealed-типа должна содержать ровно один из упомянутых выше модификаторов; однако каждая реализация может содержать свой модификатор. Например:

public sealed interface Animal permits Cat, Dog, Elephant
public final class Cat implements Animal { ... }
public sealed class Dog permits Chihuahua, Pug implements Animal {}
public non-sealed class Elephant implements Animal { ... }

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

В Scala у нас похожий уровень контроля, реализации могут быть: • final; • sealed (опять же, все реализации должны быть в одном исходном файле); • без модификатора, что делает класс открытым для расширения.

Последний (и по умолчанию) вариант соответствует non-sealed:

sealed trait Animal
final class Cat extends Animal { ... }
sealed class Dog extends Animal { ... }
class Elephant extends Animal { ... }

Материал является переводом первой части статьи "Java 15 through the eyes of a Scala programmer".

Не пропустите новые полезные статьи!

Спасибо за подписку!

Мы отправили вам письмо для подтверждения вашего email.
С уважением, OTUS!

Автор
0 комментариев
Для комментирования необходимо авторизоваться
Популярное
Сегодня тут пусто