1.背景叙述
Recurson.java
所在的文件目录:
- 此
Recursion.java
中引用了我自定义的另一个类Array
:
- 我的
Array.java
所在目录:
- 目录结构图也就是这样的:
2.编译跨文件引用类
- 此时的
Recursion.java
和Array.java
都没有任何package
和import
语句:
Array.java
自然是能够正确编译形成在D:\桌面\JAVA\1_hsp_java\javacode\chapter06_Array
内的Array.class
文件
- 可是当我们开始运行
Recursion.java
的时候就不行了,因为它引用了Array.class
,但是,JVM不知道从哪里找到这个类,就会报错找不到类:
- 我自然想到把这个
Array.class
导入Recursion.java
内-
我知道
Array.class
位于D:\桌面\JAVA\1_hsp_java\javacode\chapter06_Array
目录下,我的第一想法是,该从哪里开始导入?import D.桌面.JAVA.1_hsp_java.javacode.chapter06_Array.Array;
这个显然是不正确的,这就把windows和java混为一谈了。
-
我的第二想法是,为什么平时常用的
import java.util.Scanner;
就有用?这个java.util.Scanner
目录结构究竟在哪里?JVM又是怎么找到它们的? -
反着思考,先探究JVM是怎么找到这些类的?
我们在dos窗口下来执行,为了帮助JVM找到位于windows系统某些目录下的类,windows在系统环境变量中提供一个classpath,通过该环境变量,我们可以帮助JVM找到我们自定义的类。 -
因此,我就把
Array.class
所在的目录加入环境变量classpath
中,再来试一下能否编译成功:
-
至此, 跨文件类引用算是初步解决,把要引用的类所在的文件目录加入classpath
中,别的类在引用该类时,JVM就可以加载到这些类,甚至都不用package
和import
语句。
3.改进类加载方式
-
上述直接把类添加入classpath的方式虽然简单粗暴,却只适合少量类和简单的目录结构,一旦有很多类要引用,各个类的目录层级结构又非常复杂,那么手动添加classpath就会变成非常痛苦枯燥的工作。
-
此时,我才恍然
package
和import
的思想是多么地睿智,如果把很多类所在的一个公共的根目录加入classpath
中,就只需要加入一条,JVM就可以找到很多类所在的包了,在这个包内发生的一些错综复杂的引用通过import
和package
和构建。
-
在这种情形中,我先只把
D:\桌面\JAVA\1_hsp_java\javacode
加入classpath
中,使得JVM可以找到这个大包(不需要打包成jar) -
在这个大包内,
Recursion.java
位于chapter07_Object
文件夹下,那么在源程序中,就应该添加上package chapter07_Object;
这句话,表示生成的类将会在放在这个文件夹内,到时候JVM在查看D:\桌面\JAVA\1_hsp_java\javacode
时就能知道chapter07_Object
这个文件夹里面包含了一个Recursion.class文件
-
同样的,
Array.java
位于chapter06_Array
文件夹中,在该源程序内也应该加上package chapter06_Array;
以便别的类可以import到。 -
把上述
package
都写好,重新编译Array.java
,并且在Recursion.java
内import Chapter06_Array.Array;
这样便可以成功了。
注意:如果源程序内写有package,那么在运行时,必须加上此包前缀,否则无法运行成功。
4.体会
-
从头梳理一遍,我发现,我之前很多次尝试都失败,根源在于我混淆了windows目录结构和JVM类加载目录结构,在我的windows系统下,我所见到的目录结构都是windows的目录结构,但是JVM并不是根据这个目录来加载类的。通过classpath,使得JVM类加载结构和windows目录结构有了一个交点,但JVM在此这个交点后就完全遵循package和import所构建起的“隐形的类加载结构”,有package信息的.class文件就被隐形地放入了此package下,只有通过package.class才能访问到。
-
要证明上述说法非常简单,上面的两个源代码
Array.java
和Recursion.java
中我都写入了package
语句,这表明我把编译生成的Array.class
和Recursion.class
都扔入了隐形的类加载结构下,其中Recurson.class
位于此结构的chapter07_Object
文件夹下,此结构的切入点就是classpath 内的 D:\桌面\JAVA\1_hsp_java\javacode
。因为环境变量在整个windows系统都是可见的,因此无论在任何地方打开dos命令窗口,都可以通过java chapter07_Object.Recursion
命令来执行该class文件,这就表明两种目录是基于一个交点的不同的结构。 -
下面是我在C盘根目录下运行
Recursion.class
文件,是可以成功的。
-
反过来思考,如果一个class文件包含了package信息,那么只有通过该package路径找到该class文件这一种途径才可以执行这个class文件,把这个class文件迁移到别的地方就必须也设一个该package文件夹,并且把新的上层目录也加入classpath才可以。
-
就像我把刚才的Resursion.class文件移动到C盘后,无论如何也运行不了,因为迁移了class文件,对于JVM类加载结构而言就相当于删除了class文件,毕竟JVM是无法理解windows系统目录的,因此必须在C盘内建一个同样的目录结构,并且把C盘根目录加入classpath,这样就可以成功了,此时相当于两种目录多了一个交点:
5.联想
- 上文也提到过,为什么平时常用的
import java.util.Scanner;
就有用?这个java.util.Scanner
目录结构究竟在哪里?JVM又是怎么找到它们的? - 我去classpath中找,并没有找到包含这些类的目录文件
- 然后我了解到,JVM启动的时候就会自动加载一些jar包,一般是系统java的核心jar包,通过以下程序,可以了解到你的JVM一开始加载了哪些jar包:
/**
* 看看JVM一开始加载了那些类,为什么classpath里面不用指定这些类
*/
import java.lang.*;
import java.net.URL;
public class WhyRtJarInclude{
public static void main(String[] args) {
URL[] urls=sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urls.length; i++) {
System.out.println(urls[i].toExternalForm());
}
}
}
//以下是输出结果:
file:/E:/java/jdk1.8u271/jre/lib/resources.jar
file:/E:/java/jdk1.8u271/jre/lib/rt.jar
file:/E:/java/jdk1.8u271/jre/lib/sunrsasign.jar
file:/E:/java/jdk1.8u271/jre/lib/jsse.jar
file:/E:/java/jdk1.8u271/jre/lib/jce.jar
file:/E:/java/jdk1.8u271/jre/lib/charsets.jar
file:/E:/java/jdk1.8u271/jre/lib/jfr.jar
file:/E:/java/jdk1.8u271/jre/classes
- 我们可以看到,JVM开启时就加载了很多jre内的核心类库,我们重点关注一下
rt.jar
,顺着file:/E:/java/jdk1.8u271/jre/lib/rt.jar可以发现一个rt.jar包,解压出现以下文件夹:
6.最后
妈耶,终于搞明白了,兄弟,你悟了吗?7千多字,码字不易,有帮助的话,点个赞吧,谢谢啦^ V ^