Java反射初探

来源:互联网 发布:微信自媒体 知乎 编辑:程序博客网 时间:2024/06/06 04:50

一、反射的基本概念

Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法;这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。

我们知道Java是面向对象的语言,在面向对象中,万事万物皆对象,所以我们自己定义的一个类也是一个对象,是java.lang.Class类的实例对象。我们知道我们平时自定义了一个类是通过Object obj = new Object()的方式来获得这个类的实例对象的,那么我们该如何获得Class类的实例对象呢?我们可以通过以下三种方式获得。

1、使用类名.class方式,例如我们定义一个类,名为Example,那么可以通过如下方式获得Class对象。

Class clazz1 = Example.class;
2、使用实例对象.getClass()方法获得Class对象,可以参考如下的代码。

Example example = new Example();Class calzz2 = example.getClass();
3、使用Class.forName(类的全称)获取Class对象,可以参考如下代码。

Class clazz3 = null;try {clazz3 = Class.forName("com.imooc.reflect.Example");} catch (ClassNotFoundException e) {e.printStackTrace();}
要注意的是这个方法获取Class对象时可能会抛出异常,这是因为可能要加载的这个类不存在,所以需要对异常进行捕获。
使用这以上三种方式都可以获得Class对象,获得的Class对象都是表示Example类的,我们每定义一个新的类都会实例化一个Class对象,Example只定义了一次,所以如果要判断clazz1==clazz2==clazz3,这个判断是正确的,因为他们都表示同一个Class对象。

创建了Class对象之后,我们可以通过Class类的newInstance()方法获得这个Class对象代表的类的实例,使用这个方法的前提是这个类有无参的构造方法。

二、动态加载类初探

Java中加载类有两种方式,分别为编译时加载和运行时加载,而编译时加载是静态的,运行时加载是动态的。静态加载就是需要在编译时将所有可能使用的类加载进来,如果有一个类不存在,那么就会出现编译错误,我们常常使用的new方式创建对象就是静态加载,我们可以通过下面的实例来分析以下静态加载。
我们定义一个Office类,可以根据传入的参数不同调用不同的类的方法,具体定义如下。
public class Office {public static void main(String[] args) {if("Word".equals(args[0])) {Word word = new Word();word.start();} else if("Excel".equals(args[0])) {Excel excel = new Excel();excel.start();}}}
如果我们直接编译这段代码,编译器会发现Word和Excel没有定义,这时就会报编译错误,找不到Word和Excel。
如果我们定义一个Word,代码如下。
public class Word{public void start() {System.out.println("Word Start");}}
这时再编译Office,仍然会报编译错误,这是因为Excel类还未定义。
再定义一个Excel类,代码如下。
public class Excel{public void start() {System.out.println("Excel Start");}}
再编译Office,这时才能编译通过,然后我们可以传入参数运行程序。
从上面这个例子我们可以看出使用静态编译代码有两个弊端,首先是编译时需要把所有可能用到的类加载进来,如果一个类不存在,那么整个程序都不能运行,实际开发中很可能出现有的类新定义,有的是后补充的,那么如果使用静态加载很可能出现程序运行不了的情况。第二个弊端是我们在Office类里的定义类的代码复用性不好,在Office里我们指定了必须使用Word类和Excel类,如果我们新定义了一个PPT类,那么就需要修改Office类,如果再定义一个PDF类,则需要再次修改Office类,这样Office类的代码复用性会很差。要解决上面说的两个弊端,我们可以尝试使用动态加载。
我们在上面提到的Class.forName(类的全称)就是常用的动态加载,动态加载是指当需要用到一个类时才会去加载这个类,而要解决上面的两个问题,我们首先需要从Word和Excel类中抽象出一个接口,这个接口里有两个类的公共方法start(),例如我们定义一个接口OfficeAble。
public interface OfficeAble {public void start();}
修改Word和Excel,让这两个类都实现OfficeAble接口。
public class Word implements OfficeAble{public void start() {System.out.println("Word Start");}}
public class Excel implements OfficeAble{public void start() {System.out.println("Excel Start");}}
这时我们再定义一个OfficeBetter使用动态加载来实现之前Office的功能。
public class OfficeBetter {public static void main(String[] args) {try {Class clazz = Class.forName(args[0]);OfficeAble office = (OfficeAble) clazz.newInstance();office.start();}catch (Exception e) {e.printStackTrace();}}}
从代码里我们可以看到我们直接使用Class.forName()获取了Class对象,然后通过Class对象获得一个实现OfficeAble接口的类的实例对象并执行start()方法,这里并没有指定具体是哪一个类,这样以后再新定义一个需要使用start()方法的类时就不需要修改OfficeBetter里的代码了,只需要实现OfficeAble接口即可,扩展性也提高了,同时,即使之前的Word和Excel没有定义,编译OfficeBetter也不会报编译错误,程序仍然可以正常执行。从这个例子中我们发现功能性的类尽量使用动态加载,并对新添的类实现功能性接口(标准),这样就不用重新编译了。

三、使用Class获得类的方法、属性和构造函数

一开始我们就已经知道可以使用反射来运行时获得一个类的方法、属性等信息,下面我们通过几个例子来看一下怎样是用Class获得一个类的方法、属性和构造函数。

1、获得类的方法

我们可以使用Class.getMethods()方法或Class.getDeclaredMethods()方法来获取一个类的方法,其实在Java中方法也被封装成一个对象Method,可以参考如下实例。
public static void getClassMethodInfo(Object obj) {Class clazz = obj.getClass();System.out.println("类的全称,包括包名:" + clazz.getName());System.out.println("类的名称:" + clazz.getSimpleName());//getMethods()方法获取的是所有的public的函数,包括父类继承而来的//getDeclaredMethods()获取的是所有该类自己声明的方法,不问访问权限(继承的就没有了)Method[] methods = clazz.getDeclaredMethods();for(Method method : methods) {//获取方法的返回类型Class returnType = method.getReturnType();System.out.print(returnType.getName() + " ");//获取方法名System.out.print(method.getName() + "(");//获取方法的参数类型Class[] paramTypes = method.getParameterTypes();for(Class paramType : paramTypes) {System.out.print(paramType.getName() + ",");}System.out.println(")");}}
其中Class.getName()方法不仅可以获得一个类的名称,也可以获得基本数据类型的名称,例如int.class.getName(),也可以获得void的名称,比如void.class.getName()。

2、获得类的属性

和获得类的方法类似,我们可以通过Class.getFields()方法和Class.getDeclaredMethods()方法来获得类的属性。
public static void getClassFieldInfo(Object obj) {Class clazz = obj.getClass();//属性也是对象//封装在java.lang.reflect.Field中//Field类封装了关于成员变量的操作//getFields()方法获取的是所有的public的成员变量的信息//getDeclaredFields获取的是该类自己声明的成员变量的信息Field[] fields = clazz.getDeclaredFields();for(Field field : fields) {//获取属性的类型Class fieldType = field.getType();String fieldName = field.getName();System.out.println(fieldType.getName() + " " + fieldName);}}

3、获得类的构造方法

获得类的构造方法方式可以参考如下代码,和方法、属性类似,Java对构造方法也做了封装,封装在Constructor类中。
public static void getClassConInfo(Object obj) {Class clazz = obj.getClass();//getConstructors获取所有的public的构造函数//getDeclaredConstructors得到所有的构造函数Constructor[] cons = clazz.getDeclaredConstructors();for(Constructor con : cons) {System.out.print(con.getName() + "(");//获得构造方法的参数列表Class[] paramTypes = con.getParameterTypes();for(Class paramType : paramTypes) {System.out.print(paramType.getName() + ",");}System.out.println(")");}}

四、方法的反射

与类的反射相似,方法也是可以反射的,我们通过Method类可以对方法进行操作,那我们该如何使用反射来调用一个类的某个方法呢?首先我们可以通过方法名称和方法的参数获取到一个方法,主要使用Class.getMethod(String name, Class<?>... parameterTypes) ,然后使用Method.invoke(对象,参数列表)来调用这个方法。我们通过以下实例进行分析。
我们定义一个ClassUtil,里面有三个名为print()方法,只是参数不同。
public void print(int a, int b) {System.out.println(a + b);}public void print(String a, String b) {System.out.println(a + ", " + b);}public void print() {System.out.println("Hello Word!");}
这时我们要使用反射来调用这三个方法,例如我们要使用两个int型的参数,参数值为2和3,使用如下代码。
public static void invokeMethod(Object obj) {Class clazz = obj.getClass();try {Method method = clazz.getMethod("print",int.class,int.class);method.invoke(obj, 2, 3);} catch (NoSuchMethodException e) {e.printStackTrace();} catch (SecurityException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();} catch (IllegalArgumentException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();}}
这时再写一个main方法,来调用这个invokeMethod方法。
public static void main(String[] args) {invokeMethod(new ClassUtil());}
执行程序会得到结果5,那么如果我们要使用参数类行为String的就可以给Class.getMethod传入String类型的参数,以此类推,这样就很容易地实现了方法的反射。其实类属性的反射也是类似的,这里暂不赘述了。

五、泛型的一些思考

对于泛型的使用我们并不陌生,我们可以通过下面的例子来深入了解一下泛型。加入我们定义了两个ArrayList,一个用了泛型,一个没有用泛型,那么如果我们比较两个ArrayList表示的Class时,这两个Class对象是否相同呢?
public static void main(String[] args) throws Exception{ArrayList list1 = new ArrayList();ArrayList<String> list2 = new ArrayList<String>();list2.add("a");Class clazz1 = list1.getClass();Class clazz2 = list1.getClass();System.out.println(clazz1 == clazz2);}
运行的结果时true,那说明泛型在运行阶段是无效的,如果在我们的代码里直接给list2插入一个整形的值,编译器会报错,这说明泛型在编译时有效,那如果我们在运行时插入一个整形的值呢?要做这样的尝试我们首先就需要绕过编译过程,我们可以通过方法的反射来绕过编译,修改上面的代码如下。
public static void main(String[] args) throws Exception{ArrayList list1 = new ArrayList();ArrayList<String> list2 = new ArrayList<String>();list2.add("a");Class clazz1 = list1.getClass();Class clazz2 = list1.getClass();System.out.println(clazz1 == clazz2);Method method = clazz2.getDeclaredMethod("add", Object.class);method.invoke(list2, 1);System.out.println(list2.size());}
我们使用反射来执行ArrayList的add方法,向list2中插入一个整数1,然后再查看list2的大小,运行程序,得到的结果为2,说明插入成功了,这也就说明了泛型的限定作用只在编译阶段起作用,其目的只是为了防止程序员在写代码的时候放入了错误类型的元素,当编译完成后,程序执行时,泛型就不再起任何限定作用了。









0 0
原创粉丝点击