大众号「稀有猿诉」

Java编程言语中,注解Annotations是一种元数据,能供给代码以外的信息,是元编程的一种体现办法。注解的引进极大的增强了Java言语的元编程能力,能在编译时生成代码,大大削减了重复代码,让Java具有了适当高的动态特性,让一些高档技能如依赖注入,AOP等成为可能。今天将从根底运用,中心概念理解和创立自界说注解三个视点来彻底学会注解,并学会运用注解来进步开发效率

浅显易懂Java注解原理与运用

根底知识

注解在代码中运用是十分常见的,信任只需有编程经验的同学都对注解十分的熟悉。

什么是注解

Java 注解(Annotation)是JDK5.0及今后版别引进的,它能够用于创立文档,代码剖析,编译查看以及编译时生成代码。Java 注解是接口的一种特别完成,程序能够经过反射来获取指定程序元素的Annotion方针,然后运用该方针来获取注解里边的元数据。

注解的用法

注解的运用是十分简练明了的,Java 注解的基本语法是运用“@”符号来界说一个注解,然后在这个符号后面跟上注解的名字,并在这个名字的后面增加一个括号,括号中是这个注解所需求的参数列表。Java 注解是接口的一种特别完成,因而注解的界说办法类似于接口的界说办法。Java 注解能够分为三种类型:符号注解、单值注解和完好注解。符号注解没有成员变量,只需一个符号作用;单值注解有一个成员变量;完好注解有多个成员变量。

内置注解

Java内置了一些注解,信任写过代码或许看过代码的人都对此十分的了解,由于在代码中是十分十分的常见的。

  • @Override – 用于类的办法上,符号该办法要覆写基类(包括接口)的办法。编译器会对符号的办法作签名查看是否符合覆写规矩。
  • @Deprecated – 能够符号类,成员变量和成员办法为过期的办法,编译器会对调用这些类,成员或许办法给出正告(Compile warnings)。
  • @SuppressWarnings – 能够用在类和办法上,强制疏忽编译正告,即阻止编译器宣布编译正告。后面需求加括号,里边传入字符串或许字符串数组代表要疏忽的正告类型。
  • @FunctionalInterface – 这是在Java 8版别中引进的,用在接口上,符号接口是一个函数式接口(即只需一个办法的接口,能够直接用一个lambda来作为接口的实例)。
  • @SafeVarargs – 用于办法和结构办法上,断言varargs参数(即可变长参数)会被安全地运用。比方涉及泛型的可变长参数会有『unchecked』正告,加了@SafeVarargs时编译器不会再给出『unchecked』正告。

经过这些内置注解能够了解注解的类型和特色,并掌握注解的运用办法,这是学习自界说注解,即注解高档玩法的根底。

理解注解

能够发现注解并不直接对其润饰的代码产生影响,它是为代码供给额定的信息,它是代码的元数据,注解与代码一同构成了编译器的完好输入,编译器凭借注解能够生成并得到最终完好的代码。

注解自身无论是运用仍是界说都相对直观和简练,十分简略理解,由于注解自身便是一种元数据,供给一种符号或许额定的数据。要点在于注解的处理,这是注解功能发挥作用的当地也便是注解功能逻辑完成的当地。

元编程

注解是程序的元数据,所以这属于元编程范畴。元编程Metaprogramming也即是以代码为操作方针和方针输出的编程范式,元编程是生产力工具,能够削减重复代码,大大的进步代码开发效率。大多数通用编程言语都支持元编程,像C/C++言语中的宏,Java中的注解,反射和动态代理,大Python中的装修器(Decorators装修器是高阶函数对函数进行操作)和元类(Metaclasses,对类进行操作可理解为类的模板)等等都是元编程。

优秀的结构(Spring)和范畴驱动开发(DDD)都是元编程的典型运用。

关于Java的元编程,引荐这两篇文章:

注解的分类

注解是向编译器供给额定信息的一种元编程机制,那么根据机制的简略到杂乱,能够把注解分为5个类型:

符号注解(Marker Annotations)

最简略的注解,对于某个声明进行符号,编译器会对被符号的声明进行查看和处理。如@Override和@Deprecated。

单值注解(Single Value Annotations)

需求给注解传递一个参数且只需一个参数,如@SuppressWarnings(“unchecked”)。

全值注解(Full Annotations)

需求给注解传递许多参数(多个键值对),如:

@Test(owner="Paul", values="Class Greeks")
public void testSomeMethod() {
    // ...
}

类型注解(Type Annotations)

能够用在类型被声明的当地,比方办法返回值,办法的参数声明等,如:

public @NonNull String transform(@Nullable String source) { ... }

重复注解(Repeating Annotations)

惯例的注解在同一个当地只能呈现一次,但重复注解能够在同一个当地呈现多次,如:

@Words(word="Hello", value=1)
@Words(word="World", value=2)
public void method() {
    // ...
}

自界说注解

注解的运用是十分的直观和简练的,无论是内置注解仍是各种结构界说好了的注解,运用起来那是适当的香。但这远远不够,由于注解最大的威力在于元编程,比方代码操作和代码生成,这是削减重复劳动(重复代码)和供给开发效率的大杀器。所以咱们有必要学会高档玩法,即自界说注解。

元注解

元注解,也即界说注解时所需求的注解。这有点类似于编译器自举,言语自身界说了一个最根底的注解,在其根底之上能够扩展出更多的注解,而注解的处理是经过反射,只需知道一些特别的符号就能够了,其他的都是逻辑。

@Inherited

默许情况下,在基类中运用的注解是不会被子类承继的,假如注解自身符号有@Inherited,那么注解就会呈现在被运用的承继体系中:

@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Company {
    String name() default "ABC";
    String city() default "xyz";
}
@Company
public class Employee { .. }
public class Manager extends Employee { ... }

这个中,假如把@Inherited从注解Company中去掉,那么给类Employee加的注解在其子类Manager中就不会得到承继。大部分时分界说注解时都要加上@Inherited符号。

@Documented

运用了@Documented符号的注解能够呈现在文档中(JavaDocs)。

@Repeatable

对应着可重复的注解,指定着能够在哪些标识符上面重复注解。

@Target

指定注解能够作用于何种标识符,假如不指定则能够运用于任何标识符即任何程序元素。可选的选项有:

  • ElementType.ANNOTATION_TYPE – 能够用于其他的注解上面
  • ElementType.CONSTRUCTOR – 能够用于结构办法上面
  • ElementType.FIELD – 能够用于成员变量上面
  • ElementType.LOCAL_VARIABLE – 能够用于办法的本地变量(栈内变量)
  • ElementType.METHOD – 能够用于办法上面
  • ElementType.PACKAGE – 能够用于包(package)上面。
  • ElementType.PARAMETER – 能够用于办法的参数上面。
  • ElementType.TYPE – 能够用于类型声明的当地(即类,接口和枚举的声明)。

能够指定一个@Target(ElementType.METHOD)或许多个方针@Target({ElementType.FIELD, ElementType.LOCAL_VARIABLE})。

@Retention

元注解@Retention用于指定注解保留的生命周期。注解是一种元数据,方针是代码,而代码是有生命周期的:修改或许说源码时;编译时;运行时。这是程序代码的典型生命周期。而@Retention的作用便是指明注解保留到哪个生命周期。

  • RetentionPolicy.SOURCE – 在源码时保留,编译时就被丢弃,也便是说在编译时并不运用。一般用于编译前的源码处理工具运用,如javadoc,以及代码生成。
  • RetentionPolicy.CLASS – 编译后仍会保留在class文件中,但在运行时(便是JVM加载class时)被丢弃。主要是在编译时运用(比方生成代码)。
  • RetentionPolicy.RUNTIME – 保留到运行时,在运行时能够被运用。

自界说注解

注解能够视为一个特别的接口,注解的界说便是界说一个接口,而每个接口便是其完成。注解的处理器利用反射获取注解接口的类型信息,再结合注解供给的数据就生成接口的完成代码。这便是注解的作业机制。

用@interface就能够声明一个自界说注解,通用的格式是:

[Access Modifier] @interface <Annotation name> {
    <Type> <Method name>() [default value];
}

能够看到注解实质上是一种接口,但它有一些具体的约束规矩:

  • 注解的办法不能有参数和反常签名(throws)
  • 办法的返回值不受约束,能够是任意类型
  • 办法的默许返回值是可选的(即能够有,也能够没有)
  • 界说注解时能够运用元注解,这种套娃机制能够完成更为杂乱和更为强壮的注解

看一个完好自界说注解的

@Documented
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodInfo {
    String author() default "Kevin";
    String date();
    int revision() dfeault 1;
    String comments();
}

运行时注解解析

界说了注解后,就能够在代码中运用了,但这还没完,还需求对注解进行解析和处理。在运行时需求用到反射来解析注解,反射API中有专门用于处理注解的API:

  • AnnotatedElement – 这是反射接口处理注解的中心类型,它是反射类型Method,Field和Constructor的基类,经过它的办法来获取注解Annotation实例。
  • Annotation来处理具体的注解

注意注意,注解的解析和处理用的是反射,所以注解界说时要用RententionPolicy.RUNTIME,否则用反射是拿不到注解信息的,由于反射是在运行时(Runtime)。下面咱们会用一个完好的实例来学习如何处理自界说注解。

完好示

至此注解的概念的原理都清楚了,融会贯通一下,用一个完好的来展现自界说注解。

Step 1:界说注解

直接复用前面界说的@MethodInfo。

Step 2:运用注解

public class MethodInfoExample {
    @Override
    @MethodInfo(author = "Alex", comments = "Main method", date = "Mar 29 2024", revision = 2)
    public String toString() {
        return "toString method Overridden";
    }
    @Deprecated
    @MethodInfo(comments = "Deprecated method", date = "Mar 30, 2024")
    public static void oldMethod() {
        System.out.println("Old method out!");
    }
    @SuppressWarnings({"unchecked", "deprecation"})
    @MethodInfo(author = "Paul", comments = "Main method", date = "Mar 31 2024")
    public void genericsMethod() throws FileNotFoundException {
        List list = new ArrayList();
        list.add("Xyz");
        oldMethod();
    }
}

Step 3:解析注解

public class MethodInfoParsing {
    public static void main(String[] args) {
        try {
            Method[] methods = MethodInfoParsing.class
                    .getClassLoader().loadClass("MethodInfoExample").getDeclaredMethods();
            for (Method method : methods) {
                if (!method.isAnnotationPresent(MethodInfo.class)) {
                    continue;
                }
                for (Annotation annotation : method.getDeclaredAnnotations()) {
                    System.out.println("Annotation " + annotation + " on method " + method.getName());
                }
                MethodInfo info = method.getAnnotation(MethodInfo.class);
                if ("Paul".equals(info.author())) {
                    System.out.println("From Pauls: " + method.getName());
                }
            }
        } catch (ClassNotFoundException e) {
        }
    }
}

注解处理器

在运行时解析注解比较简略,较费事的是在编译时(Compile time)处理注解,这时的处理又特别的要害,由于像代码生成是在这一阶段做的。编译时处理注解需求用到Annotation Processor

浅显易懂Java注解原理与运用

一个典型的Annotation processor完成进程:

  • 完成一个Processor,一般经过承继AbstractProcess
  • 覆写办法process来处理注解,这里边过滤出想要处理的注解,然后用JavaWriter来生成Java文件(或许粗暴的用PrintWriter也能够)。
  • 注册完成好的Processor给编译器:能够经过编译指令javac -processor来指定处理器;也能够把处理器打成jar包然后当成库增加到项目中,由于编译器在开端编译前会自动的去搜索注解和注解处理器。

能够参阅如下文章来具体了解Annotation processor的完成进程:

这里是一系列优秀的Annotation processor事例

为什么用注解

注解是十分高雅的元编程办法,能够生成代码(削减重复),降低耦合。比方著名的单元测验结构JUnit,在其4.0时(即JUnit4)就用注解代替了承继。在JUnit3要这样写测验:

// Using JUnit 3.0
public class MyClassTest extends TestCase {
    private MyClass instance;
    @Override
    protected void setup() throws Exception {
        super.setup();
        instance = new MyClass();
    }
    @Override
    protected void tearDown() throws Exception {
        super.tearDown();
    }
    public void testSomeMethod() {
        assertNotNull(instance);
        assertEquals("Hello, world", instance.say());
    }
}

这是类MyClass的一个简略的测验用例。在JUnit4运用了注解后,就能够这样写了:

// Using JUnit 4.0
public class MyClassTest {
    private MyClass instance;
    @Before
    private void setup() {
        instance = new MyClass();
    }
    @After
    private void tearDown() {}
    @Test
    public void testSomeMethod() {
        assertNotNull(instance);
        assertEquals("Hello, world", instance.say());
    }
}

经过注解@Before符号测验前预备和@After测验后清理,用@Test符号测验用例,也不必承继TestCase了,整体测验代码十分的高雅。这便是注解的作用。

什么时分用注解

注解的实质是程序的元数据,为编译器供给的代码以外的额定的数据。注解是高雅的元编程的一种办法,能够削减重复的代码,进步开发效率。所以每逢需求削减重复代码,生成代码,供给元数据时就要用注解来完成。特别是特定范畴的问题,十分适合很多运用注解,如数据库(Room),网络请求(Retrofit),单元测验(JUnit)等等。并且注解的大部分运用都是在编译时生成代码,也不影响性能,所以可劲造儿,尽可能的运用注解吧。

总结

本文从注解的根底用法动身,再到中心概念的论述,最后用一个自界说注解的例子展现如何用注解来完成元编程,全方位的论述了注解。信任经过此文对注解的理解会更上一个层次。

参阅资料

欢迎搜索并关注 大众号「稀有猿诉」 获取更多的优质文章!

原创不易,「打赏」「点赞」「在看」「收藏」「分享」 总要有一个吧!