前言:
因此引入集合。集合能够存放不同的对象,同时能够使用方法进行CRUD,灵活度高。
1. 集合
集合分类:(必须记住)
- 单列集合:直接存放元素。
- 双列集合:以键值对的形式存放元素。
注意:以上单列集合、双列集合中都只显示常用的实现类,实际上还有其他的实现类
2. 单列集合
2.1 Collection接口
- Collection接口的常用方法:即单列集合都有的方法
注意:
toArray()
方法只能将集合转为Object类型的数组,如果要将集合要转为指定类型的数组,只能使用toArray(T[] a)
方法。
- 上面黄色背景的方法使用default修饰,故不被子类继承。但是由于是接口,如果不被重写将无意义,所以接口中default修饰的方法都会在子类中被重写。事实上,这就是要告诉子类,想用该方法就得重写。
①关于removeIf方法:https://blog.csdn.net/czydream/article/details/81585069
②关于spliterator() 、stream() 、parallelStream() 方法:https://blog.csdn.net/starexplode/article/details/80567758/- 遍历集合
- 使用迭代器:Collection接口继承了Iterable接口,故Collection对象使用
.iterator()
方法可获取迭代器对象,再使用迭代器对象的next()
方法可不断获取元素对象。
- 使用for-each循环:(for-each的底层依旧使用的是迭代器实现)
2.2 List接口
2.2.1 List接口介绍
- 实现List接口的特点:
- 元素有序,且可重复 (故可重复添加多个null)
- 支持索引。【使用
.get(index)
方法进行索引】
3. 每一个元素都对应一个整型的序号记载其在容器中的位置,故也可根据序号访问- 常用方法:
- Collection接口的常用方法。
- List接口的常用方法:
replace()方法:https://vimsky.com/examples/usage/java-programming_library_arraylist_replaceall.html#:~:text=replaceAll%20%28%29%20%E6%96%B9%E6%B3%95%E4%B8%8D%E8%BF%94%E5%9B%9E%E4%BB%BB%E4%BD%95%E5%80%BC%E3%80%82%20%E7%9B%B8%E5%8F%8D%EF%BC%8C%E5%AE%83%E5%B0%86%20arraylist,%E7%9A%84%E6%89%80%E6%9C%89%E5%80%BC%E6%9B%BF%E6%8D%A2%E4%B8%BA%20operator%20%E4%B8%AD%E7%9A%84%E6%96%B0%E5%80%BC%E3%80%82%20%E7%A4%BA%E4%BE%8B%201%EF%BC%9A%E5%B0%86%E6%89%80%E6%9C%89%E5%85%83%E7%B4%A0%E6%9B%B4%E6%94%B9%E4%B8%BA%E5%A4%A7%E5%86%99
- 遍历List:三种方法。 在Collection的两种遍历方式在基础上,加上使用 fori 当作数组遍历.【迭代遍历效率优于索引遍历】
- 细节:
- List对象具有特殊的迭代器
ListIterator
。
- 该迭代器除了正常迭代器操作外(如查找),还允许①元素插入、②删除、③替换、④双向访问(next方法、previous方法)。
点击进入详细情况- 两个重载方法
2.2.2 排序
- 使用list对象的sort()方法。使用方法和Array.sort()类似。
- 1
- 3
2.2.3 List接口的三个实现类
- ArrayList:① 底层使用的是数组结构改查很快,但增删较慢;② 线程不同步,即不是线程安全,但效率高
- LinkedList:① 底层使用的是链表结构,改查较慢,增删较快 ② 线程不同步
- Vector:① 查改、增删都慢。②但是线程安全,故效率低
2.2.3.1 ArrayList类
- ArrayList具有一个成员字段:
transient Object[] elementData;
。ArrayList的底层就是用数组elementData存放数据的。 transient 关键字详细介绍- 扩容机制:ArrayList有两个构造器。
① 使用无参构造器:elementData的容量为0;第一次添加数据,会自动扩容到10;存满后自动再次扩容时,为原来的1.5倍。
故0,10,15,22(向下取整),...
② 使用指定大小的构造器:elementData的容量为指定大小;再次扩容时为原来的1.5倍。
- 特点:① 改查很快,但增删较慢;② 线程不同步,即不是线程安全,但效率高
- 特有方法:查API
扩容机制案例:
import java.util.ArrayList;
public class Person{
public static void main(String[] args) {
ArrayList list = new ArrayList();
for (int i = 1; i <= 10; i++) {
list.add(i);
}
for (int i = 11; i <= 15; i++) {
list.add(i);
}
list.add(100);
}
}
使用以上代码进行调试,可以得到扩容的流程:
2.2.3.2 Vector类
- Vector存放也是使用elementData数组存放数据。但是该数组是
protected Object[] elementData;
,故可以被序列化。- 扩容机制
- 特点:① 查询、增删都慢。②但是线程安全,可以看到其每个方法都使用synchronized修饰。
- 特有方法:查API
2.2.3.3 LinkedList类
- LinkedList使用的双向链表存放数据。
- LinkedList有两个属性:
①transient Node<E> first;
指向首结点
②transient Node<E> last;
指向尾结点- Node类是LinkedList的一个静态内部类。
- 特点:① 改查较慢,增删较快 ② 线程不同步
- 特有方法:查API
2.3 Set接口
- 实现Set接口的特点:
① 元素无序:指添加顺序与取出顺序可能不一致。但是要注意每次取出的顺序是一致的。
② 元素不重复(故只能有一个null):故使用add()方法添加元素时可能添加失败。(返回值为false就是添加失败)注意:Set的本质是使用Map的key。将value设为固定值。
- 常用方法:
- Collection接口的常用方法。
- Set接口的常用方法:
- 遍历Set:遍历Collection的2种方法。即 迭代器 和 foreach
2.3.1 HashSet类
2.3.1.1 HashSet介绍
- 类定义:实现了
Set
接口。
- 构造器:【对应HashMap的4个构造器】
HashSet()
:table初始容量为0,默认第一次扩容为16;默认加载因子为0.75HashSet(int initialCapacity)
:table初始容量为0,指定第一次扩容大小;默认加载因子为0.75HashSet(int initialCapacity, float loadFactor)
:table初始容量为0,指定第一次扩容大小;指定加载因子HashSet(Map<? extends K, ? extends V> m)
:将t的键值对复制到新HashTable中。- 方法:
- Collection接口和Set接口的方法。
- HashSet特有的方法
- 细节:
- HashSet底层是HashMap。将结点中的value置为固定值。
- 元素重复的定义:满足以下两个条件则元素重复
- 两者的hash值相同
- 使用 == 比较为True;或者使用.equal比较为True
注意:在Object类中讲过,无论是否重写hashCode和equals方法,都保持一个特性
equals相等,则hashCode必相等
2.3.1.2 HashSet底层结构
HashSet底层使用的是HashMap。使用其key,将其value设置为固定值。
可跳转到3.1.1.2
小节。
2.3.1.4 LinkedHashSet类
LinkedHashSet底层是LinkedHashMap。可跳转到3.1.1.4
小节。
2.3.2 TreeSet类
- 类定义:继承了
AbstractSet
类。
- 构造器:【对应TreeMap的4个构造器】
TreeSet()
:TreeSet中的元素自然排序。TreeSet(Collection<? extends E> c)
:将集合c中元素复制到TreeSet集合中。并自然排序。TreeSet(Comparator<? super E> comparator)
:TreeSet中的元素根据指定的比较器进行排序。【通常使用匿名内部类做为形参】
TreeSet(SortedSet<E> s)
:将集合s中元素复制到TreeSet集合中。并TreeSet元素顺序与集合s元素顺序一致。- 方法:
- Collection接口、Set接口方法
- TreeSet的方法
- 细节:
- TreeSet底层使用的TreeMap。
- 元素是否重复:
① 排序规则不同,调用的比较方法不同。
(1)若采用自然排序:则以添加元素对象实现的Compareable接口的compareTo方法去比较。【若要添加元素没有实现Compareable接口,则会报异常,如下图所示:】
(2)若采用比较器:则以比较器中的compare方法去比较。
② 根据返回结果判断是否重复: 将要添加元素与TreeSet中元素逐个传入 Compareable接口的compareTo方法 或 比较器compare方法 中。若结果为0,则重复,不添加元素;若所有比较结果都不为0,则不重复并添加改元素。
3. 双列集合
3.1 Map接口
- 实现Map接口的特点:
- 以键值对的方式存放数据。
① 键值对存放在Map.Entry或其子类实例中。若键值对存放在Entry的子类中,可利用多态向上转型为Entry。
② Map.Entry是Map的一个内部类
③ Entry类中有两个重要方法:getKey()
、getValue()
- key不允许重复,value可以重复。【故至多有一个key = null,可以有多个value = null】
- key和value都是Object类型,但常用String类型的实参作为key。
- Map接口的常用方法:即双列集合都有的方法
注意:CRUD中,Collection的增改为add、set;Map的增改为put、replace。
- 遍历集合:4种方法
- 获取键值对遍历:使用entrySet方法获取
Set<Entry<K, v>>
,而遍历set有两种方式。
- 获取所有键,再根据键获取值:使用keySet方法获取
Set<k>
,而遍历set有两种方式。- 细节:
- 使用put方法,如果要添加的元素在Map中已经存在,则会覆盖原来的value,等同于修改。 看源码可知replace方法底层调用了put(key, value)方法,可知put方法包含了替换。事实上确实是这样,从HashMap的源码中就可以看到。
- entrySet包含Map中所有的键值对元素,但是其不是复制元素,而是指向元素。故通过entrySet可以修改Map中元素的value。
3.1.1 HashMap类
3.1.1.1 HashMap介绍
- 类定义:继承了
AbstractMap
类,实现了Map
接口。
- 构造器:
HashMap()
:table初始容量为0,默认第一次扩容为16;默认加载因子为0.75HashMap(int initialCapacity)
:table初始容量为0,指定第一次扩容大小;默认加载因子为0.75HashMap(int initialCapacity, float loadFactor)
:table初始容量为0,指定第一次扩容大小;指定加载因子HashMap(Map<? extends K, ? extends V> m)
:将t的键值对复制到新HashTable中。- 重要字段:
table
:为一个 Node[ ] 数组类型【而HashMap中是Node[ ]】。Node是一个内部类,键值对是放在Node类实例中。Node实现了Entry接口,故Node类型可向上转型为Entry。
size
:包含Node键值对的数量。threshold
:HashMap的阈值loadFactor
:加载因子,默认为0.75。modCount
:用来实现“fail-fast”机制的(也就是快速失败)。所谓快速失败就是在并发集合中,某线程进行迭代操作时,若有其他线程对其进行结构性的修改(是结构上面的修改,而不是简单的修改集合元素的内容),这时迭代器会立马感知到,并且立即抛出ConcurrentModificationException异常,而不是等到迭代完成之后才告诉你已经出错了。【https://blog.csdn.net/a745233700/article/details/83387688】transient Set<Map.Entry<K,V>> entrySet
:存放键值对的Node向上转型为Entry后组成的集合。- 方法:
- Map接口的方法
- HashMap的方法
- 细节:增删改查时间复杂度都为 O(1)
3.1.1.2 HashMap底层结构
HashMap底层数据结构:
① 初始时是拉链法(数组 + 链表
) 的散列结构,
② 当添加元素达到某条件后链表会转为红黑树,即转为(数组 + 红黑树
)
③ 当删除元素达到某条件后红黑树会转回为链表,该操作叫做剪枝
。
- (数组 + 链表) 阶段添加元素:
- 根据key的hashCode经过某算法得到该key的hash值—>hash值经过某算法得到该元素要存放在table的索引值index。
注意两点:
- hash值并不是hashCode值,而是由hashCode经过某算法得到。
- 只要hashCode值相同,则对应的hash值、index值都相同。
- 找到table,看table[index]处是否已经存放元素。
① 若没有,则直接添加;
② 若有,则看table[index]处 并 遍历此处的链表,逐个与之比较是否key重复。如果遍历完链表都不重复,则添加到链表尾部【TreeMap是采用头插法】
;若重复,则将value值替换。- 若添加元素成功,则还需要判断是否将链表转为红黑树。
注意:
- 本节中的链表指:不包含table[index]处的结点,将其后续的结点叫做链表
- 元素重复的定义:比较两元素的key,满足以下两个条件则元素重复
- . hash值相同
- . 使用 == 比较为True;或者使用.equal比较为True
- . 对于正常的
Integer
,如果值为-128 ~ 127
, 是按值传递。但是如果HashMap的key为Integer
,那么尽管值在-128 ~ 127
,还是当做引用数据类型。- 数组table的扩容机制:
- 初始为0;
- 第一次扩容为16;
- ① 后续每次添加元素后,再判断是否扩容
(1)是否达到阈值:hashMap中元素个数(包括table 和 链表中的元素)>
阈值(0.75 * n
) 时会再次扩容【如果等到快满时再扩容会导致在高并发时效率很低】
(2)链表长度是否过长:table[index]处的链表长度>=
8时,会再扩容。
② table扩容为原来的两倍。- (数组 + 链表) 转为 (数组 + 红黑树) 的时机:添加元素后
① 当旧table容量 >= 64
(旧table指:添加元素后,若此次要扩容,是使用扩容前的table),
② 且table[index]处链表长度 >= 8
时,将该链表转为红黑树结构。- (数组 + 红黑树) 转为 (数组 + 链表) 的时机:删除元素后
① 如果红黑树根 root 为空
② 或者 root 的左子树或右子树为空,
③ 或者 root.left.left(根的左子树的左子树)为空
都会发生红黑树退化成链表。- (数组 + 红黑树) 阶段删除元素:
- 根据要删除键值对的key的hashCode经过某算法得到该key的hash值—>hash值经过某算法得到索引值index。
- 找到table,看table[index]处是否已经存放元素。
① 若无元素,则该键值对不存在,删除失败。
② 若有元素,则看table[index]处 并 遍历此处的树,逐个与之比较是否相同。相同则删除成功。
【remove(key)只看key是否相同,remove(key, value)还要看value是否相同】
【是否相同的条件和添加元素时是否重复的条件一样】
- 若删除该键值对成功,再判断是否要将红黑树转为链表结构。
- 关于删除元素时的坑:若更改了s中的内容删除,再删除以key=s的键值对,会导致前后hash值不同,从而导致index值不同,一定会删除失败。
很好的两个数组table的扩容机制案例,用IDEA调试去感受。
(1)达到阈值后扩容
public static void main(String[] args) {
HashMap hashMap = new HashMap();
/** 达到阈值后扩容
* 1. 初始时table = null
* 2. 数字1被添加进去后,table的容量变为16
* 3. 数字13被添加进去后,table具有13个元素 > 阈值12 ,故table扩容为32
* 4. 数字25被添加进去后,table具有25个元素 > 阈值24 ,故table扩容为64
* 5. 数字49被添加进去后,table具有49个元素 > 阈值48 ,故table扩容为128
*/
for (int i = 1; i <= 49; i++) {
hashMap.put(i, i);
}
}
(2)达到链表长度后扩容
import java.util.*;
public class Person{
public static void main(String[] args) {
HashMap hashMap = new HashMap();
/** 达到链表长度后扩容
* 1. 初始时table = null
* 2. 数字1被添加进去后,table的容量变为16
* 3. 数字9被添加进去后,链表长度(包含table[index]处的结点)为9 > 8 ,故table扩容为32
* 4. 数字10被添加进去后,链表长度(包含table[index]处的结点)为10 > 8 ,故table扩容为64
* 5. 数字11被添加进去后,链表长度(包含table[index]处的结点) >= 8且table容量 >= 64。故将该链表转为红黑树。
*/
for (int i = 1; i <= 11; i++) {
hashMap.put(new ABC(i), i);
}
}
}
class ABC {
public int age;
ABC(int age) {
this.age = age;
}
// 使得其有相同的hash值,故有相同的index
@Override
public int hashCode() {
return 1;
}
}
3.1.1.3 源码分析
3.1.1.3.1 put方法
3.1.1.3.2 remove方法
3.1.1.4 LinkedHashMap类
- LinkedHashMap继承了HashMap类
- 底层结构:
数组 + 链表 + 双向链表
。就是在HashMap上多加了一个双向链表。
数组 + 链表
结构的维护和HashMap一样。故也会树化等。- 添加元素的先后顺序依次采用
双向链表
相连
3.1.2 HashTable类
3.1.2.1 HashTable介绍
- 类定义:继承了
Dectionary
类,实现了Map
接口。
- 构造器:
Hashtable()
:table初始容量为11,加载因子为0.75.Hashtable(int initialCapacity)
:指定table容量。Hashtable(int initialCapacity, float loadFactor)
:指定table容量和加载因子。Hashtable(Map<? extends K, ? extends V> t)
:将t的键值对复制到新HashTable中,其table初始容量见下图,加载因子为默认的0.75
- 重要字段:
table
:为一个 Entry[ ] 数组类型【而HashMap中是Node[ ]】,Entry代表了“拉链”的节点,每一个Entry存放了一个键值对。count
:包含Entry键值对的数量。threshold
:HashTable的阈值loadFactor
:加载因子,默认为0.75。modCount
:用来实现“fail-fast”机制的(也就是快速失败)。所谓快速失败就是在并发集合中,某线程进行迭代操作时,若有其他线程对其进行结构性的修改(是结构上面的修改,而不是简单的修改集合元素的内容),这时迭代器会立马感知到,并且立即抛出ConcurrentModificationException异常,而不是等到迭代完成之后才告诉你已经出错了。【https://blog.csdn.net/a745233700/article/details/83387688】- 方法:
- Map接口的方法
- HashTable的方法
- 细节:
- key和value都不能为空。
- HashTable是线程安全的。
3.1.2.2 HashTable底层结构
- HashTable底层结构与工作原理:拉链法(
数组 + 链表
) 的散列结构。- 添加元素:
- hashCode经过某算法得到该元素的hash值—>hash值经过某算法得到其要存放在table的索引值index。
- 找到table,看table[index]处 并 遍历此处的链表,逐个比较是否与之重复。如果遍历完链表都不重复,则添加
table[index]处
;若重复,则中断不添加该元素。注意:HashMap是将元素添加到链表末尾。
- 数组table的扩容机制:
- 初始为11;
- ① 后续每次添加元素后,达到阈值后扩容:hashTable中元素个数(包括table 和 链表中的元素) > 阈值(
0.75 * n
) 时会再次扩容。
② 扩容为2 * n + 1
大小。
3.1.2.3 put()方法 底层源码分析
3.1.2.4 Properies类
继承HashTable
类,常用于存储加载配置文件后的数据。
3.1.3 TreeMap
类定义:继承了
AbstractMap
类。
构造器:
TreeMap()
:TreeSet中的元素自然排序。TreeMap(Map<? extends K, ? extends V> m)
:将集合c中元素复制到TreeSet集合中。并自然排序。TreeMap(Comparator<? super K> comparator)
:TreeSet中的元素根据指定的比较器进行排序。【通常使用匿名内部类做为形参】
TreeMap(SortedMap<K, ? extends V> m)
:将集合s中元素复制到TreeSet集合中。并TreeSet元素顺序与集合s元素顺序一致。方法:
- Map接口的方法
- TreeMap的方法
底层结构:
细节:
- 增删改查时间复杂度都为 O(logn)
- TreeMap是有序的,则使用TreeMap一定要有排序规则,故要求:
① 要么 在添加元素时,要添加元素实现了Compareable接口 (比如String、Integer都是从小到大排序)
② 要么 在创建TreeMap对象时,传入比较器- 不能插入 null 对象,否则会抛出 NullPointerException 异常
- 元素是否重复:
① 排序规则不同,调用的比较方法不同。
(1)若采用自然排序:则以添加元素的key对象实现的Compareable接口的compareTo方法去比较。【若要添加键值对的key没有实现Compareable接口,则会报异常】
(2)若采用比较器:则以比较器中的compare方法去比较。
② 根据返回结果判断是否重复: 将要添加元素的key与TreeMap中元素的key逐个传入 Compareable接口的compareTo方法 或 比较器compare方法 中。若结果为0,则重复,不添加元素,但是将值替换
;若所有比较结果都不为0,则不重复并添加改元素(其中返回值小于0,则说明该元素排在前面;若返回值大于0,则说明该元素排在后面)。
4. 以上类分别在何种情况下使用?
5. Collections工具类
全都是静态方法: