注解与反射

## 注解(Annotation)

从JDK5开始,Java增加对元数据的支持,也就是注解,注解与注释是有一定区别的,可以把注解理解为代码里的特殊标记,这些标记可以在编译,类加载,运行时被读取,并执行相应的处理。通过注解开发人员可以在不改变原有代码和逻辑的情况下在源代码中嵌入补充信息。

​ ——百度百科

注解可以对程序做出解释,也可以被其他的程序进行读取,如编译器等。

  • 注解的格式:@注释名 还可以加一些参数值:
//eg
@SuppressWarnings(value="unchecked")
  • 注解可以附加在package,class,method,field等上面,相当于给他们添加额外辅助信息。可以通过反射机制编程实现对这些元数据的访问。

内置注解

  1. @Override:只适用于修饰方法,表示一个方法声明打算重写超累中的另一个方法声明。

  2. @Deprecated:可以修饰方法,属性,类,表示已废弃,不鼓励使用。

  3. @SuppressWarnings:用来抑制编译时的警告信息。

    该注解使用时需要添加参数,参数是已经定义好的直接拿来用即可:

    //单个参数
    @SuppressWarnings("unchecked")
    //多个参数
    @SuppressWarnings(value={"unchecked","deprecation"})
    

元注解

元注解的作用是用来注解其他的注解。

  1. @Target:描述该注解的使用范围。(有参数)

    @Target(value = {ElementType.METHOD,ElementType.TYPE})
    @interface MyAnnotation{ }
    

    这表示该注解MyAnnotation可以用在方法和类上

  2. @Retention:表示需要在什么级别保存该注释信息,用于描述注解的生命周期。(有参数)

    @Target(value = {ElementType.METHOD,ElementType.TYPE})
    @Retention(value = RetentionPolicy.RUNTIME)
    @interface MyAnnotation{ }
    

    (级别:SOURCE < CLASS <RUNTIME)这里表明该注解是运行时周期,周期是最大的

  3. @Document:说明该注解将被包含在javadoc中。(无参数)

  4. @Inherited:说明子类可以继承父类中的该注解。(无参数)

自定义注解

使用@interface自定义注解,格式为:

[public] @interface MyAnnotation{ }

它自动继承Annotation接口,public视情况判断需不需要。

注解内有一些要注意的点:

  1. 有些注解内会有一些方法,譬如元注解内::

    public @interface Target {
        ElementType[] value();
    }
    

    这里的每一个“方法”实际上是声明了一个配置参数,而其名称就是参数名,参数的值也定义在对应的返回值里面。

    我们也可以通过default来声明参数的默认值。

  2. 对于一个注解,如果定义了参数,使用注解时如果没有默认值就必须要赋值。

  3. 注解内参数的定义格式:

    参数类型 参数名() [default 默认值]

  4. 如果只定义一个参数,建议用value命名。因为value命名时且该注解只有value一个参数,可以在使用注解时不用写参数名:

    @Retention(value = RetentionPolicy.RUNTIME)
    @interface MyAnnotation0{
        String value();
    }
    
    class Test{
        @MyAnnotation0("hhh")//不用写value=
        public void test();
    }
    

以下说明相关例子:

public class Demo01 {
    @MyAnnotation(age = 20,id = 10)
    public void test(){

    }
}

//定义一个注解
@Target(value = {ElementType.METHOD,ElementType.TYPE})//可以用在方法,类
@Retention(value = RetentionPolicy.RUNTIME)//生命周期为RUNTIME
@interface MyAnnotation{
    String name() default "";
    int age() default 10;
    int id();
}

静态 动态语言

动态语言:

在运行时可以改变其结构的语言。例如新的函数、对象、代码等被引进,已有的函数可以被删除,或者是其他结构上的变化。

包括Object-C,C#,JavaScript,PHP,Python等

静态语言:

在运行时不可以改变其结构的语言。包括Java,C,C++。

Java是一门“准动态语言”,即具有一定的动态性,利用反射机制可以实现类似动态语言的特性。

反射(Reflection)

反射机制是Java被视为动态语言的关键,反射机制允许程序在执行期间借助于Reflection API取得任何类的内部信息,并能够直接操作任意对象的内部属性和方法。

注:反射是一种解释型的操作,这种操作性能会差一点。

Class类

首先明确一点:类在内存中只加载一次,也就是说,每个类在内存中只有一个Class对象:

Class c1 = Class.forName("AnnotationLearning.User");
Class c2 = Class.forName("AnnotationLearning.User");
//一个类在内存中只有一个Class对象
//一个类被加载后,类的整个结构会被封装在该类的Class对象中
System.out.println(c1.hashCode());
System.out.println(c2.hashCode());

然后明确Class类的注意事项:

  • Class对象只会由系统建立
  • 一个加载类在JVM中只会有一个Class实例
  • 一个Class对象对应的是一个加载到JVM中的一个class文件
  • 每个类的实例都会记得自己是由哪个Class实例生成
  • 通过Class可以完整得到一个类中的所有被加载的结构
  • Class类是反射机制的根源,针对任何你想动态加载运行的类,只有先获得相应Class对象才能执行下一步操作。

获取Class类实例的方法

  1. 通过各自类的class属性获取:Class class = Person.class

  2. 各自类的对象调用getClass()方法:Class class = person.getClass()

    注:可以使用getSuperClass()获取父类

  3. 已知一个类的全类名,且在该路径下,可以使用forName()获取,例子如上面的代码所述,要记得抛出ClassNotFoundException异常。

  4. 基本数据类型的包装类可以使用 类名.Type

  5. 可以利用ClassLoader

一些偏底层知识

Java内存分析

image-20220405150748484

  1. Java栈:栈中只保存基本数据类型的数据和自定义对象的引用(不是对象)。

    每个线程包含一个栈区,每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。

  2. 堆:堆中保存new的对象和数组。

    编译器不必知道要从堆里分配多少存储空间,也不必知道存储的数据要在堆里停留多长的时间。因此,用堆保存数据时会得到更大的灵活性。但是,在堆里分配存储空间时会花掉更长的时间。

  3. 方法区:存放所有的:①类(class),②静态变量(static变量),③静态方法,④常量和⑤成员方法。也包含运行时常量池。

    方法区中存放的都是在整个程序中永远唯一的元素。这也是方法区被所有的线程共享的原因。

类的加载过程:

当程序主动使用某个类,如果该类还未加载到内存中时,系统将经过三步将类初始化:

  1. 类加载:将类的class文件读入内存,并创建一个对应的Class对象。

  2. 类链接:将类的二进制数据合并到JRE(java运行环境)中:

    • 验证规范和安全问题
    • 为类的static变量分配内存、设置默认初始值(该默认值不是在类里面初始化的值,而是系统判断变量类型默认赋予的值)
    • 常量池中的常量名替换成地址
  3. 类初始化:JVM开始对类初始化:

    • 执行类构造器<clinit>()方法的过程

      所谓clinit方法,它是由所有static成员和代码块组成合并而来。

    • 当初始化一个类,如果发现其父类没有初始化,先触发父类的初始化

    • 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步

这里提到一个问题:一个对象初始化加载过程到底是怎样的?

  1. 若有父类,先加载其父类

  2. 为静态成员分配内存,设置默认初始值

  3. 静态代码块和类static成员变量合并,按照代码的先后顺序依次执行

    static {
            System.out.println("静态代码块");
            num = 30;
        }
        static int num = 20;
    

    譬如这种就是先执行静态代码块,num从0变为30,再执行num=20。如果顺序颠倒结果就是30。

  4. 执行非静态代码块以及普通类实例变量,按照代码的先后顺序依次执行。

  5. 构造方法

类的初始化

什么时候发生类的初始化?

  • 主动引用(会发生)
    • 虚拟机启动必定初始化main方法所在类
    • new一个新对象时
    • 调用类的静态成员和静态方法时
    • 使用reflect包的方法对类进行反射调用
    • 初始化一个类时如果有父类会先初始化父类
  • 被动引用(不会发生)
    • 当访问一个静态域,如果没有声明这个域的类,不会被初始化。如:当通过子类引用父类的静态变量,子类不会初始化。
    • 通过类定义对应数组,不会触发初始化
    • 引用常量不会触发此类的初始化。

类加载器

将类的class文件字节码读入内存,将这些静态数据转换成方法区的运行时数据结构,并在堆内创建一个对应的Class对象。

当某类通过加载到类加载器中时,它将维持加载一段时间,JVM垃圾回收机制可以回收这些Class对象。

类加载器包括:

  1. 引导类加载器:C++编写,JVM自带的类加载器,负责Java平台核心库。该加载器无法直接获取。
  2. 扩展类加载器:负责jre/lib/ext目录下的jar包或者java.ext.dirs指定目录下的jar包装入工作库。
  3. 系统类加载器:负责java -classpath或者java.class.path指定目录下的类与jar包装入工作库,是最常用加载器。
//获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
//获取系统类加载器的父类加载器--扩展类加载器
ClassLoader parent = systemClassLoader.getParent();
//获取扩展类加载器的父类加载器--引导类加载器
ClassLoader parent1 = parent.getParent();

System.out.println(systemClassLoader);
System.out.println(parent);
System.out.println(parent1);

获取系统类加载器可以加载的路径:

System.sout.println("System.getProperty("java.class.path")");

补充:双亲委派机制

​ 类加载器会将类的class文件字节码读入内存,当我们要加载这个类的class文件,将其读入内存时,需要调用加载器。

​ 首先明白一点,之前也提到过:当某类通过加载到类加载器中时,它将维持加载一段时间。那么当我们先前加载了某一个类,这个类将会在加载器中维持一段时间,后面如果又用到同样的类,就不用重复加载。

​ 双亲委派机制中:

  1. 当我们调用加载器,先查看是否该类在系统类加载器中被加载过,如果有,就不用重复加载;如果没有,直接将该请求丢给父类加载器,也就是扩展类加载器。同理,再继续看扩展类加载器有没有加载过,有就不管,没有就把加载请求丢给引导类加载器。最后直到引导类加载器,如果加载过就不管。

  2. 如果直到引导类加载器都没有加载过该类怎么办?那么查看引导类能否加载该包,也就是目录内有没有该包可以进行加载。如果有就加载,没有就将请求丢给子类加载器,也就是扩展类加载器。如果扩展类目录也没有该包可以加载,那么丢给子类加载器,也就是系统类加载器,以此类推。

​ 这两步,第一步理解为提高效率,不要重复加载;第二步为了保证安全,不要有篡改包。提高效率很好理解,关键是安全如何保证?如果有人想替换系统级别的类:String.java,自行定义一个java.lang.String类篡改它的实现,在这种机制下,String类已经被系统加载过的话,就不用重复加载;若是没有加载过,就会从引导类开始查询可否加载,一步步往下查,而String类在引导类可以加载的,所以保证了一定的安全。

获取类的运行时结构

  1. 获取类名:

    Class c1 = Class.forName("*******");
    System.out.println(c1.getName());
    
  2. 获取类的属性:

    • getFields()方法:

      Field[] fields = c1.getFields();
      for(Field field : fields){
          System.out.println(field);
      }
      

      这个方法只能获取public属性

    • getDeclaredFields()方法:

      Field[] fields = c1.getDeclaredFields();
      for(Field field : fields){
          System.out.println(field);
      }
      

      可以获得所有属性

    • getDeclaredField(String name)方法,getField(String name)方法,这两个可以获得指定属性,同样的,前者可以获得所有,后者只有public。

  3. 获取类的方法:

    • getMethods()方法:

      Method[] me = c1.getMethods();
      for(Method method : me){
          System.out.println(method);
      }
      

      该方法可以获取本类及其父类的所有public方法

    • getDeclaredMethods()方法:

      Method[] me = c1.getDeclaredMethods();
      for(Method method : me){
          System.out.println(method);
      }
      

      该方法可以获取本类的所有方法

    • getMethod(String name , Class<?>... parameterTypes)获取指定方法:

      Method method = c1.getMethod("getName" , null);
      Method method = c1.getMethod("setName" , String.class);
      
    • getConstuctors():

      Constructor[] con = c1.getConsturctors();
      for(Constructor constructor :con){
          System.out.println(constructor);
      }
      

      该方法用于获取public构造方法

    • getDeclaredConstuctors()

      Constructor[] con = c1.getDeclaredConsturctors();
      for(Constructor constructor :con){
          System.out.println(constructor);
      }
      

      该方法用于获取所有构造方法

    • getConstuctor(Class<?>... parameterTypes)获得指定构造方法:

      Constructor constructor = c1.getConstuctor(String.class,int.class,int.class);
      

动态创建对象

我们可以通过反射机制动态创建对象:

//创建对应Class对象
Class<?> c1 = Class.forName("AnnotationLearning.User");

//构造一个对象
User user = (User) c1.newInstance();//本质是调用无参构造器构造的
System.out.println(user);

newInstance()方法本质是调用无参构造器,所以创建的对象的类必须:

  1. 有一个无参构造器。
  2. 构造器的访问权限需要足够

当然,我们也可以调用有参构造,稍微绕一下,创建对应的构造器对象:

Constructor constructor = c1.getDeclaredConstructor(String.class,int.class,int.class);
User user = (User) constructor.newInstance("小明",10,30);
System.out.println(user);

动态调用方法

与上面获取有参构造类似,先获取Class对象,再创建对应的对象之后,再创建对应的Method对象:

Method setName = c1.getDeclaredMethod("setName", String.class);
setName.invoke(user,"小红");
System.out.println(user.getName());

其中调用了一个invoke()方法,是指激活的意思,第一个参数是指激活的对象,后面的参数是指传参。

动态操作属性

与上面获取也类似,先获取Class对象,创建对应的对象之后,再操作属性:

//通过反射操作属性
Field name = c1.getDeclaredField("name");
name.setAccessible(true);//设置不检查属性权限
name.set(user,"小刚");
System.out.println(user);

这里有一些注意事项:

  1. 由于某些类里的私有属性不能直接操作,所以想直接操作属性需要关闭检查方法,也就是setAccessible(true),这是权限检查访问的开关,记住,true是关闭检查。
  2. set方法的参数,前者是操作的对象目标,后者是修改的内容。

反射操作泛型

为了通过反射操作泛型,Java新增了:

ParameterizedType:表示一种参数化类型,譬如Collection<String>

GenericArrayType:表示一种元素类型是参数化类型或者类型变量的数组类型。

TypeBariable:是各种类型变量的公共父接口。

WildcardType:代表一种通配符类型表达式。

新增这些类型来代表不能归一到Class类中的类型但和原始类型齐名的类型。

举个例子:

首先创建两个方法:

public void test01(Map<String,User> map, List<User> list){
    System.out.println("123456");
}

public Map<String,User> test02(){
    System.out.println("654321");
    return null;
}

接下来我们在main函数中写这些内容:

Class c1 = Class.forName("AnnotationLearning.Demo04");
Method method = c1.getDeclaredMethod("test01",Map.class,List.class);
Type[] types = method.getGenericParameterTypes();//获得泛型的参数类型
for (Type type :
        types) {
    System.out.println(type);//循环输出参数类型
    if (type instanceof ParameterizedType){//如果该泛型参数类型是一个参数化类型
        Type[] actualTypeArguments = ((ParameterizedType) type).getActualTypeArguments();//获取该参数内的泛型类型
        for (Type actualTypeArgument:
             actualTypeArguments) {//循环输出泛型
            System.out.println(actualTypeArgument);
        }
    }
}

这样就可以遍历test01的传参的泛型内容了。

同理,想要遍历test02方法的返回值类型:

Method method1 = c1.getDeclaredMethod("test02", null);
Type type = method1.getGenericReturnType();//获取泛型返回值类型
System.out.println(type);
if (type instanceof ParameterizedType) {//如果该泛型参数类型是一个参数化类型
    Type[] actualTypeArguments = ((ParameterizedType) type).getActualTypeArguments();//获取该参数内的泛型类型
    for (Type actualTypeArgument :
            actualTypeArguments) {
        System.out.println(actualTypeArgument);
    }
}

我们可以将这两段代码中的ParameterizedType替换为之前说的其他三个类型,来实现不同的功能。

反射操作注解

我们编写两个注解,一个用于对类的注解,一个用于对属性的注解,并创建一个对应的entity类(可以类比为数据库内创建一个学生表):

@AnnotationClass("db_Student")
class TestStudent{//一个学生类
    @AnnotationField(columnName = "Std_name",Type = "varchar",length = 10)
    private String name;
    @AnnotationField(columnName = "Std_id",Type = "int",length = 10)
    private int id;
    @AnnotationField(columnName = "Std_age",Type = "int",length = 10)
    private int age;

    public TestStudent(){

    }

    public TestStudent(String name, int id, int age) {
        this.name = name;
        this.id = id;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}


//类名注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface AnnotationClass{
    String value();
}

//属性注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface AnnotationField{
    String columnName();//列名
    String Type();//属性数据类型
    int length();//属性长度
}

我们创建好后,如何访问注解里面的内容呢?

首先是利用反射获得注解:

Class aClass = Class.forName("AnnotationLearning.TestStudent");

//通过反射获得注解
Annotation[] annotations = aClass.getAnnotations();
for (Annotation annotation :
        annotations) {
    System.out.println(annotation);
}

这个的输出结果为一整个注解,但是如何单独获取注解里面的内容?譬如我想获取对name的属性注解里面的columnName的内容?或者是类名的?以下为方法:

//获得类注解的value值
AnnotationClass annotationClass = (AnnotationClass)aClass.getAnnotation(AnnotationClass.class);//获取该学生类的注解对象
String value = annotationClass.value();//获取注解内的value属性
System.out.println(value);

//获得指定属性的注解
Field field = aClass.getDeclaredField("name");//获取学生类的name属性
AnnotationField annotationField = (AnnotationField) field.getAnnotation(AnnotationField.class);//获取name属性的注解对象
//获取注解里面的各个属性
System.out.println(annotationField.columnName());
System.out.println(annotationField.Type());
System.out.println(annotationField.length());

这样就可以实现对注解内容的访问。

posted @ 2022-04-06 21:56  可乐加品客  阅读(50)  评论(0)    收藏  举报