# Java - 集合使用注意事项
# 集合判空
《阿里巴巴 Java 开发手册》的描述如下
判断所有集合内部的元素是否为空, 使用 isEmpty ()
方法, 而不是 size () == 0
的方式
原因
isEmpty ()
方法可读性更好isEmpty ()
方法时间复杂度为O (1)
, 而size ()
方法有很多复杂度大于O (1)
# 集合转 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));
// 空指针异常
bookList.stream().collect(Collectors.toMap(Person::getName, Person::getPhoneNumber));
2
3
4
5
6
7
8
9
10
11
原因
首先, 我们来看 java.util.stream.Collectors
类的 toMap ()
方法, 可以看到其内部调用了 Map
接口的 merge ()
方法
public static <T, K, U, M extends Map<K, U>>
Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction,
Supplier<M> mapSupplier) {
// 调用 merge () 方法
BiConsumer<M, T> accumulator
= (map, element) -> map.merge(keyMapper.apply(element),
valueMapper.apply(element), mergeFunction);
return new CollectorImpl<>(mapSupplier, accumulator, mapMerger(mergeFunction), CH_ID);
}
2
3
4
5
6
7
8
9
10
11
Map
接口的 merge ()
方法如下, 这个方法是接口中的默认实现
default V merge(K key, V value,
BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
Objects.requireNonNull(remappingFunction);
// 调用 requireNonNull () 方法判断 value 是否为空
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;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
merge ()
方法会先调用 Objects.requireNonNull ()
方法判断 value
是否为空
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}
2
3
4
5
# 集合遍历
《阿里巴巴 Java 开发手册》的描述如下
不要在 foreach
循环里进行元素的 remove / add
操作. remove
元素请使用 Iterator
方式, 如果并发操作, 需要对 Iterator
对象加锁
原因
foreach
循环底层还是依赖 Iterator
. 但 remove () / add ()
方法还是直接调用集合自己的方法, 而非 Iterator
的 remove () / add ()
方法
这就导致 Iterator
莫名其妙地发现自己有元素被 remove / add
, 然后, 它就会抛出一个 ConcurrentModificationException
来提示用户发生了并发修改异常. 这就是单线程状态下产生的 fail-fast
机制
小贴士
fail-fast
机制 : 多个线程对 fail-fast
集合进行修改的时候, 可能会抛出 ConcurrentModificationException
. 即使是单线程下也有可能会出现这种情况
解决方法
Java 8
开始, 可以使用Collection#removeIf ()
方法删除满足条件的元素List<Integer> list = new ArrayList<>(); for (int i = 1; i <= 10; ++i) { list.add(i); } // 删除list中的所有偶数 list.removeIf(filter -> filter % 2 == 0); // [1, 3, 5, 7, 9] System.out.println(list);
1
2
3
4
5
6
7
8
9Iterator
遍历操作使用普通的
for
循环使用
fail-safe
的集合类.java.util
包下的所有类都是fail-fast
的, 而java.util.concurrent
包下的所有类都是fail-safe
的
# 集合去重
《阿里巴巴 Java 开发手册》的描述如下
可以利用 Set
元素唯一的特性, 可以快速对一个集合进行去重操作, 避免使用 List
的 contains ()
进行遍历去重或者判断包含操作.
例子
// Set 去重代码示例
public static <T> Set<T> removeDuplicateBySet(List<T> data) {
if (CollectionUtils.isEmpty(data)) {
return new HashSet<>();
}
return new HashSet<>(data);
}
// List 去重代码示例
public static <T> List<T> removeDuplicateByList(List<T> data) {
if (CollectionUtils.isEmpty(data)) {
return new ArrayList<>();
}
List<T> result = new ArrayList<>(data.size());
for (T current : data) {
if (!result.contains(current)) {
result.add(current);
}
}
return result;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
两者的核心差别在于 contains ()
方法的实现
HashSet
中contains ()
方法
HashSet
的 contains ()
方法底部依赖的 HashMap
的 containsKey ()
方法, 时间复杂度接近于 O (1)
(没有出现哈希冲突的时候为 O (1)
)
private transient HashMap<E,Object> map;
public boolean contains(Object o) {
return map.containsKey(o);
}
2
3
4
我们有 N
个元素插入进 Set
中, 那时间复杂度就接近是 O (n)
ArrayList
的contains ()
方法
ArrayList
的 contains()
方法是通过遍历所有元素的方法来做的,时间复杂度接近是 O(n)。
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
我们的 List
有 N
个元素, 那时间复杂度就接近是 O (n ^ 2)
# 集合转数组
《阿里巴巴 Java 开发手册》的描述如下
使用集合转数组的方法, 必须使用集合的 toArray (T[] array)
, 传入的是类型完全一致, 长度为 0 的空数组
toArray (T[] array)
方法的参数是一个泛型数组, 如果 toArray
方法中没有传递任何参数的话返回的是 Object
类型数组
String [] s= new String[]{
"dog", "lazy", "a", "over", "jumps", "fox", "brown", "quick", "A"
};
List<String> list = Arrays.asList(s);
Collections.reverse(list);
// 没有指定类型的话会报错
s = list.toArray(new String[0]);
2
3
4
5
6
7
原因
由于 JVM
优化,new String[0]
作为 Collection.toArray ()
方法的参数现在使用更好, new String[0]
就是起一个模板的作用, 指定了返回数组的类型, 0 是为了节省空间, 因为它只是为了说明返回的类型
详细说明
Arrays of Wisdom of the Ancients (opens new window)
# 数组转集合
《阿里巴巴 Java 开发手册》的描述如下
使用工具类 Arrays.asList ()
把数组转换成集合时, 不能使用其修改集合相关的方法, 它的 add () / remove () / clear ()
方法会抛出 UnsupportedOperationException
异常
介绍
Arrays.asList ()
在平时开发中还是比较常见的, 我们可以使用它将一个数组转换为一个 List
集合
String[] myArray = {"Apple", "Banana", "Orange"};
List<String> myList = Arrays.asList(myArray);
// 上面两个语句等价于下面一条语句
List<String> myList = Arrays.asList("Apple","Banana", "Orange");
2
3
4
JDK
源码对于这个方法的说明
/**
* 返回由指定数组支持的固定大小的列表. 此方法作为基于数组和基于集合的 API 之间的桥梁,
* 与 Collection.toArray ()结合使用. 返回的 List 是可序列化并实现 RandomAccess 接口
*/
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
2
3
4
5
6
7
注意事项
Arrays.asList ()
是泛型方法, 传递的数组必须是对象数组, 而不是基本类型int[] myArray = {1, 2, 3}; List myList = Arrays.asList(myArray); // 1 System.out.println(myList.size()); // 数组地址值 System.out.println(myList.get(0)); // 报错 : ArrayIndexOutOfBoundsException System.out.println(myList.get(1)); int[] array = (int[]) myList.get(0); // 1 System.out.println(array[0]);
1
2
3
4
5
6
7
8
9
10
11当传入一个原生数据类型数组时,
Arrays.asList ()
的真正得到的参数就不是数组中的元素, 而是数组对象本身. 此时List
的唯一元素就是这个数组, 这也就解释了上面的代码我们使用包装类型数组就可以解决这个问题
使用集合的修改方法 :
add()
,remove()
,clear()
会抛出异常List myList = Arrays.asList(1, 2, 3); // 运行时报错 : UnsupportedOperationException myList.add(4); // 运行时报错 : UnsupportedOperationException myList.remove(1); // 运行时报错 : UnsupportedOperationException myList.clear();
1
2
3
4
5
6
7Arrays.asList ()
方法返回的并不是java.util.ArrayList
, 而是java.util.Arrays
的一个内部类, 这个内部类并没有实现集合的修改方法或者说并没有重写这些方法List myList = Arrays.asList(1, 2, 3); //class java.util.Arrays$ArrayList System.out.println(myList.getClass());
1
2
3
如何正确的将数组转为
ArrayList
手动实现工具类
//JDK 1.5+ static <T> List<T> arrayToList(final T[] array) { final List<T> l = new ArrayList<T>(array.length); for (final T s : array) { l.add(s); } return l; } Integer [] myArray = { 1, 2, 3 }; System.out.println(arrayToList(myArray).getClass());//class java.util.ArrayList
1
2
3
4
5
6
7
8
9
10
11
12
13最简便的方法
List list = new ArrayList<>(Arrays.asList("a", "b", "c"))
1Stream
(Java 8
, 推荐)Integer [] myArray = { 1, 2, 3 }; List myList = Arrays.stream(myArray).collect(Collectors.toList()); // 基本类型也可以实现转换(依赖 boxed 的装箱操作) int [] myArray2 = { 1, 2, 3 }; List myList = Arrays.stream(myArray2).boxed().collect(Collectors.toList());
1
2
3
4
5Guava
不可变集合, 使用
ImmutableList
(opens new window) 类及其of ()
(opens new window) 与copyOf ()
(opens new window) 工厂方法 : (参数不能为空)// from varargs List<String> il = ImmutableList.of("string", "elements"); // from array List<String> il = ImmutableList.copyOf(aStringArray);
1
2
3
4可变集合, 使用
List
(opens new window) 类及其newArrayList
(opens new window) 工厂方法 :// from collection List<String> l1 = Lists.newArrayList(anotherListOrCollection); // from array List<String> l2 = Lists.newArrayList(aStringArray); // from varargs List<String> l3 = Lists.newArrayList("or", "string", "elements");
1
2
3
4
5
6
Apache Commons Collections
List<String> list = new ArrayList<String>(); CollectionUtils.addAll(list, str);
1
2List.of ()
方法 (Java 9
)Integer[] array = {1, 2, 3}; List<Integer> list = List.of(array);
1
2