Java8 教程

Java8 注解增强

在 Java 中,注解(Annotation)是一种元数据形式,它为程序元素(如类、方法、变量等)添加额外的信息,它可以被编译器或运行时环境读取和处理,以实现各种功能。

Java8 对注解处理提供了两点改进:

(1)可重复的注解

(2)可用于类型的注解

可重复的注解

在 Java8 之前,注解只能用来标注方法和字段。例如:

// 类初始化时调用
@PostConstruct 
public void initData() {
    //...
}

// 在构造函数之后调用
@Resource("jdbc:derby:sample")
private Connection conn;

并且,在同一个元素上同一个注解不能重复使用,例如:

// 在构造函数之后调用
@Resource("jdbc:derby:sample")
@Resource("jdbc:derby:sample2") // 非法的
private Connection conn;

但是,我们可以在同一元素上应用不同的注解是可以的,例如:

@JsonIgnore
@Resource("jdbc:derby:sample") // ok
private Connection conn;

很快,涌现了越来越多使用注解的地方,从而导致了一些不得不需要重复使用相同注解的情况。例如,要表示数据库中的一个复合主键,你需要指定多列:

@Entity
@PrimaryKeyJoinColumn(name="ID"),
@PrimaryKeyJoinColumn(name="REGION")
public class Item {
    //...
}

由于这是不可能做到的,所以这些注解只能被包装到一个父容器注解中,例如:

@Entity
// 父容器注解
@PrimaryKeyJoinColumns({
    // 子注解
    @PrimaryKeyJoinColumn(name="ID"),
    @PrimaryKeyJoinColumn(name="REGION")
})
public class Item {
    //...
}

幸运的是,在 Java8 之后再也不用编写这样丑陋的代码了,可以这样使用:

@Entity
@PrimaryKeyJoinColumn(name="ID"),
@PrimaryKeyJoinColumn(name="REGION")
public class Item {
    //...
}

如果你仅仅想使用重复注解,并且你的框架已经支持重复注解,则知道上述注解知识已经可以了。

但是,对于一个框架开发人员,重复注解的知识点要稍微复杂一点。毕竟,AnnotatedElement 接口有一个方法

会获取类型为 T 的注解(如果有的话)。那么对于拥有同一类型的多个注解来说,该方法应该如何处理呢? 只返回第一个注解? 那样可能会给遗留代码带来意想不到的行为。要解决这个问题,可重复注解必须做到如下两点:

(1)将注解标注为

(2)提供一个容器注解。

例如:

package com.hxstrive.jdk8.annotation;

import java.lang.annotation.Annotation;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Method;

/**
 * JDK8 注解增强
 * @author hxstrive.com
 */
public class AnnotationDemo2 {
    // 容器注解
    @Retention(RetentionPolicy.RUNTIME)
    static @interface MyTests {
        // 用来放在容器注解的子注解
        AnnotationDemo2.MyTest[] value();
    }

    // 自定义注解
    @Repeatable(MyTests.class) // 指定该注解的容器注解
    @Retention(RetentionPolicy.RUNTIME)
    static @interface MyTest {
        String value() default "";
    }

    @MyTest("value1")
    @MyTest("value2")
    public void test() {
        System.out.println("test");
    }

    public static void main(String[] args) throws Exception {
        Class<AnnotationDemo2> clazz = AnnotationDemo2.class;
        Method method = clazz.getMethod("test");

        Annotation[] annotations = method.getAnnotations();
        for(Annotation annotation : annotations) {
            System.out.println(annotation);
            //@com.hxstrive.jdk8.annotation.AnnotationDemo2$MyTests(value=[
            // @com.hxstrive.jdk8.annotation.AnnotationDemo2$MyTest(value=value1),
            // @com.hxstrive.jdk8.annotation.AnnotationDemo2$MyTest(value=value2)
            //])
        }

        MyTest myTest = method.getAnnotation(MyTest.class);
        System.out.println(myTest); // null

        MyTest[] myTests = method.getAnnotationsByType(MyTest.class);
        for(MyTest my : myTests) {
            System.out.println(my);
        }
        //@com.hxstrive.jdk8.annotation.AnnotationDemo2$MyTest(value=value1)
        //@com.hxstrive.jdk8.annotation.AnnotationDemo2$MyTest(value=value2)
    }

}

上面示例中,当用户同时提供两个或更多注解时,它们会被自动包装为一个注解。

当在 test() 方法的反射 Method 对象上调用 method.getAnnotation(MyTest.class) 时,会返回 null。这是因为该元素实际上被标注为容器注解 MyTests。

当你实现一个处理可重复注解的方法时,会发现使用 getAnnotationsByType 方法更方便,method.getAnnotationsByType(MyTest.class) 会返回一个包含 MyTest 注解的数组。

可用于类型的注解

在 Java8 之前,注解只能被标注在一个声明上。声明是用来定义某个新名称的代码段。以下是一些声明的例子:

@Entity 
public class Person {
    //...
}

@SuppressWarnings("unchecked") 
List<Person> people = query.getResultList();

在 Java8 中,你可以在任何类型上标注注解。这对于结合使用检查常见错误的工具非常有用。一个常见的错误是,由于开发人员没有考虑到一个引用可能是 null, 从而抛出了一个 NullPointerException 异常。现在假设你在永远不希望为 null 的变量上标注了 @NonNull 注解,那么工具就可以检查出下面代码是正确的:

private @NonNull List<String> names = new ArrayList<>();
//...
names.add("Fred"); // 不可能出现 NullPointerException 异常

当然,工具还应该检测出任何可能会导致 names 变为 null 的语句:

names = null; // 空指针检查程序会将该语句标记为一个错误
names = readNames(); // 如果 readNames 返回一个 @NonNull 字符串,则没问题

到处编写这样的注解似乎很枯燥乏味,但是在实际中,这些工作可以被一些简单的假设来代替,如:Checker 框架

在上面的例子中,names 变量被声明为 @NonNull。这个注解可能在 Java8 之前就存在了,但是如何表示列表中的元素应该是非 null 的呢? 从逻辑上讲,应该这样表示:

private List<@NonNull String> names;

现在,这类注解可以在 Java8 中合法使用了。

方法参数反射

在 JDK8 中,可以通过反射得到参数的名称了,它可以减少注解中的重复代码。以一个普通的 JAX-RS 方法为例:

public Person getEmployee(@PathParam("dept") Long dept, @QueryParam("id") Long id)

在大多数情况下,方法参数的名称都与注解参数相同,或者我们可以将它们刻意统一起来。如果注解处理方法可以读取方法参数的名称,那么我们只需要编写如下代码:

public Person getEmployee(@PathParam Long dept, @QueryParam Long id)

Java8 新提供的类 java.lang.reflect.Parameter 已经使其成为现实。不幸的是,为了获取类文件中的必需信息,你需要使用 javac -parameters SourceFile.java 的方式来编译源代码,例如:

package com.hxstrive.jdk8.reflect;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;

public class ReflectDemo1 {

    public void show(String msg) {
        System.out.println(msg);
    }

    public static void main(String[] args) throws Exception {
        Class<ReflectDemo1> clazz = ReflectDemo1.class;
        Method show = clazz.getMethod("show", String.class);
        Parameter[] parameters = show.getParameters();
        for(Parameter parameter : parameters) {
            System.out.println("method: " + show.getName() + ", args: " + parameter.getName());
        }
    }

}

使用 javac -parameters -d ./target/classes ./src/main/java/com/hxstrive/jdk8/reflect/ReflectDemo1.java 命令编译上面代码。

然后使用 java com.hxstrive.jdk8.reflect.ReflectDemo1 命令运行示例,如下:

D:\demo_jdk8> javac -parameters -d ./target/classes ./src/main/java/com/hxstrive/jdk8/reflect/ReflectDemo1.java
D:\demo_jdk8> cd .\target\classes\
D:\demo_jdk8\target\classes> java com.hxstrive.jdk8.reflect.ReflectDemo1                 
method: show, args: msg

从输出可以得知,成功获取了 show 方法的 msg 参数命。

说说我的看法
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
公众号