GUAVA--集合(新集合類型)

1、新集合類型

Guava 引入了不少 JDK 沒有的、但咱們發現明顯有用的新集合類型。這些新類型是爲了和 JDK 集合框架共存,而沒有往 JDK 集合抽象中硬塞其餘概念。做爲通常規則,Guava 集合很是精準地遵循了 JDK 接口契約。java

1.一、Multiset

統計一個詞在文檔中出現了多少次,傳統的作法是這樣的:數組

Map<String, Integer> counts = new HashMap<String, Integer>();
for (String word : words) {
	Integer count = counts.get(word);
	if (count == null) {
		counts.put(word, 1);
	} else {
	counts.put(word, count + 1);
	}
}

這種寫法很笨拙,也容易出錯,而且不支持同時收集多種統計信息,如總詞數。安全

Guava 提供了一個新集合類型 Multiset,它能夠屢次添加相等的元素。維基百科從數學角度這樣定義 Multiset:集合[set]概念的延伸,它的元素能夠重複出現…與集合[set]相同而與元組[tuple]相反的是,Multiset 元素的順序是可有可無的:Multiset {a, a, b}和{a, b, a}是相等的」。——譯者注:這裏所說的集合[set]是數學上的概念,Multiset繼承自 JDK 中的 Collection 接口,而不是 Set 接口,因此包含重複元素並無違反原有的接口契約。數據結構

Multiset和Set的區別就是能夠保存多個相同的對象。在JDK中,List和Set有一個基本的區別,就是List能夠包含多個相同對象,且是有順序的,而Set不能有重複,且不保證順序(有些實現有順序,例如LinkedHashSet和SortedSet等)因此Multiset佔據了List和Set之間的一個灰色地帶:容許重複,可是不保證順序。框架

常見使用場景:Multiset有一個有用的功能,就是跟蹤每種對象的數量,因此你能夠用來進行數字統計。 常見的普通實現方式以下:工具

public void testWordCount() {
	String strWorld = "wer|dffd|ddsa|dfd|dreg|de|dr|ce|ghrt|cf|gt|ser|tg|ghrt|cf|gt|"
			+ "ser|tg|gt|kldf|dfg|vcd|fg|gt|ls|lser|dfr|wer|dffd|ddsa|dfd|dreg|de|dr|"
			+ "ce|ghrt|cf|gt|ser|tg|gt|kldf|dfg|vcd|fg|gt|ls|lser|dfr";
	String[] words = strWorld.split("\\|");
	Map<String, Integer> countMap = new HashMap<String, Integer>();
	for (String word : words) {
		Integer count = countMap.get(word);
		if (count == null) {
			countMap.put(word, 1);
		} else {
			countMap.put(word, count + 1);
		}
	}
	System.out.println("countMap:");
	for (String key : countMap.keySet()) {
		System.out.println(key + " count:" + countMap.get(key));
	}
}

上面的代碼實現的功能很是簡單,用於記錄字符串在數組中出現的次數。這種場景在實際的開發過程仍是容易常常出現的,若是使用實現Multiset接口的具體類就能夠很容易實現以上的功能需求:優化

public void testMultsetWordCount() {
	String strWorld = "wer|dfd|dd|dfd|dda|de|dr";
	String[] words = strWorld.split("\\|");
	List<String> wordList = new ArrayList<String>();
	for (String word : words) {
		wordList.add(word);
	}
	Multiset<String> wordsMultiset = HashMultiset.create();
	wordsMultiset.addAll(wordList);

	for (String key : wordsMultiset.elementSet()) {
		System.out.println(key + " count:" + wordsMultiset.count(key));
	}
}

Multiset接口定義的接口主要有:spa

  • add(E element) :向其中添加單個元素
  • add(E element,int occurrences) : 向其中添加指定個數的元素
  • count(Object element) : 返回給定參數元素的個數
  • remove(E element) : 移除一個元素,其count值 會響應減小
  • remove(E element,int occurrences): 移除相應個數的元素
  • elementSet() : 將不一樣的元素放入一個Set中
  • entrySet(): 相似與Map.entrySet 返回Set<Multiset.Entry>。包含的Entry支持使用getElement()和getCount()
  • setCount(E element ,int count): 設定某一個元素的重複次數
  • setCount(E element,int oldCount,int newCount): 將符合原有重複個數的元素修改成新的重複次數
  • retainAll(Collection c) : 保留出如今給定集合參數的全部的元素
  • removeAll(Collectionc) : 去除出現給給定集合參數的全部的元素

Multiset不是Mapcode

須要注意的是Multiset不是一個Map<E,Integer>,儘管Multiset提供一部分相似的功能實現。其它值得關注的差異有:orm

Multiset中的元素的重複個數只會是正數,且最大不會超過Integer.MAX_VALUE。設定計數爲0的元素將不會出現multiset中,也不會出現elementSet()和entrySet()的返回結果中。

multiset.size() 方法返回的是全部的元素的總和,至關因而將全部重複的個數相加。若是須要知道每一個元素的個數可使用elementSet().size()獲得.(於是調用add(E)方法會是multiset.size()增長1)。

multiset.iterator() 會循環迭代每個出現的元素,迭代的次數與multiset.size()相同。 iterates over each occurrence of each element, so the length of the iteration is equal to multiset.size()。

Multiset 支持添加、移除多個元素以及從新設定元素的個數。執行setCount(element,0)至關於移除multiset中全部的相同元素。

調用multiset.count(elem)方法時,若是該元素不在該集中,那麼返回的結果只會是0。

Multiset的實現Guava提供了Multiset的多種實現,這些實現基本對應了JDK中Map的實現:

Map 對應的Multiset 是否支持null元素
HashMap HashMultiset Yes
TreeMap TreeMultiset Yes(若是 comparator 支持的話)
LinkedHashMap LinkedHashMultiset Yes
ConcurrentHashMap ConcurrentHashMultiset No
ImmutableMap ImmutableMultiset No

1.二、Multimap

在平常的開發工做中,咱們有的時候須要構造像Map<K, List<V>>或者Map<K, Set<V>>這樣比較複雜的集合類型的數據結構,以便作相應的業務邏輯處理。

像 Map<String, List<StudentScore>> StudentScoreMap = new HashMap<String, List<StudentScore>>()這樣的數據結構,本身實現起來太麻煩,你須要檢查key是否存在,不存在時則建立一個,存在時在List後面添加上一個。這個過程是比較痛苦的,若是你但願檢查List中的對象是否存在,刪除一個對象,或者遍歷整個數據結構,那麼則須要更多的代碼來實現。

Guava的Multimap就提供了一個方便地把一個鍵對應到多個值的數據結構。讓咱們能夠簡單優雅的實現上面複雜的數據結構,讓咱們的精力和時間放在實現業務邏輯上,而不是在數據結構上,下面咱們具體來看看Multimap的相關知識點。

public class MultimapTest {

    Map<String, List<StudentScore>> StudentScoreMap = new HashMap<String, List<StudentScore>>();
    
    public void testStudentScore(){
        
        for(int i=10;i<20;i++){
            StudentScore studentScore=new StudentScore();
            studentScore.CourseId=1001+i;
            studentScore.score=100-i;
            addStudentScore("peida",studentScore);
        }
        
        System.out.println("StudentScoreMap:"+StudentScoreMap.size());
        System.out.println("StudentScoreMap:"+StudentScoreMap.containsKey("peida"));
            
        System.out.println("StudentScoreMap:"+StudentScoreMap.containsKey("jerry"));
        System.out.println("StudentScoreMap:"+StudentScoreMap.size());
        System.out.println("StudentScoreMap:"+StudentScoreMap.get("peida").size());

        List<StudentScore> StudentScoreList=StudentScoreMap.get("peida");
        if(StudentScoreList!=null&&StudentScoreList.size()>0){
            for(StudentScore stuScore:StudentScoreList){
                System.out.println("stuScore one:"+stuScore.CourseId+" score:"+stuScore.score);
            }
        }
    }
    
    public void addStudentScore(final String stuName,final StudentScore studentScore) {
        List<StudentScore> stuScore = StudentScoreMap.get(stuName);
        if (stuScore == null) {
            stuScore = new ArrayList<StudentScore>();
            StudentScoreMap.put(stuName, stuScore);
        }
        stuScore.add(studentScore);
    }
}

class StudentScore{
    int CourseId;
    int score;
}
}

上面的代碼和數據結構用Multimap來實現:

public class MultimapTest {

	Map<String, List<StudentScore>> StudentScoreMap = new HashMap<String, List<StudentScore>>();
	
	public void teststuScoreMultimap(){
	    Multimap<String,StudentScore> scoreMultimap = ArrayListMultimap.create(); 
	    for(int i=10;i<20;i++){
	        StudentScore studentScore=new StudentScore();
	        studentScore.CourseId=1001+i;
	        studentScore.score=100-i;
	        scoreMultimap.put("peida",studentScore);
	    }
	    System.out.println("scoreMultimap:"+scoreMultimap.size());
	    System.out.println("scoreMultimap:"+scoreMultimap.keys());
	    }
	}
	
	class StudentScore{
	    int CourseId;
	    int score;
	}
}

Multimap也支持一系列強大的視圖功能:

  1. asMap把自身Multimap<K, V>映射成Map<K, Collection<V>>視圖。這個Map視圖支持remove和修改操做,可是不支持put和putAll。嚴格地來說,當你但願傳入參數是不存在的key,並且你但願返回的是null而不是一個空的可修改的集合的時候就能夠調用asMap().get(key)。(你能夠強制轉型asMap().get(key)的結果類型-對SetMultimap的結果轉成Set,對ListMultimap的結果轉成List型-可是直接把ListMultimap轉成Map<K, List<V>>是不行的。)
  2. entries視圖是把Multimap裏全部的鍵值對以Collection<Map.Entry<K, V>>的形式展示。
  3. keySet視圖是把Multimap的鍵集合做爲視圖
  4. keys視圖返回的是個Multiset,這個Multiset是以不重複的鍵對應的個數做爲視圖。這個Multiset能夠經過支持移除操做而不是添加操做來修改Multimap。
  5. values()視圖能把Multimap裏的全部值「平展」成一個Collection<V>。這個操做和Iterables.concat(multimap.asMap().values())很類似,只是它返回的是一個完整的Collection。

儘管Multimap的實現用到了Map,但Multimap<K, V>不是Map<K, Collection<V>>。由於二者有明顯區別:

  1. Multimap.get(key)必定返回一個非null的集合。但這不表示Multimap使用了內存來關聯這些鍵,相反,返回的集合只是個容許添加元素的視圖。
  2. 若是你喜歡像Map那樣當不存在鍵的時候要返回null,而不是Multimap那樣返回空集合的話,能夠用asMap()返回的視圖來獲得Map<K, Collection<V>>。(這種狀況下,你得把返回的Collection<V>強轉型爲List或Set)。
  3. Multimap.containsKey(key)只有在這個鍵存在的時候才返回true。
  4. Multimap.entries()返回的是Multimap全部的鍵值對。可是若是須要key-collection的鍵值對,那就得用asMap().entries()。
  5. Multimap.size()返回的是entries的數量,而不是不重複鍵的數量。若是要獲得不重複鍵的數目就得用Multimap.keySet().size()。

Multimap提供了豐富的實現,因此你能夠用它來替代程序裏的Map<K, Collection<V>>,具體的實現以下:

實現 鍵行爲相似 值行爲相似
ArrayListMultimap HashMap ArrayList
HashMultimap HashMap HashSet
LinkedListMultimap* LinkedHashMap* LinkedList*
LinkedHashMultimap** LinkedHashMap LinkedHashMap
TreeMultimap HashMap TreeSet
ImmutableListMultimap ImmutableMap ImmutableList
ImmutableSetMultimap ImmutableMap ImmutableSet

除了兩個不可變形式的實現,其餘全部實現都支持 null 鍵和 null 值*LinkedListMultimap.entries()保留了全部鍵和值的迭代順序。詳情見 doc 連接。

**LinkedHashMultimap 保留了映射項的插入順序,包括鍵插入的順序,以及鍵映射的全部值的插入順序。

請注意,並不是全部的 Multimap 都和上面列出的同樣,使用 Map<K, Collection>來實現(特別是,一些 Multimap 實現用了自定義的 hashTable,以最小化開銷)若是你想要更大的定製化,請用Multimaps.newMultimap(Map, Supplier)或 list 和 set 版本,使用自定義的 Collection、List 或 Set 實現 Multimap。

1.三、BiMap

傳統上,實現鍵值對的雙向映射須要維護兩個單獨的 map,並保持它們間的同步。但這種方式很容易出錯,並且對於值已經在 map 中的狀況,會變得很是混亂。例如:

出現下面一種場景的狀況,咱們就須要額外編寫一些代碼了。首先來看下面一種表示標識序號和文件名的map結構。

Map<Integer,String> logfileMap = Maps.newHashMap();
logfileMap.put(1,"a.log");
logfileMap.put(2,"b.log");
logfileMap.put(3,"c.log");
System.out.println("logfileMap:"+logfileMap);

當咱們須要經過序號查找文件名,很簡單。可是若是咱們須要經過文件名查找其序號時,咱們就不得不遍歷map了。固然咱們還能夠編寫一段Map倒轉的方法來幫助實現倒置的映射關係。

public static <S,T> Map<T,S> getInverseMap(Map<S,T> map) {
	 Map<T,S> inverseMap = new HashMap<T,S>();
	 for(Entry<S,T> entry: map.entrySet()) {
		 inverseMap.put(entry.getValue(), entry.getKey());
	 }
	 return inverseMap;
 }
public void logMapTest(){
	Map<Integer,String> logfileMap = Maps.newHashMap();
	logfileMap.put(1,"a.log");
	logfileMap.put(2,"b.log");
	logfileMap.put(3,"c.log");

	System.out.println("logfileMap:"+logfileMap);

	Map<String,Integer> logfileInverseMap = Maps.newHashMap();

	logfileInverseMap=getInverseMap(logfileMap);

	System.out.println("logfileInverseMap:"+logfileInverseMap);
}

上面的代碼能夠幫助咱們實現map倒轉的要求,可是還有一些咱們須要考慮的問題:

  1. 如何處理重複的value的狀況。不考慮的話,反轉的時候就會出現覆蓋的狀況.
  2. 若是在反轉的map中增長一個新的key,倒轉前的map是否須要更新一個值呢?

在這種狀況下須要考慮的業務之外的內容就增長了,編寫的代碼也變得不那麼易讀了。這時咱們就能夠考慮使用Guava中的BiMap了。

Bimap使用很是的簡單,對於上面的這種使用場景,咱們能夠用很簡單的代碼就實現了:

public void BimapTest(){
	BiMap<Integer,String> logfileMap = HashBiMap.create(); 
	logfileMap.put(1,"a.log");
	logfileMap.put(2,"b.log");
	logfileMap.put(3,"c.log");
	System.out.println("logfileMap:"+logfileMap);
	BiMap<String,Integer> filelogMap = logfileMap.inverse();
	System.out.println("filelogMap:"+filelogMap);
}

logfileMap.put(5,"d.log") 會拋出java.lang.IllegalArgumentException: value already present: d.log的錯誤。若是咱們確實須要插入重複的value值,那能夠選擇forcePut方法。可是咱們須要注意的是前面的key也會被覆蓋了。

public void BimapTest(){
	BiMap<Integer,String> logfileMap = HashBiMap.create();
	logfileMap.put(1,"a.log");
	logfileMap.put(2,"b.log");
	logfileMap.put(3,"c.log");

	logfileMap.put(4,"d.log");
	logfileMap.forcePut(5,"d.log");
	System.out.println("logfileMap:"+logfileMap);
}

inverse方法會返回一個反轉的BiMap,可是注意這個反轉的map不是新的map對象,它實現了一種視圖關聯,這樣你對於反轉後的map的全部操做都會影響原先的map對象。

鍵–值實現 值–鍵實現 對應的BiMap實現
HashMap HashMap HashBiMap
ImmutableMap ImmutableMap ImmutableBiMap
EnumMap EnumMap EnumBiMap
EnumMap HashMap EnumHashBiMap
注:Maps 類中還有一些諸如 synchronizedBiMap 的 BiMap 工具方法。

一般來講,當你想使用多個鍵作索引的時候,你可能會用相似 Map<FirstName, Map<LastName, Person>>的實現,這種方式很醜陋,使用上也不友好。Guava 爲此提供了新集合類型 Table,它有兩個支持全部類型的鍵:」行」和」列」。Table 提供多種視圖,以便你從各類角度使用它:

  • rowMap():用 Map<R, Map<C, V>>表現 Table<R, C, V>。一樣的, rowKeySet()返回」行」的集合Set。
  • row(r) :用 Map<C, V>返回給定」行」的全部列,對這個 map 進行的寫操做也將寫入 Table 中。
  • 相似的列訪問方法:columnMap()、columnKeySet()、column(c)。(基於列的訪問會比基於的行訪問稍微低效點)
  • cellSet():用元素類型爲 Table.Cell<R, C, V>的 Set 表現 Table<R, C, V>。Cell 相似於 Map.Entry,但它是用行和列兩個鍵區分的。

Table 有以下幾種實現:

  • HashBasedTable:本質上用 HashMap<R, HashMap<C, V>>實現;
  • TreeBasedTable:本質上用 TreeMap<R, TreeMap<C,V>>實現;
  • ImmutableTable:本質上用 ImmutableMap<R, ImmutableMap<C, V>>實現;注:ImmutableTable對稀疏或密集的數據集都有優化。
  • ArrayTable:要求在構造時就指定行和列的大小,本質上由一個二維數組實現,以提高訪問速度和密集 Table 的內存利用率。ArrayTable 與其餘 Table 的工做原理有點不一樣,請參見 Javadoc 瞭解詳情。
public void TableTest(){
	Table<String, Integer, String> aTable = HashBasedTable.create();

	for (char a = 'A'; a <= 'C'; ++a) {
		for (Integer b = 1; b <= 3; ++b) {
			aTable.put(Character.toString(a), b, String.format("%c%d", a, b));
		}
	}

	System.out.println(aTable.column(2));
	System.out.println(aTable.row("B"));
	System.out.println(aTable.get("B", 2));

	System.out.println(aTable.contains("D", 1));
	System.out.println(aTable.containsColumn(3));
	System.out.println(aTable.containsRow("C"));
	System.out.println(aTable.columnMap());
	System.out.println(aTable.rowMap());

	System.out.println(aTable.remove("B", 3));
}

1.四、ClassToInstanceMap

ClassToInstanceMap 是一種特殊的 Map:它的鍵是類型,而值是符合鍵所指類型的對象。

爲了擴展 Map 接口,ClassToInstanceMap 額外聲明瞭兩個方法:T getInstance(Class) 和 T putInstance(Class, T),從而避免強制類型轉換,同時保證了類型安全。

ClassToInstanceMap 有惟一的泛型參數,一般稱爲 B,表明 Map 支持的全部類型的上界。

ClassToInstanceMap<Number> numberDefaults = MutableClassToInstanceMap.create();
numberDefaults.putInstance(Integer.class, Integer.valueOf(0));

大體意思爲:

有時,咱們的映射鍵並非全部類型都相同:它們是類型,如鍵分別是Integer、Bigdecimal等,可是他們都是屬於Number類型.咱們若是但願將它們映射到該類型的值,就可使用Guava爲此提供的新集合類型 - ClassToInstanceMap。

值得注意的是:

  • ClassToInstanceMap除了擴展Map接口以外,ClassToInstanceMap還提供了T getInstance(類)和T putInstance(類, T)方法,這消除了強制類型安全強制轉換的須要。
  • ClassToInstanceMap只有一個類型參數.表示了全部的key不能超出這個類型參數的範圍。
  • 從技術上講,ClassToInstanceMap實現了Map<Class<? extends B>, B>或者換句話說,從map的子類B B .這可使泛型類型實例參與ClassToInstanceMap有點混亂,可是隻要記住B老是上限的類型映射——一般,B是對象。
public void classToInstanceMapTest(){
    //可存放全部數字類型的map
    ClassToInstanceMap<Number> numberDefaults= MutableClassToInstanceMap.create();
    numberDefaults.putInstance(Integer.class, 1);
    numberDefaults.putInstance(Double.class, 1.00);
    numberDefaults.putInstance(BigDecimal.class, new BigDecimal("52"));
    numberDefaults.put(Integer.class, 2);
    numberDefaults.replace(Integer.class, 3); //修改
    numberDefaults.getInstance(Integer.class); //獲取value 3
    //經過傳入新值和舊值計算,修改舊值,例: 將Integer,class的值+3
    numberDefaults.merge(Integer.class,2,(x,y)->x.intValue()+y.intValue());
    numberDefaults.forEach((x,y)-> System.out.println(x+","+y));
}

1.五、RangeSet

RangeSet描述了一組不相連的、非空的區間。當把一個區間添加到可變的RangeSet時,全部相連的區間會被合併,空區間會被忽略。例如:

RangeSet<Integer> rangeSet = TreeRangeSet.create();
rangeSet.add(Range.closed(1, 10)); // {[1,10]}
rangeSet.add(Range.closedOpen(11, 15));//不相連區間:{[1,10], [11,15)}
rangeSet.add(Range.closedOpen(15, 20)); //相連區間; {[1,10], [11,20)}
rangeSet.add(Range.openClosed(0, 0)); //空區間; {[1,10], [11,20)}
rangeSet.remove(Range.open(5, 10)); //分割[1, 10]; {[1,5], [10,10], [11,20)}

請注意,要合併 Range.closed(1, 10)和 Range.closedOpen(11, 15)這樣的區間,你須要首先用 Range.canonical(DiscreteDomain)對區間進行預處理,例如 DiscreteDomain.integers()。

注:RangeSet不支持 GWT,也不支持 JDK5 和更早版本;由於,RangeSet 須要充分利用 JDK6 中 NavigableMap 的特性。

RangeSet 的實現支持很是普遍的視圖:

  • complement():返回 RangeSet 的補集視圖。complement 也是 RangeSet 類型,包含了不相連的、非空的區間。
  • subRangeSet(Range):返回 RangeSet 與給定 Range 的交集視圖。這擴展了傳統排序集合中的 headSet、subSet 和 tailSet 操做。
  • asRanges():用 Set<Range>表現 RangeSet,這樣能夠遍歷其中的Range。
  • asSet(DiscreteDomain)(僅 ImmutableRangeSet 支持):用 ImmutableSortedSet表現 RangeSet,以區間中全部元素的形式而不是區間自己的形式查看。(這個操做不支持 DiscreteDomain 和 RangeSet 都沒有上邊界,或都沒有下邊界的狀況)

爲了方便操做,RangeSet 直接提供了若干查詢方法,其中最突出的有:

  • contains(C):RangeSet 最基本的操做,判斷 RangeSet 中是否有任何區間包含給定元素。
  • rangeContaining(C):返回包含給定元素的區間;若沒有這樣的區間,則返回 null。
  • encloses(Range):簡單明瞭,判斷 RangeSet 中是否有任何區間包括給定區間。
  • span():返回包括 RangeSet 中全部區間的最小區間。

1.六、RangeMap

RangeMap 描述了」不相交的、非空的區間」到特定值的映射。和 RangeSet 不一樣,RangeMap 不會合並相鄰的映射,即使相鄰的區間映射到相同的值。例如:

RangeMap<Integer, String> rangeMap = TreeRangeMap.create();
rangeMap.put(Range.closed(1, 10), "foo"); //{[1,10] => "foo"}
rangeMap.put(Range.open(3, 6), "bar"); //{[1,3] => "foo", (3,6) => "bar", [6,10] => "foo"}
rangeMap.put(Range.open(10, 20), "foo"); //{[1,3] => "foo", (3,6) => "bar", [6,10] => "foo", (10,20) => "foo"}
rangeMap.remove(Range.closed(5, 11)); //{[1,3] => "foo", (3,5) => "bar", (11,20) => "foo"}

遍歷:

Map<Range<Integer>, Integer> map = rangeMap.asMapOfRanges();
Set<Map.Entry<Range<Integer>, Integer>> entrySet = map.entrySet();
Set<Range<Integer>> keySet = map.keySet();
Collection<Integer> values = map.values();

RangeMap 提供兩個視圖:

  • asMapOfRanges():用 Map<Range, V>表現 RangeMap。這能夠用來遍歷 RangeMap。
  • subRangeMap(Range):用 RangeMap 類型返回 RangeMap 與給定 Range 的交集視圖。這擴展了傳統的 headMap、subMap 和 tailMap 操做。
相關文章
相關標籤/搜索