上篇我們簡單分析了一下規範中的命名規範、變量申明的時機、if與大括號的規範、包裝類與基礎類型常見問題和規範以及項目開發中的空指針等問題,本篇我們繼續聊聊幾個常見的但是企業開發中比較容易忽略的細節。

不要使用枚舉類型作為返回值

還記得阿里巴巴Java開發手冊上有很多地方提到了關於枚舉的規範:

【參考】枚舉類名帶上 Enum 後綴,枚舉成員名稱需要全大寫,單詞間用下劃線隔開。説明:枚舉其實就是特殊的類,域成員均為常量,且構造方法被默認強制是私有。

【推薦】如果變量值僅在一個固定範圍內變化用 enum 類型來定義。

【強制】二方庫裏可以定義枚舉類型,參數可以使用枚舉類型,但是接口返回值不允許使用枚舉類型或者包含枚舉類型的 POJO 對象。

一個是推薦的參考命名規範,一個是推薦的使用方法,但是我們注意到有一個強制的規範,一般情況下強制的規範都是為了避免企業開發一些細節的風險而標記出來的,可以看到阿里手冊中有提到不允許在接口交互過程中使用枚舉類型的參數進行傳遞,那麼這是為什麼呢?

我們知道,枚舉的使用場景一般為在需要一組同類型的固定常量的時候,我們可以使用枚舉來作為標記替代,因此枚舉類不需要多例存在,保證了單例以後則是可以減少了內存開銷。

並且我們也知道,在編寫枚舉的時候,如果出現如果被 abstract 或 final 修飾,或者是枚舉常量重複,都會立刻被編譯器報錯。並且在整個枚舉類中,除了當前的枚舉常量以外,並不存在其他的任何實例。因為有了這些特性,使得我們在開發過程中,使用常量的時候比較容易。

但是我們深入瞭解後,可以知道枚舉的clone方法被final修飾,因此enum常量並不會被克隆,並且枚舉類禁止通過反射創建實例,保證了絕對的單例,在反序列化的過程中,枚舉類不允許出現實例不相同的情況。而枚舉類在使用過程中最重要的兩個方法分別是:

1.用來根據枚舉名稱獲取對應枚舉常量的 publicstaticT valueOf(String)方法

2.用來獲取當前所有的枚舉常量的 publicstaticT[]values()方法

3.用來克隆的函數- clone

我們分別根據這幾個方法來研究下為什麼有枚舉的這幾個規範,首先我們來看看clone方法的註釋:



/**
 
   
  
* Throws CloneNotSupportedException. This guarantees that enums
 
   
  
* are never cloned, which is necessary to preserve their "singleton"
 
   
  
* status.
 
   
  
*
 
   
  
* @return (never returns)
 
   
  
*/



從註釋上我們就能看出來,枚舉類型不支持clone方法,如果我們調用clone方法,會拋 CloneNotSupportedException異常,而與之相關的還有反射創建實例的 newInstance方法,我們都知道,如果不調用clone方法,一般可以使用反射,並且setAccessible 為 true 後調用 newInstance方法,即可構建一個新的實例,而我們可以看到此方法的源碼:




public T newInstance(Object... initargs)
 
   
  
throwsInstantiationException, IllegalAccessException,
 
   
  
IllegalArgumentException, InvocationTargetException
 
   
  
{
 
   
  
.........
 
   
  
//如果當前的類型為枚舉類型,那麼調用當前方法直接拋異常
 
   
  
if((clazz.getModifiers() & Modifier.ENUM) != 0)
 
   
  
thrownewIllegalArgumentException("Cannot reflectively create enum objects");
 
   
  
.........
 
   
  
return inst;
 
   
  
}




從這我們可以看出,枚舉為了保證不能被克隆,維持單例的狀態,禁止了clone和反射創建實例。那麼我們接着來看序列化,由於所有的枚舉都是Eunm類的子類及其實例,而Eunm類默認實現了 SerializableComparable接口,所以默認允許進行排序和序列化,而排序的方法 compareTo的實現大概如下:

/**
 
   
  
* Compares this enum with the specified object for order. Returns a
 
   
  
* negative integer, zero, or a positive integer as this object is less
 
   
  
* than, equal to, or greater than the specified object.
 
   
  
*
 
   
  
* Enum constants are only comparable to other enum constants of the
 
   
  
* same enum type. The natural order implemented by this
 
   
  
* method is the order in which the constants are declared.
 
   
  
*/
 
   
  
public final int compareTo(E o) {
 
   
  
 Enum> other = (Enum>)o;
 
   
  
 Enum<E> self = this;
 
   
  
 if (self.getClass() != other.getClass() && // optimization
 
   
  
 self.getDeclaringClass() != other.getDeclaringClass())
 
   
  
 throw new ClassCastException();
 
   
  
 return self.ordinal - other.ordinal;
 
   
  
}




而ordinal則是代表每個枚舉常量對應的申明順序,説明枚舉的排序方式默認按照申明的順序進行排序,那麼序列化和反序列化的過程是什麼樣的呢?我們來編寫一個序列化的代碼,debug跟代碼以後,可以看到最終是調用了 java.lang.Enum#valueOf方法來實現的反序列化的。而序列化後的內容大概如下:

arn_enum.CoinEnum?xr?java.lang.Enum?xpt?PENNYq?t?NICKELq?t?DIMEq~?t?QUARTER



大概可以看到,序列化的內容主要包含枚舉類型和枚舉的每個名稱,接着我們看看 java.lang.Enum#valueOf方法的源碼:

/**
 
   
  
* Returns the enum constant of the specified enum type with the
 
   
  
* specified name. The name must match exactly an identifier used
 
   
  
* to declare an enum constant in this type. (Extraneous whitespace
 
   
  
* characters are not permitted.)
 
   
  
*
 
   
  
* 
Note that for a particular enum type {@code T}, the
 
   
  
* implicitly declared {@code public static T valueOf(String)}
 
   
  
* method on that enum may be used instead of this method to map
 
   
  
* from a name to the corresponding enum constant. All the
 
   
  
* constants of an enum type can be obtained by calling the
 
   
  
* implicit {@code public static T[] values()} method of that
 
   
  
* type.
 
   
  
*
 
   
  
* @param The enum type whose constant is to be returned
 
   
  
* @param enumType the {@code Class} object of the enum type from which
 
   
  
* to return a constant
 
   
  
* @param name the name of the constant to return
 
   
  
* @return the enum constant of the specified enum type with the
 
   
  
* specified name
 
   
  
* @throws IllegalArgumentException if the specified enum type has
 
   
  
* no constant with the specified name, or the specified
 
   
  
* class object does not represent an enum type
 
   
  
* @throws NullPointerException if {@code enumType} or {@code name}
 
   
  
* is null
 
   
  
* @since 1.5
 
   
  
*/
 
   
  
public static <T extends Enum<T>> T valueOf(Class<T> enumType,
 
   
  
String name) {
 
   
  
 T result = enumType.enumConstantDirectory().get(name);
 
   
  
 if (result != null)
 
   
  
 return result;
 
   
  
 if (name == null)
 
   
  
 throw new NullPointerException("Name is null");
 
   
  
 throw new IllegalArgumentException(
 
   
  
 "No enum constant " + enumType.getCanonicalName() + "." + name);
 
   
  
}




從源碼和註釋中我們都可以看出來,如果此時A服務使用的枚舉類為舊版本,只有五個常量,而B服務的枚舉中包含了新的常量,這個時候在反序列化的時候,由於name == null,則會直接拋出異常,從這我們也終於看出來,為什麼規範中會強制不允許使用枚舉類型作為參數進行序列化傳遞了。

慎用可變參數

在翻閲各大規範手冊的時候,我看到阿里手冊中有這麼一條:

【強制】相同參數類型,相同業務含義,才可以使用 Java 的可變參數,避免使用 Object 。説明:可變參數必須放置在參數列表的最後。(提倡同學們儘量不用可變參數編程)

正例: public ListlistUsers(String type, Long... ids) {...}

吸引了我,因為在以前開發過程中,我就遇到了一個可變參數埋下的坑,接下來我們就來看看可變參數相關的一個坑。

相信很多人都編寫過企業裏使用的工具類,而我當初在編寫一個Boolean類型的工具類的時候,編寫了大概如下的兩個方法:

private static boolean and(boolean... booleans) {
 
   
  
 for (boolean b : booleans) {
 
   
  
 if (!b) {
 
   
  
 return false;
 
   
  
 }
 
   
  
 }
 
   
  
 return true;
 
   
  
}
 
   
  
private static boolean and(Boolean... booleans) {
 
   
  
 for (Boolean b : booleans) {
 
   
  
 if (!b) {
 
   
  
 return false;
 
   
  
 }
 
   
  
 }
 
   
  
 return true;
 
   
  
}




這兩個方法看起來就是一樣的,都是為了傳遞多個布爾類型的參數進來,判斷多個條件連接在一起,是否能成為true的結果,但是當我編寫測試的代碼的時候,問題出現了:

public static void main(String[] args) {
 
   
  
 boolean result = and(true, true, true);
 
   
  
 System.out.println(result);
 
   
  
}
 
   
  
這樣的方法會返回什麼呢?其實當代碼剛剛編寫完畢的時候,就會發現編譯器已經報錯了,會提示:
 
   
  
Ambiguous method call. Both and (boolean...) in BooleanDemo and and (Boolea
 
   
  
n...) in BooleanDemo match.



模糊的函數匹配,因為編譯器認為有兩個方法都完全滿足當前的函數,那麼為什麼會這樣的呢?我們知道在Java1.5以後加入了自動拆箱裝箱的過程,為了兼容1.5以前的jdk版本,將此過程設置為了三個階段:

java中怎麼去聲明一個枚舉的值 java枚舉命名規範_阿里巴巴java開發規範

而我們使用的測試方法中,在第一階段,判斷jdk版本,是不是不允許自動裝箱拆箱,明顯jdk版本大於1.5,允許自動拆箱裝箱,因此進入第二階段,此時判斷是否存在更符合的參數方法,比如我們傳遞了三個布爾類型的參數,但是如果此時有三個布爾參數的方法,則會優先匹配此方法,而不是匹配可變參數的方法,很明顯也沒有,此時就會進入第三階段,完成裝箱拆箱以後,再去查找匹配的變長參數的方法,這個時候由於完成了拆箱裝箱,兩個類型會視為一個類型,發現方法上有兩個匹配的方法,這時候就會報錯了。

那麼我們有木有辦法處理這個問題呢?畢竟我們熟悉的 org.apache.commons.lang3.BooleanUtils工具類中也有類似的方法,我們都明白,變長參數其實就是會將當前的多個傳遞的參數裝入數組後,再去處理,那麼可以在傳遞的過程中,將所有的參數通過數組包裹,這個時候就不會發生拆箱裝箱過程了!例如:

@Test
 
   
  
public void testAnd_primitive_validInput_2items() {
 
   
  
 assertTrue(
 
   
  
 ! BooleanUtils.and(new boolean[] { false, false })
 
   
  
}



而參考其他框架源碼大神的寫法中,也有針對這個的編寫的範例:

java中怎麼去聲明一個枚舉的值 java枚舉命名規範_java 枚舉類型enum用法_02

通過此種方法可以保證如果傳入的是基本類型,直接匹配當前方法,如果是包裝類型,則在第二階段以後匹配到當前函數,最終都是調用了BooleanUtils中基本類型的and方法。

List的去重與xxList方法

List作為我們企業開發中最常見的一個集合類,在開發過程中更是經常遇到去重,轉換等操作,但是集合類操作的不好很多時候會導致我們的程序性能緩慢或者出現異常的風險,例如阿里手冊中提到過:

【 強 制 】ArrayList的subList結果不可強轉成 ArrayList,否則會拋出ClassCastException異常,即java.util.RandomAccessSubList cannot be cast to java.util.ArrayList。

【強制】在SubList場景中,高度注意對原集合元素的增加或刪除,均會導致子列表的 遍歷、增加、刪除產生 ConcurrentModificationException 異常。

【強制】使用工具類 Arrays.asList () 把數組轉換成集合時,不能使用其修改集合相關的 方法,它的add/remove/clear 方法會拋出 UnsupportedOperationException 異常。

而手冊中的這些xxList方法則是我們開發過程中比較常用的,那麼為什麼阿里手冊會有這些規範呢?我們來看看第一個方法 subList,首先我們先看看SubList類和ArrayList類的區別,從類圖上我們可以看出來兩個類之間並沒有繼承關係:

java中怎麼去聲明一個枚舉的值 java枚舉命名規範_java中怎麼去聲明一個枚舉的值_03

所以手冊上不允許使用 subList強轉為ArrayList,那麼為什麼原集合不能進行增刪改查操作呢?我們來看看其源碼:

/**
 
   
  
* Returns a view of the portion of this list between the specified
 
   
  
* {@code fromIndex}, inclusive, and {@code toIndex}, exclusive. (If
 
   
  
* {@code fromIndex} and {@code toIndex} are equal, the returned list is
 
   
  
* empty.) The returned list is backed by this list, so non-structural
 
   
  
* changes in the returned list are reflected in this list, and vice-versa.
 
   
  
* The returned list supports all of the optional list operations.
 
   
  
*
 
   
  
* 
This method eliminates the need for explicit range operations (of
 
   
  
* the sort that commonly exist for arrays). Any operation that expects
 
   
  
* a list can be used as a range operation by passing a subList view
 
   
  
* instead of a whole list. For example, the following idiom
 
   
  
* removes a range of elements from a list:
 
   
  
* 
 
   
  
* list.subList(from, to).clear();
 
   
  
* 
 
   
  
* Similar idioms may be constructed for {@link #indexOf(Object)} and
* {@link #lastIndexOf(Object)}, and all of the algorithms in the
* {@link Collections} class can be applied to a subList.
*
* 
The semantics of the list returned by this method become undefined if
 
   
  
* the backing list (i.e., this list) is structurally modified
* any way other than via the returned list. (Structural modifications are
* those that change the size of this list, or otherwise perturb it in such
* a fashion that iterations in progress may yield incorrect results.)
*
* @throws IndexOutOfBoundsException {@inheritDoc}
* @throws IllegalArgumentException {@inheritDoc}
*/
publicList subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
returnnewSubList(this, 0, fromIndex, toIndex);
}

我們可以看到代碼的邏輯只有兩步,第一步檢查當前的索引和長度是否變化,第二步構建新的SubList出來並且返回。從註釋我們也可以瞭解到,SubList中包含的範圍,如果對其進行增刪改查操作,都會導致原來的集合發生變化,並且是從當前的index + offSet進行變化。

那麼為什麼我們這個時候對原來的ArrayList進行增刪改查操作的時候會導致SubList集合操作異常呢?我們來看看ArrayList的add方法:

/**
 
   
  
* Appends the specified element to the end of this list.
 
   
  
*
 
   
  
* @param e element to be appended to this list
 
   
  
* @return true (as specified by {@link Collection#add})
 
   
  
*/
 
   
  
publicboolean add(E e) {
 
   
  
 ensureCapacityInternal(size + 1); // Increments modCount!!
 
   
  
 elementData[size++] = e;
 
   
  
returntrue;
 
   
  
}




我們可以看到一點,每次元素新增的時候都會有一個 ensureCapacityInternal(size+1);操作,這個操作會導致modCount長度變化,而modCount則是在SubList的構造中用來記錄長度使用的:

SubList(AbstractList<E> parent,
 
   
  
int offset, int fromIndex, int toIndex) {
 
   
  
 this.parent = parent;
 
   
  
 this.parentOffset = fromIndex;
 
   
  
 this.offset = offset + fromIndex;
 
   
  
 this.size = toIndex - fromIndex;
 
   
  
 this.modCount = ArrayList.this.modCount; // 注意:此處複製了 ArrayList的 modCount
 
   
  
}




而SubList的get操作的源碼如下:

public E get(int index) {
 
   
  
 rangeCheck(index);
 
   
  
 checkForComodification();
 
   
  
returnArrayList.this.elementData(offset + index);
 
   
  
}




可以看到每次都會去校驗一下下標和modCount,我們來看看 checkForComodification方法:

private void checkForComodification() {
 
   
  
 if (ArrayList.this.modCount != this.modCount)
 
   
  
 throw new ConcurrentModificationException();
 
   
  
}




可見每次都會檢查,如果發現原來集合的長度變化了,就會拋出異常,那麼使用SubList的時候為什麼要注意原集合是否被更改的原因就在這裏了。

那麼為什麼asList方法的集合不允許使用新增、修改、刪除等操作呢?

我們來看下和ArrayList的方法比較:

java中怎麼去聲明一個枚舉的值 java枚舉命名規範_java 可變參數_04

很明顯我們能看出來,asList構建出來的List沒有重寫 addremove 函數,説明該類的集合操作的方法來自父類 AbstactList,我們來看看父類的add方法:

public void add(int index, E element) {
 
   
  
 throw new UnsupportedOperationException();
 
   
  
}




從這我們可以看出來,如果我們進行add或者remove操作,會直接拋異常。

集合去重操作

我們再來看一個企業開發中最常見的一個操作,將List集合進行一次去重操作,我本來以為每個人都會選擇使用 Set來進行去重,可是當我翻看團隊代碼的時候發現,居然很多人偷懶選了List自帶的 contains方法判斷是否存在,然後進行去重操作!我們來看看一般我們使用Set去重的時候編寫的代碼:

public static <T> Set<T> removeDuplicateBySet(List<T> data) {
 
   
  
 if (CollectionUtils.isEmpty(data)) {
 
   
  
 return new HashSet<>();
 
   
  
 }
 
   
  
 return new HashSet<>(data);
 
   
  
}




而HashSet的構造方法如下:

publicHashSet(Collection extends E> c) {
 
   
  
 map = newHashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
 
   
  
 addAll(c);
 
   
  
}



主要是創建了一個HashMap以後進行addAll操作,我們來看看addAll方法:

public boolean addAll(Collection extends E> c) {
 
   
  
 boolean modified = false;
 
   
  
 for (E e : c)
 
   
  
 if (add(e))
 
   
  
 modified = true;
 
   
  
 return modified;
 
   
  
}




從這我們也可以看出來,內部循環調用了add方法進行元素的添加:

public boolean add(E e) {
 
   
  
 return map.put(e, PRESENT)==null;
 
   
  
}




而add方法內部依賴了hashMap的put方法,我們都知道hashMap的put方法中的key是唯一的,即天然可以避免重複,我們來看看key的hash是如何計算的:

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




可以看到如果 key 為 null ,哈希值為 0,否則將 key 通過自身 hashCode 函數計算的的哈希值和其右移 16 位進行異或運算得到最終的哈希值,而在最終的 putVal方法中,判斷是否存在的邏輯如下:

p.hash == hash && ((k = p.key) == key || (key != null&& key.equals(k)))

而看到這我們基本已經明瞭了,set的hash計算還是依靠元素自身的hashCode計算,只要我們需要去重的元素實例遵循了重寫hashCode也重寫equals的規則,保持一致,直接使用set進行去重還是很簡單的。反過來我們再來看看List的 contains方法的實現:

/**
 
   
  
* Returns true if this list contains the specified element.
 
   
  
* More formally, returns true if and only if this list contains
 
   
  
* at least one element e such that
 
   
  
* (o==null ? e==null : o.equals(e)).
 
   
  
*
 
   
  
* @param o element whose presence in this list is to be tested
 
   
  
* @return true if this list contains the specified element
 
   
  
*/
 
   
  
public boolean contains(Object o) {
 
   
  
 return indexOf(o) >= 0;
 
   
  
}


可以看到其實是依賴於indexOf方法來判斷的:

/**
 
   
  
* Returns the index of the first occurrence of the specified element
 
   
  
* in this list, or -1 if this list does not contain the element.
 
   
  
* More formally, returns the lowest index i such that
 
   
  
* (o==null ? get(i)==null : o.equals(get(i))),
 
   
  
* or -1 if there is no such index.
 
   
  
*/
 
   
  
public int indexOf(Object o) {
 
   
  
 if (o == null) {
 
   
  
 for (int i = 0; i < size; i++)
 
   
  
 if (elementData[i]==null)
 
   
  
 return i;
 
   
  
 } else {
 
   
  
 for (int i = 0; i < size; i++)
 
   
  
 if (o.equals(elementData[i]))
 
   
  
 return i;
 
   
  
 }
 
   
  
 return -1;
 
   
  
}




可以看到indexOf的邏輯為,如果為null,則遍歷全部元素判斷是否有null,如果不為null也會遍歷所有元素的equals方法來判斷是否相等,所以時間複雜度接近 O(n^2),而Set的containsKey方法主要依賴於getNode方法:

/**
 
   
  
* Implements Map.get and related methods.
 
   
  
*
 
   
  
* @param hash hash for key
 
   
  
* @param key the key
 
   
  
* @return the node, or null if none
 
   
  
*/
 
   
  
finalNode getNode(int hash, Object key) {
 
   
  
Node[] tab; Node first, e; int n; K k;
 
   
  
if((tab = table) != null&& (n = tab.length) > 0&&
 
   
  
(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 instanceofTreeNode)
 
   
  
return((TreeNode)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);
 
   
  
}
 
   
  
}
 
   
  
returnnull;
 
   
  
}


可以看到優先通過計算的hash值找到table的第一個元素比較,如果相等直接返回第一個元素,如果是樹節點則從樹種查找,不是則從鏈中查找,可以看出來,如果hash衝突不是很嚴重的話,查找的速度接近 O(n),很明顯看出來,如果數量較多的話,List的 contains速度甚至可能差距幾千上萬倍!

字符串與拼接

在Java核心庫中,有三個字符串操作的類,分別為 StringStringBufferStringBuilder,那麼勢必會涉及到一個問題,企業開發中經常使用到字符串操作,例如字符串拼接,但是使用的不對會導致出現大量的性能陷阱,那麼在什麼場合下使用String拼接什麼時候使用其他的兩個比較好呢?我們先來看一個案例:

public String measureStringBufferApend() {
 
   
  
StringBuffer buffer = new StringBuffer();
 
   
  
 for (int i = 0; i < 10000; i++) {
 
   
  
 buffer.append("hello");
 
   
  
 }
 
   
  
 return buffer.toString();
 
   
  
}
 
   
  
//第二種寫法
 
   
  
public String measureStringBuilderApend() {
 
   
  
StringBuilder builder = new StringBuilder();
 
   
  
 for (int i = 0; i < 10000; i++) {
 
   
  
 builder.append("hello");
 
   
  
 }
 
   
  
 return builder.toString();
 
   
  
}
 
   
  
//直接String拼接
 
   
  
public String measureStringApend() {
 
   
  
 String targetString = "";
 
   
  
 for (int i = 0; i < 10000; i++) {
 
   
  
 targetString += "hello";
 
   
  
 }
 
   
  
 return targetString;
 
   
  
}



使用JMH測試的性能測試結果可以看出來,使用StringBuffer拼接比String += 的方式效率快了200倍,StringBuilder的效率比Stirng += 的效率快了700倍,這是為什麼呢?

原來String的 += 操作的時候每一次都需要創建一個新的String對象,然後將兩次的內容copy進來,再去銷燬原來的String對象,再去創建。。。。而StringBuffer和StringBuilder之所以快,是因為內部預先分配了一部分內存,只有在內存不足的時候,才會去擴展內存,而StringBuffer和StringBuilder的實現幾乎一樣,唯一的區別就是方法都是 synchronized包裝,保證了在併發下的字符串操作的安全性,因此導致性能會有一定幅度的下降。

那麼是不是String拼接一定就是最快的呢?

也不一定,例如下面的例子:

public void measureSimpleStringApend() {
 
   
  
 for (int i = 0; i < 10000; i++) {
 
   
  
 String targetString = "Hello, " + "world!";
 
   
  
 }
 
   
  
}
 
   
  
//StringBuilder拼接
 
   
  
public void measureSimpleStringBuilderApend() {
 
   
  
 for (int i = 0; i < 10000; i++) {
 
   
  
 StringBuilder builder = new StringBuilder();
 
   
  
 builder.append("hello, ");
 
   
  
 builder.append("world!");
 
   
  
 }
 
   
  
}



相信有經驗的就會發現,直接兩個字符串片段直接 + 的拼接方式,效率竟然比StringBuilder還要快!這個巨大的差異,主要來自於 Java 編譯器和 JVM 對字符串處理的優化。" Hello, " + "world! " 這樣的表達式,並沒有真正執行字符串連接。

編譯器會把它處理成一個連接好的常量字符串"Hello, world!"。這樣,也就不存在反覆的對象創建和銷燬了,常量字符串的連接顯示了超高的效率。

但是我們需要注意一點,如果説拼接的兩個字符串不是片段常量,而是一個變量,那麼效率就會急劇下降,jvm是無法對字符串變量操作進行優化的,例如:

public void measureVariableStringApend() {
 
   
  
 for (int i = 0; i < 10000; i++) {
 
   
  
 String targetString = "Hello, " + getAppendix();
 
   
  
 }
 
   
  
}
 
   
  
private String getAppendix() {
 
   
  
 return "World!";
 
   
  
}


因此我們可以從中總結出來,使用字符串拼接的幾個實踐建議:

1.Java 的編譯器會優化常量字符串的連接,我們可以放心地把長的字符串換成多行,需要注意的是使用 + 拼接常量,而不是 += ,這兩個是不同的概念。

2.帶有變量的字符串連接,StringBuilder 效率更高。如果效率敏感的代碼,建議使用StringBuilder,並且在日常開發中一般都不建議使用StringBuffer,除非當前的確有嚴格的併發安全的要求。

java中怎麼去聲明一個枚舉的值 java枚舉命名規範_阿里巴巴java開發規範_05