值得写一写的javac

阅读更多

很多书籍和网络教程关于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"参数,看一看编译器和虚拟机对类的定位,这是一个挺有趣的东西。回头准备写一写虚拟机类定位的东西,这块也是一个容易被遗忘和忽略的知识点。

 

好了,全文完,感谢你看了如此多的废话,休息一下眼睛吧。

你可能感兴趣的:(javac,classpath,ide,编译器,虚拟机)