本篇笔记主要介绍了 Java 面向对象编程的核心概念和实践。涵盖了类与对象的基本概念、封装与访问控制、继承与多态、接口与抽象类等重要主题。通过理论讲解和代码示例,系统地阐述了 Java 面向对象编程的设计理念和实现方法,帮助读者深入理解面向对象程序设计的思想。(由 claude-3.5-sonnet 生成摘要)
1. Ch09 Objects and Classes
1.1. Object-Oriented Programming (OOP)
- 对象(object):
- 对象一般用来表示现实世界中可以被识别的实体,具有唯一标识、状态和行为。
- 状态(state):由数据字段(属性)及其当前值组成。
- 行为(behavior):由方法定义。
- 对象与基本数据类型的区别
- 基本类型变量:存储实际值。
- 对象引用变量:存储对象的内存地址。
- Java 中的参数传递始终是值传递。因为 Java 中的对象实际上是一种引用,我们在传递参数时是将对应的地址作为值传递进去的;而在传递基本类型时那更是直接传递这个值了。
- 所有对象的实例都存储在堆中,只有局部的基本类型变量和对象引用存储在栈中,这是 Java 语言的一大特性。
- 类(class):
- 类是定义相同类型对象的构造。
- Java 中的类用 变量(variable) 来表示状态,用 方法(method) 来表示行为。
- UML 类图(UML class diagram):通过图形化方式展示类的属性和方法。
1.2. Constructors
- 构造函数(constructor) 是一类特殊的方法,用于创建对象并初始化其属性。
- 使用
new
运算符创建对象并触发构造函数。 - 要求:
- 构造函数的名称必须与类名相同。
- 无返回类型(不是
void
,直接不用写)。
- 默认构造函数(default constructor)
- 如果类没有定义构造函数,Java 会自动提供一个默认的空的无参构造函数。
- 当且仅当类没有显式声明的构造函数时才会创建。
- 默认初始化:
- 对于没有进行初始化的对象中的变量会分配默认值(default value):
- 引用类型:
null
- 数值类型:
0
- 字符类型:
\u0000
- 引用类型:
- 注意:在方法内的局部变量并不会有默认值,这种时候会 CE 报错(variable not initialized)!
- 这其实是关系到使用
new
运算符创建对象时,是在堆中申请了一块新的空间,Java 会将其中的 data fields 自动初始化为全 0,这也就是不同类型所对应的默认值。
- 对于没有进行初始化的对象中的变量会分配默认值(default value):
- 指定初始化:
- 示例
public class Test { static int STATIC_ONE = 1; static int STATIC_TWO; { STATIC_TWO = 2; } }
- 指定初始化的其实是在默认初始化的动作之后执行的。(如在这里执行
STATIC_TWO = 2;
语句之前STATIC_TWO
的值应为 0。)
- 示例
- 三种初始化方法的顺序是:① 默认初始化 ② 指定初始化(直接给变量赋值,或者一个直接写在 class 里的块) ③ 构造函数。
1.3. Garbage Collection (GC)
- 垃圾回收(garbage collection):Java 虚拟机(JVM)会自动回收不再被引用的对象。
- 内存泄漏(memory leak):如果程序不正确地管理对象的引用,让其无法自动释放,就会存在内存泄漏的问题。
- 这里
return elements[--size]
时没有释放elements
数组中对对应对象的引用(因为这不是一个基本类型的栈,而是一个 Object 的栈),这种时候就会有内存泄漏的问题产生。
- 这里
- 手动释放引用:可以通过显式地将对象引用赋值为
null
,来提示 JVM 进行回收。- 示例:
Circle c1 = new Circle(5.0); c1 = null; // c1指向的对象会被回收
- 示例:
1.4. Static Variables & Methods
- 使用
static
关键字修饰全局变量、常量和方法。- 对于全局常量,使用
final static
修饰。
- 对于全局常量,使用
- 静态成员:属于类,而不是某个实例。
- 实例成员:属于特定对象的实例。
- 访问限制:
- 静态方法只能访问静态的变量和方法。
- 实例方法可以访问静态和实例的变量和方法。
- 生命周期:第一次通过类名访问静态变量或静态方法时,Java 会将类加载进内存,为这个类分配一块空间,包含了定义、变量和方法信息,还有类的静态变量,并对静态变量复制。类在加载进内存之后一般不会释放,直到程序结束。一般情况下,类只会这样加载一次。(具体参见“类加载过程”的部分)
1.5. Encapsulation and Access Control
- 封装(encapsulation):保护数据,便于维护。
- 访问修饰符(visibility modifiers):
private
:仅类内部可访问。public
:所有类均可访问。- 默认(无修饰符):同一包内可访问。
- 通过让类提供
get
和set
方法来从外部访问私有数据字段。 private Constructor
:通过private
修饰构造函数,防止从外部创建类的示例。- 示例 1(只能静态访问):Java 提供的
Math
类的构造函数就是private Math() {}
。 - 示例 2(只能被类的静态方法调用):单例模式
- 示例 3(只能被类的其他构造函数调用,用于减少重复代码)
- 示例 1(只能静态访问):Java 提供的
1.6. Immutable Objects and Classes
- 不可变对象:不可变类的对象,一旦创建,其内容不能更改。(典例:
String
) - 不可变类的设计规则:
- 不提供修改对象状态的方法。
- ==将类声明为
final
,防止继承=。- 还可以采用一种更为灵活的方式让类的所有构造器都变为私有的或包级私有的,并添加公共的静态工厂来代替公有的构造器。(如常量池技术,下一章会详细介绍。)
- 所有字段声明为
private
和final
。 - 确保没有返回可变对象引用的方法。
- 不可变类的优点:
- 不可变对象是线程安全的。
- 不需要进行保护性拷贝,因为对象的引用不会被修改。(TODO:?)
- 可以提供静态工厂,把频繁被请求的实例缓存起来。
- 对于特殊不可变对象的部分常用方法,可以直接提供结果。
- 必要时进行保护性拷贝:必须要返回对象的场合,重新创建一个临时对象返回,确保当前对象不被修改。
- 另一个例子:类具有公有的静态 final 数组域,或者返回这种域的访问方法,这是安全漏洞的一个常见根源。
- 另一个例子:类具有公有的静态 final 数组域,或者返回这种域的访问方法,这是安全漏洞的一个常见根源。
1.7. this
- 用途:
- 引用当前对象。
- 调用类的其他构造函数。
- 示例:
class Circle { private double radius; public Circle(double radius) { this.radius = radius; // 只能通过 this 引用当前对象的 radius 字段 } public Circle() { this(1.0); // 调用另一个构造函数 } public double getArea() { return this.radius * this.radius * Math.PI; // 这里一般省略 this } }
1.8. Package
- 包(package):用于组织类,避免命名冲突。
- 如果缺省 package 语句,则类属于默认包(default package)。
- 约定俗称的包命名方式为将公司域名倒过来写。
- 编译器在编译源文件时不会检查目录结构,但是会在运行时报错。
- 导入(import):
- 使用
import
语句导入包中的某个类或整个包。 - 默认行为:
import java.lang.*
。 - 当导入的多个包中存在同名类时,可以用通过指定包名来区分。
- 举例:
import java.util.*; import java.sql.*; Date d = new Date(); // 错误 java.util.Date d = new java.util.Date(); // 正确
- 举例:
- 静态导入(static import):
- 使用
import static
语句导入包中的静态方法和静态域。- 示例:
import static java.lang.System.*; out.println("Hello, World!"); // System.out exit(0); // System.exit
- 示例:
- 使用
- 使用
1.9. JAR
- 创建 JAR 文件:
- 命令行:
jar -cvf filename.jar files
- 命令行:
- 运行 JAR 文件:
- 命令行:
java -jar filename.jar
- 命令行:
- 在 JAR 中打包资源。
2. Ch10 Thinking in Objects
2.1. Association & Aggregation & Composition
- 关联(association):表示对象之间的多重性关系。
- 示例:学生和教师之间的关联。
- 聚合(aggregation):一种方向性关联关系,表示 "has-a" 关系。
- 示例:一个部门包含多个员工。
- 组合(composition):聚合的一种特殊形式(即也是一种 "has-a" 关系),表示更强的依赖关系。
- 示例:一个人包含一个心脏,心脏不能脱离人存在。
- 聚合和组合的区别:组合更为严格,聚合的多个对象可以均独立存在,而组合的多个对象不能独立存在。
2.2. Wrapper Classes
- 包装类(wrapper class):指将基本数据类型封装为对象的类。
- 如
Boolean
,Integer
,Double
等等。
- 如
- 特点:
- 没有无参构造器(总得来个参数才能被包装吧)。
- 包装类对象是不可变的。
- 数值包装类(numeric wrapper class):
- 所有数值包装类都继承自
Number
类,有doubleValue
、intValue
、longValue
、floatValue
、shortValue
等方法,用于把包装类对象转换为对应的基本数据类型。 - 所有数值包装类都有
MAX_VALUE
和MIN_VALUE
两个静态常量:- 对于整数(
Integer
,Short
,Byte
,Long
):用来表示该类型的最大值和最小值。 - 对于浮点数(
Float
,Double
):MIN_VALUE
用来表示该类型能表示的最小正数,MAX_VALUE
用来表示该类型能表示的最大值。
- 对于整数(
- 数值包装类的
valueOf()
静态方法不光可以将基本类型转化为对应包装类对象,也可以把字符串转化为对应包装类对象。 - 整型包装类的
parseInt(str, radix)
静态方法还可以把指定进制的字符串转化为对应整型包装类。 - 浮点数包装类的
parseDouble(str)
静态方法还可以把字符串转化为对应浮点数包装类。
- 所有数值包装类都继承自
2.3. Automatic Boxing & Unboxing
- 自动装箱(automatic boxing):基本类型自动转换为包装类对象。
- 示例:
Integer[] intArray = {2, 4, 3};
- 自动装箱会带来额外的性能开销,在需要频繁装箱和拆箱的场合,建议使用基本类型。
- 举例:这段代码答案是正确的,但是速度会慢特别多:
Long sum = 0L; for (long i = 0; i < Integer.MAX_VALUE; i++) { sum += i; } System.out.println(sum);
- 举例:这段代码答案是正确的,但是速度会慢特别多:
- 示例:
- 自动拆箱(automatic unboxing):包装类对象自动转换为基本类型。
- 示例:
System.out.println(intArray[0] + intArray[1] + intArray[2]);
- 示例:
2.4. BigInteger
& BigDecimal
BigInteger
和BigDecimal
都是不可变类。BigInteger
:支持任意大小的整数运算。- 示例:
BigInteger a = new BigInteger("9223372036854775807"); BigInteger b = new BigInteger("2"); BigInteger c = a.multiply(b); // 结果:18446744073709551614
- 示例:
BigDecimal
:支持高精度的浮点数运算。- 示例:
BigDecimal a = new BigDecimal(1.0); BigDecimal b = new BigDecimal(3); BigDecimal c = a.divide(b, 20, BigDecimal.ROUND_UP); System.out.println(c);
- 示例:
2.5. Interned Strings
- 因为字符串是不可变的且被频繁使用,为了提升性能并节省内存,Java 引入了 字符串池(string pool) 的技术,只为每一种相同的字符串只创建一份实例,这样的实例被称为 被池化的(interned) 字符串。
- 使用
new
关键词创建的字符串一定会创建一个新的对象(不会被池化),使用 string initializer 创建的对象会被池化,即只有在字符串池中没有相同字符串时才会创建新的对象。- 示例:(第一个判断是 false,第二个判断是 true)
- 可以使用字符串对象的
intern()
方法显式地将new
关键词创建的字符串加入字符串池。
- 示例:(第一个判断是 false,第二个判断是 true)
- 像这种字符串拼接的情况,如果不能在编译器常量化得到结果的化,则会编译到
StringBuilder
来拼接,并在最后返回时创建一个新的字符串对象,这种时候是不会被自动池化的,需要显式调用intern()
方法。
2.6. Constant Pool
- Java 的常量池技术,是提升创建某些对象的性能而出现的,当需要一个对象时,直接从池中取一个出来,能节省不少创建对象的时间。
- 常量池其实就是一块内存空间,存在于方法区中。
- 对于字符串类,JVM 编译器会在编译器将字符串字面量常量化,即直接加入到常量池中。
- 对于整数包装类,只会对 -128 到 127 之间的整数进行常量化,且不创建或管理超出这一范围的整数包装类对象。
- 为了性能提升,应尽量使用
Integer.valueOf(int)
方法来创建对象,而不是使用new Integer(int)
方法。 - 示例:这里划红框的两个部分的区别是前者可以在编译器优化,后者则在运行自动拆箱并计算。
- 为了性能提升,应尽量使用
- 对于浮点数包装类,没有实现常量池。
2.7. StringBuilder
& StringBuffer
StringBuilder
:- 非线程安全,但性能更高。适用于单线程场景。
- 方法:
+StringBuilder()
:构造空 StringBuilder,默认容量为 16。+StringBuilder(capacity: int)
:构造空 StringBuilder,容量为capacity
。+StringBuilder(s: String)
:构造一个 StringBuilder,内容为字符串s
。+append(data: char[]) : StringBuilder
:将字符数组追加到此字符串生成器中。+append(data: char[], offset: int, len: int) : StringBuilder
:将字符数组从offset
开始,长度为len
的字符追加到此字符串生成器中。+append(v: aPrimitiveType) : StringBuilder
:将原始类型的值作为字符串追加。+append(s: String) : StringBuilder
:将字符串追加到此字符串生成器中。+delete(startIndex: int, endIndex: int) : StringBuilder
:删除指定范围内的字符。+deleteCharAt(index: int) : StringBuilder
:删除指定索引处的字符。+insert(index: int, data: char[], offset: int, len: int) : StringBuilder
:将字符数组从offset
开始,长度为len
的字符插入到指定索引处。+insert(offset: int, data: char[], len: int) : StringBuilder
:在指定位置插入字符数组。+insert(offset: int, s: String) : StringBuilder
:在指定位置插入字符串。+replace(startIndex: int, endIndex: int, s: String) : StringBuilder
:用指定字符串替换指定范围内的字符。+reverse() : StringBuilder
:反转此字符串生成器中的字符。+setCharAt(index: int, ch: char) : void
:在指定索引处设置新字符。+toString() : String
:返回一个字符串对象。+capacity() : int
:返回此字符串生成器的容量。+charAt(index: int) : char
:返回指定索引处的字符。+length() : int
:返回字符串生成器中的字符数。+setLength(newLength: int) : void
:设置字符串生成器的新长度。+substring(startIndex: int) : String
:返回从 startIndex 开始到末尾的子字符串。+substring(startIndex: int, endIndex: int) : String
:返回从 startIndex 到 endIndex-1 的子字符串。+trimToSize() : void
:减少用于字符串生成器的存储空间大小。
StringBuffer
:- 线程安全,适用于多线程场景。
- 接口与
StringBuilder
相同。
2.8. Enum
- 使用
enum
关键字定义。 - 每个枚举值都是该枚举类的实例,所有枚举类都是
java.lang.Enum
的子类。- 没有可访问的构造器,不能通过
new
关键词创建枚举类,是真正的 final 类。
- 没有可访问的构造器,不能通过
- 提供编译时的类型安全检查,若声明参数的类型为枚举类,则只能传入该参数的非 null 对象引用一定属于该枚举类的某个值。
- 在 enum 类外使用枚举值时,需要使用
enumName.enumValue
的完全限定名形式。 - 可以在
switch
语句中使用,此时可以不使用完全限定名。 - 方法:
ordinal()
方法:返回该枚举值的顺序,这个顺序就是根据枚举值声明的顺序确定的,从 0 开始。name()
方法:返回该枚举值的名称。toString()
方法:返回该枚举值的名称。values()
静态方法:返回枚举类的所有值。valueOf(name: String)
静态方法:可以通过枚举值的名字返回对应的枚举值实例。
- 高级用法(关联数据):构造函数参数通过括号给出,方法通过大括号给出;两者都可缺省。
- 在一些需要用到枚举类编号的地方,不建议直接用
ordinal()
方法得到编号,因为这种时候返回的值就和代码中编码的顺序有关了,可以考虑通过这种方式:public enum Fruit { APPLE(1), PEAR(2), ORANGE(3); // 每个枚举值的构造函数参数 private final int number; // 定义一个字段,用于存储水果编号 Fruit(int num) { // 构造函数,用于初始化每个枚举值的字段 number = num; } public int numberOfFruit() { return number; } }
- 特定于常量的方法实现:
public enum Operation { PLUS { double apply(double x, double y) { return x + y; } }, MINUS { double apply(double x, double y) { return x - y; } }, TIMES { double apply(double x, double y) { return x * y; } }, DIVIDE { double apply(double x, double y) { return x / y; } }; abstract double apply(double x, double y); }
- 策略枚举:
- 在一些需要用到枚举类编号的地方,不建议直接用
3. Ch11 Inheritance and Polymorphism
3.1. Superclasses and Subclasses
- 超类的构造函数是否会被继承不会被 继承(inherite),但可以显式或隐式调用。
- 显式调用:必须使用
super
关键字。- 使用
super
调用必须放在构造函数的第一行。 - 不能使用超类构造函数的名称来调用超类的构造函数。
- 使用
- 如果未显式调用,会默认调用超类的无参构造函数(没有就会CE),且顺序在子类构造函数之前。
- 显式调用:必须使用
- 使用
super.methodName()
调用超类的方法。 - 调用顺序类初始化时构造函数的调用顺序:
- 初始化对象的存储空间为默认值(
0
、null
或false
)。 - 调用父类的构造函数。
- 按顺序分别调用类成员变量和实例成员变量的初始化表达式。
- 调用子类的构造函数剩余部分。
- 初始化对象的存储空间为默认值(
- 当子类的 实例变量(instance variable) 和超类的变量重名时,子类变量会隐藏超类变量。
- 注意,这并不以为着超类变量会被覆盖,重名的实例变量和类变量是两个不同的变量,都会被保留。
- 可以使用
super.variableName
访问被隐藏的超类变量。 - 如果我们把子类实例赋值给超类对象的引用,也会访问到被隐藏的类变量。
- 继承时与超类重名的静态方法和静态变量的处理,都遵循类似的方式。
3.2. Overriding
- 子类可以重写从超类继承的方法,这种特性称为 重写(overriding)。
- 要求:
- 方法签名(方法名和参数列表)必须完全相同。
- 只有可访问的方法才能被重写(即 private 的方法不能被重写)。
- 否则其实是实现了两个无关的方法。
- 静态方法不能被重写,只能被隐藏。
- 重写后的方法不能比被重写的方法拥有更严格的访问权限。
@Override
注解:编译器会负责检查是否真的重写了方法,否则会报 CE。推荐始终使用@Override
注解,尽管它不是必须的。- 重载的方法选择是静态绑定的(compile-time), 而重写的方法选择是动态绑定的(runtime)。
- 在父类构造函数中调用被重写的方法,调用的实际上是子类的重写后的方法。(这也是动态绑定机制的一个体现。)
- 注意:如果父类中的方法是
private
的话,那这里实际上没有发生重写,调用的还是父类的方法。
- 注意:如果父类中的方法是
3.3. Polymorphism
- 多态(polymorphism):一个超类类型的变量可以引用子类的对象。
- 示例:
GeometricObject obj = new Circle();
- 示例:
- 选用的方法调用(如果存在重写的情况,则)在运行时解析,就是上文提到的动态绑定机制。
3.4. Generic Programming & Object Casting
- 泛型编程(generic programming):允许我们编写可以处理多种类型的代码,而不需要为每种类型单独编写代码;多态是实现泛型编程的基础。我
- 向上转换(upcasting):子类对象可以隐式转换为超类类型。
- 示例:
Object o = new Student();
- 示例:
- 向下转换(downcasting):超类转化为子类类型时必须显式转换。
- 向下转换并不总是成功,在类型转换失败时会抛出一个异常。而不是像 C++ 的
dynamic_cast
一样返回一个空指针。 - 可以使用
instanceof
检查能否进行类型转换。 - 示例:
Student s = (Student) o;
- 向下转换并不总是成功,在类型转换失败时会抛出一个异常。而不是像 C++ 的
instanceof
运算符:用于测试对象是否是某个类的实例。- 示例:
if (o instanceof Circle) { Circle c = (Circle) o; }
- 示例:
3.5. equals
method & ==
operator
==
运算符用于比较两个对象的引用是否相等,即比较两个对象是否指向同一个内存地址。==
只能用于比较基本类型的内容是否相同,在比较对象引用类型时,只会比较引用是否相同而不会比较内容。
equals
方法用于比较两个对象的内容是否相等,即比较两个对象的属性值是否相同。- 对于自己定义的类,这需要重写
equals
方法。 - 重写
equals
方法。Object.equals
方法的参数是Object
类型,为了成功重写我们需要保持这一方法签名相同,并用instanceof
检查参数是否是当前类的实例。-
public class Bigram { private final char first; private final char second; public Bigram(char first, char second) { this.first = first; this.second = second; } public boolean equals(Bigram b) { // 错误 return b.first == first && b.second == second; } @Override public boolean equals(Object o) { // 正确 (需要与 Object 类的 equals 方法签名相同) if (!(o instanceof Bigram)) return false; Bigram b = (Bigram) o; return b.first == first && b.second == second; } }
- 对于自己定义的类,这需要重写
3.6. The final
Modifier
final
类:不能被继承。final
方法:不能被子类重写。final
变量:不能被修改。- 如果是基本类型变量,可以理解为类似常量的概念(只能被赋值一次,之后不能被修改)。
- 如果是对象引用变量,则只是引用的地址是常量,而对象内部的内容是可以更改的。
3.7. Review: Handling Objects
- 类加载过程:第一次使用类时,才会加载类
- 分配内存保存类的信息
- 给类变量(静态变量)赋默认值
- 加载父类
- 设置父子关系
- 执行类初始化代码
- 定义静态变量时的赋值语句
- 静态初始化代码块
- 对象创建过程:
- 分配内存
- 对所有实例变量赋默认值(
0
、null
或false
) - 执行实例初始化代码
- 方法调用过程:
- 由于是动态绑定,所以这一实例是什么类就从什么类开始找方法。
- 如果在当前类中找不到,就依次向父类寻找,直到找到为止。
- 如果找不到方法,则报
NoSuchMethodError
错误。 - 如果存在重载的情况,则会根据参数链表选择最匹配的方法,如果同时存在多个最优匹配,则会报错(不过需要注意,可能父类的方法被子类重写了,这种时候只会找到子类的方法)。
- 如何应对继承的双面性?
- 避免使用继承:
- 使用
final
限制继承。 - 优先使用组合而非继承。
- 使用
- 正确使用继承:
- 确保超类设计稳定。
- 使用接口代替继承。
- 避免使用继承:
3.8. Decorator Pattern
- 装饰器模式(decorator pattern):允许我们通过将对象放入包含行为的特殊封装对象中来为原对象绑定新的行为。
3.9. Nested Classes
- 嵌套类(nested class):在一个类中嵌套另一个类。外面的类叫 外部类(outer class),里面的类叫 内部类(inner class)。
- 内部类和外部类可以互相访问对方的私有成员变量和方法,多个内部类可以相互访问对方的私有成员变量和方法。
- 内部类可以声明为 private 从而实现对外完全隐藏,拥有更好的封装性。
- 内部类需要调用方法时:
- 首先检查内部类是否有该方法,如果有就调用。
- 如果内部类没有该方法,则检查并调用外部类的方法。
- 如果内部类和外部类的方法重名且想要调用外部类的方法,需要使用
OuterClass.this.methodName()
。 - 如果内部类和外部类的方法重名但参数列表不同,则 Java 会根据参数列表选择最匹配的方法,注意这一过程只在内部类中进行,而不会跨越到外部类的方法。
- 四种内部类:
- 静态内部类(static inner class):
- 使用
static
修饰。 - 可以访问外部类的静态成员变量和静态方法,但是不能访问外部类的实例成员变量和实例方法。
- 可以在外部类外被使用,例:
new OuterClass.StaticInnerClass()
。
- 使用
- 成员内部类(member inner class):
- 没有
static
修饰。 - 成员内部类需要与一个外部类实例绑定,可以访问外部类的所有成员变量和方法。
- 在外部类外使用时需要外部类实例才能创建。
outer.new InnerClass()
。
- 没有
- 方法内部类(local class):
- 定义在方法中且只能在方法中使用。
- 方法内部类也区分是否是
static
修饰的,如果是static
则只能访问外部类的静态成员,否则可以访问所有成员。 - 方法内部类访问方法中的参数和局部变量时,这些变量需要声明为
final
的。这些变量实际上会在方法内部类创建时被复制一份作为方法内部类自己的成员变量所使用。
- 匿名内部类(anonymous class):
- 匿名内部类没有单独的类定义,而是在创建对象的同时定义类。
- 匿名内部类没有构造函数,但可以通过参数列表调用对应的父类构造函数。
- 和方法内部类相同,可以访问外部类的所有变量和方法,也可以访问方法中的
final
参数和局部变量。
- 静态内部类(static inner class):
4. Ch12 Abstract Classes and Interfaces
4.1. Abstract Classes
- 抽象类(abstract class) 是相对于 具体类(concrete class) 而言的:
- 抽象类是一种不能直接实例化的类(即不能通过 new 操作创建对象)。
- 抽象类可以包含抽象方法(没有实现的方法)和具体方法(有实现的方法)。非抽象类不能包含抽象方法。
- 抽象方法必须在子类中被完全实现,否则子类也必须是抽象类。
- 即使父类是具体类,子类也可以是抽象类。
- 即使父类中实现了某一方法,子类也可以重写该方法为抽象方法。
- 抽象类不能实例化,但可以作为一种数据类型使用。例如可以定义一个抽象类数组,用于存放其作为具体类的子类对象。
4.2. Interfaces
- 接口(interface) 是一种特殊的类的结构,用于定义类的行为规范。
- 一个类只能继承一个抽象类,但是一个类可以实现多个接口。
- 如果多个接口中存在相同的方法签名,则会被编译器检测并报 CE。
- 接口中只能包含常量和抽象方法。
- 所有方法默认是
public abstract
的。 - 所有成员变量默认是
public static final
的。
- 所有方法默认是
- 接口中不能有构造器。
- (Java 8 新增)静态方法:
- 属于接口本身,而不是实现类。
- 只能通过接口名调用,而不能通过实现类或对象调用。
- (Java 8 新增)默认方法:
- 通过
default
关键字修饰。 - 提供了接口方法的默认实现,实现类可以选择重写该默认方法。
- 通过
- (Java 9 新增)私有方法:
- 只能在接口中被其他默认方法和或静态方法调用,不能被实现类访问。
- (Java 9 新增)私有静态方法:
- 只能在接口中被其他静态方法调用,不能被实现类或默认方法访问。
- 接口可以通过
extends
关键词继承另一个接口。
- 一个类只能继承一个抽象类,但是一个类可以实现多个接口。
- 相比于抽象类被设计用于描述“是什么”,接口被设计用于描述“能做什么”。
- 常用的接口如:
Comparable
、Cloneable
。
- 常用的接口如:
- 类似于抽象类,接口不能被实例化,但可以作为数据类型使用。
- 示例:自定义类实现
Cloneable
接口,用于标记类的对象可被克隆。@Override public Object clone() { try { return super.clone(); } catch (CloneNotSupportedException ex) { return null; } }
4.3. 面向对象的设计原则
- 原则 1:不要重复自己(DRY)
- 消除重复代码,降低维护成本。
- 三次法则:
- 第一次写重复代码。
- 第二次写时考虑重构。
- 第三次写时必须重构。
- 原则 2:封装变化
- 封装可能会变化的代码,减少对其他部分的影响。
- 原则 3:开闭原则(OCP)
- 软件实体应对扩展开放,对修改关闭。
- 原则 4:单一职责原则(SRP)
- 每个类只负责一项职责,避免职责耦合。
- 原则 5:依赖倒置原则(DIP)
- 高层模块不依赖低层模块,二者都依赖于抽象。
- 原则 6:合成复用原则(CRP)
- 优先使用组合而非继承来实现复用。
- 原则 7:里氏代换原则(LSP)
- 子类必须能够替换掉父类。
- 原则 8:接口隔离原则(ISP)
- 使用多个专门的接口,而不是一个庞大的接口。
- 原则 9:针对接口编程
- 使用接口而非具体实现,提升代码灵活性。
- 原则 10:委托原则
- 将职责委托给类本身而非客户端,减少代码重复。