Java 语言既具有编译型语言的特征,也具有解释型语言的特征。因为 Java 程序要经过先编译,后解释两个步骤。
一个Java程序,需要经过编译和运行两步,才能看到程序实现的效果。
编译是由编译器完成的,将源码一次性翻译成字节码,编译完成后生成中间的字节码文件,也就是 .class文件
。
运行是由解释器完成的,将字节码一句一句地解释为机器码并执行。运行的过程是解释执行字节码的过程。
小纸条
Java虚拟机和解释器??
解释器是解释型语言中解释执行代码的东西,常见的解释型语言有 Python、JavaScript、PHP
等等。不同语言使用的解释器不同,而Java语言中Java虚拟机是解释器的一种实现方式。
我们需要编译,是需要生成Java 虚拟机可以理解的代码,也就是字节码。字节码,是Java虚拟机可以理解的代码,简短点理解就是,字节码是代码😅。
因为字节码是面向虚拟机,不面向任何特定的处理器,所以使得Java语言有可移植的特点,不需要重新编译就可以在不同的操作系统上运行,也就是Java语言可以跨平台。而现在,Docker 之类的可以容易实现跨平台,其他语言也可以。
Java 程序经过编译,会生成中间的字节码文件。原始的Java程序是 .java
结尾的文件,编译之后,会生成一堆 .class
结尾的字节码文件。一个 Java 文件会生成一个对应的.class
文件,两个文件同名,后缀不同。
单从外观上来看,编译完成之后,和之前的差异是多了 out
或者 target
文件夹,在这些文件夹中有大量的.class文件
。比如,单文件项目生成的 .class
文件如下:
Maven
项目生成的 .class
文件如下:
当类加载器把一个类加载到内存时,会在内存中创建一个 Class对象
。一个类的Class对象
和类的普通Java对象
实例同名。
Java中每个类都对应一个Class对象
,同一个类不会产生多个相同的Class对象
。
Class对象
,包含了和类相关的信息。Class对象
是用来创建所有的普通的对象(.java
文件中类的对象实例)。当一个 类的Class对象
未被加载时,类加载器会查询同名的 .class
文件,将其载入内存。Class对象
被载入内存后,会被用来创建这个类的所有对象。
小纸条
类加载器加载的过程,就是查找 .class文件
,创建一个 Class对象
。
Class对象
载入内存后的使用情况是Java虚拟机中的内存模型部分,也就是运行时的数据区域。
因为Class对象
中包含类的信息,所以如果想要在运行时使用类的类型信息,可以从Class对象
获取到,这样就需要先得到Class对象
的引用。
在拿到一个类的 Class对象
的引用后,可以直接知道对应的这个类的信息。比如,
Class example = LinkedList.class; // 推荐,获取 LinkedList 类的 Class对象引用
example.getName(); // 获得类名(包括包名)
example.getSimpleName(); // 获得类名(不包括包名)
example.isInterface(); // 是否时一个接口
example.getSuperclass(); // 查询直接基类
一般情况下获取一个类的Class对象引用
:通过类创建一个对象实例,然后获取Class对象引用
。比如,
// 创建一个 LinkedList 对象实例
List list = new LinkedList<>();
// 获取 LinkedList 的Class对象引用
Class c = list.getClass();
这种用法为了获取Class对象引用
而创建一个对象,是消耗资源,降低效率的。
直接获取Class对象引用
的方法有以下几种:
方法 | 备注1 | 备注2 |
---|---|---|
类名.class | 需要知道类的明确名字 | 在加载完之后不会自动初始化该Class对象 |
对象名.getClass() | 需要先创建一个对象 | |
ClassLoader.getSystemClassLoader().loadClass() | 需要知道类的全名,包括包名 | |
Class.forName() | 需要知道类的全名,包括包名 | 在加载完之后会自动初始化该Class对象 |
通过Class对象
引用可以操纵Class对象
,Class对象
和普通对象涉及的一些函数如下图:
获取Class对象引用
的代码例子如下:
// 1. 知道具体类的情况
Class example1 = LinkedList.class;
// 2. 传入类的路径。需要处理异常
Class example2 = Class.forName("java.util.LinkedList");
// 3. 通过对象实例instance.getClass()
Class example3 = new LinkedList().getClass();
// 4. 通过类加载器xxxClassLoader.loadClass()传入类路径获取,Class 对象不会进行初始化。
// 需要处理异常
Class example4 = ClassLoader.getSystemClassLoader().loadClass("java.util.LinkedList");
小纸条
初始化Class对象
,是初始化静态方法和静态块。
Java 在运行中识别对象和类的信息的方法有两种:
一种是传统的RTTI (run-time type identification,运行时类型识别),假设在编译时已经知道所有的类型。
一种是反射,在运行时才发现和使用类的信息。在反射机制中,是在运行时打开和检查的class文件
,class文件
在编译时是不可获取的。
如果想要在运行时分析类以及执行类中方法,就需要用到反射。通过反射可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。反射,是在需要创建动态的代码的时候会有用,一般不需要直接使用反射。
反射是直接通过Class对象引用
来获取类的信息以及类中的方法,而不需要通过创建一个对象实例再获得Class对象引用
从而获取类的信息。比如,下面的代码:
// 获取 LinkedList 类的 Class对象引用
Class example = LinkedList.class;
// 获取 LinkedList 类的属性和方法
Method[] methods = example.getMethods(); // 获取该类的所有方法
Constructor[] constructors = example.getConstructors(); // 获取该类的所有构造函数
Field[] fields = example.getFields(); // 获取该类的所有public属性
Class类和java.lang.reflect
类库一起支持了反射(具体怎么支持才实现的,不知道😂),这样,通过类库就可以在运行时获取某个类的信息,比如变量、构造函数、方法。比如下图(下图中的Student
类只是为了说明反射中可以获取到的东西,实际中不这么设置属性)
通过反射获取类的信息的方法比如下面:
Class example = Student.class; // 获取Student对象引用
// 获取类的所有public变量
Field[] fields = example.getFields(); // [public java.lang.String code.input.Student.name]
// 类的第一个public变量的信息
fields[0].getName(); // 变量名是 name
fields[0].getType(); // 变量类型名是 java.lang.String
fields[0].getDeclaringClass().getName(); // 声明的类名是 code.input.Student
在编译完成之后,是在运行期间需要做的事情。类型的加载和连接就是在运行期间。
在虚拟机可以真正运行代码之前,需要先把.class文件
加载进虚拟机中,这样才能运行和使用这些.class文件
,生成虚拟机可以使用的对象。加载的过程,就是把.class文件
加载到内存,根据.class文件
在内存中创建Class对象
,并对数据进行校验、解析、初始化,最终形成虚拟机可以直接使用的Java类型,这也是Java虚拟机的类加载机制。
类加载是由类加载器完成的,选择用哪个类加载器来加载类是使用双亲委派机制来确定的。