主頁 > 後端開發 > Java集合原始碼剖析——基于JDK1.8中HashMap的實作原理

Java集合原始碼剖析——基于JDK1.8中HashMap的實作原理

2021-10-01 08:11:12 後端開發

文章目錄:

1.HashMap原始碼注釋翻譯

2.HashMap中的屬性

3.HashMap中的方法

3.1 構造方法

3.2 get方法

3.3 put方法

3.4 remove方法

3.5 hash方法

3.6 resize方法

3.7 size方法

3.8 isEmpty方法

3.9 clear方法

3.10 containsKey方法

3.11 containsValue方法

3.12 replace方法

3.13 關于遍歷map集合的三個方法

4.傳統HashMap的缺點——引入紅黑樹


1.HashMap原始碼注釋翻譯

* Hash table based implementation of the <tt>Map</tt> interface.  This
* implementation provides all of the optional map operations, and permits
* <tt>null</tt> values and the <tt>null</tt> key.  (The <tt>HashMap</tt>
* class is roughly equivalent to <tt>Hashtable</tt>, except that it is
* unsynchronized and permits nulls.)  This class makes no guarantees as to
* the order of the map; in particular, it does not guarantee that the order
* will remain constant over time.

翻譯一下大概就是在說,這個哈希表是基于 Map 介面的實作的,它允許 null 值和null 鍵,它不是執行緒同步的,同時也不保證有序,

* <p>This implementation provides constant-time performance for the basic
* operations (<tt>get</tt> and <tt>put</tt>), assuming the hash function
* disperses the elements properly among the buckets.  Iteration over
* collection views requires time proportional to the "capacity" of the
* <tt>HashMap</tt> instance (the number of buckets) plus its size (the number
* of key-value mappings).  Thus, it's very important not to set the initial
* capacity too high (or the load factor too low) if iteration performance is
* important.

再來看看這一段,講的是 Map 的這種實作方式為 get(取)和 put(存)帶來了比較好的性能,但是如果涉及到大量的遍歷操作的話,就盡量不要把 capacity 設定得太高(或 load factor 設定得太低),否則會嚴重降低遍歷的效率,

影響 HashMap 性能的兩個重要引數:“initial capacity”(初始化容量)和”loadfactor“(負載因子),簡單來說,容量就是哈希表桶的個數,負載因子就是鍵值對個數與哈希表長度的一個比值,當比值超過負載因子之后,HashMap 就會進行 rehash操作來進行擴容,


  • HashMap集合底層結構是 陣列 + 單向鏈表 + 紅黑樹,
  • HashMap集合中的key、value均可為 null,其中key是無序不可重復的,
  • HashMap集合的默認初始化容量是16,默認加載因子是 0.75,擴容之后是原容量的2倍,
  • 如果HashMap集合中某個桶中的結點數超過了8,則單向鏈表結點會被替換成紅黑樹結點;當桶中的結點數小于6時,會將樹形結點轉回單向鏈表結點,只有當哈希表中的元素數量超過64時,才會進行樹形化(即轉換成紅黑樹這種結構),否則只是進行擴容,

HashMap 的大致結構如下圖所示,其中哈希表是一個陣列,我們經常把陣列中的每一個節點稱為一個桶,哈希表中的每個節點都用來存盤一個鍵值對,在插入元素時,如果發生沖突(即多個鍵值對映射到同一個桶上)的話,就會通過鏈表的形式來解決沖突,因為一個桶上可能存在多個鍵值對,所以在查找的時候,會先通過 key 的哈希值先定位到桶,再遍歷桶上的所有鍵值對,找出 key 相等的鍵值對,從而來獲取 value,


2.HashMap中的屬性

//默認的初始容量為 2^4=16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

//最大的容量上限為 2^30
static final int MAXIMUM_CAPACITY = 1 << 30;

//默認的加載因子為 0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;

//變成樹型結構的臨界值為 8
static final int TREEIFY_THRESHOLD = 8;

//恢復鏈式結構的臨界值為 6
static final int UNTREEIFY_THRESHOLD = 6;

//當哈希表的大小超過這個閾值,才會把鏈式結構轉化成樹型結構,否則僅采取擴容來嘗試減少沖突
static final int MIN_TREEIFY_CAPACITY = 64;

//哈希表
transient Node<K,V>[] table;

//哈希表中鍵值對的個數
transient int size;

//哈希表被修改的次數
transient int modCount;

//它是通過 capacity*load factor 計算出來的,當 size 到達這個值時,就會進行擴容操作
int threshold;

//負載因子,決定了HashMap集合的資料密度
//負載因子過大,發生碰撞的幾率會越高
//負載因子過小,就越容易觸發擴容,擴容自然也會影響性能
//按照其他語言的參考及研究經驗,會考慮將負載因子設定為0.75,此時平均檢索長度接近于常數
final float loadFactor;

下面是 Node 類的定義,它是 HashMap 中的一個靜態內部類,哈希表中的每一個節點都是 Node 型別,我們可以看到 Node 類中有 4 個屬性,其中除了 key 和
value 之外,還有 hash 和 next 兩個屬性,hash 是用來存盤 key 的哈希值的,next 是在構建鏈表時用來指向后繼節點的,

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

3.HashMap中的方法

3.1 構造方法

關于HashMap原始碼中的構造方法,無非是在更改 初始化容量、加載因子這些引數,這里就不再多說了,(主要是后面的get、put方法)

public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

3.2 get方法

get方法首先是創建了一個 Node 結點物件,然后其中呼叫了 getNode 方法,所以我們著重來看一下這個 getNode 方法,

這個 getNode() 方法首先:如果哈希表不為空 && key 對應的桶上不為空,然后根據哈希表元素個數與哈希值求模( 使用的公式是 (n - 1) &hash )得到 key 所在的桶的頭結點,如果頭結點恰好命中(是我們要get的那個key),則直接回傳,如果頭結點沒有命中,則繼續向后續結點進行判斷,如果頭節點恰好是紅黑樹節點TreeNode,就呼叫紅黑樹節點的 getTreeNode() 方法,否則就執行 do-while 遍歷鏈表節點,

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) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    //如果哈希表不為空 && key 對應的桶上不為空
    if ((tab = table) != null && (n = tab.length) > 0 &&
        //根據哈希表元素個數與哈希值求模( 使用的公式是 (n - 1) &hash )得到 key 所在的桶的頭結點
        (first = tab[(n - 1) & hash]) != null) {
        //是否直接命中
        if (first.hash == hash && // always check first node
            ((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;
}

get方法實作原理敘述:假如我們呼叫原型是 map.get("a"),首先會呼叫 a 的 hash() 方法得到 a 所對應的哈希值,然后通過哈希演算法 tab[(n-1) & hash] 轉換成桶下標(陣列下標),通過這個桶下標快速定位到當前桶結點上,如果比對成功(hashCode、equals都回傳true),則回傳這個桶結點,如果當前桶結點上什么都沒有則回傳null,如果當前桶結點沒有直接命中,它下面還掛載了其他結點,則繼續判斷后續結點,為紅黑樹結構則轉為紅黑樹的get方法獲取結點;如果不是則為普通單向鏈表結構,此時拿著 a 和單向鏈表上的每一個結點進行 equals 方法比對(因為hashCode只有比對成功才會到當前桶結點下繼續比對),有一個equals回傳 true 則比對成功,回傳對應的結點,比對不成功最侄訓傳 null,

3.3 put方法

put 方法的具體實作是在 putVal 方法中,所以我們重點看下面的 putVal 方法,

第一個if判斷,我們一看就知道:做的是哈希表是否為空的判斷,如果為空,呼叫 resize 方法,這個方法作用就是新創建一個哈希表,

第二個if判斷:如果要插入的鍵值對的key對應的哈希值與當前桶結點的哈希值比對為null(不沖突),則直接為這個key創建一個新結點newNode()插入就行了,

下面走到else:與第二個if相反,走到else則說明,要插入的鍵值對與當前桶結點發生沖突了,if是說如果桶上結點的key與我們要插入的key重復,直接確定插入的位置就是該結點(e = p),else if是指采用紅黑樹方法則呼叫紅黑樹對應的方法進行插入,else表示不是紅黑樹,那只能是傳統的單向鏈表結構,只有桶上結點在上面的if中比對不成功,才會走到這個else,它執行的就是將要插入的key與當前桶下掛載的結點一一進行比對,如果比對到鏈表末尾還沒找到重復的key,則newNode創建新結點將要插入的key添加到鏈表末尾,如果鏈表長度超過臨界值,則轉為紅黑樹,else的最后如果找到了重復的key,就break跳出,

else下面的if:就是說明此時已經找到了(我們要插入的key與桶中某個結點的key相等),那么就將要插入key對應的value值覆寫掉原先的舊值,同時回傳覆寫掉的那個舊值,

最后的if則是進行是否擴容的判斷,

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

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //如果哈希表為空,則先創建一個哈希表
    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);
    else {
        Node<K,V> e; K k;
        //如果桶上節點的 key 與當前 key 重復,那你就是我要找的節點了
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        //如果是采用紅黑樹的方式處理沖突,則通過紅黑樹的 putTreeVal 方法去插入這個鍵值對
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        //否則就是傳統的單向鏈表結構
        else {
            for (int binCount = 0; ; ++binCount) {
                //到了鏈尾還沒找到重復的 key,則說明 HashMap 沒有包含該鍵
                if ((e = p.next) == null) {
                    //創建一個新節點插入到尾部
                    p.next = newNode(hash, key, value, null);
                    //如果鏈的長度大于 TREEIFY_THRESHOLD 這個臨界值,則把鏈變為紅黑樹
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                //找到了重復的 key
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        //這里表示在上面的操作中找到了重復的鍵,所以這里把該鍵的值替換為新值
        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;
}

put方法實作原理敘述:如果哈希表為null,則創建哈希表;通過計算哈希值確定要映射到哪個桶,如果要插入的key與當前桶沒有沖突,則直接插入;如果要插入的key與當前桶上結點沖突,則處理碰撞沖突(如果是紅黑樹則采用紅黑樹方法進行插入;否則就是單向鏈表,對當前桶上結點一一遍歷,如果最終都不沖突,則將該key插入到鏈表末尾;如果鏈表長度達到臨界值,則轉為紅黑樹);如果桶中存在重復的鍵,則將該鍵的舊值替換為要插入的新值,最終判斷size是否大于閾值,大于則執行擴容操作,

3.4 remove方法

remove 方法的具體實作在 removeNode 方法中,所以我們重點看下面的 removeNode 方法,

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) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    //如果當前 key 映射到的桶不為空
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node<K,V> node = null, e; K k; V v;
        //如果桶上的節點就是要找的 key,則直接命中
        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);
            }
        }
        //比對找到的 key 的 value 跟要洗掉的是否匹配
        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
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

3.5 hash方法

在get 方法和put方法中都需要先計算 key映射到哪個桶上,然后才進行之后的操作,計算的主要代碼如下:

(n - 1) & hash

上面代碼中的 n 指的是哈希表的大小,hash 指的是 key 的哈希值,hash 是通過下面這個方法計算出來的,采用了二次哈希的方式,其中 key 的 hashCode 方法是一個 native 方法:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

這個 hash 方法先通過 key 的 hashCode 方法獲取一個哈希值,再拿這個哈希值與它的高 16 位的哈希值做一個異或操作來得到最后的哈希值,計算程序可以參考下圖,

為啥要這樣做呢?注釋中是這樣解釋的:如果當 n 很小,假設為 64 的話,那么 n-1 即為 63(0x111111),這樣的值跟 hashCode()直接做與操作,實際上只使用了哈希值的后 6 位,如果當哈希值的高位變化很大,低位變化很小,這樣就很容易造成沖突了,所以這里把高低位都利用起來,從而解決了這個問題,

正是因為與的這個操作,決定了 HashMap 的大小只能是 2 的冪次方,想一想,如果不是2的冪次方,會發生什么事情?即使你在創建HashMap的時候指定了初始大小,HashMap 在構建的時候也會呼叫下面這個方法來調整大小:

這個方法的作用看起來可能不是很直觀,它的實際作用就是把 cap 變成第一個大于等于 2 的冪次方的數,

例如,16 還是 16,13 就會調整為 16,17 就會調整為 32,

static final int tableSizeFor(int cap) {
    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;
}

3.6 resize方法

當HashMap中的元素越來越多的時候,hash沖突的幾率也就越來越高,因為陣列的長度是固定的,所以為了提高查詢的效率,就要對HashMap的陣列進行擴容,而在HashMap陣列擴容之后,最消耗性能的點就出現了:原陣列中的資料必須重新計算其在新陣列中的位置,并放進去,這就是resize,

那么HashMap 什么時候進行擴容呢 ?

當HashMap中的元素個數超過陣列大小(陣列總大小length不是陣列中個數 size)*loadFactor 時 , 就 會 進 行 數 組 擴 容 , loadFactor 的 默 認 值
(DEFAULT_LOAD_FACTOR)為0.75,這是一個折中的取值,也就是說,默認情況下,陣列大小(DEFAULT_INITIAL_CAPACITY)為16,那么當HashMap中元素個數超過16*0.75=12(這個值就是代碼中的threshold值,也叫做臨界值)的時候,就把陣列的大小擴展為 2*16=32,即擴大一倍,然后重新計算每個元素在陣列中的位置,而這是一個非常消耗性能的操作,所以如果我們已經預知HashMap中元素的個數,那么預設元素的個數能夠有效的提高HashMap的性能,


在這里有一個需要注意的地方,有些文章指出當哈希表的 桶占用超過閾值時就進行擴容,這是不對的;

實際上是當哈希表中的 鍵值對 個數超過閾值時,才進行擴容的,

3.7 size方法

回傳map集合中鍵值對的個數,

public int size() {
    return size;
}

3.8 isEmpty方法

判斷map集合是否為空,

public boolean isEmpty() {
    return size == 0;
}

3.9 clear方法

清空map集合,首先判斷哈希表是否為空,為空的情況下,將size置為0,遍歷哈希表依次清空每隔鍵值對,

public void clear() {
    Node<K,V>[] tab;
    modCount++;
    if ((tab = table) != null && size > 0) {
        size = 0;
        for (int i = 0; i < tab.length; ++i)
            tab[i] = null;
    }
}

3.10 containsKey方法

判斷map集合中是否包含指定的key,內部實作是呼叫了getNode方法,這個在上面將get方法的時候已經說過了,

public boolean containsKey(Object key) {
    return getNode(hash(key), key) != null;
}

3.11 containsValue方法

判斷map集合中是否包含指定的value,

public boolean containsValue(Object value) {
    Node<K,V>[] tab; V v;
    if ((tab = table) != null && size > 0) {
        for (int i = 0; i < tab.length; ++i) {
            for (Node<K,V> e = tab[i]; e != null; e = e.next) {
                if ((v = e.value) == value ||
                    (value != null && value.equals(v)))
                    return true;
            }
        }
    }
    return false;
}

3.12 replace方法

將map集合中指定key對應的舊值替換為一個新值,

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;
}

3.13 關于遍歷map集合的三個方法

這三個方法分別是 keySet、entrySet、forEach,原始碼我這里就不再多說了,下面給出的是測驗代碼,

@Test
public void test1() {
    Map<String,Object> map = new HashMap<>();
    map.put("1001","張起靈");
    map.put("1002","小哥");
    map.put("1003","小宋");

    System.out.println("keySet方法遍歷map集合: ");
    Set<String> stringSet = map.keySet();
    Iterator<String> iterator = stringSet.iterator();
    while (iterator.hasNext()) {
        String key = iterator.next();
        Object value = map.get(key);
        System.out.println("鍵: " + key + ", 值: " + value);
    }

    System.out.println("======================================");

    System.out.println("entrySet方法遍歷map集合: ");
    Set<Map.Entry<String,Object>> entrySet = map.entrySet();
    Iterator<Map.Entry<String,Object>> entryIterator = entrySet.iterator();
    while (entryIterator.hasNext()) {
        Map.Entry<String, Object> entry = entryIterator.next();
        String key = entry.getKey();
        Object value = entry.getValue();
        System.out.println("鍵: " + key + ", 值: " + value);
    }

    System.out.println("======================================");

    System.out.println("forEach方法遍歷map集合: ");
    map.forEach((key,value) -> System.out.println("鍵: " + key + ", 值: " + value));
}


4.傳統HashMap的缺點——引入紅黑樹

(1)JDK 1.8 以前 HashMap 的實作是 陣列+鏈表,即使哈希函式取得再好,也很難達到元素百分百均勻分布,

(2)當 HashMap 中有大量的元素都存放到同一個桶中時,這個桶下有一條長長的鏈表,這個時候 HashMap 就相當于一個單鏈表,假如單鏈表有 n 個元素,遍歷的時間復雜度就是 O(n),完全失去了它的優勢,

(3)針對這種情況,JDK 1.8 中引入了紅黑樹(查找時間復雜度為 O(logn))來優化這個問題,

HashMap 是陣列+ 鏈表+ 紅黑樹(JDK1.8 增加了紅黑樹部分)實作的,

關于紅黑樹的三個關鍵引數,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/304519.html

標籤:java

上一篇:6000字總結動態記憶體管理

下一篇:Java--敲重點!JDK1.8 HashMap特性及底層陣列+單鏈表+紅黑樹知識(建議收藏)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more