深入学习Java中的注解(Annotation)

Java中的注解(Annotation)

    • 注解和注释有啥区别呢?
    • 初学注解
      • 1.注解的写法
      • 2.注解放置在哪里
      • 3.注解的作用
      • 4.Java中有一些人家写好的注解供我们使用
      • 5.注解中可以携带信息,也可以不携带
    • 自定义一个注解类型
      • 注解元素的默认值
      • 元注解
        • @Target
        • @Retention
        • @Inherited
        • @Document
      • 自己使用自己描述的注解
    • 如何解析注解内携带的信息(反射机制)
      • 做个小案例,感受一下控制反转和依赖注入的基本原理

单词Annotation可翻译为:注释、注解。

单词Comment可翻译为:评论、议论、解释。

在Java当中,Comment充当注释的含义,Annotation充当注解的含义。

注解和注释有啥区别呢?

Java中有单行注释、多行注释、文档注释。

注释Comment
单行注释 //
多行注释 /* /
文档注释 /
* */

这些注释都是在编译以后不会出现在字节码中的,仅仅是存在于java源文件中给程序员看的,它们对于程序的执行没有任何影响。而注解就不同了,它是代码级别的,是会编译到class字节码当中,对程序的运行产生影响或者对编译产生影响。注解严格意义上来说就是Java中的类成员,它和属性、方法、构造方法是一样的级别。

初学注解

注解在Java中确实也很常见,但是人们常常不会自己定义一个注解拿来用。我们虽然很少去自定义注解,但是学会注解的写法,注解的定义,学会利用反射解析注解中的信息,在开发中能够使用到,这是很关键的。

代码运行离不开一些配置信息的支撑,有些配置信息我们选择保存在文件中(.xml .properties),再利用IO流的技术读取这些配置信息去使用,它们都是配置和代码分离的形式,这种方式的好处是低耦合,代码已经打包压缩好了不用动,而配置信息修改起来很方便,通过修改配置,可以让代码完成不同的任务,这就很方便。不好的地方在于:开发人员写的代码和配置不在一个文件中,开发过程中翻看就不是很方便。相当于开发麻烦了,维护却简单了。

随着开发的越来越多,人们发现文件中的一些配置,是写到那里很少去更改的,没有人会轻易修改那些重要的配置信息,这些信息就像被写死了一样,所以这些信息就可以用注解写,写到代码里去,因为注解和代码是内聚在一起的,开发过程就很方便。总结来说是各有优劣吧。

1.注解的写法

@XXX [(一些信息)]

这是使用注解的写法,使用注解前必须先定义注解,我们可以使用像@Override这样的注解,那是因为这些注解在Java中已经写好了,我们直接拿来用就好(下面会讲到如何自定义注解)。

注解中的[一些信息]可能存在,也可能不存在,这需要看注解的定义者是如何定义该注解的。

2.注解放置在哪里

注解可以放置在:类的上面、属性上面、方法上面、构造方法上面、局部变量上面、参数前面。

注解能够放置在哪里,这也需要看注解的定义者是如何定义该注解的。

3.注解的作用

  1. 用来充当注释的作用(仅仅是一个文字的说明) 例如:@Deprecated
  2. 用来做代码的检测(验证) 例如:@Override
  3. 可以携带一些信息(内容) 就类似于:文件.properties .xml 的作用

4.Java中有一些人家写好的注解供我们使用

  • @Deprecated 用来说明方法是废弃的
  • @Override 用来做代码检测,检测此方法是否是一个重写方法,如果不是,该注解会报错
  • @SuppressWarnings({"","",""}) 它里面的参数是一个String类型的数组,如果数组内的元素只有一个长度 ,则大括号{}可以省略,该注解中可以定义的有意义参数值包括:
    • unused:变量定义后未被使用
    • serial:类实现了序列化接口 不添加序列化ID号
    • rawtypes:集合没有定义泛型
    • deprecation:方法已经废弃
    • *unchecked:出现了泛型的问题 可以不检测
    • all:包含了以上所有(不推荐)

5.注解中可以携带信息,也可以不携带

注意:注解信息不能随意写,注解信息的类型只能是如下的类型:

  1. 基本数据类型
  2. String类型
  3. 枚举类型enum
  4. 注解类型@
  5. 数组类型[],数组的内部只能存储如上的四种类型

自定义一个注解类型

通过@interface 定义一个新的注解类型

public @interface MyAnnatation {

}

可以发现注解的写法与接口非常相似(可以利用接口的特点来记忆注解)

  • 可以描述public static final的属性 比较少见
  • 可以描述public abstract的方法 方法要求必须有返回值 返回值类型必须是如上那些(基本数据类型、String类型、枚举类型、注解类型、数组类型)
public @interface MyAnnatation {
	int NUM = 9;//注解中写属性,很少见
	String test();//方法要求必须有返回值
}

注解元素的默认值

注解元素必须有确定的值,要么在定义注解的默认值中指定,要么在使用注解时指定,非基本类型的注解元素的值不可为null。因此, 使用空字符串或0作为默认值是一种常用的做法。这个约束使得处理器很难表现一个元素的存在或缺失的状态,因为每个注解的声明中,所有元素都存在,并且都具有相应的值,为了绕开这个约束,我们只能定义一些特殊的值,例如空字符串或者负数,一次表示某个元素不存在,在定义注解时,这已经成为一个习惯用法。

元注解

我们自己定义的注解如果想要拿来使用,光定义还不够 ,还需要做很多细致的说明(需要利用Java提供好的注解来说明)

这就需要使用到元注解(也是注解 不是拿来使用的 是用来说明注解的):

@Target

@Target说明了Annotation所修饰的对象范围:Annotation可被用于 packages、types(类、接口、枚举、Annotation类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数)。在Annotation类型的声明中使用了target可更加明晰其修饰的目标。

作用:用于描述注解的使用范围(即:被描述的注解可以用在什么地方)

取值(ElementType)有:

  1. CONSTRUCTOR:用于描述构造器
  2. FIELD:用于描述域
  3. LOCAL_VARIABLE:用于描述局部变量
  4. METHOD:用于描述方法
  5. PACKAGE:用于描述包
  6. PARAMETER:用于描述参数
  7. TYPE:用于描述类、接口(包括注解类型) 或enum声明

更详细的@Target定义可参照下表:

Target类型 描述
ElementType.TYPE 应用于类、接口(包括注解类型)、枚举
ElementType.FIELD 应用于属性(包括枚举中的常量)
ElementType.METHOD 应用于方法
ElementType.PARAMETER 应用于方法的形参
ElementType.CONSTRUCTOR 应用于构造函数
ElementType.LOCAL_VARIABLE 应用于局部变量
ElementType.ANNOTATION_TYPE 应用于注解类型
ElementType.PACKAGE 应用于包
ElementType.TYPE_PARAMETER 1.8版本新增,应用于类型变量)
ElementType.TYPE_USE 1.8版本新增,应用于任何使用类型的语句中(例如声明语句、泛型和强制转换语句中的类型)

@Retention

描述当前的这个注解存在什么作用域中
注解存在的三种作用域:SOURCECLASSRUNTIME
对应于:源代码文件—>编译—>字节码文件—>加载—>内存执行

@Retention定义了该Annotation被保留的时间长短:某些Annotation仅出现在源代码中,而被编译器丢弃;而另一些却被编译在class文件中;编译在class文件中的Annotation可能会被虚拟机忽略,而另一些在class被装载时将被读取(请注意并不影响class的执行,因为Annotation与class在使用上是被分离的)。使用这个meta-Annotation可以对 Annotation的“生命周期”限制。

作用:表示需要在什么级别保存该注释信息,用于描述注解的生命周期(即:被描述的注解在什么范围内有效)

取值(RetentionPoicy)有:

  1. SOURCE:在源文件中有效(即源文件保留)
  2. CLASS:在class文件中有效(即class保留)
  3. RUNTIME:在运行时有效(即运行时保留)
生命周期类型 描述
RetentionPolicy.SOURCE 编译时被丢弃,不包含在类文件中
RetentionPolicy.CLASS JVM加载时被丢弃,包含在类文件中,默认值
RetentionPolicy.RUNTIME 由JVM 加载,包含在类文件中,在运行时可以被获取到

Retention meta-annotation类型有唯一的value作为成员,它的取值来自java.lang.annotation.RetentionPolicy的枚举类型值。

@Inherited

描述当前这个注解是否能被子类对象继承(不太常用)

@Inherited 元注解是一个标记注解,@Inherited阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。

注意:@Inherited annotation类型是被标注过的class的子类所继承。类并不从它所实现的接口继承annotation,方法并不从它所重载的方法继承annotation。

当@Inherited annotation类型标注的annotation的Retention是RetentionPolicy.RUNTIME,则反射API增强了这种继承性。如果我们使用java.lang.reflect去查询一个@Inherited annotation类型的annotation时,反射代码检查将展开工作:检查class和其父类,直到发现指定的annotation类型被发现,或者到达类继承结构的顶层。

@Document

描述这个注解是否能被Javadoc 或类似的工具文档化(不常用)

自己使用自己描述的注解

自定义一个注解:

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;

@Target({METHOD,CONSTRUCTOR,FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
    String[] value();//方法不是做事情 为了携带信息 搬运给该注解的解析者使用
    //按道理讲 注解定义者肯定和注解解析者是同一个人,而注解的使用者,它们无需定义和解析注解
    /*方法名刚好是value,并且只有一个方法,使用该注解的时候就可以不指定方法名*/
}

使用自己的注解:

由注解的定义可知,该注解可以放置在方法、构造方法、属性上。

作用范围是运行时RUNTIME

public class Person {
    @MyAnnotation("TOM")
    private String name;
}

问题1. 在注解里面描述了一个方法,方法没有参数,方法有返回值String[]
使用注解的时候,让我们传递参数,如何理解该过程?

可以理解为:注解的方法做事,把我们传递给它的参数搬运走了,给了别人,别人解析这些注解中的参数,做相应的处理

问题2. 使用别人写好的注解不用写方法名,我们自己定义的方法必须写名字*

如果我们自己定义的注解 只有一个方法 方法名字叫value
在使用的时候就可以省略方法名
如果传递的信息是一个数组 数组内只有一个元素 可以省略数组大括号{}
如果方法是两个以上 每一个方法都要使用 并且每一个方法必须写名字

举例:

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;

@Target({METHOD,FIELD})
@Retention(RetentionPolicy.SOURCE)
public @interface YouAnnotation {
    String [] value();
    int count();
    double price();
}
public class Student {
    //注解中有3个方法,则3个方法都要使用到,并且要指明方法名
    //如果数组中只有一个数据,则{}可以省略
    @YouAnnotation(value = "qa",count = 8,price = 9.9)
    private String name;
}

如何解析注解内携带的信息(反射机制)

解析注解其实也很简单,它的思路是:先看该注解声明在了哪里,比如,一个属性name上面声明了一个注解,那么就利用反射,先找到这个类,然后找到这个属性name,根据这个属性,调用getAnnotation()方法,得到这个注解,然后调用这个注解对象的getClass方法获取注解的类型,用注解的类型,调用getMethod(methodName)方法,根据方法名获取注解中的方法,然后,这个方法对象调用 invoke方法去执行方法,方法的参数是那个annotation对象,方法的返回值就是这个注解中携带的信息了。

public class Demo {
    public static void main(String[] args) {
        Class<?> clazz = Person.class;
        try {
            Field field = clazz.getDeclaredField("name");
            MyAnnotation annotation = field.getAnnotation(MyAnnotation.class);
            Class<? extends MyAnnotation> aClass = annotation.getClass();
            Method method = aClass.getMethod("value");
            String[] values = (String[])method.invoke(annotation);
            System.out.println(Arrays.toString(values));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

做个小案例,感受一下控制反转和依赖注入的基本原理

Spring的核心特性是控制反转(IOC)和面向切面编程(AOP)。控制反转是指对象的控制权不在我们手里了,而是交给了Spring给我们创建。我们只需要把实体类定义好,提供好无参构造方法和get、set方法就好了,它给你的对象自动赋值了,这就叫依赖注入(DI)。只不过,它给属性赋的值是从数据库中读取出来的,而我们这里选择把属性值写在注解中,然后读取注解中的值,给属性赋值。我这个案例它只能够处理9种属性类型,包括8种基本类型的包装类以及String,其他的类型还不能支持。

package test_annotation;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

/**
 * @author 乔澳
 * @version 1.0
 * @title: MySpringDemo
 * @projectName Demo1
 * @description:
 * @date 2020/8/17   18:22
 */
public class MySpringDemo {
    public Object getBean(String className){
        Object obj = null;
        Class<?> clazz = null;
        try {
            clazz = Class.forName(className);
            //获取无参构造方法
            Constructor con = clazz.getConstructor();
            //调用无参构造方法创建对象
            obj = con.newInstance();
            //解析注解中的信息
            //注解放在属性上面,首先获取所有的属性
            Field[] fields = clazz.getDeclaredFields();
            for(int i = 0;i<fields.length;i++){
                //根据属性获取属性上面声明的注解
                Annotation annotation = fields[i].getAnnotation(MyAnnotation.class);
                Class<?> aClass = annotation.getClass();
                Method aMethod = aClass.getMethod("value");
                //获取注解中的值
                String[] values = (String[]) aMethod.invoke(annotation);
                //获取属性名
                String fieldName = fields[i].getName();
                //要给属性赋值,属性是私有的,虽然反射可以操作私有属性,但是很不合理
                //我们利用字符串的拼接,得到set方法的名字,再拿到set方法给属性赋值
                String firstLetter = fieldName.substring(0,1).toUpperCase();//首字母大写
                String otherLetters = fieldName.substring(1);
                StringBuilder setMethodName = new StringBuilder("set");
                setMethodName.append(firstLetter);
                setMethodName.append(otherLetters);
                //拿到属性的类型,下面要用到
                Class<?> fieldType = fields[i].getType();
                //根据set方法名字拿到set方法
                Method setMethod = clazz.getMethod(setMethodName.toString(),fieldType);
                //调用set方法,给对象赋值,如果属性不是String类型,是基本类型的包装类
                //如果属性是Character类型,做单独处理
                if (fieldType==Character.class){
                    //把字符串转化为字符
                    Character c = values[0].toCharArray()[0];
                    setMethod.invoke(obj,c);
                }else{
                    //不是Character类型,而是String 或其他7种包装类,调用包装类的带String参数的构造方法,构造值  比如;new Integer(String v)
                    setMethod.invoke(obj,fieldType.getConstructor(String.class).newInstance(values[0]));
                }
                //对于属性为数组、集合、对象的情况,这里没有做处理
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return obj;
    }
}

测试:

写一个实体类

package test_annotation;

public class Person {
    @MyAnnotation("Tom")
    private String name;
    @MyAnnotation("18")
    private Integer age;
    @MyAnnotation("男")
    private String sex;
    @MyAnnotation("A")
    private Character bloodType;//血型
    public Person(){}
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Integer getAge() {
        return age;
    }
    public void setAge(Integer age) {
        this.age = age;
    }
    public String getSex() {
        return sex;
    }
    public void setSex(String sex) {
        this.sex = sex;
    }
    public Character getBloodType() {
        return bloodType;
    }
    public void setBloodType(Character bloodType) {
        this.bloodType = bloodType;
    }
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", sex='" + sex + '\'' +
                ", bloodType=" + bloodType +
                '}';
    }
}

main方法:

package test_annotation;

public class TestMain {
    public static void main(String[] args) {
        MySpringDemo msd = new MySpringDemo();
        Person p = (Person) msd.getBean("test_annotation.Person");
        System.out.println(p);
    }
}

参考链接:
https://www.cnblogs.com/bingosblog/p/5852510.html
https://blog.csdn.net/zt15732625878/article/details/100061528?utm_source=app

你可能感兴趣的:(JAVA)