在Java开发里,数据结构的选择往往直接决定了程序的运行效率和可维护性。JDK自带的集合框架已经覆盖了绝大多数业务场景,但从ArrayList到ConcurrentHashMap,它们的内部机制和适用条件差别很大。如果只凭习惯随便选,很容易在大数据量或高并发时出现性能问题。

一、线性表结构:ArrayList与LinkedList
ArrayList是基于动态数组实现的线性表,在内存中占用连续空间。这种结构让它能通过下标在常数时间访问任意元素,但在中间位置插入或删除时,需要移动后续所有元素,开销随数据量线性增长。当数组容量不足时,ArrayList会创建一个更大的数组并拷贝原有数据,因此预先通过构造函数指定容量能减少扩容带来的损耗。
LinkedList采用双向链表存储,每个节点包含前驱和后继指针。它在头部或尾部增删元素非常高效,只需要修改相邻节点的引用,但随机访问必须从链表头或尾遍历到目标位置,时间复杂度为O(n)。下面的代码展示了两者在遍历方式上的差异:
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
public class ListDemo {
public static void main(String[] args) {
List<Integer> arrayList = new ArrayList<>();
List<Integer> linkedList = new LinkedList<>();
// ArrayList随机访问高效
for (int i = 0; i < 100000; i++) {
arrayList.add(i);
}
int val = arrayList.get(50000); // 直接定位
// LinkedList随机访问低效
for (int i = 0; i < 100000; i++) {
linkedList.add(i);
}
int slowVal = linkedList.get(50000); // 需要遍历
}
}
从实践角度看,除非频繁在表头做插入删除,否则应优先使用ArrayList。LinkedList除了遍历慢,还会因为节点对象额外存储指针而增加内存占用。在JDK 8之后,即便使用增强for循环,LinkedList的迭代器仍然要逐个跳转节点,无法像数组那样利用CPU缓存预取。
二、映射结构:HashMap与TreeMap
HashMap是最常用的键值对结构,底层在JDK 8中由数组加链表加红黑树组成。它通过键的hashCode计算桶位置,理想情况下增删查改都能接近O(1)。当多个键哈希冲突且链表长度超过8时,链表会转为红黑树,将最坏情况的时间复杂度从O(n)降到O(log n)。需要注意的是,作为键的对象必须正确重写hashCode和equals方法,否则会出现逻辑上相等的键被当成不同键处理。
TreeMap基于红黑树实现,会自动按照键的自然顺序或指定的比较器排序。它适合需要范围查询或按顺序遍历键的场景,但每次插入和删除都要维持树的平衡,单步操作成本为O(log n)。下面示例展示两者排序行为的不同:
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
public class MapDemo {
public static void main(String[] args) {
Map<String, Integer> hashMap = new HashMap<>();
hashMap.put("banana", 3);
hashMap.put("apple", 1);
hashMap.put("pear", 2);
System.out.println(hashMap); // 顺序不确定
Map<String, Integer> treeMap = new TreeMap<>();
treeMap.put("banana", 3);
treeMap.put("apple", 1);
treeMap.put("pear", 2);
System.out.println(treeMap); // 按字母顺序输出
}
}
在选择时,如果只关心快速查找而不要求顺序,HashMap是默认选项。当业务需要获取最小键、最大键或进行区间统计,比如计算某时间段内的订单,TreeMap会更合适。另外HashMap允许一个null键,TreeMap若使用自然排序则不支持null键,否则会抛出空指针异常。
三、线程安全场景下的数据结构
在多线程环境中,普通的HashMap或ArrayList都可能因并发修改产生数据不一致甚至死循环。虽然可以用Collections.synchronizedMap包装,但那种方式会对整个容器加锁,并发度很低。JDK并发包提供的ConcurrentHashMap采用更细粒度的控制,在JDK 8里利用CAS和 synchronized 锁定单个桶头节点,读操作大多无锁,因此吞吐量明显更高。
对于单纯的生产者消费者模型,还可以使用ArrayBlockingQueue或LinkedBlockingQueue等阻塞队列,它们内部已经处理好等待与唤醒逻辑。下面的代码演示了ConcurrentHashMap的基本线程安全用法:
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentDemo {
public static void main(String[] args) throws InterruptedException {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
Runnable task = () -> {
for (int i = 0; i < 100; i++) {
map.merge("count", 1, Integer::sum);
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(map.get("count")); // 输出200
}
}
需要强调的是,ConcurrentHashMap只能保证单个操作的原子性,像先判断存在再插入这种复合操作仍需使用merge或computeIfAbsent等提供的方法。如果业务要求跨多个键的事务一致性,就要考虑引入外部锁或使用数据库事务,而非依赖集合本身。
四、如何根据业务做选型
选型的核心是先明确操作特征:读多写少且需要随机访问,选ArrayList;写多读少且频繁在两端操作,考虑LinkedList或ArrayDeque;键值查找且无序,HashMap最合适;需要排序或范围扫描,用TreeMap;高并发共享数据,优先ConcurrentHashMap或并发队列。同时应关注数据规模,小数据量下不同结构差异不明显,但达到十万或百万级时,时间复杂度带来的差距会被放大数十倍。
此外,编码时应面向List、Map等接口编程,而非具体实现类,这样后续更换结构只需改动实例化处。比如将List<String> list = new ArrayList<>()改为LinkedList,调用方代码无需调整。合理利用JDK提供的数据结构,既能减少重复造轮子,也能借助成熟实现规避大量边界错误。