内容学习于:edu.aliyun.com


引言

  集合根据数据存储的不同分为两种形式:单值集合、二元偶对象集合,在之前所使用的Collection都属于单值集合,而本次所讲解的Map属于二元偶对象集合,所谓的二元偶对象指的是存储的数据为“key = value"结构对,在使用的时候可以根据key查询出相应的value的内容,所以Collection和Map存储数据的目的分别为: Collection 是为了数据的输出而存储,而Map是为了数据的查询而存储。

1. Map接口简介

  java.util.Map是进行二元偶对象数据存储的最大父接口,在里面所有存放的内容会按照“key = value"的形式进行保存,所以在数据存放的时候就需要保存有两个内容,Map接口的常用方法如下:

No. 方法名称 类型 描述
01 V put(K key, V value) 普通 向集合中保存数据,如果key存在则发生替换,同时返回旧的内容,如果key不存在,返回null
02 V get(Object key) 普通 通过key查询对应内容
03 V remove(Object key) 普通 根据key删除对应的内容
04 int size() 普通 获取集合长度
05 Collection values() 普通 获取所有内容
06 Set keySet() 普通 获取所有的key(key不能重复)
07 Set<Map.Entry<K,V>> entrySet() 普通 将所有的内容以Map.Entry集合的形式返回

  如下图所示:

  程序会按照“key=value"的形式进行保存,同时此时的存储是无序的(Map的功能是进行查询,是否有序意义不大),但是使用此种方式创建的Map集合,如果可用of()创建Map出现有key重复的问题,那么程序就会出现如下的异常:
Exception in thread “main” java.langllgalArgumentException: duplicate key: three
  因为key作为Map操作的核心控制点,所以这个内容的重复实际上对于整个的Map而言就需要进行更新,如果要想正确的使用Map接口,那么就必须使用它的几个子类,常见的子类: HashMap、 LinkedHashMap、 TreeMap、Hashtable。

2. HashMap

  HashMap是Map接口中最为常见的一一个子类,也是主要使用的一个子类,此类通过名称就可以发现,采用Hash方式进行存储,所以其存储的时候都是无序的,此类的定义结构如下:

  • public class HashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable

  类结构如下图所示:

观察下面操作:

public class JavaAPIDemo {
    public static void main(String[] args) throws Exception {
        Map<String,String> map = new HashMap<>();
        System.out.println("【未发生替换】"+map.put("Hello","你好"));
        System.out.println("【已发生替换】"+map.put("Hello","你还好不好?"));
        map.put(null,"empty");
        map.put("empty",null);
        System.out.println(map);
    }
}

结果:

【未发生替换】null
【已发生替换】你好
{null=empty, Hello=你还好不好?, empty=null}

  通过以上的存储可以发现,Map中的key绝对是唯一的标记,不可能重复,当未发生替换时,会返回一个null;当发生替换时,会返回旧的数据;同时key和value都可以存储null值。使用Map集合的意义在于需要根据key进行内容的查找。

  如果只是进行方法的使用研究那么对于HashMap而言意义不大,因为必须要关注HashMap的源代码实现机制(JDK 1.8之后HashMap算法进行了重大的变更)。

无参构造:

public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted 
    }
final float loadFactor;// The load factor for the hash table.
static final float DEFAULT_LOAD_FACTOR = 0.75f;	//(默认扩充的阈值为75%)

添加数据:

public V put(K key, V value) {
	
        return putVal(hash(key), key, value, false, true);// putVal实现的节点相关调用以及扩容的调用(resize())
    }

//resize()
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;// aka 16(默认容量大小保存16个) 
transient Node<K,V>[] table; //(默认是根据一个数组的形式存储)
static final int MAXIMUM_CAPACITY = 1 << 30;  //(最大存储)
newCap = oldCap << 1  //(每次扩容1倍)

性能保证:

static final int TREEIFY_THRESHOLD = 8;(树状阈值)
if (binCount >= TREEIFY_THRESHOLD - 1) {// -1 for 1st
     treeifyBin(tab, hash);//(进行树状结构转换)
     break;
}

  对于HashMap来讲,如果要进行扩容,则表示当前的存储容量达到“75%”的时候才会选择扩容,在JDK1.8之后,如果HashMap中的数据存储容量达到8位,那么为了保证数据的查询性能,HashMap 会将原始的链表存放结构转为红黑树结构进行保存,可以利用红黑树中自旋的处理实现树的平衡修复。

3. LinkedHashMap

  HashMap之中进行数据存储的时候并不会进行顺序的定义,所以如果现在要想实现顺序的存储,就可以利用LinkedHashMap子类来完成,这个类的定义如下:

  • public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>

  如下图所示:

  在以后如果要想考虑到有序的HashMap存储的时候就使用它的子类:LinkedHashMap。

4. TreeMap

  java.util.TeeMap实现的是- -个排序的树结构,可以依据key的自然顺序实现排序的处理,此类的定义如下:

  • public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>

  如下图所示:

  既然此时是要进行key数据的排序,那么在使用的过程之中key的数据内容就绝对不允许设置为null

5. Hashtable

  类集中有三老( 出现时间很古老,让人聊起来觉得很显老,使用起来觉得资格很老),Vectory、 Enumeration、 Hashtable 就是常见的三老,Hashtable是在JDK1.0的时候提出的集合操作最早偶对象存储,观察下此类的定义:

  • public class Hashtable<K,V>
    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, Serializable

  如下图所示:

  Hashtable在进行数据存储的时候是不允许存放null数据的,不管是key还是value,如果发现有null,那么最终都会出现“NullPointerException",而相比较HashMap不管是在key还是在value上都没有关于null的限制。

面试题:请解释HashMap与Hashtable的区别?

  • HashMap在进行存储的时候默认的大小为16,在存储量达到8位之后为了保证数据的查询性能使用红黑树进行存储,HashMap中的全部方法都使用异步处理,属于非线程的安全操作;
  • Hashtable进行存储时默认的大小为11, Hashtable中的方法使用同步处理,属于线程安全的操作。

6. Map.Entry

  通过一系列的分析之后实际上就可以得出整个Map接口的基本使用情况,但是对于数据的存储必须进行详细说明,Map集合与Collection集合的最大不同在于,它所存储的数据是二元偶对象。
  在Map里面由于需要存放有两个内容,很明显为了可以进行整体的处理方便,就在Map接口里面定义有一个Map.Entry的内部接口,此接口主要是进行key和value封装的,而且在Map接口里面也可以发现Map.Entry的子类:

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
}

  Map.Entry里面可以包装key和value,那么也可以通过Map.Entry获取对应的key和value,此接口定义如下:

public static interface Map.Entry<K,V>{
		public K getKey();
		public V getValue();
}

  为了清楚Map.Entry的作用,在Map接口里面定义有一个entry()方法,此方法可以非常方便的创建Map.Entry 实例。

范例:创建Map.Entry对象

  • JDK 1.9之后增加的方法:static <K,V> Map.Entry<K,V> entry(K k, V v)
public class JavaAPIDemo {
    public static void main(String[] args) throws Exception {
        Map.Entry<String,String> entry = Map.entry("MLDN","www.mldn.cn");
        System.out.println("Key = " +entry.getKey() + "、Value = " + entry.getValue());

    }
}

结果:

Key = MLDN、Value = www.mldn.cn

  Map.Entry实质上定义了一个Map偶对象的存储标准,所有的Map接口的子类都依据此标准实现相应的节点数据的存储,也可以直接利用此实例实现key与value的分离。

7. Iterator输出Map集合

  面对于集合数据的输出,肯定要考虑使用Iterator接口完成,但是在Map接口中并没有任何-一个方法可以直接获取到Iterator
接口实例,所以此时就必须经过一系列的转换得来。
  之所以在Map接口中没有提供有直接获取Iterator 接口实例的方法,原因就在于它的存储不是一个普通的数据,而是一个偶对象,而Iterator每一次可 以输出的全部都是单个实例,为此基本的输出流程如下:

  • 1、 通过Map接口中的entrySet()方法,将Map实例转为Set接口实例;
    方法: Set<Map.Entry<K,V>> entrySet()
  • 2、获取 了Set 集合实例之后就可以调用iterator(方法获取Iterator接口实例,泛型类型为“Map.Entry<K,V> ”;
  • 3、通过 Iterator 进行迭代操作,获取每一组的“Map. Entry<K,V>"实例,进行key与value的分离。

使用Iterator输出:

public class JavaAPIDemo {
    public static void main(String[] args) throws Exception {
        Map<String,String> map = new HashMap<>();
        map.put("aaa","111");
        map.put("bbb","222");
        map.put("ccc","333");
        Set<Map.Entry<String,String>> set = map.entrySet();//变成set对象
        Iterator<Map.Entry<String,String>> iterator = set.iterator();
        while (iterator.hasNext()){
            Map.Entry<String,String> entry = iterator.next();//获得map.entry实例
            System.out.println("Key = " +entry.getKey() + "、Value = " + entry.getValue());
        }

    }
}

结果:

Key = aaa、Value = 111
Key = ccc、Value = 333
Key = bbb、Value = 222

  结构如下图所示:

  从JDK 1.5之后Map集合也可以使用foreach 进行输出,因为其内部实现了Iterator 接口:

源代码:

final class EntryIterator extends HashIterator
        implements Iterator<Map.Entry<K,V>> {
        public final Map.Entry<K,V> next() { return nextNode(); }
    }

使用foreach输出:

public class JavaAPIDemo {
    public static void main(String[] args) throws Exception {
        Map<String,String> map = new HashMap<>();
        map.put("aaa","111");
        map.put("bbb","222");
        map.put("ccc","333");
        for (Map.Entry<String,String> entry : map.entrySet()){
            System.out.println("Key = " +entry.getKey() + "、Value = " + entry.getValue());
        }

    }
}

结果:

Key = aaa、Value = 111
Key = ccc、Value = 333
Key = bbb、Value = 222

  不管是何种集合最终的输出的归宿只有一点就是通过Iterator, 但是需要注意的是,Map 一般很少直接输出,因为其功能主要是进行数据查询。

8. 自定义Key

  对于此时的Map集合可以发现,设置的K和V两个泛型类型只要是引用数据类型就可以了,那么这也就包括了自定义的类,也就是说自定义的类也可以称为Map中的key类型。但是此时作为key类型所在的类一定 要覆写hashCode()与equals()两个方法,因为牵扯到对象比较问题。
  Map集合根据key获取数据时的流程:

  • 1、利用hashCode()方法生成结果进行比较,因为这只是一个数字,它的比较速度会更加的快;
  • 2、如果发现哈希码相同的时候才会进行内容的比较。

自定义对象查询:

class Member{
    private String name;
    private int age;
    public Member(String name,int age){
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Member{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Member member = (Member) o;
        return age == member.age &&
                Objects.equals(name, member.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}


public class JavaAPIDemo {
    public static void main(String[] args) throws Exception {
        Map<Member,String> map = new HashMap<>();
        map.put(new Member("张三",15),"aaa");
        System.out.println(map.get(new Member("张三",15)));
    }
}

结果:

aaa

  在以后实际编写的代码过程之中,如果遇见了Map集合,则一般对于Map集合中的Key类型,最为常见的是String,其次就是Long或者是Integer.
经过现在的分析可以得出一个结论:哈希码实际上是进行对象比较的关键所在,而在进行Map存储的时候实际上也是依靠哈希码得到的一个存储空间,但是在很多情况下依然有可能会出现哈希冲突的问题,那么这个时候的解决方案(四种解决方案):开放定址法、链地址法、再哈希法、建立公共溢出区 ,而Java是利用链地址法的形式解决的。把所有重复的内容放在一个链表之中进行保存。
  如下图所示:

总结

  • 1、只要碰见单值集合优先考虑使用List, List 接口优先考虑的是ArrayList;
  • 2、 只要进行key的内容查找操作,就属于Map接口,Map接口考虑HashMap子类;
  • 3、集合的输出全部使用Iterator接口完成。