Java基础概念与路径问题总结

  学习Java,就一定会碰到路径使用问题,如果不将它搞懂,在实际开发中碰到其实挺烦的,这并不是高级难题而是基础知识,碰到它的几率还挺大的。所以无论如何,我们必须把Java的路径问题彻底解决才行。

最基础的三个概念

我们学Java的首先就会碰到以下这三个东西。

JVM

JVM(Java Virtual Machine)即Java虚拟机,它主要用于执行被编译成Java字节码文件的Java程序。

JVM是Java程序实现跨平台的核心所在,它将Java程序与平台之间解耦
  • 针对不同的平台,就会有不同的Java虚拟机实现
  • 这样对于同一个Java程序(同一份Java字节码),就会被具体平台相关联的Java虚拟机“翻译”成适合该平台执行的机器指令。
  • 最后Java虚拟机执行被“翻译”后的Java程序

所以这就是书上或老师常说的Java程序一次编译,到处运行的具体原因。

JRE

JRE(Java Runtime Environment)即Java运行时环境,它包含了运行Java程序所需的一切:JVM实现以及Java核心类库

为什么会有两套JRE?

  默认情况下,我们都是选择完整安装JDK所有内容的。当我们安装完JDK后,我们会发现JDK安装目录下会有两套JRE,其中一套在Java\jre目录中,另一套在Java\jdk\jre目录中。但奇怪的是,只要有一套JRE就可以运行Java程序了啊,为什么要给我们装上两套呢?其实这是因为不管是用于开发的工具javac命令等,或者非开发的工具java命令等,它们都是用Java实现的,因此它们也是Java程序,所以JDK就自行附带一套JRE用于运行它们。

运行Java程序时用哪一套JRE

既然安装了多套JRE,那么在运行Java程序时,运用哪套JRE的决定权都在java命令上,它会按以下步骤查找:

  • java.exe所在目录
  • java.exe所在目录的父目录
  • PATH环境变量,从左往右查找路径
  • 注册表HKEY_LOCAL_MACHINE\SOFTWARE\JavaSoft\Java Runtime Encironment里面有一个名为JavaHome的键所对应的路径值

环境变量

  下面所要介绍的都是我们在刚接触Java时就能接触到的,或许因为这些知识实在是太过基础了,显得完全不起眼,所以我们总是忽略它们的存在以及作用。这里对环境变量的讨论都是在Windows的基础上进行的。

JAVA_HOME

JAVA_HOME是安装JDK后我们设置的一个自定义环境变量,其值被指定为JDK安装目录的路径

比如:我的JDK安装在E盘,那么就配置这个值:E:\Program\Java\jdk1.8.0_121

这个环境变量并不是必须配的,它用于简化另一个环境变量PATH所配置的路径,不过挺多教程、书籍上都是这么教的,所以就这么写上了。

PATH

安装完JDK后必须对环境变量PATHPath进行配置(这不同于JAVA_HOME,这是必须的),配置有两种方式:

  1. 如果有配置JAVA_HOME,那么一般都在PATH环境变量中加上这个路径值%JAVA_HOME%\bin即可,多个路径之间用分号;隔开。
  2. 没配置JAVA_HOME的话,那就直接配置JDK安装目录下的bin目录的完整路径

通常,都会使用第一种方式,先配置JAVA_HOME,再配置PATH

为什么要给PATH环境变量进行配置?

  • 在编译.java文件要用到javac命令;在运行已编译好的.class文件要用到java命令

  • 当我们查看JDK的bin目录,我们可以发现里面存放了很多可执行文件,这些都是早已编写好的工具,我们执行各种命令其实是调用这些工具而已
    比如编译Java文件用的javac命令相当于运行javac.exe,而执行Java程序的java命令就是运行java.exe,执行用于打包的命令jar命令就是运行jar.exe

  • 所以只有在PATH系统变量里配置JDK的bin目录的路径,那样在任何地方都能方便地调用JDK开发工具,不然每次使用这些命令时都写出它们的完整路径,那就十分麻烦了

JDK8的PATH有所变动!

在安装JDK8后,它会自动给环境变量PATH加上这一个路径:C:\ProgramData\Oracle\Java\javapath,这样即使我们没有手动去配置环境变量,打开控制台也是能直接执行java命令的。
打开这个目录可以发现里面有3个文件:java.exejavaw.exejavaws.exe,但这没有我们熟知的javac.exe编译工具,只有一个我们最熟悉的运行工具,所以说怎么样还是要配置PATH环境变量。

同时,最好是把这个自动给PATH配置的路径给删除掉,以免以后发生不必要的问题。

CLASSPATH

环境变量CLASSPATH是用来告知各种应用包括JDK的各种工具在哪里可以找到.class字节码文件,通常它包含一个或多个目录。

原理细节

  1. 当要运行Java程序时,即执行java命令,那后面肯定是指定全限定类名,即带包名的类名
  2. 首先,Java解析器会将全限定类名中的每个句点.替换成斜杠(/还是\取决于操作系统),这样就得到一个相对路径
  3. 然后,解析器会把CLASSPATH里所配置的绝对路径一一作为参考路径,尝试从它们里面找到该相对路径所指代的.class字节码文件

所以现在很清楚了,如果在运行Java程序时提示找不到某个类,通常这就说明在CLASSPATH所配置的所有路径都找不到某个类,你需要看看CLASSPATH环境变量是否没配好。

配置和查看方式

这里只说Windows环境下的做法,在命令行对CLASSPATH的操作如下。

查看CLASSPATH的配值:

1
echo %CLASSPATH%

CLASSPATH配置指定路径:

1
2
3
set CLASSPATH=path1;path2;path3;...

set classpath=path1;path2;path3;...

将所有路径清除:

1
2
3
set CLASSPATH=

set classpath=

不写路径就是把所有配值清除。

当然也可以在系统环境变量增加CLASSPATH配置,很多教程都是给它配置上.;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar,但我发现其实配不配置也是OK的,不配置也没发现什么大问题。

理解相对路径

在Java程序里使用各种各样的路径,你要根据路径所处的上下文处理好一些细节问题,不然会各种碰壁,特别是使用相对路径的时候,要更加细心谨慎。而使用绝对路径来定位资源一般是不会有问题的,都是绝对的了,绝对路径代表是哪个资源就是哪个资源,是定死不变的。所以一般在开发种遇到涉及路径的问题,其实90%都是关于相对路径的问题。所以,绝对路径没啥好讨论的,这里只探讨相对路径,并且也只针对普通磁盘文件IO这一种情况来讨论。

先来看一段程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import java.io.File;

public class RelativePathProblem {
/* 通过IDE执行main方法的运行结果:
* user.dir = H:\Hope\java-ee
* "a/b/c.txt" absolute path = H:\Hope\java-ee\a\b\c.txt
*
* 通过系统控制台执行main方法
* 1.步骤
* 1.1 先在控制台把classpath设置为包含该类对应的.class文件的包目录的父目录路径
* 比如: 在Windows系统环境下, 包含该类对应的.class文件的包目录在D:\classes目录内,
* 那么就在命令行输入命令 set classpath=D:\classes
* 注意: 如果你把.class文件拷贝到任意目录, 切记, 要把包含它的包目录也一起拷贝才行
*
* 1.2 执行java命令运行main方法
* 注意: 执行时, 必须指定权限定类名, 不然会提示找不到类
*
* 2.运行结果
* 运行结果会根据执行java命令时所在的目录变化而变化
*
* 2.1 执行java命令时所在的目录是D:\, 则执行结果是:
* user.dir = D:\
* "a/b/c.txt" absolute path = D:\\a\b\c.txt
*
* 2.2 执行java命令时所在的目录是H:\books
* user.dir = H:\books
* "a/b/c.txt" absolute path = H:\books\a\b\c.txt
*/
public static void main(String[] args) {
File file = new File("a/b/c.txt");
System.out.println("user.dir = " + System.getProperty("user.dir"));
System.out.println("\"a/b/c.txt\" absolute path = " + file.getAbsolutePath());
}
}

这里我通过IDEIntelliJ IDEA以及Windows的控制台执行这个程序,得出的运行结果就如注释里所写那样。

所以从这个运行结果就能够分析并总结出2点:

  • 其实通过IDE执行Java程序跟使用控制台执行Java程序一个样,只不过IDE每次在执行java命令前先把路径设置为当前工程/模块的根目录
  • 给处理磁盘文件的各种IO组件指定相对路径,该相对路径在运行时的真实含义是根据执行java命令时所在的路径变化而变化

总的来说就是:对于Java普通磁盘IO这种情况来说,所指定的相对路径是相对于执行java命令时所在的路径。

但其实就算是其他情况,我觉得也是大同小异,下面是一点点思路(或许不太正确),当自己使用相对路径的时候可以像下面一样思考:

  1. 光相对路径自身是无法指代某个具体资源的,即只有相对路径是毫无意义的
  2. 相对路径必须依靠一个绝对路径作为参考,那它才能定位到某个具体资源
  3. 要想知道所使用的相对路径是否精确地定位到自己所想要的资源,那先得反问一下自己,自己给相对路径所指定作为参考的绝对路径是否符合自己的需求

所以,当相对路径不能根据自己的需求工作时,那就首先自问一句:该相对路径是相对于哪个绝对路径?确认绝对路径正是解决需求的路径,那才从别的角度入手。

与类字节码相关的相对路径

需求

在实际开发中,这么一个需求是很常见的:程序需要读取某些外部文件从而获得某些配值。

直接在程序中使用绝对路径,是没问题,特别是该程序只是自己使用的话,的确是毫无问题;但如果说要给别人用,那就十分麻烦,首先,别人不知道要将资源文件放到哪个固定位置让程序读取,这你要告诉别人,其次,别人可能用的系统和你的不一样,那文件系统都不同,这样就更没辙了,程序完全没法用。所以,使用绝对路径,程序的通用性是十分差的,一般我们都不会在程序中用绝对路径。

所以,相对路径成了我们唯一的选择,相对路径的灵活性大,这样也会使得程序的通用性大大增强,但正如前面所讲,相对路径容易错误,要确定它所定位的是哪个资源文件,那得看它相对于哪个绝对路径,这个绝对路径最好是程序运行文件所在位置或其相关位置,这样不管该程序放到哪运行,都能让相对路径准确无误地定位到资源文件。而Java中正好有现成的技术让开发者简单地进行实现。

Java中的技术

在Java中,通过调用ClassClassLoadergetResource方法并给它指定相对路径,这样就可以获取相对于字节码所在位置或其相关位置资源

注意到这里的方法名以及后头重点标出来的资源两字!!获取的是资源,资源、资源、资源,这个概念在Java中很重要。
存储于磁盘上的文件只不过是我们最常见的一种资源,在Java内置的API中,凡是涉及到资源概念的,这都表明这里的资源并不是单纯指的文件,资源可不是单单以文件的形式存在,它可以是通过某些程序在内存上生成,也可以存在于网络上,要通过网络连接才能进行获取,比如各种流。这应该是一个通用的概念,所以这是明白一次,再套用到其他编程语言其实也差不了多少。

底层原理

想要知道getResource方法是如何获取资源,那得要看源码,对底层要有所了解才行。

首先来看Class类用于获取资源的getResource方法的源码,毕竟这个类是我们最常用的,肯定是首先关心它内部那个获取资源的方法是如何工作的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* Finds a resource with a given name. The rules for searching resources
* associated with a given class are implemented by the defining
* {@linkplain ClassLoader class loader} of the class. This method
* delegates to this object's class loader. If this object was loaded by
* the bootstrap class loader, the method delegates to {@link
* ClassLoader#getSystemResource}.
*
* <p> Before delegation, an absolute resource name is constructed from the
* given resource name using this algorithm:
*
* <ul>
*
* <li> If the {@code name} begins with a {@code '/'}
* (<tt>'&#92;u002f'</tt>), then the absolute name of the resource is the
* portion of the {@code name} following the {@code '/'}.
*
* <li> Otherwise, the absolute name is of the following form:
*
* <blockquote>
* {@code modified_package_name/name}
* </blockquote>
*
* <p> Where the {@code modified_package_name} is the package name of this
* object with {@code '/'} substituted for {@code '.'}
* (<tt>'&#92;u002e'</tt>).
*
* </ul>
*
* @param name name of the desired resource
* @return A {@link java.net.URL} object or {@code null} if no
* resource with this name is found
* @since JDK1.1
*/
public java.net.URL getResource(String name) {
name = resolveName(name);
ClassLoader cl = getClassLoader0();
if (cl==null) {
// A system class.
return ClassLoader.getSystemResource(name);
}
return cl.getResource(name);
}

方法实现很简单,首先它调用resolveName处理传递进去的相对路径,然后尝试获取该构筑该字节码的自定义类加载器,如果获取到就通过它来获取资源,否则就用默认的类加载器进行资源获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Add a package name prefix if the name is not absolute Remove leading "/"
* if name is absolute
*/
private String resolveName(String name) {
if (name == null) {
return name;
}
if (!name.startsWith("/")) {
Class<?> c = this;
while (c.isArray()) {
c = c.getComponentType();
}
String baseName = c.getName();
int index = baseName.lastIndexOf('.');
if (index != -1) {
name = baseName.substring(0, index).replace('.', '/')
+"/"+name;
}
} else {
name = name.substring(1);
}
return name;
}

该主题另外写一篇….

参考资料