目录
反射机制
Class 类
类初始化(类加载)
类的生命周期(七大阶段)
1.加载(接入.class文件)
2.验证(连接 linking 的第一阶段,为了安全)
3.准备*(分配内存及初步初始化)
4.解析(字符翻译成引用阶段)
5.初始化(代码正式开始执行的阶段)
6.使用
7.卸载
接口的加载
类和对象的初始化方法
反射原理(Class对象)
Class.forName() 方法
反射的使用
反射获取构造方法并使用
Constructor 类(管理构造函数的类)
反射方法的其它使用:
1.通过反射越过泛型检查
泛型(代码标准化)
如何通过反射拿到泛型的真实类型
泛型接口
泛型方法
泛型的类型擦除*
类型擦除带来的问题
泛型数组(栈)
类型边界(更灵活的类型设置)
通配符 ?(类型辅助限定)
上界通配符
下界通配符
扩展:泛型规范和建议
泛型命名规范:
使用泛型的建议
Java 的反射机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。这种动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制。(引用词条)
简短的说:反射就是在程序运行状态中访问类及其信息等等
而反射的原理在于Class对象!所以我们先讲讲Class类相关的知识,知道的可以跳到反射原理
Class类是个特殊类,它映射着JVM运行时的类或接口的信息。
当程序主动使用某个类时,如果该类还未加载到内存中的话,则JVM会通过加载、连接、初始化来对该类进行加载。
特点:
- 类型的加载、连接、初始化都是在程序运行时完成的
- 让程序更加的灵活(因为加载是在运行时才完成的)
解析:
注意:
(由图可知,类从被加载到虚拟机内存——>类被卸载出内存后死亡。一共经历了七个阶段)
注意:验证、准备、解析三个阶段统称为连接(linking),而有五个阶段(加载、验证、准备、初始化、卸载)的顺序是绝对固定的。所以类加载必须有这种顺序开始,所以其它解析和使用的顺序是不固定的,比如解析可以放在初始化后开始,这就支持了Java的运行时绑定(动态绑定/晚期绑定)
加载阶段图解:
检查后加载:.class文件中二进制数据被读取到内存中,并放到运行时数据区的方法区,最后在堆中创建一个 java.lang.Class 对象(用来封装类在方法区的数据结构)。这个Class对象是类加载的最终产物,它对外提供了访问方法区内数据结构的接口。
根据图总结:.class文件(二进制数据) ——> 读取到内存 ——> 数据放入到方法区 ——> 堆中创建类对应的Class对象 ——> 由Class对象提供访问方法区的接口
(Class 对象位置 和 HotSpot 虚拟机:JDK1.7位于永久代。JDK1.8位于元空间并移除了永久代)继而它们实现了方法区,并可放入Class对象。
加载.class文件的方式:类加载器是程序运行的基础,其通常由JVM提供的(又叫系统类加载器)
- 从本地系统中加载
- 通过网络下载
- 从zip、jar等归档文件中加载
- 从专用数据库中提取
- 将.java文件动态编译成.class文件
JVM 为 java 类编译 Class 对象并存储在同名的 .class 文件中,在运行时需要生成该类的对象时, JVM 就会检查(该类是否装载到内存中)?生成实例对象:则把该类的.class文件装载到内存中
三元运算符,hhh
目的:确保被加载类的正确性(确保.class文件字节流的包含信息是否符合当前虚拟机的要求)
4个步骤的验证动作:
- 文件格式验证
- 元数据验证
- 字节码验证
- 符合引用验证
注意:验证阶段对于安全性比较重要,但对程序运行本身无影响。所以类经过反复验证,就可以考虑用 -Xverifynone 参数来关闭大部分的类验证操作,以缩短虚拟机类加载的时间。
验证阶段完成后,JVM 就会为类变量分配内存(分配内存于方法区)并初始化。也就是正式为类变量(静态变量)分配内存并设置其初始值。
在准备阶段是否被分配内存呢?
public static String s1 = "我已经被分配内存啦!";
public String s2 = "555 我还没有被分配内存呢!";
注意:JVM不会为类成员变量(实例变量)分配内存,实例变量的内存分配需等到初始化阶段才开始。而且类变量(静态变量)的初始化是赋系统的默认值,而不是程序员代码里设置的值。(如int类型就是0,String类型就是null)但是!如果它拥有 final 修饰符的话就直接是程序员代码里设置的值了。
因static提前分配内存的前提下,它们初始化为什么呢?
public static int i1 = 9;
//初始化为0,而不是代码设置里的9
public static final int i2 = 666;
//因为final修饰符,直接初始化为666
原因:因为final是常量/最终修饰符,赋值后就不改变(在程序运行期间),所以它修饰的变量就提前赋程序员设置的值。而非final修饰的变量的值可能随时被改变(在后面的阶段中),所以就不会提前赋值。
闯过准备阶段后就来到解析阶段。引用下大佬的话:解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符 7 类符号引用进行。
注释:
到这里程序才真正开始执行(前面只是准备工作、热热身)。
类的使用方式:
- 主动使用:当某个类的首次被主动使用时,才会让它初始化/执行。下面是主动使用的种类:
- 创建类的实例,即 new 一个对象
- 其定义静态变量/静态方法被访问(常量池的除外)
- 体内有常量被访问
- 其子类被初始化
- 反射
- Java虚拟机 = 启动类的类(JavaTest)
- 包含 main 方法的类(首先被初始化的类)
- 被动使用
JVM 从入口方法(main方法)开始执行代码。
当代码执行完毕后,JVM就开始消耗其创建的Class对象,最后负责程序运行的JVM也退出了内存。
JVM(Java虚拟机)何时结束它的生命周期?
接口的加载阶段和类有所不同(接口更“懒”):类初始化时要求自己的所有父类都完成了初始化,但是接口初始化时只对用到的父接口进行初始化。
编辑器按下面要素的在代码中的出现顺序,排序执行:
类初始化方法的成分要素: 静态变量的赋值语句、静态代码块
对象初始化方法的成分要素: 实例变量的赋值语句、构造代码块(如果没检测到构造方法的代码就不会执行对象初始化方法,所以对象初始化方法一般在创建类对象时执行)
请说出下列代码的执行顺序吧!
class First {
public static int i = 1;
static {
System.out.println("First的静态代码块");
}
}
public class ActiveUse {
public static void main(String[] args) {
First f;
System.out.println("定义了一个对象引用 f");
f = new First();
System.out.println("实例化了 f 对象");
}
}
隔离答案分界线
隔离答案分界线
隔离答案分界线
隔离答案分界线
隔离答案分界线
隔离答案分界线
运行结果:
定义了一个对象引用 f
First的静态代码块
实例化了 f 对象
我们知道,加载阶段时将.class文件读入内存后,相应的创建了一个Class对象。而反射的本质是通过Class对象来反向分析对象的各种组成信息(包括静态变量、构造方法、方法、所在包等等)
明确的是:
- Class对象是自动被创建且一个类只会产生一个
- Class对象就相当于类对象的“原本”,new的对象只是这个“原本”的“拓本”(所以相同类型都共享一个Class对象)
注意:Class 类没有公共构造方法。而Class对象是由加载类时的JVM(进一步说是其调用类加载器的defineClass方法)自动构造的。(所以我们不必手动创建Class对象,JVM会帮我们创建)
格式 | 效能 |
---|---|
getName() | 返回此Class对象表示的实体(类、接口、数组类、基本类型或void)名 |
newInstance() | 用默认构造器(无参构造器)为类创建一个实例/对象 |
getClassLoader() | 主要返回该类的类加载器 |
getComponentType() | 主要返回数组组件类型的 Class |
getSuperclass() | 返回此Class对象表示的实体的超类的 Class |
isArray() | 判断此Class对象是否表示一个数组类 |
forName 和 newInstance 可以结合起来使用,根据存储在字符串中的类名创建对象。
Object obj = Class.forName("完整类名带包名").newInstance();
JVM为每个类型管理一个独一无二的Class对象,即Class对象是唯一的。因此可以使用 == 操作符来比较类对象。
if(e.getClass() == Employee.class) ...
forName() 是个获取Class对象(加载类)方法,并且是个静态方法。
newInstance() | new | |
---|---|---|
What | 初始化一个类(使用类加载机制) | 生成一个实例(创建一个新的类对象) |
Why | 考虑到设计问题 | 普通正常创建对象 |
How | 弱类型,低效率,无参构造 | 强类型,高效率,能调用任何构造(非封装影响) |
反射相关的 API 分为:
反射的本质已经说过了,就是在Class对象的基础上反向获取类对象的各种成分信息。所以反射第一步就是获取要被反射的类的Class对象
格式 | 备注 |
---|---|
Class.forName("完整类名带包名") | 静态方法(需要该类的全路径名) |
对象.getClass() | 已有对象,又来反射?画蛇添足吧 |
类型.class | 需要导入该类的包 |
格式 | 效能 |
---|---|
public Constructor[] getConstructors() | 所有 public 构造方法 |
public Constructor[] getDeclaredConstructors() | 所有的构造方法 |
public Constructor getConstructor(Class... parameterTypes) | 单个的 public 构造方法 |
public Constructor[] getDeclaredConstructors(Class... parameterTypes) | 单个的构造方法 |
(图例:有下划线的是批量操作,无下划线+倾斜的是单个操作。)带 declared 关键字的方法是不受封装影响的。当然不止是构造方法,如类方法、类属性都可适用的。
格式 | API 的解释 |
T newInstance(Object... initargs) | 使用此 Constructor 对象表示的构造方法来创建该构造方法的声明类的实例,并用指定的初始化参数初始化该实例 |
泛型用在编译期间,编译过后泛型擦除。所以可以用反射来越过泛型检查的。
例子略,以后补充。
先来引用一下词条:泛型程序设计是程序设计语言的一种风格或范式。泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型。
对于Java来说,泛型是被参数化的类或接口,是对类型的约定。
在我看来,接口是类的统一规范,而泛型就是对象类型的统一规范。而泛型接口就是规范中的规范。
特点:
- 经过编译器支持的泛型是降低了代码的冗余度/提升了效率(因为减少了类型转换的次数)
- 通过检查添加元素的类型,提升了安全性
- 提升了程序的健壮性和规范性
格式:写在类名/接口后面
注意!由于泛型是参数化类型的规范,代表只有实际传参时/程序运行时JVM才能拿到其具体真实类型,所以影响它相关的实例化(但是可以通过转型间接完成实例化及通过容器),而且泛型类型不能是基本数据类型,不过可以是包装类。
private int maxSize;
private T[] items;
private int top;
public StackT(int maxSize){
this.maxSize = maxSize;
this.items = (T[]) new Object[maxSize];
this.top = -1;
}
(通过转型创建泛型数组)
实际上因为编译后就会擦除泛型,所以Java泛型我们很难通过 new 的方式来实例化对象(还有获取其真实类型)。不过通过反射的机制还是可以做到的,而Java获取 Class 实例的方式有 3 种:
格式 | 能否获取到泛型的真实类型 | 备注 |
---|---|---|
Class.forName("完整类名带包名") | ✔ | 只要知道该类的全限定类名的路径即可 |
对象.getClass() | × | 没有对象,因为无法使用 new 来实例化泛型对象 |
类型.class | × | 获取不到真实类型,因为泛型编译后就会擦除,程序运行时就获取不到泛型的真实类型了 |
构造例子:
public interface Content {
T text();
}
泛型接口的两种实现类:(类头)
构造格式:
封装等级 [是否静态] [返回类型] 方法名(参数列表:T obj或T... args) {
}
(在使用泛型方法时,通常不必写明泛型类型参数,因为编译器会自动推断出具体类型。即类型参数推断(type argument inferece)。类型参数推断只对赋值操作起效,其它操作比如参数传递时并不起效。假如将某个泛型方法的调用结果作为参数传递给另一个方法时,编译器并不会执行类型参数推断:编译器认为它是 )
Java程序运行大体上分为:编译阶段、运行阶段。
而对于泛型来说,当编译阶段过后就会被擦除;所以在运行阶段时泛型的真实信息已经丢失了。
(所以泛型的生命周期仅仅在编译阶段)以下是类型擦除的工作内容:(准备运行阶段)
擦除的代价是显而易见的,这使得泛型不能用于显式地引用运行类型的操作,比如转型、instanceof 操作、new 表达式... 因为运行时关于类型的参数信息都丢失了。所以当你在写泛型代码时就必须时刻提醒自己,它们本质上都是 Object 类型(运行时)
new T();
//这样是行不通的,因为编译器无法确定 T 是否有默认构造函数(无参构造函数)
不过我们可以通过 Class 类及其方法(isInstance()、.class等等)来弥补类型擦除的问题,例子略。(工厂模式)
跟上面的工厂模式一样,使用 Class 类的方法把类信息存储起来,需要时再拿出来调用即可。
语法形式:
<类型参数 extends XXX>
或:
《T extands 类/接口1 & 接口2 & 接口3...》
(可见,如果设置多个类型边界:除第一个可以设置为类,后面只能是接口了)
通配符 ? | 泛型类型 T | |
---|---|---|
简单区别 | 不能写到赋值语句中 | 可以写到赋值语句中 |
通常用法场景 | 传递参数 | 定义具体类型 |
即限定类型为:A及其子类
即限定类型为:B及其父类
注意:上界通配符和下界通配符是不兼容的。我们知道泛型是不能向上转型的(因为需要运行绑定),但是我们可以通过通配符来间接完成向上转型。而通配符和泛型搭配使用!
泛型名 | 描述 |
---|---|
E | Element(元素) |
N | Number(数字) |
K | Key(键) |
V | Value(值) |
S、U、V、etc. | 2nd、3rd、4th、types |