date:2021/6/6
version: 1.0
ch4 对象与类
4.1 面向对象程序设计概述
- 面向对象程序设计(简称OOP)是当今主流的程序设计范型
- 面向对象的程序是由对象组成的, 每个对象包含对用户公开的特定功能部分和隐藏的实现部分
- 面向对象更加适用于解决规模较大的问题
类
- 类(class) 是构造对象的模板或蓝图
- 由类构造(construct) 对象的过程称为创建类的实例(instance)
- 封装
- 封装:将数据和行为组合在一个包中, 并对对象的使用者隐藏了数据的实现方式
- 对象中的数据称为实例字段(instance field)
- 操纵数据的过程称为方法(method)
- 每个对象都有一组特定的实例字段,这些值的集合就是这个对象的当前状态(state)
- 封装的要义:
- 实现封装的关键在于绝对不能让类中的方法直接地访问其他类的实例域。程序仅通过对象的方法与对象数据进行交互。
- 即,需要将类中的实例字段全设置为 private,然后根据具体要求决定是否提供对应的 public 方法(set、get)
为什么要封装?
封装给对象赋予了“黑盒” 特征, 这是提高重用性和可靠性的关键。这意味着一个类可以全面地改变存储数据的方式,只要仍旧使用同样的方法操作数据, 其他对象就不会知道或介意所发生的变化。
对象
- 对象的三个主要特性:
- 对象的行为(behavior)
- 对象的状态(state)
- 对象的状态:对象中当前的实例字段状态信息
- 对象状态的改变必须通过调用方法实现;否则,便破坏了封装性
- 对象标识(identity)
- 每个对象都有一个唯一的标识(identity)
识别类
- OOP首先从设计类开始,然后再往每个类中添加方法
- 识别类的简单规则是在分析问题的过程中寻找名词,而方法对应着动词
所谓“ 找名词与动词” 原则只是一种经验,具体是开发情况而定
类之间的关系
- 类之间的关系
- 依赖(“uses-a”)
- 一个类的方法操纵另一个类的对象
- 应该尽可能地将相互依赖****的类减至最少,即让类之间的耦合度最小
- 聚合(“has-a”)
- 类 A 的对象包含类B 的对象
- 继承(“is-a”)
- 用于表示特殊与一般关系
- 依赖(“uses-a”)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Aur7XF3D-1622980463733)(./img/ch 4/ch4 表4-1 UML符号.png)]
使用预定义类
对象与对象变量
-
要想使用对象,就必须首先构造对象, 并指定其初始状态。然后,才能对对象应用方法。
-
Java语言中,使用构造器( constructor ) 构造新实例
- 构造器的名字应该与类名相同
- 构造对象:在构造器前面加上new 操作符。如,new Date()
-
对象变量
- 对象变量可以引用指定类型的对象。但是,对象变量不是一个对象。
- 如果对象变量没有引用指定类型的对象,则不能将该类的方法应用于这个变量上
Date deadline; // deadline doesn’t refer to any object
这里定义了一个对象变量deadline,但是实际上它没有引用Date类型的对象。
此时,不能将任何 Date 方法应用于这个变量上
-
解释 Date deadline = new Date();
- 这条语句分两个部分:
- new Date() 构造了一个Date 类型的对象, 并且它的值是对新创建对象的引用;
- 这个引用存储在变量deadline 中。
- 这条语句分两个部分:
Java 类库中的LocalDate 类
没细看…
4.3 用户自定义类
-
文件名必须与public 类的名字相匹配
-
在一个源文件中, 只能有一个公有类,但可以有任意数目的非公有类
-
将实例字段标记为 private
- 关键字 private 确保只有类自身的方法能够访问这些实例字段, 而其他类的方法不能够读写这些实例字段
-
所有的 Java 对象都是在堆中构造的, 构造器总是伴随着 new 操作符一起使用
用 var 声明局部变量
-
在 Java10 中。如果可以从变量的初始值推导它们的类型,可以用 var 关键字声明局部变量
注意: var 关键字只能用于方法中的局部变量。参数和字段的类型必须声明。
使用 null 引用
-
一个对象变量包含一个对象的引用,或者包含一个特殊值 null,后者表示没有引用任何对象
-
如果使用 null 调用一个方法,则会产生一个 NullPointerException 异常
-
解决NullPointerException 异常的两种方法:
-
“宽容型”
-
把 null 参数转化成一个适当的非 null 值
-
Java9 提供了 requireNonNullElse(obj, default)
-
如果 obj 不为 null 则返回 obj,或者如果 obj 为 null 则返回默认对象
name = requireNonNullElse(obj, default)
相当于
if (obj == null) name= default; else name = obj;
-
-
-
“严格型”
- ”严格型“干脆拒绝 null 参数
- requireNonNull(obj, String message)
- 这种方法异常报告会提供这个问题描述,并准确地之处问题所在的位置
-
隐式参数与显式参数
-
方法用于操作对象以及存取它们的实例
-
Java 类中的方法包含两类参数:
-
隐式参数
- 隐式参数:出现在方法名前面的对象
-
显式参数
- 显式参数:方法名后面括号中的参数
public void raiseSalary(double byPercent) { double raise = salary * byPercent / 100; salary += raise; } number007.raiseSalary(5); // 这里number007就是隐式参数,raiseSalary括号中的5就是显式参数
Java中,关键字 this 指示隐式参数
-
封装的优点
-
根据实际情况设置获得或者设置实例字段的值
-
一私有的数据域;
-
一公有的域访问器方法;
-
一个公有的域更改器方法。
后两者set、get根据实际情况提供
-
-
封装的优点:
-
可以改变内部实现,除了该类的方法之外, 不会影响其他代码
一开始将存储名字的字段是设置为 String name;
对应的 getName 方法返回 return name;
现在如果将存储的名字字段改为
String firstName;
String lastName;
那么getName 方法可以改为返回 return firstName + " " + lastName程序的其他部分不用改变
-
更改器方法可以执行错误检查, 然而直接对域进行赋值将不会进行这些处理
比如,setSalary 方法可以检查薪金是否小于0
-
注意:不要编写返回引用可变对象的访问器方法
- 如果需要返回一个可变对象的引用, 应该首先对它进行克隆( clone )
- 若直接返回可变对象的引用(不可隆的话),可能会破坏封装性
class Employee { private Date hireDay; // getHireDay 方法返回了一个Date 类对象。 public Date getHireDay(){ return hireDay; // Bad } } // Date 对象是可变的, 这一点就破坏了封装性!原因见如下代码: Employee harry = ... Date d = harry.getHi reDayO ; double tenYearsInMilliSeconds = 10 * 365.25 * 24 * 60 * 60 * 1000; d.setTime(d.getTime() - (long) tenYearsInMilliSeconds) ; // let 's give Harry ten years of added seniority // 这里 d 和harry.hireDay 引用同一个对象,对d调用更改器方法就可以自动地改变这个雇员对象的私有状态! // 即,破坏了封装性! // 如果需要返回一个可变对象的引用, 应该首先对它进行克隆( clone )。 class Employee { public Date getHireDay() { return (Date) hireDay.cloneO; // Ok } }
基于类的访问权限
- 一个方法可以访问所属类的所有对象的私有数据
- 类外不可以访问 private 字段
class Employee {
public boolean equals(Employee other) {
return name.equals(other.name);
}
}
// 调用
if (harry.equals(boss)) . . . // 这个方法同时访问了 harry 的字段 和 boss 的私有字段,但是是合法的。因为它们同属 Employee 类
final 实例字段
-
构建对象时必须初始化 final 实例字段
-
构造器执行之后,final 实例字段均被设置,且在对象创建之后不可修改
-
final 修饰实例字段分为两种情况:
-
final 修饰基本类型(8种)字段, 或不可变类的字段
-
此时,final 效果贼好,不会造成歧义。因为此时 final 修饰的这两种类型字段,以后都不可变了…
-
String类就是不可变类,其中的 char[] value数组就是 final修饰的。所以,String长度不可变
-
-
final 修饰可变的类
- final final 修饰可变的类时,只是表示存储对象引用不会再指向其他对象;但是,这个对象的内容可以更改
private final StringBuiIcier sb; sb = new StringBuilder("abc"); // 初始化:sb="abc" // sb = new StringBuilder("efg"); // error:不能指向另外的引用 sb.append("efg"); // ok。可以修改sb状态,sb == "abcefg"
-
4.4 静态域与静态方法
静态字段
- 静态字段用 static 修饰,每个类只有一个这样的字段
- 静态字段属于类,而不属于任何单个的对象
- 非静态实例字段,每个对象都有自己的一个副本
- 有些编程语言中,静态字段被称为类字段
静态常量
- 静态变量使用得比较少, 但静态常量却使用得比较多
- public static final double PI = 3.14159265358979323846;
- System.out 便是一个静态常量
静态方法
-
静态方法不是在对象上执行的方法
-
静态方法没有隐式参数
-
在下面两种情下需要使用静态方法:
-
一方法不需要访问对象状态,其所需参数都是通过显式参数提供(例如: Math.pow)
-
一个方法只需要访问类的静态字段(例如:Employee.getNextID)
public static int getNextID { return nextld; // returns static field } int n = Employee.getNextID;
-
-
可以使用对象调用静态方法,但是容易混淆。建议使用类名, 而不是对象来调用静态方法。
工厂方法
- 静态方法还有另外一种常见的用途,即使用静态工厂方法来创建对象
- 以下两种情况,使用静态工厂方法而不是构造创建对象
- 无法命名构造器。
- 构造器的名字必须与类名相同。但是, 这里希望将得到的货币实例和百分比实例采用不用的名字。
- 当使用构造器时,无法改变所构造的对象类型
- 无法命名构造器。
ch 4.5 方法参数
Java 中方法的参数传递只有按值调用,没有c++中的按引用调用。
- 也就是说, 方法得到的是所有参数值的一个副本
- 方法参数共有两种类型:
- 基本数据类型(8种)
- 引用类型
Java 中方法参数的使用情况:
- 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。
- 一个方法可以改变一个对象参数的状态。
- 一个方法不能让对象参数引用一个新的对象。
ch 4.6 对象构造
重载
- 重载只能通过参数列表(即,参数个数、参数类型)来区分,不可以通过方法的返回类型来区分
默认字段初始化
- 如果在构造器中没有显式地给域赋予初值, 那么就会被自动地赋为默认值
-
- 数值为 0
- 布尔值为 false
- 对象引用为 null
- 方法中的局部变量必须明确地初始化
虽然字段不明确初始化会自动赋值为默认值,但是这样回影响代码的可读性…
所以,建议还是把字段也明确初始化
无参数构造器
-
如果在编写一个类时没有编写构造器, 那么系统就会提供一个无参数构造器
- 这个构造器将所有的实例域设置为默认值
- 数值型数据设置为0、布尔型数据设置为false、所有对象变量将设置为null
-
如果类中提供了至少一个构造器, 但是没有提供无参数的构造器,若要使用无参数的构造器则需要手动添加
显式字段初始化
- private String name = “”;
- 实例字段初始化在构造器之前执行
- 当一个类的所有构造器都希望把相同的值赋予某个特定的实例域时,这种方式特别有用
调用另一个构造器
- 关键字 this 指示方法的隐式参数
- this(…) 还可以 调用同一个类的另一个构造器
- this(…) 必须放在构造的第一条语句执行;否则,其前代码可能会无效…
初始化块
-
初始化数据域的方法:
- 在构造器中设置值
- 在声明中赋值
- 初始化块
-
对象初始化块
// object initialization block { id = nextld; nextld++; }
- 每次构造类的对象,对象初始化块都会被执行
-
静态初始化块
- 如果对类的静态域进行初始化的代码比较复杂,那么可以使用静态的初始化块
// static initialization block static { Random generator = new Random0; nextld = generator.nextlnt(lOOOO) ; }
- 静态初始化块只执行一次,且在对象初始化块之前执行
-
构造器的具体处理步骤:
- 如果构造器第一行调用了第二个构造器, 则基于所提供的的参数执行第二个构造器
- 否则,
- 所有数据域被初始化为默认值(0、false 或null)。
- 按照在类声明中出现的次序, 依次执行所有域初始化语句和初始化块。
- 先执行静态初始化块,再执行对象初始化块。
- 静态初始化块只执行一次,对象初始化块在每次创建这个类的对象时均执行
- 执行这个构造器的主体.
对象析构与 finalize 方法
- Java 有自动的垃圾回收器,不需要人工回收内存
- Java 不支持析构器
- 如果某个资源需要在使用完毕后立刻被关闭,对象用完时,可以应用一个close 方法来完成相应的清理操作
ch4 4.7 包
- Java 允许使用包(package) 将类组织起来
- 使用包的主要原因是确保类名的唯一性
- 为了保证包名的绝对唯一性,将公司的因特网域名(这显然是独一无二的) 以逆序的形式作为包名
类的导入
-
一个类可以使用所属包中的所有类, 以及其他包中的公有类( public class)。
-
可以采用两种方式访问另一个包中的公有类
- 在每个类名之前添加完整的包名
- 较为繁琐
- 使用 import 语句
- 简单、常用
- 只能使用星号() 导入一个包, 而不能使用 import java. 导入以java 为前缀的所有包
- 在每个类名之前添加完整的包名
-
如果 import 两个包中包含了同名的类,则会发生命名冲突。主要有以下两种解决方案
- 如果只是使用用一个包中的同名类时,可以考虑增加一个特定的 import 语句来解决这个问题
- 如果两个个包中的同名类都被使用时,则需要在每个类名前加上完整的包名
其中,方法2通用,但是相对可能繁琐一些…
静态导入
-
import 语句不仅可以导入类,还增加了导入静态方法和静态域的功能。
- import static java.lang.System.*;
- 就可以使用System 类的静态方法和静态域,而不必加类名前缀:
- out.println(“Goodbye, World!”); // i.e., System.out
但是,这种编写形式不利于代码的清晰度。最好少用或者不用…
在包中增加类
- 要想将一个类放入包中, 就必须将包的名字放在源文件的开头, 包中定义类的代码之前
- package leetcode.t01twosum;
- 如果没有在源文件中放置 package 语句,这个源文件中的类就属于无名包
- 编译器在编译源文件的时候不检查目录结构
- 如果包与目录不匹配,或许可以成功编译(如果不依赖于其他包时),但是无法成功运行(虚拟机就找不到类)
包作用域
- 标记为 public 的部分可以被任意的类使用;
- 标记为 private 的部分只能被定义它们的类使用
- 如果没有指定public 或 private, 这个部分(类、方法或变量)可以被同一个包中的所有方法访问
注意:变量必须显式地标记为 private, 不然的话将默认为包可见。显然,这样做会破坏封装性。
类路径
- 类的路径必须与包名匹配
- 类路径是所有包含类文件的路径的集合
- UNIX、Windos 配置类路径
- … 见书上 p141
- javac 编译器总是在当前的目录中查找文件,但 Java 虚拟机仅在类路径中有 “.” 目录的时候才查看当前目录
- 如果没有设置类路径, 默认的类路径包含 “.” 目录;
- 然而如果设置了类路径却忘记了包含 “.” 目录, 则程序仍然可以通过编译, 但却不能运行*(因为此时 JVM找不到当前目录的文件)。*
设置类路径
… p142
4.8 JAR 文件
- 在将应用程序打包时,希望只向用户提供一个单独的文件,而不是一个包含大量文件的目录结构。
- JAR文件便是为此而设计的,一个JAR 文件可以包含文件、图像、声音等其他类型的文件。
- JAR文件是压缩的,它使用了 ZIP 压缩格式
创建 JAR 文件
- jar 工具位于 jdk/bin 目录下
- 创建 JAR 文件的常用命令: jar cvf jarFileName file1 file2 …
- jar 命令的格式:jar options file1 file2 …
- jar 程序选项(options) p143
清单文件
- 除了类文件、图像和其他资源外,每个 JAR 文件还包含一个清单文件,用于描述归档文件的特殊特性
- 清单文件位于 JAR 文件的 META-INF 子目录中
可执行JAR文件
- 执行命令: java -jar jarFileName.jar
jar文件这一部分没仔细看…
4.9 文档注释
- JDK 中的 javadoc 工具可以由源文件生成一个 HTML 文档
- ch3 中联机 API 文档就是通过对标准Java 类库的源代码运行 javadoc 生成的
注释的插入
- 每个/** . . . */ 文档注释包含标记以及之后紧跟着自由格式文本( free-form text )
- 标记由 @ 开始, 如@author 或@param
- 自由格式文本的第一句应该是一个概要性的句子
可以为以下各个特性编写注释
- 模块
- 包
- 需要在每一个包目录中添加一个单独的文件
- 提供一个以package.html 命名的HTML 文件
- 提供一个以package-info.java 命名的Java 文件
- 需要在每一个包目录中添加一个单独的文件
- 公有类与接口
- 类注释必须放到 import 语句之后,类定义之前
- 公有的和受保护的构造器及方法
- 每一个方法注释必须放在所描述的方法之前
- 可以使用以下标记
- @param 变量描述
- @return 描述
- ©throws 类描述
- 公有的和受保护的字段
- 只需要对公有字段(通常指的是静态常量)建立文档
可以直接将类、方法和变量的注释放置在 Java 源文件中, 只要用/** . . . */ 文档注释界定就可以了。
但是, 要想产生包注释,就需要在每一个包目录中添加一个单独的文件。(见上)
注释抽取
… p151
4.10 类设计技巧
书上提供的几个类设计技巧
- 一定要保证数据私有
- 绝对不要破坏封装性
- 当数据保持私有时, 它们的表示形式的变化不会对类的使用者产生影响, 即使出现bug 也易于检测。
- 一定要对数据初始化
- Java 不对局部变量进行初始化, 但是会对对象的实例域进行初始化
- 最好不要依赖于系统的默认值, 而是应该显式地初始化所有的数据
- 不要在类中使用过多的基本类型
- 就是说,用其他的类代替多个相关的基本类型的使用
- 这样会使类更加易于理解且易于修改
- 不是所有的域都需要独立的域访问器和域更改器
- 在对象中,常常包含一些不希望别人获得或设置的实例域
- 将职责过多的类进行分解
- 类名和方法名要能够体现它们的职责
- 命名类名的良好习惯是采用一个名词( Order )、前面有形容词修饰的名词( RushOrder)或动名词(有“ -ing” 后缀)修饰名词(例如, BillingAddress )
- 对于方法来说,习惯是访问器方法用小写get 开头, 更改器方法用小写的set 开头
- 优先使用不可变的类
- 更改对象的问题在于, 如果多个线程试图同时更新一个对象, 就会发生并发更改。其结果是不可预料的。如果类是不可变的,就可以安全地在多个线程间共享其对象
- 因此, 要尽可能让类是不可变的
- 当然,并不是所有类都应当是不可变的