天天看点

Java基础——注解与反射1. 简介2. 注解3. 反射

1. 简介

最近在学习框架技术时频繁地遇到注解与反射相关的原理,之前学习的javaSE中虽然学过,但是记忆模糊,因此打算重新复习一下这方面的知识。

2. 注解

2.1 概念

Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制。Java 语言中的类、方法、变量、参数和包等都可以被标注。

注解与注释有相似的,他们都是对其标注的代码的一种说明。但是,注解更加复杂:

  • 有明确的编写语法:注解的声明需要符合java的命名规范,且自定义注解需要满足Java的语法;
  • 有明确的使用位置:注解的使用位置是需要指明的;
  • 有明确的有效期:一个注解的生命周期包括:源代码,class文件,和运行时。
  • 具有强制性:注解所规定的内容用户在使用时必须遵守,否则编译无法通过。

java中是使用反射的机制来读取注解的。

2.2 核心机制

1. 元注解 meta-annotation

java中提供了四个元注解,即注解的注解,我们在编写自己的注解时,需要关注这四个注解,并按照需求对这四个注解进行赋值。

  • @Document:说明该注解将写入javadoc中;
  • @Retention:说明该注解可以被保存到什么时候(SOURCE<CLASS<RUNTIME);
  • @Target: 描述该注解的使用范围(METHOD,CLASS,FIELD);
  • @Inherited:说明子类可以继承父类中的该注解。

我们来看一下Target注解的内容:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    /**
     * Returns an array of the kinds of elements an annotation type
     * can be applied to.
     * @return an array of the kinds of elements an annotation type
     * can be applied to
     */
    ElementType[] value();
}
           

ElementType是一个枚举类型,其可枚举的值包括:

public enum ElementType {
    /** Class, interface (including annotation type), or enum declaration */
    TYPE,

    /** Field declaration (includes enum constants) */
    FIELD,

    /** Method declaration */
    METHOD,

    /** Formal parameter declaration */
    PARAMETER,

    /** Constructor declaration */
    CONSTRUCTOR,

    /** Local variable declaration */
    LOCAL_VARIABLE,

    /** Annotation type declaration */
    ANNOTATION_TYPE,

    /** Package declaration */
    PACKAGE,

    /**
     * Type parameter declaration
     *
     * @since 1.8
     */
    TYPE_PARAMETER,

    /**
     * Use of a type
     *
     * @since 1.8
     */
    TYPE_USE,

    /**
     * Module declaration.
     *
     * @since 9
     */
    MODULE
}
           

2. 自定义注解

java中使用@interface来创建自己的注解,此时会自动帮我们继承

java.lang.annotation.Annotation

接口。格式为:

@元注解
@interface MyAnnotation {
}
           

我们可以在注解中定义参数,其格式为

参数类型 参数名();

且只能定义String、基本数据类型,数组和enum。注意:注解中参数的赋值是没有顺序的。如果该注解只有一个值,那么说明时可以使用

value=xxx

的方式赋值。

@Target(value = ElementType.TYPE)
@interface MyAnnotation1 {
//    注解的参数,如果使用了注解并且没有为参数设置初始值,那么就会报错
    String name();
//    如果该参数的值默认是空,那么不需要初始化
    String addr() default "";
}
@MyAnnotation1(name="yindarui")
@Target(value = ElementType.METHOD)
@interface MyAnnotation2 {
}
           

使用value作为注解的参数名时,当只有一个参数,在赋值时可忽略字符串。

@MyAnnotation3
public class AnnotationTest {
//    在使用时可以忽略
    @MyAnnotation2("zhangsan")
    @MyAnnotation3
    public static void main(String[] args) {

    }
}
@Target(value = ElementType.TYPE)
@interface MyAnnotation1 {
//    注解的参数,如果使用了注解并且没有为参数设置初始值,那么就会报错
    String name();
//    如果该参数的值默认是空,那么不需要初始化
    String addr() default "";
    int id() default 0; // 0表示默认为0
    int sal() default -1; // -1表示报不存在
    String[] client() default {"zhangsan", "lisi"};
}
@MyAnnotation1(name="yindarui")
@Target(value = ElementType.METHOD)
@interface MyAnnotation2 {
//    使用value作为参数名
    String value();
}

//Target注解的value是一个数组,所以可以指定多个作用域
@Target(value={ElementType.TYPE, ElementType.METHOD})
@interface MyAnnotation3 {

}
           

3. 反射

3.1 概念

反射机制使得java具备了动态性,即在java程序运行的时候,程序可以根据运行时的代码,改变自身的一些结构。

反射机制允许java程序在运行时,借助一些API来获取一个类的全部信息,并直接对该类中的属性、方法进行操作。

3.2 核心机制

我们知道,java的代码在编写完成,通过编译器编译之后会生成

.class

文件,这称为字节码文件。一个字节码文件会通过类加载器加载到Java虚拟机中,在被调用时进行实例化。

**反射机制想创建对象也必须获取到该对象的字节码文件。**在java中,class文件也是一个类。通常通过

Class c = Class.forName("java.lang.String");

,或者

Object.getClass()

获得。

类加载机制

首先我们先了解一下class文件。

先通过反射的方式获取到class文件:如下的三个HashCode值是相同的,说明class文件在JVM运行时只会保存一份。一个类在被加载到内存中,其所有的内容都会被封装到Class对象。

Class person1 = Class.forName("com.yindarui.annotation.Person");
Person person = new Person();
Class person2 = person.getClass();
System.out.println(person1.hashCode());
System.out.println(person2.hashCode());
//结果是一样的
1967205423
1967205423
           

那么到底什么是Class类呢?所有的其他类都由Class类创建,但是Class类本身也是一个类。其封装了一个对象(编写的代码)里面的全部内容。JVM可以通过Class对象创建别的对象。同样的,对于任何一个对象,JVM都可以通过对象的全限定名找到其Class文件,并获取该Class文件的全部内容。

3.3 使用Class类

Class类用方法:

类型 方法 功能
static forName(String name) 返回指定类名的对象
public Object newInstance() 通过class对象调用默认的构造函数返回一个对象
public String getName() 返回该class对象的全限定名
public Class getSuperClass() 返回该class对象的父类的class对象
public Class[] getInterfaces() 返回该class对象实现的全部接口
public ClassLoader getClassLoader() 返回该class对象的类加载器
public Constructor[] getConstructors() 返回该class对象实现的全部接口
public Method getMethod(String name, Class<?>… parameterTypes)) 返回该class对象指定的方法对象
public Method[] getMethods()) 返回该class对象的全部方法对象
public Field getDeclaredFields() 返回Field的一个数组

如果你熟悉java类包含的内容,那么你一定会很好理解Class类中提供的方法。这几乎包括了一个类中所有的信息。(还可以获取Annotation,这就是框架中大量使用的)

1. 获取Class文件

ArrayList<Class> classes = new ArrayList<>();
classes.add(Object.class); //根类
classes.add(Iterable.class); // 接口
classes.add(Override.class); // 注解
classes.add(ElementType.class); // 枚举类
classes.add(String[].class); // 数组
classes.add(String[][].class); // 数组
classes.add(int.class); // 基本数据类型
classes.add(void.class); // 空类型
classes.add(Class.class); // Class本身也是一个
for (int i = 0; i < 9; i++) {
    System.out.println(classes.get(i).hashCode());
}
// 结果:对于数组而言,一维数组和二维数组即使类型相同,其对应的Class文件也不同。
1426407511
589431969
1967205423
42121758
20671747
257895351
874088044
783286238
705927765
           

2. java 的内存模型

Java基础——注解与反射1. 简介2. 注解3. 反射

类的加载过程是:

  1. load:将class文件读入内存,并创建一个Java.lang.Class对象。class文件是静态的代码,需要将这些代码转化成运行时的一些数据结构,比如为一些值设计数据结构。
  2. link:将类的二进制数据处理并放入JRE;

    1. 验证:检查代码,确保没有错误;

    2. 准备:为类变量分配内存空间并设置默认的初始值;

    3. 解析:虚拟机中的常量池的符号引用替换直接引用。

  3. initialize:JVM对类进行初始化。首先会执行

    <clint>()

    方法,即类构造器,完成对类的初始化。初始化类的时候,必须先初始化其父类。

我们来看一下这个过程:静态代码块会在类被加载时就执行,我们在获取一个类的Class对象时就会把这个Class文件加载进来。

要明确:加载只是把文件读入,并进行分配一定的内存空间,而初始化一个类则会消耗大量的资源,使用反射的方式加载类属于被动加载,很多时候不会触发多余的类的初始化:图片来自b站狂神视频

Java基础——注解与反射1. 简介2. 注解3. 反射

3. 类加载器

在JVM的内存模型中,类加载器就是负责找到这个类的class文件并加载到JVM中。存储class文件的地方叫方法区,这个区中保存这加载过的class文件,并提供一定的缓存机制,以减少频繁的从磁盘中加载类文件。

java提供了几种类加载器:图片来自b站狂神视频

Java基础——注解与反射1. 简介2. 注解3. 反射

所谓的不同的类加载器,其实就是负责加载不同地方的class文件。

ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);
ClassLoader parent = systemClassLoader.getParent();
System.out.println(parent); // AppClassLoader
ClassLoader parent1 = parent.getParent(); // PlatformClassLoader
System.out.println(parent1); // null
System.out.println(Object.class.getClassLoader()); // null
           

双亲委派机制

JVM在加载类的时候是从启动加载器向下找的,即如果我们自定义一个java.lang.String类型,是无法被加载进JVM的。因为JVM通过boostrap ClassLoader 获取时就发现了这个类,加载后就停止寻找了。

这样做保证了Java代码的执行的正确性与安全性。

4. 通过反射创建对象

使用newInstance方法创建对象需要该类有无参构造器。

此外,我们还可以用别的方法创建:

  1. Class类的getDeclaredConstructor() 获取构造器;
  2. 向构造器中传递参数的class文件,包括该构造器的所有参数;
  3. 通过Constructor进行实例化。
package com.yindarui.annotation;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 *
 */
public class Constructor {

    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class c1 = Class.forName("com.yindarui.annotation.Student");
        java.lang.reflect.Constructor stuCon = c1.getDeclaredConstructor(int.class, String.class);
        Object yindarui = stuCon.newInstance(1, "yindarui");
        System.out.println(yindarui.toString());
        Method setId = c1.getDeclaredMethod("setId", int.class);
        System.out.println("通过invoke来修改id参数");
        setId.invoke(yindarui,20);
        System.out.println(yindarui.toString());
        System.out.println("通过invoke来修改name参数, 此时setName方法是私有的");
        Method setName = c1.getDeclaredMethod("setName", String.class);
        setName.setAccessible(true);
        setName.invoke(yindarui,"helloworld");
        System.out.println(yindarui.toString());
    }
}

class Student {
    private int id;
    private String name;

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

    public void setId(int id) {
        this.id = id;
    }
    private void setName(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

           

结果:

Java基础——注解与反射1. 简介2. 注解3. 反射

总之,通过反射来操作属性和方法时,一定要传递你要操作的对象。注意:如果你访问的属性、方法或者构造器是私有的,那么在调用之前一定要使用setAccessible来获取权限。

5. 反射获取注解

注解开发的核心就是利用反射机制获取一个class文件中的注解内容。通常有几种方式:

  • 通过class获取全部注解;
  • 通过指定某个注解名称获取注解;
  • 通过某个属性或者方法获取标记在其上的注解;
package com.yindarui.annotation;
import java.lang.annotation.*;
import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Field;


@MyAnnotation5("hello")
public class ReflectionForAnnotation {
    @MyAnnotation4("id = 10")
    public int id;
    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException {
        Class aClass = Class.forName("com.yindarui.annotation.ReflectionForAnnotation");
        Field id = aClass.getDeclaredField("id");
//        获取全部的注解
        Annotation[] annotation2 = aClass.getAnnotations();
//        获取指定的注解
        MyAnnotation5 annotation1 = (MyAnnotation5) aClass.getAnnotation(MyAnnotation5.class);
        System.out.println(annotation1);
        System.out.println(annotation1.value());
//       通过属性名获取标记在属性上的注解,方法也一样
        MyAnnotation4 annotation = id.getAnnotation(com.yindarui.annotation.MyAnnotation4.class);
//       获取在id属性上的全部注解
        Annotation[] annotations = id.getAnnotations();
        System.out.println(annotation.value());
    }
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface MyAnnotation4{
    String value();
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface MyAnnotation5{
    String value();
}
           

结果:

Java基础——注解与反射1. 简介2. 注解3. 反射