java注解与反射的基本使用(这一篇就够了!)

一、注解(Annotation)

1.什么是注解?
相信大家对注解应该并不陌生,在现在信息飞速发展的年代,各种优秀的框架或许都离不开注解的使用,像我们在实现接口一个方法时,也会有@Override注解。注解说白了就是对程序做出解释,与我们在方法、类上的注释没有区别,但是注解可以被其他程序所读取,进行信息处理,否则与注释没有太大的区别。
2.内置注解
内置注解就是我们的jdk所带的一些注解。常用的三个注解:

  • @Override
    这个应该都不陌生,修辞方法,表示打算重写超类中的方法声明。
  • @Deprecated
    这个注解我们应该也不会陌生,我们可能看不到这个注解,但是我们肯定在使用一些方法时会出现横线。表示废弃,这个注释可以修辞方法,属性,类,表示不鼓励程序员使用这样的元素,通常是因为他很危险或有更好的选择。
    在这里插入图片描述
  • @SuperWarnings
    这个注解主要是用来抑制警告信息的,我们在写程序时,可能会报很多黄线的警告,但是不影响运行,我们就可以用这个注解来抑制隐藏它。与前俩个注解不同的是我们必须给注解参数才能正确使用他。
参数 说明
deprecation 使用了过时的类或方法的警告
unchecked 执行了未检查的转换时的警告 如:使用集合时未指定泛型
fallthrough 当在switch语句使用时发生case穿透
path 在类路径、源文件路径中有不存在路径的警告
serial 当在序列化的类上缺少serialVersionUID定义时的警告
finally 任何finally子句不能完成时的警告
all 关于以上所有的警告

上表中就是@SuperWarnings注解的一些参数,按需使用即可。
@SuperWarnings(“finally”)
@SuperWarnings(value={“unchecked”,“path”})
在这里插入图片描述在这里插入图片描述
3.自定义注解

格式:public @interface 注解名 { 定义体 }

  • 使用@interface自定义注解时,自动继承了java.lang.annotation.Annotation接口
  • 其中的每一个方法实际上是声明了一个配置参数
  • 方法的名称就是参数的名称
  • 返回值类型就是参数的类型(返回值类型只能是基本类型、Class、String、enum)
  • 可以通过default来声明参数的默认值
  • 如果只有一个参数成员,一般参数名为value
  • 我们在使用注解元素时必须要有值,可以定义默认值,空字符串,0或者-1
public @interface TestAnnotation {

    //参数默认为空
    String value() default "";
    
}

4.元注解
我们在自定义注解时,需要使用java提供的元注解,就是负责注解的其他注解。java定义了四个标准的meta-annotation类型,他们被用来提供对其他注解类型声明。

  • @Target
    这个注解的作用主要是用来描述注解的使用范围,说白了就是我们自己定义的注解可以使用在哪个地方。
所修饰范围 取值ElementType
package 包 PACKAGE
类、接口、枚举、Annotation类型 TYPE
类型成员(方法,构造方法,成员变量,枚举值) CONSTRUCTOR:用于描述构造器。FIELD:用于描述域。METHOD:用于描述方法
方法参数和本地变量 LOCAL_VARIABLE:用于描述局部变量。PARAMETER:用于描述参数

在这里插入图片描述
我们自定义一个注解,在声明元注解时可以看到提供我们的所有常量。我们以ElementType.METHOD为例。

@Target(ElementType.METHOD)
public @interface TestAnnotation {

	//参数默认为空
    String value() default "";
    
}

我们在来测试一下这个注解
在这里插入图片描述
结果显而易见,当我们将注解放在我们的变量时,编译器给我们报了一个错,翻译过来的意思就是这个注解不被允许放在这里。而放在方法上就可以安静的放在那里。

  • @Retention
    这个注解的作用就是我们需要告诉编译器我们需要在什么级别保存该注释信息,用于描述注解的生命周期。
取值RetentionPolicy 作用
SOURCE 在源文件中有效(即源文件保留)
CLASS 在class文件中有效(即class保留)
RUNTIME 在运行时有效(即运行时保留)注:为RUNTIME时可以被反射机制所读取
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {

	//参数默认为空
    String value() default "";
    
}

在一般情况下我们使用RUNTIME即可。这样在程序运行时我们也可以通过反射机制来读取到该注解。

  • @Document
  • @Inherited

上面俩个注解我们使用的就不算很多了,大家有兴趣可以自行百度一下,与生成文档树有关好像。
我们一会可以通过反射机制读取到我们的注解。

二、反射(Reflect)

1.什么是反射?
反射指的是我们可以在运行期间加载、探知、使用编译期间完全未知的类。是一个动态的机制,允许我们通过字符串来指挥程序实例化,操作属性、调用方法。使得代码提高了灵活性,但是同时也带来了更多的资源开销。
加载完类之后,在堆内存中,就产生了一个 Class 类型的对象(一个 类只有一个 Class 对象),这个对象就包含了完整的类的结构信息。 我们可以通过这个对象看到类的结构。这个对象就像一面镜子,透过 这个镜子看到类的结构,所以,我们形象的称之为:反射。
2.class类
我们在使用反射时,需要先获得我们需要的类,而java.lang.Class这个类必不可少,他十分特殊,用来表示java中类型 (class/interface/enum/annotation/primitive type/void)本身。

  • Class类的对象包含了某个被加载类的结构。一个被加载的类对应一个 Class对象。
  • 当一个class被加载,或当加载器(class loader)的defineClass()被 JVM调用,JVM 便自动产生一个Class 对象。

我们应该如何获取Class类的对象?
我们先创建一个普通的实体类。

package sml.reflect;

public class User {

    //这里name用的是私有类型
    private String name;
    //这里age用的是公有类型
    public int age;

	//无参构造器
    public User(){}
    
    //有参构造器
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
  1. 通过Class.forName()获取(最常用
public class TestReflect {
    public static void main(String[] args) {
        try {
            //获取User的Class对象,参数为需要获取类对象的全类名
           Class aClass = Class.forName("sml.reflect.User");
        //因为是动态编译,所有我们需要抛出类未找到的异常   
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
  1. 通过getClass()获取
public class TestReflect {
    public static void main(String[] args) {
        //new一个user对象
        User user = new User();
        //通过user对象来获取User类对象
        Class aClass = user.getClass();
    }
}
  1. 通过.class获取
public class TestReflect {
    public static void main(String[] args) {
        //通过导包获取类名点class来获取类对象
        Class aClass = User.class;
    }
}

3.反射的基本操作
我们获取到Class对象后,可以获取该类的某些信息。在这里我们来看一些常用的。
1)获取类名

Class aClass = Class.forName("sml.reflect.User");
 //获取全类名
String name = aClass.getName();
 //获取简单的类名
String simpleName = aClass.getSimpleName();

结果:
在这里插入图片描述
2)获取类的字段、某些变量

Class aClass = Class.forName("sml.reflect.User");

//获取该类的所有public字段,包括父类的
Field[] fields = aClass.getFields();
//根据字段名获取该类的public字段
Field field = aClass.getField("age");

//获取该类的所有字段,不包括父类(仅自定义)
Field[] fields1 = aClass.getDeclaredFields();
//根据字段名获取该类的字段
Field field1 = aClass.getDeclaredField("name");

注意:我们仔细看注释,不带Declared的方法职能获取到public字段,且包括父类的,带Declared的方法可以获取到所有的自定义的字段!
测试一下:
在这里插入图片描述
在这里插入图片描述
3)获取类的方法

Class aClass = Class.forName("sml.reflect.User");

//获取该类的所有public方法,包括父类的
Method[] methods = aClass.getMethods();
//根据方法名获取该类的public方法
Method method = aClass.getMethod("getName");
//如果该类为重写方法,可以在第二个参数加上重写方法的参数类型,不写为无参数的方法
Method paramMethod = aClass.getMethod("getName",String.class)

//获取该类的所有方法,不包括父类(仅自定义)
Method[] declaredMethods = aClass.getDeclaredMethods();
//根据方法名获取该类的方法
Method declaredMethod = aClass.getDeclaredMethod("getName");

注:获取方法的方式与获取字段的方法一样,在这里我们需要注意的是重写的方法,一个类中存在俩个或多个方法名是一样的,因此在根据方法名获取方法时,提供第二个参数,为可变参数。参数类型为我们获取方法的参数类型的类,如果不写默认为无参方法。

4)获取类的构造器

 Class aClass = Class.forName("sml.reflect.User");

//获取该类的所有构造器,包括父类
Constructor[] constructors = aClass.getConstructors();
//根据构造器的参数类型来获取指定构造器,不写为无参构造器
Constructor constructor = aClass.getConstructor();
Constructor constructor1 = aClass.getConstructor(String.class,int.class);

//获取该类的所有构造器,不包括父类
Constructor[] declaredConstructors = aClass.getDeclaredConstructors();
//根据构造器的参数类型来获取指定的自定义构造器,不写为无参构造器
Constructor declaredConstructor = aClass.getDeclaredConstructor();
Constructor declaredConstructor1 = aClass.getDeclaredConstructor(String.class, int.class);

注:在我们获取类构造器和类方法时涉及到可变参数的知识,大家可以自行百度一下,或者查阅官方文档,也不难,就是在我们不确定参数有几个时,就可以写成可变参数,我们在使用时可以传多个参数。注意我们写的参数都为类对象!

我们获取到构造器第一想法应该是实例化对象。

5)类的实例化

Class aClass = Class.forName("sml.reflect.User");

//通过class类直接实例化,使用的是User类的无参构造器
User user = (User) aClass.newInstance();
            
//获取构造器来进行实例化,这里获取有参构造器
Constructor declaredConstructor = aClass.getDeclaredConstructor(String.class, int.class);
//根据构造器进行实例化
User user1 = (User) declaredConstructor.newInstance("sml",18);

注:我们在使用类对象直接实例化时,一定要确保需实例化的类中存在无参构造器,否则会报错。默认获取的是Object类型,因此最后需要进行下类型转化。
测试:我们在User类中重写toString方法打印下获取的对象看一下
在这里插入图片描述
我们获取到对象是不是该通过获取的对象调用方法,NO!我们通过反射来调用对象的方法。
6)方法的调用

Class aClass = Class.forName("sml.reflect.User");

User user = (User) aClass.newInstance();
//获取setName方法
Method setName = aClass.getDeclaredMethod("setName", String.class);
//通过获取的方法来调用(invoke),invoke方法有俩个参数
//第一个是调用底层方法的对象,也就是通过哪个对象来调用方法
//第二个为可变参数,是用于方法调用的参数
setName.invoke(user,"sml");

测试:我们打印下该对象看一下方法执行了没有
在这里插入图片描述
注:如果我们调用的方法为私有方法,虽然编译器通过,在运行时会报错的(java.lang.IllegalAccessException),这是因为java的安全检查。我们可以使用setAccessible(true)这个方法来跳过安全检查。

Class aClass = Class.forName("sml.reflect.User");

User user = (User) aClass.newInstance();
Method setName = aClass.getDeclaredMethod("setName", String.class);
//若setName为私有方法,跳过安全检查
setName.setAccessible(true);
setName.invoke(user,"sml");

我们在写程序时一般通过getset方法来操作字段,下面我们同样也是通过反射来操作字段。
7)字段的操作

Class aClass = Class.forName("sml.reflect.User");

User user = (User) aClass.newInstance();
//获取name字段
Field name = aClass.getDeclaredField("name");
//通过该字段的set方法来改变该字段的值,该字段有俩个参数
//第一个为应该修改其字段的参数
//第二个为被修改字段的新值
name.set(user,"sml");

测试:我们打印下该对象,看一下name字段改变没有
在这里插入图片描述
呀,报错了。看这个错眼熟不,我们在上面说过,这是因为我们的name字段为私有,我们不能直接去操作该字段,需要跳过安全检查,我们加上name.setAccessible(true);再来运行一下。
在这里插入图片描述

这还不够,反射很强,我们说过反射的对象像一面镜子,我们能看到的东西都可以获取,我们下面来读取一下参数泛型,返回值泛型!
8)泛型的操作(Generic)

对于泛型我们应该不会陌生,java采用泛型擦除的机制来引入泛型。也就是说java的泛型仅仅是给编译器javac使用的,确保数据的安全性和免去强制类型转换的麻烦。但是一旦编译完成,所有和泛型有关的数据全部擦除。
为了通过反射操作这些类型以迎合实际开发的需要,Java就新增了ParameterizedTypeGenericArrayTypeTypeVariableWildcardType几种类型来代表不能被归一到Class 类中的类型但是又和原始类型齐名的类型,这四种类型实现了Type接口。

类型 含义
ParameterizedType 参数化类型,带有类型参数的类型,即常说的泛型,如:List《T》
TypeVariable 类型变量,如参数化类型Map《E,Y》中的Y、K等类型变量,表示泛指任何类
GenericArrayType (泛型)数组类型,比如List《T》[],T[]这种。注意,这不是我们说的一般数组,而是表示一种【元素类型是参数化类型或者类型变量的】数组类型
WildcardType 代表通配符表达式,或泛型表达式,比如【?】【? super T】【? extends T】。虽然WildcardType是Type的一个子接口,但并不是Java类型中的一种

我们演示一下反射读取ParameterizedType类型。

public class TestReflect {

    //测试方法,返回类型与参数都为泛型
    public static Map<String,User> GenericityTest(List<User> list,User user){
        return null;
    }

    public static void main(String[] args) {
        try {
            //先获取到该类
            Class aClass = Class.forName("sml.reflect.TestReflect");
            //获取到测试方法
            Method genericityTest = aClass.getDeclaredMethod("GenericityTest", List.class,User.class);
            //获取到类型参数数组,就是获取方法所有的参数类型
            Type[] genericParameterTypes = genericityTest.getGenericParameterTypes();
            for (Type genericParameterType : genericParameterTypes) {
                //输出一下类型参数
                System.out.println(genericParameterType);
                //我们在循环时判断该参数类型,若该参数属于参数化类型
                if(genericParameterType instanceof ParameterizedType){
                    //若属于参数化类型,则获取类型对象的数组,表示此类型的实际类型参数
                    Type[] actualTypeArguments = ((ParameterizedType) genericParameterType).getActualTypeArguments();
                    for (Type actualTypeArgument : actualTypeArguments) {
                        //打印下实际类型参数
                        System.out.println(actualTypeArgument);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

我们看着可能会很复杂,我们来分析一下。
首先,genericityTest这个为我们获取的方法,我们通过genericityTest来获取该方法的参数,注意看,这里返回的是Type类型,也就是所有类型的父接口,我们打印下看,就是返回的List与User,也就是他的俩个参数List《User》,User
在这里插入图片描述
接下来我们遍历下类型Type参数数组,我们现在需要的是读取参数化类型,那么我们对每一个Type参数进行判断,如果该Type参数属于ParameterizedType参数化类型,那么我们在获取到该泛型的实际类型参数,也就是List中的User注意,只有List《User》才属于参数化类型,可以查看上面的表
在这里插入图片描述
注:我们在上面演示的只是获取方法的参数,那么我们如何获取返回值的类型?下面第二个方法

//获取方法所有的参数类型
Type[] genericParameterTypes = genericityTest.getGenericParameterTypes();
//获取返回值的参数类型,返回值只有一个,所有不是数组
Type genericReturnType = genericityTest.getGenericReturnType();

9)注解的操作
注解的操作相对就比较简单了,如果我们想读取类上、方法上或字段上的注解,我们仅需要获取到你需要读取的注解所修辞的类、方法或字段来获取就可以。

我们以获取方法上的注解来测试一下:
这里我们还是使用我们在文章首部创建的TestAnnotation注解,只能放在方法上,保留到运行时。

public class AnnotationTest {

    @TestAnnotation("sml")
    public static void main(String[] args) {

        try {
            Class aClass = Class.forName("sml.annotation.AnnotationTest");
            Method main = aClass.getDeclaredMethod("main",String[].class);
            //根据我们的main方法获取main方法上的注解
            TestAnnotation declaredAnnotation = main.getDeclaredAnnotation(TestAnnotation.class);
            //获取到他的值
            String value = declaredAnnotation.value();
            System.out.println(value);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

注:如果我们需要获取类上的注解,只需要获取到类对象,然后.getDeclaredAnnotation()即可,其实不管是获取类上的注解还是字段上的注解都是一样的方法,关于有无Declared的方法,看到这里大家应该也有了解了。

最后说一下,到这里我们可能没有体会到注解的太大作用,在后面的文章我会写一篇手写SpringMVC框架的博客,当然只是简单到不能简单的版本。如果将这篇文章看会,在自己写一下,关于注解与反射我相信大家应该都会的差不多了。

发布了11 篇原创文章 · 获赞 12 · 访问量 1544

猜你喜欢

转载自blog.csdn.net/weixin_45056780/article/details/105127722