8月 3日

Java习题

1、 Vector、ArrayList、LinkedList 有什么区别?

​ 这三者都是集合框架中的 List 的实现类,具体功能也比较近似,但因为具体的设计区别,在行为、性能、线程安全等方面,又有很大不同。

​ Vector 是 Java 早期提供的线程安全的动态数组,如果不需要线程安全,并不建议选择,毕竟同步是有额外开销的。Vector 内部是使用对象数组来保存数据,可以根据需要,自动增加容量,当数组装满时,会创建新的数组,并拷贝原有数组数据到新数组中。

​ ArrayList 是应用更加广泛的动态数组实现,它本身不是线程安全的,所以性能要好很多。功能与 Vector 一致。

​ LinkedList 是基于链表实现的集合,它也不是线程安全的,它和 ArrayList 的区别是查找慢,但是增删快。

​ Vector 和 ArrayList 作为动态数组,其内部元素以数组形式顺序存储的,所以非常适合随机访问的场合。除了尾部插入和删除元素,往往性能会相对较差,比如我们在中间位置插入一个元素,需要移动后续所有元素。而 LinkedList 进行节点插入、删除却要高效得多,但是随机访问性能则要比动态数组慢。

2、Hashtable、HashMap、TreeMap有什么不同?

​ Hashtable、HashMap、TreeMap 都是最常见的一些 Map 实现,是以键值对的形式存储和操作数据的集合容器。

​ Hashtable 是早期 Java 类库提供的一个哈希表实现,是线程安全的,不支持 null 的键和值,由于线程安全导致的性能开销,所以一般很少使用。

​ HashMap 是应用更加广泛的哈希表实现,行为上大致上与 HashTable 一致,主要区别在于 HashMap 不是线程安全的,支持 null 键和值等。

​ TreeMap 则是基于红黑树的一种提供顺序访问的 Map。

(1) 元素特性

​ Hashtable 中的 key、value 都不能为 null,HashMap 中的 key、value 可以为 null,很显然只能有一个 key 为 null 的键值对数据,但是允许有多个值为 null 的键值对数据,TreeMap 如果没有实现 Comparator 接口,则 key 不可以为 null;当实现 Comparator 接口时,如果未对 null 情况进行判断,则 key 不可以为 null。

(2)顺序特性

​ Hashtable、HashMap 具有无序特性,TreeMap 实现了 SortMap 接口,能够对保存的记录根据键进行排序。所以一般需要排序的情况下是选择 TreeMap 来进行,默认为升序排序方式,可自定义实现 Comparator 接口实现排序方式。

(3)初始化与增长方式

​ 初始化时:Hashtable 在不指定容量的情况下的默认容量为 11,且不要求底层数组的容量一定要为 2 的整数次幂。

​ HashMap 默认容量为 16,且要求容量一定为 2 的整数次幂。

​ 扩容时:Hashtable 将容量变为原来的 2 倍加 1;HashMap 扩容将容量变为原来的 2 倍。

3、HashSet 是如何保证数据不可重复的?

​ HashSet 的底层其实就是 HashMap,只不过 HashSet 实现了 Set 接口并且把数据作为 K 值,而 V 值一直使用一个相同的虚值来保存,我们可以看到源码:

public boolean add(E e) {

  return map.put(e, PRESENT)==null;// 调用 HashMap 的 put 方法,PRESENT 是一个至始至终都相同的虚值

}

​ 由于 HashMap 的 K 值本身就不允许重复,并且在 HashMap 中如果 K/V 相同时,会用新的 V 覆盖掉旧的 V,然后返回旧的 V,那么在 HashSet 中执行这一句话始终会返回一个 false,导致插入失败,这样就保证了数据的不可重复性。

4、HashMap 数据结构

​ HashMap 底层的数据是数组,被称为哈希桶,每个桶存放的是链表,链表中的每个节点,就是 HashMap 中的每个元素。在 JDK 8 当链表长度大于等于 8 时,就会转成红黑树的数据结构,以提升查询和插入的效率。

5、HashMap 的扩容机制

​ HashMap 有两个重要的参数:容量(Capacity)和负载因子(LoadFactor)。

​ 容量(Capacity ):是指 HashMap 中桶的数量,默认的初始值为 16。

​ 负载因子(LoadFactor):也被称为装载因子,LoadFactor 是用来判定 HashMap 是否扩容的依据,默认值为 0.75f,装载因子的计算公式 = HashMap 存放的 KV 总和(size)/ Capacity。

​ 当 HashMap 中的元素越来越多的时候,碰撞的几率也就越来越高(因为数组的长度是固定的),所以为了提高查询的效率,就要对 HashMap 的数组进行扩容,数组扩容这个操作也会出现在 ArrayList 中,所以这是一个通用的操作,很多人对它的性能表示过怀疑,不过想想我们的“均摊”原理,就释然了,而在 HashMap 数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是 resize。

​ 那么 HashMap 什么时候进行扩容呢?当 HashMap 中的元素个数超过 LoadFactor 时,就会进行数组扩容,LoadFactor 的默认值为 0.75,也就是说,默认情况下,数组大小为 16,那么当 HashMap 中元素个数超过 160*0.75=12 的时候,就把数组的大小扩展为 2*16=32,即扩大一倍。

6、HashMap 在 JDK 7 和 JDK 8 中有哪些不同?

​ HashMap 在 JDK 7 和 JDK 8 的主要区别如下。

​ 存储结构:JDK 7 使用的是数组 + 链表;JDK 8 使用的是数组 + 链表 + 红黑树。

​ 存放数据的规则:JDK 7 无冲突时,存放数组;冲突时,存放链表;JDK 8 在没有冲突的情况下直接存放数组,有冲突时,当链表长度小于 8 时,存放在单链表结构中,当链表长度大于 8 时,树化并存放至红黑树的数据结构中。

​ 插入数据方式:JDK 7 使用的是头插法(先将原位置的数据移到后 1 位,再插入数据到该位置);JDK 8 使用的是尾插法(直接插入到链表尾部/红黑树)。

7、JDK 1.7 使用 HashMap 可能会遇到什么问题?如何避免?

​ HashMap 在并发场景中可能出现死循环的问题,这是因为 HashMap 在扩容的时候会对链表进行一次倒序处理,假设两个线程同时执行扩容操作,第一个线程正在执行 B→A 的时候,第二个线程又执行了 A→B ,这个时候就会出现 B→A→B 的问题,造成死循环。

​ 解决的方法:升级 JDK 版本,在 JDK 8 之后扩容不会再进行倒序,因此死循环的问题得到了极大的改善,但这不是终极的方案,因为 HashMap 本来就不是用在多线程版本下的,如果是多线程可使用 ConcurrentHashMap 替代 HashMap。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

YMurmansk

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

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

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

打赏作者

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

抵扣说明:

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

余额充值