大众号「稀有猿诉」
在Java编程言语中,注解Annotations是一种元数据,能供给代码以外的信息,是元编程的一种体现办法。注解的引进极大的增强了Java言语的元编程能力,能在编译时生成代码,大大削减了重复代码,让Java具有了适当高的动态特性,让一些高档技能如依赖注入,AOP等成为可能。今天将从根底运用,中心概念理解和创立自界说注解三个视点来彻底学会注解,并学会运用注解来进步开发效率。
根底知识
注解在代码中运用是十分常见的,信任只需有编程经验的同学都对注解十分的熟悉。
什么是注解
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。
一个典型的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)等等。并且注解的大部分运用都是在编译时生成代码,也不影响性能,所以可劲造儿,尽可能的运用注解吧。
总结
本文从注解的根底用法动身,再到中心概念的论述,最后用一个自界说注解的例子展现如何用注解来完成元编程,全方位的论述了注解。信任经过此文对注解的理解会更上一个层次。
参阅资料
- Lesson: Annotations
- Annotations in Java
- Java Annotations
- Creating a Custom Annotation in Java
- An Introduction to Annotations and Annotation Processing in Java
- 教科书级解说,秒懂最具体Java的注解
- Java 根底 – 注解机制详解
- java注解的实质以及注解的底层完成原理
欢迎搜索并关注 大众号「稀有猿诉」 获取更多的优质文章!
原创不易,「打赏」,「点赞」,「在看」,「收藏」,「分享」 总要有一个吧!