一、Java集合基础认知(面试预热)

1.1 为什么Java集合是开发必备?

解决数组固定长度的痛点,支持动态扩容,无需手动管理内存;

提供丰富的操作方法(增删改查、排序、去重),简化开发流程;

适配不同业务场景:有序存储、去重存储、键值对存储等,按需选型;

底层封装了数据结构(链表、数组、哈希表等),无需手动实现复杂逻辑。

1.2 本文核心:三大核心集合体系

Java集合主要分为三大体系,覆盖绝大多数开发场景,核心区别如下:

List(列表):有序、可重复,适合需要按插入顺序存储、允许重复元素的场景;

Set(集合):无序、不重复,适合需要去重、判断元素是否存在的场景;

Map(映射):键唯一、值可重复,适合键值对存储、快速查询的场景。

重点提醒:集合与数组的核心区别——数组长度固定,集合动态扩容;数组可存储基本类型和引用类型,集合仅能存储引用类型(基本类型需用包装类)。

二、List 列表:有序可重复,开发最常用

2.1 核心特性与底层结构

List 是有序集合,元素可重复,支持根据索引快速访问,核心实现类有两个:ArrayList、LinkedList,底层结构差异决定了其性能特点:

ArrayList:底层基于动态数组实现,查询效率高(通过索引访问,O(1)),增删效率低(需移动数组元素,O(n));

LinkedList:底层基于双向链表实现,增删效率高(仅需修改指针,O(1)),查询效率低(需遍历链表,O(n))。

2.2 实战代码:List 核心用法(必掌握)

以下代码覆盖List的常用操作(增删改查、遍历),对比ArrayList和LinkedList的用法差异:

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class ListDemo {
    public static void main(String[] args) {
        // 1. ArrayList:查询快、增删慢,适合查询频繁的场景
        List<String> arrayList = new ArrayList<>();
        // 增:添加元素(允许重复)
        arrayList.add("zhangsan");
        arrayList.add("zhangsan"); // 重复元素,正常添加,与Set的核心区别
        arrayList.add("lisi");
        // 查:根据索引获取元素(ArrayList优势)
        String first = arrayList.get(0);
        System.out.println("ArrayList第一个元素:" + first); // 输出:zhangsan
        // 改:修改指定索引的元素
        arrayList.set(1, "wangwu");
        // 删:删除指定元素(需遍历,效率低)
        arrayList.remove("lisi");
        // 遍历:for循环(适合ArrayList)
        System.out.println("ArrayList遍历:");
        for (int i = 0; i < arrayList.size(); i++) {
            System.out.println(arrayList.get(i));
        }

        // 2. LinkedList:增删快、查询慢,适合增删频繁的场景
        List<String> linkedList = new LinkedList<>();
        linkedList.add("zhencong");
        linkedList.add("java");
        // 头部添加、尾部添加(LinkedList优势)
        ((LinkedList<String>) linkedList).addFirst("first");
        ((LinkedList<String>) linkedList).addLast("last");
        // 头部删除、尾部删除
        ((LinkedList<String>) linkedList).removeFirst();
        ((LinkedList<String>) linkedList).removeLast();
        // 遍历:增强for循环(通用)
        System.out.println("LinkedList遍历:");
        for (String s : linkedList) {
            System.out.println(s);
        }
    }
}

2.3 应用场景(落地实战)

ArrayList:首页数据展示、分页查询结果存储、频繁查询的列表(如用户订单列表);

LinkedList:消息队列、栈、队列(如任务队列)、频繁增删的列表(如实时消息列表);

通用场景:需要按插入顺序存储、允许重复元素的所有场景(如用户浏览记录)。

2.4 易错点与避坑指南(面试高频)

坑点1:ArrayList 扩容机制——默认初始容量10,扩容时按1.5倍扩容,频繁扩容会浪费内存,建议初始化时指定容量(如 new ArrayList<>(100));

坑点2:LinkedList 不要用for循环遍历——需每次遍历到指定索引,效率极低,优先用增强for或迭代器;

坑点3:List 的 remove 方法——remove(int index) 是删除索引,remove(Object obj) 是删除元素,避免混淆(如 list.remove(1) 是删除索引1,不是元素1)。

三、Set 集合:无序不重复,去重首选

3.1 核心特性与底层结构

Set 是无序集合,元素不可重复,核心实现类有两个:HashSet、TreeSet,底层结构决定了其核心特性:

HashSet:底层基于哈希表(HashMap)实现,无序、去重,查询、增删效率高(O(1));

TreeSet:底层基于红黑树实现,有序(自然排序或自定义排序)、去重,查询、增删效率 O(log n)。

核心重点:Set 去重的原理——依赖元素的 equals() 和 hashCode() 方法,两个方法必须同时重写,否则去重失效。

3.2 实战代码:Set 去重特性与核心用法

以下代码对比HashSet和TreeSet的用法,重点演示去重逻辑,贴合实际开发场景:

import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;

public class SetDemo {
    public static void main(String[] args) {
        // 1. HashSet:无序、自动去重(开发最常用,效率高)
        Set<String> hashSet = new HashSet<>();
        // 添加元素(重复元素自动过滤)
        hashSet.add("zhangsan");
        hashSet.add("zhangsan"); // 重复元素,不会被添加
        hashSet.add("lisi");
        hashSet.add("wangwu");
        // 遍历:无序(输出顺序与添加顺序无关)
        System.out.println("HashSet(无序去重):");
        for (String s : hashSet) {
            System.out.println(s);
        }
        // 判断元素是否存在(HashSet优势,O(1)效率)
        boolean exists = hashSet.contains("lisi");
        System.out.println("HashSet中是否存在lisi:" + exists); // 输出:true

        // 2. TreeSet:有序、自动去重(需排序场景使用)
        Set<String> treeSet = new TreeSet<>();
        treeSet.add("zhangsan");
        treeSet.add("zhangsan"); // 重复元素,自动过滤
        treeSet.add("lisi");
        treeSet.add("wangwu");
        // 遍历:有序(默认按自然字典序排序)
        System.out.println("TreeSet(有序去重):");
        for (String s : treeSet) {
            System.out.println(s);
        }
        // 自定义排序(如按字符串长度排序)
        Set<String> customTreeSet = new TreeSet<>((o1, o2) -> o1.length() - o2.length());
        customTreeSet.add("apple");
        customTreeSet.add("banana");
        customTreeSet.add("pear");
        System.out.println("TreeSet(自定义排序):" + customTreeSet);
    }
}

3.3 应用场景(落地实战)

HashSet:数据去重(如用户浏览记录去重、活动参与名单去重)、快速判断元素是否存在(如判断用户是否已报名);

TreeSet:有序去重场景(如排行榜、按时间排序的去重数据、按规则排序的列表);

通用场景:所有需要去重、无需保证顺序(或需要指定顺序)的场景。

3.4 易错点与避坑指南(面试高频)

坑点1:HashSet 去重失效——未重写元素的 equals() 和 hashCode() 方法,导致无法判断元素是否重复;

坑点2:TreeSet 排序异常——元素未实现 Comparable 接口,且未指定自定义比较器,会抛出 ClassCastException;

坑点3:Set 无序误解——HashSet 是“无序”(不保证插入顺序),不是“乱序”,TreeSet 是有序的,需区分两者差异。

四、Map 映射:键值对存储,快速查询首选

4.1 核心特性与底层结构

Map 是键值对集合,核心特性:键唯一、值可重复,键和值均为引用类型,核心实现类是 HashMap(开发最常用),底层结构如下:

HashMap:底层基于哈希表实现(JDK1.8 后是数组+链表+红黑树),查询、增删效率高(O(1));

核心逻辑:通过键的 hashCode() 计算存储位置,通过 equals() 判断键是否重复,重复则覆盖值。

4.2 实战代码:Map 核心用法+对象作为键

以下代码覆盖Map的常用操作,重点演示“自定义对象作为键”的场景(开发高频,面试必问):

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

// 自定义对象作为Map的键(必须重写equals和hashCode)
class UserKey {
    private String name;
    private Integer id;

    public UserKey(String name, Integer id) {
        this.name = name;
        this.id = id;
    }

    // getter方法
    public String getName() { return name; }
    public Integer getId() { return id; }

    // 重写equals和hashCode:保证相同对象的hashCode相等,equals返回true
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        UserKey userKey = (UserKey) o;
        return Objects.equals(name, userKey.name) && Objects.equals(id, userKey.id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, id);
    }

    // 重写toString,方便打印
    @Override
    public String toString() {
        return "UserKey{name='" + name + "', id=" + id + "}";
    }
}

public class MapDemo {
    public static void main(String[] args) {
        // 1. 普通Map:键为String,值为String
        Map<String, String> map = new HashMap<>();
        // 增/改:key存在则覆盖值,不存在则添加
        map.put("zhencong", "后端开发");
        map.put("zhencong", "分布式开发"); // 相同key,覆盖值
        map.put("lisi", "前端开发");
        // 查:根据key获取值
        String value = map.get("zhencong");
        System.out.println("key=zhencong的值:" + value); // 输出:分布式开发
        // 删:根据key删除键值对
        map.remove("lisi");
        // 遍历:遍历所有键值对(开发常用)
        System.out.println("普通Map遍历:");
        for (Map.Entry<String, String> entry : map.entrySet()) {
            System.out.println("key:" + entry.getKey() + ",value:" + entry.getValue());
        }

        // 2. 自定义对象作为Map的键(面试高频场景)
        Map<UserKey, String> userMap = new HashMap<>();
        UserKey key1 = new UserKey("zhangsan", 1);
        UserKey key2 = new UserKey("zhangsan", 1); // 与key1是同一个对象(重写了equals和hashCode)
        UserKey key3 = new UserKey("lisi", 2);

        userMap.put(key1, "Java工程师");
        userMap.put(key2, "Redis工程师"); // 覆盖值(key1和key2相等)
        userMap.put(key3, "MySQL工程师");

        // 遍历自定义key的Map
        System.out.println("自定义key的Map遍历:");
        for (Map.Entry<UserKey, String> entry : userMap.entrySet()) {
            System.out.println("key:" + entry.getKey() + ",value:" + entry.getValue());
        }
        System.out.println("userMap的大小:" + userMap.size()); // 输出:2(key1和key2合并)
    }
}

关键提醒:自定义对象作为Map的键时,必须同时重写 equals() 和 hashCode() 方法,否则Map无法判断键是否重复,会导致相同对象被当作不同键存储,造成数据异常。

4.3 实战拓展:Map 实现去重+排序(贴合业务场景)

实际开发中,常遇到“去重+排序”的场景(如用户金额去重,按金额排序),以下代码结合Map和List实现,贴合真实业务:

public class User {
    private String name;
    private int money;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }
}
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Test1 {

    public static void main(String[] args) {
        User user = new User();
        user.setName("ryn");
        user.setMoney(100);

        User user2 = new User();
        user2.setName("wangwu");
        user2.setMoney(10);

        User user3 = new User();
        user3.setName("ryn");
        user3.setMoney(1000);

        List<User> users = new ArrayList<>();
        users.add(user);
        users.add(user2);
        users.add(user3);

        check(users);
    }
    private static void check(List<User> users) {
        //分组
        //这个map留的是所有用户去重后的金额,key是用户名,value是金额
        Map<String, Integer> map = new HashMap<>();
            for (User user : users) {
                Integer money = map.get(user.getName());
                if (money == null) {
                    map.put(user.getName(), user.getMoney());
                    continue;
                }
                if (money > user.getMoney()) {
                    continue;
                }
                map.put(user.getName(), user.getMoney());
            }
        //按照金额进行排序
        List<Map.Entry<String, Integer>> list = new ArrayList<>(map.entrySet());
        sort(list);
        for (Map.Entry<String, Integer> stringIntegerEntry : list) {
            System.out.println(stringIntegerEntry.getKey() + "----" + stringIntegerEntry.getValue());
        }
    }
    private static void sort(List<Map.Entry<String, Integer>> list) {
        for (int i = 0; i < list.size() - 1; i++) {
            for (int j = 0; j < list.size() - 1 - i; j++) {
                if (list.get(j).getValue() < list.get(j + 1).getValue()) {
                    Map.Entry<String, Integer> temp = list.get(j);
                    list.set(j, list.get(j + 1));
                    list.set(j + 1, temp);
                }
            }
        }
    }

}

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

public class Test2 {
    public static void main(String[] args) {
        User user1 = new User();
        user1.setName("zhencong");
        user1.setMoney(100);

        User user2 = new User();
        user2.setName("wangwu");
        user2.setMoney(10);

        User user3 = new User();
        user3.setName("zhencong");
        user3.setMoney(1000);

        List<User> users = new ArrayList<>();
        users.add(user1);
        users.add(user2);
        users.add(user3);

        //统计每个人的金额总数
        //按梯度输出人员数量以及人名 0-100,101-1000,1001-10000

        // 1. 统计每个人的金额总数
        Map<String, Integer> map1 = new HashMap<>();
        for (User user : users) {
            String name = user.getName();
            int money = user.getMoney();
            // 累加金额:如果已存在则累加,不存在则初始化为当前金额
            map1.put(name, map1.getOrDefault(name, 0) + money);
        }

        // 输出每个人的总金额
        for (Map.Entry<String, Integer> every : map1.entrySet()) {
            System.out.println("姓名:" + every.getKey() + ",总金额:" + every.getValue());
        }

        // 2. 按梯度分组:0-100,101-1000,1001-10000
        // 定义梯度分组的容器:key为梯度描述,value为该梯度下的用户列表
        Map<String, List<String>> group = new HashMap<>();
        // 初始化三个梯度分组
        group.put("0-100", new ArrayList<>());
        group.put("101-1000", new ArrayList<>());
        group.put("1001-10000", new ArrayList<>());

        // 遍历统计后的总金额,按梯度分组
        for (Map.Entry<String, Integer> entry : map1.entrySet()) {
            String name = entry.getKey();
            int totalMoney = entry.getValue();

            if (totalMoney >= 0 && totalMoney <= 100) {
                group.get("0-100").add(name);
            } else if (totalMoney >= 101 && totalMoney <= 1000) {
                group.get("101-1000").add(name);
            } else if (totalMoney >= 1001 && totalMoney <= 10000) {
                group.get("1001-10000").add(name);
            }
        }

        // 按梯度输出人员数量和人名
        System.out.println("\n=== 按金额梯度分组结果 ===");
        for (Map.Entry<String, List<String>> entry : group.entrySet()) {
            String gradient = entry.getKey();
            List<String> names = entry.getValue();
            System.out.println("梯度 " + gradient + ":");
            System.out.println("  人员数量:" + names.size());
            System.out.println("  人员名单:" + (names.isEmpty() ? "无" : String.join("、", names)));
        }

    }

}

4.4 应用场景(落地实战)

普通Map:配置信息存储、用户信息缓存、键值对查询(如根据用户ID查询用户名);

自定义对象作为键:复杂场景的键值对存储(如根据用户对象查询用户详情、根据订单对象查询订单状态);

去重+排序:用户排行榜、数据统计去重(如统计每个用户的最大消费金额并排序)。

4.5 易错点与避坑指南(面试高频)

坑点1:HashMap 键为null——HashMap 允许键为null(仅一个),值可多个null,Hashtable 不允许键和值为null;

坑点2:HashMap 线程不安全——多线程环境下修改HashMap会导致死循环,需使用 ConcurrentHashMap;

坑点3:Map 遍历方式选择——遍历所有键值对用 entrySet()(效率高),仅遍历键用 keySet(),仅遍历值用 values();

坑点4:hashCode 和 equals 的关系——同一个对象的 hashCode 一定相等;hashCode 相等的两个对象,equals 不一定相等(哈希冲突);equals 相等的两个对象,hashCode 必须相等。

五、三大集合对比总结(必背表格)

集合类型

核心特点

底层结构

典型实现类

典型应用场景

List

有序、可重复、支持索引访问

ArrayList(数组)、LinkedList(双向链表)

ArrayList、LinkedList

分页查询、消息队列、浏览记录、有序列表

Set

无序、不重复、支持元素存在判断

HashSet(哈希表)、TreeSet(红黑树)

HashSet、TreeSet

数据去重、元素存在判断、有序排行榜

Map

键唯一、值可重复、键值对映射

HashMap(数组+链表+红黑树)

HashMap、ConcurrentHashMap

配置存储、快速查询、去重+排序、对象映射

六、面试高频题(必背)

6.1 List 相关

Q1:ArrayList 和 LinkedList 的区别?如何选型? A1:① 底层结构:ArrayList 是数组,LinkedList 是双向链表;② 性能:ArrayList 查询快、增删慢,LinkedList 增删快、查询慢;③ 选型:查询频繁用 ArrayList,增删频繁用 LinkedList。

Q2:ArrayList 的扩容机制是什么? A2:默认初始容量10,当元素数量超过容量时,按1.5倍(oldCapacity >> 1)扩容,扩容时会创建新数组,将原数组元素复制到新数组,频繁扩容会浪费内存。

6.2 Set 相关

Q1:HashSet 去重的原理是什么? A1:依赖元素的 equals() 和 hashCode() 方法:① 先通过 hashCode() 计算存储位置;② 若该位置无元素,直接存储;③ 若有元素,通过 equals() 判断是否相同,相同则不存储,不同则存储(哈希冲突处理)。

Q2:HashSet 和 TreeSet 的区别? A2:① 有序性:HashSet 无序,TreeSet 有序;② 底层结构:HashSet 是哈希表,TreeSet 是红黑树;③ 效率:HashSet 增删查效率 O(1),TreeSet 是 O(log n);④ 去重:两者都支持去重,TreeSet 额外支持排序。

6.3 Map 相关

Q1:HashMap 的底层结构(JDK1.8)? A1:底层是数组+链表+红黑树:① 数组是哈希桶,每个桶存储链表(哈希冲突时);② 当链表长度超过8,且数组长度≥64时,链表转为红黑树;③ 当红黑树节点数少于6时,转回链表,平衡性能和内存。

Q2:为什么自定义对象作为 Map 的键,必须重写 equals 和 hashCode? A2:① Map 判断键是否重复,先比较 hashCode,再比较 equals;② 若不重写,会使用 Object 类的默认方法,默认 hashCode 是对象的内存地址,即使两个对象内容相同,也会被当作不同键,导致去重失效。

七、总结(开发/面试必备)

本文围绕Java三大核心集合(List、Map、Set),结合实战代码,完整讲解了用法、底层原理、应用场景和易错点,核心要点总结如下:

选型原则:有序可重复用 List,去重用 Set,键值对存储用 Map,根据性能需求选择具体实现类;

核心重点:Set 和 Map 的去重,依赖 equals() 和 hashCode() 方法,必须同时重写;

性能优化:ArrayList 初始化指定容量,LinkedList 避免for循环遍历,HashMap 避免多线程修改;

面试高频:三大集合的区别、底层结构、扩容机制、去重原理,是后端面试必问,务必掌握。

看完这一篇,Java集合的核心用法和避坑点基本全覆盖,日常开发遇到集合相关问题,直接对照查阅即可,建议收藏备用!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐