近几年来,JDK秉承着“半年一个release,三年一个LTS”的发布原则,目前最新的LTS版本是Java 17(2021/09)。
注: LTS
即Long Term Support,长期支持版本。
最近一段时间,项目组终于打破了“你发任你发,我用Java8”的传统,准备升级到Java 17,所以我也来学习一下Java新版本的特性,看看Java 17(严格讲是从9到17)给我们带来了哪些新特性。
Java语言升级的官方文档,请参考: https://docs.oracle.com/en/java/javase/17/language/java-language-changes.html
record (Java 16)
record
是一种特殊的类,它是简单的数据聚集的建模,比传统意义上的数据类减少了很多“繁文缛节”。
这些繁文缛节,包含了访问器(Getter/Setting方法),构造器, equals()
方法, hashCode()
方法, toString()
方法等。使用 record
时无需关心这些,应该有的东西它会自动帮你弄好。
例如:
public record MyRecord(int id, String name) {
}
MyRecord myRecord1 = new MyRecord(1, "Tom");
在构造器中定义好数据,接下来就可以用 myRecord1.id()
和 myRecord.name()
来访问数据了。
注意,不是 myRecord1.id
,也不是 myRecord1.getId()
。
我们并没有给 MyRecord
定义构造器,系统会自动把id和name保存到MyRecord里。
当然也可以自定义构造器,例如:
public record MyRecord(int id, String name) {
public MyRecord(int id, String name) {
this.id = id;
this.name = name;
}
}
这种构造器称为canonical(规范) constructor。
注意:上例中的构造器完全可以省略。IntelliJ IDEA会提示“Redundant canonical constructor”。
这是因为构造器只做了赋值操作。如果还做一些其它事情,比如:
public record MyRecord(int id, String name) {
public MyRecord(int id, String name) {
System.out.println("hello " + name);
this.id = id;
this.name = name;
}
}
现在,IntelliJ IDEA会提示“Canonical constructor can be converted to compact form”。确认后,会变成如下称为compact(紧凑) constructor的形式:
public record MyRecord(int id, String name) {
public MyRecord {
System.out.println("hello " + name);
}
}
注意,record的所有field都是final的,不能改变,因为record的意图只是简单的“数据载体”。
instanceof (Java 16)
我们都很熟悉以下代码:
public void f(Object obj) {
if (obj instanceof MyObj) {
MyObj myObj = (MyObj) obj;
myObj.doSth();
}
}
这种“先判断,再强转”的方式,现在改进如下:
public void f(Object obj) {
if (obj instanceof MyObj myObj) {
myObj.doSth();
}
}
注意,新旧方式都无需判断null,如果 obj
是null,不会出现空指针异常。
myObj
被称为 Pattern variable
。
可以用 final
修饰 myObj
,标识其不可更改:
public void f(Object obj) {
if (obj instanceof final MyObj myObj) {
myObj.doSth();
// myObj = new MyObj(); // 编译错误
}
}
上面的例子只是减少了一行代码,下面的代码改写后,可读性更佳:
public void f(Object obj) {
if (obj instanceof MyObj) {
MyObj myObj = (MyObj) obj;
if ("aaa".equals(myObj.getName())) {
myObj.doSth();
}
}
}
改写后如下:
public void f(Object obj) {
if (obj instanceof MyObj myObj && "aaa".equals(myObj.getName())) {
myObj.doSth();
}
}
Stream API (Java 16)
假如有一个MyObj的list,需要过滤出age大于20的元素,代码如下:
List<MyObj> list2 = list.stream().filter(e -> e.getAge() > 20).collect(Collectors.toList());
如果我们需要一个不可修改的list,则代码改写如下:
List<MyObj> list2 = Collections.unmodifiableList(list.stream().filter(e -> e.getAge() > 20).collect(Collectors.toList()));
或者简写如下:
List<MyObj> list2 = list.stream().filter(e -> e.getAge() > 20).collect(Collectors.toUnmodifiableList());
甚至还可以更进一步简写如下:
List<MyObj> list2 = list.stream().filter(e -> e.getAge() > 20).toList();
Text block (Java 15)
即文本块。对于一段email模板或者JSON数据,例如:
{
"info": [
{
"name": "张三",
"sex": "男",
"age": 20
},
{
"name": "李四",
"sex": "女",
"age": 30
},
{
"name": "王五",
"sex": "男",
"age": 40
}
]
}
这是一段格式良好的JSON数据,在Java中用字符串表示为:
String str = "{\n" +
"\t\"info\": [{\n" +
"\t\t\t\"name\": \"张三\",\n" +
"\t\t\t\"sex\": \"男\",\n" +
"\t\t\t\"age\": 20\n" +
"\t\t},\n" +
"\t\t{\n" +
"\t\t\t\"name\": \"李四\",\n" +
"\t\t\t\"sex\": \"女\",\n" +
"\t\t\t\"age\": 30\n" +
"\t\t},\n" +
"\t\t{\n" +
"\t\t\t\"name\": \"王五\",\n" +
"\t\t\t\"sex\": \"男\",\n" +
"\t\t\t\"age\": 40\n" +
"\t\t}\n" +
"\t]\n" +
"}";
像这样把转义符 \n
、 \t
、 \"
和字符串内容混杂在一起的方式,非常不直观。
从Java 15开始,引入了“文本块”的概念,例如:
String str = """
{
"info": [
{
"name": "张三",
"sex": "男",
"age": 20
},
{
"name": "李四",
"sex": "女",
"age": 30
},
{
"name": "王五",
"sex": "男",
"age": 40
}
]
}
""";
简言之,就是“所见即所得”,大大提高了直观性。
注意事项:
- 字符串和开头的
"""
之间必须换行,否则报错; - 字符串和结尾的
"""
之间可以不换行。事实上如果有换行,则字符串末尾也会多出一个空行; - 文本的缩进,取决于它们和第一行文本的相对位置。在本例中,第二行的
"info": [
前面有8个空格,但是相对于第一行的{
,只有4个空格,所以实际上第二行文本前面是4个空格; - 每行文本末尾的空格将被自动移除。例如,对本例的本文块,如果把第三行的
{
之后加几个空格,则实际得到的字符串里并没有这些空格; - 文本块里面也支持像
%s
、%d
之类的格式化;
sealed class (Java 17)
在父类/子类继承关系中,通常是子类在定义中指明其派生自哪个父类,而父类则并不知道自己被哪些子类所继承。
如果你想明确指定某一个类可被哪些子类所继承,则可以用 sealed class
。
例如,对于 Fruit
类,只允许 Apple
、 Banana
、 Orange
这3个子类继承:
Fruit
类的定义如下:
public sealed class Fruit permits Apple, Banana, Orange {
}
Fruit
的子类在定义时,必须包含以下三个修饰符之一:
sealed
:子类也是sealed class,必须显式指定其子类;non-sealed
:普通类;final
:不可再被继承;
Apple
类被定义为 sealed
:
public sealed class Apple extends Fruit permits Fuji {
}
此处 Fuji
也同理,其定义必须包含三个修饰符之一。
Banana
类被定义为 non-sealed
:
public non-sealed class Banana extends Fruit {
}
Orange
类被定义为 final
:
public final class Orange extends Fruit {
}
Switch Expression (Java 14)
看下面的代码:
public String getDay(int day) {
String result = "";
switch (day) {
case 0:
result = "Sunday";
break;
case 1:
result = "Monday";
break;
case 2:
result = "Tuesday";
break;
case 3:
result = "Wednesday";
break;
case 4:
result = "Thursday";
break;
case 5:
result = "Friday";
break;
case 6:
result = "Saturday";
break;
default:
result = "Unknown";
}
return result;
}
这是一个典型的switch语句,注意别忘了 break
。
新的写法如下:
public String getDay(int day) {
String result = switch (day) {
case 0 -> "Sunday";
case 1 -> "Monday";
case 2 -> "Tuesday";
case 3 -> "Wednesday";
case 4 -> "Thursday";
case 5 -> "Friday";
case 6 -> "Saturday";
default -> "Unknown";
};
return result;
}
这看起来类似于Lamda表达式,注意在各个case分支之间不需要 break
。
另外,如果一个case分支里有多个语句,写法如下:
......
case 0 -> {
System.out.println(day);
yield "Sunday";
}
......
同样,这跟Lamda写法是一致的。但要注意这里不是 return
而是 yield
。
Local Variable Type Inference (Java10)
局部变量类型推断。看下面的代码:
List<String> list1 = List.of("aaa", "bbb");
List<String> list2 = List.of("ccc", "ddd");
List<List<String>> list3 = List.of(list1, list2);
现在,这段代码可以简写为:
var list1 = List.of("aaa", "bbb");
var list2 = List.of("ccc", "ddd");
var list3 = List.of(list1, list2);
相比较一下,是不是清爽了很多呢。
注意,类型推断发生在编译期,这意味着编译器知道变量的确切类型,所以仍然可以把 list3
看作一个List:
list3.forEach(e -> e.forEach(System.out::println));
结果如下:
aaa
bbb
ccc
ddd
不能给var变量赋null值,因为编译器无法推断 null
的类型:
var obj = null; // 编译错误
var变量的类型实际上是确定的,不能自相矛盾:
var obj = 1;
obj = "aaa"; // 编译错误
obj
不能一会儿是数值,一会儿是字符串。
注意: var
并不是一个关键字(为啥呢?):
var var = 1; // OK
此处把变量命名为 var
,这是OK的(好像是自找麻烦……)。
copyOf() (Java 10)
public void consumeList(List<String> list) {
list.add("ccc");
}
public void test() {
List<String> list = new ArrayList<String>();
list.add("aaa");
list.add("bbb");
consumeList(list);
System.out.println(list);
}
在 test()
方法里创建了一个List,然后调用 consumeList()
方法来消费该list,然而, consumeList()
方法改变了List的内容,这可能不是期望的行为。为了避免这样的行为,可用List的 copyOf()
方法来生成一个不可改变的List:
consumeList(List.copyOf(list));
运行程序,则会在 list.add("ccc");
处抛出 java.lang.UnsupportedOperationException
异常。
注意,用 List.of()
方法创建的List,也是不可改变的List。
List<String> list = List.of("xxx", "yyy");
list.add("zzz"); // 运行期报错:aUnsupportedOperationException
其它集合类,比如 Set
和 Map
,也有类似的方法,不再赘述。
Predicate.not() (Java 11)
假定你有一个数值的List:
List<Integer> list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
现在要过滤出所有的偶数:
list = list.stream().filter(e -> e % 2 == 0).toList();
如果反过来,要过滤所有的非偶数(即奇数,这么说是为了表示取反)。
list = list.stream().filter(((Predicate<Integer>) (e -> e % 2 == 0)).negate()).toList();
这里用到了Predicate的 negate()
方法来取反。
注意,Predicate前面加了强制类型转换,否则就报错。。。可以换种写法,看的更清楚:
Predicate<Integer> predicate = e -> e % 2 == 0;
list = list.stream().filter(predicate.negate()).toList();
这里显式定义了一个Predicate变量,并调用其 negate()
方法来取反。
使用 Predicate.not()
方法的写法如下:
list = list.stream().filter(Predicate.not(e -> e % 2 == 0)).toList();
如果Predicate实际上是一个方法:
public static boolean isEven(Integer e) {
return e % 2 == 0;
}
则过滤偶数的代码如下:
list = list.stream().filter(TestPredicateNot::isEven).toList();
注:此处 TestPredicateNot
是该方法定义所在的类。
过滤奇数的代码如下:
list = list.stream().filter(((Predicate<Integer>)(TestPredicateNot::isEven)).negate()).toList();
同理,此处必须强转,否则报错。
如果使用 Predicate.not()
方法,代码如下:
list = list.stream().filter(Predicate.not(TestPredicateNot::isEven)).toList();
CompletableFuture.completeOnTimeout (Java 9)
在 main()
方法中启动一个异步线程来做一些事情,在本例中只是sleep了5秒,同时在主线程中sleep 10秒,为的是让异步线程先结束:
public static void handleResult(String result) {
System.out.println(new Date() + ": ==========result: " + result + "===========");
}
public static void main(String[] args) {
System.out.println(new Date() + ": main start");
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
System.out.println(new Date() + ": thread start");
try {
Thread.sleep(5 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(new Date() + ": thread end");
return "hello";
});
future.thenAccept(e -> handleResult(e));
try {
Thread.sleep(10* 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(new Date() + ": main end");
}
程序运行结果如下:
Sat Feb 26 22:05:31 CST 2022: main start
Sat Feb 26 22:05:31 CST 2022: thread start
Sat Feb 26 22:05:36 CST 2022: thread end
Sat Feb 26 22:05:36 CST 2022: ==========result: hello===========
Sat Feb 26 22:05:41 CST 2022: main end
OK,一切都是期望结果。
现在,给异步线程设置一个3秒钟的timeout时间。把下面的代码加到 future.thenAccept(e -> handleResult(e));
之前:
future.completeOnTimeout("default timeout result", 3, TimeUnit.SECONDS);
程序运行结果如下:
Sat Feb 26 22:10:26 CST 2022: main start
Sat Feb 26 22:10:27 CST 2022: thread start
Sat Feb 26 22:10:30 CST 2022: ==========result: default timeout result===========
Sat Feb 26 22:10:32 CST 2022: thread end
Sat Feb 26 22:10:37 CST 2022: main end
可见,timeout并不会真正停止task的运行,也不会给task发interrupt信号。
关于CompletableFuture的详细介绍,参见我另一篇文档。
String.formatted() (Java 15)
假定字符串模板定义如下:
String template = "Hello, my name is %s, and I am %d years old.";
要想填充姓名和年龄:
String result = String.format(template, "Tom", 20);
新方法要更简洁一些:
String result = template.formatted("Tom", 20);