Java笔记(23)反射
1.类加载器
当我们写出一个java类的时候,这个扩展名为".java"的文件经过编译器编译成一个扩展名为".class"的文件,class文件中存储的是经过编译后的可由java虚拟机执行的虚拟机指令。当我们需要使用某个类时,虚拟机会经历下面几个步骤:
- 加载这个类的class文件,并在内存中创建这个class文件的Class对象
- 验证这个文件中的信息和数据是否符合虚拟机需求
- 为类成员初始化(static修饰的字段)这里不包含用final修饰的static,因为final在编译的时候就会分配了,注意这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
- 解析,主要将常量池中的符号引用替换为直接引用的过程。
- 初始化:类加载最后阶段,若该类具有超类(父类),则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段进行赋值,成员变量也将在这个阶段被初始化为默认值)。
上面是类加载的几个步骤,而类加载器就是用来完成这几个步骤的,虚拟机提供了3种类加载器,来分别加载不同类型的类;
- (Bootstrap)根类加载器:主要加载的是JVM自身需要的类,这个类加载器使用C++语言实现的,是虚拟机自身的一部分,它负责将java安装路径文件lib下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,注意由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)。
- 扩展(Extension)类加载器:扩展类加载器是指Sun公司(已被Oracle收购)实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类,它负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。
- 系统(System)类加载器:也称应用程序加载器是指 Sun公司实现的sun.misc.Launcher$AppClassLoader。它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。
2.反射概述
当我们知道一个类是由类加载器加载到内存中的,并且创建了一个Class对象在内存中,那我们怎么使用这个Class对象呢?对此,Java就提供了一个反射机制,它可以在运行状态中,对于任意一个类都可以知道这个类的所有属性和方法,对于任意一个对象,都能调用它任意的方法或属性,包括各种权限修饰符所限制的(例如private)
可以看出反射机制的功能非常强大,它只要获取到一个类的字节码文件对象,就可以剖析获取到该类的所有信息,而想要使用这种机制,我们就先要得到class文件对象,其实也就是得到Class类的对象,所有字节码文件对象都是Class的对象;
3.获取class文件对象的方式
1.通过Object类中的getClass()方法
//先定义一个Person类做实验对象
public class Person {
private String name;
public int age;
String address;
public Person(){}
private Pereson(String name,int age){
this.name = name;
this.age = age;
}
public Person(String name,int age,String address){
this.name = name;
this.age = age;
this.address = address;
}
public void show(){
System.out.println("show" + name);
}
private void method(){
System.out.println("method" + name);
}
}
//获取Person类的字节码文件对象
public class Demo {
public static void main(String[] args) throws ClassNotFoundException {
Person p = new Person();
Person p2 = new Person();
Class c1 = p.getClass();
Class c2 = p2.getClass();
//注意c1==c2,因为Class对象基于其class文件的,Person对象的字节码文件只有一个,字节码文件对象当然就相同
}
}
2.类名.class的方式获取class对象
Class c3 = Person.class;
3.通过Class类的forName静态方法获取Class对象
Class c4 = Class.forName("类的全路径"); //带包名的路径,例如cn.test.Person
这三种获取Class文件对象的方式在开发中比较常用的是第三种方式,因为其需求的是一个类的路径的字符串,在反射中是比较灵活的;
4.通过Class对象获取其构造方法
有了一个class文件的Class对象后,我们就可以获得其的构造方法信息了,下面是案例:
//还是使用上面例子中的Person对象
public static void main(String[] args){
//获取Class对象,泛型里如果不知道获取的类是什么类型就使用?
Class<?> c = Class.forName("cn.test.Person");
//获得所有公共构造方法,获取所有构造方法请使用getDeclaredConstructors()方法
Constructor[] cons = c.getConstructors();
for(Constructor con : cons){
System.out.println(con);
}
//获取单个带参构造方法
Constructor con = c.getConstructor(String.class,int.class,String.class);
//通过带参构造方法对象创建对象
Object obj = con.newInstance("王一",20,"北京");
}
//获取私有构造方法
Constructor con = c.getDeclaredConstructor(String.class,int.class);
//由于访问的是私有,要先通过一个方法取消Java语言访问检查
con.setAccessible(true);
Object obj = con.newInstance("李四",20);
5.通过Class对象获取成员变量和方法
//依旧使用上面那个例子
//获取class文件对象
Class<?> c = Class.forName("cn.protest.Person");
Constructor<?> con = c.getConstructor();
//通过无参构造创建对象
Object obj = con.newInstance();
//获取单个成员变量对象
Field nameField = c.getDeclaredField("name");
//取消访问检查
nameField.setAccessible(true);
//将指定对象上的成员变量赋值
nameField.set(obj, "王二");
System.out.println(obj);
获取方法案例
//获取字节码文件对象
Class<?> c = Class.forName("cn.protest.Person");
// 获取所有方法,获取的是自己的和父类的公共方法
// Method[] methods = c.getMethods();
//
// 获取自己的所有方法
// Method[] methods2 = c.getDeclaredMethods();
Constructor<?> con = c.getConstructor();
Object obj = con.newInstance();
//获取单个方法的方法有两个参数,第一个是方法名称,第二个是一个可变参数,接收的是方法的参数
Method m1 = c.getMethod("show"); //这里show方法没有参数,后面的参数就不传了
//调用obj对象的m1方法
m1.invoke(obj); //有两个参数,后面的参数是一个可变参数,传递的是方法的参数
如果要访问私有方法,要先取消检查
6.通过反射越过泛型检查
ArrayList<Integer> list = new ArrayList<Integer>();
//获取集合的Class对象
Class<?> c = list.getClass();
//获取该对象的add方法,泛型默认类型是Object类型
Method m = c.getMethod("add", Object.class);
m.invoke(list, "hello");
System.out.println(list);
//可以看到通过该方式越过了集合的泛型检查
7.动态代理
代理模式是一种常用的设计模式,其目的就是为其他对象提供一个代理以控制对某个真实对象的访问。代理类负责为委托类预处理消息,过滤消息并转发消息,以及进行消息被委托类执行后的后续处理。
案例:
//用户操作接口
public interface UserDao {
//有两个方法,注册和登录
public abstract void register();
public abstract void login();
}
public class UserDaoImpl implements UserDao {
@Override
public void register() {
System.out.println("注册功能");
}
@Override
public void login() {
System.out.println("登录功能");
}
}
//代理实例的处理类,负责代理对象的具体执行
public class MyInvocationHandle implements InvocationHandler {
//目标对象
private Object target;
public MyInvocationHandle(Object target) {
super();
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("权限校验");
Object result = method.invoke(target, args);
System.out.println("日志记录");
return result;
}
}
//测试
public class Test {
public static void main(String[] args) {
//创建用户操作对象
UserDao ud = new UserDaoImpl();
//创建代理对象
MyInvocationHandle handle = new MyInvocationHandle(ud);
//这个方法返回的是一个代理后的UserDao对象
UserDao proxy = (UserDao) Proxy.newProxyInstance(ud.getClass().getClassLoader(), ud.getClass().getInterfaces(), handle);
proxy.login();
//通过代理对象再不修改原有类的源码上增强了对象的方法功能
}
}
8.枚举
枚举类似于单例模式中的单例类,不可以在外部直接创建对象,但其内部提供了一些自己已经造好的对象,供外部访问;与单例不同的是,枚举可以给出多个对象给外界,但必须是有限数量的,枚举出来的对象一般是用静态成员变量的方式存在的。
public class MeiJu {
private String name;
//创建枚举对象
public static final MeiJu LEFT = new MeiJu("left");
public static final MeiJu RIGHT = new MeiJu("right");
//私有构造方法,使得外界不能随意创建对象
private MeiJu(String name){
this.name = name;
}
}
//第二种实现枚举类的方式
public enum MeiJu {
LEFT,RIGHT;
//私有构造方法,使得外界不能随意创建对象
private MeiJu(String name){
this.name = name;
}
}
//实现原理与第一个相同,只不过编译器自动帮我们做了枚举的声明