《阿里巴巴 Java開發手冊》讀後感

Java3y發表於2018-11-16

前言

只有光頭才能變強

前一陣子一直在學Redis,結果在黃金段位被虐了,暫時升不了段位了,每天都拿不到首勝(好煩)。

趁著學校校運會,合理地給自己放了一個小長假,然後就回家了。回到家才發現當時618買了一堆書,這堆書還有沒撕包裝的呢....於是我翻出了最薄的一本《阿里巴巴 Java開發手冊》

手冊

這本書一共就90多頁,一天就可以通讀完了,看完之後我又來博文了。

注意:

  • 書上很多的規範是可以用IDE來避免的,也有很多之前已經知道的了。
  • 所以,這篇文章只記錄我認為比較重要,或者說是我之前開發時沒有注意到的一些規範(知識點)。
  • 該文章的內容肯定沒有書上寫得那麼全的,如果感興趣的同學可以去買一本來讀一下~

PDF官方地址:

一、Java相關

  1. POJO是DO/DTO/BO/VO的統稱,禁止命名為xxxPOJO
  2. 獲取多個物件的方法中list作為字首
  3. 獲取統計值的方法用count作為字首
  4. POJO類中的布林型別(Boolean)的變數都不要加is字首,否則部分框架解析會引起序列化錯誤
    • 如果你的變數名帶is的話,比如isActive,框架解析的時候可能就當成active了。
  5. 如果是形容能力的介面名稱,取對應的形容詞為介面名(通常是-able的形式)
  6. 不允許任何魔法值(未經預先定義的常量)直接出現在程式碼中
  7. Object的euqals方法容易丟擲空指標異常,應使用常量或者有值的物件來呼叫equals。推薦使用java.util.Object#equals工具類
  8. 所有POJO類的屬性全部使用包裝資料型別,RPC的返回值和引數必須使用包裝資料型別,所有的區域性變數都使用基本資料型別。定義VO/DTO/DO等POJO類時,不要設定任何屬性的預設值
    • 如果你的類屬性使用int這樣的基本資料型別,預設值是0。一般情況下該變數沒有賦值,一般想表達的是不存在(null),而不是0。
  9. 構造方法禁止加入任何的業務邏輯,如果初始化邏輯可以放在init方法中。set/get方法也不要增加業務邏輯。
    • 如果set/get方法放入業務邏輯,有時候排查問題就變得很麻煩了
  10. 工具類Arrays.asList()把陣列轉成List時,不能使用其修改集合的相關方法。比如說add、clear、remove
  11. 在JDK7以及以上版本中,Comparator要滿足三個條件,不然呼叫Arrays.sort()或者Collections.sort()會報異常。
    • x,y 的比較結果和 y,x 的比較結果相反
    • 傳遞性:x>y並且y>z,那麼x一定大於z
    • 對稱性:x=y,則 x,z 比較結果和y,z比較結果相同
  12. 使用entrySet遍歷Map類集合K/V,而不是用keySet方式遍歷
    • keySet遍歷了兩次,一次是轉成Iterator物件,一次是從hashMap中取出key所對應的value,如果JDK8可以使用Map.foreach方法
  13. 執行緒資源必須由執行緒池提供,不允許在應用中自行顯示建立執行緒。執行緒池不允許用Executors建立,通過ThreadPoolExecutor的方式建立,這樣的處理方式能夠讓編寫程式碼的工程師更加明確執行緒池的執行規則,規避資源耗盡的風險。
  14. SimpleDateFormat是執行緒不安全的類,一般不要定義為static變數,如果定義為static,必須加鎖,或者使用DateUtils工具類
    • 如果是JDK8應用,可以使用Instant(針對時間統計等場景)代替Date,LocalDateTime代替Calendar,DateTimeFormatter代替SimpleDateFormat
  15. 避免Random例項被多執行緒使用,雖然共享該例項是執行緒安全的,但會因競爭同一seed導致效能下降
    • 在JDK7之後,可以直接使用API ThreadLocalRandom,而在JDK7 之前,需要編碼保證每個執行緒持有一個例項。
  16. 類、類屬性、類方法的註釋必須使用 Javadoc 規範,使用 /**內容*/ 格式,不得使用 //xxx 方式
  17. 所有的抽象方法(包括介面中的方法)必須要用 Javadoc 註釋,除了返回值、引數、異常說明外,還必須指出該方法做什麼事情,實現什麼功能。所有的類都必須新增建立者和建立日期
  18. 對於暫時被註釋掉,後續可能恢復使用的程式碼片斷,在註釋程式碼的上方,使用三個斜槓///來說明註釋程式碼的理由
  19. 保證單元測試的獨立性。為了保證單元測試穩定可靠且便於維護,單元測試之間不能互相呼叫,也不能依賴執行的先後順序
  20. 高併發伺服器建議調小TCP協議的time_await超時時間,調大最大事件控制程式碼數(fd),

1.1值得說明的點

一、不允許任何魔法值(未經預先定義的常量)直接出現在程式碼中

例子:


    Negative example:
    //Magic values, except for predefined, are forbidden in coding.
    if (key.equals("關注公眾號:Java3y")) {
        //...
    }

    Positive example:
    String KEY_PRE = "關注公眾號:Java3y";
    if (KEY_PRE.equals(key)) {
        //...
    }

複製程式碼

ps:我猜是把先常量定義出來,後續引用/修改的時候就很方便了。


二、Object的euqals方法容易丟擲空指標異常,應使用常量或者有值的物件來呼叫equals。推薦使用java.util.Object#equals工具類

java.util.Object#equals的原始碼(已經判斷null的情況了)


	public static boolean equals(Object a, Object b) {
        return (a == b) || (a != null && a.equals(b));
    }
複製程式碼

三、工具類Arrays.asList()把陣列轉成List時,不能使用其修改集合的相關方法。

因為返回的ArrayList是一個內部類,並沒有實現集合的修改方法。後臺的資料仍是陣列,這裡體現的是介面卡模式。

ArrayList在這裡是內部類


四、在JDK7以及以上版本中,Comparator要滿足自反性,傳遞性,對稱性,不然呼叫Arrays.sort()或者Collections.sort()會報異常。

The implementor must ensure that sgn(compare(x, y)) == -sgn(compare(y, x)) for all x and y. (This implies that compare(x, y) must throw an exception if and only if compare(y, x) throws an exception.)

The implementor must also ensure that the relation is transitive: ((compare(x, y)>0) && (compare(y, z)>0)) implies compare(x, z)>0.

Finally, the implementor must ensure that compare(x, y)==0 implies that sgn(compare(x, z))==sgn(compare(y, z)) for all z.

  • 1) x,y 的比較結果和 y,x 的比較結果相反。
  • 2) 傳遞性:x>y,y>z,則 x>z。
  • 3) 對稱性:x=y,則 x,z 比較結果和 y,z 比較結果相同。

反例:下例中沒有處理相等的情況,實際使用中可能會出現異常:


new Comparator<Student>() {
    @Override
    public int compare(Student o1, Student o2) {
        return o1.getId() > o2.getId() ? 1 : -1;
    }
}
複製程式碼

使用entrySet遍歷Map類集合K/V,而不是用keySet方式遍歷

首先我們來看一下使用keySet是如何遍歷HashMap的:


    public static void main(String[] args) throws InterruptedException {

        HashMap<String, String> hashMap = new HashMap<>();
        hashMap.put("關注公眾號:", "Java3y");
        hashMap.put("堅持原創", "Java3y");
        hashMap.put("點贊", "關注,轉發,分享");


        // 得到keySet,遍歷keySet得到所有的key
        Set<String> strings = hashMap.keySet();
        Iterator<String> iterator = strings.iterator();
        while (iterator.hasNext()) {

            // HashMap的每個key
            String key = iterator.next();

			// 通過key可以獲得對應的value,如果有看過HashMap的同學知道get方法的時間複雜度是O(1)
            System.out.println("key = " + key + ", value = " + hashMap.get(key));
        }

    }



複製程式碼

再來看一下原始碼:


// 1. 得到keySet,如果不存在,則建立
public Set<K> keySet() {
    Set<K> ks = keySet;
    if (ks == null) {
        ks = new KeySet();
        keySet = ks;
    }
    return ks;
}

// 2.初始化ks (實際上就是Set集合[HashMap的內部類],在初始化時需要順便初始化iterator)
ks = new AbstractSet<K>() {
    public Iterator<K> iterator() {
        return new Iterator<K>() {
            private Iterator<Entry<K,V>> i = entrySet().iterator();

            public boolean hasNext() {
                return i.hasNext();
            }

            public K next() {
                return i.next().getKey();
            }

            public void remove() {
                i.remove();
            }
        };
    }

};




複製程式碼

再來看一下entrySet,可以直接拿到key和value,不用再使用get方法來得到value,所以比keySet更加推薦使用


    public static void main(String[] args) throws InterruptedException {

        HashMap<String, String> hashMap = new HashMap<>();
        hashMap.put("關注公眾號:", "Java3y");
        hashMap.put("堅持原創", "Java3y");
        hashMap.put("點贊", "關注,轉發,分享");


        // 得到entrySet,遍歷entrySet得到結果
        Set<Map.Entry<String, String>> entrySet = hashMap.entrySet();
        Iterator<Map.Entry<String, String>> iterator = entrySet.iterator();
        while (iterator.hasNext()) {
            Map.Entry<String, String> entry = iterator.next();
            System.out.println("key = " + entry.getKey() + ", value = " + entry.getValue());
        }
    }
複製程式碼

如果是JDK8的話,推薦直接使用Map.forEach()就好了,我們也來看看用法:


public static void main(String[] args) throws InterruptedException {

    HashMap<String, String> hashMap = new HashMap<>();
    hashMap.put("關注公眾號:", "Java3y");
    hashMap.put("堅持原創", "Java3y");
    hashMap.put("點贊", "關注,轉發,分享");

    
    // forEach用法
    hashMap.forEach((key, value) -> System.out.println("key = " + key + ", value = " + value));
}

複製程式碼

其實在原始碼裡邊我們可以發現,forEach實際上就是封裝了entrySet,提供forEach給我們可以更加方便地遍歷Map集合



	// forEach原始碼
    default void forEach(BiConsumer<? super K, ? super V> action) {
        Objects.requireNonNull(action);
        for (Map.Entry<K, V> entry : entrySet()) {
            K k;
            V v;
            try {
                k = entry.getKey();
                v = entry.getValue();
            } catch(IllegalStateException ise) {
                // this usually means the entry is no longer in the map.
                throw new ConcurrentModificationException(ise);
            }
            action.accept(k, v);
        }
    }
複製程式碼

五、SimpleDateFormat是執行緒不安全的類,一般不要定義為static變數,如果定義為static,必須加鎖,或者使用DateUtils工具類。

有以下的例子可以正確使用SimpleDateFormat:


// 1. 在方法內部使用,沒有執行緒安全問題
private static final String FORMAT = "yyyy-MM-dd HH:mm:ss";
public String getFormat(Date date){
    SimpleDateFormat dateFormat = new SimpleDateFormat(FORMAT);
    return dateFormat.format(date);
}


// 2. 每次使用的時候加鎖      
private static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public void getFormat(){
    synchronized (SIMPLE_DATE_FORMAT){
    SIMPLE_DATE_FORMAT.format(new Date());
    ….;
}
        
// 3. 使用ThreadLocal,每個執行緒都有自己的SimpleDateFormat物件,互不干擾
private static final ThreadLocal<DateFormat> DATE_FORMATTER = new ThreadLocal<DateFormat>() {
    @Override
    protected DateFormat initialValue() {
        return new SimpleDateFormat("yyyy-MM-dd");
    }
};

// 4. 使用DateTimeFormatter(This class is immutable and thread-safe.)

    DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    System.out.println(timeFormatter.format(LocalDateTime.now()));

複製程式碼

如果是JDK8應用,可以使用Instant代替Date,LocalDateTime代替Calendar,DateTimeFormatter代替SimpleDateFormat。


二、資料庫相關

  1. 表達是否概念的欄位,必須使用isxxx的方式命名,資料型別是unsigned tinyint(1表示是,0表示否)
  2. 小數型別用decimal,禁止使用float和double。
  3. varchar是可變字串,不預選分配儲存空間的話,長度不要超過5000個字元。如果超過則用text,獨立一張表,用主鍵對應,避免影響到其他欄位的索引效率
  4. 表必備的三個欄位:id(型別是unsigned bigint),gmt_create(建立時間),gme_modified(修改時間)
  5. 欄位允許適當冗餘,以提高查詢效能,但必須考慮資料一致性。冗餘的欄位必須不是頻繁修改的欄位,不是varhar超長欄位(更不能是text欄位)
  6. 單錶行數超過500萬行或者單表容量超過2GB才推薦進行分庫分表(如果預計三年都達不到這個資料量,不要在建立表的時候就分庫分表!)
  7. 超過三個表禁止使用join,需要join的欄位,資料型別必須保持一致,當多表關聯查詢時,保證被關聯的欄位需要有索引
  8. 在varchar欄位上建立索引時,必須指定索引長度,沒必要對全欄位建立索引,頁面搜尋嚴禁左模糊或者全模糊,如果需要則通過搜尋引擎來解決。
    • 充分利用好最左字首匹配特性!
  9. 利用延遲關聯或者子查詢優化超多也分場景。
  10. 如果有全球化需要,均以utf-8編碼。如果需要儲存表情,選擇utf8mb4進行儲存。

2.1值得說明的點

一、利用延遲關聯或者子查詢優化超多也分場景。

MySQL並不是跳過 offset行,而是取 offset+N行,然後返回放棄前offset行,返回N行,那當 offset特別大的時候,效率就非常的低下,要麼控制返回的總頁數,要麼對超過特定閾值的頁數進行SQL改寫。

例子:


// 優化前

SELECT id, cu_id, name, info, biz_type
	, gmt_create, gmt_modified, start_time, end_time, market_type
	, back_leaf_category, item_status, picuture_url
FROM relation
WHERE biz_type = '0'
	AND end_time >= '2014-05-29'
ORDER BY id ASC
LIMIT 149420, 20;


// 優化後

SELECT a.*
FROM relation a, (
		SELECT id
		FROM relation
		WHERE biz_type = '0'
			AND end_time >= '2014-05-29'
		ORDER BY id ASC
		LIMIT 149420, 20
	) b
WHERE a.id = b.id

複製程式碼

解釋:其實這裡就是通過使用覆蓋索引查詢返回需要的主鍵,再根據主鍵關聯原表獲得需要的資料。這樣就是充分利用了索引


三、未解決的問題

在看《手冊》的時候還有一些知識點沒看過、沒實踐過、涉及到的知識點比較多的,在這裡先mark一下,後續再遇到或者有空的時候再回來補坑~

  • 使用CountDownLatch進行非同步轉同步操作,每個執行緒退出前必須呼叫 countDown方法,執行緒執行程式碼注意 catch 異常,確保 countDown 方法被執行到,避免主執行緒無法執行至 await 方法,直到超時才返回結果。說明: 注意,子執行緒丟擲異常堆疊,不能在主執行緒 try-catch 到。
  • 對於一寫多讀,是可以解決變數同步問題, 但是如果多寫,同樣無法解決執行緒安全問題。如果是 count++操作,使用如下類實現: AtomicInteger count = new AtomicInteger(); count.addAndGet(1);如果是 JDK8,推薦使用 LongAdder 物件,比 AtomicLong 效能更好(減少樂觀鎖的重試次數)。
  • 使用JDK8的Optional類來防止NPE問題。

當然了,如果你有比較好的資料閱讀,也可以在評論區告訴我。我也會mark住好好看看。

比如說:“3y,我發現Optional類有篇文章寫得很不錯,url是xxxx(書籍的名稱是xxx)


由於現在沒有一定的經驗積累,所以以下的章節得回頭看:

  • 《手冊》中的“日誌規約”,“工程結構”、“設計規範”

最後

看我上面寫的內容就知道,除了一些規範外,還有很多實用的小技巧,這些對我們開發是有幫助的。我這個階段也有一些沒怎麼接觸過的("日誌","設計","二方庫"),這些都需要我在成長中不斷的回看才行

  • ps:我會回來補坑的。

引用書上的一句話:

很多程式設計方式客觀上沒有對錯之分,一致性很重要,可讀性很重要,團隊溝通效率很重要。程式設計師天生需要團隊協作,而協作的正能量要放在問題的有效溝通上。個性化應儘量表現在系統架構和演算法效率的提升上,而不是在合作規範上進行糾纏不休的討論、爭論,最後沒有結論。

作者(孤盡)在知乎回答的一句話:

翻完了不代表記住了,記住了不代表理解了,理解了不代表能夠應用上去,真正的知識是實踐,實踐,實踐

如果你覺得我寫得還不錯,瞭解一下:

相關文章