String深入解析

String具有不变性的原因:

  1. String被final修饰,它不可能被继承,也就是任何对String的操作方法,都不会被继承覆写
  2. 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;
}
  1. 判断类型是否相同
  2. 判断长度是否相同
  3. 比较每一个字符是否相等

    当上述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三者异同

相同点:

  1. 在特定情况下,都使用红黑树
  2. 底层的hash算法相同
  3. 在迭代过程中,如果Map的数据结果被改动,都会报ConcurrentModificationException

    不同点:
  4. HashMap以数组为主,查询快,TreeMap以红黑树为主,利用红黑树特点进行排序,LinkedHashMap在HashMap上增加链表结构,实现顺序插入和最少访问删除
  5. 三者使用场景不同
  6. 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;
}

java源码解析的更多相关文章

  1. 【Java源码解析】Thread

    简介 线程本质上也是进程.线程机制提供了在同一程序内共享内存地址空间运行的一组线程.对于内核来讲,它就是进程,只是该进程和其他一下进程共享某些资源,比如地址空间.在Java语言里,Thread类封装了 ...

  2. Java源码解析——集合框架(三)——Vector

    Vector源码解析 首先说一下Vector和ArrayList的区别: (1) Vector的所有方法都是有synchronized关键字的,即每一个方法都是同步的,所以在使用起来效率会非常低,但是 ...

  3. Java源码解析——集合框架(二)——ArrayBlockingQueue

    ArrayBlockingQueue源码解析 ArrayBlockingQueue是一个阻塞式的队列,继承自AbstractBlockingQueue,间接的实现了Queue接口和Collection ...

  4. [Java源码解析] -- String类的compareTo(String otherString)方法的源码解析

    String类下的compareTo(String otherString)方法的源码解析 一. 前言 近日研究了一下String类的一些方法, 通过查看源码, 对一些常用的方法也有了更透彻的认识,  ...

  5. [java源码解析]对HashMap源码的分析(二)

    上文我们讲了HashMap那骚骚的逻辑结构,这一篇我们来吹吹它的实现思想,也就是算法层面.有兴趣看下或者回顾上一篇HashMap逻辑层面的,可以看下HashMap源码解析(一).使用了哈希表得“拉链法 ...

  6. java源码解析——Stack类

    在java中,Stack类继承了Vector类.Vector类和我们经常使用的ArrayList是类似的,底层也是使用了数组来实现,只不过Vector是线程安全的.因此可以知道Stack也是线程安全的 ...

  7. java源码解析之Object类

    一.Object类概述   Object类是java中类层次的根,是所有类的基类.在编译时会自动导入.Object中的方法如下: 二.方法详解   Object的方法可以分成两类,一类是被关键字fin ...

  8. Java源码解析——集合框架(五)——HashMap源码分析

    HashMap源码分析 HashMap的底层实现是面试中问到最多的,其原理也更加复杂,涉及的知识也越多,在项目中的使用也最多.因此清晰分析出其底层源码对于深刻理解其实现有重要的意义,jdk1.8之后其 ...

  9. Java源码解析——集合框架(四)——LinkedListLinkedList原码分析

    LinkedList源码分析 LinkedList也和ArrayList一样实现了List接口,但是它执行插入和删除操作时比ArrayList更加高效,因为它是基于链表的.基于链表也决定了它在随机访问 ...

随机推荐

  1. mysql8.0版本忘记root密码

    1.先关掉系统服务 net stop mysql 2.进入mysql安装目录的bin文件中,以管理员的方式运行cmd,然后输入如下命令,实现无密码登陆 mysqld --console --skip- ...

  2. [C++] C++中的宏定义详解

    转载自:C++中的宏定义 和 C++宏定义详解 一.#define解析     #define是C语言中提供的宏定义命令,其主要目的是为程序员在编程时提供一定的方便,并能在一定程度上提高程序的运行效率 ...

  3. [vue] vue服务端渲染nuxt.js

    初始化 使用脚手架工具 create-nuxt-app 快速创建 npx create-nuxt-app <项目名> npx create-nuxt-app 执行一些选择 在集成的服务器端 ...

  4. java数据结构——红黑树(R-B Tree)

    红黑树相比平衡二叉树(AVL)是一种弱平衡树,且具有以下特性: 1.每个节点非红即黑; 2.根节点是黑的; 3.每个叶节点(叶节点即树尾端NULL指针或NULL节点)都是黑的; 4.如图所示,如果一个 ...

  5. koa和exprsss区别

    koa和exprsss区别 koa没有内置中间件 express有几个内置的中间件,如express.static()//加载静态资源 koa不再有req,res请求,它是封装在context里面 c ...

  6. 基于Spark的电影推荐系统(电影网站)

    第一部分-电影网站: 软件架构: SpringBoot+Mybatis+JSP 项目描述:主要实现电影网站的展现 和 用户的所有动作的地方 技术选型: 技术 名称 官网 Spring Boot 容器 ...

  7. 认识Airflow的DAG

    前文Airflow的第一个DAG已经跑起来了我们的第一个任务. 本文就来丰富这个任务. 回顾我们的任务内容 我们定义了DAG的名称为Hello-World, 这个叫dag_id, 补充说明descri ...

  8. Spring 梳理-MVC-前端控制器DispatchServlet及URL请求处理过程

    大多数基于java的web框架,都有前端控制器 spring mvc请求过程 1. 请求会首先发送到DispatchServlet,这是spring的前置Servlet,它会接收请求并转发给sprin ...

  9. linux 查看文件大小命令

    1.# ls -l (k) ls -l total -rw-r----- root root Oct : catalina.--.log -rw-r----- root root Oct : cata ...

  10. java架构之路-(分布式zookeeper)zookeeper真实使用场景

    上几次博客,我说了一下Zookeeper的简单使用和API的使用,我们接下来看一下他的真实场景. 一.分布式集群管理✨✨✨ 我们现在有这样一个需求,请先抛开Zookeeper是集群还是单机的概念,下面 ...