Java中的注解与反射与动态反射

注解

从一开始学习Java,我们就一直听到注解(Annotation),那么注解是什么呢?注解和注释是有点关系的,都有个注字,都能用来描述程序。

  • 注释:用文字描述程序的,给程序员看的
  • 注解:用于说明程序的,程序员既能看懂,机器也能解析。注解其实是一个接口,与类同等级,但注解并非是程序一部分,我们可以理解为标签。

注解格式是@注释名(参数值),注解不仅仅只能用来描述程序,也可以被程序读取,对程序做出规定的约束或者其他功能。

1.内置注解

内置注解指的是JDK帮我们编写好了的注解,这些经常用于我们的日常使用。大致有以下三个:

  • @override :检测被注解标注的方法是否继承自父接口,当然也会对方法进行检测,看是否重写成功(检查方法名、参数类型等)
@Override 
public String toString() {
    return "Test03{}";
  }
  • @Deprecated: 该注解标注的内容不推荐使用,可能存在风险或者已经过时
@Deprecated  
public static void test() {
    System.out.println("Deprecated");
 }
  • @suppressWarings :压制警告,参数值是一个枚举数组,表示这个注解用在什么地方(类、接口、方法等)
@SuppressWarnings("all") 
public void test2() {
    ArrayList arrayList = new ArrayList();
}

public static void main(String[] args) {
    test();
}

2.元注解

对于普通注解,我们是用来描述程序的,那么对于元注解顾名思义,元注解就是用来描述注解的注解,从注解的底层源码可以看出:

在这里插入图片描述
这个是@Override的注解源码,其中里面利用到了@Target@Retention这两个元注解。元注解十分重要,学习这个也有利于我们自己自定义注解。下面讲解一下元注解:

  • @Target:表示了注解作用的地方,比如类、方法、成员变量、接口等,参数是一个枚举类型的数组.
  • @Retention:表示需要在什么级别保存改注解,描述了注解的生命周期(SOURCE < CLASS < RUNTIME),SOURCE表示只能存在于源代码时期,CLASS表示能存活到于字节码文件,RUNTIME表示能存活到运行时期,一般来说都是RUNTIME
  • @Documented:说明改注解将包含在javadoc中
  • @Inherited:表示子类能继承父类的该注解

3.自定义注解

如果我们要手造框架的话,自定义注解就必不可少,框架很多功能其实就是利用了注解和反射来实现的。而自定义注解,就又要用到我们上面提到的元注解,自定义注解格式大致如下:

元注解
public @interface 注解名{
	//注解的参数:参数类型+参数名+圆括号
	E 参数名();
}

以上,便定义了一个注解。看起来很简单对吧,但还是有一些小细节需要注意:

  • 注解中定义变量的写法和Java普通写法有点不同,是以圆括号结尾,有点类似于方法的写法了。
  • 如果注解定了变量,那我们使用必须赋值,但变量定义时如果有使用default给予了默认值,那就不用赋值。
  • 如果注解里只有一个变量,且名为value,则赋值时可以不用写变量名。
public class Test {
    //注解可以显示赋值,如果没有默认值,我们就必须给注解赋值
    @MyAnnotation2(name = "陈平安",schools = "清华")
    public void test() {
    }
    
    @My3("宁姚")
    public void test2() {
    }
}

@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@interface  MyAnnotation2{
    //注解的参数:参数类型+参数名
    String name() default ""; //加了默认值
    int age() default 0;
    int id() default -1;   //如果默认值为-1.则不存在
    String[] schools() default {"清华","北大"};
}

@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@interface My3{
    String value();  //只有一个变量,且名为value
}

以上便是注解的全部内容了,注解的内容其实十分简单。但也许你也会很奇怪,怎么注解看起来这么简单,却有那么强的功能,这个其实就是要通过和反射一起结合使用,注解反射天生一对,离开了反射,注解什么都算不是。下面来讲解下反射。


反射

1.知识补充:动态语言和静态语言

在学习反射前,先进行一些知识补充:语言的精动态,学习这些将有利于我们理解反射。

  • 静态语言,代表:Java、C、C++
    静态语言就是在编译阶段进行类型检查,当编写源程序的时候,出现不符合语法的规范,就会提示错误,在编译时变量的数据类型即可确定。也即运行时结构不可变的语言。
int a = 123; // a是整数类型变量
a = "mooc"; // 错误:不能把字符串赋给整型变量
  • 动态语言,代表:JS、PHP
    所谓动态语言,就是类型检查是在运行的时候做的,变量使用之前不需要类型声明,通常变量的类型是就是被赋值的那个值的类型。运行时期机构可变。

    例如JavaScript等这种脚本语言就是动态语言,在编译阶段它不会判断代码是否符合规范,在运行的时候才会去判断。

a = 123    # a是整数
print a
a = 'imooc'   # a变为字符串
print a

如果我们对这两种语言有一些了解,就很容易了解到它们的区别,例如JS相较于Java,语法十分不规范十分宽松。那这个和反射有什么关系呢?

Java不是动态语言,但Java可以称之为“准动态语言”。即Java有一定的动态性,我们可以利用反射机制获得类似动态语言的特性。Java的动态性让编程的时候更加灵活。

2.反射概述

1)第一个问题:反射是什么?

其实对于理解反射是什么这一个问题,最好是深入了解Java的编译过程也就是JVM的相关知识之后,才能对反射有一个较好的理解。但也不妨影响我这个弱鸡对他的理解。

在这里插入图片描述

对于一个类的正常生命流程,是编译期→运行期这样一个顺序。然而Java给我们提供了一个特殊的机制,在编译期时(上图第一个阶段加载完成后)会把我们的源代码转换成.class文件并在内存中生成一个Class对象,我们的反射便是借助这一个Class对象来实现的。

反射,我觉得最重要的是这里面的那个“反”字,说明有什么东西反过来了。如下图,我们正常编程的逻辑就是按箭头的顺序执行的。但假如我们是反射,就是反过来,就是从1到2这样的逻辑,我们是通过操作我们编写的类生成的Class对象来获取类的信息或者操作类。在这里插入图片描述
在这里插入图片描述
总结一下,正常编程:类→正常执行,反射:类→class对象→类→执行,也就是说从类或对象中推导出Class类,然后再从Class类中获得类的信息或对类进行操作,这一行为就是反射。

2)第二个问题:反射能干什么?

上面我们曾提到Java是一门“准动态”语言,反射提供了动态的特性。同时它也是我们框架中的一个很重要的组成部分,反射是框架设计的灵魂。

class是一切反射的根源,JAVA反射机制是在运行状态中,对于任何一个类,通过反射都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性。

这样说可能也不能让你了解到反射有什么作用,下面看看反射的应用就知道了。

3.反射应用

1.获取类的Class对象

注意:每一个类只有一个class对象!!!

//1.通过对象获得
Class c1 = person.getClass();
System.out.println(c1.hashCode());

//2.forName获得
Class c2 = Class.forName("com.chy.reflection.Test");
System.out.println(c2.hashCode());

//3.通过类名.class获得
Class c3 = Stu.class;
System.out.println(c3.hashCode());

//4. 基本内置类型的包装类都有一个Type属性
Class c4 = Integer.TYPE;
System.out.println(c4.hashCode());

//获得父类类型
Class c5 = c1.getSuperclass();
System.out.println(c5);
        

当然也不仅仅只有类才有class对象,对于基本类型、数组、接口等也有class对象。对于数组来说,只要数组类型和维度一样(与长度无关),它们都是同一个class对象。

Class c1 = Object.class;  //类
Class c2 = Comparable.class;  //接口
Class c3 = String[].class;   //一维数组
Class c4 = int[][].class;   //二维数组
Class c5 = Override.class;  //注解
Class c6 = ElementType.class;  //枚举
Class c7 = Integer.class;   //基本数据类型
Class c8 = void.class;  //void
Class c9 = Class.class;  //Class

2.利用class对象创建实例

  • Object newInstance():创建实例化对象
  • Constructor getDeclaredConstructor(Class ....args):获取指定参数的构造器
/*
	使用.newInstance()方法
	前提:
		1.类中必须有一个无参构造器
		2.构造器的访问权限必须足够
*/
	Class c = Test.class;
	Test test = (Test)c.newInstance();


/*
	通过获取构造器来创造实例化对象
	注意:
		1.此时就可以无视构造器的访问权限
		2.不一定要有无参构造器
		3.可以创造指定参数的构造器
*/
	Class c = Test.class;
	Constructor con = c.getDeclaredConstructor(String.class,int.class);
	Test test = (Test)con.newInstance("chy",18);

3.利用class对象调用方法

  • Method getDeclaredMethod(String 方法名,Class 参数的class对象):获取指定的方法
  • void setAccessible(boolean flag):启动和禁用访问安全的开关,默认为false,但传参为true时,就可以调用private方法。
  • Object invoke(Object obj,Object ...args):激活方法,第一个参数是要调用该方法的对象,其他是参数
class user{
private String name;

public String getName() {
            return name;
        }

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

Class c = user.class;
Method setName = c.getDeclaredMethod("setName", String.class);
Method getName = c.getDeclaredMethod("getName");
setName.setAccessible(true);

setName.invoke(user3,"chy");
getName.invoke(user3);

注意:一般来说setAccessible不宜为true,因为不安全。不过一旦开启这个禁用开关,则可以提高性能,对于使用频繁的方法,应该打开这个开关。

4.利用class对象获取类的信息

  • Field getField(String name):获取类的属性,只能获取public
  • Field getDeclaredField(String name):获取类的属性,包括private
  • String getName():获取类的名字(全类名)
  • String getSimpleName():获取类的简单名字
		//获取类的名字
        System.out.println(c1.getName());  //获得包名+类名
        System.out.println(c1.getSimpleName());  //获得类名

        //获得类的属性
		Field field = c1.getField("name");
		Field field = c1.getDeclaredField("name");

        Field[] fields = c1.getFields();  //只能找到public属性

        fields = c1.getDeclaredFields();  //找到全部属性
        for (Field field :fields) {
            System.out.println(field);
        }

5.利用class对象操作泛型和利用class对象操作注解

这里偷懒一下,博客上有很多的,这个其实没什么特殊的,和上面的操作都是类似的。以后有空再来补。


代理模式

相信大家都对代理模式不陌生,这里为什么会涉及到代理模式呢,因为动态代理的原理就是用到了反射,所以这里也简析一下代理模式。

代理模式是什么呢?比如我们有两个类,一个实现某种功能简称功能类,一个通过调用功能类来满足自身目的检查客户类,原来客户类和功能类是直接交互的,客户类→功能类

现在我们使用代理模式,就再多出一个类,代理类,它横插在客户类和功能类之间,是他们必须通过代理类来交互客户类→代理类→功能类。这时很多人就可能会不理解了,说你这个不是多此一举吗,直接交互不好吗,还要引入一个新的类。

确实,我们平时确实不一定要用到这个模式,用这个确实是多次一举。但在某些场合的时候,使用这个代理模式就是一个极佳的 punchline 了。

下面举个例子:比如我们的WEB项目,现在需要增加一个日志功能,在我们每次调用方法的时候把它记录下来,那怎么办呢?在原来的代码上改咯?不,不能这么做,这么做不够谨慎,有可能改着改着原来代码无法运行了。这时候代理模式就来了,我们只需新建一个代理类,这个代理类不仅能保有原来的功能,也在这个基础上增加了日志功能,专业术语上这个过程叫做增强。

使用代理模式就很好地降低了耦合度,将功能分离开来。其实这个也是横向开发思想的体现。

在这里插入图片描述

代理模式有两种:静态代理和动态代理。
在这里插入图片描述
首先角色分析

  • 抽象角色:一般是接口或者抽象类,也就是这里面的租房这个功能
  • 真实角色:被代理的角色,相当于房东
  • 代理角色:代理类,一般会添加一些附属操作来实现增强,相当于这里面的中介
  • 客户:访问代理对象的类,相当于这里面的租客

1.静态代理

这里就不具体展示了,主要讲一下静态代理是什么,用代码展示一下。

//租房,抽象角色
public interface Rent {
    public void rent();
}


//房东,真实角色
class Host implements Rent{
    @Override
    public void rent() {
        System.out.println("房东卖房子...");
    }
}

//代理角色,中介
class Proxy implements Rent{
    private Host host;

    public void setHost(Host host) {
        this.host = host;
    }

    @Override
    public void rent() {
        fare();		//多了一个中介方法,可以类比成我们项目中要多添加的功能
        host.rent();
    }
    
    public void fare(){
        System.out.println("交中介费");
    }
}

//客户角色,租客
class Client{
    public static void main(String[] args) {
        Host host = new Host();
        Proxy proxy = new Proxy();
        proxy.setHost(host);
        proxy.rent();
    }
}

静态代理一般很少用,较多采用的是动态代理。静态代理其实就是我们真正去写出一个代理类,不过弊端也是很明显的,如果有n个真实角色,那就要写n个代理类,太麻烦了。

众所周知,程序猿是一种懒惰的动物。

2.动态代理

相较于静态代理,动态代理就很方便,不过这个很绕很绕。首先要了解以下这个类:

  • InvocationHandler:这个叫做调用处理程序实现的接口,每一个代理角色都有一个对应的调用处理程序,看不懂没关系,只需要知道我们是通过它的一个方法来获取代理对象(Proxy对象),然后复写这个接口里面的invoke方法。

    方法一:newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h),第一个参数是类加载器,第二个参数是要代理的接口的class对象,第三个参数就是要一个InvocationHandler对象。这个方法就会返回一个Proxy对象。

    方法二:invoke,这就是我们的代理逻辑,我们要让代理对象做什么增强都需要做什么。这个方法比较复杂,我讲不明白,大家看代码理解吧。注意:每次我们调用代理对象操作时,就会自动调用这个方法。

不懂没关系,通过例子多加理解便可。对于静态代理中的那个例子进行改造,变成动态代理,代码如下:

//租房,抽象角色
public interface Rent {
    public void rent();
}

//房东,真实角色
class Host implements Rent{

    @Override
    public void rent() {
        System.out.println("房东卖房子...");
    }
}

//代理角色,中介
class ProxyInvocationHandler implements InvocationHandler {
    private Rent rent;

    public void setRent(Rent rent) {
        this.rent = rent;
    }

	//获取代理对象
    public Object getProxy(){
        return Proxy.newProxyInstance(this.getClass().getClassLoader(),rent.getClass().getInterfaces(),this);
    }

	//附加方法
    public void fare(){
        System.out.println("交中介费");
    }

    @Override
    public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
    	//主要代理逻辑
    	//这个method指的是真实对象里的方法,比如rent方法
        fare();
        Object result = method.invoke(rent,objects);
        return result;
    }
}

//客户角色,租客
class Client{
    public static void main(String[] args) {
        Host host = new Host();
        ProxyInvocationHandler proxyInvocationHandler = new ProxyInvocationHandler();
        //这里利用到了多态
        proxyInvocationHandler.setRent(host);
        //获取代理对象
        Rent proxy = (Rent)proxyInvocationHandler.getProxy();
        //这里虽然只是调用了rent方法,但是仍然会调用交中介费的方法
        proxy.rent();
    }
}

以上就是动态代理的例子,其实我们可以直接记住这个格式,实际应用时改一下一些细节即可。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值