动力节点王鹤SpringBoot3学习笔记——JDK新特性

时间:2021-08-02 01:07:02

一、JDK关注的新特性

1.1搭建学习环境

JDK:JDK19

OpenJDK: https://jdk.java.net/19/

Liberica JDK: https://bell-sw.com/pages/downloads/ , 是一个OpenJDK发行版,为云原生,容器特别优化。 Maven:构建和依赖管理,版本选择3.6以上

配置本地仓库和阿里云镜像 IDEA2022.3.1Ultimate:主要的开发工具,我是30天试用版本

数据库:MySQL 5以上版本

火狐浏览器:版本用比较新的,中文版本。

文本工具:EditPlus, Sublime任意。


1.2有用的新特性

JDK8-19新增了不少新特性,这里我们把实际常用的新特性,给大家介绍一下。 包括以下几个方面:

  • Java Record
  • Swich开关表达式
  • Text Block文本块
  • var 声明局部变量
  • sealed 密封类

1.2.1 Java Record

Java14中预览的新特性叫做Record,在Java中,Record是一种特殊类型的Java类。可用来创建不可变类,语法简短。参考JEP 395. Jackson 2.12支持Record类。

任何时候创建Java类,都会创建大量的样板代码,我们可能做如下:

  • 每个字段的set,get方法
  • 公共的构造方法
  • 重写hashCode, toString(), equals()方法

Java Record避免上述的样板代码,如下特点:

  • 带有全部参数的构造方法
  • public访问器
  • toString(),hashCode(),equals()
  • 无set,get方法。没有遵循Bean的命名规范
  • final类,不能继承Record,Record为隐士的final类。除此之外与普通类一样
  • 不可变类,通过构造创建Record
  • final属性,不可修改
  • 不能声明实例属性,能声明static成员

IDEA创建新的Maven工程 Lession01-feature

1.2.1.1 看看Record怎么用

IDEA新建Class,选择类Record

动力节点王鹤SpringBoot3学习笔记——JDK新特性

step1: 创建Student Record

public record Student(Integer id,String name,String email,Integer age) {
}

step2:创建Record对象


public static void main(String[] args) {

Student lisi = new Student(1001, "lisi","lisi@qq.com",20);
System.out.println("lisi = " + lisi.toString());

Student zhangsan = new Student(1002, "zhangsan","lisi@qq.com",20);    System.out.println("zhangsan = " + zhangsan.toString());    System.out.println("lisi.equals(zhangsan) = " + lisi.equals(zhangsan));    System.out.println("lisi.name() = " + lisi.name());
System.out.println("zhangsan.name() = " + zhangsan.name());

}

现在能查看控制台输出:

lisi = Student[id=1001, name=lisi, email=lisi@qq.com, age=20]

zhangsan = Student[id=1002, name=zhangsan, email=lisi@qq.com, age=20]

lisi.equals(zhangsan) = false

lisi.name() = lisi

zhangsan.name() = zhangsan

Record通过构造方法创建了只读的对象,能够读取每个属性,不能设置新的属性值。 Record用于创建不可变的对象,同时减少了样板代码。

Record对每个属性提供了public访问器,例如lisi.name()

1.2.1.2Instance Methods 

Record是Java类,和普通Java类一样定义方法。下面定义方法concat,将姓名和年龄一起打印输出。

我们创建普通的方法concat,将name和age连接为一个字符串输出。 

step1:创建实例方法


public record Student(Integer id,String name,String email,Integer age) {

public String concat(){

return String.format("姓名:%s,年龄是:%d", this.name,this.age);

}
}

step2: 调用实例方法


public static void main(String[] args) {

Student lisi = new Student(1001, "lisi","lisi@qq.com",20);

String nameAndAge = lisi.concat();

System.out.println( nameAndAge);

}

最后控制台输出: 姓名:lisi,年龄是:20

1.2.1.3静态方法 Static Method

Record类定义静态方法,试用静态方法与普通类一样。

step1: 创建静态方法


public record Student(Integer id,String name,String email,Integer age) {

public String concat(){

return String.format("姓名:%s,年龄是:%d", this.name,this.age);

}

/**  静态方法  */

public static String emailUpperCase(String email){

return Optional.ofNullable(email).orElse("no email").toUpperCase();

}
}

step2:测试静态方法


public static void main(String[] args) {

String emailUpperCase = Student.emailUpperCase("lisi@163.com");    System.out.println("emailUpperCase = " + emailUpperCase);

}

1.2.1.4 Record的构造方法

我们可以在Record中添加构造方法, 有三种类型的构造方法分别:是紧凑的,规范的和定制构造方法

  • 紧凑型构造方法没有任何参数,甚至没有括号。
  • 规范构造方法是以所有成员作为参数
  • 定制构造方法是自定义参数个数

step1: 紧凑和定制构造方法


public record Student(Integer id,String name,String email,Integer age) {
/紧凑构造方法/

public Student {

System.out.println("id"+ id );

if( id < 1 ){

throw new RuntimeException("ok");

}

}

/自定义构造方法/

public Student(Integer id, String name) {

this(id, name, null, null);

}
}

step2:编译Student.java -> Student.class

public record Student(Integer id, String name, String email, Integer age) {
/** 紧凑构造方法和规范构造方法合并了 */

public Student(Integer id, String name, String email, Integer age) {

System.out.println("id" + id);

if (id < 1) {

throw new RuntimeException("ok");    }
else {

this.id = id;

this.name = name;

this.email = email;

this.age = age;

}

}
public Student(Integer id, String name) {

this(id, name, (String)null, (Integer)null);

}
}

1.2.1.5Record与Lombok

Java Record是创建不可变类且减少样板代码的好方法。Lombok是一种减少样板代码的工具。两者有表面上的重叠部分。可能有人会说Java Record会代替Lombok. 两者是有不同用途的工具。

Lombok提供语法的便利性,通常预装一些代码模板,根据您加入到类中的注解自动执行代码模板。这样的库纯粹是为了方便实现POJO类。通过预编译代码。将代码的模板加入到class中。

Java Record是语言级别的,一种语义特性,为了建模而用,数据聚合。简单说就是提供了通用的数据类,充当“数据载体",用于在类和应用程序之间进行数据传输。

1.2.1.6Record实现接口

Java Record可以与普通类一样实现接口,重写接口的方法。

step1: 创建新的接口,定义一个规范方法。


public interface PrintInterface {

/** 输出自定义描述信息 */

void print();
}

step2: 创建新的Record实现接口,重写接口的方法,实现当前Record有关的业务逻辑


public record ProductRecord(String id,String name,Integer qty)
implements PrintInterface {
@Override

public void print() {
String productDesc = String.join("-", id, name, qty.toString());
System.out.println("商品信息 = " + productDesc);
}
}

ProductRecord实现print()方法,打印商品详情。

step3:测试print方法


public static void main(String[] args) {

ProductRecord product = new ProductRecord("P001", "手机", 100);
product.print();

}

1.2.1.7Local Record

Record可以作为局部对象使用。在代码块中定义并使用Record,下面定义一个SaleRecord

step1:定义Local Record


public static void main(String[] args) {

//定义Java Record

record SaleRecord(String saleId,String productName,Double money){};

//创建Local Record

SaleRecord saleRecord = new SaleRecord("S22020301", "手机", 3000.0);

//使用SaleRecord

System.out.println("销售记录 = " + saleRecord.toString());
}

控制台输出: 销售记录 = SaleRecord[saleId=S22020301, productName=手机, money=3000.0]

1.2.1.8嵌套Record

多个Record可以组合定义, 一个Record能够包含其他的Record。

我们定义Record为Customer,存储客户信息,包含了Address和PhoneNumber两个Record

step1:定义Record

public record Address(String city,String address,String zipcode) {}

public record PhoneNumber(String areaCode,String number) {}

public record Customer(String id,  String name,  PhoneNumber phoneNumber,                       
Address address) {}

step2: 创建Customer对象


public static void main(String[] args) {

Address address = new Address("北京", "大兴区凉水河二街-8号10栋三层", "100176");PhoneNumber phoneNumber = new PhoneNumber("010", "400-8080-105");
Customer customer = new Customer("C1001", "李项", phoneNumber, address);
System.out.println("客户 = " + customer.toString());

}

控制台输出: 客户 = Customer[id=C1001, name=李项, phoneNumber=PhoneNumber[areaCode=010, number=400-8080-105], address=Address[city=北京, address=大兴区凉水河二街9号10栋三层, zipcode=100176]]

1.2.1.9instanceof 判断Record类型

instanceof 能够与 Java Record一起使用。编译器知道记录组件的确切数量和类型。

step1:声明Person Record,拥有两个属性name和age


public record Person(String name,Integer age) {
}

step2: 在一个业务方法判断当是Record类型时,继续判断age年龄是否满足18岁。


public class SomeService {

public boolean isEligible(Object obj){

// 判断obj为Person 记录类型

if( obj instanceof Person(String name, Integer age)){

return age >= 18;

}

return false;
}
}

instanceof 还可以下面的方式


if( obj instanceof Person(String name, Integer age) person){

return person.age() >= 18;
}
或者

if( obj instanceof Person p){

return p.age() >= 18;
}

step3: 测试代码

public static void main(String[] args) {
SomeService service = new SomeService();

boolean flag = service.isEligible(new Person("李四", 20));

System.out.println("年龄符合吗?" + flag);
}

控制台输出: 控制台输出flag为true

处理判断中Record为null

Java Record能够自动处理null。

step1:record为null


public static void main(String[] args) {
SomeService service = new SomeService();
boolean eligible = service.isEligible(null);

System.out.println("年龄符合吗?" + eligible);
}

控制台输出eligible为false ,Debug调试代码,发现if语句判断为false,不执行 。

总结

  • abstract类java.lang.Record是所有Record的父类。
  • 有对于equals(),hashCode(),toString()方法的定义说明
  • Record类能够实现 java.io.Serializable序列化或反序列化
  • Record支持泛型,例如 record Gif( T t ) { }
  • java.lang.Class类与Record类有关的两个方法:


boolean isRecord() : 判断一个类是否是Record类型
RecordComponent[] getRecordComponents():Record的数组,表示此记录类的所有记录组件
Customer customer = new Customer(....);

RecordComponent[] recordComponents = customer.getClass().getRecordComponents();
for (RecordComponent recordComponent : recordComponents) {

System.out.println("recordComponent = " + recordComponent);

}

boolean record = customer.getClass().isRecord();

System.out.println("record = " + record);

1.2.2 Switch

Switch的三个方面,参考:JEP 361

  • 支持箭头表达式
  • 支持yied返回值
  • 支持Java Record

1.2.2.1 箭头表达式,新的case标签

Switch新的语法,case label -> 表达式|throw 语句|block

case label_1, label_2, ..., label_n -> expression;|throw-statement;|block

step1:新的case 标签

week:表示周日(1)到周六(7),1和7是休息日,其他是工作日。如果1-7以外为无需日期


public static void main(String[] args) {

int week = 7;

String memo = "";

switch (week){

case 1 -> memo = "星期日,休息";

case 2,3,4,5,6-> memo="工作日";

case 7 -> memo="星期六,休息";

default ->  throw new IllegalArgumentException("无效的日期:");

}

System.out.println("week = " + memo);

}

1.2.2.2 yeild返回值

yeild让switch作为表达式,能够返回值

语法

变量 = switch(value) { case v1: yield 结果值; case v2: yield 结果值;case v3,v4,v5.. yield 结果值 }

示例: yield返回值,跳出switch块

public static void main(String[] args) {
    int week = 2;
    //yield是switch的返回值, yield跳出当前switch块
    String memo  = switch (week){
      case 1: yield "星期日,休息";
      case 2,3,4,5,6: yield "工作日";
      case 7: yield "星期六,休息";
      default: yield "无效日期";
    };

    System.out.println("week = " + memo);
}


无需中间变量, switch作为表达式计算,可以得到结果。yield是表达式的返回值

示例:多表达式, case 与yield 结合使用

public static void main(String[] args) {
int week = 1;
//yield是switch的返回值, yield跳出当前switch块
String memo  = switch (week){
case 1 ->{
System.out.println("week=1的 表达式部分");
yield "星期日,休息";
}
case 2,3,4,5,6 ->{
System.out.println("week=2,3,4,5,6的 表达式部分");
yield "工作日";
}
case 7 -> {
System.out.println("week=7的 表达式部分");
yield "星期六,休息";
}
default -> {
System.out.println("其他语句");
yield "无效日期";
}
};
System.out.println("week = " + memo);}

提示:

case 标签-> 与 case 标签:不能混用。 一个switch语句块中使用一种语法格式。 switch作为表达式,赋值给变量,需要yield或者case 标签-> 表达式。->右侧表达式为case返回值。

示例:

public static void main(String[] args) {
int week = 1;
//yield是switch的返回值, yield跳出当前switch块
String memo  = switch (week){
case 1 ->{
System.out.println("week=1的 表达式部分");
yield "星期日,休息";
}
case 2,3,4,5,6 ->{
System.out.println("week=2,3,4,5,6的 表达式部分");
yield "工作日";
}
case 7 -> "星期六,休息";
default -> "无效日期";
};
System.out.println("week = " + memo);
}

1.2.2.3 Java Record

switch表达式中使用record,结合 case 标签-> 表达式,yield实现复杂的计算

step1: 准备三个Record


public record Line(int x,int y) {
}
public record Rectangle(int width,int height) {
}
public record Shape(int width,int height) {
}

step2: switch record

public static void main(String[] args) {
Line line = new Line(10,100);
Rectangle rectangle = new Rectangle(100,200);
Shape shape = new Shape(200,200);
Object obj = rectangle;
int result = switch (obj){
case Line(int x,int y) -> {
System.out.println("图形是线, X:"+x+",Y:"+y);
yield x+y;
}
case Rectangle(int w,int h) -> w * h;
case Shape(int w,int h) ->{
System.out.println("这是图形,要计算周长");
yield 2* (w + h);
}
default -> throw new IllegalStateException("无效的对象:" + obj);
};
System.out.println("result = " + result);
}

case Line , Rectangle,Shape 在代码块执行多条语句,或者箭头->表达式。

1.2.3 Text Block

Text Block处理多行文本十分方便,省时省力。无需连接 "+",单引号,换行符等。Java 15 ,参考JEP 378.

1.2.3.1 认识文本块

语法:使用三个双引号字符括起来的字符串.


"""
内容
"""

例如:


String name = """lisi"""; //Error 不能将文本块放在单行上
String name= """lisi
20""";   //Error 文本块的内容不能在没有中间行结束符的情况下跟随三个开头双引号
String myname= """
zhangsan
20
""";  //正确

文本块定义要求:

  • 文本块以三个双引号字符开始,后跟一个行结束符。
  • 不能将文本块放在单行上
  • 文本块的内容也不能在没有中间行结束符的情况下跟随三个开头双引号

三个双引号字符""" 与两个双引号""的字符串处理是一样的。与普通字符串一样使用。例如equals() , "==" , 连接字符串(”+“), 作为方法的参数等。

1.2.3.2 文本块与普通的双引号字符串一样

Text Block使用方式与普通字符串一样,==,equals比较,调用String类的方法。

step1:字符串比较与方法

public void fun1() {
String s1= """
lisi
""";
String s2 = """
lisi
""";
//比较字符串
boolean b1 = s1.equals(s2);
System.out.println("b1 = " + b1);
//使用 == 的比较
boolean b2 = s1 == s2;
System.out.println("b2 = " + b2);
String msg = """
hello world""";
//字符串方法substring
String sub = msg.substring(0, 5);
System.out.println("sub = " + sub);
}

step2:输出结果


b1 = true
b2 = true
sub = hello

1.2.3.3 空白

  • JEP 378中包含空格处理的详细算法说明。
  • Text Block中的缩进会自动去除,左侧和右侧的。
  • 要保留左侧的缩进,空格。将文本块的内容向左移动(tab键)

示例:


public void fun2(){
//按tab向右移动,保留左侧空格
String html= """

动力节点,Java黄埔军校


""";
System.out.println( html);
}

示例2:indent()方法


public void fun3(){
String colors= """
red
green
blue
""";
System.out.println( colors);
//indent(int space)包含缩进,space空格的数量
String indent = colors.indent(5);
System.out.println( indent);
}

输出:

red

green

blue

red      
       green      
       blue

1.2.3.4 文本块的方法

Text Block的格式方法formatted()


public void fun4(){
String info= """
Name:%s
Phone:%s
Age:%d
""".formatted("张三","13800000000",20);
System.out.println("info = " + info);
}

String stripIndent():删除每行开头和结尾的空白

String translateEscapes() :转义序列转换为字符串字面量

1.2.3.5 转义字符

新的转义字符"\",表示隐士换行符,这个转义字符被Text Block转义为空格。通常用于是拆分非常长的字符串文本 ,串联多个较小子字符串,包装为多行生成字符串。 新的转义字符,组合非常长的字符串。

示例

public void fun5(){
String str= """
Spring Boot是一个快速开发框架 \
基于\"Spring\"框架,创建Spring应用 \
内嵌Web服务器,以jar或war方式运行 \
""";
System.out.println("str = " + str);
}

输出

Spring Boot是一个快速开发框架 基于Spring框架,创建Spring应用 内嵌Web服务器,以jar或war方式运行

总结:

  1. 多行字符串,应该使用Text Block
  2. 当Text Block可以提高代码的清晰度时,推荐使用。比如代码中嵌入SQL语句
  3. 避免不必要的缩进,开头和结尾部分。
  4. 使用空格或仅使用制表符 文本块的缩进。混合空白将导致不规则的缩进。
  5. 对于大多数多行字符串, 分隔符位于上一行的右端,并将结束分隔符位于文本块单独行上。例如:


String colors= """
red
green
blue
""";

1.2.4 var

在JDK 10及更高版本中,您可以使用var标识符声明具有非空初始化式的局部变量,这可以帮助您编写简洁的代码,消除冗余信息使代码更具可读性,谨慎使用.

1.2.4.1 var 声明局部变量

var特点

  • var是一个保留字,不是关键字(可以声明var为变量名)
  • 方法内声明的局部变量,必须有初值
  • 每次声明一个变量,不可复合声明多个变量。 var s1="Hello", age=20; //Error
  • var动态类型是编译器根据变量所赋的值来推断类型
  • var代替显示类型,代码简洁,减少不必要的排版,混乱。

var优缺点

  • 代码简洁和整齐。
  • 降低了程序的可读性(无强类型声明)

示例:


//通常
try (Stream result = dbconn.executeQuery(query)) {
//...
//推荐
try (var customers = dbconn.executeQuery(query)) {
//...
}
比较 Stream result 与  var customers

1.2.4.2 使用时候使用var

  • 简单的临时变量
  • 复杂,多步骤逻辑,嵌套的表达式等,简短的变量有助理解代码
  • 能够确定变量初始值
  • 变量类型比较长时

示例:


public void fun1(){
var s1="lisi";
var age = 20;
for(var i=0;i<10;i++){
System.out.println("i = " + i);
}
List strings = Arrays.asList("a", "b", "c");
for (var str: strings){
System.out.println("str = " + str);
}
}

1.2.5 sealed

sealed 翻译为密封,密封类(Sealed Classes)的首次提出是在 Java15 的 JEP 360 中,并在 Java 16 的 JEP 397 再次预览,而在 Java 17 的 JEP 409 成为正式的功能。

Sealed Classes主要特点是限制继承

Sealed Classes主要特点是限制继承,Java中通过继承增强,扩展了类的能力,复用某些功能。当这种能力不受控。与原有类的设计相违背,导致不预见的异常逻辑。

Sealed Classes限制无限的扩张。

Java中已有sealed 的设计

  • final关键字,修饰类不能被继承
  • private限制私有类

sealed 作为关键字可在class和interface上使用,结合permits 关键字。定义限制继承的密封类

1.2.5.1Sealed Classes

sealed class 类名 permits 子类1,子类N列表 {}

step1::声明sealed Class


public sealed class Shape permits Circle, Square, Rectangle {
private Integer width;
private Integer height;
public void draw(){
System.out.println("=Shape图形");
}
}

permits表示允许的子类,一个或多个。

step2::声明子类

子类声明有三种

  • final 终结,依然是密封的
  • sealed 子类是密封类,需要子类实现
  • non-sealed 非密封类,扩展使用,不受限

示例:

//第一种 final
public final class Circle extends Shape {
}
//第二种 sealed class
public sealed class Square extends Shape permits RoundSquare {
@Override
public void draw() {
System.out.println("=Square图形");
}
}
//密封类的子类的子类
public final class RoundSquare extends Square{
}
//非密封类 , 可以被扩展。放弃密封
public non-sealed class Rectangle extends Shape {
}
//继承非密封类
public class Line extends  Rectangle{
}

密封类不支持匿名类与函数式接口

1.2.5.2 Sealed Interface

密封接口同密封类

step1:声明密封接口


public sealed interface SomeService permits SomeServiceImpl {
void doThing();
}

step2:实现接口


public final class SomeServiceImpl implements SomeService {
@Override
public void doThing() {
}
}

以上类和接口要在同一包可访问范围内。