Java面試??贾瓹oncurrentHashMap多線程擴(kuò)容機(jī)制詳解
一、引言
ConcurrentHashMap 技術(shù)在互聯(lián)網(wǎng)技術(shù)使用如此廣泛,幾乎所有的后端技術(shù)面試官都要在 ConcurrentHashMap 技術(shù)的使用和原理方面對小伙伴們進(jìn)行 360° 的刁難。
作為一個在互聯(lián)網(wǎng)公司面一次拿一次 Offer 的面霸,打敗了無數(shù)競爭對手,每次都只能看到無數(shù)落寞的身影失望的離開,略感愧疚(請允許我使用一下夸張的修辭手法)。
本篇文章大約讀取時間十分鐘,帶你徹底掌握 ConcurrentHashMap 的 魔鬼式的多線程擴(kuò)容 邏輯,目錄如下:

二、擴(kuò)容流程
我們上一篇文章講了關(guān)于 ConcurrentHashMap 的存儲流程:聊聊ConcurrentHashMap的存儲流程,熟悉了其數(shù)據(jù)是如何的放到數(shù)組、鏈表、紅黑樹中
今天這篇文章,我們重點講一下 ConcurrentHashMap 魔鬼式的多線程擴(kuò)容邏輯
系好安全帶,我們發(fā)車了!
1、treeifyBin方法觸發(fā)擴(kuò)容
// 在鏈表長度大于等于8時,嘗試將鏈表轉(zhuǎn)為紅黑樹
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
// 數(shù)組不能為空
if (tab != null) {
// 數(shù)組的長度n,是否小于64
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
// 如果數(shù)組長度小于64,不能將鏈表轉(zhuǎn)為紅黑樹,先嘗試擴(kuò)容操作
// 這里的擴(kuò)容傳入的數(shù)據(jù)是當(dāng)前數(shù)據(jù)的2倍
tryPresize(n << 1);
// 省略部分代碼……
}
}2、tryPreSize方法-針對putAll的初始化操作
private final void tryPresize(int size) {
// 如果當(dāng)前傳過來的size大于最大值MAXIMUM_CAPACITY >>> 1,則取 MAXIMUM_CAPACITY
// 反之取 tableSizeFor(size + (size >>> 1) + 1)= size的1.5倍 + 1
// tableSizeFor:這個方法我們之前也講過,找size的最近二次冪
// 這個地方的c
int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : tableSizeFor(size + (size >>> 1) + 1);
int sc;
// 當(dāng)前的sizeCtl大于等于0,證明不屬于異常狀態(tài)
// static final int MOVED = -1; // 代表當(dāng)前hash位置的數(shù)據(jù)正在擴(kuò)容!
// static final int TREEBIN = -2; // 代表當(dāng)前hash位置下掛載的是一個紅黑樹
// static final int RESERVED = -3; // 預(yù)留當(dāng)前索引位置……
while ((sc = sizeCtl) >= 0) {
Node<K,V>[] tab = table; int n;
// 如果當(dāng)前的數(shù)組為空,走初始化的邏輯
// 有人可能會好奇,這里怎么會為空呢?
// 大家可以去看下putAll的源碼(放在下面),源碼里面第一句調(diào)用的就是tryPresize的方法
if (tab == null || (n = tab.length) == 0) {
// 進(jìn)來執(zhí)行初始化
// sc是初始化長度,初始化長度如果比計算出來的c要大的話,直接使用sc,如果沒有sc大,
// 說明sc無法容納下putAll中傳入的map,使用更大的數(shù)組長度
n = (sc > c) ? sc : c;
// 還是老配方,CAS將SIZECTL嘗試從sc變成-1
if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
// Step1:判斷引用
// Step2:創(chuàng)建數(shù)組
// Step3:修改sizeCtl數(shù)值
if (table == tab) {
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
}
}
// 如果計算出來的長度c如果小于等于sc,直接退出循環(huán)結(jié)束方法
// 數(shù)組長度大于等于最大長度了,直接退出循環(huán)結(jié)束方法
// 不需要擴(kuò)容
else if (c <= sc || n >= MAXIMUM_CAPACITY)
break;
// 省略部分代碼
}
}
// putAll方法
public void putAll(Map<? extends K, ? extends V> m) {
// 直接走擴(kuò)容的邏輯
tryPresize(m.size());
for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
putVal(e.getKey(), e.getValue(), false);
}3、tryPreSize方法-計算擴(kuò)容戳
private final void tryPresize(int size) {
int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
tableSizeFor(size + (size >>> 1) + 1);
int sc;
while ((sc = sizeCtl) >= 0) {
Node<K,V>[] tab = table;
// 當(dāng)前數(shù)組的長度
int n;
// 判斷引用是否一致
if (tab == table) {
// 計算擴(kuò)容標(biāo)識戳,根據(jù)當(dāng)前數(shù)組的長度計算一個16位的擴(kuò)容戳
// 1、保證后面的SIZECTL賦值是負(fù)數(shù)
// 2、記錄當(dāng)前從什么長度開始擴(kuò)容
int rs = resizeStamp(n);
// 這里存在bug
// 由于我們上面(sc = sizeCtl) >= 0的判斷,sc只能大于0
// 這里永遠(yuǎn)大于等于0
if (sc < 0) {
// 協(xié)助擴(kuò)容的代碼
}
// 代表當(dāng)前沒有線程進(jìn)行擴(kuò)容,我是第一個擴(kuò)容的
else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
}
}
}
// 計算擴(kuò)容標(biāo)識戳
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}4、tryPreSize方法-對sizeCtl的修改
private final void tryPresize(int size) {
// sc默認(rèn)為sizeCtl
while ((sc = sizeCtl) >= 0) {
else if (tab == table) {
// rs:擴(kuò)容戳 00000000 00000000 10000000 00011010
int rs = resizeStamp(n);
if (sc < 0) {
// 說明有線程正在擴(kuò)容,過來幫助擴(kuò)容
// 我們之前第一個線程擴(kuò)容的時候,將sizeCtl設(shè)置成:10000000 00011010 00000000 00000010
// 所以:sc = 10000000 00011010 00000000 00000010
Node<K,V>[] nt;
// 依然有BUG
// 當(dāng)前線程擴(kuò)容時,老數(shù)組長度是否和我當(dāng)前線程擴(kuò)容時的老數(shù)組長度一致
// 00000000 00000000 10000000 00011010
if ((sc >>> RESIZE_STAMP_SHIFT) != rs
// 10000000 00011010 00000000 00000010
// 00000000 00000000 10000000 00011010
// 這兩個判斷都是有問題的,核心問題就應(yīng)該先將rs左移16位,再追加當(dāng)前值。
// 這兩個判斷是BUG
// 判斷當(dāng)前擴(kuò)容是否已經(jīng)即將結(jié)束
|| sc == rs + 1 // sc == rs << 16 + 1 BUG
// 判斷當(dāng)前擴(kuò)容的線程是否達(dá)到了最大限度
|| sc == rs + MAX_RESIZERS // sc == rs << 16 + MAX_RESIZERS BUG
// 擴(kuò)容已經(jīng)結(jié)束了。
|| (nt = nextTable) == null
// 記錄遷移的索引位置,從高位往低位遷移,也代表擴(kuò)容即將結(jié)束。
|| transferIndex <= 0)
break;
// 如果線程需要協(xié)助擴(kuò)容,首先就是對sizeCtl進(jìn)行+1操作,代表當(dāng)前要進(jìn)來一個線程協(xié)助擴(kuò)容
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
// 上面的判斷沒進(jìn)去的話,nt就代表新數(shù)組
transfer(tab, nt);
}
// 是第一個來擴(kuò)容的線程
// 基于CAS將sizeCtl修改為:10000000 00011010 00000000 00000010
// 將擴(kuò)容戳左移16位之后,符號位是1,就代碼這個值為負(fù)數(shù)
// 低16位在表示當(dāng)前正在擴(kuò)容的線程有多少個:低16位為2,代表當(dāng)前有一個線程正在擴(kuò)容
// 為什么低16位值為2時,代表有一個線程正在擴(kuò)容
// 每一個線程擴(kuò)容完畢后,會對低16位進(jìn)行-1操作,當(dāng)最后一個線程擴(kuò)容完畢后,減1的結(jié)果還是-1,
// 當(dāng)值為-1時,要對老數(shù)組進(jìn)行一波掃描,查看是否有遺漏的數(shù)據(jù)沒有遷移到新數(shù)組
else if (U.compareAndSwapInt(this, SIZECTL, sc,(rs << RESIZE_STAMP_SHIFT) + 2))
// 調(diào)用transfer方法,并且將第二個參數(shù)設(shè)置為null,就代表是第一次來擴(kuò)容!
transfer(tab, null);
}
}
}5、transfer方法-計算每個線程遷移的長度
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
// 數(shù)組長度
int n = tab.length;
// 每個線程一次性遷移多少數(shù)據(jù)到新數(shù)組
int stride;
// 基于CPU的線程數(shù)來計算:NCPU = Runtime.getRuntime().availableProcessors()
// MIN_TRANSFER_STRIDE = 16
// NCPU = 4
// 舉個例子:N = 1024 - 512 - 256 - 128 / 4 = 32
// 如果算出來每個線程的長度小于16的話,直接使用16
// 如果大于16的話,則使用N
// 如果線程數(shù)只有1的話,直接就是原數(shù)組長度
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE){
stride = MIN_TRANSFER_STRIDE;
}
}6、 transfer方法-構(gòu)建新數(shù)組并查看標(biāo)識屬性
// 以32位長度數(shù)組擴(kuò)容到64位為例子
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
// 數(shù)組長度
int n = tab.length;
// 每個線程一次性遷移多少數(shù)據(jù)到新數(shù)組
int stride;
// 第一個進(jìn)來擴(kuò)容的線程需要把新數(shù)組構(gòu)建出來
if (nextTab == null) {
try {
// 將原數(shù)組長度左移一位創(chuàng)建新數(shù)組
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
// 賦值操作
nextTab = nt;
} catch (Throwable ex) {
sizeCtl = Integer.MAX_VALUE;
return;
}
// 將成員變量的新數(shù)組賦值volatile修飾
nextTable = nextTab;
// 遷移數(shù)據(jù)時用到的標(biāo)識volatile修飾
transferIndex = n;
}
// 新數(shù)組長度
int nextn = nextTab.length;
// 老數(shù)組遷移完數(shù)據(jù)后,做的標(biāo)識
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// 遷移數(shù)據(jù)時,需要用到的標(biāo)識
boolean advance = true;
boolean finishing = false;
}7、transfer方法-線程領(lǐng)取遷移任務(wù)
// 以32位長度數(shù)組擴(kuò)容到64位為例子
// 數(shù)組長度
int n = tab.length;
// 每個線程一次性遷移多少數(shù)據(jù)到新數(shù)組
int stride;
// 遷移數(shù)據(jù)時用到的標(biāo)識volatile修飾
transferIndex = n;
// 新數(shù)組長度
int nextn = nextTab.length;
// 老數(shù)組遷移完數(shù)據(jù)后,做的標(biāo)識
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// advance:true,代表當(dāng)前線程需要接收任務(wù),然后再執(zhí)行遷移
// 如果為false,代表已經(jīng)接收完任務(wù)
boolean advance = true;
// 標(biāo)識遷移是否結(jié)束
boolean finishing = false;
// 開始循環(huán)
for (int i = 0, bound = 0;;) {
Node<K,V> f;
int fh;
while (advance) {
int nextIndex;
int nextBound;
// 第一次:這里肯定進(jìn)不去
// 主要判斷當(dāng)前任務(wù)是否執(zhí)行完畢
if (--i >= bound || finishing)
advance = false;
// 第一次:這里肯定也進(jìn)不去
// 判斷transferIndex是否小于等于0,代表沒有任務(wù)可領(lǐng)取,結(jié)束了。
// 在線程領(lǐng)取任務(wù)會,會對transferIndex進(jìn)行修改,修改為transferIndex - stride
// 在任務(wù)都領(lǐng)取完之后,transferIndex肯定是小于等于0的,代表沒有遷移數(shù)據(jù)的任務(wù)可以領(lǐng)取
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
// nextIndex=32
// stride=16
// nextIndex > stride ? nextIndex - stride : 0:當(dāng)前的nextIndex是否大于每個線程切割的
// 是:nextIndex - stride/否:0
// 將TRANSFERINDEX從nextIndex變成16
else if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?nextIndex - stride : 0))) {
// 對bound賦值(16)
bound = nextBound;
// 對i賦值(31)
i = nextIndex - 1;
// 賦值,代表當(dāng)前領(lǐng)取任務(wù)結(jié)束
// 該線程當(dāng)前領(lǐng)取的任務(wù)是(16~31)
advance = false;
}
}
}線程領(lǐng)取擴(kuò)容任務(wù)的流程:

8、transfer方法-擴(kuò)容是否已經(jīng)結(jié)束
// 判斷擴(kuò)容是否已經(jīng)結(jié)束!
// i < 0:當(dāng)前線程沒有接收到任務(wù)!
// i >= n: 遷移的索引位置,不可能大于數(shù)組的長度,不會成立
// i + n >= nextn:因為i最大值就是數(shù)組索引的最大值,不會成立
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
// 第一次到這里:必定是false
// 如果再次到這里的時候,最后一個擴(kuò)容的線程也掃描完了
if (finishing) {
nextTable = null;
table = nextTab;
// 重新更改當(dāng)前的sizeCtl的數(shù)值:0.75N
sizeCtl = (n << 1) - (n >>> 1);
return;
}
// 到這里代表當(dāng)前線程已經(jīng)不需要去擴(kuò)容了
// 那么它要把當(dāng)前并發(fā)擴(kuò)容的線程數(shù)減一
// SIZECTL ----> sc - 1
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
// 判斷當(dāng)前這個線程是不是最后一個擴(kuò)容線程
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
// 如果不是的話,直接返回就好了
return;
// 如果是的話,當(dāng)前線程還需要將舊數(shù)據(jù)全部掃描一遍,校驗一下是不是都遷移完了
finishing = advance = true;
// 將當(dāng)前的i重新變成原數(shù)組的長度
// 重新進(jìn)行一輪校驗
i = n; // recheck before commit
}
}9、 transfer方法-遷移數(shù)據(jù)(鏈表)
// 如果當(dāng)前的i位置上沒有數(shù)據(jù),直接不需要遷移
else if ((f = tabAt(tab, i)) == null)
// 將原來i位置上CAS標(biāo)記成fwd
// fwd的Hash為Move
advance = casTabAt(tab, i, null, fwd);
// // 拿到當(dāng)前i位置的hash值,如果為MOVED,證明數(shù)據(jù)已經(jīng)遷移過了。
else if ((fh = f.hash) == MOVED)
// 那么直接返回即可,主要是最后一個擴(kuò)容線程掃描的時候使用
advance = true; // already processed
else {
// 鎖住
synchronized (f) {
// 再次校驗
if (tabAt(tab, i) == f) {
// ln:null - lowNode
Node<K,V> ln;
// hn:null - highNode
Node<K,V> hn;
// 如果當(dāng)前的fh是正常的
if (fh >= 0) {
// 求當(dāng)前的runBit
// 這里只會有兩個結(jié)果:0 或者 n
int runBit = fh & n;
// 用當(dāng)前l(fā)astRun指向f
Node<K,V> lastRun = f;
// 進(jìn)行鏈表的遍歷
for (Node<K,V> p = f.next; p != null; p = p.next) {
// 求每一個鏈表的p.hash & n
int b = p.hash & n;
// 如果當(dāng)前的b不等于上一個節(jié)點
// 需要更新下runBit的數(shù)據(jù)和lastRun的指針
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
// 如果最后runBit=0的話
// lastRun賦值給ln
if (runBit == 0) {
ln = lastRun;
hn = null;
}
// 如果最后runBit=0的話
// lastRun賦值給hn
else {
hn = lastRun;
ln = null;
}
// 從數(shù)組頭節(jié)點開始一直遍歷到lastRun位置
for (Node<K,V> p = f; p != lastRun; p = p.next) {
// hash/key/value
int ph = p.hash; K pk = p.key; V pv = p.val;
// 如果當(dāng)前的是0,則掛到ln下面
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
// 如果當(dāng)前的是1,則掛到hn下面
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
// 將新數(shù)組i位置設(shè)置成ln
setTabAt(nextTab, i, ln);
// 將新數(shù)組i+N位置設(shè)置成hn
setTabAt(nextTab, i + n, hn);
// 將原來數(shù)組的i位置設(shè)置成fwd,代表遷移完畢
setTabAt(tab, i, fwd);
// 將advance設(shè)置成true,保證進(jìn)行上面的while循環(huán)
// 執(zhí)行上面的i--,進(jìn)行下一節(jié)點的遷移計劃
advance = true;
}
}
}
}
}9.1 LastRun機(jī)制
假如當(dāng)前我需要將數(shù)組從 16 擴(kuò)容至 32,我們看下原來的結(jié)構(gòu):

當(dāng)我們進(jìn)行下面這一步時:
int runBit = fh & n;
for (Node<K,V> p = f.next; p != null; p = p.next) {
// 求每一個鏈表的p.hash & n
int b = p.hash & n;
// 如果當(dāng)前的b不等于上一個節(jié)點
// 需要更新下runBit的數(shù)據(jù)和lastRun的指針
if (b != runBit) {
runBit = b;
lastRun = p;
}
}圖片如下:紫色代表 0,粉色代表 1

在遷移之前,我們要掌握一個知識,由于我們的數(shù)組是 2 倍擴(kuò)容,所以原始數(shù)據(jù)的數(shù)據(jù)一定會落在新數(shù)組的 2 和 N + 2 的位置

最后將我們的數(shù)組從原始數(shù)組直接遷移過來,由于 lastRun 之后位置的數(shù)據(jù)都是一樣的hash,所以直接全量遷移即可,不需要挨個遍歷,這也是實施 lastRun 的原因,減少時間復(fù)雜度

最終遷移效果如上,遷移完畢,減少 i 的下標(biāo),繼續(xù)遷移下一個數(shù)組的位置。
10、協(xié)助擴(kuò)容
// 如果當(dāng)前的hash是正在擴(kuò)容
if ((fh = f.hash) == MOVED){
// 進(jìn)行協(xié)作式擴(kuò)容
tab = helpTransfer(tab, f);
}
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab;
int sc;
// 第一個判斷:老數(shù)組不為null
// 第二個判斷:新數(shù)組不為null (將新數(shù)組賦值給nextTab)
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
// 拿到擴(kuò)容戳
int rs = resizeStamp(tab.length);
// 第一個判斷:fwd中的新數(shù)組,和當(dāng)前正在擴(kuò)容的新數(shù)組是否相等。
// 相等:可以協(xié)助擴(kuò)容。不相等:要么擴(kuò)容結(jié)束,要么開啟了新的擴(kuò)容
// 第二個判斷:老數(shù)組是否改變了。 相等:可以協(xié)助擴(kuò)容。不相等:擴(kuò)容結(jié)束了
// 第三個判斷:如果正在擴(kuò)容,sizeCtl肯定為負(fù)數(shù),并且給sc賦值
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
// 判斷擴(kuò)容戳是否一致
// 看下transferIndex是否小于0,如果小于的話,說明擴(kuò)容的任務(wù)已經(jīng)被領(lǐng)完了
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
// 如果還有任務(wù)的話,CAS將當(dāng)前的SIZECTL加一,協(xié)助擴(kuò)容
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
// 協(xié)助擴(kuò)容的代碼,上面剛剛分析過
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}三、流程圖

以上就是Java面試??贾瓹oncurrentHashMap多線程擴(kuò)容機(jī)制詳解的詳細(xì)內(nèi)容,更多關(guān)于Java ConcurrentHashMap的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java基礎(chǔ)強(qiáng)化訓(xùn)練輸入錯誤即結(jié)束進(jìn)程
本文主要介紹了Java編程的基礎(chǔ)知識強(qiáng)化應(yīng)用,文中實例涉及到了許多基礎(chǔ)知識,new對象,控制臺輸入,if語句等。很實用,需要的朋友可以參考下2017-09-09
Java StringUtils字符串分割轉(zhuǎn)數(shù)組的實現(xiàn)
這篇文章主要介紹了Java StringUtils字符串分割轉(zhuǎn)數(shù)組的實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09
Java中抽象類和接口的區(qū)別_動力節(jié)點Java學(xué)院整理
java抽象類和接口最本質(zhì)的區(qū)別是接口里不能實現(xiàn)方法--接口中的方法全是抽象方法。抽象類中可實現(xiàn)方法--抽象類中的方法可以不是抽象方法,下文給大家簡單介紹下,需要的的朋友參考下2017-04-04
Java Mybatis框架多表操作與注解開發(fā)詳解分析
MyBatis 是一款優(yōu)秀的持久層框架,它支持自定義 SQL、存儲過程以及高級映射。MyBatis 免除了幾乎所有的 JDBC 代碼以及設(shè)置參數(shù)和獲取結(jié)果集的工作。MyBatis 可以通過簡單的 XML 或注解來配置和映射原始類型、接口和 Java POJO為數(shù)據(jù)庫中的記錄2021-10-10
java學(xué)習(xí)筆記之DBUtils工具包詳解
下面小編就為大家分享一篇java學(xué)習(xí)筆記之DBUtils工具包詳解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-01-01
解決java執(zhí)行cmd命令調(diào)用ffmpeg報錯Concat error - No such filter ''[0,0]
這篇文章主要介紹了java執(zhí)行cmd命令,調(diào)用ffmpeg報錯Concat error - No such filter '[0,0]'解決方法,本文通過截圖實例代碼說明給大家介紹的非常詳細(xì),對大家的工作或?qū)W習(xí)有一定的參考借鑒價值,需要的朋友可以參考下2020-03-03
Java如何獲取數(shù)組和字符串的長度(length還是length())
這篇文章主要介紹了Java如何獲取數(shù)組和字符串的長度(length還是length()),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12

