Java学习记录:对象与类(二)
类路径:
类存储在文件系统的子目录中。类的路径必须与包名匹配。
另外,类文件也可以存储在JAR(java归档)文件中。在一个JAR文件中,可以包含多个压缩格式的类文件和子目录,这样既可以节省空间又可以改善性能。在程序中用到第三方的库时,你通常会得到一个或多个需要包含的JAR文件。(第11章会讲JAR文件)
【提示:JAR文件使用ZIP格式组织文件和子目录。可以使用任何ZIP工具查看JAR文件。】
为了使类能够被多个程序共享,需要做到下面几点:
1. 把类文件放到一个目录中,例如/home/user/classdir。需要注意,这个目录是包树状结构的基目录。如果希望增加com.horstmann.corejava.Employee类,那么Employee.class类文件就必须位于子目录/home/user/classdir/com/horstmann/corejava中。
2. 将JAR文件放在一个目录中,例如/home/user/archives。
3. 设置类路径(class path)。类路径是所有包含类文件的路径的集合。
在UNIX环境中,类路径中的各项之间用冒号(:)分割:
/home/user/classdir:.:/home/user/archives/archive.jar
而在Windows环境中,则以分号(;)分割:
C:\classdir;.;C:\archives\archive.jar
不论是UNIX还是Windows,都用句点(.)表示当前目录。
类路径包括:
·基目录/home/user/classdir或C:\classdir;
·当前目录(.)
·JAR文件/home/user/archives/archive.jar或者C:\archives\archive.jar.
从java6开始,可以为JAR文件目录指定一个通配符,如下:
/home/user/classdir:.:/home/user/archives/’*’
或者
C:\classdir;.;C:\archives\*
在UNIX中,*必须转义以防止shell扩展。
archives目录中的所有JAR文件(但不包括.class文件)都包含在这个类路径中。
由于总是会搜索JavaAPI的类,所以不必显式地包含在类路径中。
【警告:javac编译器总是在当前目录中查找文件,但只有当类路径中包含“.”目录时,java虚拟机才会查看当前目录。如果你没有设置类路径,那么没有什么问题,因为默认的类路径会包含”.“目录。但是如果你设置了类路径却忘记包含”.“目录,那么尽管你的程序可以没有错误地通过编译,但不能运行。】
类路径所列出地目录和归档文件是搜寻类地起始点。下面看一个类路径示例:
/home/user/classdir:.:/home/user/archives/archive.jar
假定虚拟机要搜寻com.horstmann.corejava.Employee类的类文件。它首先要查看JavaAPI类,显然,在那里找不到相应的类文件,所以转而查看类路径。它会查找以下文件:
·/home/user/classdir/com/horstmann/corejava/Employee.class
·com/horstmann/corejava/Employee.class(从当前目录开始)
·com/horstmann/corejava/Employee.class(/home/user/archives/archive.jar中)
编译器查找文件要比虚拟机复杂得多。如果引用了一个类,而没有指定这个类的包,那么编译器将首先查找包含这个类的包。它会查看所有的import指令,确定其中是否包含这个类。例如,假定源文件包含指令:
Import java.util.*;
Import com.horstmann.corejava.*;
并且源代码引用了Employee类。编译器将尝试查找java.lang.Employee(因为总是会默认导入java.lang包)、java.util.Employee、com.horstmann.corejava.Employee和当前包中的Employee。它会在类路径所有位置中搜索以上各个类。如果找到一个以上的类,就会产生编译时错误(因为完全限定类名必须是唯一的,所以import语句的次序并不重要)。
编译器的任务不止这些,它还要查看源文件是否比类文件新。如果是这样的话,那么源文件就会自动地重新编译。在前面已经知道,只可以导入其他包中的公共类。一个源文件只能包含一个公共类,并且文件名与公共类名必须匹配。因此,编译器很容易找到公共类的源文件。不过,还可以从当前包中导入非公共类。这些类有可能在与类名不同的源文件中定义。如果从当前包中导入一个类,那么编译器就要搜索当前包中的所有源文件,查看哪个源文件定义了这个类。
设置类路径
最好使用-classpath(或-cp,或者Java中的--class-path)选项指定类路径:
java -classpath /home/user/classdir:.:/home/user/archive.jar MyProg
或者
java -classpath c:\classdir;.;c:\archives\archive.jar MyProg
整个指令必须写在一行中。将这样一个很长的命令行放在一个shell脚本或一个批处理文件中是个不错的主意。
利用-classpath选项设置类路径是首选的方法,另一种方法是通过设置CLASSPATH环境变量来指定类路径。具体细节依赖于所使用的shell。在Bourne Again shell(bash)中,命令如下:
export CLASSPATH=/home/user/classdir:.:/home/user/archives/archive.jar
在Windows shell中,命令如下:
set CLASSPATH=c:\classdir;.;c:\archives\archive.jar
直到退出shell为止,类路径设置均有效。
【警告:有人建议永久地设置CLASSPATH环境变量。一般来说,这是一个糟糕地想法。人们有可能会忘记全局设置,因此,当他们的类没有正确地加载时,就会感到很奇怪。一个颇受诟病的示例是Windows中Apple QuickTime安装程序。很多年来,它都将CLASSPATH全局设置为指向它需要的一个JAR文件,而没有在类路径中包含当前目录。因此,当程序编译后却不能运行时,无数JAVA程序员不得不花费很多精力去解决这个问题。】
【警告:过去,有人建议完全绕过类路径,将所有的JAR文件都放在jre/lie/ext目录中。这种机制在Java9中已经过时,不过不管怎样这都是一个不好的建议。从扩展目录加载一些已经遗忘很久的类时,这会让人非常困惑。】
【注释:在Java9中,还可以从模块路径加载类。本书卷II的第9章将讨论模块和模块路径。】
JAR文件:
在将应用程序打包时,你希望只向用户提供一个单独的文件,而不是一个包含大量类文件的目录结构,Java归档(JAR)文件就是为此目的而设计的。JAR文件既可以包含类文件,也可以包含诸如图像和声音等其他类型的文件。此处,JAR文件是压缩的,它使用了我们熟悉的ZIP压缩格式。
创建JAR文件:
可以使用jar工具制作JAR文件(在默认的JDK安装中,这个工具位于jdk/bin目录下)。创建一个新JAR文件最常用的命令使用以下语法:
jar cvf jarFileName file1file2...
例如:
jar cvf CalculatorClasses.jar*.class icon.gif
通常,jar命令的格式如下:
jar options file1file2...
可以将应用程序和代码库打包在JAR文件中。例如,如果想在一个Java程序中发送邮件,可以使用打包在文件javax.mail.jar中的一个库。
以下是jar程序选项:
用法: jar {ctxui}[vfmn0PMe] [jar-file] [manifest-file] [entry-point] [-C dir] files ...
选项:
-c 创建新档案
-t 列出档案目录
-x 从档案中提取指定的 (或所有) 文件
-u 更新现有档案
-v 在标准输出中生成详细输出,详细输出告诉您每个文件添加到 JAR 文件时的名称。
-f 指定档案文件名
-m 包含指定清单文件中的清单信息。
-n 创建新档案后执行 Pack200 规范化
-e 为捆绑到可执行 jar 文件的独立应用程序指定应用程序入口点
-0 (数字零)选项表示仅存储而不使用 ZIP 压缩 JAR 文件
-P 保留文件名中的前导 '/' (绝对路径) 和 ".." (父目录) 组件
-M 指示不应生成默认清单文件。Jar 工具默认会自动将一个清单文件添加到 Jar 归档文件中,其路径名为 META-INF/ manifest
-i 为指定的 jar 文件生成索引信息
-C 更改为指定的目录并包含以下文件
如果任何文件为目录, 则对其进行递归处理。
清单文件名, 档案文件名和入口点名称的指定顺序
与 'm', 'f' 和 'e' 标记的指定顺序相同。
示例 1: 将两个类文件归档到一个名为 classes.jar 的档案中:
jar cvf classes.jar Foo.class Bar.class
示例 2: 使用现有的清单文件 'mymanifest' 并
将 foo/ 目录中的所有文件归档到 'classes.jar' 中:
jar cvfm classes.jar mymanifest -C foo/ .
关于jar的常见用法请访问:jar工具详解 - 简书 (jianshu.com)
清单文件:
除了类文件、图像和其他资源外,每个JAR文件还包含一个清单文件(meanifest),用于描述归档文件的特殊特性。
清单文件被命名为MANIFEST.MF,它位于JAR文件的一个特殊的META.INF子目录中。合法的最小清单文件极其简单:
Manifest-Version: 1.0
简单的清单文件可能包含更多条目。这些清单条目被分组为多个节。第一节被称为主节(main section)。它作用于整个JAR文件。随后的条目可以指定命名实体的属性,如单个文件、包或者URL。它们都必须以一个Name条目开始。节与节之间用空行分开。例如:
Manifest-Version: 1.0
lines describing this archive
Name: Woozle.class
lines describing this file
Name: com/mycompany/mypkg/
lines describing this package
要想编辑清单文件,需要将希望添加到清单文件中的行放到文本文件中,然后运行。
jar cfm jarFileName manifestFileName...
例如,要创建一个包含清单文件的JAR文件,应该运行:
java cfm MyArchive.jar manifest.mf com/mycompany/mypkg/*.class
要想更新一个已有的JAR文件的清单,则需要将增加的部分放置到一个文本文件中,然后执行以下命令:
java ufm MyArchive.jar manifest-additions.mf
【注释:请参见http://docs.oracle.com/javase/10/docs/specs/jar/jar.html获得有关JAR文件和清单文件格式的更多信息。】
可执行JAR文件:
可以使用jar命令中的e选项指定程序的入口点,即通常调用java执行程序时指定的类:
java cvfe Myprogram.jar com.mycompany.mypkg.MainAppClass files to add
或者,可以在清单文件中指定程序的主类,包括以下形式的语句:
Main-Class: com.mycompany.mypkg.MainAppClass
不要为主类名加扩展名.class。
【警告:清单文件的最后一行必须以换行符结束。否则,将无法正确地读取清单文件。常见的一个错误时创建了一个只包含Main-Class行而没有行结束符的文本文件。】
不论使用哪一种方法,用户都可以简单地通过下面的命令来启动程序:
jar -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)。
多版本JAR文件:
随着模块和包强封装的引入,之前可以访问的一些内部API不再可用。这可能要求库提供商为不同Java版本发布不同的代码。为此,java9引入了多版本JAR(multi-release JAR)。
为了保证向后兼容,特定于版本的类文件放在META-INF/versions目录中:
Application.class
BuildingBlocks.class
Util.class
META-INF
->MANIFEST.MF(with line Multi-Release: true)、
Versions
->9->Application.class
->BuildingBlocks.class
->10->BuildingBlocks.class
假设Application类使用了CssParser类,那么遗留版本的Application.class文件可以使用com.sun.javafx.css.CssParer,而Java9版本可以使用javafx.css.Cssparser。
Java8完全不知道META-INF/versions目录,它只会加载遗留的类。Java9读取这个JAR文件时,则会使用新版本。
要增加不同的版本的类文件,可以使用--release标志:
Jar uf MyProgram.jar --release 9 Application.class
要从头构建一个多版本JAR文件,可以使用-C选项,对应每个版本要切换到一个不同的类文件目录:
Jar cf Myprogram.jar -C bin/8 . --release 9 -C bin/9 Application.class
面向不同版本编译时,要使用--release标志和-d标志来指定输出目录:
Javac -d bin/8 --release 8...
在java9中,-d选项会创建这个目录(如果原先该目录不存在)。
--release标志也是Java9新增的。在较早的版本中,需要使用-source、-target和-bootclasspath标志。JDK现在为之前的两个API版本提供了符号文件。在Java9中,编译时可以将--release设置为9、8或7.
多版本JAR并不适用于不同版本的程序或库。对于不同的版本,所有类的公共API都应当是一样的。多版本JAR的唯一作用是使你的某个特定版本的程序或库能够使用多个不同的JDK版本。如果你新增了功能或者改变了一个API,就应当提供一个新版本的JAR。
【注释:javap之类的工具并没有改造为可以处理多版本JAR文件。如果调用
javap -classpath Myprogram.jar Application.class
你会得到类的基本版本(毕竟它与更新的版本应该有相同的公共API)。如果必须查看更新的版本,则可以调用:
javap -classpath Myprogram.jar\!/META-INF/versions/9/Application.class
关于命令行选项的说明:
Java开发包(JDK)的命令行选项一直以来都使用单个短横线加多字母选项名的形式,如:
java -jar...
javac -Xlint:unchecked -classpath...
但jar命令是个例外,这个命令遵循经典的tar命令选项格式,而没有短横线:
jar cvf...
从Java9开始,Java工具开始转向一种更常用的选项格式,多字母选项名前面加两个短横线,另外对于常用的选项可以使用单字母快捷方式。例如,调用Linux ls命令时可以提供一个“human-readable”选项:
ls --human-readble
或者
ls -h
在Java9中,可以使用--version而不是version,另外可以使用--class-path而不是-classpath。在本书卷II的第9章中可以看到,--module-path选项有一个快捷方式-p。
详细内容可以参见JEP293增强请求(http://openjdk.java.net/jeps/293)。在所有清洁工作中,作者还提出要标准化选项参数。带--和多字母选项的参数用空格或者一个等号(=)分隔:
Java --class-path /home/user/classdir...
或者
Javac --class-path=/home/user/classdir...
单字母选项的参数可以用空格分隔,或者直接跟在选项后面:
Javac -p moduledir...
或者
Javac -pmoduledir...
【警告:后一种方式现在不能使用,而且一般来讲这也不是一个好主意。如果模块目录恰好是arameters或rocessor,这就很容易与遗留的选项(parameters或processor)发生冲突,这又何必呢?】
无参数的单字母选项可以组合在一起:
Jar -cvf Myprogram.jar -e mypackage.MyProgram */*.class
【警告:目前不能使用这种方式,这肯定会带来混淆。假设javac有一个-c选项,那么javac -cp是指java -c -p还是-cp?】
这就会带来一些混乱,希望过段时间能够解决这个问题,尽管我们想要远离这些古老的jar选项,但最好还是等到尘埃落定为妙。不过,如果你想做到最现代化,那么可以安全地使用jar命令的长选项:
jar --create --verbose ==file jarFileName file1file2...
对于单字母选项,如果不组合,也是可以使用的:
jar -c -v -f jarFileName file1file2...
文档注释:
JDK包含一个很有用的工具,叫做javadoc,它可以由源文件生成一个HTML文档。
如果在源代码中添加以特殊定界符/**开始的注释,那么你也可以很容易生成一个看上去具有专业水准的文档。这是一种很好的方法,因为这样可以将代码与注释放在一个地方。应该知道,如果将文档存放在一个单独的文件中,随着时间的推移,代码和注释很可能出现不一致。不过,如果文档注释与源代码在同一个文件中,就可以很容易地同时修改源代码和注释,然后重新运行javadoc。
注释的插入:
Javadoc实用工具从下面几项中抽取信息:
·模块;
·包;
·公共类与接口;
·公共的和受保护的字段;
·公共的和受保护的构造器及方法。
第5章中将介绍受保护特性,第6章中将介绍接口,模块在卷II的第9章介绍。
可以(而且应该)为以上各个特性编写注释。各个注释放置在所描述特性的前面。注释以/**开始,并以*/结束。
每个/**。。。*/文档注释包含标记以及之后紧跟着的自由格式文本(free-form text)。标记以@开始,如@since或@param。
自由格式文本的第一个句子应该是一个概要陈述。Javadoc工具自动地将这些句子抽取出来生成概要页。
在自由格式文本中,可以使用HTML修饰符,例如,用于强调的<em>.....</em>、用于着重强调的<strong>.....</strong>、用于项目符号列表的<ul>/<li>以及用于包含图像的<img..../>等。要键入等宽代码,需要使用{@code.....}而不是<code>......</code>——这样一来,就不用操心对代码的<字符转义了。
【注释:如果文档中有到其他文件的链接,如图像文件(例如,图表或用户界面组件的图像),就应该将这些文件放到包含源文件的目录下的一个子目录doc-files中国。Javadoc工具将从源目录将doc-files目录及其内容复制到文档目录中。在链接中需要使用doc-files目录,例如<imgsrc=”doc-files/uml.png” alt= “UML dagram”/>
类注释:
类注释必须放在import语句之后,class定义之前。
下面是一个类注释的例子:
/**
* A{@code Card}object represents a playing card, such
* as "Queen of Hearts". A card has a suit (Diamond, Heard,
* Spade or Club) and a value (1 = Ace, 2. . . 10, 11 = Jack,
* 12 = Queen, 13 = King)
*/
public class Card {
. . .
}
【注释:没有必要在每一行的开始都添加*,例如,以下注释同样是合法的:
**
A{@code Card}object represents a playing card, such
as "Queen of Hearts". A card has a suit (Diamond, Heard,
Spade or Club) and a value (1 = Ace, 2. . . 10, 11 = Jack,
12 = Queen, 13 = King)
*/
不过,大部分IDE会自动提供星号,而且换行改变时,还会重新放置星号。】
方法注释:
每个方法注释必须放在所描述的方法之前。除了通用标记之外,还可以使用下面的标记:
·@param variable descripition
这个标记将给当前方法的“parameters”(参数)部分添加一个条目。这个描述可以占据多行,并且可以使用HTML标记。一个方法的所有@param标记必须放在一起。
·@return description
这个标记将给当前方法添加“returns”(返回)部分。这个描述可以跨多行,并且可以使用HTML标记。
·@throws class description
这个标记将添加一个注释,表示这个方法有可能抛出异常。有关异常的详细内容将在第7章种讨论。
下面是一个方法注释的示例:
/**
* Raises the salary of an employee.
* @param byPercent the percentage by which to raise the salary (e.g., 10 means 10%)
* @return the amount of the raise
*/
public double raiseSalary(double byPercent){
double raise = this.salary * byPercent / 100;
this.salary += raise;
return raise;
}
字段注释:
只需要对公共字段(通常指的是静态常量)增加文档注释。例如,
/**
* The "Hearts" card suit
*/
public static final int HEARTS = 1;
通用注释:
标记@since text会建立一个“since”(始于)条目。Text(文本)可以是对引入这个特性的版本描述。例如,@since 1.7.1
类文档注释中可以使用下面的标记:
·@author name
这个标记将建立一个“author”(作者)条目。可以有多个@author标记,每个@author标记对应一个作者。并不是非得使用这个标记,你的版本控制系统能够更好地跟踪作者。
·@version text
这个标记将建立一个“version”(版本)条目。这里的text可以是对当前版本的任何描述。
通过@see和@link标记,可以使用超链接,链接到javadoc文档的相关部分或外部文档。
标记@see reference将在“see also”(参见)部分增加一个超链接。它可以用于类中,也可以用于方法中。这里的reference(引用)可以有以下选择:
1.package.class#feature label
2.<a href=". . .">label</a>
3."test"
第一种情况是最有用的。只要提供类、方法或变量的名字,javadoc就在文档中插入一个超链接。例如,
@see com.horstmann.corejava.Employee#raiseSalary(double)
会建立一个超链接,链接到com.horstmann.corejava.Employee类的raiseSalary(double)方法。可以省略包名,甚至把包名和类名都省去,这样一来,这会位于当前包或当前类。
需要注意,一定要使用井号(#),而不要使用句号(.)分隔类名与方法名(或类名与变量名)。Java编译器自身可以熟练地确定句点在分割包、子包、类、内部类以及方法和变量时的不同含义。但是javadoc工具就没有这么聪明了,因此必须对它提供帮助。
如果@see标记后面有一个<字符,就需要指定一个超链接。可以超链接到任何URL。例如:
@see <a href="www.horstmann.com/corejava.html">The Core Java home page</a>
在上述各种情况下,都可以指定一个可选的标签(label),这回显示为链接锚(link anchor)。如果省略了标签,则用户看到的锚就是目标代码名或URL。
如果@see标记后面有一个双引号(“)字符,文本就会显示在”see also”部分。例如,
@see "Core Java 2 volume 2"
可以为一个特性添加多个@see标记,但必须将它们放在一起。
如果愿意,可以在任何文档注释中放置指向其他类或方法的超链接。可以在注释中的任何位置插入一个形式如下的特殊标记:
{@link package.class#feature label}
这里的特性描述规则与@see标记的规则相同。
最后,在Java9中,还可以使用{@index entry}标记为搜索框增加一个条目。
包注释:
可以直接将类、方法和变量的注释放置在Java源文件中,只要用/**......*/文档注释界定就可以了。但是,要想产生包注释,就需要在每一个包目录中添加一个单独的文件。可以有如下两个选择:
1. 提供一个名为package-info.java的Java文件。这个文件必须包含一个初始的Javadoc注释,以/**和*/界定,后面是一个package语句。它不能包含更多的代码或注释。
2. 提供一个名为package.html的HTML文件,抽取标记<body>. . .</body>之间的所有文本。
注释提取:
在这里,假设你希望HTML文件将放在名为docDirectory的目录下。执行以下步骤:
1. 切换到源文件目录,其中包含想要生成文档的源文件。如果有嵌套的包要生成文档,例如com.horstmann.corejava,就必须切换到包含子目录com的目录(如果提供overview.html文件的话,这就是这个文件所在的目录)。
2. 如果是一个包,应该运行命令:
javadoc -d docDirectory nameOfPakage
或者,如果要为多个包生成文档,运行:
javadoc -d docDirectory nameOfPakage1 nameOfpackage2. . .
如果你的文件在无名包中,则应该运行:
javadoc -d docDirectory *.java
如果省略了-d docDirectory选项,HTML文件就会提取到到当前目录下。这样可能很混乱,因此我不提倡这种做法。
可以使用很多命令行选项对javadoc程序进行微调。例如,可以使用-auther和-version选项在文档包含@author和@version标记(默认情况下,这些标记会被省略)。另一个很有用的选项是-link,用来为标准类添加超链接。例如,如果使用命令:
javadoc -link http://docs.oracle.com/javase/9/docs/api *.java
那么,所有的标准类库的类都会自动地链接到Oracle网站的文档。
如果使用-linksource选项,那么每个源文件就会转换为HTML(不对代码着色,但包含行号),并且每个类和方法名将变为指向源代码的超链接。
还可以为所有源文件提供一个概要注释。把它放在一个类似overview.html的文件中,运行javadoc工具,并提供命令行选项-overview filename。将抽取标价<body>......</body>之间的所有文本。当用户从导航栏中选择“Overview”时,就会显示这些内容。
有关其他的选项,请查阅javadoc工具的联机文档http://docs.oracle.com/javase/9/javadoc/javadoc.html。
类设计技巧:
我们不会面面俱到,也不希望过于沉闷,所以在这一章结束之前再简单地介绍几点技巧。应用这些技巧可以使你设计的类更能得到专业OOP圈子的认可。
1.一定要保证数据私有。
这是最重要的;绝对不要破坏封装性。有时候,可能需要编写一个访问器方法或更改器方法,但最好还是保持实例字段的私有性。很多惨痛的教训告诉我们,数据的表示形式很可能会改变,但它们的使用方式却不会经常变化。当数据保持私有时,表示形式的变化不会对类的使用者产生影响,而且也更容易检测bug。
2.一定要初始化数据。
Java不会为你初始化局部变量,但是会对对象的实例字段进行初始化。最好不要依赖于系统的默认值,而是应该显式地初始化所有变量,可以提供默认值,也可以在所有构造器中设置默认值。
3.不要在类中使用过多的基础类型。
其想法是要用其他类,而不是使用多个相关的基本类型。这样会使类更易于理解,也更易于修改。例如,可以用一个名为Address的新类替换一个Customer类中的以下实例字段:
private String street;
private String city;
private String state;
private int zip;
这样一来,可以很容易地处理地址的变化,例如,可能需要处理国际地址。
4.不是所有的字段都需要单独的字段访问器和更改器。
你可能需要获得或设置员工的工资。而一旦构造了员工对象,肯定不需要更改雇佣日期。另外,在对象中,常常包含一些不希望别人获得或设置的实例字段,例如,Address类中的州缩写数组。
5.分解有过多职责的类。
这样说似乎有点含糊,究竟多少算是“过多”?每个人的看法都不同。但是,如果明显地可以将一个复杂的类分解成两个概念上更为简单的类,就应该进行分解。(但另一方面,也不要走极端。如果设计10个类,每个类只有一个方法,显然就有些矫枉过正了。)
下面是一个反面的设计示例。
public class CardDeck // bad design
{
private int[] value;
private int[] suit;
public CardDeck(){....}
public void shuffle{.....}
public int getTopValue(){.....}
public int getTopSuit(){.....}
public void draw(){......}
}
实际上,这个类实现了两个独立的概念:一副牌(包含shuffle方法和draw方法)和一张牌(包含查看面值和花色的方法)。最好引入一个表示一张牌的Card类。现在有两个类,每个类分别完成自己的职责:
public class CardDeck // bad design
{
private Card[] cards;
public CardDeck(){....}
public void shuffle{.....}
public int getTopValue(){.....}
public int getTopSuit(){.....}
public void draw(){......}
}
public class Card
{
private int value;
private int suit;
public Card(int aValue, int aSuit){.......}
public int getValue(){.......}
public int getSuit(){......}
}
6.类名和方法名要能够体现它们的职责。
变量应该有一个能够反映其含义的名字,类似地,类也应该如此(在标准类库中,确实存在着一些含义不明确的例子,如Date类实际上是一个描述时间的类)。
对此有一个很好的惯例:类名应当是一个名词(Order),或者是前面有形容词修饰的名词(RushOrder),或者是有动名词(有“-ing”后缀)修饰的名词(例如,BillingAddress)。对于方法来说,要遵循标准惯例:访问器方法用小写get开头(getSalary),更改器方法用小写的set开头(setSalary)。
7. 优先使用不可变的类。
LocalDate类以及java.time包中的其他类是不可变的——没有方法能够修改对象的状态。类似plusDays的方法并不会更改对象,而是会返回状态已修改的新对象。
更改对象的问题在于,如果多个线程试图同时更新一个对象,就会发生并发更改,其结果是不可预料的。如果类是不可变的,就可以安全地在多个线程间共享其对象。
因此,要尽可能让类是不可能变的,这是一个很好的想法。对于表示值的类,如一个字符串或一个时间点,这尤其容易。计算会生成新值,而不是更新原来的值。
当然,并不是所有类都应当是不可变的。如果员工加薪时让raiseSalary方法返回一个新的Employee对象,这会很奇怪。
本章介绍了有关对象和类的基础知识,这使得Java可以作为一种“基于对象”的语言。要真正做到面向对象,程序设计语言还必须支持继承和多态。Java提供了对这些特性的支持,具体内容将在下一章中介绍。
【学习参考书籍:《Java核心技术卷I》】