Java 第四周学习笔记:多态、接口、内部类与 String
第四周(P114--132):从面向对象的三大支柱收尾,走到 Java 最常用的 API------String。 这篇按知识点梳理,末尾附上我自己周复盘测出来的 8 个"假会"(以为会了其实不会)。
一、多态:编译看左边,运行看右边
1.1 三个前提
多态不是随便就能用的,必须同时满足三条:
- 有继承或实现关系
- 有方法重写
- 父类引用指向子类对象
java
ini
Animal a = new Cat(); // ← 第 3 条
记法:有爹、有重写、爹指儿。三条缺一不可。
1.2 调用规则
java
scss
Animal a = new Cat();
a.eat(); // ✅ 编译过,运行调 Cat 的 eat
a.catchMouse(); // ❌ 编译不过
编译看左边 (Animal 里有没有这个方法),运行看右边 (实际执行 Cat 的重写版本)。
catchMouse() 编译不过,是因为 Animal 里根本没声明这个方法------编译期只认左边那个类型。
1.3 好处与坏处
好处:一个方法能接住一整族对象。
java
javascript
// 不用多态:来一种新动物就得新写一个方法
void feedCat(Cat c) { c.eat(); }
void feedDog(Dog d) { d.eat(); }
// 用多态:一个方法通吃,新动物直接传进来
void feed(Animal a) { a.eat(); }
坏处 :父类引用调不了子类特有的方法 (就是上面 catchMouse() 那个问题)。
解决 :向下转型。
java
ini
Cat c = (Cat) a; // 向下转型(手动、有风险)
c.catchMouse(); // ✅ 现在能调了
| 方向 | 写法 | 安全性 |
|---|---|---|
| 向上转型 | Animal a = new Cat(); |
自动,安全 |
| 向下转型 | Cat c = (Cat) a; |
手动,类型不对会运行时报错 |
二、抽象类与接口
2.1 abstract 能修饰什么
类和 方法。
java
csharp
public abstract class Animal { // 抽象类
public abstract void eat(); // 抽象方法
public void sleep() { // 普通方法也可以有
System.out.println("睡觉");
}
}
抽象方法的特点:
- 没有方法体 (
eat();而不是eat() { }) - 必须待在抽象类里
- 子类必须重写(除非子类也声明为
abstract)
单向规则:有抽象方法 → 必须是抽象类;但抽象类不一定要有抽象方法。
2.2 子类不想实现怎么办
子类也声明为 abstract。
java
scala
public abstract class Cat extends Animal {
// 不实现 eat(),那就继续挂着
}
2.3 什么时候用继承,什么时候用接口
口诀:is-a 用继承,can-do 用接口。
| 继承 | 接口 | |
|---|---|---|
| 表达 | is-a(是什么) | can-do(能做什么) |
| 例子 | Cat extends Animal------猫是动物 |
Bird implements Flyable------鸟能飞 |
| 数量 | 只能 1 个(单继承) | 可以多个(多实现) |
判断步骤:
- 问「B 是 A 吗?」→ 是 → 继承
- 问「B 能做 A 这件事吗?」→ 是 → 接口
- 两者都成立 → 继承 + 接口一起用
典型例子 :BasketballPlayer 既是运动员(is-a,继承 Athlete),又能说英语(can-do,实现接口)。
2.4 JDK8 为什么给接口加 default 方法
为了向后兼容。
JDK8 之前,接口一旦发布就不能再改------加一个方法,所有实现类都得跟着改,否则编译不过。default 方法允许接口提供默认实现,老代码不受影响。
2.5 abstract 与 final 冲突
java
java
public abstract final class A // ❌ 编译报错
abstract 要求子类继承并重写,final 禁止继承------两者需求相反。
三、内部类
- 成员内部类:定义在类的成员位置
- 静态内部类 :加
static - 局部内部类:定义在方法里
- 匿名内部类 :没有名字的类对象------一次性地创建子类/实现类的实例
匿名内部类的要点 :它本质是对象,那个"类"只是顺带定义的,用完即弃。
四、String:不可变带来的连锁反应
4.1 不可变意味着什么
String 是 final 类,不可变(immutable) ------一旦创建,内容永远不变。
这带来一个最容易踩的坑:
java
arduino
String s = "hello";
s.replace('l', 'L'); // ❌ 这行等于白干
System.out.println(s); // 还是 hello
为什么? 因为 replace 不会改原串 ,而是造一个新的还给你。你没接住,新串就被丢弃了。
正确写法:
java
ini
s = s.replace('l', 'L'); // ✅ 必须赋值回去
铁律 :String 的所有"修改"方法都不改自己,只返回新串------
java
ini
s = s.substring(1);
s = s.trim();
s = s.replace("a", "b");
s = s.toUpperCase();
判断技巧 :方法名像"动词"(改、切、换、去),前面就要有 s =。
再强调一次 := 才是接住,return 是交出去 。return s 交出去的还是老的 s,它没有"接住"的能力。
4.2 == 和 equals
java
javascript
"abc" == "abc" // true
new String("abc") == "abc" // false
两次比的都是地址 ,区别在地址是否相同:
"abc"是字面量,存在常量池 ,写两次指的是同一个对象 → 地址相同 →truenew String("abc")强制在堆里新建 → 地址不同 →false
比内容要用 equals()。
4.3 常用方法
| 方法 | 说明 |
|---|---|
substring(begin, end) |
截取,含头不含尾 (substring(2,5) 取 2、3、4) |
substring(begin) |
从 begin 截到末尾 |
replace(旧, 新) |
替换,返回新 String(有两个重载:char / String) |
split(分隔符) |
按分隔符切成数组 |
trim() |
只剪两端空格,中间不动 |
charAt(i) |
取指定位置的字符 |
split 的坑:
java
bash
"192.168.1.1".split(".") // ❌ 返回空数组!"." 在正则里是"任意字符"
"192.168.1.1".split("\.") // ✅ 要转义
"a,b,,c".split(",") // {"a","b","","c"} 长度 4,空段保留
"a,b,,".split(",") // {"a","b"} 长度 2,尾部空串默认被丢
4.4 StringBuilder
为什么需要它 :String 不可变,循环拼接会产生大量垃圾对象。
java
ini
String s = "";
for (int i = 0; i < 1000; i++) {
s += i; // ❌ 每次循环都 new 一个新 String,造 1000 个垃圾对象
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(i); // ✅ 在同一个对象上改
}
要点:
-
内部是
char[]数组 -
append()返回this(自己),所以可以链式调用 :sb.append("a").append("b")- 判断依据:看到方法能链式调用,就知道它返回的是自己
-
reverse()是 StringBuilder 特有,String 没有(因为改不了内容) -
互转:
new StringBuilder(str)/sb.toString()
五、我的 8 个"假会"(周复盘测出来的)
回顾这周,闭卷自测暴露的问题比想象中多。所谓"假会"就是:以为自己会了,一测才发现不会。
① 多态三前提,我只说得出 1 条
只记得"有继承关系",漏了方法重写 和父类引用指向子类对象。
② 多态的好处坏处,会用不会说
代码写得对(编译/运行规则全对),但让我用术语描述"多态有什么用"就卡住了。
③ is-a / can-do 口诀会背,换个问法接不上
"抽象类 vs 接口的区别"这道题,我答得含糊------明明背过口诀,却没跟这个问法对上。
④ abstract 只能修饰方法?
错,能修饰类和 方法。
⑤ 抽象方法带方法体(第 12 题答错)
java
csharp
abstract void f() { } // ❌ 有花括号 = 有方法体,报错
abstract void f(); // ✅ 分号结尾
⑥ 匿名内部类是"没有名字的类"?
不准确 。应该是没有名字的类对象------本质是对象,类只是顺带定义的。
⑦ "abc" == "abc" 结果对,理由说反了
我答"比较的是数值",错 。两次比的都是地址,区别在地址是否相同(常量池复用 vs new 强制新建)。
⑧ 最严重的:以为 return 能"接住"
java
arduino
String s = "hello";
s.replace('l', 'L');
return s; // 我以为 s 已经更新了
这个错误我犯了三次:学的时候、当天快检、周复盘自测。
根因是没建立正确的心智模型:
代码块
方法返回新值 → 必须用 = 把它绑到变量上 → 变量才更新
return 是交出去,= 才是接住。
六、一条认知突破
循环体内对数组做原地修改时,下一轮看到的不再是原数组。
以 "abc" 打乱为例:
| 轮次 | 我以为在操作的 | 实际在操作的 |
|---|---|---|
| i=0 | [a,b,c] |
[a,b,c] ← 只有第一轮是原值 |
| i=1 | [a,b,c] ❌ |
[c,b,a] ✅ |
| i=2 | [a,b,c] ❌ |
[b,c,a] ✅ |
推广 :冒泡排序、插入排序、双指针交换、快排分区、数组去重全是这个道理 。分析循环要跟着状态走,不能盯着初始值。
小结
这一周最大的收获不是"学会了 String 有哪些方法",而是发现"以为自己会"和"真的会"之间有多大差距。
8 个假会里,最扎心的是第 ⑧ 条------同一个错误犯了三次。但换个角度想:正因为周复盘把它挖出来了,它现在才真的被记住了。
知识不是看会的,是测出来的。
本系列按周更新,记录 Java 后端学习路线上的每个阶段。