七十、子列表只是原列表的一个视图
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适用于不变量的集合排序,但不适用于可变量的排序,特别是不确定何时元素会发生变化的数据集合。