Java核心知识集合
Posted 烟锁迷城
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java核心知识集合相关的知识,希望对你有一定的参考价值。
1、List接口
1.1、ArrayList
初始设定参数,可以看到,ArrayList本质是一个数组。
- 默认长度为10
- 空数组
- 默认长度空数组
- 数据数组
- 元素个数
//默认数组长度
private static final int DEFAULT_CAPACITY = 10;
//空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
//默认长度的空元素数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//存储数据元素的数组
transient Object[] elementData; // non-private to simplify nested class access
//集合中元素个数
private int size;
初始化操作
- 无参构造:就是将默认长度空数组赋予存储数组
- 有参构造:根据传入的当前数组长度进行判断,大于0就正常创建,等于0就赋予空数组,小于0就抛出异常
//无参构造
public ArrayList() {
//将默认的空数组赋予存储数组
//相当于this.elementData = {}
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//有参构造,赋予ArrayList初始长度
public ArrayList(int initialCapacity) {
//如果初始长度大于0,创造一个新的Object数组
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
//如果等于0,赋予默认空数组
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
//如果小于0,抛出异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
添加方法(add)
- ensureCapacityInternal(size + 1)检测当前数组长度是否会越界,然后赋予数组数值。
- ensureCapacityInternal方法中,查看当前数据数组是否为空,若为空,就取得默认长度和当前元素个数+1之间比较大的那个,若不为空,直接传入ensureExplicitCapacity方法中
- ensureExplicitCapacity方法中,modCount为操作次数,来自于AbstractList,执行自增。如果元素个数比数据数组长度大,执行grow函数
- grow方法中,旧长度是数据数组长度,
int newCapacity = oldCapacity + (oldCapacity >> 1);
新长度是旧长度加上旧长度的右位移1位的运算结果,其实可以看做1.5倍取整。如果新长度比需求长度小,就取需求长度。如果新长度比MAX_ARRAY_SIZE(MAX_ARRAY_SIZE 是Integer的最大值-8)大,就执行hugeCapacity方法获取新长度。最后,将数据数组复制到一个长度为新长度的新数组中。 int newCapacity = oldCapacity + (oldCapacity >> 1);
就是1.5倍扩容的根据
public boolean add(E e) {
//检测当前数组长度,动态扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
//将数据放入数组
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
//如果这是一个空数组
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//取得默认数组长度与传入长度参数比较大的那个
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
//赋予新的函数
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
//操作次数,自增,来自于AbstractList
modCount++;
// 如果元素个数比数据数组长度大,执行grow函数
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
//旧长度是数组元素长度
int oldCapacity = elementData.length;
//新长度为旧长度加上旧长度的右位移运算结果(就是旧长度的一半,结果取整)
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果新长度比需求长度小,就将需求长度赋予新长度
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//如果新长度比MAX_ARRAY_SIZE大,执行hugeCapacity方法
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//复制数组到长度为新长度的新数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
获取方法(get)
- rangeCheck检测数组下标越界
- elementData根据下标获取对应的数组数据
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
E elementData(int index) {
return (E) elementData[index];
}
更新方法(set)
- rangeCheck检测数组下标越界
- elementData根据下标获取对应的数组数据
- 数组数据替换,返回旧数据
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
移除方法(remove)
- rangeCheck检测数组下标越界
- 操作次数自增,elementData根据下标获取对应的数组数据
- numMoved为要移动元素的个数
- 使用数组移动,将需要移除的数组位覆盖
- 将最后一位数组位移除,返回旧数据。
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
FailFast机制
快速失败机制,Java集合类为了应对并发访问在集合迭代的过程中,内部结构发生变化的一种防护机制,这种错误检查机制为这种可能发生错误通过抛出java.util.ConcurrentModificationException错误来快速失败。
源码示例:
- expectedModCount预期操作次数被modCount操作次数赋予
- checkForComodification方法检测,实际操作次数是否与预期操作次数不符合,如果不符合,就代表其他线程也对这个List进行过操作,造成了其内部结构的改变。
int expectedModCount = modCount;
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
1.2、LinkedList
LinkedList是通过双向链表实现的,因此他的数据结构具有双向链表的优缺点,因此顺序查找有优势,随机查找有劣势,其关键的双向链表实现类为:
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;
}
}
添加方法(add,push)
添加方法有两种,add和push
- push是一个链表的添加操作,添加到头部。
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
- add是一个链表的添加操作。添加到尾部
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
获取方法(get)
- 二分查找,从如果比中位数小,那就从头开始遍历,反之从末尾开始遍历
Node<E> node(int index) {
//二分查找,从如果比中位数小,那就从头开始遍历,反之从末尾开始遍历
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
修改方法(set)
- 检查下表是否合法
- 获取对应的数据
- 记录原来的数值,赋予新的值,返回原来的数值
public E set(int index, E element) {
//检查下表是否合法
checkElementIndex(index);
//获取对应的数据
Node<E> x = node(index);
//记录原来的数值
E oldVal = x.item;
//赋予新的值
x.item = element;
//返回原来的数值
return oldVal;
}
1.3、Vector
类似ArrayList使用动态数组进行存储,线程安全,但已经不常用。
Vector在每一个操作方法中都增加了synchronized关键字,保证线程安全,会消耗很多性能。
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
如果想要在线程不安全的情况下使用ArrayList,可以使用Collections.synchronizedList(list);
转换为线程安全的List。
2、Set接口
2.1、HashSet
HashSet实现Set接口,由哈希表支持。
- 数据结构为哈希表
- 一个没有重复元素的集合
- 不保证Set的迭代顺序,不保证顺序永久不变
- 允许null作为元素
- 通过HashMap实现
在初始化代码中,可以看到是初始化了一个HashMap
public HashSet() {
map = new HashMap<>();
}
add方法中,会把数据放到key值里,在value中放入一个自定义Object对象。
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
2.2、TreeSet
TreeSet实现Set接口。
- 基于TreeMap的NavigableMap实现
- 使用元素的自然顺序进行排序,或者根据创建set时提供的Comparator进行排序
在初始化代码里可以看到,TreeSet是将数据保存在TreeMap中
public TreeSet() {
this(new TreeMap<E,Object>());
}
TreeSet(NavigableMap<E,Object> m) {
this.m = m;
}
add方法中,会把数据放到key值里,在value中放入一个自定义Object对象。
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
3、Map接口
- 能存储唯一的列的数据(Key唯一,不可重复)Set
- 能存储可以重复的数据(Value可重复)List
- 值得顺序取决于键的顺序
- 键和值都是可以存储null元素的
3.1、TreeMap
TreeMap本质上是红黑树的实现
红黑树节点源码如下:
static final class Entry<K,V> implements Map.Entry<K,V> {
K key;//key
V value;//value
Entry<K,V> left;//左子节点
Entry<K,V> right;//右子节点
Entry<K,V> parent;//父节点
boolean color = BLACK;//根节点默认黑色
}
put方法
- 赋予局部变量根节点root,检测是否为跟节点
- 若根节点为空,检查key是否为空,建立新节点,将key,value放入
- 若根节点不为空,进入比较器判断
- 循环判断,将t赋予父节点,key与t节点的key比较大小,小于则将父节点的左子节点赋给t,大于则将父节点的右子节点赋给t,相等就直接修改数值,直到t为空
- 此时t就是要插入节点的父节点
- 根据之前key的比较结果,小于父节点则插入的节点在父节点的左侧,大于父节点则插入的节点在父节点的右侧
- fixAfterInsertion实现红黑树的平衡
public V put(K key, V value) {
//赋予局部变量根节点root
Entry<K,V> t = root;
//检测是否为跟节点
if (t == null) {
//检查key是否为空
compare(key, key);
//建立新节点,将key,value放入
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
//父节点
Entry<K,V> parent;
//比较器
Comparator<? super K> cpr = comparator;
//如果比较器不为空
if (cpr != null) {
do {
//将t赋予父节点
parent = t;
//key与t节点的key比较大小
cmp = cpr.compare(key, t.key);
if (cmp < 0)
//将父节点的左子节点赋给t
t = t.left;
else if (cmp > 0)
//将父节点的右子节点赋给t
t = t.right;
else
//相等就直接修改数值
return t.setValue(value);
} while (t != null);
}
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
//t就是要插入节点的父节点
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
//插入的节点在父节点的左侧
parent.left = e;
else
//插入的节点在父节点的右侧
parent.right = e;
//实现红黑树的平衡
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
fixAfterInsertion红黑树平衡
此处处理和红黑树平衡方式一样,不再赘述。
private void fixAfterInsertion(Entry<K,V> x) {
//将节点设置为红色
x.color = RED;
//循环的条件是添加的条件不为空
//不是root节点
//父节点是红色
while (x != null && x != root && x.parent.color == RED) {
//父节点是否为祖父节点的左侧节点
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
//获取父节点的兄弟节点
Entry<K,V> y = rightOf(parentOf(parentOf(x)));
//叔叔节点是红色
if (colorOf(y) == RED) {
//设置父节点颜色为黑色
setColor(parentOf(x), BLACK);
//设置叔叔节点颜色为黑色
以上是关于Java核心知识集合的主要内容,如果未能解决你的问题,请参考以下文章