文章目录:
以申雪萍老师PPT标号为章节。
部分内容来源于AI总结。(每个PPT都接近100页,实在看不完了)
system提示词:
目前你是北航面向对象(OOP Java)课程的老师,你有一些学生因为一些原因没有来上课,但是马上就要期末考试了,这些同学来问你,你一定要把所有知识点都教给他们,让他们听完你的讲授都能拿100分。
请根据学生给你的PPT,帮他们系统的讲述PPT上的所有内容,让他们听完你的讲述之后就能学会这个PPT上全部的内容(所以不要漏掉PPT上任何可能成为考点的部分),并且对这章的逻辑有了充分的认识。
你的学生都很聪明,所以不要用弱智般的比喻或拟人来讲解,但可以用一些非常贴合或者常用的例子或者比喻。
Lesson 1 概述
1. 各种编程语言区别
面向过程(Procedure Oriented):如C语言、Fortran。
- 特征:关注“怎么做”,强调步骤。
面向对象(Object Oriented):如Java、C++。
- 特征:关注“谁来做”,强调对象之间的交互。
- 优势:更符合人类思维,易于复用、扩展、维护。
执行方式的区别:
- 编译型(Compilation):一次性翻译成目标程序(如C++),执行快。
- 解释型(Interpretation):边解释边执行,跨平台性好但慢。
Java的特殊性(高频考点):Java是编译与解释相结合。
- 源代码(
.java) -> 编译 -> 字节码(.class) -> JVM解释执行。 - 注:JVM中有JIT(即时编译器),能把热点字节码编译成本地机器码,解决性能问题
- 源代码(
2. 面向对象编程概念
数据(属性)和操作(行为)封装在一起。数据是主语,函数是谓语。
window.move(x, y)-> “窗口”自己执行“移动”这个行为。
三要素:
- 封装(Encapsulation):隐藏细节,通过接口交互。
- 继承(Inheritance):代码复用,子类继承父类。
多态(Polymorphism):
- 操作名称的多态(重载 Overload)。
- 同一操作不同行为(重写 Override)。
类(Class)与对象(Object)的关系:
类(Class):是模具,是蓝图,是抽象的。它定义了某一类事物的共性(属性和方法、构造方法)。
- PPT例子:
Grandma类,Car类。
- PPT例子:
对象(Object):是饼干,是实物,是实例(Instance)。它是内存中真是存在的实体。
- PPT例子:101岁的王奶奶,88岁的张奶奶。
3. Java特性
1. Java的历史与版本
- Java之父:James Gosling(Sun公司 -> Oracle收购)。
三大体系(虽然现在改名叫Java SE/EE/ME,但旧称呼J2SE等也要知道):
- J2SE (Java SE):标准版,我们要学的基础核心,用于桌面应用。
- J2EE (Java EE):企业版,用于服务器、大型企业应用。
- J2ME (Java ME):微型版,用于嵌入式(现在虽式微,但概念要知道)。
2. Java的特性(Features)
- 跨平台(Platform Independent):这是Java的金字招牌。"Write Once, Run Anywhere"。
- 面向对象。
- 简单性(相比C++,没有指针,自动内存管理)。
- 多线程、分布式、健壮性。
3. 运行机制:JVM(Java Virtual Machine) 这是理解“跨平台”的关键:
- Java源码不是直接编译成机器码,而是编译成字节码(Bytecode,即.class文件)。
- 任何安装了JVM的操作系统(Windows, Linux, Mac)都能运行字节码。
- JVM屏蔽了底层操作系统的差异。
Lesson 2 面向对象的思维模式
1. 面向对象的思维
我们要把现实世界的东西变成代码,需要经过“抽象”。PPT里提到了静态属性和动态属性:
静态属性 $\rightarrow$ 成员变量 (Fields/Attributes):比如姓名、年龄、体重。
- 设计原则:通常设为
private,通过方法访问(封装)。
- 设计原则:通常设为
动态属性 $\rightarrow$ 方法 (Methods):比如
刷卡()、收银()、跑()。- 考点:消息 (Message)。当PPT里说“向对象发送消息”时,你要马上反应过来,这就是在调用这个对象的方法。例如
tv.open(),就是给电视机对象发了一个“开机”的消息。
- 考点:消息 (Message)。当PPT里说“向对象发送消息”时,你要马上反应过来,这就是在调用这个对象的方法。例如
面向对象的方法论:
- 1个工具:抽象 (Abstract)。摒弃细节(比如收银员今天穿什么袜子我们不关心),提取共性(我们需要她的工号和收款能力)。
- 2个概念:类 (Class)、对象 (Object)。
- 3个特性:封装 (Encapsulation)、继承 (Inheritance)、多态 (Polymorphism)。这是OOP的三大支柱。
4个步骤:
- OOA (分析):做什么?(What)
- OOD (设计):怎么做?(How - 设计类、接口)
- OOP (实现):写代码 (Coding)。
- OOT (测试):验证功能。
2. 类之间的关系
这是考试中最容易混淆的部分。一定要分清以下关系,并在UML图和代码中识别出来:
1. 关联 (Association)
- 含义:拥有关系,“Has-a”。类A和类B是平等的,但长期持有对方的引用。
代码体现:成员变量。(体现为成员变量 (Field)。 类A中有一个属性是类B(或者类B的集合)。)
- 例如:
Customer类里面有一个成员变量private Set<Order> orders;。 - 关系可以是双向的,也可以是一对一、一对多、多对多。
- 例如:
- UML:实线箭头。
2. 依赖 (Dependency)
- 含义:使用关系,“Use-a”。类A用到类B,但不是长期拥有,用完就扔。
代码体现:局部变量、方法参数。(体现为局部变量。 类A的方法中,类B作为参数传入,或者作为返回值,或者在方法内部临时new了一下。)
- PPT案例:
Dog抓Mouse。 - 代码:
public void catchMouse(Mouse m) { ... }。 - 注意:Mouse 是作为参数传进来的,Dog 并没有一个成员变量叫
myMouse。这就是依赖和关联的区别。
- PPT案例:
- UML:虚线箭头。
3. 聚集/聚合 (Aggregation)
- 含义:整体与部分,“Part-of”。台灯和灯泡。
- 特征:这是“关联”的一种特例,强调整体与部分的关系,但这个“部分”是可以独立存在的。 经典比喻:停车场和汽车。 停车场(整体)里有很多汽车(部分)。但是,如果停车场倒闭拆迁了,汽车会随之销毁吗?不会,汽车可以开走去别的地方。汽车的生命周期不由停车场决定。
- UML:空心菱形。
public class ParkingLot {
private Car[] cars;
// 考点:Car是在外面创建好,传进来的。
// 停车场没了,Car对象在外面依然活着。
public void setCars(Car[] cars) {
this.cars = cars;
}
}4. 组合 (Composition) (PPT P75提到)
- 含义:强整体与部分。
- 特征:生命周期一致。人死了,心脏也就停了。代码上通常是在构造函数里直接
new出来,外部拿不到内部部件的引用。 - UML:实心菱形。
public class Person {
private Heart heart;
public Person() {
// 考点:在内部直接new,同生共死!
// Person对象创建,Heart就创建;Person销毁,Heart也就没了引用被回收。
this.heart = new Heart();
}
}5. 泛化 (Generalization)
- 含义:继承,“Is-a”。
- 代码:
extends。 - UML:实线空心三角箭头。
6. 实现 (Realization)
- 含义:类实现接口,“Like-a”。
- 代码:
implements。 - UML:虚线空心三角箭头。
其中,聚集和组合属于关联,关联的其他情况:并非是整体-部分的关系。只要看到 new 出现在构造方法里面,那就是组合;如果看到对象是 作为参数传进来赋值给成员变量的,那基本就是聚集或一般关联(再根据语境判断是否是整体部分关系),一般关联可能是非构造方法也能修改的。
按照这个流程图:
Q1: 它是成员变量吗?(长期持有)
- No (是局部变量) $\rightarrow$ 依赖 (Dependency)
- Yes $\rightarrow$ 进入关联家族判定 (继续Q2)
Q2: 它们是“整体与部分” (Whole-Part) 的关系吗?
- No (它们是平等的,如老师和学生) $\rightarrow$ 一般关联 (Association)
- Yes $\rightarrow$ 进入Q3
Q3: 部分(Part)能否脱离整体(Whole)独立生存?
- Yes (球队解散,球员还在) $\rightarrow$ 聚集 (Aggregation)
- No (人死灯灭,心脏停止) $\rightarrow$ 组合 (Composition)
3. Java中的内存与变量
内存管理:
栈 (Stack):
- 存局部变量。
- 存基本数据类型的具体数值 (
int a = 12)。 - 存引用类型的地址 (
Student s,这个s只是一个遥控器/地址)。
堆 (Heap):
- 存new出来的对象实体。
- 所有的对象属性(成员变量)都存在堆里。
执行过程:
Student s = new Student();- 在堆中开辟空间,创建Student对象。
- 在栈中定义变量
s。 - 把堆中对象的内存地址赋值给
s。
数据类型:
- 基本类型 (8种):
byte, short, int, long, float, double, char, boolean。它们不是对象,直接存值。 - 引用类型:类、接口、数组。它们存的是地址。
默认初始值:
| 数据类型 | 默认初始值 | 备注 |
|---|---|---|
| byte | 0 | 注意是数字0 |
| short | 0 | |
| int | 0 | 高频考点 |
| long | 0L | 也是0 |
| float | 0.0f | |
| double | 0.0d | 高频考点 |
| char | '\u0000' | 这是一个空字符(NUL),打印出来看不见,对应的int值是0 |
| boolean | false | 高频考点(千万别记成true) |
所有引用类型(类、接口、数组),默认值统统是 null。
| 数据类型 | 默认初始值 | 例子 |
|---|---|---|
| String | null | String s; // s是null |
| 数组 | null | int[] arr; // arr本身是null |
| 自定义类 | null | Student s; // s是null |
Lesson 3 类的基本知识
1. 类的设计原则
- 命名规范:要有意义。类名首字母大写(
Student),变量/方法首字母小写(studentName)。 - 私有化属性 (Encapsulation):把数据藏起来。尽量用
private修饰属性,防止外部随意修改,只提供public的方法来访问。 - 初始化:变量尽量要在使用前初始化,虽然成员变量有默认值,但显式初始化是好习惯。
- 单一职责:一个类只做一件事。不要把修车的逻辑写在造车的类里。
2. 类的定义与成员变量
1. 类的定义
警告:一个 .java 源文件中可以写多个 class,但是只能有一个 public class,而且这个 public 类的名字必须和文件名完全一致。其他的类(非 public)只能在包内被访问。
类的类型说明符主要有两个:final、abstract。
final的字面意思是“最终的”、“不可更改的”。凡是被final修饰的类,绝对不能被继承。也就是说,这个类没有子类。abstract代表“抽象的”、“不具体的”。代表这个类只是一个概念或模板,它描述了一类事物的共性,但自己本身不够完整,不足以成为一个独立的对象。- 不能被实例化:你不能
new一个抽象类。 - 必须被继承:抽象类存在的唯一意义就是被继承。子类必须实现抽象类中所有的抽象方法(除非子类自己也是抽象类)。
- 不能被实例化:你不能
final和abstract是死对头,永远不可能同时修饰同一个类。
Question:抽象类和接口什么区别?
| 维度 | 抽象类 (Abstract Class) | 接口 (Interface) |
|---|---|---|
| 关键字 | extends | implements |
| 继承数量 | 只能继承一个 (单继承) | 可以实现多个 (多实现) |
| 成员变量 | 可以有各种类型的变量 (private, protected等) | 全是常量 (默认 public static final) |
| 构造函数 | 有 (用于子类初始化父类属性) | 无 (接口没有对象的概念,无法实例化) |
| 方法实现 | 可以有抽象方法,也可以有具体实现的普通方法 | 传统上只有抽象方法 (public abstract) |
| 设计目的 | 代码复用 (子类共用父类代码) | 解耦、定义标准 (你只要符合接口标准,我就能用你) |
2. 成员变量与修饰符 (PPT 8-14)
- 定义格式:
[修饰符] 类型 变量名 [= 值]; 访问控制符(后面还有):
public:全世界都能访问。private:只有本类自己能访问(封装的核心)。protected:本包 + 子类访问。默认(不写):只有本包能访问。
- static vs 实例变量:加了
static属于类(全班共享的黑板),不加属于对象(每个人自己的笔记本)。
3. this关键字:
- 定义:
this代表当前对象的引用(即“我”)。 - 核心用途(必考):解决局部变量和成员变量重名的问题。
3. 类的内存模型与参数传递
1. 基本类型 vs 引用类型
基本类型(int, double等):传递的是值的副本(Deep Copy 概念)。
- 你在方法里把参数改了,外面的变量完全不受影响。
- PPT 案例:
show(x)里把 x 改成 5,主函数里的 x 还是 4。
引用类型(对象、数组):传递的是地址的副本(Shallow Copy 概念)。
- 你在方法里通过引用修改了对象的属性(如
d.x = 6),外面的对象会受到影响,因为大家指向堆内存里的同一个房子。 - 考点陷阱:如果在方法里
d = new Demo(),这时候你改变了引用指向的地址,那就和外面断开联系了,外面的对象不会变。
- 你在方法里通过引用修改了对象的属性(如
2. 内存
(同Lesson 2)
- Stack (栈):存局部变量(方法里的变量)、对象的引用(遥控器)。
- Heap (堆):存对象实体(电视机)、成员变量。
逻辑:
Person p = new Person();p在栈里,存的是一个地址(比如 0x123)。new Person()在堆里,开辟了真正的空间。- 赋值
=就是把堆的地址交给栈里的p。
4.方法重载与构造函数
1. 方法重载 (Overloading)
- 定义:同一个类中,方法名相同,参数列表不同(个数、类型、顺序)。
- 考点:返回值类型不同不算重载。
void method()和int method()放在一起会报错。(如果参数列表不同,返回值类型什么样都不会报错) - 例子:
System.out.println()就是典型的重载,它可以接收 int, String, char 等各种类型。
2. 构造函数 (Constructor)
- 特征:方法名与类名相同,没有返回值类型(连 void 都没有)。
- 作用:对象出生时的“初始化”。
默认构造函数(高频考点):
- 如果你不写任何构造函数,Java 赠送你一个无参的空构造函数。
- 如果你写了任何一个构造函数(比如带参数的),Java 不再赠送无参构造函数。
- 后果:如果你写了
Teacher(String name),然后试图用new Teacher(),编译器会直接报错。
- 构造函数重载:可以有多个构造函数(无参、全参、部分参),方便不同的初始化需求。
5. 对象的生命周期与垃圾回收
1. 垃圾回收机制 (GC)
- 什么算垃圾:当一个对象没有任何引用指向它时(即栈里没人拿遥控器对准它了)。
- 如何回收:JVM 自动回收,程序员无法精准控制。
- 强制回收:
System.gc()或Runtime.gc()。注意,这只是给 JVM 提建议“该打扫卫生了”,JVM 不一定马上执行。 finalize():对象被回收前的一句遗言。虽然现在 Java 版本不推荐用了,但 PPT 里有,考试可能会考“对象临死前调用什么方法”。
2. 匿名对象
- 写法:
new Student().show();(没有左边的Student s =)。 - 特点:只能用一次。用完那一行代码,它就变成了垃圾,等着被回收。通常用在只调用一次方法,或者作为参数传递时。
6.变量的作用域与初始化
1. 成员变量 vs 局部变量
| 特性 | 成员变量 (Instance Variable) | 局部变量 (Local Variable) |
|---|---|---|
| 定义位置 | 类中,方法外 | 方法内或代码块内 |
| 内存位置 | 堆 (Heap) | 栈 (Stack) |
| 生命周期 | 随对象创建而生,对象消亡而死 | 方法执行时生,方法结束弹栈即死 |
| 默认值 | 有 (int=0, boolean=false, Object=null) | 无 (必须显式赋值才能使用,否则编译报错) |
2. tostring方法
- 来源:所有类都继承自
Object类,默认都有toString()。 - 默认行为:打印
类名@哈希码(内存地址的某种映射),人类看不懂。 - 重写 (Override):为了打印出人话(比如对象的属性值)。
- 自动调用:当你执行
System.out.println(obj)时,Java 自动调用obj.toString()。
Lesson 4_1 Static关键字
这一章节主要关于static关键字的。
1. 内存中的存储
内存图:
- 栈 (Stack):存局部变量、引用。
- 堆 (Heap):存
new出来的对象(实例变量)。 - 数据区/方法区 (Data/Code Area):存类的元数据、静态变量、代码本身。
核心逻辑:
- 实例变量(不加 static):存在堆里。每
new一个对象,就有一份拷贝。你有一个名字,我有一个名字,互不干扰。 - 静态变量(加 static):存在数据区里。它是类级别的,全班只有这一份,所有人共享。
使用static的原因:
- 共享数据:比如统计“一共造了多少个 Person 对象”?这个计数器不能属于某个人,必须属于“人类”这个概念。
- 独立工具:有些方法不需要依赖对象就能用,比如
Math.sqrt(),你不需要造一个Math对象就能算平方根。
2. 静态变量与静态方法
1. 静态属性(类变量)
- 定义:
static int count; - 访问方式:推荐用
类名.变量名(如Person.count)。虽然用对象名.变量名也能访问,但这会误导读者以为它是实例变量,不推荐。
2. 静态方法(类方法)
- 定义:
public static void showCount() {...} - 本质:它在类加载的时候就已经存在了,不需要
new对象就能调用。 铁律:
- 静态方法里,绝对不能访问非静态成员(变量或方法)!
- 静态方法里,绝对不能出现
this关键字!
3. 代码块的执行顺序
静态代码块 (Static Block)
- 写法:
static { ... } - 时机:类加载时执行。
- 频率:这辈子只执行一次。不管你
new了多少个对象,它只在第一次用到这个类时跑一次。 - 作用:通常用来给静态变量赋初值。
- 写法:
非静态代码块 (Instance Block)
- 写法:
{ ... }(没名字,没 static) - 时机:
new对象时,在构造函数之前执行。 - 频率:每
new一次,执行一次。
- 写法:
构造函数 (Constructor)
- 时机:
new对象时,在非静态代码块之后执行。 - 频率:每
new一次,执行一次。
- 时机:
考试口诀(执行顺序): 父类静态 -> 子类静态 -> 父类非静态 -> 父类构造 -> 子类非静态 -> 子类构造
为什么 main 方法是 static 的?
public static void main(String[] args)- 因为
main是程序的入口。在程序开始跑的时候,还没有任何对象被创建出来。JVM 必须能通过类名.main()直接把程序拉起来。如果它不是 static,JVM 就得先new一个主类对象,那由谁来new呢?这就陷入死循环了。
4. 静态内部类
static class(静态内部类)。
- 这点只要记住:只有内部类才能被
static修饰,普通的类(Top-level class)不能是 static 的。 - 静态内部类不需要依赖外部类的对象就能创建(因为它跟外部类的类绑定,不跟对象绑定)。
Lesson 4_2 封装
1. 封装的概念
封装不仅仅是为了“藏”,更是为了“控”。它保证了数据的安全性、一致性,并且让代码更易维护
1. 封装的本质:隐藏内部实现细节,暴露接口
PPT里用了一个很好的例子:台式机箱。
- 内部: CPU、显卡、主板杂乱地连在一起,电流裸露。如果散落在外面,既不安全(容易被泼水、短路),也不易用(你不可能每次开机都去短接主板引脚)。
- 机箱(封装层): 把复杂的内部元件锁起来,只给你留出开机键、USB口。
- 考点理解: 封装就是把“数据”(部件)和“处理数据的方法”(电路逻辑)绑定在一起,隐藏内部实现细节,只对外提供受控的访问方式(接口)。
2. 封装分为三个层次
封装不仅仅是类级别的,它是系统级的概念:
- 系统级: 系统分模块,模块分拆子模块,通过接口交互。
- 包(Package)级: 包与包之间通过 public 类交互,包内的类可以对外隐藏。
- 类(Class)级: 这是我们代码最常写的,属性私有化,方法公开化。
2. 访问控制修饰符
Java有四种权限:
| 修饰符 | 同一个类 | 同一个包 | 子类 (不同包) | 全局 (不同包非子类) |
|---|---|---|---|---|
| private | ✅ | ❌ | ❌ | ❌ |
| (default) | ✅ | ✅ | ❌ | ❌ |
| protected | ✅ | ✅ | ✅ (有条件) | ❌ |
| public | ✅ | ✅ | ✅ | ✅ |
- private:只能在同个类访问
- default:你什么都不写,就是 default。它具有包访问权限。但不在一个包的子类也不能访问。
protected:
- 同包内,随便访问(同 default)。
子类在另一个包里,可以访问父类的 protected 成员。但是! 只能在子类内部,通过“继承下来的身份”去访问。
- 在不同包的子类
Child中,你可以调用super.protectedMethod()或者this.protectedMethod()。 - 但是,如果你在
Child类里new Parent(),你是不能通过parentObj.protectedMethod()来访问的。因为此时你对于Parent对象来说,只是一个外部使用者,而不是继承关系。 - 总结: 跨包时,protected 是给“儿子继承用的”,不是给“儿子创建父类对象用的”。
- 在不同包的子类
3. 单例模式
保证一个类在内存中只有唯一一个实例(比如配置文件读取器、日志管理器)。
实现三板斧:
- 构造方法私有化 (
privateConstructor):外界不能new了。 - 内部持有静态实例 (
private staticVariable):自己持有自己。 - 提供静态公有方法 (
public staticMethod):外界获取实例的唯一入口。
两种写法:
1. 饿汉式 (Hungry Mode) —— 推荐,简单粗暴
public class Singleton {
// 类加载时直接new出来,"我很饿,先吃为敬"
private static Singleton instance = new Singleton();
private Singleton() {} // 封死构造口
public static Singleton getInstance() {
return instance;
}
}- 优缺点: 线程安全,调用效率高;但如果从来没用过这个类,会浪费一点点内存。
2. 懒汉式 (Lazy Mode)
public class Singleton {
private static Singleton instance = null; // 先不急着new
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 等有人要用了,再new,线程不安全
instance = new Singleton();
}
return instance;
}
}- 优缺点: 节省内存(延迟加载)。
- 重大考点: PPT明确指出,这种写法线程不安全(多线程同时进入 if 判断会 new 出多个对象)。虽然PPT没要求写出双重检查锁(DCL),但你必须知道它是不安全的。
4. 高内聚,低耦合
封装的好:
- 高内聚: 类的内部功能紧密相关,自己能干的事自己干完。
- 低耦合: 外部只通过最少的接口与你交互。
Lesson 5 继承
举例:我们要把公用的特征(属性如age, weight)和行为(方法如eat, sleep)抽取出来,放在一个基类(父类/SuperClass)里,比如Animal。然后让具体的动物(子类/SubClass)去继承它。
- 继承是为了代码复用、易于维护、易于扩展。
- 它建立了类与类之间的层次结构。
1. Java的继承规则
1. 单继承(Single Inheritance)
- 规则:Java中的类只能有一个直接父类。你不能写
class Son extends Father, Mother。 - 对比:C++支持多继承,Java不支持类的多继承,但支持接口(Interface)的多继承(这是后话,但要记住区别)。
- 原因:避免“菱形继承”问题(如果父母都有
eat()方法,儿子调用谁的?),简化语言复杂度。
2. 关键字 extends
- 语法:
class 子类 extends 父类。 - 如果你不写extends,Java默认让所有类都继承自
java.lang.Object。它是所有类的祖宗。
3. 子类继承了什么? 这是一个经典的判断题考点:
- 继承了:所有的属性和方法(包括私有的!)。
- 但是(注意):虽然继承了父类的
private成员,但子类不能直接访问。必须通过父类的public/protected方法(如getter/setter)去操作它们。就像你继承了你父亲的保险柜,这柜子是你的了,但你没有密码打不开,得用你父亲留下的钥匙。 - 没继承:构造方法(Constructor)。子类不能继承父类的构造方法,只能调用。(
super())
2. 构造方法
1. 初始化的顺序 当你 new 一个子类对象时,内存里发生的事情是这样的:
- 先父后子:必须先在这个对象中把“父类部分”初始化完毕,才能初始化“子类部分”。这就好比盖房子,必须先打好地基(父类),才能盖二楼(子类)。
2. super 关键字在构造器中的用法
- 隐式调用:如果你的子类构造器里没写
super(...),编译器会自动在第一行塞入一个super()——即调用父类的无参构造方法。 - 显式调用:如果父类没有无参构造方法(比如父类只定义了一个带参数的构造器
public Animal(String name)),那么子类必须在构造器的第一行显式地调用super(name)。否则编译报错。
因为构造方法里面也可以调用自己的其他构造方法,有this关键字:
super()或this()必须放在构造方法的第一行。- 因此,一个构造器里不能同时出现
super()和this()(因为都要争第一)。
3. 覆盖与隐藏
1. 变量隐藏 (Field Hiding)
- 如果在子类里定义了一个和父类同名的成员变量。
- 结果:父类的变量被“隐藏”了,但没有消失。
- 访问:在子类中,用
this.变量访问子类的,用super.变量访问父类的。 - 考点:变量没有多态性!
Parent p = new Child();
System.out.println(p.x); // 输出的是 Parent 的 x,不是 Child 的 x记住:看左边。引用类型是谁,就访问谁的变量。
2. 方法覆盖 (Method Overriding) (or 重写)
- 定义:子类重写父类的方法,名称、参数列表、返回类型必须完全一致。
- 目的:修改父类的行为。比如
Animal的sleep是站着睡,Lion重写成躺着睡。 规则:
- 权限不能更低(父类public,子类不能变成private)。
- 不能抛出比父类更多或更宽泛的异常。
final方法不能被覆盖(编译报错)。static方法不能被覆盖(没有多态性,所以是隐藏)。
- 考点:方法具有多态性!
Parent p = new Child();
p.eat(); // 此时运行的是 Child 的 eat() 方法!记住:看右边。实际对象是谁,就调用谁的方法(动态绑定)。
4. 向上转型
举例:
- 操作:
Animal a = new Lion(); - 逻辑:把子类对象当做父类对象来看待。这是安全的(狮子肯定是动物)。
- 代价:丢失具体特性。一旦转型为
Animal,你就不能调用Lion特有的方法(比如钻火圈())了,除非你再强制转回去(向下转型)。
口诀:“编译看左边,运行(多态)看右边(,运行非多态看左边)”
调用重写(Override)的方法 —— 看右边
这是多态的核心!
- 规则:如果父类有一个方法
eat(),子类重写了eat()。 分析:
- 编译阶段(看左边):编译器检查
Animal类里有没有eat()方法?有,编译通过。 - 运行阶段(看右边):JVM 发现
a指向的实际内存对象是Lion,所以会动态绑定到Lion的eat()方法。
- 编译阶段(看左边):编译器检查
- 结果:执行 子类(Lion) 的方法。
- 规则:如果父类有一个方法
调用隐藏的变量(同名成员变量) —— 看左边
这是一个巨大的陷阱!请打三个星号。Java中,变量没有多态性!
- 规则:父类有
String name = "Animal", 子类也有String name = "Lion"。 分析:
- 变量的绑定是静态的,在编译时就确定了。
- 编译器只看
a是什么类型?是Animal。好,那就取Animal的name。
- 结果:获取 父类(Animal) 的变量值。
- 规则:父类有
调用子类独有的方法 —— 编译报错
- 规则:子类有
drillHole()(钻火圈),父类没有。 分析:
- 编译阶段(看左边):编译器去查
Animal类,发现根本没有drillHole()这个方法。 - 编译器会直接报错:
Cannot find symbol。它不管你右边是不是狮子,它只知道你现在的身份是“动物”。
- 编译阶段(看左边):编译器去查
- 结果:编译不通过。
- 解决:必须先向下转型(强制类型转换):
((Lion)a).drillHole();。
- 规则:子类有
调用子类独有的变量 —— 编译报错
- 规则:子类有
furColor,父类没有。 - 分析:同上。编译器看左边
Animal类里没有这个属性。 - 结果:编译不通过。
- 规则:子类有
调用静态方法(Static Method) —— 看左边
这是另一个陷阱。如果父类和子类都有同名的
static方法,这不叫重写,叫隐藏。- 规则:父类有
static void test(),子类也有static void test()。 - 分析:静态方法属于类,不属于对象。它不参与多态的动态绑定。
- 编译器看引用类型是
Animal,就直接绑定到Animal.test()。 - 结果:执行 父类(Animal) 的静态方法。
- 规则:父类有
调用父类独有的方法 —— 看左边
- 规则:父类有
breathe(),子类没有重写,也没有删掉(当然也不能删)。 - 分析:这属于继承的基本功能。子类继承了父类的方法。
- 结果:执行 父类 的方法(因为子类直接继承过来了,实际上也是子类的方法)。
- 规则:父类有
| 成员类型 | 检查规则 | 最终执行/访问是谁的? | 备注 |
|---|---|---|---|
| 普通实例方法 | 编译看左,运行看右 | 子类 (Lion) | 这是唯一体现多态的地方! |
| 成员变量 | 编译看左,运行看左 | 父类 (Animal) | 变量没有多态! |
| 静态方法 | 编译看左,运行看左 | 父类 (Animal) | 静态方法不看对象! |
| 子类独有方法/变量 | 编译看左 | 编译报错 | 父类里找不到,必须强转 |
5. 继承与组合对比
PPT最后对比了继承与组合(Composition)。
1. Is-a (继承)
- Lion is an Animal.
- 优点:代码复用,多态支持。
- 缺点:强耦合。父类变,子类必须变。破坏封装(子类能看到父类protected的内容)。
2. Has-a (组合/聚合)
- Car has an Engine. (不要让Car继承Engine)
- Student has a Book.
- 实现:在类里面定义另一个类的对象作为成员变量。
- 黄金法则:优先使用组合,而非继承 (Favor Composition over Inheritance)。 除非你真的需要向上转型(多态),否则尽量用组合。
Lesson 6 多态
举例:
原始写法(没有多态):
如果你写一个
Master类,给猫看病写一个cure(Cat c),给狗看病写一个cure(Dog d)。问题: 如果明天我养了一只草泥马,你是不是得修改
Master类的代码,加一个cure(Alpaca a)?结论: 这叫代码耦合度高,修改不仅麻烦,还容易改崩原来的代码。
多态写法(优化后):
我们定义一个父类
Pet,让Cat、Dog都继承它。 在Master类里,我只写一个方法:cure(Pet p)。原理: 无论传进去的是Cat还是Dog,只要是Pet的子类,这个方法都能接收。
好处: 以后再加草泥马、大象,
Master类的代码一行都不用改。这就叫可扩展性(Open-Closed Principle,开闭原则)。
1. 多态的两种分类
静多态(编译时多态)
- 关键词: 方法重载(Overloading)。
- 机制: 也就是同一个类里,方法名一样,参数列表(类型、个数、顺序)不一样。
- 为什么叫“静”? 因为编译器在编译代码的时候(还没运行),看到你传入的参数类型,就已经确定了你要调用哪一个方法。比如
add(int, int)和add(float, float),写代码时就定死了。这叫前期绑定(Early Binding)。
动多态(运行时多态)—— 这是本章的核心!
- 关键词: 方法覆盖(Overriding)。
- 机制: 父类有一个
eat(),子类重写了eat()。 - 为什么叫“动”? 因为编译器只知道你调用的是
Pet.eat(),但具体是狗吃骨头还是猫吃鱼,只有程序运行起来,真正传入了具体的对象(new Dog 还是 new Cat)时才能确定。这叫后期绑定(Late Binding)。
2. 动多态的实现
三大要素:
- 继承:必须有父子类关系(或者接口实现)。
- 覆盖(Override):子类必须重写父类的方法。
向上转型:父类的引用指向子类的对象。
- 代码长这样:
Father f = new Son();
- 代码长这样:
核心:
当 Father f = new Son(); 时:
- 编译看左边: 能不能调用某个方法,看父类
Father里有没有定义这个方法。如果没有,编译报错。 - 运行看右边: 真正执行的时候,执行的是子类
Son里重写后的逻辑。
(上面Lesson 5里详细阐述了这一点,所以能看出来只有类里面的非静态方法具有多态性)
3. 抽象类
- 痛点: 比如
Animal父类有一个cry()方法。但是Animal只是一个概念,它怎么叫?没法写方法体。写空方法体也不合适。 解决: 使用
abstract修饰方法,没有方法体,直接分号结束。public abstract void cry();
抽象类的使用规则:
- 如果有抽象方法,类必须是抽象类。
- 抽象类不能被实例化(不能
new Animal()),它只能用来当爹(被继承)或者当引用类型(Animal a = ...)。 - 子类继承抽象类,必须重写所有的抽象方法,除非子类自己也是个抽象类。
- 抽象方法 不能被 private 、 final 或 static 修饰
4. 部分总结
(1)方法覆盖(Override)的“三一原则”
子类重写父类方法时:
- 名称、参数列表必须完全一致。
- 返回值:必须兼容(如果是基本类型必须一样,如果是引用类型,子类返回类型可以是父类返回类型的子类)。
访问权限:子类不能比父类更“封闭”。
- 父类是
public,子类必须是public。 - 父类是
protected,子类可以是protected或public。 - 比喻:老爸都很开明,儿子不能太内向。
- 父类是
- 异常:子类抛出的异常不能比父类更宽泛(只能抛出父类异常的子类或不抛出)。
(2)“隐藏”与“覆盖”的区别
多态只对普通实例方法有效。以下三种情况不发生多态(即:不看右边,只看左边引用类型):
- static 方法:静态方法是属于类的,编译时就绑定了。
Father f = new Son(); f.staticMethod();调用的永远是Father的静态方法。这叫方法隐藏,不是覆盖。 - private 方法:私有方法子类看不见,无法覆盖。
- 成员变量(字段):属性没有多态!
f.name访问的是Father里的name,不管右边new的是谁。
(3)构造方法
- 构造方法不能被继承,因此也不能被重写(Override),虽然可以被重载(Overload)。
Lesson 7_1 接口
- 关键字:
interface。 - 它是一个比抽象类更抽象的存在。抽象类里还可以有点普通方法,接口里(在Java 8之前)只有抽象方法和常量。
举例:
概念层面的接口:你的电脑上有一个USB口。
- 电脑(系统)不需要知道你插的是鼠标、键盘还是打印机。
- 电脑只关心:你这个插头是不是USB标准的。
- 结论:接口就是一种标准,一种契约,描述了“系统对外能提供什么服务”,而不关心“具体怎么实现的”。
1. 接口的语法规则
- 定义:用
interface代替class。 成员变量:接口里没有变量,只有常量!
- 你写
int a = 1; - 编译器自动补全为
public static final int a = 1;(全局静态常量)。
- 你写
方法:接口里全是抽象方法(没有方法体)。
- 你写
void run(); - 编译器自动补全为
public abstract void run(); - 注意:考试时问你访问权限,永远是 public。
- 你写
- 构造方法:接口没有构造方法(因为不能 new 接口)。
实现:用
implements关键字。- 一个类可以实现多个接口(
implements A, B, C)。 - 铁律:如果你实现了接口,就必须重写里面所有的方法,否则你这个类就必须变成抽象类。
- 一个类可以实现多个接口(
2. 为什么要用接口
PPT第25页举了 Person、Runner、Swimmer 的例子。
痛点:Java规定一个类只能有一个亲爹(单继承)。
- 如果
Runner是类,Swimmer是类,Animal也是类。 Person想同时继承这三个,Java说:不行,你只能选一个爹。
- 如果
解法:接口代表一种“能力”(Capability),或者说像是一个“资格证”。
Person extends Animal(亲爹是动物)。implements Runner, Swimmer(考取了跑步证,考取了游泳证)。- 结论:接口实现了多重继承的效果。
3. 接口回调(接口多态)
原理:
- 我定义一个接口
Runner,里面有个run()。 Person实现了Runner。- 代码这么写:
Runner r = new Person(); // 接口引用指向实现类的对象
r.run(); // 实际上调用的是 Person 的 run()这就叫接口回调。
应用场景(PPT里的SoundMaker案例):
- 你写一个方法
playSound(SoundMaker s)。 - 你可以传
Radio进去,也可以传Phone进去。 - 只要它们都实现了
SoundMaker接口,程序就能跑。这就是面向接口编程。
4. 接口与抽象类对比
| 特性 | 抽象类 (Abstract Class) | 接口 (Interface) |
|---|---|---|
| 关系 | IS-A (它是...) | HAS-A / CAN-DO (它能...) |
| 本质 | 家族血统(亲爹) | 契约、标准、能力(资格证) |
| 继承限制 | 只能继承一个 (extends) | 可以实现多个 (implements) |
| 成员变量 | 各种类型都可以 | 只能是 public static final 常量 |
| 构造方法 | 有 (给子类用) | 无 |
| 方法 | 可以有普通方法,也可以有抽象方法 | 全是抽象方法 (Java 8前) |
另外的知识:
- Native关键字:看到
native修饰的方法,知道它是用 C/C++ 写的底层代码就行了(比如操作硬件的),Java 只是调用它。这通常是判断题的一个选项。
Lesson 7_2 复合和双向关联
1.继承 vs 复合(Is-a vs Has-a)
(此处复合 == Lesson 2写的组合)
继承 (Inheritance) —— "Is-a" 关系
- 定义:子类继承父类,是“是一个”的关系。
- 例子:
Student extends Person。学生 是 人。 - 特点:耦合度很高。老爸有的(非私有),儿子都有。这是一种很强的绑定。
复合/包容 (Composite/Aggregation) —— "Has-a" 关系
- 定义:一个类把另一个类的对象当作自己的成员变量。是“有一个”的关系。
例子:PPT里的电脑(PC)。
PC不是CPU,也不是HardDisk。- 但是
PC里有CPU,里有HardDisk。
class CPU { ... } class HardDisk { ... } class PC { // 这就叫复合(Has-a) private CPU cpu; private HardDisk hd; public void setCPU(CPU c) { this.cpu = c; } }
- 考试口诀:优先使用复合,少用继承。(这是软件工程原则,复合更灵活)。
- 如果两个类仅仅是想借用代码,而不是真正的血缘关系,千万别用继承。
2. 双向关联
PPT用了“学生选课(Student & Subject)”这个案例,演示了代码是如何一步步进化的。这是本章的重难点。
场景设定
- Student:学生类(比如张三)。
- Subject:学科/专业类(比如计算机系)。
第一阶段:单向关联(One-way)
- 逻辑:只在
Student类里写一个Subject类型的变量。 表现:
- 张三知道自己在计算机系(
zhangsan.getSubject()能查到)。 - 但是,计算机系不知道自己有哪些学生!
- 张三知道自己在计算机系(
- 缺点:信息不对称,我要想查“计算机系所有学生”,得遍历全校学生一个个问,效率太低。
第二阶段:双向关联(Two-way)—— "互相拥有"
逻辑:
Student里有Subject。Subject里有一个Student[]数组(或者 List)。
- 表现:张三知道自己在哪个系,系里也有一份名单记录了张三。
但是,上面的写法需要添加关系两次(两个类各一次),容易出现数据不一致问题。所以给出了另外一种处理方案:
目标:我只要设置一边,另一边自动同步。 比如,我只要执行 zhangsan.setSubject(csDept),系统自动帮我在 csDept 的名单里加上张三。
在 Student 类的 setSubject 方法里,不能只做简单的赋值,要多做两件事:
- 清理旧关系:如果张三原来是物理系的,得先从物理系名单里把他划掉。
- 建立新关系:把张三加到新系(计算机系)的名单里。
参考代码:
// 在 Student 类中
public void setSubject(Subject newSubject) {
// 1. 如果新系和旧系一样,啥也不用做,省得死循环
if (this.subject == newSubject) return;
// 2. 如果原来有系(比如从物理转系),先从旧系的名单里把自己删掉
if (this.subject != null) {
this.subject.removeStudent(this);
}
// 3. 正常赋值
this.subject = newSubject;
// 4. 【关键一步】在新系的名单里,把自己加上
// 这里的 this 就是当前的 Student 对象
if (newSubject != null) {
newSubject.addStudent(this);
}
}// 在 Subject 类中
public void addStudent(Student s) {
// 1. 把学生加入数组/集合
this.students.add(s);
// 2. 【回马枪】确保学生那边也指向了这个系
// 如果学生现在的系不是我,我得让他改成我
if (s.getSubject() != this) {
s.setSubject(this);
}
}



1 条评论