關於面試被面試官暴懟:“幾年研究生白讀” 的前因後果

JavaBuild發表於2024-06-20

    中午一個網友來信說自己和麵試官幹起來了,看完他的描述真是苦笑不得,這年頭是怎麼了,最近網際網路CS訊息滿天飛,怎麼連面試官都SB起來了呢?

    大概是這樣的:這位網友面試時被問及了Serializable介面的底層實現原理,因為這是一個標識性的空介面,大部分同學在學習時都秉持著會用就行(說實話,Build哥在這之前也沒怎麼細研究過,都是拿來就用),幾乎不太去關注底層的東西,這位網友亦是如此,在這種情況下,自然回答的心虛,這下可被面試官抓住了把柄,一頓帶有人身攻擊的狂輸出,讓面試現場變成了撕B現場,具體可看聊天截圖😂😂😂

image

    基於這位網友的面試經歷,Build哥又趕緊去重新學了一下Serializable關鍵字,以及它背後的實現,別到時候咱也被暴懟,下面咱們一起來重溫一下。

一、序列化與反序列化

首先,我們先來了解一下兩個概念 序列化反序列化

  • 序列化: 將Java物件轉換為一個位元組序列(包含物件的資料、物件的型別和物件中儲存的屬性等資訊)的過程,以便於在網路上傳輸或者儲存在檔案中。
  • 反序列化: 是序列化的逆過程,將位元組序列轉為Java物件的過程。

1.1 序列化與反序列化的應用場景

  • 物件在進行網路傳輸(比如遠端方法呼叫 RPC 的時候)之前需要先被序列化,接收到序列化的物件之後需要再進行反序列化;
  • 將物件儲存到檔案(如系統中excle的上傳與下載)之前需要進行序列化,將物件從檔案中讀取出來需要進行反序列化;
  • 將物件儲存到資料庫(如 Redis)之前需要用到序列化,將物件從快取資料庫中讀取出來需要反序列化;
  • 將物件儲存到記憶體之前需要進行序列化,從記憶體中讀取出來之後需要進行反序列化。

序列化與發序列化的流轉過程可參考下圖:
image

有個問題,如果在我的物件中,有些變數並不想被序列化應該怎麼辦呢?

答:不想被序列化的變數我們可以使用transientstatic關鍵字修飾;transient 關鍵字的作用是阻止例項中那些用此關鍵字修飾的的變數序列化;當物件被反序列化時,被 transient 修飾的變數值不會被持久化和恢復;而static關鍵字修飾的變數並不屬於物件本身,所以也同樣不會被序列化!具體原因,我們在後面會解釋,繼續往下看。

二、Java中的序列流

    為了探討Java物件序列化與反序列化的過程,以及Serializable關鍵字在整個過程中的作用,我們先來提一個 序列流 的概念,剛好我們最近也在寫關於Java IO的相關部落格。

    Java 的序列流(ObjectInputStream 和 ObjectOutputStream)是一種可以將 Java 物件序列化和反序列化的流。這個屬於基本的位元組輸入流與輸出流的演變,之前的博文中已經介紹了它們的用法,在這裡就不再展開了。

  • ObjectOutputStream:將序列化後的位元組序列寫入到檔案、網路等輸出流中。
  • ObjectInputStream:可以讀取 ObjectOutputStream 寫入的位元組流,並將其反序列化為相應的物件(包含物件的資料、物件的型別和物件中儲存的屬性等資訊)。

三、序列化實戰

    OK,有了上面兩個理論知識作為鋪墊,我們接下來就可以進行序列化的實戰了,首先,我們要先建立一個包含簡單屬性的類,這裡我們建立了一個Person類,裡面有name和age兩個屬性欄位。然後,我們透過ObjectOutputStream流將物件寫出到檔案(序列化),然後再透過ObjectInputStream讀取檔案中的資料,輸出為一個person物件(反序列化)。

話不多說,直接上程式碼:

public class Test {
    public static void main(String[] args) throws IOException {
        //初始化物件資訊
        Person person = new Person();
        person.setName("JavaBuild");
        person.setAge(30);
        System.out.println(person.getName()+" "+person.getAge());

        //序列化過程
        try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("E:\\person.txt"));) {
          objectOutputStream.writeObject(person);
        } catch (IOException e) {
            e.printStackTrace();
        }

        //反序列化過程
        try (ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("E:\\person.txt"));) {
            Person p = (Person) objectInputStream.readObject();
            System.out.println(p.getName() + " " + p.getAge());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }

    }
}
class Person {

    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

然後我們執行一下,結果,哦吼!報錯了,提示了NotSerializableException,原因是我們在建立Person類時,並沒有實現Serializable介面。

image

很多初學的同學會很奇怪,跟進這個Serializable介面中發現裡面空空如也,為啥我們不實現它就無法進行序列化呢?

image

跟著上面報錯中的堆疊資訊,我們進入ObjectOutputStream的writeObject0方法中一探究竟!其中有部分原始碼如下:

// 判斷物件是否為字串型別,如果是,則呼叫 writeString 方法進行序列化
if (obj instanceof String) {
    writeString((String) obj, unshared);
}
// 判斷物件是否為陣列型別,如果是,則呼叫 writeArray 方法進行序列化
else if (cl.isArray()) {
    writeArray(obj, desc, unshared);
}
// 判斷物件是否為列舉型別,如果是,則呼叫 writeEnum 方法進行序列化
else if (obj instanceof Enum) {
    writeEnum((Enum<?>) obj, desc, unshared);
}
// 判斷物件是否為可序列化型別,如果是,則呼叫 writeOrdinaryObject 方法進行序列化
else if (obj instanceof Serializable) {
    writeOrdinaryObject(obj, desc, unshared);
}
// 如果物件不能被序列化,則丟擲 NotSerializableException 異常
else {
if (extendedDebugInfo) {
    throw new NotSerializableException(
        cl.getName() + "\n" + debugInfoStack.toString());
} else {
    throw new NotSerializableException(cl.getName());
}
}

從這段原始碼中我們可以發現,在序列化的時候,writeObject0方法內部會對物件進行型別判斷,包括字串、陣列、列舉或Serializable,這些條件都不滿足的話,就會丟擲NotSerializableException異常,因此,即便Serializable介面什麼都沒有,但需要是初始化的類實現了它的話,就滿足了obj instanceof Serializable,可以進行序列話操作!

我們將上面的測試程式碼中Person類實現Serializable介面後,再看結果:
image

序列化與反序列化都成功了,並獲得了預期的列印結果。

那麼它們的具體實現流程是怎麼樣的呢?

  • 序列化: 以 ObjectOutputStream 為例吧,跟如它的原始碼時發現,它在序列化的時候會依次呼叫 writeObject()→writeObject0()→writeOrdinaryObject()→writeSerialData()→invokeWriteObject()→defaultWriteFields()。
  • 反序列化: 以 ObjectInputStream 為例,它在反序列化的時候會依次呼叫 readObject()→readObject0()→readOrdinaryObject()→readSerialData()→defaultReadFields()。

四、總結

由此可見,Serializable 介面之所以定義為空,是因為它只起到了一個標識的作用,告訴程式實現了它的物件是可以被序列化的,但真正序列化和反序列化的操作並不需要它來完成,就像這裡的序列流才是主要實現序列化的驅動器!

相關文章