注解

前言

注解其实是Java中非常重要的一个部分,而且也是C++中完全空白的一块知识。很多人会认为注解就是什么@OverrideFunctionalInterface这种写不写都无所谓的提示性的东西。其实这种想法是完全错误的。Java中很多的框架都是需要借助注解来实现的。不过现阶段我们就简单的了解一下注解的使用就完事了。

注解的声明

注解其实也是一个像类、接口一样的东西。之前我们学习反射的时候,也学到了可以通过反射来获取注解,甚至注解也可以对应一个Class对象。比如说Override.class我们也是使用过的。那么我们就从Override这个最常见的注解开始看起。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

这就是Override注解的声明。上面也有两个注解,但是声明里面什么都没有写。不过我们可以看出注解的声明和接口的声明是非常的类似的。我们使用@interface的方式来声明注解。注解中什么事情都没有干,其实注解中也干不了什么事情。我们需要使用反射的方式给注解赋予一定的功能。

有的注解中可以使用参数。比如我们经常使用的注解@SuppressWarnnings("all"),这个注解中我们给定了一个参数all,那么这个注解的声明又是如何呢?

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

其中上面也有几个注解。不过声明的里面就是String[] value()。这个看起来有点儿像是一个函数。其实不是函数,这就是注解中的参数,形式是类型 名字()。如果只有一个参数的话,建议使用value。其中上面的参数的类型是String[]代表可以接受多个值,比如说我们就可以使用@SuppressWarnnings(value = {"unchecked", "rawtypes"})这种写法,不过由于只有一个属性而且名字还是value的话,我们就可以省略前面的value =可以直接写@SuppressWarnnings({"unchecked", "rawtypes"}),这就是为什么上面我们建议如果只有一个元素建议使用value作为名字。

元注解

上面我们看到的注解的上面都有一些注解。这些注解是什么玩意呢?如果你看这些注解的代码的话,你还会发现这种注解的上面还会有自己。其实这些注解就是元注解,是用来描述注解用的。

元注解@Target表示的是下面的注解可以在哪儿出现。他的值是一个ElementType对象数组。其中共有一下的几个属性。

元素类型 应用于
ANNOTATION_TYPE 注解类型声明
PACKEG
TYPE 类和接口(包括Enum和注解)
METHOD 方法
CONSTRUCTOR 构造器
FIELD 字段
PARAMETER 方法或者构造器的参数
LOCAL_VARIABLE 局部变量
TYPE_PARAMETER 类型参数
TYPE_USE 类型用途

其中@Retention是用于指定可以从哪里访问注解。有以下的三种选择。

类型 说明
RetentionPolicy.SOURCE 源码
RetentionPolicy.CLASS 类文件
RetentionPolicy.RUNTIME 运行期

其中第一个源码级别的就是,只有源代码处理器是可见的。比如上面我们提高的两个注解都是源码级别的注解。当.java文件编译成为.class文件之后,我们是无法访问到这个注解的。

其中类文件级别是默认的级别。如果我们不指定就是这个级别。此时.class文件中我们是可以访问到这个注解的。不过当.class文件在JVM中运行的时候,注解将会消失。

其中运行级别就是无论什么时候,注解都不会消失,此时我们可以通过反射来获取到注解并且对注解进行一些处理。这也就是意味着我们使用反射是无法访问到声明为RetentionPolicy.SOURCE或者RetenttionPolicy.CLASS的注解的,比如说上面的那个两个注解。一般我们声明注解的时候都需要声明为RUNTIME级别,因为我们需要使用反射去处理注解。

还有元注解@Documented是给javadoc这些文档化工具的提示的。@Inherited注解只用于类,表示这个注解是可以继承的,这个类的所有的子类都会拥有一个注解。@Repeatable表示这个注解可以在同一个地方被多次使用。不过我们需要做额外的工作。比如定义一个可以重复的@TestCase

@Repeatable(TestCases.class)
@interface TestCase {
    String params();
    String expected();
}

@interface TestCases {
    TestCase[] value();
}

此时当我们提供多个@TestCase的时候,会自动的装入@TestCases的容器中,这使得处理注解变得更为的复杂。

自定义注解

现在我们就开始自定义一个注解用来练习,我们经常会使用toString()方法来输出类的信息。不过我们可能不想包含所有的实例变量或者是我们想跳过这个变量的名称。比如对Point类来说,我们更喜欢[5, 10]而不是Point[x = 5, y = 10]。此时我们可以自定义一个注解来实现这个功能。而且这个注解是对所有的类都是有效的。

@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@interface ToString {
    boolean includeName() default true;
}

我们同样定义了两个类用于测试。

@ToString(includeName = false)
class Point {
    @ToString(includeName = false)
    private int x;
    @ToString(includeName = false)
    private int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public void setX(int x) {
        this.x = x;
    }

    public int getY() {
        return y;
    }

    public void setY(int y) {
        this.y = y;
    }
}

@ToString
class Rectangel {
    @ToString(includeName = false)
    private Point topLeft;
    @ToString
    private int width;
    @ToString
    private int heighth;

    public Rectangel(Point topLeft, int width, int heighth) {
        this.topLeft = topLeft;
        this.width = width;
        this.heighth = heighth;
    }

    public Point getTopLeft() {
        return topLeft;
    }

    public void setTopLeft(Point topLeft) {
        this.topLeft = topLeft;
    }

    public int getWidth() {
        return width;
    }

    public void setWidth(int width) {
        this.width = width;
    }

    public int getHeighth() {
        return heighth;
    }

    public void setHeighth(int heighth) {
        this.heighth = heighth;
    }
}

此时我们希望一个矩形输出可以是Rectangle[[5, 10], width = 20, height = 30]这样的。

不过在运行的时候我们是不可以修改一个类的toString方法的,所以我们只好使用反射的方法写一个格式化任何对象的方法。

运行时的注解处理

之前我们说反射的时候,也已经说过了关于注解的API了。如果一个注解是不可重复的,我们一般就可以使用getAnnotation来定位他。如果是可重复的,那就比较复杂了,我们不可以使用getAnotation的方式,因为会返回null,因为这个可重复注解被装进容器注解中去了。此时我们需要使用getAnnotationByType方法,他会浏览容器,并返回可重复注解的数组。

下面我们就要开始使用反射来处理上面的@ToString注解了。

public class ToStrings {
    public static String toString(Object object) {
        if (object == null) {
            return "null";
        }

        Class<?> clazz = object.getClass();
        ToString tostr = clazz.getAnnotation(ToString.class);
        if (tostr == null) {
            return object.toString();
        }

        StringBuilder result = new StringBuilder();
        if (tostr.includeName()) {
            result.append(clazz.getSimpleName());
        }
        result.append("[");
        boolean first = true;

        for (Field f : clazz.getDeclaredFields()) {
            tostr = f.getAnnotation(ToString.class);
            if (tostr != null) {
                if (first) {
                    first = false;
                } else {
                    result.append(", ");
                }

                f.setAccessible(true);
                if (tostr.includeName()) {
                    result.append(f.getName());
                    result.append(" = ");
                }

                try {
                    result.append(ToStrings.toString(f.get(object)));
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }

        }

        result.append("]");
        return result.toString();
    }
}

然后我们可以在原来的类中测试。

public static void main(String[] args) {
    Rectangel rectangel = new Rectangel(new Point(5, 10), 20, 30);
    System.out.println(ToStrings.toString(rectangel));
    // Rectangel[[5, 10], width = 20, heighth = 30]
}

输出完全正确。不过需要注意的是,这个注解不仅仅是为上面的这两个类写的,如果我们新建一个其他的类,只要在对应的地方加上@ToString注解都是可以用的。比如说我们新建一个Student对象。

@ToString
public class Student {
    @ToString
    private String name;
    @ToString
    private int age;
    @ToString
    private double score;
    private long qq;

    public Student(String name, int age, double score, long qq) {
        this.name = name;
        this.age = age;
        this.score = score;
        this.qq = qq;
    }
}

然后写一个测试。

@Test
public void test() {
    Student sher = new Student("sher", 18, 99.8, 10086);
    System.out.println(ToStrings.toString(sher));
    // Student[name = sher, age = 18, score = 99.8]
}

此时我们没有修改任何@ToStringToStrings.toString()的任何代码,也完成了这个功能,可见注解的强大之处。上面我们是使用反射的方式在运行期间处理注解,其实我们还是可以在源码级别中处理注解,然后一起编译的。不过那种东西实在是有点儿复杂,不适合此时了解。

总结

上面算是对注解的一个简单的了解,以及如何创建一个自定义的注解,然后使用反射的方式实现这个注解的功能。


一枚小菜鸡