《编写高质量Java》(八)

七十、子列表只是原列表的一个视图

List接口提供了subList方法,其作用是返回一个列表的子列表。来看下subList的源码:

public List subList(int fromIndex, int toIndex){
    return ( this instanceof RandomAccess ?
    new RandomAccessSubList(this, fromIndex, toIndex) :
    new SubList(this, fromIndex, toIndex));
}

subList方法是由AbstractList来实现的,它会根据是不是可以随机存取来提供不同的SubList实现方式,不过随机存储的使用频率比较高,而且RandomAccessSubList也是SubList的子类,所以所有的操作都是由SubList来实现的。我们来看看SubList的源码:

class SubList extends AbstractList{
    private AbstractList l;
    private int offset;
    
    SubList(AbstractList list, int fromIndex, int toIndex){
        l = list;
        offset = fromIndex;
        size = toIndex - fromIndex;
    }

    public E get(int index){
        return l.get(index + offset);
    }

    public void add(int index, E element){
        l.add(index + offset, element);
    }
    /*其他代码省略*/
}

通过阅读这段代码, 我们就非常清楚subList的实现原理:它返回的SubList类也是AbstractList子类,其所有的方法get,set,add,remove等都是在原始列表上的操作,它自身并没有生成一个数组或者链表,也就是子列表只是原列表的一个视图(View),所有的修改动作都反映在了原列表上。

七十一、推荐使用subList处理局部列表

例如,一个列表有100个元素,要删除索引位置为20~30的元素。可以直接使用下面一行代码即可实现:

list.subList(20, 30).clear();

七十二、生成子列表后不要再操作原列表

生成子列表后,修改原列表,则子列表的size方法会出现并发修改异常,这是因为:subList取出的列表是原始列表的一个视图,原数据集修改了,但是subList取出的子列表不会重新生成一个新列表(这点与数据库视图是不相同的),后面在对子列表进行操作时,就会检测到修改计数器与预期的不相同,于是就抛出了并发修改异常。
出现这个问题的最终原因还是在子列表提供的size方法的检查上,来看看size源码:

public int size(){
    checkForComodification();
    return size;
}

private void checkForComodification(){
    if(l.modCount != expectedModCount){
        throw new ConcurrentModificationException();
    }
}

expectedModCount是从什么地方来的呢?它是在SubList子列表的构造函数中赋值的,其值等于生成子列表时的修改次数(modCount 变量)。因此在生成子列表后再去修改原列表,l.modCount就必然比expectedModCount大1,不再保持相等了,于是就抛出了异常。

对于子列表操作,因为视图是动态生成的,生成子列表后再操作原列表,必然会导致“视图”的不稳定,最有效的办法就是通过Collections.unmodifiableList();设置列表为只读状态。

七十三、使用Comparator进行排序

在Java中要实现对数据排序有两种方式,一种是实现Comparable接口,一种是实现Comparator接口,两者的不同:

  • 实现了Comparable接口的类表明自身是可比较的,有了比较才能进行排序。
  • 而Comparator接口是一个工具类接口,它的名字也已经表明了他的作用:用作比较,它与原有类的逻辑没有关系,只是实现两个类的比价逻辑。

所以,一个类可以有很多的比较器,只要有业务需求就可以产生比较器,有比较器就可以产生N多种排序,而Comparable接口的排序只能说是实现类的默认排序算法。

七十四、不推荐使用binarySearch对列表进行检索

二分查找的一个前提是:数据集已经实现升序排列,否则二分法查找到的值是不准确的。使用binarySearch的二分法查找比indexOf的遍历算法性能上高很多,特别是在大数据集而且目标又快接近尾部时,binarySearch方法与indexOf相比,性能上会提升几十倍。从性能的角度考虑时可以选择binarySearch。

七十五、集合中的元素必须做到compareTo和equals同步

  • indexOf依赖equals方法查找,binarySearch则依赖compareTo方法查找。
  • equals是判断元素是否相等,compareTo是判断元素在排序中的位置是否相同
    实现了compareTo方法,就应该覆写equals方法,确保两者同步。

七十六、集合运算时使用更优雅的方式

  • 并集(合集):list1.addAll(list2)
  • 交集:list1.retainAll(list2)
  • 差集:list1.removeAll(list2)
  • 无重复的并集:list2.removeAll(list1);list1.addAll(list2)

七十七、使用shuffle打乱列表

Collections.shuffle(**)可以打乱一个列表的顺序。shuffle一般很少用到,使用场景如下:

  • 可以用在程序的“伪装”上
  • 可以用在抽奖程序中
  • 可以用在安全传输方面

七十八、减少HashMap中元素的数量

用HashMap作为数据集容器,偶尔会出现内存溢出的问题,这经常与HashMap有关。

  • HashMap比ArrayList多了一次封装,把String类型的键值对转换成Entry对象后再放入数组。
  • HashMap的size大于数组容量的0.75倍时,就开始扩容。

七十九、集合中的哈希码不要重复

在一个列表中查找某值是非常耗费资源的,随机存取的列表是遍历查找,顺序存储列表是链表查找,或者是Collections的二分法查找,但这都不够快,毕竟都是遍历。最快的还是以Hash开头的集合(如HashMap、HashSet等类)。
HashMap中的hashCode应避免冲突

八十、多线程使用Vector或HashTable

Vector是ArrayList的多线程版本,HashTable是HashMap的多线程版本。
基本上所有的集合都有一个叫做快速失败的校验值机,当一个集合在被多个线程修改和访问时,就可能出现ConcurrentModificationException异常,这是为了确保集合方法一致而设置的保护措施,它的实现原理就是modCount修改计数器:在读列表时,如果modCount发生变化(也就是有其他线程修改)则会抛出ConcurrentModificationException异常。这与线程同步是两码事,线程同步是为了保护集合中的数据不被脏读、脏写而设置的。

八十一、非稳定排序推荐使用List

Set和List最大的区别在于Set中的元素不可以重复。在Set的实现类中有一个比较常用的类需要了解一下:TreeSet。该类实现了类默认排序为升序的Set集合,如果插入一个元素,默认会按照升序排序。
SortedSet接口(TreeSet实现了该接口)只是定义了在给集合加入元素时将其进行排序,并不能保证元素修改后的排序结果,因此TreeSet适用于不变量的集合排序,但不适用于可变量的排序,特别是不确定何时元素会发生变化的数据集合。

八十二、由点及面,一叶知秋——集合大家族

你可能感兴趣的:(《编写高质量Java》(八))