22.5 注解的应用:定制序列化

在上一章,我们演示了一个简单的通用序列化类SimpleMapper,在将对象转换为字符串时,格式是固定的,本节演示如何对输出格式进行定制化。我们实现一个简单的类SimpleFormatter,它有一个方法:

1
public static String format(Object obj)

我们定义两个注解:@Label@Format@Label用于定制输出字段的名称,@Format用于定义日期类型的输出格式,它们的定义如下:

1
2
3
4
5
6
7
8
9
10
11
@Retention(RUNTIME)
@Target(FIELD)
public @interface Label {
String value() default "";
}
@Retention(RUNTIME)
@Target(FIELD)
public @interface Format {
String pattern() default "yyyy-MM-dd HH:mm:ss";
String timezone() default "GMT+8";
}

可以用这两个注解来修饰要序列化的类字段,比如:

1
2
3
4
5
6
7
8
9
static class Student {
@Label("姓名")
String name;
@Label("出生日期")
@Format(pattern="yyyy/MM/dd")
Date born;
@Label("分数")
double score;
//其他代码

我们可以这样来使用SimpleFormatter:

1
2
3
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Student zhangsan = new Student("张三", sdf.parse("1990-12-12"), 80.9d);
System.out.println(SimpleFormatter.format(zhangsan));

输出为:

1
2
3
姓名:张三
出生日期:1990/12/12
分数:80.9

可以看出,输出使用了自定义的字段名称和日期格式,SimpleFormatter.format()是怎么利用这些注解的呢?我们看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static String format(Object obj) {
try {
Class<? > cls = obj.getClass();
StringBuilder sb = new StringBuilder();
for(Field f : cls.getDeclaredFields()) {
if(! f.isAccessible()) {
f.setAccessible(true);
}
Label label = f.getAnnotation(Label.class);
String name = label ! = null ? label.value() : f.getName();
Object value = f.get(obj);
if(value ! = null && f.getType() == Date.class) {
value = formatDate(f, value);
}
sb.append(name + ":" + value + "\n");
}
return sb.toString();
}
catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}

对于日期类型的字段,调用了formatDate,其代码为:

1
2
3
4
5
6
7
8
9
private static Object formatDate(Field f, Object value) {
Format format = f.getAnnotation(Format.class);
if(format ! = null) {
SimpleDateFormat sdf = new SimpleDateFormat(format.pattern());
sdf.setTimeZone(TimeZone.getTimeZone(format.timezone()));
return sdf.format(value);
}
return value;
}

这些代码都比较简单,我们就不解释了。

22.4 查看注解信息

创建了注解,就可以在程序中使用,注解指定的目标,提供需要的参数,但这还是不会影响到程序的运行。要影响程序,我们要先能查看这些信息。我们主要考虑@Retention为RetentionPolicy.RUNTIME的注解,利用反射机制在运行时进行查看和利用这些信息。

在上一章,我们提到了反射相关类中与注解有关的方法,这里汇总说明下,Class、Field、Method、Constructor中都有如下方法:

1
2
3
4
5
6
7
8
9
//获取所有的注解
public Annotation[] getAnnotations()
//获取所有本元素上直接声明的注解,忽略inherited来的
public Annotation[] getDeclaredAnnotations()
//获取指定类型的注解,没有返回null
public <A extends Annotation> A getAnnotation(Class<A> annotationClass)
//判断是否有指定类型的注解
public boolean isAnnotationPresent(
Class<? extends Annotation> annotationClass)

Annotation是一个接口,它表示注解,具体定义为:

1
2
3
4
5
6
7
public interface Annotation {
boolean equals(Object obj);
int hashCode();
String toString();
//返回真正的注解类型
Class<? extends Annotation> annotationType();
}

实际上,内部实现时,所有的注解类型都是扩展的Annotation。

对于Method和Contructor,它们都有方法参数,而参数也可以有注解,所以它们都有如下方法:

1
public Annotation[][] getParameterAnnotations()

返回值是一个二维数组,每个参数对应一个一维数组。我们看个简单的例子:

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
37
public class MethodAnnotations {
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
static @interface QueryParam {
String value();
}
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
static @interface DefaultValue {
String value() default "";
}
public void hello(@QueryParam("action") String action,
@QueryParam("sort") @DefaultValue("asc") String sort){
//…
}
public static void main(String[] args) throws Exception {
Class<? > cls = MethodAnnotations.class;
Method method = cls.getMethod("hello",
new Class[]{String.class, String.class});
Annotation[][] annts = method.getParameterAnnotations();
for(int i=0; i<annts.length; i++){
System.out.println("annotations for paramter " + (i+1));
Annotation[] anntArr = annts[i];
for(Annotation annt : anntArr){
if(annt instanceof QueryParam){
QueryParam qp = (QueryParam)annt;
System.out.println(qp.annotationType()
.getSimpleName()+":"+ qp.value());
}else if(annt instanceof DefaultValue){
DefaultValue dv = (DefaultValue)annt;
System.out.println(dv.annotationType()
.getSimpleName()+":"+ dv.value());
}
}
}
}
}

这里定义了两个注解@QueryParam@DefaultValue,都用于修饰方法参数,方法hello使用了这两个注解,在main方法中,我们演示了如何获取方法参数的注解信息,输出为:

1
2
3
4
5
annotations for paramter 1
QueryParam:action
annotations for paramter 2
QueryParam:sort
DefaultValue:asc

代码比较简单,就不赘述了。

定义了注解,通过反射获取到注解信息,但具体怎么利用这些信息呢?我们看两个简单的示例,一个是定制序列化,另一个是DI(依赖注入)容器。

22.3 创建注解

框架和库是怎么实现注解的呢?我们来看注解的创建。

我们通过一些例子来说明,先看@Override的定义:

1
2
3
4
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

定义注解与定义接口有点类似,都用了interface,不过注解的interface前多了@。另外,它还有两个元注解@Target@Retention,这两个注解专门用于定义注解本身。@Target表示注解的目标,@Override的目标是方法(ElementType.METHOD)。ElementType是一个枚举,主要可选值有:

  • TYPE:表示类、接口(包括注解),或者枚举声明;
  • FIELD:字段,包括枚举常量;
  • METHOD:方法;
  • PARAMETER:方法中的参数;
  • CONSTRUCTOR:构造方法;
  • LOCAL_VARIABLE:本地变量;
  • MODULE:模块(Java 9引入的)。

目标可以有多个,用{}表示,比如@SuppressWarnings@Target就有多个。Java 7的定义为:

1
2
3
4
5
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
String[] value();
}

如果没有声明@Target,默认为适用于所有类型。

@Retention表示注解信息保留到什么时候,取值只能有一个,类型为RetentionPolicy,它是一个枚举,有三个取值。

  • SOURCE:只在源代码中保留,编译器将代码编译为字节码文件后就会丢掉。
  • CLASS:保留到字节码文件中,但Java虚拟机将class文件加载到内存时不一定会在内存中保留。
  • RUNTIME:一直保留到运行时。

如果没有声明@Retention,则默认为CLASS。

@Override和@SuppressWarnings都是给编译器用的,所以@Retention都是Retention-Policy.SOURCE。

可以为注解定义一些参数,定义的方式是在注解内定义一些方法,比如@Suppress-Warnings内定义的方法value,返回值类型表示参数的类型,这里是String[]。使用@Suppress-Warnings时必须给value提供值,比如:

1
@SuppressWarnings(value={"deprecation", "unused"})

当只有一个参数,且名称为value时,提供参数值时可以省略”value=”,即上面的代码可以简写为:

1
@SuppressWarnings({"deprecation", "unused"})

注解内参数的类型不是什么都可以的,合法的类型有基本类型、String、Class、枚举、注解,以及这些类型的数组。

参数定义时可以使用default指定一个默认值,比如,Guice中Inject注解的定义:

1
2
3
4
5
6
@Target({ METHOD, CONSTRUCTOR, FIELD })
@Retention(RUNTIME)
@Documented
public @interface Inject {
boolean optional() default false;
}

它有一个参数optional,默认值为false。如果类型为String,默认值可以为””,但不能为null。如果定义了参数且没有提供默认值,在使用注解时必须提供具体的值,不能为null。

@Inject多了一个元注解@Documented,它表示注解信息包含到生成的文档中。
与接口和类不同,注解不能继承。不过注解有一个与继承有关的元注解@Inherited,它是什么意思呢?我们看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class InheritDemo {
@Inherited
@Retention(RetentionPolicy.RUNTIME)
static @interface Test {
}
@Test
static class Base {
}
static class Child extends Base {
}
public static void main(String[] args) {
System.out.println(Child.class.isAnnotationPresent(Test.class));
}
}

Test是一个注解,类Base有该注解,Child继承了Base但没有声明该注解。main方法检查Child类是否有Test注解,输出为true,这是因为Test有注解@Inherited,如果去掉,输出会变成false。

22.2 框架和库的注解

各种框架和库定义了大量的注解,程序员使用这些注解配置框架和库,与它们进行交互,我们先来看一些例子,包括Jackson、依赖注入容器、Servlet 3.0、Web应用框架等,最后我们总结下使用注解的思维逻辑。

1. Jackson

Jackson是一个通用的序列化库,程序员可以使用它提供的注解对序列化进行定制,比如:

  • 使用@JsonIgnore@JsonIgnoreProperties配置忽略字段。
  • 使用@JsonManagedReference@JsonBackReference配置互相引用关系。
  • 使用@JsonProperty@JsonFormat配置字段的名称和格式等。

在Java提供注解功能之前,同样的配置功能也是可以实现的,一般通过配置文件实现,但是配置项和要配置的程序元素不在一个地方,难以管理和维护,使用注解就简单多了,代码和配置放在一起,一目了然,易于理解和维护。

2.依赖注入容器

现代Java开发经常利用某种框架管理对象的生命周期及其依赖关系,这个框架一般称为DI(Dependency Injection)容器。DI是指依赖注入,流行的框架有Spring、Guice等。在使用这些框架时,程序员一般不通过new创建对象,而是由容器管理对象的创建,对于依赖的服务,也不需要自己管理,而是使用注解表达依赖关系。这么做的好处有很多,代码更为简单,也更为灵活,比如容器可以根据配置返回一个动态代理,实现AOP,这部分我们在下一章再介绍。

看个简单的例子,Guice定义了Inject注解,可以使用它表达依赖关系,比如像下面这样:

1
2
3
4
5
6
7
public class OrderService {
@Inject
UserService userService;
@Inject
ProductService productService;
//…
}

3. Servlet 3.0

Servlet是Java为Web应用提供的技术框架,早期的Servlet只能在web.xml中进行配置,而Servlet 3.0则开始支持注解,可以使用@WebServlet配置一个类为Servlet,比如:

1
2
@WebServlet(urlPatterns = "/async", asyncSupported = true)
public class AsyncDemoServlet extends HttpServlet {…}

4. Web应用框架

在Web开发中,典型的架构都是MVC(Model-View-Controller),典型的需求是配置哪个方法处理哪个URL的什么HTTP方法,然后将HTTP请求参数映射为Java方法的参数。各种框架如Spring MVC、Jersey等都支持使用注解进行配置,比如,使用Jersey的一个配置示例为:

1
2
3
4
5
6
7
8
9
10
11
12
@Path("/hello")
public class HelloResource {
@GET
@Path("test")
@Produces(MediaType.APPLICATION_JSON)
public Map<String, Object> test(
@QueryParam("a") String a) {
Map<String, Object> map = new HashMap<>();
map.put("status", "ok");
return map;
}
}

类HelloResource将处理Jersey配置的根路径下/hello下的所有请求,而test方法将处理/hello/test的GET请求,响应格式为JSON,自动映射HTTP请求参数a到方法参数String a。

5.神奇的注解

通过以上的例子,我们可以看出,注解似乎有某种神奇的力量,通过简单的声明,就可以达到某种效果。在某些方面,它类似于我们之前介绍的序列化,序列化机制中通过简单的Serializable接口,Java就能自动处理很多复杂的事情。它也类似于我们在并发部分中介绍的synchronized关键字,通过它可以自动实现同步访问。

这些都是声明式编程风格,在这种风格中,程序都由三个组件组成:

  • 声明的关键字和语法本身。
  • 系统/框架/库,它们负责解释、执行声明式的语句。
  • 应用程序,使用声明式风格写程序。

在编程的世界里,访问数据库的SQL语言、编写网页样式的CSS,以及后续章节将要介绍的正则表达式、函数式编程都是这种风格,这种风格降低了编程的难度,为应用程序员提供了更为高级的语言,使得程序员可以在更高的抽象层次上思考和解决问题,而不是陷于底层的细节实现

第22章 注解

前一章我们探讨了反射,反射相关的类中都有方法获取注解信息,我们在前面章节中也多次提到过注解,注解到底是什么呢?在Java中,注解就是给程序添加一些信息,用字符@开头,这些信息用于修饰它后面紧挨着的其他代码元素,比如类、接口、字段、方法、方法中的参数、构造方法等。注解可以被编译器、程序运行时和其他工具使用,用于增强或修改程序行为等。

这么说比较抽象,下面我们会具体介绍。先介绍Java的一些内置注解,然后介绍一些框架和库的注解,了解了注解的使用之后,介绍怎么创建注解,如何利用反射查看注解信息,最后我们介绍注解的两个应用:定制序列化和依赖注入容器。

22.1 内置注解

Java内置了一些常用注解:@Override、@Deprecated、@SuppressWarnings,我们简要介绍。

1. @Override

@Override修饰一个方法,表示该方法不是当前类首先声明的,而是在某个父类或实现的接口中声明的,当前类“重写”了该方法,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
static class Base {
public void action() {};
}
static class Child extends Base {
@Override
public void action(){
System.out.println("child action");
}
@Override
public String toString() {
return "child";
}
}

Child的action方法重写了父类Base中的action方法,toString方法重写了Object类中的toString方法。这个注解不写也不会改变这些方法是“重写”的本质,那有什么用呢?它可以减少一些编程错误。如果方法有Override注解,但没有任何父类或实现的接口声明该方法,则编译器会报错,强制程序员修复该问题。比如,在上面的例子中,如果程序员修改了Base方法中的action方法定义,变为了:

1
2
3
static class Base {
public void doAction() {};
}

但是,程序员忘记了修改Child方法,如果没有Override注解,编译器不会报告任何错误,它会认为action方法是Child新加的方法,doAction会调用父类的方法,这与程序员的期望是不符的,而有了Override注解,编译器就会报告错误。所以,如果方法是在父类或接口中定义的,加上@Override吧,让编译器帮你减少错误。

2. @Deprecated

@Deprecated可以修饰的范围很广,包括类、方法、字段、参数等,它表示对应的代码已经过时了,程序员不应该使用它,不过,它是一种警告,而不是强制性的,在IDE如Eclipse中,会给Deprecated元素加一条删除线以示警告。比如,Date中很多方法就过时了:

1
2
3
4
@Deprecated
public Date(int year, int month, int date)
@Deprecated
public int getYear()

在声明元素为@Deprecated时,应该用Java文档注释的方式同时说明替代方案,就像Date中的API文档那样,在调用@Deprecated方法时,应该先考虑其建议的替代方案。

从Java 9开始,@Deprecated多了两个属性:since和forRemoval。since是一个字符串,表示是从哪个版本开始过时的;forRemoval是一个boolean值,表示将来是否会删除。比如,Java 9中Integer的一个构造方法就从版本9开始过时了,其代码为:

1
2
3
4
@Deprecated(since="9")
public Integer(int value) {
this.value = value;
}

3. @SuppressWarnings

@SuppressWarnings表示压制Java的编译警告,它有一个必填参数,表示压制哪种类型的警告,它也可以修饰大部分代码元素,在更大范围的修饰也会对内部元素起效,比如,在类上的注解会影响到方法,在方法上的注解会影响到代码行。对于Date方法的调用,可以这样压制警告:

1
2
3
4
5
@SuppressWarnings({"deprecation", "unused"})
public static void main(String[] args) {
Date date = new Date(2017, 4, 12);
int year = date.getYear();
}

Java提供的内置注解比较少,我们日常开发中使用的注解基本都是自定义的。不过,一般也不是我们定义的,而是由各种框架和库定义的,我们主要还是根据它们的文档直接使用。

21.3 反射与泛型

在介绍泛型的时候,我们提到,泛型参数在运行时会被擦除,这里,我们需要补充一下,在类信息Class中依然有关于泛型的一些信息,可以通过反射得到。泛型涉及一些更多的方法和类,上面的介绍中进行了忽略,这里简要补充下。

Class有如下方法,可以获取类的泛型参数信息:

1
public TypeVariable<Class<T>>[] getTypeParameters()

Field有如下方法:

1
public Type getGenericType()

Method有如下方法:

1
2
3
public Type getGenericReturnType()
public Type[] getGenericParameterTypes()
public Type[] getGenericExceptionTypes()

Constructor有如下方法:

1
public Type[] getGenericParameterTypes()

Type是一个接口,Class实现了Type, Type的其他子接口还有:

  • TypeVariable:类型参数,可以有上界,比如T extends Number;
  • ParameterizedType:参数化的类型,有原始类型和具体的类型参数,比如List<String>
  • WildcardType:通配符类型,比如?、? extends Number、? superInteger。

我们看一个简单的示例,如代码清单21-2所示。

代码清单21-2 通过反射获取泛型信息示例
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
public class GenericDemo {
static class GenericTest<U extends Comparable<U>, V> {
U u;
V v;
List<String> list;
public U test(List<? extends Number> numbers) {
return null;
}
}
public static void main(String[] args) throws Exception {
Class<? > cls = GenericTest.class;
//类的类型参数
for(TypeVariable t : cls.getTypeParameters()) {
System.out.println(t.getName() + " extends " +
Arrays.toString(t.getBounds()));
}
//字段:泛型类型
Field fu = cls.getDeclaredField("u");
System.out.println(fu.getGenericType());
//字段:参数化的类型
Field flist = cls.getDeclaredField("list");
Type listType = flist.getGenericType();
if(listType instanceof ParameterizedType) {
ParameterizedType pType = (ParameterizedType) listType;
System.out.println("raw type: " + pType.getRawType()
+ ", type arguments:"
+ Arrays.toString(pType.getActualTypeArguments()));
}
//方法的泛型参数
Method m = cls.getMethod("test", new Class[] { List.class });
for(Type t : m.getGenericParameterTypes()) {
System.out.println(t);
}
}
}

程序的输出为:

1
2
3
4
5
U extends [java.lang.Comparable<U>]
V extends [class java.lang.Object]
U
raw type: interface java.util.List, type arguments:[class java.lang.String]
java.util.List<? extends java.lang.Number>

代码比较简单,我们就不赘述了。

本章介绍了Java中反射相关的主要类和方法,通过入口类Class,可以访问类的各种信息,如字段、方法、构造方法、父类、接口、泛型信息等,也可以创建和操作对象、调用方法等,利用这些方法,可以编写通用的、动态灵活的程序,本章演示了一个简单的通用序列化/反序列化类SimpleMapper。

反射虽然是灵活的,但一般情况下,并不是我们优先建议的,主要原因是:
1)反射更容易出现运行时错误,使用显式的类和接口,编译器能帮我们做类型检查,减少错误,但使用反射,类型是运行时才知道的,编译器无能为力。
2)反射的性能要低一些,在访问字段、调用方法前,反射先要查找对应的Field/Method,要慢一些。

简单地说,如果能用接口实现同样的灵活性,就不要使用反射

本章介绍的很多类(如Class、Field、Method、Constructor)都可以有注解,注解到底是什么呢?让我们下章探讨。

21.2 应用示例

介绍了Class的这么多方法,有什么用呢?我们看个简单的示例,利用反射实现一个简单的通用序列化/反序列化类SimpleMapper ,它提供两个静态方法:

1
2
public static String toString(Object obj)
public static Object fromString(String str)

toString将对象obj转换为字符串,fromString将字符串转换为对象。为简单起见,我们只支持最简单的类,即有默认构造方法,成员类型只有基本类型、包装类或String。另外,序列化的格式也很简单,第一行为类的名称,后面每行表示一个字段,用字符’=’分隔,表示字段名称和字符串形式的值。我们先看SimpleMapper的用法,如代码清单21-1所示。

代码清单21-1 简单的通用序列化/反序列化类SimpleMapper的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

public static String toString(Object obj)
public static Object fromString(String str)
public class SimpleMapperDemo {
static class Student {
String name;
int age;
Double score;
//省略了构造方法, getter/setter和toString方法
}
public static void main(String[] args) {
Student zhangsan = new Student("张三", 18, 89d);
String str = SimpleMapper.toString(zhangsan);
Student zhangsan2 = (Student) SimpleMapper.fromString(str);
System.out.println(zhangsan2);
}
}

代码先调用toString方法将对象转换为了String,然后调用fromString方法将字符串转换为了Student,新对象的值与原对象是一样的,输出如下所示:

1
Student [name=张三, age=18, score=89.0]

我们来看SimpleMapper的示例实现(主要用于演示原理), toString的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static String toString(Object obj) {
try {
Class<? > cls = obj.getClass();
StringBuilder sb = new StringBuilder();
sb.append(cls.getName() + "\n");
for(Field f : cls.getDeclaredFields()) {
if(! f.isAccessible()) {
f.setAccessible(true);
}
sb.append(f.getName() + "=" + f.get(obj).toString() + "\n");
}
return sb.toString();
} catch(IllegalAccessException e) {
throw new RuntimeException(e);
}
}

代码比较简单,我们就不赘述了。fromString的代码为:

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
public static Object fromString(String str) {
try {
String[] lines = str.split("\n");
if(lines.length < 1) {
throw new IllegalArgumentException(str);
}
Class<? > cls = Class.forName(lines[0]);
Object obj = cls.newInstance();
if(lines.length > 1) {
for(int i = 1; i < lines.length; i++) {
String[] fv = lines[i].split("=");
if(fv.length ! = 2) {
throw new IllegalArgumentException(lines[i]);
}
Field f = cls.getDeclaredField(fv[0]);
if(! f.isAccessible()){
f.setAccessible(true);
}
setFieldValue(f, obj, fv[1]);
}
}
return obj;
} catch(Exception e) {
throw new RuntimeException(e);
}
}

它调用了setFieldValue方法对字段设置值,其代码为:

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
private static void setFieldValue(Field f, Object obj, String value)
throws Exception {
Class<? > type = f.getType();
if(type == int.class) {
f.setInt(obj, Integer.parseInt(value));
} else if(type == byte.class) {
f.setByte(obj, Byte.parseByte(value));
} else if(type == short.class) {
f.setShort(obj, Short.parseShort(value));
} else if(type == long.class) {
f.setLong(obj, Long.parseLong(value));
} else if(type == float.class) {
f.setFloat(obj, Float.parseFloat(value));
} else if(type == double.class) {
f.setDouble(obj, Double.parseDouble(value));
} else if(type == char.class) {
f.setChar(obj, value.charAt(0));
} else if(type == boolean.class) {
f.setBoolean(obj, Boolean.parseBoolean(value));
} else if(type == String.class) {
f.set(obj, value);
} else {
Constructor<? > ctor = type.getConstructor(
new Class[] { String.class });
f.set(obj, ctor.newInstance(value));
}
}

setFieldValue根据字段的类型,将字符串形式的值转换为了对应类型的值,对于基本类型和String以外的类型,它假定该类型有一个以String类型为参数的构造方法。

示例的完整代码在github上,地址为 https://github.com/swiftma/program-logic ,位于包shuo.laoma.dynamic.c84下。

第21章 反射

从本章开始,我们来探讨Java中的一些动态特性,包括反射、注解、动态代理、类加载器等。利用这些特性,可以优雅地实现一些灵活通用的功能,它们经常用于各种框架、库和系统程序中,比如:
1)14.5节介绍的Jackson,利用反射和注解实现了通用的序列化机制。
2)有多种库(如Spring MVC、Jersey)用于处理Web请求,利用反射和注解,能方便地将用户的请求参数和内容转换为Java对象,将Java对象转变为响应内容。
3)有多种库(如Spring、Guice)利用这些特性实现了对象管理容器,方便程序员管理对象的生命周期以及其中复杂的依赖关系。
4)应用服务器(如Tomcat)利用类加载器实现不同应用之间的隔离,JSP技术利用类加载器实现修改代码不用重启就能生效的特性。
5)面向方面的编程AOP(Aspect Oriented Programming)将编程中通用的关注点(如日志记录、安全检查等)与业务的主体逻辑相分离,减少冗余代码,提高程序的可维护性, AOP需要依赖上面的这些特性来实现。

本章主要介绍反射机制,后续章节介绍其他内容。

在一般操作数据的时候,我们都是知道并且依赖于数据类型的,比如:
1)根据类型使用new创建对象。
2)根据类型定义变量,类型可能是基本类型、类、接口或数组。
3)将特定类型的对象传递给方法。
4)根据类型访问对象的属性,调用对象的方法。

编译器也是根据类型进行代码的检查编译的。

反射不一样,它是在运行时,而非编译时,动态获取类型的信息,比如接口信息、成员信息、方法信息、构造方法信息等,根据这些动态获取到的信息创建对象、访问/修改成员、调用方法等。这么说比较抽象,下面我们会具体说明。反射的入口是名称为Class的类,我们先介绍Class类,随后举例说明反射的应用,接着讨论反射与泛型,最后进行总结。

21.1 Class类

在介绍类和继承的实现原理时,我们提到,每个已加载的类在内存都有一份类信息,每个对象都有指向它所属类信息的引用。Java中,类信息对应的类就是java.lang.Class。注意不是小写的class, class是定义类的关键字。所有类的根父类Object有一个方法,可以获取对象的Class对象:

1
public final native Class<? > getClass()

Class是一个泛型类,有一个类型参数,getClass()并不知道具体的类型,所以返回Class<?>

获取Class对象不一定需要实例对象,如果在写程序时就知道类名,可以使用<类名>.class获取Class对象,比如:

1
Class<Date> cls = Date.class;

接口也有Class对象,且这种方式对于接口也是适用的,比如:

1
Class<Comparable> cls = Comparable.class;

基本类型没有getClass方法,但也都有对应的Class对象,类型参数为对应的包装类型,比如:

1
2
3
4
Class<Integer> intCls = int.class;
Class<Byte> byteCls = byte.class;
Class<Character> charCls = char.class;
Class<Double> doubleCls = double.class;

void作为特殊的返回类型,也有对应的Class:

1
Class<Void> voidCls = void.class;

对于数组,每种类型都有对应数组类型的Class对象,每个维度都有一个,即一维数组有一个,二维数组有一个不同的类型。比如:

1
2
3
4
5
6
String[] strArr = new String[10];
int[][] twoDimArr = new int[3][2];
int[] oneDimArr = new int[10];
Class<? extends String[]> strArrCls = strArr.getClass();
Class<? extends int[][]> twoDimArrCls = twoDimArr.getClass();
Class<? extends int[]> oneDimArrCls = oneDimArr.getClass();

枚举类型也有对应的Class,比如:

1
2
3
4
enum Size {
SMALL, MEDIUM, BIG
}
Class<Size> cls = Size.class;

Class有一个静态方法forName,可以根据类名直接加载Class,获取Class对象,比如:

1
2
3
4
5
6
try {
Class<? > cls = Class.forName("java.util.HashMap");
System.out.println(cls.getName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

注意forName可能抛出异常ClassNotFoundException。

有了Class对象后,我们就可以了解到关于类型的很多信息,并基于这些信息采取一些行动。Class的方法很多,大部分比较简单直接,容易理解,下面,我们分为若干组,包括名称信息、字段信息、方法信息、创建对象和构造方法、类型信息等,进行简要介绍。

1.名称信息

Class有如下方法,可以获取与名称有关的信息:

1
2
3
4
public String getName()
public String getSimpleName()
public String getCanonicalName()
public Package getPackage()

getSimpleName返回的名称不带包信息,getName返回的是Java内部使用的真正的名称,getCanonicalName返回的名称更为友好,getPackage返回的是包信息,它们的不同如表格21-1所示。

表21-1 不同Class对象的各种名称方法的返回值

epub_923038_139
需要说明的是数组类型的getName返回值,它使用前缀[表示数组,有几个[表示是几维数组;数组的类型用一个字符表示,I表示int, L表示类或接口,其他类型与字符的对应关系为:boolean(Z)、byte(B)、char(C)、double(D)、float(F)、long(J)、short(S)。对于引用类型的数组,注意最后有一个分号;。

2.字段信息

类中定义的静态和实例变量都被称为字段,用类Field表示,位于包java.lang.reflect下,后文涉及的反射相关的类都位于该包下。Class有4个获取字段信息的方法:

1
2
3
4
5
6
7
8
//返回所有的public字段,包括其父类的,如果没有字段,返回空数组
public Field[] getFields()
//返回本类声明的所有字段,包括非public的,但不包括父类的
public Field[] getDeclaredFields()
//返回本类或父类中指定名称的public字段,找不到抛出异常NoSuchFieldException
public Field getField(String name)
//返回本类中声明的指定名称的字段,找不到抛出异常NoSuchFieldException
public Field getDeclaredField(String name)

Field也有很多方法,可以获取字段的信息,也可以通过Field访问和操作指定对象中该字段的值,基本方法有:

1
2
3
4
5
6
7
8
9
10
//获取字段的名称
public String getName()
//判断当前程序是否有该字段的访问权限
public boolean isAccessible()
//flag设为true表示忽略Java的访问检查机制,以允许读写非public的字段
public void setAccessible(boolean flag)
//获取指定对象obj中该字段的值
public Object get(Object obj)
//将指定对象obj中该字段的值设为value
public void set(Object obj, Object value)

在get/set方法中,对于静态变量,obj被忽略,可以为null,如果字段值为基本类型, get/set会自动在基本类型与对应的包装类型间进行转换;对于private字段,直接调用get/set会抛出非法访问异常IllegalAccessException,应该先调用setAccessible(true)以关闭Java的检查机制。看段简单的示例代码:

1
2
3
4
5
6
List<String> obj = Arrays.asList(new String[]{"老马", "编程"});
Class<? > cls = obj.getClass();
for(Field f : cls.getDeclaredFields()){
f.setAccessible(true);
System.out.println(f.getName()+" - "+f.get(obj));
}

代码比较简单,就不赘述了。除了以上方法,Field还有很多其他方法,比如:

1
2
3
4
5
6
7
8
9
10
11
12
//返回字段的修饰符
public int getModifiers()
//返回字段的类型
public Class<? > getType()
//以基本类型操作字段
public void setBoolean(Object obj, boolean z)
public boolean getBoolean(Object obj)
public void setDouble(Object obj, double d)
public double getDouble(Object obj)
//查询字段的注解信息,下一章介绍注解
public <T extends Annotation> T getAnnotation(Class<T> annotationClass)
public Annotation[] getDeclaredAnnotations()

getModifiers返回的是一个int,可以通过Modifier类的静态方法进行解读。比如,假定Student类有如下字段:

1
public static final int MAX_NAME_LEN = 255;

可以这样查看该字段的修饰符:

1
2
3
4
5
6
7
Field f = Student.class.getField("MAX_NAME_LEN");
int mod = f.getModifiers();
System.out.println(Modifier.toString(mod));
System.out.println("isPublic: " + Modifier.isPublic(mod));
System.out.println("isStatic: " + Modifier.isStatic(mod));
System.out.println("isFinal: " + Modifier.isFinal(mod));
System.out.println("isVolatile: " + Modifier.isVolatile(mod));

输出为:

1
2
3
4
5
public static final
isPublic: true
isStatic: true
isFinal: true
isVolatile: false

3.方法信息

类中定义的静态和实例方法都被称为方法,用类Method表示。Class有如下相关方法:

1
2
3
4
5
6
7
8
9
//返回所有的public方法,包括其父类的,如果没有方法,返回空数组
public Method[] getMethods()
//返回本类声明的所有方法,包括非public的,但不包括父类的
public Method[] getDeclaredMethods()
//返回本类或父类中指定名称和参数类型的public方法,
//找不到抛出异常NoSuchMethodException
public Method getMethod(String name, Class<? >... parameterTypes)
//返回本类中声明的指定名称和参数类型的方法,找不到抛出异常NoSuchMethodException
public Method getDeclaredMethod(String name, Class<? >... parameterTypes)

通过Method可以获取方法的信息,也可以通过Method调用对象的方法,基本方法有:

1
2
3
4
5
6
7
//获取方法的名称
public String getName()
//flag设为true表示忽略Java的访问检查机制,以允许调用非public的方法
public void setAccessible(boolean flag)
//在指定对象obj上调用Method代表的方法,传递的参数列表为args
public Object invoke(Object obj, Object... args) throws
IllegalAccessException, Illegal-ArgumentException, InvocationTargetException

对invoke方法,如果Method为静态方法,obj被忽略,可以为null, args可以为null,也可以为一个空的数组,方法调用的返回值被包装为Object返回,如果实际方法调用抛出异常,异常被包装为InvocationTargetException重新抛出,可以通过getCause方法得到原异常。看段简单的示例:

1
2
3
4
5
6
7
8
9
Class<? > cls = Integer.class;
try {
Method method = cls.getMethod("parseInt", new Class[]{String.class});
System.out.println(method.invoke(null, "123"));
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}

Method还有很多方法,可以获取其修饰符、参数、返回值、注解等信息,具体就不列举了。

4.创建对象和构造方法

Class有一个方法,可以用它来创建对象:

1
public T newInstance() throws InstantiationException, IllegalAccessException

它会调用类的默认构造方法(即无参public构造方法),如果类没有该构造方法,会抛出异常InstantiationException。看个简单示例:

1
2
Map<String, Integer> map = HashMap.class.newInstance();
map.put("hello", 123);

newInstance只能使用默认构造方法。Class还有一些方法,可以获取所有的构造方法:

1
2
3
4
5
6
7
8
//获取所有的public构造方法,返回值可能为长度为0的空数组
public Constructor<? >[] getConstructors()
//获取所有的构造方法,包括非public的
public Constructor<? >[] getDeclaredConstructors()
//获取指定参数类型的public构造方法,没找到抛出异常NoSuchMethodException
public Constructor<T> getConstructor(Class<? >... parameterTypes)
//获取指定参数类型的构造方法,包括非public的,没找到抛出异常NoSuchMethodException
public Constructor<T> getDeclaredConstructor(Class<? >... parameterTypes)

类Constructor表示构造方法,通过它可以创建对象,方法为:

1
2
public T newInstance(Object ... initargs) throws InstantiationException,
IllegalAccessException, IllegalArgumentException, InvocationTargetException

看个例子:

1
2
3
Constructor<StringBuilder> contructor= StringBuilder.class
.getConstructor(new Class[]{int.class});
StringBuilder sb = contructor.newInstance(100);

除了创建对象,Constructor还有很多方法,可以获取关于构造方法的很多信息,包括参数、修饰符、注解等,具体就不列举了。

5.类型检查和转换

我们之前介绍过instanceof关键字,它可以用来判断变量指向的实际对象类型。instanceof后面的类型是在代码中确定的,如果要检查的类型是动态的,可以使用Class类的如下方法:

1
public native boolean isInstance(Object obj)

也就是说,如下代码:

1
2
3
if(list instanceof ArrayList){
System.out.println("array list");
}

和下面代码的输出是相同的:

1
2
3
4
Class cls = Class.forName("java.util.ArrayList");
if(cls.isInstance(list)){
System.out.println("array list");
}

除了判断类型,在程序中也往往需要进行强制类型转换,比如:

1
2
3
4
List list = ..
if(list instanceof ArrayList){
ArrayList arrList = (ArrayList)list;
}

在这段代码中,强制转换到的类型是在写代码时就知道的。如果是动态的,可以使用Class的如下方法:

1
public T cast(Object obj)

比如:

1
2
3
public static <T> T toType(Object obj, Class<T> cls){
return cls.cast(obj);
}

isInstance/cast描述的都是对象和类之间的关系,Class还有一个方法,可以判断Class之间的关系:

1
2
//检查参数类型cls能否赋给当前Class类型的变量
public native boolean isAssignableFrom(Class<? > cls);

比如,如下表达式的结果都为true:

1
2
3
Object.class.isAssignableFrom(String.class)
String.class.isAssignableFrom(String.class)
List.class.isAssignableFrom(ArrayList.class)

6.Class的类型信息

Class代表的类型既可以是普通的类,也可以是内部类,还可以是基本类型、数组等,对于一个给定的Class对象,它到底是什么类型呢?可以通过以下方法进行检查:

1
2
3
4
5
6
7
8
public native boolean isArray()  //是否是数组
public native boolean isPrimitive() //是否是基本类型
public native boolean isInterface() //是否是接口
public boolean isEnum() //是否是枚举
public boolean isAnnotation() //是否是注解
public boolean isAnonymousClass() //是否是匿名内部类
public boolean isMemberClass() //是否是成员类,成员类定义在方法外,不是匿名类
public boolean isLocalClass() //是否是本地类,本地类定义在方法内,不是匿名类

7.类的声明信息

Class还有很多方法,可以获取类的声明信息,如修饰符、父类、接口、注解等,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//获取修饰符,返回值可通过Modifier类进行解读
public native int getModifiers()
//获取父类,如果为Object,父类为null
public native Class<? super T> getSuperclass()
//对于类,为自己声明实现的所有接口,对于接口,为直接扩展的接口,不包括通过父类继承的
public native Class<? >[] getInterfaces();
//自己声明的注解
public Annotation[] getDeclaredAnnotations()
//所有的注解,包括继承得到的
public Annotation[] getAnnotations()
//获取或检查指定类型的注解,包括继承得到的
public <A extends Annotation> A getAnnotation(Class<A> annotationClass)
public boolean isAnnotationPresent(
Class<? extends Annotation> annotationClass)

8.类的加载

Class有两个静态方法,可以根据类名加载类:

1
2
public static Class<? > forName(String className)
public static Class<? > forName(String name, boolean initialize,ClassLoader loader)

ClassLoader表示类加载器,第24章会进一步介绍,initialize表示加载后,是否执行类的初始化代码(如static语句块)。第一个方法中没有传这些参数,相当于调用:

1
Class.forName(className, true, currentLoader)

currentLoader表示加载当前类的ClassLoader。

这里className与Class.getName的返回值是一致的。比如,对于String数组:

1
2
3
String name = "[Ljava.lang.String; ";
Class cls = Class.forName(name);
System.out.println(cls == String[].class);

需要注意的是,基本类型不支持forName方法,也就是说,如下写法:

1
Class.forName("int");

会抛出异常ClassNotFoundException。那如何根据原始类型的字符串构造Class对象呢?可以对Class.forName进行一下包装,比如:

1
2
3
4
5
6
7
8
public static Class<? > forName(String className)
throws ClassNotFoundException{
if("int".equals(className)){
return int.class;
}
//其他基本类型略
return Class.forName(className);
}

需要说明的是,Java 9还有一个forName方法,用于加载指定模块中指定名称的类:

1
public static Class<? > forName(Module module, String name)

参数module表示模块,这是Java 9引入的类,当找不到类的时候,它不会抛出异常,而是返回null,它也不会执行类的初始化。

9.反射与数组

对于数组类型,有一个专门的方法,可以获取它的元素类型:

1
public native Class<? > getComponentType()

比如:

1
2
String[] arr = new String[]{};
System.out.println(arr.getClass().getComponentType());

输出为:

1
class java.lang.String

java.lang.reflect包中有一个针对数组的专门的类Array(注意不是java.util中的Arrays),提供了对于数组的一些反射支持,以便于统一处理多种类型的数组,主要方法有:

1
2
3
4
5
6
7
8
9
10
//创建指定元素类型、指定长度的数组
public static Object newInstance(Class<? > componentType, int length)
//创建多维数组
public static Object newInstance(Class<? > componentType, int... dimensions)
//获取数组array指定的索引位置index处的值
public static native Object get(Object array, int index)
//修改数组array指定的索引位置index处的值为value
public static native void set(Object array, int index, Object value)
//返回数组的长度
public static native int getLength(Object array)

需要注意的是,在Array类中,数组是用Object而非Object[]表示的,这是为什么呢?这是为了方便处理多种类型的数组。int[]String[]都不能与Object[]相互转换,但可以与Object相互转换,比如:

1
2
int[] intArr = (int[])Array.newInstance(int.class, 10);
String[] strArr = (String[])Array.newInstance(String.class, 10);

除了以Object类型操作数组元素外,Array也支持以各种基本类型操作数组元素,如:

1
2
3
4
public static native double getDouble(Object array, int index)
public static native void setDouble(Object array, int index, double d)
public static native void setLong(Object array, int index, long l)
public static native long getLong(Object array, int index)

10.反射与枚举

枚举类型也有一个专门方法,可以获取所有的枚举常量:

1
public T[] getEnumConstants()

20.4 任务执行服务

关于任务执行服务,我们介绍了:

  • 任务执行服务的基本概念。
  • 主要实现方式:线程池。
  • 定时任务。

(1)基本概念

任务执行服务大大简化了执行异步任务所需的开发,它引入了一个“执行服务”的概念,将“任务的提交”和“任务的执行”相分离,“执行服务”封装了任务执行的细节,对于任务提交者而言,它可以关注于任务本身,如提交任务、获取结果、取消任务,而不需要关注任务执行的细节,如线程创建、任务调度、线程关闭等。

任务执行服务主要涉及以下接口:

  • Runnable和Callable:表示要执行的异步任务。
  • Executor和ExecutorService:表示执行服务。
  • Future:表示异步任务的结果。

使用者只需要通过ExecutorService提交任务,通过Future操作任务和结果即可,不需要关注线程创建和协调的细节。

(2)线程池

任务执行服务的主要实现机制是线程池,实现类是ThreadPoolExecutor。线程池主要由两个概念组成:一个是任务队列;另一个是工作者线程。任务队列是一个阻塞队列,保存待执行的任务。工作者线程主体就是一个循环,循环从队列中接收任务并执行。ThreadPool-Executor有一些重要的参数,理解这些参数对于合理使用线程池非常重要,18.2节对这些参数进行了详细介绍。

ThreadPoolExecutor实现了生产者/消费者模式,工作者线程就是消费者,任务提交者就是生产者,线程池自己维护任务队列。当我们碰到类似生产者/消费者问题时,应该优先考虑直接使用线程池,而非“重新发明轮子”,自己管理和维护消费者线程及任务队列。

(3)定时任务

异步任务中,常见的任务是定时任务。在Java中,有两种方式实现定时任务:
1)使用java.util包中的Timer和TimerTask。
2)使用Java并发包中的ScheduledExecutorService。

Timer有一些需要特别注意的事项:
1)一个Timer对象背后只有一个Timer线程,这意味着,定时任务不能耗时太长,更不能是无限循环。
2)在执行任何一个任务的run方法时,一旦run抛出异常,Timer线程就会退出,从而所有定时任务都会被取消。

ScheduledExecutorService的主要实现类是ScheduledThreadPoolExecutor,它没有Timer的问题。
1)它的背后是线程池,可以有多个线程执行任务。
2)任务执行线程会捕获任务执行过程中的所有异常,一个定时任务的异常不会影响其他定时任务。

所以,实践中建议使用ScheduledExecutorService。

针对多线程开发的两个核心问题:竞争和协作,本章总结了线程安全和协作的多种机制,针对高层服务,本章总结了并发容器和任务执行服务,它们让我们在更高的层次上访问共享的数据结构,执行任务,而避免陷入线程管理的细节。

有一些并发的内容,我们没有讨论,比如以下内容。
1)Java 7引入的Fork/Join框架,Java 8中有并行流的概念,可以让开发者非常方便地对大量数据进行并行操作,背后基于的就是Fork/Join框架,关于流我们在第26章会进一步介绍。
2)CompletionService,在异步任务程序中,一种场景是:主线程提交多个异步任务,然后希望有任务完成就处理结果,并且按任务完成顺序逐个处理,对于这种场景,Java并发包提供了一个方便的方法,那就是使用CompletionService。这是一个接口,它的实现类是ExecutorCompletionService,它通过一个额外的结果队列,方便了对于多个异步任务结果的处理,细节可参考微信公众号“老马说编程”第79篇文章。
3)Java 8引入组合式异步编程CompletableFuture,它可以方便地将多个有一定依赖关系的异步任务以流水线的方式组合在一起,自然地表达任务之间的依赖关系和执行流程,大大简化代码,提高可读性。关于CompletableFuture,我们也到第26章介绍。

从下一章开始,我们来探讨Java中的一些动态特性,比如反射、注解、动态代理等,它们到底是什么呢?

20.3 容器类

线程安全的容器有两类:一类是同步容器;另一类是并发容器。在15.2节,我们介绍了同步容器。关于并发容器,我们介绍了:

  • 写时复制的List和Set。
  • ConcurrentHashMap。
  • 基于SkipList的Map和Set。
  • 各种队列。

(1)同步容器

Collections类中有一些静态方法,可以基于普通容器返回线程安全的同步容器,比如:

1
2
3
public static <T> Collection<T> synchronizedCollection(Collection<T> c)
public static <T> List<T> synchronizedList(List<T> list)
public static <K, V> Map<K, V> synchronizedMap(Map<K, V> m)

它们是给所有容器方法都加上synchronized来实现安全的。同步容器的性能比较低,另外,还需要注意一些问题,比如复合操作和迭代,需要调用方手工使用synchronized同步,并注意不要同步错对象。

而并发容器是专为并发而设计的,线程安全、并发度更高、性能更高、迭代不会抛出ConcurrentModificationException、很多容器以原子方式支持一些复合操作。

(2)写时复制的List和Set

CopyOnWriteArrayList基于数组实现了List接口,CopyOnWriteArraySet基于CopyOn-WriteArrayList实现了Set接口,它们采用了写时复制,适用于读远多于写,集合不太大的场合。不适用于数组很大且修改频繁的场景。它们是以优化读操作为目标的,读不需要同步,性能很高,但在优化读的同时牺牲了写的性能。

(3)ConcurrentHashMap

HashMap不是线程安全的,在并发更新的情况下,HashMap的链表结构可能形成环,出现死循环,占满CPU。ConcurrentHashMap是并发版的HashMap,通过细粒度锁和其他技术实现了高并发,读操作完全并行,写操作支持一定程度的并行,以原子方式支持一些复合操作,迭代不用加锁,不会抛出ConcurrentModificationException。

(4)基于SkipList的Map和Set

ConcurrentHashMap不能排序,容器类中可以排序的Map和Set是TreeMap和TreeSet,但它们不是线程安全的。Java并发包中与TreeMap/TreeSet对应的并发版本是Concurrent-SkipListMap和ConcurrentSkipListSet。ConcurrentSkipListMap是基于SkipList实现的,Skip-List称为跳跃表或跳表,是一种数据结构,主要操作复杂度为O(log2(N))。并发版本采用跳表而不是树,是因为跳表更易于实现高效并发算法。

ConcurrentSkipListMap没有使用锁,所有操作都是无阻塞的,所有操作都可以并行,包括写。与ConcurrentHashMap类似,迭代器不会抛出ConcurrentModificationException,是弱一致的,也直接支持一些原子复合操作。

(5)各种队列

各种阻塞队列主要用于协作,非阻塞队列适用于多个线程并发使用一个队列的场合,有两个非阻塞队列:ConcurrentLinkedQueue和ConcurrentLinkedDeque。Concurrent-LinkedQueue实现了Queue接口,表示一个先进先出的队列;ConcurrentLinkedDeque实现了Deque接口,表示一个双端队列。它们都是基于链表实现的,都没有限制大小,是无界的,这两个类最基础的实现原理是循环CAS,没有使用锁。