目录
为什么 ArrayList 的 elementData 加上 transient 修饰?
在 Queue 中 poll()和 remove()有什么区别?
为什么 ArrayList 的 elementData 加上 transient 修饰?
ArrayList 中的数组定义如下:
private transient Object[] elementData;
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
// 类的实现部分
}
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out array length
s.writeInt(elementData.length);
// Write out all elements in the proper order.
for (int i = 0; i < size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
List 和 Set 的区别?
List | Set | |
特点 | 有序容器,元素可以重复,可以插入多个null元素,元素都有索引 | 无序容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。 |
常用实现类 | ArrayList、LinkedList 和 Vector。 | HashSet、LinkedHashSet 以及 TreeSet。 |
遍历方式 | 支持for循环,也可以用迭代器。 | 只能用迭代器。 |
检索元素效率 | 高 | 低 |
删除和插入效率 | 低,因为会引起其他元素位置改变 | 高,插入和删除不会引起元素位置改变 |
Set接口
说一下 HashSet 的实现原理?
HashSet是如何保证数据不可重复的?
当我们向HashSet中添加元素时,判断元素是否已经存在的依据不仅仅是比较hash值,还需要结合equals方法进行比较。这是因为HashSet的add方法实质上是调用了HashMap的put方法。
在HashMap中,key是唯一的。从源码中可以看出,HashSet添加的元素实际上作为了HashMap的key。当HashMap中两个数据的Key或Value相同时,会使用新的Value覆盖旧的Value,然后返回旧的Value。这种处理方式保证了数据的不可重复性。注意,HashMap在比较key是否相等时,是先比较hashcode,再用equals方法进行比较。
以下是HashSet 部分源码:
private static final Object PRESENT = new Object();
private transient HashMap<E, Object> map;
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) {
// 调用HashMap的put方法, PRESENT是一个至始至终都相同的虚值
return map.put(e, PRESENT) == null;
}
hashCode()与equals()的相关规定:
==与equals的区别
== | equals() |
判断两个变量或实例是否指向同一个内存空间 | 判断两个变量或实例所指向的内存空间的值是否相同 |
对内存地址进行比较 | 对字符串的内容进行比较 |
引用是否相同 | 值是否相同 |
HashSet与HashMap的区别
HashMa
p
|
HashSet
|
实现了Map接口
|
实现Set接口
|
存储键值对
|
仅存储对象
|
调用put()向map中添加元素
|
调用add()方法向Set中添加元素
|
HashMap 使用键(Key)计算Hashcode
|
HashSet 使用成员对象来计算 hashcode 值,对于两个对象来说 hashcode 可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回 false
|
相对于 HashSet 较快,因为它是使用唯一的键获取对象
|
较 HashMap 来说效率较低
|
Queue
BlockingQueue是什么?
Java.util.concurrent.BlockingQueue是一个队列。在进行检索或移除一个元素的时候,它会等待队列变为非空;当在添加一个元素时,它会等待队列中的可用空间。BlockingQueue接口是Java集合框架的一部分,主要用于实现生产者-消费者模式(23种设计模式之一)。我们不需要担心等待生产者有可用的空间,或消费者有可用的对象,因为它都在BlockingQueue的实现类中被处理了。Java提供了集中BlockingQueue的实现,ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue、SynchronousQueue等。
在 Queue 中 poll()和 remove()有什么区别?
相同点:都是返回第一个元素,并在队列中删除返回的对象。
不同点:如果没有元素 poll()会返回 null,而 remove()会直接抛出 NoSuchElementException 异常。
示例代码:
import java.util.LinkedList;
import java.util.Queue;
public class Main {
public static void main(String[] args) {
Queue<String> queue = new LinkedList<>();
queue.offer("string"); // 添加元素
System.out.println(queue.poll()); // 按照队列的元素顺序返回元素,并在队列中删除该元素
System.out.println(queue.remove()); // 返回队列中的第一个元素,并在队列中删除该元素
System.out.println(queue.size()); // 返回队列中的元素个数
}
}
Map接口
说一下 HashMap 的实现原理?
HashMap在JDK1.7和JDK1.8中有哪些不同?
JDK1.8之前
JDK1.8之后
jdk1.8中HashMap数据结构
JDK1.7 与 JDK1.8 比较?
不同
|
JDK 1.7
|
JDK 1.8
|
存储结构
|
数组 + 链表
|
数组 + 链表 + 红黑树
|
初始化方式 | 单独函数:inflateTable() | 直接集成到了扩容函数resize()中 |
hash值的计算方式 |
扰动处理
= 9次扰动 = 4次位运算 + 5次异或运算
|
扰动处理
= 2次扰动 = 1次位运算 + 1次异或运算
|
存放数据的规则
|
无冲突时,存放数组;冲突时,存放链表
|
无冲突时,存放数组;
冲突 & 链表长度< 8:存放单链表;冲突 & 链表长度 > 8:树化并存放红黑树
|
插入数据 方式
|
头插法
(先将原位置的数据移到后1 位,再插入数据到该位置)
|
尾插法
(直接插入到链表尾部/红黑树)
|
扩容后存
储位置的 计算方式
|
全部按照原来方法进行计算
(即 hashCode ->> 扰动函数 ->> (h&length-1))
|
按照扩容后的规律计算
(即扩容后的位置=原位置 or 原位置 + 旧容量)
|
HashMap的put方法的具体流程?
在上文中的hash函数中,通过对哈希值的高16位和低16位进行异或运算,可以使得哈希值的分布更加均匀,从而减少碰撞的发生。这是因为,异或运算可以使得哈希值的每一位都发生改变,从而使得不同的键更有可能产生不同的哈希值,减少了在哈希表中碰撞的可能性。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 实现Map.put和相关方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K, V>[] tab;
Node<K, V> p;
int n, i;
// 步骤 ①:tab为空则创建
// table未初始化或者长度为0,进行扩容
if ((tab = table) == null || (n = tab.length) == 0) {
n = (tab = resize()).length;
}
// 步骤②:计算index,并对null做处理
// (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
if ((p = tab[i = (n - 1) & hash]) == null) {
tab[i] = newNode(hash, key, value, null);
} else {
Node<K, V> e;
K k;
// 步骤③:节点key存在,直接覆盖value
// 比较桶中第一个元素(数组中的结点)的hash值相等,key相等
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) {
// 将第一个元素赋值给e,用e来记录
e = p;
} else {
// 步骤④:判断该链为红黑树
// hash值不相等,即key不相等;为红黑树结点
if (p instanceof TreeNode) {
// 放入树中
e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
} else {
// 步骤⑤:该链为链表
// 为链表结点
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;
}
}
// 判断链表中结点的key值与插入的元素的key值是否相等
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
break;
}
// 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
p = e;
}
}
}
// 步骤⑥:判断当前的key已经存在的情况下,再来一个相同的hash值、key值时,返回新来的value这个值
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null) {
// 用新值替换旧值
e.value = value;
// 访问后回调
afterNodeAccess(e);
}
return oldValue;
}
}
// 结构性修改
++modCount;
// 步骤⑦:超过最大容量就扩容
// 实际大小大于阈值则扩容
if (++size > threshold) {
resize();
}
// 插入后回调
afterNodeInsertion(evict);
return null;
}