总结java中的反射机制

来源:互联网 发布:淘宝美工工资待遇知乎 编辑:程序博客网 时间:2024/06/02 01:47

我们都知道,java是面向对象的,在面向对象的世界里,万物皆对象。那么在java中,真的都是对象么?如果你回答是的,那就错了,在java语言中,静态的成员和基本数据类型就不是面向对象的,刨除这两种情况,剩下的都是面向对象的。

那么问题就来了,类是不是对象呢?如果是的话又是那个类的对象呢?
答案是:类是对象,类是java.lang.Class类的实例对象而且我们把这个Class类的实例对象称为该类的类类型(class type)

一、反射的表示方法
重点来了,我们该有疑问了,如果类是一个对象,那么这个对象该怎么表示呢?
(1)这个类对象有三种表示方式:
1、通过类名.class调用
2、通过该类的对象的getClass方法
3、通过Class.forName(“这里是类的完整路径”)调用

public class ReflectTest1 {    public static void main(String[] args) {        User user = new User();        //第一种表示方式,并且告诉我们任何一个类都有一个隐含的静态成员变量class        Class c1 = User.class;        //第二种表示方式,通过该类的对象的getClass方法        Class c2 = user.getClass();        //第三种表示方式        try {            Class c3 = Class.forName("com.liangzi.test.User");        }catch (ClassNotFoundException e){            e.printStackTrace();        }    }}class User{    public void print(){        System.out.println("hello");    }}

我们把c1,c2,c3叫做User类的类类型(class type),那现在又有一个问题了,c1,c2,c3他们是一样的么?
我们可以尝试着比较一下看一看。

System.out.println(c1 == c2);System.out.println(c2 == c3);

当然,结果是相等的,它们都会输出true,因为c1,c2,c3都代表了User类的类类型,而一个类只可能是Class类的一个实例对象,所以说它们一定是相等的。

上面代码我们可以看到,我们用了

User user = new User();

来创建了一个User的对象实例,现在,我们完全可以通过类的类类型来创建该类的实例对象了
代码如下:

try {    //这个newInstance使用时需要保证User类有无参构造方法    User user1 = (User)c1.newInstance();    user1.print();} catch (InstantiationException e) {    e.printStackTrace();} catch (IllegalAccessException e) {    e.printStackTrace();}

上面的类类型我们可以用c1,c2,c3的任何一个,都可以创建一个User类的实例对象,但是要注意,使用newInstance方法的时候需要保证User类有无参构造方法,上面已经注释出来了,然后调用User类的print方法,会输出hello,说明我们成功了。

我们还可以用类类型获取到该类的名字:

Class int1 = int.class;Class int2 = Integer.class;Class double1 = double.class;Class double2 = Double.class;Class string1 = String.class;//可以把string1叫做String类型的字节码Class void1 = void.class;System.out.println(int1.getName());System.out.println(int2.getName());System.out.println(double1.getName());System.out.println(double2.getSimpleName());System.out.println(string1.getName());System.out.println(void1.getName());

上面注释说可以把string1叫做String类型的字节码,是比较大众化的叫法,其实官方的称呼是把string1叫做String类的类类型,还有int1和int2代表得不是同一个类类型,因为Integer是int的包装类,int是基本类型,Integer是包装类,getName方法获取全部名称包含包名,getSimpleName方法获取的是不包含包名的类名。

(2)Class.forName(“类的全称”)不光代表了类的类类型,还代表了动态加载类
我们得区分清编译和运行的区别:
编译时刻加载的类是静态加载类(用new操作的,例如A a = new A( );)
运行时刻加载的类是动态加载类(Class.forName(“类的全称”))

在编译工具我们感觉得可能不是特别明显,需要我们在外面用记事本等工具操作,然后用javac进行编译,用java进行运行。
例如:
我们编写一个Letter.java文件,内容如下:

public class Letter{    public static void main(String[] args){        if("A".equals(args[0])){            A a = new A();            a.print();        }        if("B".equals(args[0])){            B b = new B();            b.print();        }    }}

我们在dos窗口执行javac Letter.java命令,报错如下:
这里写图片描述

说明在编译的时候,JVM已经检查了类A和类B是否已经存在,如果不存在,就会报错了,上面的错误就是在编译的时候提示类A和类B找不到,所以报错了,这是在编译的时候加载类的情况

现在我们把类A加入进去,类A的代码如下:

public class A{    public void print(){        System.out.println("This is class A");    }}

我们先编译一下A.java,然后我们在执行javac Letter.java命令,结果如下:
这里写图片描述

现在我们有了A类,就报错为B类找不到,但是我们如果只想用A类,而不用B类的情况下运行,就运行不了了,如果我们有很多个类,有其中一个类有错误,那整个程序就不能执行了,这肯定不是我们希望看到的,如果我们真的在关键时刻出现了这样的错误,那可能会导致很严重的后果,这就是静态加载的弊端,所以我们现在需要动态加载类

好的,那就请看下面的操作(开启秀操作模式———>>>>>>>):
这个是NewLetter类的代码:

public class NewLetter{    public static void main(String[] args){        try        {            //动态加载类,在运行时刻才加载的            Class c = Class.forName(args[0]);            //通过该类的类类型,创建该类的对象            LetterInterface li = (LetterInterface)c.newInstance();            li.print();        }        catch(Exception e)        {            e.printStackTrace();        }    }}

这个是LetterInterface接口的代码:

public interface LetterInterface{    public void print();}

我们可以看到NewLetter类中Class c = Class.forName(args[0]);是在动态时刻加载类,
LetterInterface li = (LetterInterface)c.newInstance();是通过该类的类类型,创建该类的对象,现在又有一个问题,我们传进来的值时A类的还是B类的呢?所以这时候我们就可以使用接口了,只要其他的类实现了这个接口,那我们就可以实例化这个类的对象,我们以后创建新的类的时候也不需要在更改这个代码,只需要实现这个接口就好了。

现在,我们更改一下A类的代码,实现LetterInterface 接口,代码如下:

public class A implements LetterInterface{    public void print(){        System.out.println("This is class A");    }}

再次编译一下A类,现在执行javac NewLetter.java,结果如下:
这里写图片描述
并没有报错,我们可以正常的编译通过,我们在执行java NewLetter A,结果如下:
这里写图片描述
发现我们可以正常输出结果,然后我们在执行java NewLetter B,结果如下:
这里写图片描述
现在它才报错,提示我们类B找不到

现在我们可以很清楚的认识到,动态加载的好处,如果我们的程序里面有很多的类,那我们最好采用动态加载的形式,这样如果其中一个类出现了问题,其他的类还可以正常工作,对我们造成的损失应该会小一点

二、现在,我们来了解一下怎么获取类里面的成员函数,成员变量和构造函数的信息
(1)下面是怎样获取类中的成员函数:
请先阅读以下代码:

import java.lang.reflect.Method;public class ReflectTest2 {    public static void main(String[] args) {        ClassUtil.printClassMethodMessage("hello");        System.out.println("-----------------------------");        ClassUtil.printClassMethodMessage(new Integer(1));    }}class ClassUtil{    //打印类的成员函数的信息,obj代表传入类的实例对象    public static void printClassMethodMessage(Object obj){        //获取当前类的类类型        Class c = obj.getClass();        System.out.println("当前类名称是:"+c.getName());        /*引用了Method类,方法对象         * 一个成员方法就是一个Method的对象         * 获取成员方法有两种情况c.getMethods()方法和getDeclaredMethods()方法         * c.getMethods方法获取的是public方法,包括从父类继承过来的         * getDeclaredMethods方法获取的是该类自己声明的所有方法,不区分访问权限         * */        //Method[] methods = c.getMethods();        Method[] methods = c.getDeclaredMethods();        for(int i = 0; i < methods.length; i ++){            //获取方法的返回值的类类型            Class returnType = methods[i].getReturnType();            System.out.print(returnType.getName()+" ");            //获取方法的名称,包含包的名称            System.out.print(methods[i].getName()+"(");            //获取从参数的类类型            Class[] paramTypes = methods[i].getParameterTypes();            for(Class cl : paramTypes)                System.out.print(cl.getName()+",");            System.out.println(")");        }    }}

上面的代码新建了一个ClassUtil类来对类进行操作,里面定义了一个printClassMethodMessage来输出类里面成员函数的信息,参数是一个对象,首先我们要做的是利用这个对象获取到这个类的类类型,这里用到了Method类,成员方法就是Method 的对象,然后调用getMethods或者getDeclaredMethods方法回去该类的成员函数,getMethods方法获取的是public方法,包括从父类继承过来的,getDeclaredMethods方法获取的是该类自己声明的所有方法,不区分访问权限,到这里,我们就可以直接输出了,输出的结果包括成员函数的返回值,名字,和参数类型,不过,为了更详细的了解反射的操作,我们分别利用getReturnType()和methods[i].getName()分别获取了成员方法的返回值类型和方法名称,用getParameterTypes( )方法获取了参数值,这就说明了我们该如何获取类的成员函数信息

(2)下面是怎样获取类中的成员变量:

import java.lang.reflect.Field;public class ReflectTest2 {    public static void main(String[] args) {        ClassUtil.printClassFieldsMessage("hello");        System.out.println("-----------------------------");        ClassUtil.printClassFieldsMessage(new Integer(1));    }}class ClassUtil{    //获取类的成员变量的方法    public static void printClassFieldsMessage(Object obj){        Class c = obj.getClass();        /*成员变量也是对象,是java.lang.reflect.Field类的对象         * getFields()方法获取的是所有public的成员变量信息         * getDeclaredFields()方法获取的是该类自己声明的成员变量信息         * */        //Field[] fields = c.getFields();        Field[] fields = c.getDeclaredFields();        for(Field field : fields){            //获取成员变量的类型的类类型            Class fieldType = field.getType();            //获取类型的名称            String typeName = fieldType.getName();            //获取成员变量的名称            String fieldName = field.getName();            System.out.println(typeName+" "+fieldName);        }    }}

成员变量也是对象,是java.lang.reflect.Field类的对象,这其中的 getFields()方法获取的是所有public的成员变量信息,而getDeclaredFields()方法获取的是该类自己声明的成员变量信息,和上面的获取成员函数方法类似,我们获取到成员变量fields 以后,调用getType( )方法获取该成员变量的类型的类类型fieldType ,然后调用fieldType 的getName()方法获取到该类型的名称,在调用成员变量field的getName()方法获取该成员变量的名称

(3)下面是怎样获取该类中的构造函数的信息:

package com.liangzi.test;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.Method;public class ReflectTest2 {    public static void main(String[] args) {        ClassUtil.printClassConstructorMessage("hello");        System.out.println("-----------------------------");        ClassUtil.printClassConstructorMessage(new Integer(1));    }}class ClassUtil{    //获取类的构造函数的信息    public static void printClassConstructorMessage(Object obj){        Class c = obj.getClass();        /*构造函数也是对象,是java.lang.reflect.Constructor的对象         * getConstructors()获取的是所有public的构造函数         * getDeclaredConstructors()获取的是所有的构造函数         * */        //Constructor[] constructors = c.getConstructors();        Constructor[] constructors = c.getDeclaredConstructors();        for(Constructor cs : constructors){            System.out.print(cs.getName()+"(");            //获取构造函数的参数列表----得到的是参数列表的类类型            Class[] paramTypes = cs.getParameterTypes();            for(Class cc : paramTypes)                System.out.print(cc.getName()+",");            System.out.println(")");        }    }}

构造函数也是对象,是是java.lang.reflect.Constructor的对象, getConstructors()获取的是所有public的构造函数, getDeclaredConstructors()获取的是所有的构造函数,代码结构和上面获取成员函数和成员变量的类似,就不再赘述了。

三、方法的反射(如何获取某个方法、方法的反射操作)
我们知道,我们需要方法的名称和方法的参数列表才能唯一的确定一个方法
看下面的具体代码实例:

import java.lang.reflect.Method;public class ReflectTest3 {    public static void main(String[] args) {        A a = new A();        Class c = a.getClass();        Object obj = null;        try {            /*获取一个方法,需要确定方法名和参数列表             * getMethod获取的是public的方法             * getDelcaredMethod自己声明的方法             * */            //Method method = c.getMethod("print");            Method method = c.getMethod("print", new Class[]{});            /*方法的反射是用m对象进行方法调用的,和a.print()效果是一样的             * 方法如果没有返回值,则返回null,如果有返回值,就返回具体的返回值             * getMethod方法的参数可以是数组,也可以直接写,因为定义的是可变数组             * invoke方法的参数也可以是数组,还可以是直接写,因为定义的也是可变数组             * */            obj = method.invoke(a);            obj = method.invoke(a, new Object[]{});            System.out.println(obj);            System.out.println("=================================");            //Method method2 = c.getMethod("print",int.class,int.class);            Method method2 = c.getMethod("print", new Class[]{int.class,int.class});            obj = method2.invoke(a, 10,20);            obj = method2.invoke(a, new Object[]{10,10});            System.out.println(obj);            System.out.println("=================================");            //Method method3 = c.getMethod("print", String.class,String.class);            Method method3 = c.getMethod("print", new Class[]{String.class,String.class});            obj = method3.invoke(a, "abc","def");            obj = method3.invoke(a, new Object[]{"aaa","bbb"});            System.out.println(obj);        } catch (Exception e) {            e.printStackTrace();        }    }}class A{    public void print(){        System.out.println("This is null");    }    public void print(int a,int b){        System.out.println(a+b);    }    public int print(String a,String b){        System.out.println(a.toLowerCase()+b.toUpperCase());        return 100;    }}

我们用getMethod方法获取类里面的方法,第一个参数是方法的名字,第二个参数是参数列表,参数列表可以用class数组的形式表示,也可以有几个参数类类型写几个参数类类型,因为里面定义的是可变参数,写几个都可以,如果类里面的方法没有参数,我们可以不写,也可以放入一个空的class数组,然后我们用method.invoke(对象,参数列表)方法进行方法的反射操作,返回类型是类里面函数的返回类型

四、关于泛型的进一步认识
首先 你要知道JVM在对泛型进行操作的时候,会有一步叫做泛型擦除,也就是在编译以后泛型里面的类型只有Object类型了,不再区分String,Integer等等,了解了泛型擦出以后就好理解下面的代码了:

import java.lang.reflect.Method;import java.util.ArrayList;public class ReflectTest4 {    public static void main(String[] args) {        ArrayList list = new ArrayList();        ArrayList<String> list1 = new ArrayList<String>();        list1.add("This is String");        Class c1 = list.getClass();        Class c2 = list1.getClass();        //返回true就说明编译的时候进行了泛型擦除        System.out.println(c1 == c2);        try {            Method m = c2.getMethod("add", Object.class);            m.invoke(list1,111);            System.out.println(list1.size());//如果size是2,说明我们绕过了编译操作            System.out.println(list1);        } catch (Exception e) {            e.printStackTrace();        }    }}

现在我们可以看到,我们用反射操作将一个int类型的数字放到了String类型的泛型当中,如果我们直接调用list1的add方法放入这个111那么一定会报错的,现在我们反而成功了,这说明了:
(1)反射操作是编译之后的操作
(2)java中集合的泛型,是为了防止输入错误的,只在编译阶段有效,过了编译,泛型就会被擦出
(3)我们可以用反射操作绕过编译,从而可以放入不同类型的值
(4)用反射放入不是泛型要求的类型以后,如果我们要遍历的话,就不能使用迭代来进行遍历了

总结:这些是反射操作的一些基本小知识,学到了,就自己总结一下,整理一番,顺便加深一下记忆。

0 0