目录
1. 手写ArrayList
1.1 ArrayList 底层原理细节
-
底层结构是一个长度可以动态增长的
数组(顺序表)
// 指向数组的引用 transient Object[] elementData; // transient修饰的变量,不参与序列化
数组特点:在内存中分配连续的空间,只存储数据,不存储地址信息
-
节省存储空间(无需存储地址和数据间的关系)、索引查询效率高(其它地址可以通过首地址计算得到)
-
插入和删除效率低,必须提前分配固定数量的空间,可能会导致空闲浪费,按照内容查询效率低
-
-
通过无参构造方法,创建对象时,JDK1.7初始长度
10
,JDK1.8初始长度为0
,在第一次添加元素时就进行扩容扩容到长度为10
(后续每次扩容1.5倍) -
提供了一个内部类
Itr
,实现了 Iterator 接口,用来对 ArrrayList 进行遍历
1.2 自定义:List、Iterator、异常处理
List
public interface List {
/**
* 返回线性表的大小,即数据元素的个数
* @return
*/
int size();
/**
* 返回线性表中序号为 i 的数据元素
* @param i
* @return
*/
Object get(int i);
/**
* 如果线性表为空返回true,否则返回false
* @return
*/
boolean isEmpty();
/**
* 判断线性表中是否包含数据元素 e
* @param e
* @return
*/
boolean contains(Object e);
/**
* 返回数据元素 e 在线性表中的序号
* @param e
* @return
*/
int indexOf(Object e);
/**
* 将数据元素 e 插入到线性表中 i 号的位置
* @param i
* @param e
*/
void add(int i, Object e);
/**
* 将数据元素 e 插入到线性表的末尾
* @param e
*/
void add(Object e);
/**
* 删除线性表中序号为 i 的元素,并返回值
* @param i
* @return
*/
Object remove(int i);
/**
* 删除线性表中第一个与 e 相同的元素
* @param e
* @return
*/
boolean remove(Object e);
/**
* 替换线性表中序号为 i 的数据元素为 e,返回原数据元素
* @param i
* @param e
* @return
*/
Object replace(int i, Object e);
/**
* 迭代List
* @return
*/
Iterator iterator();
}
Iterator
public interface Iterator<T> {
/**
* 是否还有下一个元素
* @return
*/
boolean hasNext();
/**
* 下一个返回的元素值
* @return
*/
T next();
}
IndexOutOfBoundsException
public class IndexOutOfBoundsException extends RuntimeException{
public IndexOutOfBoundsException() {
}
public IndexOutOfBoundsException(String message) {
super(message);
}
}
1.3 自定义:测试类
public class TestArrayList {
public static void main(String[] args) {
List list = new ArrayList();
list.add("1111");
list.add("aaaa");
list.add("bbbb");
list.add("3333");
list.add("2222");
list.add("1111");
list.add("aaaa");
list.add("bbbb");
list.add("3333");
list.add("2222");
// 添加第 11 个元素时,注意扩容
list.add("1111");
// 添加到指定位置,注意移动元素
list.add(3, "AAAA");
System.out.println(list.size());
System.out.println(list.isEmpty());
System.out.println(list.get(2));
System.out.println(list.contains("4444"));
System.out.println(list.indexOf("2222"));
// 注意判断查找 null 时
System.out.println(list.indexOf(null));
System.out.println(list.toString());
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
// 光标已经走到最后面,还往后走,进行报错
System.out.println(iterator.next());
}
}
1.4 自定义:ArrayList
最重要的扩容底层:
ArrayList类
package com.lwclick.list;
import java.util.Arrays;
/**
* @author LIUWEI
* @className ArrayList
* @date 2021/9/27 22:42
* @description 手写的ArrayList
*/
public class ArrayList implements List{
/**
* ArrayList底层是一个【长度可以动态增长】的数组,elementData是数组的引用
*/
private transient Object[] elementData;
/**
* 集合中存储了元素的个数,不是数组空间的容量length
* 增加、删除元素时,size的值都要发生变化
*/
private int size;
public ArrayList() {
this(10);
}
public ArrayList(int initialCapacity) {
elementData = new Object[initialCapacity];
}
@Override
public int size() {
return size;
}
@Override
public Object get(int i) {
if (i >= size || i < 0) {
throw new IndexOutOfBoundsException("数组索引越界" + i);
}
return elementData[i];
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public boolean contains(Object e) {
return indexOf(e) >= 0;
}
@Override
public int indexOf(Object e) {
int index = -1;
if (e == null) { // 为空则比较引用
for (int i = 0; i < size; i++) {
if (e == elementData[i]) {
return i;
}
}
} else { // 不为空则比较内容
for (int i = 0; i < size; i++) {
if (e.equals(elementData[i])) {
return i;
}
}
}
return index;
}
@Override
public void add(int index, Object e) {
if (elementData.length == size) {
grow();
}
// 添加到指定位置时,一定要移动元素,不然会覆盖
for (int i = size; i > index; i--) {
elementData[i] = elementData[i - 1];
}
elementData[index] = e;
size++;
}
@Override
public void add(Object e) {
// 如果数组已满,需要先扩容
if (elementData.length == size) {
grow();
}
// 添加元素到最后:初始elementData为空的,size的值也为0,所以就是 size的位置
elementData[size] = e;
// 存储的元素进行 ++
size++;
}
private void grow() {
// 1.新创建一个更大容量的数组
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // 使用右移,是为了不出现小数(右移时注意优先级)
Object[] newArr = new Object[newCapacity];
// 2.将原来数组的元素,拷贝到新数组中,索引一一对应
for (int i = 0; i < oldCapacity; i++) {
newArr[i] = elementData[i];
}
// 3.成员变量elementData指向扩容后的新数组
elementData = newArr;
// 可以使用一句话实现
// elementData = Arrays.copyOf(elementData, newCapacity);
}
@Override
public Object remove(int index) {
if (index >= size || index < 0) {
throw new IndexOutOfBoundsException("数组索引越界" + index);
}
Object e = elementData[index];
// 移动元素
int numMoved = size - index - 1;
if (numMoved > 0) {
// System.arraycopy(源数组, 从源数组的起始位置开始, 拷贝到目标数组, 到目标数组的开始位置, 复制的个数)
System.arraycopy(elementData, index + 1, elementData, index, numMoved);
}
// 最后的元素指向 null,交给GC
elementData[--size] = null;
return e;
}
@Override
public boolean remove(Object e) {
if (e == null) { // 如果为空,比较地址
for (int i = 0; i < size; i++) {
if (e == elementData[i]) {
fastRemove(i);
return true;
}
}
} else {
for (int i = 0; i < size; i++) {
if (e.equals(elementData[i])) {
fastRemove(i);
return true;
}
}
}
return false;
}
private void fastRemove(int index) {
int numMoved = size - index - 1;
if (numMoved > 0) {
System.arraycopy(elementData, index + 1, elementData, index, numMoved);
}
elementData[--size] = null;
}
@Override
public Object replace(int index, Object e) {
if (index >= size || index < 0) {
throw new IndexOutOfBoundsException("数组索引越界" + index);
}
if (e == null) {
if (e != elementData[index]) {
Object oldValue = elementData[index];
elementData[index] = e;
return oldValue;
}
} else {
if (!e.equals(elementData[index])) {
Object oldValue = elementData[index];
elementData[index] = e;
return oldValue;
}
}
return this;
}
@Override
public Iterator iterator() {
return new Itr();
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder("[");
for (int i = 0; i < size; i++) {
builder.append(elementData[i] + ",");
}
if (size > 0) {
builder.deleteCharAt(builder.length() - 1);
}
builder.append("]");
return builder.toString();
}
private class Itr<T> implements Iterator<T> {
// 定义一个光标依次指向数组
int cursor = 0;
@Override
public boolean hasNext() {
return cursor != size;
}
@Override
public T next() {
if (cursor >= size) {
throw new RuntimeException("元素已经遍历结束");
}
return (T)elementData[cursor++];
}
}
}
2. 单链表
2.1 单链表技能点
- 存储空间不连续
- 每个结点对应一个数据元素,结点由指针域和数据域组成
- 元素的逻辑关系通过结点间的链接关系体现,逻辑上相邻,物理上不一定相邻
- 按索引查询慢,存储空间大
- 插入、删除速度快
- 有元素才会分配空间,不会有闲置的结点
注:为了对空表、非空表的情况以及首元结点进行统一处理,常增加一个头结点
2.2 手写单链表
测试代码:
public static void main(String[] args) {
//创建线性顺序表
List list = new SingleLinkedList();
//向末尾添加元素
list.add("11111");
list.add("aaaaa");
list.add("bbbbb");
list.add("33333");
list.add("22222");
// 添加到指定位置
list.add(3, "AAAAA");
//进行各种操作验证添加
System.out.println(list.size());
System.out.println(list.isEmpty());
System.out.println(list.get(2));
System.out.println(list.contains("44444"));
System.out.println(list.indexOf("22222"));
System.out.println(list.toString());
}
add 操作及 get 操作的底层思想:
List类:
public interface List {
// 返回线性表的大小,即数据元素的个数。
int size();
// 返回线性表中序号为 i 的数据元素
Object get(int i);
// 如果线性表为空返回 true,否则返回 false。
boolean isEmpty();
// 判断线性表是否包含数据元素 e
boolean contains(Object e);
// 返回数据元素 e 在线性表中的序号
int indexOf(Object e);
// 将数据元素 e 插入到线性表中 i 号位置
void add(int i, Object e);
// 将数据元素 e 插入到线性表末尾
void add(Object e);
// 将数据元素 e 插入到元素 obj 之前
boolean addBefore(Object obj, Object e);
// 将数据元素 e 插入到元素 obj 之后
boolean addAfter(Object obj, Object e);
// 删除线性表中序号为 i 的元素,并返回之
Object remove(int i);
// 删除线性表中第一个与 e 相同的元素
boolean remove(Object e);
// 替换线性表中序号为 i 的数据元素为 e,返回原数据元素
Object replace(int i, Object e);
}
Node类:
public class Node {
Object data;
Node next; // 指向的元素也为结点
public Node() {
}
public Node(Object data, Node next) {
this.data = data;
this.next = next;
}
public Node(Object data) {
this.data = data;
}
// getter / setter / toString方法
}
单链表 SingleLinkedList 类:
public class SingleLinkedList implements List {
private Node head = new Node(); // 事先存在一个头结点
private int size; // 结点数量,默认是0(不包含头结点)
@Override
public int size() {
return size;
}
@Override
public Node get(int index) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException("索引越界:" + index);
}
// 定义一个 p :指向头结点
Node p = head;
// 循环去找第 index 个结点
for (int i = 0; i <= index; i++) {
p = p.next; // !!!指向下一个结点
}
return p;
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public void add(int index, Object e) {
if (index < 0 || index > size) {
throw new IndexOutOfBoundsException("索引越界" + index);
}
// p 首先指向头结点
Node p = head;
if (index > 0) {
// 让指针移动到第 index 个结点
p = get(index - 1);
}
// 新建一个结点,并加入数据
Node node = new Node(e);
// 指定该新结点指向下一个结点的地址,也就是p指向的下一个结点的地址
node.next = p.next;
// 修改p指向的下一个结点的地址,为该新结点
p.next = node;
// 结点数量++
size++;
}
@Override
public void add(Object e) {
add(size, e);
}
@Override
public Object remove(int index) {
if (index < 0 || index > size) {
throw new IndexOutOfBoundsException("索引越界" + index);
}
Node p = head;
if (index > 0) {
p = get(index - 1);
}
// 将指向第 index 个结点先存起来
Node currentNode = p.next;
// 第 index 的结点指向下一个结点的地址,直接改为第 index + 1 个结点指向下一个结点的地址
p.next = p.next.next;
// 存起来的结点指向下一个结点的地址,直接指向 null
currentNode.next = null;
size--;
return null;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder("[");
/** 直接调用get,相当于频繁的去查找 */
// for (int i = 0; i < size; i++) {
// Node node = get(i);
// builder.append(node.data + ",");
// }
Node p = head;
for (int i = 0; i < size; i++) {
// 顺藤摸瓜
p = p.next; // p 指向第 i个结点
builder.append(p.data + ",");
}
if (size != 0) {
builder.charAt(builder.length() - 1);
}
return builder.toString();
}
}
3. LinkedList
3.1 底层原理
底层是一个双向链表(添加、删除元素效率高,按索引查询效率低)
同时实现了 List、Deque、Queue接口,所以可以当作线性表、队列、双端队列、栈来用
不存在容量不足的问题
3.2 手写LinkedList
LinkedList类:
public class LinkedList implements List {
transient int size = 0; // 有几个结点
transient Node first; // 指向链表的第一个结点(类似于C的指针,并不是结点对象)
transient Node last; // 指向链表的最后一个结点
// 内部类,Node 结点类
private static class Node<E> {
E item; // 数据
Node<E> next; // 指向下一个结点
Node<E> prev; // 指向前一个结点
public Node(Node<E> prev, E item, Node<E> next) {
this.item = item;
this.next = next;
this.prev = prev;
}
@Override
public String toString() {
return "Node{item=" + item + ", next=" + next + '}';
}
}
@Override
public int size() {
return size;
}
@Override
public Object get(int i) {
Node node = node(i);
return node.item;
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public void add(int i, Object e) {
if (i == size) {
linkLast(e);
} else {
linkBefore(e, node(i));
}
}
/**
* 查询指定索引的结点
* @param index
* @return
*/
private Node node(int index) {
if (index < (size >> 1)) { // 前一半结点,从第一个结点开始向后查询
Node p = first;
for (int i = 0; i < index; i++)
p = p.next;
return p;
} else { // 后一半结点,从后边向前
Node l = last;
for (int i = size - 1; i > index; i--)
l = l.prev;
return l;
}
}
@Override
public void add(Object e) {
linkLast(e);
}
// 在最后面插入数据!!!!
private void linkLast(Object e) {
final Node l = last;
final Node newNode = new Node(l, e, null);
last = newNode;
if (l == null) {
first = newNode;
} else {
l.next = newNode;
}
size++;
}
// 在最前面插入数据
private void linkBefore(Object e, Node succ) {
final Node pred = succ.prev;
final Node newNode = new Node(pred, e, succ);
succ.prev = newNode;
if (pred == null) {
first = newNode;
} else {
pred.next = newNode;
}
size++;
}
}
分析:linkLast
添加结点时的内存分配图
4. 手写HashMap
4.1 底层原理
-
底层结构是哈希表,采用了 顺序表+链表 结合结构,同一个链表上的所有元素的存储地址都是相同的,是发生冲突的元素
-
链表上每个节点就是一个 Entry,字段包括四部分
同一个链表上节点的哈希码可能不同,存储地址(主数组的索引)相同
-
哈希表的优点:
- 添加快、查询快(通过计算得到存储位置,而不是比较)
- 无序,key 唯一
-
HashMap 关键参数
-
默认主数组长度为 16
-
默认装填因子 0.75 (当存储元素达到
数组长度 * 装填因子
时,就需要扩容) -
每次主数组扩容为原来的 2 倍
-
JDK 8,当链表长度大于 8 时,链表变为红黑树
-
4.2 手写 HashMap
Map接口
public interface Map {
/**
* 放置元素
* @param key
* @param value
*/
void put(Object key, Object value);
/**
* 获取元素
* @param key
* @return
*/
Object get(Object key);
/**
* 储存元素个数
* @return
*/
int size();
/**
* 是否为空
* @return
*/
boolean isEmpty();
interface Entry {
/**
* 获取 Entry的Key值
* @return
*/
Object getKey();
/**
* 获取 Entry的 value
* @return
*/
Object getValue();
}
}
TestMap测试类
public static void main(String[] args) {
Map map = new HashMap();
map.put(23,"China");
map.put(36,"Japan");
map.put(48,"America");
map.put(86,"the United States");
map.put(67,"France");
map.put(23,"Italian");
map.put(47,"England");
System.out.println(map.size());
System.out.println(Arrays.toString(map.table));
System.out.println(map);
}
HashMap 类
public class HashMap implements Map{
/** 指向哈希表【主数组】的引用变量(数组的名字)*/
transient Entry[] table;
/** 哈希表中节点的数量(链表节点的个数) */
transient int size = 0;
public HashMap() {
this(16);
}
public HashMap(int capacity) {
table = new Entry[capacity]; // !!! JDK7中,初始化时,不管指定多大的容量都会处理为 2^n
}
/** 链表中节点类 */
class Entry implements Map.Entry {
// key值,final修饰,不可变
final Object key;
// value值
Object value;
// 指向下一个节点
Entry next;
// key的哈希码
int hash;
public Entry(int hash, Object key, Object value, Entry next) {
this.key = key;
this.value = value;
this.next = next;
this.hash = hash;
}
@Override
public Object getKey() {
return key;
}
@Override
public Object getValue() {
return value;
}
@Override
public String toString() {
if (next != null) {
return "Entry{" +
"key=" + key +
", value=" + value +
", next=" + next +
", hash=" + hash +
'}';
} else {
return "Entry{" +
"key=" + key +
", value=" + value +
", hash=" + hash +
'}';
}
}
}
@Override
public void put(Object key, Object value) {
// 计算哈希码
int hash = key.hashCode();
// 根据哈希码,计算存储位置(主数组的索引)
int index = hash % table.length;
// 存入指定位置
if (table[index] == null) { // 情况1: 存储位置为空,直接放入即可
table[index] = new Entry(hash, key, value, null);
size++;
} else {
// 如果指定的位置不为空,发生了冲突,需要逐个比较 // 情况2:使用新的 value 覆盖旧的value
Entry entry = table[index]; // 指向链表的第一个节点
while (entry != null) {
// 进行 key 的比较,如果找到了相同的key
if (entry.hash == hash && (entry.key == key || key.equals(entry.key))) {
// 使用新的value,替换旧的value
entry.value = value;
return;
}
// 指向 链表的下一个节点
entry = entry.next;
}
// 情况3:如果发生了冲突,并且没有相同的 key,添加一个新的节点【到链表的首节点】
Entry firstEntry = table[index];
table[index] = new Entry(hash, key, value, firstEntry);
size++;
}
}
@Override
public Object get(Object key) {
// 计算哈希码
int hash = key.hashCode();
// 根据哈希码,计算索引位置(主数组的索引)
int index = hash % table.length;
// 查找对应的 Entry
Entry entry = null;
if (table[index] != null) { // 情况2: table[index] 不为空,找到了key
Entry currentEntry = table[index];
while (currentEntry != null) {
if (currentEntry.hash == hash && (currentEntry.key == key || key.equals(currentEntry.key))) {
entry = currentEntry;
break;
}
// 指向链表的下一个节点
currentEntry = currentEntry.next;
}
// 情况3:没找到对应的key,entry仍然为 null
}
// 情况1: table[index] 为 null
return entry == null ? null : entry.getValue();
}
@Override
public int size() {
return size;
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder("{");
for (int i = 0; i < table.length; i++) {
if (table[i] != null) {
Entry entry = table[i]; // 指向索引为 i 的链表的第一个节点
do {
builder.append(entry.key + "=" + entry.value + ",");
entry = entry.next;
} while (entry != null);
}
}
if (size > 0) {
builder.deleteCharAt(builder.length() - 1);
}
builder.append("}");
return builder.toString();
}
}
5. 手写 HashSet
- HashSet 底层就是 HashMap,哈希表的每个 Entry 的key是 HashSet 中的每个元素,而 Entry 的value统一为 new Object()
Set接口
public interface Set {
int size();
void add(Object obj);
boolean isEmpty();
boolean contains(Object obj);
}
TestHashSet测试类
public static void main(String[] args) {
HashSet set = new HashSet();
set.add("JavaSE");
set.add("MySQL");
set.add("JavaScript");
set.add("JavaSE");
System.out.println(set.size());
System.out.println(set.contains("JavaSE"));
System.out.println(set.contains("JavaSe"));
System.out.println(set);
}
HashSet类
public class HashSet implements Set {
private HashMap map;
private static final Object PRESENT = new Object();
public HashSet() {
map = new HashMap<>();
}
@Override
public int size() {
return map.size();
}
@Override
public void add(Object obj) {
map.put(obj, PRESENT);
}
@Override
public boolean isEmpty() {
return map.isEmpty();
}
@Override
public boolean contains(Object obj) {
return map.get(obj) != null;
}
}
6. 新一代并发集合类
6.1 三代集合类发展过程
-
第一代线程安全集合类
Vector、HashTable(使用 synchronized 修饰方法)
-
第二代线程非安全集合类(主流)
ArrayList、HashMap(线程不安全,但是性能好)
如果要保证线程安全 ==》 使用
Collections.synchronizedList(list)
,Collections.synchronizedMap(m)
(底层使用 synchronized 代码块锁,但是锁在方法里面) -
第三代线程安全集合类
java.util.concurrent.*;
ConcurrentHashMap
CopyOnWriteArrayList
CopyOnWriteArraySet (注意:底层为 list)
底层大都采用 Lock锁(JDK1.8的 ConcurrentHashMap 不使用 Lock锁)
代码说明:
public static void main(String[] args) {
// 第一代 同步方法锁 锁住的是this 效率底下
Vector v = new Vector();
// 第二代 同步代码块 锁住的也是 this
List list = new ArrayList();
list = Collections.synchronizedList(list);
// 第三代 大都是 Lock 锁
ConcurrentHashMap chm; // 1.8 不是
CopyOnWriteArrayList cwal; // 可以当作 ArrayList 使用
}
6.2 新一代并发集合类
-
ConcurrentHashMap:分段(segment)锁定 + Lock锁
使用多个锁来控制对 hash 表的不同部分(段 segment)进行的修改,采用ReentrantLock 锁来实现,如果多个操作发生在不同的段上,它们就可以并发的进行
JDK1.8中
:摒弃了Segment的概念,采用了volatile + CAS
实现无锁化操作(底层由 数组+链表+红黑树 的方式,锁哈希表的链表中的首元素) -
CopyOnWriteArrayList: CopyOnWrite + Lock 锁
对于写操作(set()、add()、remove()等
每个操作都复制一份数组,修改完成后改变数组的引用
)使用 ReentrantLock 的 lock 和 unlock 来加锁和解锁,读操作不需要加锁只能保证数据的最终一致性,不能保证数据的实时一致性(读的是旧数组中的元素)
-
CopyOnWriteArraySet
与 HashMap 及 HashSet 的方式类似,只是底层结构变为了数组,所以与 ArrayList 性质类似
但是ArrayList 要求元素有序、不唯一,而 CopyOnWriteArraySet 要求元素顺序无所谓,唯一,所以在添加元素时,需要先判断是否已经有了元素