🐸 青蛙编程网
首页 / Java / 面向对象 / 4. 接口与常用集合

接口与常用集合:标准与工具箱

这是什么:接口(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);泛型是瓶身标签(贴了"盐"的罐子,塞糖进去门卫不放行)。

动手改

  1. 往 fruits 里加"樱桃",然后直接 System.out.println(fruits) 打印整个列表
  2. 往 price 里放"樱桃 8",把三种水果的价格都打印出来
  3. 再定义一个接口 Swimmable(含 swim 方法),让 Bird 同时实现两个接口——体会"接口可以实现多个,继承只能 extends 一个"
  4. 用增强 for 遍历 fruits,打印"第 x 种水果:xxx"的清单
  5. 给 Animal 的例子升级:让 Cat、Dog 各自实现一个 Pet 接口(含 play 方法),用一个 Pet[] 数组统一喊它们玩耍

下一章:综合练习,把四章所学做成一个小项目 🐸

📥 下载本章代码

ZIP 内含本章全部代码文件与说明,解压即用,与教程内容同步生成。