Java集合总览

news2025/1/18 10:49:34

1.总览

Java中的集合分List、Set、Queue、Map 4种类型。

List:大多数实现元素可以为null,可重复,底层是数组或链表的结构,支持动态扩容

Set:大多数实现元素可以为null但只能是1个,不能重复,

2.List

2.1 ArrayList

ArrayList继承AbstractList实现List接口,底层是对象数组,Object[]

可以有多个null,初始大小为10,每次扩容为原容量的1.5倍,

注意:通过下表查找元素效率高,查询要便利所有、插入、删除要大量移动元素效率低

2.2 LinkedList

LinkedList继承AbstractSequentialList实现List、Deque接口,底层是Node链表,可以双向遍历

可以有多个null,初始化没有没有初始任何存储,当有元素进来时,默认从last节点插入,构造链表

注意:通过下标查找元素效率低要遍历到下标位置,查询要遍历所有,插入头和尾很快,插入和删除索引位置需要先遍历到那里再插入和删除

2.3 Vector(线程安全)

Vector继承AbstractList实现List接口,底层和ArrayList一样也是对象数组,Object[]

它和ArrayList的唯一区别是: 它是线程安全的,实现的方式是每个有线程安全风险的方法上添加 synchronized 进行同步

2.4 Stack(线程安全)

Stack继承Vector,是个先进后出的结构,底层和ArrayList一样也是对象数组,Object[]

2.5 CopyOnWriteArrayList(线程安全)

CopyOnWriteArrayList实现了List接口,底层和ArrayList一样也是对象数组,volatile Object[] array,注意这里用volatile进行了修饰,保证当array引用更改时,所有线程都能获取到最新的引用。

CopyOnWriteArrayList原理:读支持多线程并发读,写时复制原则

set(index, e) 过程:

  1. 先加锁,
  2. 获取原数组引用,
  3. 获取原值,
  4. 原值和新值不同时拷贝新数组,新数组值更新,更新array指向,返回旧值
  5. 原值和新值相同是,更新array指向(还是原来的),返回旧值(还是原来的)
  6. 解锁

add(e)过程:

  1. 先加锁,
  2. 获取原数组引用,
  3. 拷贝新数组,新数组值更新,更新array指向,返回true,解锁

3.Set

3.1 HashSet

HashSet继承AbstractHashSet实现Set接口,底层实际是HashMap或LinkedHashMap,在底层数据结构是:Node[]数组+单链表+红黑树

可以为null,但只会有一个null,初始大小为16,加载因子是0.75,扩容时是2的倍数

当调用HashSet的add方法时,实际是调用HashMap的put方法,key是add的值,value是一个共享的Object变量。

注意:因为不是线程安全的,调用size方法,返回的size可能会和真实元素个数不一致

3.2 LinkedHashSet

LinkedHashSet继承HashSet,它的代码很少,因为它借助HashSet的LinkedHashMap为底层实现,完成插入顺序的HashSet

其余都和HashSet一致

3.3 TreeSet

TreeSet继承AbstractSet实现NavigableSet接口,和HashSet相比:

  1. 集合种的元素不保证插入排序,默认使用元素自然排序,可以自定义排序器
  2. jdk1.8之后,集合中的元素不可以为null

TreeSet使用TreeMap实现,底层用的是红黑树。

注意:与HashSet相比,TreeSet的性能稍低,add、remove、search等操作时间复杂度为O(log n),按照顺序打印n个元素为O(n)

3.4 CopyOnWriteArraySet(线程安全)

CopyOnWriteArraySet继承AbstractSet,底层使用的是CopyOnWriteArrayList,

当加入的数据不存在时,再添加

3.5 ConcurrentSkipListSet(线程安全)

ConcurrentSkipListSet继承了AbstractSet实现了NavigableSet接口,底层使用ConcurrentNavigableMap实现,和HashSet使用HashMap实现一致

数据不能为null,使用ConcurrentNavigableMap的key存储值,Boolean.TRUE最为ConcurrentNavigableMap的value

注意:此类线程安全且有序,可以传递一个排序器

4.Queue

Queue是一种先进先出的数据结构,形如我们现时生活中的排队,第一个到的排在最前面,后面到的依次向后排,第一个处理完后处理第二个。

Deque是在Queue的基础上支持从尾部取数据的功能,Deque能适合更多的场合

4.1ArrayDeque

ArrayDeque继承AbstractCollection实现了Deque接口,它底层是一个Object[]数组,它支持自动扩容,默认初始容量是16,当元素达到队列的容量后就自动以2的倍数扩容,元素不能是null。

原理:ArrayDeque里面维护了head和tail两个指针,两个指针初始都是0,表示的是Object[]数组的下标,

正常使用队列使用的是offer方法,offer调用的是addLast方法,此方法是先在tail位置放置元素,然后再移动tail指针指向下一个位置,如果下一个位置就是head指针指向的位置则表示数组已全部放置了数据,需要对其进行扩容

addFirst使用的是head指针,head指针向左移动,[head = (head - 1) & (elements.length - 1)] = 15,在数组的最后一个位置放入元素,如果再addFirst就是向14这个位置放置元素,

总结:ArrayDeque是一个双端队列,内部使用head和tail指针进行控制元素的进队和出队

注意:数字在计算机中是以补码的方式存储的,正数的补码等于自己的原码,负数的补码等于自己的原码除符号位取反再+1,如1的原码和补码都是00000001,-1的原码是10000001,反码是11111110,补码是11111111

4.2 PriorityQueue

PriorityQueue继承AbstractQueue,它底层是一个Object[]数组,它支持自动扩容,默认初始容量是11,当元素达到队列的容量后,它会判断当前容量是否小于64,小于64的话,容量+2否则容量扩大一倍;元素不能是null。

原理:PriorityQueue其实是一种堆排序结构,默认是小根堆,堆底层使用Object[]数组实现。

堆:1.堆中的某个节点总是大于或者不小于其父亲节点的值 2.堆总是一颗完全二叉树

重要方法逻辑:

插入元素:1.先将元素放入到堆的最后一个节点的位置(也就是数组的有效元素的最后的一个位置)注:此时空间不够时需要进行扩容。 2. 将最后插入的节点向上调整,直到满足堆的性质

删除元素(弹出堆的第一个元素):1. 将堆顶元素和堆中的最后一个元素进行交换 2.删除堆的最后一个元素 3.对堆顶元素进行向下调整

效率:加入一个数的时间复杂度是logN;取最大数且继续维持大根堆的时间复杂度是logN;

4.3 DelayQueue(线程安全)

DelayQueue继承AbstractQueue实现BlockingQueue。它底层是PriorityQueue,依赖PriorityQueue的能力来存储元素。DelayQueue里的元素都有一个延期时间实现Delayed接口,PriorityQueue按照延期时间进行排序。当还未到延期时间,DelayQueue弹出的数据为空。DelayQueue的线程安全是使用ReentrantLock进行控制。

4.3.1 Thread lead 成员变量的用处

leader来减少不必要的等待时间,那么这里我们的DelayQueue是怎么利用leader来做到这一点的呢:

这里我们想象着我们有个多个消费者线程用take方法去取,内部先加锁,然后每个线程都去peek第一个节点.如果leader不为空说明已经有线程在取了,设置当前线程等待

if (leader != null)
   available.await();

如果为空说明没有其他线程去取这个节点,设置leader并等待delay延时到期,直到poll后结束循环

else {
     Thread thisThread = Thread.currentThread();
     leader = thisThread;
     try {
          available.awaitNanos(delay);
     } finally {
          if (leader == thisThread)
              leader = null;
     }
 }

take方法中 为什么释放first元素

first = null; // don't retain ref while waiting

我们可以看到doug lea后面写的注释,那么这段代码有什么用呢?

 想想假设现在延迟队列里面有三个对象。

  • 线程A进来获取first,然后进入 else 的else ,设置了leader为当前线程A
  • 线程B进来获取first,进入else的阻塞操作,然后无限期等待
  • 这时他是持有first引用的
  • 如果线程A阻塞完毕,获取对象成功,出队,这个对象理应被GC回收,但是他还被线程B持有着,GC链可达,所以不能回收这个first.
  • 假设还有线程C 、D、E.. 持有对象1引用,那么无限期的不能回收该对象1引用了,那么就会造成内存泄露.

4.4 ConcurrentLinkedQueue(线程安全)

ConcurrentLinkedQueue继承AbstractQueue实现Queue接口。它是一个线程安全的先进先出队列实现,内部维护了head和tail两个volatile的指针,它底层是链表,自定义了Node节点,Node类中用volatile声明了item和next变量,并使用CAS的方式设置节点的next节点等,不支持null元素。

下面以offer方法的注释来详细说明下ConcurrentLinkedQueue

public boolean offer(E e) {
    // 检查e是不是null,是的话抛出NullPointerException异常。
    checkNotNull(e);
    // 创建新的节点
    final Node<E> newNode = new Node<E>(e);

    // 将“新的节点”添加到链表的末尾。
    for (Node<E> t = tail, p = t;;) {
        Node<E> q = p.next;
        // 情况1:q为空,p是尾节点插入
        if (q == null) {
            // CAS操作:如果“p的下一个节点为null”(即p为尾节点),则设置p的下一个节点为newNode。
            // 如果该CAS操作成功的话,则比较“p和t”(若p不等于t,则设置newNode为新的尾节点),然后返回true。
            // 如果该CAS操作失败,这意味着“其它线程对尾节点进行了修改”,则重新循环。
            if (p.casNext(null, newNode)) {
                if (p != t) // hop two nodes at a time
                    casTail(t, newNode);  // Failure is OK.
                return true;
            }
        }
        // 情况2:p和q相等
        else if (p == q)
            p = (t != (t = tail)) ? t : head;
        // 情况3:其它
        else
            p = (p != t && t != (t = tail)) ? t : q;
    }
}

5.5 LinkedBlockingQueue(线程安全)

LinkedBlockingQueue继承AbstractQueue实现BlockQueue接口,底层是链表,使用ReentrantLock进行并发安全控制。不允许添加null。

注意:如果不传入参数则默认大小是Integer.max,如果传入参数则大小就是传入的数值,当queue中元素达到最大值时,put 添加元素操作会被阻塞,等queue中不满时才能继续插入,使用Condition notEmpty进行await和signal进行等待和唤醒

5.6 ArrayBlockingQueue(线程安全)

ArrayBlockingQueue继承AbstractQueue实现BlockingQueue,底层是数组,使用ReentrantLock进行并发安全控制。不允许添加null。

原理:使用takeIndex和putIndex对取出和放入的位置进行控制,当take时,取taskIndex的位置元素,当put时put putIndex位置的元素,take和put后,判断takeIndex和putIndex是否等于数组的长度,等于的话从0继续开始

注意:传入参数就是底层数组的大小,当queue中元素达到最大值时,put 添加元素操作会被阻塞,等queue中不满时才能继续插入,使用Condition notEmpty notFull两个进行控制

5.Map

5.1 HashMap

HashMap继承AbstractMap实现Map接口。底层是数组+链表+红黑树。允许key和value为null

扩容:HashMap初始容量是16,默认负载因子是0.75,当HashMap中的元素数量大于 容量*负载因子则进行容量*2的扩容操作。

引入红黑树的原因:降低查找相同hash值节点的速度,红黑树也是一种平衡二叉树,当节点数量大于等于8且HashMap中的元素大于等于64的时候才会将链表转化成红黑树,当节点数量小于等于6的时候红黑树退化成链表

5.1.1 put方法

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

// 这就是那个扰动函数 作用:让key的hash值的高16位也参与路由运算(在hash表数组长度还不是很大的情况下,让hash值的高16位也参与进来)
// key如果是null则就放到 数组的0位置
// h      = 0010 1100 1010 0011 1000 0101 1101 1100
// ^
// h>>>16 = 0000 0000 0000 0000 0010 1100 1010 0011
//        = 0010 1100 1010 0011 1010 1001 0111 1111       
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    // tab: 引用当前哈希表的数组
    // p: 当前哈希表的元素
    // n: 哈希表数组的长度
    // i: 路由寻址的结果               
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 如果当前hashmap的数组为null或数据为0, hashmap设计为第一次向其插入数据的时候会初始化(延迟初始化)
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 最简单的情况,寻址找到的数组的位置正好是null,则直接把当前k v封装成node放进去    
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else { // 存在和要插入元素相同hash值的元素
        // e: node临时元素
        // k: 临时的一个key
        Node<K,V> e; K k;
        // 表示数组中的元素与当前要插入的元素 完全一致,表示后续需要进行替换操作
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // P元素已经树化了
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {// 还是链表结构,且链表的头元素和要插入的元素的key不一致
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) { // 当前元素是最后一个元素,
                    // 则将新节点放到p的后面
                    p.next = newNode(hash, key, value, null);
                    // 判断后的元素是否达到  TREEIFY_THRESHOLD(8)
                    if (binCount >= TREEIFY_THRESHOLD - 1) 
                        treeifyBin(tab, hash); // 树化操作
                    break;
                }
                // 在链表中找到一个key和当前要插入元素的key一致的元素
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // 找到了要替换的数据, 判断onlyIfAbsent后进行替换数据操作
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

5.1.2 resize方法

// 为什么需要扩容:为了解决哈希冲突导致的链化影响查询效率的问题,通过扩容来缓解
final Node<K,V>[] resize() {
    // oldTab:扩容前的哈希表
    Node<K,V>[] oldTab = table;
    // oldCap:扩容之前的table数组的长度
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // oldThr:扩容之前的扩容阈值,触发本次扩容的阈值
    int oldThr = threshold;
    // newCap:扩容之后哈希数组的大小
    // newThr:扩容之后,下次再次触发扩容的条件
    int newCap, newThr = 0;
    // 下面if  else 就是计算本次扩容之后 数组的大小newCap, 以及下次再次触发扩容的大小newThr      
    if (oldCap > 0) {
        // 哈希表已经初始化过了,这是一次正常扩容
        if (oldCap >= MAXIMUM_CAPACITY) { // 扩容之前的哈希数组大小已经达到最大阈值了,则不扩容,且设置扩容条件为
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 容量翻倍
            newThr = oldThr << 1; 
    }
    else if (oldThr > 0) // 哈希表还是null,如下情况:1.new HashMap(initCapacity, loadFatory) 2.new HashMap(initCapacity) 3.new HashMap(map)
        newCap = oldThr;
    else {// 哈希表还是null,且 只有 new HashMap()时
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    //newThr为0时,通过newCap和loadFactory计算出一个newThr
    if (newThr == 0) { 
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // 创建一个更大的数组
    table = newTab;
    if (oldTab != null) { // 说明扩容之前,哈希里面已经有数据了
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) { // 当前哈希数组位置有数据
                oldTab[j] = null; // 置空,方便JVM回收内存
                if (e.next == null) // 当前元素没有和任何数据发生碰撞, 最简单情况
                    newTab[e.hash & (newCap - 1)] = e; // 新计算哈希表的索引将值放进去就行
                else if (e instanceof TreeNode) // 当前节点已树化
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // 链表情况
                    // 低位链表:存放在扩容之后的数组的下标位置,与当前数组的下标位置一致
                    Node<K,V> loHead = null, loTail = null;
                    // 高位链表:存放在扩容之后的数组的下标位置为当前数组的下标位置 + 扩容之前数组的长度
                    Node<K,V> hiHead = null, hiTail = null;
                    
                    Node<K,V> next;
                    do {
                        next = e.next;
                        // hash -> ... 0 1111
                        // oldCap->000 1 0000
                        //       ->000 0 0000   这样就巧妙的分出 loHead和hiHead了
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    
                    if (loTail != null) { // 低位链表有数据, 处理链表指向
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) { // 高位链表有数据, 处理链表指向
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

5.1.3 get方法

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node<K,V> getNode(int hash, Object key) {
    // tab:当前哈希表的数组
    // first:哈希表数组桶位的头元素
    // e:临时node元素
    // n:哈希表的数组的长度
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) { // 哈希表不等于空,且要取的数据的key的哈希值在哈希表中存在
        if (first.hash == hash && // 哈希表数组中的头元素就是需要查找的数据,则直接返回
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) { // 当前桶位不止一个元素,可能树、可能链表
            if (first instanceof TreeNode) // 树
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do { // 循环每个节点判断是否相等,是则返回
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

5.1.4 remove方法

public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    //tab:引用哈希表中的数组
    //p:当前Node元素
    //n:哈希表的长度
    //index:寻址结果
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) { // 哈希有数据,且路由到的数组索引也是有数据的,需要进行查找操作并且删除
        // node:查找到的结果
        // e:当前node的下一个节点
        Node<K,V> node = null, e; K k; V v;
        
        if (p.hash == hash && 
            ((k = p.key) == key || (key != null && key.equals(k)))) // 第一种情况 当前数组索引中的元素就是你要删除的元素
            node = p;
        else if ((e = p.next) != null) { // 当前数组索引不是你要删除的元素,且数组索引的头节点后续还有节点,不是树就是链表
            if (p instanceof TreeNode) // 第二种情况 是树
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else { // 第三种情况 链表
                do {// 循环链表查询
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            if (node instanceof TreeNode) // 树 情况删除节点
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p) // 数组头节点是要删除的元素
                tab[index] = node.next;
            else // 链表元素删除 node是p.next
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

public boolean remove(Object key, Object value) {
    return removeNode(hash(key), key, value, true, true) != null;
}

5.1.5 replace方法

// key和oldValue都相同 替换value
public boolean replace(K key, V oldValue, V newValue) {
    Node<K,V> e; V v;
    if ((e = getNode(hash(key), key)) != null &&
        ((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
        e.value = newValue;
        afterNodeAccess(e);
        return true;
    }
    return false;
}

// key相同,替换value
public V replace(K key, V value) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) != null) {
        V oldValue = e.value;
        e.value = value;
        afterNodeAccess(e);
        return oldValue;
    }
    return null;
}

5.2 TreeMap

TreeMap继承AbstractMap实现NavigableMap接口,底层是红黑树结构

可以传一个自定义排序器,对元素进行排序,元素的key不能为null

5.3 LinkedHashMap

LinkedHashMap继承HashMap实现Map接口,底层使用哈希表与双向链表来保存所有元素,哈希表使用的是HashMap,双向链表实现是LinkedHashMap重新定义了哈希表的数组中保存元素的Entry,它除了保存当前对象的引用外,还保存了其上一个元素-before和下一个元素-after的引用,从而在哈希标的基础上又构建了双向链表。

  • put方法

当用户调用put方法时,实际时调用HashMap的put方法,LinkedHashMap没有重写put方法,但是重写了put方法中的newNode方法,此方法会生成一个LinkedHashMap的Entry并构建好链表结构后返回

  • remove方法

当用户调用remove方法,实际还是调用HashMap的remove方法,在remove方法最后,会调用afterNodeRemove方法,此方法LinkedHashMap进行了重写,在这个方法里,LinkedHashMap重新维护的双向链表结构

LinkedHashMap支持insert排序和访问排序,默认insert排序。

支持key和value都为null

5.4 ConcurrentHashMap(线程安全)

ConcurrentHashMap继承AbstractMap实现ConcurrentMap接口,jdk1.8采用了更细粒度的锁,采用Node+Synchronized+CAS算法实现,降低了锁粒度,并发性能更好,并且结构上与HashMap更加一致

key和value都不允许为null,

  • size方法:

size方法并不简单返回一个计数器的值,每次调用它,它都会调用sumCount()方法进行计算,baseCount+countCells数组存储的值,baseCount会在每次修改Map影响个数的时候修改,如果CAS的方式修改失败,修改数放入countCells。

5.5 ConcurrentSkipListMap(线程安全)

ConcurrentSkipListMap继承AbstractMap实现了ConcurrentNavigableMap接口,底层是一个跳表的并发Map集合,没有使用哈希表。

其元素可以通过自定义排序器进行排序,不允许key和value的值为null

原理:

  • 底层结构:HeadIndex、Index、Node

Node:key value和指向下一个Node节点的next,单向链表

Index:SkipList的索引节点。内部2个指针,down:指向下一层,right:指向右边的index,还有一个代表当前节点的node

HeadIndex:index的子类,多了一个level属性,只在头部使用

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1416478.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

LeetCode(1)

目录 时间复杂度分析&#xff1a; 递归 题1&#xff1a;爬楼梯 解法1&#xff1a;递归 解法2&#xff1a;循环 题2&#xff1a;两数之和 解法1&#xff1a;暴力枚举 解法2&#xff1a; 哈希表 题3&#xff1a;合并两个有序数组 解法1&#xff1a;直接合并后排序 解法2&…

华为机考入门python3--(0)模拟题3-计算字符串重新排列数

分类&#xff1a;排列组合 知识点&#xff1a; 计算字符串中每个字符出现的次数 Counter(string) 计算列表中每个元素出现的次数 Counter(list) 阶乘 math.factorial(num) 排列去重 题目来自【华为招聘模拟考试】 先把每个字符当成唯一出现过一次&#xff0c;计算所有排列…

Linux文本三剑客---awk经典案例

awk&#xff08;是一种处理文本文件的应用程序&#xff0c;它依次处理文件的每一行&#xff0c;并读取里面的每一个字段。&#xff09; awk 包含几个特殊的内建变量&#xff08;可直接用&#xff09;如下所示&#xff1a; 1、获取根分区剩余大小 #可以使用df -h命令来查看所有…

详解顺序结构双指针处理算法

&#x1f380;个人主页&#xff1a; https://zhangxiaoshu.blog.csdn.net &#x1f4e2;欢迎大家&#xff1a;关注&#x1f50d;点赞&#x1f44d;评论&#x1f4dd;收藏⭐️&#xff0c;如有错误敬请指正! &#x1f495;未来很长&#xff0c;值得我们全力奔赴更美好的生活&…

如何在Ubuntu安装配置SVN服务端并实现无公网ip访问内网资料库

&#x1f308;个人主页: Aileen_0v0 &#x1f525;热门专栏: 华为鸿蒙系统学习|计算机网络|数据结构与算法 ​&#x1f4ab;个人格言:“没有罗马,那就自己创造罗马~” 文章目录 前言1. Ubuntu安装SVN服务2. 修改配置文件2.1 修改svnserve.conf文件2.2 修改passwd文件2.3 修改au…

swift - reduce简介

reduce 减少&#xff0c;降低&#xff1b;&#xff08;烹调中&#xff09;使变浓稠&#xff0c;收汁&#xff1b;<美>节食减肥&#xff1b;使沦为&#xff0c;使陷入&#xff08;不好的境地&#xff09;&#xff1b;迫使&#xff0c;使不得不&#xff08;做&#xff09;&…

【C++】输入输出、缺省参数、函数重载

目录 C的输入和输出 缺省参数 概念 缺省参数的分类 全缺省参数 半缺省参数 函数重载 概念 C支持函数重载的原理--名字修饰 C的输入和输出 #include<iostream> // std是C标准库的命名空间名&#xff0c;C将标准库的定义实现都放到这个命名空间中 using namespace …

BKP备份寄存器、RTC实时时钟

目录 1. BKP (Backup Registers)备份寄存器 2. RTC (Real Time Clock)实时时钟 1. BKP (Backup Registers)备份寄存器 BKP可用于存储用户应用程序数据。当VDD (2.0-3.6V)电源被切断,他们仍然由VBAT (1.8-3.6V)维持供电。当系统在待机模式下被唤醒&#xff0c;或系统复位或…

【大数据】Flink 架构(一):系统架构

Flink 架构&#xff08;一&#xff09;&#xff1a;系统架构 1.Flink 组件1.1 JobManager1.2 ResourceManager1.3 TaskManager1.4 Dispatcher 2.应用部署2.1 框架模式2.2 库模式 3.任务执行4.高可用设置4.1 TaskManager 故障4.2 JobManager 故障 Flink 是一个用于状态化并行流处…

BeanUtils和BeanCopier性能复制Bean工具比较

文章目录 一、前言二、实验三、原理1、BeanUtils2、BeanCopier 四、总结 一、前言 我们本篇比较的是复制Bean对象的工具&#xff0c;分别是org.springframework.beans.BeanUtils和 net.sf.cglib.beans.BeanCopier 二、实验 import net.sf.cglib.beans.BeanCopier; import org…

部署LNMP、Nginx+FastCGI、Nginx地址重写语法,地址重写应用案例

1 案例1&#xff1a;部署LNMP环境 1.1 问题 安装部署LNMP环境实现动态网站解析 静态网站 在不同环境下访问&#xff0c;网站内容不会变化 动态网站 在不同环境下访问&#xff0c;网站内容有可能发生变化 安装部署Nginx、MariaDB、PHP、PHP-FPM&#xff1b;启动Nginx、Mari…

java—AWT

AWT 课程&#xff1a;1、GUI编程简介_哔哩哔哩_bilibili 一.介绍 包含了很多类和接口&#xff01;GUI&#xff01;元素&#xff1a;窗口、按钮、文本框java.awt 二.窗口 1.构造 2.方法 // 实例化frame类Frame frame new Frame("这个一个框");// 设置可见性frame.…

游戏设计模式

单列模式 概念 单例模式是一种创建型设计模式&#xff0c;可以保证一个类只有一个实例&#xff0c;并提供一个访问该实例的全局节点。 优点 可以派生&#xff1a;在单例类的实例构造函数中可以设置以允许子类派生。受控访问&#xff1a;因为单例类封装他的唯一实例&#xf…

Cyberdog2 docker环境软件源无法被验证问题

搭建docker系统后更新软件源sudo apt-get update出现异常 经过查询GPT&#xff0c;使用如下方式成功解决 从keyserver.ubuntu.com获取缺失的公钥&#xff0c;并添加到apt-key中。具体命令如下&#xff1a; gpg --keyserver keyserver.ubuntu.com --recv-keys F42ED6FBAB17C6…

C++的关键字,命名空间,缺省参数,函数重载以及原理

文章目录 前言一、C关键字(C98)二、命名空间命名空间介绍命名空间的使用 三、C输入【cin】& 输出【cout】四、缺省参数缺省参数概念缺省参数分类缺省参数的使用小结一下 五、函数重载函数重载介绍函数重载类型 六、C支持函数重载的原理--名字修饰(name Mangling)【重点】 前…

【开源】基于JAVA语言的智慧社区业务综合平台

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 业务类型模块2.2 基础业务模块2.3 预约业务模块2.4 反馈管理模块2.5 社区新闻模块 三、系统设计3.1 用例设计3.2 数据库设计3.2.1 业务类型表3.2.2 基础业务表3.2.3 预约业务表3.2.4 反馈表3.2.5 社区新闻表 四、系统展…

[BUUCTF]-PWN:hitcon2014_stkof解析

又是一道堆题&#xff0c;先看保护 关键信息&#xff0c;64位&#xff0c;没开pie。再看ida 大致就是alloc创建堆块&#xff0c;free释放堆块&#xff0c;fill填充堆块内容&#xff0c;以及一个看起来没啥用的函数&#xff0c;当然我也没利用这个函数去解题 这里有两种解法 解…

Python tkinter (6) Listbox

Python的标准Tk GUI工具包的接口 tkinter系列文章 python tkinter窗口简单实现 Python tkinter (1) —— Label标签 Python tkinter (2) —— Button标签 Python tkinter (3) —— Entry标签 Python tkinter (4) —— Text控件 GUI 目录 Listbox 创建listbox 添加元素…

Java版大厂算法题1——数字颠倒

问题描述 输入一个整数&#xff0c;将这个整数以字符串的形式逆序输出&#xff0c;程序不考虑负数的情况&#xff0c;若数字含有0&#xff0c;则逆序形式也含有0。如果输入为100&#xff0c;则输出为001。 数据范围&#xff1a;0<n<(2^30)-1 * 输入描述&#xff1a;输入…

2023启示录|虚拟人这一年

图片&#xff5c;《银翼杀手 2049》剧照 作者丨程心 编辑丨罗辑 2023 年&#xff0c;大模型 “救活” 了很多行业&#xff0c;其中最为反转的&#xff0c;就是把虚拟数字人&#xff08;以下简称虚拟人&#xff09;从活死人墓里拉了出来。 还没开年&#xff0c;在 2022 年火…