@author:posper
@version 1.0: 2021/6/22-2021/6/25
@version 1.2: 2021/7/12
参考:jdk 1.8 官方文档
本文档是根据 动力节点 “集合” 视频以及《Java 核心卷1》ch9 整理的笔记
集合的学习阶段:
- 掌握集合继承结构图
- 每个集合接口的特点
- 集合实现类的底层数据结构是什么?
- 掌握在什么情况下用何种集合?
- 如何创建一个集合?
- 如何对一个具体集合进行 CRUD?
- 如何迭代遍历一个集合?
第一遍暂时只掌握前两个阶段,源码阅读有点难顶…
ch 9 集合
1、集合概述
- 集合中存储的是 Java 对象的内存地址(即,集合中存储的是对象的引用)
- 集合不能直接存储基本数据类型,另外集合也不能直接存储 Java 对象,
- Java 中的集合类和集合接口都在
java.util
包下
1.1 集合的继承结构图
- 通过集合对象的
iterator()
方法可以得到其对应的Iterator
对象 - Iterator 接口中有 4 个方法:
- boolean hashNext();
- E next();
- remove();
- default void forEachRemaining(Consumer<? super E> action);
1.2 Collection 继承结构图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mdss4gNV-1626169591784)(img\ch 9 集合\Collection.png)]
1.3 Map 继承结构图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9d5dp3z8-1626169591787)(img\ch 9 集合\Map.png)]
1.4 集合特点总结
常见集合实现类底层数据结构
- List 实现类
- ArrayList:底层是数组
- LinkedList:底层是双向链表
- Vector:底层是数组,线程安全的,效率较低,现在使用较少(现在有其他的方法保证线程安全,基本不用 Vector)
- Set 实现类
- HashSet:底层是 HashMap,放到 HashSet 集合中的元素等同于放到 HashMap 集合 key 部分中
- TreeSet:底层是 TreeMap,放到 TreeSet 集合中的元素等同于放到TreeMap 集合 key 部分了
- Map 实现类
- HashMap:底层是哈希表
- Hashtable:底层也是哈希表,只不过是线程安全的,效率较低,现在使用较少
- Properties:是线程安全的,是特殊的 Map,其中 key 和 value 只能存储字符串 String
- TreeMap:底层是二叉树。TreeMap 集合的 key 可以自动按照大小顺序排序
- Deueue 实现类
- ArrayQueue:底层是循环数组。双端队列,队头队尾都可以入队/出队
- LinkedList:还可以作为双端队列,底层是双向链表
- PriorityQueue:底层是堆。队头元素保持是 max/min
常用集合接口的特点
-
List 集合存储元素的特点:
- 有序可重复
- 有序:存进去的顺序和取出的顺序相同,每一个元素都有下标
- 可重复:存进去1,还可以在存储一个1
-
Set(Map)集合存储元素的特点:
- 无序不可重复
- 无序:存进去的顺序和取出的顺序不一定相同。另外,Set 中元素没有下标
- 不可重复:存进去 1,不能再存储 1 了
-
SortedSet 集合存储元素的特点:
- 首先是无序不可重复的,但是 SortedSet 集合中的元素是可排序的
- 无序:存进去的顺序和取出的顺序不一定相同。另外,Set 中元素没有下标
- 不可重复:存进去1,不能再存储 1 了
- 可排列:可以按照大小顺序排列
-
SortedMap 集合存储元素的特点:
- 存放于 SortedMap 中
key
部分元素的特点与 SortedSet 相同; - SortedMap 中
value
中的元素:无序,可重复,不会自动排序。
- 存放于 SortedMap 中
-
Map 集合的 key,就是一个 Set 集合
- 往 Set 集合中存储数据,实际上是放到了 Map 集合的 key 部分了
- Map 没有继承 Iterator 接口,所以不能直接用迭代器遍历 Map。
- 但是,Map 的 key 是个 set ,可以利用 key 来进行迭代
2、Collection 接口
2.1 Collection 接口常见方法
add(Object obj)
- 不使用泛型时,可以向 Collection 中添加任何对象类型
- 基本数据类型会自动装箱
- 使用泛型时,只能添加泛型类型对象
- 不使用泛型时,可以向 Collection 中添加任何对象类型
boolean remove(Object obj)
- 删除集合中 obj 元素
int size()
- 返回集合中元素个数
boolean isEmpty()
- 判断集合是否为空(是否集合包含元素,即判断集合元素个数是否为 0)
boolean contains(Object obj)
- 判断集合是否元素 obj
- 底层会调用 Collection 中存放元素的
equals()
方法来比较元素是否相同
Object[] toArray()
这个方法使用不多将集合转换为数组,但是返回的数组将是 Object[] 类型
<T> T toArray(T[] a)
- 这个使用较多
- 将集合转换成数组,且返回数组类型为特定类型(而不是 Object[])
2.2 利用迭代器访问集合元素
利用迭代器访问集合元素步骤(3 步)
-
获取集合对象的迭代器对象:
iterator()
方法Collection c = new ArrayList(); // 创建一个未使用泛型的集合 Iterator it = c.iterator(); // 1、获取集合对象的迭代器对象 it
-
通过 Iterator 的
hasNext()
方法判断当前集合是否还有没有被迭代的元素 -
通过
next()
方法返回迭代器中的下一个元素- 如果没有使用泛型,则 next() 方法返回的是 Object 类型;
- 如果使用泛型,则返回泛型的具体类型
// 2、通过获取的迭代器对象 it 开始迭代(遍历)集合 while (it.hasNext()) { // hasNext 判断当前集合是否还有未被迭代的元素 // 3、返回迭代器“越过的“元素 Object obj = it.next(); // 因为上面Collection 没用泛型,所以这里返回的是 Object 类型。用泛型的话,则返回泛型类型 System.out.println(obj); }
- 利用迭代器遍历集合元素对于所有继承 Collection 接口的集合都通用…
- for each 循环 可以处理任何实现了
Iterable
接口的对象- Collection 接口继承了 Iterable 接口。因此,Java 中集合都可以使用 “for each”循环
Iterator 还有个子接口,即列表迭代器: ListIterator,常用来迭代链表。
迭代器的注意事项
注意:集合结构(状态)只要改变(即,集合被增/删/改),迭代器必须重新获取!!!!
- 当集合结构发生改变,迭代器未重新获取时,如果调用 next() 方法,则会抛出异常
ConcurrentModificationException
- 在使用迭代器迭代集合的过程中,不能使用 Collection 中的 add() 和 remove() 方法
- 本质其实还是,集合结构改变时,需要重新获取迭代器对象),会报异常~
- 不能使用 Collection 中的 add() 和 remove() 方法
- 但是,可以使用 Iterator 中的 remove() 方法
- 如果一个集合同时关联多个迭代器,这些迭代器只能读取集合。
Collection c = new ArrayList();
// Iterator it = c.iterator(); // 1、error
c.add(1);
c.add(2);
// it.next(); // 异常
Iterator it = c.iterator(); // ok
2.3 contains() 方法详解
- contains 方法在底层调用了集合所存放数据类型的 equals 方法
- 如果集合存放的数据元素类型中重写了 equals 方法,则 contains 方法比较的是对象的内容是否相同;(比如,String)
- 如果集合存放的数据元素类型中未重写 equals 方法,则调用的是 Object 中的 equals 方法,比较的是内存地址是否相同。
- !!!结论:存放在集合中的类型,一定要重写 equals() 方法!!!!
注意:Java 中 String 类和 8 大基本数据类型对应的包装类型中都重写了 equals 方法.
public class CollectionTest05 {
public static void main(String[] args) {
Collection c = new ArrayList(); // 创建一个未使用泛型的集合
User user1 = new User("Jack");
c.add(user1);
User user2 = new User("Jack");
// 未在 User 中重写 equals() 方法时,返回 false
// System.out.println(c.contains(user2));
// 在 User 中重写 equals() 方法后,返回 true
System.out.println(c.contains(user2));
// 存放 String
Collection c2 = new ArrayList(); // 创建一个未使用泛型的集合
String s1 = new String("abc");
c2.add(s1);
String s2 = new String("abc"); // s1 和 s2内存地址不同,但是内容相同
System.out.println(c.contains(s2)); // 这里返回true,因为 String 类中重写了 equals 方法,比较的是内容是否相同
}
}
class User {
private String name;
public User(String name) {
this.name = name;
}
@Override
/**
* 重写 equals 方法 (参考《Java核心卷1》11版 p177)
*/
public boolean equals(Object otherObject) {
if (this == otherObject) { // 1
return true;
}
if (otherObject == null) { // 2
return false;
}
if (getClass() != otherObject.getClass()) { // 3
return false;
}
User user = (User) otherObject; // 4
return Objects.equals(name, user.name); // 5
}
}
2.4 remove() 方法详解
- remove 方法在底层也调用了集合所存放数据类型的 equals 方法
- 结论:存放在集合中的类型,一定要重写 equals 方法
**Note:**Collection 中的 remove() 方法其他细节同上 contains() 方法
2.5 打印 Collection 具体实现类中的内容
-
方法1:使用 foreach 循环
-
方法2:使用迭代器 (见 2.2)
-
方法3:直接调用超类
AbstractCollection
中的toString()
方法- 调试时用,最简洁;
- 但是,只能按照固定格式输出
List list = new ArrayList(); // 非线程安全的 list.add(1); list.add(2); System.out.println(list); // [1, 2]
AbstractCollection 中的 toString() 源码
public abstract class AbstractCollection<E> implements Collection<E> {
....
public String toString() {
Iterator<E> it = iterator();
if (! it.hasNext())
return "[]";
StringBuilder sb = new StringBuilder();
sb.append('[');
for (;;) {
E e = it.next();
sb.append(e == this ? "(this Collection)" : e);
if (! it.hasNext())
return sb.append(']').toString();
sb.append(',').append(' ');
}
}
}
3、List 接口
List 接口特有方法:
- E get(int index)
- E set(int index, E val))
- add(int index, E val)
- … 详见 api 文档…
3.1 ArrayList
-
ArrayList 底层就是一个 Object[] 数组
-
ArrayList 底层数组默认初始化容量为 10
- jdk1.8 中 ArrayList 底层先创建一个长度为 0 的数组
- 当第一次添加元素(调用 add() 方法)时,会初始化为一个长度为 10 的数组
-
当 ArrayList 中的容量使用完之后,则需要对容量进行扩容…
-
ArrayList 扩容后是原容量的 1.5 倍
-
ArrayList 容量使用完后,会“自动”创建容量更大的数组,并将原数组中所有元素拷贝过去,这会导致效率降低…
- 参考:《Java 核心卷1》11版 p187
-
优化:可以使用构造方法 ArrayList (int capacity) 或 ensureCapacity(int capacity) 提供一个初始化容量
- 避免刚开始就一直扩容,造成效率较低…
-
-
ArrayList 构造方法
ArrayList()
:创建一个初始化容量为 10 的空列表ArrayList(int initialCapacity)
:创建一个指定初始化容量为 initialCapacity 的空列表ArrayList(Collection<? extends E> c)
:创建一个包含指定集合中所有元素的列表
-
ArrayList 特点
-
优点:
- 向 ArrayList 末尾添加元素(add() 方法)时,效率较高
- 往往加元素情况较多…
- 查询效率高
- 不单纯是因为有下标,是因为其底层是数组(内存地址是连续的)
- 向 ArrayList 末尾添加元素(add() 方法)时,效率较高
-
缺点:
- 扩容会造成效率较低
- 可以通过指定初始化容量,在一定程度上对其进行改善
- 另外数组无法存储大数据量(因为很难找到一块很大的连续内存空间)
- 向 ArrayList 中间添加元素(add(int index)),需要移动元素,效率较低
- 但是,向 ArrayList 中间位置增/删元素的次数较少时不影响;
- 如果增/删操作较多,可考虑改用链表
- 扩容会造成效率较低
-
-
如何将 ArrayList 变成线程安全的?
- 调用
Collections
工具类中的static <T> List<T> synchronizedList(List<T> list)
方法
List list = new ArrayList(); // 非线程安全的 list.add(1); list.add(2); Collections.synchronizedList(list); // 变成线程安全的了
注意:ArrayList 是非线程安全的
- 调用
3.2 LinkedList
3.2.1 LinkedList 特点
底层数据结构:LinkedList 底层是一个双向链表
优点:
- 增/删效率高
缺点:
- 查询效率较低
- LinkedList 有下标,但是是“虚假”的下标;
- LinkedList 也可以调用 get(int index) 方法,返回链表中第 index 个元素
- 但是,每次查找都要从头结点开始遍历
- 如果频繁查找(调用 get(index) 方法)的话,可以改用 ArrayList,提高查询效率
3.2.2 LinkedList 部分源码解读
boolean add(E e)
方法
public class LinkedList<E> extends AbstractSequentialList<E> implements ... {
// 均不参与序列化
transient int size = 0; // 链表长度
transient Node<E> first; // 指向链表第一个节点
transient Node<E> last; // 指向链表最后一个节点
public boolean add(E e) { // 添加元素
linkLast(e); // 向链表末尾添加元素 e
return true;
}
void linkLast(E e) { // 向链表末尾添加元素 e
final Node<E> l = last; // 暂时保存最后一个元素的指针
final Node<E> newNode = new Node<>(l, e, null);
last = newNode; // newNode 作为最后一个节点
if (l == null) // 当前链表为空
first = newNode; // 第一次添加的节点,即为 first
else // 链表不空
l.next = newNode; // 当前链表的最后一个节点next 指向newNode
size++;
modCount++;
}
// LinkedList 底层是一个双向链表
private static class Node<E> { // LinkedList 中的节点是一个 private 的静态内部类 Node
E item;
Node<E> next; // 后继
Node<E> prev; // 前驱
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
}
3.2.3 ListIterator 接口
- LinkedList 类中的
void add(E e)
方法只能将数据添加到链表的末尾(见 3.2.2) - 如果要将对象添加到链表的中间位置,则需要使用
ListIterator
接口的void add(E e)
方法
ListIterator 接口-常见方法:
ListIterator
中 remove() 方法- 调用 next() 之后,remove() 方法删除的是 “光标” 左侧的元素;(此时,类似键盘的 backspace)
- 调用 previous() 之后,remove() 删除的是 “光标” 右侧的元素
ListIterator
中 add() 方法- 调用 next() 之后,在 “光标” 左侧添加一个元素;
- 调用 previous() 之后,add 是在 “光标” 右侧添加元素
3.3 Vector
- Vector 底层是数组
- 初始化容量为 10
- 扩容:原容量使用完后,会进行扩容。新容量扩大为原始容量的 2 倍
- Vector 是线程安全的(里面方法都带有
synchronized
关键字),效率较低,现在使用较少 - 如何将 ArrayList 变成线程安全的?
- 调用 Collections 工具类中的 static List synchronizedList(List list) 方法
- 参见上面 3.2 中
3.4 队列
3.4.1 Queue
- (普通)队列:队尾插入(入队),队头删除(出队)
3.4.2 Deque
- 双端队列:队头/队尾都可以入队/出队
3.4.3 PriorityQueue
- 优先队列底层使用的是 “堆” 实现的
- 保持***队头元素***是 min/max 的,并***未对所有元素都进行排序***;
- 优先队列经典用法是进行任务调度
- 每个任务都一个优先级;
- 优先级“最高”的任务,位于队头
- 注意:向优先队列中添加 自定义类型元素***时,必须给出***比较规则,否则抛出异常 ClassCastException
4、Set
4.0 泛型
详参考 ch 8 泛型程序设计
-
jdk 1.5 引入,之前都是使用 Object[]
-
使用 Object[] 的缺点(2个)
- 1)获取一个值时必须进行强制类型转换
- 2)调用一个方法前必须使用 instanceof 判断对象类型
-
泛型的好处
- 1)减少了强制类型转换的次数
- 获取数据值更方便
- 2)类型安全
- 调用方法时更安全
- 1)减少了强制类型转换的次数
-
泛型只在编译时期起作用,运行阶段 JVM 看不见泛型类型(JVM 只能看见对应的原始类型,因为进行了类型擦除)
-
带泛型的类型,但是在使用时没有指定泛型类型时,默认使用 Object 类型
List list = new HashTestrrayList(); // 默认可以放任意 Object 类型
4.1 HashSet
- 特点:HashSet 无序(没有下标),不可重复
- HashSet 底层是 HashMap,向 HashSet 中添加元素相当于插入到 HashMap 的 key 部分
- HashMap 底层是哈希表,Java 中 HashMap 采用的是 “拉链法” 解决数据冲突
- 注意:如果利用 HashSet 对自定义类型进行去重,必须同时重写 equals() 和 hashCode() 方法
详参考
6、HashMap
部分
4.2 TreeSet
- 特点:TreeSet 无序(没有下标),不可重复,但是可以自动排序
- TreeSet 底层是 TreeMap,向 TreeSet 中添加元素相当于插入到 TreeMap 的 key 部分
- TreeMap 中采用的是 “红黑树” 对 key 进行排序
HashSet 为 HashMap 的 key 部分;TreeSet 为 TreeMap 的 key 部分。
所以,这里没有重点讲。重点掌握 HashMap 和 TreeMap。
5、Map
Map
和Collection
没有继承关系- Map 以 (key ,value) 的形式存储数据:键值对
- key 和 value 存储的都是对象的内存地址(引用)
5.1 Map 接口常见方法
方法签名 | 功能 |
---|---|
Set keySet() | 返回 map 中得 keySet 视图 |
Collection values() | 返回 map 中的 value 集合(Collection) 视图 |
Set<Map.Entry<K, V>> entrySet() | 返回 map 对应的 Map.Entry 集合(set)视图 |
V put(K key, V value) | 向 map 中添加键为 key,值为 value的元素(map中有key,则更新) |
V get(Object key) | 返回 map 中 key 对应的 value;如果 map 中不含 key,则返回 null |
default V getOrDefault(Objectl) key, V] defaultValue) | 返回 map 中 key 对应的 value;如果 map 中不含 key,则返回 defaultValue |
V remove(Object key) | 移除 map 中 key |
void clear() | 清空 map |
boolean isEmpty() | 判空(判断 map 中 size 是否为 0) |
boolean containsKey(Object key) | 查看 map 中是否包含 key |
boolean containsValue(Object value) | 查看 map 中是否包含 value |
default void forEach(BiConsumer<? super K, ? super V> action) | 迭代访问 map 中所有的 key and value |
注意:Map.Entry<K, V> 是 Map 中的一个内部接口。接口中的内部接口默认是 public static 的。
- HashMap 中使用了一个静态内部类 Node 实现了 Map.Entry<K, V> 接口
- 详见
6、HashMap 部分源码解析
- 详见
5.2 Map 的遍历方法
两种常见方法是,通过获取 Map 的 keySet 或 Map.Entry<K, V> 视图,来遍历 key,value。
Note:
- 不能向 keySet 视图中添加元素;
- 在视图上,调用迭代器 remove() 方法,会将原始 map 中对应的 key:value 删除
第一类方法
第一类方法:先获取 map 的 keySet,然后取出 key 对应的 value
特点:
- 效率相对较低。(因为还要根据 key 从哈希表中查找对应的 value)
方法1
- 通过 foreach 遍历 map.keySet(),取出对应的 value
public static void printMap1(Map<Integer, String> map) { if (map.size() == 0) { System.out.println("the map is empty..."); return; } System.out.println("the elements in the map are like below..."); for (Integer key : map.keySet()) { System.out.println(key + " : " + map.getOrDefault(key, "")); }}
方法2
- 通过***迭代器*** 迭代 map.keySet(),来取出对应的 value
注意: 在视图上,调用迭代器 remove() 方法,会将原始 map 中对应的 key:value 删除
public static void printMap2(Map<Integer, String> map) { Set<Integer> keySet = map.keySet(); Iterator<Integer> it = keySet.iterator(); while (it.hasNext()) { Integer cntKey = it.next(); // it.remove(); // 在视图上,调用迭代器 remove() 方法,会将 map 中对应的 key:value 删除 System.out.println(cntKey + " : " + map.getOrDefault(cntKey, "")); }}
第二类方法
调用
map.entrySet()
方法,获取 entrySet,然后直接从 entrySet 中同时获取 key 和 value。
特点:
- 效率较高(直接从 node 中同时获取key,value)
- 适用于大数据量 map 遍历
方法3
- 调用 map.entrySet(),然后使用 foreach 遍历 entrySet
public static void printMap3(Map<Integer, String> map) {
Set<Map.Entry<Integer, String>> entry = map.entrySet();
for (Map.Entry<Integer, String> it : entry) {
System.out.println(it.getKey() + " : " + it.getValue());
}
}
方法4
- 调用 map.entrySet(),然后使用***迭代器***遍历 entrySet
注意: 在视图上,调用迭代器 remove() 方法,会将 map 中对应的 key:value 删除
public static void printMap4(Map<Integer, String> map) {
Set<Map.Entry<Integer, String>> entrySet = map.entrySet();
Iterator<Map.Entry<Integer, String>> it = entrySet.iterator();
while (it.hasNext()) {
Map.Entry<Integer, String> node = it.next();
// it.remove(); // 在视图上,调用迭代器 remove() 方法,会将 map 中对应的 key:value 删除
System.out.println(node.getKey() + " : " + node.getValue());
}
}
第三类方法
使用 Map 中的 forEach
方法,以及 lambda
表达式
map.forEach((k, v) -> System.out.println(k + " : " + v));
第四类方法
- 使用超类
AbstractMap
中的toString()
方法- AbstractMap 重写了 toString 方法,打印格式为 {key=value,…}
- 特点:
- 调试时,使用最为方便;
- 但是,只能打印固定格式(不过一般也不影响调式)
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "1ab");
map.put(3, "ad");
map.put(2, "adc");
System.out.println(map); // {1=1ab, 2=adc, 3=ad}
5.3 HashMap
5.3.1 HashMap 概述
- HashMap 底层是一个数组
- 数组中每个元素是一个单向链表(即,采用拉链法解决哈希冲突)
- 单链表的节点每个节点是 Node<K, V> 类型(见下 6.2 源码)
- 同一个单链表中所有 Node 的 hash值不一定一样,但是他们对应的数组下标一定一样
- 数组下标利用哈希函数/哈希算法(eg:对 len 取模)根据 hash值计算得到的
- 数组中每个元素是一个单向链表(即,采用拉链法解决哈希冲突)
- HashMap 是数组和单链表的结合体
- 数组查询效率高,但是增删元素效率较低
- 单链表在随机增删元素方面效率较高,但是查询效率较低
- HashMap 将二者结合起来,充分它们各自的优点
- HashMap 特点
- 无序、不可重复
- 无序:因为不一定挂在单链表的哪一个节点上了
- 为什么不可重复?
- 通过重写
equals()
方法保证的 (见6.3 & 6.4 put/get
方法原理)
- 通过重写
举个栗子:HashMap 有点像查字典。首先,从字典目录(对应 HashMap 数组)中查找要待查生字(key)的拼音/笔画(hashCode),然后根据拼音/笔画(hashCode)查找其对应的页码(利用哈希函数得到 hashCode 对应的数组下标),然后翻到指定页码(数组下标),再从当前页码(链表)中查找生字(从链表中查找 key)
5.3.2 HashMap 部分源码解析
- HashMap 中使用了一个静态内部类 Node 实现了
Map.Entry<K, V>
接口
public class HashMap extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
// 数组 + 链表,即采用“拉链法”解决哈希冲突
transient Node<K,V>[] table; // 哈希表(其中,数组中每个元素又是一个链表,称为一个“桶”)
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认初始化容量(必须是 2 的次幂)
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认加载因子(当容量超过加载因子时,将进行再散列)
static final int TREEIFY_THRESHOLD = 8; // 单链表元素超过 8 个,则转变为红黑树
static final int UNTREEIFY_THRESHOLD = 6; // 红黑树节点数量小于 6 时,会重新变为单链表
static class Node<K,V> implements Map.Entry<K,V> { // 静态内部类 Node
final int hash; // 哈希值(由 key 经过哈希函数计算得到)
final K key; // key 是 final 修饰
V value; // value 和 key 不能用泛型类型在 Map.Etry<K, V> 接口定义
Node<K,V> next; // 下一个节点的地址
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
..... // 省略
}
}
-
HashMap 默认初始化容量: 16
- 必须是 2 的次幂,这也是 jdk 官方推荐的
- 这是因为达到散列均匀,为了提高 HashMap 集合的存取效率,所必须的
-
HashMap 默认加载因子:0.75
- 数组容量达到 3/4 时,开始扩容
-
JDK 8 之后,对 HashMap 底层数据结构(单链表)进行了改进:
-
如果单链表元素超过 8 个,则将单链表转变为红黑树;
-
如果红黑树节点数量小于 6 时,会将红黑树重新变为单链表。
这种改进的目的,仍是为了提高检索效率,二叉树的检索会再次缩小扫描范围。提高效率。
-
5.3.3 put 方法原理
- 第一步:先将 key, value 封装到
Node
对象中 - 第二步:底层会调用 key 的
hashCode()
方法得出 hash 值 - 第三步:通过哈希函数/哈希算法,将 hash 值转换为数组的下标
- 如果下标位置上没有任何元素,就把
Node
添加到这个位置上; - 如果下标位置上有单链表,此时会将当前
Node
中的 key 与单链表上每一个节点中的 key 进行equals()
比较- 如果所有的 equals() 方法返回都是 false,那么这个新节点 Node 将被添加到链表的末尾;
- 如果其中有一个 equals() 返回了 true,那么链表中对应的这个节点的 value 将会被新节点 Node 的 value 覆盖。(保证了不可重复)
- 如果下标位置上没有任何元素,就把
HashMap 中允许 key 和 value 为 null,但是只能有一个(不可重复)!
HashTable 中 key 和 value 都不允许为 null。
5.3.4 get 方法原理
- 第一步:先调用 key 的
hashCode()
方法得出 hash 值 - 第二步:通过哈希函数/哈希算法,将 hash 值转换为数组的下标
- 通过数组下标快速定位到数组中的某个位置:
- 如果这个位置上什么也没有(没有链表),则返回 null;
- 如果这个位置上有单链表,此时会将当前
Node
中的 key 与链表上每一个节点中的 key 进行equals()
比较。- 如果所有的 equals 方法返回都是 false,那么 get 方法返回 null;
- 如果其中有一个 equals 返回了 true,那么这个节点的 value 便是我们要找的 value,此时 get 方法最终返回这个要找的 value。
- 通过数组下标快速定位到数组中的某个位置:
注意:放在 HashMap 中 key 的元素(或者放在 HashSet 中的元素)需要同时重写 hashCode() 和 equals() 方法!!!
5.3.5 同时重写 hashCode() 和 equals() 方法
- 重写 hashCode() 方法时要达到散列分布均匀!!!
- 如果 hashCode() 方法返回一个固定的值,那么 HashMap 底层则变成了一个单链表;
- 如果 hashCode() 方法所有返回的值都不同,此时 HashMap 底层则变成了一个数组。
- 这两种情况称之为,散列分布不均匀。
equals()
和hashCode()
方法一定要同时重写(直接用 eclipse/IDEA 生成就行)
public class Student { String name; public Student(String name) { this.name = name; } ... // 重写 equals() 方法 ... // 重写 hashCode() 方法}public static void main(String[] args) { Set<Student> set = new HashSet<>(); Student stu1 = new Student("Amy"); Student stu2 = new Student("Amy"); set.add(stu1); set.add(stu2); // 如果只从重写 equals,但是未重写 hashCode 方法,这里 size 将会是 2 // 所以,equals 和 hashCode方法一定要同时重写(直接用 eclipse 生成就行) System.out.println("size = " + set.size()); System.out.println(set); // 调用 AbstractCollection 中重写的 toString() 方法}
5.4 TreeMap
5.4.1 TreeMap 概述
- TreeSet/TreeMap 底层是红黑树(自平衡二叉树)
- TreeSet/TreeMap 迭代器采用的是中序遍历方式
- TreeMap 特点:
- 无序,不可重复,但是可根据 key 排序
5.4.2 排序规则
-
TreeSet/TreeMap中key 可以自动对 String 类型或8大基本类型的包装类型进行排序
-
但是,TreeSet 无法直接对自定义类型进行排序
- 直接将自定义类型添加到 TreeSet/TreeMap中 key 会报错
java.lang.ClassCastException
- 原因:是因为自定义类型没有实现
java.lang.Comparable
接口(此时,使用的是 TreeSet 的无参构造器)
- 直接将自定义类型添加到 TreeSet/TreeMap中 key 会报错
-
对 TreeSet/TreeMap 中 key 中的元素,必须要指定排序规则。主要有两种解决方案:
- 方法一:放在集合中的自定义类型实现
java.lang.Comparable
接口,并重写 compareTo 方法 - 方法二:选择 TreeSet/TreeMap 带比较器参数的构造器 ,并从写比较器中的 compare 方法
- 比较器有 3 种常见的实现方法:
- 定义一个 Comparator 接口的实现类
- 使用匿名内部类
- lambda 表达式(Comparator 是函数式接口)
- 利用 -> 的 lambda表达式 重写 compare 方法
- 利用 Comparator.comparing 方法
- 比较器有 3 种常见的实现方法:
- 方法一:放在集合中的自定义类型实现
-
两种排序规则如何选择呢?
- 当比较规则不会发生改变的时候,或者说比较规则只有一个的时候,建议实现 Comparable 接口;
- 当比较规有多个,并且需要在多个比较规则之间频繁切换时,建议使用 Comparator 比较器。
方法1
// 利用 TreeSet 对自定义类型排序
/**
* 方法1:自定义类型实现 Comparable 接口
* @date 2021-06-25 14:33
* @author preci
* @version 1.0
*
*/
public class Person implements Comparable<Person> {
int age;
public Person(int age) {
this.age = age;
}
@Override
public int compareTo(Person o) {
return this.age - o.age; // 按照年龄升序排序
}
}
public static void main(String[] args) {
Set<Person> persons = new TreeSet<>();
persons.add(new Person(1));
persons.add(new Person(25));
persons.add(new Person(10));
persons.add(new Person(8)); // output:1, 8, 10, 25 (年龄升序)
}
方法2
/**
* 方法2:利用比较器 Comparator
* @date 2021-06-25 14:08
* @author preci
* @version 1.0
*
*/
class Cat { // 没有实现 Comparable 接口
int age;
public Cat(int age) {
this.age = age;
}
}
public static void main(String[] args) {
// 1、使用接口实现类
// Set<Cat> set = new TreeSet<>(new MyCmp()); // 传递一个比较器对象给 TreeSet 构造器
// 2、使用匿名内部类
Set<Cat> set = new TreeSet<>(new Comparator<Cat>() {
@Override
public int compare(Cat o1, Cat o2) {
return o1.age - o2.age;
}
});
// (3) 使用 lambda 表达式,传递一个比较器对象
// Set<Cat> set = new TreeSet<>((o1, o2) -> o1.age - o2.age);
set.add(new Cat(1));
set.add(new Cat(15));
set.add(new Cat(12)); // output:1,12,15
}
// 创建一个比较器类
class MyCmp implements Comparator<Cat> {
@Override
public int compare(Cat o1, Cat o2) {
return o1.age - o2.age;
}
}
5.5 HashTable
- 作用同 HashMap 类似
- 线程安全,但效率低。
- HashTable 现已弃用,而是使用
ConcurrentHashMap
来⽀支持线程安全,ConcurrentHashMap 的效率会更更高,因为 ConcurrentHashMap 引⼊入了了分段锁。
- HashTable 现已弃用,而是使用
5.6 Properties
- Properties 是 HashTable 的直接子类;
- Properties 是特殊的 Map,其中 key 和 values 都是
String
- 常用方法
String getProperty(String key)
Object setProperty(String key, String) value)
- 使用场景:Properties 常用配合 IO 来读/写属性配置文件(比如,username=password)
5.7 WeakHashMap
- 作用:用来删除长期存活的 map 中的那些无用的 key;
- JVM 的 GC 会跟踪活动的对象。只要 map 是活动的,其中的所有桶也就是活动的,尽管桶中有些 key 已经不会再被用到了,此时 GC 也无法对那些不用的 key 进行垃圾回收。
- WeakHashMap 使用弱引用对象(
WeakReference
)保存 map 的key
;- 正常情况下,GC 只会将不再活动的对象,进行回收;
- 但是,如果某个对象只能由
WeakReference
引用,GC 也会将其回收
5.8 LinkedHashMap
-
继承自 HashMap,因此具有和 HashMap 一样的快速查找特性
-
内部维护了一个双向链表,⽤用来维护插入顺序或者 LRU 顺序
-
利用 LinkedHashMap 实现 LRU 缓存步骤:
- 自定义一个类,继承
LinkedHashMap<K, V>
- 重写
removeEldestEntry
方法
public class LRUCache<K, V> extends LinkedHashMap<K, V> { final static int MAX_SIZE = 100; // 最大容量,map 中的元素个数超过这个容量时,将会删除最近最久未使用元素 @Override protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { return size() > MAX_SIZE; // 大于 100 时,将删除最近最久未使用的元素 } public LRUCache() { // (初始化容量,装载因子,是否开启 LRU 顺序) super(MAX_SIZE, 0.75f, true); // true:开启 LRU 顺序 } }
- 自定义一个类,继承
6、视图
- 视图(View)可以获得其他实现了
Collection
接口或Map
接口的对象。- 比如,
keySet()
方法返回了一个实现了 Set 接口的类对象(但不是 HashSet,也不是 TreeSet),由这个类的方法操纵原有的 map,这种集合称为视图。
- 比如,
视图
类型也实现了Set
接口,Set
接口中的方法视图大多都能用- 但是,注意
不可修改视图
不调用 Set 中的增删改方法
- 但是,注意
6.1 子范围视图
为集合建立一个属于其一部分的子范围视图。
- List (根据下标索引建立)
subList(int fromIndex, int toIndex);
// 左闭右开区间
- SortedSet (根据排序顺序)
SortedSet<E> subSet(E fromElement, E toElement);
// 左闭右开区间SortedSet<E> headSet(E toElement);
// 左闭右开:[0, to)SortedSet<E> tailSet(E fromElement);
// 左闭右开区间
- SortedMap (根据排序后的 key)
SortedMap<E> subMap(K fromElement, K toElement);
// 左闭右开区间SortedMap<E> headMap(K toElement);
// 左闭右开:[0, to)SortedMap<E> tailMap(K fromElement);
// 左闭右开区间
注意:
- 子范围视图修改,也会影响到原有集合;
- 原集合被结构性修改后,子范围视图要重新获取;否则,再次操作子范围视图时,将会抛出异常
ConcurrentModificationException
- 这里比较比较神奇,list 中的 增/删 才叫结构性修改,此时 subList 将会抛出异常;
- 但是,list 进行 set 后,不叫结构性修改,此时 subList 会跟着 list 同步改变。
List<Integer> list = new ArrayList<Integer>();
for (int i = 0; i < 15; i++) {
list.add(i);
}
// 子范围视图
List<Integer> subList = list.subList(5, 8); // 左闭右开:[666, 6, 7]
subList.remove(0); // 子范围视图修改,也会影响到原有集合(原集合 5 被删掉了)
System.out.println(subList); // [6, 7]
System.out.println(list); // [0, ..., 4, 6, 7, ..., 14] ”5被删了“
list.remove(0); // 修改原集合,结构性修改
// System.out.println(subList); // error。抛出异常 ConcurrentModificationException,需要重新获取 subList
list.set(5, new Student(11, "")); // 对于list来说,set 不是 结构性修改
System.out.println(subList); // OK。但是,子范围视图也会跟着改变,subList = [666, 6, 7]
6.2 不可修改的视图
Collections
类中以unmodifiable
开头的 8 个静态方法,会生成集合的不可修改视图(unmodifiable view)- 不可修改视图如果被修改,将会抛出异常
java.lang.UnsupportedOperationException
,原集合仍保持不变
注意:
- 只是不能对“不可修改视图”调用集合的
增/删/改
方法,但是仍然可以通过集合的原始引用对集合进行修改。- 根据原集合引用修改集合后,unmodifiableList 也会跟着改变
// 不可修改的视图
List<Integer> unmodifiableList = Collections.unmodifiableList(list); // list:[0, 1, ..., 5]
System.out.println(unmodifiableList);
// unmodifiableList.add(12); // error。异常,UnsupportedOperationException。不可修改视图不能修改
list.add(888); // ok。通过集合原始引用仍然可以修改
System.out.println(list); // list:[0, 1, ..., 5, 888]
System.out.println(unmodifiableList); // unmodifiableList:[0, 1, ..., 5, 888],跟着改变
6.3 同步视图
Collections
类中synchronized 开头
的静态方法,会将非线程安全的集合转换为线程安全的- 将 List 转换为线程安全:
static <T> List<T>
synchronizedList(List<T> list)
- 将 map 转换为线程安全:
static <K,V> Map<K,V>
synchronizedMap(Map<K,V> m)
- 将 List 转换为线程安全:
6.4 检查型视图
- 作用:检查型视图用来对泛型类型可能出现的问题提供调试支持。
ArrayList<String> strings = new ArrayList<String>();
strings.add("abc");
ArrayList rawList = strings; // 没有使用泛型,默认可以为 Object 类型
rawList.add(new Date()); // 此时,strings 中包含 Date 对象了。如果执行 strings 的 get 方法,将会出错
// 检查型视图
List<String> safeStrings = Collections.checkedList(strings, String.class); // 此时,只能接受 String 类型
ArrayList rawList2 = (ArrayList) safeStrings;
rawList2.add(new Date()); // error。抛出异常,ClassCastException
7、Collections 工具类
7.1 排序
Collections.sort(List list)
Collections.sort(List list, Compataor cmp)
- 如果需要对自定义类型排序,则需要给比较规则:
- 1)方法1:自定义类型实现 Comparable 接口;
- 2)方法2:实现一个比较器。
- 如果需要对自定义类型排序,则需要给比较规则:
HashSet 可以利用 ArrayList 构造器转换为 list
Set<String> set = new HashSet<>();set.add("aa");set.add("b");ArrayList list = new ArrayList(set); // 将 set 转化为 listCollections.sort(list);
-
对二维数组排序
// 对二维数组排序,需要执行比较规则int[][] people = new int[100][2];Arrays.sort(people, new Comparator<int[]>() { @Override public int compare(int[] o1, int[] o2) { if (o1[0] != o2[0]) { return o2[0] - o1[0]; // 按照第一维降序排序 } return o1[1] - o2[1]; // 若第一维相同,再按照第二维升序排序 }});
Java 中数组进 sort 采用的是优化后的快排;
而是用 sort 对链表进行排序时,是先将链表复制到一个数组中,是用 sort 对数组进行排序,然后再将排序后的序列复制到链表中。
-
也可以使用
List
接口中的default void sort(Comparator<? super E> c)
方法进行排序list.sort((o1, o2) -> o2 - o1); //
7.2 集合和数组的转换
- 数组 to 集合:
List.of(arr)
- 但是,这个方法
Java 9
之后才能用
- 集合 to 数组:
- 不能直接使用 toArray() 方法,这样返回的是 Object[] 数组;
- 使用 toArray() 方法的变体,
toArray(new ElementType[length]);