阿里Java编程规约【五】 集合处理

  1. 【强制】关于 hashCode 和 equals 的处理,遵循如下规则:
      1) 只要重写 equals ,就必须重写 hashCode 。
      2) 因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须重写这两个方法。
      3) 如果自定义对象做为 Map 的键,那么必须重写 hashCode 和 equals 。
    说明: String 重写了 hashCode 和 equals 方法,所以我们可以非常愉快地使用 String 对象作为 key 来使用。

《Effective Java》告诉我们重写hashcode方法的最佳实践方式。
一个好的hashcode方法通常最好是不相等的对象产生不相等的hash值,理想情况下,hashcode方法应该把集合中不相等的实例均匀分布到所有可能的hash值上面。在企业开发中,最好使用第三方库如Apache commons来生成hashocde方法。

  1. 【强制】 ArrayList 的 subList 结果不可强转成 ArrayList ,否则会抛出 ClassCastException异常,即 java.util.RandomAccessSubList cannot be cast to java.util.ArrayList .
    说明: subList 返回的是 ArrayList 的内部类 SubList ,并不是 ArrayList ,而是ArrayList 的一个视图,对于 SubList 子列表的所有操作最终会反映到原列表上。
  • 推荐使用subList处理局部列表
    例如: 删除指定范围的元素 list.subList(20, 30).clear();
  1. 【强制】在 subList 场景中,高度注意对原集合元素个数的修改,会导致子列表的遍历、增加、删除均会产生 ConcurrentModificationException 异常。
  • subList生成子列表后,保持原列表的只读状态。一般subList方法和 Collections.unmodifiableList()配合着使用
  • 如果一定要更改子列表,重新构造新的ArrayList,使用public ArrayList(Collection c)
  1. 【强制】使用集合转数组的方法,必须使用集合的toArray(T[] array),传入的是类型完全一样的数组,大小就是 list.size()
    说明:使用 toArray 带参方法,入参分配的数组空间不够大时, toArray 方法内部将重新分配内存空间,并返回新数组地址 ; 如果数组元素大于实际所需,下标为 [ list.size() ] 的数组元素将被置为 null ,其它数组元素保持原值,因此最好将方法入参数组大小定义与集合元素个数一致。
    正例:
List list = new ArrayList(2);
list.add("guan");
list.add("bao");
String[] array = new String[list.size()];
array = list.toArray(array);

反例:直接使用 toArray 无参方法存在问题,此方法返回值只能是 Object[] 类,若强转其它类型数组将出现 ClassCastException 错误。

  1. 【强制】使用工具类 Arrays.asList() 把数组转换成集合时,不能使用其修改集合相关的方法,它的 add / remove / clear 方法会抛出UnsupportedOperationException 异常。
    说明: asList 的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法。 Arrays.asList体现的是适配器模式,只是转换接口,后台的数据仍是数组。
String[] str = new String[] { "you", "wu" };
List list = Arrays.asList(str);

第一种情况: list.add("yangguanbao"); 运行时异常。
第二种情况: str[0] = "gujin"; 那么 list.get(0) 也会随之修改。

  1. 【强制】泛型通配符来接收返回的数据,此写法的泛型集合不能使用 add 方法,而 不能使用 get 方法,做为接口调用赋值时易出错。
    说明:扩展说一下 PECS(Producer Extends Consumer Super) 原则:
    第一、频繁往外读取内容的,适合用
    第二、经常往里插入的,适合用

  2. 【强制】不要在 foreach 循环里进行元素的 remove / add 操作。 remove 元素请使用 Iterator方式,如果并发操作,需要对 Iterator 对象加锁。

正例:

Iterator iterator = list.iterator();
  while (iterator.hasNext()) {
  String item = iterator.next();
  if (删除元素的条件) {
    iterator.remove();
  }
}

反例:

List list = new ArrayList();
  list.add("1");
  list.add("2");
  for (String item : list) {
    if ("1".equals(item)) {
      list.remove(item);
    }
}

说明:以上代码的执行结果肯定会出乎大家的意料,那么试一下把“1”换成“2”,会是同样的结果吗?

  • 修改一定要使用Iterator。
  • 反例中改成2,抛出ConcurrentModificationException,因为2是数组的结束边界。
  1. 【强制】 在 JDK 7 版本及以上, 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 比较结果相同。

反例:下例中没有处理相等的情况,实际使用中可能会出现异常:

new Comparator() {
    @Override
          public int compare(Student o1, Student o2) {
        return o1.getId() > o2.getId() ? 1 : -1;
    }
};
  1. 【推荐】集合初始化时,指定集合初始值大小。
    说明: HashMap 使用 HashMap(int initialCapacity) 初始化,
    正例: initialCapacity = (需要存储的元素个数 / 负载因子) + 1。注意负载因子(即loader factor)默认为 0.75, 如果暂时无法确定初始值大小,请设置为 16(即默认值)。
    反例: HashMap 需要放置 1024 个元素,由于没有设置容量初始大小,随着元素不断增加,容量 7 次被迫扩大, resize 需要重建 hash 表,严重影响性能。
  • 预估数组大小,能够提高程序效率,写代码的时候脑袋里面要有运行的思想。
  • 想了解性能和容量评估,请参考互联网性能与容量评估的方法论和典型案例。
阿里Java编程规约【五】 集合处理_第1张图片
HashSet 和 HashMap存放数据时采用的数据结构
  1. 【推荐】使用 entrySet遍历 Map 类集合 KV ,而不是 keySet 方式进行遍历。
    说明: keySet 其实是遍历了 2 次,一次是转为 Iterator 对象,另一次是从 hashMap 中取出key 所对应的 value 。而 entrySet 只是遍历了一次就把 key 和 value 都放到了 entry 中,效率更高。
    如果是 JDK 8,使用 Map . foreach 方法。
    正例: values() 返回的是 V 值集合,是一个 list 集合对象 ;keySet() 返回的是 K 值集合,是一个 Set 集合对象 ;entrySet() 返回的是 K - V 值组合集合。

  2. 【推荐】高度注意 Map 类集合 K / V 能不能存储 null 值的情况,如下表格:


    阿里Java编程规约【五】 集合处理_第2张图片

反例: 由于 HashMap 的干扰,很多人认为 ConcurrentHashMap 是可以置入 null 值,而事实上,存储 null 值时会抛出 NPE 异常。

  1. 【参考】合理利用好集合的有序性 (sort) 和稳定性 (order) ,避免集合的无序性 (unsort) 和不稳定性 (unorder) 带来的负面影响。
    说明:
    有序性是指遍历的结果是按某种比较规则依次排列的。
    稳定性指集合每次遍历的元素次序是一定的。如: ArrayList 是 order / unsort;HashMap 是 unorder / unsort;TreeSet 是order / sort

注: 这里HashMap不稳定性是指rehash后输出顺序则会变化。

  1. 【参考】利用 Set 元素唯一的特性,可以快速对一个集合进行去重操作,避免使用 List 的contains 方法进行遍历、对比、去重操作。

如果不需要精确去重,参考布隆过滤器(Bloom Filter)。

总结:
Java 的集合类 API 有很大的选择余地;Java 7 至少提供了 58 个不同的集合类。在编写应用时,选择恰当的集合类,以及恰当地使用集合类,是一个重要的性能考量。
使用集合类的第一条规则是,选择适合应用的算法需求的集合类。该建议并不是特定于 Java 的。LinkedList 不适合做搜索;如果需要访问一段随机的数据,应该将集合保存到 HashMap 中。如果数据需要有序排列,则应使用 TreeMap,而不是尝试在应用中做排序。如果会用索引访问数据,则使用 ArrayList;但如果会频繁地向该数组中间插入数据,则不要使用它,诸如此类。根据算法选择要使用哪个集合类,这非常重要,但是在 Java 中做选择和在其他编程语言中做选择并没有多少区别。
然而在使用 Java 的集合类时,还有一些特殊的地方需要考虑。

  1. 同步还是非同步
    几乎所有的 Java 集合类都是非同步的(主要的例外是 HashtableVector 及与其相关的类)。
    在早期的 Java 版本中,同步——甚至是不存在竞争时的同步——是个很大的性能问题,所以当第一个重大修订版本发布时,集合类框架采用了相反的做法:所有新的集合类默认都是非同步的。即使从那时开始同步性能已经有了显著提高,但仍然不是没有成本的;能够选择非同步的集合类,可以帮助大家编写更快的程序(偶尔会出现因并发修改某个非同步的集合而导致的 bug)。

  2. 设定集合的大小
    ArrayList 类调整数组大小的方法是,在现有基础上增加约一半。所以 elementData 数组的大小最初是 10,然后是 15,22,33,以此类推。不管使用何种算法调整数组大小(参见后面方框内的文字),都会导致一些内存被浪费(这反过来又会影响应用花在执行 GC 上的时间)。此外,每当数组必须调整大小时,都伴随一个成本很高的数组复制操作,将老数组中的内容转移到新数组中。
    要减少这些性能损失,必须尽可能准确地估计一下集合最终的大小,并用这个值来构建集合。

非集合类中的数据扩展
很多非集合类也会在内部数组中保存大量数据。比如,ByteArrayOutputStream类必须把写入到该流中的所有数据保存到一个内部缓冲区中;类似地,StringBuilder 和 StringBuffer 类也必须将所有字符保存到一个内部的字符数组中。
这些类大多会使用同样的算法调整内部数组的大小:需要调整时就加倍。这意味着,平均而言,内部的数组要比当前包含的数据多 25%。
这里的性能考量是相似的:使用的内存量多于 ArrayList 这个例子,需要复制数据的次数要少一些,但原理是一样的。在构建某个对象时,如果可以设置其大小,可以评估一下这个对象最终会保存多少数据,然后选择接受大小参数的那个构造函数。

  1. 集合与内存使用效率
    我们刚看了一个集合的内存使用效率没有达到最佳的例子:在用于保存集合中的元素的底层存储中,往往会浪费一些内存。
    对于元素比较稀疏的集合(只有一两个元素),这存在较大的问题。如果这样的集合用得非常多,则会浪费大量内存。解决方案之一就是在创建集合时设定其大小。另一种方案是,考虑一下这种情况是不是真的需要集合。
    大部分开发者被问及如何快速地排序任意一个数组时,答案都会是快速排序(quicksort)。而好的性能工程师希望了解数组的大小:如果数组足够小,那最快的方式是使用插入排序(insertion sort)。(对于较小的数组来说,基于快速排序的算法通常会使用插入排序;就 Java 而言,Arrays.sort() 方法的实现就假定,少于 47 个元素的数组用插入排序比用快速排序更快。)数组大小至关重要。

附录:


阿里Java编程规约【五】 集合处理_第3张图片
图1:Java 集合的基本架构
阿里Java编程规约【五】 集合处理_第4张图片
表1:实现Set接口的类
阿里Java编程规约【五】 集合处理_第5张图片
表2:实现List接口的类
阿里Java编程规约【五】 集合处理_第6张图片
表3:实现Map接口的类

参考(References)

《码出高效 阿里巴巴Java开发手册 终极版(1.3.0)》
《Java性能权威指南》
《Java技术手册 第6版》
《Java面向对象编程(第2版)》

你可能感兴趣的:(阿里Java编程规约【五】 集合处理)