Java集合之Map

Map接口概述

前边对Java单列集合Collection有了基本的了解,现在开始学习下集合的Map接口。Map是一个非常有用的数据结构,该接口是一个双列集合,所谓双列就是Map是依照键(key)-值(value)对的序列来存储元素,该元素是两个对象,其中的键(key)是唯一的,不能重复,而每个键对应的值(value)则不同,value可以重复。

对于Map这种特点在生活中挺常见的,比如,现在每个人都有一个身份证号码与自己的名字对应,当你在买车票时会直接根据你的身份证号码来搜索具体的人也就是你的名字,而不是通过你的名字来确定身份证号码,为什么不能用名字直接买票呢,相比大家肯定都能正确的回答,即就是生活中存在许许多多同名同姓的人,但是他们的身份证号码却是唯一的,就相当于Map的key,名字就相当于value。

常用的实现类和实现方法

实现类

接下来就看看Map接口的实现类,有好多种实现类,这里主要说说比较常用的几种:

  • 1、Hashtable: 
    底层是哈希表数据结构,线程是同步的,不可以存入null键,null值。 
    效率较低,被HashMap 替代。
  • 2、HashMap: 
    底层是哈希表数据结构,线程是不同步的,可以存入null键,null值。 
    要保证键的唯一性,需要覆盖hashCode方法,和equals方法。 

    LinkedHashMap: 
    该子类基于哈希表又融入了链表。可以Map集合进行增删提高效率。

  • 3、TreeMap: 
    底层是二叉树数据结构。可以对map集合中的键进行排序。需要使用Comparable或者Comparator 进行比较排序。return 0,来判断键的唯一性。

Map接口的所有实现类都会有两个标准的构造方法:创建一个空映射的无参构造方法和创建一个与其参数具有相同键值映射关系的新映射

常用的方法

  • 1、添加元素: 
        V put(K key, V value): (可以相同的key值,但是添加的value值会覆盖前面的,返回值是前一个,如果没有就返回null) 
        putAll(Map m): 从指定映射中将所有映射关系复制到此映射中(可选操作)。
  • 2、删除元素 
        remove() 删除关联对象,指定key对象 
        clear() 清空集合对象
  • 3、获取元素 
        value get(key);可以用于判断键是否存在的情况。当指定的键不存在的时候,返回的是null。
  • 4、对元素进行判断: 
        boolean isEmpty() 长度为0返回true否则false 
        boolean containsKey(Object key) 判断集合中是否包含指定的key 
        boolean containsValue(Object value) 判断集合中是否包含指定的value
  • 5、获取集合长度: 
        Int size()

了解了Map接口的常用实现类和方法,接下来就可以做一些简单的代码测试了,创建HashMap实现类创建一个对象,使用以上五种常用方法进行测试:

 
 
  1. public class Test{
  2. public static void main(String[] args) {
  3. Map<Integer,String> mp = new HashMap<Integer,String>();
  4. mp.put(1,"aaa"); //添加成功返回null
  5. mp.put(2,"bbb");
  6. System.out.println("ccc添加返回值:"+mp.put(3,"ccc"));
  7. //mp.put(test, 3); //这种肯定是会报错的,因为创建mp对象时就已经使用泛型<>指定了键值对的类型为Integer-String
  8. mp.remove(2); //根据键key删除元素
  9. System.out.println(mp);
  10. String var = mp.get(3); //键对应的值存在则返回,不存在返回null
  11. System.out.println("获取键值为3元素的value:"+var);
  12. System.out.println("map集合的长度为:"+mp.size());
  13. boolean bl = mp.isEmpty();
  14. System.out.println("mp集合是否为空:"+bl);
  15. mp.clear();
  16. System.out.println("mp集合是否为空:"+mp.isEmpty());
  17. }
  18. }

Map接口实现类的迭代方式

Map接口提供了三种collection视图,允许以键集、值集或者键值对映射关系集来查看集合中的具体元素。对于这三种方式分别对应三个方法实现:

  • 1.keySet() 以键集的视图呈现Map集合元素,返回Set集合类型
  • 2.values() 值集,返回Collection集合类型
  • 3.entrySet() 返回键值映射关系的Set视图(三种方法更多信息请查看API文档)

接下来,通过实例来了解这三种迭代方法;

 
 
  1. //第一种遍历:keySet,键集
  2. Set<Integer> keys = mp.keySet();
  3. Iterator<Integer> sitr = keys.iterator();
  4. while(sitr.hasNext()){
  5. Integer key = sitr.next();
  6. System.out.println("mp集合的键为:"+key);
  7. //虽然keySet方法只能获取键集,但还是可以使用get(key)方法获取对应的值
  8. System.out.println("mp集合的值为:"+mp.get(key));
  9. }
  10. //第二种遍历value,这种方法只能迭代集合的值集不能获取到键
  11. Collection<String> values = mp.values();
  12. Iterator<String> citr = values.iterator();
  13. while(citr.hasNext()){
  14. String str = citr.next();
  15. System.out.println("集合的值为:"+str);
  16. }
  17. //第三种遍历方法:entrySet,键-值对
  18. Set<Map.Entry<Integer,String>> entry = mp.entrySet();
  19. Iterator<Map.Entry<Integer,String>> eitr = entry.iterator();
  20. while(eitr.hasNext()){
  21. Entry<Integer,String> ee = eitr.next();
  22. System.out.println("集合的键为:"+ee.getKey());
  23. System.out.println("集合的值为:"+ee.getValue());
  24. }
  25. //以上几种在JDK 5后还可以采用for-each循环遍历形式
  26. Map<String, Integer> hashmap = new HashMap<String, Integer>();
  27. for(Map.Entry<String, Integer> map : hashmap.entrySet()){
  28. System.out.println("key="+map.getKey()+" value="+map.getValue());
  29. }

Map实现类具体分析

HashMap实现类

HashMap实现类内部是基于数据结构哈希表实现,出现于JDK1.2版本,并且可以允许null的键值,是非线程同步的。想要深入学习该实现类,就得先了解哈希表的基本原理。

hash表

大家都知道数据结构数组和链表,数组是查找容易、插入和删除困难;链表则是插入、删除容易,查找困难。而这个哈希表就是集数组和链表结构的优点于一身的一种数据结构。Hash表采用一个映射函数f:key->value将关键字映射到该记录在表中的位置,从而在想要查找该条记录时,可以直接根据关键字和映射关系(Hash函数)计算出其在表中的存储位置(也就是Hash地址)。 
常用的Hash函数构造方法有以下几种: 
1.直接定址法、2.平方取中法、3.折叠法、4.除留取余法 
对于上边四种方法,很容易产生冲突,即就是不同的关键字经过Hash函数处理得到的Hash地址可能一样,造成混乱,因而为了应对这种冲突情况自然有相应的解决方法,比如有:a.开放定址法;b.链地址法 
哈希表有多种这种解决冲突的方法,这里说一种常用的链地址法(拉链法) 

 
如图所示,链地址法可以理解为一个链表的数组形式,左边是数组形式,每个数组的元素是一个链表。具体原理可以这样理解,就是将关键字采用相应的Hash函数(比如除留取余法)处理得到相应的Hash地址,然后在存储空间中寻址若不存在则直接依据数组结构进行存储存储,倘若该Hash地址已存在,则在数据相应Hash地址位开辟一个链表结构一次存储相同的Hash地址。

哈希表的内容还有很多,这里主要就说这么多,明白了这些对于HashMap实现类的原理理解也就容易多了。

HashMap存储原理

先来看看HashMap实现类的默认构造方法,根据参数(容量长度和增长因子)的不通分为以下四种:

 
 
  1. public HashMap() //采用默认容量(16)和增长因子(0.75)
  2. public HashMap(int initialCapacity) //指定集合长度
  3. public HashMap(int initialCapacity, float loadFactor) //指定集合长度和增长因子
  4. public HashMap(Map<? extends K, ? extends V> m) // 构造一个映射关系与指定 Map 相同的新 HashMap,容量和增长因子均默认值

容量长度即就是集合的长度空间大小;增长因子是用于在集合空间不够用时增大集合容量的增长率。

 
 
  1. static final int DEFAULT_INITIAL_CAPACITY = 16;
  2. static final float DEFAULT_LOAD_FACTOR = 0.75f;
  3. /**
  4. * Constructs an empty <tt>HashMap</tt> with the default initial capacity (16) and the default load factor (0.75).
  5. */
  6. public HashMap() {
  7. this.loadFactor = DEFAULT_LOAD_FACTOR;
  8. threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
  9. table = new Entry[DEFAULT_INITIAL_CAPACITY];
  10. init();
  11. }

HashMap类的元素存储主要是使用put方法实现,想要搞清楚其存储原理,就可以从put方法的实现代码中去研究学习。接下来就看看put的实现代码:

 
 
  1. public V put(K key, V value) {
  2. if (key == null)
  3. return putForNullKey(value);
  4. int hash = hash(key.hashCode());
  5. int i = indexFor(hash, table.length);
  6. for (Entry<K,V> e = table[i]; e != null; e = e.next) {
  7. Object k;
  8. if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
  9. V oldValue = e.value;
  10. e.value = value;
  11. e.recordAccess(this);
  12. return oldValue;
  13. }
  14. }
  15. modCount++;
  16. addEntry(hash, key, value, i);
  17. return null;
  18. }

如代码所示,往HashMap添加元素的时候,首先会判断key值是否为null,若为空则调用putForNullKey方法:

 
 
  1. private V putForNullKey(V value) {
  2. for (Entry<K,V> e = table[0]; e != null; e = e.next) {
  3. if (e.key == null) {
  4. V oldValue = e.value;
  5. e.value = value;
  6. e.recordAccess(this);
  7. return oldValue;
  8. }
  9. }
  10. modCount++;
  11. addEntry(0, null, value, 0);
  12. return null;
  13. }

将其放置在数组的第一个链表中(for循环table[0]),若存在null则使用新value更新原有value,否则调用addEntry方法;回到上一步,key不为null,调用hash计算键key的哈希表码值,根据hash码值和哈希表table长度计算将其放入数组的第几个链表(也就是数组的索引),此时存在两种情况: 

        情况1:如果算出的位置目前已经存在其他的键key,那么还会调用该键的equals方法与这个位置上的键进行比较,如果equals方法返回的是false,那么该键允许被存储,如果equals方法返回的是true,那么该键被视为重复,不允存储,但是会将该键对应的值value存入,更新旧的value。 

       情况2: 如果算出的位置目前没有任何元素存储,那么该键key可以直接添加到哈希表中,调用addEntry方法:

rehash操作

 
 
  1. void addEntry(int hash, K key, V value, int bucketIndex) {
  2. if ((size >= threshold) && (null != table[bucketIndex])) {
  3. resize(2 * table.length);
  4. hash = (null != key) ? hash(key) : 0;
  5. bucketIndex = indexFor(hash, table.length);
  6. }
  7. createEntry(hash, key, value, bucketIndex);
  8. }

当HashMap集合的大小size大于等于阈值(默认容量16和加载因子0.75的乘机),并且table[bucketIndex]不为null时,就会发生ReHash操作,也就是达到了容量上限需要扩容了,主要发生在resize方法中:

 
 
  1. void resize(int newCapacity) {
  2. Entry[] oldTable = table;
  3. int oldCapacity = oldTable.length;
  4. if (oldCapacity == MAXIMUM_CAPACITY) {
  5. threshold = Integer.MAX_VALUE;
  6. return;
  7. }
  8. Entry[] newTable = new Entry[newCapacity];
  9. boolean oldAltHashing = useAltHashing;
  10. useAltHashing |= sun.misc.VM.isBooted() &&
  11. (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
  12. boolean rehash = oldAltHashing ^ useAltHashing;
  13. transfer(newTable, rehash);
  14. table = newTable;
  15. threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
  16. }

若原来的容量大小已经是MAXIMUM_CAPACITY(2^30),则将阈值threshold设置为整数的最大值Integer.MAX_VALUE((2^31)-1);否则创建一张新表,调用transfer方法将当前表中的所有entries拷贝到新表中。

以上就是HashMap集合的一些基本原理。接下来看看和HashMap类似的HashTable实现类。

HashTable实现类

HashTable实现类是在JDK1.0版本出现的,底层实现同HashMap一样都是基于哈希表的。HashTable不允许null键和值,是线程同步的,在JDK1.2版本时被HashMap取代,两者主要区别就是:线程安全性,同步(synchronization),以及速度。

在单线程时,由于HashTable是线程安全的,性能肯定是不如HashMap的。而且HashMap的迭代器是快速失败的(fail-fast),当在迭代时有其他线程改变了HashMap的结构(增加或移除元素,除过迭代器本身的remove方法),就会抛出ConsurrentModificationException异常。

对于HashMap不是线程安全的,在JDK 5时加入了ConcurrentHashMap,可以支持多个并发线程。

TreeMap实现类

HashMap实现类底层是基于哈希表数据结构实现(可以理解为数组和链表的合体)。而TreeMap实现类底层是基于红黑树(二叉树)数据结构实现的,往TreeMap添加元素的时候,如果元素的键具备自然顺序或者创建映射时提供了Comparator接口,那么就会按照键的这两种特性进行排序存储。 
基于树实现必然会牵扯到左右子树节点的定义,下来先看看TreeMap类的节点是如何定义的:

 
 
  1. static final class Entry<K,V> implements Map.Entry<K,V> {
  2. K key;
  3. V value;
  4. Entry<K,V> left = null; //左子树
  5. Entry<K,V> right = null; //右子树
  6. Entry<K,V> parent; //父节点
  7. boolean color = BLACK;
  8. Entry(K key, V value, Entry<K,V> parent) {
  9. this.key = key;
  10. this.value = value;
  11. this.parent = parent;
  12. }
  13. public K getKey() {
  14. return key;
  15. }
  16. /**
  17. * Returns the value associated with the key
  18. * @return the value associated with the key
  19. */
  20. public V getValue() {
  21. return value;
  22. }
  23. /**
  24. * 该方法功能是当新插入的节点的key和当前节点的key相等时,会
  25. * 以新插入key对应的value1更新当前节点key对应的value2,然后将该value2返回
  26. */
  27. public V setValue(V value) {
  28. V oldValue = this.value;
  29. this.value = value;
  30. return oldValue;
  31. }
  32. public boolean equals(Object o) {
  33. if (!(o instanceof Map.Entry))
  34. return false;
  35. Map.Entry<?,?> e = (Map.Entry<?,?>)o;
  36. return valEquals(key,e.getKey()) && valEquals(value,e.getValue());
  37. }
  38. public int hashCode() {
  39. int keyHash = (key==null ? 0 : key.hashCode());
  40. int valueHash = (value==null ? 0 : value.hashCode());
  41. return keyHash ^ valueHash;
  42. }
  43. public String toString() {
  44. return key + "=" + value;
  45. }
  46. }

TreeMap实现的对象中添加节点信息,如果插入的节点的键key已存在,则会更新旧键的value值,并返回被替换的value,否则put方法返回null

 
 
    1. public V put(K key, V value) {
    2. Entry<K,V> t = root;
    3. if (t == null) { //当前节点为空则直接以插入键值对新建节点
    4. compare(key, key); // type (and possibly null) check
    5. root = new Entry<>(key, value, null);
    6. size = 1;
    7. modCount++;
    8. return null;
    9. }
    10. int cmp;
    11. Entry<K,V> parent;
    12. // split comparator and comparable paths
    13. Comparator<? super K> cpr = comparator;
    14. if (cpr != null) { //Comparator比较器不为空
    15. do {
    16. parent = t;
    17. cmp = cpr.compare(key, t.key);
    18. if (cmp < 0) //当前节点键大于插入节点的键,继续遍历当前节点的左孩子
    19. t = t.left;
    20. else if (cmp > 0) //小于,遍历右孩子
    21. t = t.right;
    22. else
    23. return t.setValue(value); //键已存在,更新value值
    24. } while (t != null);
    25. }
    26. else {//比较器为空,采用Comparable接口的comparaTo方法比较
    27. if (key == null)
    28. throw new NullPointerException();
    29. Comparable<? super K> k = (Comparable<? super K>) key;
    30. do {
    31. parent = t;
    32. cmp = k.compareTo(t.key);
    33. if (cmp < 0) //当前key大于插入key,继续遍历左子树
    34. t = t.left;
    35. else if (cmp > 0) //当前key小于插入key,遍历右子树
    36. t = t.right;
    37. else
    38. return t.setValue(value);
    39. } while (t != null);
    40. }
    41. //将所有子树全部遍历为空后(即就是到叶子节点),则将插入节点作为parent的子节点
    42. Entry<K,V> e = new Entry<>(key, value, parent);
    43. if (cmp < 0)
    44. parent.left = e;
    45. else
    46. parent.right = e;
    47. fixAfterInsertion(e); //插入新节点,调用fixAfterInsertion方法调整红黑树
    48. size++;
    49. modCount++;
    50. return null;
    51. }

fixAfterInsertion方法调整红黑树主要由三个方法setColor(设置颜色)rotateLeft(左旋)、rotateRight(右旋)实现,这也是红黑树的核心操作,对于这部分更为详细的介绍可以参看文章http://www.cnblogs.com/chenssy/p/3746600.html 

TreeMap添加不具备自然顺序的元素和自定义Comparator接口 
接下来看看测试代码,继续深入了解下,键值对象不具有自然顺序时,键所属的类没有实现Comparable接口,也没有在创建TreeMap对象的时候传入比较器时,运行代码时就会报错:

 
 
  1. class staff{
  2. int id;
  3. String name;
  4. staff(int id,String name) {
  5. super();
  6. this.id = id;
  7. this.name = name;
  8. }
  9. @Override
  10. public String toString() {
  11. return "["+this.id+","+this.name+"]";
  12. }
  13. }
  14. public class TreeMap_test {
  15. public static void main(final String[] args) {
  16. Map<staff,String> map = new TreeMap<staff,String>();
  17. map.put(new staff(2,"张三"), "语文");
  18. map.put(new staff(1,"李四"), "数学");
  19. map.put(new staff(5,"王五"), "英语");
  20. System.out.println(map);
  21. }
  22. }

程序中因为键值是自定义staff的类型,并没有自然顺序,在输入键值时调用默认的compare方法就无法进行比较,因而运行程序时会报错如下:

 
 
  1. Exception in thread "main" java.lang.ClassCastException: staff cannot be cast to java.lang.Comparable
  2. at java.util.TreeMap.compare(Unknown Source)
  3. at java.util.TreeMap.put(Unknown Source)
  4. at TreeMap_test.main(TreeMap_test.java:25)

因而在使用TreeMap添加元素时应该注意如下两点:

  • 1.往TreeMap添加元素的时候,如果元素的键不具备自然顺序特性, 那么键所属的类必须要实现Comparable接口,把键的比较规则定义在CompareTo方法上;
  • 2.往TreeMap添加元素的时候,如果元素的键不具备自然顺序特性,而且键所属的类也没有实现Comparable接口,那么就必须在创建TreeMap对象的时候传入比较器。
 
 
  1. //1.键所属的类实现Comparable接口
  2. class staff implements Comparable<staff>{
  3. @Override
  4. public int compareTo(staff o) {
  5. return this.id-o.id;
  6. }
  7. }
  8. //2.自定义Comparator接口,在创建TreeMap对象时传入比较器
  9. class Mycomparetor implements Comparator<staff> {
  10. @Override
  11. public int compare(staff o1,staff o2) {
  12. return o1.id - o2.id;
  13. }
  14. }
  15. public class TreeMap_test {
  16. public static void main(final String[] args) {
  17. Mycomparetor comparetor = new Mycomparetor();
  18. Map<staff,String> map = new TreeMap<staff,String>(comparetor);
  19. }
  20. }

运行程序就会按键中的id排序:

 
 
  1. {[1,李四]=数学, [2,张三]=语文, [5,王五]=英语}

以上就是自己学习Java集合之Map接口的一些总结,虽说不是很深入,但是从不懂到编写基本的集合代码肯定没有问题,后边还得继续加油。。。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值