Java常用API详解(二):集合类API(ArrayList/HashMap/HashSet)实战,一篇吃透
哈喽,各位Java学习者、开发者小伙伴!上一篇我们讲解了Java基础高频API(String、StringBuilder、Object、枚举),都是日常开发的"基础工具"。今天这篇,我们聚焦集合类API------Java开发中处理批量数据的核心工具,也是后端开发、框架源码、面试中的高频考点。
很多新手刚接触集合时,会混淆ArrayList和LinkedList、HashMap和HashSet的用法,不知道什么时候用哪个,甚至写出效率低下、易报错的代码。本文将重点讲解3个最常用的集合类:ArrayList、HashMap、HashSet,从"底层原理→核心API→实战场景→避坑指南"全方位拆解,结合上一篇的API知识串联实战,所有代码可直接复制运行,新手也能轻松上手!
先回顾上一篇的核心知识点:我们用枚举替代魔法值,用String、StringBuilder处理字符串,用Object类的重写提升代码可读性。今天的集合API,会频繁结合这些知识点,实现更完整的企业级业务逻辑。
前置铺垫:为什么需要集合?(数组的局限性)
在没有集合之前,我们存储批量数据只能用数组,但数组有两个致命局限,而集合完美解决了这些问题,这也是集合成为开发必备工具的核心原因:
-
- 数组长度固定:一旦创建,无法动态扩容,添加元素超过长度会报错;
-
- 数组操作繁琐:没有内置的增删改查方法,比如删除元素需要手动移动数组元素,代码冗余。
而Java集合(位于java.util包下),本质是"动态容器",支持动态扩容,提供了丰富的增删改查API,无需关心底层实现,专注业务逻辑即可。Java集合体系核心分为两大分支:Collection(存储单个元素) 和Map(存储键值对),本文重点讲解最常用的3个实现类:ArrayList(Collection分支)、HashMap(Map分支)、HashSet(Collection分支)。
一、ArrayList API(最常用的List集合,开发首选)
ArrayList是List接口的最常用实现类,底层基于动态数组实现 ,核心特点:有序、可重复、支持索引访问,查询速度快(直接通过索引定位),尾部增删快,中间增删慢(需移动元素),非线程安全,是日常开发中处理批量数据的首选集合。
适用场景:频繁查询、尾部增删,少量中间增删(如用户列表、订单列表展示)。
1.1 核心初始化方式(3种,重点掌握前2种)
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class ArrayListTest {
public static void main(String[] args) {
// 1. 无参构造(最常用,初始容量为10,扩容时默认1.5倍扩容)
List<String> list1 = new ArrayList<>();
// 2. 指定初始容量(适合提前知道元素数量,减少扩容次数,提升性能)
List<Integer> list2 = new ArrayList<>(20);
// 3. 基于已有集合/数组初始化(偶尔用到)
List<String> list3 = new ArrayList<>(Arrays.asList("Java", "API", "集合"));
}
}
补充:JDK 8中,ArrayList无参构造的初始容量为0,首次添加元素时才扩容为10,后续每次扩容为原容量的1.5倍,通过Arrays.copyOf()复制数组实现扩容。
1.2 高频核心API(按使用频率排序,必记)
所有API结合代码示例,可直接复制运行,重点掌握"增删改查"四大核心操作:
java
import java.util.ArrayList;
import java.util.List;
public class ArrayListApiTest {
public static void main(String[] args) {
// 初始化ArrayList(存储用户名称,结合上一篇的String API)
List<String> userNames = new ArrayList<>();
// 1. 增:添加元素(核心API)
userNames.add("张三"); // 尾部添加元素(最常用)
userNames.add("李四");
userNames.add(1, "王五"); // 指定索引插入(后续元素自动后移)
System.out.println("添加后:" + userNames); // 输出:[张三, 王五, 李四]
// 2. 查:获取元素/判断元素(核心API)
String name = userNames.get(0); // 根据索引获取元素(索引从0开始)
System.out.println("索引0的元素:" + name); // 输出:张三
boolean hasLi = userNames.contains("李四"); // 判断是否包含指定元素
System.out.println("是否包含李四:" + hasLi); // 输出:true
int size = userNames.size(); // 获取集合元素个数
System.out.println("集合大小:" + size); // 输出:3
boolean isEmpty = userNames.isEmpty(); // 判断集合是否为空
System.out.println("集合是否为空:" + isEmpty); // 输出:false
// 3. 改:修改指定索引的元素
String oldName = userNames.set(2, "赵六"); // 修改索引2的元素,返回被替换的旧元素
System.out.println("被替换的旧元素:" + oldName); // 输出:李四
System.out.println("修改后:" + userNames); // 输出:[张三, 王五, 赵六]
// 4. 删:删除元素(3种常用方式)
userNames.remove(1); // 方式1:根据索引删除,返回被删除的元素
System.out.println("删除索引1后:" + userNames); // 输出:[张三, 赵六]
userNames.remove("张三"); // 方式2:根据元素删除(删除第一个匹配的)
System.out.println("删除张三后:" + userNames); // 输出:[赵六]
// 方式3:按条件删除(JDK 8+,结合Lambda,实战常用)
userNames.add("孙七");
userNames.add("周八");
userNames.removeIf(s -> s.length() == 2); // 删除长度为2的元素
System.out.println("按条件删除后:" + userNames); // 输出:[]
// 5. 清空集合
userNames.clear();
System.out.println("清空后:" + userNames); // 输出:[]
// 6. 遍历集合(3种常用方式,重点掌握前2种)
List<String> newList = new ArrayList<>(Arrays.asList("A", "B", "C"));
// 方式1:增强for循环(最常用,简洁)
for (String s : newList) {
System.out.print(s + " "); // 输出:A B C
}
System.out.println();
// 方式2:普通for循环(需要索引时用)
for (int i = 0; i < newList.size(); i++) {
System.out.print(newList.get(i) + " "); // 输出:A B C
}
System.out.println();
// 方式3:迭代器(遍历过程中删除元素推荐用)
Iterator<String> iterator = newList.iterator();
while (iterator.hasNext()) {
String s = iterator.next();
if ("B".equals(s)) {
iterator.remove(); // 迭代器删除,避免ConcurrentModificationException
}
}
System.out.println("迭代器删除后:" + newList); // 输出:[A, C]
}
}
1.3 新手常踩坑点(重点避坑)
-
坑点1:索引越界异常(
IndexOutOfBoundsException)→ 原因:索引小于0或大于等于集合大小(size()-1);解决:获取索引前先判断索引范围,或用增强for循环遍历。 -
坑点2:遍历集合时用
for循环删除元素 → 会导致索引错乱,抛ConcurrentModificationException;解决:用迭代器(Iterator)删除,或JDK 8+的removeIf()方法。 -
坑点3:频繁在ArrayList中间增删元素 → 效率极低(需移动大量元素);解决:频繁中间增删,改用LinkedList(后续会简单介绍)。
-
坑点4:ArrayList存储基本类型 → 报错;原因:集合只能存储引用类型,基本类型需用包装类(如int→Integer、long→Long)。
补充:ArrayList vs LinkedList(快速区分,避免用错)
很多新手会混淆这两个List实现类,用表格快速区分,按需选择:
| 特性 | ArrayList | LinkedList |
|---|---|---|
| 底层实现 | 动态数组 | 双向链表 |
| 查询速度 | 快(O(1),直接索引访问) | 慢(O(n),需遍历链表) |
| 增删速度(中间) | 慢(需移动元素) | 快(仅修改节点指针) |
| 适用场景 | 频繁查询、尾部增删 | 频繁中间增删、作为队列/栈使用 |
注意:日常开发中,ArrayList的使用频率远高于LinkedList,除非有频繁中间增删的场景,否则优先用ArrayList。
二、HashMap API(最常用的Map集合,键值对存储首选)
HashMap是Map接口的最常用实现类,底层基于哈希表(数组+链表/红黑树)实现 ,核心特点:无序、键唯一、值可重复,查询、增删速度快(O(1)),非线程安全,支持存储null键(仅1个)和null值(多个),是开发中存储键值对数据的首选集合。
适用场景:存储键值对数据(如用户ID→用户信息、配置项key→配置值、字典映射)。
2.1 核心初始化方式(2种,重点掌握)
java
import java.util.HashMap;
import java.util.Map;
public class HashMapTest {
public static void main(String[] args) {
// 1. 无参构造(最常用,初始容量16,负载因子0.75)
Map<Integer, String> userMap = new HashMap<>();
// 2. 指定初始容量(适合提前知道键值对数量,减少扩容次数)
Map<String, Integer> scoreMap = new HashMap<>(30);
}
}
补充:负载因子0.75表示,当HashMap中元素个数超过"容量×0.75"时,会自动扩容为原容量的2倍(JDK 8);初始容量和负载因子的设置,会影响HashMap的性能,默认值(16和0.75)是时间和空间的平衡选择。
2.2 高频核心API(按使用频率排序,必记)
结合上一篇的User类、枚举API,实现实战场景(存储用户信息),代码可直接复用:
java
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
// 自定义User类(重写equals()和hashCode(),避免HashMap键重复问题)
class User {
private Integer id;
private String name;
private Integer age;
public User(Integer id, String name, Integer age) {
this.id = id;
this.name = name;
this.age = age;
}
// 重写equals()和hashCode()(HashMap键唯一的核心保障)
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return age.equals(user.age) && id.equals(user.id) && name.equals(user.name);
}
@Override
public int hashCode() {
return java.util.Objects.hash(id, name, age);
}
// 重写toString(),便于调试
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
// getter/setter省略
}
public class HashMapApiTest {
public static void main(String[] args) {
// 初始化HashMap:key=用户ID(Integer),value=用户对象(User)
Map<Integer, User> userMap = new HashMap<>();
// 1. 增:添加键值对(核心API)
userMap.put(1, new User(1, "张三", 25));
userMap.put(2, new User(2, "李四", 23));
userMap.put(3, new User(3, "王五", 24));
// 注意:key重复时,会覆盖原有的value
userMap.put(1, new User(1, "张三(更新)", 26));
System.out.println("添加后:" + userMap);
// 2. 查:获取值/判断键(核心API)
User user = userMap.get(2); // 根据key获取value
System.out.println("key=2的用户:" + user); // 输出:User{id=2, name='李四', age=23}
boolean hasKey = userMap.containsKey(3); // 判断是否包含指定key
System.out.println("是否包含key=3:" + hasKey); // 输出:true
boolean hasValue = userMap.containsValue(new User(2, "李四", 23)); // 判断是否包含指定value
System.out.println("是否包含该用户:" + hasValue); // 输出:true
int size = userMap.size(); // 获取键值对个数
System.out.println("键值对个数:" + size); // 输出:3
// 3. 改:修改value(本质是put(),key存在则覆盖)
userMap.put(3, new User(3, "王五(更新)", 25));
System.out.println("修改后:" + userMap);
// 4. 删:删除键值对(根据key删除)
User removedUser = userMap.remove(2); // 删除指定key,返回被删除的value
System.out.println("被删除的用户:" + removedUser); // 输出:User{id=2, name='李四', age=23}
System.out.println("删除后:" + userMap);
// 5. 遍历HashMap(3种常用方式,重点掌握前2种)
// 方式1:遍历所有key,再根据key获取value(最常用)
Set<Integer> keys = userMap.keySet();
for (Integer key : keys) {
User u = userMap.get(key);
System.out.println("key=" + key + ",value=" + u);
}
// 方式2:遍历所有键值对(entrySet(),高效)
for (Map.Entry<Integer, User> entry : userMap.entrySet()) {
Integer key = entry.getKey();
User value = entry.getValue();
System.out.println("key=" + key + ",value=" + value);
}
// 方式3:遍历所有value(不关心key时用)
for (User u : userMap.values()) {
System.out.println("value=" + u);
}
// 6. 清空集合
userMap.clear();
System.out.println("清空后:" + userMap); // 输出:{}
}
}
2.3 新手常踩坑点(重中之重,面试高频)
-
坑点1:HashMap的key重复问题 → 原因:未重写
equals()和hashCode(),导致相同内容的对象被当作不同key;解决:自定义对象作为key时,必须重写这两个方法(如上面的User类),确保"相同内容的对象,hashCode相同、equals返回true"。 -
坑点2:HashMap是有序的 → 错误!HashMap是无序的(存储顺序≠插入顺序);解决:需要有序的键值对,改用LinkedHashMap。
-
坑点3:HashMap线程安全 → 错误!HashMap非线程安全,多线程环境下修改会导致数据错乱;解决:多线程场景用ConcurrentHashMap(后续讲解),或用
Collections.synchronizedMap(userMap)包装。 -
坑点4:用null作为key时,多次put会覆盖 → 原因:HashMap只允许1个null key,重复put会覆盖原value。
三、HashSet API(去重首选,无序集合)
HashSet是Set接口的最常用实现类,底层基于HashMap实现 (用key存储元素,value为默认常量),核心特点:无序、元素唯一、无索引,不支持通过索引访问,查询、增删速度快(O(1)),非线程安全,允许存储1个null元素,是开发中"去重"的首选集合。
适用场景:元素去重(如用户标签去重、订单编号去重)、无需保证顺序的场景。
3.1 核心初始化方式(2种)
java
import java.util.HashSet;
import java.util.Set;
public class HashSetTest {
public static void main(String[] args) {
// 1. 无参构造(最常用)
Set<String> tagSet = new HashSet<>();
// 2. 基于已有集合初始化(去重常用)
Set<String> oldSet = new HashSet<>();
oldSet.add("Java");
oldSet.add("Java"); // 重复元素,自动忽略
Set<String> newSet = new HashSet<>(oldSet);
}
}
3.2 高频核心API(用法简洁,必记)
HashSet无索引,因此没有get()、set()等与索引相关的方法,核心API围绕"增删改查"(无改,改本质是删了再增):
java
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class HashSetApiTest {
public static void main(String[] args) {
// 初始化HashSet(存储用户标签,实现去重)
Set<String> tagSet = new HashSet<>();
// 1. 增:添加元素(核心API,重复元素自动忽略)
tagSet.add("Java");
tagSet.add("后端");
tagSet.add("API");
tagSet.add("Java"); // 重复元素,不会添加
System.out.println("添加后:" + tagSet); // 输出:[Java, 后端, API](顺序不固定)
// 2. 查:判断元素是否存在(核心API)
boolean hasTag = tagSet.contains("后端");
System.out.println("是否包含后端标签:" + hasTag); // 输出:true
int size = tagSet.size();
System.out.println("标签个数:" + size); // 输出:3
boolean isEmpty = tagSet.isEmpty();
System.out.println("是否为空:" + isEmpty); // 输出:false
// 3. 删:删除元素
boolean isRemoved = tagSet.remove("API"); // 删除指定元素,返回是否删除成功
System.out.println("是否删除API标签:" + isRemoved); // 输出:true
System.out.println("删除后:" + tagSet); // 输出:[Java, 后端]
// 4. 遍历HashSet(2种常用方式,无普通for循环,因为无索引)
// 方式1:增强for循环(最常用)
for (String tag : tagSet) {
System.out.print(tag + " "); // 输出:Java 后端(顺序不固定)
}
System.out.println();
// 方式2:迭代器(遍历过程中删除元素推荐用)
Iterator<String> iterator = tagSet.iterator();
while (iterator.hasNext()) {
String tag = iterator.next();
if ("Java".equals(tag)) {
iterator.remove();
}
}
System.out.println("迭代器删除后:" + tagSet); // 输出:[后端]
// 5. 清空集合
tagSet.clear();
System.out.println("清空后:" + tagSet); // 输出:[]
}
}
3.3 核心注意点(去重的关键)
-
- HashSet去重的原理:依赖元素的
equals()和hashCode()方法,和HashMap的key去重原理一致------先比较hashCode,若不同则直接存入;若相同,再用equals()判断,若为false则存入,若为true则忽略(视为重复元素)。
- HashSet去重的原理:依赖元素的
-
- 自定义对象存入HashSet,需重写
equals()和hashCode(),否则无法实现去重(和HashMap的key要求一致)。
- 自定义对象存入HashSet,需重写
-
- HashSet与ArrayList的区别:ArrayList有序、可重复、有索引;HashSet无序、不可重复、无索引,核心用途是去重。
补充:若需要"去重且保证插入顺序",改用LinkedHashSet(底层基于LinkedHashMap实现),API与HashSet完全一致,仅多了"有序"特性。
四、实战综合案例(整合三篇API,企业级场景)
结合上一篇的String、StringBuilder、枚举API,以及本篇的集合API,写一个"用户管理系统"简化版,实现用户的增删改查、标签去重、角色关联,整合所有知识点,代码可直接复用:
java
import java.util.*;
// 1. 枚举:用户角色(避免魔法值,衔接上一篇)
enum UserRole {
ADMIN("管理员", 1),
USER("普通用户", 2),
GUEST("游客", 3);
private final String desc;
private final int code;
UserRole(String desc, int code) {
this.desc = desc;
this.code = code;
}
// 自定义API:根据编码获取枚举
public static UserRole getByCode(int code) {
for (UserRole role : UserRole.values()) {
if (role.code == code) {
return role;
}
}
return null;
}
// getter方法
public String getDesc() {
return desc;
}
public int getCode() {
return code;
}
}
// 2. 自定义User类(重写equals()、hashCode()、toString())
class User {
private Integer id;
private String name;
private Integer age;
private UserRole role;
private Set<String> tags; // 用户标签(用HashSet去重)
public User(Integer id, String name, Integer age, UserRole role) {
this.id = id;
this.name = name;
this.age = age;
this.role = role;
this.tags = new HashSet<>(); // 初始化标签集合(自动去重)
}
// 新增:添加用户标签(去重)
public void addTag(String tag) {
// 结合String API,去除标签前后空格,避免无效标签
if (tag != null && !tag.trim().isEmpty()) {
this.tags.add(tag.trim());
}
}
// 重写equals()和hashCode()(用于HashMap、HashSet去重)
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return age.equals(user.age) && id.equals(user.id) && name.equals(user.name) && role == user.role;
}
@Override
public int hashCode() {
return Objects.hash(id, name, age, role);
}
// 重写toString(),结合StringBuilder拼接
@Override
public String toString() {
return new StringBuilder()
.append("User{")
.append("id=").append(id)
.append(", name='").append(name).append('\'')
.append(", age=").append(age)
.append(", role=").append(role.getDesc())
.append(", tags=").append(tags)
.append('}')
.toString();
}
// getter/setter省略
}
// 3. 业务逻辑:用户管理(整合所有API)
public class UserManager {
public static void main(String[] args) {
// 用HashMap存储用户:key=用户ID,value=用户对象(高效查询)
Map<Integer, User> userMap = new HashMap<>();
// 1. 添加用户(结合枚举API、HashSet标签去重)
User admin = new User(1, "张三", 25, UserRole.getByCode(1));
admin.addTag("Java");
admin.addTag("后端");
admin.addTag("Java"); // 重复标签,自动去重
userMap.put(1, admin);
User user1 = new User(2, "李四", 23, UserRole.getByCode(2));
user1.addTag("前端");
user1.addTag("Vue");
userMap.put(2, user1);
System.out.println("所有用户:");
for (User user : userMap.values()) {
System.out.println(user);
}
// 2. 查询用户(根据ID查询,结合HashMap API)
User queryUser = userMap.get(1);
System.out.println("\n查询ID=1的用户:" + queryUser);
// 3. 修改用户(更新姓名,结合HashMap API)
queryUser.setName("张三(管理员)");
userMap.put(1, queryUser); // 覆盖原用户
System.out.println("\n修改后ID=1的用户:" + userMap.get(1));
// 4. 删除用户(根据ID删除)
userMap.remove(2);
System.out.println("\n删除ID=2后,剩余用户:" + userMap.values());
// 5. 统计用户数量(结合HashMap API)
System.out.println("\n当前用户数量:" + userMap.size());
// 6. 提取所有用户标签(去重,结合HashSet API)
Set<String> allTags = new HashSet<>();
for (User user : userMap.values()) {
allTags.addAll(user.getTags()); // 批量添加标签,自动去重
}
System.out.println("\n所有用户标签(去重后):" + allTags);
}
}
运行结果(参考):
所有用户: User{id=1, name='张三', age=25, role=管理员, tags=[Java, 后端]} User{id=2, name='李四', age=23, role=普通用户, tags=[前端, Vue]} 查询ID=1的用户:User{id=1, name='张三', age=25, role=管理员, tags=[Java, 后端]} 修改后ID=1的用户:User{id=1, name='张三(管理员)', age=25, role=管理员, tags=[Java, 后端]} 删除ID=2后,剩余用户:[User{id=1, name='张三(管理员)', age=25, role=管理员, tags=[Java, 后端]}] 当前用户数量:1 所有用户标签(去重后):[Java, 后端]
五、总结与后续预告
本文作为Java常用API系列的第二篇,重点讲解了3个最常用的集合类API,都是日常开发中"每天都会用到"的核心工具,总结如下:
-
- ArrayList:有序、可重复、有索引,底层数组,查询快,首选用于批量数据存储、频繁查询;
-
- HashMap:无序、键唯一、值可重复,底层哈希表,首选用于键值对存储、高效查询;
-
- HashSet:无序、元素唯一、无索引,底层HashMap,首选用于元素去重;
-
- 核心共性:都非线程安全,自定义对象作为key/元素时,必须重写
equals()和hashCode()。
- 核心共性:都非线程安全,自定义对象作为key/元素时,必须重写
新手建议:把本文的代码全部复制运行一遍,重点练习"集合的增删改查"和"去重逻辑",尤其是HashMap和HashSet的去重原理,这是面试高频考点;同时注意规避文中提到的坑点(如索引越界、线程安全、去重失败)。
后续预告:Java常用API(三)将讲解「工具类API」(Collections、Arrays)和「日期时间API」(LocalDateTime等),这些API能进一步提升开发效率,解决日常开发中的高频问题,敬请关注!
原创不易,点赞+收藏+关注,后续持续更新Java核心干货,欢迎在评论区交流集合API使用中的问题~