Java中的注解与反射

目录

注解:

1、什么是注解

2、元注解

3、如何使用注解

4、注解级别应用场景

反射:

反射获取对象实例

反射操作对象字段

反射调用对象方法

注解+反射模式的优势与弊端

优势:

弊端:

总结:


注解:

1、什么是注解

注解本身是一个比较好理解也比较容易学习的东西,但是和一些其他的Java机制结合起来会变得复杂且功能性强大。在普通的日常编程中其实自己使用注解进行开发的情况并不多,注解长什么样呢?

@Override

这个东西想必大家并不陌生吧,这就是一个注解,只不过已经是由Oracle公司开发好的。那么这个注解有什么用呢,单独来说Override这个注解是用来检查父类是否存在这个原方法,不存在则编译错误。

我们进入到Override的源码来看一下:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

我们发现是这么一个玩意儿,  这啥啊,这明明啥也没写啊。他是咋能实现这么伟大的功能的。这个注解的详细原理我们待会儿再说。我们先知道其实注解原本就长这样。

我们定义注解的代码就是这么简单:

public @interface Pillow {
}

我甚至直接这样定义也没有问题,这样的注解我就已经可以使用了。这就是注解的定义方式。

2、元注解

我们发现Override注解和我们自定义的注解上有一些不同,怎么Override注解头上还有注解。其实不止是这样,我们进入@Target注解看一下:

Target注解定义头上居然还有一个Target,什么情况,这是在玩套娃吗。

其实这些注解上的注解就是元注解,元注解是用于定义在自定义类型注解上的注解,其中比较重要的两个元注解就是@Retention 和 @Target

先说@Target, 这个元注解的意思是想要将你自定义的注解应用到哪些地方:类上、方法上、参数上、注解上等等,其中应用于注解上的ElementType.ANNOTATION_TYPE就让自定义注解变为了元注解,这种属性可以添加多个,用{}括起来即可。

再说@Retention,这个元注解的意思是你想要将自定义注解保留在哪个阶段,一共有三种:源码级别(SOURCE)、编译级别(CLASS)、运行时级别(RUNTIME),级别逐级递增,大的级别包含小级别时期,比如运行时级别在源码和编译阶段都存在。来做一个练习:

分析@Override

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

答:作用在方法上且保留源码级别

3、如何使用注解

使用注解的方式非常简单,只需在想添加注解的地方加注解就可以了,注意所添加的注解要符合元注解定义。

我们在定义注解的时候,可以给注解添加属性,这些属性必须是基本数据类型、String、Class、枚举类型、注解类型或这些类型的数组,并且可以添加默认值。注意添加属性的时候有()别漏掉。

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Pillow {
    String value() default "xxx"; // 默认值为xxx
    int id() default 0; // 注意不能用Integer
}

这里定义属性名也有讲究,如果定义的属性名有一个叫value() 那么注意了:

一般使用注解是这样的 @Pillow(value = "run", id = 666)

而如果只有一个value值需要指定的话,就可以这么简写 @Pillow("run"),也就是这么写默认是给value指定值,但如果有两个及以上的值必须要指定的时候就不能简写。

4、注解级别应用场景

刚才我们讲了Rentention来指明注解级别,下面讲解不同级别的应用场景,最主要部分是运行时与反射联合操作的情况

源码级别:源码级别因为其核心是源码,大多应用场景是语法检查与编码,比如@Override就提供了父类检查机制,其原理是编译器内置了对这个注解的处理,就像是定义int a = "123"会编译异常一样。在Lombok中@Data注解自动提供getter和setter等方法,它也是在源码级别,原理是编译期的源码增强技术。

字节码级别:字节码级别是在编译后操作字节码文件,所以多用于字节码增强技术。AOP面向切面编程就是典型的字节码技术通过描字节码中的注解,根据注解信息对类进行修改(如添加代理逻辑、插入日志代码等),但运行时无需保留注解信息。

运行时级别:这也是我们在定义的时候用的最多的一个阶段,其主要是通过编译后通过反射拿到对应注解信息,再对相应的目标进行操作。如果没学过反射,蒙圈是正常的,往下会细讲反射就会慢慢理解。

反射:

反射是 Java 中一种强大的机制,允许程序在运行时获取类的信息、操作类的属性和方法,而无需在编译期知道类的具体信息。简单来说,反射让程序能够 “看透” 类的内部结构,并动态操作它们。

反射都是基于class的,只要拿到一个类的class,就可以知道他里边的全部信息,甚至修改。

下面是反射的常用方法

反射获取对象实例

自定义一个Student类

package com.software.demo18;

public class Student {
    private String name;
    private int age;

    public Student() {
    }

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

    private void study(String other) {
        System.out.println(name + "正在学习," + other + "别来沾边!!");
    }

    public String getName() {
        return name;
    }
}

四种方式获取类对象,一般最常用第三种:

        // 1. 通过类名.class获取(编译期已知类)
        Class<Student> cls1 = Student.class;

        // 2. 通过对象.getClass()获取(已知实例)
        Student student = new Student();
        Class<? extends Student> cls2 = student.getClass();

        // 3. 通过Class.forName("全类名")获取(运行时动态加载,最常用)
        Class<?> cls3 = Class.forName("com.software.demo18.Student");

        // 4. 通过类加载器获取
        ClassLoader classLoader = Run.class.getClassLoader();
        Class<?> cls4 = classLoader.loadClass("com.software.demo18.Student");

 获取Student对象实例:

        // 通过构造器获取对象实例
        Constructor<?> constructor = cls1.getConstructor(String.class, int.class);
        Student student2 = (Student) constructor.newInstance("张三", 18);
        System.out.println(student2.name);

反射操作对象字段

我们直接实例化一个Student:

Student student = new Student("李四",21);

在上面我们已经创建好了类对象,这里我们用cls3来演示,这里要说明一下,我们只是拿到了类对象,就只是拿到了类而已。

下面演示如何把李四这个学生的姓名改成张三,这里还要提前说一点,在我们定义的Student类中,只有一个name的get方法,并且属性值都是私有的。那么如何访问私有属性呢?其实这里就用到了反射的特性,可以直接通过操作字节码将私有属性改为共有的就能访问了。方法为setAccessible(true)。

        Student student = new Student("李四",21);

        Class<?> cls3 = Class.forName("com.software.demo18.Student");

        Field name = cls3.getDeclaredField("name");
        name.setAccessible(true); // 修改访问权限
        name.set(student,"张三"); // 传入实例对象和修改后的内容

        System.out.println(student.getName()); // 这里的输出就变成张三了

还有一个问题需要注意,我们虽然修改了访问权限,但是在编码的时候直接访问。原因是反射是通过动态修改,他是在运行时将权限修改,而在源码时期该权限并未修改。

反射调用对象方法

我将调用Student的study方法:

我们还可以通过反射去调对象相应的方法,实现如下:

这里我们还是用李四的实例。

        Student student = new Student("李四",21);

        Class<?> cls3 = Class.forName("com.software.demo18.Student");


        // 第一个参数为方法名,第二个参数以后就是要传入的形参
        Method method = cls3.getDeclaredMethod("study", String.class);                                 
        method.setAccessible(true); // 防止权限不足

        method.invoke(student,"张三"); // 输出"李四正在学习,张三别来沾边!!"

由此我们得出,我们可以通过反射动态的调用对象的任意方法,即使他是私有的。

通过这几个例子,我们初步的了解到了反射的作用,当然我只是演示了反射比较典型的例子,他还有更加强大的功能,详细都在最开始的树形图中。

注解+反射模式的优势与弊端

优势:

提高代码灵活性与动态性

注解定义规则,反射在运行时动态解析,无需硬编码逻辑。

简化配置与逻辑

传统开发中,配置(如 XML)与代码分离,需要手动解析配置。注解 + 反射将配置嵌入代码,反射自动处理。。

支持动态行为定制

反射可以在运行时根据注解信息动态调整逻辑。

弊端:

增大性能开销

通过演示我们发现,反射通过查找类的属性。即使没找到,每一次调用反射的时候也会遍历整个文件,这无疑提高了性能的开销,据统计通过反射调用方法一般比直接调用慢10~100倍。 

绕过编译期检查,提高风险

演示中,我们直接修改了访问权限,调用了私有方法,这就增加了代码风险。

总结:

注解和反射是Java的高级语言特性,他们结合在一起特别在一些框架的开发中起到了举足轻重的作用,在面试中也难免作为面试内容出现。使用其便捷的同时也要注意到性能和安全方面的问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值