Java集合

1.谈谈List,Set,Map的区别?

List:存储的元素有序,可重复

Set:存储的元素无序,不可重复

Map:采用key-value键值对来存储元素,key无序,不可重复;value无序,可重复

2.ArrayList和LinkedList的区别?

ArrayList底层是数组,Linkedlist底层是双向链表

1)线程安全的角度:两者都是不同步的,所以线程不安全

2)插入和删除的角度:①ArrayList:头插法和删除时间复杂度为O(n),因为把元素放入第一个位置时后面的元素索引都要向后移动;尾插法和删除直接时间复杂度为O(1);插入或删除第i个位置时间复杂度近似O(n-i)。②LinkedList:头插法和删除时间复杂度为O(1);尾插法和删除时间复杂度为O(1),插入或删除第i个位置时间复杂度近似O(n),因此插入和删除LinkedList的效率更高。

3)随机访问的角度:ArrayList能够通过索引快速定位元素位置,进行随机访问,而LinkedList则需要从头遍历。

4)内存空间的角度: ArrayList 的空间浪费主要体现在在 list 列表的结尾会预留⼀定的容量空间,⽽ LinkedList 的空间花费则体现在它的每⼀个元素都需要消耗⽐ ArrayList 更多的空间 (因为要存放直接后继和直接前驱以及数据)

3.ArrayList和Vector的区别?

0)底层都使用Object[ ] 存储实现

1)ArrayList是线程不安全的,Vector从源码中可以看出有sychronized修饰,所以是线程安全的,因此ArrayList比Vector的效率也更高

2)两者都采用线性连续存储,ArrayList扩容时增加50%,Vector增加一倍

ArrayList与Vector区别 - 明天,你好啊 - 博客园 (cnblogs.com)

4.HashMap和HashTable的区别

1)线程安全的角度,HashMap线程不安全(如果要线程安全可以使用CurrentHashMap),HashTable里面的方法元素都用sychronized修饰,所以线程安全。

2)效率的角度,因为HashMap线程不安全,HashTable线程安全,所以HashMap比HashTable效率高。

3)初始化容量大小和每次扩容大小,①创建时如果没给定初始量,则HashTable默认大小为11,扩容之后为原来的2n+1,HashMap默认为16,扩容后为原来的2倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而HashMap 会将其扩充为 2 的幂次方大小

4)底层结构,JDK1.8 以后的 HashMap 在解决哈希冲突时有了᫾⼤的变化,当链表⻓度 ⼤于阈值(默认为 8)(将链表转换成红⿊树前会判断,如果当前数组的⻓度⼩于 64,那么 会选择先进⾏数组扩容,⽽不是转换为红⿊树)时,将链表转化为红⿊树,以减少搜索时 间。Hashtable 没有这样的机制。

下面这个方法保证了 HashMap 总是使用2的幂作为哈希表的大小。

/**
 * Returns a power of two size for the given target capacity.
 */
 static final int tableSizeFor(int cap) {
 int n = cap - 1;
 n |= n >>> 1;
 n |= n >>> 2;
 n |= n >>> 4;
 n |= n >>> 8;
 n |= n >>> 16;
 return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n +
1;
 }

5.HashMap和HashSet的区别?

6.HashMap的底层实现

继承关系

HshMap基于Map接口实现,使用key-value键值对存储数据

HashMap是不同步的,意味着线程不安全

1.8以前,HashMap通过数组+链表实现,1.8以后通过数组+链表+红黑树实现,具体的加入数据过程如下,当加入数据之前,首先会通过hashcode()计算hash值,比如取余运算hash%length,这样就计算出索引位置,如果该位置没有元素,那么直接加入,如果有元素,则说明hash值相同,这时候就要使用equals()方法,如果相同则直接覆盖value,如果不同则添加到链表中。当链表长度大于8,但数组容量小于64时,会选择对数组扩容;当链表长度大于8且数组容量大于64时链表转化为红黑树。

补充:这样做的目的是因为数组比较小,尽量避开红黑树结构,这种情况下变为红黑树结构,反而会降低效率,因为红黑树需要逬行左旋,右旋,变色这些操作来保持平衡。同时数组长度小于64时,搜索时间相对要快些。所以结上所述为了提高性能和减少搜索时间,底层阈值大于8并且数组长度大于64时,链表才转换为红黑树,具体可以参考 treeifyBin() 方法

HashMap 集合类的成员

1) serialVersionUID(序列化版本号)

private static final long serialVersionUID = 362498820763181265L;

 2) DEFAULT_INITIAL_CAPACITY

集合的初始化容量(必须是 2 的 n 次幂)

// 默认的初始容量是16	1 << 4 相当于 1*2的4次方
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

问题:为什么必须是 2 的 n 次幂?如果输入值不是 2 的幂比如 10 会怎么样?

根据上述讲解我们已经知道,当向 HashMap 中添加一个元素的时候,需要根据 key 的 hash 值,去确定其在数组中的具体位置。HashMap 为了存取高效,减少碰撞,就是要尽量把数据分配均匀,每个链表长度大致相同,这个实现的关键就在把数据存到哪个链表中的算法。

这个算法实际就是取模,hash % length,计算机中直接求余效率不如位移运算。所以源码中做了优化,使用 hash & (length - 1),而实际上 hash % length 等于 hash & ( length - 1) 的前提是 length 是 2 的 n 次幂。
 

怎么做到使得传入的初始化值变成2的n次幂?

/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
  int n = cap - 1;
  n |= n >>> 1;
  n |= n >>> 2;
  n |= n >>> 4;
  n |= n >>> 8;
  n |= n >>> 16;
  return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

上述方法当在实例化 HashMap 实例时,如果给定了 initialCapacity,由于 HashMap 的 capacity 必须都是 2 的幂,因此这个方法用于找到大于等于 initialCapacity 的最小的 2 的幂。也就是说不是2的n次幂时会找到大于给定值的最小2的次幂,比如你传10,通过tableSizeFor会变成16.

3) DEFAULT_LOAD_FACTOR

默认的负载因子(默认值 0.75)

static final float DEFAULT_LOAD_FACTOR = 0.75f;

loadFactor 是用来衡量 HashMap 满的程度,表示HashMap的疏密程度,影响 hash 操作到同一个数组位置的概率,计算 HashMap 的实时负载因子的方法为:size/capacity,而不是占用桶的数量去除以 capacity。capacity 是桶的数量,也就是 table 的长度 length。

loadFactor 太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor 的默认值为 0.75f 是官方给出的一个比较好的临界值。

当 HashMap 里面容纳的元素已经达到 HashMap 数组长度的 75% 时,表示 HashMap 太挤了,需要扩容,而扩容这个过程涉及到 rehash、复制数据等操作,非常消耗性能。所以开发中尽量减少扩容的次数,可以通过创建 HashMap 集合对象时指定初始容量来尽量避免
为什么负载因子设置为0.75,初始化临界值是12?

例如:负载因子是0.4。 那么16*0.4--->6 如果数组中满6个空间就扩容会造成数组利用率太低了。
	 负载因子是0.9。 那么16*0.9--->14 那么这样就会导致链表有点多了,导致查找元素效率低。

所以既兼顾数组利用率又考虑链表不要太多,经过大量测试 0.75 是最佳方案。

threshold 计算公式:capacity(数组长度默认16) * loadFactor(负载因子默认0.75)。

这个值是当前已占用数组长度的最大值。当 Size >= threshold 的时候,那么就要考虑对数组的 resize(扩容),也就是说,这个的意思就是 衡量数组是否需要扩增的一个标准。 扩容后的 HashMap 容量是之前容量的两倍。

4) MAXIMUM_CAPACITY

集合最大容量

static final int MAXIMUM_CAPACITY = 1 << 30; // 2的30次幂

5) TREEIFY_THRESHOLD

// 当桶(bucket)上的结点数大于这个值时会转为红黑树
static final int TREEIFY_THRESHOLD = 8;

 问题:为什么 Map 桶中结点个数超过 8 才转为红黑树?

在 HashMap 中有一段注释说明:

Because TreeNodes are about twice the size of regular nodes, we use them only when bins
contain enough nodes to warrant use (see TREEIFY_THRESHOLD). And when they become too
small (due to removal or resizing) they are converted back to plain bins.  In usages with
well-distributed user hashCodes, tree bins are rarely used.  Ideally, under random hashCodes, 
the frequency of nodes in bins follows a Poisson distribution 
(http://en.wikipedia.org/wiki/Poisson_distribution) 
with a parameter of about 0.5 on average for the default resizing
threshold of 0.75, although with a large variance because of resizing granularity. Ignoring variance, 
the expected occurrences of list size k are (exp(-0.5) * pow(0.5, k) / factorial(k)). The first values are:

翻译:因为树结点的大小大约是普通结点的两倍,所以我们只在箱子包含足够的结点时才使用树结点(参见TREEIFY_THRESHOLD)。
当它们变得太小(由于删除或调整大小)时,就会被转换回普通的桶。在使用分布良好的用户 hashCode 时,很少使用树箱。
理想情况下,在随机哈希码下,箱子中结点的频率服从泊松分布
(http://en.wikipedia.org/wiki/Poisson_distribution) ,默认调整阈值为0.75,平均参数约为0.5,尽管由 
于调整粒度的差异很大。忽略方差,列表大小k的预朗出现次数是(exp(-0.5) * pow(0.5, k) / factorial(k))。 
第一个值是:

0:    0.60653066
1:    0.30326533
2:    0.07581633
3:    0.01263606
4:    0.00157952
5:    0.00015795
6:    0.00001316
7:    0.00000094
8:    0.00000006
more: less than 1 in ten million

TreeNodes 占用空间是普通 Nodes 的两倍,所以只有当 bin 包含足够多的结点时才会转成 TreeNodes,而是否足够多就是由 TREEIFY_THRESH〇LD 的值决定的。当 bin 中结点数变少时,又会转成普通的 bin。并且我们查看源码的时候发现,链表长度达到 8 就转成红黑树,当长度降到 6 就转成普通 bin。

这样就解释了为什么不是一开始就将其转换为 TreeNodes,而是需要一定结点数才转为 TreeNodes,说白了就是权衡空间和时间。

这段内容还说到:当 hashCode 离散性很好的时候,树型 bin 用到的概率非常小,因为数据均匀分布在每个 bin 中,几乎不会有 bin 中链表长度会达到阈值。但是在随机 hashCode 下,离散性可能会变差,然而 jdk 又不能阻止用户实现这种不好的 hash 算法,因此就可能导致不均匀的数据分布。不理想情况下随机 hashCode 算法下所有 bin 中结点的分布频率会遵循泊松分布,我们可以看到,一个 bin 中链表长度达到 8 个元素的槪率为 0.00000006,几乎是不可能事件。所以,之所以选择 8,不是随便決定的,而是裉据概率统计决定的。

7.ConcurrentHashMap 和 Hashtable 的区别

两者的区别主要体现在实现线程安全不同

1)底层数据结构:jdk1.7ConcurrentHashMap采用的是分段锁(segment)+数组+链表,jdk1.8使用的是Node数组+链表+红黑树;Hashtable采用的是jdk1.7时的HashMap的结构数组+链表。

2)实现线程安全的方式(重要): ① 在 JDK1.7 的时候, ConcurrentHashMap (分段锁) 对整个桶数组进⾏了分割分段( Segment ),每⼀把锁只锁容器其中⼀部分数据,多线程访问 容器⾥不同数据段的数据,就不会存在锁竞争,提高并发访问率。 到了 JDK1.8 的时候已经 摒弃了 Segment 的概念,而是直接用Node 数组+链表+红黑树的数据结构来实现,并发 控制使用 synchronized 和 CAS 来操作。② Hashtable (同⼀把 锁) :使用synchronized 来保证线程安全,效率非常低下。当⼀个线程访问同步⽅法时,其 他线程也访问同步⽅法,可能会进入阻塞或轮询状态,如使用put 添加元素,另⼀个线程不 能使用put 添加元素,也不能使用get,竞争会越来越激烈效率越低。

示意图:

Hashtable

JDK1.7 的 ConcurrentHashMap:

8. 比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同

HashSet 是 Set 接口的主要实现类 , HashSet 的底层是 HashMap ,线程不安全的,可以存 储 null 值; LinkedHashSet 是 HashSet 的子类,能够按照添加的顺序遍历; TreeSet 底层使用红黑树,能够按照添加元素的顺序进⾏遍历,排序的方式有自然排序和定制排序。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

#HashMap#

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值