Java漫谈

Posted LackMemory

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java漫谈相关的知识,希望对你有一定的参考价值。

问题

    接上一篇文章中提到的问题:为什么java要求每个.java文件中最多只能有一个public类,并且文件名称还要和这个public类的名字保持一致呢?这个问题其实可以分为三个子问题:
    1. 为什么要以类名来命名.java文件?
    2. 为什么要以public类来命名.java文件?
    3. 为什么一个.java文件只能存在一个public类?
    第三个问题略显多余,因为一旦确认必须要以public类名来命名一个.java文件,那么显然这个文件只能存在一个public类,不然以哪个类来命名呢?可以肯定的是,这与java编译器的设计是有关系的。那是不是现在去扒一下javac的源代码?逻辑上讲,这是最彻底的办法。然而时间有限,这里采用取巧的办法——从javac的行为去推测可能的内部机理。

编译依赖

先看个例子:

class Demo1 
    public static void main(String[] args) 
        System.out.println(Demo2.a);
    


class Demo2 
    public static int a = 9;

    public static void main(String[] args) 
        System.out.println(a);
    

    在类Demo1中访问了类Demo2的属性a,两个类分别存储在文件test1.java、test2.java中编译:

➜  show ls
test1.java  test2.java
➜  show javac test1.java 
test1.java:3: 错误: 找不到符号
        System.out.println(Demo2.a);
                           ^
  符号:   类 Demo2
  位置: 类 Demo1
1 个错误

    编译器报错了,说找不到类Demo2,但Demo2的源码文件就在同一目录下,这说明编译器需要的是Demo2的class文件而非源码文件。猜测需要先把Demo2编译出来,Demo1的编译才能通过,验证如下:

➜  show ls
test1.java  test2.java
➜  show javac test2.java 
➜  show ls
Demo2.class  test1.java  test2.java
➜  show javac test1.java 
➜  show ls
Demo1.class  Demo2.class  test1.java  test2.java

    果然如此,现在Demo1的源码可以正常编译出来。这说明了一件事:若类A依赖于类B,即类A的代码会访问到类B的代码,那么在编译类A的时候会需要类B的class文件,从而类B必须先编译出来。

自动编译

    如果类A依赖的类有很多,那就需要人工将这些被依赖的类一个个提前编译出来——很累,而且容易出错,这种活儿显然更适合让机器去做。能不能让javac自动先去把这些依赖类先去编译了呢?可以,但是要编译依赖类,首先得找到类的源代码放在哪里。javac虽然知道依赖的类名称,然而源码文件的名称可能跟类的名称不一致,所以无法推断这个类的源码文件的名称。
    说到这里思路就已经很清晰了,为了能让javac自动编译依赖类,需要强制添加一种约束:源码文件名称要和它保存的类的名字保持一致。javac是不是按照这种思路设计的呢?把源码文件的名字改为它保存的类名,编译如下:

➜  show ls
Demo1.java  Demo2.java
➜  show javac Demo1.java 
➜  show ls
Demo1.class  Demo1.java  Demo2.class  Demo2.java

    虽然没有手动编译类Demo2,但是在编译类Demo1的时候,javac“顺便”自动把Demo2也给编译了。于是证实了我的推测:javac确实是根据类名去寻找依赖类的源码文件的。
    所以,如果一个类依赖了其他的类,为了使这个类能顺利通过编译,有两个选择:手动把所有的依赖类编译出来;或者让javac自动编译。如果选择后者,需要把依赖类保存在以它的名字命名的源码文件里。
    回到开篇的问题,第一个子问题有了答案:以类名来命名源码文件可以提高编译的效率——自动编译依赖类。要回答第二个子问题,还要牵扯权限,放在下篇。

重要参数

    前面的例子说明了javac的编译大致过程:若不存在依赖类,直接编译;否则,寻找依赖类的class文件;若没有找到class文件,根据类名去寻找源代码文件自动编译出class文件。
    那么问题来了:javac去哪里寻找class文件?又去哪里寻找源代码文件?javac自动编译出的class文件存放在哪里?上例中,所有的编译步骤都是在同一个目录下完成 的,这是一般还是特殊的情况?

-classpath参数

    在上例中,javac之所以能够找到Demo2的class文件,是因为它“碰巧”放在了当前目录下,即执行javac命令时所在的目录——javac在寻找class文件的时候,默认是从当前目录开始搜索的。然而,更一般的,javac编译的时候,并非每次依赖类都这么碰巧出现在当前目录下,这时候就需要使用-classpath参数指定所需class文件的路径。
    -classpath可以简写为-cp,它的使用有几条规则:
    1. 若没有使用这个参数,默认只搜索当前目录;否则,以指定路径为准(覆盖默认值);
    2. 可以是绝对路径也可以是javac执行时所在目录的相对路径;
    3. 可以指定多个路径,使用冒号分隔(针对Linux系统);
    4. 存在多个路径时,按照从后往前的顺序搜索,找到为止;
    5. 不可以指定具体的class文件名,只能指定到其所在的目录(目录后面的斜线可加可不加);
    6. javac不会递归搜索指定目录的子目录
    7. 可以具体到某个jar包(将一组class文件打包)名称;
    8. 可以使用通配符“*”来匹配目录下所有jar包(不可以使用“*.jar”的形式,单独一个星号即可),但是不能匹配任何具体的class文件。这意味着class文件只能使用目录;

➜  show tree
.
├── Demo1.java
└── test
    ├── Demo2.class
    └──Demo2.java

1 directory, 3 files
➜  show javac Demo1.java 
Demo1.java:3: 错误: 找不到符号
        System.out.println(Demo2.a);
                           ^
  符号:   变量 Demo2
  位置: 类 Demo1
1 个错误
➜  show javac -cp test Demo1.java
➜  show ls 
Demo1.class  Demo1.java  test

    将Demo2.class转移到子目录test后,编译test1.java报错;指定classpath后,重新编译成功。

-d参数

    上例中,test子目录里的Demo2.class是手工转移过去的。实际上,javac提供了-d参数,用来指定编译好的class文件的存放目录,前提是这个目录已经存在了,否则会报错——javac不会主动去建立指定目录。

➜  show ls
Demo1.java  Demo2.java
➜  show javac Demo2.java -d test
javac: 找不到目录: test
用法: javac <options> <source files>
-help 用于列出可能的选项
➜  show mkdir test
➜  show javac Demo2.java -d test
➜  show tree
.
├── Demo1.java
├── Demo2.java
└── test
    └── Demo2.class

1 directory, 3 files

使用-d参数可以使源码文件和class文件分开保存在不同的文件夹,方便管理,避免混乱。

-sourcepath参数

    同上,javac之所以能够找到Demo2的源代码文件,是因为它“碰巧”放在了当前目录下,而当前目录也的确是javac默认搜索源代码的目录。当源代码不在当前目录的时候,需要使用 -sourcepath参数指定源码文件的路径。需要注意的是,这个参数指定的是依赖类的源码文件路径,对于将要被编译的目标类,仍然要直接写明其路径,告诉javac到底要编译的是哪个文件。
    如下,show目录下新建src和target子目录,分别用来存放源代码和class文件。然后在show目录下直接编译Demo1.java:

➜  show tree
.
├── src
│   ├── Demo1.java
│   └── Demo2.java
└── target

2 directories, 2 files
➜  show javac -sourcepath src -d target -cp target src/Demo1.java 
➜  show tree
.
├── src
│   ├── Demo1.java
│   └── Demo2.java
└── target
    ├── Demo1.class
    └── Demo2.class

2 directories, 4 files

    有了这三个参数,javac命令就可以在任何目录下执行,不再局限于被编译文件所在的目录。


我已开通个人博客,欢迎访问。

以上是关于Java漫谈的主要内容,如果未能解决你的问题,请参考以下文章

漫谈版本控制系统

java - [推荐] 面试基础知识漫谈(参考)

JAVA 蔡羽 基础知识漫谈

Java漫谈

Java漫谈

漫谈反射