读改善java程序的151个建议(8)

72.生成子列表后,不要再操作原列表
先看例子:
List < String  > list= new  ArrayList < String >();
              list.add(  "A" );
              list.add(  "B" );
              list.add(  "C" );
              
                List <  String > subList=list.subList(0, 2);
              list.add(  "D" );
              
                System .out. println  ( "list size="  +list.size());
                System .out. print  ( "sublist size="  +subList.size());//这里会的报并发修改异常
为什么会是sublist的并发修改异常呢?这里并没有多线程修操作啊。那是因为sublist是由list的subList方法得到,是原列表list的一个视图,原列表修改了,但subList取出的子列表不会重新生成一个新列表。后面在对sublist取size操作时,会检测到修改计数器与与预期的不同,于是就抛异常了。size()方法的原码中会先进行修改计数器的检测,由于原列表修改,原列表的修改计数器发生了变化,但子列表中仍然是原来的计数器值,所以在不相等时会抛出异常。通过subList方法得到的子列表,调用其他方法时,也会检测修改计数器,例如,set,get,add等方法,若生成子列表后,再修改原列表,子列表再使用这些方法时就会抛异常。
一种有效的就去是,在原列表生成完子列表后,通过Collections.unmodifiableList方法设置原列表的只读状态,这样的话,就可以避免出现异常情况.(一种防御式编程),如:
List<String> list=new ArrayList<String>();
list.add("A");
list.add("B");
list.add("C");
List<String> subList=list.subList(0, 2);
list=Collections.unmodifiableList(list);
list.add("D");//这样的话,在这里会运行异常,提示不支持操作。实际上只能对list进行只读操作,但可对子列表进行写操作
               subList.add("E");//对子列表的修改将反应到原列表

73.使用Comparator进行排序
在java中给数据排序,有身份种实现,一种是comparable接口,一种是实现comparator接口.
java中为什么要有两个排序接口呢?
实现了Comparable接口的类表明自身是可比较的,有了比较才能进行排序;而Comparator接口是一个工具类接口,它的名字(比较器)也已经表明了它的作用:用作比较,它与原有类的逻辑没有关系,只是实现两个类的比较逻辑,从这方面来说,一个类可以有很多的比较器,只要有业务需求就可以产生比较器,有比较器就可以产生N多种排序,而Comparable接口的排序只能说是实现类的默认排序算法,一个类稳定、成熟后其compareTo方法基本不会改变,也就是说一个类只能有一个固定的、由compareTo方法提供的默认排序算法。
实现了Comparable接口的类,在具体实现compareTo方法时,可使用apache工具类的方法,
org.apache.commons.lang3.builder.CompareToBuilder ,eg:
@Override
         public  int  compareTo  ( Employee  obj  ) {
                return  new  CompareToBuilder  (). append  (postion,  obj .postion).  append ( id, obj .id). toComparison  ();
              
       }

public  class  PositionComparator  implements  Comparator <  Employee > {

         @Override
         public  int  compare  ( Employee  o1  ,  Employee  o2  ) {  
              
                return  o1  . getPostion  (). compareTo  ( o2  . getPostion  ());
       }

}
              
总之,Comparable接口可以作为实现类的默认排序法,Comparator接口则是一个类的扩展排序工具

74.不推荐使用binarySearch对列表进行检索
对一个列表进行检索时,我们使用的最多的是indexOf方法,它简单,好用,而且也不会出错 ,虽然它只能检索到第一个符合条件的值,但是我们可以生成子列表后再检索,这样也就可以查找出所有符合条件的值了。Collections工具类也提供了一个检索方法:binarySerach:它是使用二分搜索法搜索指定列表,以获得指定对象,其实现的功能与indexOf是相同的,只是使用的是二分法搜索列表。下面的例子:
         List <  String > cities=  new  ArrayList  < String  >();
              cities.add(  "广州" );
              cities.add(  "北京" );
              cities.add(  "北京" );
              cities.add(  "香港" );
              cities.add(  "香港" );
              
                int  index1=cities.indexOf( "北京"  );
                int  index2= Collections  .binarySearch (cities,  "北京"  );
                System .out. println  ( "index1="  +index1+ ",index2="  +index2);
返回结果一个是1,一个是2.
为什么?问题出在二分法搜索上,二分法搜索就是“折半折半再折半”的搜索方法。在上例中,折半的过程中第一次就遇到了“北京”,恰好对应就是索引2
其实两者算法都没问题,只是我们用错了情景,因为二分法查询的一个首要前提是:数据集已经实现升序排列,否则二分法查找的值是不准确的。不排序怎么确定是在小区中查找还是在大区中查找呢。
在实际业务中,如果我们先对原始数据排序,再使用二分法搜索,那有可能会影响原始数据的位置,如果业务数据与位置无法那还好,如果相关那就需要再拷贝数据再排序了。当然我们也可以直接使用indexOf方法。从性能上来说,binarySerach的二分查找比indexOf的遍历算法性能高很多,特别是在大数据集而且目标值又接近尾部时。
总之,根据实际业务场景综合考虑。

75:集合中的元素必须做到compareTo和equals同步
主要是要理解两点:
indexOf依赖equals方法查找,binarySearch则依赖compareTo方法查找。
equals是判断元素是否相等,compareTo是判断元素在排序中的位置是否相同。
即然一个是决定排序位置,一个是决定相等,那我们就应该保证当排序位置相同时,其equals也相同,否则会逻辑混乱。
注意:实现了compareTo方法,就应该覆写equals方法 ,确保两者同步。

76:集合运算时,使用更优雅的方式
并集:list1.addAll(list2);
交集:list1.retainAll(list2);
差集:list1.removeAll(list2);
无重复并集:list2.removeAll(list1);list1.addAll(list2);

77.使用shuffle打乱列表
Collections.shuffle

78.减少HashMap中元素的数量
本建议主要其实主要是基于对HashMap底组数据结构进行理解。
其实是Entry类型的数组,往HashMap中添加元素时,会根据key值hash算法得到对应的索引值,即确定在Entry类型数组中的位置,将key与value转为Entry对象,存入数组中(或更新已有entry)
在往HashMap中新增对象时,还会涉及到扩容的问题,HashMap的无参构造函数,通过查看源码可知容量默认是16(长度永远是2的N次幂),装载因子是0.75,当Entry[]数组长度达到12的时候,将会进行2倍的扩容(resize),扩容在大数据量的情况下,会有性能风险。因些我认为,在具体业务中如果能确定业务数据的大小,则最好指定hashmap的初始容量等。(此处还有更多的知识,参看源码 )
此处不得不说说ArrayList的扩容,当大于初始容量时,ArrayList才会进行扩容,扩容不是2倍,而是1.5倍+1 源码中( i * 3 / 2 + 1) 


79.集合中的哈希码不要重复
在这一建议中,更进一步详细讲解了hashmap的存储结构。
首先一个问题,建议78中提到是通过对key进行hash得到值,再定位在在table中的位置,那hashmap是如何来做到避免哈希冲突呢
我们先来看一下源码(往HashMap中添加元素)
         public  V  put  ( K  paramK ,  V  paramV ) {
                if  (  paramK  ==  null )
                       return  putForNullKey  ( paramV  );
                int  i = hash(  paramK .hashCode());
                int  j = indexFor(i,  this .table.length);
                for  ( Entry  localEntry =  this .table[j]; localEntry !=  null ; localEntry = localEntry.next) {
                       Object  localObject1;
                       if  ((localEntry. hash != i)
                                  || (((localObject1 = localEntry. key) !=  paramK ) && (!( paramK
                                                .equals(localObject1)))))
                             continue ;
                       Object  localObject2 = localEntry.value;
                     localEntry. value =  paramV ;
                     localEntry.  recordAccess ( this  );
                       return  localObject2;
              }
                this .modCount += 1;
                addEntry (i,  paramK ,  paramV , j);
                return  null  ;
       }

上面的方法中调用到的hash()与indexFor()方法源码如下:
     static  int  hash  ( int  paramInt ) {
                paramInt  ^=  paramInt  >>> 20 ^  paramInt  >>> 12;
                return  ( paramInt  ^  paramInt  >>> 7 ^  paramInt  >>> 4);
       }
     得到唯一的hashCode
         static  int  indexFor  ( int  paramInt1 ,  int  paramInt2 ) {
                return  ( paramInt1  &  paramInt2  - 1);
       }
     再通过indexFor 做与运算得到在数组中的位置。(这两个方法有一定的深度)
简单的说,hash方法和indexFor方法就是把哈希码转变成数组的下标。
但是,正是因为经过indexFor方法中的与运算,来得到数组中的位置,有可能存在冲突的可能,即对于一个固定的哈希算法f(k),允许出现f(k1)=f(k2),但是k1不等于k2的情况,也就是说两个不同的Entry,可能产生相同的哈希码,HashMap是如何处理这种冲突问题的呢?答案是通过链表,每个链值对都是一个Entry,其中每个Entry都有一个next变量,也就是说它会指向下一个键值对--很明显,这应该是一个单向链表,该链表是由addEntry方法完成的。其源代码如下:
void  addEntry  ( int  paramInt1 ,  K  paramK ,  V  paramV ,  int  paramInt2 ) {
                Entry  localEntry =  this .table [ paramInt2  ];
                this .table[ paramInt2 ] =  new  Entry ( paramInt1 ,  paramK ,  paramV , localEntry);
                if  (  this .size++ <  this .threshold)
                       return ;
                resize (2 *  this .table.length);
       }
这段程序涵盖两个业务逻辑:如果新加入的键值对的hashCode是唯一的,那直接插入到数组中,Entry的next值则为null;如果新加入的键值对的hashCode与其他元素冲突,则替换掉数组中的当前值,并把新加入的Entry的next变量指向被替换掉的元素--于是,一个链表就生成了。
HashMap存储结构图:


总之,HashMap的存储主要还是数组,遇到哈希冲突的时候则使用链表解决。因此,在HashMap的查找中,如果HashMap中的哈希码相同,它的查找效果与ArrayList没什么两样。所以此建议提出,HashMap中的hashCode应避免冲突。

80.多线程使用Vector 或 HashTable
Vector是ArrayList的多线程版本,HashTable中HashMap的多线程版本。
这里首先要区分两个概念:线程安全,同步修改异常
基本上所有的集合类都有一个叫做快速失败(Fail-Fast)的校验机制,当一个集合在被多个线程修改并访问时,就可能会出现ConcurrentModificationException异常,这是为了确保集合方法一致而设置的保护措施,它的实现原理就是我们经常提到的modCount修改计数器:如果在读列表时,modCount发生变化(也就是有其他线程修改)则会抛出此异常。这与线程同步是两码事,线程同步是为了保护集合中的数据不被脏读、脏写而设置的。
ArrayList修改为Vector,因为Vector的每个方法前都加上了synchronized关键字,同时只会允许一个线程进入该方法,确保了程序的可靠性。虽然我们在系统开发中我们一再说明,除非必要,否则不要使用synchronized,这是从性能的角度考虑的,但是一旦涉及多线程时(注意这里说的是真正的多线程,不是并发修改的问题,比如一个线程增加,一个线程删除,这不属于多线程的范畴),Vector会是最佳选择,当然自己在程序中加synchronized也是可行的方法。
HashMap的线程安全类HashTable与此相同

81.非稳定排序推荐使用List
public  static  void  main ( String []  args ){
                SortedSet <  Person > persons=  new  TreeSet  < Person  >();
              persons.add(  new  Person  (180));
              persons.add(  new  Person  (175));
              
                for ( Person  p:persons){
                       System .out. println  (p. getHeigh  ()); //先输出175,再输出180
              }
              
              persons.first().  setHeigh (185);
              
                for ( Person  p:persons){
                       System .out. println  (p. getHeigh  ()); //先输出185,再输出180
              }
              
              persons=  new  TreeSet  < Person  >( new  ArrayList < Person >(persons));
              
                for ( Person  p:persons){
                       System .out. println  (p. getHeigh  ()); //先输出180,再输出185
              }
       }

对于不变量的排序,例如直接量(也就是8个基本类型)、String类型等,推荐使用TreeSet,而对于可变量,例如我们自己写的类,可能会在逻辑处理中中改变其排序关键值的,则建议使用List自行排序。
如果用用List解决排序问题,就需要自行解决元素重得利问题(若要剔除也很简单,转变为HashSet,剔除后再转回来),若采用TreeSet,则需要解决元素修改后的排序问题。
总之,SortedSet中的元素被修改后可能会影响其排序位置

82.集合大家族
可划分为以下几类:
1)List
实现List接口的集合主要有:ArrayList,LinkedList,Vector,Stack,其中ArrayList是一个动态数组,LinkedList是一个双向链表,Vector是一个线程安全的动态数组,Stack是一个对象栈,遵循先进后出的原则
2)set
set是不包含重复元素的集合,其主要的实现类有:EnumSet,HashSet,TreeSet,其中EnumSet是枚举类型的专用Set,所有元素都是枚举类型;HashSet是以哈希码决定其元素位置的Set,其原理与HashMap相似,它提供快速的插入和查找方法,TreeSet是一个自动排序的Set,它实现了SortedSet接口
3)Map
Map是一个大家族,它可以分为排序Map和非排序Map,排序Map主要是TreeMap类,它根据Key值进行自动排序;非排序Map主要包括:HashMap,HashTable,Properties,EnumMap等,其中Properties是HashTable的子类,它的主要用途是从Property文件中加载数据,并提供方便的读写操作;EnumMap则是要求其key必须是某一个枚举类型。
Map中还有一个WeakHashMap
4)Queue
队列,它分为两类,一类是阻塞式队列,队列满了以后再插入元素则会抛出异常,主要包括:ArrayBlockingQueue、PriorityBlockingQueue、LinkedBlockingQueue,其中ArrayBlockingQueue是一个以数组方式实现的有界阻塞队列,PriorityBlockingQueue是依照优先级组建的队列,LinkedBlockingQueue是通过链表实现的阻塞队列;另一类是非阻塞队列,无边界的,只要内存允许,都可以持续追加元素,我们最经常使用的PriorityQueue类。
还有一种队列,是双端队列,支持在头、尾两端插入和移除元素,它的主要实现类是:ArrayDeque、LinkedBlockingDeque、LinkedList
5)数组
数组与集合的最大区别就是数组能够容纳基本类型,而集合就不行 ,更重要的一点就是所有的集合底层存储的都是数组
6)工具类
数组工具类是java.util.Arrays和java.lang.reflect.Array,集合工具类是java.util.Collections
7)扩展类
Apache的common-collecitons扩展包
Google的google-collections扩展包

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值