电竞比分网-中国电竞赛事及体育赛事平台

分享

您不知道的 5 件事……: Java Collections API,第 2 部分

 阿青哥Joe 2018-06-30


您覺(jué)得自己懂 Java 編程?事實(shí)上,大多數(shù)程序員對(duì)于 Java 平臺(tái)都是淺嘗輒止,只學(xué)習(xí)了足以完成手頭上任務(wù)的知識(shí)而已。在本 系列 中,Ted Neward 深入挖掘 Java 平臺(tái)的核心功能,揭示一些鮮為人知的事實(shí),幫助您解決最棘手的編程困難。

java.util 中的 Collections 類(lèi)旨在通過(guò)取代數(shù)組提高 Java 性能。如您在 第 1 部分 中了解到的,它們也是多變的,能夠以各種方式定制和擴(kuò)展,幫助實(shí)現(xiàn)優(yōu)質(zhì)、簡(jiǎn)潔的代碼。

Collections 非常強(qiáng)大,但是很多變:使用它們要小心,濫用它們會(huì)帶來(lái)風(fēng)險(xiǎn)。

1. List 不同于數(shù)組

Java 開(kāi)發(fā)人員常常錯(cuò)誤地認(rèn)為 ArrayList 就是 Java 數(shù)組的替代品。Collections 由數(shù)組支持,在集合內(nèi)隨機(jī)查找內(nèi)容時(shí)性能較好。與數(shù)組一樣,集合使用整序數(shù)獲取特定項(xiàng)。但集合不是數(shù)組的簡(jiǎn)單替代。

要明白數(shù)組與集合的區(qū)別需要弄清楚順序位置 的不同。例如,List 是一個(gè)接口,它保存各個(gè)項(xiàng)被放入集合中的順序,如清單 1 所示:

清單 1. 可變鍵值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.util.*;
public class OrderAndPosition
{
    public static <T> void dumpArray(T[] array)
    {
        System.out.println("=============");
        for (int i=0; i<array.length; i++)
            System.out.println("Position " + i + ": " + array[i]);
    }
    public static <T> void dumpList(List<T> list)
    {
        System.out.println("=============");
        for (int i=0; i<list.size(); i++)
            System.out.println("Ordinal " + i + ": " + list.get(i));
    }
     
    public static void main(String[] args)
    {
        List<String> argList = new ArrayList<String>(Arrays.asList(args));
        dumpArray(args);
        args[1] = null;
        dumpArray(args);
         
        dumpList(argList);
        argList.remove(1);
        dumpList(argList);
    }
}

當(dāng)?shù)谌齻€(gè)元素從上面的 List 中被移除時(shí),其 “后面” 的各項(xiàng)會(huì)上升填補(bǔ)空位。很顯然,此集合行為與數(shù)組的行為不同(事實(shí)上,從數(shù)組中移除項(xiàng)與從 List 中移除它也不完全是一回事兒 — 從數(shù)組中 “移除” 項(xiàng)意味著要用新引用或 null 覆蓋其索引槽)。

2. 令人驚訝的 Iterator!

無(wú)疑 Java 開(kāi)發(fā)人員很喜愛(ài) Java 集合 Iterator,但是您最后一次使用 Iterator 接口是什么時(shí)候的事情了?可以這么說(shuō),大部分時(shí)間我們只是將 Iterator 隨意放到 for() 循環(huán)或加強(qiáng) for() 循環(huán)中,然后就繼續(xù)其他操作了。

但是進(jìn)行深入研究后,您會(huì)發(fā)現(xiàn) Iterator 實(shí)際上有兩個(gè)十分有用的功能。

第一,Iterator 支持從源集合中安全地刪除對(duì)象,只需在 Iterator 上調(diào)用 remove() 即可。這樣做的好處是可以避免 ConcurrentModifiedException,這個(gè)異常顧名思意:當(dāng)打開(kāi) Iterator 迭代集合時(shí),同時(shí)又在對(duì)集合進(jìn)行修改。有些集合不允許在迭代時(shí)刪除或添加元素,但是調(diào)用 Iteratorremove() 方法是個(gè)安全的做法。

第二,Iterator 支持派生的(并且可能是更強(qiáng)大的)兄弟成員。ListIterator,只存在于 List 中,支持在迭代期間向 List 中添加或刪除元素,并且可以在 List 中雙向滾動(dòng)。

雙向滾動(dòng)特別有用,尤其是在無(wú)處不在的 “滑動(dòng)結(jié)果集” 操作中,因?yàn)榻Y(jié)果集中只能顯示從數(shù)據(jù)庫(kù)或其他集合中獲取的眾多結(jié)果中的 10 個(gè)。它還可以用于 “反向遍歷” 集合或列表,而無(wú)需每次都從前向后遍歷。插入 ListIterator 比使用向下計(jì)數(shù)整數(shù)參數(shù) List.get() “反向” 遍歷 List 容易得多。

3. 并非所有 Iterable 都來(lái)自集合

Ruby 和 Groovy 開(kāi)發(fā)人員喜歡炫耀他們?nèi)绾文艿麄€(gè)文本文件并通過(guò)一行代碼將其內(nèi)容輸出到控制臺(tái)。通常,他們會(huì)說(shuō)在 Java 編程中完成同樣的操作需要很多行代碼:打開(kāi) FileReader,然后打開(kāi) BufferedReader,接著創(chuàng)建 while() 循環(huán)來(lái)調(diào)用 getLine(),直到它返回 null。當(dāng)然,在 try/catch/finally 塊中必須要完成這些操作,它要處理異常并在結(jié)束時(shí)關(guān)閉文件句柄。

這看起來(lái)像是一個(gè)沒(méi)有意義的學(xué)術(shù)上的爭(zhēng)論,但是它也有其自身的價(jià)值。

他們(包括相當(dāng)一部分 Java 開(kāi)發(fā)人員)不知道并不是所有 Iterable 都來(lái)自集合。Iterable 可以創(chuàng)建 Iterator,該迭代器知道如何憑空制造下一個(gè)元素,而不是從預(yù)先存在的 Collection 中盲目地處理:

清單 2. 迭代文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// FileUtils.java
import java.io.*;
import java.util.*;
public class FileUtils
{
    public static Iterable<String> readlines(String filename)
        throws IOException
    {
        final FileReader fr = new FileReader(filename);
        final BufferedReader br = new BufferedReader(fr);
         
        return new Iterable<String>() {
            public <code>Iterator</code><String> iterator() {
                return new <code>Iterator</code><String>() {
                    public boolean hasNext() {
                        return line != null;
                    }
                    public String next() {
                        String retval = line;
                        line = getLine();
                        return retval;
                    }
                    public void remove() {
                        throw new UnsupportedOperationException();
                    }
                    String getLine() {
                        String line = null;
                        try {
                            line = br.readLine();
                        }
                        catch (IOException ioEx) {
                            line = null;
                        }
                        return line;
                    }
                    String line = getLine();
                };
            }  
        };
    }
}
//DumpApp.java
import java.util.*;
public class DumpApp
{
    public static void main(String[] args)
        throws Exception
    {
        for (String line : FileUtils.readlines(args[0]))
            System.out.println(line);
    }
}

此方法的優(yōu)勢(shì)是不會(huì)在內(nèi)存中保留整個(gè)內(nèi)容,但是有一個(gè)警告就是,它不能 close() 底層文件句柄(每當(dāng) readLine() 返回 null 時(shí)就關(guān)閉文件句柄,可以修正這一問(wèn)題,但是在 Iterator 沒(méi)有結(jié)束時(shí)不能解決這個(gè)問(wèn)題)。

4. 注意可變的 hashCode()

Map 是很好的集合,為我們帶來(lái)了在其他語(yǔ)言(比如 Perl)中經(jīng)??梢?jiàn)的好用的鍵/值對(duì)集合。JDK 以 HashMap 的形式為我們提供了方便的 Map 實(shí)現(xiàn),它在內(nèi)部使用哈希表實(shí)現(xiàn)了對(duì)鍵的對(duì)應(yīng)值的快速查找。但是這里也有一個(gè)小問(wèn)題:支持哈希碼的鍵依賴(lài)于可變字段的內(nèi)容,這樣容易產(chǎn)生 bug,即使最耐心的 Java 開(kāi)發(fā)人員也會(huì)被這些 bug 逼瘋。

假設(shè)清單 3 中的 Person 對(duì)象有一個(gè)常見(jiàn)的 hashCode() (它使用 firstNamelastNameage 字段 — 所有字段都不是 final 字段 — 計(jì)算 hashCode()),對(duì) Mapget() 調(diào)用會(huì)失敗并返回 null

清單 3. 可變 hashCode() 容易出現(xiàn) bug
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// Person.java
import java.util.*;
public class Person
    implements Iterable<Person>
{
    public Person(String fn, String ln, int a, Person... kids)
    {
        this.firstName = fn; this.lastName = ln; this.age = a;
        for (Person kid : kids)
            children.add(kid);
    }
     
    // ...
     
    public void setFirstName(String value) { this.firstName = value; }
    public void setLastName(String value) { this.lastName = value; }
    public void setAge(int value) { this.age = value; }
     
    public int hashCode() {
        return firstName.hashCode() & lastName.hashCode() & age;
    }
    // ...
    private String firstName;
    private String lastName;
    private int age;
    private List<Person> children = new ArrayList<Person>();
}
// MissingHash.java
import java.util.*;
public class MissingHash
{
    public static void main(String[] args)
    {
        Person p1 = new Person("Ted", "Neward", 39);
        Person p2 = new Person("Charlotte", "Neward", 38);
        System.out.println(p1.hashCode());
         
        Map<Person, Person> map = new HashMap<Person, Person>();
        map.put(p1, p2);
         
        p1.setLastName("Finkelstein");
        System.out.println(p1.hashCode());
         
        System.out.println(map.get(p1));
    }
}

很顯然,這種方法很糟糕,但是解決方法也很簡(jiǎn)單:永遠(yuǎn)不要將可變對(duì)象類(lèi)型用作 HashMap 中的鍵。

5. equals()Comparable

在瀏覽 Javadoc 時(shí),Java 開(kāi)發(fā)人員常常會(huì)遇到 SortedSet 類(lèi)型(它在 JDK 中唯一的實(shí)現(xiàn)是 TreeSet)。因?yàn)?SortedSetjava.util 包中唯一提供某種排序行為的 Collection,所以開(kāi)發(fā)人員通常直接使用它而不會(huì)仔細(xì)地研究它。清單 4 展示了:

清單 4. SortedSet,我很高興找到了它!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.*;
public class UsingSortedSet
{
    public static void main(String[] args)
    {
        List<Person> persons = Arrays.asList(
            new Person("Ted", "Neward", 39),
            new Person("Ron", "Reynolds", 39),
            new Person("Charlotte", "Neward", 38),
            new Person("Matthew", "McCullough", 18)
        );
        SortedSet ss = new TreeSet(new Comparator<Person>() {
            public int compare(Person lhs, Person rhs) {
                return lhs.getLastName().compareTo(rhs.getLastName());
            }
        });
        ss.addAll(perons);
        System.out.println(ss);
    }
}

使用上述代碼一段時(shí)間后,可能會(huì)發(fā)現(xiàn)這個(gè) Set 的核心特性之一:它不允許重復(fù)。該特性在 Set Javadoc 中進(jìn)行了介紹。Set 是不包含重復(fù)元素的集合。更準(zhǔn)確地說(shuō),set 不包含成對(duì)的 e1 和 e2 元素,因此如果 e1.equals(e2),那么最多包含一個(gè) null 元素。

但實(shí)際上似乎并非如此 — 盡管 清單 4 中沒(méi)有相等的 Person 對(duì)象(根據(jù) Personequals() 實(shí)現(xiàn)),但在輸出時(shí)只有三個(gè)對(duì)象出現(xiàn)在 TreeSet 中。

與 set 的有狀態(tài)本質(zhì)相反,TreeSet 要求對(duì)象直接實(shí)現(xiàn) Comparable 或者在構(gòu)造時(shí)傳入 Comparator,它不使用 equals() 比較對(duì)象;它使用 Comparator/ComparablecomparecompareTo 方法。

因此存儲(chǔ)在 Set 中的對(duì)象有兩種方式確定相等性:大家常用的 equals() 方法和 Comparable/Comparator 方法,采用哪種方法取決于上下文。

更糟的是,簡(jiǎn)單的聲明兩者相等還不夠,因?yàn)橐耘判驗(yàn)槟康牡谋容^不同于以相等性為目的的比較:可以想象一下按姓排序時(shí)兩個(gè) Person 相等,但是其內(nèi)容卻并不相同。

一定要明白 equals()Comparable.compareTo() 兩者之間的不同 — 實(shí)現(xiàn) Set 時(shí)會(huì)返回 0。甚至在文檔中也要明確兩者的區(qū)別。

結(jié)束語(yǔ)

Java Collections 庫(kù)中有很多有用之物,如果您能加以利用,它們可以讓您的工作更輕松、更高效。但是發(fā)掘這些有用之物可能有點(diǎn)復(fù)雜,比如只要您不將可變對(duì)象類(lèi)型作為鍵,您就可以用自己的方式使用 HashMap。

至此我們挖掘了 Collections 的一些有用特性,但我們還沒(méi)有挖到金礦:Concurrent Collections,它在 Java 5 中引入。本 系列 的后 5 個(gè)竅門(mén)將關(guān)注 java.util.concurrent。

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買(mǎi)等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類(lèi)似文章 更多