在将应用程序打包时,你一定希望只向用户提供一个单独的文件,而不是一个包含大量类文件的目录结构,Java归档(JAR)文件就是为此目的而设计的。一个JAR文件既可以包含类文件,也可以包含诸如图像和声音等其他类型的文件。此外,JAR文件是压缩的,它使用了我们熟悉的ZIP压缩格式。

8.1 创建JAR文件

可以使用jar工具制作JAR文件(在默认的JDK安装中,这个工具位于jdk/bin目录下)。创建一个新JAR文件最常用的命令使用以下语法:
jar cvf jarFileName file1 file2 ….
例如:
jar cvf CalculatorClass.jar *.class icon.gif
通常,jar命令的格式如下:
jar options.file1 file2 ……
可以将应用程序和代码库打包在JAR文件中。例如,如果想在一个Java程序中发送邮件,就可以使用打包在文件javax.mail.jar 中的一个库。
如下是jar程序的所有选项。它们类似于UNIX tar命令的选项。

选项 说明
c 创建一个新的或者空的存档文件并加入文件。如果指定的文件名是目录,jar程序将会对它们进行递归处理
C 临时改变目录,例如:
jar cvf jarFileName.jar -C classes *.class
切换到calsses子目录以便增加类文件
e 在清单文件中创建一个入口点,例如:jar cvfe EmployeeText.jar studies.other.EmployeeText .\studies\other\*.class
f 指定JAR文件名作为第二个命令行参数。如果没有这个参数,jar命令会将结果写至标准输出(在创建JAR文件时)或者从标准输人读取(在解压或者列出JAR文件内容时)
i 建立索引文件(用于加快大型归档中的查找)
m 将一个清单文件添加到JAR文件中。清单是对归档内容和来源的一个说明。每个归档有—个默认的清单文件。但是,如果想验证归档文件的内容,可以提供自己的清单文件
M 不为条目创建清单文件
t 显示内容表
u 更新一个已有的JAR文件
v 生成详细的输出结果
x 解压文件。如果提供一个或多个文件名,只解压这些文件;否则,解压所有文件
0 存储,但不进行ZIP压缩

8.2 文件清单

除了类文件、图像和其他资源外,每个JAR文件还包含一个清单文件( manifest),用于描述归档文件的特殊特性。
清单文件被命名为MANIFEST.MF,它位于JAR文件的一个特殊的META-INF子目录中。符合标准的最小清单文件极其简单:
Manifest-Version: 1.0
复杂的清单文件可能包含更多条目。这些清单条目被分成多个节。第一节被称为主节main section)。它作用于整个JAR文件。随后的条目用来指定命名实体的属性,如单个文件、包或者URL。它们都必须以一个Name条目开始。节与节之间用空行分开。例如:

  1. Manifest-Version: 1.0
  2. lines describing this archive
  3. Name: woozle.class
  4. lines describing this file
  5. Name: com/mycompany/mypkg/
  6. lines describing this package
  7. Main-Class: com.mycompany.mypkg.MainAppClass

记住:添加文件清单MANIFEST.MF在最后一行一定要换行,不然会报错,文件清单要求必须以换行符结尾。
要想编辑清单文件,需要将希望添加到清单文件中的行放到文本文件中,然后运行:
jar cvfm jarFileName manifestFileName . . .
例如,要创建一个包含清单文件的JAR文件(manifest.mf是自己创建的文件清单,此文件在运行jar命令的根目录下),应该运行:
jar cvfm MyArchive.jar manifest.mf com/mycompany/mypkg/*.class
要想更新一个已有的JAR文件的清单,则需要将增加的部分放置到一个文本文件中,然后执行以下命令:
jar uvfm MyArchive.jar manifest-additions.mf
查看jar文件内容:
jar tf .\MyArchive.jar

8.3可执行JAR文件

可以使用jar命令中的e选项指定程序的入口点,即通常需要在调用java程序启动器时指定的类:
jar cvfe MyProgram. jar com.mycompany.mypkg.MainAppClass files to add
例如:jar cvfe EmployeeText.jar studies.other.EmployeeText .\studies\other*.class
或者,可以在清单文件中指定程序的主类,包括以下形式的语句:
Main-Class: com.mycompany.mypkg.MainAppClass
不要为主类名增加扩展名.class。
不论使用哪一种方法,用户可以简单地通过下面的命令来启动程序:
java -jar MyProgram.jar
取决于操作系统的配置,用户甚至可以通过双击JAR文件图标来启动应用程序。下面是各种操作系统的操作方式:

  • ·在 Windows平台中,Java运行时安装程序将为”. jar”扩展名创建一个文件关联,会用javaw -jar命令启动文件(与java命令不同,javaw命令不打开shell窗口)。
  • ·在 Mac OS X平台中,操作系统能够识别”.jar”扩展名文件。双击JAR文件时就会执行Java程序。

不过,人们对JAR文件中的Java程序与原生应用还是感觉不同。在 Windows平台中,可以使用第三方的包装器工具将JAR文件转换成Windows可执行文件。包装器是一个Windows程序,有大家熟悉的扩展名.exe,它可以查找和加载Java虚拟机(JVM),或者在没有找到JVM时会告诉用户应该做些什么。有许多商业的和开源的产品,例如,Launch4J(http://launch4j .sourceforge.net)和 IzPack ( http://izpack.org))。

8.4命令行选项的说明

Java开发包(JDK)的命令行选项一直以来都使用单个短横线加多字母选项名的形式,如:
java -jar . . .
javac -Xlint:unchecked -classpath . . .
但jar命令是个例外,这个命令遵循经典的tar命令选项格式,而没有短横线:
jar cvf . . .
从Java 9开始,Java工具开始转向一种更常用的选项格式,多字母选项名前面加两个短横线,另外对于常用的选项可以使用单字母快捷方式。例如,调用Linux ls命令时可以提供一个“human-readable”选项:
ls —human-readable
或者
ls -h
在Java 9中,可以使用—version而不是-version,另外可以使用—class-path而不是-classpath。在本书卷Ⅱ的第9章中可以看到,—module-path选项有一个快捷方式-p。
详细内容可以参见JEP 293增强请求( http://openjdk. java.net/jeps/293)。在所有清理工.作中,作者还提出要标准化选项参数。带—和多字母的选项的参数用空格或者一个等号(=)分隔:
javac —class-path /home/user/classdir . . .

javac —class-path=/home/user/classdir . .
单字母选项的参数可以用空格分隔,或者直接跟在选项后面:
javac -p moduledir . . .

javac -pmoduledir . . .
无参数的单字母选项可以组合在一起:
jar -cvf MyProgram.jar -e mypackage.MyProgram /.class
警告:目前不能使用这种方式。这肯定会带来混淆。假设javac有一个-c选项。那么javac-cp是指javac -c-p还是-cp?
这就会带来一些混乱,希望过段时间能够解决这个问题。尽管我们想要远离这些古老的jar选项,但最好还是等到尘埃落定为妙。不过,如果你想做到最现代化,那么可以安全地使用jar命令的长选项:
jar —create —verbose —file jarFileName file, file2 . . .
对于单字母选项,如果不组合,也是可以使用的:
jar -c -v -f jarFileName file filez . . .

8.5类的设计技巧

1.一定要保证数据私有。
这是最重要的;绝对不要破坏封装性。有时候,可能需要编写一个访问器方法或更改器方法,但是最好还是保持实例字段的私有性。很多惨痛的教训告诉我们,数据的表示形式很可能会改变,但它们的使用方式却不会经变化。当数据保持私有时,表示形式的变化不会对类的使用者产生影响,而且也更容易检测bug。
2.一定要对数据进行初始化。
Java不会为你初始化局部变量,但是会对对象的实例字段进行初始化。最好不要依赖于系统的默认值,而是应该显式地初始化所有的数据,可以提供默认值,也可以在所有构造器中设置默认值。
3.不要在类中使用过多的基本类型。
这个想法是要用其他的类替换使用多个相关的基本类型。这样会使类更易于理解,也更易于修改。例如,用一个名为Address的新类替换一个Customer类中以下的实例字段:
private String street;
private String city;
private String state;
private int zip;
这样一来,可以很容易地处理地址的变化,例如,可能需要处理国际地址。
4.不是所有的字段都需要单独的字段访问器和字段更改器。
你可能需要获得或设置员工的工资。而一旦构造了员工对象,肯定不需要更改雇用日期。另外,在对象中,常常包含一些不希望别人获得或设置的实例字段,例如,Address类中的州缩写数组。
5.分解有过多职责的类。
这样说似乎有点含糊,究竟多少算是“过多”﹖每个人的看法都不同。但是,如果明显地可以将一个复杂的类分解成两个更为简单的类,就应该将其分解(但另一方面,也不要走极端。如果设计10个类,每个类只有一个方法,显然就有些矫枉过正了)。
下面是一个反面的设计示例。
image.png
实际上,这个类实现了两个独立的概念:一副牌(包含shuffle方法和draw方法)和一张牌(包含查看面值和花色的方法)。最好引人一个表示一张牌的Card类。现在有两个类,每个类完成自己的职责:
image.png
6.类名和方法名要能够体现它们的职责。
与变量应该有一个能够反映其含义的名字一样,类也应该如此(在标准类库中,也存在着一些含义不明确的例子,如Date类实际上是一个用于描述时间的类)。
对此有一个很好的惯例:类名应当是一个名词(Order),或者是前面有形容词修饰的名词( RushOrder),或者是有动名词(有“-ing”后缀)修饰的名词(例如,BillingAddress)。对于方法来说,要遵循标准惯例:访问器方法用小写get开头( getSalary),更改器方法用小写的set开头( setSalary)。
7.优先使用不可变的类。
LocalDate类以及java.time包中的其他类是不可变的——没有方法能修改对象的状态。类似plusDays 的方法并不是更改对象,而是返回状态已修改的新对象。
更改对象的问题在于,如果多个线程试图同时更新一个对象,就会发生并发更改。其结果是不可预料的。如果类是不可变的,就可以安全地在多个线程间共享其对象。
因此,要尽可能让类是不可变的,这是一个很好的想法。对于表示值的类,如一个字符串或一个时间点,这尤其容易。计算会生成新值,而不是更新原来的值。
当然,并不是所有类都应当是不可变的。如果员工加薪时让raiseSalary方法返回一个新的Employee对象,这会很奇怪。