第一节 . ArrayList
一、 ArrayList
特点:
在内存中分配连续的空间,只存储数据,不需要存储地址信息,位置就隐含着地址。
优点:
1.节省存储空间,因为分配给数据的存储单元全用于存放节点的数据(不考虑C/C++语言中数组需指定大小的情况),节点之间的逻辑关系没有占用额外的存储空间。
素。
2.索引查找效率高,即每一个节点对应一个序号,由该序号可以直接计算出来节点的存储地址。
缺点:
1.增加和删除元素需要移动元素,效率较低。
2.必须提前分配固定数量的空间,如果存储元素少,可能导致空间浪费。
3.按照内容查询效率低,以为需要逐个比较判断。
二、ArrayList底层实现细节
1.底层使用动态增长的数组实现。
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
2.通过无参数构造方法创造对象时,JDK1.7初始长度是10。JDK1.8初始长度是0,在第一次添加元素时在给定长度10。
3.当容量不足时,扩容增加50%。
int newCapacity = oldCapacity + (oldCapacity >> 1);
4.提供了一个内部类Itr,实现了Iterator接口,用来对ArrayList进行遍历
public Iterator iterator() {
return new Itr();
}
private class Itr implements Iterator {
}
手写List接口
/**
* 手写List接口
*/
public interface List {
//返回线性表的大小,即数据元素的个数
public int size();
//返回现象表中序号为i的数据元素
public Object get(int i);
//如果线性表为空返回true,否则返回false
public boolean isEmpty();
//判断线性表是否包含数据元素e
public boolean contain(Object e);
//返回数据元素e在线性表中的序号
public int indexOf(Object e);
//将线性元素e插入到线性表中序号i的位置
public void add(int i,Object e);
//将数据元素e插入到线性表的末尾
public void add(Object e);
//将数据元素e插入到元素obj之前
public boolean addBefore(Object obj,Object e);
//删除线性表中序号为i的元素,并返回
public Object remove(int i);
//删除线性表中第一个与e相同的元素
public boolean remove(Object e);
//替换线性表中序号为i的数据元素为e,返回原数据元素
public Object replace(Object e);
public Iterator iterator();
}
手写Iterator接口
/**
*手写Iterator接口
*/
public interface Iterator<T> {
boolean hasNext();
T next();
}
自定义指针越界异常
/**
*自定义指针越界异常
*/
public class IndexOutOfBoundsException extends RuntimeException {
public IndexOutOfBoundsException() {
}
public IndexOutOfBoundsException(String message) {
super(message);
}
}
手写ArrayList
import java.util.Arrays;
import java.util.NoSuchElementException;
/**
* 手写ArrayList
*/
public class ArrayList implements List {
private transient Object[] elementData;//ArrayList底层是一个长度可以动态增长的数组
private int size;//集合中元素的个数,不是数组分配了多少个空间
public ArrayList(){
this(10);
}
public ArrayList(int initialCapacity){
if(initialCapacity<0){
throw new IllegalArgumentException("初始化长度必须大于等于零"+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 contain(Object e) {
return indexOf(e)!=-1;
}
@Override
public int indexOf(Object e) {
int index=-1; //表示不存在
for (int i = 0; i <size ; i++) {
if(elementData[i].equals(e)){
index=i;
break;
}
}
return index;
}
@Override
public void add(int i, Object e) {
if(size==elementData.length){
//数组的扩容
elementData= Arrays.copyOf(elementData,size<<1);//一句话解决
}
//后移后面的元素
for(int index=size-1;index>=i;index--){
elementData[index+1]=elementData[index];
}
//添加元素
elementData[i]=e;
//数量加1
size++;
}
@Override
public void add(Object e) {
if(size==elementData.length){
// System.out.println("扩容了");
// //创建一个新数组,长度是原来的2倍
// Object[] newArray=new Object[size<<1];
// //将就数组的内容都写入到新数组中
// for (int i = 0; i <size ; i++) {
// newArray[i]=elementData[i];
// }
// //数组引用指向新数组
// elementData=newArray;
elementData=Arrays.copyOf(elementData,size<<1);//一句话解决
}
//添加元素
elementData[size]=e;
//数量加1
size++;
}
@Override
public boolean addBefore(Object obj, Object e) {
return false;
}
@Override
public Object remove(int index) {
Object oldValue = elementData[index];
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // Let gc do its work
return oldValue;
}
@Override
public boolean remove(Object e) {
return false;
}
@Override
public Object replace(Object e) {
return null;
}
@Override
public Iterator iterator() {
return new Itr();
}
private class Itr<T> implements Iterator<T>{
int cursor=0;
@Override
public boolean hasNext() {
return cursor<size;
}
@Override
public T next() {
int i=cursor;
if(cursor>=size){
throw new NoSuchElementException("没有这个元素了,cusor:"+i);
}
cursor++;
return (T)elementData[i];
//取出光标指向的元素
//return (T)elementData[cursor++];//注意两点:1.取完元素光标后移一位;2.类型的强制转换,因为elementData里面的元素是Object类型的
}
}
@Override
public String toString() {
StringBuilder builder=new StringBuilder("[");
for (int i = 0; i < size; i++) {
builder.append(elementData[i]+",");
}
if(size>0){ //为什么size>0呢?避免在没有元素的时候,把左侧的大括号砍掉
builder.deleteCharAt(builder.length()-1);
}
builder.append("]");
return builder.toString();
}
}
ArrayList测试
/**
*ArrayList测试
*/
public class TestArrayList {
public static void main(String[] args) {
java.util.ArrayList list2;
//list2.toString();
//list2.iterator();
//创建线性顺序表
List list = new ArrayList();
//向末尾添加元素
list.add("11111");
list.add("aaaaa");
list.add("bbbbb");
list.add("33333");
list.add("22222");
list.add(3, "AAAAA");
//进行各种操作验证添加
System.out.println(list.get(30));
System.out.println(list.size());
System.out.println(list.isEmpty());
System.out.println(list.contains("11111"));
System.out.println(list.indexOf("22222"));
System.out.println(list.toString());
Iterator<String> it = list.iterator();
while(it.hasNext()){
String elem = it.next();
System.out.println(elem);
}
// it.next();
}
}
第二节 手写SingleLinkedList
一.单链表特点
单链表特点:
1.数据元素的存储是不连续的存储空间,每个存储节点对应一个需要存储的数据元素
2.每个节点是由数据域和指针域组成。元素之间的逻辑关系通过存储节点之间的链接关系反应出来。逻辑上相邻的节点物理上不一定相邻。
3.数据元素的存储对应的是不连续的存储空间,每个存储结点对应一个需要存储的数据元素。
4.每个结点是由数据域和指针域组成。 元素之间的逻辑关系通过存储节点之间的链接关系反映出来。逻辑上相邻的节点物理上不必相邻。
5.添加节点
6.删除节点
二.带头节点的单链表
在使用单链表实现线性表的时候,为了使程序更加简洁,我们通常在单链表的最前面添加一个哑元结点,也称为头结点。在头结点中不存储任何实质的数据对象,其 next 域指向线性表中 0 号元素所在的结点,可以对空表、非空表的情况以及对首元结点进行统一处理,编程更方便,常用头结点。
一个带头结点的单链表实现线性表的结构图如图 所示:
SingleLinkedList(带头节点实现的单链表)的优缺点:
缺点:
1.比顺序存储结构的存储密度小,(每个节点都有数据域和指针域组成,所以相同空间内假设全存满的话,顺序比链式存储的更多)。
2.查找节点时链式存储要比顺序存储慢(每个节点地址不连续、无规律、导致按照索引查询效率低下)
优点:
1.插入、删除灵活(不必移动节点,只要改变节点中的指针,但是需要先定位到元素上)
2.有元素才会分配节点空间,不会有闲置的节点。
List的接口(见上面这里省略)
定义单链表节点
/**
* 单链表的一个节点
*/
public class Node {
Object data;//存储数据
Node next;//指向下一个节点的引用变量
public Node(){
//super(); //这里不写也相当于有了,调用父类无参构造方法
}
public Node(Object data) {
this.data = data;
}
public Node(Object data, Node next) {
this.data = data;
this.next = next;
}
@Override
public String toString() {
return "Node{" +
"data=" + data +
", next=" + next +
'}';
}
}
实现单链表
/**
*手写单向链表
*/
public class SingleLinkedList implements List {
private Node head=new Node();//头节点,不存储数据 初始创建 用来指向第一个初始节点
private int size;//节点的数量,元素的个数
@Override
public int size() {
return size;
}
@Override
public Object get(int i) { //根据索引查找第i个元素
//按照索引添加元素,只能从头开始,顺藤摸瓜
Node p=head;
//从头开始,以此指向下一个节点,最终定位在第i个节点
for (int index = 0; index <= i; index++) {
//指向下一个节点
p=p.next;
}
return p;
//return p.data; 这个是得到p节点上的数据
}
@Override
public boolean isEmpty() {
return size==0;
}
@Override
public boolean contain(Object e) {
return false;
}
@Override
public int indexOf(Object e) {
return 0;
}
@Override
public void add(int i, Object e) {
//1.请先找到第i-1个节点
Node previousNode=head;
if(i>0){
previousNode=(Node)get(i-1);
}
//2.添加节点
//新建一个节点 并指向下一个节点
Node newNode=new Node(e,previousNode.next);
//前一个节点指向新的节点
previousNode.next=newNode;
//3.数量加1
size++;
}
//添加链表最后一个元素,就是想索引为size的地方添加元素
@Override
public void add(Object e) {
this.add(size,e);
}
@Override
public String toString() {
StringBuilder builder=new StringBuilder("[");
//指向索引是0的节点,第一个节点
Node p=head.next;
//从第0个节点开始,以此指向下一个节点,最终定位在第i个节点
for (int index = 0; index < size ; index++) {
if(index==size-1){
builder.append(p.data);
}else{
//取出其值
builder.append(p.data+",");
}
//指向下一个节点
p=p.next;
}
builder.append("]");
return builder.toString();
}
@Override
public boolean addBefore(Object obj, Object e) {
return false;
}
@Override
public Object remove(int i) {
Node previousNode=head;
//1.先找到i-1个
if(i>0){
previousNode=(Node)get(i-1);
}
//2.删除第i个元素
//2.1指针指向第i个节点
//Node currentNode=previousNode.next;
//2.2前一个节点指向后一个节点
previousNode.next=previousNode.next.next;
//2.3当前节点不再指向后一个节点
previousNode.next.next.next=null;
//数量减1
size--;
return null;
}
@Override
public boolean remove(Object e) {
return false;
}
@Override
public Object replace(Object e) {
return null;
}
@Override
public Iterator iterator() {
return null;
}
}
测试单链表
public class TestSingleLinkedList {
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");
list.remove(4);
//进行各种操作验证添加
//System.out.println(list.get(3));
//System.out.println(list.get(0));
System.out.println(list.size());
System.out.println(list.isEmpty());
// System.out.println(list.contains("11111"));
// System.out.println(list.indexOf("22222"));
System.out.println(list.toString()); //[11111,aaaaa,bbbbb,... 22222]
}
}
第三节 手写LinkedList
1.LinkedList底层原理
1.LinkedList底层是一个双向链表;添加、删除元素效率高;按照索引查询效率低。可以两个方向查询。
2.每一个节点都包含一个对前一个和后一个元素的引用
3.LinkedList同时实现了List、Deque、Queue接口,所以可以当做线性表、队列,双端队列,栈使用。
4.LinkedList是非同步的(线程不安全的)
5.不存在LinedkList容量不足的问题
定义List接口(略)
定义LinkedList
/**
*双向链表源代码阅读
*/
public class LinkedList implements List{
transient int size=0; //一共有几个元素
transient Node first; //指向双向链表的第一个元素
transient Node last;//指向双向链表的最后一个元素
//双向链表的一个节点
private static class Node<E> {
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;
}
}
@Override
public int size() {
return size;
}
@Override
public Object get(int i) {
return node(i).item;
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public void add(int index, Object element) {
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
void linkBefore(Object e, Node succ) {
// assert succ != null;
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++;
}
@Override
public void add(Object e) {
linkLast(e);//添加到最后
}
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++;
}
/**
* 查询指定索引的节点
* @param index
* @return 返回的是一个Node
*/
Node node(int index) { //20个元素 找第3个 和第16个 很大的区别
// assert isElementIndex(index);
if (index < (size >> 1)) { //前一半从0开始向后找
Node x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else { //后一半,从后向前找
Node x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
}
测试LinkedList
public class TestList {
public static void main(String[] args) {
//创建线性顺序表
LinkedList list = new LinkedList();
//向末尾添加元素
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.toString());
System.out.println(list.get(0));
}
}
底层内存分析图:
第四节 手写HashMap
1.HashMap原理
1.底层结构是哈希表,采用了顺序表+链表结合结构;同一个链表的上所有元素的存储地址都是相同的,是发生冲突的元素。
2.链表上每个节点的就是一个Entry,字段包括四部分。
hash(哈希码) key(键) value(值) next(指向下一个Entry节点)
3.哈希表的优点:添加快、查询快(通过计算得到存储位置,不是通过比较)。无序、(key)唯一。
4.关键参数: 默认主数组长度16;默认装填因子0.75。每次主数组扩容为原来的2倍。JDK8,当链表长度大于8,链表变成红黑树。
5.第一步计算哈希码,不仅调用了hashCode(),有进行了多次散列。目的在于key不同,哈希码尽量不同,减少冲突。
6.细节:发生冲突,经过比较不存在相同key的元素,要添加一个新的节点。不是加到链表最后,而是添加到链表最前。
**说明:**第一步计算哈希码,不仅调用了HashCode(),又进行了多次散列,目的在于key不同,哈希码尽量不同,减少冲突。
定义HashMap接口
/**
* 定义一个Map接口
*/
public interface Map {
//定义方法
public void put(Object key, Object value);
public Object get(Object key);
public int size();
public boolean isEmpty();
//定义内部接口
interface Entry {
public Object getKey();
public Object getValue();
}
}
手写HashMap
/**
*手写HashMap
*/
public class HashMap implements Map{
static final int DEFAULT_INITIAL_CAPACITY = 16; //默认的主数组的长度
transient Entry[] table; //这是一个数组的引用 用来指向一个数组 (即指向哈希表的主数组的) 数组的元素都是Entry
transient int size;//键值对的数量 Entry的数量
/**
* int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1; 如果指定的初始长度不是2的幂,会转化成2的幂
*/
public HashMap(){
table=new Entry[DEFAULT_INITIAL_CAPACITY]; //创建了一个长度为16的数组
}
@Override
public void put(Object key, Object value) {
//1.计算哈希码
int hash=key.hashCode();
//计算存储位置
int index=hash%table.length; //源码里面用的位运算,位运算效率更高
//存储到指定的位置
if(table[index]==null){
table[index]=new Entry(key,value,null,hash);
size++;
}else{ //该位置已经存放了元素,要首先查询是否存在相同key的Entry
Entry entry=table[index]; //指向链表的第一个元素
while(entry!=null){
if(entry.getKey().equals(key) && entry.hash==hash){ //找到了
//使用新的value覆盖就得value
entry.value=value;
//退出方法
return;
}
//指向下一个节点
entry=entry.next;
}
//如果没有相同的key,添加一个新的节点,到链表的第一个位置
Entry firstEntry=table[index];
table[index] = new Entry(key,value,firstEntry,hash);
size++;
}
}
@Override
public Object get(Object key) {
//1.计算哈希值
int hash=key.hashCode();
//2.计算存储位置
int index=hash%table.length;
//3.去指定的位置查找
Entry e=null; //默认不存在
if(e!=null){ //该位置有元素
Entry entry=table[index]; //链表指向第一个元素
while(entry!=null){
//比较
if(entry.hash==hash && entry.getKey().equals(key)){
e=entry;
break;
}
//指向下一个节点
entry=entry.next;
}
}
return e==null?null:e.getValue();
}
@Override
public String toString() {
StringBuilder builder=new StringBuilder("{");
for (int i = 0; i < table.length ; i++) {
if(table[i]!=null){
Entry entry=table[i];
while(entry!=null){
builder.append(entry.key+"="+entry.value+",");
//指向下一个节点
entry=entry.next;
}
}
}
if(size!=0){
builder.deleteCharAt(builder.length()-1);
}
builder.append("}");
return builder.toString();
}
@Override
public int size() {
return size;
}
@Override
public boolean isEmpty() {
return size==0;
}
class Entry implements Map.Entry{
final Object key; //key 这个用final修饰,必须得给赋一次值,我们这里在构造方法里面给它赋值
Object value;
Entry next;
int hash;
public Entry(Object key, Object value, Entry next, int hash) {
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() {
return "Entry{" +
"key=" + key +
", value=" + value +
", next=" + next +
", hash=" + hash +
'}';
}
}
}
测试HashMap
public class TestHashMap {
public static void main(String[] args) {
HashMap map = new HashMap();
map.put(23,"Italian");
map.put(47,"England");
map.put(23,"China");
map.put(36,"Japan");
map.put(48,"America");
map.put(86,"the United States");
map.put(67,"France");
System.out.println(map.size());
System.out.println(map.get(23));//Entry ---China
System.out.println(Arrays.toString(map.table));
System.out.println(map.toString());
}
}
第五节. 手写HashSet
1.底层就是HashMap,哈希表的每个Entry的key是每个元素,value统一都是new Object()。
定义Set接口
public interface Set {
public int size();
public void add(Object obj);
public boolean isEmpty();
public boolean contains(Object obj);
}
手写HashSet
public class HashSet implements Set {
private transient HashMap map;//底层有一个HashMap的引用,指向一个哈希表
private static final Object PRESENT = new Object();//Set中向map中存放键值对,value统一的是PRESENT
public HashSet() {
map = new HashMap();
}
public void add(Object obj){
// map.put(obj,new Object());//每次创建一个新的Object对象
map.put(obj,PRESENT);//每次指向同一个Object对象
}
@Override
public int size() {
return map.size();
}
@Override
public boolean isEmpty() {
return map.size()==0;
}
@Override
public boolean contains(Object obj) {
return map.get(obj) != null;
}
}
第六节.新一代集合并发类
第一代 线程安全集合类
Vector、Hashtable
是怎么保证线程安排的: 使用synchronized修饰方法
缺点:效率低下
第二代 线程非安全集合类(主流)
ArrayList、HashMap
线程不安全,但是性能好,用来替代Vector、Hashtable
使用ArrayList、HashMap,需要线程安全怎么办呢?
使用
Collections.synchronizedList(list);
Collections.synchronizedMap(m);
底层使用synchronized代码块锁
虽然也是锁住了所有的代码,但是锁在方法里边,并所在方法外边性能可以理解为稍有提高吧。毕竟进方法本身就要分配资源的 。
第三代 线程安全集合类(波浪式前进,螺旋式上升)
在大量并发情况下如何提高集合的效率和安全呢?
java.util.concurrent.*
ConcurrentHashMap:
CopyOnWriteArrayList :
CopyOnWriteArraySet:注意 不是CopyOnWriteHashSet
底层均采用Lock锁,保证安全的同时,性能也很高。
1.ConcurrentHashMap: 分段(segment)锁定+Lock锁
HashMap的线程安全班,并且性能比Hashtable、Collections.synchronizedMap(m);都有提高。使用的不是synchronized代码块锁,也不是synchronzied方法锁。并且使用了锁分离技术,使用多个锁来控制对hash表的不同部分(段segment)进行的修改,采用ReentrantLock锁来实现。如果多个修改操作发生在不同的段上,他们就可以并发进行,从而提高了效率。JDK1.7和JDK1.8的关于ConcurrentHashMap的实现差异较大,以上理论属于JDK1.7;
ConcurrentHashMap在JDK8中进行了巨大改动。它摒弃了Segment(锁段)的概念,而是启用了一种全新的方式实现,利用CAS算法。 它底层由"数组"+链表+红黑树的方式思想(JDK8中HashMap的实现), 为了做到并发,又增加了很多辅助的类,例如TreeBin,Traverser等对象内部类。
2.CopyOnWriteArrayList :CopyOnWrite+Lock锁 。
对于set()、add()、remove()等方法使用ReentrantLock的lock和unlock来加锁和解锁 ,读操作不需要加锁(之前集合安全类,即使读操作也要加锁,保证数据的实时一致)
CopyOnWrite原理:写时复制。 通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。对于读操作远远多于写操作的应用非常适合,特别在并发情况下,可以提供高性能的并发读取。 CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。
注意:读的时候不需要加锁,如果读的时候有多个线程正在向ArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的ArrayList。
3.CopyOnWriteArraySet:CopyOnWrite+Lock锁
它是线程安全的无序的集合,可以将它理解成线程安全的HashSet。有意思的是,CopyOnWriteArraySet和HashSet虽然都继承于共同的父类AbstractSet;但是,HashSet是通过"散列表(HashMap)"实现的,而CopyOnWriteArraySet则是通过"动态数组(CopyOnWriteArrayList)“实现的,并不是散列表。
CopyOnWriteArraySet在CopyOnWriteArrayList 的基础上使用了Java的装饰模式,所以底层是相同的。 而CopyOnWriteArrayList本质是个动态数组队列,所以CopyOnWriteArraySet相当于通过通过动态数组实现的"集合”!
CopyOnWriteArrayList中允许有重复的元素;但是,CopyOnWriteArraySet是一个集合,所以它不能有重复集合。
因此,CopyOnWriteArrayList额外提供了addIfAbsent()和addAllAbsent()这两个添加元素的API, 通过这些API来添加元素时,只有当元素不存在时才执行添加操作!