Java Record

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

Record 背景

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

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

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

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

Record 使用

IDEA 新建 Class,选择类 Record。如果没有,Project Struture 中更改 Module 的 JDK 版本,改为 project select 最好。

Java 8 - 17 新特性.png

public record Student(Integer id,String name,String email,Integer age) { }
 
// 测试
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());
}

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

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

实例方法 Instance Method

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 void main(String[] args) {
	Student lisi = new Student(1001, "​​lisi","lisi@qq.com​​",20);
	String nameAndAge = lisi.concat();
	System.out.println( nameAndAge);
}

静态方法 Static Method

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();
	}
}
 
public static void main(String[] args) {
	String emailUpperCase = Student.emailUpperCase("​​lisi@163.com​​"); System.out.println("emailUpperCase = " + emailUpperCase);
}

构造方法

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

  • 紧凑型构造方法没有任何参数,甚至没有括号。
  • 规范构造方法是以所有成员作为参数(推荐)
  • 定制构造方法是自定义参数个数
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);
	}
}
 
// 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);
	}
}​

Record 与 Lombok

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

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

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

Record 实现接口

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);
}}

Local Record

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

public static void main(String[] args) {
	record SaleRecord(String saleId,String productName,Double money){};
	SaleRecord saleRecord = new SaleRecord("S22020301", "手机", 3000.0);
	System.out.println("销售记录 = " + saleRecord.toString());
}

嵌套 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());
}

instanceof 判断 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){
	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);
}

Java Record 能够自动处理 null,obj instanceof Person p 中,obj 为 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 t) { }
  • java.lang.Class 类与 Record 类有关的两个方法:

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

// “嵌套 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);
 
// 输出结果
recordComponent = java.lang.String id
recordComponent = java.lang.String name
recordComponent = com.bjpowernode.pk1.PhoneNumber phoneNumber
recordComponent = com.bjpewernode .pk1.Address address
record = true

Switch

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

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

箭头表达式,新的 case 标签

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

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

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);
}

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);
} ​

示例:多表达式, 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);
}

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

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);

Text Block

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

认识文本块

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

"""
内容
"""

例如:

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

文本块定义要求:

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

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

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

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

文本块的方法

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() :转义序列转换为字符串字面量

转义字符

新的转义字符”\“,表示隐士换行符,这个转义字符被 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方式运行

总结:

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

var

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

var 声明局部变量

var 特点

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

var 优缺点

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

示例:

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

什么时候使用 var

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

示例:

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

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 关键字。定义限制继承的密封类

Sealed Classes

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

step1::声明 sealed Class

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

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

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{}
 
//第三种 non-sealed 非密封类,可以被扩展,放弃密封
public non-sealed class Rectangle extends Shape {}
//继承非密封类
public class Line extends Rectangle{}

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

Sealed Interface

密封接口同密封类

step1:声明密封接口

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

step2:实现接口

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

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