Java集合使用注意事项总结
集合判空
《阿里巴巴 Java 开发手册》的描述如下:
判断所有集合内部的元素是否为空,使用
isEmpty()
方法,而不是size()==0
的方式。
isEmpty()
方法的时间复杂度为 O(1)
,且可读性更高。而部分集合的 size()
方法的时间复杂度可能为 O(n)
,例如 java.util.concurrent
包下的 ConcurrentLinkedQueue
:
public boolean isEmpty() { return first() == null; }
Node<E> first() {
restartFromHead:
for (;;) {
for (Node<E> h = head, p = h, q;;) {
boolean hasItem = (p.item != null);
if (hasItem || (q = p.next) == null) {
updateHead(h, p);
return hasItem ? p : null;
}
else if (p == q) continue restartFromHead;
else p = q;
}
}
}
而 size()
方法则需要遍历整个链表:
public int size() {
int count = 0;
for (Node<E> p = first(); p != null; p = succ(p))
if (p.item != null)
if (++count == Integer.MAX_VALUE)
break;
return count;
}
对于 ConcurrentHashMap
,在 JDK 1.7 中,size()
方法需要遍历每个 Segment
,而 isEmpty()
只需找到第一个不为空的 Segment
。在 JDK 1.8 中,size()
和 isEmpty()
的时间复杂度相同,因为都需要调用 sumCount()
方法。
集合转 Map
《阿里巴巴 Java 开发手册》的描述如下:
在使用
java.util.stream.Collectors
类的toMap()
方法转为Map
集合时,一定要注意当 value 为 null 时会抛 NPE 异常。
示例代码如下:
class Person {
private String name;
private String phoneNumber;
// getters and setters
}
List<Person> bookList = new ArrayList<>();
bookList.add(new Person("jack", "18163138123"));
bookList.add(new Person("martin", null));
// 抛出 NullPointerException
bookList.stream().collect(Collectors.toMap(Person::getName, Person::getPhoneNumber));
Collectors.toMap()
方法内部调用了 Map
接口的 merge()
方法,而 merge()
会检查 value
是否为 null
:
default V merge(K key, V value,
BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
Objects.requireNonNull(remappingFunction);
Objects.requireNonNull(value);
V oldValue = get(key);
V newValue = (oldValue == null) ? value : remappingFunction.apply(oldValue, value);
if (newValue == null) {
remove(key);
} else {
put(key, newValue);
}
return newValue;
}
Objects.requireNonNull(value)
会在 value
为 null
时抛出 NullPointerException
。
集合遍历
《阿里巴巴 Java 开发手册》的描述如下:
不要在 foreach 循环里进行元素的
remove/add
操作。remove 元素请使用Iterator
方式,如果并发操作,需要对Iterator
对象加锁。
在 foreach 循环中,remove/add
操作直接调用集合的方法,而非 Iterator
的 remove
方法,可能会导致 ConcurrentModificationException
。
Java8 引入了 Collection#removeIf()
方法,可以用来删除满足特定条件的元素:
List<Integer> list = new ArrayList<>();
for (int i = 1; i <= 10; ++i) {
list.add(i);
}
list.removeIf(filter -> filter % 2 == 0); // 删除所有偶数
System.out.println(list); // [1, 3, 5, 7, 9]
其他方法:
- 使用普通的
for
循环。 - 使用
java.util.concurrent
包下的 fail-safe 集合类。
集合去重
《阿里巴巴 Java 开发手册》的描述如下:
可以利用
Set
元素唯一的特性,可以快速对一个集合进行去重操作,避免使用List
的contains()
进行遍历去重。
示例代码:
// 使用 Set 去重
public static <T> Set<T> removeDuplicateBySet(List<T> data) {
if (data == null || data.isEmpty()) {
return new HashSet<>();
}
return new HashSet<>(data);
}
// 使用 List 去重
public static <T> List<T> removeDuplicateByList(List<T> data) {
if (data == null || data.isEmpty()) {
return new ArrayList<>();
}
List<T> result = new ArrayList<>(data.size());
for (T current : data) {
if (!result.contains(current)) {
result.add(current);
}
}
return result;
}
HashSet
的 contains()
方法基于哈希表实现,时间复杂度接近 O(1)。而 ArrayList
的 contains()
方法需要遍历整个列表,时间复杂度为 O(n)。
集合转数组
《阿里巴巴 Java 开发手册》的描述如下:
使用集合转数组的方法,必须使用集合的
toArray(T[] array)
,传入的是类型完全一致、长度为 0 的空数组。
示例代码:
List<String> list = Arrays.asList("dog", "cat", "bird");
// 推荐方式
String[] array = list.toArray(new String[0]);
new String[0]
是推荐用法,因为 JVM 的优化使得其性能更优。
数组转集合
《阿里巴巴 Java 开发手册》的描述如下:
使用工具类
Arrays.asList()
把数组转换成集合时,不能使用其修改集合相关的方法。
Arrays.asList()
返回的是 java.util.Arrays.ArrayList
,其 add
、remove
、clear
方法未被重写,因此会抛出 UnsupportedOperationException
:
String[] array = {"a", "b", "c"};
List<String> list = Arrays.asList(array);
list.add("d"); // 抛出 UnsupportedOperationException
解决方法:
- 使用
new ArrayList<>(Arrays.asList(array))
。 - 使用 Java8 的
Stream
:Integer[] array = {1, 2, 3}; List<Integer> list = Arrays.stream(array).collect(Collectors.toList());
- 使用 Guava:
List<String> list = Lists.newArrayList(array);
- 使用 Java9 的
List.of()
:List<String> list = List.of(array);
集合排序
《阿里巴巴 Java 开发手册》的描述如下:
在进行集合排序时,尽量使用
Collections.sort()
或者List.sort()
,并使用 lambda 表达式进行排序规则的定义。
Collections.sort()
和 List.sort()
都是基于 T[]
数组的 mergeSort
或者 TimSort
算法,时间复杂度为 O(n log n)
。List.sort()
是 Java 8 引入的方法,支持直接在集合上进行排序,减少了代码量。
示例代码:
List<Integer> list = Arrays.asList(3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5);
list.sort(Comparator.naturalOrder()); // 升序排序
list.sort((a, b) -> b - a); // 降序排序
对于自定义对象,可以实现 Comparable
接口或者使用 Comparator
:
class Person {
String name;
int age;
// getter and setter
}
List<Person> people = new ArrayList<>();
people.add(new Person("Alice", 30));
people.add(new Person("Bob", 25));
people.add(new Person("Charlie", 35));
// 使用 lambda 表达式排序
people.sort(Comparator.comparingInt(Person::getAge));
使用 LinkedList
和 ArrayList
的区别
《阿里巴巴 Java 开发手册》的描述如下:
使用
ArrayList
和LinkedList
时,应该根据具体的应用场景来选择。
ArrayList
:基于动态数组实现,查找元素的时间复杂度为O(1)
,而插入和删除操作的时间复杂度为O(n)
(特别是头部插入和删除时)。适合频繁进行查找操作的场景。LinkedList
:基于双向链表实现,插入和删除操作的时间复杂度为O(1)
,但是查找操作的时间复杂度为O(n)
。适合频繁进行插入和删除操作的场景。
示例代码:
List<String> arrayList = new ArrayList<>();
arrayList.add("A");
arrayList.add("B");
List<String> linkedList = new LinkedList<>();
linkedList.add("X");
linkedList.add("Y");
// 查找操作
String first = arrayList.get(0); // O(1)
String second = linkedList.get(0); // O(n)
// 插入操作
linkedList.add(0, "Z"); // O(1)
arrayList.add(0, "Z"); // O(n)
选择时应该依据具体需求:
- 如果需要频繁插入或删除元素,使用
LinkedList
。 - 如果需要频繁访问元素,使用
ArrayList
。
使用 Map
的注意事项
《阿里巴巴 Java 开发手册》的描述如下:
在使用
Map
接口时,建议选择合适的实现类,避免使用不适合的实现方式。
HashMap
:是最常用的实现类,提供了快速的查找和插入操作,基于哈希表实现,时间复杂度为O(1)
。TreeMap
:基于红黑树实现,提供了有序的Map
,查找、插入、删除操作的时间复杂度为O(log n)
,适用于需要排序的场景。LinkedHashMap
:继承自HashMap
,维护了元素的插入顺序,适用于需要保持插入顺序的场景。
在多线程环境下,推荐使用 ConcurrentHashMap
,因为它能够提供线程安全的操作,避免使用 Hashtable
,后者性能较差且线程安全性过于严苛。
示例代码:
Map<String, Integer> map = new HashMap<>();
map.put("A", 1);
map.put("B", 2);
map.putIfAbsent("C", 3); // 如果 "C" 不存在,则放入新元素
Integer value = map.getOrDefault("D", 4); // 如果 "D" 不存在,则返回默认值 4
// 使用 LinkedHashMap 保持顺序
Map<String, Integer> linkedMap = new LinkedHashMap<>();
linkedMap.put("A", 1);
linkedMap.put("B", 2);
// 使用 TreeMap 进行有序遍历
Map<String, Integer> treeMap = new TreeMap<>();
treeMap.put("A", 1);
treeMap.put("B", 2);
Set
集合的使用
《阿里巴巴 Java 开发手册》的描述如下:
Set
集合的主要用途是去重,使用时要避免使用不适合的实现类。
HashSet
:基于哈希表实现,查找、插入操作的时间复杂度为O(1)
,适合大多数场景,性能较高。TreeSet
:基于红黑树实现,提供了有序的Set
,查找、插入操作的时间复杂度为O(log n)
,适用于需要排序的场景。LinkedHashSet
:继承自HashSet
,维持元素插入的顺序。
对于元素的去重操作,建议使用 HashSet
,它性能最佳。对于需要排序或需要保持插入顺序的情况,选择 TreeSet
或 LinkedHashSet
。
示例代码:
Set<String> set = new HashSet<>();
set.add("A");
set.add("B");
set.add("A"); // 不会插入重复元素
// 使用 TreeSet 保持有序
Set<String> sortedSet = new TreeSet<>();
sortedSet.add("B");
sortedSet.add("A");
sortedSet.add("C"); // 元素会按字母顺序排列
不要使用 Vector
和 Stack
《阿里巴巴 Java 开发手册》的描述如下:
尽量避免使用
Vector
和Stack
。
Vector
和 Stack
设计上并不符合现代的并发编程需求,Vector
的增长方式是每次增长一倍,容易造成内存浪费,且缺乏现代的线程安全机制。
推荐使用 ArrayList
和 Deque
替代 Vector
和 Stack
,如果需要栈的功能,可以使用 ArrayDeque
或 LinkedList
:
Stack<String> stack = new Stack<>();
stack.push("A");
stack.push("B");
String item = stack.pop(); // 使用 ArrayDeque 替代 Stack
Deque<String> deque = new ArrayDeque<>();
deque.push("A");
deque.push("B");
String itemDeque = deque.pop();
对于线程安全的集合操作,应该使用 ConcurrentLinkedQueue
或 CopyOnWriteArrayList
。
使用 EnumSet
和 EnumMap
《阿里巴巴 Java 开发手册》的描述如下:
当枚举类型的集合或 Map 需要存储大量元素时,优先使用
EnumSet
或EnumMap
,它们的性能更高。
EnumSet
:专为枚举类型设计的集合类,内部使用位向量实现,性能极高,所有操作的时间复杂度均为 O(1)。EnumMap
:专为枚举类型设计的 Map 实现,基于数组实现,性能比其他 Map 实现更高。
示例代码:
// 使用 EnumSet
enum Day {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
EnumSet<Day> weekend = EnumSet.of(Day.SATURDAY, Day.SUNDAY);
EnumSet<Day> allDays = EnumSet.allOf(Day.class);
// 使用 EnumMap
enum Status {
OPEN, IN_PROGRESS, DONE
}
EnumMap<Status, String> statusMap = new EnumMap<>(Status.class);
statusMap.put(Status.OPEN, "Task is open");
statusMap.put(Status.DONE, "Task is completed");
使用 Collections
工具类
《阿里巴巴 Java 开发手册》的描述如下:
在需要线程安全的集合或不可变集合时,优先使用
Collections
工具类提供的相关方法,而不是自己手动实现线程安全。
- 线程安全的集合:
Collections.synchronizedList()
和Collections.synchronizedMap()
提供了线程安全的集合实现,但它们性能不如java.util.concurrent
包下的集合。
List<String> list = Collections.synchronizedList(new ArrayList<>());
Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
- 不可变集合:
Collections.unmodifiableList()
和其他类似方法返回的集合是不可变的,任何修改操作都会抛出UnsupportedOperationException
。
List<String> immutableList = Collections.unmodifiableList(Arrays.asList("A", "B", "C"));
immutableList.add("D"); // 抛出 UnsupportedOperationException
使用 Concurrent
集合
《阿里巴巴 Java 开发手册》的描述如下:
在多线程环境中,优先使用
java.util.concurrent
包下的集合类。
java.util.concurrent
包提供了一系列线程安全的集合实现,避免了 synchronized
的手动加锁操作,同时具有更高的性能:
ConcurrentHashMap
:线程安全的哈希表,替代Hashtable
。CopyOnWriteArrayList
:适用于读多写少的场景。ConcurrentLinkedQueue
:高效的非阻塞队列。LinkedBlockingQueue
和ArrayBlockingQueue
:阻塞队列,用于生产者-消费者模型。
示例代码:
// 使用 ConcurrentHashMap
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
map.put("A", "Value1");
map.putIfAbsent("B", "Value2");
// 使用 CopyOnWriteArrayList
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("Item1");
list.addIfAbsent("Item2");
// 使用 LinkedBlockingQueue
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
queue.put("Task1");
String task = queue.take();
集合初始化
《阿里巴巴 Java 开发手册》的描述如下:
初始化集合时,指定集合初始大小,避免集合扩容影响性能。
在使用 ArrayList
和 HashMap
等集合时,如果没有指定初始容量,当集合达到容量上限时会触发扩容操作,影响性能。因此,应根据数据量预估集合的初始大小:
// 初始化时指定容量
List<String> list = new ArrayList<>(100);
Map<String, String> map = new HashMap<>(128);
扩容的代价如下:
ArrayList
:默认容量为 10,每次扩容为当前容量的 1.5 倍。HashMap
:默认容量为 16,每次扩容为当前容量的 2 倍。
避免在循环中创建集合对象
《阿里巴巴 Java 开发手册》的描述如下:
避免在循环中创建集合对象,尤其是在大规模数据处理的场景下,可能会导致频繁的 GC。
不正确的写法:
for (int i = 0; i < 10000; i++) {
List<String> list = new ArrayList<>(); // 每次循环都创建新对象
list.add("Value" + i);
}
正确的写法:
List<List<String>> result = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
List<String> list = new ArrayList<>();
list.add("Value" + i);
result.add(list); // 集中存储,避免频繁 GC
}
总结
通过合理选择和使用集合类,可以有效提升代码的性能和可维护性。在编码过程中:
- 根据场景选择合适的集合实现(如
ArrayList
和LinkedList
)。 - 注意线程安全问题,优先使用
java.util.concurrent
包下的集合类。 - 避免在循环中创建集合对象,尽量指定集合初始容量。
- 在需要不可变或线程安全集合时,使用
Collections
工具类。
以上注意事项能够帮助开发者编写更高效、更安全的代码,同时避免常见的陷阱和性能问题。