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

来源:互联网 发布:java 替换字符串 编辑:程序博客网 时间:2024/06/05 07:23
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 0
原创粉丝点击