接口与常用集合:标准与工具箱
这是什么:接口(Interface) = 只定标准、不做实现的「规矩」;集合(Collection) = 装一堆数据的「家当」,本章主角是 ArrayList 和 HashMap。
为什么用:上一章继承与多态学了 extends,但实际工作里天天见的是接口和集合:一个管「规矩」,一个管「家当」,合起来就是 Java 的内置工具箱,工作日的代码几乎每三行就有一行在跟它们打交道。
如何用:本章先用一个接口定下标准,再学 ArrayList 排队和 HashMap 查值,最后解决「数组、ArrayList、HashMap 怎么选」。
接口:只定标准,不做实现
interface 是一组"必须会做什么"的约定,方法只有签名没有方法体。类用 implements 签下合同,就必须把每个方法都实现出来,否则编译不过。
好处是什么?使用者只认标准,不认具体类——谁签了合同,谁就能往里塞,这正是多态的另一种玩法。
生活里最好的类比是插座:国家标准规定了"两孔、220V、间距多少",它不管你插的是台灯还是手机充电器——任何电器遵守标准就能用。接口就是代码界的插座标准,Flyable(会飞)就是一个标准,鸟签了合同能飞,往后写个飞机、超人签了合同照样能飞。
接口的几条家规,一次记牢:
| 规则 | 说明 |
|---|---|
| 接口方法默认公开抽象 | 不用写修饰符,实现类必须加 public |
| 实现类必须实现全部方法 | 漏一个编译不过,合同没有"部分履约" |
一个类可以 implements 多个接口 |
和 extends 单继承互补:鸟可以既"会飞"又"会游泳" |
| 接口之间也能 extends | 标准也能合并成更大的标准 |
"可实现多个"这一条,正好补上了继承单传的遗憾——继承表达"是什么"(is-a),接口表达"能做什么"(can-do),两套机制各管一头。此外,接口里还能写带默认实现的方法(default 方法),给签合同方提供"参考答案",不想改的实现类直接沿用——见到能认出即可。
接口式多态完整体验一把——用标准当类型:
interface Flyable {
void fly();
}
class Bird implements Flyable {
@Override
public void fly() {
System.out.println("小鸟扑棱棱飞起来了");
}
}
class Plane implements Flyable {
@Override
public void fly() {
System.out.println("飞机呼啸着冲上云霄");
}
}
public class Airport {
public static void main(String[] args) {
Flyable[] hangar = { new Bird(), new Plane() };
for (Flyable f : hangar) {
f.fly(); // 只认标准:是鸟就扑棱,是飞机就呼啸
}
}
}
输出:
小鸟扑棱棱飞起来了
飞机呼啸着冲上云霄
Flyable[] hangar 里鸟和飞机混装——引用类型是接口,装的是任何实现类。以后新增热气球,只要签 Flyable 合同,机库循环一行不改。"面向接口编程"六个字,指的就是这种写法:变量、参数、容器尽量用接口类型声明,具体类随时可换。
集合:会伸缩的容器
数组长度固定,装不下想加的元素;ArrayList 能随时增删,是日常最常用的"万用瓶"。HashMap 是键值对(key → value),按 key 查 value,就像查字典。
尖括号 <String> 叫泛型:声明"这个瓶子里只装 String"。取出来不用强转,塞错类型编译器当场拦截——它让容器从"什么都能塞的麻袋"变成"专物专放的格子"。
ArrayList:排队站好的一列
常用方法一网打尽:
| 方法 | 作用 | 返回 |
|---|---|---|
add(x) |
末尾添加 | void |
get(i) |
按下标取 | 对应元素 |
set(i, x) |
替换下标 i 的元素 | 旧元素 |
remove(i) |
删除下标 i 的元素 | 被删的元素 |
size() |
元素个数 | int |
contains(x) |
是否包含 | boolean |
import java.util.ArrayList;
public class ListDemo {
public static void main(String[] args) {
ArrayList<String> queue = new ArrayList<>();
queue.add("青蛙A");
queue.add("青蛙B");
queue.add("青蛙C");
queue.remove(1); // 青蛙B 插队失败被请走
queue.set(1, "青蛙D"); // 把 C 换成 D
for (int i = 0; i < queue.size(); i++) {
System.out.println("第 " + (i + 1) + " 位:" + queue.get(i));
}
System.out.println("包含青蛙A吗:" + queue.contains("青蛙A"));
}
}
输出:
第 1 位:青蛙A
第 2 位:青蛙D
包含青蛙A吗:true
遍历 ArrayList 两板斧:要下标用普通 for(注意上界是 size(),还是 < 不是 <=),只取值用增强 for——和数组完全同款,学一次管两个容器。
HashMap:按键查值的字典
| 方法 | 作用 | 备注 |
|---|---|---|
put(k, v) |
存入一对 | key 相同则覆盖旧值 |
get(k) |
按 key 取 value | key 不存在返回 null |
remove(k) |
删掉一对 | 按 key 删 |
containsKey(k) |
是否有这个 key | 查有没有比 get 更安全 |
size() |
有几对 | |
keySet() |
全部 key 的集合 | 遍历 Map 的入口 |
import java.util.HashMap;
public class MapDemo {
public static void main(String[] args) {
HashMap<String, Integer> price = new HashMap<>();
price.put("苹果", 5);
price.put("香蕉", 3);
price.put("苹果", 6); // key 重复:覆盖,苹果变 6 元
for (String name : price.keySet()) {
System.out.println(name + ":" + price.get(name) + " 元");
}
System.out.println("樱桃有货吗:" + price.containsKey("樱桃"));
System.out.println("樱桃价格:" + price.get("樱桃"));
}
}
输出:
香蕉:3
苹果:6
樱桃有货吗:false
樱桃价格:null
两个细节划重点。其一,Map 无序:打印顺序和放入顺序无关(示例里香蕉排到了苹果前面),别依赖顺序,要有序的容器是另一套家族。其二,get 不存在的 key 返回 null 而不报错——拿着 null 去拼接或计算,就会埋下"安静的 bug",所以"查有没有"用 containsKey,比直接 get 后撞 null 稳妥。
数组、ArrayList、HashMap 怎么选
| 需求 | 选谁 | 理由 |
|---|---|---|
| 数量固定、按下标读写 | 数组 | 最轻量,语法直接 |
| 数量会变、按顺序排队 | ArrayList | 自动伸缩,增删顺手 |
| 按名字查东西、一对一登记 | HashMap | 按 key 秒查,不用遍历 |
❓ 常见问题
Q1:List 和 ArrayList 到底什么关系?声明用哪个?
List 是接口(定标准:能 add、get、size),ArrayList 是实现类(真干活)。List<String> list = new ArrayList<>(); 正是"接口引用指向实现类对象"——和上一章 Animal a = new Cat(...) 是同一个思想。推荐这种写法:哪天想换实现类,只改 new 后面一处。
Q2:HashMap 的 key 可以重复吗?
不可以。key 是"字典里的词条",唯一才有查的意义;重复 put 同一个 key 是"修改"而不是"新增",新值覆盖旧值。value 倒是随便重复——苹果和香蕉可以同价。
Q3:泛型尖括号里能装 int 吗?
不能直接装基本类型,要装它们的"对象包装版":int 对应 Integer,所以写 Map<String, Integer>。Java 会在背后自动帮你把 int 和 Integer 互相转换(自动装箱/拆箱),日常几乎无感,知道这层即可。
Q4:ArrayList 和数组性能差很多吗?
入门阶段完全不用纠结。ArrayList 内部就是靠数组实现的,只是包了层"自动扩容"的外衣。数据量不大时,选型的标准是"哪个好用",不是"哪个快"。
Q5:接口里可以有成员变量吗?
可以有,但它们天生是 public static final 的常量——毕竟"标准"要人人可见、统一不变,谁都不能私改。日常更常见的做法是把常量放类里,接口里写常量见到能认出即可。
⚠️ 新手翻车现场
翻车一:实现类漏实现接口方法
interface Flyable {
void fly();
}
class Penguin implements Flyable {
// ❌ 企鹅不会飞,干脆不写 fly?
}
完整报错:
错误: Penguin不是抽象的, 并且未覆盖Flyable中的抽象方法fly()
问题:签了合同就必须全部履约,缺一个方法编译器直接拒收。报错其实很讲道理:它提示你"要么实现,要么把自己也声明成抽象类"(那是后续话题)。
修正:老老实实写 @Override public void fly() { ... }——哪怕是空实现;或者承认企鹅本就不该签这份合同,去掉 implements。
翻车二:泛型装错类型
List<String> names = new ArrayList<>();
names.add(123); // ❌ 编译报错
完整报错:
错误: 不兼容的类型: int无法转换为java.lang.String
问题:泛型是编译期的门卫,塞错类型当场拦截。这是好事——想想没有泛型的年代,错误全藏到运行时才炸。
修正:装什么类型,就 add 什么类型;真要装别的,另建一个对应泛型的列表。
翻车三:遍历集合时下标越界
List<String> list = new ArrayList<>();
list.add("a");
for (int i = 0; i <= list.size(); i++) { // ❌ 又是 <=
System.out.println(list.get(i));
}
完整报错:
Exception in thread "main" java.lang.IndexOutOfBoundsException:
Index 1, Size 1
问题:数组的越界坑在集合里原样复刻——上界写成 <=,最后一轮取到不存在的下标。报错把"你要的下标"和"实际大小"都报了,对照改就行。
修正:条件改 i < list.size();只取值就干脆用增强 for,从根上杜绝下标。
翻车四:拿 get 到的 null 直接用
HashMap<String, Integer> price = new HashMap<>();
price.put("苹果", 5);
System.out.println(price.get("樱桃") + 1); // ❌ 运行时炸
完整报错:
Exception in thread "main" java.lang.NullPointerException
问题:"樱桃"没登记过,get 返回 null,null 参与加法当场崩——不报错的是 get,炸的是用它的人,锅很难第一时间找到。
修正:先用 containsKey 确认存在再取,或取完判空 if (v != null)。
📝 本章小结
- 接口定标准:方法只有签名,类用 implements 签合同,必须实现全部方法
- 一个类可实现多个接口;接口表达"能做什么",继承表达"是什么"
- 面向接口编程:引用类型尽量声明为接口,装任何实现类都能跑
- ArrayList 会伸缩:add / get / set / remove / size / contains 六板斧
- HashMap 按键查值:put 覆盖同名 key,get 查不到返回 null,查存在用 containsKey
- 泛型尖括号定死容器装的类型,塞错编译器当场拦
- 记忆口诀:interface 定标准,implements 来签收,List 排队站,Map 查字典,尖括号管类型。
记忆锚点:接口是插座国标(谁符合标准谁就能插);ArrayList 是排队安检的长龙(只管先后顺序,随时来人走人);HashMap 是图书馆索引卡(报书名瞬间取书,报错书名给你一张空白卡——那就是 null);泛型是瓶身标签(贴了"盐"的罐子,塞糖进去门卫不放行)。
动手改
- 往 fruits 里加"樱桃",然后直接
System.out.println(fruits)打印整个列表 - 往 price 里放"樱桃 8",把三种水果的价格都打印出来
- 再定义一个接口
Swimmable(含 swim 方法),让Bird同时实现两个接口——体会"接口可以实现多个,继承只能 extends 一个" - 用增强 for 遍历 fruits,打印"第 x 种水果:xxx"的清单
- 给 Animal 的例子升级:让 Cat、Dog 各自实现一个
Pet接口(含 play 方法),用一个Pet[]数组统一喊它们玩耍
下一章:综合练习,把四章所学做成一个小项目 🐸