Java 常用语法极简通关(九):常用集合——ArrayList、HashMap、HashSet 与遍历

开篇:数组的"三宗罪"与集合的登场

第四章讲数组时埋了一句话:数组长度创建时定死,之后不能变。做设备台账管理时这个限制立刻现形:

  • 新设备入库,数组满了怎么办?——长度定死,加不了

  • 按设备编号查某台设备,数组怎么查?——只能挨个遍历,笨拙

  • 一堆设备编号里有重复,想快速去重?——数组没有现成的办法

Java 的答案在 java.util 包里:集合(Collection)家族。日常开发 90% 的场景,三个类就够用:ArrayList(会生长的数组)、HashMap(键值对字典)、HashSet(自动去重的袋子)。先看一个完整的"设备台账"程序,三个主角全部登场:

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

public class DeviceLedgerApp {

    public static void main(String[] args) {
        // ===== 1. ArrayList:设备清单,随用随加 =====
        ArrayList<String> devices = new ArrayList<>();
        devices.add("3号皮带输送机");
        devices.add("中央水泵");
        devices.add("辅扇2号");
        devices.add("中央水泵");          // 手滑重复录入

        System.out.println("清单条数:" + devices.size());
        System.out.println("第一台:" + devices.get(0));

        // ===== 2. HashSet:看看实际有多少种设备(自动去重)=====
        HashSet<String> unique = new HashSet<>(devices);
        System.out.println("去重后种数:" + unique.size());

        // ===== 3. HashMap:编号 → 设备详情,按号秒查 =====
        HashMap<String, String> ledger = new HashMap<>();
        ledger.put("PD-003", "3号皮带输送机 | 东翼大巷 | 运行中");
        ledger.put("SB-011", "中央水泵 | -580m泵房 | 检修中");
        ledger.put("FJ-102", "辅扇2号 | 三采区回风巷 | 运行中");

        // 按编号查
        String info = ledger.get("SB-011");
        System.out.println("SB-011 → " + info);

        // 改状态:同 key 再 put 就是覆盖
        ledger.put("SB-011", "中央水泵 | -580m泵房 | 运行中");

        // 遍历台账:打印所有编号和详情
        System.out.println("===== 设备台账 =====");
        for (Map.Entry<String, String> entry : ledger.entrySet()) {
            System.out.println(entry.getKey() + " : " + entry.getValue());
        }

        // 只关心编号时
        for (String code : ledger.keySet()) {
            System.out.println("编号:" + code);
        }
    }
}

输出:

清单条数:4
第一台:3号皮带输送机
去重后种数:3
SB-011 → 中央水泵 | -580m泵房 | 检修中
===== 设备台账 =====
PD-003 : 3号皮带输送机 | 东翼大巷 | 运行中
SB-011 : 中央水泵 | -580m泵房 | 运行中
FJ-102 : 辅扇2号 | 三采区回风巷 | 运行中
编号:PD-003
编号:SB-011
编号:FJ-102

三个类、一套增删改查、两种 Map 遍历,全在这了。逐个讲。

第〇部分:先认识尖括号——泛型(Generics)

ArrayList<String> 里的 <String>泛型:告诉编译器"这个清单里只装 String"。它的价值有两个:

  1. 编译期排错devices.add(123) 直接编译报错,错类型混不进去;

  2. 取出来不用强转devices.get(0) 返回的就是 String,直接用。

本系列对泛型的要求就到"会用"为止——会在集合上声明、能看懂别人的泛型代码,足够。怎么自己写泛型类/泛型方法属于进阶内容,日常业务代码极少需要自己写,本系列不讲。

HashMap 有两个尖括号:<String, String> 前者是键(key)的类型,后者是值(value)的类型。键值类型随意搭配,HashMap<String, Worker>(编号 → 工人对象)在真实项目里比比皆是。

第一部分:ArrayList——会生长的数组

最常用的集合,没有之一。内部就是数组,但自动扩容,你只管往里塞。

高频方法一张表:

方法作用例子
add(e)尾部追加devices.add("水泵")
get(i)按下标取devices.get(0)
size()元素个数注意是方法,对应数组的 length 属性
remove(i) / remove(对象)按下标/按内容删devices.remove(0)
contains(e)是否包含devices.contains("水泵")
isEmpty()是否为空判空首选,比 size() == 0 可读性好
set(i, e)按下标替换devices.set(0, "新设备")

遍历用第三章学的增强 for,天作之合:

for (String name : devices) {
    System.out.println(name);
}

ArrayList vs 数组,怎么选? 一句话:日常业务用 ArrayList,数组只在长度固定、追求极限性能、或与老 API 打交道时用。你职业生涯里写的 ArrayList 会比数组多一个数量级。

三个长度家族别记混(软考爱考):数组 arr.length(属性)、字符串 s.length()(方法)、集合 list.size()(方法)。

第二部分:HashMap——键值对字典

HashMap<String, String>:一把 key 对应一个 value,按 key 查找是 Map 的看家本领——不用遍历,get("SB-011") 一步到位。

高频方法一张表:

方法作用
put(k, v)存入;key 已存在则覆盖旧值
get(k)按 key 取;key 不存在返回 null
containsKey(k)key 是否存在
remove(k)按键删除
keySet()所有 key 的集合
values()所有 value 的集合
entrySet()所有"键值对"的集合
size()键值对个数

遍历的两种主流姿势

要 key 也要 value(最常用)——entrySet:

for (Map.Entry<String, String> entry : ledger.entrySet()) {
    System.out.println(entry.getKey() + " : " + entry.getValue());
}

Map.Entry 就是"一个键值对"的类型,遍历它一次拿到两样东西,效率最高。

只要 key——keySet:

for (String code : ledger.keySet()) { ... }

两个必知特性

  1. key 唯一,value 随便。同 key 再 put 是覆盖——案例里水泵"检修中"被改成"运行中"就靠这个特性。反过来想:更新 Map 不用先查再改,直接 put

  2. get 一个不存在的 key 返回 null,不报错。接着用这个 null 就会喜提空指针异常:

String info = ledger.get("XX-999");
System.out.println(info.length());   // NullPointerException!

稳妥姿势:

String info = ledger.get("XX-999");
if (info != null) {
    System.out.println(info.length());
}

或者用 ledger.getOrDefault("XX-999", "无此设备")——查不到给默认值,防空指针的神器。

  1. HashMap 不保证顺序。遍历出来的顺序不等于 put 的顺序(案例输出恰好一致是巧合)。需要按插入顺序遍历用 LinkedHashMap,知道有这个东西即可。

第三部分:HashSet——自动去重的袋子

HashSet 是最单纯的集合:装元素、自动去重、不保顺序。案例里一句完成去重:

HashSet<String> unique = new HashSet<>(devices);   // 构造时传入 List,重复自动消失

高频方法就几个:add(重复时静默忽略)、containsremovesize。判断"某个编号在不在黑名单里"这种纯存在性场景,HashSet 的 contains 比 ArrayList 快得多,是标准选择。

三个主角的选型口诀:

按位置存取一串东西 → ArrayList;按 key 查 value → HashMap;只关心"有没有、去重" → HashSet。

第四部分:遍历中删除——集合最大的坑

先看这段"看似合理"的代码:

// 想删掉清单里所有"检修中"的设备——错误示范!
for (String name : devices) {
    if (name.equals("中央水泵")) {
        devices.remove(name);   // 运行抛 ConcurrentModificationException
    }
}

增强 for 遍历的过程中直接改集合结构,Java 会立刻抛 ConcurrentModificationException 崩溃——遍历和修改必须分开

标准解法是用迭代器(Iterator),边遍历边通过迭代器自己删除:

import java.util.Iterator;

Iterator<String> it = devices.iterator();
while (it.hasNext()) {
    String name = it.next();
    if (name.equals("中央水泵")) {
        it.remove();    // 通过迭代器删,安全
    }
}

迭代器的三板斧:hasNext()(还有吗)、next()(取下一个)、remove()(删掉刚取的那个)。这是本章讲的"第三种遍历方式",日常不如增强 for 常见,但"遍历中删除"这个场景它是唯一正解,必须会。

常踩的坑(排雷清单)

  1. 遍历中直接 remove/add:ConcurrentModificationException,用迭代器的 remove。

  2. get 完不判 null 就用:Map 查无此键返回 null,防空指针要么判 null 要么 getOrDefault。

  3. 三个长度记混:数组 length、字符串 length()、集合 size()

  4. 以为 ArrayList 能装 intArrayList<int> 编译报错!泛型必须是引用类型,装整数用包装类 ArrayList<Integer>——这就是第十章开场要讲的包装类。

  5. 指望 HashMap/HashSet 有序:不保序。要插入顺序用 LinkedHashMap。

  6. 用 == 比较集合里的字符串:从集合 get 出来的 String 比较内容照样用 equals——第二章的铁律在集合里依然有效。

本章小结

  • 泛型 <String>:编译期排错 + 免强转,会用即可。

  • ArrayList:会生长的数组,增删改查一张表,业务默认选择。

  • HashMap:键值对秒查,key 唯一、put 覆盖、get 查无返 null;遍历首选 entrySet。

  • HashSet:去重与存在性判断的专业户。

  • 遍历三姿势:增强 for(主流)、迭代器(遍历中删除的唯一正解)、entrySet(Map 专用)。

  • 选型口诀:一串用 List,按键查用 Map,去重用 Set。

差最后一块拼图:为什么 ArrayList<int> 不行、Integer 是什么、"123" 怎么变成 123。下一章系列收官:常用工具类——包装类与自动装箱拆箱、StringBuilder 复盘、Math 与日期时间。学完这章,Java 日常语法版图拼完。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值