Java - 基石

1 命令与路径查找

1.1 javac 命令和 java 命令

javac 命令用来编译源文件:
javac 参数 源文件路径

java 命令用来执行类文件:
java 参数 类名

其中,在用 java 执行类文件时指定的是类名。
也因为这个原因,后面不能跟类文件的路径(因为它并不是一个文件名)。

在编译源文件时,若其中使用了另一个源文件中定义的类,则需要查找它所依赖的类的类文件与源文件。

  • 如果两者均不存在,则报错。
  • 如果类文件不存在,但源文件存在,则编译源文件并得到类文件,查找成功。
  • 如果类文件存在,但源文件不存在,则不进行任何操作,查找成功。
  • 如果两者均存在,则查找成功。然后判断源文件是否比其类文件更新。若是,则对这个源文件重新编译。

在执行类时,需要查找它对应的类文件及其所依赖的类文件。如果不存在,则报错。

1.2 -sourcepath 与 -classpath (-cp) 和 CLASSPATH

  • -sourcepath。编译选项。通过它来指定源文件所在目录的搜索路径;
  • -cp。既可以作为编译选项,也可以作为运行选项。通过它来指定类文件所在目录的搜索路径;
  • CLASSPATH。环境变量。用来指定类文件所在目录的搜索路径。

1.3 源文件所在目录的搜索路径

  • 如果设置了 -sourcepath 参数,则它所指定的路径即为源文件所在目录的搜索路径。如果有多个路径,则用 : 分开;
  • 若它未被设置,则将 -cp 参数所指定的路径当作源文件所在目录的搜索路径。如果有多个路径,则用 : 分开;
  • -cp 未被设置,则将 CLASSPATH 环境变量所指定的路径当作源文件所在目录的搜索路径。如果有多个路径,则用 : 分开;
  • CLASSPATH 未被设置,则将当前工作目录的路径当作源文件所在目录的搜索路径。

因此,如果编译生成的类文件与其源文件在同一目录下(默认情况),则一般不使用这个参数,而用 -cp 参数替代

1.4 类文件所在目录的搜索路径

首先,编译器或虚拟机会自动将 jdk 及 jre 中的相关 jar 包的路径加载到类文件所在目录的搜索路径中。然后:

  • 如果设置了 -cp 参数,则将它所指定的路径添加到类文件所在目录的搜索路径之后;
  • 如果它未被设置,则将 CLASSPATH 环境变量所指定的路径添加到类文件所在目录的搜索路径之后;
  • CLASSPATH 未被设置,则将当前工作目录的路径添加到类文件所在目录的搜索路径之后。

1.5 package 与 import

package 关键字相当于 C++ 中的命名空间,用来对类进行组织和管理。当在开发一个比较大的工程时,引入命名空间可以有效地避免一些类同名而导致冲突的问题。

设一个源文件名为 Foo.java,其中定义了一个公有类 Foo(再无其他公有类)。并且将第一行写为:
package com.cuckootan
此时,就表示这个源文件中的类 Foo 属于 com.cuckootan 包下,且该类的完整名字为:
com.cuckootan.Foo
当要在其他文件中使用这个类或者执行这个类时,就必须要使用 com.cuckootan.Foo 的形式。

同时,必须要保证能够搜索到这个 com(比如说,通过 -cp 指定 com 目录的 父目录)。当搜索到这个目录后,就可以根据这个路径和包名来查找到这个类文件。

因此,包名的命名要和目录层次相对应。也就是说如果类 Foo 属于 com.cuckootan 这个包下,那么必须要保证在 \${某路径}/com/cuckootan 这个目录下能够找到 Foo.java 或者 Foo.class 文件。

将一个类加入到包后,每次使用这个类时都要写较长的名字,显得有些繁琐。而 import 可以解决这个问题。比如:
import com.cuckootan.Foo
之后,就可以直接写 Foo 来使用这个类了。

实质上,编译器会将类名用 import 后面的带有包名的完整类名替代。
不过,在执行这个类时,仍然需要用它的带包名的完整名。

如果 import 多个包中相同的类时,若直接使用这个类名来使用这个类,则会发生歧义。此时仍然需要写带有包名的完整类名。

1.6 示例

设有这么一个项目,其目录结构如下所示:

1
2
3
4
5
.
├── com
│   └── cuckootan
│   └── Foo.java
└── Test.java

Test.java 中内容如下:

1
2
3
4
5
6
7
public class Test
{
public static void main(String[] args)
{
System.out.println(Foo.a);
}
}

Foo.java 中内容如下:

1
2
3
4
5
6
7
8
9
public class Foo
{
public static int a = 3;

public static void main(String[] args)
{
System.out.println("hello");
}
}

设当前工作目录在 com 的父目录中,且并未设置 CLASSPATH

编译:
javac -cp ./com/cuckootan/ Test.java

运行:
java -cp .:./com/cuckootan Test

由于在编译和执行时需要知道 Foo 类的位置,编译器和装载器会根据相应的搜索路径去查找。
从中也可以看出,不引入 package 机制也是完全可以的。

如果现在将 Foo 类加入到 cuckootan 包中,即如下:

1
2
3
4
5
6
7
8
9
10
11
package cuckootan;

public class Foo
{
public static int a = 3;

public static void main(String[] args)
{
System.out.println("hello");
}
}

那么在 Test.java 中必须用 cuckootan.Foo 来使用这个类,也即如下:

1
2
3
4
5
6
7
public class Test
{
public static void main(String[] args)
{
System.out.println(cuckootan.Foo.a);
}
}

编译:
javac -cp ./com Test.java

运行:
java -cp .:./com Test

指定 cuckootan 所在目录的路径,从而根据路径以及包名找到类文件 Foo.class。

当然,在 Test.java 中,也可以先 import 这个带有包名的完整类名,从而在使用这个类时简化书写。

1
2
3
4
5
6
7
8
9
import cuckootan.Foo;

public class Test
{
public static void main(String[] args)
{
System.out.println(Foo.a);
}
}

编译与运行同上。

2 Java 工程目录组织

./src/main 中存放源文件。
./target 中存放类文件,编译时使用 -d 参数指定这个目录。


Reference