Java--运用反射

Java在需要使用某个类的时候才会将.class文档进行载入,在JVM产生java.lang.Class实例代表该文档,从Class实例开始,就可以获得类的许多类型。.class文档反应了类的基本信息,因而从Class等API取得类信息的方式就称为反射。

Class与.class文档

Java真正需要某个类时才会加载对应的.class文档,而非程序启动的时候加载所有的类,因为大多数用户只会使用应用程序部分资源,在需要某些功能时才加载对应资源,可以让系统资源应用更有效率。

java.lang.Class的实例代表Java应用程序运行时加载的.class文档,Class类没有公开的构造函数,实例是由JVM自动产生的,每个.class文档加载时,JVM会自动生成Class对象。

可以通过Object的getClass方法,或是通过.class常量取得每个对象对应的Class对象。如果是基本类型,可以使用其对应的打包器类加上.TYPR取得Class对象。例如Integer.TYPE可取得int的Class对象,如果要取的Integer.class文档的Class,可以使用Integer.class。

在取得类的Class对象后,就可以操作它的公开方法取得类的基本信息,比如下面的String类:

import static java.lang.System.out;

public class ClassInfo {
    public static void main(String[] args){
        Class stringClass = String.class;

        out.println("类的名称:" + stringClass.getName());
        out.println("是否为接口:" + stringClass.isInterface());
        out.println("是否为基本类型:" + stringClass.isPrimitive());
    }
}

Java只有在真正使用类时才会加载.class文档,也就是要使用指定类生成对象时。使用类声明参考名称并不会加载.class文档。我们来看一个测试类:

import static java.lang.System.out;

public class Some {
    static {
        out.println("加载.class文档");
    }
}
import static java.lang.System.out;

public class SomeDemo {
    public static void main(String[] args){
        Some s;

        out.println("声明参考类型");

        s = new Some();

        out.println("生成Some实例");
    }
}

如上面的代码,我们先构造一个Some类,在里面定义好static区块,当首次加载.class文档时,会默认执行静态区块。运行第二个程序之后我们会发现当类声明参考名称时不会载入.class文档,只有生成类实例时,才会执行static区块。

类信息是在编译时期存储在.class 文档中,这是Java支持执行运行时类型识别的方式。编译时期若使用到相关的类,编译程序会检查对应的.class文档中记载的信息,以确定是否可以完成编译。执行时期使用某类时,会先检查是否有对应的Class对象,如果没有,会加载对应的.class文档并生成对应的Class实例。

默认的JVM只会使用一个Class实例来代表一个.class文档,每个类的实例都会知道自己由哪个Class实例生成。默认使用getClass方法和.class生成的Class实例是同一个对象,比如下面的代码会返回true:

System.out.println("".getClass() == String.class);

使用Class.forName()

在某些应用中,无法事先知道开发人员要使用哪个类,因而必须让开发人员可以事后指定类名称来动态加载类。

可以使用Class.forName()来实现动态加载类,可用字符串指定类名称来获得类相关的信息。

Class.forName()在找不到指定的类时会抛出ClassNotFoundException异常。

Class.forName()的另一版本可以指定类名称,加载类时是否执行静态区块与类加载器:

static Class forName(String name, boolean initialize, ClassLoader loader)

在这个版本中将initialize设置为false,这样加载.class文档时并不会立即执行static区块,而是在建立类实例时才执行static区块。例如:

import static java.lang.System.out;

class Some2{
    static {
        out.println("[执行静态区块]");
    }
}

public class SomeDemo2 {
    public static void main(String[] args) throws ClassNotFoundException{
        Class aClass = Class.forName("Some2", false, SomeDemo2.class.getClassLoader());
        out.println("已载入Some2.class");

        Some2 some2;
        out.println("声明Some2参考名称");

        some2 = new Some2();
        out.println("生成Some2实例");
    }
}

这个版本的Class.forName()需要一个类加载器,我们可以通过取得SomeDemo2.class文档的Class实例后在使用getClassLoader方法,取得加载SomeDemo2的类加载器,最后传递给Class.forName()使用。

事实上,如果使用第一个版本的Class.forName()方法,等同于:

Class.forName(className, true, currentLoader);

其中currentLoader是目前类的类加载器。关于类加载器咱们下次再说。


从Class获得信息

Class对象代表加载的.class文档,取得Class对象之后,就可以取得.class文档中记载的信息,像是包,构造函数,方法成员,数据类型。每个类型都会有对应的类型。例如要取得指定的String类的包名称,可以这样:

Package package = String.class.getPackage();
System.out.println(package.getName);     //显示java.lang

我们可以分别取回数据成员,构造函数与方法成员。代码如下:

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

import static java.lang.System.out;

public class ClassViewer {
    public static void showPackageInfo(Class clz){
        Package p = clz.getPackage();
        out.printf("package %s\n", p.getName());
    }

    public static void showFiledsInfo(Class clz) throws SecurityException{
        //取得声明的数据成员代表
        Field[] fields = clz.getDeclaredFields();

        for(Field field : fields){
            //显示修饰权限
            out.printf("%s %s %s", Modifier.toString(field.getModifiers()),
                    field.getType().getName(), field.getName());
        }
    }

    public static void view(String aClass) throws ClassNotFoundException{
        Class aClass1 = Class.forName(aClass);

        showPackageInfo(aClass1);

        out.println("{");
        showFiledsInfo(aClass1);
        out.println("}");
    }

    public static void main(String[] args){
        try {
            ClassViewer.view(args[0]);
        }catch (ArrayIndexOutOfBoundsException e){
            e.printStackTrace();
        }catch (ClassNotFoundException e){
            e.printStackTrace();
        }
    }
}

从Class建立对象

如果知道类名称,可以使用new关键字建立实例,如果事先不知道类名称呢?我们可以先使用Class.forName()动态加载.class文档,取得Class对象之后,使用其newInstance()建立类实例。如:

Class aClass = Class.forName(args[0]);
Object object  = aClass.newInstance();

如果实际加载类定义了无参数构造函数,就可以使用这种方式创建对象。为何会有事先不知道类名又建立类实例的需求?例如,你想采用影片链接库来播放动画,然而负责操作影片链接库的部门迟迟未动工,怎么办?可以利用接口定义出影片链接库该有的功能。如:

public interface Player{
    void play(String video);
}

可以要求操作影片链接库的部门,必须操作Player完成你想要的功能,而你可以先完成你的动画播放:

import java.util.Scanner;

public class MediaMaster {
    public static void main(String[] args) throws ClassNotFoundException,
            InstantiationException, IllegalAccessException{
        String playerImp1 = System.getProperty("PlayerImp1");
        Player player = (Player) Class.forName(playerImp1).newInstance();

        System.out.println("输入想要播放的影片:");
        player.play(new Scanner(System.in).nextLine());
    }
}

在这个程序中,没有写死操作Player的类名称,这可以在程序启动时,通过系统属性PlayerImp1指定。如操作Player的类名称为ConsolePlayer,而其操作如下:

public class ConsolePlayer implements Player{
    @Override
    public void play(String video){
        System.out.println("正在播放" + video);
    }
}

若类定义有多个构造函数,也可以指定使用哪个构造函数生成对象,这必须在调用Class的getConstructor方法时指定参数类型,取得代表构造参数的Constructor对象,再利用Constructor的newInstance方法指定创建时的参数值来建立对象。例如:需要动态加载java.lang.List操作类,只知道操作类会有一个int的构造函数,可以指定List的初始容量,则可以这样创建:

Class aClass = Class.forName(args[0]);  //取得.class文档
Constructor constructor = aClass.getConstructor(Integer.TYPE);    //取得构造函数
List list = (List) constructor.newInstance(100);              //利用构造函数建立实例

反射API有许多方法都接受不定长度自变量。

若要动态生成数组,必须使用java.lang.reflect.Array的newInstance方法。如以下动态生成长度为10的数java.lang.ArrayList组:

Class aClass = java.util.ArrayList.class;
Object[] objects = Array.newInstance(aClass, 10); 
objects[0] = new ArrayList();
ArrayList arrayList = objects[0];

为什么使用Array.newInstance()建立数组实例?因为以上程序片段,objects参考的数组实例,每个索引处都是ArrayList类型,而不是Object类型。我们来看一个例子:

import java.lang.reflect.Array;

public class Student<E> {
    private Object[] elems;

    public Student(int capacity){
        elems = new Object[capacity];
    }

    public Student(){
        this(16);
    }

    public E[] toArray(){
        E[] elements = null;

        if(elems.length > 0){
            elements = (E[]) Array.newInstance(elems[0].getClass(), elems.length);
        }

        for(int i = 0; i < elems.length; i++){
            elements[i] = (E)elems[i];
        }

        return elements;
    }
}

在调用toArray()时,如果ArrayList收集对象长度不为0,可以从第一个索引取得被收集对象实际的Class实例,此时就可以用它配合Array.newInstance()建立数组实例。


操作对象方法与成员

java.lang.reflect.Method实例是方法的代表对象,可以使用invoke方法动态调用指定的方法:例如有个Students类:

public class Students {
    private String name;
    private Integer score;

    public Students(){};

    public Students(String name, Integer score){
        this.name = name;
        this.score = score;
    }

    public void setName(String name){
        this.name = name;
    }

    public String getName(){
        return name;
    }

    public void setScore(Integer score){
        this.score = score;
    }

    public Integer getScore(){
        return score;
    }
}

以下程序片段可以动态生成Students实例,并通过setName方法设定名称,用getName方法取得名称:

Class aClass = Class.forName("Students");
Constructor constructor = aClass.getConstructor(String.class, Integer.TYPE);
Object object = constructor.newInstance("Justin", 90);

//指定方法名称与参数类型,调用getMethod方法取得对应的公开的Method实例
Method setter = aClass.getMethod("setName", String.class);

//指定参数值调用对象object的方法
setter.invoke(object, "justin");
Method getter = aClass.getMethod("getName");
out.println(getter.invoke(object));

接下来,让我们设计一个BeanUtil类,可以指定Map对象与类名称调用getBean方法,这个方法会抽取Map的内容并封装为指定类的实例。例如如果Map收集了学生的信息,那么getBean方法返回的就是Student实例,如下:

Map<String, Object> data = new HashMap<>();

data.put("name", "justin");
data.put("score", 90);

Student student = (Student)Beanutil.getBean(data, Student);

ou.printf("%s %d\n",studnet.getName(), student.getScore);

完整的代码如下:

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Map;

public class BeanUtil {
    public static <T> T getBean(Map<String, Object> data, String className) throws Exception{
        Class aClass = Class.forName(className);
        Object object = aClass.newInstance();

        data.entrySet().forEach(entry ->{
            String setter = String.format("set%s%s", entry.getKey().substring(0, 1).toUpperCase(),
                    entry.getKey().substring(1));

            try {
                Method method = aClass.getMethod(setter, entry.getValue().getClass());
                if(Modifier.isPublic(method.getModifiers())){
                    method.invoke(bean, entry.getValue());
                }
            }catch (IllegalAccessException | IllegalArgumentException | 
                    NoSuchMethodException | SecurityException | InvocationTargetException e){
                throw new RuntimeException(e);
            }
        });

        return (T) bean;
    }
}

也可以使用反射机制存取类数据成员,有兴趣的同学下去了解吧。


代理

在反射API中有个Proxy类,可动态建立接口的操作对象。我们先来看一个例子:如果需要在执行某些方法时进行日志记录,我们可能会这样攥写:

import java.util.logging.Level;
import java.util.logging.Logger;

import static java.lang.System.out;

public class HelloSpeaker {
    public void Hello(String name){
        //方法开始前进行日志记录
        Logger.getLogger(HelloSpeaker.class.getName()).log(Level.INFO,
                "Hello()方法开始...");

        //程序主要功能
        out.println("hello, %s\n", name);

        //方法执行完毕前留下日志

        Logger.getLogger(HelloSpeaker.class.getName()).log(Level.INFO,
                "Hello()方法结束...");
    }
}

我们可以看到,将日志功能写进了HelloSpeaker类中的Hello方法,虽然这样完成了需求,但是如果一个程序中到处都需要这种日志服务,难道到处写日志记录代码?这对以后的代码维护是非常不便的,若有天我们不再需要日志程序代码,就必须找出进行日志程序代码加以删除,无法简单的将日志服务从既有的程序中移去。

可以使用代理机制来解决这个问题,在这里讨论两种代理方式:静态代理和动态代理。


静态代理

在静态代理当中,代理对象与被代理对象必须实现同一接口,在代理对象中可以实现日志服务,必要时调用代理对象,这样被代理对象就可以仅攥写本身应有的职责,例如可以定义一个Hello接口:

public interface Hello {
    void hello(String name);
}

如果有个HelloSpeaker类操作了Hello接口:

public class HelloSpeaker implements Hello{
    public void hello(String name){
        System.out.printf("hello %s\n", name);
    }

在这个类中没有任何日志程序代码,日志程序代码会放至代理对象中,代理对象同样也要操作Hello接口:

import java.util.logging.Level;
import java.util.logging.Logger;

public class HelloProxy implements Hello{
    private Hello helloObj;

    public HelloProxy(Hello helloObj){
        this.helloObj = helloObj;
    }

    public void hello(String name){
        Logger.getLogger(HelloSpeaker.class.getName()).log(Level.INFO,
                "Hello()方法开始...");

        helloObj.hello(name);

        Logger.getLogger(HelloSpeaker.class.getName()).log(Level.INFO,
                "Hello()方法结束...");
    }
}

在HelloProxy类的hello方法中,真正调用Hello接口的hello方法前后可以安排日志程序代码。可以这样使用代理对象:

Hello proxy = new HelloProxy(new HelloSpeaker());
proxy.hello("justin");

创建代理对象HelloProxy必须指定被代理对象HelloSpeaker,代理对象代理HelloSpeaker执行hello方法,在实际调用HelloSpeaker的hello方法前后加上日志,HelloSpeaker在攥写时就不用加上日志,可以专心做自己的事情。

但是,静态代理必须为个别接口操作出个别代理类,在应用程序行为复杂时,多个接口就必须定义多个代理对象,也是很麻烦的。


动态代理

反射API中提供动态代理相关类,可让你不必为特定接口操作特定代理对象。使用动态代理机制,可以使用一个处理者代理多个接口的操作对象。

处理者类必须操作java.lang.reflect.InvocationHandler接口,例如:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.logging.Level;
import java.util.logging.Logger;

public class LoggingHandler implements InvocationHandler{
    private Object target;

    public Object bind(Object target){
        this.target = target;

        //动态建立代理对象
        return Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(), this);
    }

    //代理对象的方法被调用时会调用此方法
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
        Object result = null;

        try {
            log(String.format("%s() 呼叫开始...", method.getName()));

            result = method.invoke(target, args);

            log(String.format("%s() 呼叫结束...", method.getName()));
        }catch (IllegalAccessException | IllegalArgumentException | 
                InvocationTargetException e){
            log(e.toString());
        }

        return result;
    }

    private void log(String message){
        Logger.getLogger(LoggingHandler.class.getName()).log(Level.INFO, message);
    }
}

主要概念是使用Proxy.newProxyInstance()建立对象代理,调用时必须指定类加载器,告知要代理的接口,以及接口上定义方法被调用时的处理者。Proxy.newProxyInstance()底层会使用原生方式生成代理对象的Class实例,并利用它来生成代理对象,代理对象会操作指定要代理的接口。

如果操作Proxy.newProxyInstance()返回的代理对象,在每次操作时会调用处理器数值。可以在invoke方法中实现日志,利用被代理对象,被调用的方法Method与参数值实现被代理对象的职责。

接下来我们用LoggingHandler的bind()来绑定被代理对象:

public class ProxyDemo {
    public static void main(String[] args){
        LoggingHandler loggingHandler = new LoggingHandler();

        Hello helloProxy = (Hello) loggingHandler.bind(new HelloSpeaker());

        helloProxy.hello("justin");
    }
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值