Java HashMap-HashTable-HashSet等简介与常见问题

        最常见的应该是在问HashMap与HashTable的区别,偶尔会谈及后两者的区别。今天我们先简单区别一下这几个泛型类。

        1.定义

        2.HashMap 与HashTable 的区别

        3.HashSet 

        4.TreeSet和TreeMap

        5.常见问题



1.定义

HashMap 是一个散列表,它存储的内容是键值对(key-value)映射;

Hashtable 也是一个散列表,它存储的内容是键值对(key-value)映射;

HashSet类实现了Set接口,由一个实际上是HashMap实例的散列表​​支持,它可视为集合存储,且不允许集合中有重复的值;

TreeSet 是一个有序的集合,它的作用是提供有序的Set集合;


2.HashMap 与HashTable 的区别

(a)类别:Hashtable继承自Dictionary类,而HashMap继承自AbstractMap类。但二者都实现了Map接口;

(b)线程安全性不同:最重要的一点是,HashMap并非线程安全的,而HashTable是具有线程安全的(具有同步锁Synchronize,也就是说每次只能有一个线程进行访问,访问前线程必须先取得其同步锁)。也因此,在多线程使用时候,我们并不需要对HashTable进行同步处理,而HashMap则需要;

(c)key和value是否允许null值 :其中key和value都是对象,并且不能包含重复key,但可以包含重复的value;

            HashTable的key和value都不可空,但是HashMap均可以,但是HashMap的key只能有一个为null(补充一下:当get()方法返回null值时,可能是 HashMap中没有该键,也可能使该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断);

(d)迭代器:HashMap使用的迭代器Iterator是fail-fast迭代器,但是HashTable使用的迭代器enumerator不是fail-fast的。也因此,当HashMap结构发生改变(增加或者删除),会抛出ConcurrentModificationException,但是本身的remove()方法等不会抛出该异常。但是这并不是一个一定发生的行为,要看JVM。同理,这也是Enumeration和Iterator的区别;

       Fail-safe和iterator迭代器相关。如果某个集合对象创建了Iterator或者ListIterator,然后其它的线程试图“结构上”更改集合对象,将会抛出ConcurrentModificationException异常。但其它线程可以通过set()方法更改集合对象是允许的,因为这并没有从“结构上”更改集合。但是假如已经从结构上进行了更改,再调用set()方法,将会抛出IllegalArgumentException异常。

 (e)处理速度:主要由于线程同步影响,造成HashTable的速度要比HashMap要来的慢,也因此在单一线程内推荐使用HashMap,因为性能要好过Hashtable;

 (f)是否提供contains方法:HashMap把Hashtable的contains方法去掉了,改成containsValue和containsKey,因为contains方法容易让人引起误解。

Hashtable则保留了contains,containsValue和containsKey三个方法,其中contains和containsValue功能相同;

 (g)哈希值的使用不同:HashTable直接使用对象的hashCode,代码是这样的:

              int hash = key.hashCode();

              int index = (hash & 0x7FFFFFFF) % tab.length;

        而HashMap重新计算hash值,而且用与代替求模:

            int hash = hash(k);

            int i = indexFor(hash, table.length);

   (h)内部实现使用的数组初始化和扩容方式不同:Hashtable和HashMap它们两个内部实现方式的数组的初始大小和扩容的方式。HashTable中hash数组默认大小是11,增加的方式是 old*2+1;

HashMap中hash数组的默认大小是16,而且一定是2的指数。

    补充一下,为了实现HashMap的同步问题,可以通过Collections获取:Map Collections.synchronizedMap(Map m)    这个方法返回一个同步的Map,这个Map封装了底层的HashMap的所有方法,使得底层的HashMap即使是在多线程的环境中也是安全的。而且Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。

    然而,我们能用ConcurrentHashMap替代HashTable吗?这要取决于你的需求,一般情况下是可以的。因为 ConcurrentHashMap同步性能更好,因为它仅仅根据同步级别对map的一部分进行上锁。但是HashTable提供更强的线程安全性。

    要详细了解ConcurrentHashMap见《构建一个更好的 HashMap---ConcurrentHashMap》


3.HashSet

    上面说到它不允许集合中有重复的值,就是在将对象存储在HashSet之前,要先确保对象重写equals()和hashCode()方法,这样才能比较对象的值是否相等,以确保set中没有储存相等的对象。如果我们没有重写这两个方法,将会使用这个方法的默认实现。而且对于 HashSet 而言,它是基于 HashMap 实现的,HashSet 底层采用 HashMap 来保存所有元素。

    当然,我们平常并不需要那么繁琐,可以直接添加进去,public boolean add(Object o)方法用来在Set中添加元素,当元素值重复时则会立即返回false,如果成功添加的话会返回true。

    HashSet和HashMap的区别  *HashMap* —— *HashSet*:

    HashMap实现了Map接口 —— HashSet实现了Set接口

    HashMap储存键值对 —— HashSet仅仅存储对象(且无重复对象)

    使用put()方法将元素放入map中 —— HashSet使用add()方法将元素放入set中

    HashMap中使用键对象来计算hashcode值 —— HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false

    HashMap比较快,因为是使用唯一的键来获取对象 —— HashSet较HashMap来说比较慢


4.TreeSet和TreeMap

    TreeMap 和 TreeSet 是 Java Collection Framework 的两个重要成员,其中 TreeMap 是 Map 接口的常用实现类,而 TreeSet 是 Set 接口的常用实现类。虽然 TreeMap 和 TreeSet 实现的接口规范不同,但 TreeSet 底层是通过 TreeMap 来实现的(如同HashSet底层是是通过HashMap来实现的一样),因此二者的实现方式完全一样。而 TreeMap 的实现就是红黑树算法。

与HashSet完全类似,TreeSet里面绝大部分方法都市直接调用TreeMap方法来实现的。

    相同点:

    TreeMap和TreeSet都是有序的集合,也就是说他们存储的值都是拍好序的。

    TreeMap和TreeSet都是非同步集合,因此他们不能在多线程之间共享,不过可以使用方法Collections.synchroinzedMap()来实现同步

    运行速度都要比Hash集合慢,他们内部对元素的操作时间复杂度为O(logN),而HashMap/HashSet则为O(1)。

    不同点:

    最主要的区别就是TreeSet和TreeMap非别实现Set和Map接口

    TreeSet只存储一个对象,而TreeMap存储两个对象Key和Value(仅仅key对象有序)

    TreeSet中不能有重复对象,而TreeMap中可以存在


5.常见问题

“你知道HashMap的工作原理吗?” “你知道HashMap的get()方法的工作原理吗?”

    答:“HashMap是基于hashing的原理,我们使用put(key, value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。当我们给put()方法传递键和值时,我们先对键调用hashCode()方法,返回的hashCode用于找到bucket位置来储存Entry对象。”这里关键点在于指出,HashMap是在bucket中储存键对象和值对象,作为Map.Entry。这一点有助于理解获取对象的逻辑。如果你没有意识到这一点,或者错误的认为仅仅只在bucket中存储值的话,你将不会回答如何从HashMap中获取对象的逻辑。这个答案相当的正确,也显示出面试者确实知道hashing以及HashMap的工作原理。

“如果两个键的hashcode相同,你如何获取值对象?”

     面试者会回答:当我们调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,然后获取值对象。面试官提醒他如果有两个值对象储存在同一个bucket,他给出答案:将会遍历链表直到找到值对象。面试官会问因为你并没有值对象去比较,你是如何确定确定找到值对象的?除非面试者直到HashMap在链表中存储的是键值对,否则他们不可能回答出这一题。

    其中一些记得这个重要知识点的面试者会说,找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。完美的答案!

    许多情况下,面试者会在这个环节中出错,因为他们混淆了hashCode()和equals()方法。因为在此之前hashCode()屡屡出现,而equals()方法仅仅在获取值对象的时候才出现。一些优秀的开发者会指出使用不可变的、声明作final的对象,并且采用合适的equals()和hashCode()方法的话,将会减少碰撞的发生,提高效率。不可变性使得能够缓存不同键的hashcode,这将提高整个获取对象的速度,使用String,Interger这样的wrapper类作为键是非常好的选择。

    如果你认为到这里已经完结了,那么听到下面这个问题的时候,你会大吃一惊。

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

    除非你真正知道HashMap的工作原理,否则你将回答不出这道题。默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。如果你能够回答这道问题,下面的问题来了:

“你了解重新调整HashMap大小存在什么问题吗?”

    你可能回答不上来,这时面试官会提醒你当多线程的情况下,可能产生条件竞争(race condition)。

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

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

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

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

“我们可以使用CocurrentHashMap来代替Hashtable吗?”

    上面也简单说过了,这里再说明一下,毕竟这是另外一个很热门的面试题,因为ConcurrentHashMap越来越多人用了。我们知道Hashtable是synchronized的,但是ConcurrentHashMap同步性能更好,因为它仅仅根据同步级别对map的一部分进行上锁。ConcurrentHashMap当然可以代替HashTable,但是HashTable提供更强的线程安全性。看看查看《HashMap Vs ConcurrentHashMap》Hashtable和ConcurrentHashMap的区别。

    归纳一下,我们可以发现,这些问题主要围绕着探索这些知识点:

        hashing的概念

        HashMap中解决碰撞的方法

        equals()和hashCode()的应用,以及它们在HashMap中的重要性

        不可变对象的好处

        HashMap多线程的条件竞争

        重新调整HashMap的大小


    当然,这些知识还是很粗浅的,而且主要来源于多个参考博客,如有错误和缺点,敬请指正!

---------------------

参考博文:

作者:speedme  来源:CSDN   题目:java中的HashTable,HashMap和HashSet

链接:https://blog.csdn.net/SpeedMe/article/details/22485681

作者:微wx笑  来源:CSDN   题目:Java中Map与HashMap,Hashtable,HashSet的区别

链接:https://blog.csdn.net/testcs_dn/article/details/41925595

作者:speedme  来源:CSDN   题目:java集合类TreeMap和TreeSet

链接:https://blog.csdn.net/SpeedMe/article/details/22661671

---------------------

你可能感兴趣的:(Java HashMap-HashTable-HashSet等简介与常见问题)