數(shù)組和集合——讀《編寫高質量代碼:改善Java程序的151個建議》(五)

讀書,收獲,分享
建議后面的五角星僅代表筆者個人需要注意的程度。
Talk is cheap.Show me the code

建議60:性能考慮,數(shù)組是首選★★★☆☆

在Java中由于數(shù)組沒有List、Set、Map這些集合類用起來方便,于是數(shù)組在實際的系統(tǒng)開發(fā)中用得越來越少了,我們通常只有在閱讀一些開源項目時才會看到它們的身影。但是在基本類型處理方面,數(shù)組還是占優(yōu)勢的,而且集合類的底層也都是通過數(shù)組實現(xiàn)的。

如例:

    //對數(shù)組求和
    public static int sum1(int[] arr) {
        int sum = 0;
        for (int i = 0; i < arr.length; i++) {
            sum += arr[i];
        }
        return sum;
    }
    //對列表求和
    public static int sum2(List<Integer> list) {
        int sum = 0;
        for (int i = 0; i < list.size(); i++) {
            //此處做了自動拆箱操作
            sum += list.get(i);
        }
        return sum;
    }
//所以,效率:sum1() > sum2();
//在實際測試中發(fā)現(xiàn):對基本類型進行求和計算時,數(shù)組的效率是集合的10倍。

基本類型是在棧內(nèi)存中操作的,而對象則是在堆內(nèi)存中操作的,棧內(nèi)存的特點是速度快,容量小,堆內(nèi)存的特點是速度慢,容量大(從性能上來講,基本類型的處理占優(yōu)勢)。

注意:性能要求較高的場景中使用數(shù)組替代集合。

建議61:若有必要,使用變長數(shù)組★★☆☆☆

Java中的數(shù)組是定長的,經(jīng)過初始化聲明就不可改變長度,這在實際使用中非常不方便。但是可以通過對數(shù)組擴容“婉轉”地解決該問題,代碼如下:

public static <T> T[] expandCapacity(T[] datas, int newLen) {
        newLen = newLen < 0 ? 0 : newLen;
        //生成一個新數(shù)組,并拷貝原值
        return Arrays.copyOf(datas, newLen);
}

注意:在實際開發(fā)中,如果確實需要變長的數(shù)據(jù)集,數(shù)組也是在考慮范圍之內(nèi)的,不能因固定長度而將其否定。

建議62:警惕數(shù)組的淺拷貝★★★☆☆

通過 Arrays.copyOf()方法產(chǎn)生的數(shù)組是一個淺拷貝,這與序列化的淺拷貝完全相同:基本類型是直接拷貝值,其他都是拷貝引用地址。數(shù)組的clone()方法也是與此相同的,同樣是淺拷貝,而且集合的clone()方法也都是淺拷貝。

注意:雖然很多時候淺拷貝可以解決業(yè)務問題,但更多時候會留下隱患,需要我們提防又提防

建議63:在明確的場景下,為集合指定初始容量★★★☆☆

通過閱讀ArrayListadd()方法來看,它是怎么自動管理長度的,代碼如下:

public boolean add(E e) {
        //擴展長度
        ensureCapacity(size + 1);
        //追加元素
        elementData[size++] = e;
        return true;
}
public void ensureCapacity(int minCapacity) {
        //修改計數(shù)器
        modCount++;
        //上次(原始)定義的數(shù)組長度
        int oldCapacity = elementData.length;
        //當前需要的長度超過了數(shù)組長度
        if (minCapacity > oldCapacity) {
            Object[] oldData = elementData;
            //計算新數(shù)組長度
            int newCapacity = (oldCapacity * 3) / 2 + 1;
            if (newCapacity < minCapacity) {
                newCapacity = minCapacity;
            }
            //數(shù)組拷貝,形成新數(shù)組
            elementData = Arrays.copyOf(elementData, newCapacity);
        }
}
        /**
         * 注意看新數(shù)組的長度計算方法,并不是增加一個元素,elementData的長度就加1,
         * 而是在達到elementData長度的臨界點時,才將elementData擴容1.5倍,
         * 這樣實現(xiàn)有什么好處呢?好處是避免了多次調用copyOf方法的性能開銷,
         * 否則每加一個元素都要擴容一次,那性能豈不是非常糟糕?!
         * 這里為什么是1.5倍,而不是2.5倍、3.5倍?
         * 原因是一次擴容太大(比如擴容2.5倍),占用的內(nèi)存也就越大,浪費的內(nèi)存也就越多
         * (1.5倍擴容,最多浪費33%的數(shù)組空間,而2.5倍則最多可能浪費60%的內(nèi)存);
         * 而一次擴容太?。ū热缑看螖U容1.1倍),則需要多次對數(shù)組重新分配內(nèi)存,性能消耗嚴重。
         * 經(jīng)過測試驗證,擴容1.5倍即滿足了性能要求,也減少了內(nèi)存消耗。
         */

elementData的初始容量為10,代碼如下:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    private static final long serialVersionUID = 8683452581122892189L;

    /**
     * Default initial capacity.
     * 默認初始容量
     */
    private static final int DEFAULT_CAPACITY = 10;
    // ***省略***
}   

ArrayList的擴容機制可以看出,如果不設置初始容量,系統(tǒng)就按照1.5倍的規(guī)則擴容,每次擴容都是一次數(shù)組的拷貝,如果數(shù)據(jù)量很大,這樣的拷貝會非常耗費資源,而且效率非常低下。如果我們已經(jīng)知道一個ArrayList的可能長度,然后對ArrayList設置一個初始容量則可以顯著提高系統(tǒng)性能(在大數(shù)據(jù)量下,是否指定容量會使性能相差5倍以上)。

其他類型集合的擴容處理方式與ArrayList相似,只是數(shù)組的長度計算方式不同而已。

注意:非常有必要在集合初始化時聲明容量。

建議64:多種最值算法,適時選擇★★☆☆☆

對一批數(shù)據(jù)進行排序,然后找出其中的最大值或最小值,有多種實現(xiàn)方式,示例如下:

//(1)自行實現(xiàn),快速查找最大值   
public static int max1(int[] data) {
        int max = data[0];
        for (int i : data) {
            max = max > i ? max : i;
        }
        return max;
}

//(2)先排序,后取值
public static int max2(int[] data) {
        //先排序,clone()是為了不改變數(shù)組原有順序
        Arrays.sort(data.clone());
        return data[data.length - 1];
 }

//增加復雜度,查找僅次于最大值的元素的實現(xiàn) 
public static int getSecond(Integer[] data) {
        //先轉為列表
        List<Integer> dataList = Arrays.asList(data);
        //在轉為TreeSet,去重并升序排列
        TreeSet<Integer> ts = new TreeSet<Integer>(dataList);
        return ts.lower(ts.last());

 }

首先max1()的效率肯定是高于max2(),但在實際測試中我們發(fā)現(xiàn),如果數(shù)組數(shù)量少于1萬,兩者基本上沒有差別,在同一個毫秒級別里,此時就可以不用自己寫算法了,直接使用數(shù)組先排序后取值的方式。

至于getSecond()的要求稍微復雜些,使用集合是最簡單的方式,若從性能方面來考慮,數(shù)組是最好的選擇。

注意:最值計算時使用集合最簡單,使用數(shù)組性能最優(yōu),需適時選擇。

建議65:避開基本類型數(shù)組轉換列表陷阱★☆☆☆☆

示例如下:

    public static void main(String[] args) {
        int[] data = {1, 2, 3, 4, 5};
        List list = Arrays.asList(data);
        System.out.println(list.size());
        //運行結果1
    }

    //asList源代碼
    public static <T> List<T> asList(T... a) {
        return new Arrays.ArrayList<>(a);
    }
        /**
         * asList方法輸入的是一個泛型變長參數(shù),但基本類型是不能泛型化的
         * 于是在main()中把data(一個int類型的數(shù)組)作為了T的類型。
         * 所以,在上述例子中,用基本類型的包裝類即可。
         * */

注意:基本類型數(shù)組不能作為asList()的輸入?yún)?shù),否則會引起程序邏輯混亂。

建議66:asList方法產(chǎn)生的List對象不可更改★☆☆☆☆

asList()源碼:

public static <T> List<T> asList(T... a) {
    //此ArrayList非java.util.ArrayList,而是Arrays工具類的一個內(nèi)置類
    return new ArrayList<>(a);
}
//內(nèi)置類
private static class ArrayList<E> extends AbstractList<E> implements 
    RandomAccess, java.io.Serializable {
        //存儲列表元素的數(shù)組
        private final E[] a;
        //唯一的構造函數(shù)
        ArrayList(E[] array) {
           if (array == null)
            throw new NullPointerException();
            a = array;
        }
    //***省略余下源碼***
         
        /**
         *  1. ArrayList是一個靜態(tài)私有內(nèi)部類,除了Arrays能訪問外,其他類都不能訪問
         *  2. List.add和List.remove方法它都沒有實現(xiàn), 于是數(shù)組是多長,轉換成的列表也就是多長(不可變性)
         */  
}   

建議67:不同的列表選擇不同的遍歷方法★★☆☆☆

ArrayListLinkedList為例:

    //測試得出,使用最優(yōu)的遍歷方式,性能提升了60%以上
    public static int avg(List<Integer> list) {
        int sum = 0;
        if (list instanceof RandomAccess) {//ArrayList數(shù)組實現(xiàn)了RandomAccess接口(隨機存取接口)
            //可以隨機存取,則使用下標遍歷
            for (int i = 0; i < list.size(); i++) {
                sum += list.get(i);
            }
        } else {
            //有序存取,使用foreach方式
            for (int i : list) {
                sum += i;
            }
        }
        return sum / list.size();
    }

RandomAccess接口(隨機存取接口) : 實現(xiàn)了RandomAccess則表明這個類可以隨機存取,對ArrayList來說也就標志著其數(shù)據(jù)元素之間沒有關聯(lián),即兩個位置相鄰的元素之間沒有相互依賴和索引關系,可以隨機訪問和存儲。

Java中的foreach語法是iterator(迭代器)的變形用法,也就是說對于ArrayList,需要先創(chuàng)建一個迭代器容器,然后屏蔽內(nèi)部遍歷細節(jié),對外提供hasNext、next等方法。問題是ArrayList實現(xiàn)了RandomAccess接口,已表明元素之間本來沒有關系,可是,為了使用迭代器就需要強制建立一種互相“知曉”的關系,比如上一個元素可以判斷是否有下一個元素,以及下一個元素是什么等關系,這也就是通過foreach遍歷耗時的原因。

LinkedList類不是隨機存取的,而是有序存取的,它實現(xiàn)了雙向鏈表,每個數(shù)據(jù)結點中都有三個數(shù)據(jù)項:前節(jié)點的引用(Previous Node)、本節(jié)點元素(Node Element)、后繼節(jié)點的引用(Next Node),也就是說在LinkedList中的兩個元素本來就是有關聯(lián)的,我知道你的存在,你也知道我的存在,使用foreach也就是迭代器方式效率更高。

注意:列表遍歷不是那么簡單的,其中很有“學問”,適時選擇最優(yōu)的遍歷方式,不要固化為一種

建議68:頻繁插入和刪除時使用LinkedList★☆☆☆☆

下面以ArrayListLinkedList的元素插入算法為例:

ArrayList的插入(add方法)算法:

    public void add(int index, E element) {
        /*省略校驗代碼*/
        //若需要擴容,則增大底層數(shù)組的長度
        ensureCapactiy(size + 1);
        //給index下標之后的元素(包括當前的元素)的下標加1,空出index位置
        System.arraycopy(elementData, index, elementData, index + 1, size - index);
        //賦值index位置元素
        elementData[index] = element;
        //列表長度+1
        size++;
    }
        /**
         *  注意看arraycopy方法,只要是插入一個元素,其后的元素就會向后移動一位,
         *  雖然arraycopy是一個本地方法,效率非常高,但頻繁的插入,每次后面的元素都要拷貝一遍,效率就變低了,
         *  特別是在頭位置插入元素時。
         * */

LinkedList的插入(add方法)算法:

    public void add(int index, E element) {
        ///調用了私有addBefore方法
        addBefore(element, (index == size ? header : entry(index)));

    }
    //該方法實現(xiàn)了在一個元素之前插于元素的算法
    private Entry<E> addBefore(E e, Entry<E> entry) {
        //組裝一個新節(jié)點,previous指向節(jié)點的前節(jié)點,next指向原節(jié)點
        Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);
        //前節(jié)點next指向自己
        newEntry.previous.next = newEntry;
        //后節(jié)點的previous指向自己
        newEntry.next.previous = newEntry;
        //長度+1
        size++;
        //修改計數(shù)器+1;
        modCount++;
        return newEntry;
    }
        /**
         *  這是一個典型的雙向鏈表插入算法,把自己插入到鏈表,
         *  然后再把前節(jié)點的next和后節(jié)點的previous指向自己。
         *  這樣一個插入元素(也就是Entry對象)的過程中,沒有任何元素會有拷貝過程,只是引用地址改變了,
         *  那效率當然就高了。
         * */

經(jīng)過實際測試得知,LinkedList的插入效率比ArrayList快50倍以上

LinkedList刪除和插入效率高,ArrayList修改元素效率高,具體源碼,不再粘貼。

得出,如果有大量的寫操作(更多的是插入和刪除動作),推薦使用LinkedList,不過何為少量,何為大量呢?畢竟80%的性能是消耗在20%的代碼上的,所以還是要適時選擇,比如一個實時交易的系統(tǒng),即使寫作操再少,使用LinkedList也比ArrayList合適,因為此類系統(tǒng)是爭分奪秒的,多N個毫秒可能就會造成交易數(shù)據(jù)不準確;而對于一個批量系統(tǒng)來說,幾十毫秒、幾百毫秒,甚至是幾千毫秒的差別意義都不大,這時是使用LinkedList還是ArrayList就看個人愛好了,當然,如果系統(tǒng)已經(jīng)處于性能臨界點了那就必須使用LinkedList。

建議69:列表相等只需關心元素數(shù)據(jù)★★☆☆☆

示例代碼:

    public static void main(String[] args) {
        ArrayList<String> strs = new ArrayList<>();
        strs.add("A");

        Vector<String> strs2 = new Vector<>();
        strs2.add("A");

        System.out.println(strs.equals(strs2));
        //運行結果:true
    }

    //AbstractList中equals的源代碼:
    public boolean equals(Object o) {
        if (o == this)
            return true;
        //判斷是否是列表,注意:只需要實現(xiàn)List接口
        if (!(o instanceof List))
            return false;
        //通過迭代器訪問所有元素
        ListIterator<E> e1 = listIterator();
        ListIterator<?> e2 = ((List<?>) o).listIterator();
        //遍歷兩個list的元素
        while (e1.hasNext() && e2.hasNext()) {
            E o1 = e1.next();
            Object o2 = e2.next();
            //只要存在不相等就退出
            if (!(o1==null ? o2==null : o1.equals(o2)))
                return false;
        }
        //長度是否相等
        return !(e1.hasNext() || e2.hasNext());
    }

得出,列表只是一個容器,只要是同一種類型的容器(如List),不用關心容器的細節(jié)差別(如ArrayListLinkedList),只要確定所有的元素數(shù)據(jù)相等,那這兩個列表就是相等的。

其他的集合類型,如SetMap等與此相同,也是只關心集合元素,不用考慮集合類型。

注意:判斷集合是否相等時只須關注元素是否相等即可。

建議70:子列表只是原列表的一個視圖★★☆☆☆

ListsubList方法示例:

    public static void main(String[] args) {
        //定義一個列表list
        List<String> list = new ArrayList<>();
        list.add("A");
        list.add("B");
        //構造一個包含list的列表list1
        List<String> list1 = new ArrayList<>(list);
        //subList生成與list相同的列表list2
        List<String> list2 = list.subList(0, list.size());
        //list2增加一個元素
        list2.add("C");
        System.out.println(list.equals(list1));
        System.out.println(list.equals(list2));
        //運行結果:
        //false,true
    }

為什么subList生成的新列表,在添加了新元素后,還與原列表相等呢?

subList源碼:

    public List<E> subList(int fromIndex, int toIndex) {
        return (this instanceof RandomAccess ?
                new RandomAccessSubList<>(this, fromIndex, toIndex) :
                new SubList<>(this, fromIndex, toIndex));
    }
        /**
         *subList方法是由AbstractList實現(xiàn)的,它會根據(jù)是不是可以隨機存取來提供不同的SubList實現(xiàn)方式,
         * 不過,隨機存儲的使用頻率比較高,而且RandomAccessSubList也是SubList子類,
         * 所以所有的操作都是由SubList類實現(xiàn)的(除了自身的SubList方法外)
         * */
//SubList類的代碼
class SubList<E> extends AbstractList<E> {
    //原始列表
    private final AbstractList<E> l;
    //偏移量
    private final int offset;
    private int size;
    //構造函數(shù),list參數(shù)就是我們的原始列表  
    SubList(AbstractList<E> list, int fromIndex, int toIndex) {
      /***省略校驗代碼***/
        //傳遞原始列表
        l = list;
        offset = fromIndex;
        //子列表的長度
        size = toIndex - fromIndex;
        this.modCount = l.modCount;
    }
    //獲取指定位置的元素
    public E get(int index) {
         /***省略校驗代碼***/
        //從原始字符串中獲得指定位置的元素
        return l.get(index+offset);
    }
    //增加或插入
    public void add(int index, E element) {
       /***省略校驗代碼***/
        //直接增加到原始字符串上
        l.add(index+offset, element);
        this.modCount = l.modCount;
        size++;
    }
     /***其他代碼省略***/
}

從源碼得出:subList方法的實現(xiàn)原理,它返回的SubList類也是AbstractList的子類,其所有的方法如get、set、add、remove等都是在原始列表上的操作,它自身并沒有生成一個數(shù)組或是鏈表,也就是子列表只是原列表的一個視圖(View),所有的修改動作都反映在了原列表上。

最后,為什么變量listlist1不相等?

因為通過ArrayList構造函數(shù)創(chuàng)建的List對象list1實際上是新列表,它是通過數(shù)組的copyOf動作生成的,所生成的列表list1與原列表list之間沒有任何關系(雖然是淺拷貝,但元素類型是String,也就是說元素是深拷貝的),雖然此時equals是相等的,但是隨后list又增加了元素,因此equals比較肯定不相等了。

注意:subList產(chǎn)生的列表只是一個視圖,所有的修改動作直接作用于原列表。

建議71:推薦使用subList處理局部列表★★☆☆☆

示例代碼:

//需求:一個列表有10個元素,現(xiàn)在要刪除索引位置為2~6的元素   
public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            list.add(i);
        }
    //一行代碼就解決問題(subList方法所有的操作都是在原始列表上進行的)
    list.subList(2, 6).clear();
   }

建議72:生成子列表后不要再操作原列表★★☆☆☆

前面說了,subList生成的子列表是原列表的一個視圖,那在subList執(zhí)行完后,如果修改了原列表的內(nèi)容會怎樣呢?

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("A");
        list.add("B");
        list.add("C");
        //子列表
        List<String> subList = list.subList(0, 2);
        //原列表增加一個元素
        list.add("D");
        System.out.println("list長度:" + list.size());
        System.out.println("subList長度:" + subList.size());
    }

運行結果:subListsize方法異常 (并發(fā)修改異常)

list長度:4
Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1231)
    at java.util.ArrayList$SubList.size(ArrayList.java:1040)
    at com.jyswm.demo1.t151.Client.main(Client.java:19)
         /**
         * 因為subList取出的列表是原列表的一個視圖,原數(shù)據(jù)集(代碼中的list變量)修改了,
         * 但是subList取出的子列表不會重新生成一個新列表(這點與數(shù)據(jù)庫視圖是不相同的),
         * 后面在對子列表繼續(xù)操作時,就會檢測到修改計數(shù)器與預期的不相同,
         * 于是就拋出了并發(fā)修改異常。
         */

SubListsize方法的源碼

private class SubList extends AbstractList<E> implements RandomAccess {
 
    public int size() {
            checkForComodification();
            return this.size;
    }
    //checkForComodification方法就是用于檢測是否并發(fā)修改的
    private void checkForComodification() {
            if (ArrayList.this.modCount != this.modCount)
                throw new ConcurrentModificationException();
    }
}

解決辦法:

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        //子列表
        List<String> subList = list.subList(0, 2);
        //設置原列表list為只讀狀態(tài)
        list = Collections.unmodifiableList(list);
    }

注意:subList生成子列表后,保持原列表的只讀狀態(tài)。

建議73:使用Comparator進行排序★★☆☆☆

在Java中,要想給數(shù)據(jù)排序,有兩種實現(xiàn)方式,一種是實現(xiàn)Comparable接口,一種是實現(xiàn)Comparator接口,這兩者有什么區(qū)別呢?示例代碼:

/**
 * 員工類
 */
@Data
public class Employee implements Comparable<Employee> {
    /**
     * 工號
     */
    private int id;
    /**
     * 職位
     */
    private Position position;

    public Employee(int id, Position position) {
        this.id = id;
        this.position = position;
    }

    @Override
    public int compareTo(Employee o) {
        return new CompareToBuilder()
                .append(id, o.id).toComparison();
    }

}


public enum Position {
    Boss, Manager, Staff
}

先按id工號來排序

    public static void main(String[] args) {
        List<Employee> list = new ArrayList<>(5);
        list.add(new Employee(1001, Position.Boss));
        list.add(new Employee(1005, Position.Manager));
        list.add(new Employee(1003, Position.Staff));
        list.add(new Employee(1002, Position.Staff));
        list.add(new Employee(1004, Position.Staff));
        //按工號排序
        Collections.sort(list);
        for (Employee employee : list) {
            System.out.println(employee);
        }
        /**
         * 運行結果:
         * Employee(id=1001, position=Boss)
         * Employee(id=1002, position=Staff)
         * Employee(id=1003, position=Staff)
         * Employee(id=1004, position=Staff)
         * Employee(id=1005, position=Manager)
         */
    }

此時,我們希望按照職位來排序,并不重構Employee類,那怎么做呢?

Collections.sort方法,它有一個重載方法Collections. sort(List<T> list, Comparator<?super T>c),可以接受一個Comparator實現(xiàn)類,用法如下:

    class PositionComparator implements Comparator<Employee> {
        @Override
        public int compare(Employee o1, Employee o2) {
            //按照職位降序
            return o1.getPosition().compareTo(o2.getPosition());
        }
    }

//擴展: 按職位臨時倒序排列呢?注意只是臨時的,是否要重寫一個排序器?完全不用,有兩個解決辦法:
//1.直接使用Collections. reverse(List<?>list)方法實現(xiàn)倒序排列。
//2.通過Collections.sort(list, Collections.reverseOrder(new PositionComparator()))也可以實現(xiàn)倒序排列。

回到原題:在Java中,為什么要有兩個排序接口呢?

實現(xiàn)了Comparable接口的類表明自身是可比較的,有了比較才能進行排序;

Comparator接口是一個工具類接口,它的名字(比較器)也已經(jīng)表明了它的作用:用作比較,它與原有類的邏輯沒有關系,只是實現(xiàn)兩個類的比較邏輯。

從這方面來說,一個類可以有很多的比較器,只要有業(yè)務需求就可以產(chǎn)生比較器,有比較器就可以產(chǎn)生N多種排序,而Comparable接口的排序只能說是實現(xiàn)類的默認排序算法,一個類穩(wěn)定、成熟后其compareTo方法基本不會改變,也就是說一個類只能有一個固定的、由compareTo方法提供的默認排序算法。

注意: Comparable接口可以作為實現(xiàn)類的默認排序法,Comparator接口則是一個類的擴展排序工具。

建議74:不推薦使用binarySearch對列表進行檢索★★☆☆☆

示例代碼:

    public static void main(String[] args) {
        List<String> cities = new ArrayList<>();
        cities.add("上海");
        cities.add("廣州");
        cities.add("廣州");
        cities.add("北京");
        cities.add("天津");
        //indexOf方法取得索引值
        System.out.println("indexOf索引值:"+cities.indexOf("廣州"));
        //binarySearch方法取得索引值
        System.out.println("binarySearch索引值:"+Collections.binarySearch(cities, "廣州"));
        /**
         * 運行結果:
         * indexOf索引值:1
         * binarySearch索引值:2
         */
    }

binarySearch方法返回結果錯誤了,為什么?

JDK上對binarySearch的描述:使用二分搜索法搜索指定列表,以獲得指定對象。

但是二分法查詢的一個首要前提是:數(shù)據(jù)集已經(jīng)實現(xiàn)升序排列,否則二分法查找的值是不準確的。

簡單解決辦法:使用Collections.sort排下序,再用binarySearch檢索。

需要注意的場景:元素數(shù)據(jù)是從Web或數(shù)據(jù)庫中傳遞進來的,原本是一個有規(guī)則的業(yè)務數(shù)據(jù),我們?yōu)榱瞬檎乙粋€元素對它進行了排序,也就是改變了元素在列表中的位置,那誰來保證業(yè)務規(guī)則此時的正確性呢?所以說,binarySearch方法在此處受限了。當然,拷貝一個數(shù)組,然后再排序,再使用binarySeach查找指定值,也可以解決該問題(除非處于性能的的考慮,否則使用indexOf簡單、好用,而且也不會出錯)。

binarySearch的二分法查找比indexOf的遍歷算法性能上高幾十倍。

注意從:性能方面考慮,binarySearch是最好的選擇。

建議75:集合中的元素必須做到compareTo和equals同步★★☆☆☆

示例代碼:

    @Data
    static class City implements Comparable<City> {
        private String code;
        private String name;

        public City(String code, String name) {
            this.code = code;
            this.name = name;
        }

        @Override
        public int compareTo(City o) {
            //安裝城市名稱排序
            return new CompareToBuilder()
                    .append(name, o.name).toComparison();
        }

        @Override
        public boolean equals(Object obj) {
            /***省略其他判斷***/
            City city = (City) obj;
            //根據(jù)code判斷是否相等
            return new EqualsBuilder()
                    .append(code, city.code).isEquals();
        }
    }

    public static void main(String[] args) {
        //把多個城市對象放在一個List中,然后使用不同的方法查找同一個城市,看看返回值有什么異常。
        List<City> cities = new ArrayList<>();
        cities.add(new City("021", "上海"));
        cities.add(new City("021", "滬"));
        //排序
        Collections.sort(cities);
        //查找對象
        City city = new City("021", "滬");
        //indexOf方法取得索引值
        System.out.println("indexOf索引值:" + cities.indexOf(city));
        //binarySearch方法取得索引值
        System.out.println("binarySearch索引值:" + Collections.binarySearch(cities, city));
        /**
         * 運行結果:
         * indexOf索引值:0
         * binarySearch索引值:1
         */
    }

indexOf返回的是第一個元素,而binarySearch返回的是第二個元素,怎么回事呢?

這是因為indexOf是通過equals方法判斷的,equals等于true就認為找到符合條件的元素了,而binarySearch查找的依據(jù)是compareTo方法的返回值,返回0即認為找到符合條件的元素。

從這個例子中,我們可以理解兩點:

  • indexOf依賴equals方法查找,binarySearch則依賴compareTo方法查找。
  • equals是判斷元素是否相等,compareTo是判斷元素在排序中的位置是否相同。

注意:實現(xiàn)了compareTo方法,就應該覆寫equals方法,確保兩者同步。

建議76:集合運算時使用更優(yōu)雅的方式★☆☆☆☆

    public static void main(String[] args) {
       List<String> list1 = new ArrayList<>();
        list1.add("A");
        List<String> list2 = new ArrayList<>();
        list2.add("B");
    }
  1. 并集

    list1.addAll(list2);
    
  2. 交集

    list1.retainAll(list2);
    
  3. 差集

    list1.removeAll(list2);
    
  4. 無重復并集

    //刪除list1中出現(xiàn)的元素
    list2.removeAll(list1);
    //把剩余的list2元素加到list1
    list1.addAll(list2);
    

建議77:使用shuffle打亂列表★☆☆☆☆

示例代碼:

    public static void main(String[] args) {
        int num = 10;
        List<String> tags = new ArrayList<>(num);
        for (int i = 0; i < num; i++) {
            tags.add(i+"");
        }
        //打亂順序
        Collections.shuffle(tags);
    }

建議78:減少HashMap中元素的數(shù)量★★★☆☆

問題描述:同一環(huán)境中,分別向HashMapList中添加40萬個字符串元素,向Map中添加元素的程序報內(nèi)存溢出,向List中添加元素的程序卻正常。

原因一:內(nèi)部存儲結構,代碼如下:

    private static class Entry<K,V> implements Map.Entry<K,V> {
        final int hash;
        //鍵
        final K key;
        //值
        V value;
        //相同哈希碼的下一個元素
        Entry<K,V> next;
        /***省略其他代碼***/
    }

HashMap底層的數(shù)組變量名叫table,它是Entry類型的數(shù)組,保存的是一個一個的鍵值對(在我們的例子中Entry是由兩個String類型組成的)。HashMap在底層雖然也是以數(shù)組方式保存元素的,其中每一個鍵值對就是一個元素,但是HashMap把鍵值對封裝成了一個Entry對象,然后再把Entry放到了數(shù)組中。HashMapArrayList多了一次封裝,把String類型的鍵值對轉換成Entry對象后再放入數(shù)組,這就多了40萬個對象

原因二:它的擴容機制,代碼如下:

//在插入鍵值對時,會做長度校驗,如果大于或等于閥值(threshold變量),則數(shù)組長度增大一倍        
if (size++ >= threshold) {
      resize(2 * table.length)
}
//也就是說只要HashMap的size大于數(shù)組長度的0.75倍時,就開始擴容
 threshold = (int) (newCapacity * 0.75)

由上得出,Map在擴容的時候預留了太多空間。在內(nèi)存占用的臨界點,由于內(nèi)存剩余不足,無法提供下次擴容所需要的內(nèi)存,導致擴容失敗。但真正需要添加的數(shù)據(jù),可能只是需要再擴容一個單位就可以放下。所以有時會產(chǎn)生,在我們看來,系統(tǒng)所分配的內(nèi)存完全可以存放下這些數(shù)據(jù),但是在用Map存儲時卻內(nèi)存溢出了。

注意:盡量讓HashMap中的元素少量并簡單。

**建議79:集合中的哈希碼不要重復★☆☆☆☆

HashMap的查找元素為例:

  1. keyhashCode定位元素的位置,所以查找元素比較快(比List快)。
  2. hashCode沖突時,則遍歷對比(和List的遍歷對比類似),所以當hashCode沖突時,查找元素的效率會大大降低。

注意:HashMap中的hashCode應避免沖突。

建議80:多線程使用Vector或HashTable★★☆☆☆

VectorArrayList的多線程版本,Vector的每個方法前都加上了synchronized關鍵字,同時只會允許一個線程進入該方法,確保了程序的可靠性。

HashTableHashMap的多線程版本,它的實現(xiàn)與Vector相同。

建議81:非穩(wěn)定排序推薦使用List★☆☆☆☆

SetList的最大區(qū)別就是Set中的元素不可以重復(這個重復指的equals方法的返回值相等),其他方面則沒有太大的區(qū)別了,在Set的實現(xiàn)類中有一個比較常用的類:TreeSet,該類實現(xiàn)了類默認排序為升序的Set集合,如果插入一個元素,默認會按照升序排列(是根據(jù)Comparable接口的compareTo的返回值確定排序位置了)

注意: SortedSet接口(TreeSet實現(xiàn)了該接口)只是定義了在給集合加入元素時將其進行排序,并不能保證元素修改后的排序結果,因此TreeSet適用于不變量的集合數(shù)據(jù)排序,比如String、Integer等類型,但不適用于可變量的排序,特別是不確定何時元素會發(fā)生變化的數(shù)據(jù)集合。

注意:SortedSet中的元素被修改后可能會影響其排序位置。

建議82:由點及面,一葉知秋—集合大家族★☆☆☆☆

Java中的集合類實在是太豐富了,有常用的ArrayList、HashMap,也有不常用的StackQueue,有線程安全的Vector、HashTable,也有線程不安全的LinkedList、TreeMap,有阻塞式的ArrayBlockingQueue,也有非阻塞式的PriorityQueue等,整個集合家族非常龐大,而且也是錯綜復雜,可以劃分為以下幾類:

(1)List

實現(xiàn)List接口的集合主要有:ArrayList、LinkedList、Vector、Stack,其中ArrayList是一個動態(tài)數(shù)組,LinkedList是一個雙向鏈表,Vector是一個線程安全的動態(tài)數(shù)組,Stack是一個對象棧,遵循先進后出的原則。

(2)Set

Set是不包含重復元素的集合,其主要的實現(xiàn)類有:EnumSet、HashSet、TreeSet,其中EnumSet是枚舉類型的專用Set,所有元素都是枚舉類型;HashSet是以哈希碼決定其元素位置的Set,其原理與HashMap相似,它提供快速的插入和查找方法;TreeSet是一個自動排序的Set,它實現(xiàn)了SortedSet接口。

(3)Map

Map是一個大家族,它可以分為排序Map和非排序Map,排序Map主要是TreeMap類,它根據(jù)Key值進行自動排序;非排序Map主要包括:HashMap、HashTable、PropertiesEnumMap等,其中PropertiesHashTable的子類,它的主要用途是從Property文件中加載數(shù)據(jù),并提供方便的讀寫操作;EnumMap則是要求其Key必須是某一個枚舉類型。

Map中還有一個WeakHashMap類需要說明,它是一個采用弱鍵方式實現(xiàn)的Map類,它的特點是:WeakHashMap對象的存在并不會阻止垃圾回收器對鍵值對的回收,也就是說使用WeakHashMap裝載數(shù)據(jù)不用擔心內(nèi)存溢出的問題,GC會自動刪除不用的鍵值對,這是好事。但也存在一個嚴重問題:GC是靜悄悄回收的(何時回收?God knows!),我們的程序無法知曉該動作,存在著重大的隱患。

(4)Queue

隊列,它分為兩類,一類是阻塞式隊列,隊列滿了以后再插入元素則會拋出異常,主要包括:ArrayBlockingQueuePriorityBlockingQueue、LinkedBlockingQueue,其中ArrayBlockingQueue是一個以數(shù)組方式實現(xiàn)的有界阻塞隊列,PriorityBlockingQueue是依照優(yōu)先級組建的隊列,LinkedBlockingQueue是通過鏈表實現(xiàn)的阻塞隊列;另一類是非阻塞隊列,無邊界的,只要內(nèi)存允許,都可以持續(xù)追加元素,我們最經(jīng)常使用的是PriorityQueue類。

還有一種隊列,是雙端隊列,支持在頭、尾兩端插入和移除元素,它的主要實現(xiàn)類是:ArrayDequeLinkedBlockingDeque、LinkedList

(5)數(shù)組

數(shù)組與集合的最大區(qū)別就是數(shù)組能夠容納基本類型,而集合就不行,更重要的一點就是所有的集合底層存儲的都是數(shù)組。

(6)工具類

數(shù)組的工具類是java.util.Arraysjava.lang.reflect.Array,集合的工具類是java.util.Collections,有了這兩個工具類,操作數(shù)組和集合會易如反掌,得心應手。

(7)擴展類

集合類當然可以自行擴展了,想寫一個自己的List?沒問題,但最好的辦法還是“拿來主義”,可以使用Apachecommons-collections擴展包,也可以使用Google的google-collections擴展包,這些足以應對我們的開發(fā)需要。

注意:commons-collections、google-collections是JDK之外的優(yōu)秀數(shù)據(jù)集合工具包,使用拿來主義即可。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容