classpath:JVM 查找类的机制
# classpath:JVM 查找类的机制
对于刚跨入 Java 门槛的初学者来说,编译和运行一个简单的类或许很简单,但如果涉及到多个类、类存放的路径不一样、涉及到 jar 包的时候,就可能稍微复杂了。这里的难点在于对 classpath 的理解和设置上。本文通过理论与实践相结合的方式,逐步解决编译过程中各种 classpath 的方式,并有相关的练习和代码。通过本文,读者应该能在不借助 IDE 的情况下编译和运行代码。
本文环境:Windows10 + Java8。 其他环境也可以运行本博客讲解到的程序(毕竟 Java☕ 是跨平台的)
# 编译一个类
还记得我们的第一个 Java 程序吗?我们新建了一个目录,然后创建了 Hello.java 文件,编写代码:
public class Hello{
public static void main(String[] args){
System.out.println("Welcome to Java!");
}
}
2
3
4
5
我们这里假设新目录为 D:\JavaTest
然后我们在文件所在目录,打开命令行,将 Java 源文件翻译成 Java 字节码文件并执行:
D:\JavaTest> javac Hello.java
D:\JavaTest> java Hello
Welcome to Java!
2
3
我们目前是直接在存放源码的文件夹中进行编译和运行的,一些重要的问题没有暴露出来:
- 操作系统如何寻找
javac
和java
命令? 答:由于javac
和java
命令是可执行文件,操作系统会根据我们安装 Java 的时候配置的 path 去寻找这两个文件; - 操作系统如何寻找 Hello.java 和 Hello.class 文件?此时,就不是根据我们安装 Java 时的 path 变量去寻找了,到底如何查找呢?我们一步步测试并分析。
我们将命令行所在目录回退到上一级,此时,当前路径就不是源码存放的路径了,然后我们输入 javac
命令:
D:\> javac Hello.java
javac: 找不到文件: Hello.java
用法: javac <options> <source files>
-help 用于列出可能的选项
2
3
4
有报错找不到文件,这很正常,因为源码不在当前路径下。我们需要指定一个路径:
D:\> javac D:\JavaTest\Hello.java
这回不报错了,编译成功。
接下来我们尝试执行:
D:\> java D:\JavaTest\Hello
错误: 找不到或无法加载主类 D:\JavaTest\Hello
2
明明 Hello/Hello.class
是一个 class 文件,为什么就找不到了呢?
原来,Java 对待.java
文件与.class
文件是有区别的。对.java 文件可以直接指定路径给它,而 java 命令所需的.class 文件不能出现扩展名,也不能指定额外的路径给它。请牢记这一点。
那么,对于 Java 所需的 .class
文件,如何指定路径呢?必须通过 classpath 来指定。
classpath
是 JVM 用到的一个环境变量,它用来指示 JVM 如何搜索 class
。
# 全局 classpath
我们配置一个新的环境变量。首先打开环境变量窗口,然后在用户变量中新建一个变量,变量名为 classpath,变量值为 D:\JavaTest
重新打开一个新的 cmd,输入 java Hello
C:\WINDOWS\system32> java Hello
Welcome to Java!
2
可以看到,即使我们不在源码所在目录,也能运行 Java 程序;由此可见,classpath 环境变量的作用就是告诉 JDK 去哪里寻找 .class 文件。一旦设置了 classpath 变量,以后每次运行 java 或 javac 命令时,如果要查找.class 文件,都会去 classpath 里找。这是一个全局的设置。
如果有新程序就设置 classpath 变量,那么也太麻烦了,我们可以在运行 Java 程序的时候指定一个 classpath:
D:\> java -classpath D:/JavaTest Hello
Welcome to Java!
2
JDK 发现有 classpath 选项后,就会先根据此 classpath 指定的路径寻找 .class
文件,找不到再去全局的 classpath 环境变量中寻找。我们可以试着删除环境变量,打开一个新的 CMD,然后再次执行上述命令。
tips: 可以用 classpath 的缩写 cp 来代替,如下:
D:\> java -cp D:/JavaTest Hello Welcome to Java!
1
21.特别注意的是,JDK 1.5 之前,javac 命令不能用 cp 来代替 classpath,而只能用 classpath
如果
.class
文件路径带有空格,需要加上双引号,例如javac "D:\Java Test\Hello.java" java -classpath "D:\Java Test" Hello
1
2请自行尝试有空格的情况。
# 命令行里的 classpath
当我们编译的类里面,包含其他类的时候,该怎么编译呢?我们举个例子:
在 Hello.java 的同级目录下新建一个 Person.java
public class Person {
private String name;
public Person (String name){
this.name = name;
}
public String getName() {
return name;
}
}
2
3
4
5
6
7
8
9
10
修改 Hello.java
public class Hello{
public static void main(String[] args){
Person person = new Person("Peter");
System.out.println(person.getName());
}
}
2
3
4
5
6
7
接下来,我们尝试编译
D:\> javac D:/JavaTest/Hello.java
D:\JavaTest\Hello.java:3: 错误: 找不到符号
Person person = new Person("Peter");
^
符号: 类 Person
位置: 类 Hello
D:\JavaTest\Hello.java:3: 错误: 找不到符号
Person person = new Person("Peter");
^
符号: 类 Person
位置: 类 Hello
2 个错误
2
3
4
5
6
7
8
9
10
11
12
此时我们可以看到 JDK 报错找不到 Person 类。为什么之前可以通过指定源码的路径,来编译,现在不行呢?因为之前的 Hello 类中没有用到其他类,因此 JDK 不需要去寻找其他类;
而现在,我们用到了 Person 类,我们就得告诉 JDK,去哪里找这个类(即使他们在同一个文件夹下)
D:\> javac -cp D:/JavaTest D:/JavaTest/Hello.java
编译通过,可以看到生成了 Person.class 文件。
实际上由于 Hello.java 使用了 Person.java 类,JDK 先编译生成了 Person.class,然后再编译生成 Hello.class。因此,不管 Hello.java 这个主类使用了多少个其他类,只要编译这个类,JDK 就会自动编译其他类,很方便。
我们尝试运行:
D:\> java -cp D:/JavaTest Hello
Peter
2
运行成功
没有设置系统环境变量,也没有传入 -cp
参数,那么 JVM 默认的 classpath
为 .
,即当前目录。
# 多个 classpath
接下来我们试试,源码在不同目录的情况。
我们在 Hello 文件夹下新建一个名为 lib 的文件夹,将 Person.java 移动到其下面,并删除 Person.class 文件。
我们尝试编译:
D:\> javac -cp D:/JavaTest/lib D:/JavaTest/Hello.java
编译通过,因为 Person 在 lib 文件夹里,我们只需修改 classpath 为 lib 文件夹的目录即可
我们尝试运行:
D:\> java -cp D:/JavaTest Hello
Exception in thread "main" java.lang.NoClassDefFoundError: Person
at Hello.main(Hello.java:3)
Caused by: java.lang.ClassNotFoundException: Person
at java.net.URLClassLoader.findClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
... 1 more
2
3
4
5
6
7
8
9
有报错,因为我们没有指定 Person 的路径,有报错也是正常的。我们尝试指定:
D:\> java -cp D:/JavaTest/lib Hello
错误: 找不到或无法加载主类 Hello
2
还是有报错。。。为什么呢?
由于 Java 需要在不同文件夹下寻找 class 文件,而 classpath 只告诉了 JDK 一个路径,有错误也是正常的。
在本文的一开始,我们在编译一个类的时候,为什么不用指定 classpath 也能运行呢?因为默认 JVM 会从当前路径寻找类,如果指定了 classpath,就不从当前路径寻找了。
此时我们需要设置多个 classpath,以分号 “;”隔开(注意路径与分号之间不能有空格,Linux 上用冒号:分割):
D:\> java -classpath "D:\JavaTest;D:\JavaTest\lib" Hello
Peter
2
运行成功,但也暴露出一个问题,如果我们需要用到许多分处于不同文件夹下的类,那这个 classpath 的设置岂不是很长!有没有办法,对于一个文件夹下的所有.class 文件,只指定这个文件夹的 classpath,然后让 JDK 自动搜索此文件夹下面所有相应的路径?有,使用 package,且听下回分解。
# 如何编译多个类
当一个包下面很多类的时候,逐个编译是很累的。
比如我们在 JavaTest 下新建多个类,每个类都有 main 方法:
public class Hello{
public static void main(String[] args){
System.out.println("Hello");
}
}
2
3
4
5
public class Hello2{
public static void main(String[] args){
System.out.println("Hello2");
}
}
2
3
4
5
public class Hello3{
public static void main(String[] args){
System.out.println("Hello3");
}
}
2
3
4
5
我们尝试编译:
D:\JavaTest> javac Hello.java Hello2.java Hello3.java
如果类比较多,每个都要写,容易写错或者写漏。
我们可以用通配符表示编译当前目录所有 java 文件:
D:\JavaTest> javac *.java
也可以指定编译某个目录下的所有 Java 文件:
D:\> javac ./JavaTest/*.java
那能不能编译子目录下的所有文件呢?比如
javac JavaTest/**/*.java
其中,第一个星号表明所有子文件夹,第二个星号表明所有 java 文件。也就是编译 JavaTest 目录下所有子文件夹的 java 代码。很遗憾,在 Windows 下是不行的,Windows 不支持这种通配符,得逐个写出各个类…………但是在 Linux 下是可以的,笔者试着在 Git Bash 里也可以用这种通配符,有兴趣的读者请自行尝试
不过一般我们会使用 IDE 开发,会自动根据包结构编译所有 Java 源码,无需使用这么复杂的命令;但我们最好还是了解这一点,知道 IDE 背后帮我们做了什么事情。
tomcat,kafka 这些大型项目的启动运行,都是在脚本里设置 classpath 的。
# javac 的参数
命令行 -d
指定输出的 class
文件存放到具体的目录。例如:
D:\> mkdir ./JavaTest/bin
D:\> javac -d ./JavaTest/bin ./JavaTest/*.java
2
然后可以看到在 bin 目录下存放了 class 文件
D:\JavaTest\bin\Hello.class
D:\JavaTest\bin\Hello2.class
D:\JavaTest\bin\Hello3.class
2
3
注意:请自己独立完成上述实验过程,如果有问题再好好看看该博客,以确保确实掌握了 classpath 的概念和使用。
# 参考
本文主要是参考廖雪峰老师的博客,自己动手实践而得,感谢:
包 - 廖雪峰的官方网站 (opens new window)
classpath 和 jar - 廖雪峰的官方网站 (opens new window)
Java 入门实例 classpath 及 package 详解 - 橘子山寨 - BlogJava (opens new window)
对于刚跨入Java门槛的初学者来说,编译和运行一个简单的类或许很简单,但如果涉及到多个类、类存放的路径不一样、涉及到jar包的时候,就可能稍微复杂了。这里的难点在于对classpath的理解和设置上。本文通过理论与实践相结合的方式,逐步解决编译过程中各种classpath的方式,并有相关的练习和代码。通过本文,读者应该能在不借助IDE的情况下编译和运行代码。
本文环境:Windows10 + Java8。 其他环境也可以运行本博客讲解到的程序(毕竟Java☕ 是跨平台的)
# 编译一个类
还记得我们的第一个Java程序吗?我们新建了一个目录,然后创建了Hello.java文件,编写代码:
public class Hello{
public static void main(String[] args){
System.out.println("Welcome to Java!");
}
}
2
3
4
5
我们这里假设新目录为D:\JavaTest
然后我们在文件所在目录,打开命令行,将Java 源文件翻译成 Java字节码文件并执行:
D:\JavaTest> javac Hello.java
D:\JavaTest> java Hello
Welcome to Java!
2
3
我们目前是直接在存放源码的文件夹中进行编译和运行的,一些重要的问题没有暴露出来:
- 操作系统如何寻找
javac
和java
命令? 答:由于javac
和java
命令是可执行文件,操作系统会根据我们安装Java的时候配置的path去寻找这两个文件; - 操作系统如何寻找 Hello.java 和 Hello.class文件?此时,就不是根据我们安装Java时的path变量去寻找了,到底如何查找呢?我们一步步测试并分析。
我们将命令行所在目录回退到上一级,此时,当前路径就不是源码存放的路径了,然后我们输入javac
命令:
D:\> javac Hello.java
javac: 找不到文件: Hello.java
用法: javac <options> <source files>
-help 用于列出可能的选项
2
3
4
有报错找不到文件,这很正常,因为源码不在当前路径下。我们需要指定一个路径:
D:\> javac D:\JavaTest\Hello.java
这回不报错了,编译成功。
接下来我们尝试执行:
D:\> java D:\JavaTest\Hello
错误: 找不到或无法加载主类 D:\JavaTest\Hello
2
明明Hello/Hello.class
是一个class文件,为什么就找不到了呢?
原来,Java对待.java
文件与.class
文件是有区别的。对.java文件可以直接指定路径给它,而java命令所需的.class文件不能出现扩展名,也不能指定额外的路径给它。请牢记这一点。
那么,对于Java所需的 .class
文件,如何指定路径呢?必须通过classpath来指定。
classpath
是JVM用到的一个环境变量,它用来指示JVM如何搜索class
。
# 全局classpath
我们配置一个新的环境变量。首先打开环境变量窗口,然后在用户变量中新建一个变量,变量名为 classpath,变量值为 D:\JavaTest
重新打开一个新的cmd,输入java Hello
C:\WINDOWS\system32> java Hello
Welcome to Java!
2
可以看到,即使我们不在源码所在目录,也能运行Java程序;由此可见,classpath环境变量的作用就是告诉JDK去哪里寻找 .class文件。一旦设置了classpath变量,以后每次运行java或javac命令时,如果要查找.class文件,都会去classpath里找。这是一个全局的设置。
如果有新程序就设置classpath变量,那么也太麻烦了,我们可以在运行Java程序的时候指定一个classpath:
D:\> java -classpath D:/JavaTest Hello
Welcome to Java!
2
JDK发现有classpath选项后,就会先根据此classpath指定的路径寻找 .class
文件,找不到再去全局的classpath环境变量中寻找。我们可以试着删除环境变量,打开一个新的CMD,然后再次执行上述命令。
tips: 可以用classpath的缩写cp来代替,如下:
D:\> java -cp D:/JavaTest Hello Welcome to Java!
1
21.特别注意的是,JDK 1.5之前,javac命令不能用cp来代替classpath,而只能用classpath
如果
.class
文件路径带有空格,需要加上双引号,例如javac "D:\Java Test\Hello.java" java -classpath "D:\Java Test" Hello
1
2请自行尝试有空格的情况。
# 命令行里的classpath
当我们编译的类里面,包含其他类的时候,该怎么编译呢?我们举个例子:
在Hello.java的同级目录下新建一个Person.java
public class Person {
private String name;
public Person (String name){
this.name = name;
}
public String getName() {
return name;
}
}
2
3
4
5
6
7
8
9
10
修改Hello.java
public class Hello{
public static void main(String[] args){
Person person = new Person("Peter");
System.out.println(person.getName());
}
}
2
3
4
5
6
7
接下来,我们尝试编译
D:\> javac D:/JavaTest/Hello.java
D:\JavaTest\Hello.java:3: 错误: 找不到符号
Person person = new Person("Peter");
^
符号: 类 Person
位置: 类 Hello
D:\JavaTest\Hello.java:3: 错误: 找不到符号
Person person = new Person("Peter");
^
符号: 类 Person
位置: 类 Hello
2 个错误
2
3
4
5
6
7
8
9
10
11
12
此时我们可以看到JDK报错找不到Person类。为什么之前可以通过指定源码的路径,来编译,现在不行呢?因为之前的Hello类中没有用到其他类,因此JDK不需要去寻找其他类;
而现在,我们用到了Person类,我们就得告诉JDK,去哪里找这个类(即使他们在同一个文件夹下)
D:\> javac -cp D:/JavaTest D:/JavaTest/Hello.java
编译通过,可以看到生成了Person.class文件。
实际上由于Hello.java使用了Person.java类,JDK先编译生成了Person.class,然后再编译生成Hello.class。因此,不管Hello.java这个主类使用了多少个其他类,只要编译这个类,JDK就会自动编译其他类,很方便。
我们尝试运行:
D:\> java -cp D:/JavaTest Hello
Peter
2
运行成功
# 多个classpath
接下来我们试试,源码在不同目录的情况。
我们在Hello文件夹下新建一个名为lib的文件夹,将Person.java 移动到其下面,并删除Person.class文件。
我们尝试编译:
D:\> javac -cp D:/JavaTest/lib D:/JavaTest/Hello.java
编译通过,因为Person在lib文件夹里,我们只需修改classpath为lib文件夹的目录即可
我们尝试运行:
D:\> java -cp D:/JavaTest Hello
Exception in thread "main" java.lang.NoClassDefFoundError: Person
at Hello.main(Hello.java:3)
Caused by: java.lang.ClassNotFoundException: Person
at java.net.URLClassLoader.findClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
... 1 more
2
3
4
5
6
7
8
9
有报错,因为我们没有指定Person的路径,有报错也是正常的。我们尝试指定:
D:\> java -cp D:/JavaTest/lib Hello
错误: 找不到或无法加载主类 Hello
2
还是有报错。。。为什么呢?
由于Java需要在不同文件夹下寻找class文件,而classpath只告诉了JDK一个路径,有错误也是正常的。
在本文的一开始,我们在编译一个类的时候,为什么不用指定classpath也能运行呢?因为默认JVM会从当前路径寻找类,如果指定了classpath,就不从当前路径寻找了。
此时我们需要设置多个classpath,以分号 “;”隔开(注意路径与分号之间不能有空格,Linux上用冒号:分割):
D:\> java -classpath "D:\JavaTest;D:\JavaTest\lib" Hello
Peter
2
运行成功,但也暴露出一个问题,如果我们需要用到许多分处于不同文件夹下的类,那这个classpath的设置岂不是很长!有没有办法,对于一个文件夹下的所有.class文件,只指定这个文件夹的classpath,然后让JDK自动搜索此文件夹下面所有相应的路径?有,使用package,且听下回分解。
# 如何编译多个类
当一个包下面很多类的时候,逐个编译是很累的。
比如我们在JavaTest下新建多个类,每个类都有main方法:
public class Hello{
public static void main(String[] args){
System.out.println("Hello");
}
}
2
3
4
5
public class Hello2{
public static void main(String[] args){
System.out.println("Hello2");
}
}
2
3
4
5
public class Hello3{
public static void main(String[] args){
System.out.println("Hello3");
}
}
2
3
4
5
我们尝试编译:
D:\JavaTest> javac Hello.java Hello2.java Hello3.java
如果类比较多,每个都要写,容易写错或者写漏。
我们可以用通配符表示编译当前目录所有java文件:
D:\JavaTest> javac *.java
也可以指定编译某个目录下的所有Java文件:
D:\> javac ./JavaTest/*.java
那能不能编译子目录下的所有文件呢?比如
javac JavaTest/**/*.java
其中,第一个星号表明所有子文件夹,第二个星号表明所有java文件。也就是编译JavaTest目录下所有子文件夹的java代码。很遗憾,在Windows下是不行的,Windows不支持这种通配符,得逐个写出各个类…………但是在Linux下是可以的,笔者试着在Git Bash里也可以用这种通配符,有兴趣的读者请自行尝试
不过一般我们会使用IDE开发,会自动根据包结构编译所有Java源码,无需使用这么复杂的命令;但我们最好还是了解这一点,知道IDE背后帮我们做了什么事情。
tomcat,kafka这些大型项目的启动运行,都是在脚本里设置classpath的。
# javac的参数
命令行-d
指定输出的class
文件存放到具体的目录。例如:
D:\> mkdir ./JavaTest/bin
D:\> javac -d ./JavaTest/bin ./JavaTest/*.java
2
然后可以看到在bin目录下存放了class文件
D:\JavaTest\bin\Hello.class
D:\JavaTest\bin\Hello2.class
D:\JavaTest\bin\Hello3.class
2
3
# 小结
本文所有代码比较简单,也已放到Git上,并补充了相关说明:
Gitee:classpath · 小林/LearnJava - 码云 - 开源中国 (opens new window)
GitHub:classpath at master · Peter-JXL/LearnJava (opens new window)
注意:请自己独立完成上述实验过程,如果有问题再好好看看该博客,以确保确实掌握了classpath的概念和使用。
# 参考
本文主要是参考廖雪峰老师的博客,自己动手实践而得,感谢:
包 - 廖雪峰的官方网站 (opens new window)
classpath和jar - 廖雪峰的官方网站 (opens new window)
Java入门实例classpath及package详解 - 橘子山寨 - BlogJava (opens new window)