Java反射篇


Java反射

对象在运行时会有两种类型,编译时类型和运行时类型,例如:String a = new Name(),编译时为String,运行时为Name。为了准确知道该对象的类型,可以通过instanceof()方法,但是在什么都不知道的情况下,只能通过反射获取该对象的信息。

获取Class对象

每个类被加载后就会有Class对象生成,通过该对象就可以访问JVM中的这个类了。这里介绍三种获取Class对象的方法:

  • 使用Class类的forName(String clazzName)静态方法,参数为类的全限定类名。
  • 调用class属性来获取Class对象,例如:Human.class
  • 调用类实例的getClass()方法。该方法在java.lang.Object已定义。
Class对象中获取信息

Class类提供了大量实例方法来获取对应的类信息,一些如下:

  • Constructor getConstructor(Class<?> Types):返回对应类的带参的public构造器。
  • Constructor<?>[ ] getDeclaredConstructors():返回所有构造器,不受访问权限限制。
  • Method getDeclaredMethod(String name,Class<?> …parameterTypes):返回带指定形参的方法,访问权限无关。
  • Field getField(String name):返回对应类的指定名称的public成员变量。
以下用于访问注解
如下方法访问内部类

Class<?>[] getDeclaredClasses():返回类中的全部内部类。

访问外部类方法:
  • Class<?> getDeclaringClass():返回对应类所在的外部类。
  • Class<?>[] getInterfaces():返回对应类实现的全部接口。
  • Class<? super T> getSuperclass():返回对应类的父类的Class对象。
以下用于获取对应类的修饰符、所在包、类名:
  • int getModifiers():返回修饰符,返回的整数用Modifier工具类来解码。
  • Package getPackage():获取类的包名。
  • String getName():返回类名
  • String getSimpleName():返回类名简称。

还有方法判断类是否为一个接口或注解的,如:boolean isAnnotation()。上述大量方法都是分好的,很有规律。之所以有大量重复类名的方法,是因为在获取Method的时候,类重载了许多方法,想要获取指定的方法必须给出相应的形参列表:

clazz.getMethod("info",String.class);

而获取构造器时无需传入构造器名,只需要给出形参列表即可。

Java8新增的方法参数反射

Java8在java.lang.reflect包下新增一个Executable抽象基类,该对象代表可执行的类成员,同时该类派生了ConstructorMethod两个子类。抽象基类提供了获取修饰方法或构造器注解信息的方法,通过getModifiers()方法获取该方法或构造器的修饰符。此外有两个方法来得到方法和形参名及个数:

  • int getParameterCount():获取构造器或方法的形参个数。
  • Parameter[] getParameters():获取该构造器或方法的所有形参。

同时在获取形参parameter之后,还提供几个方法获取形参的参数信息:

  • getModifiers():获取形参修饰符。
  • String getName():获取形参名
  • Type getParameterizedType():获取带泛型的形参。
  • Class<?> getType():获取形参类型
  • boolean isNamePresent():返回类的class文件中是否包含方法的形参名信息。
  • boolean isVarArgs():判断参数是否为个数可变的形参

使用反射生成并操作对象

Class对象可以获得类的方法(Method对象),构造器(Constructor对象),成员变量(Field对象),这三个类都位于java.lang.reflect包下,并实现了java.lang.reflect.Member接口,程序可以提供Method调用方法,通过Constructor调用构造器创建实例,能提供Field对象访问并修改成员变量值。

创建对象

通过反射来生成对象有以下两种方式:

  • 通过Class对象的newInstance()方法来创建实例,前提是要有默认构造器。
  • 先获取Constructor对象,再调用它的newInstance()方法创建,特点是可以指定构造器创建。
Class<?> clazz = Class.forName(clazzName);
return clazz.newInstance();

第二种方法获取指定构造器可以通过getConstructor()方法来获取指定构造器。

调用方法

通过调用Class对象的getMethod()方法可以获取对应的Method对象,每个对象对应一个方法。Method对象有一个invoke()方法,签名如下:

//Object invoke(Object obj,args)
Method m = clazz.getMethod(Name,String.class);
m.invoke(o,config.getProperty(name));//执行名为Name的方法

当通过invoke方法调用对应方法时,需要有调用该方法的权限。若是private方法,可以先调用MethodsetAccessible(boolean f)方法,f为true,则该Method使用时取消访问权限检查。

访问成员变量

通过Class对象的getField()可以获取该类的成员变量,Field对象提供了如下方法来读取或设置成员变量值:

  • getXxx(Object obj):获取obj对象的该成员变量的值。Xxx对应8种基本类型,引用类型则取消后面Xxx
  • setXxx(Object obj,Xxx val):将obj对象的成员变量设置成val值,同理。

使用两个方法可以访问指定对象的所有成员变量,包括private修饰的成员变量。

操作数组

java.lang.reflect包下还提供一个Array类,Array对象可以代表数组,创建数组。

  • static Object newInstance(Class<?> componentType,int length):创建一个指定元素、长度的数组。

  • static xxx getXxx(Object array,int index):返回array数组中第index个元素。引用类型为get(Object array,int index)

  • static void setXxx(Object array,int index,Xxx val):将数组第index个元素的值设为val,如果数组元素为引用类型则方法变成set

Object arr = Array.newInstance(String.class,10);//多维再添加数字
Array.set(arr,5,"某菜鸡");
Object o = Array.get(arr,5);

用反射生成JDK动态代理

在Java的java.lang.reflect包下提供一个proxy类和InvocationHandle接口,通过使用代理类和接口可以生成JDK动态代理类和对象。

使用proxyInvocationHandle创建动态代理

Proxy提供了用于创建动态代理类和对象的静态方法,它是所有动态代理类的父类。如果在程序中为一个或多个接口动态生成实现类,就可以使用Proxy来创建动态代理类;如果需要为一个或多个接口动态创建实例,也可以使用Proxy来创建动态代理实例。

  • static Class<?> getProxyClass(ClassLoader loader,Class? >interface):创建一个代理类对应的Class对象,该代理类将实现interfces所指定的多个接口。

  • static Object newProxyInstance(ClassLoder loder,Class<>[] interfaces,InvocationHandler h):创建一个动态代理对象,该对象实现了一些接口,执行代理对象的每个方法时都会被替换执行InvocationHandle对象的invoke方法。

采用第一个方法生成代理类的时候,如果需要通过代理类创建对象,依然需要传入一个InvocationHandler对象,即一个代理对象关联一个InvocationHandler对象。

interface person{
    void walk();
    void sayHello(String name);
}
class MyInvocationHandler implements InvocationHandler{
    public Object invoke(Object proxy,Method m,Object[] args){
        System.out.println("方法"+m);
    }    
}
动态代理和AOP

由于JDK动态代理只能为接口创建动态代理,所以下面先提供一个Dog接口:

public interface Dog{
    void info();
    void run();
}
public class GDog implements Dog{
    public void info{
        System.out.println("狗");
    }
    public void run{
        System.out.println("疾跑");
    }
}

如果直接使用代理类为该接口创建动态代理对象,则动态代理对象的所有方法的执行效果将完全一样。下面提供一个DogUtil类。

public class DogUtil{
    //拦截器方法
    public void method1(){
        System.out.println("第一个通用方法");
    }
    public void method2(){
        System.out.println("通用方法");
    }
}

借助于ProxyInvocationHandler就可以实现,当调用info方法和run方法时,系统将自动把两个通用方法插入inforun方法中执行。

public class MyInvocationHandler implements InvocationHandler{
    private Object target;
    public void setTarget(Object target){
        this.target = target;
    }
    public Object invoke(Object proxy,Method m,Object[] args) throws Exception{
        DogUtil d = new DogUtil();
        d.method1();
        Object result = method.invoke(target,args);
        d.method2();
        return result;
    }
}
public class MyProxyFactory{//为target生成动态代理对象
    public static Object getProxy(Object target) throws Exception{
        MyInvocationHandler h = new MyInvocationHandler();
        h.setTarget(target);
        return Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getIInterfaces(),h);
    }//返回动态代理
}

上面动态代理工厂类提供一个getProxy()方法,该方法为target对象生成一个动态代理对象,这个对象与target实现了同样的接口。当调用动态代理对象的指定方法时,实际上将变为执行MyInvocationHandler对象的invoke方法。执行步骤为:

  1. 创建DogUtil实例
  2. 执行DogUtil实例的method1()方法
  3. 使用反射以target作为调用者执行该方法
  4. 执行DogUtil实例的method2()方法

当使用动态代理对象来代替target对象时,代理对象的方法既能插入通用方法,但GDog方法又没有像过去一样调用method1method2方法。

public class Test{
    public static void main(String[] args){
        Dog target = new GDog();
        Dog dog = (Dog)MyProxyFactory.getProxy(target);
        dog.info();
        dog.run;
    }
}

dog为实际动态代理对象,实现了Dog接口,动态代理可以很容易实现解耦,这种动态代理在AOP中称为AOP代理,AOP代理包含了目标对象的全部方法。代理中的方法与目标对象的方法有差异:AOP代理包含的方法可以在执行目标方法之前、之后插入一些通用处理。

反射与泛型

String.class的类型实际上是Class<String>,如果类型未知,则使用Class<?>,反射中使用泛型可以避免生成的对象需要类型转换。前面介绍了Array类创建数组,其实并不常用,newInstance()返回一个数组,而不是Object对象,如果要当String[]数组使用则要强制类型转换。

public static Object newInstance(Class<?> componentType,int .... dimension)

如果改为:

public static <T> T[] newInstance(Class<T> componentType,int length)

则无需类型转换,但是得到参数可变的接收了。

使用反射获取泛型信息

得到成员变量对应的Field对象后就可以获得具体的数据类型了,首先应该获得所含的成员变量:

Class<?> a = f.getType();//获取成员变量的类型

如果成员变量是含有泛型类型,则使用如下方法获取:

Type t = f.getGenericType();

之后就可以将Type对象强制转换成ParametericedType对象(被参数化的类型),它提供了两个方法:

  • getRawType():返回原始类型,没有泛型信息。
  • getActualTypeArguments():返回泛型参数的类型。

getType()方法只能获取普通类型的成员变量的数据类型,而带泛型的成员变量,应该使用getGenericType()方法。Typejava.lang.reflect包下的接口,代表所有类型的高级接口,ClassType接口的实现类。Type包括原始类型、参数化类型、数组类型、类型变量和基本类型。


文章作者: 流浪舟
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 流浪舟 !
评论
 上一篇
Java-IO流 Java-IO流
Java-IO流(一)java的IO通过java.io包下的类和接口来支持,在该包下主要有输入、输出两种IO流,每种输出、输入流又可分为字节流和字符流。此外,Java的IO流使用了一种装饰器设计模式,将IO流分成底层节点流和上层处理流,其中
2020-11-01
下一篇 
Java类加载篇 Java类加载篇
Java类加载这部分知识比较深入底层,将重点介绍类加载和反射,会提到JDK动态代理、AOP,反射等诸多知识点。当调用Java命令允许程序时,该命令会启动多个线程,它们都处于该Java虚拟机进程里。所有线程、变量处于同一个进程里,它们都使用J
2020-10-18
  目录