第4章 对象和类
4.1 面向对象程序设计概述
- 面向过程 算法第一
- 面向对象 数据第一
4.1.1 类
类 构造 实例
- 封装是将数据和行为组合在一个包中,对使用者隐藏了数据的实现方法
- 实例域是属于对象的域,区别于类域,即静态域
- 方法是操纵数据的过程
- 实例域值的集合成为对象的状态
- 继承即扩展一个类来建立一个新类
- 不能让类中的方法直接访问其它类的实例域
- 超类: Java中所有的类都来源于Object
4.1.2 对象
三个主要特性:行为、状态、标识
4.1.3 识别类
看名字,靠自己
名词多为类,动词多为方法
4.1.4 类之间的关系
依赖 use-a
聚合 has-a
继承 is-a
4.2 使用预定义类
不是所有类都有面向对象特征,例如Math类
4.2.1 对象和对象变量
构造器用来构造对象,它是一种特殊的方法,要用new操作符
如new Date()
- 构造器返回对象的引用
- 对象和对象变量不是一回事,任何对象变量的值都是对另一对象的引用
- 在没有引用对象的变量上应用方法会产生编译错误
- 对象变量可以显式设置成null,表示未引用对象,在值为null的变量上应用方法会产生运行时错误。
- 局部变量除非调用构造器new或设置为null,否则不会自动初始化
4.2.2 Java类库中的LocalDate类
LocalDate类使用静态工厂方法来代表我调用构造器
LocalDate.now()
4.2.3 更改器方法与访问器方法
LocalDate aThousandDaysLater = newYearsEve.plusDays(1000)
调用plusDays方法会生成一个新对象,原对象不会发生改变
- 更改器方法
add方法会使调用该方法的对象改变
- 访问器方法
get方法只访问对象而不修改对象
4.3 用户自定义类
- 自定义类是自己定义的主力类,通常没有main方法,但有自己的实力域和实例方法
- 一个完整的程序包括若干个类,但只有一个类有main方法
4.3.1 Employee类
class ClassName
{
instant field1
…
constructor1
…
method1
…
}
- 文件名域必须与公共类相同,每个文件最多只有一个公共类,可有多个非公共类
4.3.2 多个源文件的使用
- 也可以按照喜好,为工程中的每一个类都创建一个文件,这样编译时有两种方法:
一是使用通配符:javac Employee*.java
二是直接编译有main函数的那个类文件,编译器会自动查找其它类的相应文件。其功能类似于make
4.3.3 剖析Employee类
- 方法标记为public,使任何类的任何方法都可以调用该方法
- 实例域用private标记,只有类自身的方法能够访问,对象都不可以
- 若将实例域用public标记,就破坏了封装,不推荐
- 其它类类型的对象也可以作为实例域
4.3.4 从构造器开始
- 构造器与类同名
- 可以有多个构造器
- 构造器可以有任意个参数,也可以没有
- 构造器没有返回值
- 构造器总是和new操作符一起使用;不能像c++调用构造函数那样调用java构造器;不能对已存在的对象调用构造器来试图重置其实例域,否则产生编译错误。
- 不能再构造器中定义与类实例域同名的变量
4.3.5 隐式参数与显式参数
隐式参数即调用该方法的类对象,也称为方法调用的目标或接收者,可以用关键字this表示,这样可以区分局部变量和实例域,如
double raise = this.salary * byPercent / 100;
显式参数就是方法名后括号里的数值
- Java中方法只可以在类中定义,不可以像C++那样在类外定义,不区分内联方法
4.3.6 封装的优点
做法:为获得或设置实例域的值,提供三个方面的内容
- 设置为private属性
- public属性的域访问器方法
- public属性的域更改器方法
优点:
- 改变内部实现,除了更改该类的方法,不会影响其它代码
- 更改器方法可以执行错误检查,避免将错误的值直接赋值给实例域
注意:不能编写返回可变对象的访问器方法,所谓可变对象是指存在更改器方法的对象,若将此对象返回,则引用该对象的变量就可以在类外部更改可变对象的值,破坏封装
- 正确的做法是首先对其克隆
4.3.7 基于类的访问权限
方法可以访问所属类的所有对象的私有数据,不仅限于访问隐式参数的私有属性
- C++也一样哦
4.3.8 私有方法
当一个代码有若干辅助方法组成,这些辅助方法不应该成为公有接口,故应设置为private属性
- 这样当不需要这些代码时,完全可以将其删掉,因为它们不会被外部的其它类调用;但当方法是公有的,就不能随意删去了,因为其它代码可能依赖它
4.3.9 final实例域
如果实例域在被构造器初始化后不再修改,可以将其声明为final,例如
private final String name;
- final修饰符应该应用于基本(primitive)类型域,或不可变(immutable)类的域
- 所谓不可变类是指类的所有方法都不会改变其对象,例如String类
- final修饰可变类类型的域,例如
private final StringBuilder evaluations;
evaluations = new StringBuilder();
是指evaluations这个变量只能与其第一次引用的对象绑定,不能再引用其它对象,但是被引用的对象还是可以更改的
4.4 静态域与静态方法
static修饰符4.4.1 静态域
将域设置为static属性,如则nextId将被该类的所有实例(对象)共享,无论定义多少个该类的对象, 都只有一个静态域nextId,即使没有一个对象,静态域nextId仍存在。它属于类,不属于任何独立的对象。private static int nextId = 1;
4.4.2 静态常量
静态常量使用的次数更多,例如Math.PI。静态常量比静态域的声明多了final修饰符,而且用public属性修饰,例如看其属性就知道了,之所以敢用public,是因为final限制了它不能被修改public static final double PI = 3.14159265358979…;
4.4.3 静态方法
静态方法不能对实例域进行操作,只能访问自身类的静态域,可以认为它没有this参数。可通过类名调用,也可使用该类的对象调用,但即使那样,也不能操作类的实例域,还会给人造成混淆,多以不建议那样做。4.4.4 工厂方法
静态方法可作为静态工厂方法,用来生成不同风格的格式化对象。它的存在主要有两点原因:- 有时无法命名构造器,因为构造器与类同名。
- 当使用构造器时,无法改变多构造的对象类型,这个不解?为啥要改变类型
4.4.5 main方法
main方法也是静态方法,不需要对象来调用即可执行。
- 每个类都可以有main方法,无论其是否为public类,这样可以用于单元测试,即测试这个类的代码是否有错误
- 当一个有main方法的类(姑且叫Sub)是另一个类(姑且叫App)的一部分,使用java App命令则Sub类中的main函数永远也不会被执行。就是这么强势,哈哈
4.5 方法参数
Java中参数传递总是使用值传递
方法参数有两种类型:基本数据类型和对象引用
所以
- 一个方法不可能修改一个基本数据类型的参数
- 但可以改变对象参数的状态
因为方法得到的是参数的拷贝,而对象引用和它的任何拷贝总是引用同一个对象
但是方法不能让对象参数引用一个新的对象,还是因为值传递
4.6 对象构造
4.6.1 重载
Java允许重载任何方法,自然包括构造器方法- 当多个方法有相同的名字,不同的参数,就产生了重载
- 方法名和参数类型叫做签名,返回类型不是签名的一部分,所以不能有方法名相同、参数类型也相同却返回不同类型值的方法。
4.6.2 默认域初始化
如果没有在构造器中显式地对域进行初始化,那么就会自动初始化为默认值
- 数值默认为0
- 布尔值为false
- 对象引用为null
这不是种好的编程习惯
4.6.3 无参数的构造器
如果编写一个类时没有提供任何构造器,那么系统就会提供一个默认的无参数构造器,将实例域初始化为默认值
如果提供了构造器,但是没有提供无参数的构造器,系统不会提供默认的无参数构造器,那么在调用无参数构造器时就会发生编译错误
4.6.4 显式域初始化
可以在定义实例域时进行赋值操作,显式地初始化,这一点在所有构造器对同一个实例域赋予相同值时特别方便
- 初始值不一定非得时常量,还可以调用方法对域进行初始化,例如
class Employee
{
private static int nextId;
private int id = assignId();
…
private static int assignId()
{
int r = nextId;
nextId++;
return r;
}
…
}
4.6.5 参数名
- 构造器参数如果用简单的字母命名则不够直观
- 构造器参数可以在实例域名前加前缀a来区分,很直观
- 构造器参数可以与实例域同名,屏蔽实例域,同时使用关键字this指示隐式参数,具有技巧性
4.6.6 调用另一个构造器
public Employee(double s)
{
this(“Employee #” + nextId, s);
nextId++;
}
- 在构造器的第一行调用this(…)
- 通过调用new Employee(60000)将调用Employee(String, double)构造器,这样对公共的构造器代码只编写一次即可
- C++不可以这么做
4.6.7 初始化块
初始化数据域有三种方法,前面已经有两种方法:
- 在构造器中设置值
- 在声明中赋值
第三种是初始化块
一个类的声明中可以包括多个块,这些块会在构造类的对象时,在构造器执行前执行。块类似于方法,但是没有参数和返回值,可以执行复杂的操作
总结调用构造器初始化数据域的步骤
- 所有数据域初始化为默认值
- 按在类中出现的顺序依次执行域初始化语句和初始化块
- 如果构造器第一行调用另一个构造器,则先执行另一个构造器
- 执行构造器主体
静态域初始化也有两种方法
- 直接赋值
- 静态初始化块,即在块前加关键字static修饰,那么只执行一次?
- 否则就是默认值
4.6.8 对象析构与finalize方法
java没有析构器,有自动的垃圾回收器回收内存
如果有其它资源需要回收,在类中添加finalize方法,它会在垃圾回收器前调用,但不要用它回收紧缺的资源,因为不知道具体什么时候调用
可以手动close
4.7 包
- 利用包可以组织代码
- 可以使用嵌套层次组织包
- 所有标准的Java包都处于java和javax包层次中
- 使用包可以保证类名唯一性,类似于C++的namespace
- 建议使用因特网名的逆序作为包名
- 包和划分为子包,嵌套的包间没有任何关系,都拥有独立的类集合
4.7.1 类的导入
一个类可以使用所属包的所有类,以及其他包的公共类
对应其他包中的公共类,有两种方式
- 在类名前添加完整的包名,麻烦
使用import语句
import语句必须位于文件的顶部,package语句的后面
可以import java.util.*;也可以导入特定类improt java.util.time.LocalDate;
- *号只能用于导入一个包下的所有类,不能一个导入相同前缀的多个包
- 当导入的多个包中有同名类时,可以导入要使用的特定类,也可是使用包含包名的类全称
4.7.2 静态导入
可以使用import方法导入一个类的静态方法和静态域,这样就可以直接使用而不用加类名,不推荐。方法是:
导入一个类的所有静态域和静态方法
improt static java.lang.System.*;
也可以导入特定静态方法或域
improt static java.lang.System.out;
4.7.3 将类放入包中
使用package语句,必须放在源文件的开头
- 如果不添加package,那么源文件中的类就属于默认包default package
- 包中的文件必须放到与完整包名匹配的子目录中,例如com.horstmann.corejava包的所有源文件应放到com/horstmann/corejava子目录下,否则包和目录不匹配,虚拟机找不到类
4.7.4 包作用域
最好不要忘了添加public或private关键字,如不添加,类、方法或变量将可以被同一个包的所有方法访问,破坏封装
4.8 类路径
看了,不太懂,暂时不太需要
4.9 文档注释
/* /的用法
4.9.1 注释的插入
4.9.2 类注释
4.9.3 方法注释
4.9.4 域注释
4.9.5 通用注释
4.9.6 包和概述注释
4.9.7 注释的抽取
4.10 类设计技巧
- 数据私有,不要破坏封装
- 数据初始化
- 不要在类中使用过多的基本数据类型
- 不是所有的域都需要独立的域访问器和域更改器
- 将职责过多的类进行分解
- 类名和方法名要能体现它们的职责
- 优先使用不可变的类,但不是必须,不可变的类可以放心地在多线程程序中共享
未完待续