Java 中的map - The Map Interface.

殷老實發表於2017-08-24

翻譯來自The Map Interface

簡介

Map是一個鍵對映到值的物件。 一個Map不能包含任何的重複的鍵,也就是說每個鍵最多對映到一個值。他模擬了數學概念中的對映。Map 介面中包括了基本的操作(put,get,remove etc)和 多元操作 (putall and clear等)還有 集合檢視(keyset 等)


Java 平臺上包含了三種主要的Map 介面的實現。1. Hashmap 2. TreeMap 3. LinkedHashMap。 他們的特徵和效能正是和Hashset, Treeset, LinkedHashSet 類似。


以下的內容讓我們來看看Map 介面的細節。但是首先,這裡有幾個關於JDK 8 Map操作的例子。模擬真實世界的物件是物件導向程式設計的一個常見的任務,所以讓我們來找一個合理的真實世界的例子。 想象下在某個部門有一組員工。


// Group employees by department
Map<Department, List<Employee>> byDept = employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));

或者計算某個部門所有員工的工資總和

// Compute sum of salaries by department
Map<Department, Integer> totalByDept = employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment,
Collectors.summingInt(Employee::getSalary)));


我們還可以通過city分類people 

// Classify Person objects by city
Map<String, List<Person>> peopleByCity
         = personStream.collect(Collectors.groupingBy(Person::getCity));

或者更復雜點的map

// Cascade Collectors 
Map<String, Map<String, List<Person>>> peopleByStateAndCity
  = personStream.collect(Collectors.groupingBy(Person::getState,
  Collectors.groupingBy(Person::getCity)))


(這裡是我自己的: 在java 8中新引用的stream(), 給我門的程式設計提供了很多的便利,stream 簡單來說就是可以講collection<T>轉化成一個T的stream然後對這個stream應用一些方法 例如filter or map etc詳情請見Stream API


Map 介面的基本操作

Map的基本操作例如 (putgetcontainsKeycontainsValuesize, and isEmpty) 他們的表現是和hashtable中是一樣的。以下這段程式碼顯示的是找到輸入list中各個單詞出現的頻數。

import java.util.*;

public class Freq {
    public static void main(String[] args) {
        Map<String, Integer> m = new HashMap<String, Integer>();

        // Initialize frequency table from command line
        for (String a : args) {
            Integer freq = m.get(a);
            m.put(a, (freq == null) ? 1 : freq + 1);
        }

        System.out.println(m.size() + " distinct words:");
        System.out.println(m);
    }
}

以上程式碼只有一點稍微有點意思就是 map的put操作。他的引數是一個條件表示式,因為他會先判斷這個被操作的單詞是否已經在map中,如果在的話就講當前的頻數加1.
如果你將這個作為輸入 
if it is to be it is up to me to delegate
那麼輸入將會是
8 distinct words:
{to=3, delegate=1, be=1, it=2, up=1, if=1, me=1, is=2}

但是如果你想要結果是按照字母排序輸出的話,那麼僅僅需要把HashMap換成TreeMap。你將得到以下輸出。
8 distinct words:
{be=1, delegate=1, if=1, is=2, it=2, me=1, to=3, up=1}

但是如果你想要得到的結果是按照輸入的順序的話(單詞第一次出現位置的順序),那麼則需要把HashMap換成LinkedHashMap
8 distinct words:
{if=1, it=2, is=2, to=3, be=1, up=1, me=1, delegate=1}

像Set和List介面一樣, Map加強了equals和hashcode這倆方法,有了這倆方法我門就能邏輯上比較兩個map從而可以忽略他們的實現型別簡單來說就是如果兩個map的鍵值對是一樣的那麼這兩個map就是一樣的。

按照慣例,所有的map都提供了一個可以 把另一個Map物件作為輸入的建構函式,並且例項化一個人新的map物件包含著和輸入map物件一樣的值。以下這段程式碼就是例項化一個copy 物件,並且這個copy物件有著和m一模一樣的值。

Map<K, V> copy = new HashMap<K, V>(m);



Map 介面的Bulk操作

clear這個操作的作用和他的字面意思是一樣的。它將刪掉map上所有的mapping關係。putall操作和collection介面的addall操作很像。而且,很明顯的用途是將一個map完全放入另一個map中。他還有另一個更靈活的用法。假設有一個map被用來表示一個attribute-value對的集合;putall操作加上map的建構函式一起就能提供一個乾淨利索的方法來實現例項化並將default的賦給他自己。

(自己的理解: 他這一大段就說了一個事,想要把兩個map給放到一起我們可以使用putall方法, 值得注意的是如果兩個map有相同key的話下面程式碼的override的那個map會佔主導地位)

static <K, V> Map<K, V> newAttributeMap(Map<K, V>defaults, Map<K, V> overrides) {
    Map<K, V> result = new HashMap<K, V>(defaults);
    result.putAll(overrides);
    return result;
}


集合檢視 

集合檢視這個方法允許我們用三種方法列印這個Map:
1. Keyset - 這個map的key 的集合 - 是一個set
2. values - 這個map的值的集合 不是一個set 因為值可能是重複的。
3. entrySet - 這個map中的鍵值對。Map介面提供了一個小的巢狀式的介面Map.Entry,他是這些element的type

集合檢視提供了迭代一個map的唯一的一種方法,下面的這個例子闡述了一個迭代map的常見的方法通過使用for-each

for (KeyType key : m.keySet())
    System.out.println(key);

通過使用Iterator

// Filter a map based on some 
// property of its keys.
for (Iterator<Type> it = m.keySet().iterator(); it.hasNext(); )
    if (it.next().isBogus())
        it.remove();

迭代map的值的方法和這個方法很類似,如下

for (Map.Entry<KeyType, ValType> e : m.entrySet())
    System.out.println(e.getKey() + ": " + e.getValue());

很多人都會擔心這種方法來迭代一個map可能很慢,因為map必須建立一個新的Collection 物件當每次Collection 檢視的操作被呼叫。
不用擔心:因為map能返回一個相同的物件,當每次被要求一個Collection檢視的時候。這個就是Map 的實現在java.util中做的。


呼叫 Iterator 的remove 方法再配合使用collection view的三種方式,我們能對map中的元素進行刪除操作。 這個就是前面的Iterator的例子


通過entrySet檢視,給了我們一種能改變某個鍵的值的可能性。Map.entry的setvalue方法在迭代中可以對entry的值進行更改。注意這是唯一的一種安全的在迭代中改變map的值的方法。如果底層map在迭代中被另一種方式修改那麼這個行為將是未指定的。


集合檢視支援多種元素刪除的方式 - remove, removeall, retainAll(保持輸入set刪掉剩餘其他), 和 clear. 和 Iterator.Remove的操作。


集合檢視在某些情況下不支援元素的新增。因為他不是很符合常理當我們使用KeySet 和 values view的時候進行新增操作。同理對於entryset也是一樣的。因為map的本身就提供了put 和 putAll的操作。


(自己的觀點:以上這一小節說的挺繞的,但是其實就是說Collection view例如 keyset values entryset等等在某些情況下有某些作用,並且把他們和Map這個概念分開來講。我是這樣理解的。)


Collection 檢視的一些很厲害的用法

我們知道結合檢視有很多很厲害的工具 - contains all , remove all 等等。對於初學者,如果你想要知道一個map是否包含另一map那麼很簡單我們可以判斷第一個map的keyset是否完全包含另一個的keyset。如下

if (m1.entrySet().containsAll(m2.entrySet())) {
    ...
}

相似的,如果你想要知道是否兩個map的key是否是一樣的那麼可以使用
if (m1.keySet().equals(m2.keySet())) {
    ...
}


假設你有一個map,這個map是一個 鍵值對的集合,並且另外還有兩個set。分別代表了 必須的鍵  和 允許的鍵(允許的鍵中包括了必須的鍵)。 以下程式碼片段 代表了 判斷 這個map是否符合我們的規定 即 這個map中必須包含 所有  必須的鍵 並且不能出現 允許的鍵 之外的鍵。

static <K, V> boolean validate(Map<K, V> attrMap, Set<K> requiredAttrs, Set<K>permittedAttrs) {
    boolean valid = true;
    Set<K> attrs = attrMap.keySet();

    if (! attrs.containsAll(requiredAttrs)) {
        Set<K> missing = new HashSet<K>(requiredAttrs);
        missing.removeAll(attrs);
        System.out.println("Missing attributes: " + missing);
        valid = false;
    }
    if (! permittedAttrs.containsAll(attrs)) {
        Set<K> illegal = new HashSet<K>(attrs);
        illegal.removeAll(permittedAttrs);
        System.out.println("Illegal attributes: " + illegal);
        valid = false;
    }
    return valid;
}


假設你想要知道兩個map的鍵是否有相同的鍵可以這樣

Set<KeyType>commonKeys = new HashSet<KeyType>(m1.keySet());
commonKeys.retainAll(m2.keySet());


假設你想要從map1中移除所有map2中有的鍵值對可以這樣

m1.entrySet().removeAll(m2.entrySet());

假設你想要從map1中移除所有map2中有的鍵,你這樣這樣

m1.keySet().removeAll(m2.keySet());


這裡有一個稍微實際一點的例子,我們現在有一個map它的鍵是 職員的名字 它的值是 他的上司的名字。現在我們想要找到所有沒有上司的人,我們應該怎麼辦呢?

Set<Employee> individualContributors = new HashSet<Employee>(managers.keySet());
individualContributors.removeAll(managers.values());

另外一個假設,我現在想要解僱 Simon所管理的職員我們可以這樣。(Collecttions.singleton是一個靜態工廠他會返回一個值為simon 的 immutable set)

Employee simon = ... ;
managers.values().removeAll(Collections.singleton(simon));

當 你做完下面這些操作之後,你可能就會發現有哪些員工的manager已經不在為這個公司工作了,最大的manager按照給他自己彙報的原則來算。(這個例子還挺好玩大家好好感受感受)

Map<Employee, Employee> m = new HashMap<Employee, Employee>(managers);
m.values().removeAll(managers.keySet());
Set<Employee> slackers = m.keySet();

他的意思也就是說: map 職員-》 老闆。 首先我們複製這個map, 然後從一個map移除 所有滿足條件的老闆,這個條件是 所有的職員。因為老闆也是職員,如果沒有人離職的話,那麼移除之後的map應該是空的。但是如果不是空的那麼說明有的老闆已經不在了。那麼我們在通過得到這個map的鍵來獲得這些不在的了老闆的員工是哪些。

最後,我們在這裡僅僅羅列了很少一部分的使用例子,當你上手了之後你就會信手拈來啦。

Multimaps 多重map

 多重map就是一個map但是他的鍵對應的是多個值 。java collections 框架沒有一個專門介面給這個多重map,因為他不是很經常被使用。但是我們可以很簡單的使用list作為一個鍵的值來實現 multimaps。接下來是一個例子:從一個字典檔案按行讀取資料,每行包含一個單詞,然後我們還需要設定一個整形數,最後進行如下操作,對於每個讀入的單詞,我們要把他所有的字母重新進行排列組合,並且最後列印出來,列印出來的最小個數就是我們之前設定的整形數-如果重新排列組合後的字母個數不足我們設定的最小數,那麼不列印該字母的組合。(從命令列需要對程式有兩個輸入1. 字典檔案的名字2.那個整形數)如下

import java.util.*;
import java.io.*;

public class Anagrams {
    public static void main(String[] args) {
        int minGroupSize = Integer.parseInt(args[1]);

        // Read words from file and put into a simulated multimap
        Map<String, List<String>> m = new HashMap<String, List<String>>();

        try {
            Scanner s = new Scanner(new File(args[0]));
            while (s.hasNext()) {
                String word = s.next();
                String alpha = alphabetize(word);
                List<String> l = m.get(alpha);
                if (l == null)
                    m.put(alpha, l=new ArrayList<String>());
                l.add(word);
            }
        } catch (IOException e) {
            System.err.println(e);
            System.exit(1);
        }

        // Print all permutation groups above size threshold
        for (List<String> l : m.values())
            if (l.size() >= minGroupSize)
                System.out.println(l.size() + ": " + l);
    }

    private static String alphabetize(String s) {
        char[] a = s.toCharArray();
        Arrays.sort(a);
        return new String(a);
    }
}

如下是輸出的結果
9: [estrin, inerts, insert, inters, niters, nitres, sinter,
     triens, trines]
8: [lapse, leaps, pales, peals, pleas, salep, sepal, spale]
8: [aspers, parses, passer, prases, repass, spares, sparse,
     spears]
10: [least, setal, slate, stale, steal, stela, taels, tales,
      teals, tesla]
8: [enters, nester, renest, rentes, resent, tenser, ternes,
     treens]
8: [arles, earls, lares, laser, lears, rales, reals, seral]
8: [earings, erasing, gainers, reagins, regains, reginas,
     searing, seringa]
8: [peris, piers, pries, prise, ripes, speir, spier, spire]
12: [apers, apres, asper, pares, parse, pears, prase, presa,
      rapes, reaps, spare, spear]
11: [alerts, alters, artels, estral, laster, ratels, salter,
      slater, staler, stelar, talers]
9: [capers, crapes, escarp, pacers, parsec, recaps, scrape,
     secpar, spacer]
9: [palest, palets, pastel, petals, plates, pleats, septal,
     staple, tepals]
9: [anestri, antsier, nastier, ratines, retains, retinas,
     retsina, stainer, stearin]
8: [ates, east, eats, etas, sate, seat, seta, teas]
8: [carets, cartes, caster, caters, crates, reacts, recast,
     traces]
這裡有 字典檔案的樣本。

相關文章