Core Java (二十一) 對映表(Map介面)
對映表是一種資料結構,用於存放鍵值對。如果提供了鍵,就能查詢到值。
Map介面的方法:
Modifier and Type | Method and Description |
---|---|
void |
clear()
Removes all of the mappings from this map (optional operation).
|
boolean |
Returns true if this map contains a mapping for the specified key. |
boolean |
Returns true if this map maps one or more keys to the specified value. |
Returns a Set view
of the mappings contained in this map.
|
|
boolean |
Compares the specified object with this map for equality. |
V |
Returns the value to which the specified key is mapped, or null if this map contains no mapping for the key.
|
int |
Returns the hash code value for this map. |
boolean |
Returns true if this map contains no key-value mappings. |
Set<K> |
Returns a Set view
of the keys contained in this map.
|
V |
put(K key, V value)
Associates the specified value with the specified key in this map (optional operation).
|
void |
(Map<?
extends K,? extends V> m)
Copies all of the mappings from the specified map to this map (optional operation).
|
V |
Removes the mapping for a key from this map if it is present (optional operation). |
int |
size()
Returns the number of key-value mappings in this map.
|
Returns a Collection view
of the values contained in this map.
|
Java類庫為對映表提供了兩個通用的實現:HashMap(對鍵進行雜湊)和TreeMap(用鍵的整體順序對元素排序,並將其組織成搜尋樹),這兩個類都實現了Map介面。
HashMap比TreeMap更快,但沒有排序功能。下面重點介紹HashMap:
HashMap
散列表(HashMap)和雜湊集(HashSet)都是散列表這種資料結構。
散列表(Hash Table)
散列表由連結串列陣列實現,每個列表成為一個桶(bucket),一個桶就是一個連結串列。如下圖所示,該圖有16個桶。根據關鍵字的雜湊碼(HashCode)與桶的總數取餘得到元素的位置。圖中,0~15代表桶的標號,而右邊一些數字代表關鍵字的雜湊碼。下面這個散列表,其中4個空間被使用,即36%的空間已經被使用。而散列表的裝填因子(load factor)預設為0.75,即當75%的空間已經被使用時進行再雜湊(rehashed),桶的總數變成雙倍。注意:如果未設定,桶的總數的初始預設值為16。
這裡主要討論HashMap:
雜湊對映表(HashMap)僅僅是根據關鍵字的雜湊碼(HashCode)來得到元素要存放在哪條連結串列上,而判斷兩個物件是否相等是根據物件的equals方法確定的。如果想將Employee類的某個物件新增到HashMap中,首先要呼叫這個物件(Value)所對應的鍵(Key)的hashCode方法計算鍵的雜湊碼,然後與桶的總數取餘的到這個鍵值對的桶的索引。如果返回的雜湊碼與HashMap中已經存在的某個鍵的雜湊碼相同,則第二個值就會取代第一個值。因為鍵必須是唯一的,不能對同一個鍵存放兩個值。也就是說,雜湊對映表中,value完全是key的附屬,一點也不會影響儲存。
HashMap儲存在bucket中的是entry(條目)的引用,一個bucket其實就是一條entry連結串列。entry是一種資料結構,裡面包含了key,value,next等例項域,其中next指的是下一個引用的位置,如果next == null,則表示這條entry鏈結束了。HashMap在執行add操作的時候,總是把最新的元素新增到bucket中entry連結串列的頭部,再將這個最新元素的next指向舊連結串列,所以最早加入的元素一定在連結串列末尾。
這裡附加一道OCJP的題目來增強理解:
Given
public class Person {
private name;
public Person(String name) {
this.name = name;
}
public int hashCode() {
return 420;
}
}
Which statement is true?A. The time to find the value from HashMap with a Person key depends on the
size of the map.
B. Deleting a Person key from a HashMap will delete all map entries for all keys of type Person.
C. Inserting a second Person object into a HashSet will cause the first Person object to be
removed as a duplicate.
D. The time to determine whether a Person object is contained in a HashSet is constant and does NOT
depend on the size of the map.
Answer: A
下面來解釋一下:
紅色為翻譯
B選項:刪除HashMap中一個Person物件對應的鍵將會刪除這個雜湊對映表中Person類的全部條目。錯誤,HashMap中Person物件的鍵值不是由Person物件決定的,而是程式給定的鍵,例如staff.add("123-345", bob),就是把鍵為123-456的bob物件新增到名為staff的HashMap中,因而HashMap允許新增相同的物件。所以說,刪除一個鍵對應的Person物件並不會刪除所有的條目,他們的key都不同嘛。
C選項:向HashSet中插入另外一個Person物件將會引起第二個物件覆蓋第一個物件。錯誤,雖然Person物件的hashCode方法返回的值都是420,這僅僅表明兩個Person物件在一個entry連結串列中,接下來要呼叫equals方法,由於Person類沒有equals方法,所以呼叫Object的equals方法返回物件的儲存地址,很明顯兩個Person物件的儲存地址是不同的。綜上,HashSet中可以新增不同的Person物件,只要equals方法返回值不同就好。
D選項:判斷一個HashSet中是否存在一個Person物件的次數是常數次,和map的大小無關。錯誤,由於Person物件的hashCode返回的值都是420,所以HashSet中的Person物件都在一個bucket中,組成了一條entry連結串列,查詢速度與entry連結串列的大小息息相關。
A:選項:由key來查詢value的次數與map的大小有關。正確,map越大,即bucket的個數越多,entry鏈的長度相應的來說就越小(hashcode和桶個數取餘後的數一樣的機率就越小)。
另外,要注意的是HashMap有三個重要的檢視:
- Set<K> keySet();
- Collection<K> values();
- Set<Map.Entry<K,V>> entrySet();
以下例子利用了HashMap的一些重要的方法:
package com.xujin;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class Test{
public static void main(String...arg){
Map<String, Employee> staff = new HashMap<String, Employee>();
Employee lily = new Employee("Lily", 4000);
Employee john = new Employee("John", 6000);
staff.put("1234-9876", lily);
staff.put("345-8765", john);
Employee e = staff.get("345-8765");
System.out.println(e.getName());//John
System.out.println(staff.containsKey("1234-9876"));//true
System.out.println(staff.containsValue(lily));//true
System.out.println(staff.size());//2
//鍵集
Set<String> myKeySet = staff.keySet();
for(String myKey: myKeySet){
System.out.println(myKey);
}
/*結果:
* 345-8765
* 1234-9876
*/
//值集合(不是集set)
//staff.values()返回型別為Collection<Employee>
for(Employee myEmployee : staff.values()){
System.out.println(myEmployee);
}
/*結果:
* id:2,name:John,salary:6000.0
* id:1,name:Lily,salary:4000.0
* */
//鍵值對集
//staff.entrySet()返回型別為Set<Entry<String, Employee>>
for(Map.Entry<String, Employee> entry: staff.entrySet()){
String key = entry.getKey();
Employee value = entry.getValue();
System.out.println("key: " + key + "\nvalue: " + value);
}
/*結果:
* key: 345-8765
* value: id:2,name:John,salary:6000.0
* key: 1234-9876
* value: id:1,name:Lily,salary:4000.0
* */
staff.remove("345-8765");
System.out.println(staff.size());//1
//putAll方法,
Map<String, Manager> otherStaff = new HashMap<String, Manager>();
Manager jim = new Manager("Jim", 6000, 1000);
Manager gin = new Manager("Gin", 8000, 2000);
otherStaff.put("1111-2222", jim);
otherStaff.put("2222-3333", gin);
staff.putAll(otherStaff);
for(Map.Entry<String, Employee> entry: staff.entrySet()){
String key = entry.getKey();
Employee value = entry.getValue();
System.out.println("key: " + key + "\nvalue: " + value);
}
/*結果:
* key: 1111-2222
* value: id:3,name:Jim,salary:6000.0,bonus:1000.0
* key: 2222-3333
* value: id:4,name:Gin,salary:8000.0,bonus:2000.0
* key: 1234-9876
* value: id:1,name:Lily,salary:4000.0
* */
}
}
class Employee{
public Employee(String name){
this.name = name;
id = nextId;
nextId++;
}
public String toString(){
return "id:" + id + ",name:" + name +",salary:" + salary;
}
public Employee(String name, double salary){
this(name);//呼叫另一構造器
this.salary = salary;
}
//定義訪問器方法
public final String getName(){
return name;
}
public double getSalary(){
return salary;
}
public final int getId(){
return id;
}
//定義更改器方法
public final void setName(String name){
this.name = name;
}
public final void setSalary(double salary){
this.salary = salary;
}
public final void raiseSalary(double percent){
this.salary *= (1 + percent);
}
//定義變數
private String name = "";//例項域初始化
private double salary;
private int id;
private static int nextId = 1;
}
class Manager extends Employee{
public Manager(String name, double salary, double bonus){
super(name, salary);//super在構造器中的使用,可以呼叫超類的構造器
setBonus(bonus);
}
public String toString(){
return super.toString() + ",bonus:" + bonus;
}
public double getBonus(){
return bonus;
}
//重寫getSalary方法
public double getSalary(){
double baseSalary = super.getSalary();//呼叫了超類的getSalary方法
return baseSalary + bonus;
}
public void setBonus(double bonus){
this.bonus = bonus;
}
private double bonus;
}