今天聊聊Java里的”百宝箱”——集合框架

今天聊聊Java里的”百宝箱”——集合框架

各位道友,大家好!我是会编程的吕洞宾。今天咱们不聊炼丹,不聊修仙,来聊聊Java里那些能装东西的”百宝箱”——集合框架。就像我吕洞宾的葫芦能装仙丹一样,Java的集合框架能装各种数据,而且功能强劲得很!

1. 集合的基本概念

什么是集合?

集合就是用来存放对象的容器,就像我的葫芦能装仙丹一样。在Java中,集合框架是一组用来存储和操作对象组的类和接口。

// 就像我的葫芦能装仙丹
List<String> immortalPills = new ArrayList<>();
immortalPills.add("长生不老丹");
immortalPills.add("飞升丹");
immortalPills.add("隐身丹");

为什么需要集合?

  • 动态大小:数组大小固定,集合可以动态增长
  • 类型安全:泛型保证只能存放指定类型
  • 丰富操作:提供各种便捷的操作方法
  • 算法支持:排序、查找等算法内置

集合框架的层次结构

Collection接口
├── List接口(有序可重复)
│   ├── ArrayList
│   ├── LinkedList
│   └── Vector
├── Set接口(无序不重复)
│   ├── HashSet
│   ├── TreeSet
│   └── LinkedHashSet
└── Queue接口(队列)
    ├── PriorityQueue
    └── ArrayDeque

Map接口(键值对)
├── HashMap
├── TreeMap
├── LinkedHashMap
└── Hashtable

2. 泛型与类型安全

泛型是什么?

泛型就像给葫芦贴个标签,告知你里面装的是什么仙丹,不会拿错。

// 没有泛型的时候,就像没标签的葫芦
List rawList = new ArrayList();  // 什么都能装
rawList.add("仙丹");
rawList.add(123);  // 数字也能装进去
String pill = (String) rawList.get(1);  // 运行时报错!ClassCastException

// 有泛型的时候,就像贴了标签的葫芦
List<String> labeledList = new ArrayList<>();
labeledList.add("仙丹");
// labeledList.add(123);  // 编译时就报错!安全多了
String safePill = labeledList.get(0);  // 不需要强制转换

泛型的优点:

  1. 类型安全:编译时检查类型,避免运行时错误
  2. 代码简洁:不需要强制类型转换
  3. 代码重用:一套代码处理多种类型

泛型的使用

// 自定义泛型类
class ImmortalBag<T> {
    private T content;
    
    public void put(T item) {
        this.content = item;
    }
    
    public T take() {
        return content;
    }
}

// 使用泛型类
ImmortalBag<String> pillBag = new ImmortalBag<>();
pillBag.put("九转金丹");
String pill = pillBag.take();  // 不需要转换

ImmortalBag<Integer> powerBag = new ImmortalBag<>();
powerBag.put(9999);
int power = powerBag.take();

3. 迭代器(Iterator)

迭代器是什么?

迭代器就像我的拂尘,能一件件取出葫芦里的仙丹,不会漏掉也不会重复。

List<String> pills = new ArrayList<>();
pills.add("筑基丹");
pills.add("金丹");
pills.add("元婴丹");

// 使用迭代器遍历
Iterator<String> iterator = pills.iterator();
while (iterator.hasNext()) {
    String pill = iterator.next();
    System.out.println("取出:" + pill);
}

// 更简单的for-each循环(底层也是用迭代器)
for (String pill : pills) {
    System.out.println("服用:" + pill);
}

迭代器的特点:

  1. 统一访问:所有集合都可以用一样方式遍历
  2. 安全删除:可以在遍历时安全删除元素
  3. 单向访问:一般只能向前遍历

迭代器的使用场景

List<String> dangerousPills = new ArrayList<>();
dangerousPills.add("毒丹");
dangerousPills.add("迷魂丹");
dangerousPills.add("毒丹");  // 重复的毒丹

// 删除所有"毒丹"
Iterator<String> it = dangerousPills.iterator();
while (it.hasNext()) {
    if ("毒丹".equals(it.next())) {
        it.remove();  // 安全删除
    }
}
System.out.println("安全丹药:" + dangerousPills);

错误示范:

List<String> pills = new ArrayList<>();
pills.add("仙丹1");
pills.add("仙丹2");

// 错误!在for-each循环中修改集合
for (String pill : pills) {
    if ("仙丹1".equals(pill)) {
        pills.remove(pill);  // 抛出ConcurrentModificationException!
    }
}

4. 链表(LinkedList)

链表是什么?

链表就像一串珍珠项链,每颗珍珠都知道前后是谁,但不知道整串项链有多长。

// 双向链表
LinkedList<String> pillChain = new LinkedList<>();

// 添加元素
pillChain.add("第一颗仙丹");
pillChain.addFirst("最前面的仙丹");  // 加到开头
pillChain.addLast("最后面的仙丹");   // 加到结尾
pillChain.add(1, "中间的仙丹");     // 加到指定位置

// 访问元素
String first = pillChain.getFirst();  // 获取第一个
String last = pillChain.getLast();    // 获取最后一个

// 删除元素
pillChain.removeFirst();  // 删除第一个
pillChain.removeLast();   // 删除最后一个

链表的优缺点:

优点:

  1. 插入删除快:在中间插入删除只要改前后指针,O(1)时间复杂度
  2. 动态大小:不需要预先分配空间
  3. 内存灵活:不需要连续内存空间

缺点:

  1. 访问慢:要访问第n个元素,需要从头遍历,O(n)时间复杂度
  2. 内存开销大:每个元素都要存储前后指针

链表 vs 数组列表

// 测试插入性能
List<Integer> arrayList = new ArrayList<>();
List<Integer> linkedList = new LinkedList<>();

// 在开头插入 - LinkedList更快
long start = System.nanoTime();
for (int i = 0; i < 10000; i++) {
    linkedList.add(0, i);  // 在开头插入
}
long linkedListTime = System.nanoTime() - start;

start = System.nanoTime();
for (int i = 0; i < 10000; i++) {
    arrayList.add(0, i);  // 在开头插入 - 需要移动所有元素!
}
long arrayListTime = System.nanoTime() - start;

System.out.println("LinkedList用时:" + linkedListTime);
System.out.println("ArrayList用时:" + arrayListTime);

5. 堆栈(Stack)

堆栈是什么?

堆栈就像叠盘子,后放上去的先拿走(LIFO:Last In First Out)。

// Java中的Stack类(不推荐使用)
Stack<String> plateStack = new Stack<>();
plateStack.push("第一个盘子");  // 入栈
plateStack.push("第二个盘子");
plateStack.push("第三个盘子");

String topPlate = plateStack.peek();  // 查看栈顶,不取出
System.out.println("栈顶:" + topPlate);  // 输出:第三个盘子

String removed = plateStack.pop();  // 出栈
System.out.println("取出:" + removed);  // 输出:第三个盘子

// 更推荐使用Deque作为栈
Deque<String> betterStack = new ArrayDeque<>();
betterStack.push("仙丹1");
betterStack.push("仙丹2");
String pill = betterStack.pop();

堆栈的特点:

  1. 后进先出:最后进入的元素最先出去
  2. 只能操作栈顶:不能直接访问中间元素
  3. 常用操作:push(入栈), pop(出栈), peek(查看栈顶)

堆栈的应用场景

// 1. 括号匹配检查
public boolean checkParentheses(String expression) {
    Deque<Character> stack = new ArrayDeque<>();
    
    for (char c : expression.toCharArray()) {
        if (c == '(' || c == '[' || c == '{') {
            stack.push(c);
        } else if (c == ')' && (stack.isEmpty() || stack.pop() != '(')) {
            return false;
        } else if (c == ']' && (stack.isEmpty() || stack.pop() != '[')) {
            return false;
        } else if (c == '}' && (stack.isEmpty() || stack.pop() != '{')) {
            return false;
        }
    }
    return stack.isEmpty();
}

// 2. 撤销操作(像Photoshop的撤销功能)
Deque<String> history = new ArrayDeque<>();
history.push("初始状态");
history.push("第一次修改");
history.push("第二次修改");

// 撤销
String current = history.pop();  // 回到"第一次修改"

6. 队列(Queue)

队列是什么?

队列就像排队买仙丹,先来的先买到(FIFO:First In First Out)。

// 普通队列
Queue<String> pillQueue = new LinkedList<>();
pillQueue.offer("张三");  // 入队
pillQueue.offer("李四");
pillQueue.offer("王五");

String first = pillQueue.peek();  // 查看队首,不取出
System.out.println("队首:" + first);  // 输出:张三

String served = pillQueue.poll();  // 出队
System.out.println("服务:" + served);  // 输出:张三

// 双端队列(两头都能进出)
Deque<String> deque = new ArrayDeque<>();
deque.offerFirst("前面加入");  // 从前面加入
deque.offerLast("后面加入");   // 从后面加入
deque.pollFirst();  // 从前面取出
deque.pollLast();   // 从后面取出

队列的种类:

  1. 普通队列:先进先出
  2. 优先级队列:按优先级出队
  3. 双端队列:两头都能进出
  4. 阻塞队列:队列空时取会阻塞,队列满时加会阻塞

优先级队列(PriorityQueue)

// 按仙丹的品级排队
class ImmortalPill {
    String name;
    int grade;  // 品级,数字越小品级越高
    
    public ImmortalPill(String name, int grade) {
        this.name = name;
        this.grade = grade;
    }
}

// 使用优先级队列
PriorityQueue<ImmortalPill> pillQueue = new PriorityQueue<>(
    Comparator.comparingInt(p -> p.grade)
);

pillQueue.offer(new ImmortalPill("普通仙丹", 3));
pillQueue.offer(new ImmortalPill("极品仙丹", 1));
pillQueue.offer(new ImmortalPill("上品仙丹", 2));

// 按品级从高到低取出
while (!pillQueue.isEmpty()) {
    ImmortalPill pill = pillQueue.poll();
    System.out.println("服用:" + pill.name + "(品级:" + pill.grade + ")");
}
// 输出:
// 服用:极品仙丹(品级:1)
// 服用:上品仙丹(品级:2)
// 服用:普通仙丹(品级:3)

7. 映射(Map)

映射是什么?

映射就像我的仙丹目录,通过名字就能找到对应的仙丹。

// HashMap - 最常用的Map
Map<String, String> pillCatalog = new HashMap<>();

// 添加键值对
pillCatalog.put("长生丹", "吃了能长生不老");
pillCatalog.put("隐身丹", "吃了能隐身");
pillCatalog.put("飞升丹", "吃了能直接飞升");

// 获取值
String effect = pillCatalog.get("长生丹");
System.out.println("长生丹效果:" + effect);

// 检查是否存在
boolean hasPill = pillCatalog.containsKey("隐身丹");

// 遍历所有仙丹
for (Map.Entry<String, String> entry : pillCatalog.entrySet()) {
    System.out.println(entry.getKey() + " -> " + entry.getValue());
}

// 只遍历键
for (String pillName : pillCatalog.keySet()) {
    System.out.println("有仙丹:" + pillName);
}

// 只遍历值
for (String effectDesc : pillCatalog.values()) {
    System.out.println("有效果:" + effectDesc);
}

Map的实现类比较:

特性

HashMap

TreeMap

LinkedHashMap

Hashtable

顺序

无序

按键排序

插入顺序

无序

线程安全

允许null键

性能

O(1)

O(log n)

O(1)

O(1)

底层

数组+链表/红黑树

红黑树

数组+链表+双向链表

数组+链表

HashMap的工作原理

// HashMap的简单模拟
class SimpleHashMap<K, V> {
    // 内部是数组+链表
    private Entry<K, V>[] table;
    
    static class Entry<K, V> {
        K key;
        V value;
        Entry<K, V> next;  // 链表解决哈希冲突
        
        Entry(K key, V value) {
            this.key = key;
            this.value = value;
        }
    }
    
    // 计算哈希值,决定放在数组的哪个位置
    private int hash(K key) {
        return key.hashCode() % table.length;
    }
    
    public void put(K key, V value) {
        int index = hash(key);
        // 如果该位置有冲突,就用链表连接
    }
}

HashMap的重大致念:

  1. 哈希函数:把键转换成数组下标
  2. 哈希冲突:不同键算出一样下标
  3. 链表法:用链表解决冲突
  4. 红黑树:Java 8后,链表过长转红黑树
  5. 扩容:元素太多时扩大数组

TreeMap(有序映射)

// TreeMap会按键的自然顺序排序
Map<String, Integer> pillStock = new TreeMap<>();
pillStock.put("筑基丹", 100);
pillStock.put("元婴丹", 50);
pillStock.put("金丹", 200);
pillStock.put("仙丹", 10);

// 按键的字典序输出
for (Map.Entry<String, Integer> entry : pillStock.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue() + "颗");
}
// 输出:
// 仙丹: 10颗
// 筑基丹: 100颗
// 元婴丹: 50颗
// 金丹: 200颗

// 自定义排序
Map<String, Integer> customOrder = new TreeMap<>(
    (a, b) -> b.compareTo(a)  // 倒序
);

LinkedHashMap(保持插入顺序)

// 保持插入顺序的Map
Map<String, String> pillHistory = new LinkedHashMap<>();
pillHistory.put("第一天", "炼了筑基丹");
pillHistory.put("第二天", "炼了金丹");
pillHistory.put("第三天", "炼了元婴丹");

// 按插入顺序遍历
for (Map.Entry<String, String> entry : pillHistory.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}
// 输出:
// 第一天: 炼了筑基丹
// 第二天: 炼了金丹
// 第三天: 炼了元婴丹

8. 集合的最佳实践

选择正确的集合

// 根据需求选择集合
public class CollectionChooser {
    // 1. 需要快速随机访问
    public List<String> getFastAccessList() {
        return new ArrayList<>();  // O(1)访问
    }
    
    // 2. 需要频繁插入删除
    public List<String> getFrequentModifyList() {
        return new LinkedList<>();  // O(1)插入删除
    }
    
    // 3. 需要去重
    public Set<String> getUniqueSet() {
        return new HashSet<>();  // 自动去重
    }
    
    // 4. 需要排序
    public Set<String> getSortedSet() {
        return new TreeSet<>();  // 自动排序
    }
    
    // 5. 需要键值对
    public Map<String, Integer> getKeyValueMap() {
        return new HashMap<>();  // 快速查找
    }
    
    // 6. 需要线程安全
    public List<String> getThreadSafeList() {
        return Collections.synchronizedList(new ArrayList<>());
    }
}

性能思考

// 不同操作的性能比较
public void performanceTest() {
    int size = 100000;
    
    // ArrayList vs LinkedList
    List<Integer> arrayList = new ArrayList<>();
    List<Integer> linkedList = new LinkedList<>();
    
    // 在末尾添加 - 两者都快
    long start = System.currentTimeMillis();
    for (int i = 0; i < size; i++) {
        arrayList.add(i);
    }
    System.out.println("ArrayList添加:" + (System.currentTimeMillis() - start));
    
    start = System.currentTimeMillis();
    for (int i = 0; i < size; i++) {
        linkedList.add(i);
    }
    System.out.println("LinkedList添加:" + (System.currentTimeMillis() - start));
    
    // 随机访问 - ArrayList快许多
    start = System.currentTimeMillis();
    for (int i = 0; i < size; i++) {
        arrayList.get(i);
    }
    System.out.println("ArrayList访问:" + (System.currentTimeMillis() - start));
    
    start = System.currentTimeMillis();
    for (int i = 0; i < size; i++) {
        linkedList.get(i);  // LinkedList需要遍历!
    }
    System.out.println("LinkedList访问:" + (System.currentTimeMillis() - start));
}

线程安全思考

// 非线程安全的HashMap在多线程下的问题
Map<String, Integer> unsafeMap = new HashMap<>();

// 多个线程同时修改会出问题
Runnable task = () -> {
    for (int i = 0; i < 1000; i++) {
        unsafeMap.put("key" + i, i);
    }
};

Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();

// 可能的结果:
// 1. 数据丢失
// 2. 死循环(Java 7之前)
// 3. 程序崩溃

// 解决方案1:使用ConcurrentHashMap
Map<String, Integer> safeMap = new ConcurrentHashMap<>();

// 解决方案2:使用Collections.synchronizedMap
Map<String, Integer> synchronizedMap = 
    Collections.synchronizedMap(new HashMap<>());

总结一下

道友们,Java的集合框架就像我的百宝箱,里面装满了各种宝贝:

  1. List:像我的丹药清单,有序可重复,想找第几个就找第几个
  2. Set:像我的法宝库,每个法宝都是唯一的,不会重复
  3. Queue:像排队领仙丹,先来先得,讲究顺序
  4. Map:像我的仙丹目录,通过名字就能找到对应的仙丹

使用提议:

  • 需要快速随机访问用 ArrayList
  • 需要频繁插入删除用 LinkedList
  • 需要去重用 HashSet
  • 需要排序用 TreeSet
  • 需要键值对用 HashMap
  • 需要保持插入顺序用 LinkedHashMap
  • 需要线程安全用 ConcurrentHashMap

记住我吕洞宾的话:选对集合,事半功倍;用错集合,事倍功半。根据你的需求选择合适的集合,就像根据病情选择合适的仙丹一样重大!

© 版权声明

相关文章

暂无评论

none
暂无评论...