1、封装
1.1、简介
封装(Encapsulation)
- 含义:
- 将数据和基于数据的操作封装在一起,构成一个不可分割的独立实体。
- 将对象的状态信息隐藏在内部,提供公共接口对外提供该对象的功能。
- 好处:
- 隐藏类的实现细节。
- 限制使用者对成员变量的不合理访问。
- 实现 “高内聚低耦合”,提高程序可维护性。
- 可进行数据检查,保证对象信息的完整性。
1.2、访问修饰符
1.2.1、访问控制级别
含义 | 可访问范围 | |
---|---|---|
public | 公开 | 所有类 |
protected | 受保护的 | 同个包下的类、所在类的子类 |
default | 缺省(默认级别,无修饰符) | 同个包下的类 |
private | 私有 | 所在类内部 |
1.2.2、可修饰结构
成员变量、方法、构造方法 | 局部变量 | 外部类 | 内部类 | |
---|---|---|---|---|
public | ✔ | ❌ | ✔ | ✔ |
protected | ✔ | ❌ | ❌ | ✔ |
default | ✔ | ❌ | ✔ | ✔ |
private | ✔ | ❌ | ❌ | ✔ |
- 局部变量:
- 说明:无法使用任何访问修饰符。
- 解释:作用域为当前方法,无法被其它类访问,因此。
- 外部类:
- 说明:无法使用
private
和protected
,只能用public
和default
修饰符 。 - 解释:没有位于任何类的内部,不存在所在类的概念。
- 说明:无法使用
1.2.3、使用原则
- 成员变量:
- 实例变量:即普通属性,通常使用
private
修饰。 - 类变量:即
static
修饰的属性,考虑使用public
修饰。
- 实例变量:即普通属性,通常使用
- 工具方法:用于辅助其它方法实现预期目标,本身不对外暴露,应当使用
private
修饰方法。 - 子类重写:若某个类设计为父类,部分方法希望子类重写方法且不对外暴露,应当使用
protected
修饰。
1.3、JavaBean 规范
良好实践:JavaBean 规范。
-
每个成员变量都使用
private
修饰。 -
每个成员变量提供
public
修饰的 getter/setter 方法。class User { private int id; private String name; public int getId() {} public void setId(int id) {} public String getName() {} public void setName(String name) {} }
2、继承
2.1、案例引入
假设已有 Person 类,现需要定义一个 Student 类。
(字段如下)
class Person {
private String name;
private int age;
// 两对 getter, setter
}
class Student {
private String name;
private int age;
private int score;
// 三对 getter, setter
}
分析
-
Student 类包含了 Person 类的所有结构(属性、方法),在此基础上多了其它结构。
-
使用继承,子类可以获取父类的结构,实现代码复用。
class Student extends Person { private int score; // score的 getter, setter }
术语:假设 A 继承 B
- A:超类(super),父类(parent),基类(base)。
- B:子类(sub),扩展类(extended)。
2.2、说明
2.2.1、继承
extends(扩展)
- OOP 中实现复用的重要手段,实现 IS-A 关系。
- 从现有的类派生出子类,在此基础上可扩展新的功能。
- 无法获取的结构:
- 父类的 private 结构。
- 父类的构造方法(可以从构造方法与类同名的角度理解)。
2.2.2、继承树
-
Object:
Object
类是所有类的超类。- 即任意类都会直接或间接继承
Object
(除了 Object 本身)。 - 若定义类时没有指定
extends
,编译器会自动加上extends Object
。
- 即任意类都会直接或间接继承
-
单继承:Java 类有且仅有一个直接父类(Object 没有父类)。
-
类和类之间的继承关系,形成树状结构。
┌───────────┐ │ Object │ └───────────┘ ▲ │ ┌───────────┐ │ Person │ └───────────┘ ▲ ▲ │ │ │ │ ┌───────────┐ ┌───────────┐ │ Student │ │ Teacher │ └───────────┘ └───────────┘
2.2.3、阻止继承
-
final:使用
final
修饰某个类,则无法被继承。public final class String { }
-
sealed ... permits ...(Java 15+):使用
sealed
修饰某个类,通过permits
指定允许继承的子类名称。// 允许 A,B,C 三个类继承 public sealed class Demo permits A, B, C { ... } // 编译通过 class A extends Demo { } // 编译错误:class is not allowed to extend sealed class class D extends Demo { }
2.2.4、继承 vs 组合
根据类的关系,选择使用继承(IS-A)或组合(HAS-A)。
- 继承:表达 IS-A 关系。
- 适用场景:允许对另一个类公开所有接口。
- 示例:模板方法、工厂方法等设计模式。
- 组合:表达 HAS-A关系。
示例
class Person {
}
class Book {
}
// 学生 is 人,可使用继承
class Student extends Person {
// 组合
private Book book;
}
2.3、super 关键字
2.3.1、说明
super:表示父类,用于调用父类结构。
- 作用:访问父类的非 private 结构(属性、方法、构造方法)。
- 可以访问直接父类或间接父类的结构。
- 若继承链中有多个同名结构,就近原则。
- 构造方法调用:Java 规定,构造方法首行必须调用父类构造方法。
- 若无显式调用,编译器会自动在首行生成
super();
。 - 同一个构造方法中,只能调用一次父类构造方法。
- 若无显式调用,编译器会自动在首行生成
2.3.2、示例
Person 类有一个有参构造方法,Student 类继承 Person 类。
class Student extends Person {
private int score;
public Student(String name, int age, int score) {
this.score = score;
}
}
class Person {
protected String name;
protected int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
以上代码编译不通过,分析如下:
-
Student 类的构造方法没有显式调用 Person 的构造方法。
-
编译器自动生成
super();
,但 Person 类没有无参构造方法。public Student(String name, int age, int score) { super(); // 自动生成 this.score = score; }
解决方案:子类调用父类中存在的构造方法。
public Student(String name, int age, int score) {
super(name, age);
this.score = score;
}
2.4、转型
2.4.1、向上转型
向上转型(upcasting)
将一个子类类型安全转换为父类类型的赋值(父类引用指向子类对象)。
Person p = new Student(); // upcasting
Object o = new Student(); // upcasting
2.4.2、向下转型
向下转型(downcasting)
将一个父类类型强制转型为子类类型。
前提:父类引用指向的是子类对象。
-
p1:指向 Person 类型的对象实例,不满足前提,向下转型失败。
-
p2:指向 Student 类型的对象实例,满足前提,向下转型成功。
Person p1 = new Person(); Person p2 = new Student(); // upcasting Student s1 = (Student) p1; // 错误:ClassCastException Student s2 = (Student) p2; // ok
2.4.3、instanceof
instanceof:判断一个实例是否为指定类型。
Person p1 = new Person();
Person p2 = new Student(); // upcasting
System.out.println(p1 instanceof Person); // true
System.out.println(p1 instanceof Student); // false
System.out.println(p2 instanceof Person); // false
System.out.println(p2 instanceof Student); // true
优雅的向下转型
-
先判断后转型:
Person p = new Student(); // 判断 if (p instanceof Student) { // 向下转型 Student s = (Student) p; System.out.println(s.getName()); }
-
判断并转型(Java 14+):判断 instanceof 后可直接转型为指定变量。
Person p = new Student(); // 判断成功后,自动转型为s if (p instanceof Student s) { // 可直接使用变量s System.out.println(s.getName()); }
2.5、同名属性
子类可以定义与父类中同名的属性或方法。
同名属性:子类定义的与父类同名的属性,二者完全独立。
- 父类对象只调用自己的属性,不会调用子类的属性。
- 子类对象默认调用自己的属性(即
this
),调用父类属性需要使用super
。
子类可定义与父类方法签名完全相同的方法。
称为重写(override),见下文。
3、多态
3.1、方法重写
3.1.1、含义
重写(override)
- 含义:子类中定义了与父类方法签名完全相同的方法。
- 要求:
- 方法名、参数列表相同。
- 权限修饰符大于父类,返回值类型和抛出异常类型小于父类。
- 说明:
- 无法重写 private 方法和构造方法。
- 在子类的重写方法上添加
@Override
注解(非必要),编译器会检查是否正确重写了方法。
3.1.2、重写 Object 方法
Object 是所有类的超类。
常用的三个方法如下,必要时可以进行重写。
作用 | 默认实现 | |
---|---|---|
equals() |
判断两个实例是否逻辑相等 | == :基本类型比较值,引用类型比较地址 |
hashCode() |
计算实例的哈希值 | 基于对象的内存地址转换的值 |
toString() |
将实例输出为字符串 | 类名 + "@" + 哈希值对应的 16 进制数 |
3.1.3、重写 vs 重载
对比
重写(override) | 重载(overload) | |
---|---|---|
含义 | 在继承关系中,子类定义的与父类方法签名完全相同的方法 | 同一个类中,方法名相同、参数列表不同的方法构成重载 |
方法名 | 相同 | 相同 |
参数列表 | 相同 | 不同 |
修饰符、返回值、异常 | 权限修饰符大于父类,返回值、抛出异常类型小于父类 | 与权限修饰符、返回值、抛出异常类型无关 |
注意 | 无法重写 private 方法和构造方法 | 如果定义了两个方法名和参数列表相同的方法,编译不通过。 |
3.2、多态
3.2.1、案例引入
Student 类继承 Person 类,并且重写了父类的方法。
class Person {
public void work() {
System.out.println("Person is working");
}
}
class Student extends Person {
@Override
public void work() {
System.out.println("Student is working");
}
}
创建两个 Person 变量并调用 work() 方法,查看结果。
-
p1 指向 Person 类型实例,实际调用的是 Person 的 work() 方法。
-
p2 指向 Student 类型实例,实际调用的是 Student 的 work() 方法。
public static void main(String[] args) { Person p1 = new Person(); p1.work(); // Person is working Person p2 = new Student(); p2.work(); // Stuednt is working }
分析
- 两个变量均声明为 Person 类型,但指向的实例类型不同。
- 实例方法的调用是基于运行时实际类型的动态调用,而非变量的声明类型。
3.2.2、多态
多态(Polymorphic)
父类引用指向子类对象的一种体现。
- 含义:同一个行为在运行时具有不同表现形式,无法提前确定。
- 变量声明时,指向的具体类型在运行时才确定。
- 方法调用时,真正执行的方法取决于运行时的实际类型。
- 好处:面向抽象编程,提高代码复用性、程序可扩展性。
- 实现方式:继承父类、实现接口。
3.2.3、应用示例
需求示例:实现一个税额计算方法,计算每种收入的税额。
假定有普通类型、工资类型、免税类型。
-
收入类 Income:作为默认收入类型。
class Income { protected double income; public double getTax() { return income * 0.1; // 税率10% } }
-
其它类型收入:
class SalaryIncome extends Income { @Override public double getTax() { return income <= 5000 ? 0 : (income - 5000) * 0.2; // 5000元起征 } } class TaxExemptIncome extends Income { @Override public double getTax() { return 0; // 免税 } }
-
税额计算:
public double totalTax(Income... imcomes) { double total = 0.0; for (Income income : imcomes) { total += income.getTax(); } return total; }
-
测试:
-
无需提前知道有多少种具体类型,只要子类正确地重写父类方法。
-
利用多态调用父类定义的方法,即可实现预期目标。
-
即使有新的收入类型,只需定义一个新的子类并重写方法即可。
public static void main(String[] args) { Income[] incomes = new Income[] { new Income(3000), new Salary(10000), new TaxExemptIncome(2000) }; System.out.println(totalTax(incomes)); }
-
小结 & 思考
关键字
① this & super
- this:表示当前对象实例,用于调用当前对象的结构。
- super:表示父类,用于调用父类非 private 结构。
② final
- 修饰类:无法被继承。
- 修饰方法:无法被重写。
- 修饰属性:初始化后无法被重新赋值。
- 声明时初始化。
- 构造方法初始化。
思考
① 继承存在的问题?
- 问题:耦合性
- 破坏封装:向子类暴露了实现细节。
- 耦合:父类改变时,所有子类都可能受到影响。
- OOP 设计原则:
- 合成复用原则:优先使用组合、聚合,其次考虑继承。
- 里氏替换原则:继承必须保证父类的性质在子类中成立。
- 子类重写方法会降低多态的复用性。
- 若子类方法逻辑与父类方法不同,以多态调用父类方法时无法实现预期功能。