全文参考阿里手册,并补充一定的正例和反例,方便理解
【强制】关于 hashCode 和 equals 的处理,遵循如下规则:
1)只要覆写 equals,就必须覆写 hashCode。
2)因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须覆写这两种方法。
3)如果自定义对象作为 Map 的键,那么必须覆写 hashCode 和 equals。
说明: String 因为覆写了 hashCode 和 equals 方法, 所以可以愉快地将 String 对象作为 key 来使用。
正例:
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
public class Person {
private String name;
private int age;
// 构造方法
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 覆写 equals 方法
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
// 覆写 hashCode 方法
@Override
public int hashCode() {
return Objects.hash(name, age);
}
// Getters 和 Setters
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public static void main(String[] args) {
// 创建 Person 对象
Person person1 = new Person("Alice", 30);
Person person2 = new Person("Bob", 25);
Person person3 = new Person("Alice", 30);
// 使用 Person 对象作为 Map 的键
Map<Person, String> personMap = new HashMap<>();
personMap.put(person1, "Developer");
personMap.put(person2, "Designer");
// 输出 Map 的内容
System.out.println("Person Map:");
for (Map.Entry<Person, String> entry : personMap.entrySet()) {
System.out.println(entry.getKey().getName() + " - " + entry.getValue());
}
// 使用 Person 对象存储在 Set 中
Set<Person> personSet = new HashSet<>();
personSet.add(person1);
personSet.add(person2);
personSet.add(person3); // 与 person1 相同
// 输出 Set 的内容
System.out.println("\nPerson Set:");
for (Person person : personSet) {
System.out.println(person.getName() + " - " + person.getAge());
}
}
}
【强制】判断所有集合内部的元素是否为空,使用 isEmpty() 方法,而不是 size() == 0 的方式。
说明:在某些集合中,前者的时间复杂度为 O(1),而且可读性更好。
正例:
Map<String, Object> map = new HashMap<>(16);
if (map.isEmpty()) {
System.out.println("no element in this map.");
}
【强制】在使用 java.util.stream.Collectors 类的 toMap() 方法转为 Map 集合时,一定要使用参数类型为 BinaryOperator,参数名为 mergeFunction 的方法,否则当出现相同 key 时会抛出IllegalStateException 异常。
说明:参数 mergeFunction 的作用是当出现 key 重复时,自定义对 value 的处理策略。
正例:
List<Pair<String, Double>> pairArrayList = new ArrayList<>(3);
pairArrayList.add(new Pair<>("version", 12.10));
pairArrayList.add(new Pair<>("version", 12.19));
pairArrayList.add(new Pair<>("version", 6.28));
// 生成的 map 集合中只有一个键值对:{version=6.28}
Map<String, Double> map = pairArrayList.stream()
.collect(Collectors.toMap(Pair::getKey, Pair::getValue, (v1, v2) -> v2));
反例:
String[] departments = new String[]{"RDC","RDC","KKB"};
// 抛出 IllegalStateException 异常
Map<Integer, String> map = Arrays.stream(departments)
.collect(Collectors.toMap(String::hashCode, str -> str));
【强制】在使用 java.util.stream.Collectors 类的 toMap() 方法转为 Map 集合时,一定要注意当 value为 null 时会抛 NPE 异常。
说明:在 java.util.HashMap 的 merge 方法里会进行如下的判断:
if (value == null || remappingFunction == null)
throw new NullPointerException();
正例:
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
public class Main {
public static void main(String[] args) {
List<Pair<String, Double>> pairArrayList = new ArrayList<>(2);
pairArrayList.add(new ImmutablePair<>("version1", 8.3));
pairArrayList.add(new ImmutablePair<>("version2", null));
// 方法1:过滤掉 value 为 null 的元素
Map<String, Double> map = pairArrayList.stream()
.filter(pair -> pair.getValue() != null)
.collect(Collectors.toMap(Pair::getKey, Pair::getValue));
// 打印结果
System.out.println("方法1 - 过滤掉 null 值后的 Map: " + map);
// 方法2:提供默认值替代 null
Map<String, Double> mapWithDefault = pairArrayList.stream()
.collect(Collectors.toMap(
Pair::getKey,
pair -> pair.getValue() != null ? pair.getValue() : 0.0, // 将 null 值替换为默认值 0.0
(v1, v2) -> v2));
// 打印结果
System.out.println("方法2 - 使用默认值替代 null 值后的 Map: " + mapWithDefault);
}
}
反例:
List<Pair<String,Double>> pairArrayList = new ArrayList<>(2);
pairArrayList.add(new Pair<>("version1",8.3));
pairArrayList.add(new Pair<>("version2",null));
// 抛出 NullPointerException 异常
Map<String,Double> map = pairArrayList.stream()
.collect(Collectors.toMap(Pair::getKey,Pair::getValue, (v1,v2) -> v2));
【强制】ArrayList 的 subList 结果不可强转成 ArrayList,否则会抛出 ClassCastException 异常:java.util.RandomAccessSubList cannot be cast to java.util.ArrayList。
说明:subList() 返回的是 ArrayList 的内部类 SubList,并不是 ArrayList 本身,而是 ArrayList 的一个视图,对于SubList 的所有操作最终会反映到原列表上。
【强制】使用 Map 的方法 keySet() / values() / entrySet() 返回集合对象时,不可以对其进行添加元素操作,否则会抛出 UnsupportedOperationException 异常
【强制】Collections 类返回的对象,如:emptyList() / singletonList() 等都是 immutable list,不可对其进行添加或者删除元素的操作。
反例:如果查询无结果,返回 Collections.emptyList() 空集合对象,调用方一旦在返回的集合中进行了添加元素的操作,就会触发 UnsupportedOperationException 异常
【强制】在 subList 场景中,高度注意对父集合元素的增加或删除,均会导致子列表的遍历、增加、删除产生 ConcurrentModificationException 异常。
说明:抽查表明,90% 的程序员对此知识点都有错误的认知。
反例:
import java.util.ArrayList;
import java.util.List;
public class SubListExample {
public static void main(String[] args) {
List<String> parentList = new ArrayList<>();
parentList.add("A");
parentList.add("B");
parentList.add("C");
parentList.add("D");
parentList.add("E");
// 创建子列表
List<String> subList = parentList.subList(1, 4);
System.out.println("子列表: " + subList);
// 对父集合进行结构性修改
parentList.add("F");
// 尝试遍历子列表
try {
for (String item : subList) {
System.out.println(item);
}
} catch (ConcurrentModificationException e) {
System.out.println("捕获到 ConcurrentModificationException 异常: " + e.getMessage());
}
// 尝试在子列表中增加元素
try {
subList.add("G");
} catch (ConcurrentModificationException e) {
System.out.println("捕获到 ConcurrentModificationException 异常: " + e.getMessage());
}
}
}
正例:
import java.util.ArrayList;
import java.util.List;
public class SubListExampleCorrect {
public static void main(String[] args) {
List<String> parentList = new ArrayList<>();
parentList.add("A");
parentList.add("B");
parentList.add("C");
parentList.add("D");
parentList.add("E");
// 创建子列表
List<String> subList = parentList.subList(1, 4);
System.out.println("子列表: " + subList);
// 正确的处理方式是:不要在创建子列表后修改父集合
// 如果需要修改父集合,重新创建子列表
parentList.add("F");
// 重新创建子列表
subList = parentList.subList(1, 4);
// 遍历子列表
for (String item : subList) {
System.out.println(item);
}
// 在子列表中增加元素
subList.add("G");
System.out.println("修改后的子列表: " + subList);
System.out.println("修改后的父集合: " + parentList);
}
}
【强制】使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入的是类型完全一致、长度为0 的空数组。
反例:直接使用 toArray 无参方法存在问题,此方法返回值只能是 Object[]类,若强转其它类型数组将出现
ClassCastException 错误。
正例:
List<String> list = new ArrayList<>(2);
list.add("guan");
list.add("bao");
String[] array = list.toArray(new String[0]);
说明:使用 toArray 带参方法,数组空间大小的 length:
1)等于 0,动态创建与 size 相同的数组,性能最好。
2)大于 0 但小于 size,重新创建大小等于 size 的数组,增加 GC 负担。
3)等于 size,在高并发情况下,数组创建完成之后,size 正在变大的情况下,负面影响与 2 相同。
4)大于 size,空间浪费,且在 size 处插入 null 值,存在 NPE 隐患。
【强制】使用 Collection 接口任何实现类的 addAll() 方法时,要对输入的集合参数进行 NPE 判断。
说明:在 ArrayList#addAll 方法的第一行代码即 Object[] a = c.toArray();其中 c 为输入集合参数,如果为 null,则直接抛出异常。
正例:
public static <T> Collection<T> union(Collection<T> coll1, Collection<T> coll2) {
final ArrayList<T> list = new ArrayList<>();
if (isEmpty(coll1)) {
list.addAll(coll2);
} else if (isEmpty(coll2)) {
list.addAll(coll1);
} else {
final Map<T, Integer> map1 = countMap(coll1);
final Map<T, Integer> map2 = countMap(coll2);
final Set<T> elts = newHashSet(coll2);
elts.addAll(coll1);
int m;
for (T t : elts) {
m = Math.max(Convert.toInt(map1.get(t), 0), Convert.toInt(map2.get(t), 0));
for (int i = 0; i < m; i++) {
list.add(t);
}
}
}
return list;
}
【强制】使用工具类 Arrays.asList() 把数组转换成集合时,不能使用其修改集合相关的方法,它的 add/ remove / clear 方法会抛出 UnsupportedOperationException 异常
说明:asList 的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法。Arrays.asList 体现的是适配器模式,只是转换接口,后台的数据仍是数组。
String[] str = new String[]{ "yang","guan","bao" };
List list = Arrays.asList(str);
第一种情况:list.add(“yangguanbao”); 运行时异常。
第二种情况:str[0] = “change”; list 中的元素也会随之修改,反之亦然
【强制】泛型通配符<? extends T>来接收返回的数据,此写法的泛型集合不能使用 add 方法,而<? super T>不能使用 get 方法,两者在接口调用赋值的场景中容易出错。
说明:扩展说一下 PECS(Producer Extends Consumer Super) 原则,
生产者(Producer Extends),当你需要从集合中读取数据时候,适合用<? extends T>:表示可以安全的从集合中安全地读取 **T**
类型及其子类的数据,但不能向集合中添加 **T**
类型的数据(除了 **null**
),表示保证从集合中读取的数据至少是 **T**
类型。
消费者(Consumer Super),当你需要向集合写入数据时候,适合用<? super T>:表示可以向集合中安全地添加 **T**
类型及其父类的数据,但从集合中读取的数据只能保证是 **Object**
类型,表示集合中添加的数据至少是 **T**
类型。
正例:
生产者(Producer Extends):
import java.util.List;
public class Example {
public static void processAnimals(List<? extends Animal> animals) {
for (Animal animal : animals) {
animal.sound(); // 可以调用 Animal 类的方法
}
}
public static void main(String[] args) {
List<Dog> dogs = List.of(new Dog(), new Dog());
List<Cat> cats = List.of(new Cat(), new Cat());
processAnimals(dogs); // 可以传入 List<Dog>
processAnimals(cats); // 可以传入 List<Cat>
}
}
class Animal {
void sound() {
System.out.println("Animal sound");
}
}
class Dog extends Animal {
@Override
void sound() {
System.out.println("Bark");
}
}
class Cat extends Animal {
@Override
void sound() {
System.out.println("Meow");
}
}
消费者(Consumer Super):
import java.util.List;
import java.util.ArrayList;
public class Example {
public static void addDogs(List<? super Dog> animals) {
animals.add(new Dog()); // 可以安全地添加 Dog 类型
}
public static void main(String[] args) {
List<Animal> animals = new ArrayList<>();
addDogs(animals); // 可以传入 List<Animal>
for (Animal animal : animals) {
animal.sound(); // 可以调用 Animal 类的方法
}
}
}
class Animal {
void sound() {
System.out.println("Animal sound");
}
}
class Dog extends Animal {
@Override
void sound() {
System.out.println("Bark");
}
}
在无泛型限制定义的集合赋值给泛型限制的集合时,在使用集合元素时,需要进行instanceof 判断,避免抛出 ClassCastException 异常。
说明:毕竟泛型是在 JDK5 后才出现,考虑到向前兼容,编译器是允许非泛型集合与泛型集合互相赋值。
反例:
List<String> generics = null;
List notGenerics = new ArrayList(10);
notGenerics.add(new Object());
notGenerics.add(new Integer(1));
generics = notGenerics;
// 此处抛出 ClassCastException 异常
String string = generics.get(0)
【强制】不要在 foreach 循环里进行元素的 remove / add 操作。remove 元素请使用 iterator 方式,如果并发操作,需要对 iterator 对象加锁。
正例:
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (删除元素的条件) {
iterator.remove();
}
}
反例:
for (String item : list) {
if ("1".equals(item)) {
list.remove(item);
}
}
说明:反例中的执行结果肯定会出乎大家的意料,那么试一下把“1”换成“2”会是同样的结果吗?
【强制】在 JDK7 版本及以上,Comparator 实现类要满足如下三个条件,不然 Arrays.sort,Collections.sort 会抛 IllegalArgumentException 异常。
说明:三个条件如下
1)x,y 的比较结果和 y,x 的比较结果相反。
2)x > y,y > z,则 x > z。
3)x = y,则 x,z 比较结果和 y,z 比较结果相同。
正例:
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + '}';
}
}
public class PersonComparator implements Comparator<Person> {
@Override
public int compare(Person p1, Person p2) {
// 自反性: 比较同一对象返回 0
if (p1 == p2) return 0;
// 按年龄排序
return Integer.compare(p1.getAge(), p2.getAge());
}
@Override
public boolean equals(Object obj) {
return this == obj;
}
}
反例:下例中没有处理相等的情况,交换两个对象判断结果并不互反,不符合第一个条件,在实际使用中可能会出现异常。
new Comparator<Student>() {
@Override
public int compare(Student o1,Student o2) {
return o1.getId() > o2.getId() ? 1 : -1;
}
};
【强制】高度注意 Map 类集合 K / V 能不能存储 null 值的情况,如下表格:
反例: 由于 HashMap 的干扰, 很多人认为 ConcurrentHashMap 是可以置入 null 值, 而事实上, 存储 null 值时会抛出 NPE 异常。
【推荐】泛型集合使用时,在 JDK7 及以上,使用 diamond 语法或全省略。
说明:菱形泛型,即 diamond,直接使用<>来指代前边已经指定的类型。
正例:
// diamond 方式,即<>
HashMap<String,String> userCache = new HashMap<>(16);
// 全省略方式
ArrayList<User> users = new ArrayList(10);
【推荐】集合初始化时,指定集合初始值大小。
说明:HashMap 使用构造方法 HashMap(int initialCapacity) 进行初始化时,如果暂时无法确定集合大小,那么指定默认值(16)即可。
正例:
initialCapacity = (需要存储的元素个数 / 负载因子) + 1。注意负载因子(即 loaderfactor)默认为 0.75, 如果
暂时无法确定初始值大小, 请设置为 16(即默认值)。
反例:
HashMap 需要放置 1024 个元素,由于没有设置容量初始大小,随着元素增加而被迫不断扩容,resize() 方法
总共会调用 8 次,反复重建哈希表和数据迁移。当放置的集合元素个数达千万级时会影响程序性能。
【推荐】使用 entrySet 遍历 Map 类集合 KV,而不是 keySet 方式进行遍历。
说明:keySet 其实是遍历了 2 次,一次是转为 Iterator 对象,另一次是从 hashMap 中取出 key 所对应的 value。 而entrySet 只是遍历了一次就把 key 和 value 都放到了 entry 中, 效率更高。 如果是 JDK8, 使用Map.forEach 方法。
keySet() 返回的是 K 值集合, 是一个 Set 集合对象;
values() 返回的是 V 值集合, 是一个 list 集合对象;
entrySet() 返回的是 K-V 值组合的 Set 集合。
正例:
import java.util.HashMap;
import java.util.Map;
public class EntrySetExample {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("Alice", 30);
map.put("Bob", 25);
map.put("Charlie", 35);
// 使用 entrySet 遍历 Map
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
}
}
}
JDK8以上:
import java.util.HashMap;
import java.util.Map;
public class ForEachExample {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("Alice", 30);
map.put("Bob", 25);
map.put("Charlie", 35);
// 使用 Map.forEach 方法遍历 Map
map.forEach((key, value) -> {
System.out.println("Key: " + key + ", Value: " + value);
});
}
}
反例:
import java.util.HashMap;
import java.util.Map;
public class KeySetExample {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("Alice", 30);
map.put("Bob", 25);
map.put("Charlie", 35);
// 使用 keySet 遍历 Map (低效)
for (String key : map.keySet()) {
System.out.println("Key: " + key + ", Value: " + map.get(key));
}
}
}
【参考】合理利用好集合的有序性(sort)和稳定性(order) ,避免集合的无序性(unsort)和不稳定性(unorder)带来的负面影响。
说明:有序性是指遍历的结果是按某种比较规则依次排列的,稳定性指集合每次遍历的元素次序是一定的。如:
ArrayList 是 order / unsort;HashMap 是 unorder / unsort;TreeSet 是 order / sort。
【参考】利用 Set 元素唯一的特性,可以快速对一个集合进行去重操作,避免使用 List 的
contains() 进行遍历去重或者判断包含操作。