美文网首页
Java梳理之理解反射

Java梳理之理解反射

作者: _小二_ | 来源:发表于2017-11-19 22:35 被阅读0次

    在上一篇文章中编写注解处理器的时候就有用到反射,故而在此梳理反射的内容,大体上包括Class的获取、Class对象使用、泛型数组和动态代理等,主流的框架大体上都用到了反射、动态代理一类的,例如spring。
    最近遇到了一些事,所以有些懈怠了,这里警示一下。

    每种类型都有class对象,包括每个类、枚举、接口、注解、数组和基本数据类型,当然,还有一个关键字voidclass对象,在反射中可以通过Class对象可以拿到对应类的完整信息,源码请查看java.lang.Class类。在java.lang.reflect包下存在着一系列关于反射的类或接口,其中类或接口的关系如下所示:

    反射包类图.png

    在图中可以很清楚的看到三个类FieldMethodConstructor都继承了类AccessibleObject,其中有关于私有方法的权限设置,其它部分自己看吧~

    Class对象

    获取

    Class对象是反射的起点,它提供了操作类的工具,主要用于创建某种类型的对象(其中类型名称由字符串指定)和使用特殊技术实现类的加载(跨网络)。我们可以通过一下四种方式获得Class对象:
    1.通过使用对象的getClass()方法获得对应的Class对象。

    //得到class对象的几种方式
    String test = "test";
    Class clazz = test.getClass();
    

    2.使用类字面常量

    Class clazz1 = String.class;
    

    3.使用静态方法Class.forName()查找类的完全限定名

    try {
        clazz2 = Class.forName("java.lang.String");
    } catch (ClassNotFoundException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
    

    4.从某种能返回嵌套类或嵌套接口的反射方法中获取

    clazz2.getClasses();
    

    其中第三种方法因为通过字符串获取对象又有不同。通过Class类的源码可知,它其实是Class<T>泛型对象,例如String.class的类型是Class<String>int.class的类型是Class<Integer>,需要注意的是基本数据类型的Class对象和包装类型的Class对象是同一个类的类型的两个不同实例,即int.classInteger.class是同一个类型Class<Integer>的不同实例。参数化类型共享的是其原始类型的Class对象,如List<String>.classList.classClass对象相同,即类型是Class<List>。由此可知,上面前两种方式得到的Class对象是对应的Class<String>,但是第三种方式Class.forName("java.lang.String")得到的则是通配符Class<?>,如果想转换成确切的类型则需要方法asSubclass(Class clazz),如下所示:

    Class<? extends String> clazz2 = Class.forName("java.lang.String").asSubclass(String.class);
    
    使用

    一个普通类从结构上看,主要可能会存在包名、类名、父类,接口、成员字段、构造方法、成员方法、内部类、注解、修饰符,所以也可以通过其对应Class对象来判断获取这些元素,不止于此,通过Class对象还可以用于获取类加载器、类标记、资源文件、定位类等。
    分开来看,Class对象存在几个检查自身的方法,如isInterface()isArray()isEnum(),还有些如下所示:

    //检查判断方法
            /** class.isInstance(obj) 如果class obj1 = obj成立的话,返回true*/
            public native boolean isInstance(Object obj);
    
            /** class1.isAssignableFrom(class2) 如果class1对应的类与class2对应的类相同或超类*/
            public native boolean isAssignableFrom(Class<?> cls);
             
            /** 如果Class对象表示的是八大基本数据类型之一或者Void对象,则返回true*/
            public native boolean isPrimitive();
             
            /** 如果Class对象表示的是注解,则返回true*/
            public boolean isAnnotation()
             
             /** 如果Class对象表示的是由编译器引入的合成类型,则返回true。
              * 合成程序类型指的是在源代码中不存在对应结构的元素*/
            public boolean isSynthetic()
            
            /** 如果Class对象表示的是匿名内部类,则返回true*/
            public boolean isAnonymousClass()
            
            /** 如果Class对象表示的是局部内部类,则返回true*/
            public boolean isLocalClass()
            
            /** 如果Class对象表示的是嵌套类,则返回true*/
            public boolean isMemberClass()
            
            /** 返回编码类型为整数的类型修饰符,可以通过Modifier常量和方法对这个数值解码*/
            public native int getModifiers()
            
            /** 返回由该类型实现的所有接口对应的Type对象的数组*/
            public Type[] getGenericInterfaces()
            
            /** 返回由该类型的超类对应的Type对象的数组*/
            public Type getGenericSuperclass() 
    

    在上面检查方法中,isAssignableFrom(Class clazz)方法并不怎么熟悉,故有如下示例:

    //isAssignableFrom示例
    System.out.println(String.class.isAssignableFrom(Object.class));
    System.out.println(Object.class.isAssignableFrom(String.class)); 
    System.out.println(Object.class.isAssignableFrom(Object.class));
    输出:
    false
    true
    true
    

    其中类型修饰符Modifier包括权限访问修饰符(publicprotectedprivate)以及 abstractfinalstatic等,而基本数据类型修饰符一直是publicfinal的,数组类型的修饰符则是final的。由于注解属于之后添加的部分,并不在Modifier中。

    除上述部分以外,还有一系列方法用来获取当前对象的成员,包括之前所说的包名、类名、父类,接口、成员字段、构造方法、成员方法、内部类、注解、修饰符等。在Class类源码中可以看到,其中见的最多的当属

    public String getName()
    public Field[] getFields()
    public Method[] getMethods()
    public Constructor<?>[] getConstructors()
    public Annotation[] getAnnotations()
    public ClassLoader getClassLoader()
    public InputStream getResourceAsStream(String name)
    

    如上这几个方法。其中成员字段、方法、构造器返回的都是是public修饰的字段,如果想获取所有的,则可以使用相应的Declared方法。对于指定的字段、方法、构造器,都有相应的方法获取,如getField(String name),当然,也可以获取指定注解的。
    需要说明的是,类名区分简单名和规范名,其中关系如:java.lang.Object的简单名是Object,规范名则是java.lang.Object

    除了上面说的那部分之外,还有一个很重要的方法newInstance()用来创建对象,其条件是被调用的对象需要拥有无参构造器,如下:

    public static void main(String[] arg0) throws ClassNotFoundException, InstantiationException, IllegalAccessException{
            
            Class clazz = Class.forName("Test.Fruit");
            Object obj = clazz.newInstance();
            System.out.println(obj);
        }
    输出:
    Test.Fruit@3ce53108
    

    Field字段

    在反射包下,存在Field类用来操作对象的字段,如下所示:

    Class clazz = Class.forName("Test.Fruit");
    Field[] fields = clazz.getDeclaredFields();
    for(Field field:fields){
        System.out.print(field.getName()+",");
    }
    Fruit fruit = new Fruit();
    Object obj = clazz.newInstance();
            
    Field field = clazz.getDeclaredField("name");
    Field field1 = clazz.getDeclaredField("age");
    field1.setAccessible(true);
    System.out.println("\n获取字段名:"+field.getName());
    System.out.println("获取字段名:"+field1.getName());
            
    Object val = field.get(fruit);
    Object val1 = field.get(obj);
    System.out.println("获取字段值:"+val);
    System.out.println("获取字段值1:"+val1);
    Object val2 = field1.get(obj);
    System.out.println("获取字段1的值:"+val2);
            
            
    field.set(fruit, "橘子");
    field.set(obj, "香蕉");
    System.out.println("更新后的字段值:"+fruit.name);
    System.out.println("更新后的字段值1:"+((Fruit)obj).name);
    
    输出:
    name,age,
    获取字段名:name
    获取字段名:age
    获取字段值:水果
    获取字段值1:水果
    获取字段1的值:null
    更新后的字段值:橘子
    更新后的字段值1:香蕉
    

    在如上这个例子中Fruit类中有两个字段nameage,其中ageprivate的,所以对它进行操作的时候需要调用方法field.setAccessible(true)才可以进行操作否则就会报错,而公有字段则没有这个限制。不管怎么样,这样就将这个类的私有字段暴露出来,违反了封装特性同时带来安全问题,所以一般情况下都是不建议使用反射。

    Constructor构造器

    从名字就可以看出,这个类指代的是构造器,类中带有一些方法可以获取构造器名、修饰符、参数、参数类型等,当然其中最重要的当属 public T newInstance(Object ... initargs)方法,这个方法是实现功能的核心。如下所示:

    Class clazz = Class.forName("Test.Fruit");
    Constructor[] constructors = clazz.getDeclaredConstructors();
    System.out.println(constructors.length);
    for(Constructor constructor:constructors){
        System.out.println(constructor);
    }
    Constructor constructor = clazz.getDeclaredConstructor(String.class,String.class);
    System.out.println(constructor);
    
    Object obj = constructor.newInstance("香蕉","黄色");
    Fruit fruit = (Fruit)obj;
    System.out.println(fruit.name);
    
    输出:
    2
    Test.Fruit(java.lang.String,java.lang.String)
    Test.Fruit()
    Test.Fruit(java.lang.String,java.lang.String)
    香蕉
    

    Method方法

    很重要的一个部分Method,其中的核心方法public Object invoke(Object obj, Object... args)用来调用对象的当前方法,参数不定长,根据实际情况而定,其实目前编程来看很少使用不定参数,因为它带来很多不好的影响。Method示例如下所示:

    Class clazz = Class.forName("Test.Fruit");
    Method[] methods = clazz.getDeclaredMethods();
    for(Method method:methods){
        System.out.println("method name :"+method.getName());
    }
    Method method = clazz.getDeclaredMethod("getName");
    Object obj = clazz.newInstance();
    Object ret = method.invoke(obj);
    System.out.println("method return :"+ret);
    
    输出:
    method name :main
    method name :getName
    method return :水果
    

    Annotation注解

    在上一节已经说过一些注解的反射调用,在写注解处理器的时候通过反射来获取注解的相应信息。详情请看《Java梳理之理解注解》,当然文中说的比较浅,可以针对的去实践。

    Modifier修饰符

    之前说过,Modifier类包含了除注解之外的所有修饰符,包括权限访问修饰,如下所示:
    abstractfinalinterfacenativeprivateprotectedpublicstaticstrictsynchronizedtransientvolatile
    可以通过相应的查询方法查询是否存在某个修饰符,如下所示:

    Class clazz = Class.forName("Test.Fruit");
    int modifiers = clazz.getModifiers();
    System.out.println("this is protected ?:"+Modifier.isProtected(modifiers));
    System.out.println("this is public ?:"+Modifier.isPublic(modifiers));
    
    输出:
    this is protected ?:false
    this is public ?:true
    

    成员类

    Class类中有如下这样一个方法:
    public native Class<?> getDeclaringClass()
    而且在FieldMethodConstructor实现的接口Member中也有这个方法getDeclaringClass(),用来获取内部类。

    类加载

    在Java程序运行的时候,JVM会根据需要加载类,其中用到的则是类加载器,虽然虚拟器实现的细节不同,但是使用还是没有什么问题的。在Class类中有ClassLoader getClassLoader()方法,通过该方法可以获取类加载器,通过这个加载器则可以实现类的加载,如下所示:

    ClassLoader loader = ClassLoader.getSystemClassLoader();
    Class clazz1 = loader.loadClass("java.lang.String");
    System.out.println(clazz1);
    输出:
    class java.lang.String
    

    当然,类加载器还要一个非常有用的方法InputStream getResourceAsStream(String name),通过这个方法可以加载配置文件,如下所示:

    static {
            prop = new Properties();
            try {
                inputStream = RedisConstant.class.getClassLoader().getResourceAsStream("config.properties");
    
                prop.load(inputStream);
                inputStream.close();
                inputStream = null;
                //操作
                ...
    }
    

    数组

    说完这个,可以看一下特殊的对象:数组,在数组中是没有字段和方法的,就算length也并不是数组实际的字段,所以通过获取字段、方法和构造器来处理数组的时候,返回的都是null。为了创建数组并对其进行操作,可以使用Array的静态方法,如下:

    //返回componentType类型的数组,长度为length
    public static Object newInstance(Class<?> componentType, int length)
    //返回componentType类型的多维数组,后面的则是维数dimensions
    public static Object newInstance(Class<?> componentType, int... dimensions)

    示例如下:

    int[] ints = (int[])Array.newInstance(int.class, 10);
    等价于
    int[] ints1 = new int[10];
    
    int[] dimensions ={4,4};
    int[][] ints = (int[][])Array.newInstance(int.class, dimensions);
    等价于
    int[][] ints1 = new int[4][4];
    

    当然,创建对象这样,那么操作数组的时候也是不同的。在Array类中,存在Object get(Object array, int index)set(Object array, int index, Object value)方法,通过这两个方法可以对数组进行操作。

    代理Proxy

    Proxy类允许在运行时创建实现一个或多个接口的类。在这里可以这样理解:调用Proxy类的静态方法getProxyClass,调用时传入一个类加载器和一个接口数组,这样就可以得到对应的代理类。代理对象的构造器需要传递InvocationHandler对象,如下所示:

    Class clazz = Class.forName("Test.Fruit");
            Class proxy = Proxy.getProxyClass(clazz.getClassLoader(), clazz.getInterfaces());
            Constructor constructor = proxy.getDeclaredConstructor(InvocationHandler.class);
            Object obj = constructor.newInstance(new InvocationHandler(){
    
                @Override
                public Object invoke(Object proxy, Method method, Object[] args)
                        throws Throwable {
                    // TODO Auto-generated method stub
                    return null;
                }});
        }
    

    invoke方法中可以处理相应的操作,但是不仅仅是只有这一种方法,Proxy类还提供了一种简单易懂的代理类创建方式,即,通过newProxyInstance方法创建,创建时需要多传入一个实现了InvocationHandler接口的实例即可,如下

    public class reflectDemo {
    
        public static void main(String[] arg0) throws ClassNotFoundException, SecurityException, NoSuchMethodException, IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException{
            
            Fruit fruit = new Fruit();
            Proxy.newProxyInstance(fruit.getClass().getClassLoader(), fruit.getClass().getInterfaces(), new FruitInvocationHandler());
        }
    }
    class FruitInvocationHandler implements InvocationHandler{
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args)
                throws Throwable {
            // TODO Auto-generated method stub
            return null;
        }
        
    }
    

    上例都只是简单的原理,并没有实现具体的功能,如果有需要,可以自行根据实际情况做处理。
    到这里,大体就把反射给梳理的差不多了,这一部分是之后深入学习各种框架的基础,所以需要多多实践。本人能力有限,文中有误的地方请帮忙指出。

    参考
    《Java程序设计语言》16章
    Java反射机制详解

    相关文章

      网友评论

          本文标题:Java梳理之理解反射

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