【数据结构-源码分析】HashMap源码分析(超级详细)

news2024/11/18 17:48:01

文章内容

    • 1、HashMap简介
    • 2、类结构
    • 3、属性
    • 4、构造方法
    • 5、方法
      • 5.1、put方法
      • 5.2、resize方法
    • 6、jdk1.8的优化

1、HashMap简介

HashMap基于哈希表的Map接口实现,是以key-value存储形式存在。(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同)。在 JDK1.8 中,HashMap 是由数组 + 链表 + 红黑树构成,新增了红黑树作为底层数据结构,结构变得复杂了,但是效率也变的更高效。当一个值中要存储到Map的时候会根据Key的值来计算出他的hash,通过哈希来确认到数组的位置,如果发生哈希碰撞就以链表的形式存储。当链表长度过长时,HashMap会把这个链表转换成红黑树来存储。

但是这样的话问题来了,HashMap为什么要使用红黑树呢,这样结构的话不是更麻烦了吗??

这个问题我也没有想过,其实很多在看的时候只会在乎红黑树的实现而忽略到了为什么要使用的这个问题,我也是在写本文的时候突发疑惑。参考了网上的例子,同时也解释了为什么阀值为8:

因为Map中桶的元素初始化是链表保存的,其查找性能是O(n),而树结构能将查找性能提升到O(log(n))。当链表长度很小的时候,即使遍历,速度也非常快,但是当链表长度不断变长,肯定会对查询性能有一定的影响,所以才需要转成树。至于为什么阈值是8?我想去源码中找寻答案应该是最可靠的途径。

2、类结构

我们来看一下类结构

HashMap类结构(jdk1.8版本)
在阅读源码的时候一直有个问题很困惑就是HashMap已经继承了AbstractMap而AbstractMap类实现了Map接口,那为什么HashMap还要在实现Map接口呢?同样在ArrayList中LinkedList中都是这种结构。

据 java 集合框架的创始人Josh Bloch描述,这样的写法是一个失误。在java集合框架中,类似这样的写法很多,最开始写java集合框架的时候,他认为这样写,在某些地方可能是有价值的,直到他意识到错了。显然的,JDK的维护者,后来不认为这个小小的失误值得去修改,所以就这样存在下来了。

  • Cloneable 空接口,表示可拷贝

  • Serializable 序列化

  • AbstractMap 提供Map实现接口

3、属性

    /**
     * The default initial capacity - MUST be a power of two.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //默认容量大小(必须是二的n次幂)

    /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;	//最大容量(必须是二的n次幂)

    /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;	//默认负载因子(默认的0.75)

    /**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     */
    static final int TREEIFY_THRESHOLD = 8;	//当链表的值超过8则会转红黑树(1.8新增)

    /**
     * The bin count threshold for untreeifying a (split) bin during a
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
     * most 6 to mesh with shrinkage detection under removal.
     */
    static final int UNTREEIFY_THRESHOLD = 6;  //当链表的值小于6则会从红黑树转回链表

    /**
     * The smallest table capacity for which bins may be treeified.
     * (Otherwise the table is resized if too many nodes in a bin.)
     * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
     * between resizing and treeification thresholds.
     */
     //当Map里面的容量(即表长度)超过这个值时,链表才能进行树形化 ,否则元素太多时会扩容,而不是树形化 
     //为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD(因此树形化有两个条件,表长度 > 64 and 链表长度 > 8)
    static final int MIN_TREEIFY_CAPACITY = 64;	

    /* ---------------- Fields -------------- */

	/**
     * The table, initialized on first use, and resized as
     * necessary. When allocated, length is always a power of two.
     * (We also tolerate length zero in some operations to allow
     * bootstrapping mechanics that are currently not needed.)
     */
    transient Node<K,V>[] table;	// table用来初始化,类似容器来存放元素(必须是二的n次幂)

    /**
     * Holds cached entrySet(). Note that AbstractMap fields are used
     * for keySet() and values().
     */
    transient Set<Map.Entry<K,V>> entrySet;	// 用来存放缓存

    /**
     * The number of key-value mappings contained in this map.
     */
    transient int size;	// Map中存储的元素数量

    /**
     * The number of times this HashMap has been structurally modified
     * Structural modifications are those that change the number of mappings in
     * the HashMap or otherwise modify its internal structure (e.g.,
     * rehash).  This field is used to make iterators on Collection-views of
     * the HashMap fail-fast.  (See ConcurrentModificationException).
     */
    transient int modCount;	// 用来记录HashMap的修改次数

    /**
     * The next size value at which to resize (capacity * load factor).
     *
     * @serial
     */
    // (The javadoc description is true upon serialization.
    // Additionally, if the table array has not been allocated, this
    // field holds the initial array capacity, or zero signifying
    // DEFAULT_INITIAL_CAPACITY.)
    int threshold;	// 阈值,用来调整大小下一个容量的值(计算方式为容量*负载因子)

    /**
     * The load factor for the hash table.
     *
     * @serial
     */
    final float loadFactor;	// 负载因子,创建HashMap也可调整,比如你希望用更多的空间换取时间,可以把负载因子调的更小一些,减少碰撞。

4、构造方法

  • HashMap()
    构造一个空的 HashMap ,默认初始容量(16)和默认负载因子(0.75)。

  • HashMap(int initialCapacity)
    构造一个空的 HashMap具有指定的初始容量和默认负载因子(0.75)。

  • HashMap(int initialCapacity, float loadFactor)
    构造一个空的 HashMap具有指定的初始容量和负载因子。我们来分析一下。
    最后落到tableSizeFor方法,后面讲下;

  • HashMap(Map<? extends K, ? extends V> m)
    通过旧的Map来创建新的HashMap对象。

tableSizeFor(扰动函数)解析

这是HashMap源码中的一个方法,这个方法的作用是找到一个大于或等于cap最近的2的n次方数
例:cap = 14,return 16;cap = 76,return 128;

    static final int tableSizeFor(int cap) {
    	//-1可以保证当传入的数刚好是2的次方时,可以正确的返回其本身,例:传入的是16,经过下面的计算后还是返回16
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

先解释一下|=、>>>这两个运算符。

  • 运算符|=
    n |= n 等同于 n = n | n;
    | 是位运算符(或)
  • 运算符>>>
    >>>是无符号右移运算符
    就是把一个二进制数右移指定位数,正数高位补0,负数高位补1;
    例:
int num = ;      //00000000 00000000 00000000 10111101
//无符号右移1位
num = num>>> = 1;//00000000 00000000 00000000 01011110
//把上一步得到的num再做无符号右移2位运算得到
num = num>>> = 2;//00000000 00000000 00000000 00010111

了解了以上两个运算符的作用,应该就能初步看明白源码中的tableSizeFor方法了吧。但是大部分人第一次看的时候应该都是一脸懵逼的,颇有一种我明明都看明白了每步做了什么,为什么合起来就看不懂了的感觉。下面来解释一下。

先思考一个问题:
如果给我们一个二进制数cap = 00000000 00000000 00000000 10111101这个二进制数的最近的一个2的n次方数是多少呢?学过二进制,我们可以应该可以一眼看出来这个数是cap = 00000000 00000000 00000001 00000000
我们一眼看出来了,但是程序不行,所以要思考一种办法能通过代码找到这个数。
假设我们可以把最高位1以及其后面的位都置为1,然后加1是不是就能实现这个功能了,即:

步骤1、cap =           00000000 00000000 00000000 11111111
步骤2、cap = cap + 1 = 00000000 00000000 00000001 00000000

tableSizeFor就是通过这两个步骤实现的

    static final int tableSizeFor(int cap) {
    	//-1可以保证当传入的数是2的次方时,可以正确的返回其本身,例:传入的是16,经过下面的计算后还是返回16
    	//步骤一
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        //步骤二n+1
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

这个代码的作用就是把最高位的1后面的所有位都置为1,而且从始至终其实只用到了最高位1这1位数字参与运算,非常的巧妙,膜拜。下面由一个例子来解释一下:

有个int数n,二进制为
n = 00000000 00000001 10111010 000001101;
现在我们只看最高位的1,1后面的位数用x代替,它是0还是1都不重要(后面你会发现确实不重要),表示为:
n = 00000000 00000001 xxxxxxx xxxxxxxx;

把n做无符号右移1位运算并将结果赋值给m:
m = 00000000 00000000 1xxxxxxx xxxxxxxx;
把n与m做或运算,并将结果赋值给n,得到:
n = 00000000 00000001 1xxxxxxx xxxxxxxx;

把n做无符号右移2位运算并将结果赋值给m:
m = 00000000 00000000 011xxxxx xxxxxxxx;
把n与m做或运算,并将结果赋值给n,得到:
n = 00000000 00000001 111xxxxx xxxxxxxx;

把n做无符号右移4位运算并将结果赋值给m:
m = 00000000 00000000 0001111x xxxxxxxx;
把n与m做或运算,并将结果赋值给n,得到:
n = 00000000 00000001 1111111x xxxxxxxx;

把n做无符号右移8位运算并将结果赋值给m:
m = 00000000 00000000 00000001 1111111x ;
把n与m做或运算,并将结果赋值给n,得到:
n = 00000000 00000001 11111111 1111111x ;

把n做无符号右移16位运算并将结果赋值给m:
m = 00000000 00000000 00000000 00000001 ;
把n与m做或运算,并将结果赋值给n,得到:
n = 00000000 00000001 11111111 11111111 ;

有没有发现到不知不觉就把最高位1后面的位数全部都变成1了,而且从始至终其实只有最高位的1以及通过这个1计算得到的数参与了运算,
前面被我们表示为x的位根本没有用到。
因为java中int类型是32位的,所以5次无符号右移刚好能覆盖到32位数。同理如果是8位数只需要位移到4,如果是16位数只需要位移到8,,64位数则需要位移到32。
        n |= n >>> 1;//到这一步最多可以得到2位1
        n |= n >>> 2;//到这一步最多可以得到4位1
        n |= n >>> 4;//到这一步最多可以得到8位1
        n |= n >>> 8;//到这一步最多可以得到16位1
        n |= n >>> 16;//到这一步最多可以得到32位1
把得到的n加1就能得到了一个最近的大于n的2的次方数。

但这块有个疑问,该方法生成的值为啥赋给threshold??希望路过的大佬给解答下。

在这里插入图片描述

5、方法

5.1、put方法

在这里插入图片描述

我们可以看到put调用的是putVal来进行数据插入,但是要注意到key在这里执行了一下hash()方法,来看一下Hash方法是如何实现的。

在这里插入图片描述

理论上来说字符串的hashCode是一个int类型值,那可以直接作为数组下标了,且不会出现碰撞。但是这个hashCode的取值范围是[-2147483648, 2147483647],有将近40亿的长度,谁也不能把数组初始化的这么大,内存也是放不下的。

默认初始化的Map大小是16,所以获取的Hash值并不能直接作为下标使用,需要与数组长度进行取模运算得到一个下标值。 HashMap源码这里不只是直接获取哈希值,还进行了一次扰动计算,(h = key.hashCode()) ^ (h >>> 16)。把哈希值右移16位,也就正好是自己长度的一半(int类型32位的一半),之后与原哈希值做异或运算,这样就混合了原哈希值中的高位和低位,增大了随机性。

说白了,使用扰动函数就是为了增加随机性,让数据元素更加均衡的散列,减少碰撞。

从上面也可以得知HashMap是支持Key为空的,而HashTable是直接用过Key来获取HashCode所以key为空会抛异常。因为HashMap 使用的方法很巧妙,它通过 hash & (table.length -1)来得到该对象的保存位(数组下标),前面说过 HashMap 底层数组的长度总是2的n次方,这是HashMap在速度上的优化。当 length 总是2n次方时,hash & (length-1)运算等价于对 length 取模,也就是 hash%length,但是&比%具有更高的效率。比如 n % 32 = n & (32 -1)。

现在再来看看需要验证的问题,以及putVal方法的实现:

  1. 验证容器懒加载(put元素后分配空间)√
  2. 正常put元素(index位置元素存在及不存在的情况)√
  3. 容器扩容 √
  4. 链表转红黑树
	final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
       	// 初始化数组 table,table 被延迟到插入新数据时再进行初始化 
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;// 空表进行首次扩容初始化table
        if ((p = tab[i = (n - 1) & hash]) == null)	// i位置上元素不存在(通过i=(n-1)&hash生成元素的数组下标)
            tab[i] = newNode(hash, key, value, null); //结点不存在,就创建新的Node结点放到i位置
        else {	// i位置上元素存在
            Node<K,V> e; K k;
            // 如果i位置上原结点的hash值与待插入的一样,且key也一样,就是值的更新操作,进行覆盖
            if (p.hash == hash &&	
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)	// 红黑树结点,就调用特有的putTreeVal方法
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {	// 其他情况,则按照尾插法插入到链表
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {	// 判断是否到达链表尾部
                        p.next = newNode(hash, key, value, null);	
                        // 判断是否进行树化
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;	// 向链表尾递推
                }
            }	
            // 如果链表头结点或遍历链表过程中某个结点,满足p.hash == hash 
            // &&((k = p.key) == key || (key != null && key.equals(k))),就进行更新操作,同时返回oldValue值
            if (e != null) { // existing mapping for key
                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.2、resize方法

    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        // Cap 是 capacity 的缩写,容量。如果容量不为空,则说明已经初始化。 
        if (oldCap > 0) {
        	// 规定扩容上限就是 1 << 30
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // 按旧容量和阀值的2倍计算出新容量和阀值 
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
        	// 第一次put,由于是无参创建的map会直接用默认的容量及负载因子,同时计算阈值
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {	// 阈值为0,则按照tableSize*loadFactor计算出来
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        // 赋值新阈值、新表大小
        threshold = newThr;
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        // 将原Map中元素迁移到新Map
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)		// 判断是否链尾元素,扩容时取元素
                        newTab[e.hash & (newCap - 1)] = e;	// 元素重新rehash下标,并赋值到新Map
                    else if (e instanceof TreeNode)	// 红黑树结点,就调用特有的split方法
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        // 下面有点看不懂,望大佬解释!!!
                        do {
                            next = e.next;
                            // 将高低位区分处理:
                            // 1、低位的loHead和loTail放的桶位置不变
                            // 2、高位的loHead和loTail放的桶位置变成原索引+旧容量
                            if ((e.hash & oldCap) == 0) {	
                            // e.hash & oldCap 等价于 e.hash % (oldCap + 1) ,例如16位扩容
                            // 到32位大小的容器中,则0-15位的则计算为0,否则非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);
                        // 将某index下的单个链表区分为高位链(hi)及低位链(lo)
                        // 然后低位原位置不动,高位位置取oldCap + 原index
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

6、jdk1.8的优化

1)扩容元素拆分

拆分元素的过程中,原jdk1.7中会需要重新计算哈希值,但是到jdk1.8中已经进行优化,不在需要重新计算,提升了拆分的性能,设计的还是非常巧妙的。

随机使用一些字符串计算他们分别在16位长度和32位长度数组下的索引分配情况,发现原哈希值与扩容新增出来的长度16,进行&运算,如果值等于0,则下标位置不变。如果不为0,那么新的位置则是原来位置上加16。这样一来,就不需要计算每一个数组中元素的哈希值了。

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

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

相关文章

Oracle数据库同步复制工具Beedup产品功能(二)

接续...... 8、对象比较 Beedup提供主从库相关对象比较功能&#xff0c;比对结果包含各类对象概要统计及差异详情。 支持Oracle、SQL Server、MySQL、DB2 对象比较。 9、 对象恢复 Beedup在向从库写入数据时会禁用目标表的关联触发器&#xff0c;另外对于Oracle 序列的状态…

MySQL复制技术方案——GTID复制配置

在日常运维中&#xff0c;GTID带来的最方便的作用就是搭建和维护主从复制&#xff0c;这也是DBA日常工作中最经常的操作了。GTID的主从模式替代了MySQL前期版本中利用二进制日志文件的名称和日志位置的做法&#xff0c;使用GTID使操作和维护都变得更加简洁和可靠。 搭建主从时…

SQL SELECT 语句

SELECT 语句用于从数据库中选取数据。 SQL SELECT 语句 SELECT 语句用于从数据库中选取数据。 结果被存储在一个结果表中&#xff0c;称为结果集。 SQL SELECT 语法 SELECT column1, column2, ... FROM table_name; 与 SELECT * FROM table_name; 参数说明&#xff1a; …

SVM训练莺尾花数据集

SVM训练莺尾花数据集 代码在莺尾花数据集上训练SVM&#xff0c;数据集由莺尾花的测量值及其相应的物种标签组成。该模型使用70%数据用于训练&#xff0c;然后剩余部分进行测试。其中′fit′fit′fit′方法在训练集上训练数据&#xff0c;′score′score′score′数据在返回模型…

HTC FOCUS3在PC端串流FOHEART H1数据手套(腕带)

本教程介绍使用FOHEART H1数据手套与HTC腕带式追踪器驱动VR中的虚拟手运动&#xff0c;实现手部的追踪及定位。与之前教程&#xff08;HTC FOCUS 3连接FOHEART H1数据手套&#xff09;不同&#xff0c;这次我们的场景内容运行在PC端&#xff0c;而不是头显端&#xff0c;使用VI…

基于Vue和SpringBoot的电商管理系统的设计与实现

作者主页&#xff1a;Designer 小郑 作者简介&#xff1a;Java全栈软件工程师一枚&#xff0c;来自浙江宁波&#xff0c;负责开发管理公司OA项目&#xff0c;专注软件前后端开发&#xff08;Vue、SpringBoot和微信小程序&#xff09;、系统定制、远程技术指导。CSDN学院、蓝桥云…

8.mysql模块

目录 1 安装mysql模块 2 建立与mysql的连接 3 执行SQL语句 3.1 查询数据 3.2 插入数据 3.2.1 直接写入SQL语句 3.2.2 使用问号进行占位 3.2.3 使用对象传入 3.3 更新数据 3.3.1 使用问号进行占位 3.3.2 使用对象传入 3.4 删除数据 常见的数据库有下面几…

光驱重装系统教程

光驱重装系统是使用最长的系统安装方法&#xff0c;最早时候电脑都有光驱的&#xff0c;很多用户重装电脑系统的时候都会使用光驱重装系统&#xff0c;现在小编来为大家详细的介绍一下光驱重装系统的教程。 工具/原料&#xff1a; 系统版本&#xff1a;win7系统 品牌型号&#…

仪表板工具Stimulsoft Dashboards中的面板组件介绍

Stimulsoft Dashboards.JS是一个功能齐全的仪表盘工具&#xff0c;用于为JavaScript平台创建仪表板。 Stimulsoft Dashboards.JS官方正版下载&#xff08;qun&#xff1a;740060302&#xff09;https://www.evget.com/product/4101/download在上一篇文章中&#xff0c;主要介绍…

剑指 Offer 24. 反转链表

一、题目 定义一个函数&#xff0c;输入一个链表的头节点&#xff0c;反转该链表并输出反转后链表的头节点。 示例: 输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL 限制&#xff1a; 0 < 节点个数 < 5000 二、题目解析&…

AI算法工程师 | 09机器学习-概率图模型(六)命名实体识别与CRF

文章目录机器学习 - 概率图模型 之 命名实体识别与CRF一、命名实体识别 NER 的基本介绍1、相关概念2、主要方法&#xff08;导图&#xff09;3、标注策略二、CRF 层之 BiLSTM&#xff08;BiLSTM-CRF&#xff09;1、介绍1.1 BiLSTM-CRF 模型1.2 添加 CRF 层的好处2、CRF 层2.1 E…

华为云工程师HCIA——华为虚拟化平台使用与管理

一、FusionCompute计算虚拟化介绍 1、计算虚拟化相关概念 1.1、虚拟化介绍虚拟化介绍 1.2、虚拟化的特点&#xff08;反过来考定义也要会&#xff09; •分区&#xff1a;分区意味着虚拟化层为多个虚拟机划分服务器资源的能力&#xff1b;每个虚拟机可以同时运行一个单独的操…

xxx.Caffeine进程缓存

Caffeine 是基于Java 8的高性能&#xff0c;接近最佳的缓存库。看上图 赋代码&#xff1a; <dependency><groupId>com.github.ben-manes.caffeine</groupId><artifactId>caffeine</artifactId> </dependency> package com.heima.item.con…

网络编程 - Linux socket编程

前言 socket(套接字)是网络编程编程的一种技巧。通过socket不仅可以实现跨进程通信&#xff0c;还可以实现跨主机的网络通信。使用这种技术&#xff0c;就可以实现全国各地的通讯。例如&#xff1a;深圳的一台电脑接收来自北京一台电脑发来的信息。   本篇不涉及太底层的网络原…

hudi系列-索引机制

1. 索引机制 hudi的索引机制是为了加速upsert/delete操作&#xff0c;它维护着&#xff08;分区 key&#xff09;-> fileID之间的映射关系&#xff0c;所以可以减少对非必要base文件的合并 key是指索引key&#xff0c;可以是表的任意字段&#xff0c;在全局索引中常用主键…

Linux chmod命令详解,Linux修改文件权限

「作者主页」&#xff1a;士别三日wyx 「作者简介」&#xff1a;CSDN top100、阿里云博客专家、华为云享专家、网络安全领域优质创作者 chmod 命令一、常用操作1. 字母形式2. 数字形式3. 递归设置二、文件权限解读三、数字权限四、特殊的root权限五、SUID详解1. 设置SUID2. 取消…

chatgpt小程序版本来了,解决你们手机上想用用不了的问题,chatgpt接口用到小程序里面,调用openai接口,提供前后端源码,可以私有部署使用

现在的chatgpt被玩的都开始加广告&#xff0c;又办会员什么的的&#xff0c;今天就把小程序的前后端无广告版本源码和部署方式说一下。 先看效果&#xff1a; 部署环境 前端用的uniapp&#xff0c;基础模版&#xff0c;单页面没有太多引用 后端使用的python的falsk框架&#…

javassist学习

Java 字节码以二进制的形式存储在 .class 文件中&#xff0c;每一个 .class 文件包含一个 Java 类或接口。Javaassist 就是一个用来 处理 Java 字节码的类库。它可以在一个已经编译好的类中添加新的方法&#xff0c;或者是修改已有的方法&#xff0c;并且不需要对字节码方面有深…

第29届中国国际广告节,酷开科技喜提三奖

第29届中国国际广告节&#xff0c;酷开科技喜提三奖 12月21日至12月23日&#xff0c;在风景如画的鹭岛厦门&#xff0c;第29届中国国际广告节于厦门国际会展中心隆重举行。经过几十年的高歌前行&#xff0c;中国国际广告节业已成为业界的盛会&#xff0c;更是中国广告业发展的…

(十)VUEElement

一、 VUE 1. 概述 Vue 是一套前端框架&#xff0c;免除原生JavaScript中的DOM操作&#xff0c;简化书写。基于MVVM(Model-View-ViewModel)思想&#xff0c;实现数据的双向绑定&#xff0c;将编程的关注点放在数据上。官网&#xff1a;https://cn.vuejs.org 2. 快速入门 &#…