赞
踩
参考文章:
Java集合常见面试题总结
从上图可以知道,Java 集合框架主要包括两种类型的容器:
Collection
:用于存放单一元素;Map
:用于存储键/值对映射。注意:上图中只列举了主要的继承派生关系,并没有列举完全,如果想要深入了解,可以自行去查看源码。
(1)Collection 接口下面的集合:
List
Queue
Set
(2)Map 接口下面的集合:
(1)Collection 是 JDK 中集合层次结构中的最根本的接口,定义了集合类的基本方法。Collection 接口在 Java 类库中有很多具体的实现,其意义是为各种具体的集合提供了最大化的统一操作方式。
(2)Collections 是一个包装类,是 Collection 集合框架的工具类。它包含有各种有关集合操作的静态多态方法,常用的有:
Collections.sort(List<T> list)
:对列表进行升序排序。Collections.reverse(List<T> list)
:反转列表中的元素顺序。Collections.shuffle(List<T> list)
:随机打乱列表中的元素顺序。Collections.binarySearch(List<? extends Comparable<? super T>> list, T key)
:使用二分搜索算法查找指定元素在列表中的位置。Collections.frequency(Collection<?> c, Object o)
:返回指定集合中指定元素的出现次数。Collections.max(Collection<? extends T> coll)
:返回集合中的最大元素。Collections.min(Collection<? extends T> coll)
:返回集合中的最小元素。Collections.addAll(Collection<? super T> c, T... elements)
:将多个元素添加到集合中。Collections.disjoint(Collection<?> c1, Collection<?> c2)
:检查两个集合是否没有任何相同的元素。Collections.replaceAll(List<T> list, T oldVal, T newVal)
:将列表中所有的旧元素替换为新元素。(1)List:存储的元素是有序的、可重复的。
(2)Set:存储的元素是无序的、不可重复的,LinkedHashSet 按照插入排序,SortedSet 可排序,HashSet 无序。
(3)Queue:按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。
(4)Map:存储键值对 (key-value),其中要求 key 无序且唯一,而 value 则不要求有序,允许重复。
(1)当我们需要保存一组类型相同的数据时,可以选用数组,但是数组存在着一定的弊端,例如当数组声明后,其长度以及存储的数据类型也就固定了,并且存储的数据的特点单一。因此为了提高数据存储的灵活性、数据特点的多样性,Java 中使用了集合。
(2)在选用集合时,我们应主要根据需求和集合特点来选用:
我们常⽤的 Arraylist , LinkedList , Hashmap , HashSet , TreeSet , TreeMap , PriorityQueue 都不是线程安全的。解决办法很简单,可以使用线程安全的集合来代替。如果你要使用线程安全的集合的话, java.util.concurrent
包中提供了很多并发容器供我们使用:
ConcurrentHashMap
:可以看作是线程安全的 HashMap;CopyOnWriteArrayList
:可以看作是线程安全的 ArrayList ,在读多写少的场合性能非常好,远远好于 Vector;ConcurrentLinkedQueue
:高效的并发队列,使用链表实现。可以看做⼀个线程安全的 LinkedList ,这是⼀个非阻塞队列。BlockingQueue
:这是⼀个接口,JDK 内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合用于作为数据共享的通道。ConcurrentSkipListMap
:跳表的实现,这是⼀个 Map ,使用跳表的数据结构进行快速查找。(1)首先来看看 Iterator 与 Iterable 的英文含义:
这两个概念之间有一个包含与被包含的关系,如果一个对象是迭代器,那么这个对象肯定是可迭代的;但是反过来,如果一个对象是可迭代的,那么这个对象不一定是迭代器。
(2)Iterable 是一个接口,它定义了返回一个 Iterator 的方法,使得实现了 Iterable 接口的类可以被 foreach 循环迭代,即提供一种简单的迭代方式。其源码如下所示:
package java.lang; import java.util.Iterator; import java.util.Objects; import java.util.Spliterator; import java.util.Spliterators; import java.util.function.Consumer; public interface Iterable<T> { Iterator<T> iterator(); default void forEach(Consumer<? super T> action) { Objects.requireNonNull(action); for (T t : this) { action.accept(t); } } default Spliterator<T> spliterator() { return Spliterators.spliteratorUnknownSize(iterator(), 0); } }
(3)Iterator 也是一个接口,它定义了遍历集合元素的方法,并且更加灵活,它允许在迭代过程中删除元素。Iterator 接口的源码如下:
package java.util; import java.util.function.Consumer; public interface Iterator<E> { //判断集合中是否还有元素 boolean hasNext(); //获得集合中的下⼀个元素 E next(); //移除 iterator.next() 方法最后访问的元素 default void remove() { throw new UnsupportedOperationException("remove"); } //对集合中剩余的元素进行操作,直到元素完毕或者抛出异常 default void forEachRemaining(Consumer<? super E> action) { Objects.requireNonNull(action); while (hasNext()) action.accept(next()); } }
(4)测试 Iterator 与 Iterable:
public class IterableVsIteratorExample { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("apple"); list.add("banana"); list.add("orange"); //使用 Iterable 方式迭代 for (String fruit : list) { System.out.println(fruit); } //使用 Iterator 方式迭代 Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String fruit = iterator.next(); System.out.println(fruit); iterator.remove(); //删除元素 } System.out.println(list); //输出 [] } }
在上面的例子中,我们首先使用了 Iterable 的方式迭代 List 中的元素,即使用 foreach 循环遍历。接下来,我们使用 Iterator 迭代器遍历 List 中的元素,并删除了每个元素。需要注意的是,当我们在迭代过程中使用 Iterator 的 remove 方法删除元素时,List 中的元素也会被删除。最后,我们输出 List 中的元素,发现它已经变成了空列表。
有关迭代器模式的具体知识可以参考 Java 设计模式——迭代器模式这篇文章。
(1)在 Java 中,Comparable 接口和 Comparator 接口都用于比较对象。它们之间的主要区别是 Comparable 是在对象自身内部实现的比较方法,而 Comparator 则是在外部单独实现比较方法。
(2)实现 Comparable 接口的类可以直接进行排序,而不需要使用其他的比较器。这是因为 Comparable 接口定义了一个 compareTo 方法,该方法指定了如何比较两个对象。该方法的返回值为负数、零或正数,表示当前对象小于、等于或大于另一个对象。例如,以下是一个实现 Comparable 接口的 Student 类的例子:
package java.lang;
import java.util.*;
public interface Comparable<T> {
public int compareTo(T o);
}
public class Student implements Comparable<Student> { private String name; private int age; public Student(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } @Override public int compareTo(Student o) { return this.age - o.age; } public static void main(String[] args) { Student stu1 = new Student("A", 20); Student stu2 = new Student("B", 18); System.out.println(stu1.compareTo(stu2) > 0); // true } }
在上面的例子中,Student 类实现了 Comparable< Student > 接口,它的 compareTo 方法比较了两个 Student 对象的年龄,以此来决定它们之间的大小关系。
(3)与此相反,Comparator 接口定义了一个 compare 方法,该方法指定了如何比较两个对象。实现 Comparator 接口的类可以用于在不改变对象自身的情况下对对象进行排序。例如,以下是一个实现 Comparator 接口的 StudentAgeComparator 类的例子:
public class StudentAgeComparator implements Comparator<Student> {
@Override
public int compare(Student s1, Student s2) {
return s1.getAge() - s2.getAge();
}
}
在上面的例子中,StudentAgeComparator 类实现了 Comparator 接口,它的 compare 方法比较了两个 Student 对象的年龄,以此来决定它们之间的大小关系。此外,Comparator 也可以用于对集合进行自定义排序,举例如下:
public static void main(String[] args) { List<Student> list = new ArrayList<>(); list.add(new Student("A", 20)); list.add(new Student("D", 24)); list.add(new Student("B", 18)); list.add(new Student("D", 19)); list.sort(new Comparator<Student>() { @Override public int compare(Student stu1, Student stu2) { //先按照名字升序排序,再按照年龄降序排序 if (stu1.getName().compareTo(stu2.getName()) != 0) { return stu1.getName().compareTo(stu2.getName()); } else { return stu1.getAge() - stu2.getAge(); } } }); for (Student stu : list) { System.out.println(stu.getName() + " " + stu.getAge()); } }
最终的输出结果如下:
A 20
B 18
D 19
D 24
注:在 Java 8 中,上述代码的自定义排序部分可用 Lambda 表达式代替:
list.sort((stu1, stu2) -> {
//先按照名字升序排序,再按照年龄降序排序
if (stu1.getName().compareTo(stu2.getName()) != 0) {
return stu1.getName().compareTo(stu2.getName());
} else {
return stu1.getAge() - stu2.getAge();
}
});
(1)无序性不等于随机性 ,无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加 ,而是根据数据的哈希值决定的。
(2)不可重复性是指添加的元素按照 equals()判断时 ,返回 false,需要同时重写 equals() 方法和 hashCode() 方法。
Java 集合框架中有两种常见的容器修改检测机制:快速失败机制 “fail-fast” 和安全失败机制 “fail-safe”:
(1)快速失败机制 “fail-fast” 这种机制通过在迭代器遍历集合时,如果检测到集合在遍历过程中被修改(如添加、删除元素),会立即抛出 ConcurrentModificationException
异常,从而防止可能的并发修改造成数据不一致。快速失败机制是一种保守策略,它假定集合操作多半是非并发的,如若发生并发修改,则认为程序有错误,因此立即报错。例如:
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
System.out.println(element);
list.remove(element); // 修改集合,会触发快速失败机制
}
上述代码在遍历集合时尝试删除元素,由于并发修改了集合,会导致 ConcurrentModificationException
异常被抛出:
a
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at com.exam.Main.main(Main.java:67)
正确的做法是用迭代器的 remove()
方法,便可正常运行,具体代码如下所示:
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
System.out.println(element);
//list.remove(element); //修改集合,会触发快速失败机制
iterator.remove();
}
System.out.println(list.size()); // 0
(2)造成这种情况的原因是什么?通过上述代码可以发现两次调用的 remove() 方法不同,一个带参数据,一个不带参数。经过查看 ArrayList 源码,找到了抛出异常的代码:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
//...
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
从上面代码中可以看到如果 modCount
和 expectedModCount
这两个变量不相等就会抛出 ConcurrentModificationException
异常。
protected transient int modCount = 0; //在 AbstractList 中定义的变量
int expectedModCount = modCount; //在 ArrayList 中的内部类 Itr 中定义的变量
从上面代码可以看到, modCount 初始值为 0,而 expectedModCount 初始值等于 modCount 。也就是说在遍历的时候直接调用集合的 remove() 方法会导致 modCount 不等于 expectedModCount 进而抛出 ConcurrentModificationException 异常,而使用迭代器的 remove() 方法则不会出现这种问题。具体源码分析如下:
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { //... private class Itr implements Iterator<E> { //... public void remove() { if (lastRet < 0) throw new IllegalStateException(); checkForComodification(); try { ArrayList.this.remove(lastRet); cursor = lastRet; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } } } }
从上面代码可以看到 expectedModCount = modCount
这一行代码保证了 expectedModCount 和 modCount 是相等的,进而保证了在增强 for 循环中修改集合内容不会抛出 ConcurrentModificationException
异常。
安全失败机制 “fail-safe”:与快速失败机制不同,安全失败机制允许在迭代期间对集合进行修改操作,并且不会抛出异常。这是通过在迭代时复制原始集合的数据来实现的,迭代器操作的是被复制后的数据副本。因此,安全失败机制可以实现并发修改而不会触发异常,但可能会导致迭代结果不一致。例如:
List<String> list = new CopyOnWriteArrayList<>();
list.add("a");
list.add("b");
list.add("c");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
System.out.println(element);
list.remove(element); // 安全失败机制允许修改集合
}
上述代码使用 CopyOnWriteArrayList
类来进行迭代和修改操作,由于该类在迭代时使用了一份原始集合的快照,因此可以安全地删除元素而不会引发异常。
总结来说,快速失败机制 “fail-fast” 更适用于单线程环境,能够及早地检测到并发修改错误;而安全失败机制 “fail-safe” 更适用于多线程环境,能够保证遍历过程的稳定性。开发者在使用集合时需要根据具体的场景选择适合的机制。
相关知识点:
Java 设计模式——迭代器模式
Array 和 ArrayList 是 Java 中两种常见的数据结构,它们之间有一些区别。
int[] arr = new int[5];
。而 ArrayList 的声明和初始化比较简单,例如 ArrayList<Integer> list = new ArrayList<>();
。(1)底层数据结构
(2)扩容
有关 ArrayList 扩容的具体细节,可查看本节的 2.3。
(3)是否支持快速随机访问
RandomAccess
接口且底层是通过 Object 数组实现的,故支持快速随机访问。快速随机访问就是通过元素的下标快速获取元素,即对应于 ArrayList 中的
get(int index)
方法,其时间复杂度为 O(1)。
(4)插入和删除操作
add(E e)
方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话 (add(int index, E element)
)时间复杂度就为 O(n - i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的 n - i 个元素都要执行向后位/向前移一位的操作。add(E e)
、addFirst(E e)
、addLast(E e)
、removeFirst()
、removeLast()
),时间复杂度为 O(1),如果是要在指定位置 i 插入和删除元素的话 (add(int index, E element)
,remove(Object o)
), 时间复杂度为 O(n) ,因为需要先移动到指定位置再插入。(5)遍历操作
ArrayList 和 LinkedList 常见的遍历方式有 3 种:for(结合 get(int index)
方法)、foreach、iterator。
① 在 List 遍历方式中,foreach 的底层就是由迭代器实现的,只不过为了方便书写,做了简单的封装。
② 在遍历 LinkedList 时,尽量不要使用get(int index)
方法,因为每次都要从链表头或者表尾去遍历,时间复杂度较高。
具体细节可参考 ArrayList 和 LinkedList 的三种遍历方式 这篇文章。
(6)内存空间占用
注意:在项目中一般不会使用 LinkedList,因为大部分需用到 LinkedList 的场景都可以使用 ArrayList 来代替,并且性能通常会更好!
(1)RandomAccess
接口是 Java 标准库中的一个接口,用于标识实现了随机访问能力的集合 (Collection) 类。它本身不包含任何方法或常量,只是一个标记接口。RandomAccess
接口的作用在于提供了一种方式来表示集合是否支持快速随机访问元素的能力。如果一个集合类实现了 RandomAccess
接口,那么可以认为该集合支持高效的随机访问,即可以通过索引快速访问集合中的元素。
package java.util;
public interface RandomAccess {
}
(2)在实际编程中,当我们需要对集合进行随机访问操作时,可以使用 instanceof
关键字来判断集合是否实现了 RandomAccess
接口,从而选择合适的访问方式。如果实现了 RandomAccess
接口,可以直接通过索引访问元素;如果没有实现 RandomAccess
接口,使用迭代器逐个遍历访问可能更高效。
public class Collections {
//...
public static <T> int binarySearch(List<? extends T> list, T key, Comparator<? super T> c) {
if (c==null)
return binarySearch((List<? extends Comparable<? super T>>) list, key);
if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
return Collections.indexedBinarySearch(list, key, c);
else
return Collections.iteratorBinarySearch(list, key, c);
}
}
(3)需要注意的是,并不是所有的集合类都实现了 RandomAccess
接口。通常,数组和一些特定的集合类(如 ArrayList
)实现了该接口,而链表类(如 LinkedList
)则没有实现该接口。
(4)总的来说,RandomAccess
接口的作用是提供一种标记方式来表示集合是否支持高效的随机访问,以便我们在编程时可以根据需要选择合适的访问方式,提高程序的执行效率。
(1)联系
(2)区别
参考 Java 基础——ArrayList 的扩容机制这篇文章。
(1)Queue 是单端队列,只能从一端插入元素,另一端删除元素,实现上一般遵循先进先出 (FIFO) 规则。Queue 扩展了 Collection 的接口,根据失败后处理方式的不同可以分为以下两类方法:
Queue 接口 | 抛出异常 | 返回特殊值 |
---|---|---|
在队尾插入元素 | add(E e) | offer(E e) |
删除队首元素 | remove() | poll() |
查询队首元素 | element() | peek() |
(2)Deque 是双端队列,在队列的两端均可以插入或删除元素。Deque 同时还是 Queue 的子接口,增加了在队首和队尾进行插入和删除的方法,同样根据失败后处理方式的不同分为两类:
Deque 接口 | 抛出异常 | 返回布尔值 |
---|---|---|
在队首插入元素 | addFirst(E e) | offerFirst(E e)) |
在队尾插入元素 | addLast(E e) | offerLast(E e) |
删除队首元素 | removeFirst() | pollFirst() |
删除队尾元素 | removeLast() | pollLast() |
查询队首元素 | getFirst() | peekFirst() |
查询队尾元素 | getLast() | peekLast() |
(3)Deque 还提供有 push()
和 pop()
等其他方法,可用于模拟栈。
Deque<Integer> stack = new ArrayDeque<>();
ArrayDeque 和 LinkedList 均实现了 Deque 接口,即都具有队列的功能,但两者具有以下这些区别:
从性能的角度上考虑,选用 ArrayDeque 来实现队列更好。此外,ArrayDeque 也可以用于实现栈。
(1)HashSet、LinkedHashSet 和 TreeSet 都是 Java 中实现了 Set 接口的集合类,它们都有一些共同点,比如元素不允许重复,没有下标索引,可以进行遍历等。不同之处在于它们内部的实现机制不同,导致它们在某些方面具有特殊的性质。
(2)HashSet、LinkedHashSet 和 TreeSet 的主要区别在于以下方面:
PriorityQueue 中元素出队顺序与优先级相关,即总是优先级最高的元素先出队。这里列举其相关的一些要点:
具体参考Java 基础——List 与数组、Map 相互转换这篇文章。
(1)线程是否安全
HashMap 是非线程安全的,Hashtable 是线程安全的(Hashtable 中的方法基本都使用 synchronized 修饰,同时在效率上不如 HashMap)。如果要保证线程安全,可以使用 ConcurrentHashMap,因为目前 Hashtable 使用的频率较低。
(2)对 null 键和 null 值的支持
HashMap | Hashtable | |
---|---|---|
null 键 | 支持,但只允许有一个 | 不支持 |
null 值 | 支持 | 不支持 |
注意:如果 HashTable 中存在 null 键或 null 值,会抛出 NullPointerException。
(3)初始容量和扩容操作
HashMap | Hashtable | |
---|---|---|
初始容量 | 16,也可自己指定 | 11,也可自己指定 |
扩容操作 | 每次扩充容量变为原来的 2 倍 | 每次扩充容量变为 2n+1,n 为上一次的容量 |
(4)计算 hash 值的方式
① HashMap 计算 hash 值的方式为先调用 hashCode() 计算出来一个 hash 值,再将 hash 与 hash 右移 16 位后的值进行异或操作,从而得到最终的 hash 值,具体的代码实现如下:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
② Hashtable 则通过计算 key 的 hashCode() 来得到最终的 hash 值。
(5)解决 hash 冲突的机制
具体细节可参考这篇文章。
(1)HashSet 底层就是基于 HashMap 实现的,具体见下面 HashSet 的几个构造函数:
public HashSet() {
map = new HashMap<>();
}
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
(2)但 HashMap 和 HashSet 之间也存在着一些区别:
HashMap | HashSet | |
---|---|---|
实现接口 | 实现了 Map 接口 | 实现了 Set 接口 |
添加方式 | 调用 put() 向 map 中添加键值对 | 调用 add() 向 set 中添加元素 |
计算 hashcode | 通过键 (key) 来计算 | 使用成员对象来计算 hashcode 值,对于两个对象来说 hashcode 可能相同,所以 equals() 方法用来判断对象的相等性 |
(3)此外,在 HashSet 的实现中,存在一个私有的静态常量 PRESENT
,它的类型是 Object,并且被设置为 new Object()
。这个对象作为 HashMap 中键的占位值,并通过共享对象节省内存空间,具体分析如下:
PRESENT
的目的是为了节省内存空间。在 HashSet 中,每个元素实际上是作为 HashMap 的键存储的,而 HashMap 中的值是占位值 PRESENT
。由于所有的元素在 HashMap 中共享同一个占位值对象,通过共享对象,可以在内存上更高效地表示元素的存在。PRESENT
是一个私有的静态常量,它只在 HashSet 的实现内部使用,确保了其他类无法修改这个对象的引用值,从而保持了 HashSet 的一致性和正确性。public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable {
static final long serialVersionUID = -5024744406713321676L;
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
//...
}
(4)思考:为什么 HashSet 中 map 的 value 不将 null 作为值?
PRESENT
占位值而不是 null
主要是为了减少内存占用。如果使用 null
作为占位值,那么每个元素在 HashMap 的键值对中都需要额外存储一个占位值 null
,而这会增加内存的使用量。null
作为占位值可能会引起错误,因为 null
可能是元素的有效值之一。而使用特殊的占位值 PRESENT
可以避免这种混淆,因为 PRESENT
通常是一个编程约定,用于表示“键的存在”。PRESENT
占位值而不是 null
是为了节省内存空间,并且避免与元素有效值混淆。(1)TreeMap 和HashMap 都继承自 AbstractMap ,但是 TreeMap 还实现了 NavigableMap 接口和 SortedMap 接口:
NavigableMap
接口让 TreeMap 有了对集合中的元素进行搜索的能力:
SortedMap
接口让 TreeMap 有了对集合中的元素根据键排序的能力:
TreeMap 中的键值对默认是按 key 的升序排序,不过我们也可以指定排序的比较器。示例代码如下:
public class Person { private Integer age; public Person(Integer age) { this.age = age; } public Integer getAge() { return age; } public static void main(String[] args) { //重写排序规则 TreeMap<Person, String> treeMap = new TreeMap<>((person1, person2) -> { int num = person1.getAge() - person2.getAge(); return Integer.compare(num, 0); }); treeMap.put(new Person(3), "person1"); treeMap.put(new Person(18), "person2"); treeMap.put(new Person(35), "person3"); treeMap.put(new Person(16), "person4"); treeMap.forEach((key, value) -> System.out.println(value)); } }
输出结果如下:
person1
person4
person2
person3
(2)总之,相比于 HashMap 来说,TreeMap 主要多了对集合中的元素根据键排序的能力以及对集合内元素进行搜索的能力。
参考Java 基础——HashMap 遍历方式这篇文章。
参考Java 基础——HashMap 底层数据结构与源码分析这篇文章。
参考文章:
HashMap 为什么线程不安全?
(1)HashMap 是一种常用的 Java 集合类,用于存储键值对数据。HashMap 是一种基于哈希表实现的数据结构,其内部实现包括了数组和链表或红黑树。由于哈希表本身的特性,HashMap 存在线程不安全的问题。
(2)具体来说,当多个线程同时对 HashMap 进行读写操作时,可能会发生以下情况:
ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。
相关文章:
关于 HashMap 和 ConcurrentHashMap 理解
ConcurrentHashMap 源码分析
ConcurrentHashMap 的 key 和 value 不能为 null,主要是为了保证线程安全性。
在 ConcurrentHashMap 中,为了支持并发读写操作,需要对数据进行分段,每个段(Segment)都拥有自己的锁。如果允许插入 null 值,那么在获取值时可能无法判断对应的 key 或 value 是否存在,这将给并发读写操作带来很大的风险,可能导致死锁或者其他异常情况。
因此,对于 ConcurrentHashMap,为了保证线程安全性,禁止插入 null 值。而对于 HashMap,虽然允许插入 null 值,但需要注意空指针异常和冲突问题。
(1)ConCurrentHashMap 的 key 和 value 不能为 null 的具体分析如下:
NullPointerException
;(2)而在 HashMap 中,由于没有并发读写操作的保护措施,允许插入 null 值也不会影响线程安全性。但是,在使用 null 值作为 key 时,需要注意它可能会与已有的 key 冲突,导致覆盖原有值。而对于 null 值作为 value,HashMap 并不会出现冲突,但在获取值时也需要进行非空判断,以避免空指针异常。并且在单线程场景下的 HashMap 中,可以使用 containsKey(key)
来判断到底是否存在这个 key。
(1)当把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()
方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让加入操作成功。
(2)在 JDK1.8 中,HashSet 的 add()
方法只是简单的调用了 HashMap 的 put()
方法,并且判断了一下返回值以确保是否有重复元素。直接看一下 HashSet 中的源码:
//@return true if this set did not already contain the specified element
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
而在 HashMap 的 putVal()
方法中也能看到如下说明:
// Returns : previous value, or null if none
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
...
}
也就是说,在 JDK1.8 中,实际上无论 HashSet 中是否已经存在了某元素,HashSet都会直接插入,只是会在 add()
方法的返回值处告诉我们插入前是否存在相同元素。
(1)LinkedHashMap 是 Java 中提供的一种特殊的哈希表 (HashMap) 实现,它可以维护键值对的插入顺序或者访问顺序,并且可以通过迭代器按照插入顺序或访问顺序访问元素。
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V> {
//...
}
(2)LinkedHashMap 的底层实现原理和 HashMap 非常相似,都是基于哈希表实现的。不同的是,LinkedHashMap 在内部维护了一个双向链表,用于维护键值对的插入顺序或访问顺序。每个节点都包含了前驱节点和后继节点的引用。
(3)在 LinkedHashMap 中,每个桶 (bucket) 都是一个指向双向链表头节点的引用。当插入一个新的键值对时,它会被插入到链表的尾部,并且在哈希表中的相应位置指向链表的新的尾节点。这样就保证了键值对的插入顺序。
(4)当使用迭代器按照访问顺序访问 LinkedHashMap 中的元素时,每次访问一个元素,该元素就会被移到链表的尾部,这样就可以保证最近访问的元素总是在链表的尾部,最早访问的元素总是在链表的头部。这样,就可以方便地实现 LRU(Least Recently Used,最近最少使用)缓存淘汰策略。
(5)总的来说,LinkedHashMap 是一种可以同时保证插入顺序和访问顺序的哈希表实现,它通过维护一个双向链表来实现这一功能。
设计 LRU 的相关算法题:LeetCode_数据结构设计_中等_146.LRU 缓存
(1)TreeSet 是 Java 中的一种集合类型,它实现了 Set 接口,并使用红黑树作为底层数据结构。TreeSet 具有以下特点:
Comparable
接口或通过构造函数提供 Comparator
对象,以便进行元素的比较和排序。(2)需要注意的是,由于 TreeSet 是有序的,因此其性能通常比 HashSet 稍慢,特别是在添加和删除元素时。但是,对于需要有序性的场景,TreeSet 是一个非常有用的集合类型。
(1)链地址法是哈希表中解决哈希冲突的一种方式,Java 8 对链地址法进行了优化,使得其在处理哈希冲突时更加高效。
(2)Java 8 在实现链地址法时,使用了红黑树来替代链表。当一个哈希桶中的链表长度超过了一定的阈值(默认为 8)时,Java 8 将会使用红黑树来存储这些元素,而不是继续使用链表。这种方式可以使得哈希表在处理哈希冲突时更加高效,因为红黑树的查找、插入和删除操作的时间复杂度都是 O(log n),而链表的时间复杂度则是 O(n)。并且这个阈值是可以通过 JVM 参数来进行调整的,开发人员可以根据实际应用场景来设置合适的阈值。
(3)总之,Java 8 对链地址法进行了优化,使得哈希表在处理哈希冲突时更加高效,提高了程序的性能。
(1)相同点:poll() 和 remove() 都是从队列中取出一个元素;
(2)不同点:poll() 在获取元素失败的时候会返回空,但 remove() 失败的时候会抛出异常。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。