第18章:类加载机制与反射

18.1 类的加载、连接和初始化

使用类时,都会经历加载、连接、初始化三个步骤

18.1.1 jvm和类
  1. java命令运行某java程序,操作系统中会启动一个java虚拟机进程,无论该java程序多么复杂,该程序包含了多少线程,它们都处于java虚拟机进程里。同一个jvm的所有线程、所有变量都处于同一个进程中,它们都使用该jvm进程的内存区。
  2. jvm终止的情况
    1. 程序运行到最后正常结束
    2. System.exit()、Runtime.getRuntime().exit()
    3. 程序执行过程中遇到未捕获异常或错误
    4. 操作系统强制结束jvm进程(kill -9)
  3. jvm进程结束,该进程内存中状态丢失
18.1.2 类的加载
  1. 类的加载:将类的class文件读入内存,并为之创建一个java.lang.Class对象,同一个类(全限定类名+加载器表示同一个类)只能被加载一次
  2. Class是类的抽象,可以通过这个Class对象获取到这个类中定义的内容。类是对某一类对象的抽象,Class是对类的抽象
  3. 类的加载由类加载器完成,类加载器通常由JVM提供,JVM提供的类加载器通常称为系统类加载器,开发者也可以通过继承ClassLoader基类创建自己的类加载器
18.1.3 类的连接
  1. 类的连接:将类的二进制数据合并到JRE中。又分为如下三个阶段
    1. 验证:检验被加载的类是否有正确的内部结构,并和其他类协调一致
    2. 准备:为类的类变量分配内存,设置默认值
    3. 解析:将类的二进制数据中的符号引用替换成直接引用
18.1.4 类的初始化
  1. 类的初始化:就是对类变量赋值,之前都是默认值
  2. 类初始化之前必须先加载和连接
  3. 如果类的直接父类还没被初始化,先初始化其父类
  4. 最后使用类中初始化语句对类变量赋值
18.1.5 类初始化时机
18.1.5.1 类初始化时机
  1. 创建类实例时
    1. new
    2. 反射
    3. 序列化
  2. 调用某类静态方法、静态变量
  3. 使用反射创建该类对应的Class对象时:Class.forName(“Person”)
  4. 初始化子类
  5. java.exe运行主类
18.1.5.2 特殊情况
  1. 如果类的静态成员变量为宏变量,使用它时,不会初始化类,因为对JVM来讲他是个常量
  2. 调用类加载器的loadClass来加载类时,不会初始化。Class.forName时才初始化

18.2 类加载器

18.2.1 类加载器简介
  1. 一旦一个类被加载到JVM中,同一个类不会再次加载
  2. 什么叫同一个类:Java中用全限定类名表示唯一一个类,而JVM中,使用全限定类名+类加载器表示唯一一个类。例如pg包中有Person类,是用ClassLoader的示例k1加载的,那么Person对应的Class对象在内存中表示为(Person,pg,k1),它与(Person,pg,k2)是不同的
  3. JVM启动时,形成由三个类加载器组成的初始类加载器层次结构
    1. (根)类加载器
    2. (扩展)类加载器
    3. (系统)类加载器
  4. 根类加载器
//不是ClassLoader子类
//负责加载$JAVA_HOME/jre/lib、$JAVA_HOME/jre/lib/classes下的JAR包和类
//指定根加载器加载指定附加的类
-Xbootclasspath
-Dsun.boot.class.path   
  1. 扩展类加载器
//负责加载JRE的扩展目录($JAVA_HOME/jre/lib/ext)或系统属性"java.ext.dirs"所指定的目录下的JAR包和类
  1. 系统类加载器
//加载-classpath、CLASSPATH环境变量、java.class.path系统属性所指定的JAR包和类
18.2.2 类加载机制
  1. 全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖和引用的其他Class也由该类加载器加载,除非显示用另外一个类加载器加载
  2. 父类委托:
    1. 概念:如果一个类加载器收到了类加载器的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父加载器去完成,每个层次的类加载器都是如此,因此所有的加载请求最终都会传送到根类加载器中.只有父类加载反馈自己无法加载这个请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
    2. 优点:java类随着它的加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jart之中.无论哪一个类加载器都要加载这个类,最终都是双亲委派模型最顶端的Bootstrap类加载器去加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型.由各个类加载器自行去加载的话,如果用户编写了一个称为“java.lang.Object”的类.并存放在程序的ClassPath中,那系统中将会出现多个不同的Object类。java类型体系中最基础的行为也就无法保证,应用程序也将会一片混乱.
  3. 缓存机制:所有被加载过的Class都被缓存,当需要使用某Class,先从缓存中搜索该Class,缓存区中不存在该Class对象,系统才读取该类对应的二进制文件。所以修改Class后,必须重启JVM才生效
18.2.3 类加载器之间的关系
public class ClassLoaderPropTest {
	public static void main(String[] args) {
		ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
		//getParent方法获取到的不是继承意义上的父加载器,而是加载类时,父类委托的加载机制所使用的父加载器
		ClassLoader extensionLader = systemLoader.getParent();
		ClassLoader rootLader = extensionLader.getParent();
		//1. 系统类加载器:AppClassLoader
		System.out.println(systemLoader);
		//2. 扩展类加载器:ExtClassLoader
		System.out.println(extensionLader);
		//3. AppClassLoader、ExtClassLoader都继承了ClassLoader,AppClassLoader、ExtClassLoader本身没继承关系
		//4. 扩展类加载器的父加载器是根类加载器,但根类加载器不是用java实现,所以返回null
		System.out.println(rootLader);
	}
}

18.2.4 创建并使用自定义的类加载器
  1. 使用自定义类加载器执行java程序
//CompileClassLoader为自定义的类加载器,实际上就是一个类,这个类有main方法, 需传入两个参数,一个为该类加载器要加载的类名A,以及A的main方法中的参数
//Hello为类A
//"疯狂java讲义"是为main方法中形参列表的字符串数组传值
java CompileClassLoader Hello "疯狂java讲义"
  1. 可以通过继承ClassLoader,并重写其内方法, 来自定义类加载器
//根据指定名称加载类,返回对应Class对象,里面调用了findClass方法
loadClass(String name,boolean resolve)
//根据指定名称来查找类
findClass(String name)
//创建自定义类加载器时一般重写findClass方法而不重写loadClass,避免覆盖默认类加载器的父类委托与缓冲机制两种策略
  1. 自定义类加载器通常可以实现如下功能
    1. 运行前先编译java源文件,从而无需编译,直接执行
    2. 执行代码前,自动验证数字签名
    3. 根据用户提供的代码解密代码,从而可以实现代码混淆器来避免反编译*.class文件
    4. 根据用户需求,动态加载类
    5. 根据用户需求,把其他数据以字节码形式加载到应用中
18.2.5 使用URLClassLoader从本地或远程主机加载类
package wusihan_0102_ClassLoader;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.sql.SQLException;

public class URLClassLoaderTest {
	public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, SQLException,
			InstantiationException, IllegalAccessException, SecurityException, NoSuchMethodException,
			IllegalArgumentException, InvocationTargetException {
	    //这个urls相当于为该加载器配置classpath,路径一定要写最后一个/
	    //file前缀表示从本地文件系统加载类,如果为http前缀表示从互联网加载类
		URL[] urls = { new URL("file:D:/workspace/wusihan_0100_DailyTest/bin/") };
		//可以使用如下构造器,指定URLClassLoader的父加载器
		//URLClassLoader ul = new URLClassLoader(urls,ClassLoader.getSystemClassLoader());
		URLClassLoader ul = new URLClassLoader(urls);
		//可以不用在classpath中添加该类所在路径就可以使用该类
		Method m = ul.loadClass("com.wsh.object.Animal").getMethod("beat");
		m.invoke(ul.loadClass("com.wsh.object.Animal").newInstance(), args);
	}
}

18.3 通过反射查看类信息

Java程序中的对象运行时出现两种类型,一种是运行时类型,另一种是编译时类型。程序如果想调用该对象运行时类型的变量、方法,有以下两种途径

  1. 编译时知道运行时类型:使用instanceof判断是否可以转换,再使用强制类型转换将变量转换为其运行时类型
  2. 编译时不知道运行时类型:使用反射
18.3.1 获取Class对象
//1.使用Class自身静态方法,传入的字符串必须是全限定类名
//这种方法适用于程序只能获取类名对应的字符串
Class a = Class.forName("com.wsh.object.Animal");
//2.使用某个类的class属性
//此种方法相对于第1种更安全(因为类不存在时,编译就会报错),效率更高(不用调用方法)
Class b = URLClassLoaderTest.class;
//3.使用某个对象的getClass()方法
Animal c = new Animal();
Class d = c.getClass();
18.3.2 从Class中获取信息

以下都是Class类的方法,获取Class代表的类的信息

  1. 获取构造器
//返回此Class对象对应类的、带指定形参列表parameterTypes的public构造器
Constructor<T> getConstructor(Class<?> ... parameterTypes)
//返回此Class对象对应类的所有public构造器
Constructor<?>[] getConstructors()
//返回此Class对象对应类的、带指定形参列表parameterTypes的与访问权限无关的构造器
Constructor<T> getDeclaredConstructor(Class<?> ... parameterTypes)
Constructor<?>[] getDeclaredConstructors()
  1. 获取方法
//name为方法名,parameterTypes为形参类型
Method getMethod(String name,Class<?> ... parameterTypes)
Method[] getMethods()
Method getDeclaredMethod(String name,Class<?> ... parameterTypes)
Method[] getDeclaredMethods()
  1. 获取成员变量
Field getField(String name)
Field[] getFields()
Field getDeclaredField(String name)
Field[] getDeclaredFields()
  1. 获取Annotation:见14章
  2. 获取内部类
Class<?>[] getDeclaredClasses()
  1. 获取外部类
Class<?> getDeclaringClass()
  1. 获取实现的接口
Class<?>[] getInterfaces()
  1. 获取继承的父类
Class<? super T> getSuperclass()
  1. 获取修饰符
//获取类或接口的所有修饰符,返回的整数需要用Modifier工具类解码才能获取真实的修饰符
int getModifiers()
  1. 获取类所在包
Package getPackage()
  1. 获取全限定类名
String getName()
  1. 获取类名简称
String getSimpleName()
  1. 判断该Class对象对应类是否为接口、枚举、注解等
//是否为注解
boolean isAnnotation()
//该Class是否被注解annotationClass修饰
boolean isAnnotationPresent(Class<? extends Annotation> annotationClass)
//是否为匿名类
boolean isAnonymousClass()
//是否为数组类
boolean isArray()
//是否为枚举类
boolean isEnum()
//是否为接口
boolean isInterface()
//判断obj是否为该Class的实例,等同于obj instanceof Object
boolean isInstance(Object obj)

18.4 使用反射生成并操作对象

18.4.1 创建对象
Class a = Class.forName("wusihan_0102_ClassLoader.ClassTest");
//1.通过Class对象的newInstance()调用默认构造器创建对象
ClassTest b= (ClassTest) a.newInstance();
//2.通过Class获取Constructor对象,再使用Constructor类的newInstance方法创建对象,这种方式可以使用有参构造器创建对象
Constructor c = a.getConstructor();
ClassTest d = (ClassTest) c.newInstance();
18.4.2 调用方法
Class<?> a = Class.forName("wusihan_0102_ClassLoader.ClassTest");
Object b = a.newInstance();
Method m = a.getMethod("test", String.class);
//Method的invoke方法,第一个参数为调用该方法的对象,第二个参数为该方法形参列表的实际参数值
m.invoke(b, "参数含");
18.4.3 访问成员变量值
Animal an = new Animal();
Class<Animal> a = Animal.class;
//成员变量age在Animal类中为private修饰,想获取private属性的成员,需要用getDeclaredField方法
Field f = a.getDeclaredField("age");
//由于age为private,如果想修改其对应值,需要用setAccessible方法取消其访问权限
//Method、Constructor、Field都有setAccessible方法
f.setAccessible(true);
//getXxx(Object obj)方法,获取obj对象的该成员变量的值,如果为引用类型,省略Xxx
System.out.println(f.get(an));
//setXxx(Object obj,Xxx val):为obj对象的成员变量赋值为val
f.setInt(an, 60);
System.out.println(f.get(an));
18.4.4 操作数组
//等同于String[] arr = new String[2];
Object arr = Array.newInstance(String.class, 2);
//等同于arr[0]="df";
Array.set(arr,0,"df");
//等同于System.out.println(arr[0])
System.out.println(Array.get(arr, 0));

18.5 使用反射生成JDK动态代理

参考Spring中的AOP,以及设计模式中代理模式

18.6 反射和泛型

18.7.1 泛型和Class类
  1. 使用Class泛型,避免强制类型转换
import java.util.Date;

public class CrazyitObjectFactory2 {
	public static Object getInstance(Class cls){
		try {
			return cls.newInstance();
		} catch (InstantiationException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		}
		return null;
	}
	public static void main(String[] args) {
		//1. 由于getInstance返回类型为Object型,所以需要强制转换
		Date d = (Date) CrazyitObjectFactory2.getInstance(Date.class);
		//2. 可以这样考虑问题,如果不想强制转换,那么方法签名应改为泛型方法,如下
		//public static <T> T getInstance(Class cls)
		//3. 考虑这个T应该如何得到,既可以满足上面,又可以保证newInstance方法,可以返回这个T类型,自然想到newInstance用到了Class类中定义的泛型,	因此可以考虑使用传入的Class对象的泛型,最终变为如下
		//public static <T> T  getInstance(Class<T> cls)
		//4. 这样以后,该方法不再需要强制转换
	}
}

18.7.2 使用反射获取泛型信息
package wusihan_0102_ClassLoader;

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Map;

public class GenericTest {
	private Map<String ,Integer> score;
	public static void main(String[] args) throws NoSuchFieldException, SecurityException {
		Class<GenericTest> clazz = GenericTest.class;
		Field f = clazz.getDeclaredField("score");
		//1. Field的方法,获取成员变量类型
		Class<?> a = f.getType();
		//interface java.util.Map
		System.out.println(a);
		//2. Field的方法,获取成员变量的带泛型的类型
		Type gType = f.getGenericType();
		//java.util.Map<java.lang.String, java.lang.Integer>
		System.out.println(gType);
		if(gType instanceof ParameterizedType){
			//3. ParameterizedType就代表了一个带泛型的类型,用它可以将该类型,拆成原始类型和其泛型的类型
			ParameterizedType pType = (ParameterizedType)gType;
			//4. ParameterizedType方法,获取原始类型
			Type rType = pType.getRawType();
			//interface java.util.Map
			System.out.println(rType);
			//5. ParameterizedType方法,获取泛型类型数组
			Type[] tArgs = pType.getActualTypeArguments();
			for(Type tArg:tArgs){
				//class java.lang.String
				//class java.lang.Integer
				System.out.println(tArg);
			}
		}
	}
}

发布了32 篇原创文章 · 获赞 0 · 访问量 940

猜你喜欢

转载自blog.csdn.net/hanzong110/article/details/102611812