Java
很诱人,但对于刚跨入
Java
门槛的初学者来说,编译并运行一个无比简单的
Java
程序简直就是一个恶梦。明明程序没错,但各种各样让人摸不着头脑的错误信息真的让你百思不得其解,许多在
Java
门口徘徊了很久的初学者就这样放弃了学习
Java
的机会,很是可惜。笔者也经历过这个无比痛苦的阶段,感觉到编译难的问题就出在
classpath
的设置及对
package
的理解之上。本文以实例的方式,逐一解决在编译过程中所出现的各种
classpath
的设置问题。本文实例运行的环境是在
Windows XP + JDK 1.5.0
。对其他的环境,读者应该很容易进行相应的转换。
1. 下载并安装 JDK1.5.0 ,并按默认路径,安装到 C:/Program Files/Java/jdk1.5.0 中。
2. 用鼠标单击 WindowsXP 的“开始” -> “运行”,在弹出的运行窗口中输入 cmd ,按确定或回车,打开一个命令行窗口。
3. 在命令行中输入:
java
有一列长长的洋文滚了出来,这是 JDK 告诉我们 java 这个命令的使用方法。其中隐含了一个重要信息,即 JDK 安装成功,可以在命令行中使用 java 此命令了。
4. 在命令行中输入
javac
屏幕显示:
'javac' 不是内部或外部命令,也不是可运行的程序或批处理文件。
这是由于 windows 找不到 javac 这个命令的原因。这就不明白了, java 与 javac 都是 JDK 在同一个子目录里面的两个文件,为什么可以直接运行 java 而不能直接运行 javac 呢?原来, Sun 公司为了方便大家在安装完 JDK 后马上就可以运行 Java 类文件,在后台悄悄地将 java 命令加入了 Path 的搜索路径中,因此我们可以直接运行 java 命令 ( 但我们是看不到它到底是在哪设置的,无论是在用户的 Path 或系统的 Path 设置中均找不到这个 java 存放的路径 ) 。但 Sun 所做的到此为止,其他 JDK 的命令,一概不管,需要由用户自己添加到搜索路径中。
5. 既然如此,那我们自己添加 Path 的搜索路径吧。对“我的电脑”按右键,选“属性”,在“系统属性”窗口中选“高级”标签,再按“环境变量”按钮,弹出一个“环境变量”的窗口,在用户变量中新建一个变量,变量名为“ Path ”,变量值为 "C:/Program Files/Java/jdk1.5.0/bin;%PATH%" 。最后的 %PATH% 的意思是说,保留原有的 Path 设置,且将目前的 Path 设置新加到其前面。一路按“确定”退出 ( 共有 3 次 ) 。关掉原来的命令行窗口,依照第 2 步,重新打开一个新的命令行窗口。在此窗口中输入
javac
长长的洋文又出现了,这回是介绍 javac 的用法。设置成功。
6. So far so good. 到目前为止,我们已经可以编程了。但是,这不是一个好办法。因为随着以后我们深入学习 Java ,我们就会用到 JUnit 、 Ant 或 NetBeans 等应用工具,这些工具在安装时,都需要一个名为指向 JDK 路径的“ JAVA_HOME ”的环境变量,否则就安装不了。因此,我们需要改进第 5 步,为以后作好准备。依照第 5 步,弹出“环境变量”的窗口,在用户变量中新建一个变量,变量名为“ JAVA_HOME ”,变量值为 "C:/Program Files/Java/jdk1.5.0" 。注意,这里的变量值只到 jdk1.5.0 ,不能延伸到 bin 中。确定后,返回“环境变量”的窗口,双击我们原先设定的 Path 变量,将其值修改为“ %JAVA_HOME%/bin;%PATH% ”。这种效果与第 5 步是完全一样的,只不过多了一个 JAVA_HOME 的变量。这样,以后当我们需要指向 JDK 的路径时,只需要加入“ %JAVA_HOME% ”就行了。至此, Path 路径全部设置完毕。一路确定退出,打开新的命令行窗口,输入
javac
如果长长的洋文出现, Path 已经设置正确,一切正常。如果不是,请仔细检查本步骤是否完全设置正确。
7. 开始编程。在 C 盘的根目录中新建一个子目录,名为“ JavaTest ”,以作为存放 Java 源代码的地方。打开 XP 中的记事本,先将其保存到 JavaTest 文件夹中,在“文件名”文本框中输入 "Hello.java" 。注意,在文件名的前后各加上一个双引号,否则,记事本就会将其存为 "Hello.java.txt" 的文本文件。然后输入以下代码:
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, world");
}
}
再次保存文件。
8. 在命令行窗口中输入
cd C:/JavaTest
将当前路径转入 JavaTest 中。然后,输入
javac Hello.java
JDK 就在 JavaTest 文件夹中编译生成一个 Hello.class 的类文件。如果出现“ 1 error ”或“ XX errors ”的字样,说明是源代码的输入有误,请根据出错提示,仔细地按第 7 步的代码找出并修正错误。请读者注意甄别代码输入有误的问题与 classpath 设置有误的问题。因为本文是关于如何正确设置 classpath 及 package 的,因此,这里假设读者输入的代码准确无误。到目前为此,由于我们是在源代码的当前路径下编译,因此,不会出现 classpath 设置有误的问题。
9. 在命令行窗口中输入
java Hello
屏幕出现了
Hello world
成功了,我们已经顺利地编译及运行了第一个 Java 程序。
但是,第 8 步及第 9 步是不完美的,因为我们是在 JavaTest 这个存放源码的文件夹中进行编译及运行的,因此,一些非常重要的问题并没有暴露出来。实际上,第 8 步的“ javac Hello.java ”及第 9 步的“ java Hello ”涉及到两个问题,一是操作系统如何寻找“ javac ”及“ java ”等命令,二是操作系统如何寻找“ Hello.java ”及“ Hello.class ”这些用户自己创建的文件。对于“ javac ”及“ java ”等命令,由于它们均是可执行文件,操作系统就会依据我们在第 6 步中设置好的 Path 路径中去寻找。而对于“ Hello.java ”及“ Hello.class ”这些文件, Path 的设置不起作用。由于我们是在当前工作路径中工作, java 及 javac 会在当前工作路径中寻找相应的 java 文件 (class 文件的寻找比较特殊,详见第 11 步 ) ,
因此一切正常。下面我们开始人为地将问题复杂化,在非当前工作路径中编译及运行,看看结果如何。
10. 在命令行窗口中输入
cd C:
转入到 C 盘根目录上,当前路径离开了存放源码的工作区。输入
javac Hello.java
屏幕出现:
error: cannot read: Hello.java
1 error
找不到 Hello.java 了。我们要给它指定一个路径,告诉它到 C:/JavaTest 去找 Hello.java 文件。输入
javac C:/JavaTest/Hello.java
OK ,这回不报错了,编译成功。
11. 输入
java C:/JavaTest/Hello
这回屏幕出现:
Exception in thread "main" java.lang.NoClassDefFoundError: C:/JavaTest/Hello
意思为在“ C:/JavaTest/Hello ”找不到类的定义。明明 C:/JavaTest/Hello 是一个 .class 文件,为什么就找不到呢?原来, Java 对待 .java 文件与 .class 文件是有区别的。对 .java 文件可以直接指定路径给它,而 java 命令所需的 .class 文件不能出现扩展名,也不能指定额外的路径给它。
那么,如何指定路径呢?对于 Java 所需的 .class 文件,必须通过 classpath 来指定。
12. 依照第 5 步,弹出“环境变量”窗口,在用户变量中新建一个变量,变量名为“ classpath ”,变量值为 "C:/JavaTest" 。一路按“确定”退出。关闭原命令行窗口,打开新的命令行窗口,输入
java Hello
“ Hello world ”出来了。由此可见,在“环境变量”窗口中设置 classpath 的目的就是告诉 JDK ,到哪里去寻找 .class 文件。这种方法一旦设置好,以后每次运行 java 或 javac 时,在需要调用 .class 文件时, JDK 都会自动地来到这里寻找。因此,这是一个全局性的设置。
13. 除了这种在环境变量”窗口中设置 classpath 的方法之外,还有另一种方法,即在 java 命令后面加上一个选项 classpath ,紧跟着不带扩展名的 class 文件名。例如,
java -classpath C:/JavaTest Hello
JDK 遇到这种情况时,先根据命令行中的 classpath 选项中指定的路径去寻找 .class 文件,找不到时再到全局的 classpath 环境变量中去寻找。这种情况下,即使是没有设置全局的 classpath 环境变量,由于已经在命令行中正确地指定类路径,也可以运行。
为了在下面的例子中更好地演示 classpath 的问题,我们先将全局的 classpath 环境变量删除,而在必要时代之以命令行选项 -classpath 。弹出“环境变量”窗口,选中“ classpath ”的变量名,按“删除”键。
此外, java 命令中还可以用 cp ,即 classpath 的缩写来代替 classpath ,如 java -cp C:/JavaTest Hello 。特别注意的是, JDK 1.5.0 之前, javac 命令不能用 cp 来代替 classpath ,而只能用 classpath 。而在 JDK 1.5.0 中, java 及 javac 都可以使用 cp 及 classpath 。因此,为保持一致,建议一概使用 classpath 作为选项名称。
14. 我们再次人为地复杂化问题。关闭正在编辑 Hello.java 的记事本,然后将 JavaTest 文件夹名称改为带空格的“ Java Test ”。在命令行中输入
javac C:/Java Test/Hello.java
长长的洋文又出来了,但这回却是报错了:
javac: invalid flag: C:/Java
JDK 将带有空格的 C:/Java Test 分隔为两部分 "C:/Java" 及 "Test/Hello.java" ,并将 C:/Java 视作为一个无效的选项了。这种情况下,我们需要将整个路径都加上双引号,即
javac "C:/Java Test/Hello.java"
这回 JDK 知道,引号里面的是一个完整的路径,因此就不会报错了。同样,对 java 命令也需要如此,即
java -classpath "C:/Java Test" Hello
对于长文件名及中文的文件夹, XP 下面可以不加双引号。但一般来说,加双引号不容易出错,也容易理解,因此,建议在 classpath 选项中使用双引号。
15. 我们再来看 .java 文件使用了其他类的情况。在 C:/Java Test 中新建一个 Person.java 文件,内容如下:
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
然后,修改 Hello.java ,内容如下:
public class Hello {
public static void main(String[] args) {
Person person = new Person("Mike");
System.out.println(person.getName());
}
}
在命令行输入
javac "C:/Java Test/Hello.java"
错误来了:
C:/Java Test/Hello.java:3: cannot find symbol
symbol: class Person
JDK 提示找不到 Person 类。为什么 javac "C:/Java Test/Hello.java" 在第 14 步中可行,而在这里却不行了呢?第 14 步中的 Hello.java 文件并没有用来其他类,因此, JDK 不需要去寻找其他类,而到了这里,我们修改了 Hello.java ,让其使用了一个 Person 类。根据第 11 步,我们需要告诉 JDK ,到哪里去找所用到的类,即使这个被使用的类就与 Hello.java 一起,同在 C:/Java Test 下面!输入
javac -classpath "C:/Java Test" "C:/Java Test/Hello.java"
编译通过, JDK 在 C:/Java Test 文件夹下同时生成了 Hello.class 及 Person.class 两个文件。实际上,由于 Hello.java 使用了 Person.java 类, JDK 先编译生成了 Person.class ,然后再编译生成 Hello.class 。因此,不管 Hello.java 这个主类使用了多少个其他类,只要编译这个类, JDK 就会自动编译其他类,很方便。输入
java -classpath "C:/Java Test" Hello
屏幕出现了
Mike
成功。
16. 第 15 步说明了在 Hello.java 中如何使用一个我们自己创建的 Person.java ,而且这个类与 Hello.java 是同在一个文件夹下。在这一步中,我们将考查 Person.java 如果放在不同文件夹下面的情况。
先将 C:/Java Test 文件夹下的 Person.class 文件删除,然后在 C:/Java Test 文件夹下新建一个名为 DF 的文件夹,并将 C:/Java Test 文件夹下的 Person.java 移动到其下面。在命令行输入
javac -classpath "C:/Java Test/DF" "C:/Java Test/Hello.java"
编译通过。这时 javac 命令没有什么不同,只需将 classpath 改成 C:/Java Test/DF 就行了。
在命令行输入
java -classpath "C:/Java Test" Hello
这时由于 Java 需要找在不同文件夹下的两个 .class 文件,而命令行中只告诉 JDK 一个路径,即 C:/Java Test ,在此文件夹下,只能找到 Hello.class ,找不到 Person.class 文件,因此,错误是可以预料得到的:
Exception in thread "main" java.lang.NoClassDefFoundError: Person
at Hello.main(Hello.java:3)
果真找不到 Person.class 。在设置两个以上的 classpath 时,先将每个路径以双引号引起来,再将这些路径以“ ; ”号隔开,并且每个路径与“ ; ”之间不能带有空格。因此,我们在命令行重新输入:
java -classpath "C:/Java Test";"C:/Java Test/DF" Hello
编译成功。但也暴露出一个问题,如果我们需要用到许多分处于不同文件夹下的类,那这个 classpath 的设置岂不是很长!有没有办法,对于一个文件夹下的所有 .class 文件,只指定这个文件夹的 classpath ,然后让 JDK 自动搜索此文件夹下面所有相应的路径?有,只要使用 package 。
17. package 简介。 Java 中引入 package 的概念,主要是为了解决命名冲突的问题。比如说,在我们的例子中,我们设计了一个很简单的 Person 类,如果某人开发了一个类库,其中恰巧也有一个 Person 类,当我们使用这个类库时,两个 Person 类出现了命名冲突, JDK 不知道我们到底要使用哪个 Person 类。更有甚者,当我们也开发了一个很庞大的类库,无可避免地,我们的类库中与其他人开发的类库中命名冲突的情况就会越来越多。总不能为了避免自己的类名与其他人开发的类名相同,而让每个编程人员都绞尽脑汁地将一个本应叫 Writer 的类强行改名为 SarkuyaWriter , MikeWriter, SmithWriter 吧?
现实生活中也是如此。假如你名叫张三,又假如与你同一单位的人中有好几个都叫张三,那你的问题就来了。某天单位领导在会上宣布,张三被任命为办公室主任,你简直不知道是该哭还是该笑。但如果你的单位中只有你叫张三,你才不会在乎全国叫张三的人有多少个,因为其他张三都分布在全国各地、其他城市,你看不见他们,摸不着他们,自然不会担心。
Sun 从这个“张三问题”受到了很大的启发,为解决命名冲突问题,就采取了“眼不见心不烦”的策略:将每个类都归属到一个特定的区域中,在同一个区域中的所有类,都不允许同名;而不同区域的类,由于相互看不到,则允许有同名的类存在。这样,就解决了命名冲突的问题,正如北京的张三与上海的张三毕竟不是同一人。这个区域在 Java 中就叫 package 。由于 package 在 Java 中非常重要,如果你没有定义自己的 package , JDK 将会你的类都归到一个默认的无名 package 中。
自定义 package 的名称可以由各个程序员自由创建。作为避免命名冲突的手段, package 的名称最好足以与其他程序员的区别开来。在互联网上,每个域名都是唯一的,因此, Sun 推荐将你自己的域名倒写后作为 package 的名称。如果你没有自己的域名,很可能只是因为囊中羞涩而不去申请罢了,并不见得你假想的域名与其他域名发生冲突。例如,笔者假想的域名是 sarkuya.com ,目前就是唯一的,因此我的 package 就可以定名为 com.sarkuya 。谢谢 Java 给了我们一个免费使用我们自己域名的机会,唯一的前提是倒着写。当然,每个 package 下面还可以带有不同的子 package ,如 com.sarkuya.util , com.sarkuya.swing ,等等。
定义 package 的方式是在相应的 .java 文件的第一行加上“ package packagename; ”的字样,而且每个 .java 文件只能有一个 package 。实际上, Java 中的 package 的实现是与计算机文件系统相结合的,即你有什么样的 package ,在硬盘上就有什么样的存放路径。例如,某个类的 package 名为 com.sarkuya.util ,那么,这个类就应该必须存放在 com/sarkuya/util 的路径下面。至于这个 com/sarkuya/util 又是哪个文件夹的子路径,第 18 步会谈到。
package 除了有避免命名冲突的问题外,还引申出一个保护当前 package 下所有类文件的功能,主要通过为类定义几种可视度不同的修饰符来实现: public, protected, private, 另外加上一个并不真实存在的 friendly 类型。
对于冠以 public 的类、类属变量及方法,包内及包外的任何类均可以访问;
protected 的类、类属变量及方法,包内的任何类,及包外的那些继承了此类的子类才能访问;
private 的类、类属变量及方法,包内包外的任何类均不能访问;
如果一个类、类属变量及方法不以这三种修饰符来修饰,它就是 friendly 类型的,那么包内的任何类都可以访问它,而包外的任何类都不能访问它 ( 包括包外继承了此类的子类 ) ,因此,这种类、类属变量及方法对包内的其他类是友好的,开放的,而对包外的其他类是关闭的。
前面说过, package 主要是为了解决命名冲突的问题,因此,处在不同的包里面的类根本不用担心与其他包的类名发生冲突,因为 JDK 在默认情况下只使用本包下面的类,对于其他包, JDK 一概视而不见:“眼不见心不烦”。如果要引用其他包的类,就必须通过 import 来引入其他包中相应的类。只有在这时, JDK 才会进行进一步的审查,即根据其他包中的这些类、类属变量及方法的可视度来审查是否符合使用要求。如果此审查通不过,编译就此卡住,直至你放弃使用这些类、类属变量及方法,或者将被引入的类、类属变量及方法的修饰符改为符合要求为止。如果此审查通过, JDK 最后进行命名是否冲突的审查。如果发现命名冲突,你可以通过在代码中引用全名的方式来显式地引用相应的类,如使用
java.util.Date = new java.util.Date()
或是
java.sql.Date = new java.sql.Date() 。
package 的第三大作用是简化 classpath 的设置。还记得第 16 步中的障碍吗?这里重新引用其 java 命令:
java -classpath "C:/Java Test";"C:/Java Test/DF" Hello
我们必须将所有的 .class 文件的路径一一告诉 JDK ,而不管 DF 其实就是 C:/Java Test 的子目录。如果要用到 100 个不同路径的 .class 文件,我们就得将 classpath 设置为一个特别长的字符串,很累。 package 的引入,很好地解决了这个问题。 package 的与 classpath 相结合,通过 import 指令为中介,将原来必须由 classpath 完成的类路径搜索功能,很巧妙地转移到 import 的身上,从而使 classpath 的设置简洁明了。我们先看下面的例子。
18. 先在 Hello.java 中导入 DF.Person 。代码修改如下:
import DF.Person;
public class Hello {
public static void main(String[] args) {
Person person = new Person("Mike");
System.out.println(person.getName());
}
}
再将 DF 子文件夹中的 Person.java 设置一个 DF 包。代码修改如下:
package DF;
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
好了,神奇的命令行出现了:
javac -classpath "C:/Java Test" "C:/Java Test/Hello.java"
java -classpath "C:/Java Test" Hello
尽管这次我们只设置了 C:/Java Test 的 classpath ,但编译及运行居然都通过了!事实上, Java 在搜索 .class 文件时,共有三种方法:
一是全局性的设置,详见第 12 步,其优点是一次设置,每次使用;
二是在每次的 javac 及 java 命令行中自行设置 classpath ,这也是本文使用最多的一种方式,其优点是不加重系统环境变量的负担;
三是根据 import 指令,将其内容在后台转换为 classpath 。 JDK 将读取全局的环境变量 classpath 及命令行中的 classpath 选项信息,然后将每条 classpath 与经过转换为路径形式的 import 的内容相合并,从而形成最终的 classpath. 在我们的例子中, JDK 读取全局的环境变量 classpath 及命令行中的 classpath 选项信息,得到 C:/Java Test 。接着,将 import DF.Person 中的内容,即 DF.Person 转换为 DF/Person, 然后将 C:/Java Test 与其合并,成为 C:/Java Test/DF/Person ,这就是我们所需要的 Person.class 的路径。在 Hello.java 中有多少条 import 语句,就自动进行多少次这样的转换。而我们在命令行中只需告诉 JDK 最顶层的 classpath 就行了,剩下的则由各个类中的 import 指令代为操劳了。这种移花接木的作法为我们在命令行中手工地设置 classpath 提供了极大的便利。
应注意的一点是, import 指令是与 package 配套使用的,只有在某类通过“ package pacakgename; ”设定了包名后,才能给其他类通过 import 指令导入。如果 import 试图导入一个尚未设置包的类, JVM 就会报错。
19. 我们接下来看,当使用 JDK 类库时, classpath 如何设置。
20. 修改 Hello.java ,内容如下:
import DF.Person;
import java.util.Date;
public class Hello {
public static void main(String[] args) {
Date date = new Date();
System.out.println(date);
Person person = new Person("Mike");
System.out.println(person.getName());
}
}
21. JDK 类库存放于 C:/Program Files/Java/jdk1.5.0/jre/lib/rt.jar 文件中。关于 jar 文件的介绍,已经超出了本文的范围,感兴趣的读者可以阅读 Horstmann 写的 Core Java 一书。
jar 文件可以用 WinRar 打开。用 WinRar 打开后,可以看到里面有一些文件夹,双击其中的 java 文件夹,再双击 util 的文件夹,可以在看到 Date.class 文件就在其中。如果你看过 Data.java 或其他 JDK 类库的源码 ( 在 C:/Program Files/Java/jdk1.5.0/src.zip 文件中 ) ,你就会发现,像 java 、 util 这些文件夹均是 package 。这也是 Hello.java 第 2 行中使用了 import 指令的原因。
我们可以通过 WinRar 的查找功能来定位某个类所在的包。在“查找文件”的窗口中的“要查找的文件名”文本框中输入 Date.class ,就会查找出在 rt.jar 文件中存在两个 Date.class 文件,一个是 java/sql/Date.class ,另一个是 java/util/Date.class 。其中, sql 下面的 Date.class 文件与数据库有关,并非我们这里所需, java/util/Date.class 才是我们所要的。
rt.jar 文件就像本文中的 C:/Java Test 中一样,是 JDK 类库的唯一入口。我们可以在命令行的 classpath 选项指定 .jar 文件。需要注意, .jar 文件的 classpath 设置有些特珠。在以前的例子中,我们设置 classpath 时都是设置了路径就行了,而对于 .jar 文件,我们必须将 .jar 文件名直接加到 classpath 中。
22. 在命令行输入
javac -classpath "C:/Program Files/Java/jdk1.5.0/jre/lib/rt.jar";"C:/Java Test" "C:/Java Test/Hello.java"
java -classpath "C:/Program Files/Java/jdk1.5.0/jre/lib/rt.jar";"C:/Java Test" Hello
这样当然没有问题,因为我们指定了 rt.jar 文件及 C:/Java Test 两个 classpath 。但且慢,在命令行输入:
javac -classpath "C:/Java Test" "C:/Java Test/Hello.java"
java -classpath "C:/Java Test" Hello
不可思议的是,编译及运行成功了!令人惊讶的是在我们将 classspath 只设置为 C:/Java Test 的情况下, JDK 如何得出 java.util.Date 的 classpath ?
原因在于,就像 java 的 Path 路径已经悄悄在后台设置好一样, rt.jar 的 classpath 路径也悄悄地在后台设置了。因此,我们不必多此一举手工设置其 classpath 了。
23. 最后一点需要谈到的是,如果主类恰好也在一个 package 中 ( 在大型的开发中,其实这才是一种最常见的现象 ) ,那么 java 命令行的类名前面就必须加上包名。
在 C:/Java Test 下面新建一个文件夹,名为 NF 。将 C:/Java Test 下面的 Hello.class 删除,将 Hello.java 移到 NF 文件夹下。打开 NF 文件夹下的 Hello.java ,为其设置 package 属性。
package NF;
import DF.Person;
import java.util.Date;
public class Hello {
public static void main(String[] args) {
Date date = new Date();
System.out.println(date);
Person person = new Person("Mike");
System.out.println(person.getName());
}
}
编译与以前没啥区别,只不过是修正一下改过之后的路径。
javac -classpath "C:/Java Test" "C:/Java Test/NF/Hello.java"
而 java 命令行却有了变化
java -classpath "C:/Java Test" NF.Hello
上面命令行语句中, NF.Hello 告诉 JDK , Hello.class 在 NF 的 package 下面。
至此,本文有关 classpath 及 package 的问题的讨论已经全部结束。由此可见, Java 的入门的确非常不易。如果初学 Java 的程序员一见到 Java 的编译竟是如此的复杂,多半就会抽身而退。因此,笔者认为, Sun 在 J2SE 的 Tutorial 中故意将编译的问题尽量简单化,以吸引更多的 Java 初学者。一旦品尝了 Java 的香醇可口的美味后,就不用担心他们退出了,因为咖啡是非常容易让人上瘾的。
1. 下载并安装 JDK1.5.0 ,并按默认路径,安装到 C:/Program Files/Java/jdk1.5.0 中。
2. 用鼠标单击 WindowsXP 的“开始” -> “运行”,在弹出的运行窗口中输入 cmd ,按确定或回车,打开一个命令行窗口。
3. 在命令行中输入:
java
有一列长长的洋文滚了出来,这是 JDK 告诉我们 java 这个命令的使用方法。其中隐含了一个重要信息,即 JDK 安装成功,可以在命令行中使用 java 此命令了。
4. 在命令行中输入
javac
屏幕显示:
'javac' 不是内部或外部命令,也不是可运行的程序或批处理文件。
这是由于 windows 找不到 javac 这个命令的原因。这就不明白了, java 与 javac 都是 JDK 在同一个子目录里面的两个文件,为什么可以直接运行 java 而不能直接运行 javac 呢?原来, Sun 公司为了方便大家在安装完 JDK 后马上就可以运行 Java 类文件,在后台悄悄地将 java 命令加入了 Path 的搜索路径中,因此我们可以直接运行 java 命令 ( 但我们是看不到它到底是在哪设置的,无论是在用户的 Path 或系统的 Path 设置中均找不到这个 java 存放的路径 ) 。但 Sun 所做的到此为止,其他 JDK 的命令,一概不管,需要由用户自己添加到搜索路径中。
5. 既然如此,那我们自己添加 Path 的搜索路径吧。对“我的电脑”按右键,选“属性”,在“系统属性”窗口中选“高级”标签,再按“环境变量”按钮,弹出一个“环境变量”的窗口,在用户变量中新建一个变量,变量名为“ Path ”,变量值为 "C:/Program Files/Java/jdk1.5.0/bin;%PATH%" 。最后的 %PATH% 的意思是说,保留原有的 Path 设置,且将目前的 Path 设置新加到其前面。一路按“确定”退出 ( 共有 3 次 ) 。关掉原来的命令行窗口,依照第 2 步,重新打开一个新的命令行窗口。在此窗口中输入
javac
长长的洋文又出现了,这回是介绍 javac 的用法。设置成功。
6. So far so good. 到目前为止,我们已经可以编程了。但是,这不是一个好办法。因为随着以后我们深入学习 Java ,我们就会用到 JUnit 、 Ant 或 NetBeans 等应用工具,这些工具在安装时,都需要一个名为指向 JDK 路径的“ JAVA_HOME ”的环境变量,否则就安装不了。因此,我们需要改进第 5 步,为以后作好准备。依照第 5 步,弹出“环境变量”的窗口,在用户变量中新建一个变量,变量名为“ JAVA_HOME ”,变量值为 "C:/Program Files/Java/jdk1.5.0" 。注意,这里的变量值只到 jdk1.5.0 ,不能延伸到 bin 中。确定后,返回“环境变量”的窗口,双击我们原先设定的 Path 变量,将其值修改为“ %JAVA_HOME%/bin;%PATH% ”。这种效果与第 5 步是完全一样的,只不过多了一个 JAVA_HOME 的变量。这样,以后当我们需要指向 JDK 的路径时,只需要加入“ %JAVA_HOME% ”就行了。至此, Path 路径全部设置完毕。一路确定退出,打开新的命令行窗口,输入
javac
如果长长的洋文出现, Path 已经设置正确,一切正常。如果不是,请仔细检查本步骤是否完全设置正确。
7. 开始编程。在 C 盘的根目录中新建一个子目录,名为“ JavaTest ”,以作为存放 Java 源代码的地方。打开 XP 中的记事本,先将其保存到 JavaTest 文件夹中,在“文件名”文本框中输入 "Hello.java" 。注意,在文件名的前后各加上一个双引号,否则,记事本就会将其存为 "Hello.java.txt" 的文本文件。然后输入以下代码:
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, world");
}
}
再次保存文件。
8. 在命令行窗口中输入
cd C:/JavaTest
将当前路径转入 JavaTest 中。然后,输入
javac Hello.java
JDK 就在 JavaTest 文件夹中编译生成一个 Hello.class 的类文件。如果出现“ 1 error ”或“ XX errors ”的字样,说明是源代码的输入有误,请根据出错提示,仔细地按第 7 步的代码找出并修正错误。请读者注意甄别代码输入有误的问题与 classpath 设置有误的问题。因为本文是关于如何正确设置 classpath 及 package 的,因此,这里假设读者输入的代码准确无误。到目前为此,由于我们是在源代码的当前路径下编译,因此,不会出现 classpath 设置有误的问题。
9. 在命令行窗口中输入
java Hello
屏幕出现了
Hello world
成功了,我们已经顺利地编译及运行了第一个 Java 程序。
但是,第 8 步及第 9 步是不完美的,因为我们是在 JavaTest 这个存放源码的文件夹中进行编译及运行的,因此,一些非常重要的问题并没有暴露出来。实际上,第 8 步的“ javac Hello.java ”及第 9 步的“ java Hello ”涉及到两个问题,一是操作系统如何寻找“ javac ”及“ java ”等命令,二是操作系统如何寻找“ Hello.java ”及“ Hello.class ”这些用户自己创建的文件。对于“ javac ”及“ java ”等命令,由于它们均是可执行文件,操作系统就会依据我们在第 6 步中设置好的 Path 路径中去寻找。而对于“ Hello.java ”及“ Hello.class ”这些文件, Path 的设置不起作用。由于我们是在当前工作路径中工作, java 及 javac 会在当前工作路径中寻找相应的 java 文件 (class 文件的寻找比较特殊,详见第 11 步 ) ,
因此一切正常。下面我们开始人为地将问题复杂化,在非当前工作路径中编译及运行,看看结果如何。
10. 在命令行窗口中输入
cd C:
转入到 C 盘根目录上,当前路径离开了存放源码的工作区。输入
javac Hello.java
屏幕出现:
error: cannot read: Hello.java
1 error
找不到 Hello.java 了。我们要给它指定一个路径,告诉它到 C:/JavaTest 去找 Hello.java 文件。输入
javac C:/JavaTest/Hello.java
OK ,这回不报错了,编译成功。
11. 输入
java C:/JavaTest/Hello
这回屏幕出现:
Exception in thread "main" java.lang.NoClassDefFoundError: C:/JavaTest/Hello
意思为在“ C:/JavaTest/Hello ”找不到类的定义。明明 C:/JavaTest/Hello 是一个 .class 文件,为什么就找不到呢?原来, Java 对待 .java 文件与 .class 文件是有区别的。对 .java 文件可以直接指定路径给它,而 java 命令所需的 .class 文件不能出现扩展名,也不能指定额外的路径给它。
那么,如何指定路径呢?对于 Java 所需的 .class 文件,必须通过 classpath 来指定。
12. 依照第 5 步,弹出“环境变量”窗口,在用户变量中新建一个变量,变量名为“ classpath ”,变量值为 "C:/JavaTest" 。一路按“确定”退出。关闭原命令行窗口,打开新的命令行窗口,输入
java Hello
“ Hello world ”出来了。由此可见,在“环境变量”窗口中设置 classpath 的目的就是告诉 JDK ,到哪里去寻找 .class 文件。这种方法一旦设置好,以后每次运行 java 或 javac 时,在需要调用 .class 文件时, JDK 都会自动地来到这里寻找。因此,这是一个全局性的设置。
13. 除了这种在环境变量”窗口中设置 classpath 的方法之外,还有另一种方法,即在 java 命令后面加上一个选项 classpath ,紧跟着不带扩展名的 class 文件名。例如,
java -classpath C:/JavaTest Hello
JDK 遇到这种情况时,先根据命令行中的 classpath 选项中指定的路径去寻找 .class 文件,找不到时再到全局的 classpath 环境变量中去寻找。这种情况下,即使是没有设置全局的 classpath 环境变量,由于已经在命令行中正确地指定类路径,也可以运行。
为了在下面的例子中更好地演示 classpath 的问题,我们先将全局的 classpath 环境变量删除,而在必要时代之以命令行选项 -classpath 。弹出“环境变量”窗口,选中“ classpath ”的变量名,按“删除”键。
此外, java 命令中还可以用 cp ,即 classpath 的缩写来代替 classpath ,如 java -cp C:/JavaTest Hello 。特别注意的是, JDK 1.5.0 之前, javac 命令不能用 cp 来代替 classpath ,而只能用 classpath 。而在 JDK 1.5.0 中, java 及 javac 都可以使用 cp 及 classpath 。因此,为保持一致,建议一概使用 classpath 作为选项名称。
14. 我们再次人为地复杂化问题。关闭正在编辑 Hello.java 的记事本,然后将 JavaTest 文件夹名称改为带空格的“ Java Test ”。在命令行中输入
javac C:/Java Test/Hello.java
长长的洋文又出来了,但这回却是报错了:
javac: invalid flag: C:/Java
JDK 将带有空格的 C:/Java Test 分隔为两部分 "C:/Java" 及 "Test/Hello.java" ,并将 C:/Java 视作为一个无效的选项了。这种情况下,我们需要将整个路径都加上双引号,即
javac "C:/Java Test/Hello.java"
这回 JDK 知道,引号里面的是一个完整的路径,因此就不会报错了。同样,对 java 命令也需要如此,即
java -classpath "C:/Java Test" Hello
对于长文件名及中文的文件夹, XP 下面可以不加双引号。但一般来说,加双引号不容易出错,也容易理解,因此,建议在 classpath 选项中使用双引号。
15. 我们再来看 .java 文件使用了其他类的情况。在 C:/Java Test 中新建一个 Person.java 文件,内容如下:
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
然后,修改 Hello.java ,内容如下:
public class Hello {
public static void main(String[] args) {
Person person = new Person("Mike");
System.out.println(person.getName());
}
}
在命令行输入
javac "C:/Java Test/Hello.java"
错误来了:
C:/Java Test/Hello.java:3: cannot find symbol
symbol: class Person
JDK 提示找不到 Person 类。为什么 javac "C:/Java Test/Hello.java" 在第 14 步中可行,而在这里却不行了呢?第 14 步中的 Hello.java 文件并没有用来其他类,因此, JDK 不需要去寻找其他类,而到了这里,我们修改了 Hello.java ,让其使用了一个 Person 类。根据第 11 步,我们需要告诉 JDK ,到哪里去找所用到的类,即使这个被使用的类就与 Hello.java 一起,同在 C:/Java Test 下面!输入
javac -classpath "C:/Java Test" "C:/Java Test/Hello.java"
编译通过, JDK 在 C:/Java Test 文件夹下同时生成了 Hello.class 及 Person.class 两个文件。实际上,由于 Hello.java 使用了 Person.java 类, JDK 先编译生成了 Person.class ,然后再编译生成 Hello.class 。因此,不管 Hello.java 这个主类使用了多少个其他类,只要编译这个类, JDK 就会自动编译其他类,很方便。输入
java -classpath "C:/Java Test" Hello
屏幕出现了
Mike
成功。
16. 第 15 步说明了在 Hello.java 中如何使用一个我们自己创建的 Person.java ,而且这个类与 Hello.java 是同在一个文件夹下。在这一步中,我们将考查 Person.java 如果放在不同文件夹下面的情况。
先将 C:/Java Test 文件夹下的 Person.class 文件删除,然后在 C:/Java Test 文件夹下新建一个名为 DF 的文件夹,并将 C:/Java Test 文件夹下的 Person.java 移动到其下面。在命令行输入
javac -classpath "C:/Java Test/DF" "C:/Java Test/Hello.java"
编译通过。这时 javac 命令没有什么不同,只需将 classpath 改成 C:/Java Test/DF 就行了。
在命令行输入
java -classpath "C:/Java Test" Hello
这时由于 Java 需要找在不同文件夹下的两个 .class 文件,而命令行中只告诉 JDK 一个路径,即 C:/Java Test ,在此文件夹下,只能找到 Hello.class ,找不到 Person.class 文件,因此,错误是可以预料得到的:
Exception in thread "main" java.lang.NoClassDefFoundError: Person
at Hello.main(Hello.java:3)
果真找不到 Person.class 。在设置两个以上的 classpath 时,先将每个路径以双引号引起来,再将这些路径以“ ; ”号隔开,并且每个路径与“ ; ”之间不能带有空格。因此,我们在命令行重新输入:
java -classpath "C:/Java Test";"C:/Java Test/DF" Hello
编译成功。但也暴露出一个问题,如果我们需要用到许多分处于不同文件夹下的类,那这个 classpath 的设置岂不是很长!有没有办法,对于一个文件夹下的所有 .class 文件,只指定这个文件夹的 classpath ,然后让 JDK 自动搜索此文件夹下面所有相应的路径?有,只要使用 package 。
17. package 简介。 Java 中引入 package 的概念,主要是为了解决命名冲突的问题。比如说,在我们的例子中,我们设计了一个很简单的 Person 类,如果某人开发了一个类库,其中恰巧也有一个 Person 类,当我们使用这个类库时,两个 Person 类出现了命名冲突, JDK 不知道我们到底要使用哪个 Person 类。更有甚者,当我们也开发了一个很庞大的类库,无可避免地,我们的类库中与其他人开发的类库中命名冲突的情况就会越来越多。总不能为了避免自己的类名与其他人开发的类名相同,而让每个编程人员都绞尽脑汁地将一个本应叫 Writer 的类强行改名为 SarkuyaWriter , MikeWriter, SmithWriter 吧?
现实生活中也是如此。假如你名叫张三,又假如与你同一单位的人中有好几个都叫张三,那你的问题就来了。某天单位领导在会上宣布,张三被任命为办公室主任,你简直不知道是该哭还是该笑。但如果你的单位中只有你叫张三,你才不会在乎全国叫张三的人有多少个,因为其他张三都分布在全国各地、其他城市,你看不见他们,摸不着他们,自然不会担心。
Sun 从这个“张三问题”受到了很大的启发,为解决命名冲突问题,就采取了“眼不见心不烦”的策略:将每个类都归属到一个特定的区域中,在同一个区域中的所有类,都不允许同名;而不同区域的类,由于相互看不到,则允许有同名的类存在。这样,就解决了命名冲突的问题,正如北京的张三与上海的张三毕竟不是同一人。这个区域在 Java 中就叫 package 。由于 package 在 Java 中非常重要,如果你没有定义自己的 package , JDK 将会你的类都归到一个默认的无名 package 中。
自定义 package 的名称可以由各个程序员自由创建。作为避免命名冲突的手段, package 的名称最好足以与其他程序员的区别开来。在互联网上,每个域名都是唯一的,因此, Sun 推荐将你自己的域名倒写后作为 package 的名称。如果你没有自己的域名,很可能只是因为囊中羞涩而不去申请罢了,并不见得你假想的域名与其他域名发生冲突。例如,笔者假想的域名是 sarkuya.com ,目前就是唯一的,因此我的 package 就可以定名为 com.sarkuya 。谢谢 Java 给了我们一个免费使用我们自己域名的机会,唯一的前提是倒着写。当然,每个 package 下面还可以带有不同的子 package ,如 com.sarkuya.util , com.sarkuya.swing ,等等。
定义 package 的方式是在相应的 .java 文件的第一行加上“ package packagename; ”的字样,而且每个 .java 文件只能有一个 package 。实际上, Java 中的 package 的实现是与计算机文件系统相结合的,即你有什么样的 package ,在硬盘上就有什么样的存放路径。例如,某个类的 package 名为 com.sarkuya.util ,那么,这个类就应该必须存放在 com/sarkuya/util 的路径下面。至于这个 com/sarkuya/util 又是哪个文件夹的子路径,第 18 步会谈到。
package 除了有避免命名冲突的问题外,还引申出一个保护当前 package 下所有类文件的功能,主要通过为类定义几种可视度不同的修饰符来实现: public, protected, private, 另外加上一个并不真实存在的 friendly 类型。
对于冠以 public 的类、类属变量及方法,包内及包外的任何类均可以访问;
protected 的类、类属变量及方法,包内的任何类,及包外的那些继承了此类的子类才能访问;
private 的类、类属变量及方法,包内包外的任何类均不能访问;
如果一个类、类属变量及方法不以这三种修饰符来修饰,它就是 friendly 类型的,那么包内的任何类都可以访问它,而包外的任何类都不能访问它 ( 包括包外继承了此类的子类 ) ,因此,这种类、类属变量及方法对包内的其他类是友好的,开放的,而对包外的其他类是关闭的。
前面说过, package 主要是为了解决命名冲突的问题,因此,处在不同的包里面的类根本不用担心与其他包的类名发生冲突,因为 JDK 在默认情况下只使用本包下面的类,对于其他包, JDK 一概视而不见:“眼不见心不烦”。如果要引用其他包的类,就必须通过 import 来引入其他包中相应的类。只有在这时, JDK 才会进行进一步的审查,即根据其他包中的这些类、类属变量及方法的可视度来审查是否符合使用要求。如果此审查通不过,编译就此卡住,直至你放弃使用这些类、类属变量及方法,或者将被引入的类、类属变量及方法的修饰符改为符合要求为止。如果此审查通过, JDK 最后进行命名是否冲突的审查。如果发现命名冲突,你可以通过在代码中引用全名的方式来显式地引用相应的类,如使用
java.util.Date = new java.util.Date()
或是
java.sql.Date = new java.sql.Date() 。
package 的第三大作用是简化 classpath 的设置。还记得第 16 步中的障碍吗?这里重新引用其 java 命令:
java -classpath "C:/Java Test";"C:/Java Test/DF" Hello
我们必须将所有的 .class 文件的路径一一告诉 JDK ,而不管 DF 其实就是 C:/Java Test 的子目录。如果要用到 100 个不同路径的 .class 文件,我们就得将 classpath 设置为一个特别长的字符串,很累。 package 的引入,很好地解决了这个问题。 package 的与 classpath 相结合,通过 import 指令为中介,将原来必须由 classpath 完成的类路径搜索功能,很巧妙地转移到 import 的身上,从而使 classpath 的设置简洁明了。我们先看下面的例子。
18. 先在 Hello.java 中导入 DF.Person 。代码修改如下:
import DF.Person;
public class Hello {
public static void main(String[] args) {
Person person = new Person("Mike");
System.out.println(person.getName());
}
}
再将 DF 子文件夹中的 Person.java 设置一个 DF 包。代码修改如下:
package DF;
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
好了,神奇的命令行出现了:
javac -classpath "C:/Java Test" "C:/Java Test/Hello.java"
java -classpath "C:/Java Test" Hello
尽管这次我们只设置了 C:/Java Test 的 classpath ,但编译及运行居然都通过了!事实上, Java 在搜索 .class 文件时,共有三种方法:
一是全局性的设置,详见第 12 步,其优点是一次设置,每次使用;
二是在每次的 javac 及 java 命令行中自行设置 classpath ,这也是本文使用最多的一种方式,其优点是不加重系统环境变量的负担;
三是根据 import 指令,将其内容在后台转换为 classpath 。 JDK 将读取全局的环境变量 classpath 及命令行中的 classpath 选项信息,然后将每条 classpath 与经过转换为路径形式的 import 的内容相合并,从而形成最终的 classpath. 在我们的例子中, JDK 读取全局的环境变量 classpath 及命令行中的 classpath 选项信息,得到 C:/Java Test 。接着,将 import DF.Person 中的内容,即 DF.Person 转换为 DF/Person, 然后将 C:/Java Test 与其合并,成为 C:/Java Test/DF/Person ,这就是我们所需要的 Person.class 的路径。在 Hello.java 中有多少条 import 语句,就自动进行多少次这样的转换。而我们在命令行中只需告诉 JDK 最顶层的 classpath 就行了,剩下的则由各个类中的 import 指令代为操劳了。这种移花接木的作法为我们在命令行中手工地设置 classpath 提供了极大的便利。
应注意的一点是, import 指令是与 package 配套使用的,只有在某类通过“ package pacakgename; ”设定了包名后,才能给其他类通过 import 指令导入。如果 import 试图导入一个尚未设置包的类, JVM 就会报错。
19. 我们接下来看,当使用 JDK 类库时, classpath 如何设置。
20. 修改 Hello.java ,内容如下:
import DF.Person;
import java.util.Date;
public class Hello {
public static void main(String[] args) {
Date date = new Date();
System.out.println(date);
Person person = new Person("Mike");
System.out.println(person.getName());
}
}
21. JDK 类库存放于 C:/Program Files/Java/jdk1.5.0/jre/lib/rt.jar 文件中。关于 jar 文件的介绍,已经超出了本文的范围,感兴趣的读者可以阅读 Horstmann 写的 Core Java 一书。
jar 文件可以用 WinRar 打开。用 WinRar 打开后,可以看到里面有一些文件夹,双击其中的 java 文件夹,再双击 util 的文件夹,可以在看到 Date.class 文件就在其中。如果你看过 Data.java 或其他 JDK 类库的源码 ( 在 C:/Program Files/Java/jdk1.5.0/src.zip 文件中 ) ,你就会发现,像 java 、 util 这些文件夹均是 package 。这也是 Hello.java 第 2 行中使用了 import 指令的原因。
我们可以通过 WinRar 的查找功能来定位某个类所在的包。在“查找文件”的窗口中的“要查找的文件名”文本框中输入 Date.class ,就会查找出在 rt.jar 文件中存在两个 Date.class 文件,一个是 java/sql/Date.class ,另一个是 java/util/Date.class 。其中, sql 下面的 Date.class 文件与数据库有关,并非我们这里所需, java/util/Date.class 才是我们所要的。
rt.jar 文件就像本文中的 C:/Java Test 中一样,是 JDK 类库的唯一入口。我们可以在命令行的 classpath 选项指定 .jar 文件。需要注意, .jar 文件的 classpath 设置有些特珠。在以前的例子中,我们设置 classpath 时都是设置了路径就行了,而对于 .jar 文件,我们必须将 .jar 文件名直接加到 classpath 中。
22. 在命令行输入
javac -classpath "C:/Program Files/Java/jdk1.5.0/jre/lib/rt.jar";"C:/Java Test" "C:/Java Test/Hello.java"
java -classpath "C:/Program Files/Java/jdk1.5.0/jre/lib/rt.jar";"C:/Java Test" Hello
这样当然没有问题,因为我们指定了 rt.jar 文件及 C:/Java Test 两个 classpath 。但且慢,在命令行输入:
javac -classpath "C:/Java Test" "C:/Java Test/Hello.java"
java -classpath "C:/Java Test" Hello
不可思议的是,编译及运行成功了!令人惊讶的是在我们将 classspath 只设置为 C:/Java Test 的情况下, JDK 如何得出 java.util.Date 的 classpath ?
原因在于,就像 java 的 Path 路径已经悄悄在后台设置好一样, rt.jar 的 classpath 路径也悄悄地在后台设置了。因此,我们不必多此一举手工设置其 classpath 了。
23. 最后一点需要谈到的是,如果主类恰好也在一个 package 中 ( 在大型的开发中,其实这才是一种最常见的现象 ) ,那么 java 命令行的类名前面就必须加上包名。
在 C:/Java Test 下面新建一个文件夹,名为 NF 。将 C:/Java Test 下面的 Hello.class 删除,将 Hello.java 移到 NF 文件夹下。打开 NF 文件夹下的 Hello.java ,为其设置 package 属性。
package NF;
import DF.Person;
import java.util.Date;
public class Hello {
public static void main(String[] args) {
Date date = new Date();
System.out.println(date);
Person person = new Person("Mike");
System.out.println(person.getName());
}
}
编译与以前没啥区别,只不过是修正一下改过之后的路径。
javac -classpath "C:/Java Test" "C:/Java Test/NF/Hello.java"
而 java 命令行却有了变化
java -classpath "C:/Java Test" NF.Hello
上面命令行语句中, NF.Hello 告诉 JDK , Hello.class 在 NF 的 package 下面。
至此,本文有关 classpath 及 package 的问题的讨论已经全部结束。由此可见, Java 的入门的确非常不易。如果初学 Java 的程序员一见到 Java 的编译竟是如此的复杂,多半就会抽身而退。因此,笔者认为, Sun 在 J2SE 的 Tutorial 中故意将编译的问题尽量简单化,以吸引更多的 Java 初学者。一旦品尝了 Java 的香醇可口的美味后,就不用担心他们退出了,因为咖啡是非常容易让人上瘾的。