3

Java反射详解篇--一篇入魂 - 渊渟岳

 2 years ago
source link: https://www.cnblogs.com/dennyLee2025/p/16056246.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

1.反射概述

Java程序在运行时操作类中的属性和方法的机制,称为反射机制。

一个关键点:运行时

一般我们在开发程序时,都知道自己具体用了什么类,直接创建使用即可。但当你写一些通用的功能时没办法在编写时知道具体的类型,并且程序跑起来还会有多种类型的可能,则需要在运行时动态的去调用某个类的属性和方法,这就必须使用反射来实现。

例子说明:

Father f = new Children();

编译时变量f 为Father类型,运行时为Children类型;

public void demo(Object obj){
 // 不知道调用者传什么具体对象
    ……
}

编译时demo方法参数类型为Object,一般有两种做法

第一种做法是知道参数类型有哪几种情况,可以使用instanceof运算符进行判断,再利用强制类型转换将其转换成其运行时类型的变量即可。

第二种做法是编译时根本无法预知该对象和类可能属于哪些类,程序只依靠运行时信息动态的来发现该对象和类的真实信息,这就必须使用反射。

那反射是怎么做到在运行时获取类的属性和方法的呢?

理解类的加载机制的应该知道,当java文件编译成.class文件,再被加载进入内存之后,JVM自动生成一个唯一对应的Class对象,这个Class是一个具体的类,这个Class类就是反射学习的重点。反射的操作对象就是这个Class类,通过Class类来获取具体类的属性和方法。

2.Class类

Class 类是用于保存类或接口属性和方法信息的类,就是保存类信息的类,它类名称就叫 Class。

2.1.理解Class类

Class类和构造方法源码

public final class Class<T> implements java.io.Serializable,
                              GenericDeclaration,
                              Type,
                              AnnotatedElement {
 private final ClassLoader classLoader;
 
 private Class(ClassLoader loader) {
  classLoader = loader;
    }

 ……
}

简单分析下Class类

  1. Class类和String类都是被final关键字修饰的类,是不可以被继承的类;
  2. Class类支持泛型T,也就是说在编写程序时可以做到:反射 + 泛型;
  3. Class类实现了序列化标记接口Serializable,既是Class类可以被序列化和反序列化;
  4. Class类不能被继承,同时唯一的一个构造器还是私有的,因为设计之初就是让JVM在类加载后传入ClassLoader对象来创建Class对象(每个类或接口对应一个JVM自动生成Class对象),开发人员只是调用Class对象,并没有直接实例化Class的能力。

Class对象的创建是在加载类时由 Java 虚拟机以及通过调用类加载器中的defineClass 方法自动构造的,关于类的加载可以通过继承ClassLoader来实现自定义的类加载器,本文着重讲反射,在此不展开讲类加载相关知识。

2.2.获取Class对象的三种方式

方式一:常用方式,Class.forName("包名.类名")

public static void main(String[] args) {
    // 方式一:全限定类名字符串
    Class<?> childrenClass = null;
    try {
        childrenClass = Class.forName("com.yty.fs.Children"); // 包名.类名
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
    // 获取类名称
    System.out.println("全限定类名="+childrenClass.getName());
}

执行结果:全限定类名=com.yty.fs.Children

方式二:每个类下的静态属性 class,类名.class

public static void main(String[] args) {
    // 方式二:每个类下的静态属性 class
    Class<Children> childrenClass2 = Children.class;
    System.out.println("类名称="+childrenClass.getSimpleName());
}

执行结果:类名称=Children

方式三:每个类最终都继承了Object,Object类下的getClass()

public static void main(String[] args) {
    // 方式三:Object类下的getClass()
    Children children = new Children();
    Class<?> childrenClass3 = children.getClass();
    System.out.println("类所在包="+childrenClass3.getPackage());
}

执行结果:类所在包=package com.yty.fs

三种方式简单对比:

  • 方式一通过全限定类名字符串既可以获取,其他两种方式都要导入类Children才可以;
  • 方式二获取的Class不需要强转即可获得指定类型Class,其他两种方式获得的都是未知类型Class<?>;
  • 方式三通过实例化对象的Object中的方法获取,其他两种都不需要实例化对象。

怎么选:

  • 只有全限定类名字符串,没有具体的类可以导入的只能选方式一;
  • 有具体类导入没有实例化对象的使用方式二;
  • 作为形参使用的使用方式三,通过形参引用来获取Class。

Class类中有非常多的方法,通过案例掌握常用的方法即可。

2.3.案例一:构造方法、成员变量和成员方法的获取和使用

1.构造方法操作

1.1.所有构造方法

1.2.所有public构造方法

1.3.无参构造方法

1.4.单个私有构造方法

2.字段操作(成员变量)

2.1.获取所有成员变量

2.2.获取所有公共成员变量

2.3.获取单个公共成员变量

2.4.获取单个私有成员变量

3.方法操作(成员方法)

3.1.获取所有方法--不会获取父类的方法

3.2.获取所有公共方法--会获取父类的方法

3.3.获取单个公共方法

3.3.1.获取单个公共方法--无参方法

3.3.2.获取单个公共方法--有参方法

3.4.获取单个私有方法

具体看代码

测试类:Children类

public class Children {
    public String testString; //测试用
    private int id;
    private String name;

    // 无参构造方法
    public Children() {
        System.out.println("====无参构成方法被调用");
    }
    // 多个参数构造方法
    public Children(int id, String name) {
        this.id = id;
        this.name = name;
    }
    // default构造方法--测试
    Children(String name, int id){
        this.id = id;
        this.name = name;
    }
    // 受保护构造方法--测试
    protected Children(int id) {
        this.id = id;
    }
    // 私有构造方法--测试
    private Children(String name) {
        this.name = name;
    }
    
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Children{ id=" + id + ", name=" + name + "}";
    }

    public void printName(){
        System.out.println("====printName--"+this.name);
    }
    public void printName(String name){
        this.name = name;
        System.out.println("====printName--"+this.name);
    }
    private void demoTest(){
        System.out.println("====demoTest--执行了");
    }
}

Class类的具体操作

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

/**
 * 案例一:构造方法、成员变量和成员方法的获取和使用
 */public class Demo1 {
    public static void main(String[] args) throws Exception {
        Class<?> chilrenClass = Class.forName("com.yty.fs.Children");

        // 1.构造方法操作
        // 1.1.获取所有构造方法
        System.out.println("1.构造方法操作\n1.1.所有构造方法");
        Constructor<?>[] declaredConstructors = chilrenClass.getDeclaredConstructors();
        for (Constructor constructor : declaredConstructors){
            System.out.println(constructor.toString()); // Constructor类的toString已重写
        }

        // 1.2.获取所有public构造方法
        System.out.println("1.2.所有public构造方法");
        Constructor<?>[] constructors = chilrenClass.getConstructors();
        for(Constructor constructor : constructors){
            System.out.println(constructor.toString());
        }

        // 1.3.获取无参构造方法
        Constructor<?>  onParamConstructor = chilrenClass.getConstructor();//参数类型为null,表示无参
        System.out.println("1.3.无参构造方法:\n"+onParamConstructor.toString());
        // 实例化对象
        Object o = onParamConstructor.newInstance();
        if(o instanceof Children){
            Children children = (Children)o;
            children.setId(111);
            children.setName("myName");
            System.out.println(o.toString());// Children类重写了toString
        }

        // 1.4.获取单个私有构造方法
        // 指定了私有构造方法的参数类型,所以只会获取到一个构造方法
        Constructor<?> privateConstructor = chilrenClass.getDeclaredConstructor(String.class);
        System.out.println("1.4.单个私有构造方法:\n"+privateConstructor.toString());
        //私有构造方法需要取消访问权限检查,否则报异常:IllegalAccessExceptionw
        privateConstructor.setAccessible(true);
        Object obj = privateConstructor.newInstance("myName");
        System.out.println(o.toString());

        // 2.字段操作(成员变量)
        // 2.1.获取所有成员变量
        System.out.println("2.字段操作(成员变量)\n2.1.获取所有成员变量");
        Field[] declaredFields = chilrenClass.getDeclaredFields();
        for (Field declaredField : declaredFields){
            // 获取fieldName
            System.out.println(declaredField.getName());
        }

        // 2.2.获取所有公共成员变量
        System.out.println("2.2.获取所有公共成员变量");
        Field[] fields = chilrenClass.getFields();
        for (Field field : fields){
            // 获取fieldName
            System.out.println(field.getName());
        }

        // 2.3.获取单个公共成员变量
        System.out.println("2.3.获取单个公共成员变量");
        Field field = chilrenClass.getField("testString");
        Object o1 = chilrenClass.getConstructor().newInstance();
        field.set(o1,"yty");
        Object o1_1 = field.get(o1);
        // 获取fieldName
        System.out.println("成员变量名-值:"+field.getName()+"="+o1_1.toString());

        // 2.4.获取单个私有成员变量
        System.out.println("2.4.获取单个私有成员变量");
        Field field2 = chilrenClass.getDeclaredField("name");
        //私有成员变量需要取消访问权限检查,否则报异常:IllegalAccessExceptionw
        field2.setAccessible(true);
        Object o2 = chilrenClass.getConstructor().newInstance();
        field2.set(o2,"myName");
        Object o2_2 = field2.get(o2);
        // 获取fieldName
        System.out.println("成员变量名-值:"+field2.getName()+"="+o2_2.toString());


        // 3.方法操作(成员方法)
        // 3.1.获取所有方法(成员方法)
        System.out.println("3.方法操作(成员方法)\n3.1.获取所有方法--不会获取父类的方法");
        Method[] declaredMethods = chilrenClass.getDeclaredMethods();
        for (Method method : declaredMethods){
            // 获取方法名
            System.out.println(method.getName());
        }

        // 3.2.获取所有公共方法
        System.out.println("3.2.获取所有公共方法--会获取父类的方法");
        Method[] methods = chilrenClass.getMethods();
        for (Method method : methods){
            // 获取方法名
            System.out.println(method.getName());
        }

        // 3.3.获取单个公共方法
        System.out.println("3.3.获取单个公共方法\n3.3.1.获取单个公共方法--无参方法");
        Method printName = chilrenClass.getMethod("printName"); //方法名称
        System.out.println(printName);

        System.out.println("3.3.2.获取单个公共方法--有参方法");
        Method printName2 = chilrenClass.getMethod("printName",String.class); //方法名称,参数类型
        System.out.println("参数个数:"+printName2.getParameterCount());
        // 遍历所有参数信息
        Parameter[] parameters = printName2.getParameters();
        for (int i=0;i<printName2.getParameterCount();i++){
            Parameter param = parameters[i];
            if(param.isNamePresent()){
                System.out.println("第"+ (i+1) +"个参数信息");
                System.out.println("参数类型="+param.getType());
                System.out.println("参数名称="+param.getName());
            }
        }
        // 使用有参方法
        Object o3 = chilrenClass.getConstructor().newInstance();
        printName2.invoke(o3,"myName");//传入参数值、执行方法

        // 3.4.获取单个私有方法
        System.out.println("3.4.获取单个私有方法");
        Method demoTest = chilrenClass.getDeclaredMethod("demoTest");
        // 使用私有无参方法
        Object o4 = chilrenClass.getConstructor().newInstance();
        demoTest.setAccessible(true);
        demoTest.invoke(o4);

    }
}

执行结果:拷贝过去执行就知道了……

2.4.案例二:注解的相关操作

自定义一个测试用的注解

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(value = RetentionPolicy.RUNTIME)@Target(ElementType.FIELD)
public @interface PersonAnnotation {
    String name() default "myName";
}

注解使用类和测试用的main方法

import java.lang.reflect.Field;

public class PersonAnnotationDemo {
    @PersonAnnotation(name = "张三")
    private String name;
    private int age;

    @Override
    public String toString() {
        return "PersonAnnotationDemo{" + "name=" + name +  ", age=" + age + "}";
    }

    public static void main(String[] args) throws Exception {
        Class<?> annotateClass = Class.forName("com.yty.fs.PersonAnnotationDemo");
        Object o = annotateClass.newInstance();
        System.out.println("PersonAnnotationDemo是否是注解类:"+annotateClass.isAnnotation());

        Field[] declaredFields = annotateClass.getDeclaredFields();
        for(Field field : declaredFields){
            // PersonAnnotationDemo类中的成员变量是否有 PersonAnnotation注解
            if (field.isAnnotationPresent(PersonAnnotation.class)){
                // 获取成员变量中 单个PersonAnnotation注解
                PersonAnnotation annotation = field.getAnnotation(PersonAnnotation.class);
                // 获取 多个PersonAnnotation注解
                // PersonAnnotation[] annotationsByType = field.getAnnotationsByType(PersonAnnotation.class);
                /**
                 * 相类似的获取注解方法:getDeclaredAnnotation、getDeclaredAnnotationsByType、getAnnotations、getDeclaredAnnotations
                 */
                // 输出注解中的值
                System.out.println("输出注解中的值:"+field.getName()+"="+annotation.name());
                // 将注解的值 赋值 到PersonAnnotationDemo对象对应字段
                field.setAccessible(true);//私有字段需要忽略修饰符
                field.set(o,annotation.name());
            }
        }
        // 输出:注解的值 赋值给 对象
        System.out.println(o.toString());

    }
}

执行结果:

PersonAnnotationDemo是否是注解类:false

输出注解中的值:name=张三

PersonAnnotationDemo{name=张三, age=0}

3.反射的应用

常用于框架底层开发

3.1.实战一:通过配置文件解耦类和反射

Spring 框架通过将成员变量值以及依赖对象等信息都放在配置文件中进行管理的,类发生改变时只需要更新配置文件,对于反射模块则无需更改,从而实现了较好的解耦。

测试类

public class BigBanana {

    public void printBigBanana(String color){
        System.out.println("Do you like "+color+" BigBanana?");
    }

}

配置文件信息

bigbanana.class.name=com.yty.fs.BigBanana
bigbanana.class.method=printBigBanana
bigbanana.class.method.param=java.lang.String

反射测试类

public class TestBigBanana {
    private static Properties properties;
    // 通过Key 获取配置文件value 值
    public static String getProperty(String key) throws IOException {
        if (properties == null){
            properties = new Properties();
            FileReader fileReader = new FileReader(new File("properties.properties"));
            properties.load(fileReader);
        }
        return properties.getProperty(key);
    }

    // 测试
    public static void main(String[] args) throws Exception {

        Class<?> aClass = Class.forName(getProperty("bigbanana.class.name"));
        Object o = aClass.getConstructor().newInstance();
        Class<?> paramClass = Class.forName(getProperty("bigbanana.class.method.param"));
        Method method = aClass.getMethod(getProperty("bigbanana.class.method"),paramClass);
        method.invoke(o,"yellow");
    }

}

执行结果:Do you like yellow BigBanana?

3.2.实战二:代理卖手机--JDK动态代理

3.2.1.简单理解代理模式

通过代理的方式(增加中间层)对想要访问的类做一些控制,使代码职责清晰、通用化、智能化、易扩展。

代理模式三个要点:一个公共接口、一个具体类(被代理类)、一个代理类

代理模式分为:静态代理和动态代理

  • 静态代理:代理类在编译时已创建好具体类的对象,简言之是帮你提前new好了对象;
  • 动态代理:代理类在程序运行时才创建具体类的对象,根据程序的运行不同可能调用的具体类不同。

JDK动态代理本质是通过反射来实现,涉及InvocationHandler接口和Proxy类。

Proxy类:创建动态代理实例;

InvocationHandler对象:当执行被代理对象里的方法时,实际上会替换成调用InvocationHandler对象的invoke方法,动态的代理到接口下的实现类。

本次案例的类图关系:

  • 绿色名为JDK动态代理关键接口和类
  • 红色名为本案例要编写的关键接口和类

3.2.2.手机接口

public interface Phone {
    void sellPhone();
}

3.2.3.三款手机类

public class Huawei implements Phone {
    // 型号
    private String phoneModelName;

    public Huawei(String phoneModelName){
        this.phoneModelName=phoneModelName;
    }

    @Override
    public void sellPhone() {
        System.out.println("卖Huawei "+this.phoneModelName+" 的手机");
    }
}
public class Xiaomi implements Phone{
    // 型号
    private String phoneModelName;

    public Xiaomi(String phoneModelName){
        this.phoneModelName=phoneModelName;
    }

    public Xiaomi(){

    }
    @Override
    public void sellPhone() {
        System.out.println("卖Xiaomi "+this.phoneModelName+" 的手机");
    }
}
public class IPhone implements Phone {
    // 型号
    private String phoneModelName;

    public IPhone(String phoneModelName){
        this.phoneModelName=phoneModelName;
    }

    @Override
    public void sellPhone() {
        System.out.println("卖IPhone "+this.phoneModelName+" 的手机");
    }
}

3.2.4.Invocation 实现类

public class MyInvocationHandler implements InvocationHandler {
    private Object target;
    public MyInvocationHandler(Object target){
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        /**
         * 代理前可以考虑做些跟有趣的事
         */
        System.out.println("代理方法--start--代理前可以考虑做些跟有趣的事");
        Object invoke = method.invoke(target, args);
        /**
         * 代理后可能你有更想要做的事
         */
        System.out.println("代理方法--end--代理后可能你有更想要做的事\n");
        return invoke;
    }
}

3.2.5.代理商类

public class MyProxy {
    public static Object getProxy(Object target){
        MyInvocationHandler myInvocationHandler = new MyInvocationHandler(target);
        Object proxyInstance = Proxy.newProxyInstance(Phone.class.getClassLoader(), new Class[]{Phone.class}, myInvocationHandler);
        return proxyInstance;
    }
}

3.2.6.测试类

public class Test {

    public static void main(String[] args) {
        // 不管你想要买什么手机,只要通过同一个代理商就可以买到
        Phone huawei = (Phone) MyProxy.getProxy(new Huawei("Huawei 16 pro"));
        Phone xiaomi = (Phone) MyProxy.getProxy(new Xiaomi("MI 13 pro"));
        Phone iphone = (Phone) MyProxy.getProxy(new IPhone("IPhone 13 pro"));
        huawei.sellPhone();
        xiaomi.sellPhone();
        iphone.sellPhone();
    }
}

测试结果:

代理方法--start--代理前可以考虑做些跟有趣的事

卖Huawei Huawei 16 pro 的手机

代理方法--end--代理后可能你有更想要做的事

代理方法--start--代理前可以考虑做些跟有趣的事

卖Xiaomi MI 13 pro 的手机

代理方法--end--代理后可能你有更想要做的事

代理方法--start--代理前可以考虑做些跟有趣的事

卖IPhone IPhone 13 pro 的手机

代理方法--end--代理后可能你有更想要做的事

4.反射与泛型的简单实战

通过实战进一步理解泛型和反射。

4.1.实战一:泛型方法和反射的结合

PrintResult类:注意成员方法私有

public class PrintResult {

    private void printSuccessInfo(){
        System.out.println("printSuccessInfo--执行成功");
    }
    private void printAdd(int[] ints){
        int n=0;
        for (int i :ints){
            n=n+i;
        }
        System.out.println("求和结果="+n);
    }
}
public class Demo {

    // 执行指定类型的方法
    public <T> void demo1(T t,String methodName,int... args) throws Exception {
        Class<?> tClass = t.getClass();
        Object o = tClass.getConstructor().newInstance();
        if (args == null){
            Method method = tClass.getDeclaredMethod(methodName);
            method.setAccessible(true);
            System.out.println("执行的方法="+method.getName());
            method.invoke(o);
        }else {
            Method method = tClass.getDeclaredMethod(methodName,int[].class);
            method.setAccessible(true);
            System.out.println("执行的方法="+method.getName());
            method.invoke(o,args);
        }
    }


    public static void main(String[] args) throws Exception {
        Demo demo = new Demo();
        PrintResult printResult = new PrintResult();
        demo.demo1(printResult,"printSuccessInfo",null);//执行printResult 无参方法
        demo.demo1(printResult,"printAdd",1888,2222,333);
    }
}

执行结果:

执行的方法=printSuccessInfo

printSuccessInfo--执行成功

执行的方法=printAdd

求和结果=4443

在此可以看到泛型T 的对象t,是可以像普通的对象一样使用反射。

4.2.实战二:通过反射越过泛型检查

泛型在编译期通过类型抹除机制来完成;

反射在运行期完成执行,可以理解为反射是在运行期将编译好的list集合再新增元素进去。

public class Demo2 {

    public static void main(String[] args) throws Exception{
        List<String> list = new ArrayList<>();
        list.add("qwert");
        list.add("1234Z");
//        list.add(222); //指定了泛型类型为String后,无法add 非String的值

        Class listClass = list.getClass();
        //获取和调用 list中的add()方法
        Method m = listClass.getMethod("add", Object.class);
        m.invoke(list, 100);

        //输出List集合 -- toString已经被AbstractCollection重写的了
        System.out.println("集合中的内容:"+list.toString());
    }
}

执行结果:集合中的内容:[qwert, 1234Z, 100]

image

Java往期文章

Java全栈学习路线、学习资源和面试题一条龙

我心里优秀架构师是怎样的?

免费下载经典编程书籍

image

原创不易,三联支持:点赞、分享


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK