Reference: Oracle Documentation

Chapter 3. Java Record & Java Bean

3.1 Java Record

在 Java 14 以后,官方引入了新的 Java 关键字:record

那么这个 record 关键字究竟有什么用处呢?它和我们熟知的 class / interface / abstract class 又有什么区别呢?其实在 Java 14 以前,有一种需求写起来非常的麻烦,正因为这种需求才产生出了 record 关键字。这个需求是什么呢?

举个例子,假如现在有个应用场景,想要定义一个数据类型,它只是用来存放一些数据(例如数据库查询的结果,或者是某个服务的返回信息)。

在很多实际情况下,我们希望使用这些数据就像 Java 内置基本类型一样,是不可变数据类型。这样做有几点好处:

  • 复制构造时,不是引用传递,因此是深拷贝。这样使用起来和基本类型一样方便,但是又不用担心改错源数据(非引用链接);

  • 确保数据在多线程情况下无需同步,线程安全!

回忆下基础篇中的知识,要让 Java 类型(对象)behaves like 不可变数据类型,就必须确保:

  • 类型中的每个数据域都是 私有的、常量的privatefinal);
  • 每个数据域都只能通过 getter 方法获取,不能有任何 setter 方法;
  • 必须存在公有构造函数,并且构造函数内初始化各个数据域(常量只能这么做);
  • Object 基类继承函数 equals 返回 true 当且仅当类中的每个数据域都相等;
  • Object 基类继承函数 hashCode 在类中的每个数据域都相等时,一定返回一样的值;
  • Object 基类继承函数 toString 最好包含 类名 和 每个数据域的名称和值;

好了,假设我们现在想要保存一个 “联系人” 的信息,只包含一个名称、住址。我要定义这个类为不可变数据类型,那么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.util.Objects;

public class Person {
private final String name;
private final String address;

public Persion(String name, String address) {
this.name = name;
this.address = address;
}

@Override
public int hashCode() {
return Objects.hash(name, address);
}

@Override
public boolean equals(Object obj) {
if (this == obj) return true;
else if (!(obj instanceof Person)) return false;
else {
Person other = (Person)obj;
return Objects.equals(name, other.name) && Objects.equals(address, other.address);
}
}

@Override
public String toString() {
return "Person [name=" + name + ", address=" + address + "]";
}

/* standard getters */
String getName() { return this.name; }
String getAddress() { return this.address; }
}

……这很难评,仅仅为了将两个基本类型保存为不可变的数据类型,如此大费周章。具体来说,有几点坏处:

  • 许多代码都是和业务逻辑无关的 “模板代码”;

  • 这种写法模糊了这个类原本的作用(语义模糊):仅仅是按不可变数据类型保存两个基本类型而已!

  • 很差的扩展性。

    现在还只有两个属性,那如果我要再加一个属性呢?

    那么我要修改构造函数、修改所有的重载方法、为新的属性添加访问器。可谓麻烦。


于是,在 Java 14 中,定义了新的关键字 record,它的含义就是告诉编译器,这是个保存数据的类型,要把它定义成不可变的样子!

经过上面的铺垫,你就能理解 record 关键字的意义,以及它的作用了。

3.1.1 记录类型的构造函数

Java 规定,在使用 record 关键字定义类型时,默认构造函数存在参数,且与私有数据域一一对应

允许特殊的定义方式:

1
public record Person(String name, String address) {}

你没有看错,上面一行等价于之前用 class 定义的一大堆代码……

除了简化了默认构造函数,你仍然在此基础上自定义构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.Objects;

/* 只需括号内声明数据记录类所有的私有数据成员即可 */
public record Person(String name, String address) {
/* 简化的默认构造函数(被称为 compact constructor),不需要写形参列表、不需要手动对其初始化 */
public Person {
/* 一般没啥事能做,你可以检查检查传入的参数是否为 null */
Objects.requireNotNull(name);
}

/* 可以重载构造函数 */
public Person(String name) {
/* 允许委托构造 */
this(name, "UNKNOWN");
}
}

3.1.2 记录类型的访问器

和一般的访问器命名法有些差别,记录类型默认的访问器不使用 getXXX 命名,而是使用 数据成员的名字 直接命名。

另外,一般真的不用改记录类型的访问器,如果需要改,那么说明这个类一定不是单纯的数据记录类,请用普通类型定义!

3.1.3 记录类型的 Object 重写方法

根据定义,equalshashCode 一般都不需要你再次重写。

在某些情况下,你可能想要自定义 toString,这没有问题,就和普通的类重写的方法一样。

3.1.4 记录类型的静态变量和方法

虽然不允许有公有可写的属性,但记录类型允许定义 静态变量、静态方法,它们都可以是公有的

你可以把它们理解成对整个数据类型的配置,或者解释。

3.2 Java Bean

嗯,实际上,还有一种约定和 Java Record 应用很像的 Java 类型定义规范,它的名字是 Java Bean(Java 豆?)。

没错,Java Bean 是 Java 的一种类型定义规范,和 record 类似,它们的共性是用一个类来盛放一组数据

但是,record 追求的是不可变数据类型(数据域不可变性)、一条记录的不可变性和易操作性,而 bean 追求的是:

  1. 数据的取出放入的接口不变,保证兼容性;

  2. 数据序列化(serializable)和传输方便(注:Java Bean 出现的原因就在于此,为了让一组相关数据传输方便);

    不过 Record 也很简单,传输起来也方便,但是不可变,应该看业务需求选择。

所以,Java Bean 没有像 record 一样,它规定了一组类型定义方式:

  1. 提供一个默认的无参构造函数;
  2. 需要被序列化并且实现了 Serializable 接口;
  3. 可能有一系列可读写属性,并且一般是 private 的;
  4. 有一系列的 getter 或 setter 方法;

感性理解一下:想象一下存在这样一个箱子,其内部被分割成几个格子,每个格子用来存放特定的物品,工人取出或者放入物品后封箱,然后叫了个快递把箱子发出去了。这个箱子就是 Java Bean,取出、放入就是 getter、setter,物品就是属性,封箱发出就是序列化和传输。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.io.Serializable;

public class Person implements Serializable {
private String name;
private int age;

public void setName(String newName) {
name = newName;
}
public String getName() {
return name;
}

public void setAge(int newAge) {
age = newAge;
}
public int getAge() {
return age;
}
}

上面的 Person 类就是合格的 Java Bean(注:Serializable 接口已经在 Java 笔记1 中详细介绍)。

3.3 Java Record 与 Java Bean 的对比

Java Record Java Bean
追求不可变数据类型、数据结果表示 追求数据传输便捷性、数据访问接口规范性
Java 14+ 使用关键字 record 辅助定义 纯手工实现约定
final class,不可被继承,也没有被继承需求 普通 class,可以被继承
常用于存放、比较 和 展示数据结果 常用于完成如数据传输一类的业务逻辑

Chapter 4. 反射

Java 中的反射机制是什么?

复习一下 Java 的运行过程。我们知道,Java 虚拟机是一种解释器,是解释 Java 字节码(*.class)的一种程序。其大致运行过程如下:

运行类加载器(ClassLoader)将字节码加载到内存中 —-> 运行字节码验证器强制检查 Java 程序合法性和安全性,不符合安全规范的不予运行 —-> 读取内存中的字节码逐句解释为机器码执行;

可以说,在 Java 源文件编译为字节码之后,就形成了一个个 *.class 文件。这里的每个 *.class 文件都对应着这个类型的必要信息。在 Java 虚拟机中将这些字节码加载到内存中,构建了这个类对应的特殊的表示对象(称为 Class 对象)。这样在引用到这个类的位置就能正确地给出行为。

注意,在 Java 中,Class(首字母大写,和关键字 class 是两回事)本身就是一个类型,是承载类的信息的类(元类,meta-class),它的实例对象就叫 Class 对象。

而所谓的反射,可以说是上面的过程的运行时逆过程:

Java 的反射就是从加载到内存中的 Class 对象,反向获取其中的信息(或者说,反向映射)。

4.1 反射的意义使用场景

不过在介绍反射之前,首先谈谈它的坏处:

  • 破坏了类的封装性(因为反射是从 Class 对象反向获取信息,因此突破了类型可见性修饰符的约束,可以访问某个类的私有成员);
  • 运行时确定类型,性能肯定不好,丢掉了静态类型语言的性能优势;

  • 运行安全问题。

如此重要的缺点,已经注定了 Java 的反射机制不应该被随便使用,并且大部分场合下并不适合使用反射。

但是因为反射的重要功能,少数场合又不得不用。举几个例子:

  • Java codelinter 静态类型代码检查。比如 IDEA 的 LSP Server 在探查某个对象的方法和属性的时候(你在 IDEA 里写个对象,后面加个点就能弹出一堆方法和属性提示),除了分析上下文定义的方法以外,一种重要的手段就是通过反射分析;
  • 大型框架(例如 Springboot)很多都是配置化的(例如通过 XML 文件配置 Bean),为了保证框架的通用性,可能需要根据配置文件加载不同的类或者对象、调用不同的方法。这个时候就必须使用到反射了,它可以完成 “运行时动态加载需要的加载的对象” 的任务;
  • Java 加载某些数据库驱动的时候,需要运行时动态构建类型信息,使用时就要用反射机制;
  • 某些注解的行为需要反射(下一章 “注解” 所需要了解的知识)。

4.2 反射 API

了解它的地位后,在开始使用它。使用 Java 的反射就是使用 java.lang.Classjava.lang.reflect.* 的所有 API。

首先列出可能用到的类型:

Java 类型 类型说明
Class 用来在内存中描述一个 Java 类(所有继承于 Object 的类)
Constructor 用来在内存中描述一个 Java 类的构造函数信息,包括访问权限和动态调用信息等
Field 用来在内存中描述一个 Java 类或者 Java 接口的数据成员(或者说属性)信息,包括访问权限和动态修改等
Method 用来在内存中描述一个 Java 类或者 Java 接口的成员函数(或者说方法)信息,包括包括访问权限和动态调用信息等
Modifier 用来在内存中描述一个 Java 类或者 Java 接口的所有成员(包括属性、方法)的修饰属性,例如 public/private/static/final/synchronized/abstract 等信息

4.2.1 Class 类型与 Class 实例

我们知道了,反射需要根据内存中的 Class 对象进行操作,那么怎么得到一个普通类型所对应的 Class 对象呢?Java 提供了 3 种方法:

1
2
3
4
5
/* <Object> 表示任意类型,只要是 Object 的子类 */
/* <Object Instance> 表示任意类型对应的实例对象 */
[Method] <Object Instance>.getClass() -> Class Instance;
[Static Property] <Object>.class -> Class Instance;
[Static Method] Class.forName(String className) -> Class Instance;

下面以获取 String 类对应的 Class 对象为例。

  • 通过该类(一定继承于基类 Object)的实例 中的 getter 方法:<Object>.getClass()

    这个方法返回是这个对象所在的类型 的对应 Class 实例对象。

    1
    2
    3
    4
    String name = new String();

    /* 获取的 stringClass 实例对象就是 String 类型在 JVM 内存中对应的 Class 对象 */
    Class stringClass = name.getClass();
  • 通过该类的静态属性获得这个类所对应的 Class 实例对象。

    1
    Class stringClass = String.class;
  • 利用 Class 类型提供的静态方法,通过类名字符串查找当前内存中的 Class 对象;

    这种方法最常用,因为使用反射的时候,几乎都是不知道对象、不知道类型定义、只知道类型名的情况。

    1
    Class stringClass = Class.forName("String");

知道类如何获取某个类型的 Class 对象,那么可以对这个 Class 对象进行哪些操作呢?

  • 判断任意对象是否是这个 Class 对象描述的类的实例,或者其他什么东西:

    1
    2
    3
    4
    5
    6
    7
    /* obj 是否是这个 Class 对象描述的类型的实例 */
    public native boolean isInstance(Object obj); /* [Method] */
    /* 这个 Class 对象描述的类型是否是 Interface 类型 */
    public native boolean isInterface();
    public native boolean isArray();
    public native boolean isPrimitive(); /* 判断基本类型 */
    public native boolean isAnnotation(); /* 判断注解 */

下面以这个 Class 类型存放的是普通 Java 类为例,叙述常见的方法。

如果 Class 对象中描述的是注解,那么在下一章 “注解” 进行介绍。

如果 Class 对象中描述的是接口,那么只能获取一些成员信息,可能能调用一些静态方法或属性。

  • Class 对象创建实例(哪怕源码中没有这个类的定义也行,只要内存中有这个 Class 对象):

    1
    2
    /* 按默认构造函数创建实例 */
    public native Object newInstance(); /* [Method] */
  • 获取 Class 对象对应类的构造函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    /* 获取 Class 对象描述的类型的:所有"公有的"构造方法 */
    public Constructor[] getConstructors();
    /* 获取所有的构造方法(包括私有、受保护、默认、公有) */
    public Constructor[] getDeclaredConstructors();

    /* 获取指定的构造函数 */
    /* 这里的参数列表是各个参数类型对应的 Class 对象! */
    public Constructor getConstructor(Class... parameterTypes);
    public Constructor getDeclaredConstructor(Class... parameterTypes);
  • 获取 Class 对象对应类的数据成员(属性,静态修饰不作单独区分):

    1
    2
    3
    4
    5
    6
    7
    8
    /* 获取 Class 对象描述的类型的:所有"公有的"属性 */
    public Field[] getFields();
    /* 获取所有的属性(包括私有、受保护、默认、公有) */
    public Field[] getDeclaredFields();

    /* 获取指定名称的属性 */
    public Field getField(String fieldName);
    public Field getDeclaredField(String fieldName);
  • 获取 Class 对象对应类的成员函数(方法,静态修饰不作单独区分):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    /* 获取 Class 对象描述的类型的:所有"公有的"方法 */
    public Method[] getMethods();
    /* 获取所有的属性(包括私有、受保护、默认、公有) */
    public Method[] getDeclaredMethods();

    /* 获取指定名称的属性 */
    /* 和构造函数不一样,这里需要先给定方法名 */
    public Method getMethod(String methodName, Class... parameterTypes);
    public Method getDeclaredMethod(String methodName, Class... parameterTypes);
  • 获取 Class 对象对应类的修饰符:

    1
    2
    /* 获取类型自身的修饰符 */
    public int getModifiers();

4.2.2 Constructor 类型

在介绍 Class 类型时,我们了解了如何得到 Constructor(该类的构造函数信息)对象,那么应该如何操作它?

最常用的方法是 调用这个构造函数

1
2
/* T 为泛型 */
public native T newInstance(Object... parameterTypes);

警告:这里的参数类型必须要和取得 Constructor 对象时传入的形参类型一致。否则运行时错误。

还可以获取 Constructor 的其他信息,具体请看官方文档。

4.2.3 Field 类型

在介绍 Class 类型时,我们了解了如何得到 Field(该类的属性)对象,那么应该如何操作它?

常用的方法是,按查找到的属性信息设置对象属性、读取对象属性:

1
2
public void set(Object target, Object value);
public void get(Object target);

这里为什么要给 target 参数呢?因为我们得到的 Field 对象只是保存了原来类型属性的一部分信息,不能指明这个属性是属于具体哪个对象的。所以取值和设置时需要给定对象。

警告:这里的 value 必须和取得 Field 对象时原本类型一致,否则运行时错误。

对于私有成员,想要访问它前需要强制越过可见性修饰符:

1
public void setAccessible(boolean available);

可能产生的异常有 FieldNotFoundExceptionIllegalAccessException 等等;

还可以获取 Field 的其他信息,具体请看官方文档。

4.2.4 Method 类型

在介绍 Class 类型时,我们也了解了如何得到 Method(该类的方法)对象,那么应该如何操作它?

常用的方法是,调用它:

1
public void ObjectObject invoke(Object target, Object... parameters);

如果是私有方法,也需要通过 setAccessible 调整访问可见性。

还可以获取 Method 的其他信息,具体请看官方文档。

4.2.5 Modifier 类型

其实,除了 Class 类型,其他的 Constructor/Field/Method 类型都可以调用 getModifiers() 获取当前字段的修饰符。返回值是 int,但是可以通过 Modifier 静态方法转为可读的字符串:

1
public String toString();

可以表示的修饰符不仅有可见性修饰符,还有各种像 native / synchronized / transient / volatile / abstract / final / interface 等等,都可以检查到,使用对应的 isXXX() 实例方法即可。

4.2.6 反射的使用实例

  • 反射越过泛型检查;
  • 大型框架(以 SpringMVC 为例)字段名一类的数据类型配置反射处理相当简洁清晰;
  • 自定义注解(写 RUNTIME 注解逻辑,下一章详细叙述);

Chapter 5. 注解

Java 中一种语法称为注解,可能在大部分其他的语言中都有。在 Python / TypeScript 中,这种类似的做法称为 “装饰器”。

严格来说,Java 的注解和 Python / TypeScript 的装饰器的机制不一样。因为前者只是改变了执行方式,而后者相当于是一种语法糖,处理后替换了被装饰的对象。但是它们的语法和最终作用真的很像。

这种做法的特征是,在不改变原代码内容和逻辑的基础上,进行一些修饰和包装(就像给解释器注解这段代码的执行方式一样),使得解释执行(或者编译)这段代码时的方式有些许改变。

你没看错,“注解“ 这个东西本身,不会对原先的代码的逻辑有任何影响(这段代码编译出的字节码不会变),只是做个标记,告诉即将要读取这个注解的对象(可能是编译器、加载器,或者是程序中的其他代码),用约定好的方式来执行这段代码(比如执行之前、执行之后插入了一些其他流程)。

5.1 注解的使用和分类

你也许会在一些代码中见到这样的书写方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Resource("hello")
public class Hello {
@Inject
int n;

@PostConstruct
public void hello(@Param String name) {
System.out.println(name);
}

@Override
public String toString() {
return "Hello";
}
}

@Resource("Hello") 是有参数的注解,@PostConstruct / @Override 是无参数的注解;

@Resource("Hello") 在这个例子中是修饰类的注解,@PostConstruct / @Override 在这个例子中是修饰方法的注解,@Param 在这个例子中是修饰形参的注解。

注解的使用语法就这些,无非是无参数的 @<AnnotationName>,或者有参数的 @<AnnotationName>(...),加在它们所指定的对象头部位置。

要掌握好注解的使用方法,就先把它们按使用特征分类。一般来说,注解是按处理阶段进行分类:

  • 写给编译器看的注解(称为 编译时注解,Compile-time Annotation)。

    这类注解不会被编译进入 .class 字节码文件,它们在编译后就被编译器扔掉了;举例:

    • @Override:让编译器检查该方法是否正确地实现了覆写(C++ override 关键字有同样的功能);
    • @SuppressWarnings:告诉编译器忽略此处代码产生的警告。
  • 写给 classLoader 或者其他加载时 ~ 运行时的工具看的注解(称为 加载时注解,Load-time Annotation)。

    这类注解会被编译进入 .class 文件,但加载结束后并不会存在于内存中。这类注解只被一些底层库使用,一般我们不必自己处理。

    比如有些工具会在加载 class 的时候,对 class 做动态修改,实现一些特殊的功能。

  • 写给运行时某一部分代码看的注解(称为 运行时注解,Run-time Annotation)。

    就算是运行时注解,JVM 也并不会通过注解主动进行一些操作。只有部分代码通过 反射 读取指定的注解,进行业务逻辑的执行

    这也是最常用的注解形式,在许多框架中都会出现

5.2 自定义注解

有些情况下(比如使用框架),你可能会用很多预先定义的注解,但是你很好奇这些注解是怎么运作的,于是你就要了解,一个注解是如何定义、如何生效(进行处理)的。

搞清楚一个东西的最好方法就是从头开始做一遍,于是你准备动手搓一个自定义的注解出来。

5.2.1 注解的定义 和 实质

Java 规定,注解使用 @interface 关键字定义注解。最基本的语法如下:

1
2
3
4
5
6
7
8
public @interface MyFirstAnnotation {
/* 强烈建议为注解的每个数据域都设置一个默认值 */
int intData() default 0;
String stringData1() default "info";
String stringData2() default "";
/* 建议最常用的数据域名称设置为 value */
String value() default "";
}

注解的定义和 Java 的记录类型一样简洁,只需要声明要传给注解的参数即可(这些参数直接以访问器的形式定义,如上),不需要定义任何处理逻辑!

因为注解的处理交由某些特定的代码完成(下一节介绍),注解的定义本身 就仅仅是一个 “注解”,或者说等待处理的标识而已

此外,还需要搞清楚一件事:注解的实质就是一个 Java 类型。所有的注解都继承于接口 java.lang.annotation.Annotation;因此,上面的定义方法只不过也是一种语法糖罢了。

但是,只有这个定义还不够描述这个注解,比如,这个注解是前面分类中的什么类型?应该在什么阶段、被谁处理(生命周期)?允许修饰谁?

这些信息 可以交给描述注解的注解,也就是元注解(meta-annotation),来完成

Java 标准库中定义了一大批实用的元注解,所以一般不需要我们自己定义元注解,只要知道怎么使用元注解来定义注解就可以了。常用的元注解如下:

  • @Target(<ElementType/ElementTypes[]>) 元注解:解释当前注解所能修饰的对象类型

    参数取值:ElementType.TYPE(允许修饰类、接口),ElementType.FIELD(允许修饰属性),ElementType.METHOD(允许修饰方法),ElementType.CONSTRUCTOR(允许修饰构造函数),ElementType.PARAMETER(允许修饰方法的形式参数);

  • @Retention([RetentionPolicy]) 元注解:解释当前注解的声明周期(指定注解类型)

    可选参数取值:RetentionPolicy.SOURCE 编译时注解、RetentionPolicy.CLASS 加载时注解(默认)、RetentionPolicy.RUNTIME 运行时注解;

  • @Repeatable(<Annotation Class Instance>) 元注解:解释当前注解是否可以重复注解同一对象

    要用的话,直接在注解的定义头部加上 @Repeatable(<AnnotationName>.class)

    这里需要反射方法传入自定义注解类对应的 Class 对象。

  • @Inherited 元注解:解释当前注解是否可继承。当且仅当 @Target 参数为 ElementType.TYPE 时有效。

    这个元注解的意思是,当前注解能不能随着继承交给子类。

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 这个注解只能修饰方法 */
@Target(ElementType.METHOD)
public @interface MyFirstAnnotation {
int intData() default 0;
String stringData1() default "info";
String stringData2() default "";
}

/* 这个注解既能修饰方法,又能修饰属性 */
@Target({
ElementType.METHOD,
ElementType.FIELD
})
public @interface MyFirstAnnotation2 {
int intData() default 0;
String stringData1() default "info";
String stringData2() default "";
}
1
2
3
4
5
6
7
8
9
10
11
/* 可以注解方法和构造函数的 运行时注解 */
@Retention(RetentionPolicy.RUNTIME)
@Target({
ElementType.METHOD,
ElementType.CONSTRUCTOR
})
public @interface MyFirstAnnotation1 {
int intData() default 0;
String stringData1() default "info";
String stringData2() default "";
}

5.2.2 运行时注解的处理

由于其他两类注解一般用不到(编译时注解由编译器使用,因此我们一般只使用,不编写;加载时注解主要由底层工具库使用,涉及到class的加载,一般我们很少用到),因此此处仅叙述运行时注解的处理。

从现在开始,下文中的所有 “注解” 都指代 “运行时注解”。

我们知道,注解本身只是个注解,如果你不做任何处理,那么它将对原本的代码毫无影响,就像注释一样。

我们还知道,注解只是一个 Java 类而已,但是这个类只存放一些参数,不与外界代码有任何关联。

所以处理注解的方法一目了然:使用上一章介绍的反射机制,不仅能找到所有规定类型的注解,还能让注解发挥指定的效果。

上一章中,我们只介绍了关于类型、接口的反射 API,这里我们补充一下针对注解的反射 API:

  • 判断 Class 对象本身是否描述的是注解:public boolean isAnnotation();

  • 判断注解是否存在于指定对象上:isAnnotationPresent(<Class Object of Annotation>)

    这个方法在 Class / Field / Method / Constructor 类型中都有。

  • 从指定对象上获取注解对象:getAnnotation(<Class Object of Annotation>)

    这个方法在 Class / Field / Method / Constructor 类型中都有。

    注意:可能会返回 null,所以使用前请用 isAnnotationPresent 检查!

  • 从方法 / 构造函数中获得参数注解对象:getParameterAnnotations() -> Annotation[][]

    只有在 Method / Constructor 中存在。

再回想一下,注解是个类型,里面装的全是传入参数,并且直接提供各个传入参数的访问器方法。

有了以上的知识,就能写一个自定义的注解了。

5.2.3 实战:自定义一个运行时注解

考虑一个需求,我想定义一个修饰属性的运行时注解,如果这个参数是整数,就限制这个参数的范围为注解参数给定的范围;如果这个参数是字符串,那么限制的是字符串长度。其中最大、最小值可选。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/* File: Range.java */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Range {
int min(): default Integer.MIN_VALUE;
int max(): default Integer.MAX_VALUE;
}

/* File: RangeChecker.java */
public class RangeChecker<T> {

public void check(T obj) throws IllegalArgumentException {
Class<?> objCls = obj.getClass();
for (Field f: objCls.getFields()) {
f.setAccessible(true);
if (f.isAnnotationPresent(Range.class)) {
Range range = f.getAnnotation(Range.class);
Object originVal = null;
try {
originVal = f.get(obj);
} catch (IllegalAccessException err) {
/* Controls never reaches here. */
}
if (originVal instanceof String s) {
if (s.length() < range.min() || s.length() > range.max()) {
throw new IllegalArgumentException("Invalid field: " + f.getName());
}
} else if (originVal instanceof Integer i) {
if (i < range.min() || i > range.max()) {
throw new IllegalArgumentException("Invalid field: " + f.getName());
}
}
}
}
}
}

思考一下,RangeChecker 应该什么时候被使用?没错,这取决于你的业务逻辑。注解是 “惰性的”,只有你显式调用注解处理方法,注解的处理才会开始。

Chapter 6. Functional Interface

6.1 Definitions and Examples

Java 中的一个重要特性:函数式接口。实际上,它的规范定义是:

任何一个只存在单一抽象方法(SAM)的接口,都称之为函数接口

函数接口提供了和 TypeScript 类似的能力,它让我们可以不那么看重函数签名,仅仅从函数类型(参数、返回值类型)将函数归类。接口如下:

1
public interface Function<T, R> { /* ... */ }

这还可以让我们轻易地将一个函数作为一个参数 / 返回值,实现函数式编程。

例如,我们可以显式声明使用函数接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Function<Integer, String> intToString = Object::toString;
Function<String, String> quote = s -> "'" + s + "'";

/* compose 是函数接口的接口方法,可以将函数接口组合执行 */
Function<Integer, String> quoteIntToString = quote.compose(intToString);
/* apply 是函数接口的接口方法,执行这个函数接口的实现 */
assertEquals("'5'", quoteIntToString.apply(5));

/* 再例如这段业务代码 */
public Optional<EventDto> findEventByUuid(Long uuid) {
Optional<Event> event = eventRepository.findByUuidAndNotDeleted(uuid);
/* Optional.map 的参数就是一个函数接口,可以更方便、清晰地处理内容的映射关系 */
return event.map(Event::mapToEventDto);
}

也可以使用 new 立即定义、实例化一个函数接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* Java 中创建一个线程,其中参数类型 Runnable 就是一个函数接口 */
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("New thread created");
}
}).start();

/* 这种原地重载实例化接口的方法,我们可以称为匿名的接口实现。
* 缺点是接口实现不能复用,优点是代码简洁。
* 这对于其他任何 interface 都是可行的(不是函数接口也行) */

/* P.S. 如果你写了上面的函数接口的匿名实现,在 IDEA 中会提醒你换成匿名函数更简洁 */
/* 这样 Java 就越来越像 TypeScript 了(bushi */

此外,我们所熟知的 Java Lambda 函数(匿名函数)就是一种函数接口。

1
() -> new HelloClass()

除了匿名函数,一个类中的静态方法、在同一个类中使用的实例方法都能通过 :: 作用域符来转换为函数接口,例如:

在任意一个类方法中使用 Object::toString(实例方法省略传递 self)、使用静态方法 MyClass::aStaticMethod 等等。

6.2 Primitive Function Specializations

对于 Java primitive types(基本类型),我们没法将它们作为 generic type argument(泛型参数),所以这个时候就必须这么定义基本类型的函数接口:

1
2
3
4
@FunctionalInterface
public interface ShortToByteFunction {
byte applyAsByte(short s);
}

然后我们就能操作含有基本类型的函数,作为函数接口了!也可以把它作为参数、返回值,进行便捷的函数式操作。

是不是越看越像 C++ 的函数指针?

6.3 Cosumers & Suppliers

Suppliers 在 Java 中定义为 不含参数、只有返回值的 函数接口。它常被用在:

  • 数据计算 / 大型对象创建等需要 Lazy Load 的场景,只是先拿到 supplier,真正需要结果的时候才进行计算,实现过程的解耦;

    1
    2
    /* 举例:提供创建对象的方法 */
    () -> new ComplicatedClass()
  • 大型序列的 Lazy Generate 的场景,类似 Python 的生成器/迭代器,只有获取下一个元素时才进行计算,极大节省资源,例如一个 Fibbonacci 生成器:

    1
    2
    3
    4
    5
    6
    7
    8
    int[] fibs = {0, 1};
    Stream<Integer> fibonacci = Stream.generate(() -> {
    int result = fibs[1];
    int fib3 = fibs[0] + fibs[1];
    fibs[0] = fibs[1];
    fibs[1] = fib3;
    return result;
    });

Consumers 则与 Suppliers 相反,定义为 没有返回值、只含参数的 函数接口。它的应用场景比 Suppliers 更少,通常它隐含着 “side effect” 的含义。

我们也可以在日志逻辑中见到它:

1
names.forEach(name -> log.debug("Hello, " + name));

6.4 Predicates & Operators

在数学上有对应概念的函数接口分别是谓词断言函数 和 操作符。

谓词断言函数(predicates),可以定义为 参数是一个或多个值、返回值为 boolean 类型的函数接口。在数学上对应的概念:谓词。

我们常常在 filterfind 这样的接口中见到,例如:

1
2
3
List<String> namesWithA = names.stream()
.filter(name -> name.startsWith("A") /* predicates */)
.collect(Collectors.toList());

操作符(operators),可以定义为 参数(一个或多个)和返回值类型相同的函数接口。

我们经常在使用 Collection API 时能见到,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
List<Integer> values = Arrays.asList(3, 5, 8, 9, 12);

int sum = values.stream()
.reduce(0, (i1, i2) -> i1 + i2);

/* 或者 */

public List<UserDto> findUserByUsername(String username) {
List<User> res = userRepository.findUserByUsername(username);
return res.stream()
.map(UserService::mapToUserDto)
.collect(Collectors.toList());
}