HashMap、HashTable、LinkedHashMap、TreeMap、HashSet简单总结

来源:互联网 发布:asp微信接口源码 编辑:程序博客网 时间:2024/06/01 14:14

写在前面:

  前几天在做科大讯飞的笔试题时,遇到一个问题:就是按顺序put进HashMap,取值的时候不是按输入顺序获得的。所以就打算把这几种常用而且容易混淆的map整理一下。

  首先介绍一下什么是Map。在数组中我们是通过数组下标来对其内容索引的,而在Map中我们通过对象来对对象进行索引,用来索引的对象叫做key,其对应的对象叫做value。这就是我们平时说的键值对。

  话不多说,先上测试代码:包括HashMap、Hashtable、LinkedHashMap、TreeMap

import java.util.ArrayList;import java.util.HashMap;import java.util.Hashtable;import java.util.LinkedHashMap;import java.util.List;import java.util.Map;import java.util.TreeMap;public class mapTest {    public static void main(String[] args) {        System.out.println("HashMap的测试结果");        Map<String, Integer> map = new HashMap<String, Integer>();        map.put("dcb", 2);        map.put("dbb", 1);        map.put("ba", 1);        map.put("ab", 3);        List<Map.Entry<String, Integer>> list = new ArrayList<Map.Entry<String, Integer>>(                map.entrySet());        for (int i = 0; i < list.size(); i++) {            String id = list.get(i).toString();            System.out.println(id);        }        System.out.println("HashTable的测试结果");        Hashtable<String, Integer> tab = new Hashtable<>();        tab.put("dcb", 2);        tab.put("dbb", 1);        tab.put("ba", 1);        tab.put("ab", 3);        // Iterator<String> iterator_tab = tab.keySet().iterator();        // while(iterator_tab.hasNext()){        // Object key = iterator_tab.next();        // System.out.println(tab.get(key));        // }        List<Map.Entry<String, Integer>> list_tab = new ArrayList<Map.Entry<String, Integer>>(                tab.entrySet());        for (int i = 0; i < list_tab.size(); i++) {            String id = list_tab.get(i).toString();            System.out.println(id);        }        System.out.println("LinkedHashMap的测试结果");        Map<String, Integer> map_link = new LinkedHashMap<String, Integer>();        map_link.put("dcb", 2);        map_link.put("dbb", 1);        map_link.put("ba", 1);        map_link.put("ab", 3);        List<Map.Entry<String, Integer>> list_link = new ArrayList<Map.Entry<String, Integer>>(                map_link.entrySet());        for (int i = 0; i < list_link.size(); i++) {            String id = list_link.get(i).toString();            System.out.println(id);        }        System.out.println("TreeMap的测试结果");        Map<String, Integer> treemap = new TreeMap<String, Integer>();        treemap.put("dcb", 2);        treemap.put("dbb", 1);        treemap.put("ba", 1);        treemap.put("ab", 3);        List<Map.Entry<String, Integer>> list_tree = new ArrayList<Map.Entry<String, Integer>>(                treemap.entrySet());        for (int i = 0; i < list_tree.size(); i++) {            String id = list_tree.get(i).toString();            System.out.println(id);        }    }}

最终的输出结果如下:

HashMap的测试结果dbb=1dcb=2ba=1ab=3HashTable的测试结果dbb=1ab=3dcb=2ba=1LinkedHashMap的测试结果dcb=2dbb=1ba=1ab=3TreeMap的测试结果ab=3ba=1dbb=1dcb=2

  从结果中可以看出,四种的输入都是完全一样的,但是HashMap和HashTable的输出则是随机顺序的,LinkedHashMap是按照输入顺序输出的,而TreeMap则是对键进行排序(默认是升序)后输出的。

HashMap

HashMap原理

  HashMap是一个最常用的Map,它根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度。HashMap最多只允许一条记录的键为Null;允许多条记录的值为Null;HashMap不支持线程的同步,即任一时刻可以有多个线程同时写HashMap;可能会导致数据的不一致。如果需要同步,可以用Collections的synchronizedMap方法使HashMap具有同步的能力,这样多个线程同时访问HashMap时,能保证只有一个线程更改Map。

Map m = Collections.synchronizeMap(hashMap)

  HashMap基于hashing原理,我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,然后找到bucket位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。

  HashMap在每个链表节点中储存键值对对象。当两个不同的键对象的hashcode相同时会发生什么?它们会储存在同一个bucket位置的链表中。键对象的equals()方法用来找到键值对。如果两个键的hashcode相同,便在找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。

存储:

int hash = key.hashCode();int index = hash % Entry[].length;Entry[index] = value;

取值:

int hash = key.hashCode();int index = hash % Entry[].length;return Entry[index];

  HashMap内部维护了一个存储数据的Entry数组,HashMap采用链表解决冲突,每一个Entry本质上是一个单向链表。当准备添加一个key-value对时,首先通过hash(key)方法计算hash值,然后通过indexFor(hash,length)求该key-value对的存储位置,计算方法是先用hash&0x7FFFFFFF后,再对length取模,这就保证每一个key-value对都能存入HashMap中,当计算出的位置相同时,由于存入位置是一个链表,则把这个key-value对插入链表头。

image

  需要注意的就是Entry里除了key和value之外还有一个next,这个next相当于链表中的链域指针。那什么情况下会用到next呢。很明显当数据的hash值对map表长度去余时,结果相等。那么为了避免覆盖,所以将数据按照插入时的顺序链接到后边。下面就可以回答为什么按序put无序get的问题了。get的时候,首先是遍历数组的第一个元素,然后遍历第一个元素的链表。然后第二个元素,第二个元素的链表。。。。。So,我get到的数据极有可能是无序的了(相对于put的顺序)。

负载因子:

  Hashmap的设想是在O(1)的时间复杂度存取数据,根据我们的分析,在最坏情况下,时间复杂度很可能是o(n),但这肯定极少出现。但是某个链表中存在多个元素还是有相当大的可能的。当hashmap中的元素数量越接近数组长度,这个几率就越大。为了保证hashmap的性能,我们对元素数量/数组长度的值做了上限,此值就是负载因子。当比值大于负载因子时,就需要对内置数组进行扩容,从而提高读写性能。但这也正是问题的所在,对数组扩容,代价较大,时间复杂度时O(n)。

  故我们在hashmap需要存放的元素数量可以预估的情况下,预先设定一个初始容量,来避免自动扩容的操作来提高性能。

  如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?

  因为默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。

  那重新调整HashMap大小存在什么问题吗?

  当重新调整HashMap大小的时候,确实存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。这个时候,你可以质问面试官,为什么这么奇怪,要在多线程的环境下使用HashMap呢?

  HashMap不适用于多线程环境!!!多线程使用ConcurrentHashMap(后续总结)

继承:

  (HashMap继承AbstractMap)覆盖了equals()和hashCode()方法以确保两个相等映射返回相同的哈希码。如果两个映射大小相等、包含同样的键且每个键在这两个映射中对应的值都相同,则这两个映射相等。映射的哈希码是映射元素哈希码的总和,其中每个元素是Map.Entry接口的一个实现。因此,不论映射内部顺序如何,两个相等映射会报告相同的哈希码。

冲突:

  在查找数据时,我们理想的情况是不经过任何比较,一次存取便能得到所查记录。这就要在记录的储存位置和它的关键字之间建立一个确定的对应关系f,使每个关键字和结构中一个惟一的存储位置相对应。这个对应关系我们就称之为哈希函数,根据关键字key和f找到数据的存储位置。对于不同的关键字,可能经过哈希函数的映射后会得到同一个值,即key1!=key2 , f(key1)= f(key2),这就得不到惟一的存储位置了。对于这种情况,我们称之为冲突。

  哈希函数的构造方法:直接定址法(取关键字或关键字的某个线性函数值为哈希地址)、除留余数法、平方取中法(取关键字平方后的中间几位为哈希地址)

  处理冲突的方法:开放定址法、链地址法

  HashMap的哈希函数

  static int hash(int h) {     // This function ensures that hashCodes that differ only by     // constant multiples at each bit position have a bounded     // number of collisions (approximately 8 at default load factor).     h ^= (h >>> 20) ^ (h >>> 12);     return h ^ (h >>> 7) ^ (h >>> 4);}

更多的关于HashMap的问题()

Q1: 为什么String, Interger这样的wrapper类适合作为键?

A1: String, Interger这样的wrapper类作为HashMap的键是再适合不过了,而且String最为常用。因为String是不可变的,也是final的,而且已经重写了equals()和hashCode()方法了。其他的wrapper类也有这个特点。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。不可变性还有其他的优点如线程安全。如果你可以仅仅通过将某个field声明成final就能保证hashCode是不变的,那么请这么做吧。因为获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的。如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这样就能提高HashMap的性能。

Q2:我们可以使用自定义的对象作为键吗?

A2:这是前一个问题的延伸。当然你可能使用任何对象作为键,只要它遵守了equals()和hashCode()方法的定义规则,并且当对象插入到Map中之后将不会再改变了。如果这个自定义对象时不可变的,那么它已经满足了作为键的条件,因为当它创建之后就已经不能改变了。

Q3:我们可以使用CocurrentHashMap来代替Hashtable吗?

A3:这是另外一个很热门的面试题,因为ConcurrentHashMap越来越多人用了。我们知道Hashtable是synchronized的,但是ConcurrentHashMap同步性能更好,因为它仅仅根据同步级别对map的一部分进行上锁。ConcurrentHashMap当然可以代替HashTable,但是HashTable提供更强的线程安全性。

LinkedHashMap

定义

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

  对于LinkedHashMap而言,它继承于HashMap()、底层使用哈希表与双向链表来保存所有元素。其基本操作与父类HashMap相似,它通过重写父类相关的方法,来实现自己的链接列表特性。

  LinkedHashMap与HashMap不同之处在于,它保留了一个‘前驱链域’和一个后继链域。get的时候是按照put的顺序读取的。

Hashtable

定义:

  Hashtable在Java中的定义为:

public class Hashtable<K,V>      extends Dictionary<K,V>      implements Map<K,V>, Cloneable, java.io.Serializable{}

  从源码中,我们可以看出,Hashtable 继承于Dictionary类,实现了Map,Cloneable, java.io.Serializable接口。其中Dictionary类是任何可将键映射到相应值的类(如 Hashtable)的抽象父类,每个键和值都是对象(源码注释为:The Dictionary class is the abstract parent of any class, such as Hashtable, which maps keys to values. Every key and every value is an object.)

简介:

  Hashtable是非泛型的集合,所以在检索和存储值类型时通常会发生装箱与拆箱的操作。

  当把某个元素添加到Hashtable时,将根据键的哈希代码将该元素放入存储桶中,由于是散列算法所以会出现一个哈希函数能够为两个不同的键生成相同的哈希代码,该键的后续查找将使用键的哈希代码只在一个特定存储桶中搜索,这将大大减少为查找一个元素所需的键比较的次数。

  Hashtable 的加载因子确定元素与Hashtable 可拥有的元素数的最大比率。加载因子越小,平均查找速度越快,但消耗的内存也增加。默认的加载因子0.72通常提供速度和大小之间的最佳平衡。当创建Hashtable时,也可以指定其他加载因子。

  元素总量/Hashtable可拥有的元素数=加载因子

  当向Hashtable添加元素时,Hashtable的实际加载因子将增加。当实际加载因子达到指定的加载因子时,Hashtable中存储桶的数目自动增加到大于当前Hashtable存储桶数两倍的最小素数。

  扩容时所有的数据需要重新进行散列计算。虽然Hash具有O(1)的数据检索效率,但它空间开销却通常很大,是以空间换取时间。所以Hashtable适用于读取操作频繁,写入操作很少的操作类型。

区别:

  Hashtable与HashMap类似,但是主要有6点不同。

  1. HashTable的方法是同步的,HashMap未经同步,所以在多线程场合要手动同步HashMap。
    Map m = Collections.synchronizeMap(hashMap);这个区别就像Vector和ArrayList一样。sychronized意味着在一次仅有一个线程能够更改Hashtable。就是说任何线程要更新Hashtable时要首先获得同步锁,其它线程要等到同步锁被释放之后才能再次获得同步锁更新Hashtable。

  2. HashTable不允许null值,key和value都不可以,HashMap允许null值,key和value都可以。HashMap允许key值只能由一个null值,因为hashmap如果key值相同,新的key,value将替代旧的。

  3. HashTable有一个contains(Object value)功能和containsValue(Object value)功能一样。

  4. HashTable使用Enumeration,HashMap使用Iterator。

  5. HashTable中hash数组默认大小是11,增加的方式是 old*2+1。HashMap中hash数组的默认大小是16,而且一定是2的指数。

  6. Hashtable是线程安全的,可由多个读取器线程和一个写入线程使用。多线程使用时,如果只有一个线程执行写入(更新)操作,则它是线程安全的。

继承:

  Hashtable继承自Dictionary类。

TreeMap

定义:

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

  TreeMap能够把它保存的记录根据键排序,默认是按升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。

继承:

  (TreeMap继承自SortedMap)它用来保持键的有序顺序。SortedMap接口为映像的视图(子集),包括两个端点提供了访问方法。除了排序是作用于映射的键以外,处理SortedMap和处理SortedSet一样。添加到SortedMap实现类的元素必须实现Comparable接口,否则您必须给它的构造函数提供一个Comparator接口的实现。TreeMap类是它的唯一一份实现。

HashSet

定义:

  对于 HashSet 而言,它是基于 HashMap 实现的,HashSet 底层采用 HashMap 来保存所有元素,因此 HashSet 的实现比较简单,查看 HashSet 的源代码,可以看到如下代码:

public class HashSet<E>      extends AbstractSet<E>      implements Set<E>, Cloneable, java.io.Serializable  {      // 使用 HashMap 的 key 保存 HashSet 中所有元素     private transient HashMap<E,Object> map;      // 定义一个虚拟的 Object 对象作为 HashMap 的 value      private static final Object PRESENT = new Object();      ...      // 初始化 HashSet,底层会初始化一个 HashMap      public HashSet()      {          map = new HashMap<E,Object>();      }      // 以指定的 initialCapacity、loadFactor 创建 HashSet      // 其实就是以相应的参数创建 HashMap      public HashSet(int initialCapacity, float loadFactor)      {          map = new HashMap<E,Object>(initialCapacity, loadFactor);      }      public HashSet(int initialCapacity)      {          map = new HashMap<E,Object>(initialCapacity);      }      HashSet(int initialCapacity, float loadFactor, boolean dummy)      {          map = new LinkedHashMap<E,Object>(initialCapacity              , loadFactor);      }      // 调用 map 的 keySet 来返回所有的 key      public Iterator<E> iterator()      {          return map.keySet().iterator();      }      // 调用 HashMap 的 size() 方法返回 Entry 的数量,就得到该 Set 里元素的个数     public int size()      {          return map.size();      }      // 调用 HashMap 的 isEmpty() 判断该 HashSet 是否为空,     // 当 HashMap 为空时,对应的 HashSet 也为空     public boolean isEmpty()      {          return map.isEmpty();      }      // 调用 HashMap 的 containsKey 判断是否包含指定 key      //HashSet 的所有元素就是通过 HashMap 的 key 来保存的     public boolean contains(Object o)      {          return map.containsKey(o);      }      // 将指定元素放入 HashSet 中,也就是将该元素作为 key 放入 HashMap      public boolean add(E e)      {          return map.put(e, PRESENT) == null;      }      // 调用 HashMap 的 remove 方法删除指定 Entry,也就删除了 HashSet 中对应的元素     public boolean remove(Object o)      {          return map.remove(o)==PRESENT;      }      // 调用 Map 的 clear 方法清空所有 Entry,也就清空了 HashSet 中所有元素     public void clear()      {          map.clear();      }      ...  } 

  HashSet 的实现其实非常简单,它只是封装了一个 HashMap 对象来存储所有的集合元素,所有放入 HashSet 中的集合元素实际上由 HashMap 的 key 来保存,而 HashMap 的 value 则存储了一个 PRESENT,它是一个静态的 Object 对象。

  HashSet实现了Set接口,它不允许集合中有重复的值,当我们提到HashSet时,第一件事情就是在将对象存储在HashSet之前,要先确保对象重写equals()和hashCode()方法,这样才能比较对象的值是否相等,以确保set中没有储存相等的对象。如果我们没有重写这两个方法,将会使用这个方法的默认实现。

  public boolean add(Object o)方法用来在Set中添加元素,当元素值重复时则会立即返回false,如果成功添加的话会返回true。

  HashSet较HashMap来说比较慢。

总结

  • HashMap里面存入的键值对在取出的时候是随机的,也是我们最常用的一个Map。它根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度。在Map中插入、删除和定位元素,HashMap是最好的选择。

  • TreeMap取出来的是排序后的键值对。但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。

  • LinkedHashMap是HashMap的一个子类,如果需要输出的顺序和输入的相同,那么用LinkedHashMap可以实现。

  • HashMap:底层是哈希表数据结构,线程不同步。

  • TreeMap:底层是二叉树(红黑树)数据结构,时间复杂度平均能达到O(logn),线程不同步,可用于给Map集合中的键进行排序。

Map.Entry :

  接口,是Map的嵌套类,A map entry (key-value pair).单个键到值的映射,可以改变map中value的值(setValue())。

  通过这个集合的迭代器,您可以获得每一个条目(唯一获取方式)的键或值并对值进行更改。当条目通过迭代器返回后,除非是迭代器自身的remove()方法或者迭代器返回的条目的setValue()方法,其余对源Map外部的修改都会导致此条目集变得无效,同时产生条目行为未定义。

Itetrator it = map.entrySet().iterator();while(it.haseNext()){    Map.Entry<Integer,Integer> entry=(Map.Entry<Integer,Integer>)it.next();    entry.getKey();    entry.getValue();}

—–乐于分享,共同进步
—–Any comments greatly appreciated

原创粉丝点击