1. 程式人生 > >編寫高質量程式碼:改善Java程式的151個建議(第5章:陣列和集合___建議60~66)

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

如果你浪費了自己的年齡,那是挺可悲的。因為你的青春只能持續一點兒時間——很短的一點兒時間。 —— 王爾德

建議60:效能考慮,陣列是首選

建議61:若有必要,使用變長陣列

建議62:警惕陣列的淺拷貝

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

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

建議65:避開基本型別陣列轉換列表陷阱

建議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程式的15