剖析 Java15 新语法特性

用户头像
高翔龙
关注
发布于: 2020 年 09 月 30 日
剖析Java15新语法特性

前言

9月15日,Java社区正式发布了Java15的GA版本,这意味着大家欠Oracle的技术债开始变得越来越多。在之前的新时代背景下的Java语法特性一文中,我为大家详细介绍了Java9~14的语法全貌;而本文,我会紧接上一篇博文的内容,继续为大家剖析Java15带给我们的改变。

进一步强化的instanceof运算符

早期,如果我们需要在程序中对某个引用类型进行强制类型转换,通常情况下,为了避免在转换过程中出现类型不匹配的转换错误,我们往往会使用instanceof运算符验证目标对象是否是特定类型的一个实例,只有当表达式条件成立时,才允许进行强制类型转换,示例1-1:

if (obj instanceof String) {
String temp = (String) obj;
} else {
//...
}



上述程序示例中,整个转换过程总共需要经历2个步骤,首先需要使用instanceof关键字来匹配目标对象,当条件成立后,再使用一个新的局部变量来接收强转后的值。平心而论,这样的写法着实有点冗余,因此,从Java14开始,Java的设计者们开始对instanceof运算符进行了进一步的升级强化,为开发人员提供了模式匹配功能,以便于简化样板代码的编写。示例1-2:

if (obj instanceof String str) {
//TODO 变量str的作用域仅限if代码块
} else {
//...
}



很明显,使用instanceof的模式匹配功能后,弱化了原本代码样板化严重的问题。instanceof关键字的右边允许开发人员直接声明变量,当满足条件后,编译器会隐式强转并为其赋值。在上一篇博文中,我之所以没有为大家分享这项新特性,是因为它当时还仅仅只是个预览功能;尽管Java15它仍未转正,但从JEP提案中可以明确得知,这项语法特性在未来发生改动的可能性较小。

扩展限制支持

在谈扩展限制之前,我们首先回顾下Java访问修饰符是如何控制资源的访问权限的。当资源被声明为public时,意味着资源对所有类可见;当资源被声明为protected时,意味着仅同包类,或派生类可见;当资源被声明为default时,意味着仅同包类可见;而当资源被声明为private时,仅同类可见。



在某些情况下,如果我们希望所定义的超类具备扩展限制时,通常会采用如下2种方式:

  • 将超类定义为final;

  • 将超类的构造函数声明为default。



如果我们将超类定义为final后,那么任何类都不允许对其进行继承,也就是说,超类将不提供任何扩展支持,很明显,这样的做法显然无法有效支撑某些特定场景(比如:我们希望超类仅允许被固定的几个子类扩展)。而如果将超类的构造函数声明为default时,尽管在一定范围内可以限制超类的扩展权限(同包类可见),但如果其它包中的子类也需要对其进行扩展时则显得无能为力。因此,在Java15诞生之前,仅凭现有的技术手段均无法有效的为开发人员提供一种声明式的方式来合理限制超类对子类的扩展能力。



值得庆幸的是,Java15的到来很好的满足了上述需求,为开发人员在语法层面上提供了sealed关键字来支持这项特性,示例1-3:

public sealed interface Animal permits Tiger, Lion {}
final class Tiger implements Animal {}
final class Lion implements Animal {}



上述程序示例中,我们通过sealed关键字定义了一个具备扩展限制的超类,并在classname之后使用permits关键字来限制其扩展范围;也就是说,只有在限制范围内的固定子类才允许对超类进行扩展,否则编译器将会出现编译错误。当然,permits关键字并非是强制的,当我们所定义的子类为嵌套内部类时,编译器会在编译时进行自动类型推断,示例1-4:

public sealed interface Animal{}
final class Tiger implements Animal {}
final class Lion implements Animal {}



相信大家也看见了,示例1-3和1-4中,子类均使用了final关键字来进行修饰,这是为何?实际上,这与sealed类的约束有关。我们使用sealed的初衷是希望对超类的扩展做出限制,在开发过程中不允许任何类都对其进行继承或实现,因此对于sealed类的子类而言,就需要追加3个约束。首先是超类和子类必须被在同包内,如果要解除这个限制,就必须被包含在同一模块中,示例1-5:

module name {}



其次,permits关键字包含的子类都必须显示extends或者implements。最后,子类都必须定义一个特定的修饰符来描述是否延续超类的扩展限制;也就是说,子类也可以使用sealed、permits关键字来定义它的下级派生,并且也可以使用final关键字来禁止所有子类继承于它,甚至还可以使用non-sealed关键字来解除所有限制(如果这么做,扩展限制将失去意义)。具体怎么定义,还需要结合具体的业务场景而定,示例1-6:

public sealed interface Animal{}
/**
* 子类也可以定义sealed来延续超类的扩展限制
*/
sealed class Tiger implements Animal permits NortheastTiger {}
final class NortheastTiger extends Tiger{}
/**
* non-sealed解除所有限制
*/
non-sealed class Lion implements Animal {}
class PumaConcolor extends Lion{}

进一步强化的String类型

早在Java13的时候,Java就已经开始支持多行字符串的文本块语法定义,尽管在当时这项语法特性还仅仅只是个预览功能,但也足以让大家期待;而随着Java15的正式来临,多行文本块已经被JDK正式支持。关于多行文本块的具体使用,大家可以参考我的上一篇博文。

数据载体类支持

实际上,这项语法特性也是为了给大家“减负”使用的,目的就是为了减少样板代码的编写。在实际开发过程中,我们往往会在业务代码中定义非常多的POJO类,比如:Controller和Service之间的DTO对象、持久层的PO对象等。但是这类POJO类型本身并不会处理复杂的业务逻辑,也不会包含任何行为,其作用纯粹就是作为一个数据载体,以便于数据的传输或访问处理;但我们仍然每天都需要不胜其烦的编写一大堆样板代码,比如:setter、getter、hashCode,以及equals等。在此大家需要注意,如果一个POJO类中存在较多的字段,会严重影响其维护性和可读性,示例1-7:

public class UserEntity {
private int id;
private String account, pwd;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof UserEntity)) return false;
UserEntity that = (UserEntity) o;
return getId() == that.getId() &&
Objects.equals(getAccount(), that.getAccount()) &&
Objects.equals(getPwd(), that.getPwd());
}
@Override
public int hashCode() {
return Objects.hash(getId(), getAccount(), getPwd());
}
//省略setter/getter方法
}



为了减少样板代码的编写,Java的设计者们为开发人员提供了record关键字,专用于定义不可变的数据载体类,示例1-8:

public record UserEntity(int id, String account, String pwd) {}



上述程序示例中,我们仅通过一行代码即可完成一个POJO类的定义,大幅减少了样板代码的编写。通过record关键字定义的POJO类在对其进行反编译后我们不难发现,编译器在编译时,仍然会将其编译为一个标准的Java类型,相当于隐式帮我们实现了Lombok的功能示例1-9:

public final class UserEntity extends java.lang.Record {
private final int id;
private final java.lang.String account;
private final java.lang.String pwd;
public UserEntity(int id, java.lang.String account, java.lang.String pwd) { /* compiled code */ }
public final java.lang.String toString() { /* compiled code */ }
public final int hashCode() { /* compiled code */ }
public final boolean equals(java.lang.Object o) { /* compiled code */ }
public int id() { /* compiled code */ }
public java.lang.String account() { /* compiled code */ }
public java.lang.String pwd() { /* compiled code */ }
}



上述程序示例中,所有的字段都被声明为了final,这也说明,record类是专用于定义不可变数据。在此大家需要注意,在使用record类时,有几点是必须注意的,如下所示:

  • record类中不允许定义实例字段,但允许定义静态字段;

  • record类中允许定义静态方法和实例方法,以便于实现一些特定的数据加工任务;

  • record类中允许定义构造函数。



或许大家存在疑问,早期我们在定义POJO类时,如果遇到有很多可选参数时,往往会采用重载构造函数的方式来解决,但如果使用record类后,我们应该如何解决这个问题?实际上,既然record类允许我们定义构造函数,那这就意味着同样可以通过相似的技术手段来解决共性问题,但实际开发过程中,我更建议大家使用Builder模式,示例1-10:

public record UserEntity(int id, String account, String pwd) {
private UserEntity(Builder builder) {
this(builder.id, builder.account, builder.pwd);
}
static class Builder {
int id;
String account, pwd;
Builder(String account, String pwd) {
this.account = account;
this.pwd = pwd;
}
Builder id(int id) {
this.id = id;
return this;
}
public UserEntity build() {
return new UserEntity(this);
}
}
public static void main(String[] args) {
var userEntity = new Builder("gxl", "123456").id(100).build();
System.out.println(userEntity.toString());
}
}



至此,本文内容全部结束。如果在阅读过程中有疑问,欢迎加入微信群聊和小伙伴们一起参与讨论。

码字不易,欢迎转发

发布于: 2020 年 09 月 30 日 阅读数: 2682
用户头像

高翔龙

关注

The best way out is always through 2020.03.25 加入

《超大流量分布式系统架构解决方案》、《人人都是架构师》、《Java虚拟机精讲》作者,GIAC全球互联网架构大会讲师

评论 (1 条评论)

发布
用户头像
都是语法糖
2020 年 10 月 12 日 11:45
回复
没有更多了
剖析Java15新语法特性