1. 反射
- 定义:
将类的各个组成部分封装成对象,这就是反射机制
好处:可以在程序运行时操控这些对象;可以解耦,提高程序的可扩展性。 - 过程:
如下图所示:java代码在计算机中所经历的三个阶段
一个类经过 java.c 的编译形成 .class结尾的字节码文件,此时字节码文件存储在硬盘中,由类加载器 class.loader加载进内存,形成一个class类对象,将里面的成员变量,构造方法和成员方法分别封装为field数组,constructor数组和method数组对象,由class类对象创建我们需要的对象。
我们都知道java程序写好以后是以.java(文本文件)的文件存在磁盘上,然后,我们通过(bin/javac.exe)编译命令把.java文件编译成.class文件(字节码文件),并存在磁盘上。
但是程序要运行,首先一定要把.class文件加载到JVM内存中才能使用的,我们所讲的classLoader,就是负责把磁盘上的.class文件加载到JVM内存中
- class对象的获取方法
- 通过 Class.forName(“全类名”) 获取 :一般用于资源的加载(可以在一个类还没有被加载时加载它)
- 通过类名 . class 获取 (这样不用创建对象也可以获得class引用)
- 通过对象 . getClass() 方法获取
Class aClass = Class.forName("反射.com.explam.Person");
Class personClass = Person.class;
Person person = new Person();
Class aClass1 = person.getClass();
类字面常量(类.class)详解:
- 它更简单也更安全,因为在编译期就会受到检查。
- 不仅可以应用于普通的类,也可以应用于接口,数组以及基本数据类型。另外对于基本数据类型的包装类,还有一个标准字段TYPE(int.class == Integer.TYPE)
- 仅使用.class语法来获得对类的引用不会引发初始化,由下代码可见,编写Initable.class后并未初始化Initable类,再调用了“编译器常量”staticFinal(不需要初始化就可以读取)任未初始化,调用了staticFinal2变量后才开始类的初始化(因为staticFinal2不是“编译期常量”)
class Initable{
static final int staticFinal = 47;
static final int staticFinal2 = ClassInitialization.rand.nextInt(1000);
static{
System.out.println("Initializing Initable");
}
}
public class test(){
Class initable = Initable.class;
System.out.println("Initable.staticFinal");
System.out.println("lalala");
System.out.println("Initable.staticFinal2");
}
输出:
47
lalala
Initializing Initable
439(随机数)
- 应用
JDBC中:用于加载驱动
Class.forName("com.mysql.jdbc.Driver");
BaseDao.class.getClassLoader().getResourceAsStream():通过类加载器在classPath目录下获取资源db.properties.并且是以流的形式。
Properties properties = new Properties();
InputStream resource = BaseDao.class.getClassLoader().getResourceAsStream("db.properties");
properties.load(resource);
2. 注解
- 内置注解:
@Override:表示当前的方法定义将覆盖父类中的方法。(重写)
@Deprecated:如果使用了注解为它的元素,编译器会发出警告。
@SuppressWarnings:关闭编译器警告信息。 - 元注解:
@Target:表明该注解可以用于什么地方。可选的ElementType参数:CONSTRUCTOR,FIELD(域声明),LOCAL_VARIABLE,METHOD,PACKAGE,PARAMETER(参数声明),TYPE(类,接口或enum声明),ANNOTATION_TYPE(能给注解使用)
@Retention:表示需要在什么级别保存该注解信息。可选的RententionPolicy参数:SOURCE:注解将被编译器丢失 ;CLASS:注解在class文件中可用,但会被VM丢弃; RUNTIME:vm将在运行期也保存注解,可以通过反射机制读取到注解信息。
@Documented:将此注解包含在javadoc中。
@Inherited:允许子类继承父类中的注解。
有专门的类型编写注解,因为一个java文件中只能有一个public修饰的类。
自定义注解的方法:
元注解就是自定时注解的属性,内部定义使用注解时传入的参数(id,name),其中为name设置了默认值。
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface test {
public int id();
public String name() default "woo" ;
}
3. 重学反射:
此处学习的是韩顺平老师的反射课程。
3.1 应用场景:
我们有这样的一个需求:想根据文件中的指定信息,创建一个dog的对象并调用其的“hi”方法。
classfullpath = com.w.demo.dog
method = hi
方法1:
我们第一个想到的方法一定是用new对象的方法实现,但是使用new方法创建对象可能会导致代码的耦合性增高。
方法2:
通过io流获得配置文件,进而获得配置文件中的信息:
public void getProperties() throws IOException {
Properties properties = new Properties();
properties.load(new FileInputStream("src//re.properties"));
String classfullpath = properties.get("classfullpath").toString();
String method = properties.get("method").toString();
}
配置信息是获取到了,但是我们发现无法往下操作,因为我们获取到的是String类型的配置信息。
在很多框架结构中(比如spring),很多地方都用到了这种思想(OCP开闭原则:即通过外部的配置文件,在不改变代码本身的情况下,对程序功能进行扩展。就是功能是开放的,代码是“闭上”的)此时我们就需要使用反射!
3.2 反射入门及原理:
public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
Properties properties = new Properties();
properties.load(new FileInputStream("src//re.properties"));
String classfullpath = properties.get("classfullpath").toString();
String methodName = properties.get("method").toString();
// 加载类,返回class类型的对象 aclass
Class<?> aClass = Class.forName(classfullpath);
// 通过 aclass 获得你想加载的类的实例对象
Object o = aClass.newInstance();
// 虽然o的类型是object,但 getClass方法获得的是运行时的变量,所以获得的是我们想要的变量
System.out.println(o.getClass());
// 根据 aclass 获得 你指定的类中的指定的方法对象
// 此处获得的是一个方法的对象(万物皆对象)
Method method1 = aClass.getMethod(methodName);
System.out.println("======================================");
method1.invoke(o);//一般是通过对象调用方法,这里是通过方法的对象调用类的对象
}
运行结果:
反射原理:
加载完类后,就在堆中产生了一个class类型的对象(一个类只有一个class对象),这个对象包含了类的所有信息,就像照镜子一样可以通过这个class对象获得类的全部结构。
反射相关类:
Class:Class对象表示某个类加载进内存后在堆中出现的对象。
Constructor:代表类的构造方法,注意无参和有参构造的写法区别
Field:代表成员变量,不能直接获得私有的成员变量
Method:代表成员方法
// 获得构造方法
Constructor<?> constructor = aClass.getConstructor(); //无参构造
System.out.println(constructor);
Constructor<?> constructor2 = aClass.getConstructor(String.class);//有参构造
System.out.println(constructor2);
// 获得成员变量,因为name是私有变量,所以会报错,无法获得私有变量
// Field name = aClass.getField("name");
Field ageField = aClass.getField("age");
System.out.println(ageField.get(o));//这里和method一样都是反过来用成员变量的对象掉class实例对象
代码的运行结果:
反射优化:
反射缺点:基本是解释执行,对执行速度有影响。
解决方法:反射的一些类对象有setAccessible()方法,作用是启动和禁用访问安全检查的开关。如果我们将参数值设置为true,则表示反射对象在使用是取消访问检查,会加快一点速度,
3.2 Class类分析:
- Class就是一个普通的类。
- Class类的对象不是new出来的,是系统创建的
本质上是通过ClassLoader类中的loadClass方法
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
- 一个类的Class类对象,在内存中只有一份,因为类只加载一次。
- 每个类的实例会记得自己是由哪个Class实例所生成。
- 通过Class的一系列API可以获得一个类的完整结构。
- Class类对象是存在堆中的。
- 类的字节码二进制数据是放在方法区的。
获得Class的方法:
java程序在计算机中有三个阶段,每个阶段对应了一个获得class的方法
第一个编译阶段: Class.forName()方法:适用于根据配置文件加载类的方式,知道了指定类的全路径
String classPath = "com.w.demo.dog";
Class<?> aClass1 = Class.forName(classPath);
第二个类加载阶段:类.Class方法:适用于参数传递
Class<dog> aClass2 = dog.class;
第三个运行阶段:对象.getClass方法
dog dog = new dog();
Class<? extends com.w.demo.dog> aClass3 = dog.getClass();
其他还有:
通过类加载器获得Class对象:
ClassLoader classLoader = dog.getClass().getClassLoader();
Class<?> aClass4 = classLoader.loadClass(classPath);
通过基本数据类型获得Class对象:
Class<Integer> aClass5 = int.class;
通过包装类获得Class对象:
Class<Integer> aClass6 = Integer.TYPE;
3.3 类加载:
- 类加载分为:
静态加载:在编译期加载需要的类,如果类不存在或者找不到就会报错,依赖性强。(普通通过new创建对象)
动态加载:在运行时加载需要的类,只有执行到这段代码时才会进行类加载,运行不到就不加载,降低了依赖性。(反射)
如下面代码所示:
此代码会直接报错,因为1的情况没有导入cat的包,也没有cat这个类,把1中的代码注释掉则不会报错了,因为2中是动态加载,只有输入2的情况才会报classNotFound的错。
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
Scanner scanner = new Scanner(System.in);
int i = scanner.nextInt();
switch (i){
case 1:
//静态加载
cat cat = new cat();
cat.hi();
break;
case 2:
//动态加载
Class<?> cat1 = Class.forName("cat");
Object o = cat1.newInstance();
Method hi = cat1.getMethod("hi");
hi.invoke(o);
break;
case 3:
System.out.println("hi");
}
}
类加载的三个阶段:
- 加载:将字节码从不同的数据源(Class文件、也可能是jar包,甚至是网络)转换为二进制字节流加载到内存中,并生成一个代表该类的Class对象。
- 连接:
1.校验:进行安全校验(确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并不会危害虚拟机的自身安全)
注:正是这一步耗时相对较长,可以考虑使用 -Xverify:none来关闭大部分的类验证措施,缩短虚拟机类加载的时间。
2.准备:给静态变量,分配内存并初始化(根据数据类型的默认初始值给予默认值),这些变量所使用的内存都在方法区中进行分配。
3.解析:JVM将常量池内的符号引用替换为直接引用的过程。
//i1是实例属性,不在准备阶段赋值,不会分配内存
public int i1 = 10;
//i2是静态变量,在准备阶段被赋值为0;
public static int i2 = 20;
//i3是final修饰的静态变量,根据final的特性,这里的i3在准备阶段被赋值为30
public static final int i3 =30;
- 初始化:
1.在这个阶段才开始真正执行java代码,这个阶段会有一个clinit方法,会依次的收集程序中的静态变量和静态代码块中的语句,并对其进行自定义的赋值的初始化和合并。
2 虚拟机会保证一个类的clinit方法在多线程环境中被正确的加锁、同步,如果有多个线程同时的去初始化一个类,那么只有一个线程会去执行这个类的clinit方法,其他线程会阻塞等待,直到活动线程执行完clinit方法(对应了一个类只会有一个Class对象)
public class B {
//clinit方法内:
// System.out.println("静态代码块");
// num = 300(被覆盖)
// num = 100 ,所以最后的num值为100(因为是按顺序的)
static {
System.out.println("静态代码块");
//为什么这里不会报错呢,因为在连接阶段,num已经被加载进内存了
num = 300;
}
public static int num = 100;
}