很多书籍和网络教程关于Java编程环境搭建的内容,一般都言简意赅,赘述如下:
1、安装JDK and JRE,然后设置环境变量
2、新建"JAVA_HOME”变量,设置为JDK的安装路径
3、新建"CLASSPATH”变量,设置为"%JAVA_HOME%\lib\tools.jar;%JAVA_HOME%\lib\dt.jar;."
4、修改"Path”变量,补充"%JAVA_HOME%\bin”
5、编写一段程序,然后javac&java,Hello World
确实,搞定"Hello World”是一件挺简单的事,如果使用某些IDE甚至连上述步骤都可以略掉,javac和java他俩点了回名就被扔到了墙角。当然这无可厚非,强大的IDE让我们的编码工作变得单纯了许多。然而,他们果真不值一提么?
先想想下面这个小场景,如果刚刚读完场景你就完全明白怎么回事,请略过本文,很抱歉浪费了你看上文的时间;如果你确实还有些含糊,那不妨花上几分钟一起研究下。
场景描述:com.idearye.main.MyMain.java和com.idearye.friend.MyFriend.java两个文件,MyMain类import了MyFriend类,并在main方法中创建一个MyFriend对象,异常简单的场景。如果使用IDE,工程中创建两个包两个文件,然后build完事。但,如果不使用IDE,你是否还能正确的应用javac和java编译及运行这个程序?如果稍微再搞复杂一些,MyFriend类又import了另一个类com.idearye.enemy.MyEnemy,三个文件分处在三个包中,对你是否依然简单?
MyMain.java和MyFriend.java源代码如下:
package com.idearye.main; import com.idearye.friend.*; public class MyMain{ public static void main(String[] args){ MyFriend myFriend = new MyFriend(); } } package com.idearye.friend; public class MyFriend{ public MyFriend(){ } }
package是Java引入的非常漂亮的组织结构理念,合理的打包能让应用程序各模块得到有序的维护,因此在有点规模有点心思的程序中,基本不会出现所有文件都在同一目录的情况。
我的尝试:
第一步,直接在main目录中编译的后果是:
>> javac MyMain.java >> MyMain.java:3: package com.idearye.friend does not exist >> import com.idearye.friend.*; >> ^ >> MyMain.java:7: cannot find symbol >> symbol : class MyFriend >> location: class com.idearye.main.MyMain >> MyFriend myFriend = new MyFriend(); >> ^ >> MyMain.java:7: cannot find symbol >> symbol : class MyFriend >> location: class com.idearye.main.MyMain >> MyFriend myFriend = new MyFriend(); ^ >> 3 errors
第二步,提示MyMain找不到com.idearye.friend.MyFriend类,如果我预先编译一下MyFriend类呢?并将MyFriend.class文件拷贝到main目录中,这下你总该能找到了吧?
>> cd friend >> javac MyFriend.java >> 这步没有任何错误,将生成的class文件拷贝到main目录中 >> javac MyMain.java >> MyMain.java:3: package com.idearye.friend does not exist >> import com.idearye.friend.*; >> ^ >> MyMain.java:7: cannot find symbol >> symbol : class MyFriend >> location: class com.idearye.main.MyMain >> MyFriend myFriend = new MyFriend(); >> ^ >> MyMain.java:7: cannot find symbol >> symbol : class MyFriend >> location: class com.idearye.main.MyMain >> MyFriend myFriend = new MyFriend(); ^ >> 3 errors
嘿,当前路径下明明有class文件,而且系统的CLASSPATH环境变量中也有"."代表当前路径啊,编译器怎么还提示这个错误呢?
第三步,先不说原因,我们这样试试先,仍然回到main目录中
>> cd main >> javac -sourcepath "D:\00_code\99_OTHER\Java\src" -classpath "D:\00_code\99_OTHER\Java\classes" -d "D:\00_code\99_OTHER\Java\classes" MyMain.java >> 没有任何提示,编译成功,回到包的顶层,即"D:\00_code\99_OTHER\Java\",查看目录树 >> tree /f >> ├─classes >> │ └─com >> │ └─idearye >> │ ├─friend >> │ │ MyFriend.class >> │ └─main >> │ MyMain.class >> └─src >> └─com >> └─idearye >> ├─friend >> │ MyFriend.java >> └─main >> MyMain.java
或者,我们回到D盘根目录,玩一个跨度更大的!
>> javac -sourcepath "D:\00_code\99_OTHER\Java\src" -classpath "D:\00_code\99_OTHER\Java\classes" -d "D:\00_code\99_OTHER\Java\classes" D:\00_code\99_OTHER\Java\src\com\idearye\main\MyMain.java >> 没有任何提示,编译成功,回到包的顶层,即"D:\00_code\99_OTHER\Java\",查看目录树 >> tree /f >> ├─classes >> │ └─com >> │ └─idearye >> │ ├─friend >> │ │ MyFriend.class >> │ └─main >> │ MyMain.class >> └─src >> └─com >> └─idearye >> ├─friend >> │ MyFriend.java >> └─main >> MyMain.java
恩,可以正确编译的程序和编译错误的程序,在于为javac指定了3个参数,"-sourcepath”、"-classpath”和"-d"。这三个参数对于编译器的意味着什么呢?
我们还是先说一下为何第一步与第二步会出现失败。一个类需要指明所属的包的,包路径与类名共同组成了该类的完整名称,即某某书上说的专业术语“完全限定类名”云云。编译器要找到一个类,需要这个“完全限定类名”。我们打一个比方,好比警察要找你,光知道你的名字是没用的,警察需要知道你是哪个国家、哪个省、哪个城市、哪个片区、哪个小区、哪个门牌号,当然,作为警察,他可以穷举这个世界所有叫你这个名字的人,然后一个个的排查,好比《盗墓笔记》中张大佛爷把全国叫张起灵的都揪出来,但,这需要大量资源,警察不会这么二,于是蛋生了蛋疼的户口本;编译器也不会这么二,于是蛋生了下一段:
MyMain类中引用了MyFriend类,而MyFriend类位于com.idearye.friend包中,编译器会从"classpath”参数指明的目录中寻找"MyFriend.class"文件,然后到"sourcepath"参数指明的目录中寻找"MyFriend.java"文件。如果同时找到这两个东西,则会判断它们蛋生的时间。如果MyFriend.java新于MyFriend.class,则会重新编译MyFriend.java,否则会直接使用MyFriend.class。如果只找到了MyFriend.java,就启动编译,如果只找到了MyFriend.class,就直接使用。如果都找到,就会抛出前两种场景中著名的“Cann't Find Symbol”的错误。
而"-d"参数指明了生成的class文件保存的根目录,如果类的源文件声明了所属包,则会在该目录中生成包对应的目录树结构,这个参数很简单。我们可以回想一下诸如Eclips创建工程时,你可以指明output或者bin目录,其实IDE就是根据这个参数来放置生成的class文件,同时也会自动从这个目录中开始寻找所需要的类。
MyMain类中引用了MyFriend类,而MyFriend类位于com.idearye.friend包中,编译器会从"classpath”参数指明的目录中寻找"MyFriend.class"文件,然后到"sourcepath"参数指明的目录中寻找"MyFriend.java"文件。如果同时找到这两个东西,则会判断它们蛋生的时间。如果MyFriend.java新于MyFriend.class,则会重新编译MyFriend.java,否则会直接使用MyFriend.class。如果只找到了MyFriend.java,就启动编译,如果只找到了MyFriend.class,就直接使用。如果都找到,就会抛出前两种场景中著名的“Cann't Find Symbol”的错误。
而"-d"参数指明了生成的class文件保存的根目录,如果类的源文件声明了所属包,则会在该目录中生成包对应的目录树结构,这个参数很简单。我们可以回想一下诸如Eclips创建工程时,你可以指明output或者bin目录,其实IDE就是根据这个参数来放置生成的class文件,同时也会自动从这个目录中开始寻找所需要的类。
上面这段话,其实很直白的告诉我们一个道理,“相对论”是智慧人玩的游戏。不认识你的人想找你,你就不能只说“我在XX房间等你”,如果你们同在一个城市,你可以告诉他“我在XX区XX旅馆XX房间等你”;如果你们在不同的城市甚至不同的国家,你就必须要指定更详细的地址,但无论你们分隔多远,只要你说“我在XX国-XX城市-XX区-XX旅馆-XX房间等你”,他/她想找你就一定能够找到。其实编译器最希望的,就是你明明白白告诉他去哪里找所需要的类。这也是为什么转好JDK后,要设定系统级CLASSPATH环境变量的缘故。你可以打开dt.jar和tools.jar看一看,里面都有哪些包,是不是经常在各种教材上看到过?指定好他们,无论你的JDK安装到哪,这些系统类库总是能够使用的。
同理再扩展一下,有过JavaEE开发经验的兄弟,有没有想过为啥Tomcat这样的Servlet容器,JDBC这样的jar驱动文件只要放到一些固定的目录中,你在源程序中就只需要直接引用就可以了?其实这些容器就是利用了这个原理,只是做了一层二次的封装。
如果明白了这个道理,我们可以回到上文的“第二步”,为什么把编译好的MyFriend.class文件拷贝到main目录中,MyMain.java仍然编译不过。你想啊,MyMain中引用了com.idearye.friend.MyFriend类,而编译的当前目录是什么,是"com\idearye\main",编译器就会傻傻从main目录中去找"com\idearye\friend\MyFriend.class"。编译器不是人,不会像你一样先想想要找的人,跟自己是不是一个城市的一个区一个旅馆的,它只是傻了吧唧的。
这样看,如果你退回到"src",即package的根目录中,直接执行"javac com\idearye\main\MyMain.java",一样编译成功,为啥?就是因为执行javac时的当前目录是src,虽然你没有加上"-sourcepath"和"classpath",但系统环境变量中有"."这个代表当前路径的设置,因此编译器就会从当前目录中同时分别去"com\idearye\main\"和"com\idearye\friend\"中寻找"MyMain.class"和"MyFriend.java"文件,流程同上,当然就能够找到了。这其实是变相告诉编译器相对路径,注意这里是相对路径的概念。
IDE工具就是在利用这些概念,帮你解决了寻找类文件的复杂。所以我们在IDE下面,很简单的做一些事情。写了这么一坨东西,并不代表我喜欢自找麻烦,我依然绝对会继续使用IDE,不会自己写常常的一串路径,我不是偏执狂。
原来的题目本来是"值得写一写的javac与java",其实虚拟机的运行原理和编译器一样,也是这么寻找与定位,因此不再赘述。java命令会有另外几个参数。我建议自己编译和运行时,多使用"-verbose"参数,看一看编译器和虚拟机对类的定位,这是一个挺有趣的东西。回头准备写一写虚拟机类定位的东西,这块也是一个容易被遗忘和忽略的知识点。
好了,全文完,感谢你看了如此多的废话,休息一下眼睛吧。