今天聊聊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); // 不需要强制转换
泛型的优点:
- 类型安全:编译时检查类型,避免运行时错误
- 代码简洁:不需要强制类型转换
- 代码重用:一套代码处理多种类型
泛型的使用
// 自定义泛型类
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);
}
迭代器的特点:
- 统一访问:所有集合都可以用一样方式遍历
- 安全删除:可以在遍历时安全删除元素
- 单向访问:一般只能向前遍历
迭代器的使用场景
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(); // 删除最后一个
链表的优缺点:
优点:
- 插入删除快:在中间插入删除只要改前后指针,O(1)时间复杂度
- 动态大小:不需要预先分配空间
- 内存灵活:不需要连续内存空间
缺点:
- 访问慢:要访问第n个元素,需要从头遍历,O(n)时间复杂度
- 内存开销大:每个元素都要存储前后指针
链表 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();
堆栈的特点:
- 后进先出:最后进入的元素最先出去
- 只能操作栈顶:不能直接访问中间元素
- 常用操作: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(); // 从后面取出
队列的种类:
- 普通队列:先进先出
- 优先级队列:按优先级出队
- 双端队列:两头都能进出
- 阻塞队列:队列空时取会阻塞,队列满时加会阻塞
优先级队列(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的重大致念:
- 哈希函数:把键转换成数组下标
- 哈希冲突:不同键算出一样下标
- 链表法:用链表解决冲突
- 红黑树:Java 8后,链表过长转红黑树
- 扩容:元素太多时扩大数组
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的集合框架就像我的百宝箱,里面装满了各种宝贝:
- List:像我的丹药清单,有序可重复,想找第几个就找第几个
- Set:像我的法宝库,每个法宝都是唯一的,不会重复
- Queue:像排队领仙丹,先来先得,讲究顺序
- Map:像我的仙丹目录,通过名字就能找到对应的仙丹
使用提议:
- 需要快速随机访问用 ArrayList
- 需要频繁插入删除用 LinkedList
- 需要去重用 HashSet
- 需要排序用 TreeSet
- 需要键值对用 HashMap
- 需要保持插入顺序用 LinkedHashMap
- 需要线程安全用 ConcurrentHashMap
记住我吕洞宾的话:选对集合,事半功倍;用错集合,事倍功半。根据你的需求选择合适的集合,就像根据病情选择合适的仙丹一样重大!
© 版权声明
文章版权归作者所有,未经允许请勿转载。
相关文章
暂无评论...