String深入解析
String具有不变性的原因:
- String被final修饰,它不可能被继承,也就是任何对String的操作方法,都不会被继承覆写
- String中保存数据的是一个char数组的value,它被final修饰,它的内存地址一旦赋值无法修改
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
}
String相等判断源码
public boolean equals(Object anObject) {
// 判断内存地址是否相同
if (this == anObject) {
return true;
}
// 待比较的对象是否是 String,如果不是 String,直接返回不相等
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
// 两个字符串的长度是否相等,不等则直接返回不相等
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
// 依次比较每个字符是否相等,若有一个不等,直接返回不相等
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
- 判断类型是否相同
- 判断长度是否相同
- 比较每一个字符是否相等
当上述3个条件均成立时,两个String相等
String的replace和replaceAll区别在于前者入参可以是char也可以是String,此外replaceAll是支持正则表达式的,效率上replace高效
Long
Long自己实现了一种缓存机制,缓存了-128到127内的所有Long,如果值在Long,则从缓存中取,不会被初始化
Long中的静态内部类实现了LongCache
private static class LongCache {
private LongCache(){}
// 缓存,范围从 -128 到 127,+1 是因为有个 0
static final Long cache[] = new Long[-(-128) + 127 + 1];
// 容器初始化时,进行加载
static {
// 缓存 Long 值,注意这里是 i - 128 ,所以再拿的时候就需要 + 128
for(int i = 0; i < cache.length; i++)
cache[i] = new Long(i - 128);
}
}
使用Long时,推荐valueOf,少用parseLong方法,因为前者如果缓存命中从缓存取值,而后者不会
static
static修饰的方法时,要注意线程安全,static修饰的工具类方法要避免使用到方法外部的公共变量。工具类方法建议使用public final static
Arrays
Arrays二分查询方法能快速定位元素
其二分查找底层如下:
// a:我们要搜索的数组,fromIndex:从那里开始搜索,默认是0; toIndex:搜索到何时停止,默认是数组大小
// key:我们需要搜索的值 c:外部比较器
private static <T> int binarySearch0(T[] a, int fromIndex, int toIndex,
T key, Comparator<? super T> c) {
// 如果比较器 c 是空的,直接使用 key 的 Comparable.compareTo 方法进行排序
// 假设 key 类型是 String 类型,String 默认实现了 Comparable 接口,就可以直接使用 compareTo 方法进行排序
if (c == null) {
// 这是另外一个方法,使用内部排序器进行比较的方法
return binarySearch0(a, fromIndex, toIndex, key);
}
int low = fromIndex;
int high = toIndex - 1;
// 开始位置小于结束位置,就会一直循环搜索
while (low <= high) {
// 假设 low =0,high =10,那么 mid 就是 5,所以说二分的意思主要在这里,每次都是计算索引的中间值
int mid = (low + high) >>> 1;
T midVal = a[mid];
// 比较数组中间值和给定的值的大小关系
int cmp = c.compare(midVal, key);
// 如果数组中间值小于给定的值,说明我们要找的值在中间值的右边
if (cmp < 0)
low = mid + 1;
// 我们要找的值在中间值的左边
else if (cmp > 0)
high = mid - 1;
else
// 找到了
return mid; // key found
}
// 返回的值是负数,表示没有找到
return -(low + 1); // key not found.
}
Arrays提供了很多parallel开头的方法,比如parallelSort方法,方法底层有判断,只有数据量大于8192时,才会真正走并行的实现。
Collections
Colletcions.max
ArrayList
ArrayList无参构造器初始化时,默认大小是空数组,当第1次add的时候数组扩容到大小为10。如果第一次初始化如果大小<10,那么初始化大小按10计算
扩容后的大小是原来的1.5倍
扩容源码如下
private void ensureCapacityInternal(int minCapacity) {
//如果初始化数组大小时,有给定初始值,以给定的大小为准,不走 if 逻辑
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
//确保容积足够
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
//记录数组被修改
modCount++;
// 如果我们期望的最小容量大于目前数组的长度,那么就扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//扩容,并把现有数据拷贝到新的数组里面去
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
// oldCapacity >> 1 是把 oldCapacity 除以 2 的意思
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果扩容后的值 < 我们的期望值,扩容后的值就等于我们的期望值
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果扩容后的值 > jvm 所能分配的数组的最大值,那么就用 Integer 的最大值
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 通过复制进行扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}
ArrayList无参构造器构造,现在add一个值进去,此处的数组大小是1,下一次扩容前最大可用大小是10。
数组初始化,初加入一个值后,如果使用addAll方法,一下子加入15个值,那么最终数组的大小是16。因为
// newCapacity 本次扩容的大小,minCapacity 我们期望的数组最小大小
// 如果扩容后的值 < 我们的期望值,我们的期望值就等于本次扩容的大小
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
toArray方法
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Integer[] a1 = new Integer[3];
Integer[] a2 = list.toArray(a1);
System.out.println(a1.hashCode());
System.out.println(a2.hashCode());
System.out.println("================================");
Integer[] a3 = new Integer[2];
Integer[] a4 = list.toArray(a3);
System.out.println(a3.hashCode());
System.out.println(a4.hashCode());
}
可以发现toArray方法,如果入参数组大小小于集合大小,那么它就会返回一个新的数组,否则返回则就是入参数组
LinkedList
从头节点增加
// 从头部追加
private void linkFirst(E e) {
// 头节点赋值给临时变量
final Node<E> f = first;
// 新建节点,前一个节点指向null,e 是新建节点,f 是新建节点的下一个节点,目前值是头节点的值
final Node<E> newNode = new Node<>(null, e, f);
// 新建节点成为头节点
first = newNode;
// 头节点为空,就是链表为空,头尾节点是一个节点
if (f == null)
last = newNode;
//上一个头节点的前一个节点指向当前节点
else
f.prev = newNode;
size++;
modCount++;
}
从头节点删除
//从头删除节点 f 是链表头节点
private E unlinkFirst(Node<E> f) {
// 拿出头节点的值,作为方法的返回值
final E element = f.item;
// 拿出头节点的下一个节点
final Node<E> next = f.next;
//帮助 GC 回收头节点
f.item = null;
f.next = null;
// 头节点的下一个节点成为头节点
first = next;
//如果 next 为空,表明链表为空
if (next == null)
last = null;
//链表不为空,头节点的前一个节点指向 null
else
next.prev = null;
//修改链表大小和版本
size--;
modCount++;
return element;
}
节点查询,如果 index 处于队列的前半部分,从头开始找,size >> 1 是 size 除以 2 的意思。否则后半部分查询
// 根据链表索引位置查询节点
Node<E> node(int index) {
// 如果 index 处于队列的前半部分,从头开始找,size >> 1 是 size 除以 2 的意思。
if (index < (size >> 1)) {
Node<E> x = first;
// 直到 for 循环到 index 的前一个 node 停止
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {// 如果 index 处于队列的后半部分,从尾开始找
Node<E> x = last;
// 直到 for 循环到 index 的后一个 node 停止
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
LinkedList实现了Queue接口,在链表为空时返回值也不太一样
因为LinkedList实现双向的迭代方向,新增了一个迭代接口:ListIterator
ArrayList有最大容量,为Integer的最大值,LinkedList底层是双向链表,理论可无限大,但源码中LinkedList实际大小用int类型,这也说明了LinkedList不能超过Integer的最大值。
HashMap
底层数据结构是:数组+链表+红黑树。其中当链表的长度大于等于8时,链表会转化成红黑树,当红黑树大小小于等于6时,红黑树转化成链表。
HashMap允许null值。
//初始容量为 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//负载因子默认值
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//桶上的链表长度大于等于8时,链表转化成红黑树
static final int TREEIFY_THRESHOLD = 8;
//桶上的红黑树大小小于等于6时,红黑树转化成链表
static final int UNTREEIFY_THRESHOLD = 6;
//当数组容量大于 64 时,链表才会转化成红黑树
static final int MIN_TREEIFY_CAPACITY = 64;
//记录迭代过程中 HashMap 结构是否发生变化,如果有变化,迭代时会 fail-fast
transient int modCount;
//HashMap 的实际大小,可能不准(因为当你拿到这个值的时候,可能又发生了变化)
transient int size;
//存放数据的数组
transient Node<K,V>[] table;
// 扩容的门槛,有两种情况
// 如果初始化时,给定数组大小的话,通过 tableSizeFor 方法计算,数组大小永远接近于 2 的幂次方,比如你给定初始化大小 19,实际上初始化大小为 32,为 2 的 5 次方。
// 如果是通过 resize 方法进行扩容,大小 = 数组容量 * 0.75
int threshold;
//链表的节点
static class Node<K,V> implements Map.Entry<K,V> {
//红黑树的节点
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
key 在数组中的位置公式:tab[(n - 1) & hash]
计算hash值时,为什么需要右移16位
hash算法是h^(h>>>16),这是为了h的高低16位都能参与计算,计算出的hash更分散
为什么把数组长度取模操作换成了&操作
取模操作处理器计算慢,处理器对&操作比较擅长
为什么提倡数组大小是2的幂次方
因为只有大小是2的幂次方,才能使hash值%n(数组大小)==(n-1)&hash
hashmap的添加元素
// 入参 hash:通过 hash 算法计算出来的值。
// 入参 onlyIfAbsent:false 表示即使 key 已经存在了,仍然会用新值覆盖原来的值,默认为 false
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// n 表示数组的长度,i 为数组索引下标,p 为 i 下标位置的 Node 值
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果数组为空,使用 resize 方法初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 如果当前索引位置是空的,直接生成新的节点在当前索引位置上
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 如果当前索引位置有值的处理方法,即我们常说的如何解决 hash 冲突
else {
// e 当前节点的临时变量
Node<K,V> e; K k;
// 如果 key 的 hash 和值都相等,直接把当前下标位置的 Node 值赋值给临时变量
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果是红黑树,使用红黑树的方式新增
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 是个链表,把新节点放到链表的尾端
else {
// 自旋
for (int binCount = 0; ; ++binCount) {
// e = p.next 表示从头开始,遍历链表
// p.next == null 表明 p 是链表的尾节点
if ((e = p.next) == null) {
// 把新节点放到链表的尾部
p.next = newNode(hash, key, value, null);
// 当链表的长度大于等于 8 时,链表转红黑树
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
// 链表遍历过程中,发现有元素和新增的元素相等,结束循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//更改循环的当前元素,使 p 在遍历过程中,一直往后移动。
p = e;
}
}
// 说明新节点的新增位置已经找到了
if (e != null) {
V oldValue = e.value;
// 当 onlyIfAbsent 为 false 时,才会覆盖值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
// 返回老值
return oldValue;
}
}
// 记录 HashMap 的数据结构发生了变化
++modCount;
//如果 HashMap 的实际大小大于扩容的门槛,开始扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
HashMap、TreeMap、LinkedHashMap三者异同
相同点:
- 在特定情况下,都使用红黑树
- 底层的hash算法相同
- 在迭代过程中,如果Map的数据结果被改动,都会报ConcurrentModificationException
不同点: - HashMap以数组为主,查询快,TreeMap以红黑树为主,利用红黑树特点进行排序,LinkedHashMap在HashMap上增加链表结构,实现顺序插入和最少访问删除
- 三者使用场景不同
- api上层略有不同
HashSet
它底层基于HashMap实现,key为HashMap的key,value为private static final Object PRESENT = new Object();
// 对 HashMap 的容量进行了计算
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
HashSet的初始大小为取最大值(期望的值 / 0.75 + 1,默认值 16),expectSize * 0.75 + 1刚好不用扩容,如果对HashMap的初始化大小值选取,可借鉴此公式。
CopyOnWriteArrayList
CopyOnWriteArrayList通过锁+数组拷贝+volatile保证线程安全,它线程安全的原因是每次操作都是在新拷贝数组上操作,当操作完成后,再把新数组引用赋值给原数组。当用迭代器操作时,如果有另一线程对CopyOnWriteArrayList进行增删操作,迭代器未结束时持有的仍是旧数组,所以它是线程安全的。
注意:volatile直接修饰数组时,当数组某一元素改变仍无法保证其可见性,只有当数组引用发生改变时才保证其可见性,这就是为什么源码setArray方法是修改数组的引用
CopyOnWriteArrayList的add方法
// 添加元素到数组尾部
public boolean add(E e) {
final ReentrantLock lock = this.lock;
// 加锁
lock.lock();
try {
// 得到所有的原数组
Object[] elements = getArray();
int len = elements.length;
// 拷贝到新数组里面,新数组的长度是 + 1 的,因为新增会多一个元素
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 在新数组中进行赋值,新元素直接放在数组的尾部
newElements[len] = e;
// 替换掉原来的数组
setArray(newElements);
return true;
// finally 里面释放锁,保证即使 try 发生了异常,仍然能够释放锁
} finally {
lock.unlock();
}
}
批量删除
// 批量删除包含在 c 中的元素
public boolean removeAll(Collection<?> c) {
if (c == null) throw new NullPointerException();
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
// 说明数组有值,数组无值直接返回 false
if (len != 0) {
// newlen 表示新数组的索引位置,新数组中存在不包含在 c 中的元素
int newlen = 0;
Object[] temp = new Object[len];
// 循环,把不包含在 c 里面的元素,放到新数组中
for (int i = 0; i < len; ++i) {
Object element = elements[i];
// 不包含在 c 中的元素,从 0 开始放到新数组中
if (!c.contains(element))
temp[newlen++] = element;
}
// 拷贝新数组,变相的删除了不包含在 c 中的元素
if (newlen != len) {
setArray(Arrays.copyOf(temp, newlen));
return true;
}
}
return false;
} finally {
lock.unlock();
}
}
源码思想借鉴:批量删除是把先找出已存在的元素组成数组,然后再把原数组引用指向新数组。避免直接循环单个元素删除造成频繁数组拷贝
ConcurrentHashMap
添加元素
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
//计算hash
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//table是空的,进行初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//如果当前索引位置没有值,直接创建
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//cas 在 i 位置创建新的元素,当 i 位置是空时,即能创建成功,结束for自循,否则继续自旋
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//如果当前槽点是转移节点,表示该槽点正在扩容,就会一直等待扩容完成
//转移节点的 hash 值是固定的,都是 MOVED
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
//槽点上有值的
else {
V oldVal = null;
//锁定当前槽点,其余线程不能操作,保证了安全
synchronized (f) {
//这里再次判断 i 索引位置的数据没有被修改
//binCount 被赋值的话,说明走到了修改表的过程里面
if (tabAt(tab, i) == f) {
//链表
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
//值有的话,直接返回
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
//把新增的元素赋值到链表的最后,退出自旋
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
//红黑树,这里没有使用 TreeNode,使用的是 TreeBin,TreeNode 只是红黑树的一个节点
//TreeBin 持有红黑树的引用,并且会对其加锁,保证其操作的线程安全
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
//满足if的话,把老的值给oldVal
//在putTreeVal方法里面,在给红黑树重新着色旋转的时候
//会锁住红黑树的根节点
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
//binCount不为空,并且 oldVal 有值的情况,说明已经新增成功了
if (binCount != 0) {
// 链表是否需要转化成红黑树
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
//这一步几乎走不到。槽点已经上锁,只有在红黑树或者链表新增失败的时候
//才会走到这里,这两者新增都是自旋的,几乎不会失败
break;
}
}
}
//check 容器是否需要扩容,如果需要去扩容,调用 transfer 方法去扩容
//如果已经在扩容中了,check有无完成
addCount(1L, binCount);
return null;
}
LinkedBlockingQueue
put方法
// 把e新增到队列的尾部。
// 如果有可以新增的空间的话,直接新增成功,否则当前线程陷入等待
public void put(E e) throws InterruptedException {
// e 为空,抛出异常
if (e == null) throw new NullPointerException();
// 预先设置 c 为 -1,约定负数为新增失败
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
// 设置可中断锁
putLock.lockInterruptibly();
try {
// 队列满了
// 当前线程阻塞,等待其他线程的唤醒(其他线程 take 成功后就会唤醒此处被阻塞的线程)
while (count.get() == capacity) {
// await 无限等待
notFull.await();
}
// 队列没有满,直接新增到队列的尾部
enqueue(node);
// 新增计数赋值,注意这里 getAndIncrement 返回的是旧值
// 这里的 c 是比真实的 count 小 1 的
c = count.getAndIncrement();
// 如果链表现在的大小 小于链表的容量,说明队列未满
// 可以尝试唤醒一个 put 的等待线程
if (c + 1 < capacity)
notFull.signal();
} finally {
// 释放锁
putLock.unlock();
}
// c==0,代表队列里面有一个元素
// 会尝试唤醒一个take的等待线程
if (c == 0)
signalNotEmpty();
}
// 入队,把新元素放到队尾
private void enqueue(Node<E> node) {
last = last.next = node;
}
来源:https://blog.csdn.net/shuangyueliao/article/details/102732126