松花皮蛋的黑板報
  • 分享在京東工作的技術感悟,還有JAVA技術和業內最佳實踐,大部分都是務實的、能看懂的、可復現的

掃一掃
關注公眾號

碼出高效JAVA代碼

博客首頁文章列表 松花皮蛋me 2019-04-03 13:12


本文更多關注平時容易忽略的技巧或者細節,不是條條框框的JAVA入門教程。想到什么或者看到什么比較合適就寫下來了,不定期更新

一、序列化

隨著微服務的推行,越來越多的服務轉變成遠程服務調用,中間最重要的就是序列化和反序列化,那么數據傳輸的安全性就不可輕視,不少黑客就是通過序列化漏洞獲取到敏感數據,我們應該對重要字段進行過濾

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Person implements Serializable {
    private static final  long serialVersionUID = 1L;
    private String firstName;
    private String lastName;
    private static String job;                      // 靜態字段不參與序列化
    private transient String birthday;              // 敏感數據
    private transient String socialSecurityNumber;  // 敏感數據

 //    private static final ObjectStreamField[]
//            serialPersistentFields = {
//            new ObjectStreamField("firstName", Person.class),
//            new ObjectStreamField("lastName", Person.class)
//    };
}

1、POJO一般都實現Serializable并且設置serialVersionUID的值,以便對象傳遞和保證完整性,如果有父子類則父類必須實現
2、必須實現set\get\toString方法,但是不要在set\get方法上處理業務邏輯,因為如果直接訪問屬性,后續擴展權限非常難處理
3、int和Integer也就是基本類型和對象類型的使用,雖然有裝箱和拆箱機制,但是Integer是有緩存的,是沒有默認值的,原生的int運算時可能會出現溢出
4、private static final long serialVersionUID = 1L; 在可兼容的前提下,可以保留舊版本號,如果不兼容,或者想讓它不兼容,就手工遞增版本號
private static final long serialVersionUID = -2805284943658356093L;是根據類的結構產生的hash值,增減一個屬性、方法等,都可能導致這個值產生變化
5、在同一個流ObjectOutputStream中writeObject相同對象,序列化后的對象長度不會疊加,只是會多了引用長度
6、深克隆與淺克隆,默認實現Cloneable的POJO的clone()是淺克隆,可以通過序列化和反序列實現深克隆
7、設計實現了Serializable接口的單例,序列化會通過反射調用無參構造器返回新對象,我們可以添加readResolve()方法,自定義返回對象策略

二、Final不可繼承類

當我們為父類添加新方法、變更方法的規范、改變未繼承實現的方法都會對子類有影響。比如說我們有個子類繼承了HashTable類進行敏感數據的處理,在get\set時進行鑒權,突然某天HashTable添加了EntrySet方法,那么就有可能通過它繞過權限校驗造成問題。

三、Enum

對于數量有限的對象,應該優先考慮使用Enum,減少實例的數量,減少內存的使用。另外一種好處就是減少用數字表示,代碼可讀性更好

package com.front.ops.soa;

public enum HelloWords {
    ENGLISH ("ENGLISH","Hello"),
    SPANISH ("Spanish","hola");

    final String language;
    final  String greeting;
    HelloWords(String  language,String greeting) {
        this.language = language;
        this.greeting = greeting;
    }
}

四、雙重檢查單實例

public class Singleton {
    private volatile static Singleton instance = null;//volatile禁止指令重排序優化,因為初始化Singleton和將對象賦給instance字段的順序是不確定的,容易出現NPE異常
    private Singleton(){}
    public static Singleton getInstance() {
        if (instance == null) {   // 第一次校驗,保證只需要使用重量級鎖synchronized一次
            synchronized (Singleton.class) {
                if (instance == null) { // 第二次校驗,避免線程并發出現多次創建
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

五、InterruptedException異常處理

需要知道的一點是在觸發InterruptedException異常的同時,JVM會同時把線程的中斷標記位清除掉

Thread th = Thread.currentThread();
while(true) {
  if(th.isInterrupted()) {
    break;
  }
  // 省略業務代碼無數
  try {
    Thread.sleep(100);
  }catch (InterruptedException e){
    // 必須重新設置中斷標記位
    th.interrupt();
    e.printStackTrace();
  }
}

六、StringUtils\CollectionUtils\RestTemplate

多用工具庫,少造輪子,比如常見的List轉成有特殊分隔符的String、Curl請求,下面是RestTemplate比較常見的用法

示例一

 HttpEntity<String> entity = null;
        try {
             entity = new HttpEntity<>(objectMapper.writeValueAsString(data));
        } catch (IOException e) {
            e.printStackTrace();
        }
        ResponseEntity<String> response = restTemplate.postForEntity(getAllInterfaceAliasesByIpsUrl, entity, String.class);

示例二

     String entity = null;
    try {
        entity = objectMapper.writeValueAsString(data);
    } catch (IOException e) {
        e.printStackTrace();
    }
    ParameterizedTypeReference<String> responseType = new ParameterizedTypeReference<String>() {
    };
    RequestEntity<String> request = RequestEntity.post(URI.create(getServerStatusListByInterfaceUrl)).body(entity);
    ResponseEntity<String> response = restTemplate.exchange(request, responseType);

七、異常處理

  1. 1、盡量不要捕獲類似Exception這樣的通用異常,否則會被隱藏掉一些信息,而是應該捕獲特定異常,如InterruptedException
  2. 2、盡量不要捕獲Error或者Throwable這種異常,因為很難保證程序可以正確處理OOM問題
  3. 3、盡量不要生吐異常
  4. 4、NPE異常。推薦方法的返回值可以是null,但是必須充分注釋說明什么情況下會返回null值

八、Finally

需要關閉的連接資源等等,更加推薦使用try-with-resources語句,因為可以更好處理異常,另外需要了解的一點是,下面這種情況的finally是不會執行的。另外現在finally也不推薦使用了

try {
  // do something
  System.exit(1);
} finally {
  System.out.println("finally run");
}

final是在return表達式運行后執行的,此時要將return的結果暫存起來,待finally代碼塊執行完成后再返回緩存的結果

九、Maven管理多模塊

盡量一早就開始使用maven管理多模塊和依賴

    project
|-- pom.xml
|-- module-dao/
|   `-- pom.xml
|-- module-service/
|   `-- pom.xml
|-- module-scraper/
|   `-- pom.xml
`-- webapp/
    `-- pom.xml 

十、Assembly plugin自定義打包

<assembly xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/assembly-1.0.0.xsd">
    <id>package</id>
    <formats>
        <format>dir</format>
    </formats>
    <includeBaseDirectory>false</includeBaseDirectory>
    <fileSets>
        <fileSet>
            <directory>src/main/bin</directory>
            <outputDirectory>bin</outputDirectory>
        </fileSet>
        <fileSet>
            <directory>src/main/config</directory>
            <outputDirectory>resources</outputDirectory>
        </fileSet>
        <fileSet>
            <directory>src/main/resources</directory>
            <outputDirectory>resources</outputDirectory>
        </fileSet>
    </fileSets>
    <dependencySets>
        <dependencySet>
            <outputDirectory>lib</outputDirectory>
            <scope>runtime</scope>
        </dependencySet>
    </dependencySets>
</assembly>

十一、線程池

常見線程池:

  1. 1、new CachedThreadPool()-無線程可用時就創建新的
  2. 2、new FixedThreadPool()-任務數量超出后進入等待
  3. 3、new SingleThreadExecutor()-只有一個線程從而保證任務順序執行
  4. 4、new SingleThreadScheduledExecutor() 定時或周期性工作調度

線程池使用注意事項

  1. 1、避免任務堆積,newFixedThreadPool是創建指定數目線程的,但是其工作隊列是無限的,如果任務處理不及時,會長時間占用資源,非常容易出現OOM
  2. 2、避免過度擴展線程。如果任務邏輯有任務,拋出異常但是線程池類沒有捕獲到它,那么線程就會退出,但是由于線程的GCROOT是Thread,所以是無法被GC回收的,當這種情況足夠多時,線程池將沒有可用的線程來處理任/務
  3. 3、避免在線程池中使用ThreadLocal。ThreadLocal在整個線程生命周期都有效,用來保存線程私有的信息,常用來傳遞事務、上下文等等信息。但是在線程池中線程是復用的,那么在使用ThreadLocal后再次調用get()返回的變量根本不是當前線程的設定的變量,就會出現臟數據,除非在線程處理結束時及時進行remove清理操作

十二、讀寫鎖ReadWriteLock

1、只有寫鎖支持條件變量(比如隊列是否為空是否已滿),讀鎖調用newCondition()會拋出異常

2、先獲取讀鎖再獲取寫鎖是不允許的,但是獲取到寫鎖后可以降級為讀鎖

鎖升級是不允許的,如下代碼會阻塞

// 讀緩存
r.lock();         
try {
  v = m.get(key); 
  if (v == null) {
    w.lock();
    try {
      // 再次驗證并更新緩存
      // 省略詳細代碼
    } finally{
      w.unlock();
    }
  }
} finally{
  r.unlock();  
}

鎖的降級是可以的

class CachedData {
  Object data;
  volatile boolean cacheValid;
  final ReadWriteLock rwl =
    new ReentrantReadWriteLock();
  // 讀鎖  
  final Lock r = rwl.readLock();
  // 寫鎖
  final Lock w = rwl.writeLock();

  void processCachedData() {
    // 獲取讀鎖
    r.lock();
    if (!cacheValid) {
      // 釋放讀鎖,因為允許讀鎖的升級
      r.unlock();
      // 獲取寫鎖
      w.lock();
      try {
        // 再次檢查狀態  
        if (!cacheValid) {
          data = ...
          cacheValid = true;
        }
        // 釋放寫鎖前,降級為讀鎖
        // 降級是可以的
        r.lock();
      } finally {
        // 釋放寫鎖
        w.unlock(); 
      }
    }
    // 此處仍然持有讀鎖
    try {use(data);} 
    finally {r.unlock();}
  }
}

十三、覆寫、重載、泛型

 Father father = new Son();
 //Son覆寫了此方法
 father.doSomething();

覆寫要注意兩大一小原則,子類的訪問權限只能相同或者變大,拋出異常和返回類型只能變小,方法名和參數必須完全相同,一般情況下我們都會使用override注解,以便編譯器檢測是否符合覆寫條件

JVM在重載方法中,選擇合適的目標方法的順序如下:

  1. 1、精確匹配
  2. 2、如果是基本類型,自動轉換為更大表示范圍的基本類型
  3. 3、通過自動折箱和裝箱
  4. 4、通過子類向上轉型繼承路線依次匹配(比如參數null)
  5. 5、通過可變參數匹配

泛型可以避免重復定義方法,也可以避免使用Object作為輸入輸出,帶來強制轉換的風險(ClassCastException)

十四、并發容器

List list = Collections.
  synchronizedList(new ArrayList());
synchronized (list) {  //這行必須添加
  Iterator i = list.iterator(); 
  while (i.hasNext())
    foo(i.next());
}    

實際上Collections.synchronizedList類似封裝如下

SafeArrayList<T>{

  List<T> c = new ArrayList<>();

  synchronized
  T get(int idx){
    return c.get(idx);
  }

  synchronized
  void add(int idx, T t) {
    c.add(idx, t);
  }

  synchronized
  boolean addIfNotExist(T t){
    if(!c.contains(t)) {
      c.add(t);
      return true;
    }
    return false;
  }
}

上面的addIfNotExist其實包含了組合操作,每個操作是原子性的,但是組合操作往往是非原子性的。

CopyOnWriteArrayList就是在寫的時候不對原集合進行修改,而是重新復制一份修改完后,再修改指針,所以會導致有短暫的數據不一致。另外它的迭代器是只讀的,不支持增刪改,因為遍歷的只是快照。

十五、lost wake up問題

假如有兩個線程,一個消費者線程,一個生產者線程。生產者線程的任務可以簡化成將count加一,而后喚醒消費者;消費者則是將count減一,而后在減到0的時候陷入睡眠:

生產者偽代碼:

count+1
notify()

消費者偽代碼:

while(count<=0){
   wait()
}
count--

但是實際上可能消費者在檢查count到調用wait()之間,count就可能被改掉了,導致消費者無法執行業務操作,這是很常見的一種競態條件,所以需要把wait和notify的調用放在同步代碼塊中,否則會出現IllegalMonitorStateException問題

十六、Thread.sleep(0)的妙用

thread_fun()
{
    prepare_word.....


    while (1)//沒有sleep(0)的話,這里會一直浪費CPU時間做死循環的輪詢,無用功
    {
        if (A is finish)
            break;
        else
            sleep(0); //這里會交出B的時間片,下一次調度B的時候,接著執行這個循環
    }


    process A's data
}

Thread.Sleep(0)的作用,就是“觸發操作系統立刻重新進行一次CPU競爭”,競爭的結果也許是當前線程仍然獲得CPU控制權。因為0的原因,線程直接回到就緒隊列,而非進入等待隊列,只要進入就緒隊列,那么它就參與cpu競爭

十七、高效合理設計RPC接口

1、RPC接口異常顯式返回

Exception應該也是返回值的一部分,應該設計成Checked Exception,盡量讓調用方能夠顯式的處理

2、使用Specification規格模式解決查詢接口過多的問題

設計者應該避免太多findBy方法和各自的重載,正確的打開方式應該類似組合模式

public interface StudentApi{
    Student findBySpec(StudentSpec spec);
    List<Student> findListBySpec(StudentListSpec spec);
    Page<Student> findPageBySpec(StudentPageSpec spec);
}

十八、String對象

  1. 1、直接使用雙引號聲明出來的String對象會直接存儲在常量池中。
  2. 2、如果不是用雙引號聲明的String對象,可以使用String提供的intern方法。intern 方法會從字符串常量池中查詢當前字符串是否存在,若不存在就會將當前字符串放入常量池中
  3. 3、String s = new String(“abc”),創建了2個對象,第一個對象是”abc”字符串存儲在常量池中,第二個對象在JAVA Heap中的 String 對象,然后指向運行時常量池中的”abc”
  4. 4、jdk6常量池在Perm區,jdk7以后在Java Heap區
  5. 5、Intern對”java”、”int…”基本類型、”void”關鍵字字符串都不適用
public class TestDemo {
    public static void main(String[] args )
    {
        String str = new StringBuilder("計算機軟件").append("軟件").toString();
        String str1 = new StringBuilder("ja").append("va").toString();
        String str2 = new StringBuilder("jaa").append("va").toString();
        String str3 = new StringBuilder("cl").append("ass").toString();
        String str4 = new StringBuilder("Inte").append("ger").toString();
        String st5 = new StringBuilder("in").append("t").toString();
        System.out.println(str3=="jaava");//false
        System.out.println(str.intern()==str);//true
        System.out.println(str1.intern().equals(str1));//true
        System.out.println(str1.intern()==str1);//false
        System.out.println(str2.intern()==str2);//true
        System.out.println(str3.intern()==str3);//true
        System.out.println(str4.intern()==str4);//true
        System.out.println(st5.intern()==st5);//false
	
//        String str = "計算機"+"軟件";
//        String str1 = "ja"+"va";
//        String str2 = "jaa"+"va";
//        String str3 = "cl"+"ass";
//        String str4 = "en"+"um";
//        String st5 = "in"+"t";
//        System.out.println(str.intern()==str);//true
//        System.out.println(str1.equals(str1.intern()));//true
//        System.out.println(str1.intern()==str1);//true
//        System.out.println(str2.intern()==str2);//true
//        System.out.println(str3.intern()==str3);//true
//        System.out.println(str4.intern()==str4);//true
//        System.out.println(st5.intern()==st5);//true
    }

十九、單例模式與垃圾回收

class Singleton {
    private byte[] a = new byte[6*1024*1024];
    private static Singleton singleton = new Singleton();
    private Singleton(){}

    public static Singleton getInstance(){
        return singleton;
    }
}

class Obj {
    private byte[] a = new byte[3*1024*1024];
}

public class Client{
    public static void main(String[] args) throws Exception{
        Singleton.getInstance();
        while(true){
            new Obj();
        }
    }

運行結果:

……

[Full GC 18566K->6278K(20352K), 0.0101066 secs]

[GC 18567K->18566K(20352K), 0.0001978 secs]

[Full GC 18566K->6278K(20352K), 0.0088229 secs]

hotspot虛擬機的垃圾收集算法使用根搜索算法,可以作為根的對象有:

  1. 虛擬機棧(棧楨中的本地變量表)中的引用的對象。
  2. 方法區中的類靜態屬性引用的對象。
  3. 方法區中的常量引用的對象。
  4. 本地方法棧中JNI的引用的對象。

方法區是jvm的一塊內存區域,用來存放類相關的信息。很明顯java中單例模式創建的對象被自己類中的靜態屬性所引用,符合第二條,因此單例對象不會被jvm垃圾收集

二十、Arrays#asList容易踩的坑

我們習慣使用Arrays#asList將Array轉成ArrayList,但是其實Arrays#asList返回的其實是java.util.ArrayList,它是Arrays的定長集合類,它實現了set\get\contains方法,但是沒有實現add\remove方法,調用它的add\remove方法實際上會調用父類AbstractList的方法,但是沒有具體實現,僅僅拋出UnsupportedOperationException異常。同時java.util.ArrayList參數為可變長泛型,當調用其size方法時得到的將會是泛型對象個數,也就是一。另外asList得到的ArrayList其實是引用賦值,也就是說當外部數組或集合改變時,數組和集合會同步變化

黑龙江6+1开奖结果查询