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。