全网最全的Java集合最佳实践

全文参考阿里手册,并补充一定的正例和反例,方便理解

【强制】关于 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 值的情况,如下表格:

image.png
反例: 由于 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() 进行遍历去重或者判断包含操作。

  • 18
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值