- J3 - 白起
- 技術(集合框架 # ArrayList原始碼)
上一篇介紹了ArrayList的簡單使用(👉ArrayList簡單介紹),并分析了它的一些基本方法的內部實作步驟,但是由于篇幅的原因我有很多點都沒有很細致的去展開講,
這篇將會深入其底層原始碼進行細致的分析,盡量做到通俗易懂從而帶你們更好的理解這個ArrayList,那我們開始吧!
所有原始碼都是基于JDK1.8
一、ArrayList的屬性分析
我們先認識一下ArrayList這個類中定義的一些屬性
// 定義陣列的初始容量
private static final int DEFAULT_CAPACITY = 10;
// 定義一個空的陣列
private static final Object[] EMPTY_ELEMENTDATA = {};
// 定義一個默認的空陣列
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 定義存盤元素的陣列,transient:表述序列化的時候該修飾符修飾的屬性不被序列化
transient Object[] elementData; // non-private to simplify nested class access
// 陣列最大容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// 陣列中元素的個數
private int size;
// 陣列被修改的次數,如添加洗掉元素都會加 1
protected transient int modCount = 0;
看了這些屬性,是不是有些疑問?
問題 1 :為什么要定義兩個空的陣列DEFAULTCAPACITY_EMPTY_ELEMENTDATA和EMPTY_ELEMENTDATA?
問題 2 :為什么ArrayList中的陣列要用 transient 修飾,并且我們序列化的時候陣列還是可以被序列化與反序列化?
這兩個問題是我們看了這個類中的所有屬性時候有的疑慮,那么我們先別急,接著往后看,這些在后面都會給你們解答出來,
二、構造器分析
我們都知道,使用一個類的第一步那就是需要創建對應的物件出來才可以使用它,而創建一個物件的正常步驟(不考慮反射等方法創建物件)就是通過其構造方法創建物件,那么我們的第一步就是分析ArrayList的構造器,
ArrayList向我們提供了三種構造器:
- 無參構造器:public ArrayList()
- 帶初始容量構造器:public ArrayList(int initialCapacity)
- 帶集合引數的構造器:public ArrayList(Collection<? extends E> c)
2.1 無參構造器
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
從源代碼中我們可以看出,如果我們不帶任何引數的去創建物件那么其內部會直接將一個默認的空陣列賦值給ArrayList的陣列(elementData)
那么問題來了:為什么不是賦值這個空陣列:EMPTY_ELEMENTDATA,way?
別急我們接著看帶參構造,在那里我會說明這個問題,
2.2 帶初始容量構造器
// 帶參構造,initialCapacity:傳入的初始容量
public ArrayList(int initialCapacity) {
// 1. 判斷是否大于 0
if (initialCapacity > 0) {
// 2. 創建一個對應大小的陣列
this.elementData = new Object[initialCapacity];
// 3. 是否等于 0
} else if (initialCapacity == 0) {
// 4. 賦值一個空的陣列
this.elementData = EMPTY_ELEMENTDATA;
} else {
// 5. 傳入的容量不合法
throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
}
}
在這里其它的步驟我們都好理解,但,是不是就是這個3,4兩步是不是就有點不明所以呀!那我們來分析分析,
從上面兩個構造器來看:
- 空參的時候
ArrayList中的陣列是它:DEFAULTCAPACITY_EMPTY_ELEMENTDATA - 容量是 0 的時候
ArrayList中的陣列是它:EMPTY_ELEMENTDATA
那么問題就簡化到空容量和0容量的問題了,有的人會說這不一樣的嘛!有什么好區別的,
其實不是的,這兩個的區別還是蠻大的,我們一貫的思維就是不傳值就是空容量陣列,傳值就是對應的容量陣列,那我們有沒有想過如果一個人他就是想創建一個容量為 0 的陣列,而不是一來就給我默認擴容到 10 這個容量,
怎么樣是不是有點道理了,
所以我們可以得一個結論,DEFAULTCAPACITY_EMPTY_ELEMENTDATA和EMPTY_ELEMENTDATA就是在擴容得時候區別出來到底是擴容為 10 還是從 0 開始一步步得擴容,(這是上面問題 1 的解釋)
具體可以看這個方法
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 判斷ArrayList中的陣列是哪種型別
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// DEFAULTCAPACITY_EMPTY_ELEMENTDATA型別,直接擴容到 10
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
// EMPTY_ELEMENTDATA型別,從 0 開始一步步擴容上去
return minCapacity;
}
2.3 帶集合引數的構造器
public ArrayList(Collection<? extends E> c) {
// 將傳入得集合變成陣列,賦值給ArrayList的陣列
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// 將型別轉為Object然后再次呼叫copyOf進行賦值
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 傳入的是空的集合,那么賦值一個容量為EMPTY_ELEMENTDATA型別的空陣列
this.elementData = EMPTY_ELEMENTDATA;
}
}
這個帶集合引數的構造器,也很好理解,唯一要說明的地方就是這個
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
明明已經賦過值了elementData = c.toArray(),那為什么還要整出上面的一個邏輯?
其實關鍵在于toArray()方法,它回傳的不是一個 Object[] ,而是 E[] 型別,意味著如果不轉成 Object[] ,你想某個位置add一個Object的子類時,這個時候就會出現例外,
@Test
public void demoTest03() {
// 字串型別
String[] strings = new String[8];
strings[0] = "a";
strings[1] = "b";
// 向上轉型為Object型別
Object[] objects = strings;
// 定義一個Object型別資料
Object obj = 1;
// 賦值給Object型別陣列,在這會出錯:java.lang.ArrayStoreException
objects[3] = obj;
System.out.println(Arrays.toString(objects));
}
總結:該代碼的功能就是將elementData陣列中的所有元素變為Object型別,防止在向ArrayList中添加資料的時候拋錯(ArrayStoreException)
三、添加分析
ArrayList中向我們提供了四種添加元素的方法
- 向末尾添加元素:public boolean add(E e)
- 指定位置添添加元素:public void add(int index, E element)
- 添加一個集合元素:public boolean addAll(Collection<? extends E> c)
- 在指定位置添加集合元素:public boolean addAll(int index, Collection<? extends E> c)
我們先不急著分析這四種方法的原始碼,我們先來試者去猜想一下它們實作的時候會考慮到那些方面:
- ArrayList底層也是一個陣列,那么無限的添加是不是要考慮容量的問題(擴容)
- 既然是擴容,那么改擴多少,怎么擴容
- 向指定地方添加元素的時候 ,是不是要考慮下標合理問題
- 如果指定下標中已經有元素,那么該如何操作,是添加失敗還是覆寫
以上這些就是ArrayList在添加元素的方法內部應該注意的點,而在ArrayList中這些點主要的實作邏輯就是下面兩個方法
ensureCapacityInternal(int minCapacity):陣列容量判斷,容量夠就不做處理,容量不足就進行相應的擴容rangeCheckForAdd(index): 檢查下標時候合理,如果合理不做處理,否則拋出例外
那么我們先來分析這兩個方法
3.1 ensureCapacityInternal(int minCapacity)
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
我們可以看出它中間又呼叫了兩個方法
calculateCapacity(elementData, minCapacity):確定陣列容量ensureExplicitCapacity(object):進行相應的擴容
3.1.1 calculateCapacity(elementData, minCapacity)
// 確定陣列容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {、
// 如果陣列是默認的空陣列
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 回傳連個容量的最大值,就是DEFAULT_CAPACITY = 10
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 否者,陣列不空,回傳minCapacity
return minCapacity;
}
我們可以看出該方法有兩個引數
- elementData:存放元素的陣列
- minCapacity:可以放下元素的最小的容量
根據我寫的注釋可以看出,該方法的功能就是確定陣列的容量,空陣列就是 10 ,陣列不空就是陣列中元素個數加 1
3.1.2 ensureExplicitCapacity(object)
// 進行相應的擴容
private void ensureExplicitCapacity(int minCapacity) {
// 陣列修改次數加一
modCount++;
// 計算的最小容量是否大于陣列的長度
if (minCapacity - elementData.length > 0)
// 擴容
grow(minCapacity);
}
可以看出,該方法主要是判斷其內部的陣列是否允許再添加元素,如果容量不夠則進行擴容從而保證元素的正常添加而不溢位,
那我們具體來分析一下grow(minCapacity)方法
3.1.3 grow(minCapacity)
// 真正擴容方法
private void grow(int minCapacity) {
// 獲取陣列的長度
int oldCapacity = elementData.length;
// 計算新得長度,新長度為舊長度的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 判斷計算的新長度與傳入的最小容量的大小
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 開始擴容
elementData = Arrays.copyOf(elementData, newCapacity);
}
從這個方法,我們就可以知道如果ArrayList中如果陣列容量不足,則會擴容到原來的1.5倍,而具體的擴容操作這是要看Arrays.copyOf(elementData, newCapacity)這個方法的具體實作了,
那我們點進去瞅瞅
public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
再呼叫下面方法:
// 擴容方法的具體實作
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
// 創建指定長度的某種型別的陣列,
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
// 呼叫本地方法將舊陣列元素移動到新陣列中
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
// 回傳新陣列
return copy;
}
在這里我們可以看到,它會先創建一個指定容量大小的陣列,該陣列就是擴容后的陣列,并且需要被回傳出去,
然后這個本地方法System.arraycopy()作用就是將舊陣列元素移動到新陣列中,那為什么用非Java所寫的C++方法呢!我想應該就是為了追求效率,因為擴容會移動很多元素用C++顯然是比較快的,
由于我們不能看到native所寫的方法,那么我就畫個圖來解釋一下這個方法的原理

3.2 rangeCheckForAdd(index)
經過了上面的主流方法的分析,這個方法就相對來說簡單了很多,具體如下
private void rangeCheck(int index) {
// 如果傳入的下標大于等于陣列中的元素個數,溢位
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
怎么樣,簡單吧!就是一個簡單的判斷,滿足則不做處理否則拋錯
3.3 add(E e)
有了上面兩個重量級的方法講解,我相信現在對于添加方法來說,挺簡單的,那往下看咯!
public boolean add(E e) {
// 確保陣列容量
ensureCapacityInternal(size + 1); // Increments modCount!!
// 在陣列末尾添加元素
elementData[size++] = e;
// 回傳添加成功
return true;
}
怎么樣,簡單吧!ensureCapacityInternal這個方法我已經分析過了,它會確保我們添加元素的時候容量是充足的,然后就會直接添加元素到陣列末尾,最后再回傳成功標識,
在這里,我們也可以解釋ArrayList為什么可以添加重復的值并且輸出的值與我們輸入的值順序一致的問題,
3.4 add(int index, E element)
public void add(int index, E element) {
// 1. 檢查下標
rangeCheckForAdd(index);
// 2. 保證容量
ensureCapacityInternal(size + 1); // Increments modCount!!
// 3. 開始移動元素,空出指定下標的位置出來
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
// 4. 在指定下標出賦值
elementData[index] = element;
// 5. 陣列元素值加 1
size++;
}
1 - 5步前面都已經介紹過了,就不多說了而且我注釋已經寫的很清楚了,如果我們細心點可以得知,如果下標處有元素,則直接覆寫,
3.5 addAll(Collection<? extends E> c)
// 添加一個集合到陣列中
public boolean addAll(Collection<? extends E> c) {
// 將集合轉為陣列
Object[] a = c.toArray();
// 獲取陣列長度
int numNew = a.length;
// 保證容量
ensureCapacityInternal(size + numNew); // Increments modCount
// 開始向目標陣列中,添加元素
System.arraycopy(a, 0, elementData, size, numNew);
// 設定元素個數
size += numNew;
// 回傳結果
return numNew != 0;
}
向ArrayList中直接添加一個集合方法中我們可以看出集合元素會直接添加在末尾,和add方法基本類似,
3.6 addAll(int index, Collection<? extends E> c)
public boolean addAll(int index, Collection<? extends E> c) {
// 1. 檢查下標
rangeCheckForAdd(index);
// 2. 將集合轉為陣列
Object[] a = c.toArray();
// 3. 獲取陣列長度
int numNew = a.length;
// 4. 保證容量
ensureCapacityInternal(size + numNew); // Increments modCount
// 5. 計算需要移動元素的開始下標
int numMoved = size - index;
if (numMoved > 0)
// 6. 開始移動元素
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
// 7. 開始向目標陣列中添加元素
System.arraycopy(a, 0, elementData, index, numNew);
// 8. 設定元素個數
size += numNew;
// 9. 回傳結果
return numNew != 0;
}
在指定下標處添加一個集合的元素,關鍵點在于要計算出一個區間的下標出來,存放添加的集合資料,該實作代碼在步驟5,6處可以看出,
四、設定方法
public E set(int index, E element) {
// 檢查下標
rangeCheck(index);
// 獲取對應下標資料
E oldValue = elementData(index);
// 在對應下標處賦值
elementData[index] = element;
// 回傳原始資料
return oldValue;
}
下面是rangeCheck方法
private void rangeCheck(int index) {
// 下標是否大于ArrayList中的元素個數
if (index >= size)
// 下標越界錯誤
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
看到設定方法,是不是也很簡單,大致步驟:
- 檢查下標,下標如果比存盤的元素個數大,那就是溢位需要拋錯,否者不處理
- 然后就是保存舊值,然后再將新值賦上去
- 最后回傳舊值
五、獲取方法分析
5.1 get(int index)
public E get(int index) {
// 檢查下標
rangeCheck(index);
// 回傳對應下標值
return elementData(index);
}
這就沒什么好分析了,看看就行了😁
六、移除方法分析
對于移除方法,ArrayList中提供了挺多的,但是有些我也沒怎么用過,所以就只分析下面幾種:
- 移除對于下標元素:remove(int index)
- 移除對應元素:remove(Object o)
- 移除一個集合的元素:removeAll(Collection<?> c)
- 清空集合:clear()
不過相較于添加,移除操作還是挺簡單的,不需要考慮容量問題,只需要將對應的元素移除,然后移動元素即可,那我們往下面分析吧!
5.1 remove(int index)
public E remove(int index) {
// 檢查下標
rangeCheck(index);
// 修改次數加一
modCount++;
// 獲取對應下標值
E oldValue = elementData(index);
// 計算開始移動元素的下標
int numMoved = size - index - 1;
if (numMoved > 0)
// 開始移動元素
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 將陣列最后元素置空,并且將元素大小減一
elementData[--size] = null; // clear to let GC do its work
// 回傳舊元素
return oldValue;
}
怎么樣,是不是有些熟悉,對于這樣的實作步驟(addAll(int index, Collection<? extends E> c)),
其實這兩個方法有很多共同點,都需要檢查下標,記錄舊值,計算移動的開始下標,回傳舊值,不同的就是在添加方法的時候它需要將元素往后移,留出一個范圍出來,然后再填充值,而移除方法則需要將元素往前移來覆寫需要移除的元素,最后再將元素的末尾一個元素置空并且size減一,下面我畫個圖便于理解:

5.2 remove(Object o)
// 根據元素移除對應的資料
public boolean remove(Object o) {
if (o == null) {
// 遍歷,移除null的元素
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
// 移除
fastRemove(index);
// 回傳成功
return true;
}
} else {
// 遍歷,移除對應元素
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
// 移除
fastRemove(index);
// 回傳成功
return true;
}
}
// 回傳失敗
return false;
}
中間呼叫fastRemove方法移除
// 移除第一個遇到的相等的值
private void fastRemove(int index) {
// 修改次數加一
modCount++;
// 計算需要移動的開始下標
int numMoved = size - index - 1;
if (numMoved > 0)
// 開始將元素向前移動
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 最后的元素置為空
elementData[--size] = null; // clear to let GC do its work
}
從remove(Object o)方法我們可以知道,它只會移除第一個與對應的值相同的元素,
5.3 removeAll(Collection<?> c)
public boolean removeAll(Collection<?> c) {
// 判斷集合時候為null
Objects.requireNonNull(c);
// 批量移除
return batchRemove(c, false);
}
移除ArrayList中對應集合中的元素,共分為兩個步驟
- 判斷入參是否為null
- 開始批量移除
5.3.1 判斷為null方法
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}
這個非常簡單,就是簡單的判空,如空則拋出空指標
5.3.2 批量移除方法
private boolean batchRemove(Collection<?> c, boolean complement) {
// 定義一個指向元素陣列的物件
final Object[] elementData = this.elementData;
int r = 0, w = 0;
boolean modified = false;
try {
// 開始遍歷
for (; r < size; r++)
// 如果要洗掉的集合中,不存在ArrayList中的元素
if (c.contains(elementData[r]) == complement)
// 將集合中的元素放入elementData中,complement=true就是放入存在的元素,否者就是不存在的元素
// w是元素個數
elementData[w++] = elementData[r];
} finally {
// c.contains()會拋出例外
// 在c.contains()拋出例外的時候將例外拋出之前確定的元素進行處理
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
if (w != size) {
// 將 w 下標以后的元素置空,方便垃圾回收,w下標以前的元素就是我們需要的結果
for (int i = w; i < size; i++)
elementData[i] = null;
// 記錄修改次數
modCount += size - w;
// 元素個數
size = w;
// 成功
modified = true;
}
}
return modified;
}
可以看出這個批量移除元素的方法還是有點復雜的,不急,我們結合注釋來慢慢分析:
- 定義一個中間變數(Object陣列),指向裝有元素的陣列
- 遍歷陣列,找出需要的元素,complement = true則找出存在入參集合c中的元素,否則就找出不存在的元素
- r != size判斷是為了在出現例外的情況下也可以正常的進行元素移除
- w != size判斷是為了進行元素置空,在移除一個集合中如果 w = size 時則表示ArrayList中沒有集合中的元素,不需要進行處理,反之則需要通過置空進行移除
- 記錄修改次數然后設定元素個數
分析到這里可能有些伙伴還是不怎么理解上述的步驟,那我們接下來一段代碼一段代碼的來解讀分析
代碼片段一:
for (; r < size; r++)
// 如果要洗掉的集合中,不存在ArrayList中的元素
if (c.contains(elementData[r]) == complement)
// 將集合中的元素放入elementData中,complement=true就是放入存在的元素,否者就是不存在的元素
// w是元素個數
elementData[w++] = elementData[r];
首先這是一個回圈操作,又因為c.contains()是一個可能拋出錯誤的函式,所以這里可以分為兩種情況:
正常走完
- r 等于 size
- w 等于 ArrayList 中元素不存在c集合中的元素個數
例外退出
- r 不等于 size
- w 只等于例外出現之前滿足條件時記錄的元素個數
代碼片段二:
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
我們看條件可以發現,這是一個出現例外情況時才會滿足的情況
- r 等于出現例外時,回圈遍歷的下標
- w 等于出現例外時,回圈記錄ArrayList中不存在c集合中的元素個數
然后就是呼叫移動函式將已經判斷且滿足條件的元素和未判斷的元素進行合并
代碼片段三:
if (w != size) {
// 將 w 下標以后的元素置空,方便垃圾回收,w下標以前的元素就是我們需要的結果
for (int i = w; i < size; i++)
elementData[i] = null;
// 記錄修改次數
modCount += size - w;
// 元素個數
size = w;
// 成功
modified = true;
}
這個代碼片段就好理解了,就是將合并后的集合末尾沒有用的元素置空并將一些屬性設定到正確的值就行
5.3.5 流程圖
例外情況如下,正常情況只是減少了元素移動的步驟,大致流程都差不多

5.4 clear()
清空方法,可以看出非常的簡單,簡單的我都不想做過多的解釋了,你們看代碼就行,
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
// 賦空
elementData[i] = null;
// 元素個數設為 0
size = 0;
}
七、elementData陣列被修飾transient問題
我們知道ArrayList是支持序列化的,那為什么其中關鍵的存盤元素的陣列要被修飾成transient(序列化時忽略該陣列),矛盾了,
其實不然,我們點進原始碼可以發現,ArrayList中自己重寫了序列化和反序列化的方法,代碼如下:
// 序列化
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
// 反序列化
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in capacity
s.readInt(); // ignored
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
int capacity = calculateCapacity(elementData, size);
SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
ensureCapacityInternal(size);
Object[] a = elementData;
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
那為什么要自己實作一套序列化呢!
ArrayList底層是基于動態陣列實作的,陣列的長度是動態變化的,當陣列的長度擴容到很大的時候,其中的元素卻是寥寥幾個的話,那要是將這些沒有用的空元素也序列化到記憶體中就有點非記憶體了,所以就是考慮到這一點,ArrayList才會自己實作一套序列化標準,只序列化有用的元素,這樣可以節省空間,(開頭問題 2的答案)
好了,今天的內容到這里就結束了,關注我,我們下期見
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
CSDN:J3 - 白起
這是一個技術一般,但熱衷于分享;經驗尚淺,但臉皮夠厚;明明年輕有顏值,但非要靠才華吃飯的程式員,
長按下圖二維碼關注,來一場博友之交吧!

^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/259714.html
標籤:java
