再有人問你HashMap,把這篇文章甩給他!

来源:https://www.cnblogs.com/lfs2640666960/archive/2020/02/11/12297446.html
-Advertisement-
Play Games

作為一個Java從業者,面試的時候肯定會被問到過HashMap ...


聲明:本文以jdk1.8為主!

搞定HashMap

作為一個Java從業者,面試的時候肯定會被問到過HashMap,因為對於HashMap來說,可以說是Java==集合中的精髓==了,如果你覺得自己對它掌握的還不夠好,我想今天這篇文章會非常適合你,至少,看了今天這篇文章,以後不怕面試被問HashMap了

其實在我學習HashMap的過程中,我個人覺得HashMap還是挺複雜的,如果真的想把它搞得明明白白的,沒有足夠的內力怕是一時半會兒做不到,不過我們總歸是在不斷的學習,因此真的不必強迫自己把現在遇到的一些知識點全部搞懂。

但是,對於HashMap來說,你所掌握的應該足夠可以讓你應對面試,所以今天咱們的側重點就是學會那些經常被問到的知識點。

我猜,你肯定看過不少分析HashMap的文章了,那麼你掌握多少了呢?從一個問題開始吧

新的節點在插入鏈表的時候,是怎麼插入的?

怎麼樣,想要回答這個問題,還是需要你對HashMap有個比較深入的瞭解的,如果僅僅知道什麼key和value的話,那麼回答這個問題就比較難了。

這個問題大家可以先想想,後面我會給出解答,下麵我們一步步的來看HashMap中幾個你必須知道的知識點。

Map是個啥?

HashMap隸屬於Java中集合這一塊,我們知道集合這塊有list,set和map,這裡的HashMap就是Map的實現類,那麼在Map這個大家族中還有哪些重要角色呢?

上圖展示了Map的家族,都是狠角色啊,我們對這些其實都要瞭解並掌握,這裡簡單的介紹下這幾個狠角色:

TreeMap從名字上就能看出來是與樹有關,它是基於樹的實現,而HashMap,HashTable和ConcurrentHashMap都是基於hash表的實現,另外這裡的HashTable和HashMap在代碼實現上,基本上是一樣的,還記得之前在講解ArrayList的時候提到過和Vector的區別嘛?這裡他們是很相似的,一般都不怎麼用HashTable,會用ConcurrentHashMap來代替,這個也需要好好研究,它比HashTable性能更好,它的鎖粒度更小。

由於這不是本文的重點,只做簡單說明,後續會發文單獨介紹。

簡單來說,Map就是一個映射關係的數據集合,就是我們常見的k-v的形式,一個key對應一個value,大致有這樣的圖示

這隻是簡單的概念,放到具體的實例當中,比如在HashMap中就會衍生出很多其他的問題,那麼HashMap又是個啥?

HashMap是個啥

上面簡單提到過,HashMap是基於Hash表的實現,因此,瞭解了什麼是Hash表,那對學習HashMap是相當重要。

建議瞭解了哈希表之後再學習HashMap,這樣很多難懂的也就不那麼難理解了。

接著,HashMap是基於hash表的實現,而說到底,它也是用來存儲數據供我們使用的,那麼底層是用什麼來存儲數據的呢?可能有人猜到了,還是數組,為啥還是數組?想想之前的ArrayList。

所以,對於HashMap來說,底層也是基於數組實現,只不過這個數組可能和你印象中的數組有些許不同,我們平常整個數組出來,裡面會放一些數據,比如基礎數據類型或者引用數據類型,數組中的每個元素我們沒啥特殊的叫法。

但是在HashMap中人家就有了新名字,我發現這個知識點其實很多人都不太清楚:

在HashMap中的底層數組中,每個元素在jdk1.7及之前叫做Entry,而在jdk1.8之後人家又改名叫做Node。

這裡可能還是會有人好奇這Entry和Node長啥樣,這個看看源碼就比較清楚了,後面我們會說。

到了這裡你因該就能簡單的理解啥是HashMap了,如果你看過什麼是哈希表了,你就會清楚,在HashMap中同樣會出現哈希表所描述的那些問題,比如:

  1. 如何確定添加的元素在底層數組的哪個位置?
  2. 怎麼擴容?
  3. 出現衝突了怎麼處理?
  4. 。。。

沒事,這些問題我們後續都會談到。

HashMap初始化大小是多少

先來看HashMap的基礎用法:

HashMap map = new HashMap();
 

就這樣,我們創建好了一個HashMap,接下來我們看看new之後發生了什麼,看看這個無參構造函數吧

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

解釋下新面孔:

  1. loadFactor : 負載因數,之前聊哈希表的時候說過這個概念
  2. DEFAULT_LOAD_FACTOR : 預設負載因數,看源碼知道是0.75

很簡單,當你新建一個HashMap的時候,人家就是簡單的去初始化一個負載因數,不過我們這裡想知道的是底層數組預設是多少嘞,顯然我們沒有得到我們的答案,我們繼續看源碼。

在此之前,想一下之前ArrayList的初始化大小,是不是在add的時候才創建預設數組,這裡會不會也一樣,那我們看看HashMap的添加元素的方法,這裡是put

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

 

這裡大眼一看,有兩個方法;

  1. putVal 重點哦
  2. hash

這裡需要再明確下,這是我們往HashMap中添加第一個元素的時候,也就是第一次調用這個put方法,可以猜想,現在數據已經過來了,底層是不是要做存儲操作,那肯定要弄個數組出來啊,好,離我們想要的結果越來越近了。

先看這個hash方法:

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

 

記得之前聊哈希表的時候說過,哈希表的數據存儲有個很明顯的特點,就是根據你的key使用哈希演算法計算得出一個下標值,對吧。

而這裡的hash就是根據key得到一個hash值,並沒有得到下標值哦。

重點要看這個putVal方法,可以看看源碼:

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

 

咋樣,是不是感覺代碼一下變多了,我們這裡逐步的有重點的來看,先看這個:

if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;​

 

這個table是啥?

transient Node<K,V>[] table;
 

看到了,這就是HashMap底層的那個數組,之前說了jdk1.8中數組中的每個元素叫做Node,所以這就是個Node數組。

那麼上面那段代碼啥意思嘞?其實就是我們第一次往HashMap中添加數據的時候,這個Node數組肯定是null,還沒創建嘞,所以這裡會去執行resize這個方法。

resize方法的主要作用就是初始化和增加表的大小,說白了就是第一次給你初始化一個Node數組,其他需要擴容的時候給你擴容

看看源碼:

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        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; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        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;
                    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 { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            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;
    }
 

感覺代碼也是比較多的啊,同樣,我們關註重點代碼:

newCap = DEFAULT_INITIAL_CAPACITY; 

有這麼一個賦值操作,DEFAULT_INITIAL_CAPACITY字面意思理解就是初始化容量啊,是多少呢?

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
 

這裡是個移位運算,就是16,現在已經確定具體的預設容量是16了,那具體在哪創建預設的Node數組呢?繼續往下看源碼,有這麼一句

Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
 

ok,到這裡我們發現,第一次使用HashMap添加數據的時候底層會創建一個長度為16的預設Node數組。

那麼新的問題來了?

為啥初始化大小是16

這個問題想必你在HashMap相關分析文章中也看到過,那麼該怎麼回答呢?

想搞明白為啥是16不是其他的,那首先要知道為啥HashMap的容量要是2的整數次冪?

為什麼容量要是 2 的整數次冪?

先看這個16是怎麼來的:

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
 

這裡使用了位運算,為啥不直接16嘞?這裡主要是位運算的性能好,為啥位運算性能就好,那是因為位運算人家直接操作記憶體,不需要進行進位轉換,要知道電腦可是以二進位的形式做數據存儲啊,知道了吧,那16嘞?為啥是16不是其他的?想要知道為啥是16,我們得從HashMap的數據存放特性來說。

對於HashMap而言,存放的是鍵值對,所以做數據添加操作的時候會根據你傳入的key值做hash運算,從而得到一個下標值,也就是以這個下標值來確定你的這個value值應該存放在底層Node數組的哪個位置。

那麼這裡一定會出現的問題就是,不同的key會被計算得出同一個位置,那麼這樣就衝突啦,位置已經被占了,那麼怎麼辦嘞?

首先就是衝突了,我們要想辦法看看後來的數據應該放在哪裡,就是給它找個新位置,這是常規方法,除此之外,我們是不是也可以聚焦到hash演算法這塊,就是儘量減少衝突,讓得到的下標值能夠均勻分佈。

好了,以上巴拉巴拉說一些理念,下麵我們看看源碼中是怎麼計算下標值得:

i = (n - 1) & hash

這是在源碼中第629行有這麼一段,它就是計算我們上面說的下標值的,這裡的n就是數組長度,預設的就是16,這個hash就是這裡得到的值:

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

繼續看它:

i = (n - 1) & hash

 

這裡是做位與運算,接著我們還需要先搞明白一個問題

為什麼要進行取模運算以及位運算

要知道,我們最終是根據key通過哈希演算法得到下標值,這個是怎麼得到的呢?通常做法就是拿到key的hashcode然後與數組的容量做取模運算,為啥要做取模運算呢?

比如這裡預設是一個長度為16的Node數組,我們現在要根據傳進來的key計算一個下標值出來然後把value放入到正確的位置,想一下,我們用key的hashcode與數組長度做取模運算,得到的下標值是不是一定在數組的長度範圍之內,也就是得到的下標值不會出現越界的情況。

要知道取模是怎麼回事啊!明白了這點,我們再來看:

i = (n - 1) & hash

這裡就是計算下標的,為啥不是取模運算而是位與運算呢?使用位與運算的一方面原因就是它的性能比較好,另外一點就是這裡有這麼一個等式:

(n - 1) & hash  =  n % hash
 

因此,總結起來就是使用位與運算可以實現和取模運算相同的效果,而且位與運算性能更高!

接著,我們再看一個問題

為什麼要減一做位運算

理解了這個問題,我們就快接近為什麼容量是2的整數次冪的答案了,根據上面說的,這裡的n-1是為了實現與取模運算相同的效果,除此之外還有很重要的原因在裡面。

在此之前,我們需要看看什麼是位與運算,因為我怕這塊知識大家之前不註意忘掉了,而它對理解我們現在所講的問題很重要,看例子:

比如拿5和3做位與運算,也就是5 & 3 = 1(操作的是二進位),怎麼來的呢?

5轉換為二進位:0000 0000 0000 0000 0000 0000 0000 0101

3轉換為二進位:0000 0000 0000 0000 0000 0000 0000 0011

1轉換為二進位:0000 0000 0000 0000 0000 0000 0000 0001

所以啊,位與運算的操作就是:第一個操作數的的第n位於第二個操作數的第n位如果都是1,那麼結果的第n位也為1,否則為0

看懂了吧,不懂得話可以去補補這塊的知識,後續我也會單獨發文詳細說說這塊。

我們繼續回到之前的問題,為什麼做減一操作以及容量為啥是2的整數次冪,為啥嘞?

告訴你個秘密,2的整數次冪減一得到的數非常特殊,有啥特殊嘞,就是2的整數次冪得到的結果的二進位,如果某位上是1的話,那麼2的整數次冪減一的結果的二進位,之前為1的後面全是1

啥意思嘞,可能有點繞,我們先看2的整數次冪啊,有2,4,8,16,32等等,我們來看,首先是16的二進位是: 10000 ,接著16減一得15,15的二進位是: 1111 ,再形象一點就是:

16轉換為二進位:0000 0000 0000 0000 0000 0000 0001 0000

15轉換為二進位:0000 0000 0000 0000 0000 0000 0000 1111

再對照我給你說的秘密,看看懂了不,可以再來個例子:

32轉換為二進位:0000 0000 0000 0000 0000 0000 0010 0000

31轉換為二進位:0000 0000 0000 0000 0000 0000 0001 1111

這會總該懂了吧,然後我們再看計算下標的公式:

(n - 1) & hash  =  n % hash
 

n是容量,它是2的整數次冪,然後與得到的hash值做位於運算,因為n是2的整數次冪,減一之後的二進位最後幾位都是1,再根據位與運算的特性,與hash位與之後,得到的結果是不是可能是0也可能是1,,也就是說最終的結果取決於hash的值,如此一來,只要輸入的hashcode值本身是均勻分佈的,那麼hash演算法得到的結果就是均勻的。

啥意思?這樣得到的下標值就是均勻分佈的啊,那衝突的幾率就減少啦。

而如果容量不是2的整數次冪的話,就沒有上述說的那個特性,這樣衝突的概率就會增大。

所以,明白了為啥容量是2的整數次冪了吧。

那為啥是16嘞?難道不是2的整數次冪都行嘛?理論上是都行,但是如果是2,4或者8會不會有點小,添加不了多少數據就會擴容,也就是會頻繁擴容,這樣豈不是影響性能,那為啥不是32或者更大,那不就浪費空間了嘛,所以啊,16就作為一個非常合適的經驗值保留了下來!

出現哈希衝突怎麼解決

我們上面也提到了,在添加數據的時候儘管為實現下標值的均勻分佈做了很多努力,但是勢必還是會存在衝突的情況,那麼該怎麼解決衝突呢?

這就牽涉到哈希衝突的解決辦法了,瞭解了哈希衝突的解決辦法之後我們還要關註一個問題,那就是新的節點在插入到鏈表的時候,是怎麼插入的?

回答開篇的問題

現在你應該知道,當出現hash衝突,可以使用鏈表來解決,那麼這裡就有問題,新來的Node是應該放在之前Node的前面還是後面呢?

Java8之前是頭插法,啥意思嘞,就是放在之前Node的前面,為啥要這樣,這是之前開發者覺得後面插入的數據會先用到,因為要使用這些Node是要遍歷這個鏈表,在前面的遍歷的會更快。

為什麼使用尾插法?

但是在Java8及之後都使用尾插法了,就是放到後面,為啥這樣?

這裡主要是一個鏈表成環的問題,啥意思嘞,想一下,使用頭插法是不是會改變鏈表的順序,你後來的就應該在後面嘛,如果擴容的話,由於原本鏈表順序有所改變,擴容之後重新hash,可能導致的情況就是擴容轉移後前後鏈表順序倒置,在轉移過程中修改了原來鏈表中節點的引用關係。

這樣的話在多線程操作下就會出現死迴圈,而使用尾插法,在相同的前提下就不會出現這樣的問題,因為擴容前後鏈表順序是不變的,他們之間的引用關係也是不變的。

關於擴容

下麵我們繼續說HashMap的擴容,經過上面的分析,我們知道第一次使用HashMap是創建一個預設長度為16的底層Node數組,如果滿了怎麼辦,那就需要進行擴容了,也就是之前談及的resize方法,這個方法主要就是初始化和增加表的大小,關於擴容要知道這兩個概念:

  1. Capacity:HashMap當前長度。
  2. LoadFactor:負載因數,預設值0.75f。

這裡怎麼擴容的呢?首先是達到一個條件之後會發生擴容,什麼條件呢?就是這個負載因數,比如HashMap的容量是100,負載因數是0.75,乘以100就是75,所以當你增加第76個的時候就需要擴容了,那擴容又是怎麼樣步驟呢?

首先是創建一個新的數組,容量是原來的二倍,為啥是2倍,想一想為啥容量是2的整數次冪,這裡擴容為原來的2倍不正好符號這個規則嘛。

然後會經過重新hash,把原來的數據放到新的數組上,至於為啥要重新hash,那必須啊,你容量變了,相應的hash演算法規則也就變了,得到的結果自然不一樣了。

關於鏈表轉紅黑樹

在Java8之前是沒有紅黑樹的實現的,在jdk1.8中加入了紅黑樹,就是當鏈表長度為8時會將鏈表轉換為紅黑樹,為6時又會轉換成鏈表,這樣時提高了性能,也可以防止哈希碰撞攻擊。

HashMap增加新元素的主要步驟

下麵我們分析一下HashMap增加新元素的時候都會做哪些步驟:

  1. 首先肯定時根據key值,通過哈希演算法得到value應該放在底層數組中的下標位置
  2. 根據這個下標定位到底層數組中的元素,當然,這裡可能時鏈表,也可能時樹,知道為啥吧,給你個提醒,鏈表轉紅黑樹
  3. 拿到當前位置上的key值,與要放入的key比較,是否==或者equals,如果成立的話就替換value值,並且需要返回原來的值
  4. 當然,如果是樹的話就要迴圈樹中的節點,繼續==和equals的判斷,成立替換,否則添加到樹里
  5. 鏈表的話就是迴圈遍歷了,同樣的判斷,成立替換,否則就添加到鏈表的尾部

所以啊,這裡面的重點就是判斷放入HashMap中的元素要不要替換當前節點的元素,那怎麼判斷呢?總結起來只要滿足以下兩點即可替換:

1、hash值相等。

2、==或equals的結果為true。

感謝閱讀

好了,到了這裡就差不多了,開篇就說過HashMap可以說是Java集合的精髓了,想要徹底搞懂真心不容易,但是我們所掌握的應該足夠應對平常的面試,關於HashMap更多的高級內容,後續會繼續分享。

感謝大家的閱讀,如有錯誤之處歡迎指正!

想要閱讀更多精彩內容,可以關註我的微信公眾號:Java技術zhai,這是我的私人公眾號,專註於Java原創,主要涉及數據結構與演算法,電腦基礎以及Java核心知識的講解,期待你的參與。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 報錯信息: 1 Access to XMLHttpRequest at 'http://192.168.37.130:5050/socket.io/?EIO=3&transport=polling&t=N0oqNsW' from origin 'http://localhost:8080' has ...
  • 全局作用域和局部作用域 全局作用域 局部作用域:函數作用域 全局作用域在全局和局部都可以訪問到,局部作用域只能在局部被訪問到 var name="cyy"; function fn(){ var age=25; console.log(name);//cyy console.log(age);//2 ...
  • 什麼是網頁?html文檔經過瀏覽器內核渲染後展示出來的頁面(五大主流瀏覽器及四大內核) html文檔文件名尾碼是.html,之前存在的.htm是為支持DOM系統(目前織夢還是用.htm文件名結尾文件) 編碼是我們通過代碼的形式把想要展示的頁面寫到html文檔裡面,接著渲染作為一個動作,主要是ht... ...
  • 最初使用回調函數 ​ 由於最初j s官方沒有明確的規範,各種第三方庫中封裝的非同步函數中傳的回調函數中的參數沒有明確的規範, 沒有明確各個參數的意義, 不便於使用。 ​ 但是node中有明確的規範 ​ node中的的回調模式: 1. 所有回調函數必須有兩個參數,第一個參數表示錯誤,第二個參數表示結果 ...
  • 前言 準確的說eval處理過的代碼應該叫做壓縮代碼,不過效果上算是加密過了一樣!很多小伙伴不想直接讓別人看到自己的js代碼往往就會採取這樣的處理措施。不過,其實這樣的方法只能防禦那些小白。對於真正的大佬來說,破解就是秒秒鐘的事!真的就是秒秒鐘!話不多說,我們直接進入正題! 演示代碼: 操作步驟 1. ...
  • 前言 微信紅包業務,發紅包之後如果24小時之內沒有被領取完就自動過期失效。 架構設計 業務流程 老闆發紅包,此時緩存初始化紅包個數,紅包金額(單位分),並非同步入庫。 紅包數據入延遲隊列,唯一標識+失效時間 紅包數據出延遲隊列,根據唯一標識清空紅包緩存數據、非同步更新資料庫、非同步退回紅包金額 代碼案例 ...
  • 前言 截至2020年,Java仍然是構建Web應用程式的最流行的編程語言之一,儘管它必須面對來自Go,Python和TypeScript等新型語言的激烈競爭。 在Java世界內部,Spring框架已成為微服務開發的事實上的標準,通過諸如Spring Boot和Spring Data之類的庫,該框架易 ...
  • 本文將介紹一個重要的 "數據結構" —棧,和之前講到的 "鏈表" 、 "數組" 一樣也是一種數據呈 線性排列 的數據結構,不過在這種結構中,我們只能訪問最新添加的數據。棧就像是一摞書,拿到新書時我們會把它放在書堆的最上面,取書時也只能從最上面的新書開始取。 棧 如上就是棧的概念圖,現在存儲在棧中的只 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...