【Java】集合类 - HashSet、HashMap 和 HashTable

Posted by 西维蜀黍 on 2019-03-13, Last Modified on 2023-02-28

集合框架

集合框架有自己的接口和实现,主要分为Set接口,List接口,Map接口和Queue接口。

它们有各自的特点:

  • Set接口里存放的对象是无序,不能重复的,集合中的对象不按特定的方式排序,只是简单地把对象加入集合中。
  • List接口是一个有序的集合,同时也是可以重复的,List关注的是索引,拥有一系列和索引相关的方法,查询速度快。因为往list集合里插入或删除数据时,会伴随着后面数据的移动,所有插入删除数据速度慢。
  • Map接口中存储的是键值对,键不能重复,值可以重复。根据键得到值,对map集合遍历时先得到键的set集合,对set集合进行遍历,得到相应的值。
  • Queue接口的工作原理是 FCFS 算法(First Come, First Serve)
有序否TreeMap 允许元素重复否
Collection
List
Set AbstractSet
Set HashSet
Set TreeSet 是(用二叉树排序)
Map AbstractMap 使用key-value来映射和存储数据,Key必须惟一,value可以重复
Map HashMap 使用key-value来映射和存储数据,Key必须惟一,value可以重复
Map TreeMap 是(用二叉树排序) 使用key-value来映射和存储数据,Key必须惟一,value可以重复

List接口

List 接口对Collection接口进行了简单的扩充,它的具体实现类常用的有ArrayList和LinkedList。

你可以将任何东西放到一个List容器中,并在需要时从中取出。

ArrayList从其命名中可以看出它是一种类似数组的形式进行存储,因此它的随机访问速度极快。

而LinkedList的内部实现是链表,它适合于在链表中间需要频繁进行插入和删除操作。

Set接口

Set接口也是 Collection的一种扩展,而与List不同的时,在Set中的对象元素不能重复,也就是说你不能把同样的东西两次放入同一个Set容器中。它的常用具体实现有HashSet和TreeSet类。

HashSet能快速定位一个元素,但是你放到HashSet中的对象需要实现hashCode()方法。

而TreeSet则将放入其中的元素按序存放,这就要求你放入其中的对象是可排序的。

Map接口

Map接口是一种把键对象(key)和值对象(value)进行关联的容器,而一个值对象又可以是一个Map,依次类推,这样就可形成一个多级映射。

对于键对象来说,像Set一样,一个Map容器中的键对象不允许重复,这是为了保持查找结果的一致性;如果有两个键对象一样,那你想得到那个键对象所对应的值对象时就有问题了,可能你得到的并不是你想的那个值对象,结果会造成混乱,所以键的唯一性很重要,也是符合集合的性质的。

当然在使用过程中,某个键所对应的值对象可能会发生变化,这时会按照最后一次修改的值对象与键对应。对于值对象则没有唯一性的要求。你可以将任意多个键都映射到一个值对象上,这不会发生任何问题(不过对你的使用却可能会造成不便,你不知道你得到的到底是那一个键所对应的值对象)。

Map有两种比较常用的实现: HashMap和TreeMap。

HashSet

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

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

HashSet底层采用的是HashMap进行实现。

Refer to https://swsmile.info/post/java-hashset/

HashMap

Refer to https:/swsmile.info/post/java-hashmap/

HashTable

Hashtable是继承自Dictionary抽象类,同时实现了Map<K,V>接口。

Hashtable同样是基于哈希表实现的,同样每个元素是一个键值对,其内部也是通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长。

HashSet和HashMap的区别

HashMap HashSet
HashMap实现了Map<K,V>接口 HashSet实现了Set<E>接口
HashMap储存键值对(HashMap以一组键值对(key-value)作为元素) HashSet仅仅存储对象(HashSet以对象作为元素)
使用put()方法将元素放入map中 使用add()方法将元素放入set中
HashMap中使用键对象来计算hashcode值 HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false
HashMap比较快,因为是使用唯一的键来获取对象 HashSet较HashMap来说比较慢

HashMap 和 Hashtable 的区别

HashMap 和 Hashtable 都实现了 Map<K,V> 接口,但决定用哪一个之前先要弄清楚它们之间的分别。主要的区别有:线程安全性(Thread safety),null值允许,以及速度。

HashMap 几乎可以等价于 Hashtable,除了

  1. HashMap 是非 synchronized 的
  2. 并可以接受 null(HashMap 可以接受为 null 的键值(key)和值(value),而 Hashtable 则不行)

互斥访问(mutual exclusion)

  • HashMap 是非 synchronized,而 Hashtable 是 synchronized。
  • 这意味着 Hashtable 是线程安全的,多个线程可以共享一个 Hashtable;而如果没有正确的同步的话,多个线程是不能共享 HashMap 的。Java 5 提供了 ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。
  • 由于Hashtable是线程安全的也是synchronized,所以在单线程环境下它比HashMap要慢。如果你不需要同步,只需要单一线程,那么使用HashMap性能要好过Hashtable。

是否允许 null 值

  • Hashtable中,key和value都不允许出现null值。
  • 在HashMap中,null可以作为键,这样的键只有一个;但是,可以有一个或多个键所对应的值为null。当get()方法返回null值时,既可以表示 HashMap中没有该键,也可以表示该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断。

迭代器

另一个区别是HashMap的迭代器(Iterator)是fail-fast迭代器,而由于历史原因,Hashtable还使用了Enumeration的方式 。

所以,当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。这条同样也是Enumeration和Iterator的区别。

哈希值的使用不同

HashTable直接使用对象的hashCode,如下:

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

而HashMap重新计算hash值。

Reference