編寫高質量代碼:改善Java程序的151個建議(第5章:數組和集合___建議60~66)

若是你浪費了本身的年齡,那是挺可悲的。由於你的青春只能持續一點兒時間——很短的一點兒時間。 —— 王爾德java

建議60:性能考慮,數組是首選算法

建議61:如有必要,使用變長數組apache

建議62:警戒數組的淺拷貝數組

建議63:在明確的場景下,爲集合指定初始容量數據結構

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

建議65:避開基本類型數組轉換列表陷阱ide

建議66:asList方法產生的List對象不可修改工具

建議60:性能考慮,數組是首選性能

數組在實際的系統開發中用的愈來愈少了,咱們一般只有在閱讀一些開源項目時纔會看到它們的身影,在Java中它確實沒有List、Set、Map這些集合類用起來方便,可是在基本類型處理方面,數組仍是佔優點的,並且集合類的底層也都是經過數組實現的,好比對一數據集求和這樣的計算:測試

package OSChina.Client;

import java.util.ArrayList;
import java.util.List;

public class Client2 {
    public static void main(String[] args) {
        int datas[] = new int[10000000];
        for (int i = 0; i < 10000000; i++) {
            datas[i] = i;
        }
        int sum = 0;
        long start1 = System.currentTimeMillis();
        for (int i = 0; i < datas.length; i++) {
            sum += datas[i];
        }
        System.out.println(sum);
        long end1 = System.currentTimeMillis();
        System.out.println("數組解析耗時:" + (end1 - start1) + "ms");
        sum = 0;
        List<Integer> list = new ArrayList<Integer>();
        long start2 = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            list.add(i);
        }
        for (int i = 0; i < list.size(); i++) {
            sum += list.get(i);
        }
        System.out.println(sum);
        long end2 = System.currentTimeMillis();
        System.out.println("list解析耗時:" + (end2 - start2) + "ms");
    }
}

原理:

//對數組求和
    public static int sum(int datas[]) {
        int sum = 0;
        for (int i = 0; i < datas.length; i++) {
            sum += datas[i];
        }
        return sum;
    }

對一個int類型 的數組求和,取出全部數組元素並相加,此算法中若是是基本類型則使用數組效率是最高的,使用集合則效率次之。

再看使用List求和: 

// 對列表求和計算
    public static int sum(List<Integer> datas) {
        int sum = 0;
        for (int i = 0; i < datas.size(); i++) {
            sum += datas.get(i);
        }
        return sum;
    }

注意看sum+=datas.get(i);這行代碼,這裏其實作了一個拆箱動做,Interger對象經過intValue方法自動轉換成一個int基本類型,對於性能瀕於臨界的系統來講該方案很危險,特別是大數據量的時候,首先,在初始化list數組時都會進行裝箱操做,把一個int類型包裝成一個interger對象,雖然有整型池在,但不在整型池範圍內的都會產生一個新的interger對象,衆所周知,基本類型是在棧內存中操做的,而對象是在堆內存中操做的,棧內存的有點:速度快,容量小;堆內存的特色:速度慢,容量大。其次,在進行求和運算時,要作拆箱操做,性能消耗又產生了。對基本類型進行求和運算時,數組的效率是集合的10倍。

注:對性能要求高的場景中使用數組代替集合。

建議61:如有必要,使用變長數組

Java中的數組是定長的,一旦通過初始化聲明就不可改變長度,這在實際使用中很是不方便。

數組也能夠變長:

package OSChina.Client;

import java.util.Arrays;

public class Clinet3 {
    public static <T> T[] expandCapacity(T[] datas,int newLen){
        newLen = newLen< 0?0:newLen;
        return Arrays.copyOf(datas,newLen);
    }

    public static void main(String[] args) {
        Integer[] array = new Integer[60];
        for (int i = 0; i < 65; i++) {
            array[i] = i;
        }
        System.out.println("我是江疏影!");
    }
}

package OSChina.Client;

import java.util.Arrays;

public class Clinet3 {
    public static <T> T[] expandCapacity(T[] datas,int newLen){
        newLen = newLen< 0?0:newLen;
        return Arrays.copyOf(datas,newLen);
    }

    public static void main(String[] args) {
        Integer[] array = new Integer[60];
        array = expandCapacity(array,80);
        for (int i = 0; i < 65; i++) {
            array[i] = i;
        }
        System.out.println("我是江疏影");
    }
}

經過這樣的處理方式,曲折的解決了數組的變長問題,其實,集合的長度自動維護功能的原理與此相似。在實際開發中,若是確實須要變長的數據集,數組也是在考慮範圍以內的,不能因固定長度而將其否認之。

建議62:警戒數組的淺拷貝

import java.util.Arrays;
import org.apache.commons.lang.builder.ToStringBuilder;

public class Client62 {
    public static void main(String[] args) {
        // 氣球數量
        int ballonNum = 7;
        // 第一個箱子
        Balloon[] box1 = new Balloon[ballonNum];
        // 初始化第一個箱子中的氣球
        for (int i = 0; i < ballonNum; i++) {
            box1[i] = new Balloon(Color.values()[i], i);
        }
        // 第二個箱子的氣球是拷貝第一個箱子裏的
        Balloon[] box2 = Arrays.copyOf(box1, box1.length);
        // 修改最後一個氣球顏色
        box2[6].setColor(Color.Blue);
        // 打印出第一個箱子中的氣球顏色
        for (Balloon b : box1) {
            System.out.println(b);
        }

    }
}

// 氣球顏色
enum Color {
    Red, Orange, Yellow, Green, Indigo, Blue, Violet
}

// 氣球
class Balloon {
    // 編號
    private int id;
    // 顏色
    private Color color;

    public Balloon(Color _color, int _id) {
        color = _color;
        id = _id;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public Color getColor() {
        return color;
    }

    public void setColor(Color color) {
        this.color = color;
    }

    @Override
    public String toString() {
        //apache-common-lang包下的ToStringBuilder重寫toString方法
        return new ToStringBuilder(this).append("編號", id).append("顏色", color).toString();
    }

}

第二個箱子裏最後一個氣球的顏色毫無疑問是被修改成藍色了,不過咱們是經過拷貝第一個箱子裏的氣球而後再修改的方式來實現的,那會對第一個箱子的氣球顏色有影響嗎?咱們看看輸出結果:

最後一個氣球顏色居然也被修改了,咱們只是但願修改第二個箱子的氣球啊,這是爲什麼?這是典型的淺拷貝(Shallow  Clone)問題,之前第一章序列化時講過,可是這裏與之有一點不一樣:數組中的元素沒有實現Serializable接口。

確實如此,經過copyof方法產生的數組是一個淺拷貝,這與序列化的淺拷貝徹底相同:基本類型直接拷貝值,引用類型時拷貝引用地址。

數組的clone一樣也是淺拷貝,集合的clone也是淺拷貝。

問題找到了,解決起來也很簡單,遍歷box1的每一個元素,從新生成一個氣球對象,並放到box2數組中。

集合list進行業務處理時,須要拷貝集合中的元素,可集合沒有提供拷貝方法,本身寫很麻煩,乾脆使用list.toArray方法轉換成數組,而後經過arrays.copyof拷貝,再轉回集合,簡單邊界!但很是遺憾,有時這樣會產生淺拷貝的問題。

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

咱們常用ArrayList、Vector、HashMap等集合,通常都是直接用new跟上類名聲明出一個集合來,而後使用add、remove等方法進行操做,並且由於它是自動管理長度的,因此不用咱們特別費心超長的問題,這確實是一個很是好的優勢,但也有咱們必需要注意的事項。

package OSChina.Client;

import java.util.ArrayList;
import java.util.List;

public class Client4 {
    public static void main(String[] args) {
        List<Integer> list1 = new ArrayList<Integer>();
        long start1 = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            list1.add(i);
        }
        long end1 = System.currentTimeMillis();
        System.out.println("不設置初始長度耗時:" + (end1 - start1) + "ms");
        long start2 = System.currentTimeMillis();
        List<Integer> list2 = new ArrayList<Integer>(10000000);
        for (int i = 0; i < 10000000; i++) {
            list2.add(i);
        }
        long end2 = System.currentTimeMillis();
        System.out.println("設置初始長度耗時:" + (end2 - start2) + "ms");
    }
}

若是不設置初始容量,ArrayList的默認初始容量是10,系統會按照1.5倍的規則擴容,每次擴容都是一次數組的拷貝,若是數組量大,這樣的拷貝會很是消耗資源,並且效率很是低下。因此,要設置一個ArrayList的可能長度,能夠顯著提高系統性能。

其它集合也相似,Vector擴容2倍。

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

對一批數據進行排序,而後找出其中的最大值或最小值,這是基本的數據結構知識。在Java中咱們能夠經過編寫算法的方式,也能夠經過數組先排序再取值的方式來實現,下面以求最大值爲例,解釋一下多種算法:

package OSChina.Client;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.TreeSet;

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

    //先排序,後取值
    public static int maxSort(int[] data) {
        Arrays.sort(data);
        return data[data.length - 1];
    }

    public static void main(String[] args) {
        int datas[] = new int[100000000];
        for (int i = 0; i < 100000000; i++) {
            datas[i] = i;
        }
        int sum = 0;

        for (int i = 0; i < datas.length; i++) {
            sum += datas[i];
        }
        long start1 = System.currentTimeMillis();
        System.out.println("快速查找最大值:"+max(datas));
        long end1 = System.currentTimeMillis();
        System.out.println("快速查找最大值耗時:" + (end1 - start1) + "ms");


        long start2 = System.currentTimeMillis();
        System.out.println("先排序,後取值,最大值:"+maxSort(datas));
        long end2 = System.currentTimeMillis();
        System.out.println("先排序,後取值,最大值耗時:" + (end2 - start2) + "ms");
    }
}

從效率上將,快速查找法更快一些,只用遍歷一次就能夠計算出最大值,但在實際測試中發現,若是數組量少於10000,兩個基本上沒有區別,但在同一個毫秒級別裏,此時就能夠不用本身寫算法了,直接使用數組先排序後取值的方式。

若是數組元素超過10000,就須要依據實際狀況來考慮:本身實現,能夠提升性能;先排序後取值,簡單,通俗易懂。排除性能上的差別,二者均可以選擇,甚至後者更方便一些,也更容易想到。

總結一下,數據量不是很大時(10000左右),使用先排序後取值比較好,看着高大上?總比本身寫代碼好!,數據量過大,出於性能的考慮,能夠本身寫排序方法!

感受這條有點吹毛求疵了!

那若是要查找僅次於最大值的元素(也就是老二),該如何處理呢?要注意,數組的元素時能夠重複的,最大值多是多個,因此單單一個排序而後取倒數第二個元素時解決不了問題的。

此時,就須要一個特殊的排序算法了,先要剔除重複數據,而後再排序,固然,本身寫算法也能夠實現,可是集合類已經提供了很是好的方法,要是再使用本身寫算法就顯得有點重複造輪子了。數組不能剔除重複數據,但Set集合倒是能夠的,並且Set的子類TreeSet還能自動排序,代碼以下: 

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());
    }

注:

① treeSet.lower()方法返回集合中小於指定值的最大值。

② 最值計算使用集合最簡單,使用數組性能最優。

建議65:避開基本類型數組轉換列表陷阱

咱們在開發中常常會使用Arrays和Collections這兩個工具類和列表之間轉換,很是方便,但也有時候會出現一些奇怪的問題,來看以下代碼:

public class Client65 {
    public static void main(String[] args) {
        int data [] = {1,2,3,4,5};
        List list= Arrays.asList(data);
        System.out.println("列表中的元素數量是:"+list.size());
    }
}

也許你會說,這很簡單,list變量的元素數量固然是5了。可是運行後打印出來的列表數量爲1。

事實上data確實是一個有5個元素的int類型數組,只是經過asList轉換成列表後就只有一個元素了,這是爲何呢?其餘4個元素到什麼地方去了呢?

咱們仔細看一下Arrays.asList的方法說明:輸入一個變長參數,返回一個固定長度的列表。注意這裏是一個變長參數,看源碼:

public static <T> List<T> asList(T... a) {
    return new ArrayList<>(a);
}

asList方法輸入的是一個泛型變長參數,基本類型是不能泛型化的,也就是說8個基本類型不能做爲泛型參數,要想做爲泛型參數就必須使用其所對應的包裝類型。

解決方法:

Integer data [] = {1,2,3,4,5};

把int替換爲Integer便可讓輸出元素數量爲5.須要說明的是,不只僅是int類型的數組有這個問題,其它7個基本類型的數組也存在類似的問題,這就須要你們注意了,在把基本類型數組轉換爲列表時,要特別當心asList方法的陷阱,避免出現程序邏輯混亂的狀況。

建議66:asList方法產生的List對象不可修改

上一個建議指出了asList方法在轉換基本類型數組時存在的問題,接着咱們看一下asList方法返回的列表有何特殊的地方,代碼以下: 

package OSChina.Client;

import java.util.Arrays;
import java.util.List;

public class Client5 {
    public static void main(String[] args) {
        // 五天工做制
        Week days[] = { Week.Mon, Week.Tue, Week.Wed, Week.Thu, Week.Fri };
        // 轉換爲列表
        List<Week> list = Arrays.asList(days);
        // 增長週六爲工做日
        list.add(Week.Sat);
        /* do something */
    }
}
enum Week {
    Sun, Mon, Tue, Wed, Thu, Fri, Sat
}

10da38aff70cd1187f82a1cb3f0e84c895e.jpg

UnsupportedOperationException,不支持的操做,竟然不支持list的add方法,這是什麼緣由呢?

此ArrayList非java.util.ArrayList,而是Arrays工具類的一個內部類

咱們深刻地看看這個ArrayList靜態內部類,它僅僅實現了5個方法:

① size:元素數量

② get:得到制定元素

③ set:重置某一元素值

④ contains:是否包含某元素

⑤ toArray:轉化爲數組,實現了數組的淺拷貝

對於咱們常用list.add和list.remove方法它都沒有實現,也就是說asList返回的是一個長度不可變的列表,數組是多長,轉換成的列表也就是多長,換句話說此處的列表只是數組的一個外殼,再也不保持列表的動態變長的特性,這纔是咱們關注的重點。有些開發人員喜歡這樣定義個初始化列表: 

List<String> names= Arrays.asList("張三","李四","王五");

一句話完成了列表的定義和初始化,看似很便捷,卻隱藏着重大隱患---列表長度沒法修改。想一想看,若是這樣一個List傳遞到一個容許添加的add操做的方法中,那將會產生何種結果,若是有這種習慣的javaer,請慎之戒之,除非很是自信該List只用於只讀操做。

 

編寫高質量代碼:改善Java程序的151個建議@目錄

相關文章
相關標籤/搜索