美文网首页JAVA随笔
JDK学习之反射的使用姿势一览

JDK学习之反射的使用姿势一览

作者: 一灰灰blog | 来源:发表于2017-11-14 22:48 被阅读2次

    反射的学习使用

    日常的学习工作中,可能用到反射的地方不太多,但看看一些优秀框架的源码,会发现基本上都离不开反射的使用;因此本篇博文将专注下如何

    本片博文布局如下:

    1. 反射是什么,有什么用,可以做什么

    2. 如何使用反射

    3. 实例:

      • 写一个BeanUtils实现对象的成员变量值拷贝

    I. 反射定义

    指程序可以访问、检测和修改它本身状态或行为的一种能力

    直接说定义的话,可能并不能非常清晰的解释说明,结合作用进行描述

    反射可以干什么?

    在运行时构造任意一个类的对象。
    在运行时判断任意一个对象所属的类。
    在运行时判断任意一个类所具有的成员变量和方法。
    在运行时调用任意一个对象的方法
    

    有了上面四点,基本上你想干嘛就可以干嘛,比如我现在就有下面这个类

    public class RefectTest extends MyRefect implements IRefect {
    
        private static String s1 = "hello";
    
        private static int s2 = 100;
    
        private int s3 = 200;
    
        private boolean ans;
    
        protected RefectTest next;
    
        public RefectTest() {
        }
    
        public RefectTest(int s3, boolean ans, RefectTest next) {
            this.s3 = s3;
            this.ans = ans;
            this.next = next;
        }
        
        public RefectTest next() {
          return next;
        }
        
        private int count(int a, int b) {
          return a + b;
        }
    }
    

    现在我有了clz,其赋值语句为 Class clz = RefectTest.class, 那么我可以干啥?

    1. 创建一个 RefectTest 对象

      // 若有默认构造方法
      RefectTest instance = clz.newIntance();
      
      // 若需要传参数
      Constructor con = clz.getConstructor(int.class, boolean.class, RefectTest.class);
      RefectTest instance2 =  con.newInstance(10, true, new RefectTest());
      
    2. 判断父类是否是 MyRefect

      // 判断MyRefect是否为clz的父类
      boolean ans = MyRefect.class.isAssignableFrom(clz);
      
    3. 获取所有的成员变量

      // 获取所有的成员变量(包括私有的)
      Field[] fields = clz.getDeclaredFields();
      
    4. 获取所有的方法

      // 获取所有的成员方法(包括私有方法)
      Method[] methods = clz.getDeclaredMethods();
      

    上面给出了可以干些什么,并给了对应的简单示例,引入了几个新的类Constructor, Field, Method, 下面将详细解释这三个类是什么,怎么用

    II. 反射的使用

    努力结合实际的应用场景,给出每种利用反射的实现对应需求的使用姿势,有些场景可能并不是特别贴切,欢迎提出给合适的场景以此进行替换

    1. 通过反射创建对象

    这是个比较常见的场景,我在使用了自定义注解时,通常会这么晚

    应用场景:

    我定义了一个校验器的注解ValDot,注解中有个校验规则class对象,如下

    public interface ICheckRule {
        boolean check(Object ... obj);
    }
    
    public class DefaultCheckRule implements ICheckRule {
        @Override
        public boolean check(Object... obj) {
            return false;
        }
    }
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface CheckDot {
        // 校验规则
        Class<? extends ICheckRule> check() default DefaultCheckRule.class;
    }
    

    上面定义了注解和校验条件,接着进入整体,在切面中,需要获取

    @Aspect
    @Component
    public class CheckAspect {
        @Before("@annotation(checkDot)")
        public void process(JoinPoint joinPoint, CheckDot checkDot)
          throws IllegalAccessException, InstantiationException {
            // 注意,这里获取注解上的校验规则类,并获取实例
            ICheckRule rule = checkDot.check().newInstance();
            
            if(rule.check(joinPoint.getArgs())) {
                throw new IllegalStateException("check argument error!");
            }
        }
    }
    

    上面是一个较好的利用反射获取实例的应用场景,想一想,如果不用反射,这个校验规则怎么传进来呢,这个时候就没那么方便了(当然也不是不可以,最简单的就是拿一个Holder持有类名到类对象的映射关系,然后在注解中传类名,也可以达到上面的效果)

    还有一种场景可能就比较蛋疼了,如果一个类没有默认构造方法,通过反射就没法直接用class.newInstanace()


    Constructor构造器类

    根据Class优先获取到 Constructor 对象,然后传入需要的构造参数, 测试如下

    public class ConTest {
    
        private int a,b;
    
        public ConTest(int a, int b) {
            this.a = a;
            this.b = b;
        }
    
        @Override
        public String toString() {
            return "ConTest{" + "a=" + a + ", b=" + b + '}';
        }
    
        public static void main(String[] args) throws Exception {
            Class clz = ConTest.class;
            // 获取对应的构造器(注意参数类型)
            Constructor constructor = clz.getConstructor(int.class, int.class);
            // 创建实例(注意参数要匹配)
            ConTest test = (ConTest) constructor.newInstance(10, 20);
            System.out.println(test.toString());
        }
    }
    

    输出

    ConTest{a=10, b=20}
    

    一般常用下面四种方式获取

    // 根据参数类型获取匹配的构造器
    Constructor getConstructor(Class[] params)
     
    // 获取所有的
    Constructor[] getConstructors() 
    
    // 相比较前面的,这里可以获取私有方法 
    Constructor getDeclaredConstructor(Class[] params)
     
    // 可以获取私有方法
    Constructor[] getDeclaredConstructors()
    

    2. 判断class的继承关系

    判断是否为基础数据类型

    基本类型较为特殊,所以JDK很人性化的给封装了一个方法,Class#isPrimitive

    因此返回true的类型有:

    • int
    • long
    • short
    • byte
    • char
    • boolean

    封装后的类型,返回的依然是false

    <font color="red">附带一句,是没有null.class这种用法的</font>


    判断是否为另一个类的子类,另一个接口的实现类

    通常我们利用 instanceof 关键字来判断继承关系,但是这个是针对对象来的,现在给一个class,要怎么玩?

    看下面,主要就是 Class#isAssignableFrom() 的功劳了

    public class ExtendTest {
    
        interface ITest {}
        
        abstract class ATest {
            abstract public void print();
        }
    
        class TestClz extends ATest implements ITest {
            @Override
            public void print() {
                System.out.println("TestClz");
            }
        }
    
    
        public static void main(String[] args) {
            Class clz = TestClz.class;
            
            System.out.println(ATest.class.isAssignableFrom(clz));
            System.out.println(ITest.class.isAssignableFrom(clz));
        }
    }
    

    需要注意一点,父类作为调用方,子类作为参数

    结合泛型时,获取泛型的实际类型

    泛型,又是一个有意思的功能,这里不多说,继承一个泛型基类,然后问题是如何通过反射获得泛型签名中的类型,一般会在继承或实现泛型接口时会用到它。

    class A<T, ID> {
    }
    
    class B extends A<String, Integer> {
    }
    
    public static void main(String[] args) {
        System.out.println(B.class.getGenericSuperclass());
    }
    

    换成泛型接口呢 ?

    interface A<T, ID> {  
    }  
      
    class B implements A<String, Integer> {  
    }
    
    public static void main(String[] args) {
        ParameterizedType parameterizedType = (ParameterizedType) B.class.getGenericInterfaces()[0];  
        Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();  
        for (Type actualTypeArgument : actualTypeArguments) {  
            System.out.println(actualTypeArgument);  
        }
    }
    

    3. 获取成员变量

    获取成员变量,主要是根据 B.class.getDeclaredFields() 来获取所有声明的变量,这个应用场景会和下面的获取方法并执行联合一起说明

    // 获取指定的公共成员变量
    Field getField(String name)
     
    // 获得所有公共字段 
    Field[] getFields()
    
    // 获取指定声明的成员变量(包括prive)
    Field getDeclaredField(String name)
    
    // 获取所有声明的成员变量
    Field[] getDeclaredFields()
    

    这个主要返回 Field对象,现在有了Field,可以做些啥?

    1. 判断成员的修饰 Field#getModifiers()

      int modify = field.getModifiers();
      // 是否是静态变量
      boolean ans = Modifier.isStatic(modifier);
      // 是否是公共变量
      boolean ans = Modifier.isPublic(modifier);
      // 是否不可变
      boolean ans = Modifier.isFinal(modifier);
      // ...
      
    2. 获取成员的变量名 : field#getName()

    3. 获取成员对应的value: field#get(instance)

      • 对于静态成员,instance可以为null
      • 对于非静态成员,instance必须为一个实例对象
    4. 获取注解: field#getAnnotations()

      • 这个就厉害了,hibernate的校验框架,在成员变量上加一个注解Max,就可以设置参数的最大值,其实就是通过反射获取到注解,然后进行相应的逻辑

    4. 获取方法

    获取方法,同上面的差不多,也有四种方式

    // 根据方法名,参数类型获取公共方法
    Method getMethod(String name, Class[] params)
    
    // 获取所有的公共方法
    Method[] getMethods()
    
    // 根据方法名,参数类型,获取声明的方法(包括私有)
    Method getDeclaredMethod(String name, Class[] params)
    
    // 获取所有声明的方法
    Method[] getDeclaredMethods()
    

    返回了一个Method类,那么这个东西又有一些什么功能?

    1. 获取方法名 Method#getName()

    2. 获取方法所在的类 : Method#getDeclaringClass()

    3. 获取方法返回类型 : Method#getReturnType()

    4. 获取方法上的注解 : Method#getAnnotations()

    5. 执行方法 有了这个就可以做很多事情了,实例中给出说明

      // 设置方法可访问(即私有方法也可以被调用)
      method.setAccessible(true);
      // instance为实例对象, args为传入参数
      method.invoke(instance, args)
      

    III. 实例DEMO

    通过反射的方式,实现一个 BeanUtils,实现Bean的拷贝

    当一个Bean有较多的成员变量时,如果我们采用最原始的setXXX()来一次赋值的时候,一是实现比较繁琐,其次就是当Bean的字段发生变动之后,也需要同步的修改,那么我们借助反射的方式,实现一个优雅的 BeanUtils 工具类

    public class BeanUtils {
        public static void copy(Object source, Object dest) throws Exception {
            Class destClz = dest.getClass();
    
            // 获取目标的所有成员
            Field[] destFields = destClz.getDeclaredFields();
            Object value;
            for (Field field : destFields) { // 遍历所有的成员,并赋值
                // 获取value值
                value = getVal(field.getName(), source);
    
                field.setAccessible(true);
                field.set(dest, value);
            }
        }
        private static Object getVal(String name, Object obj) throws Exception {
            try {
                // 优先获取obj中同名的成员变量
                Field field = obj.getClass().getField(name);
                field.setAccessible(true);
                return field.get(obj);
            } catch (NoSuchFieldException e) {
                // 表示没有同名的变量 
            }
    
            // 获取对应的 getXxx() 或者 isXxx() 方法
            name = name.substring(0, 1).toUpperCase() + name.substring(1);
            String methodName = "get" + name;
            String methodName2 = "is" + name;
            Method[] methods = obj.getClass().getMethods();
            for (Method method : methods) {
                if (method.getParameterCount() > 0) {// 只获取无参的方法
                    continue;
                }
                if (method.getName().equals(methodName)
                        || method.getName().equals(methodName2)) {
                    return method.invoke(obj);
                }
            }
            
            // 没有匹配到 因为如果原属性为基本数据类型,赋值null为报错
            throw new Exception();
        }
    }
    

    IV. 小结

    反射的四种用途

    1. 创建一个 RefectTest 对象

      // 若有默认构造方法
      RefectTest instance = clz.newIntance();
      
      // 若需要传参数
      Constructor con = clz.getConstructor(int.class, boolean.class, RefectTest.class);
      RefectTest instance2 =  con.newInstance(10, true, new RefectTest());
      
    2. 判断父类是否是 MyRefect

      // 判断MyRefect是否为clz的父类
      boolean ans = MyRefect.class.isAssignableFrom(clz);
      
    3. 获取所有的成员变量

      // 获取所有的成员变量(包括私有的)
      Field[] fields = clz.getDeclaredFields();
      
    4. 获取所有的方法

      // 获取所有的成员方法(包括私有方法)
      Method[] methods = clz.getDeclaredMethods();
      
      
      

    使用注意事项

    1. 操作私有变量,私有方法时,先设置field.setAccessible(true);确保可访问
    2. 反射会带来额外的性能开销
    3. 可以用 Class#isAssignableFrom() 来判断类继承关系
    4. 可以用 Class#isPrimitive()判断是否为基本数据类型
    5. 可以用 Class#getGenericSuperclass() 获取泛型类型

    V. 其他

    声明

    尽信书则不如,已上内容,纯属一家之言,因本人能力一般,见解不全,如有问题,欢迎批评指正

    扫描关注,java分享

    QrCode

    相关文章

      网友评论

        本文标题:JDK学习之反射的使用姿势一览

        本文链接:https://www.haomeiwen.com/subject/hmeqvxtx.html