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");
}
}