Java Review(三十九、类加载机制与反射
static
{
// 使用静态初始化块为变量 b 指定出初始值
b = 6;
System.out.println("----------");
}
// 声明变量 a 时指定初始值
static int a = 5;
static int b = 9; // ①
static int c;
public static void main(String[] args)
{
System.out.println(Test.b);
}
}
上面代码先在静态初始化块中为 b 变量赋值, 此时类变量 b 的值为 6; 接着程序向下执行, 执行到①号代码处, 这行代码也属于该类的初始化语句, 所以程序再次为类变量 b 赋值。 也就是说, 当 Test 类初始化结束后, 该类的类变量 b 的值为 9。
JVM 初始化一个类包含如下几个步骤:
假如这个类还没有被加载和连接, 则程序先加载并连接该类。
假如该类的直接父类还没有被初始化,则先初始化其直接父类
假如类中有初始语句 , 则系统依次执行这些初始化语句
[](()类初始化的时机
当 Java 程序首次通过下面 6 种方式来使用某个类或接口时, 系统就会初始化该类或接口:
创建类的实例。 为某个类创建实例的方式包括: 使用 new 操作符来创建实例, 通过反射来创建实例, 通过反序列化的方式来创建实例。
调用某个类的类方法(静态方法)。
访问某个类或接口的类变量, 或为该类变量赋值。
使用反射方式来强制创建某个类或接口对应 的 java.lang.Class 对 象 。 例 如 代 码 : Class.forName(MPerson"), 如果系统还未初始化 Person 类, 则这行代码将会导致该 Person 类被初始化, 并返回 Person 类对应的 java.lang.Class 对象。
初始化某个类的子类。 当初始化某个类的子类时, 该子类的所有父类都会被初始化。
直 接 使 用 java.exe 命令来运行某个主类。 当运行某个主类时, 程序会先初始化该主类。
[](()类加载器
======================================================================
类加载器负责将.class 文件( 可能在磁盘上, 也可能在网络上) 加载到内存中, 并为之生成对应的 java.lang.Class 对象。
[](()类加载机制
类加载器负责加载所有的类, 系统为所有被载入内存中的类生成 java.lang.Class 实例。
一个类被载入 JVM 中, 同 一个类就不会被再次载入了——正如一个对象有一个唯一的标识一样, 一个载入 JVM 中的类也有一个唯一的标识。 在 Java 中, 一
个类用其全限定类名( 包括包名和类名) 作为标识; 但在 JVM 中, 一个类用其全限定类名和其类加载器作为唯一标识。 例如, 如果在 pg 的包中有一个名为 Person 的类, 被类加载器 ClassLoader 的实例 kl 负责加载, 则该 Person 类对应的 Class 对象在 JVM 中表示为 Person、pg、kl ) 这意味着两个类加载器加载的同名类: (Person 、pg、 kl ) 和( Person、 pg、 kl2) 是不同的, 它们所加载的类也是完全不同、互不兼容的。
当 JVM 启动时, 会形成由三个类加载器组成的初始类加载器层次结构。
根类加载器(Bootstrap ClassLoader):其负责加载 Java 的核心类,比如 String、System 这些类
拓展类加载器(Extension ClassLoader):其负责加载 JRE 的拓展类库
系统类加载器(System ClassLoader):其负责加载 CLASSPATH 环境变量所指定的 JAR 包和类路径
除了可以使用 Java 提供的类加载器之外, 开发者也可以实现自己的类加载器, 自定义的类加载器通过继承 ClassLoader 来实现。
用户类加载器:用户自定义的加载器,以类加载器为父类
类加载器的层次
**双亲委派模型:如上图所示的类加载器之间的这种层次关系,就称为类加载器的双亲委派模型(Parent Delegation Model)。该模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。子类加载器和父类加载器不是以继承(Inheritance)的关系来实现,而是通过组合(Composition)关系来复用父加载器的代码。
双亲委派模型的工作过程为:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的类加载请求都会传给顶层的启动类加载器,只有当父加载器反馈自己无法完成该加载请求(该加载器的搜索范围中没有找到对应的类)时,子加载器才会尝试自己去加载。
**
JVM 的类加载机制主要有如下三种:
全盘负责。 所谓全盘负责, 就是当一个类加载器负责加载某个 Class 时, 该 Class 所依赖的和引用的其他 Class 也将由该类加载器负责载入, 除非显式使用另外一个类加载器来载入。
父类委托。 所谓父类委托, 则是先让 parent (父) 类加载器试图加载该 Class, 只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。
缓存机制。缓存机制将会保证所有加载过的 Class 都会被缓存, 当程序中需要使用某个 Class 时,类加载器先从缓存区中搜寻该 Class, 只有当缓存区中不存在该 Class 对象时, 系统才会读取该类对应的二进制数据, 并将其转换成 Class 对象, 存入缓存区中。 这就是为什么修改了 Class 后,必须重新启动 JVM, 程序所做的修改才会生效的原因。
下面程序示范了访问 JVM 的类加载器:
import java.util.*;
import java.net.*;
import java.io.*;
public class ClassLoaderPropTest {
public static void main(String[] args) throws IOException {
// 获取系统类加载器
ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
System.out.println("系统类加载器:" + systemLoader);
/*
获取系统类加载器的加载路径——通常由 CLASSPATH 环境变量指定 如果操作系统没有指定 CLASSPATH 环境变量,默认以当前路径作为
系统类加载器的加载路径
*/
Enumeration<URL> em1 = systemLoader.getResources("");
while (em1.hasMoreElements()) {
System.out.println(em1.nextElement());
}
// 获取系统类加载器的父类加载器:得到扩展类加载器
ClassLoader extensionLader = systemLoader.getParent();
System.out.println("扩展类加载器:" + extensionLader);
System.out.println("扩展类加载器的加载路径:" + System.getProperty("java.ext.dirs"));
System.out.println("扩展类加载器的 parent: " + extensionLader.getParent());
}
}
运行结果:
系统类加载器是 AppClassLoader 的实例, 扩展类加载器 PlatformClassLoader
的实例。 实际上, 这两个类都是 URLClassLoader 类的实例。
根类加载器并不是 Java 实现的,而且由于程序通常须访问根加载器,因此访问扩展类加载器的父类加载器时返回 NULL。
类加载器加载 Class 大致要经过如下 8 个步骤(对应上面的双亲委派模型):
(1) 检测此 Class 是否载入过(即在缓存区中是否有此 Class),如果有则直接进入第 8 步,否则接着执行第 2 步。
(2) 如果父类加载器不存在(如果没有父类加载器,则要么 parent -定是根类加载器,要么本身就 是根类加载器),则跳到第 4 步执行; 如果父类加载器存在,则接着执行第 3 步。
(3) 请求使用父类加载器去载入目标类,如果成功载入则跳到第 8 步,否则接着执行第 5 步。
(4) 请求使用根类加载器来载入目标类,如果成功载入则跳到第 8 步,否则跳到第 7 步。
(5) 当前类加载器尝试寻找 Class 文件(从与此 ClassLoader 相关的类路径中寻找),如果找到则执 行第 6 步,如果找不到则跳到第 7 步。
(6) 从文件中载入 Class,成功载入后跳到第 8 步。
(7) 抛出 ClassNotFoundException 异常。
(8) 返回对应的 java.lang.Class 对象。
其中,第 5、6 步允许重写 ClassLoader 的 findClass()方法来实现自己的载入策略,甚至重写 loadClass() 方法来实现自己的载入过程。
API:[java.lang.ClassLoader](()
[](()创建并使用自定义的类加载器
JVM 中除根类加载器之外的所有类加载器都是 ClassLoader 子类的实例, 开发者可以通过扩展 ClassLoader 的子类, 并重写该 ClassLoader 所包含的方法来实现自定义的类加载器。 查阅 API 文档中关于 ClassLoader 的方法不难发现, ClassLoader 中包含了大量的 protected 方法 这些方法都可被子类重写。
ClassLoader 类有如下两个关键方法:
loadClass(String name,boolean resolve) 该方法为 ClassLoader 的入口点, 根据指定名称来加载类, 系统就是调用 ClassLoader 的该方法来获取指定类对应的 Class 对象。
findClass(String name) 根据指定名称来查找类。如果需要实现自定义的 ClassLoader, 则可以通过重写以上两个方法来实现, 通常推荐重写 fmdClass()方法, 而不是重写 loadClass()方法。
loadClass()方法的执行步骤如下:
用 flndLoadedClass(String) 来检查是否已经加载类, 如果己经加载则直接返回。
在父类加载器上调用 loadClass()方法。如果父类加载器为 null,则使用根类加载器来加载。
调用 findClass(String)方法查找类。
从上面步骤中可以看出,重写 findClass()方法可以避免覆盖默认类加载器的父类委托、缓冲机制两 种策略;如果重写 loadClass()方法,则实现逻辑更为复杂。
在 ClassLoader 里还有一个核心方法:Class defineClass(String name, byte[] b, int off, int len),该方法 负责将指定类的字节码文件(即 Class 文件,如 Hello.class)读入字节数组 byte[] b 内,并把它转换为 Class 对象,该字节码文件可以来源于文件、网络等。
defineClass()方法管理 JVM 的许多复杂的实现,它负责将字节码分析成运行时数据结构,并校验有效性等。
除此之外,ClassLoader 里还包含如下一些普通方法。
findSystemClass(String name):从本地文件系统装入文件。它在本地文件系统中寻找类文件,如 果存在,就使用 defineClass()方法将原始字节转换成 Class 对象,以将该文件转换成类。
static getSystemClassLoader():这是一个静态方法,用于返回系统类加载器。
getParent():获取该类加载器的父类加载器。
resolveClass(CIass<?> c):链接指定的类。类加载器可以使用此方法来链接类 c。
findLoadedClass(String name):如果此 Java 虚拟机已加载了名为 name 的类,则直接返回该类对 应的 Class 实例,否则返回 null。该方法是 Java 类加载缓存机制的体现。
下面程序开发了一个自定义的 ClassLoader,该 ClassLoader 通过重写 findClass()方法来实现自定义 的类加载机制。这个 ClassLoader 可以在加载类之前先编译该类的源文件,从而实现运行 Java 之前先编 译该程序的目标,这样即可通过该 ClassLoader 直接运行 Java 源文件。
public class MyClassLoader extends ClassLoader {
private String root;
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] loadClassData(String className) {
String fileName = root + File.separatorChar
className.replace('.', File.separatorChar) + ".class";
try {
InputStream ins = new FileInputStream(fileName);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int length = 0;
while ((length = ins.read(buffer)) != -1) {
baos.write(buffer, 0, length);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public String getRoot() {
return root;
}
public void setRoot(String root) {
this.root = root;
}
public static void main(String[] args) {
MyClassLoader classLoader = new MyClassLoader();
classLoader.setRoot("E:\temp");
Class<?> testClass = null;
try {
testClass = classLoader.loadClass("com.neo.classloader.Test2");
Object object = testClass.newInstance();
System.out.println(object.getClass().getClassLoader());
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
自定义类加载器的核心在于对字节码文件的获取,如果是加密的字节码则需要在该类中对文件进行解密。由于这里只是演示,我并未对 class 文件进行加密,因此没有解密的过程。这里有几点需要注意:
1、这里传递的文件名需要是类的全限定性名称,即 com. paddx.test . classloading.Test 格式的,因为 defineClass 方法是按这种格式进行处理的。
2、最好不要重写 loadClass 方法,因为这样容易破坏双亲委托模式。
3、这类 Test 类本身可以被 AppClassLoader 类加载,因此我们不能把 com/paddx/test/classloading/Test.class 放在类路径下。否则,由于双亲委托机制的存在,会直接导致该类由 AppClassLoader 加载,而不会通过我们自定义类加载器来加载。
[](()URLCIassLoader 类
Java 为 ClassLoader 提供了一个 URLClassLoader 实现类, 该类也是系统类加载器和扩展类加载器的父类( 此处的父类, 就是指类与类之间的继承关系)。 URLClassLoader 功能比较强大, 它既可以从本地文件系统获取二进制文件来加载类, 也可以从远程主机获取二进制文件来加载类。
在应用程序中可以直接使用 URLClassLoader 加载类, URLClassLoader 类提供了如下两个构造器:
URLClassLoader(URL[] urls): 使用默认的父类加载器创建一个 ClassLoader 对象, 该对象将从 urls 所指定的系列路径来查询并加载类。
URLClassLoader(URL[] urls,ClassLoader parent): 使用指定的父类加载器创建一个 ClassLoader 对象, 其他功能与前一个构造器相同。
一旦得到了 URLClassLoader 对象之后, 就可以调用该对象的 loadClass()方法来加载指定类。 下面程序示范了如何直接从文件系统中加载 MySQL 驱动, 并使用该驱动来获取数据库连接。 通过这种方式来获取数据库连接, 可以无须将 MySQL 驱动添加到 CLASSPATH 环境变量中。
import java.sql.*;
import java.util.*;
import java.net.*;
public class URLClassLoaderTest {
private static Connection conn;
// 定义一个获取数据库连接方法
public static Connection getConn(String url, String user, String pass) throws Exception {
if (conn == null) {
// 创建一个 URL 数组
URL[] urls = { new URL("file:mysql-connector-java-5.1.30-bin.jar") };
// 以默认的 ClassLoader 作为父 ClassLoader,创建 URLClassLoader
URLClassLoader myClassLoader = new URLClassLoader(urls);
// 加载 MySQL 的 JDBC 驱动,并创建默认实例
Driver driver = (Driver) myClassLoader.loadClass("com.mysql.jdbc.Driver").getConstructor().newInstance();
// 创建一个设置 JDBC 连接属性的 Properties 对象
Properties props = new Properties();
// 至少需要为该对象传入 user 和 password 两个属性
props.setProperty("user", user);
props.setProperty("password", pass);
// 调用 Driver 对象的 connect 方法来取得数据库连接
conn = driver.connect(url, props);
}
return conn;
}
public static void main(String[] args) throws Exception {
System.out.println(getConn("jdbc:mysql://localhost:3306/mysql", "root", "32147"));
}
}
API:[java.net.URLClassLoader](()
[](()通过反射查看类信息
===========================================================================
Java 程序中的许多对象在运行时都会出现两种类型: 编译时类型和运行时类型, 例如代码: Person p=new Student();,这行代码将会生成一个 p 变量, 该变量的编译时类型为 Person,运行时类型为 Student;除此之外, 还有更极端的情形, 程序在运行时接收到外部传入的一个对象, 该对象的编译时类型是 Object,但程序又需要调用该对象运行时类型的方法。
[](()获得 Class 对象
在 Java 程序中获得 Class 对象通常有如下三种方式:
使用 Class 类的 forName(String clazzName)静态方法。 该方法需要传入字符串参数, 该字符串参数的值是某个类的全限定类名( 必须添加完整包名)。
调用某个类的 class 属性来获取该类对应的 Class 对象。 例如, Person.class 将会返回 Person 类对应的 Class 对象。
调用某个对象的 getClass()方法。 该方法是 java.lang.Object 类中的一个方法, 所以所有的 Java 对象都可以调用该方法, 该方法将会返回该对象所属类对应的 Class 对象。
对于第一种方式和第二种方式都是直接根据类来取得该类的 Class 对象, 相比之下, 第二种方式有如下两种优势:
代码更安全。 程序在编译阶段就可以检查需要访问的 Class 对象是否存在。
程序性能更好。 因为这种方式无须调用方法, 所以性能更好。
也就是说, 大部分时候都应该使用第二种方式来获取指定类的 Class 对象。 但如果程序只能获得一个字符串, 例如”java.lang.String”, 若需要获取该字符串对应的 Class 对象, 则只能使用第一种方式, 使用 Class 的 forName(String clazzName)方法获取 Class 对象时, 该方法可能抛出一个 ClassNotFoundException 异常。
一旦获得了某个类所对应的 Class 对象之后, 程序就可以调用 Class 对象的方法来获得该对象和该类的信息了。
[](()从 Class 中获取信息
Class 类提供了大量的实例方法来获取该 Class 对象所对应类的详细信息,Class 类大致包含如下方法, 下面每个方法都可能包括多个重载的版本, 应该参照官方 API。
下面 4 个方法用于获取 Class 对应类所包含的构造器:
Connstructor<\T> getConstructor(Class<?>…parameterTypes): 返回此 Class 对象对应类的、 带指定形参列表的 public 构造器。
Constructor<?>[]getConstructors(): 返回此 Class 对象对应类的所有 public 构造器。
Constructor<\T> getDeclaredConstructor(Class<?>…parameterTypes): 返回此 Class 对象对应类的、带指定形参列表的构造器, 与构造器的访问权限无关。
Constructor<?>[]getDeclaredConstructors(): 返回此 Class 对象对应类的所有构造器, 与构造器的访问权限无关。
下面 4 个方法用于获取 Class 对应类所包含的方法:
Method getMethod(String name,Class<?>…parameterTypes): 返回此 Class 对象对应类的、 带指定形参列表的 public 方法。
Method[]getMethods(): 返回此 Class 对象所表示的类的所有 public 方法。
Method getDeclaredMethod(String name,Class<?>…parameterTypes): 返回此 Class 对象对应类的、带指定形参列表的方法, 与方法的访问权限无关。
Method[]getDeclaredMethods(): 返回此 Class 对象对应类的全部方法, 与方法的访问权限无关。
如下 4 个方法用于访问 Class 对应类所包含的成员变量:
Field getField(String name): 返回此 Class 对象对应类的、 指定名称的 public 成员变量。
Field[]getFields(): 返回此 Class 对象对应类的所有 public 成员变量。
Field getDeclaredField(String name): 返回此 Class 对象对应类的、 指定名称的成员变量, 与成员变量的访问权限无关。
Field[]getDeclaredFields(): 返回此 Class 对象对应类的全部成员变量, 与成员变量的访问权限无关。
如下几个方法用于访问 Class 对应类上所包含的 Annotation:
<A extends Annotation〉 A getAnnotation(Class<\A> annotationClass): 尝试获取该 Class 对象对应类上存在的、 指定类型的 Annotation; 如果该类型的注解不存在, 则返回 null。
<A extends Annotation〉 A getDeclaredAnnotation(Class<\A> annotationClass): 这是 Java 8 新增的方法, 该方法尝试获取直接修饰该 Class 对象对应类的、 指定类型的 Annotation; 如果该类型的注解不存在, 则返回 null。
Annotation[] getAnnotations(): 返回修饰该 Class 对象对应类上存在的所有 Annotation。
Annotation[] getDeclaredAnnotations(): 返回直接修饰该 Class 对应类的所有 Annotation。
< A extends Annotation> A[] getAnnotationsByType(Class<\A> annotationClass): 该方法的功能与前面介绍的 getAnnotation()方法基本相似。 但由于 Java 8 增加了重复注解功能, 因此需要使用该方法获取修饰该类的、 指定类型的多个 Annotation。
< A extends Annotation> A[] getDeclaredAnnotationsByType(Class<\A> annotationClass): 该方法的
功能与前面介绍的 getDeclaredAnnotations ()方法基本相似。 但由于 Java 8 增加了重复注解功能,因此需要使用该方法获取直接修饰该类的、 指定类型的多个 Annotation。
如下方法用于访问该 Class 对象对应类包含的内部类:
Class<?>[] getDeclaredClasses(): 返回该 Class 对象对应类里包含的全部内部类。
如下方法用于访问该 Class 对象对应类所在的外部类:
Class<?> getDeclaringClass(): 返回该 Class 对象对应类所在的外部类。
如下方法用于访问该 Class 对象对应类所实现的接口:
Class<?>[] getlnterfaces(): 返回该 Class 对象对应类所实现的全部接口。
如下几个方法用于访问该 Class 对象对应类所继承的父类:
Class<? super T> getSuperclass(): 返回该 Class 对象对应类的超类的 Class 对象。
如下方法用于获取 Class 对象对应类的修饰符、 所在包、 类名等基本信息:
int getModifiers(): 返回此类或接口的所有修饰符。 修饰符由 public、 protected、 private、 final、static、 abstract 等对应的常量组成, 返回的整数应使用 Modifier 工具类的方法来解码, 才可以获取真实的修饰符。
Package getPackage(): 获取此类的包。
String getName(): 以字符串形式返回此 Class 对象所表示的类的名称。
String getSimpleName(): 以字符串形式返回此 Class 对象所表不的类的简称。
除此之外, Class 对象还可调用如下几个判断方法来判断该类是否为接口、 枚举、 注解类型等:
boolean isAnnotation(): 返回此 Class 对象是否表示一个注解类型(由 @interface 定义)。
boolean isAnnotationPresent(Class<? extends Annotation〉 annotationClass): 判断此 Class 对象是否使用了 Annotation 修饰。
boolean isAnonymousClass(): 返回此 Class 对象是否是一个匿名类。
boolean isArray(): 返回此 Class 对象是否表不一个数组类。
boolean isEnum(): 返回此 Class 对象是否表不一个枚举(由
boolean islnterface(): 返回此 Class 对象是否表示一个接口( 使用 interface 定义)。
boolean isInstance(Object obj): 判断 obj 是否是此 Class 对象的实例, 该方法可以完全代替 instanceof 操作符。
上面的多个 getMethod()方法和 getConstructor()方法中, 都需要传入多个类型为 Class<?〉的参数, 用于获取指定的方法或指定的构造器。 关于这个参数的作用, 假设某个类内包含如下三个 info 方法签名:
public void info()
public void info(String str)
public void info(String str , Integer num)
在程序中获取该方法使用如下代码:
// 前一个参数指定方法名, 后面的个数可变的 Class 参数指定形参类型列表
clazz.getMethod("info" , String.class)
如果需要获取第三个 info 方法, 则使用如下代码:
// 前一个参数指定方法名, 后面的个数可变的 Class 参数指定形参类型列表
clazz.getMethod("info" , String.class, Integer.class)
下面程序示例了如何通过该 Class 对象来获取对应类的详细信息:
import java.util.*;
import java.lang.reflect.*;
import java.lang.annotation.*;
// 定义可重复注解
@Repeatable(Annos.class)
@interface Anno {
}
@Retention(value = RetentionPolicy.RUNTIME)
@interface Annos {
Anno[] value();
}
// 使用 4 个注解修饰该类
@SuppressWarnings(value = "unchecked")
@Deprecated
// 使用重复注解修饰该类
@Anno
@Anno
public class ClassTest {
// 为该类定义一个私有的构造器
private ClassTest() {
}
// 定义一个有参数的构造器
public ClassTest(String name) {
System.out.println("执行有参数的构造器");
}
// 定义一个无参数的 info 方法
public void info() {
System.out.println("执行无参数的 info 方法");
}
// 定义一个有参数的 info 方法
public void info(String str) {
System.out.println("执行有参数的 info 方法" + ",其 str 参数值:" + str);
}
// 定义一个测试用的内部类
class Inner {
}
public static void main(String[] args) throws Exception {
// 下面代码可以获取 ClassTest 对应的 Class
Class<ClassTest> clazz = ClassTest.class;
// 获取该 Class 对象所对应类的全部构造器
Constructor[] ctors = clazz.getDeclaredConstructors();
System.out.println("ClassTest 的全部构造器如下:");
for (Constructor c : ctors) {
System.out.println(c);
}
// 获取该 Class 对象所对应类的全部 public 构造器
Constructor[] publicCtors = clazz.getConstructors();
System.out.println("ClassTest 的全部 public 构造器如下:");
for (Constructor c : publicCtors) {
System.out.println(c);
}
// 获取该 Class 对象所对应类的全部 public 方法
Method[] mtds = clazz.getMethods();
System.out.println("ClassTest 的全部 public 方法如下:");
for (Method md : mtds) {
System.out.println(md);
}
// 获取该 Class 对象所对应类的指定方法
System.out.println("ClassTest 里带一个字符串参数的 info()方法为:" + clazz.getMethod("info", String.class));
// 获取该 Class 对象所对应类的上的全部注解
Annotation[] anns = clazz.getAnnotations();
System.out.println("ClassTest 的全部 Annotation 如下:");
for (Annotation an : anns) {
System.out.println(an);
}
System.out.println("该 Class 元素上的 @SuppressWarnings 注解为:"
Arrays.toString(clazz.getAnnotationsByType(SuppressWarnings.class)));
System.out.println("该 Class 元素上的 @Anno 注解为:" + Arrays.toString(clazz.getAnnotationsByType(Anno.class)));
// 获取该 Class 对象所对应类的全部内部类
Class<?>[] inners = clazz.getDeclaredClasses();
System.out.println("ClassTest 的全部内部类如下:");
for (Class c : inners) {
System.out.println(c);
}
// 使用 Class.forName 方法加载 ClassTest 的 Inner 内部类
Class inClazz = Class.forName("ClassTest$Inner");
// 通过 getDeclaringClass()访问该类所在的外部类
System.out.println("inClazz 对应类的外部类为:" + inClazz.getDeclaringClass());
System.out.println("ClassTest 的包为:" + clazz.getPackage());
System.out.println("ClassTest 的父类为:" + clazz.getSuperclass());
}
}
API:[java.lang.Class](()
[](()使用反射生成并操作对象
=============================================================================
Class 对象可以获得该类里的方法( 由 Method 对象表示)、 构造器( 由 Constructor 对象表示)、 成员变量( 由 Field 对象表示), 这三个类都位于 java.lang.reflect 包下, 并实现了 java.lang.reflect.Member 接口。 程序可以通过 Method 对象来执行对应的方法, 通过 Constructor 对象来调用对应的构造器创建实例, 能通过 Field 对象直接访问并修改对象的成员变量值。
[](()创建对象
通过反射来生成对象需要先使用 Class 对象获取指定的 Constructor 对象, 再调用 Constructor 对象的 newlnstance()方法来创建该 Class 对象对应类的实例。 通过这种方式可以选择使用指定的构造器来创建实例。
在很多 Java EE (例如 Spring)框架中都需要根据配置文件信息来创建 Java 对象,从配置文件读取的只是某个类的字符串类名, 程序需要根据该字符串来创建对应的实例, 就必须使用反射。
下面程序就实现了一个简单的对象池, 该对象池会根据配置文件读取 key-value 对, 然后创建这些对象, 并将这些对象放入一个 HashMap 中:
import java.util.*;
import java.io.*;
public class ObjectPoolFactory {
// 定义一个对象池,前面是对象名,后面是实际对象
private Map<String, Object> objectPool = new HashMap<>();
// 定义一个创建对象的方法
// 该方法只要传入一个字符串类名,程序可以根据该类名生成 Java 对象
private Object createObject(String clazzName) throws Exception, IllegalAccessException, ClassNotFoundException {
// 根据字符串来获取对应的 Class 对象
Class<?> clazz = Class.forName(clazzName);
// 使用 clazz 对应类的默认构造器创建实例
return clazz.getConstructor().newInstance();
}
// 该方法根据指定文件来初始化对象池
// 它会根据配置文件来创建对象
public void initPool(String fileName)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
try (FileInputStream fis = new FileInputStream(fileName)) {
Properties props = new Properties();
props.load(fis);
for (String name : props.stringPropertyNames()) {
// 每取出一对 key-value 对,就根据 value 创建一个对象
// 调用 createObject()创建对象,并将对象添加到对象池中
objectPool.put(name, createObject(props.getProperty(name)));
}
} catch (Exception ex) {
System.out.println("读取" + fileName + "异常");
}
}
public Object getObject(String name) {
// 从 objectPool 中取出指定 name 对应的对象
return objectPool.get(name);
}
public static void main(String[] args) throws Exception {
ObjectPoolFactory pf = new ObjectPoolFactory();
pf.initPool("obj.txt");
System.out.println(pf.getObject("a")); // ①
System.out.println(pf.getObject("b")); // ②
}
}
程序调用 Class 对象的 newlnstance()方法即可创建一个 Java 对象。 程序中的 initPool()方法会读取属性文件, 对属性文件中每个 key-value 对创建一个 Java 对象, 其中 value 是该 Java 对象的实现类, 而 key 是该 Java 对象放入对象池中的名字。 为该程序提供如下属性配置文件:
obj.txt
a=java.util.Date
b=javax.swing.JFrame
如果不想利用默认构造器来创建 Java 对象, 而想 《一线大厂 Java 面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》无偿开源 威信搜索公众号【编程进阶路】 利用指定的构造器来创建 Java 对象, 则需要利用 Constructor 对象, 每个 Constructor 对应一个构造器。 为了利用指定的构造器来创建 Java 对象, 需要如下三个步骤:
获取该类的 Class 对象:
利用 Class 对象的 getConstructor()方法来获取指定的构造器。
调用 Constructor 的 newlnstance()方法来创建 Java 对象。
下面程序利用反射来创建一个 JFrame 对象, 而且使用指定的构造器:
import java.lang.reflect.*;
public class CreateJFrame {
public static void main(String[] args) throws Exception {
// 获取 JFrame 对应的 Class 对象
Class<?> jframeClazz = Class.forName("javax.swing.JFrame");
// 获取 JFrame 中带一个字符串参数的构造器
Constructor ctor = jframeClazz.getConstructor(String.class);
// 调用 Constructor 的 newInstance 方法创建对象
Object obj = ctor.newInstance("测试窗口");
// 输出 JFrame 对象
System.out.println(obj);
}
}
[](()调用方法
当获得某个类对应的 Class 对象后, 就可以通过该 Class 对象的 getMethods()方法或者 getMethod()方法来获取全部方法或指定方法—这两个方法的返回值是 Method 数组, 或者 Method 对象。每个 Method 对象对应一个方法, 获得 Method 对象后, 程序就可通过该 Method 来调用它对应的方法。 在 Method 里包含一个 invoke()方法, 该方法的签名如下:
Object invoke(Object obj,Object…args): 该方法中的 obj 是执行该方法的主调, 后面的 args 是执行该方法时传入该方法的实参。
下面程序对前面的对象池工厂进行加强, 允许在配置文件中增加配置对象的成员变量的值, 对象池工厂会读取为该对象配置的成员变量值, 并利用该对象对应的 setter 方法设置成员变量的值:
import java.util.*;
import java.io.*;
import java.lang.reflect.*;
public class ExtendedObjectPoolFactory {
// 定义一个对象池,前面是对象名,后面是实际对象
private Map<String, Object> objectPool = new HashMap<>();
private Properties config = new Properties();
// 从指定属性文件中初始化 Properties 对象
public void init(String fileName) {
try (FileInputStream fis = new FileInputStream(fileName)) {
config.load(fis);
} catch (IOException ex) {
System.out.println("读取" + fileName + "异常");
}
}
// 定义一个创建对象的方法
// 该方法只要传入一个字符串类名,程序可以根据该类名生成 Java 对象
private Object createObject(String clazzName) throws Exception {
// 根据字符串来获取对应的 Class 对象
Class<?> clazz = Class.forName(clazzName);
// 使用 clazz 对应类的默认构造器创建实例
return clazz.getConstructor().newInstance();
}
// 该方法根据指定文件来初始化对象池
// 它会根据配置文件来创建对象
public void initPool() throws Exception {
for (String name : config.stringPropertyNames()) {
// 每取出一个 key-value 对,如果 key 中不包含百分号(%)
// 这就表明是根据 value 来创建一个对象
// 调用 createObject 创建对象,并将对象添加到对象池中
if (!name.contains("%")) {
objectPool.put(name, createObject(config.getProperty(name)));
}
}
}
// 该方法将会根据属性文件来调用指定对象的 setter 方法
public void initProperty() throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
for (String name : config.stringPropertyNames()) {
// 每取出一对 key-value 对,如果 key 中包含百分号(%)
// 即可认为该 key 用于控制调用对象的 setter 方法设置值
// %前半为对象名字,后半控制 setter 方法名
if (name.contains("%")) {
// 将配置文件中的 key 按 %分割
String[] objAndProp = name.split("%");
// 取出调用 setter 方法的参数值
Object target = getObject(objAndProp[0]);
// 获取 setter 方法名:set + "首字母大写" + 剩下部分
String mtdName = "set" + objAndProp[1].substring(0, 1).toUpperCase() + objAndProp[1].substring(1);
// 通过 target 的 getClass()获取它的实现类所对应的 Class 对象
Class<?> targetClass = target.getClass();
// 获取希望调用的 setter 方法
Method mtd = targetClass.getMethod(mtdName, String.class);
// 通过 Method 的 invoke 方法执行 setter 方法
// 将 config.getProperty(name)的值作为调用 setter 方法的参数
mtd.invoke(target, config.getProperty(name));
}
}
}
public Object getObject(String name) {
// 从 objectPool 中取出指定 name 对应的对象
return objectPool.get(name);
}
public static void main(String[] args) throws Exception {
ExtendedObjectPoolFactory epf = new ExtendedObjectPoolFactory();
epf.init("extObj.txt");
epf.initPool();
epf.initProperty();
System.out.println(epf.getObject("a"));
}
}
为上面程序提供如下配置文件:
ext0bj.text
a=javax.swing.JFrame
b=javax.swing.JLabel
#set the title of a
a%title=Test Title
Spring 框架就是通过这种方式将成员变量值以及依赖对象等都放在配置文件中进行 ,管理的, 从而实现了较好的解耦。 这也是 Spring 框架的 IOC 的原理。
当通过 Method 的 invoke()方法来调用对应的方法时,Java 会要求程序必须有调用该方法的权限。 如果程序确实需要调用某个对象的 private 方法,则可以先调用 Method 对象的如下方法: * setAccessible(boolean flag):将 Method 对象的 accessible 设置为指定的布尔值。值为 true,指示 该 Method 在使用时应该取消 Java 语言的访问权限检査;值为 false,则指示该 Method 在使用时 要实施 Java 语言的访问权限检查。
API:[java.lang.reflect.Method](()
[](()访问成员变量值
通过 Class 对象的 getFields()或 getField()方法可以获取该类所包括的全部成员变量或指定成员变量。
Field 提供了如下两组方法来读取或设置成员变量值:
getXxx(Object obj): 获取 obj 对象的该成员变量的值。 此处的 Xxx 对应 8 种基本类型, 如果该成员变量的类型是引用类型, 则取消 get 后面的 Xxx。
setXxx(Object obj ,Xxx val): 将 obj 对象的该成员变量设置成 val 值。 此处的 Xxx 对 应 8 种 基 本类型, 如果该成员变量的类型是引用类型, 则取消 set 后面的 Xxx。
使用这两个方法可以随意地访问指定对象的所有成员变量, 包括 private 修饰的成员变量。
import java.lang.reflect.*;
class Person {
private String name;
private int age;
public String toString() {
return "Person[name:" + name + " , age:" + age + " ]";
}
}
public class FieldTest {
public static void main(String[] args) throws Exception {
// 创建一个 Person 对象
Person p = new Person();
// 获取 Person 类对应的 Class 对象
Class<Person> personClazz = Person.class;
// 获取 Person 的名为 name 的成员变量
// 使用 getDeclaredField()方法表明可获取各种访问控制符的成员变量
Field nameField = personClazz.getDeclaredField("name");
// 设置通过反射访问该成员变量时取消访问权限检查
nameField.setAccessible(true);
// 调用 set()方法为 p 对象的 name 成员变量设置值
nameField.set(p, "Yeeku.H.Lee");
// 获取 Person 类名为 age 的成员变量
Field ageField = personClazz.getDeclaredField("age");
// 设置通过反射访问该成员变量时取消访问权限检查
ageField.setAccessible(true);
// 调用 setInt()方法为 p 对象的 age 成员变量设置值
ageField.setInt(p, 30);
System.out.println(p);
}
}
API:[java.lang.reflect.Field](()
[](()操作数组
在 java.lang.reflect 包下还提供了一个 Array 类, Array 对象可以代表所有的数组。 程序可以通过使用 Array 来动态地创建数组, 操作数组元素等。
Array 提供了如下几类方法:
static Object newInstance(Class<?> componentType,int… length): 创建一个具有指定的元素类型、指定维度的新数组。
static xxx getXxx(Object array,int index): 返回 array 数组中第 index 个元素。 其中 xxx 是各种基本数据类型, 如果数组元素是引用类型, 则该方法变为 get(Object array,int index)。
static void setXxx(Object array,int index,xxx val): 将 array 数组中第 index 个元素的值设为 val。其中 XXX 是各种基本数据类型, 如果数组元素是引用类型, 则该方法变成 set(Object array, int index,Object val)。
下面程序示范了如何使用 Array 来生成数组, 为指定数组元素赋值, 并获取指定数组元素的方式:
import java.lang.reflect.*;
public class ArrayTest1 {
public static void main(String args[]) {
try {
// 创建一个元素类型为 String ,长度为 10 的数组
Object arr = Array.newInstance(String.class, 10);
// 依次为 arr 数组中 index 为 5、6 的元素赋值
Array.set(arr, 5, "疯狂 Java 讲义");
Array.set(arr, 6, "轻量级 Java EE 企业应用实战");
// 依次取出 arr 数组中 index 为 5、6 的元素的值
Object book1 = Array.get(arr, 5);
Object book2 = Array.get(arr, 6);
评论