这部分在做算法题和数据相关内容非常重要,
最重要的是ArrayList、HashSet和HashMap需要看源码。
数组的缺点:长度固定;
而集合长度不固定,功能丰富;
集合可以动态保存任意多个对象,提供增删改查操作对象的方法
集合框架图
单列集合-Collection-单个元素;双列集合-Map-存放键值对key value;


Collection接口常用方法
add,remove,isEmpty,addAll,clear,contains查找元素是否存在,size元素个数等(后续详细讲解);
遍历元素的两种方法
java
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Test {
public static void main(String[] args) {
//泛型写法,规定存的数据类型,右边可以省略
List<String> list = new ArrayList<>();
list.add("Tom");
list.add("Jack");
list.add("Rose");
// 遍历方式1:Iterator迭代器(是接口),用于遍历Collection集合中的元素
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) { // 判断是否还有下一个元素
String str = iterator.next(); // 返回下一个元素并且指针后移
System.out.println(str);
}
// 遍历方式2:增强for循环
// 底层对于Collection仍然是Iterator
for (String str : list) { // 元素类型 变量名 : 集合名/数组名
System.out.println(str);
}
}
}
List接口和常用方法
List接口是Collection接口的子接口,List集合类中元素有序且可重复;
每个元素都有对应顺序索引,可以使用Collection的遍历方法
List常用方法有继承自Collection,也有自己的List方法
(ArrayList的方法= 继承自Collection的方法 + 继承自List的方法 + ArrayList特有方法)
java
//存字符串类型
List<String> list1 = new ArrayList<>();
list1.add("jack"); //添加一个元素到集合末尾(存String对象)
list1.add("tom");
list1.addAll(listn); //把另一个集合的所有元素追加到末尾
list1.remove("tom"); //按对象删除元素
list1.add(1, "amy"); //在索引为1位置插入对象(原索引1及后面元素后移)
list1.set(1, "lily"); //替换
System.out.println(list1.contains("jack")); //true判断集合中是否包含某个元素
System.out.println(list1.size()); //返回集合中元素个数
System.out.println(list1.isEmpty()); //判断集合是否为空
System.out.println(list1.get(1)); //按索引取出对应元素(从0开始)
System.out.println(list.indexOf("jack")); //返回指定元素第一次出现的索引
System.out.println(list1.subList(1, 3)); //截取子列表,左闭右开
//toArray()把集合转换成指定类型的数组
Object[] obj = list1.toArray();
String[] str = list1.toArray(new String[0]); //自动创建合适长度
for (String s : str) {
System.out.println(s);
}
//存Integer整数类型
List<Integer> list2 = new ArrayList<>();
list2.add(20); //添加一个元素到集合末尾(存Integer对象)
list2.add(10); //自动装箱相当于list2.add(Integer.valueOf(10))
list2.remove(0); //删除索引=0的对象
其他方法
clear()清空集合中的所有元素;retainAll(Collection c)只保留指定元素其余都删除;removeAll(Collection c)删除当前集合中所有指定元素;
ArrayList类
长度可变,有序,可重复,支持索引访问,可以放入空值null且可以多个,是线程不安全的没有synchronized,底层是动态数组;
常用方法参考List;
java
ArrayList<Object> arraylist = new ArrayList<>();
arraylist.add(null);
!!ArrayList底层结构和源码!!
非常重要
ArrayList中存在Object类型的数组elementData,创建ArrayList对象如使用无参构造器,则elementData初始容量为0,第一次添加扩容为10,再次扩容为1.5倍;
如使用指定大小的构造器,则elementData初始容量为指定大小,this.elementData = new Object[capacity],直接每次扩容为1.5倍;
ArrayList源码如下:(step into函数直接列出来了)(JDK新版源码有优化)
java
//无参构造器扩容源码
public ArrayList() { //创建空elementData数组
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 确定是否需要扩容
elementData[size++] = e; // 将元素放入数组,并将size加1
return true; // 添加成功返回true
}
private void ensureCapacityInternal(int minCapacity) {//确定minCapacity
// 如果当前数组是空的(初始状态)
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 第一次扩容为10和当前所需容量之间取最大值
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); //10
}
ensureExplicitCapacity(minCapacity); // 进入具体的显式容量检查
}
private void ensureExplicitCapacity(int minCapacity) { //10
modCount++; // 记录集合被修改次数
// 如果所需最小容量大于当前数组长度,则进行扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity); //真正扩容的方法
}
private void grow(int minCapacity) { //真正扩容的方法
int oldCapacity = elementData.length; //0
//扩容机制:新容量 = 旧容量 + 旧容量右移一位/2 = 1.5倍旧容量
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity; //10
if (newCapacity - MAX_ARRAY_SIZE > 0) //数组最大限制
newCapacity = hugeCapacity(minCapacity);
//拷贝原数组数据到扩容后的新数组中
elementData = Arrays.copyOf(elementData, newCapacity);
}
Vector类
继承了AbstractList,实现了List接口,是线程安全的有synchronized,Vector过时的原因:性能低,设计老是java1.0的产物;
java
protected Object[] elementData; //底层也是对象数组
Vector<Object> vector = new Vector<>(); //默认容量10,然后2倍扩
vector.add();
//扩容关键源码
//2倍扩容,capacityIncrement是容量增量固定大小
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);

LinkedList类
有序,可重复,底层实现了双向链表和双端队列,可添加任意元素包括null,线程不安全;
双向链表:first和last分别指向首尾,每个节点Node对象,头尾添加删除效率高,随机访问慢(查询慢,增删快);

java
LinkedList<Object> list = new LinkedList<>(); //remove默认删除第一个,可以用遍历
list.add(1);
list.add(2);
//add源码-初始化public LinkedList(){} first = null,last = null;
//执行add方法-执行linkLast方法
void linkLast(E e) {
final Node<E> l = last; // 暂存当前的尾节点
// 创建新节点,prev指向l=last,next为null
final Node<E> newNode = new Node<>(l, e, null);
last = newNode; // 更新尾节点指针为新节点
if (l == null)
first = newNode; // 如果原链表为空,则新节点也是头节点
else
l.next = newNode; // 否则让旧尾节点的next指向新节点
size++; // 链表长度加 1
modCount++; // 修改计数加 1
}
大部分情况以查询为主,会选择ArrayList;增删较多选LinkedList;
Set接口和常用方法
无序(添加和取出顺序不一定一致),没有索引,不允许重复元素,最多包含一个null
Set接口也是Collection接口的子接口,其方法可以用,遍历方法可以用迭代器和增强for,但是普通for索引不可以用;
最常用功能:去重,快速判断某个元素是否存在;
java
Set<Object> set = new HashSet<>();
set.add("tom");
set.add("jack");
set.add(null);
System.out.println(set); //无序的,取出顺序不同但固定,元素不能重复
//Set的两种遍历方法
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
Object obj = iterator.next();
System.out.println(obj);
}
for(Object obj : set){ //普通for循环不能用
System.out.println(obj);
}
HashSet类及其常用方法
实现Set接口,HashSet底层是HashMap,可以存null,不能有重复元素,无序(不保证元素的顺序和取出一致),没有索引,允许一个null;
查询和增删通常较快
java
HashSet<Object> set1 = new HashSet<>();
set1.add(new Dog("tom"));
set1.add(new Dog("tom")); //new相同名字的不同对象可以添加
set1.add(new String("abc"));
set1.add(new String("abc")); //不能添加
HashSet<String> set = new HashSet<>();
System.out.println(set.add("Java")); // true
System.out.println(set.add("Python")); // true
System.out.println(set.add("Java")); // false
set.add("abc");
set.add("eee");
set.remove("eee");
System.out.println(set.contains("Java")); //判断集合中是否包含某个元素
System.out.println(set.size()); //返回集合中元素个数
System.out.println(set.isEmpty()); //判断集合是否为空
Object[] obj = set.toArray();
String[] str = set.toArray(new String[0]); //把集合转成数组(指定/不指定数据类型)
for (String s : str) {
System.out.println(s);
}
其他方法:clear()清空集合;addAll()把另一个集合中的元素全部加入当前集合;removeAll()删除当前集合中指定元素;retainAll()只保留指定元素;
!!HashSet底层结构和源码!!
非常重要
底层是 HashMap=数组+链表+红黑树
添加一个元素时先得到hash值 → 转成索引值,找到存储表table看索引位置是否已经存放元素,没有则直接加入,有元素则调用equals比较,如果相同则放弃添加,如果不同则添加到后面(链表);

扩容机制:第一次初始化是16,临界值12,到达临界值会2倍扩容到16*2=32,新的临界值是24,以此类推;
判断扩容是++size,只要加入节点Node到达临界值12(链表末尾/table表都算);
如果一条链表元素到达TREEIFY_THRESHOLD(默认8),并且table大小到达MIN_TREEIFY_CAPACITY(默认64),会进行树化(Node变成TreeNode-红黑树);如果一条链表到达8但是table没到达64,则对table进行2倍扩容;
java
HashSet hashSet = new HashSet();
//初始化16空间,存入index=3位置,key="java",value=PRESENT
hashSet.add("java");
//经计算存入index=9位置,key="php",value=PRESENT
hashSet.add("php");
//冲突情况:
hashSet.add("java");
//HashSet源码解读-add
public HashSet(){
map = new HashMap<>();
}
public boolean add(E e) { //执行add方法,e = "java"
//static final Object PRESENT = new Object();共享静态对象
return map.put(e, PRESENT)==null; //返回true成功
}
public V put(K key, V value) {//执行put方法 key="java" value=PRESENT
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) { //hash方法 计算key的hash值
int h; //使用高16位与低16位异或,减少哈希碰撞
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//putVal方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i; //辅助变量
//table是HashMap的一个数组,类型是Node[]
if ((tab = table) == null || (n = tab.length) == 0)
//resize方法-扩容
//1.负责初始化内存空间newCap = DEFAULT_INITIAL_CAPACITY = 16
//2.计算阈值newThr = 容量*负载因子 = 12,超过阈值则扩容2倍
//3.Node[] newTab = new Node[newCap];并赋给table
n = (tab = resize()).length; //此时table初始化为16个位置
//key计算的hash值 计算key存放到table表的哪个索引位置p
//p为null表示位置还未存放元素,则创建Node存入
//该位置已有元素会进入else{
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null); //key="java" value=PRESENT
else {
Node<K,V> e; K k;
//情况1-p指向当前索引位置3对应链表的第一个元素
//若p和传入key的hash值相同且key equals相同/同一对象,则不能加入
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//情况2-判断p是不是一颗红黑树,如果是有单独方法添加元素
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//情况3-与已有每个链表元素循环比较
//全部不同则存入该位置的链表末端,有相同则不能加入break
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
//链表末端创建Node存入
p.next = newNode(hash, key, value, null);
//如果该链表满8个结点(且table大于64)则调用treeifyBin方法
//对当前链表树化(转红黑树),否则先table扩容解决链表过长
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e; //p循环往链表下一个元素移动
}
}
if (e != null) { //暂存旧值
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue; //添加失败在这里返回不加modCount
}
}
++modCount; //修改次数
if (++size > threshold) //如果超过阈值12则resize方法扩容
resize();
afterNodeInsertion(evict); //空方法
return null; //返回null表示添加成功
}
可以重写equals方法,自己定义HashSet判断相同/同一对象的标准
java
public class HashSet01 { //要求创建Employee对象放入HashSet当name和age值相同
public static void main(String[] args) { //认为是相同员工不能放入HashSet
HashSet h1 = new HashSet("tom", 19);
HashSet h2 = new HashSet("jark", 29);
HashSet h3 = new HashSet("tom", 19);
//不重写的情况下加入3个,因为是不同对象不同hash值(不符合要求)
//重写equals和hashCode实现
}
}
class Employee{
private String name;
private int age;
//构造器和getter和setter省略
//重写equals() and hashCode()实现:相同员工的hash值和equals相同
@Override
public boolean equals(Object o) {
if(this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Employee employee = (Employee) o;
return age == Employee.age && Objects.equals(name, Employee.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
LinkedHashSet
是HashSet的子类,底层是LinkedHashMap(是HashMap的子类),底层维护了数组+双向链表;根据元素的hashCode值决定元素的存储位置,双向链表使其实现取出和插入顺序一致,after指向下一个结点,before指向前一个结点,不能添加重复元素;

维护hash表(有head和tail)和双向链表
添加元素时先计算hash值,再求索引确定元素在table的位置,加入双向链表,已存在则不添加
存数据的结点类型LinkedHashMap$Entry
Map接口和常用方法
保存具有映射关系的数据key-value(双列元素),可以是任何引用类型的数据,封装存放到HashMap$Node对象中,key不许重复-替换机制,key只能一个为null,value可重复可以多个null;没有索引,通常无序;
常用String类作为key,k-v单向一对一关系,通过get方法传入key会返回对应value;
源码看HashSet部分,不保证存入取出顺序一致,底层是hash表存储;
java
import java.util.HashMap;
import java.util.Map;
public class Test {
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
map.put("01", "tom"); //添加元素
map.put("02", "jack");
map.put("01", "amy"); //会替换掉tom,等同于修改
//Map接口的遍历方法
//1.先取key
Set<String> keyset = map.keySet(); //取出所有的key放在一个set集合
for(String key : keyset){
System.out.println(key + "-" + map.get(key));
}
//2.只取value
Collection<String> values = map.values();
for(String value : values){
System.out.println(value);
}
//通过entrySet获取
//Map.Entry<String, String>获取一组键值对
//map.entrySet()的作用把所有键值对作为Entry对象取出,放入一个Set集合
Set<Map.Entry<String, String>> entrySet = map.entrySet();
for(Map.Entry<String, String> entry : entrySet){
System.out.println(entry.getKey() + "-" + entry.getValue());
}
}
}
//源码中底层存放位置,创建一个节点对象
//HashMap$Node node = newNode(hash, key, value, null);
一对key-value存放在一个Node对象中,为了方便遍历,Map可以通过entrySet集合的Map.Entry对象返回键值对,HashMap$Node实现了Map.Entry;

HashMap的常用方法
HashMap没有实现同步,线程不安全
java
Map<String, String> map = new HashMap<>();
map.put("01", "Tom"); //添加一组键值对(如果key已存在则覆盖)
map.put("02", "Jack");
map.put("03", "Amy");
System.out.println(map.get("01")); //Tom根据key获取对应的value
System.out.println(map.remove("01")); //根据key删除一组键值对
System.out.println(map.containsKey("01")); //判断map中是否包含某个key
System.out.println(map.containsValue("Tom")); //判断map中是否包含某个value
System.out.println(map.size()); //map中键值对个数
System.out.println(map.isEmpty()); //判断map是否为空
//根据key取值value,如果key不存在则返回默认值
System.out.println(map.getOrDefault("01", "默认值"));
其他方法:clear()清空所有键值对;putAll()把另一个Map的所有键值对放入当前Map中;
HashMap底层机制和源码
HashMap底层 数组+链表+红黑树;table表,每一个是HashMap$Node对象;
扩容机制和HashSet相同,源码相同

java
Map map = new HashMap();
map.put("01", "tom");
map.put("02", "jack");
map.put("01", "amy");
//源码
//1.执行构造器new HashMap();初始化加载因子0.75;初始化HashMap$Node[] table = null;
//2.执行put方法(key="01",value="tom")通过key计算hash值
//3.执行putVal方法
//3.1 resize扩容 Node[] newTab = new Node[newCap];
//3.2 计算key存放到table表的索引位置------中间省略完全一致
//相同key的情况下-进else-key的hash值相同且key内容相同-break
//e!=null 会替换value值 e.value=value
数组和集合的比较
数组是固定长度,创建时要确认;集合优势是不固定长度和丰富的功能;
区别是数组既可以存基本数据类型(int, double)又可以存对象(引用类型),集合只能存对象(引用类型)会自动装箱(int→Integer)
ArrayList存单个元素,底层动态数组,有序允许重复;
HashSet存单个元素,底层是哈希表,无序元素不能重复;
HashMap存键值对key-value,底层是哈希表,无序key唯一;
java
int[] b = new int[5];
int[] a = {25, 13, 15};
Arrays.sort(a);
List<String> list = new ArrayList<>();
list.add("写代码");
Set<Integer> userIds = new HashSet<>();
userIds.add(1001);
Map<String, String> studentMap = new HashMap<>();
studentMap.put("202301", "张三");
最常用的三种集合类型比较
| 特性 | ArrayList | HashSet | HashMap |
|---|---|---|---|
| 底层结构 | 动态数组 | 哈希表 (其实是包裹了一个 HashMap) | 哈希表 (数组+链表+红黑树) |
| 存储形式 | 单个元素 (Value) | 单个元素 (Value) | 键值对 (Key-Value) |
| 顺序性 | 有序 (按插入顺序排列) | 无序 | 无序 (Key 无序) |
| 唯一性 | 允许重复 | 元素唯一 (不可重复) | Key 唯一,Value 可重复 |
| 操作类型 | ArrayList | HashSet | HashMap |
|---|---|---|---|
| 添加 | add(E e) |
add(E e) |
put(K key, V value) |
| 获取 | get(int index) |
N/A (通常用增强 for) | get(Object key) |
| 删除 | remove(int index) |
remove(Object o) |
remove(Object key) |
| 判断存在 | contains(Object o) |
contains(Object o) |
containsKey(key) / containsValue(v) |
| 大小 | size() |
size() |
size() |
| 清空 | clear() |
clear() |
clear() |
| 遍历特有 | list.forEach(e -> ...) |
set.iterator() |
map.keySet(), map.entrySet() |