Java8下的集合操作


使用Lambda表達式遍歷集合

Java8為Iterable接口新增了一個forEach(Consumer action)默認方法,該方法所需參數的類型是一個函數式接口,而Iterable接口是一個Collection接口的父接口,因此Collection集合也可以直接調用該方法。

當程序調用Iterable的forEach(Consumer action)遍歷集合元素時,程序會依次將集合元素傳給Consumer的accept(T t)方法(該接口中唯一的抽象方法)。正因為Consumer是函數式接口,因此可以使用Lambda表達式來遍歷集合元素。

public class CollectionEach {
    public static void main(String[] args) {
        //創建一個集合
        Collection books = new HashSet();
        books.add("三國演義");
        books.add("水滸傳");
        books.add("紅樓夢");
        //使用forEach()方法遍歷
        books.forEach(obj-> System.out.println("迭代集合元素:"+obj));
    }
}

使用Java8增強的Iterator遍歷集合元素

Iterator接口也是Java集合框架的成員,但它與Collection系列、Map系列的集合不一樣:Collection系列集合、Map系列集合主要用於盛裝其他對象,而Iterator則用於遍歷Collection集合中的元素,Iterator也被稱為迭代器。

Iterator接口隱藏了各種Collection實現類的底層細節,向應用程序提供了遍歷Collection集合元素的統一編程接口。Iterator接口里定義了如下4個方法:

  • boolean hasNext():如果被迭代的集合元素還沒有被遍歷完,則返回true
  • Object next():返回集合里的下一個元素
  • void remove():刪除集合里上一次next方法返回的元素
  • void forEachRemaining(Consumer action):這是Java8為Iterator新增的默認方法,該方法可以使用Lambda表達式來遍歷集合元素
//創建一個集合
Collection books = new HashSet();
books.add("三國演義");
books.add("水滸傳");
books.add("紅樓夢");

//獲取books集合對應的迭代器
Iterator it = books.iterator();
it.forEachRemaining(System.out::println);

運行結果如下:

水滸傳
三國演義
紅樓夢

下面程序示范了通過Iterator接口來遍歷集合元素。

public class IteratorTest {
    public static void main(String[] args) {
        //創建一個集合
        Collection books = new HashSet();
        books.add("三國演義");
        books.add("水滸傳");
        books.add("紅樓夢");

        //獲取books集合對應的迭代器
        Iterator it = books.iterator();
//        it.forEachRemaining(System.out::println);
        while (it.hasNext()) {
            //it.next()方法返回的數據類型是Object類型,因此需要強制類型轉換
            String book = (String) it.next();
            System.out.println(book);
            if (book.equals("紅樓夢")) {
                //從集合中刪除上一次next()方法返回的元素
                it.remove();
            }
            //對book變量賦值,不會改變集合元素本身
            book="測試";
        }
        System.out.println(books);
    }
}

從上面代碼可看出,Iterator僅用於遍歷集合,Iterator本身不具有盛裝對線的能力。如果需要創建Iterator對象,則必須有一個被迭代的集合。沒有集合的Iterator仿佛無本之木,沒有存在的價值。

上面book="測試";對迭代變量book進行賦值,但當再次輸出books集合時,會看到集合里的元素沒有任何改變。這就可以得到一個結論:當使用Iterator對集合元素進行迭代時,Iterator並不是把集合元素本身傳給了迭代變量,而是把集合元素的值傳給了迭代變量,所以修改迭代變量的值對集合元素本身沒有任何影響。

當使用Iterator迭代訪問Collection集合元素時,Collection集合里的元素不能被改變,只有通過Iterator的remove()方法刪除上一次next()方法返回的集合元素才可以;否則將會發生Exception in thread "main" java.util.ConcurrentModificationException異常

//創建一個集合
Collection books = new HashSet();
books.add("三國演義");
books.add("水滸傳");
books.add("紅樓夢");

//獲取books集合對應的迭代器
Iterator it = books.iterator();
//        it.forEachRemaining(System.out::println);
while (it.hasNext()) {
    //it.next()方法返回的數據類型是Object類型,因此需要強制類型轉換
    String book = (String) it.next();
    System.out.println(book);
    if (book.equals("三國演義")) {
        //從集合中刪除上一次next()方法返回的元素
//                it.remove();
        books.remove(book);
    }
    //對book變量賦值,不會改變集合元素本身
    book="測試";
}

上面books.remove(book);代碼位於Iterator迭代塊內,也就是在Iterator迭代Collection集合過程中修改了Collection集合,所以程序將在運行時引發異常。

Iterator迭代器采用的是快速失敗(fail-fast)機制,一旦在迭代過程中檢測到該集合已經被修改(通常是程序中的其他線程修改),程序立即引發java.util.ConcurrentModificationException異常,而不是顯示修改后的結果,這樣可以避免共享資源而引發的潛在問題。

注意:上面的程序如果改為刪除"紅樓夢"字符串,則不會引發異常,這樣可能有些讀者會"心存僥幸"地想:在迭代時好像也可以刪除集合元素啊。實際上這是一種危險的行為:對於HashSet以及后面的ArrayList等,迭代時刪除元素都會導致異常——只有在刪除集合中的某個特定元素時才不會拋出異常,這是由集合類的實現代碼決定的,程序員不應該這么做。

使用foreach循環遍歷集合元素

除了使用Iterator接口迭代訪問Collection集合里的元素之外,使用Java5提供的foreach循環迭代訪問集合元素更加便捷。

public class ForeachTest {
    public static void main(String[] args) {
        //創建一個集合
        Collection books = new HashSet();
        books.add("三國演義");
        books.add("水滸傳");
        books.add("紅樓夢");

        for (Object obj:books) {
            //此處的book變量也不是集合元素本身
            String book = (String)obj;
            System.out.println(book);
            if (book.equals("三國演義")){
                //引發java.util.ConcurrentModificationException異常
                books.remove(book);
            }
        }
        System.out.println(books);
    }
}

與使用Iterator接口迭代訪問集合元素類似的事,foreach循環中的迭代變量也不是集合元素本身,系統只是依次把集合元素的值賦給迭代變量,因此在foreach循環中修改迭代變量的值也沒有任何實際意義。

同樣,當使用foreach循環迭代訪問集合元素時,該集合也不能被改變,否則將引發ConcurrentModificationException異常。

使用Java8新增的Predicate操作集合

Java8為Collection集合新增了一個removeIf(Predicate filter)方法,該方法將會批量刪除符合filter條件的所有元素。該方法需要一個Predicate(謂詞)對象作為參數,Predicate也是函數式接口,因此可以使用Lambda表達式作為參數。

下面程序示范了使用Predicate來過濾集合

public class PredicateTest {
    public static void main(String[] args) {
        //創建一個集合
        Collection books = new HashSet<>();
        books.add("輕量級Java EE企業應用實戰");
        books.add("瘋狂Java講義");
        books.add("瘋狂Ios講義");
        books.add("瘋狂Ajax講義");
        books.add("瘋狂Android講義");
        //使用Lambda表達式(目標類型是Predicate)過濾集合
        books.removeIf(ele->((String)ele).length()<10);
        System.out.println(books);
    }
}

上面books.removeIf(ele->((String)ele).length()<10);調用了Collection集合的removeIf()方法批量刪除集合中符合條件的元素,程序傳入一個Lambda表達式作為過濾條件:所有長度小於10的字符串元素都會被刪除。運行結果是

[瘋狂Android講義, 輕量級Java EE企業應用實戰]

使用Predicate可以充分簡化集合的運算,假設依然有上面程序所示的books集合,如果程序有如下三個統計需求:

  • 統計書名中出現"瘋狂"字符串的圖書數量
  • 統計書名中出現"Java"字符串的圖書數量
  • 統計書名長度大於10的圖書數量

如果采用傳統的編程方式來完成這些需求,則需要執行三次循環,但采用Predicate只需要一個方法即可。如下:

public class PredicateTest1 {
    public static void main(String[] args) {
        //創建一個集合
        Collection books = new HashSet<>();
        books.add("輕量級Java EE企業應用實戰");
        books.add("瘋狂Java講義");
        books.add("瘋狂Ios講義");
        books.add("瘋狂Ajax講義");
        books.add("瘋狂Android講義");
        //統計書名包含"瘋狂"子串的圖書數量
        System.out.println(calAll(books,ele->((String)ele).contains("瘋狂")));
        //統計書名包含"Java"子串的圖書數量
        System.out.println(calAll(books,ele->((String)ele).contains("Java")));
        //統計書名字符串長度大於10的圖書數量
        System.out.println(calAll(books,ele->((String)ele).length()>10));
    }
    public static int calAll(Collection books, Predicate p){
        int total =0;
        for (Object obj:books) {
            //使用Predicate的test()方法判斷該對象是否滿足Predicate指定的條件
            if (p.test(obj)){
                total++;
            }
        }
        return total;
    }
}

上面程序先定義了一個calAll()方法,該方法將會使用Predicate判斷每個集合元素是否符合特定條件——該條件通過Predicate參數動態傳入。

使用Java8新增的Stream操作集合

Java8還新增了Stream、InStream、LongStream、DoubleStream等流式API,這些API代表多個支持串行和並行聚集操作的元素。上面4個接口中,Stream是一個通用的流接口,而IntStream、LongStream、DoubleStream則代表元素類型為int、long、double的流。

Java8還為上面每個流式API提供了對應的Builder,例如Stream.Builder、IntStream.Builder、LongStream.Builder、DoubleStream.Builer,開發者可以通過這些Builder來創建對應的流。

獨立使用Stream的步驟如下:

  1. 使用Stream或XXXStream的builder()類方法創建該Stream對應的Builder
  2. 重復調用Builder的add()方法向該流中添加多個元素
  3. 調用Builder的build()方法獲取對應的Stream
  4. 調用Stream的聚集方法

在上面4個步驟中,第4步可以根據具體需求來調用不同的方法,Stream提供了大量的聚集方法供用戶調用,具體可參考Stream或XXXStream的API文檔。對於大部分聚集方法而言,每個Stream只能執行一次。

public class IntStreamTest {
    public static void main(String[] args) {
        IntStream is = IntStream.builder().add(20).add(13).add(-2).add(18).build();
        //下面調用聚集方法的代碼每次只能執行一行
        System.out.println("is所有元素的最大值:"+is.max().getAsInt());
        System.out.println("is所有元素的最小值:"+is.min().getAsInt());
        System.out.println("is所有元素的總和:"+is.sum());
        System.out.println("is所有元素的總數:"+is.count());
        System.out.println("is所有元素的平均值:"+is.average());
        System.out.println("is所有元素的平方是否都大於20:"+is.allMatch(ele->ele*ele>20));
        System.out.println("is是否包含任一元素的平方大於20:"+is.anyMatch(ele->ele*ele>20));
        //將is映射成一個新的Stream,新的Stream的每個元素是原Stream元素的2倍+1
        IntStream newIs = is.map(ele -> ele * 2 + 1);
        //使用方法引用的方式來遍歷集合元素
        newIs.forEach(System.out::println);
    }
}

如上面代碼中的很多行都使用了is,這樣就會產生Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed。所以上面的使用is的代碼,每次只能執行一行,要把其他使用is的代碼注釋掉。




除此之外,Java8允許使用流式API來操作集合,Collection接口提供了一個Stream()默認方法,該方法可返回該集合對應的流,接下來即可通過流式API來操作集合元素。由於Stream可以對集合元素進行整體的聚集操作,因此Stream極大地豐富了集合的功能。

例如,上一個知識點的示例程序中,該程序需要額外定義一個calAll()方法來遍歷集合元素,然后依次對每個集合元素進行判斷——這太麻煩了。如果使用Stream,即可直接對集合中所有元素進行批量操作。下面使用Stream來改寫這個程序:

public class CollectionStream {
    public static void main(String[] args) {
        //創建一個集合
        Collection books = new HashSet<>();
        books.add("輕量級Java EE企業應用實戰");
        books.add("瘋狂Java講義");
        books.add("瘋狂Ios講義");
        books.add("瘋狂Ajax講義");
        books.add("瘋狂Android講義");
        //統計書名包含"瘋狂"子串的圖書數量
        System.out.println(books.stream().filter(ele->((String)ele).contains("瘋狂")).count());
        //統計書名包含"Java"子串的圖書數量
        System.out.println(books.stream().filter(ele->((String)ele).contains("Java")).count());
        //統計書名字符串長度大於10的圖書數量
        System.out.println(books.stream().filter(ele->((String)ele).length()>10).count());
        //先調用Collection對象的Stream()方法將集合轉化為Stream
        //再調用Stream的mapToInt()方法獲取原有的Stream對應的IntStream
        //調用forEach()方法遍歷IntStream中每個元素
        System.out.println("集合中每個元素的長度:");
        books.stream().mapToInt(ele->((String)ele).length()).forEach(System.out::println);
    }
}

通過上面的代碼可以看出,程序只要調用Collection的Stream()方法即可返回該結合對應的Stream,接下來就可通過Stream提供的方法對所有集合元素進行處理,這樣大大地簡化了集合編程的代碼,這也是Stream編程帶來的優勢。

Java8改進的List接口和ListIterator接口

Java8為List集合增加了sort()和replaceAll()兩個常用的默認方法,其中sort()方法需要一個Comparator對象來控制元素排序,程序可以使用Lambda表達式來作為參數:而replaceAll()方法則需要一個UnaryOperator來替換所有集合元素,UnaryOperator也是一個函數式接口,因此程序也可以使用Lambda表達式作為參數。

public class ListTest {
    public static void main(String[] args) {
        List books = new ArrayList();
        //向books集合中添加4個元素
        books.add("輕量級Java EE企業應用實戰");
        books.add("瘋狂Java講義");
        books.add("瘋狂Ios講義");
        books.add("瘋狂Ajax講義");
        books.add("瘋狂Android講義");
        //使用目標類型為Comparator的Lambda表達式對List集合排序
        books.sort((o1,o2)->((String)o1).length()-((String)o2).length());
        System.out.println(books);
        //使用目標類型為UnaryOperator的Lambda表達式來替換集合中所有元素
        //該Lambda表達式控制使用每個字符串的長度作為新的集合元素
        books.replaceAll(ele->((String)ele).length());
        System.out.println(books);
    }
}

運行結果:

[瘋狂Ios講義, 瘋狂Java講義, 瘋狂Ajax講義, 瘋狂Android講義, 輕量級Java EE企業應用實戰]
[7, 8, 8, 11, 16]

上面程序中books.sort((o1,o2)->((String)o1).length()-((String)o2).length());控制對List集合進行排序,傳給sort()方法的Lambda表達式指定的排序規則是:字符串長度越大,字符串越大,因此執行完該代碼后,List集合匯總的字符串會按由短到長的順序排列。

books.replaceAll(ele->((String)ele).length());傳給replaceAll()方法的Lambda表達式指定了替換集合元素的規則:直接用集合元素(字符串)的長度作為新的集合元素。

與Set只提供一個iterator()方法不同,List還額外提供了一個listIterator()方法,該方法返回一個ListIterator對象,ListIterator接口繼承了Iterator接口,提供了專門操作List的方法。ListIterator接口在Iterator接口基礎上增加了如下方法:

  • boolean hasPrevious():返回該迭代器關聯的集合是否還有上一個元素
  • Object previous():返回該迭代器的上一個元素
  • void add(Object o):在指定位置插入一個元素

拿ListIterator與普通的Iterator進行對比,不難發現ListIterator增加了向前迭代的功能(Iterator只能向后迭代),而且ListIterator還可通過add()方法向List集合中添加元素(Iterator只能刪除元素)。下面示范ListIterator的用法:

public class ListIteratorTest {
    public static void main(String[] args) {
        String[] books = {"瘋狂Java講義","瘋狂ios講義","輕量級Java EE企業應用實戰"};
        List bookList = new ArrayList();
        for (int i = 0; i < books.length; i++) {
            bookList.add(books[i]);
        }
        ListIterator lit = bookList.listIterator();
        while (lit.hasNext()) {
            System.out.println(lit.next());
            lit.add("---------分隔符---------");
        }
        System.out.println("===========下面開始反向迭代=====");
        while (lit.hasPrevious()) {
            System.out.println(lit.previous());
        }
    }
}

從上面程序中可以看出,使用ListIterator迭代List集合時,開始也需要采用正向迭代,即先使用next()方法進行迭代,在迭代過程中可以使用add()方法向上一次迭代元素的后面添加一個新元素。運行結果:

瘋狂Java講義
瘋狂ios講義
輕量級Java EE企業應用實戰
===========下面開始反向迭代=====
---------分隔符---------
輕量級Java EE企業應用實戰
---------分隔符---------
瘋狂ios講義
---------分隔符---------
瘋狂Java講義

Java8為Map新增的方法

Java8除了為Map增加remove(Object key,Object value)默認方法外,還增加了如下方法:

public class MapTest {
    public static void main(String[] args) {
        Map map = new HashMap();
        //存放多個鍵值對
        map.put("瘋狂Java講義",109);
        map.put("瘋狂IOS講義",99);
        map.put("瘋狂Ajax講義",79);
        //嘗試替換key為"瘋狂xml講義"的value,由於原map中沒有對應的key
        //因此map沒有改變,不會添加新的鍵值對
        map.replace("瘋狂xml講義",66);
        System.out.println(map);
        //使用原value與傳入參數計算出來的結果覆蓋原有的value
        map.merge("瘋狂IOS講義",10,(oldVal,param)->(Integer)oldVal+(Integer)param);
        System.out.println(map);
        //當key為"java"對應的value為null時,使用計算的結果作為新value
        map.computeIfAbsent("java",(key)->((String)key).length());
        System.out.println(map);
        //當key為"java"對應的value存在時,使用計算的結果作為新value
        map.computeIfPresent("java",(key,value)->(Integer)value*(Integer)value);
        System.out.println(map);
    }
}

運行結果:

{瘋狂Ajax講義=79, 瘋狂IOS講義=99, 瘋狂Java講義=109}
{瘋狂Ajax講義=79, 瘋狂IOS講義=109, 瘋狂Java講義=109}
{瘋狂Ajax講義=79, java=4, 瘋狂IOS講義=109, 瘋狂Java講義=109}
{瘋狂Ajax講義=79, java=16, 瘋狂IOS講義=109, 瘋狂Java講義=109}

Java8改進的HashMap和Hashtable實現類

HashMap和Hashtable都是Map接口的典型實現類,它們之間的關系完全類似於ArrayList和Vector的關系:Hashtable是一個古老的Map實現類,它從JDK1.0就有了,當它出現時,Java還沒有提供Map接口。

Java8改進了HashMap的實現,使用HashMap存在key沖突時依然具有較好的性能。
除此之外,Hashtable與HashMap存在兩點典型區別。

  • Hashtable是一個線程安全的Map實現,但HashMap是線程不安全的實現,所以HashMap比HashTable的性能高一點;但如果有多個線程訪問同一個Map對象時,使用HashTable實現類會更好
  • Hashtable不允許使用null作為key和value,如果視圖把null值放進Hashtable中,會出現空指針異常;但HashMap可以使用null作為key或value

由於HashMap里的key不能重復,所以HashMap里最多只有一個key-value對的key為null,但可以有無數多個key-value對的value為null。下面程序示范了用null值作為HashMap的key和value的情形。

public class NullInHashMap {
    public static void main(String[] args) {
        HashMap hm = new HashMap();
        //試圖將兩個可以為null值的key-value對放入HashMap中
        hm.put(null,null);
        hm.put(null,null);
        //將一個value為null值的key-value對放入HashMap中
        hm.put("a",null);
        //輸出map
        System.out.println(hm);
    }
}

執行結果:

{null=null, a=null}

上面程序試圖向HashMap中放入三個鍵值對,其中第二個hm.put(null,null);無法放入,因為map中已經有一個鍵值對的key為null值,所以無法再放入key為null的鍵值對。hm.put("a",null);可以放入鍵值對,因為一個HashMap中可以有多個value為null值。

為了成功地在HashMap、Hashtable中存儲、獲取對象,用作key的對象必須實現hashCode()方法和equals()方法。
與HashSet集合不能保證元素的順序一樣,HashMap、Hashtable也不能保證其中key-value對的順序。類似於HashSet,HashMap、Hashtable判斷兩個key相等的標准也是:兩個key通過equals()方法比較返回true,兩個key的hashCode值也相等。

除此之外,HashMap、Hashtable中還包含一個containsValue()方法,用於判斷是否包含指定的value。那么HashMap、Hashtable如何判斷兩個value相等呢?HashMap、Hashtable判斷兩個value相等的標准更簡單:只要兩個對象通過equals()方法比較返回true即可。下面程序示范了Hashtable判斷兩個可以相等的標准和兩個value相等的標准。

public class HashtableTest {
    public static void main(String[] args) {
        Hashtable ht = new Hashtable();
        ht.put(new A(60000),"瘋狂Java講義");
        ht.put(new A(89898),"輕量級Java EE企業應用實戰");
        ht.put(new A(1234),new B());
        System.out.println(ht);
        //只要兩個對象通過equals()方法比較返回true
        //Hashtable就認為它們是相等的value
        //由於Hashtable中有一個B對象
        //它與任何對象通過equals()方法比較都相等,所以下面輸出true
        System.out.println(ht.containsValue("測試字符串"));
        //只要兩個A對象的count相等,它們通過equals()方法比較返回true,且hashCode值相等
        //Hashtable即認為它們是相同的key,所以下面輸出true
        System.out.println(ht.containsKey(new A(89898)));
        //下面語句可以刪除最后一個鍵值對
        ht.remove(new A(1234));
        System.out.println(ht);
    }
}

class A{
    int count;
    public A(int count){
        this.count=count;
    }

    //根據count的值來判斷兩個對象是否相等
    @Override
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        }
        if (obj!=null && obj.getClass() ==A.class){
            A a = (A)obj;
            return this.count == a.count;
        }
        return false;
    }

    //根據count來計算hashCode值
    @Override
    public int hashCode() {
        return this.count;
    }
}

class B {
    //重寫equals()方法,B對象與任何對象通過equals()方法比較都返回true

    @Override
    public boolean equals(Object obj) {
        return true;
    }
}

運行結果:

{com.tianhao.luo.collection.map.A@15f2a=輕量級Java EE企業應用實戰, com.tianhao.luo.collection.map.A@ea60=瘋狂Java講義, com.tianhao.luo.collection.map.A@4d2=com.tianhao.luo.collection.map.B@4554617c}
true
true
{com.tianhao.luo.collection.map.A@15f2a=輕量級Java EE企業應用實戰, com.tianhao.luo.collection.map.A@ea60=瘋狂Java講義}

上面程序定義了A類和B類,其中A類判斷兩個A對象相等的標准是count實例變量:只要兩個A對象的count變量相等,則通過equals()方法比較它們返回true,它們的hashCode值也相等;而B對象則可以與任何對象相等。

Hashtable判斷value相等的標准是:value與另外一個對象通過equals()方法比較返回true即可。上面程序中的ht對象包含了一個B對象,它與任何對象通過equals()方法比較總是返回true,所以第一個輸出判斷為true。在這種情況下,不管傳給ht對象的containsValue()方法參數是什么,程序總是返回true。

根據Hashtable判斷兩個key相等的標准,程序在第二個輸出判斷也是true,因為兩個A對象雖然不是同一個對象,但它們通過equals()方法比較返回true,且hashCode值相等,Hashtable即認為它們是同一個key。類似的是,程序在最后ht.remove(new A(1234));可以刪除對應的鍵值對。

與HashSet類似的是,如果使用可變對象作為HashMap、Hashtable的key,並且程序修改了作為key的可變對象,則也可能出現與HashSet類似的情形:程序再也無法准確訪問到map中被修改過的key。

public class HashMapErrorTest {
    public static void main(String[] args) {
        HashMap ht = new HashMap();
        //此處的A類與前一個程序的A類是同一個類
        ht.put(new A(60000),"瘋狂Java講義");
        ht.put(new A(87563),"輕量級Java EE企業應用實戰");
        //獲得Hashtable的key Set集合對應的Iterator迭代器
        Iterator it = ht.keySet().iterator();
        //取出Map中第一個key,並修改它的count值
        A first = (A)it.next();
        first.count=87563;
        System.out.println(ht);
        ht.remove(new A(87563));
        System.out.println(ht);
        //無法獲取剩下的value,下面兩行代碼都將輸出null
        System.out.println(ht.get(new A(87563)));
        System.out.println(ht.get(new A(60000)));
    }
}

該程序使用的還是上一個程序定義的A類實例作為key,而A對象是可變對象。當用first.count=87563;修改了A對象之后,實際上修改了HashMap集合中元素的key,這就導致該key不能被准確訪問。當程序試圖刪除count為87653的A對象時,只能刪除沒有被修改的key所對應的key-value對。程序最后都不能訪問"瘋狂Java講義"字符串,這都是因為它對應的key被修改過的原因。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM