Предстоящий выпуск Java будет 14-й версией, которую планируется сделать общедоступной в марте 2020 года. Как и уже выпущенные версии в рамках нового 6-месячного цикла выпуска, ожидается , что JDK 14 будет иметь несколько новых функций как на уровне языка, так и на уровне JVM. ,
Однако, если мы посмотрим на список возможностей, то заметим довольно много языковых возможностей, которые ожидают разработчики: записи, выражения переключения (которые существуют в JDK 13, но в режиме предварительного просмотра) и сопоставление с образцом. Давайте посмотрим на записи, которые кажутся интересным дополнением к языку.
Вам также может понравиться:
Представляем Java Record
Предпосылки
Все, что нам нужно, это двоичный файл JDK 14 с ранним доступом с веб-сайта OpenJDK: https://jdk.java.net/14/ .
Что такое запись?
Запись в основном представляет собой «класс данных», особый вид класса, предназначенный для хранения в нем чистых данных. Семантика записей уже существует в подобных конструкциях в других языках, таких как классы данных в Kotlin. Объявляя тип как запись, разработчик четко выражает свое намерение, чтобы тип представлял только данные. Синтаксис объявления записи намного проще и лаконичнее по сравнению с использованием обычного класса, где обычно требуется реализовать основные Object
методы, такие как equals()
и hashCode()
(часто называемые «стандартным» кодом). Записи кажутся интересным выбором при моделировании таких вещей, как классы модели предметной области (которые могут быть сохранены через ORM) или объекты передачи данных (DTO).
Хороший способ подумать о том, как записи реализованы на языке, — это запомнить перечисления. Enum — это также класс, имеющий специальную семантику с более приятным синтаксисом. Поскольку оба они все еще являются классами, многие функции, доступные в классах, сохраняются, поэтому баланс между простотой и гибкостью в их дизайне сохраняется.
Записи являются функцией языка предварительного просмотра , что означает, что, хотя она полностью реализована, она еще не стандартизирована в JDK и может использоваться только путем активации флага. Функции предварительного просмотра языка могут быть обновлены или даже удалены в будущих версиях. Подобно выражениям переключения, оно может стать окончательным и постоянным в будущей версии.
Пример записи
Вот пример того, как выглядит основная запись:
Джава
1
package examples;
2
3
record Person (String firstName, String lastName) {}
У нас есть Person
запись, определенная в пакете с двумя компонентами: firstName
и lastName
, и пустое тело.
Давайте попробуем скомпилировать его — обратите внимание на --enable-preview
опцию:
Оболочка
xxxxxxxxxx
1
> javac --enable-preview --release 14 Person.java
2
Note: Person.java uses preview language features.
3
Note: Recompile with -Xlint:preview for details.
Как это выглядит под капотом?
Как упоминалось ранее, запись — это просто класс с целью хранения и раскрытия данных. Давайте посмотрим на сгенерированный байт-код с помощью javap
инструмента:
Оболочка
xxxxxxxxxx
1
>javap -v -p Person.class
Простой текст
xxxxxxxxxx
1
Classfile examples/Person.class
2
Last modified Dec 22, 2019; size 1273 bytes
3
SHA-256 checksum 6f1b325121ca32a0b6127180eff29dcac4834f9c138c9613c526a4202fef972f
4
Compiled from "Person.java"
5
final class examples.Person extends java.lang.Record
6
minor version: 65535
7
major version: 58
8
flags: (0x0030) ACC_FINAL, ACC_SUPER
9
this_class: #8 // examples/Person
10
super_class: #2 // java/lang/Record
11
interfaces: 0, fields: 2, methods: 6, attributes: 4
12
Constant pool:
13
#1 = Methodref #2.#3 // java/lang/Record."":()V
14
#2 = Class #4 // java/lang/Record
15
#3 = NameAndType #5:#6 // "":()V
16
#4 = Utf8 java/lang/Record
17
#5 = Utf8
18
#6 = Utf8 ()V
19
#7 = Fieldref #8.#9 // examples/Person.firstName:Ljava/lang/String;
20
#8 = Class #10 // examples/Person
21
#9 = NameAndType #11:#12 // firstName:Ljava/lang/String;
22
#10 = Utf8 examples/Person
23
#11 = Utf8 firstName
24
#12 = Utf8 Ljava/lang/String;
25
#13 = Fieldref #8.#14 // examples/Person.lastName:Ljava/lang/String;
26
#14 = NameAndType #15:#12 // lastName:Ljava/lang/String;
27
#15 = Utf8 lastName
28
#16 = Fieldref #8.#9 // examples/Person.firstName:Ljava/lang/String;
29
#17 = Fieldref #8.#14 // examples/Person.lastName:Ljava/lang/String;
30
#18 = InvokeDynamic #0:#19 // #0:toString:(Lexamples/Person;)Ljava/lang/String;
31
#19 = NameAndType #20:#21 // toString:(Lexamples/Person;)Ljava/lang/String;
32
#20 = Utf8 toString
33
#21 = Utf8 (Lexamples/Person😉Ljava/lang/String;
34
#22 = InvokeDynamic #0:#23 // #0:hashCode:(Lexamples/Person;)I
35
#23 = NameAndType #24:#25 // hashCode:(Lexamples/Person;)I
36
#24 = Utf8 hashCode
37
#25 = Utf8 (Lexamples/Person😉I
38
#26 = InvokeDynamic #0:#27 // #0:equals:(Lexamples/Person;Ljava/lang/Object;)Z
39
#27 = NameAndType #28:#29 // equals:(Lexamples/Person;Ljava/lang/Object;)Z
40
#28 = Utf8 equals
41
#29 = Utf8 (Lexamples/Person;Ljava/lang/Object😉Z
42
#30 = Utf8 (Ljava/lang/String;Ljava/lang/String😉V
43
#31 = Utf8 Code
44
#32 = Utf8 LineNumberTable
45
#33 = Utf8 MethodParameters
46
#34 = Utf8 ()Ljava/lang/String;
47
#35 = Utf8 ()I
48
#36 = Utf8 (Ljava/lang/Object😉Z
49
#37 = Utf8 SourceFile
50
#38 = Utf8 Person.java
51
#39 = Utf8 Record
52
#40 = Utf8 BootstrapMethods
53
#41 = MethodHandle 6:#42 // REF_invokeStatic java/lang/runtime/ObjectMethods.bootstrap:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object;
54
#42 = Methodref #43.#44 // java/lang/runtime/ObjectMethods.bootstrap:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object;
55
#43 = Class #45 // java/lang/runtime/ObjectMethods
56
#44 = NameAndType #46:#47 // bootstrap:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object;
57
#45 = Utf8 java/lang/runtime/ObjectMethods
58
#46 = Utf8 bootstrap
59
#47 = Utf8 (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle😉Ljava/lang/Object;
60
#48 = String #49 // firstName;lastName
61
#49 = Utf8 firstName;lastName
62
#50 = MethodHandle 1:#7 // REF_getField examples/Person.firstName:Ljava/lang/String;
63
#51 = MethodHandle 1:#13 // REF_getField examples/Person.lastName:Ljava/lang/String;
64
#52 = Utf8 InnerClasses
65
#53 = Class #54 // java/lang/invoke/MethodHandles$Lookup
66
#54 = Utf8 java/lang/invoke/MethodHandles$Lookup
67
#55 = Class #56 // java/lang/invoke/MethodHandles
68
#56 = Utf8 java/lang/invoke/MethodHandles
69
#57 = Utf8 Lookup
70
{
71
private final java.lang.String firstName;
72
descriptor: Ljava/lang/String;
73
flags: (0x0012) ACC_PRIVATE, ACC_FINAL
74
75
private final java.lang.String lastName;
76
descriptor: Ljava/lang/String;
77
flags: (0x0012) ACC_PRIVATE, ACC_FINAL
78
79
public examples.Person(java.lang.String, java.lang.String);
80
descriptor: (Ljava/lang/String;Ljava/lang/String😉V
81
flags: (0x0001) ACC_PUBLIC
82
Code:
83
stack=2, locals=3, args_size=3
84
0: aload_0
85
1: invokespecial #1 // Method java/lang/Record."":()V
86
4: aload_0
87
5: aload_1
88
6: putfield #7 // Field firstName:Ljava/lang/String;
89
9: aload_0
90
10: aload_2
91
11: putfield #13 // Field lastName:Ljava/lang/String;
92
14: return
93
LineNumberTable:
94
line 3: 0
95
MethodParameters:
96
Name Flags
97
firstName
98
lastName
99
100
public java.lang.String toString();
101
descriptor: ()Ljava/lang/String;
102
flags: (0x0001) ACC_PUBLIC
103
Code:
104
stack=1, locals=1, args_size=1
105
0: aload_0
106
1: invokedynamic #18, 0 // InvokeDynamic #0:toString:(Lexamples/Person;)Ljava/lang/String;
107
6: areturn
108
LineNumberTable:
109
line 3: 0
110
111
public final int hashCode();
112
descriptor: ()I
113
flags: (0x0011) ACC_PUBLIC, ACC_FINAL
114
Code:
115
stack=1, locals=1, args_size=1
116
0: aload_0
117
1: invokedynamic #22, 0 // InvokeDynamic #0:hashCode:(Lexamples/Person;)I
118
6: ireturn
119
LineNumberTable:
120
line 3: 0
121
122
public final boolean equals(java.lang.Object);
123
descriptor: (Ljava/lang/Object😉Z
124
flags: (0x0011) ACC_PUBLIC, ACC_FINAL
125
Code:
126
stack=2, locals=2, args_size=2
127
0: aload_0
128
1: aload_1
129
2: invokedynamic #26, 0 // InvokeDynamic #0:equals:(Lexamples/Person;Ljava/lang/Object;)Z
130
7: ireturn
131
LineNumberTable:
132
line 3: 0
133
134
public java.lang.String firstName();
135
descriptor: ()Ljava/lang/String;
136
flags: (0x0001) ACC_PUBLIC
137
Code:
138
stack=1, locals=1, args_size=1
139
0: aload_0
140
1: getfield #16 // Field firstName:Ljava/lang/String;
141
4: areturn
142
LineNumberTable:
143
line 3: 0
144
145
public java.lang.String lastName();
146
descriptor: ()Ljava/lang/String;
147
flags: (0x0001) ACC_PUBLIC
148
Code:
149
stack=1, locals=1, args_size=1
150
0: aload_0
151
1: getfield #17 // Field lastName:Ljava/lang/String;
152
4: areturn
153
LineNumberTable:
154
line 3: 0
155
}
156
SourceFile: "Person.java"
157
Record:
158
java.lang.String firstName;
159
descriptor: Ljava/lang/String;
160
161
java.lang.String lastName;
162
descriptor: Ljava/lang/String;
163
164
BootstrapMethods:
165
0: #41 REF_invokeStatic java/lang/runtime/ObjectMethods.bootstrap🙁Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle😉Ljava/lang/Object;
166
Method arguments:
167
#8 examples/Person
168
#48 firstName;lastName
169
#50 REF_getField examples/Person.firstName:Ljava/lang/String;
170
#51 REF_getField examples/Person.lastName:Ljava/lang/String;
171
InnerClasses:
172
public static final #57= #53 of #55; // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
Интересно … Несколько вещей, которые мы можем заметить:
- Класс помечен
final
, что означает, что мы не можем создать его подкласс. - Класс extends
java.lang.Record
, который является базовым классом для всех записей, очень похожjava.lang.Enum
на базовый класс для всех перечислений. - Есть два частных конечных поля, названных в честь двух компонентов записи:
firstName
иlastName
. - Существует общественный конструктор , который генерируется для нас:
public examples.Person(java.lang.String, java.lang.String)
. Глядя на его тело, легко увидеть, что он просто присваивает два аргумента двум полям. Конструктор эквивалентен: - Есть два метода получения
firstName()
иlastName()
. - Генерируются три другие методы:
toString()
,hashCode()
иequals()
. Все они полагаются наinvokedynamic
динамический вызов соответствующего метода, содержащего неявную реализацию. Существует метод начальной загрузки,ObjectMethods.bootstrap
который берет имена компонентов записи и методы ее получения и генерирует методы. Их поведение соответствует тому, что мы ожидаем иметь:
Джава
xxxxxxxxxx
1
public Person(String firstName, String lastName) {
2
this.firstName = firstName;
3
this.lastName = lastName;
4
}
Джава
xxxxxxxxxx
1
Person john = new Person("John", "Doe");
2
System.out.println(john.firstName()); // John
3
System.out.println(john.lastName()); // Doe
4
System.out.println(john); // Person[firstName=John, lastName=Doe]
5
6
Person jane = new Person("Jane", "Dae");
7
Person johnCopy = new Person("John", "Doe");
8
9
System.out.println(john.hashCode()); // 71819599
10
System.out.println(jane.hashCode()); // 71407578
11
System.out.println(johnCopy.hashCode()); // 71819599
12
System.out.println(john.equals(jane)); // false
13
System.out.println(john.equals(johnCopy)); // true
Добавление объявлений членов в записи
Мы не можем добавлять поля экземпляра в записи, что ожидается, учитывая, что такое состояние должно быть частью компонентов. Однако мы можем добавить статические поля:
Джава
xxxxxxxxxx
1
record Person (String firstName, String lastName) {
2
static int x;
3
}
Мы можем определить статические методы и методы экземпляра, которые могут воздействовать на состояние объекта:
Джава
xxxxxxxxxx
1
record Person (String firstName, String lastName) {
2
static int x;
3
4
public static void doX() {
5
x++;
6
}
7
8
public String getFullName() {
9
return firstName + " " + lastName;
10
}
11
}
Мы также можем добавить конструкторы и изменить канонический конструктор (тот, который принимает два String
параметра). Если мы хотим переопределить канонический конструктор, мы можем опустить параметры и назначения для полей:
Джава
xxxxxxxxxx
1
record Person (String firstName, String lastName) {
2
public Person {
3
if(firstName == null || lastName == null) {
4
throw new IllegalArgumentException("firstName and lastName must not be null");
5
// We can also omit assigning fields, the compiler will auto-add them
6
}
7
}
8
9
public Person(String fullName) {
10
this(fullName.split(" ")[0], fullName.split(" ")[1]);
11
}
12
}
Вывод
Записи предоставляют возможность правильной реализации классов данных без необходимости написания подробного кода. Классы простых данных сокращены с нескольких строк кода до однострочного. Выполняются другие языковые функции, которые хорошо работают с записями, например сопоставление с образцом. Для более глубокого погружения в записи и справочную информацию см. Исследовательский документ Брайана Гетца на OpenJDK .