映射(map) 数据结构就是为此设计的。映射用来存放键 / 值对。如果提供了键, 就能够查找到值。
Java 类库为映射提供了两个通用的实现:HashMap 和 TreeMap。这两个类都实现了 Map 接口。散列映射对键进行散列, 树映射用键的整体顺序对元素进行排序, 并将其组织成搜索树。散列或比较函数只能作用于键。与键关联的值不能进行散列或比较。应该选择散列映射还是树映射呢? 与集一样, 散列稍微快一些, 如果不需要按照排列顺序访问键, 就最好选择散列。
1 Map接口
Map接口提供了一些映射表的基本操作,下面是这些方法的总结:
(1)查询操作
int size(); boolean isEmpty(); boolean containsKey(Object); boolean containsValue(Object); V get(Object);
这些方法的含义都很明确。需要注意的是,containsKey方法、containsValue方法和get方法的参数类型都是Object。
(2)修改方法
V put(K,V); V remove(Object); void putAll(Map extends K,? extends V>); void clear();
put方法用于添加一个键值对,如果键已经存在就更新值并返回旧值。remove方法删除给定键的键值对并返回值。putAll方法将一个Map中的所有键值对添加到映射表中。clear方法删除所有元素。
(3)视图方法
Set keySet(); Collection values(); Set> entrySet();
这三个方法返回三个视图:键集、值集合(不是集)和键值对集。对于视图会在后续的文章中作介绍。
在Map接口中还定义了一个子接口:Entry,用来操作键值对。
这个接口主要有一下几个方法:
K getKey(); V getValue(); V setValue(V value); boolean equals(Object o); int hashCode();
(4)Entry: 键值对 对象。(Entry是Map中用来保存一个键值对的,而Map实际上就是多个Entry的集合)
Map.Entry是Map声明的一个内部接口,此接口为泛型,定义为Entry。 在Map类设计是,提供了一个嵌套接口(static修饰的接口):Entry。Entry将键值对的对应关系封装成了对象,即键值对对象,这样我们在遍历Map集合时,就可以从每一个键值对(Entry)对象中获取对应的键与对应的值。
Entry为什么是静态的?
Entry是Map接口中提供的一个静态内部嵌套接口,修饰为静态可以通过类名调用。
Map集合遍历键值对的方式:
Set> entrySet(); //返回此映射中包含的映射关系的Set视图
该方法返回值是Set集合,里面装的是Entry接口类型,即将映射关系装入Set集合。
实现步骤:
1,调用Map集合中的entrySet()方法,将集合中的映射关系对象存储到Set集合中
2,迭代Set集合
3,获取Set集合的元素,是映射关系的对象
4,通过映射关系对象的方法,getKey()和getValue(),获取键值对
Map map = new HashMap(); map.put("key1", "value1"); map.put("key2", "value2"); map.put("key3", "value3"); //第一种:普遍使用,二次取值 System.out.println("通过Map.keySet遍历key和value:"); for (String key : map.keySet()) { System.out.println("key= "+ key + " and value= " + map.get(key)); } //第二种 System.out.println("通过Map.entrySet使用iterator遍历key和value:"); Iterator> it = map.entrySet().iterator(); while (it.hasNext()) { Map.Entry entry = it.next(); System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue()); } //第三种:推荐,尤其是容量大时 System.out.println("通过Map.entrySet遍历key和value"); for (Map.Entry entry : map.entrySet()) { System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue()); } //第四种 System.out.println("通过Map.values()遍历所有的value,但不能遍历key"); for (String v : map.values()) { System.out.println("value= " + v); }
2 散列映射表:HashMap
散列映射表主要用到散列技术,可以快速对一个元素进行查找。HashMap类中的主要域如下:
transient Node[] table; transient int size; int threshold; final float loadFactor;
其中使用table来存储元素,size表示映射表中键值对的个数,threshold是一个域值,当元素个数超过这个域值后,就会自动扩展映射表的大小。而loadFactor是一个加载因子,表示threshold与table长度的比值。
可以看到,table是一个数组,数组中存储Node类型的值。Node表示一个键值对,定义如下:
static class Node implements Map.Entry { final int hash; final K key; V value; Node next; Node(int hash, K key, V value, Node next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final K getKey(); public final V getValue(); public final String toString(); public final int hashCode(); public final V setValue(V newValue); public final boolean equals(Object o); }
是一个静态内部类,这表示一个键值对,可见HashMap将键值对作为一个整体来操作。
在Node中,有存储键的key,存储值的value,存储散列值的hash,还有一个next引用,可见这是一个链表。
既然有散列值hash,那么这个值是如何计算的呢?方法如下:
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
是一个纯粹的数学方式。
有了散列值,HashMap又是如何散列的呢?
HashMap使用hash值确定一个键值对在table中的位置,具体方法是,用hash%table.length,结果就是在table中的下标。如果有多个hash在table中的同一个位置,那么就构成一个链表。存储方式大致是这样的:
在HashMap中,table的默认大小是16,以后每次扩大容量都会是原来的二倍,因此,table的大小一直是2的幂。由于这点,HashMap在计算一个hash的位置的时候,使用了非常巧妙的方法:
int n=table.length; int index=hash&(n-1);
这就相当于计算hash%table.length。
了解了键值对的表示方式和HashMap的存储方式之后,就要对键值对进行操作了。常见的操作有查找、插入和删除。接下来就介绍这些操作:
(1)查找
HashMap中,对于查找操作,定义了一个私有方法getNode。这个方法有两个参数:hash和key,根据哈希值和键来找键值对。源代码定义如下:
final Node getNode(int hash, Object key) { Node[] tab; Node first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { if (first instanceof TreeNode) return ((TreeNode)first).getTreeNode(hash, key); do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
首先根据hash定位键值对在table中的位置,找到之后首先检查第一个元素,如果符合就返回,如果不符合就遍历这个链表,知道找到符合的键值对。如果没有找到,说明没有这个键值对,返回null。
这个方法是查找操作的基本方法,HashMap中的查找方法比如containsKey等都是调用这个方法完成操作的。
java根据Map的值(value)取键(key) 的实现方法有4种,分别为:
(1)使用for循环遍历
(2)使用Iterator迭代器
(3)使用KeySet迭代
public static Object getKey(HashMap map, String v) { System.out.printIn("输入想要找的"); String str = console.next();//键盘得到 Set keySet = student.keySet(); for(String key,keySet){ if(str.equal(key)){ System.out.print("Key="+key+" Val="+student.get(key))); } } }
(4)使用EnterySet迭代
public static Object getKey(HashMap map, String v) {String key = ""; Iterator it = map.entrySet().iterator(); while (it.hasNext()) { Map.Entry entry = (Entry) it.next(); Object obj = entry.getValue(); if (obj != null && obj.equals(value)) { key = (String) entry.getKey(); } } return key; }
(2)添加键值对
HashMap中定义了一个基本方法putVal,这个方法将给定的键和值加入映射表中,定义如下:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node[] tab; Node p; int n, i; //如果表为空,即里面没有元素,则使用resize方法创建一个表 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //找到给定的键值对对应的位置,如果对应位置还没有元素,则创建一个Node作为链表的头 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); //对应的位置已经有元素了,即发生了冲突,那么就在后面形成一个链表 else { Node e; K k; //待插入的键与已有的键相同,则更新值 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); //在链表中找合适的位置(要么有已存在的键,要么没有) else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //更新值,并返回旧值 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
添加操作和查找操作有点类似,首先定位待插入的键值对在table中的位置,如果里面没有元素,直接插入即可;如果里面已经有元素,即发生了冲突,就将这个元素加入到这个链表中。
put方法就是调用这个方法的。
(3)删除键值对
有加入操作就有删除操作。基本方法是removeNode:
final Node removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { Node[] tab; Node p; int n, index; if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { Node node = null, e; K k; V v; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; else if ((e = p.next) != null) { if (p instanceof TreeNode) node = ((TreeNode)p).getTreeNode(hash, key); else { do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break; } p = e; } while ((e = e.next) != null); } } if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { if (node instanceof TreeNode) ((TreeNode)node).removeTreeNode(this, tab, movable); else if (node == p) tab[index] = node.next; else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; } } return null; }
首先也是定位到节点的位置,matchValue是是否匹配值,如果为true,就是说只有值也匹配的时候才删除这个键值对。HashMap中的remove方法就是调用这个方法。
(4)遍历
public class Test{ public static void main(String[] args) { Map map = new HashMap(); map.put("1", "value1"); map.put("2", "value2"); map.put("3", "value3"); //第一种:普遍使用,二次取值 System.out.println("通过Map.keySet遍历key和value:"); for (String key : map.keySet()) { System.out.println("key= "+ key + " and value= " + map.get(key)); } //第二种 System.out.println("通过Map.entrySet使用iterator遍历key和value:"); Iterator> it = map.entrySet().iterator(); while (it.hasNext()) { Map.Entry entry = it.next(); System.out.println("key= " + entry.getK } //第三种:推荐,尤其是容量大时 System.out.println("通过Map.entrySet遍历key和value"); for (Map.Entry entry : map.entrySet()) { System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue()); } //第四种 System.out.println("通过Map.values()遍历所有的value,但不能遍历key"); for (String v : map.values()) { System.out.println("value= " + v); } } }
(5)容量扩展
当当前元素个数size等于threshold时,即使没有达到table的容量,也需要对table进行扩展。HashMap中的resize方法完成这个操作。
这个方法比较复杂,方法分为两部分,第一个部分就是确定新映射表的大小,考虑的主要问题是数值溢出。因为默认的大小是16,每次扩容都会是原来的2倍,很容易溢出。当发生这种情况时,就将threshold值置为Integer.MAX_VALUE。
第二个部分就是将原来映射表里的内容移到新的映射表中。这只需要两层循环就好。第一层循环是在table数组上,第二层是每个table元素也是一个链表,需要循环一次。然后把每个键值对放在新的映射表中的合适位置即可。
以上就是一些基本的操作,HashMap的修删改查方法都是基于这些方法实现的。
接下来就是HashMap的视图(view)操作。
(1)键集:keySet
方法keySet可以返回一个由键构成的集,注意KeySet既不是HashSet,也不是TreeSet,而是扩展了AbstractSet抽象类的一个内部类:
final class KeySet extends AbstractSet { public final int size() { return size; } public final void clear() { HashMap.this.clear(); } public final Iterator iterator() { return new KeyIterator(); } public final boolean contains(Object o) { return containsKey(o); } public final boolean remove(Object key) { return removeNode(hash(key), key, null, false, true) != null; } public final Spliterator spliterator() { return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0); } public final void forEach(Consumer super K> action) { Node[] tab; if (action == null) throw new NullPointerException(); if (size > 0 && (tab = table) != null) { int mc = modCount; for (int i = 0; i < tab.length; ++i) { for (Node e = tab[i]; e != null; e = e.next) action.accept(e.key); } if (modCount != mc) throw new ConcurrentModificationException(); } } }
这个类实现了Set接口,也是一个Collection,因此可以与使用任何集合一样使用keySet。
比如,可以枚举映射表中的所有键:
Set keys=map.keySet(); for(String key:keys) { do something with key }
(2)值集合:values
values方法返回一个由值构成的集合,注意不是集,因为HashMap仅要求键唯一,不需要值唯一。返回的这个集合是扩展了AbstractCollection类的一个内部类:
final class Values extends AbstractCollection
(3)键值对集合:entrySet
这个方法返回由所有键值对构成的集合。这个方法返回的集是扩展了AbstractSet类的内部类:
final class EntrySet extends AbstractSet>
这样,就可以同时查看键和值了,以避免对值进行查找:
for(Map.Entry entry:staff.entrySet()) { String key=entry.getKey(); Employee value=entry.getValue(); dosomething with key,value }
下面的代码演示了映射表的操作过程。首先将键值对添加到映射表中。然后,从映射表中删除一个键,同时与之对应的值也别删除了。接下来,修改与某一个键对应的值,并调用get方法获得这个值。最后,对元素进行迭代:
import java.util.*; public class MapTest { public static void main(String[] args) { Map staff = new HashMap<>(); staff.put("144-25-5464", new Employee("Amy Lee")); staff.put("567-24-2546", new Employee("Harry Hacker")); staff.put("157-62-7935", new Employee("Gary Cooper")); staff.put("456-62-5527", new Employee("Francesca Cruz")); // print all entries System.out.println(staff); // remove an entry staff.remove("567-24-2546"); // replace an entry staff.put("456-62-5527", new Employee("Francesca Miller")); // look up a value System.out.println(staff.get("157-62-7935")); // iterate through all entries for (Map.Entry entry : staff.entrySet()) { String key = entry.getKey(); Employee value = entry.getValue(); System.out.println("key=" + key + ", value=" + value); } } }
结果如下:
当创建 HashMap 时,有一个默认的负载因子(load factor),其默认值为 0.75,这是时间和空间成本上一种折衷:增大负载因子可以减少 Hash 表(就是那个 Entry 数组)所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的的操作(HashMap 的 get() 与 put() 方法都要用到查询);减小负载因子会提高数据查询的性能,但会增加 Hash 表所占用的内存空间。
掌握了上面知识之后,我们可以在创建 HashMap 时根据实际需要适当地调整 load factor 的值;如果程序比较关心空间开销、内存比较紧张,可以适当地增加负载因子;如果程序比较关心时间开销,内存比较宽裕则可以适当的减少负载因子。通常情况下,程序员无需改变负载因子的值。
如果开始就知道 HashMap 会保存多个 key-value 对,可以在创建时就使用较大的初始化容量,如果 HashMap 中 Entry 的数量一直不会超过极限容量(capacity * load factor),HashMap 就无需调用 resize() 方法重新分配 table 数组,从而保证较好的性能。当然,开始就将初始容量设置太高可能会浪费空间(系统需要创建一个长度为 capacity 的 Entry 数组),因此创建 HashMap 时初始化容量设置也需要小心对待。
3 树映射表:TreeMap
TreeMap用键的整体顺序对元素进行排序,底层使用红黑树实现。迭代时,会按照顺序迭代。
下面的代码演示了TreeMap的使用。这里使用的是默认的比较器:
public class MapTest { public static void main(String[] args) { Map staff = new TreeMap<>(); staff.put("144-25-5464", new Employee("Amy Lee",9000)); staff.put("567-24-2546", new Employee("Harry Hacker",5000)); staff.put("157-62-7935", new Employee("Gary Cooper",7500)); staff.put("456-62-5527", new Employee("Francesca Cruz",8000)); for(Map.Entry entry:staff.entrySet()){ String key = entry.getKey(); Employee value = entry.getValue(); System.out.println("key=" + key + ", value=" + value); } } }
结果如下:
TreeMap在构造时还可以指定一个比较器,根据比较器对键进行排序:
public class MapTest { public static void main(String[] args) { Map staff = new TreeMap<>(new Comparator() { @Override public int compare(String o1, String o2) { return o2.compareTo(o1); } }); staff.put("144-25-5464", new Employee("Amy Lee",9000)); staff.put("567-24-2546", new Employee("Harry Hacker",5000)); staff.put("157-62-7935", new Employee("Gary Cooper",7500)); staff.put("456-62-5527", new Employee("Francesca Cruz",8000)); for(Map.Entry entry:staff.entrySet()){ String key = entry.getKey(); Employee value = entry.getValue(); System.out.println("key=" + key + ", value=" + value); } } }
这里构造一个比较器,使得按照键反向排列。结果如下:
注意,TreeMap只能对键进行排序,不能对与键关联的值进行排序。
=================================================================
TreeSet 类与散列集十分类似, 不过, 它比散列集有所改进。树集是一个有序集合( sorted collection) 。可以以任意顺序将元素插入到集合中。在对集合进行遍历时,每个值将自动地按照排序后的顺序呈现。
将一个元素添加到树中要比添加到散列表慢,但是,与检查数组或链表中的重复元素相比还是快很多。
TreeSet类
TreeSet和HashSet类似,不过,它比HashSet有所改进。TreeSet是一个有序集合,可以以任意顺序将元素插入到集合中。在对集合进行遍历时,每个值将自动按照排序后的顺序呈现。例如,假设插入三个字符串,然后访问添加的所有元素:
SortedSet sorter=new TreeSet<>(); sorter.add("B"); sorter.add("A"); sorter.add("C"); for(String s:sorter)System.out.println(s); //结果是:A B C
TreeSet的底层是使用TreeMap实现的,是一个红黑树。每次添加一个元素到树中时,都被放置在正确的排序位置上。因此,迭代器总是以排好序的顺序访问每个元素。
将一个元素添加到树中要比添加到一个散列表中要慢,但是,与将元素添加到数组中或链表中要快。如果树中一共有n个元素,将元素插入到正确位置的时间为logn。
与TreeMap一样,构造一个TreeSet也需要一个比较器,可以使用默认的比较器,也可以使用自己的比较器。使用自己的比较器时,需要给TreeSet的构造器传递一个Comparator对象。
下面的程序创建了两个Item对象的树集。第一个按照部件编号排序,这是Item对象的默认顺序。第二个通过使用一个定制的比较器来按照描述信息排序:
import java.util.*; public class TreeSetTest { public static void main(String[] args) { SortedSet- parts = new TreeSet<>(); parts.add(new Item("Toaster", 1234)); parts.add(new Item("Widget", 4562)); parts.add(new Item("Modem", 9912)); System.out.println(parts); SortedSet
- sortByDescription = new TreeSet<>(new Comparator
- () { public int compare(Item a, Item b) { String descrA = a.getDescription(); String descrB = b.getDescription(); return descrA.compareTo(descrB); } }); sortByDescription.addAll(parts); System.out.println(sortByDescription); } } import java.util.*; /** * An item with a description and a part number. */ public class Item implements Comparable
- { private String description; private int partNumber; /** * Constructs an item. * * @param aDescription * the item's description * @param aPartNumber * the item's part number */ public Item(String aDescription, int aPartNumber) { description = aDescription; partNumber = aPartNumber; } /** * Gets the description of this item. * * @return the description */ public String getDescription() { return description; } public String toString() { return "[\n\tdescripion=" + description + ",\n\tpartNumber=" + partNumber +"\n]\n"; } public boolean equals(Object otherObject) { if (this == otherObject) return true; if (otherObject == null) return false; if (getClass() != otherObject.getClass()) return false; Item other = (Item) otherObject; return Objects.equals(description, other.description) && partNumber == other.partNumber; } public int hashCode() { return Objects.hash(description, partNumber); } public int compareTo(Item other) { return Integer.compare(partNumber, other.partNumber); } }
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、
Map(HashMap,TreeMap,SortedMap)
• V get(Object key)
获取与键对应的值;
返回与键对应的对象, 如果在映射中没有这个对象则返回 null。键可以null。
• default V getOrDefault(Object key, V defaultValue)
获得与键关联的值;返回与键关联的对象, 或者如果未在映射中找到这个键, 则返回defaultValue。
• V put(K key, V value)
将键与对应的值关系插入到映射中。如果这个键已经存在, 新的对象将取代与这个键对应的旧对象。这个方法将返回键对应的旧值。如果这个键以前没有出现过则返null。键可以为 null, 但值不能为 null
• void putAl 1(Map extends K , ? extends V> entries)
将给定映射中的所有条目添加到这个映射中。
• boolean containsKey(Object key)
如果在映射中已经有这个键, 返回 true。
• boolean containsValue(Object value)
如果映射中已经有这个值, 返回 true。
•default void forEach(BiConsumer super K ,? super V> action)
对这个映射中的所有键 / 值对应用这个动
•default V merge(K key, V value, BiFunction super V ,? super V ,?extends V> remappingFunctlon)
如果 key 与一个非 null 值 v 关联, 将函数应用到 v 和 value, 将 key 与结果关联, 或者如果结果为 null, 则删除这个键。否则, 将 key 与 value 关联, 返回 get(key。)
• default V compute(K key, BiFunction super K,? super V ,? extends V>remappingFunction)
将函数应用到 key 和 get(key。) 将 key 与结果关联, 或者如果结果为 mill, 则删除这个键。返回 get(key。)
•default V computeIfPresent(K key , BiFunction super K , ? super V , ?extends V > remappingFunction )
如果 key 与一个非 null 值 v 关联,将函数应用到 key 和 v, 将 key 与结果关联, 或者如果结果为 null, 则删除这个键。返回 get(key。)
•default V computeIfAbsent(K key , Function super K , ? extends V>mappingFunction )
将函数应用到 key, 除非 key 与一个非 mill 值关联。将 key 与结果关联, 或者如果结果为 null, 则删除这个键。返回 get(key。)
•default void repl aceAl 1 (BiFunction super K ,? super V , ? extendsV > function)
在所有映射项上应用函数。将键与非 mill 结果关联, 对于 null 结果, 则将相应的键删除
//HashMap package com.datastruct; import com.testBinbin.Employee; import java.util.HashMap; public class DateStruct { public static void main(String[] args){ HashMap company = new HashMap<>(); company.put(0,new Emplyee("a",10000)); company.put(1,new Emplyee("b",10000)); company.put(2,new Emplyee("c",10000)); company.remove(1); company.put(4,new Emplyee("dd",88888)); System.out.println(company.values()); company.forEach((k,v)-> System.out.println(k+":"+v)); System.out.println(company.get(2)); Emplyee e = company.get(0); } }
更新映射项:
正常情况下,可以得到与一个键关联的原值,完成更新, 再放回更新后的值。不过,必须考虑一个特殊情况, 即键第一次出现。
===========================================================