天天看點

【Java幹貨】Java中7個常見代碼審查案例

一、ConcurrentHashMap 陷阱:

以下代碼使用Watcher來記錄特定Key的調用次數,考慮到線程安全,使用了ConcurrentHashMap, 這段代碼真的能實作線程安全嗎?

public class Watcher
{
       private ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

       public void add(String key)
        {
              Integer value = map.get(key);
              if(value == null)
              {
                   map.put(key, 1);
               }
               else
               {
                   map.put(key, value + 1);
                }
             }
         }           

答案:這個代碼片段是線程非安全的,比如 A、B 兩個線程同時調用 add("newKey")方法,如下表所示。

【Java幹貨】Java中7個常見代碼審查案例

T4 時刻時,線程 B 覆寫了線程 A 在 T3 時刻的值,系統調用了 add("newKey")兩次,但“newKey”的值為 1。

以下是一個正确的代碼片段:

private ConcurrentHashMap<String, AtomicInteger> countMap
      = new ConcurrentHashMap<String, AtomicInteger> ();
public void add(String key){
           AtomicInteger count = countMap.computeIfAbsent(key k->new AtomicInteger());
           //自增
           count.incrementAndGet();
       }           

二、字元串搜尋:

以下代碼的作用是搜尋“{”出現的位置,是否有性能更好的替代方法?

String str = ...;
int index = str.indexOf("{");           

答案:考慮到隻搜尋單個 char,String 還提供了根據 char 來搜尋的 API。高效代碼如下:

int index = str.indexOf('{'};           

類似的還有 String.replace(char,char)、StringBuilder.append(char),比如拼接一個 JSON,可以使用 append(char)方法:

StringBuilder sb = new StringBuilder();
sb.append('{')...append(':')...apend('}')           

三、I/O輸出:

以下代碼輸出 HTML,從性能的角度考慮有沒有調整的地方?

try {
   writer.write("<html>");
   writer.write("<body>");
   writer.write("<h1>"+topic.getTitle+"</h1>");
   writer.write("</body>");
   writer.write("</html>");
 } catch (IOException e) {
   throws e;
  }           

答案:可以考慮合并輸出,減小 write 方法的調用次數。Writer 類内部的一些操作可能會影 響性能,比如,如果 Writer 是一個 StringWriter 執行個體,那麼 write 方法内部還會有擴容操作。如果是一個 Servlet 的 Writer,則可能會檢測是否需要輸出到用戶端。合并調用有助于提高性能:

writer.write("<html><body>");
writer.write("<h1>"+topic.getTitle+"</h1>");
writer.write("</body></html>");           

另一方面,可以考慮使用模闆技術,比如模闆引擎。編寫一個模闆:

<html>
    <body>
         <h1>${topic.title}</h1>
    </body>
</html>           

模闆使得輸出内容更容易維護。同時模闆引擎也非常高效,性能幾乎跟采用 StringBuilder拼接字元串一樣高。大部分模闆引擎都會合并靜态文本,但很多 Web 伺服器的 JSP 在反編譯的時候卻沒有合并靜态文本。

四、字元串拼接:

以下代碼需要把對象的位址清單拼接成一個字元串,如何提高性能?

public String buildProvince(List<Org> orgs){
     StringBuilder sb = new StringBuilder();
      for(Org org:orgs){
          if(sb.length()!=0){
             sb.append(",")
           }
           sb.append(org.getProvinceId())
       }
       return sb.toString();
     }           

答案:可以考慮預先指定一個 StringBuilder 的容量,如果 provinceId 的長度不超過 3 位,那麼可以估算出 StringBuilder 的容量。使用逗号分隔,可以将 append(",")改成 append(',')。最後,也可以預先将 provinceId 對應的 int 轉為字元串以避免 int 轉 String 的性能消耗。調整後的代碼如下:

//建立一個緩存
static int cacheSize = 1024;
static String[] caches = new String[cacheSize];
static {
   for(int i=0;i<cacheSize;i++){
       caches[i] = String.valueOf(i);
    }
  }

  public String buildProvince(List<Org> orgs){
       if(orgs.isEmpty()){
         return "";
        }
        StringBuilder sb = new StringBuilder(orgs.size()*4);
        for(Org org:orgs){
            if(sb.getLength!=0){
              sb.append(',')
            }
            sb.append(int2String(org.getProvinceId())
          }
          return sb.toString();
       }
    
       public static String int2String(int data) {
            if (data < cacheSize) {
              return caches[data];
            } else {
              return String.valueOf(data);
             }
         }           

五、方法的入參和出參:

以下這段代碼是否有改善的地方?

public Map<String, Object> queryPathInfo(Map<String, Object> param) {
      return Dao.selectOne(basepath + "selRedeemInfo", param);
}           

類似的,有的方法使用 JsonObject 作為輸入參數或傳回值:

JsonObject funcName(Context req, JsonObject params)           

答案:無論使用 Map,還是使用 JsonObject,作為入參或傳回值,都非常難以閱讀。除非再次閱讀方法的完整實作過程,才會知道 Map 或 JsonObject 到底包含什麼樣的資料。建議使用具體的對象代替這種了解較為抽象的類:

public PathInfo queryPathInfo(PathQuery param) {
       //大部分 Dao 架構都支援映射資料庫結果集到一個個具體的類上
       return Dao.selectOne(basepath + "selRedeemInfo", param,PathInfo.class);
}           

六、RPC 調用定義的傳回值:

一個 RPC 架構的傳回值的定義如下,使用 Object 數組是否可行?

public Object[] rpcCall(Request request ){
      try{
         Object ret = proxy.query(request);
          return new Object[]{true,ret};
      }catch(Exception ex){
          return new Object[]{false};
      }
    }           

類似的,一個 JPA 的查詢結果如下:

@Query("select t.itemtype, count(t.id) from TxxItem t where t.parentCode like ?1
and t.state='1' group by t.itemtype ")
        public List<Object[]> getFaciCount(String parentCode );           

答案:使用對象,而不要使用數組。

public CallResult rpcCall(Request request){}
class CallResult{
   boolean success;
   Object ret;
}           

JPA 的查詢結果傳回 List,這也使得維護者不得不閱讀 SQL 才知道傳回的值,而且對于數字類型,count 傳回的結果根據驅動或資料庫的不同而不同,有的傳回 BigDecimal,有的傳回BigInteger,這種寫法帶來了隐患。

七、Integer 的使用:

以下代碼判斷兩個整數是否相等,代碼是否有問題?

public boolean isEqual(Integer a,Integer b){
return a==b;
}           

下面這段代碼的最終結果是 null 嗎?

Double a = 1D;
Double b = 2D;
Double c = null;
Double d = isSuccess() ? a*b : c;           

答案:這裡涉及裝箱和拆箱,第一個例子使用“==”比較兩個對象是否是同一個,在數字-128 到 127 之間,因為這個範圍的裝箱都使用了緩存,是以可以這麼用,如果數字不在這個範圍内,則應該用 equals 進行比較。

應該改成如下代碼:

public boolean isEqual(Integer a,Integer b){
     if(a==b){
        return true;
      }
      if(a==null||b==null){
        return false;
      }
      return a.equals(b);
    }           

在三元表達式中,如果 isSuccess()傳回 false,則代碼片段會抛出空指針。位元組碼如下:

INVOKESTATIC com/ibeetl/code/style/Test1.isSuccess ()Z

IFEQ L0
//比較,如果為 true
  ALOAD 1 //變量 a
  INVOKEVIRTUAL java/lang/Double.doubleValue ()D
  ALOAD //變量 b
  INVOKEVIRTUAL java/lang/Double.doubleValue ()D
  //a*b
  DMUL
  GOTO L1
L0
//false:
   ALOAD 3 //變量 c,拆箱導緻空指針
   INVOKEVIRTUAL java/lang/Double.doubleValue ()D
L1
INVOKESTATIC java/lang/Double.valueOf (D)Ljava/lang/Double;
ASTORE 4
RETURN           

可以看到,在三元表達式中,由于 a*b 有拆箱,是以變量 c 也會拆箱,導緻空指針。可以改成以下代碼:

Double d = null;
if(isSuccess()){
   d = a*b;
}else{
   d = c;
}           

内容摘自《高性能Java系統權威指南》第八章

【Java幹貨】Java中7個常見代碼審查案例

李家智 著

本書特點:

内容上,總結作者從事Java開發20年來在頭部IT企業的高并發系統經曆的真實案例,極具參考意義和可讀性。

對于程式員和架構師而言,Java 系統的性能優化是一個超正常的挑戰。這是因為 Java 語言和 Java 運作平台,以及 Java 生态的複雜性決定了 Java 系統的性能優化不再是簡單的更新配置或者簡單的 “空間換時間”的技術實作,這涉及 Java 的各種知識點。

本書從高性能、易維護、代碼增強以及在微服務系統中編寫Java代碼的角度來描述如何實作高性能Java系統,結合真實案例,讓讀者能夠快速上手實戰。

風格上,本書的風格偏實戰,讀者可以下載下傳書中的示例代碼并運作測試。讀者可以從任意一章開始閱讀,掌握性能優化知識為公司的系統所用。

本書适合:

中進階程式員和架構師;

以及有志從事基礎技術研發、開源工具研發的極客閱讀;

也可以作為 Java 筆試和面試的參考書。