Java 类型与反射
Java的类与Class
- RTTI(Run-Time Type Identification)运行时类型识别
任何对象在任何时刻都可以知道自己是什么类型的(不管你当前是什么类型)。我们可以用Object.getClass
方法获取他的类型(他的Class 对象)。举一个例子:
public class Main {
public static void main(String[] args) {
Item obj = new RPC().getItemById(1);
// class com.github.lazyben.Goods
System.out.println(obj.getClass());
}
}
interface Item { }
class Goods implements Item { }
class RPC {
public Item getItemById(int id){
return new Goods();
}
}
- Class对象是什么?从某种意义上来说,java有两种对象:实例对象和Class对象。每个类的运行时的类型信息就是用Class对象表示的。它包含了与类有关的信息。其实我们的实例对象就通过Class对象来创建的。简单点来说Class对象就是一个类的说明书,JVM根据这个说明书创造类的实例。
总结一下Class是什么:1.它是类的一种 2.对象内容是你创建类的类型信息 3.不能new生产,只有JVM创建。
有了以上的知识后我们就可以更好的理解 instanceof
和 强制类型转换 了。因为在运行时 JVM 清楚地知道该对象到底是什么类型的(它是由哪一份Class“说明书”装配出来的)。我们也可以更好地理解静态变量:它可以理解为属于这个Class对象的一个变量。
类加载与ClassLoader
假如我们有一堆的Class对象说明书Cat
、Dog
、Object
,这些Class对象又是从哪里来的呢?
- 一个类是什么时候被加载的?答案是在第一次被使用的时候,举一个例子:
package com.github.lazyben;
public class Main {
public static void main(String[] args) {
new WhiteCat();
}
}
class Animal{}
class Cat extends Animal{}
class WhiteCat extends Cat{}
我们使用-verbose:class
打印出类加载过程,并用grep lazyben
过滤出自己的类(方便查看)。这里很容易看出类是从target
目录下来的,且加载顺序符合类的加载顺序。java自带的包在不同的地方,同样的我们可以grep Object一探究竟。
java -verbose:class -classpath /Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/......./target/classes com.github.lazyben.Main | grep lazyben
// 打印出以下的信息
[Loaded com.github.lazyben.Main from file:/Users/lazyben/..../target/classes/]
[Loaded com.github.lazyben.Animal from file:/Users/lazyben/..../target/classes/]
[Loaded com.github.lazyben.Cat from file:/Users/lazyben/..../target/classes/]
[Loaded com.github.lazyben.WhiteCat from file:/Users/lazyben/..../target/classes/]
下图是类的加载过程,经过下面几个过程,我们就将一个Class对象变到类内存里面,使得我们可以根据这份说明书去创造实例对象。
-
那么Class对象是被谁加载出来的?
Classloader
负责从外部系统中加载⼀个类。Classloader
是一个加载类的对象。加载一个类时,这个类对应的Java⽂件并不⼀定需要存在,这个字节码(.class文件)也不⼀定需要存在:文件的本质就是字节流,因此我们可以随便去哪获得这传字节流。而且我们可以动态生成。这是Java世界丰富多彩的应⽤的基⽯:整个Spring就是基于此。 -
ClassLoader的双亲委派加载模型
如果一个人写了一个恶意的java.lang.String
做一些坏事,如何避免呢?答案就是双亲委派加载模型。ClassLoader.loadClass
方法加载一个类。我们打开其源代码,可以看到在加载一个类的时候,先会去看其父亲是否存在,如果存在则会拿到他的父亲加载的这个类。如果父亲没加载,它才会自己尝试去加载它。所以一些最基本的类都会在最开始由双亲委派加载模型最顶端的启动类加载器加载,不会轮到下面的类加载器加载。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
}
// .......
return c;
}
}
- 自定义类加载器
一般来说,要实现自定义的类加载器,需要完成以下2个步骤:- 如果类名对应的字节码文件存在,则将它读取成为字节数组
1.1 调用ClassLoader.defineClass()方法将字节数组转化为Class对象 - 如果类名对应的字节码文件不存在,则抛出ClassNotFoundException
- 如果类名对应的字节码文件存在,则将它读取成为字节数组
public class MyClassLoader extends ClassLoader {
// 存放字节码文件的目录
private final File bytecodeFileDirectory;
public MyClassLoader(File bytecodeFileDirectory) {
this.bytecodeFileDirectory = bytecodeFileDirectory;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
final File file = new File(bytecodeFileDirectory, name + ".class");
if (file.exists()) {
try {
final byte[] bytes = Files.readAllBytes(file.toPath());
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
e.printStackTrace();
}
}
throw new ClassNotFoundException(name);
}
public static void main(String[] args) throws Exception {
File projectRoot = new File(System.getProperty(...);
MyClassLoader myClassLoader = new MyClassLoader(projectRoot);
Class testClass = myClassLoader.loadClass(...);
Object testClassInstance = testClass.getConstructor().newInstance();
String message = (String) testClass.getMethod(...).invoke(testClassInstance);
}
}
反射
用一句话概括反射:它是运行时行为,动态调用。如何根据参数动态创建⼀个对象?如何根据参数动态调⽤⼀个⽅法?如何根据参数动态获取⼀个属性?答案都是反射。这意味着我们有了无与伦比的灵活性。但是一般来说反射的性能比较差,因为JDK无法预测被调用的方法,无法实施优化。
根据参数动态创建⼀个对象,其中forName
根据一个类名(全限定类名)返回一个Class对象。
Class klass = Class.forName(args[0]);
Object obj = klass.getConstructor().newInstance();
根据参数动态调⽤⼀个⽅法
Cat cat = new Cat();
cat.getClass().getMethod(args[0]).invoke(cat);
根据参数动态获取⼀个属性
Cat cat = new Cat();
cat.getClass().getField(args[0]).get(cat);
- 一个小实战:使用反射实现一个Java Bean到Map的转换器,传入一个遵守Java Bean约定的对象,读取它的所有属性,存储成为一个Map。基本思想:
- 遍历map中的所有键值对,寻找klass中名为setXXX,且参数为对应值类型的方法(即setter方法)
- 使用反射创建klass对象的一个实例
- 使用反射调用setter方法对该实例的字段进行设值
public class MapBeanConverter {
public static Map<String, Object> beanToMap(Object bean) {
return Stream.of(bean.getClass().getDeclaredMethods())
.filter(MapBeanConverter::isJavaBeanMethod)
.collect(Collectors.toMap(MapBeanConverter::getMethodName, method -> getMethodReturnValue(method, bean)));
}
private static Object getMethodReturnValue(Method method, Object bean) {
try {
return method.invoke(bean);
} catch (IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
return null;
}
private static String getMethodName(Method method) {
String methodName = method.getName();
String name = methodName.startsWith("get") ? methodName.substring(3) : methodName.substring(2);
return Character.toLowerCase(name.charAt(0)) + name.substring(1);
}
private static boolean isJavaBeanMethod(Method method) {
final String methodName = method.getName();
boolean isStartWithGet = methodName.startsWith("get") && methodName.length() > 3;
boolean isStartWithIs = methodName.startsWith("is") && methodName.length() > 2;
return (isStartWithGet || isStartWithIs)
&& (method.getParameterCount() == 0)
&& Character.isUpperCase(methodName.charAt(isStartWithGet ? 3 : 2));
}
public static <T> T mapToBean(Class<T> klass, Map<String, Object> map) {
try {
final T instance = klass.getConstructor().newInstance();
map.forEach((key, value) -> {
String methodName = "set" + Character.toUpperCase(key.charAt(0)) + key.substring(1);
try {
klass.getMethod(methodName, value.getClass()).invoke(instance, value);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
});
return instance;
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e ) {
e.printStackTrace();
}
return null;
}
public static class DemoJavaBean {
private Integer id;
private String name;
private final String privateField = "privateField";
public int isolate() {
System.out.println(privateField);
return 0;
}
public String is() { return ""; }
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public String getName() { return name; }
public String getName(int i) { return name + i; }
public void setName(String name) { this.name = name; }
public boolean isLongName() { return name.length() > 10; }
}
public static void main(String[] args) {
DemoJavaBean bean = new DemoJavaBean();
bean.setId(100);
bean.setName("AAAAAAAAAAAAAAAAAAA");
System.out.println(beanToMap(bean));
Map<String, Object> map = new HashMap<>();
map.put("id", 123);
map.put("name", "ABCDEFG");
System.out.println(mapToBean(DemoJavaBean.class, map));
}
}