Java11 快速启动指南(全)
原文:
zh.annas-archive.org/md5/2871146470097edf311ff3f7f16c22d2
译者:飞龙
协议:CC BY-NC-SA 4.0
前言
第一章, 类型推断,Java 长期以来一直因冗长而受到批评,Java 10 通过使用关键字 var 引入了局部变量的类型推断。它不应与动态绑定混淆。Java 仍然有静态绑定。类型推断是从之前的 Java 版本演变而来的,从 Java 5 的泛型到 Java 7 的 try-with-resources,再到 Java 8 中推断的 lambda 参数类型。Java 编译器不是让您输入变量类型信息,而是推断它并将其添加到字节码中。
第二章, 应用程序类数据共享,应用程序类数据共享(ADS)扩展了类数据共享(CDS),它允许 JVM 记录一组类并将它们处理成一个共享的归档文件。这个归档文件可以在下一次运行时映射到 JVM 进程以减少启动时间。文件可以在 JVM 之间共享,这可以在同一主机上运行多个 JVM 时减少整体内存占用。
第三章, 垃圾收集器优化,Java 垃圾收集器既是一大福音,也是一大痛点。一直在持续努力优化垃圾收集过程。随着 Java 9 的发布,G1 成为默认的垃圾收集器。在 Java 10 中,G1 通过允许完全 GC 并行化而变得更加高效。通过垃圾收集器接口,应用程序可以部署使用替代的垃圾收集器。
第四章, API 改进及其他变更,这还不是全部,Java 10 还有更多要提供——它包括对现有 API 的改进,如添加创建不可修改集合的 API,线程局部握手以停止选定的线程,以及将 JDK 森林合并到一个单一仓库中。
第五章, lambda 参数的局部变量语法,Java 11 最受期待的特性之一,lambda 参数的局部变量语法引入了与 lambda 参数一起使用 var 的用法。本章将涵盖其语法和用法,以及面临的挑战。
第六章, Epsilon 及其设计考虑因素,Java 11 引入了 Epsilon;它减少了垃圾收集的延迟。在本章中,您将了解为什么需要它以及其设计考虑因素。
第七章, HTTP 客户端及其他变更,随着 Java 11 的发布,自从 Java EE 迁移到 Eclipse 基金会并更名为 Jakarta EE 以来,已开始移除弃用的 Java EE 和 corba 包。Java 11 重写了 Java 9 中引入的 HTTP 客户端的实现,使其完全异步。
第八章, 增强的枚举项目 Amber,现有的枚举提供了有限的功能。Amber 项目将通过允许类型变量(泛型枚举)和执行对枚举常量的更精确的类型检查来增强枚举。
第九章,数据类及其使用*,数据类是存储对象状态的包装器。尽管 IDE 可以生成此类访问器和突变方法,但开发者仍需要扫描以确定它是否隐藏任何业务逻辑。通过定义数据类并使用关键字 data,数据类使这种语言仪式变得更加简单。本章涵盖了数据类的需求、定义和使用。
第十章,原始字符串字面量*,字符串连接是程序员经常使用的方法,用于返回对象的字符串表示形式、JSON 或 XML 请求或响应、SQL 查询等。到目前为止,Java 的字符串连接使用了笨拙的连接运算符组合、引号和特殊字符的否定(使用反斜杠),这使得编写和维护变得不方便。本章通过演示原始字符串字面量的创建和使用,简化了字符串连接。
第十一章,Lambda 剩余*,lambda 剩余包括使用下划线(_)表示未命名的方法、异常或 lambda 参数。本章涵盖了实现它的挑战、创建和使用。
第十二章,模式匹配和 switch 表达式*,为了入门,模式匹配将涵盖类型测试和常量模式,以增强 Java 语言结构。本章将向您介绍模式及其如何显著影响 switch 表达式的使用方式。
第十三章,基于值的优化*,当对象存储在数组中时,会有额外的开销,它们的直接值不会存储在数组中。对象数组存储有关数组对象的元数据和多个数据引用。这比所需的内存多得多。值类型,一种新的语言修改,允许创建数据类型;这些数据类型仅使用存储实际值所需的内存。
第十四章,泛型改进*,随着向 Java 添加值类型的提议,限制泛型参数为对象是有害的。本章涵盖了泛型改进,以扩展泛型类型以支持泛型类和接口在原始类型上的特殊化。
第十五章,过滤器与延续*,自从首次发布以来,Java 因其对线程创建的支持而变得流行。纤程和延续将使它迈出更大的步伐;通过创建超轻量级的线程,称为纤程。
第十六章,JVM 和本地代码*,类似于连接大西洋和太平洋的巴拿马运河,Project Panama 计划弥合 JVM 和本地代码之间的差距。本章将向您介绍如何向 Java 开发者开放本地库的思考过程,例如用 C 编写的库。
第一章:类型推断
使用局部变量或var
的类型推断是 Java 10 中最受讨论的明星特性之一。它减少了语言的冗长性,同时没有牺牲 Java 可靠的静态绑定和类型安全。编译器使用代码中已经可用的信息推断类型,并将其添加到它生成的字节码中。
每个新概念都有自己的优点、局限性和复杂性。使用var
进行类型推断也不例外。随着您学习本章,您将看到使用var
将如何让您着迷、让您沮丧,但最终取得胜利。
在本章中,我们将涵盖以下主题:
-
什么是类型推断?
-
使用
var
的类型推断 -
与
var
一起工作的注意事项 -
类型推断与动态绑定
什么是类型推断?
想象一下解决一个谜题,就像以下图像中所示的那样,它以提示的形式呈现多个约束。您通过解决约束来得出答案。您可以将类型推断与生成约束然后解决它们来比较,以确定编程语言中的数据类型。类型推断是编译器通过使用代码中已经可用的信息来确定数据类型的能力;字面值、方法调用及其声明。对于开发者来说,类型推断减少了冗长性:
为了您的参考,前一个谜语的答案是 87(只需翻转图像,你就能在序列中找到数字)。
类型推断对 Java 来说并不新鲜。随着 Java 10 中引入局部变量var
,它被提升到了一个新的水平。
让我们通过使用var
的示例进行深入研究。
使用 var 的类型推断
以下代码行显示了在 Java 10 之前您是如何定义局部变量(以及所有其他变量)的:
String name = "Java11";
LocalDateTime dateTime = new LocalDateTime.now();
从 Java 10 开始,使用var
,您可以在局部变量的声明中省略强制显式类型,如下所示:
var name = "Java11"; // variable 'name' inferred as String
var dateTime = new LocalDateTime.now(); // var 'dateTime' inferred as LocalDateTime
您认为前面的代码似乎没有提供很多好处吗?如果,而不是以下?
HashMap<Integer, String> map = new HashMap<Integer, String>();
您可以使用以下方法:
var map = new HashMap<Integer, String>();
前面的示例在左侧删除了很多字母,使其更加简洁。
当您不再明确声明变量的数据类型时,编译器接管以确定或推断变量类型。类型推断是编译器评估代码中已存在的信息的能力,例如,字面值、操作、方法调用或它们的声明,以确定变量类型。它遵循一系列规则来推断变量类型。作为开发者,当您选择使用var
进行类型推断时,您应该了解编译器的推断算法和其他规则,以免得到意外结果。
每当出现新功能时,您都应该遵守一些规则、限制,并尝试遵循最佳实践以从中受益。让我们从使用 var
定义的变量的强制初始化开始。
使用 var
的类型推断不是动态类型;Java 仍然是一种强静态类型语言。使用 var
可以使您的代码更简洁;您可以从局部变量的定义中省略其类型。
强制非空初始化
使用 var
定义的局部变量必须在声明时进行初始化,否则代码将无法编译。编译器无法推断未初始化变量或被赋予空值的变量的类型。以下代码将无法编译:
var minAge; // uninitialized variable
var age = null; // variable assigned a null value
以下图像说明了如果未初始化的变量 age
去寻求进入 Mr. Java 编译器的位置会发生什么。编译器不会让它进入:
使用 var
定义变量时,必须始终伴随其初始化;否则,代码将无法编译。
局部变量
var
的使用仅限于局部变量。这些变量用于存储中间值,与实例和静态变量相比,它们的生命周期最短。局部变量在方法、构造函数或初始化块(实例或静态)内定义。在方法或初始化块内,它们可以在 if
-else
循环、switch
语句或 try-with-resources
构造中定义。以下是一个 Person
类的示例,该类在多个地方定义了局部变量:
public class Person { { var name = "Aqua Blue"; // instance initializer block } static { var anotherLocalVar = 19876; // static initializer block } Person() { var ctr = 10; // constructor for (var loopCtr = 0; loopCtr < 10; ++loopCtr) { // loop - for initializer switch(loopCtr) { case 7 :{ var probability = ctr / loopCtr; // switch System.out.println(probability); break; } } } } public String readFile() throws IOException { var filePath = "data.txt"; try (var reader = new BufferedReader(new FileReader(filePath))) { // try-with-resources return reader.readLine(); } }
}
由于这些地方很多,您可能觉得很难记住它们。我们刚刚请求 Google Maps 找到所有可能的实例,这是我们得到的结果:
本章包含一些代码检查练习,供您快速尝试新主题。练习使用了两位假设程序员的姓名,Pavni 和 Aarav。
代码检查——第一部分
我们的程序员之一,Aarav,重构了他团队成员 Pavni 的代码。代码不再输出 char
及其对应的 ASCII 数字,这些数字存储在一个 char
数组中。你能帮帮 Aarav 吗?以下是代码:
class Foo { public static void main(String args[]) { try { char[] name = new char[]{'S','t','r','i','n','g'}; for (var c : name) { System.out.println(c + ":" + (c + 1 - 1)); } } catch (var e) { //code } }
}
代码检查的答案——var
类型不能用于指定捕获处理程序中的异常类型 (var e)
。
使用 var
与原始数据类型
这看起来是最简单的,但外表可能具有欺骗性。尝试执行以下代码:
var counter = 9_009_998_992_887; // code doesn't compile
您可能会假设一个整数文字值(在这种情况下为 9_009_998_992_887
)如果超出原始 int
类型的范围,将被推断为 long
类型。然而,事实并非如此。由于整数文字值的默认类型是 int
,您需要将前缀 L
或 l
添加到值之后,如下所示:
var counter = 9_009_998_992_887L; // code compiles
同样,为了使一个 int
文字值被推断为 char
类型,您必须使用显式转换,如下所示:
var aChar = (char)91;
当你将5
除以2
时,结果是什么?你说2.5
吗?但这在 Java 中(总是)不是这样工作的!当整数用作除法运算的操作数时,结果是整数而不是小数。小数部分被舍弃以得到整数结果。尽管这是常见的,但当你期望编译器推断变量类型时,这可能会显得有些奇怪。以下是一个例子:
// type of result inferred as int; 'result' stores 2
var divResult = 5/2; // result of (5/2), that is 2 casted to a double; divResult stores 2.0
var divResult = (double)(5/ 2); // operation of a double and int results in a double; divResult stores 2.5
var divResult = (double)5/ 2;
尽管这些情况与var
类型没有直接关系,但开发者认为编译器会推断特定类型的假设,导致不匹配。以下是一个快速图像来帮助你记住这一点。
整数字面量的默认类型是int
,浮点数的默认类型是double
。将100
赋值给使用var
定义的变量将推断其类型为int
;而不是byte
或short
。
在算术运算中,如果任一操作数是char
、byte
、short
或int
,结果至少会被提升为int
。
byte b1 = 10;
char c1 = 9;
var sum = b1 + c1; // inferred type of sum is int
类似地,对于至少包含一个操作数为long
、float
或double
值的算术运算,结果会被提升为相应的long
、float
或double
类型:
byte cupsOfCoffee = 10;
long population = 10L;
float weight = 79.8f;
double distance = 198654.77; var total1 = cupsOfCoffee + population; // inferred type of total1 is long
var total2 = distance + population; // inferred type of total2 is double
var total3 = weight + population; // inferred type of total3 is float
原始变量隐式扩展的规则在理解 Java 编译器如何推断具有原始值的变量时起着重要作用。
使用派生类的类型推断
在 JDK 9 和之前的版本中,你可以定义一个基类变量并将其赋值为其派生类的实例。你可以使用这个变量访问的成员仅限于在基类中定义的成员。使用var
之后就不再是这样了;因为变量的类型是通过分配给它的实例的具体类型来推断的。
假设Child
类扩展了Parent
类。当你创建一个局部变量并将其赋值为Child
类的实例时,变量的类型被推断为Child
。这看起来很简单。以下是一个例子:
class Parent { void whistle() { System.out.println("Parent-Whistle"); }
}
class Child extends Parent { void whistle() { System.out.println("Child-Whistle"); } void stand() { System.out.println("Child-stand"); }
}
class Test{ public static void main(String[] args) { var obj = new Child(); obj.whistle(); obj.stand(); // type of obj inferred as Child }
}
如果你使用一个可以返回Child
类或Parent
类实例的方法来赋值变量obj
,会发生什么?以下是修改后的代码:
class Parent { void whistle() { System.out.println("Parent-Whistle"); }
} class Child extends Parent { void whistle() { System.out.println("Child-Whistle"); } void stand() { System.out.println("Child-stand"); }
} class Test{ public static Parent getObject(String type) { if (type.equals("Parent")) return new Parent(); else return new Child(); } public static void main(String[] args) { var obj = getObject("Child"); obj.whistle(); obj.stand(); // This line doesn't compile }
}
在前面的代码中,方法getObject()
返回的实例类型在代码执行之前无法确定。在编译时,变量obj
的类型被推断为Parent
;因此main()
方法无法编译。
使用var
定义的变量类型是在编译时推断的。如果方法返回类型用于赋值使用var
定义的变量,其推断类型是方法的返回类型;而不是在运行时返回的实例类型。
类型推断 – 使用接口
让我们将前面章节中学到的知识扩展到接口的使用。想象一下Child
类实现了MarathonRunner
接口:
interface MarathonRunner{ default void run() { System.out.println("I'm a marathon runner"); }
} class Child implements MarathonRunner { void whistle() { System.out.println("Child-Whistle"); } void stand() { System.out.println("Child-stand"); }
}
让我们定义一个局部变量obj
,将其赋值为Child
类的实例:
class Test{ public static void main(String[] args) { var obj = new Child(); // inferred type of var obj is Child obj.whistle(); obj.stand(); obj.run(); }
}
如果使用返回类型为 MarathonRunner
的方法初始化相同的变量,其推断类型为 MarathonRunner
(无论它返回的实例类型如何):
class Test{ public static MarathonRunner getObject() { return new Child(); } public static void main(String[] args) { var obj = getObject(); // inferred type of var obj is MarathonRunner obj.whistle(); obj.stand(); obj.run(); }
}
使用 var
与数组
使用 var
并不意味着只是丢弃局部变量的类型;剩下的内容应该能够使编译器推断其类型。想象一下一个定义 char
类型数组的方法:
char name[] = {'S','t','r','i','n','g'};
你不能丢弃其类型,并使用以下任一代码来定义它:
var name[] = {'S','t','r','i','n','g'};
var[] name = {'S','t','r','i','n','g'};
var name = {'S','t','r','i','n','g'};
这里是向编译器提供相关信息以使其能够推断类型的一种方法:
var name = new char[]{'S','t','r','i','n','g'};
看起来 Java 编译器已经因为程序员的这个假设而感到有些吃力,如下面的图片所示:
你不能仅仅丢弃数据类型来使用 var
。剩下的内容应该能够使编译器推断出被分配的值的类型。
泛型的类型推断
引入泛型的动机是为了包含类型安全。它使开发者能够指定他们使用具有固定或类型范围的类、接口和集合类的意图。违反这些意图将通过编译错误强制执行,而不是运行时异常;提高合规性标准。
例如,以下是定义一个用于存储 String
值的 ArrayList
的方法(在赋值右侧重复 <String>
是可选的):
List<String> names = new ArrayList<>();
然而,将 List<String>
替换为 var
将会使泛型中的类型安全受到威胁:
var names = new ArrayList<>();
names.add(1);
names.add("Mala");
names.add(10.9);
names.add(true);
上述代码允许向 names
添加多个数据类型,这并不是意图。使用泛型时,首选的方法是向编译器提供相关信息,以便它可以正确推断其类型:
var names = new ArrayList<String>();
当使用 var
与泛型时,确保你在赋值右侧的尖括号内传递相关数据类型;这样你就不会失去类型安全。
是时候进行我们的下一个代码检查了。
代码检查 – 第二部分
我们的程序员之一,Pavni,尝试在泛型和集合类中使用 var
,但她的代码似乎没有输出排序好的钢笔集合。你能帮忙吗?
class Pen implements Comparable<Pen> { String name; double price; Pen(String name, double price) { this.name = name; this.price = price; } public int compareTo(Pen pen) { return ((int)(this.price-pen.price)); } public String toString() { return name; } public static void main(String args[]) { var pen1 = new Pen("Lateral", 219.9); var pen2 = new Pen("Pinker", 19.9); var pen3 = new Pen("Simplie", 159.9); var penList = List.of(pen1, pen2, pen3); Collections.sort(penList); for (var a : penList) System.out.println(a); }
}
代码检查的答案——问题是尝试使用 Collections.sort()
修改不可变集合。这是为了强调所有问题并不都与 var
的使用相关。
将推断变量传递给方法
尽管 var
的使用仅限于局部变量的声明,但这些变量(包括原始类型和引用类型)可以作为值传递给方法。推断出的类型和方法期望的类型必须匹配,才能使代码编译。
在下面的示例代码中,Child
类实现了 MarathonRunner
接口。Marathon
类中的 start()
方法期望传入的参数是 MarathonRunner
对象(实现此接口的类的实例)。变量 aRunner
的推断类型是 Child
。由于 Child
类实现了 MarathonRunner
,因此可以将 aRunner
传递给 start()
方法,aRunner
的推断类型(Child
)与 start()
方法期望的类型(MarathonRunner
)相匹配,使得代码能够编译。
这是代码:
interface MarathonRunner { default void run() { System.out.println("I'm a marathon runner"); }
}
class Child implements MarathonRunner { void whistle() { System.out.println("Child-Whistle"); } void stand() { System.out.println("Child-stand"); }
}
class Marathon { public static void main(String[] args) { var aRunner = new Child(); // Inferred type is Child start(aRunner); // ok to pass it to start (param - MarathonRunner) } public static void start(MarathonRunner runner) { runner.run(); }
}
只要变量的推断类型与方法参数的类型相匹配,就可以将其作为参数传递给它。
将值重新赋给推断变量
对于所有非最终变量,你可以将值重新赋给推断变量。只需确保重新赋的值与其推断类型相匹配。变量的类型仅推断一次。
var age = 9; // type inferred as int
age = 10.9; // won't compile StringBuilder query = new StringBuilder("SELECT"); // Type - StringBuilder
query = query.toString() + "FROM" + "TABLE"; // won't compile; // can't convert String // to StringBuilder
使用 var
定义的局部变量的类型仅推断一次。
显式类型转换与推断变量
假设一个同事将 29
赋值给一个推断的局部变量,比如 age
,假设编译器会推断变量 age
的类型为 byte
:
var age = 29; // inferred type of age is int
然而,编译器会推断变量 age
的类型为 int
,因为整数字面量的默认类型是 int
。要修复前面的假设,你可以使用显式数据类型,或者通过使用显式类型转换覆盖编译器的默认推断机制,如下所示:
byte age = 29; // Option 1 - no type inference
var age = (byte)29; // Option 2 - explicit casting
通过使用显式类型推断,你可以覆盖编译器的默认类型推断机制。这可能是为了修复现有代码中的假设。
类似地,你可以使用显式类型转换与其他原始数据类型,如 char
和 float
:
var letter = (char)97; // inferred type of letter is char
var debit = (float)17.9; // inferred type of debit is float
在前面的示例中,如果没有显式类型转换,赋值为整数字面量的变量会被推断为 int
类型,而小数会被推断为 double
类型。
以下是一个使用显式类型转换的引用变量示例:
class Automobile {}
class Car extends Automobile { void check() {}
}
class Test{ public static void main(String[] args) { var obj = (Automobile)new Car(); obj.check(); // Won't compile; type of obj is Automobile }
}
使用类型推断的显式类型转换来修复任何现有的假设。我不建议使用显式类型转换来初始化推断变量;这违背了使用 var
的目的。
使用显式类型转换赋值 null
再次,虽然将 null
显式转换为 var
类型的值没有意义,但这是一种有效的代码:
var name = (String)null; // Code compiles
尽管上一行代码在语法上是正确的,但这是一种不良的编码实践。请避免这样做!
Java 早期版本中的类型推断
虽然 var
在 Java 10 中将推断提升到了新的水平,但类型推断的概念在它的早期版本中就已经存在。让我们看看 Java 早期版本中类型推断的示例。
Java 5 中的类型推断
泛型引入了一种类型系统,使开发者能够对类型进行抽象。它限制了类、接口或方法只能与指定类型的实例一起工作,提供了编译时的类型安全。泛型被定义为为集合框架添加编译时安全性。泛型使程序能够在编译期间检测到某些错误,从而防止它们在运行时代码中蔓延。在开发阶段修复错误比在生产阶段修复错误要容易。
Java 在 Java 5 中为泛型方法类型参数使用了类型推断。而不是以下这样的代码:
List<Integer> myListOfIntegers = Collections.<Integer>emptyList(); // 1
你可以使用:
List<Integer> myListOfIntegers = Collections.emptyList(); // 1
Java 7 中的类型推断
Java 7 引入了泛型构造函数参数的类型推断。这实际上意味着以下这样的代码行:
List<String> myThings = new ArrayList<String>();
可以替换为:
List<String> myThings = new ArrayList<>();
前面的内容不应与以下内容混淆,后者试图将泛型与原始类型混合:
List<String> myThings = new ArrayList();
Java 7 也允许对泛型方法进行类型推断。例如,在 MyClass
类中定义的泛型方法 print()
:
class MyClass<T> { public <X> void print(X x) { System.out.println(x.getClass()); }
}
可以以下两种方式之一调用(第三行代码使用类型推断来推断传递给 print()
方法的参数类型):
MyClass<String> myClass = new MyClass<>();
myClass.<Boolean>deliver(new Boolean("true"));
myClass.deliver(new Boolean("true"));
Java 8 中的类型推断
Java 8 通过 lambda 函数引入了 Java 中的函数式编程。lambda 表达式可以推断其形式参数的类型。因此,可以不用以下这样的代码行:
Consumer<String> consumer = (String s) -> System.out.println(s);
你可以编写:
Consumer<String> consumer = s -> System.out.print(s);
挑战
var
的使用并非没有挑战,这既包括 Java 语言的开发者,也包括其用户。让我们从 var
使用受限的原因开始讨论。
限制失败假设的作用域
如你所知,var
类型的使用仅限于 Java 中的局部变量。它们不允许在公共 API 中使用,例如作为方法参数或方法的返回类型。一些语言支持对所有类型变量的类型推断。Java 可能会在未来允许这样做(谁知道呢?会发生吗?如果会,什么时候?)。
然而,有强有力的理由限制推断变量的作用域,以便及早发现由于假设与实际情况不符而产生的错误。公共 API 的契约应该是明确的。使用公共 API 的类型推断将允许这些错误被捕获和纠正得晚一些。
公共 API 的契约应该是明确的,它们不应该依赖于类型推断。
这里有一个实际例子,说明了假设与实际情况不符可能导致错误。
最近,我的孩子随学校出国参加学生交流项目。学校要求我为她签证申请发送一组照片。我打电话给我的摄影师,要求他打印签证照片(指定国家)。两天后,学校要求我重新提交照片,因为之前提交的照片不符合规则。
发生了什么问题?学校和我都未明确说明照片的规格。学校认为我会知道规格;我则认为摄影师会知道规格(因为他已经做了很多年)。在这种情况下,至少有一个人假设结果符合特定的输出,而没有明确指定输出。没有明确的合同,期望和实际之间总是存在不匹配的范围。
尽管存在困惑,错误在应用程序提交给大使馆之前被发现并得到了纠正。
这里有一张有趣的图片,可以解释为什么类型推断的使用仅限于局部变量,当局部实例和静态变量在比赛中竞争时,只有局部变量能够到达终点线:
破坏现有代码
使用var
作为类、接口、方法、方法参数或变量名称的代码,在 JDK 10 及以后的版本中将无法编译。以下是一个在多个地方使用var
的代码示例;它将无法编译:
class var {} // can't use var as class name
interface var {} // can't use var as interface name
class Demo {int var = 100; // can't use var as instance variable namestatic long var = 121; // can't use var as static variable namevoid var() { // can't use var as method nameint var = 10; // cant use var as the name of a local variable}void aMethod(String var) {} // can't use var as the name of method parameter
}
重要的是要使用最新的 Java 发布版本测试你的生产代码,即使你并不打算将你的生产代码部署到它们上。这将有助于消除与你的生产代码的任何兼容性问题;有助于将其迁移到未来的 Java 版本发布。
不可表示类型
你可以在程序中使用的一些 Java 类型,如int
、Byte
、Comparable
或String
,被称为可表示类型。编译器内部使用的类型,如匿名类的子类,你无法在程序中编写,被称为不可表示类型。
到目前为止,变量的类型推断似乎很容易实现,只需获取传递给方法、从方法返回的值的有关信息,并推断类型。但当涉及到非可表示类型的推断——null
类型、交叉类型、匿名类类型和捕获类型时,事情并不像那样简单。
例如,你认为以下推断变量的类型是什么:
// inferred type java.util.ImmutableCollections$ListN
var a = List.of(1, "2", new StringBuilder());
var b = List.of(new ArrayList<String>(), LocalTime.now());
前面的案例不是类型之一。它们既没有被禁止推断,也没有被规范化为可表示类型。
有意义的变量名
新功能应该负责任地使用。当你使用变量名删除显式数据类型时,名称就成为了焦点。使用推断类型时,你有责任使用描述性和适当的变量名,以便它们在代码中更有意义。正如你所知,一段代码只写一次,但会被阅读很多次。
例如,以下代码行在一段时间后对你或你的团队成员(尤其是大型或分布式团队)可能不会很有意义:
var i = getData(); // what does getData() return? Is 'i' a good name?
关键问题是——变量i
是用来做什么的,方法getData()
返回什么?想象一下在你离开后,将与这段代码一起工作的维护团队的困境。
此外,定义与目的不匹配的变量名称也无济于事。例如,创建一个名为database
的连接对象并将一个URL
实例分配给它,或者定义一个名为query
的变量并将一个Connection
实例分配给它,都没有太多意义:
var database = new URL("http://www.eJavaGuru.com/malagupta.html");
var query = con.getConnection();
当你省略局部变量的显式类型时,其名称就占据了中心舞台。仔细且负责任地选择它们的名称,这使它们的目的明确无误。
代码重构
使用var
的类型推断被引入以减少 Java 语言的冗长性。它将帮助程序员在方法中输入更少的代码。编译器推断使用var
声明的变量的类型并将其插入到字节码中。
我认为没有理由重构现有或遗留代码,用var
替换方法中的显式数据类型。这不会在任何方面带来任何好处。
不要重构你的现有或遗留代码,用var
替换方法中的现有显式类型。这不会带来任何好处。
类型推断与动态绑定
使用var
的类型推断并没有推动 Java 走向动态绑定的领域。Java 仍然是一种强类型静态语言。Java 中的类型推断是语法糖。编译器推断类型并将其添加到字节码中。在动态绑定中,变量类型在运行时推断。这可能导致更晚发现更多错误。
调试应用程序是一笔昂贵的交易,在寻找错误并修复它所需的时间和精力方面。你越早发现错误,修复它就越容易。在单元测试期间修复错误比在集成测试期间修复相同的错误,或者说是当应用程序上线几个月后出现错误时,要容易得多。在开发阶段修复错误比在生产或维护阶段修复错误要容易。
摘要
在本章中,你介绍了 Java 10 中引入的局部变量推断,或称var
。var
类型使你能够在方法中省略局部变量的显式数据类型。你介绍了var
使用的各种注意事项。限于局部变量,使用var
定义的变量必须用非空值初始化。它们可以与所有类型的变量一起使用——原始类型和对象。使用var
定义的变量也可以传递给方法并从方法返回,方法声明兼容性规则适用。
为了避免在使用泛型时冒着类型安全的风险,确保你传递相关信息,同时使用var
与泛型一起。尽管这没有太多意义,但使用var
定义的变量允许显式类型转换。你同样也介绍了 Java 之前版本中存在的类型推断,即 Java 5、7 和 8 版本。在最后,你还解释了为什么类型推断仅限于局部变量,而不允许在公共 API 中使用。
使用具有意义的变量名一直被推荐,并且这一点非常重要。使用var
之后,这一点变得更加重要。由于var
提供了语法糖,因此没有必要重构现有的或遗留代码以在其中使用var
。
第二章:应用课程数据共享
即将推出...!
第三章:垃圾收集器优化
即将推出...!
第四章:API 改进及其他变更
即将推出...
第五章:Lambda 参数的局部变量语法
即将推出..!
第六章:Epsilon 及其设计考虑因素
即将推出...!
第七章:HTTP 客户端和其他更改
即将推出...!
第八章:Project Amber 中的增强枚举
在 Java 5 中,枚举引入了一种强大的方式来定义有限和预定义的常量集,具有类型安全。枚举使您能够定义一个新的类型(如类或接口),具有状态和行为。Project Amber 正在增强枚举,将其提升到下一个层次;通过添加类型变量(泛型)并允许对枚举进行更严格的类型检查。这两个特性将使枚举具有常量特定的类型信息和常量特定的状态和行为。这些增强将减少将枚举重构为类以使用泛型的需求。
在本章中,我们将涵盖以下主题:
-
增强枚举的原因是什么?
-
为枚举常量添加状态和行为
-
如何创建泛型枚举?
-
访问特定常量的状态和行为
-
对枚举常量执行更严格的类型检查
-
挑战
Java 5 中枚举的介绍
枚举为常量的使用引入了类型安全,这些常量之前是用静态最终变量定义的,例如 int
类型或其他类型。想象一下将衬衫的尺寸限制为一些预定义的尺寸,比如 Small
、Medium
或 Large
。以下是使用枚举(例如 Size
)来实现这一点的示例:
enum Size {SMALL, MEDIUM, LARGE}
Java 的编码规范建议使用大写字母来定义枚举常量(如 SMALL
)。枚举常量中的多个单词可以使用下划线分隔。
这就是如何在类(例如 Shirt
)中使用枚举 Size
来限制其尺寸为枚举 Size
中定义的常量:
class Shirt { Size size; // instance variable of type Size Color color; Shirt(Size size, Color color) { // Size object with Shirt instantiation this.size = size; this.color = color; }
}
Shirt
类中类型为 Size
的实例变量限制了分配给它的值只能是 Size.SMALL
、Size.MEDIUM
和 Size.LARGE
。以下是如何使用枚举常量创建 Shirt
类实例的另一个类(例如 GarmentFactory
类)的示例:
class GarmentFactory { void createShirts() { Shirt redShirtS = new Shirt(Size.SMALL, Color.red); Shirt greenShirtM = new Shirt(Size.MEDIUM, Color.green); Shirt redShirtL = new Shirt(Size.LARGE, Color.red); }
}
枚举定义了一个具有预定义常量值的新类型。枚举为常量值添加了类型安全。
反编译枚举 – 背后的事情
每个用户定义的 enum
都隐式地扩展了 java.lang.Enum
类。在幕后,前面章节中定义的一行枚举 Size
(Size
)被编译成类似以下的内容:
final class Size extends Enum // 'enum' converted to final class
{ public static final Size SMALL; // variables to store public static final Size MEDIUM; // enum constants public static final Size LARGE; // private static final Size $VALUES[]; // array of all enum constants static { // static initializer SMALL = new Size("SMALL", 0); // to initialize enum constants MEDIUM = new Size("MEDIUM", 1); // LARGE = new Size("LARGE", 2); // $VALUES = (new Size[] { // SMALL, MEDIUM, LARGE // & populate array of enum constants }); } public static Size[] values() { return (Size[])$VALUES.clone(); // Avoiding any modification to } // $VALUES by calling methods public static Size valueOf(String s) { return (Size)Enum.valueOf(Size, s); } private Size(String s, int i) { super(s, i); }
}
枚举是语法糖。编译器将你的枚举结构扩展为 java.lang.Enum
以创建一个类。它添加了变量、初始化器和方法以获得所需的行为。
为枚举常量添加状态和行为
枚举常量可以有自己的状态和行为。实现这一点的其中一种方法是在枚举中定义实例变量和方法。所有这些都可以被枚举常量访问。让我们修改上一节中定义的枚举 Size
,给它添加状态和行为。
每个枚举常量都可以定义一个常量特定的类体 - 定义新的状态和行为或覆盖它所定义的枚举方法的默认行为。以下是一个示例:
enum Size { SMALL(36, 19), MEDIUM(32, 20) { // Constant specific class body int number = 10; // variable specific to MEDIUM int getSize() { // method specific to MEDIUM return length + width; } }, LARGE(34, 22) { @Override public String toText() { // overriding method toText for return "LARGE"; // constant LARGE } }; int length; // instance variable accessible int width; // to all enum constants Size(int length, int width) { // enum constructor; accepts length this.length = length; // and width this.width = width; } int getLength() { // method accessible to all enum return length; // constants } int getWidth() { // method accessible to all enum return width; // constants } public String toText() { // method accessible to all enum return length + " X " + width; // constants }
}
在前面的例子中,枚举 Size
定义了三个枚举常量 - SMALL
、MEDIUM
和 LARGE
。它还定义了实例变量(length
和 breadth
)、构造函数和 getLength()
、getWidth
和 toText()
方法。
访问枚举常量的状态和行为
目前,枚举常量可以访问:
-
所有枚举常量共有的状态和行为
-
由枚举常量重写的方法
对于在前面部分定义的 Size
枚举,你可以按照以下方式访问所有枚举常量共有的状态和行为:
System.out.println(Size.SMALL.toText()); // toString is defined for all constants
上述代码输出:
36 X 19
你也可以访问特定枚举常量重写的行为:
System.out.println(Size.LARGE.toText());
上述代码输出:
LARGE
但是,你不能访问特定于枚举常量的状态或行为:
System.out.println(Size.MEDIUM.number); // Doesn't compile
System.out.println(Size.MEDIUM.getSize()); // Doesn't compile
使用 MEDIUM
常量无法访问 getSize()
方法或 number
变量。这是因为,MEDIUM
创建了一个匿名类并覆盖了 Size
枚举的方法。由于它仍然通过一个 Size
类型的变量引用,无法访问特定于常量的状态或行为,而这个变量没有定义它们。以下是一张图片来帮助你记住这一点:
现有的枚举不允许访问特定于枚举常量的状态或行为,因为这会创建一个匿名类来执行此操作。
访问枚举特定常量状态和行为的解决方案
解决这个问题并访问枚举常量特定成员的一种方法是为所有成员定义它们,但只允许使用特定成员(我知道,这是一个愚蠢的方法)。我有意移除了与展示如何实现此功能无关的代码:
enum Size {SMALL(36, 19),MEDIUM(32, 20),LARGE(34, 22);int length; // instance variable accessibleint width; // to all enum constantsSize(int length, int width) { // enum constructor; accepts lengththis.length = length; // and widththis.width = width;}int getSize() {if (this == MEDIUM)return length + width;else // throws runtime exceptionthrow new UnsupportedOperationException(); // if used with constans} // other than MEDIUM
}
让我们尝试使用枚举常量访问方法 getSize()
:
System.out.println(MEDIUM.getSize());
System.out.println(LARGE.getSize());
上述代码的输出如下:
52
线程异常 - java.lang.UnsupportedOperationException
。
首先,添加不适用于所有枚举常量的代码(方法 getSize()
)会破坏封装。在前面的例子中,我在主体中定义了 getSize()
,而只有枚举常量 MEDIUM
需要这个方法。这既不理想,也不推荐。
将它与一个基类及其派生类的排列进行比较,并在基类中添加所有特定于不同派生类的行为。这对我来说听起来很疯狂。请不要在工作时尝试这样做。
不要在工作时尝试这个解决方案;它没有定义封装的代码。
另一个例子 - 使用枚举常量进行继承
这里是另一个枚举的例子,它通过将子类的实例传递给枚举构造函数与一组子类一起工作。为了说明这一点,我已经修改了 Size
枚举,我们从本章开始就一直使用它。以下是修改后的代码:
class Measurement {} // base class
class Small extends Measurement { // derived class String text = "Small"; // state specific to class Small
}
class Medium extends Measurement { // derived class public int getLength() { // behavior specific to class Medium return 9999; }
}
class Large extends Measurement {} // derived class enum Size { SMALL(new Small()), // constant created using Small instance MEDIUM(new Medium()), // constant created using Medium instance LARGE(new Large()); // constant created using Large instance private Measurement mObj; // Measurement is base class of // classes Small, Medium & Large Size(Measurement obj) { // wraps Measurement instance as an Enum instance mObj = obj; } Measurement getMeasurement() { // get the wrapped instance return mObj; }
}
再次强调,你不能访问特定于枚举常量的代码的状态和行为。以下是一个例子:
class Test1 { public static void main(String args[]) { var large = Size.LARGE; System.out.println(large.getMeasurement().getLength()); // doesn't compile // the type of the variable used
// to wrap the value of enum
// constant is Measurement }
}
增强枚举正是为了解决这个问题而出现的。JEP 301 通过添加类型变量或泛型来引入增强枚举。让我们在下一节中看看它是如何工作的。
向枚举添加泛型
让我们重写前面示例(列表 8.x)中的枚举代码,向枚举 Size
添加类型变量。以下是修改后的代码:
enum Size <T extends Measurement> { // enum with type parameter SMALL(new Small()), MEDIUM(new Medium()), LARGE(new Large()); private T mObj; Size(T obj) { mObj = obj; } T getMeasurement() { return mObj; }
}
class Measurement {}
class Small extends Measurement { String text = "Small";
}
class Medium extends Measurement {}
class Large extends Measurement { public int getLength() { return 40; }
}
class Measurement {}
class Small extends Measurement { String text = "Small";
}
class Medium extends Measurement {}
class Large extends Measurement { public int getLength() { return 40; }
} class Measurement {}
class Small extends Measurement { String text = "Small";
}
class Medium extends Measurement {}
class Large extends Measurement { public int getLength() { return 40; }
}
以下代码可以用来访问特定常量的行为,如下所示:
var large = Size.LARGE;
System.out.println(large.getMeasurement().getLength());
在增强枚举(添加了泛型)的情况下,你将能够访问枚举常量的特定状态或行为。
让我们再来看一个通用枚举的例子,它可以用来限制用户数据到某些类型。
以下示例创建了一个泛型枚举 Data
,它可以传递一个类型参数 T
:
public enum Data<T> { NAME<String>, // constants of generic AGE<Integer>, // enum Data ADDRESS<Address>;
}
FormData
类定义了一个泛型方法,它可以接受枚举 Data
的常量以及用于枚举常量的相同类型的值:
public class FormData { public <T> void add(Data<T> type, T value) { //..code }
}
以下是使用枚举 Data
的常量来限制传递给 add
方法的值类型的组合的方法:
FormData data = new FormData();
data.add(Data.NAME, "Pavni"); // okay; type of NAME and Pavni is String
data.add(Data.AGE, 22); // okay; type of AGE and 22 is Integer
data.add(Data.ADDRESS, "California"); // Won't compile. "California" // is String, not Address instance
在数据不匹配的情况下,代码将在编译时失败,这使得开发者更容易纠正她的假设或程序流程。
编译错误总是比运行时异常要好。使用泛型枚举 Data
将使代码在编译时失败,对于传递给 add()
的值组合不匹配。
枚举常量的更精确类型化
增强枚举的两个主要目标之一是执行枚举的更精确类型检查。目前,所有枚举常量的类型是它们定义的枚举。参考我们的枚举示例 Size
,这本质上意味着所有枚举常量的类型,即 SMALL
、MEDIUM
和 LARGE
,是 Size
,这是不正确的(如下面的图像所示):
尽管枚举常量可以定义特定于常量的类体,包括变量和方法,但常量类型不够精确,无法访问特定于枚举常量的值。即使在泛型枚举的情况下,枚举常量的静态类型也不够精确,无法捕获个别常量的完整类型信息。
摘要
在本章中,你从 Java 5 中枚举如何引入类型安全到常量开始。你了解了枚举可以拥有状态和行为,不仅仅是适用于枚举中所有常量的行为;还可以是特定于常量的。然而,使用现有的枚举无法访问特定于枚举常量的状态和行为。
你了解了增强枚举如何使用泛型并允许访问特定于常量的状态和行为。通过示例,你也了解了当类型参数添加到枚举中时,如何促进枚举常量的更精确类型化。
第九章:数据类及其用法
在 Project Amber 项目中,正在对数据类进行工作。它提议通过引入具有关键字 record 的特殊类,为开发者提供一种简化的方式来建模数据作为数据。数据类的状态可以通过类头来捕获,这与现有的普通 Java 对象(POJO)目前提供的功能形成鲜明对比。
在本章中,我们将涵盖以下主题:
-
数据类的介绍
-
数据类的必要性及其局限性
-
数据类的聚合和展开形式
-
数据类与模式匹配
-
抽象数据类和接口的继承
-
添加变量和方法
-
覆盖默认行为
数据类的介绍
我们知道有两种版本的数据类——POJO(旧的和现有的方式)以及新提议的数据类。为了欣赏在 Project Amber 下进行工作的数据类,你需要了解现有 POJO 类的功能和局限性,以及为什么我们需要新提议的数据类。
POJO 不是使用语言结构实现的。拟议的数据类将包括对编程语言的更改或添加。
什么是数据类
作为 Java 开发者,你可能已经在你的某些或所有项目中使用并创建了 POJO。这是一个封装了一组数据的类,没有额外的行为来操作其状态。它通常包括构造函数、访问器、修改器和从对象类继承的覆盖方法,即——hashCode()
、equals()
和toString()
。访问器和修改器允许访问和分配状态变量。此外,修改器可能包括检查分配给实例状态的值范围的代码。以下是一个示例:
final class Emp { private String name; private int age; public Emp(String name, int age) { this.name = name; this.age = age; } // accessor methods - getName, getAge public String getName() { return name; } public int getAge() { return age; } // mutator methods - setName, setAge public void setName() { this.name = name; } public void setAge() { this.age = age; } public boolean equals(Object obj) { if (obj == null || (!(obj instanceof Emp))) return false; else { if ( ( ((Emp)obj).getName().equals(this.name) && ( ((Emp)obj).getAge() ) == this.age)) { return true; } else return false; } } public String toString() { return name + ":" + age; } public int hashCode() { // ..code }
}
其中一个案例是使用Emp
类将员工数据保存到您的数据库中。以下是一个示例:
interface EmpDAO { Emp read(); void write(Emp emp); List<Emp> getAllEmp();
}
同样,您也可以使用Emp
类将消息传递过去,通过网络发送,将其插入 JSON 对象,等等。
这看起来都很好。更重要的是,自从 Java 被引入开发者以来,这一直工作得很好。那么,问题是什么?
添加数据类到语言中的必要性
想象一下保卫一个国家的边境。通常的做法是由国防部队来守卫。安全水平是否会根据邻国之间的关系(友好、中立或紧张)而改变?如果边境是渗透的,或者说,例如,像西欧对申根国那样,会发生什么?现在,将保卫一个国家的边境与保卫我们的家园或,比如说,保卫房间里一个柜子的内容进行比较。
尽管前面示例中的每个实例都在谈论实体的安全性和其免受物理攻击的保护,但它们都有不同的需求。
类似地,到目前为止,Java 中的类被用来模拟广泛的需求。虽然这对于很多情况来说效果很好,但对于某些情况则不适用。如果你想使所有的大小都适合,你将需要对大多数进行调整。以下图像显示了使用相同裤子尺寸为身高和腰围各不相同的人穿裤子的情况:
在过去,枚举被添加到 Java 语言中(版本 5)。尽管一个类可以被编程来创建原始类型或对象的枚举,但枚举简化了开发者的过程。
枚举减少了开发者的编码工作。同时,它使枚举的意图对用户来说更加明确
在前面的章节中,Emp
POJO 只是一个数据的载体。然而,为了让一个类表现得像一个数据类,需要开发者定义多个方法——构造函数、访问器、修改器以及从对象类中来的其他方法。你可能会争辩说你可以使用 IDE 轻松地为你的类生成所有这些方法。你说得对!而且这样做很简单。
但它只关注代码的编写部分。对于类的用户来说,代码的阅读情况如何呢?作为开发者,我们都明白一段代码可能只编写一次,但它会被多次阅读。这就是为什么经验丰富的程序员强调良好的编码实践,以便理解、阅读和维护代码。
当数据类的定义被引入到语言中时,代码的读者就会知道它仅仅是一个数据类的明确意图。开发者不需要深入挖掘代码,寻找除了作为数据类之外的其他代码,这样他们就不会错过任何重要信息。
它还将防止开发者使用半成品类作为数据类。有时开发者倾向于使用可能不包括所有相关方法(如equals()
或hashCode()
)的类作为数据类,但这确实是在应用程序中插入细微错误的良方。像Map
这样的集合类需要类实现其equals()
和hashCode()
方法才能正常高效地工作。
通过语言的变化引入数据类将减少语言的冗长性,向所有人传达结构的目的。
深入了解数据类
定义数据类的语法看起来很简单。然而,语法和语义都很重要。
一个例子
让我们从重新定义Emp
类开始,这是我们在本章开头使用的,将其定义为数据类:
record Emp(String name, int age) { } // data class - one liner code
前面的代码使用关键字record
来定义数据类,接受逗号分隔的变量名和类型,这些是存储状态所必需的。编译器会自动为数据类生成默认实现 Object 方法(equals()
、hashCode()
、toString()
)。
代码看起来清晰且紧凑。读者会立即知道这一行代码的意图,这是一个携带数据name
(类型String
)和age
(类型int
)的数据载体。对读者来说另一个优点是,她不需要阅读构造函数、访问器、修改器或对象类的其他方法,只需确认它们正在做它们应该做的事情,不多也不少,这是他们应该知道的。
在幕后,Java 编译器将记录类Emp
转换为以下形式:
final class Emp extends java.lang.DataClass { final String name; final int age; public Emp(String name, int age) { this.name = name; this.age = age; } // deconstructor // public accessor methods // default implementation of equals, hashCode, and toString
}
前面的数据类是一个非抽象数据类的示例。数据类也可以定义为抽象数据类。非抽象数据类隐式为最终类。在两种情况下,数据类都会获得hashCode()
、equals()
和toString()
以及访问器方法的默认实现。对于抽象数据类,构造函数将是受保护的。
在以下图像中,编译器很高兴将数据类的单行代码转换为完整的类:
数据类隐式为最终类。
数据类的聚合形式和展开形式
数据类的聚合形式将是数据类的名称。其展开形式将指的是用于存储其数据的变量。从聚合到展开形式的转换也被称为解构模式。
参考我们前节中使用的示例:
record Emp(String name, int age) { }
Emp
是数据类Emp
的聚合形式。其展开形式将是String name
和int age
。语言需要在这两者之间提供简单的转换,以便它们可以与其他语言结构,如switch
一起使用。
解构模式指的是将数据类从聚合形式转换为展开形式。
局限性
当你使用关键字record
来定义你的数据类时,你将受到语言允许你做什么的限制。你将不再能精细控制你的数据类是否可扩展,其状态是否可变,你是否可以控制分配给字段值的范围,字段的可访问性。在添加额外字段或多个构造函数方面,你也可能受到限制。
数据类在 Oracle 仍在开发中。更详细的内容仍在完善中。就在一个月前,关键字datum
被用来定义数据类,现在已改为record
。
现在,开发者不再局限于使用单一编程语言。Java 程序员通常使用,或者了解在 JVM 上运行的 Scala、Kotlin 或 Groovy 等其他编程语言。使用不同语言的体验带来了对数据类(使用record
定义)的能力和限制的许多期望和假设。
过去的例子——定义枚举的变化
在枚举引入之前,开发者经常使用public
、static
和final
变量来定义常量。例如:
class Size { public final static int SMALL = 1; public final static int MEDIUM = 2; public final static int LARGE = 3;
}
使用public
、static
、final
、int
变量的主要缺点是类型安全,任何int
值都可以分配给类型为int
的变量,而不是Size.SMALL
、Size.MEDIUM
或Size.LARGE
常量。
Java 5 引入了枚举,这是语言结构的补充,使开发者能够定义常量的枚举。以下是一个快速示例:
enum Size {SMALL, MEDIUM, LARGE}
class SmallTShirt { Size size = Size.SMALL; //..other code
}
使用类型为Size
的变量时,赋值限于Size
中定义的常量。枚举是语言如何以一定的约束为代价简化模型实现的完美示例。枚举限制了可扩展性到接口。除此之外,枚举是完整的类。作为开发者,你可以向其添加状态和行为。另一个好处是枚举也可以在switch
结构中使用,这之前仅限于原始类型和String
类。
新的语言结构就像一种新的人际关系——无论是生物学上的还是其他方面的。它有自己的快乐和悲伤。
使用数据类进行模式匹配
当你使用record
关键字定义你的数据类时,你将获得转换数据类聚合和展开形式的额外优势。例如,以下是 switch 语句如何展开数据的示例:
interface Garment {}
record Button(float radius, Color color);
record Shirt(Button button, double price);
record Trousers(float length, Button button, double price);
record Cap(..) switch (garment) { case Shirt(Button(var a1, var a2), Color a3): ... case Trousers(float a1, Button(var a2, var a3), double a4): ... ....
}
switch 语句可以使用数据类,而不必使用其展开形式。以下也是可以的:
switch (garment) { case Shirt(Button a1, Color a2): ... case Trousers(float a1, Button a2, double a3): ... ....
}
封装状态
记录类封装了字段,提供了 JavaBean 风格访问器的默认实现(设置字段值的公共方法)。值可以在数据类实例初始化期间分配,使用其构造函数。
数据类和继承
目前,提议拒绝以下继承情况:
-
数据类扩展常规类
-
常规类扩展数据类
-
数据类可以扩展另一个数据类
允许上述任何一种情况都会违反数据类作为数据承载者的契约。目前,为数据类及其与接口和抽象数据类的继承提出以下限制:
-
非抽象和抽象数据类可以扩展其他抽象数据类
-
抽象或非抽象数据类可以扩展任何接口(s)。
以下图像总结了这些继承规则。
让我们从定义一个抽象数据类开始。
扩展抽象数据类
在以下示例中,Emp
抽象数据类正被非抽象数据类Manager
扩展:
abstract record Emp(String name, int age);
record Manager(String name, int age, String country) extends Emp(name, age);
当非抽象数据类扩展抽象数据类时,它接受其头部的所有数据,包括为其自身和其基类所必需的数据。
数据类可以扩展单个抽象数据类。
实现接口
数据类可以实现接口及其抽象方法,或者只是继承其默认方法。以下是一个示例:
interface Organizer {}
interface Speaker { abstract void conferenceTalk();
} abstract record Emp(String name, int age); record Manager(String name, int age, String country) extends Emp(name, age) implements Organizer; record Programmer(String name, int age, String programmingLang) extends Emp(name, age) implements Organizer, Speaker { public void conferenceTalk() { //.. code } };
数据类可以实现单个或多个接口。
额外变量
虽然允许,但在向数据类添加变量或字段之前,请自问——字段是否来源于状态? 不来源于状态的字段,对数据类初始概念的严重违反。以下是一个示例,它定义了一个额外的字段style
,该字段来源于数据类Emp
的状态:
record Emp(String name, int age) { private String style; Emp(String name, int age) { //.. initialize name and age if (age => 15 && age =< 30) style = "COOL"; else if (age >= 31 && age <= 50) style = "SAFE"; else if (age >= 51) style = "ELEGANT"; } public String getStyle() { return style; }
}
上述代码表现良好,因为数据类Emp
的状态仍然来源于其状态(字段name
和age
)。getStyle
方法不会干扰Emp
的状态,它纯粹是实现细节。
覆盖隐式行为
假设你希望在数据类实例化期间限制可以传递给其字段的值。这是可行的,只需覆盖默认构造函数。以下是一个示例:
record Emp(String name, int age) { // override default constructor @Override public Emp(String name, int age) { // validate age if (age > 70) throw new IllegalArgumentException("Not employable above 70 years"); else { // call default constructor default.this(name, age); } }
}
同样,你也可以覆盖对象方法的默认实现,如equals()
、hashCode()
和toString()
以及其他访问器方法。
覆盖数据类方法的默认行为并不会违背其创建的目的。它们仍然作为数据类工作,对它们的工作有更精细的控制。让我们将其与之前用于建模数据类的 POJOs 进行比较。编译器不会为 POJO 自动生成任何方法。因此,用户仍然需要阅读所有代码,寻找不是其方法默认实现的代码。在数据类的情况下,这种覆盖行为非常明确。因此,用户不必担心阅读所有代码,他/她可以假设行为有默认实现,该实现尚未被开发者覆盖。
明确地覆盖行为说明了数据类偏离其默认行为的地方,从而减少了用户阅读代码以理解其行为所需的代码量。
额外的方法和构造函数
编译器为数据类生成默认构造函数、访问器方法和从对象类继承的方法的默认实现。开发者可以覆盖构造函数并向数据类添加更多方法:
record Emp(String name, int age) { // overloading constructor public Emp(String name, String style) { this.name = name; if (style.equals("COOL") age = 20; else if (style.equals("SAFE") age = 30; else if (style.equals("ELEGANT") age = 50; else age = 70; } } public String fancyOutput() { // additional method return "My style is COOL"; }
}
可变性
关于数据类是否应指定为可变或不可变的工作仍在进行中。两者都有其自身的优缺点。不可变数据在多线程、并行或并发系统中表现良好。另一方面,可变数据也是数据。可变数据适用于需要频繁修改数据的情况。以下图像展示了这种混淆:
线程安全
由于数据类尚未指定为不可变,开发者有责任在使用它们时确保线程安全配置。
摘要
数据类提议为开发者提供一种简单简洁的方式来模型化数据作为数据。它将包括通过引入关键字 record 的语言变化。Oracle 对数据类的工作仍在进行中。提议的数据类与现有的 POJOs 不同,POJOs 是完整的类,开发者将其建模为数据类。数据类将使用关键字 record 在其类头中封装数据。
到目前为止,类已经被用来模型化和实现广泛的需求。尽管这已经工作得很好,但它确实需要在某些地方做额外的工作,那里的成本远远超过了它提供的利益。通过过去的例子(枚举),你已经了解了添加一个功能如何使开发者能够以简洁的方式定义实体,但可能会失去一些更精细的控制。同样,数据类提供了一种简化封装一组数据的方式。
你已经了解了数据类的必要性及其在减少任何开发者需要阅读的代码中的重要性。数据类的主要目标是模型化数据作为数据,而不是减少样板代码。
Java 平台提供了访问数据类状态的方法、默认构造函数以及类对象方法(equals()
、hashCode()
和toString()
)的默认实现。你可以在数据类中添加字段或行为(包括构造函数),并且也可以覆盖所有方法的默认实现。数据类可以被定义为抽象的。它们可以扩展抽象类并实现接口。
你已经了解了数据类的聚合形式和展开形式。数据类可以与其他语言结构,如 switch 一起使用。
默认情况下,数据类是不可变的,包括定义为数据成员的数组。由于这些结构不是不可变的,当与它们一起工作时,开发者必须包含代码以确保线程安全。
第十章:原始字符串字面量
即将推出...!
第十一章:Lambda 剩余
即将推出...!
第十二章:模式匹配和开关表达式
即将推出...!
第十三章:基于价值的优化
即将推出...!
第十四章:泛型改进
即将推出...!
第十五章:过滤器和延续
即将推出..!
第十六章:JVM 和本地代码
即将推出...!