Java基础——注解与反射

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类用方法:

类型方法功能
staticforName(String name)返回指定类名的对象
publicObject newInstance()通过class对象调用默认的构造函数返回一个对象
publicString getName()返回该class对象的全限定名
publicClass getSuperClass()返回该class对象的父类的class对象
publicClass[] getInterfaces()返回该class对象实现的全部接口
publicClassLoader getClassLoader()返回该class对象的类加载器
publicConstructor[] getConstructors()返回该class对象实现的全部接口
publicMethod getMethod(String name, Class<?>… parameterTypes))返回该class对象指定的方法对象
publicMethod[] getMethods())返回该class对象的全部方法对象
publicField 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 的内存模型

在这里插入图片描述类的加载过程是:

  1. load:将class文件读入内存,并创建一个Java.lang.Class对象。class文件是静态的代码,需要将这些代码转化成运行时的一些数据结构,比如为一些值设计数据结构。
  2. link:将类的二进制数据处理并放入JRE;
    1. 验证:检查代码,确保没有错误;
    2. 准备:为类变量分配内存空间并设置默认的初始值;
    3. 解析:虚拟机中的常量池的符号引用替换直接引用。
  3. initialize:JVM对类进行初始化。首先会执行<clint>()方法,即类构造器,完成对类的初始化。初始化类的时候,必须先初始化其父类。

我们来看一下这个过程:静态代码块会在类被加载时就执行,我们在获取一个类的Class对象时就会把这个Class文件加载进来。
要明确:加载只是把文件读入,并进行分配一定的内存空间,而初始化一个类则会消耗大量的资源,使用反射的方式加载类属于被动加载,很多时候不会触发多余的类的初始化:图片来自b站狂神视频
在这里插入图片描述

3. 类加载器

在JVM的内存模型中,类加载器就是负责找到这个类的class文件并加载到JVM中。存储class文件的地方叫方法区,这个区中保存这加载过的class文件,并提供一定的缓存机制,以减少频繁的从磁盘中加载类文件。
java提供了几种类加载器:图片来自b站狂神视频
在这里插入图片描述所谓的不同的类加载器,其实就是负责加载不同地方的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 + '\'' +
                '}';
    }
}

结果:
在这里插入图片描述


总之,通过反射来操作属性和方法时,一定要传递你要操作的对象注意:如果你访问的属性、方法或者构造器是私有的,那么在调用之前一定要使用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();
}

结果:
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值