Java集合框架之Set,Map
Posted morning~
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java集合框架之Set,Map相关的知识,希望对你有一定的参考价值。
大家好,我是Morning,在CSDN写文,分享一些Java基础知识,一些自己认为在学习过程中比较重要的东西,致力于帮助初学者入门,希望可以帮助你进步。感兴趣的欢迎关注博主,和博主一起学习Java知识。大家还可以去专栏查看之前的文章,希望未来能和大家共同探讨技术。
书接上回,这 一篇,一起学习Set,Map接口,和它的实现类。
Set接口
set接口等同于Collection接口,不过其方法的行为有更严谨的定义。set的add方法不允许增加重复的元素。要适当地定义set的equals方法:只要俩个set包含同样的元素就认为它们是相同的,而不要求这些元素有相同的顺序。hashCode方法的定义要保证包含相同元素的俩个set会得到相同的散列码。
——Java核心技术 卷一
public interface Set<E> extends Collection<E>
//一些方法
set不允许包含相同的元素,如果调用 add 方法来添加俩个相同的元素,那么在添加第二个元素时就会返回 false。这个接口有俩个比较常用的实现类:HashSet,TreeSet。HashSet是一个没有重复元素的一个无序集合,而TreeSet是一个有序集。
HashSet
HashSet 使用数组和链表来实现散列表,不同 hashcode 的值存放在不同数组下标的位置,相同 hashcode 的值存放在相同数组下标的链表上的不同位置上。
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
//一些方法
HashSet类中有四个构造方法:
public HashSet() //构造一个空的散列集
public HashSet(Collection<? extends E> c) //构造一个散列集,并将集合中的所有元素添加到这个散列集中
public HashSet(int initialCapacity, float loadFactor) //构造一个有指定容量和装填因子的空散列集
public HashSet(int initialCapacity) //构造一个指定容量的空散列集
现在让我们来看一下add方法的源码:
public boolean add(E e)
return map.put(e, PRESENT)==null;
可以看到这里面是调用了 map.put() 方法,map 在HashSet中的声明如下:
private transient HashMap<E,Object> map;
由此可见HashSet和HashMap有很紧密的联系。
HashSet的底层是用一个HashMap来实现的。HashSet再添加时如何扩容问题在下文HashMap中再详细介绍。
散列表
Java中,散列表用链表,数组实现。也就可以这么理解,在数组不同索引位置上放的是一个链表,要想查找表中对象的位置,要先计算它的散列码,然后与数组长度取余,所得到的结果就是保存这个元素的索引。在保存对象时,通过这个计算方式得到了索引位置,如果这个位置没有其他元素,那就将元素直接插入到该位置就好了,如果这个位置已经有其他元素了,那么需要将这个新的对象与已有的对象进行比较,查看这个对象是否已经存在。如果不存在就在链表的末尾存储这个对象。
TreeSet
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable
//一些方法
树集是一个有序集合,可以以任意顺序将元素插入集合中。
TreeSet排序是用一个红黑树完成的,每次将一个元素添加到树中时,都会将其放置到正确的排序位置上。由于树集时有序的,所以将一个元素添加到树中要比添加到散列表中慢,所以如果不需要数据是有序的,就没必要为了排序而影响性能。
它也有四个构造函数:
public TreeSet() //构造一个空的树集,排序方式是自然排序
public TreeSet(Comparator<? super E> comparator) //构造一个空树集,指定比较器,就是排序方式
public TreeSet(Collection<? extends E> c) //构造一个树集,并将集合中的所有元素添加到这个树集中
public TreeSet(SortedSet<E> s) //构造一个树集,并增加一个集合或者有序集中的所有元素,如果是有序集的话,要使用同样的顺序。
上面提到的自然排序是用元素的 compareTo() 方法来比较元素的大小关系(比如你存入v,b,a,就会输出a,b,v),然后将集合元素按照升序排列。
Map接口
集是一个集合,允许你快速的查找现有的元素。但是要查找一个元素,需要有所要查找的那个元素的准确副本。这不是一种常见的查找方式。通常,我们知道某些关键信息,希望查找与之关联的元素。映射(map)数据结构就是为此设计的。映射用来存放键 / 值对。如果提供了键,就可以查找到值。
——Java核心技术
public interface Map<K,V>
int size(); //返回映射中的元素数
V get(Object key); //返回指定键对应的值
V remove(Object key); //从映射中删除指定键对应的元素。
Java为映射提供了俩个通用的实现:HashMap和TreeMap
HashMap
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
实现了Map接口,存储的内容是键值对(key-value)映射,将键进行散列,散列或比较函数只应用于键,与键相关的值不进行散列或比较。
HashMap底层的结构是,散列表(数组和链表)和红黑树
我用反射的方式输出了这个map的容量,可以看到如果使用无参的构造方法,那么map的容量默认为16。我们也可以通过传参的方式,来指定它的容量,值的一提的是,你指定的容量,一定要为 2 的幂次方,且要小于 int 范围内最大的 2 的幂次方数(1073741824)。
//指定容量
HashMap<String,String> map = new HashMap<String, String>(8);
这个构造方法还是调用了下面的这个构造方法,只是把默认的负载因子值传进去了:
public HashMap(int initialCapacity)
this(initialCapacity, DEFAULT_LOAD_FACTOR);
//调用这个构造方法
public HashMap(int initialCapacity, float loadFactor)
//判断是否小于0,抛出异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//判断是否大于最大值
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//判断负载因子的合法性
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor; //更新负载因子的值
//判断需要扩容的临界值
this.threshold = tableSizeFor(initialCapacity);
put方法,直接看源码:
public V put(K key, V value)
return putVal(hash(key), key, value, false, true);
调用put方法后,会根据键值来计算一个值(位置),然后调用putVal来存放元素。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict)
Node<K,V>[] tab; Node<K,V> p; int n, i;
//hash表为空或者长度为0的话,创建hash表
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;//resize()HashMap扩容的方法
if ((p = tab[i = (n - 1) & hash]) == null)
//如果位置上没有元素,就加进去,成为链表的头结点
tab[i] = newNode(hash, key, value, null);
else
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果这个值的类型为树结构的话,就直接添加到树中,不会判断是否超过8
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else
//binCount链表的长度
for (int binCount = 0; ; ++binCount)
if ((e = p.next) == null)
//如果链表不为空,就往下一个节点添加
p.next = newNode(hash, key, value, null);
//如果这个链表的长度大于8,就会转为红黑树存储
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
//如果该键已经有映射关系的话,用这次的值覆盖掉之前的值
if (e != null) // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
++modCount;
//如果里面的元素超过 threshold 的话就扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
通过代码中的注释,相信你已经初步了解了 put 方法,总的来说就是添加时首先是用 k 通过哈希函数计算位置,位置上如果没有元素添加在链表的头结点,如果有插入到链表的下一个节点,当链表的长度为大于等于8时,转为红黑树存储,如果该键已经有映射关系的话,用这次的值覆盖掉之前的值,最后判断要不要扩容。如果要扩容,会扩容为原来容量的2倍。这样效率会高一些,也可以减少哈希冲突。
负载因子
上文中我们提到了负载因子,那么负载因子是什么呢?
负载因子也叫装填因子,是一个0.0~1.0之间的数,确定散列表填充的百分比,当大于这个百分比时,散列表进行再散列。在map中,也就是说,当put的元素数量超过一定值的时候,就会扩容,这个值就是负载因子*容量。
HashMap中的负载因子默认是0.75:
static final float DEFAULT_LOAD_FACTOR = 0.75f;
就是说如果map中的元素超过容量的3/4就要进行扩容。
那么为什么这里要默认为0.75呢?这了也是规定了一个相对合理的值,如果为 1 的话效率太低,满了之后才扩容;如果为0.5的话,浪费空间。所以选了一个居中的值。
TreeMap
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
它没有实现Map接口是因为AbstractMap实现了Map接口,TreeMap是一个能比较元素大小的Map集合,可以对 key 值进行大小排序。其中,可以使用自然顺序,也可以使用集合中自定义的比较器来进行排序。底层是红黑树结构。
TreeMap中运用到的红黑树结构,是数据结构的一种,相关知识太多,大家感兴趣的话可以在网上查阅资料(博主对树结构的理解不是很透彻😢😢)。
好了,本次的分享到这里就结束了。博主会在日后给大家分享其他的知识,和大家一起探讨,有兴趣的可以关注博主。文中有什么不当的地方,欢迎大家在评论区指出,大家一起探讨、学习。🤞🤞🤞
参考书籍:Java核心技术 卷一(第11版)
以上是关于Java集合框架之Set,Map的主要内容,如果未能解决你的问题,请参考以下文章