StackOverflow 周報 – 與高關注的問題過過招(Java)
- 2019 年 10 月 3 日
- 筆記
本篇文章是 Stack Overflow 周報的第二周,共收集了 4 道高關注的問題和對應的高贊回答。公眾號「渡碼」為日更,歡迎關注。
DAY1. serialVersionUID 的重要性
關注: 2820,最高贊: 2152
這篇文章介紹一下 Java 中 serialVersionUID 屬性的含義以及重要性。從屬性可以看出它與序列化有關係,所以在 java.io.Serializable 接口的注釋中對它有詳細的介紹,下面我們對照文檔注釋來學習一下。Java 中每個可序列化的類都有一個版本號與之關聯,這個版本號就是 serialVersionUID。它在對象反序列化時使用, 用於判斷該類的發送方和接收方的 serialVersionUID 是否一致,如果接收方裝載的類的 serialVersionUID 與發送方不一致,則拋出 InvalidClassException 異常。一個可序列化的類可以顯示地聲明 serialVersionUID 屬性,但必須是 static,final,long 修飾的,如:
ANY-ACCESS-MODIFIER static final long serialVersionUID = 42L;
下面結合實際的例子來看看 serialVersionUID 的用法以及作用,上面說了 serialVersionUID 屬性是定義在可序列化的類中,所以我們的類需要實現 java.io.Serializable 接口。因此,我們定義的 Person 類如下:
package com.cnblogs.duma.week2; import java.io.Serializable; public class Person implements Serializable { private static final long serialVersionUID = 42L; public int age; public String name; public Person(int age, String name){ this.age = age; this.name = name; } @Override public String toString() { return age + "," + name; } }
接下來,我們在定義兩個方法分別用來序列化和反序列化,序列化方法如下:
// 將 Person 對象序列化後存到文件 public static void ser() { Person p = new Person(28, "duma"); System.out.println("person Seria:" + p); try { FileOutputStream fos = new FileOutputStream("person.txt"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(p); oos.flush(); oos.close(); } catch (IOException e) { e.printStackTrace(); } }
序列化方法如下:
// 從文件中反序列化出 Person 對象 public static void deser() { Person p; try { FileInputStream fis = new FileInputStream("person.txt"); ObjectInputStream ois = new ObjectInputStream(fis); p = (Person) ois.readObject(); ois.close(); System.out.println(p.toString()); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } }
先調用 ser 方法,成功後可以看到項目根目錄下生成了 person.txt 文件,然後在調用 deser 方法可以成功反序列化 Person 對象並輸出結果。這波操作就是正常的序列化和反序列化操作。回到今天的主題,為了驗證 serialVersionUID 屬性的作用,我們可以在調用完 ser 方法後,先修改 serialVersionUID 值,然後再調用 deser 方法,這時就會拋出 java.io.InvalidClassException 異常。
明白了 serialVersionUID 屬性的含義和作用,接下來我們再來看看它的重要性。在我們的例子中我們顯式地定義了 serialVersionUID 屬性,如果沒有顯式地指定 serialVersionUID,序列化運行時會根據類的信息計算一個默認值,《Effective Java》一書中提到這些信息包括類名、實現的接口、public和protected的成員。雖然有默認值,但 Java 官方文檔強烈建議我們顯式地定義 serialVersionUID 屬性,因為默認的 serialVersionUID 依賴類的信息,而類的信息可能在不同編譯器下會不同。因此,如果發送方和接收方使用的編譯器不同,有可能導致默認的 serialVersionUID 不一致從而導致接收方無法正常反序列化,同時 Java 官方也建議使用 private 修飾 serialVersionUID,這樣可以防止子類繼承這個屬性。
對於上面提到的《Effective Java》一書中的內容,我們可以做個簡單的驗證。因為生成默認的 serialVersionUID 會用到 public 成員信息,那我們改變成員變量就會導致 serialVersionUID 值改變。首先我們將 Person 類中的 serialVersionUID 屬性刪掉,調用 ser 方法序列化。然後在 Person 中加一個成員,比如:public String nickname = “zhangsan”; ,然後調用 deser 方法,可以看到程序拋出 java.io.InvalidClassException 異常。這同時也警示我們盡量顯示地定義 serialVersionUID 屬性。
DAY2. 創建線程到底用哪種方式
關注: 1972,最高贊: 1583
我們知道 Java 實現線程的方式有兩種, 一種是繼承 Thread 類,另一種是實現 Runnable 接口。那麼問題來了,這兩種方式有什麼區別呢?我們應該用哪種方式更好呢?下面先簡單看下這兩種方式的代碼。
1. 繼承 Thread 類
static class MyThread extends Thread { @Override public void run() { super.run(); } }
2. 實現 Runnable 接口
static class Workder implements Runnable { @Override public void run() { } }
對於這兩種方式的選擇,Stack Overflow 的回答者普遍認為優先選擇實現 Runnable 接口的方式,理由如下:
- 在面向對象中,繼承意味着添加新功能、修改或者改進父類的行為,如果我們沒有這方面的改動,那就盡量避免使用繼承,因為我們的代碼只是單純需要執行一些任務,而不需要改造 Thread 的行為,所以繼承 Runnable 接口更合理,所以上面代碼中繼承的方式類名是 Thread1 是一種(is a)Thread,而實現 Runnable 接口的類名是 Worker,一個 Worker 對象(工人)的“工作”邏輯可以放在 run 方法中,然而這並不意味這這個工人 7*24 小時一直工作。
- 由於 Java 中不支持多繼承,因此繼承 Thread 類意味着無法在繼承其他的類,影響代碼的擴展性。
- 繼承 Thread 意味着每個線程都有一個唯一的 Thread 的對象與之對應,而實現 Runnable 接口可以讓多個線程共享同一個對象。
總之,如果我們的類定位在單純地執行任務,並不需要改造 Thread 類,那我們就應該實現 Runnable 接口。反之,如果我們需要改造 Thread 類,或者它是一種(is a)線程,那我們就繼承 Thread 類。我目前正在寫的一本關於 RPC 的書中,創建線程就是以繼承 Thread 為主。
DAY3. 反射非用不可嗎
關注: 1960,最高贊: 1611
相信有 Java 基礎的朋友都知道反射的概念。然而如果你僅僅了解概念,而在工程實踐中沒有應用的話,那可能總是感覺有層窗戶紙模模糊糊的。我之前學習反射就有這種感覺。那麼今天這篇文章我就完整地梳理一下反射的概念和作用,結合 Hadoop RPC 框架聊聊反射為什麼非用不可或者說用了反射是不是給程序帶來了非常大的便利性。
首先,我們看定義:反射是語言提供了一種在運行時檢查和動態調用類、方法和屬性的能力。基於這個能力,反射一般大量應用在框架中,如:Spring,Hadoop。從反射的定義我們可能會問一個問題,為什麼要在運行時動態地調用?既然 Java 是靜態語言,任何需要調用的東西為什麼不在編譯時就確定好呢?這個問題也就是在問反射的作用是什麼以及是不是非用不可。
我們可以簡單猜想一下,以類的反射為例,當我們使用第三方框架時,框架並不知道用戶定義了什麼類,因此框架想要使用用戶的類,只能在運行時動態地檢查類是否存在,再進行調用。下面以 Hadoop 的 MapReduce 框架為例,看一下它使用反射的一個例子。
用戶自定義的類如下:
// 需要繼承 Mapper 基類,Hadoop 框架才能正常使用它 public class WordCountMapper extends Mapper<Object, Text, Text, IntWritable> { // Hadoop 框架會在創建 WordCountMapper 對象後調用 map 方法 @Override protected void map(Object key, Text value, Context context) throws IOException, InterruptedException { // 數據處理邏輯 } }
Hadoop 框架通過反射調用 WordCountMapper 的代碼如下:
// taskContext.getMapperClass() 為運行時用戶傳入的類,WordCountMapper org.apache.hadoop.mapreduce.Mapper<INKEY,INVALUE,OUTKEY,OUTVALUE> mapper = (org.apache.hadoop.mapreduce.Mapper<INKEY,INVALUE,OUTKEY,OUTVALUE>) ReflectionUtils.newInstance(taskContext.getMapperClass(), job);
// run 方法中循環調用 map 方法處理數據 mapper.run(mapperContext);
上面是 Hadoop 代碼的一部分,它將類的反射代碼封裝在 ReflectionUtils.newInstance 方法中,該方法用類的默認構造方法動態地創建一個對象。上述代碼中該方法創建的對象被強轉為 Mapper 類,這就是為什麼因為我們的 WordCounterMapper 類要繼承 Mapper。
因此,可以看到 Hadoop 框架在編譯的時候並不知道用戶定義了WordCountMapper 類,只能在運行時根據配置動態地檢查、調用。當然為了框架能夠正常使用我們定義的類,就需要定義類時符合框架定義的規範,在我們的例子中需要遵循的規範是實現一個 Mapper 基類,並且需要有默認構造函數。如果我們在代碼中修改 WordCountMapper 的構造函數,那就不符合框架的規範,反射就會報錯,如下:
public class WordCountMapper extends Mapper<Object, Text, Text, IntWritable> { // 有參構造函數覆蓋默認構造函數 public WordCountMapper(int a) { a = 100; } }
再次運行,當 Hadoop 調用 ReflectionUtils.newInstance 時找不默認構造函數便會以下報錯:
Error: java.lang.RuntimeException: java.lang.NoSuchMethodException: com.cnblogs.duma.mapreduce.WordCountMapper.<init>() at org.apache.hadoop.util.ReflectionUtils.newInstance(ReflectionUtils.java:135) at org.apache.hadoop.mapred.MapTask.runNewMapper(MapTask.java:751)
接下來,再舉一個動態調用方法的例子,假設我們要在運行時才能檢查並調用某方法,寫法如下:
Method method = foo.getClass().getMethod("doSomething", null); method.invoke(foo, null);
這種場景也是框架比較喜歡用的,比如:Java 的單元測試框架 Junit4,通過反射檢查類中帶有 @Test 註解的方法,然後調用他們運行單元測試。
從上面兩個例子可以看到為什麼框架對反射如此鍾情。框架不需要關心用戶定義了什麼類,只要用戶的代碼符合框架定義的規範,框架就會在運行時進行檢查,並按照自己定義的規範調用代碼即可。因此反射可以讓框架和用戶的應用解耦,使得開發更方便。
DAY4. 一行代碼搞定數組的初始化、搜索、打印
我們平時遇到的好多問題可能一行代碼就搞定了。平時遇到問題可以多想想是不是已經有工具已經實現了, 如果有的話可以直接拿來用,避免重複造輪子。這篇文章今天發在公眾號上,算是關注公眾號讀者的一個福利吧。後續再發博客。
以上便是 Stack Overflow 的第二周周報,希望對你有用,後續會繼續更新,如果想看日更內容歡迎關注公眾號。
公眾號「渡碼」,分享更多高質量內容