Core-Java第四章
对象与类
一、面向对象程序设计概述
面向对象程序设计(object-oriented programming,OOP)是当今主流的程序设计范型,取代了结构化或过程式编程技术。
面向对象的程序是由对象组成的,每个对象包含对用户公开的特定功能部分和隐藏的实现部分。
程序中的很多对象来自标准库,还有一些是自定义的。
在以往的程序开发中,通过设计一些列的过程来求解问题,一旦确定了这些过程,就需要考虑数据存储的方式,这也是Pascal
语言开发者说过的算法+数据结构=程序。
但是在面向对象语言中,需要先考虑数据,然后再考虑对数据的算法。

1.1 类
类(class)是构造对象的模板或蓝图。由类构造(construct)对象的过程称为创建类的实例(instance)
标准Java库提供了几千个类,可以用于各种目的,如用户界面设计、日期、日历和网络编程。
封装(encapsulation,有时称为数据隐藏)是处理对象的一个重要概念。
从形式上看,封装就是将数据和行为组合在一个包中,并对对象的使用者隐藏具体的实现方式。
对象中的数据称为实例字段(instance field),操作数据的过程称为方法(method)。
作为一个类的实例,特定对象都有一组特定的实例字段值。这些值的集合称为对象的当前状态。
实现封装的关键在于,绝对不能让类中的方法直接访问其他类的实例字段。
程序只能通过对象的方法与对象的数据进行交互。
1.2 对象
对象的三个重要特性:
- 对象行为(behavior)--可以对对象完成哪些操作,或者可以对对象应用哪些方法
- 对象状态(state)--当调用那些方法时,对象会如何相应
- 对象标识(identity)--如何区分具有相同行为和状态的不同对象
1.3 识别类
识别类的一个简单经验就是在分析问题的过程中寻找名词,而方法对应着动词
1.4 类之间的关系
在类之间,最常见的关系有:
- 依赖(“uses-a”)
- 聚合(“has-a”)
- 继承(“is-a”)
依赖(dependence),是一种最明显也是最常见的关系,例如,Order类使用Account类是因为Order对象需要访问Account对象查看信用状态。如果一个类的方法使用或操纵另一个类的对象,我们就说一个类依赖于另一个类。应该尽可能减少类之间的依赖,用软件工程的思想即减少类之间的耦合。
聚合(aggregation),即“has-a”关系,如一个Order对象中有一些Item对象。包容关系意味着类A的对象包含类B的对象。
继承(inheritance),即“is-a”关系,表示一个更特殊的类和一个更一般的类之间的关系。例如,RushOrder类由Order类继承而来。在更特殊的RushOrder类中包含了一些用于优先处理的特殊方法,还提供了一个计算运费不同的方法,而其他方法都是从Order类中继承而来。一般来说,如果类A扩展类B,类A不但包含从类B继承而来的方法,还会有一些额外的功能。
二、使用预定义类
在Java中,并不是所有的类都表现出面向对象的典型特性,如Math类等,只需要知道方法名和参数即可。
2.1 对象与对象变量
在Java程序设计语言中,要使用构造器(constructor,或称构造函数)构造新实例。构造器是一种特殊的方法,用来构造并初始化对象。
构造器的名字与类名相同,要想构造一个对象,需要在构造器前面加上new操作符
new Date()
在Java中,任何对象变量的值都是对存储在另外一个地方的某个对象的引用。new操作符的返回值也是一个引用。
1 | Date deadline = new Date(); |
表达式new Date()构造了一个Date类型对象,它的值是对新创建对象的一个引用。这个引用存储在变量deadline中。
Java中的对象变量不等于C++中的引用,相当于对象指针,如
Date birthday
在C++中类似于Date* birthday
如果把变量复制到另一个变量,两个变量就指向同一个日期,即它们是同一个对象的指针。
所有的Java对象都存储在内存的堆区,当一个对象包含另一个对象变量时,它只是包含着另一个堆对象的指针。
2.2 Java类库中的LocalDate类
Date类的实例有一个状态,即特定的时间点。
Date类用于处理人类记录日期的日历不是很有用,所以类设计者决定把时间点命名和保存时间分开。
在Java中有用于表示时间点的Date类,有日历表示法表示日期的LocalDate类。
不要使用构造器来构造LocalDate对象,实际上,应该使用静态工厂方法(factory method),它会代表你调用构造器。
LocalDate.now();
会构造一个新对象,表示构造这个对象时的时间。
也可以提供年月日来构造
LocalDate.of(2021,7,10);
LocalDate类封装了实例字段来维护所设置的日期。如果不查看源代码,就不可能知道类内部的日期表示。
访问对象会修改对象的方法称为更改器方法(mutator method)
只访问对象而不修改对象的方法有时称为访问器方法(accessor method)。
- static LocalDate now()
构造一个表示当前日期的对象
- static LocalDate of(int year,int month,int dat)
构造一个表示给定日期的对象
int getYear()
int getMonthValue()
int getDayOfMonth()
得到当前日期的年月日
- DayOfWeek getDayOfWeek
得到当前日期是星期几,作为DayOfWeek类的一个实例返回。调用getValue来得到1~7之间的一个数,表示这是星期几,1表示星期一,7表示星期日。
LocalDate plusDays(int n)
LocalDate minusDays(int n)
生产当前日期之前或之后n天的日期
三、 用户自定义类
3.1 Employee类
3.2 多个源文件使用
3.3 剖析Employee类
3.4 构造器
关键字private确保只有Employee类自身的方法可以访问这些实例字段,而其他类无法访问到。
- 构造器与类同名
- 每个类可以有一个以上的构造器
- 构造器可以有0个、1个或多个参数
- 构造器没有返回值
- 构造器总是伴随着new操作符一起调用
3.5 用var声明局部变量
在Java10中,如果可以从变量的初始值推导出他们的类型,那么可以用var
关键字声明局部变量,而无须指定类型。
这样可以避免重复写类型名,var关键字只能用于方法中的局部变量。
3.6 使用null引用
3.7 隐式参数与显式参数
隐式参数是出现在方法名前的Employee类型的对象,又称为方法调用者,方法调用目标,接收者
显式参数是方法名括号中的变量,显式地列在方法声明中
3.8 封装的优点
确保了私有字段不受到外界的修改。
注意不要编写返回可变对象引用的访问器方法。如果需要返回一个可变对象的引用,首先应该对它进行克隆(clone)。对象克隆是指存放在另一个新位置上的对象副本。
3.9 基于类的访问权限
方法可以访问所属类任何对象的私有特性,而不限于隐式参数this。在每一个方法中,关键字this指示隐式参数。
3.10 私有方法
如果一个复杂的方法需要通过几个辅助方法实现,而又不希望这些辅助方法称为公共接口的一部分,最好将这样的方法设置为私有方法。
3.11 final实例字段
可以将实例字段定义为final,这样的字段必须在构造对象时初始化,确保每一个构造器执行完成之后,final字段的值已设置,并且以后也不能再修改这个字段。
四、静态字段与静态方法
4.1 静态字段
如果将一个字段定义为static,每个类只能由一个这样的字段。
4.2 静态常量
维护了一个属于类的常量,避免每个对象需要创建一个常量实例。
4.3 静态变量
静态方法是不在对象上执行的方法,即没有隐式参数。静态方法不能访问实例字段,因为静态方法无法在对象上执行操作。但是静态方法可以访问静态字段。
当方法不需要访问对象状态,需要的所有参数都由显示参数提供且方法只需要访问类的静态字段,可使用静态方法。
4.4 工厂方法
静态方法还可以用于静态工厂方法来构建对象,这里的好处在于可以重命名构造器,如果使用构造器,则名称必须与类名相同,如果希望有不同名的构造器时,无法解决;使用构造器也无法改变构造对象的类型,而工厂方法实际上将返回其他类型的对象。
4.5 main方法
main方法不对任何对象进行操作。事实上,在启动程序时还没有任何对象,静态的main方法将执行并构造程序所需要的对象。
- static
void requireNonNull(T obj); - static
void requireNonNull(T obj, String message); - static
void requireNonNull(T obj, Supplier messageSupplier);
如果obj为null, 这些方法会抛出一个NullPointerException异常而没有消息或者给定的消息。
- static
T requireNonNullElse(T obj, T defaultObj); - static
T requireNonNullElseGet(T obj, Supplier defaultSupplier);
如果obj部位null则返回obj, 或者如果obj为null则返回默认对象
五、方法参数
按值调用(call by value)表示方法接收的是调用者提供的值。
按引用调用(call by reference)表示方法接收的是调用者提供的变量地址。
在Java中总是按值调用,方法得到的是所有参数值的一个副本,方法不能修改传递给它任何参数变量的内容。
方法不能修改基本数据类型的参数(即数值型或布尔型)
方法可以修改对象参数的状态
方法不能让一个对象参数引用一个新的对象
六、对象构造
6.1 重载
多个方法有相同的名字,不同的参数,称为重载(overload)。编译器查找匹配的过程称为重载解析(overloading resolution)。Java允许重载任何方法,要完整的描述一个方法,需要指定方法名以及参数类型。这叫做方法的签名(signature)。
返回值不是方法签名的一部分,不能有两个方法名,参数列表相同却返回值不同的方法!
6.2 默认字段初始化
如果在构造器中没有显示地为字段设置初始值,那么就会自动地赋为默认值
数值型为0;
布尔值为false;
对象引用为null;
6.3 无参数的构造器
如果没有编写构造器,编译器会为我们提供这样一个无参构造器。这个构造器将会把所有实例字段设置为默认值。
6.4 显式字段初始化
通过重载类的构造器方法,可以采用多种形式设置类的实例字段的初始状态。初始值不一定为常量值,也可以利用方法调用初始化一个字段。
6.5 参数名
参数变量会遮蔽同名的实例字段,所以可以在实例字段前加this关键字表示隐式参数调用,也就是所需要构造的对象。
6.6 调用另一个构造器
this不仅可以表示隐式参数,还可以用于其他构造器的第一个语句,表示这个构造器将会调用同一个类的另一个构造器。
6.7 初始化块
初始化数据字段有两种方法:
- 在构造器中设置值;
- 在声明中赋值;
实际上,还可以通过初始化块(initialization block)。在一个类的声明中,可以有多个代码块,只要构造这个类的对象,这些块就会执行。
静态的初始化块会在类第一次加载的时候,将会进行静态字段的初始化
6.8 对象析构与finalize方法
在C++中,有显式的析构器方法,其中放置一些当对象不再使用时需要执行的清理代码。在析构器中,最常见的操作时回收分配给对象的存储空间。由于Java会自动地完成垃圾回收,所以不需要人工回收内存。
不要使用finalize方法,这个方法原本要在垃圾回收器清理对象之前调用。
七、包
Java允许使用包(package)来将类组织在一个集合中。借助包可以方便地组织自己的代码,并将自己的代码与别人提供的代码库分开管理。
7.1 包名
使用包的主要原因时要确保类名的唯一性。假如两个程序员都建立了Employee类,只要把类放置在不同的包中,就不会产生冲突。事实上,为了保证包名的绝对唯一性,要用一个因特网域名以逆序的形式的作为包名,然后对不同的工程使用不同的子包。
完全限定名 : 如com.horstmann.corejava.Employee称为类的全限定名
7.2 类的导入
一个类可以使用所属包中的所有类,以及其他包中的公共类。
可以使用完全限定名(fully qualified name);就是包名后跟着类名。
java.time.LocalDate today = java.time.LocalDate.now();
更简单的方式是使用import语句。import是一种引用包中各个类的简捷方式。
7.3 静态导入
有一种import语句允许导入静态方法和静态字段,而不只是类。
如在源文件顶部添加import static java.lang.System.*;
就可以使用System类的静态方法和静态字段,而不必加前缀名。
7.4 在包中增加类
要想把类放入包中,就必须将包的名字放在源文件的开头,即放在定义这个包中各个类的代码之前。
如果没有在源文件中放置package语句,那么这个类就属于无名包(unnamed package)。
7.5 包访问
标记为public的部分可以由任意类使用;
标记为private的部分只能由定义他们的类使用;
7.6 类路径
类存储在文件系统的子目录中。类路径必须和包名匹配。
类也可以存储在Jar文件中,jar以zip形式组织文件和子目录,可以使用任何zip工具查看jar文件
7.7 设置类路径
可以使用-classpath选择指定类路径
八、JAR文件
一个jar文件既可以包含类文件,也可以包含诸如图像和声音等其他类型的文件。jar文件是压缩的,它使用了我们常见的zip压缩格式。
8.1 创建JAR文件
可以使用jar工具来制作JAR文件(在默认的JDK安装中,这个工具位于jdk/bin目录下)。创建一个新的JAR文件最常见的命令使用如下语法:
1 | jar cvf jarFileName file1 file2 ... |
jar程序选项
选项 | 说明 |
---|---|
c | 创建一个新的或者空的存档文件并加入文件。如果指定的文件名是目录,jar程序将会对它们进行递归处理 |
C | 临时改变目录,例如:jar cvf jarFile.jar -C classes *.class,切换到classes目录以便增加类文件 |
e | 在清单文件中创建一个入口点 |
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。
要想编辑清单文件,需要将希望添加到清单文件的行放到文本文件中,然后运行:
1 | jar cfm jarFIleName manifestFileName... |
要想更行一个已有的jar文件清单,则需要将添加的部分放置到一个文本文件中,然后执行如下命令
1 | jar ufm jarFileName manifest-additions.mf |
8.3 可执行jar文件
可以使用jar命令的e选择指定程序的入口点,即通常需要在调用Java程序启动器时指定的类。
1 | java cvfe jarFileName classpath files to add |
或者,可以在清单文件中指定程序的主类
1 | Main-Class: classpath.mainClassName |
在Mac OSX平台,jar文件可以直接双击打开
在Windows平台可以通过
1 | java -jar jarFileName.jar |
来运行jar包,也可以使用第三方工具如Launch4J和LzPack来将jar文件转换为.exe文件
8.4 多版本jar文件
面对不同版本编译时,要使用--release标志和-d标志来指定输出目录:
1 | java -d bin/8 --release 8 ... |
8.5 命令行选项的说明
JDK命令行一直都以单个短横线加多字母选项名的形式,但是jar命令是个例外,同时从Java9开始,Java工具开始转向一种更为常用的选项格式,多字母选项名前加两个短横线,另外对于常见的选项可以使用单字母快捷方式。
九、文档注释
JDK包含有一个很有用的工具,叫做Javadoc,它可以由源文件生产一个HTML文档。
如果在源代码中添加以特殊定界符/**开始注释,可以快速生产一个看上去具有专业水平的文档。
9.1 注释的插入
javadoc实用工具从下面几项中抽取信息:
模块;
包;
公共类与接口;
公共的和受保护的字段;
公共的和受保护的构造器和方法;
每个/ * * ...*/文档注释包括标记以及之后紧跟着的自由格式文本(free-form text)。标记以@开始,如@since,@param。
9.2 类注释
类注释必须放在import语句之后,类定义之前。
9.3 方法注释
方法注释必须放在方法之前。
- @param variable description
这个标记将给当前方法的“parameters”(参数)部分添加一个条目。这个描述可以占据多行,并且可以使用HTML标记。一个方法的所有@param标记必须放在一起。
- @return description
这个标记将给当前方法添加“return”(返回)部分。这个描述可以跨多行,并且可以使用HTML标记。
- @throws class description
这个标记将添加一个注释,表示这个方法有可能抛出异常。
9.4 字段注释
只需要对公共字段(通常指静态常量)建立文档。
9.5 通用注释
@since text会建立一个“since”(始于)条目。text(文本)可以是引入这个特性的版本的任何描述。
@author name表示产生一个作者条目,可以使用多个,每个对应一个作者;
@version text 这个标记将会产生一个“version”(版本)条目。这里的文本可以是对当前版本的任何描述。
@see 和 @link 可以使用超链接,链接到Javadoc文档的相关部分或外部文档。
@see @reference将在“see also"参见部分添加一个超链接。它可以用于类中,也可以用于方法中。
9.6 包注释
可以直接将类、方法和常量的注释放置在Java源文件中,要想产生包注释,需要在每一个包中添加一个单独的文件。
提供一个名为package-info.java的Java文件。这个文件必须包括一个初始的以/** */界面的javadoc注释,后面是一个package语句,不能包含的代码或注释
提供一个名为package.html的HTML文件。会抽取标记之间的所有文本。
9.7 注释抽取
如果是一个包
1 | javadoc -d docDiretory nameOfPackage |
如果是多个包
1 | javadoc -d docDiretory nameOfPackage1 nameOfPackage2 |
如果文件在无名的包中
1 | javadoc -d docDiretory *.java |
十、类设计技巧
- 一定要保证数据私有
- 一定要对数据进行初始化
- 不要在类中使用过多的基本数据类型
- 不是所有的字段都需要单独的字段访问器和字段更改器
- 分解有过多职责的类
- 类名和方法名要能够体现它们的职责
- 优先使用不可变的类