上图
实现了Deque的类,既可以作为栈使用,也可以作为队列使用。
HashMap和LinekHashMap的主要区别是HashMap没有按照插入顺序来保存,但是LinekHashMap可以额外使用一个双向链表来保存插入的顺序。
ArrryList基于动态数组实现,LinkedList基于双端链表实现,LinkedList由于实现了DEQUE接口,因此可以作为队列和栈使用。
ArrayDeque由于是基于动态数组实现,不用像LinkedList一样需要保存结点的前后信息,所以效率会更高。
ConcurrentHashMap是线程安全的,而HashMap是非线程安全的。
java集合主要可以分为Map和Collection
-
Map:Map是用于存储 键值对(key-value pairs) 的数据结构,每个键唯一地映射到一个值。常见的实现有HashMap、TreeMap、LinkedHashMap等。 -
Collection:Collection是一个更通用的接口,代表一组 元素(elements) 。Collection是 Java 集合框架的根接口,主要包含三个子接口:List、Set和Queue。
1、Map
HashMap
HashMap 的特点
- 无序:
HashMap不保证键值对的顺序,它的存储是基于哈希算法的,因此插入元素的顺序和遍历时的顺序可能会不一样。 - 允许
null键和null值:HashMap允许一个null键和多个null值。这是它与Hashtable的一个重要区别,因为Hashtable不允许null键或值。 - 键的唯一性:
HashMap中的键是唯一的,不能重复。如果插入了一个已经存在的键,新的值会替换旧的值。 - 效率高:由于
HashMap基于哈希表实现,因此它的查找、插入和删除操作在平均情况下都能达到 O(1) 的时间复杂度(在哈希冲突不严重的情况下)。 - 线程不安全:
HashMap不是线程安全的。如果多个线程同时访问HashMap并且至少有一个线程在修改HashMap的结构,则可能导致数据不一致。如果需要线程安全的Map,可以使用Collections.synchronizedMap()或使用ConcurrentHashMap。
HashMap 的内部实现
HashMap 的底层结构是 数组 + 链表 + 红黑树。
- 数组:
HashMap使用一个数组来存储键值对,每个数组位置被称为一个 "桶"(bucket)。每个键通过哈希函数映射到一个特定的桶中。 - 链表:当两个或多个键的哈希值相同时,它们会被存储在同一个桶中,这时这些键值对会形成一个链表。
- 红黑树:为了提高查找效率,当一个桶中的链表长度超过一定阈值(默认是 8)时,这个链表会被转换成一棵红黑树,从而将查找时间复杂度从 O(n) 降低到 O(log n)。
HashMap 的主要方法
put(K key, V value):将键值对插入到HashMap中,如果键已经存在,则替换旧值。get(Object key):通过键来获取对应的值。remove(Object key):通过键删除对应的键值对。containsKey(Object key):检查HashMap中是否包含指定的键。containsValue(Object value):检查HashMap中是否包含指定的值。keySet():返回HashMap中所有键的集合。values():返回HashMap中所有值的集合。entrySet():返回HashMap中所有键值对的集合。
import java.util.HashMap;
public class HashMapExample {
public static void main(String[] args) {
// 创建一个 HashMap 实例
HashMap<String, Integer> map = new HashMap<>();
// 向 HashMap 中添加键值对
map.put("Apple", 1);
map.put("Banana", 2);
map.put("Orange", 3);
// 通过键获取值
System.out.println("Value for Apple: " + map.get("Apple"));
// 迭代所有键值对
for (HashMap.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + " = " + entry.getValue());
}
// 删除一个键值对
map.remove("Banana");
// 查看是否包含某个键或值
System.out.println("Contains 'Orange': " + map.containsKey("Orange"));
System.out.println("Contains value 2: " + map.containsValue(2));
}
}
LinkedHashMap
HashMap 已经非常强大了,但它是无序的。如果我们需要一个有序的 Map,就要用到 LinkedHashMap。LinkedHashMap 是 HashMap 的子类,它使用链表来记录插入/访问元素的顺序。
LinkedHashMap 可以看作是 HashMap + LinkedList 的合体,它使用了哈希表来存储数据,又用了双向链表来维持顺序。
TreeMap
TreeMap 实现了 SortedMap 接口,可以自动将键按照自然顺序或指定的比较器顺序排序,并保证其元素的顺序。内部使用红黑树来实现键的排序和查找。
2、 list
主要是基于动态数组的ArrayList 和基于链表实现的LinkedList。
ArrayList 和 LinkedList 是 Java 集合框架中的两个常用类,它们分别实现了 List 接口,但它们的底层实现和操作特性有很大的区别。
1. 底层实现
ArrayList: 基于 动态数组 实现。它在底层维护一个可变大小的数组。当数组容量不足时,会创建一个新的更大容量的数组,并将旧数组中的元素复制到新的数组中。LinkedList: 基于 双向链表 实现。每个节点包含一个数据域和两个指针域,分别指向前一个和后一个节点。插入和删除操作通过改变指针的指向来完成。
2. 存储方式
ArrayList: 连续的内存空间。所有元素存储在一个连续的数组中,因此可以通过索引直接访问元素,访问效率高。LinkedList: 不连续的内存空间。元素分散存储,通过指针连接,访问时需要从头遍历到指定位置,因此随机访问效率低。
3. 访问速度
ArrayList: 随机访问速度快。由于是基于数组实现的,访问任意元素的时间复杂度是 O(1) 。LinkedList: 随机访问速度慢。需要遍历链表找到指定位置的元素,时间复杂度是 O(n) 。
4. 插入和删除
ArrayList: 插入和删除的效率较低,特别是在 中间位置 插入或删除元素时。因为需要移动后续的元素来保持数组的连续性,时间复杂度是 O(n) 。LinkedList: 插入和删除的效率较高,尤其是 在链表的头部或中间 进行操作时,时间复杂度是 O(1) 。只需调整节点的指针即可,不需要移动其他元素。
5. 内存占用
ArrayList: 由于是数组结构,内存利用率高,没有指针开销。但当数组需要扩容时,会多占用一部分内存以预留空间,导致可能会浪费一些内存。LinkedList: 每个元素都需要存储两个额外的指针(前驱和后继),因此相对于ArrayList,LinkedList的内存开销较大。
6. 扩容机制
ArrayList: 当元素数量超过数组容量时,ArrayList会自动扩容,通常是将容量扩展为原来的 1.5 倍。扩容需要进行数组拷贝,性能开销较大。LinkedList: 不需要扩容。LinkedList是基于链表实现的,动态地分配内存,因此不会出现数组容量不足的问题。
7. 线程安全
ArrayList和LinkedList都是非线程安全的。如果在多线程环境中使用,需要手动进行同步或者使用Collections.synchronizedList()。
8. 使用场景
ArrayList: 适用于频繁 查询、随机访问 的场景,例如要通过索引快速获取某个元素的场景。LinkedList: 适用于频繁 插入、删除 操作的场景,尤其是在中间或两端插入、删除元素的情况。
9. 双端队列与栈支持
LinkedList: 实现了Deque接口,因此可以作为双端队列使用,也可以作为栈使用。LinkedList具有push()、pop()、offer()、poll()等方法,可以高效实现栈和队列操作。ArrayList: 虽然可以使用add()和remove()模拟栈或队列的操作,但它并不直接实现Deque接口,操作并不高效。
List 的实现类还有一个 Vector,是一个元老级的类,比 ArrayList 出现得更早。ArrayList 和 Vector 非常相似,只不过 Vector 是线程安全的,像 get、set、add 这些方法都加了 synchronized 关键字,就导致执行效率会比较低,所以现在已经很少用了。
Stack 是 Vector 的一个子类,本质上也是由动态数组实现的,只不过还实现了先进后出的功能(在 get、set、add 方法的基础上追加了 pop「返回并移除栈顶的元素」、peek「只返回栈顶元素」等方法),所以叫栈。但是Stack也是线程安全的,由于添加了synchronized 关键字,所以执行效率会比较低。
3、Set
Set 的特点是存取无序,不可以存放重复的元素,不可以用下标对元素进行操作,和 List 有很多不同。
在 Java 集合框架中,Set 是一个接口,它继承自 Collection 接口,用于存储 不重复 的元素集合。Set 的实现类有多个,它们各自有不同的特性和应用场景。
1. Set 接口
-
特点:
- 集合中的元素不能重复。
- 不保证存取元素的顺序。
- 常用方法继承自
Collection接口,例如add(),remove(),contains(),size()等。
2. Set 接口的主要实现类
HashSet
-
基于
HashMap实现,它使用哈希表来存储元素,因此具有良好的查找、插入和删除性能。 -
特点:
- 无序: 不能保证元素的存取顺序。
- 允许存储
null值,但只能存一个null。 - 插入、删除、查找的时间复杂度为 O(1) 。
-
使用场景: 适合需要快速查找、删除,并且不关心元素顺序的场景。
Set<String> hashSet = new HashSet<>();
hashSet.add("Apple");
hashSet.add("Banana");
hashSet.add("Orange");
LinkedHashSet
-
基于
HashSet和LinkedList实现,继承自HashSet,同时维护了元素的插入顺序。 -
特点:
- 有序: 元素按照插入顺序排列。
- 性能与
HashSet类似,但由于维护了插入顺序,插入、删除、查找的时间复杂度略高,但仍然接近 O(1) 。
-
使用场景: 适合既需要保证元素不重复,又希望保留插入顺序的场景。
Set<String> linkedHashSet = new LinkedHashSet<>();
linkedHashSet.add("Apple");
linkedHashSet.add("Banana");
linkedHashSet.add("Orange");
TreeSet
-
基于
TreeMap实现,底层采用 红黑树 数据结构,能够按照元素的自然顺序或指定的比较器顺序对元素进行排序。 -
特点:
- 有序: 元素按自然顺序(如数字从小到大)或者指定的比较器顺序排序。
- 插入、删除、查找的时间复杂度为 O(log n) 。
-
使用场景: 适合需要对元素进行排序和去重的场景。
Set<Integer> treeSet = new TreeSet<>();
treeSet.add(3);
treeSet.add(1);
treeSet.add(2);
EnumSet
-
专门为枚举类型设计的
Set实现,使用位向量实现,效率极高。 -
特点:
- 只能存储枚举类型的元素。
- 与其他
Set的实现相比,EnumSet的性能非常好,因为它使用的是位运算。
-
使用场景: 适合需要存储枚举类型元素的场景。
enum Day { MONDAY, TUESDAY, WEDNESDAY }
Set<Day> enumSet = EnumSet.of(Day.MONDAY, Day.WEDNESDAY);
3. Set 接口的选择依据
HashSet: 适合需要高效查找和不需要顺序的场景。LinkedHashSet: 适合既需要不重复元素又需要保留插入顺序的场景。TreeSet: 适合需要对元素排序的场景(按自然顺序或指定顺序)。EnumSet: 适合存储枚举类型元素,性能极佳。
4、Queue
Queue 接口是 Java 集合框架中的一部分,表示一个 先进先出 (FIFO) 的集合。它继承自 Collection 接口,主要用于实现队列数据结构。
Deque 接口
-
定义:
Deque是一个线性集合,支持在 两端 添加、移除元素。与Queue不同,它允许元素从队列的两端入队和出队。 -
常见操作:
- 队列操作: 在头部添加、移除元素,或者在尾部添加、移除元素。
- 栈操作: 可以使用
push()方法将元素压入栈顶,pop()方法弹出栈顶元素。
Deque 接口的常用实现类
ArrayDeque
-
基于动态数组实现的双端队列,类似于
ArrayList。 -
特点:
- 无固定容量限制,底层数组会根据需要自动扩展。
- 既可以用作 队列,也可以用作 栈,常用于替代
Stack类。 - 效率较高,它的性能优于
LinkedList,因为不涉及额外的链表节点操作。
-
使用场景: 适合需要高效插入、删除操作的双端队列场景,或者作为栈使用。
Deque<String> arrayDeque = new ArrayDeque<>();
arrayDeque.addFirst("First");
arrayDeque.addLast("Last");
System.out.println(arrayDeque); // 输出 [First, Last]
LinkedList
-
基于链表实现的双端队列,它实现了
Deque接口。 -
特点:
- 底层采用双向链表实现,因此插入和删除操作的时间复杂度为 O(1) 。
- 可以作为 队列、双端队列 和 栈 使用。
- 相比
ArrayDeque,LinkedList在内存使用上会更多,因为每个节点需要存储前驱和后继指针。
-
使用场景: 适合频繁在两端进行插入和删除操作,或者需要存储大量数据且不频繁随机访问的场景。
Deque<String> linkedListDeque = new LinkedList<>();
linkedListDeque.addFirst("First");
linkedListDeque.addLast("Last");
System.out.println(linkedListDeque); // 输出 [First, Last]
作为栈使用时的常用方法
push(E e): 将元素压入栈顶(相当于addFirst()或offerFirst())。pop(): 弹出栈顶元素(相当于removeFirst()或pollFirst())。peek(): 查看栈顶元素但不移除(相当于peekFirst())。
这些方法都是遵循 后进先出 (LIFO) 的栈结构。
作为队列使用时的常用方法
offer(E e): 将元素添加到队列的末尾(相当于addLast()),返回true表示成功,队列满时返回false。poll(): 从队列的头部删除并返回元素(相当于removeFirst()),如果队列为空,返回null。peek(): 查看队列头部的元素但不删除它(相当于getFirst()或peekFirst()),如果队列为空,返回null。
5、一些基础面试题
1、如果要进行大量的增加和删除使用arraylist好还是linkedlist好?
linkedlist
2、hashmap为什么容量是2的幂次方?
提高哈希效率
- 当
HashMap的容量是 2 的幂次方时,可以利用位运算来快速计算索引。因为(n-1)&hash,在n为2的幂前提下和hash%n是一样的,优化了取模运算。这样能提高性能,因为位运算比除法运算更快。
减少冲突
- 为了使每一个桶发生哈希冲突的概率相同,从而避免某些桶大量发生哈希冲突,而某些桶不发生哈希冲突的情况,浪费资源
3、HashMap为什么是线程不安全的?
4、讲讲synchronized,如果synchronized不可重入会发生什么事情?
5、hashmap扩容机制
同时 也要讲一下重新计算hashcode还有数据复制两个过程
6、讲一讲java中hashmap(讲下put)
7、list和set的区别
8、Java中的线程安全的集合是什么?
concurrenthashmap是线程安全的