本文已經收錄到Github倉庫,該倉庫包含電腦基礎、Java基礎、多線程、JVM、資料庫、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分散式、微服務、設計模式、架構、校招社招分享等核心知識點,歡迎star~ Github地址:https://github.c ...
本文已經收錄到Github倉庫,該倉庫包含電腦基礎、Java基礎、多線程、JVM、資料庫、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分散式、微服務、設計模式、架構、校招社招分享等核心知識點,歡迎star~
Github地址:https://github.com/Tyson0314/Java-learning
Java的特點
Java是一門面向對象的編程語言。面向對象和麵向過程的區別參考下一個問題。
Java具有平臺獨立性和移植性。
- Java有一句口號:
Write once, run anywhere
,一次編寫、到處運行。這也是Java的魅力所在。而實現這種特性的正是Java虛擬機JVM。已編譯的Java程式可以在任何帶有JVM的平臺上運行。你可以在windows平臺編寫代碼,然後拿到linux上運行。只要你在編寫完代碼後,將代碼編譯成.class文件,再把class文件打成Java包,這個jar包就可以在不同的平臺上運行了。
Java具有穩健性。
- Java是一個強類型語言,它允許擴展編譯時檢查潛在類型不匹配問題的功能。Java要求顯式的方法聲明,它不支持C風格的隱式聲明。這些嚴格的要求保證編譯程式能捕捉調用錯誤,這就導致更可靠的程式。
- 異常處理是Java中使得程式更穩健的另一個特征。異常是某種類似於錯誤的異常條件出現的信號。使用
try/catch/finally
語句,程式員可以找到出錯的處理代碼,這就簡化了出錯處理和恢復的任務。
Java是如何實現跨平臺的?
Java是通過JVM(Java虛擬機)實現跨平臺的。
JVM可以理解成一個軟體,不同的平臺有不同的版本。我們編寫的Java代碼,編譯後會生成.class 文件(位元組碼文件)。Java虛擬機就是負責將位元組碼文件翻譯成特定平臺下的機器碼,通過JVM翻譯成機器碼之後才能運行。不同平臺下編譯生成的位元組碼是一樣的,但是由JVM翻譯成的機器碼卻不一樣。
只要在不同平臺上安裝對應的JVM,就可以運行位元組碼文件,運行我們編寫的Java程式。
因此,運行Java程式必須有JVM的支持,因為編譯的結果不是機器碼,必須要經過JVM的翻譯才能執行。
Java 與 C++ 的區別
- Java 是純粹的面向對象語言,所有的對象都繼承自 java.lang.Object,C++ 相容 C ,不但支持面向對象也支持面向過程。
- Java 通過虛擬機從而實現跨平臺特性, C++ 依賴於特定的平臺。
- Java 沒有指針,它的引用可以理解為安全指針,而 C++ 具有和 C 一樣的指針。
- Java 支持自動垃圾回收,而 C++ 需要手動回收。
- Java 不支持多重繼承,只能通過實現多個介面來達到相同目的,而 C++ 支持多重繼承。
JDK/JRE/JVM三者的關係
JVM
英文名稱(Java Virtual Machine),就是我們耳熟能詳的 Java 虛擬機。Java 能夠跨平臺運行的核心在於 JVM 。
所有的java程式會首先被編譯為.class的類文件,這種類文件可以在虛擬機上執行。也就是說class文件並不直接與機器的操作系統交互,而是經過虛擬機間接與操作系統交互,由虛擬機將程式解釋給本地系統執行。
針對不同的系統有不同的 jvm 實現,有 Linux 版本的 jvm 實現,也有Windows 版本的 jvm 實現,但是同一段代碼在編譯後的位元組碼是一樣的。這就是Java能夠跨平臺,實現一次編寫,多處運行的原因所在。
JRE
英文名稱(Java Runtime Environment),就是Java 運行時環境。我們編寫的Java程式必須要在JRE才能運行。它主要包含兩個部分,JVM 和 Java 核心類庫。
JRE是Java的運行環境,並不是一個開發環境,所以沒有包含任何開發工具,如編譯器和調試器等。
如果你只是想運行Java程式,而不是開發Java程式的話,那麼你只需要安裝JRE即可。
JDK
英文名稱(Java Development Kit),就是 Java 開發工具包
學過Java的同學,都應該安裝過JDK。當我們安裝完JDK之後,目錄結構是這樣的
可以看到,JDK目錄下有個JRE,也就是JDK中已經集成了 JRE,不用單獨安裝JRE。
另外,JDK中還有一些好用的工具,如jinfo,jps,jstack等。
最後,總結一下JDK/JRE/JVM,他們三者的關係
JRE = JVM + Java 核心類庫
JDK = JRE + Java工具 + 編譯器 + 調試器
Java程式是編譯執行還是解釋執行?
先看看什麼是編譯型語言和解釋型語言。
編譯型語言
在程式運行之前,通過編譯器將源程式編譯成機器碼可運行的二進位,以後執行這個程式時,就不用再進行編譯了。
優點:編譯器一般會有預編譯的過程對代碼進行優化。因為編譯只做一次,運行時不需要編譯,所以編譯型語言的程式執行效率高,可以脫離語言環境獨立運行。
缺點:編譯之後如果需要修改就需要整個模塊重新編譯。編譯的時候根據對應的運行環境生成機器碼,不同的操作系統之間移植就會有問題,需要根據運行的操作系統環境編譯不同的可執行文件。
總結:執行速度快、效率高;依靠編譯器、跨平臺性差些。
代表語言:C、C++、Pascal、Object-C以及Swift。
解釋型語言
定義:解釋型語言的源代碼不是直接翻譯成機器碼,而是先翻譯成中間代碼,再由解釋器對中間代碼進行解釋運行。在運行的時候才將源程式翻譯成機器碼,翻譯一句,然後執行一句,直至結束。
優點:
- 有良好的平臺相容性,在任何環境中都可以運行,前提是安裝瞭解釋器(如虛擬機)。
- 靈活,修改代碼的時候直接修改就可以,可以快速部署,不用停機維護。
缺點:每次運行的時候都要解釋一遍,性能上不如編譯型語言。
總結:解釋型語言執行速度慢、效率低;依靠解釋器、跨平臺性好。
代表語言:JavaScript、Python、Erlang、PHP、Perl、Ruby。
對於Java這種語言,它的源代碼會先通過javac編譯成位元組碼,再通過jvm將位元組碼轉換成機器碼執行,即解釋運行 和編譯運行配合使用,所以可以稱為混合型或者半編譯型。
面向對象和麵向過程的區別?
面向對象和麵向過程是一種軟體開發思想。
-
面向過程就是分析出解決問題所需要的步驟,然後用函數按這些步驟實現,使用的時候依次調用就可以了。
-
面向對象是把構成問題事務分解成各個對象,分別設計這些對象,然後將他們組裝成有完整功能的系統。面向過程只用函數實現,面向對象是用類實現各個功能模塊。
以五子棋為例,面向過程的設計思路就是首先分析問題的步驟:
1、開始游戲,2、黑子先走,3、繪製畫面,4、判斷輸贏,5、輪到白子,6、繪製畫面,7、判斷輸贏,8、返回步驟2,9、輸出最後結果。
把上面每個步驟用分別的函數來實現,問題就解決了。
而面向對象的設計則是從另外的思路來解決問題。整個五子棋可以分為:
- 黑白雙方
- 棋盤系統,負責繪製畫面
- 規則系統,負責判定諸如犯規、輸贏等。
黑白雙方負責接受用戶的輸入,並告知棋盤系統棋子佈局發生變化,棋盤系統接收到了棋子的變化的信息就負責在屏幕上面顯示出這種變化,同時利用規則系統來對棋局進行判定。
面向對象有哪些特性?
面向對象四大特性:封裝,繼承,多態,抽象
1、封裝就是將類的信息隱藏在類內部,不允許外部程式直接訪問,而是通過該類的方法實現對隱藏信息的操作和訪問。 良好的封裝能夠減少耦合。
2、繼承是從已有的類中派生出新的類,新的類繼承父類的屬性和行為,並能擴展新的能力,大大增加程式的重用性和易維護性。在Java中是單繼承的,也就是說一個子類只有一個父類。
3、多態是同一個行為具有多個不同表現形式的能力。在不修改程式代碼的情況下改變程式運行時綁定的代碼。實現多態的三要素:繼承、重寫、父類引用指向子類對象。
- 靜態多態性:通過重載實現,相同的方法有不同的參數列表,可以根據參數的不同,做出不同的處理。
- 動態多態性:在子類中重寫父類的方法。運行期間判斷所引用對象的實際類型,根據其實際類型調用相應的方法。
4、抽象。把客觀事物用代碼抽象出來。
面向對象編程的六大原則?
- 對象單一職責:我們設計創建的對象,必須職責明確,比如商品類,裡面相關的屬性和方法都必須跟商品相關,不能出現訂單等不相關的內容。這裡的類可以是模塊、類庫、程式集,而不單單指類。
- 里式替換原則:子類能夠完全替代父類,反之則不行。通常用於實現介面時運用。因為子類能夠完全替代基(父)類,那麼這樣父類就擁有很多子類,在後續的程式擴展中就很容易進行擴展,程式完全不需要進行修改即可進行擴展。比如IA的實現為A,因為項目需求變更,現在需要新的實現,直接在容器註入處更換介面即可.
- 迪米特法則,也叫最小原則,或者說最小耦合。通常在設計程式或開發程式的時候,儘量要高內聚,低耦合。當兩個類進行交互的時候,會產生依賴。而迪米特法則就是建議這種依賴越少越好。就像構造函數註入父類對象時一樣,當需要依賴某個對象時,並不在意其內部是怎麼實現的,而是在容器中註入相應的實現,既符合里式替換原則,又起到瞭解耦的作用。
- 開閉原則:開放擴展,封閉修改。當項目需求發生變更時,要儘可能的不去對原有的代碼進行修改,而在原有的基礎上進行擴展。
- 依賴倒置原則:高層模塊不應該直接依賴於底層模塊的具體實現,而應該依賴於底層的抽象。介面和抽象類不應該依賴於實現類,而實現類依賴介面或抽象類。
- 介面隔離原則:一個對象和另外一個對象交互的過程中,依賴的內容最小。也就是說在介面設計的時候,在遵循對象單一職責的情況下,儘量減少介面的內容。
簡潔版:
- 單一職責:對象設計要求獨立,不能設計萬能對象。
- 開閉原則:對象修改最小化。
- 里式替換:程式擴展中抽象被具體可以替換(介面、父類、可以被實現類對象、子類替換對象)
- 迪米特:高內聚,低耦合。儘量不要依賴細節。
- 依賴倒置:面向抽象編程。也就是參數傳遞,或者返回值,可以使用父類類型或者介面類型。從廣義上講:基於介面編程,提前設計好介面框架。
- 介面隔離:介面設計大小要適中。過大導致污染,過小,導致調用麻煩。
數組到底是不是對象?
先說說對象的概念。對象是根據某個類創建出來的一個實例,表示某類事物中一個具體的個體。
對象具有各種屬性,並且具有一些特定的行為。站在電腦的角度,對象就是記憶體中的一個記憶體塊,在這個記憶體塊封裝了一些數據,也就是類中定義的各個屬性。
所以,對象是用來封裝數據的。
java中的數組具有java中其他對象的一些基本特點。比如封裝了一些數據,可以訪問屬性,也可以調用方法。
因此,可以說,數組是對象。
也可以通過代碼驗證數組是對象的事實。比如以下的代碼,輸出結果為java.lang.Object。
Class clz = int[].class;
System.out.println(clz.getSuperclass().getName());
由此,可以看出,數組類的父類就是Object類,那麼可以推斷出數組就是對象。
Java的基本數據類型有哪些?
- byte,8bit
- char,16bit
- short,16bit
- int,32bit
- float,32bit
- long,64bit
- double,64bit
- boolean,只有兩個值:true、false,可以使⽤用 1 bit 來存儲
簡單類型 | boolean | byte | char | short | Int | long | float | double |
---|---|---|---|---|---|---|---|---|
二進位位數 | 1 | 8 | 16 | 16 | 32 | 64 | 32 | 64 |
包裝類 | Boolean | Byte | Character | Short | Integer | Long | Float | Double |
在Java規範中,沒有明確指出boolean的大小。在《Java虛擬機規範》給出了單個boolean占4個位元組,和boolean數組1個位元組的定義,具體 還要看虛擬機實現是否按照規範來,因此boolean占用1個位元組或者4個位元組都是有可能的。
為什麼不能用浮點型表示金額?
由於電腦中保存的小數其實是十進位的小數的近似值,並不是準確值,所以,千萬不要在代碼中使用浮點數來表示金額等重要的指標。
建議使用BigDecimal或者Long來表示金額。
什麼是值傳遞和引用傳遞?
- 值傳遞是對基本型變數而言的,傳遞的是該變數的一個副本,改變副本不影響原變數。
- 引用傳遞一般是對於對象型變數而言的,傳遞的是該對象地址的一個副本,並不是原對象本身,兩者指向同一片記憶體空間。所以對引用對象進行操作會同時改變原對象。
java中不存在引用傳遞,只有值傳遞。即不存在變數a指向變數b,變數b指向對象的這種情況。
瞭解Java的包裝類型嗎?為什麼需要包裝類?
Java 是一種面向對象語言,很多地方都需要使用對象而不是基本數據類型。比如,在集合類中,我們是無法將 int 、double 等類型放進去的。因為集合的容器要求元素是 Object 類型。
為了讓基本類型也具有對象的特征,就出現了包裝類型。相當於將基本類型包裝起來,使得它具有了對象的性質,並且為其添加了屬性和方法,豐富了基本類型的操作。
自動裝箱和拆箱
Java中基礎數據類型與它們對應的包裝類見下表:
原始類型 | 包裝類型 |
---|---|
boolean | Boolean |
byte | Byte |
char | Character |
float | Float |
int | Integer |
long | Long |
short | Short |
double | Double |
裝箱:將基礎類型轉化為包裝類型。
拆箱:將包裝類型轉化為基礎類型。
當基礎類型與它們的包裝類有如下幾種情況時,編譯器會自動幫我們進行裝箱或拆箱:
- 賦值操作(裝箱或拆箱)
- 進行加減乘除混合運算 (拆箱)
- 進行>,<,==比較運算(拆箱)
- 調用equals進行比較(裝箱)
- ArrayList、HashMap等集合類添加基礎類型數據時(裝箱)
示例代碼:
Integer x = 1; // 裝箱 調⽤ Integer.valueOf(1)
int y = x; // 拆箱 調⽤了 X.intValue()
下麵看一道常見的面試題:
Integer a = 100;
Integer b = 100;
System.out.println(a == b);
Integer c = 200;
Integer d = 200;
System.out.println(c == d);
輸出:
true
false
為什麼第三個輸出是false?看看 Integer 類的源碼就知道啦。
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
Integer c = 200;
會調用 調⽤Integer.valueOf(200)
。而從Integer的valueOf()源碼可以看到,這裡的實現並不是簡單的new Integer,而是用IntegerCache做一個cache。
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
}
...
}
這是IntegerCache靜態代碼塊中的一段,預設Integer cache 的下限是-128,上限預設127。當賦值100給Integer時,剛好在這個範圍內,所以從cache中取對應的Integer並返回,所以a和b返回的是同一個對象,所以==
比較是相等的,當賦值200給Integer時,不在cache 的範圍內,所以會new Integer並返回,當然==
比較的結果是不相等的。
String 為什麼不可變?
先看看什麼是不可變的對象。
如果一個對象,在它創建完成之後,不能再改變它的狀態,那麼這個對象就是不可變的。不能改變狀態的意思是,不能改變對象內的成員變數,包括基本數據類型的值不能改變,引用類型的變數不能指向其他的對象,引用類型指向的對象的狀態也不能改變。
接著來看Java8 String類的源碼:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
}
從源碼可以看出,String對象其實在內部就是一個個字元,存儲在這個value數組裡面的。
value數組用final修飾,final 修飾的變數,值不能被修改。因此value不可以指向其他對象。
String類內部所有的欄位都是私有的,也就是被private修飾。而且String沒有對外提供修改內部狀態的方法,因此value數組不能改變。
所以,String是不可變的。
那為什麼String要設計成不可變的?
主要有以下幾點原因:
- 線程安全。同一個字元串實例可以被多個線程共用,因為字元串不可變,本身就是線程安全的。
- 支持hash映射和緩存。因為String的hash值經常會使用到,比如作為 Map 的鍵,不可變的特性使得 hash 值也不會變,不需要重新計算。
- 出於安全考慮。網路地址URL、文件路徑path、密碼通常情況下都是以String類型保存,假若String不是固定不變的,將會引起各種安全隱患。比如將密碼用String的類型保存,那麼它將一直留在記憶體中,直到垃圾收集器把它清除。假如String類不是固定不變的,那麼這個密碼可能會被改變,導致出現安全隱患。
- 字元串常量池優化。String對象創建之後,會緩存到字元串常量池中,下次需要創建同樣的對象時,可以直接返回緩存的引用。
既然我們的String是不可變的,它內部還有很多substring, replace, replaceAll這些操作的方法。這些方法好像會改變String對象?怎麼解釋呢?
其實不是的,我們每次調用replace等方法,其實會在堆記憶體中創建了一個新的對象。然後其value數組引用指向不同的對象。
為何JDK9要將String的底層實現由char[]改成byte[]?
主要是為了節約String占用的記憶體。
在大部分Java程式的堆記憶體中,String占用的空間最大,並且絕大多數String只有Latin-1字元,這些Latin-1字元只需要1個位元組就夠了。
而在JDK9之前,JVM因為String使用char數組存儲,每個char占2個位元組,所以即使字元串只需要1位元組,它也要按照2位元組進行分配,浪費了一半的記憶體空間。
到了JDK9之後,對於每個字元串,會先判斷它是不是只有Latin-1字元,如果是,就按照1位元組的規格進行分配記憶體,如果不是,就按照2位元組的規格進行分配,這樣便提高了記憶體使用率,同時GC次數也會減少,提升效率。
不過Latin-1編碼集支持的字元有限,比如不支持中文字元,因此對於中文字元串,用的是UTF16編碼(兩個位元組),所以用byte[]和char[]實現沒什麼區別。
String, StringBuffer 和 StringBuilder區別
1. 可變性
- String 不可變
- StringBuffer 和 StringBuilder 可變
2. 線程安全
- String 不可變,因此是線程安全的
- StringBuilder 不是線程安全的
- StringBuffer 是線程安全的,內部使用 synchronized 進行同步
什麼是StringJoiner?
StringJoiner是 Java 8 新增的一個 API,它基於 StringBuilder 實現,用於實現對字元串之間通過分隔符拼接的場景。
StringJoiner 有兩個構造方法,第一個構造要求依次傳入分隔符、首碼和尾碼。第二個構造則只要求傳入分隔符即可(首碼和尾碼預設為空字元串)。
StringJoiner(CharSequence delimiter, CharSequence prefix, CharSequence suffix)
StringJoiner(CharSequence delimiter)
有些字元串拼接場景,使用 StringBuffer 或 StringBuilder 則顯得比較繁瑣。
比如下麵的例子:
List<Integer> values = Arrays.asList(1, 3, 5);
StringBuilder sb = new StringBuilder("(");
for (int i = 0; i < values.size(); i++) {
sb.append(values.get(i));
if (i != values.size() -1) {
sb.append(",");
}
}
sb.append(")");
而通過StringJoiner來實現拼接List的各個元素,代碼看起來更加簡潔。
List<Integer> values = Arrays.asList(1, 3, 5);
StringJoiner sj = new StringJoiner(",", "(", ")");
for (Integer value : values) {
sj.add(value.toString());
}
另外,像平時經常使用的Collectors.joining(","),底層就是通過StringJoiner實現的。
源碼如下:
public static Collector<CharSequence, ?, String> joining(
CharSequence delimiter,CharSequence prefix,CharSequence suffix) {
return new CollectorImpl<>(
() -> new StringJoiner(delimiter, prefix, suffix),
StringJoiner::add, StringJoiner::merge,
StringJoiner::toString, CH_NOID);
}
String 類的常用方法有哪些?
- indexOf():返回指定字元的索引。
- charAt():返回指定索引處的字元。
- replace():字元串替換。
- trim():去除字元串兩端空白。
- split():分割字元串,返回一個分割後的字元串數組。
- getBytes():返回字元串的 byte 類型數組。
- length():返回字元串長度。
- toLowerCase():將字元串轉成小寫字母。
- toUpperCase():將字元串轉成大寫字元。
- substring():截取字元串。
- equals():字元串比較。
new String("dabin")會創建幾個對象?
使用這種方式會創建兩個字元串對象(前提是字元串常量池中沒有 "dabin" 這個字元串對象)。
- "dabin" 屬於字元串字面量,因此編譯時期會在字元串常量池中創建一個字元串對象,指向這個 "dabin" 字元串字面量;
- 使用 new 的方式會在堆中創建一個字元串對象。
什麼是字元串常量池?
字元串常量池(String Pool)保存著所有字元串字面量,這些字面量在編譯時期就確定。字元串常量池位於堆記憶體中,專門用來存儲字元串常量。在創建字元串時,JVM首先會檢查字元串常量池,如果該字元串已經存在池中,則返回其引用,如果不存在,則創建此字元串並放入池中,並返回其引用。
String最大長度是多少?
String類提供了一個length方法,返回值為int類型,而int的取值上限為2^31 -1。
所以理論上String的最大長度為2^31 -1。
達到這個長度的話需要多大的記憶體嗎?
String內部是使用一個char數組來維護字元序列的,一個char占用兩個位元組。如果說String最大長度是2^31 -1的話,那麼最大的字元串占用記憶體空間約等於4GB。
也就是說,我們需要有大於4GB的JVM運行記憶體才行。
那String一般都存儲在JVM的哪塊區域呢?
字元串在JVM中的存儲分兩種情況,一種是String對象,存儲在JVM的堆棧中。一種是字元串常量,存儲在常量池裡面。
什麼情況下字元串會存儲在常量池呢?
當通過字面量進行字元串聲明時,比如String s = "程式新大彬";,這個字元串在編譯之後會以常量的形式進入到常量池。
那常量池中的字元串最大長度是2^31-1嗎?
不是的,常量池對String的長度是有另外限制的。。Java中的UTF-8編碼的Unicode字元串在常量池中以CONSTANT_Utf8類型表示。
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}
length在這裡就是代表字元串的長度,length的類型是u2,u2是無符號的16位整數,也就是說最大長度可以做到2^16-1 即 65535。
不過javac編譯器做了限制,需要length < 65535。所以字元串常量在常量池中的最大長度是65535 - 1 = 65534。
最後總結一下:
String在不同的狀態下,具有不同的長度限制。
- 字元串常量長度不能超過65534
- 堆內字元串的長度不超過2^31-1
Object常用方法有哪些?
Java面試經常會出現的一道題目,Object的常用方法。下麵給大家整理一下。
Object常用方法有:toString()
、equals()
、hashCode()
、clone()
等。
toString
預設輸出對象地址。
public class Person {
private int age;
private String name;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
public static void main(String[] args) {
System.out.println(new Person(18, "程式員大彬").toString());
}
//output
//me.tyson.java.core.Person@4554617c
}
可以重寫toString方法,按照重寫邏輯輸出對象值。
public class Person {
private int age;
private String name;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
@Override
public String toString() {
return name + ":" + age;
}
public static void main(String[] args) {
System.out.println(new Person(18, "程式員大彬").toString());
}
//output
//程式員大彬:18
}
equals
預設比較兩個引用變數是否指向同一個對象(記憶體地址)。
public class Person {
private int age;
private String name;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
public static void main(String[] args) {
String name = "程式員大彬";
Person p1 = new Person(18, name);
Person p2 = new Person(18, name);
System.out.println(p1.equals(p2));
}
//output
//false
}
可以重寫equals方法,按照age和name是否相等來判斷:
public class Person {
private int age;
private String name;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (o instanceof Person) {
Person p = (Person) o;
return age == p.age && name.equals(p.name);
}
return false;
}
public static void main(String[] args) {
String name = "程式員大彬";
Person p1 = new Person(18, name);
Person p2 = new Person(18, name);
System.out.println(p1.equals(p2));
}
//output
//true
}
hashCode
將與對象相關的信息映射成一個哈希值,預設的實現hashCode值是根據記憶體地址換算出來。
public class Cat {
public static void main(String[] args) {
System.out.println(new Cat().hashCode());
}
//out
//1349277854
}
clone
Java賦值是複製對象引用,如果我們想要得到一個對象的副本,使用賦值操作是無法達到目的的。Object對象有個clone()方法,實現了對
象中各個屬性的複製,但它的可見範圍是protected的。
protected native Object clone() throws CloneNotSupportedException;
所以實體類使用克隆的前提是:
- 實現Cloneable介面,這是一個標記介面,自身沒有方法,這應該是一種約定。調用clone方法時,會判斷有沒有實現Cloneable介面,沒有實現Cloneable的話會拋異常CloneNotSupportedException。
- 覆蓋clone()方法,可見性提升為public。
public class Cat implements Cloneable {
private String name;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
public static void main(String[] args) throws CloneNotSupportedException {
Cat c = new Cat();
c.name = "程式員大彬";
Cat cloneCat = (Cat) c.clone();
c.name = "大彬";
System.out.println(cloneCat.name);
}
//output
//程式員大彬
}
getClass
返回此 Object 的運行時類,常用於java反射機制。
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
public static void main(String[] args) {
Person p = new Person("程式員大彬");
Class clz = p.getClass();
System.out.println(clz);
//獲取類名
System.out.println(clz.getName());
}
/**
* class com.tyson.basic.Person
* com.tyson.basic.Person
*/
}
wait
當前線程調用對象的wait()方法之後,當前線程會釋放對象鎖,進入等待狀態。等待其他線程調用此對象的notify()/notifyAll()喚醒或者等待超時時間wait(long timeout)自動喚醒。線程需要獲取obj對象鎖之後才能調用 obj.wait()。
notify
obj.notify()喚醒在此對象上等待的單個線程,選擇是任意性的。notifyAll()喚醒在此對象上等待的所有線程。
講講深拷貝和淺拷貝?
淺拷貝:拷⻉對象和原始對象的引⽤類型引用同⼀個對象。
以下例子,Cat對象裡面有個Person對象,調用clone之後,克隆對象和原對象的Person引用的是同一個對象,這就是淺拷貝。
public class Cat implements Cloneable {
private String name;
private Person owner;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
public static void main(String[] args) throws CloneNotSupportedException {
Cat c = new Cat();
Person p = new Person(18, "程式員大彬");
c.owner = p;
Cat cloneCat = (Cat) c.clone();
p.setName("大彬");
System.out.println(cloneCat.owner.getName());
}
//output
//大彬
}
深拷貝:拷貝對象和原始對象的引用類型引用不同的對象。
以下例子,在clone函數中不僅調用了super.clone,而且調用Person對象的clone方法(Person也要實現Cloneable介面並重寫clone方法),從而實現了深拷貝。可以看到,拷貝對象的值不會受到原對象的影響。
public class Cat implements Cloneable {
private String name;
private Person owner;
@Override
protected Object clone() throws CloneNotSupportedException {
Cat c = null;
c = (Cat) super.clone();
c.owner = (Person) owner.clone();//拷貝Person對象
return c;
}
public static void main(String[] args) throws CloneNotSupportedException {
Cat c = new Cat();
Person p = new Person(18, "程式員大彬");
c.owner = p;
Cat cloneCat = (Cat) c.clone();
p.setName("大彬");
System.out.println(cloneCat.owner.getName());
}
//output
//程式員大彬
}
兩個對象的hashCode()相同,則 equals()是否也一定為 true?
equals與hashcode的關係:
- 如果兩個對象調用equals比較返回true,那麼它們的hashCode值一定要相同;
- 如果兩個對象的hashCode相同,它們並不一定相同。
hashcode方法主要是用來提升對象比較的效率,先進行hashcode()的比較,如果不相同,那就不必在進行equals的比較,這樣就大大減少了equals比較的次數,當比較對象的數量很大的時候能提升效率。
為什麼重寫 equals 時一定要重寫 hashCode?
之所以重寫equals()
要重寫hashcode()
,是為了保證equals()
方法返回true的情況下hashcode值也要一致,如果重寫了equals()
沒有重寫hashcode()
,就會出現兩個對象相等但hashcode()
不相等的情況。這樣,當用其中的一個對象作為鍵保存到hashMap、hashTable或hashSet中,再以另一個對象作為鍵值去查找他們的時候,則會查找不到。
Java創建對象有幾種方式?
Java創建對象有以下幾種方式:
- 用new語句創建對象。
- 使用反射,使用Class.newInstance()創建對象。
- 調用對象的clone()方法。
- 運用反序列化手段,調用java.io.ObjectInputStream對象的readObject()方法。
說說類實例化的順序
Java中類實例化順序:
- 靜態屬性,靜態代碼塊。
- 普通屬性,普通代碼塊。
- 構造方法。
public class LifeCycle {
// 靜態屬性
private static String staticField = getStaticField();
// 靜態代碼塊
static {
System.out.println(staticField);
System.out.println("靜態代碼塊初始化");
}
// 普通屬性
private String field = getField();
// 普通代碼塊
{
System.out.println(field);
System.out.println("普通代碼塊初始化");
}
// 構造方法
public LifeCycle() {
System.out.println("構造方法初始化");
}
// 靜態方法
public static String getStaticField() {
String statiFiled = "靜態屬性初始化";
return statiFiled;
}
// 普通方法
public String getField() {
String filed = "普通屬性初始化";
return filed;
}
public static void main(String[] argc) {
new LifeCycle();
}
/**
* 靜態屬性初始化
* 靜態代碼塊初始化
* 普通屬性初始化
* 普通代碼塊初始化
* 構造方法初始化
*/
}
equals和==有什麼區別?
-
對於基本數據類型,==比較的是他們的值。基本數據類型沒有equal方法;
-
對於複合數據類型,==比較的是它們的存放地址(是否是同一個對象)。
equals()
預設比較地址值,重寫的話按照重寫邏輯去比較。
常見的關鍵字有哪些?
static
static可以用來修飾類的成員方法、類的成員變數。
static變數也稱作靜態變數,靜態變數和非靜態變數的區別是:靜態變數被所有的對象所共用,在記憶體中只有一個副本,它當且僅當在類初次載入時會被初始化。而非靜態變數是對象所擁有的,在創建對象的時候被初始化,存在多個副本,各個對象擁有的副本互不影響。
以下例子,age為非靜態變數,則p1列印結果是:Name:zhangsan, Age:10
;若age使用static修飾,則p1列印結果是:Name:zhangsan, Age:12
,因為static變數在記憶體只有一個副本。
public class Person {
String name;
int age;
public String toString() {
return "Name:" + name + ", Age:" + age;
}
public static void main(String[] args) {
Person p1 = new Person();
p1.name = "zhangsan";
p1.age = 10;
Person p2 = new Person();
p2.name = "lisi";
p2.age = 12;
System.out.println(p1);
System.out.println(p2);
}
/**Output
* Name:zhangsan, Age:10
* Name:lisi, Age:12
*///~
}
static方法一般稱作靜態方法。靜態方法不依賴於任何對象就可以進行訪問,通過類名即可調用靜態方法。
public class Utils {
public static void print(String s) {
System.out.println("hello world: " + s);
}
public static void main(String[] args) {
Utils.print("程式員大彬");
}
}
靜態代碼塊只會在類載入的時候執行一次。以下例子,startDate和endDate在類載入的時候進行賦值。
class Person {
private Date birthDate;
private static Date startDate, endDate;
static{
startDate = Date.valueOf("2008");
endDate = Date.valueOf("2021");
}
public Person(Date birthDate) {
this.birthDate = birthDate;
}
}
靜態內部類
在靜態方法里,使用⾮靜態內部類依賴於外部類的實例,也就是說需要先創建外部類實例,才能用這個實例去創建非靜態內部類。⽽靜態內部類不需要。
public class OuterClass {
class InnerClass {
}
static class StaticInnerClass {
}
public static void main(String[] args) {
// 在靜態方法里,不能直接使用OuterClass.this去創建InnerClass的實例
// 需要先創建OuterClass的實例o,然後通過o創建InnerClass的實例
// InnerClass innerClass = new InnerClass();
OuterClass outerClass = new OuterClass();
InnerClass innerClass = outerClass.new InnerClass();
StaticInnerClass staticInnerClass = new StaticInnerClass();
outerClass.test();
}
public void nonStaticMethod() {
InnerClass innerClass = new InnerClass();
System.out.println("nonStaticMethod...");
}
}
final
-
基本數據類型用final修飾,則不能修改,是常量;對象引用用final修飾,則引用只能指向該對象,不能指向別的對象,但是對象本身可以修改。
-
final修飾的方法不能被子類重寫
-
final修飾的類不能被繼承。
this
this.屬性名稱
指訪問類中的成員變數,可以用來區分成員變數和局部變數。如下代碼所示,this.name
訪問類Person當前實例的變數。
/**
* @description:
* @author: 程式員大彬
* @time: 2021-08-17 00:29
*/
public class Person {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
this.方法名稱
用來訪問本類的方法。以下代碼中,this.born()
調用類 Person 的當前實例的方法。
/**
* @description:
* @author: 程式員大彬
* @time: 2021-08-17 00:29
*/
public class Person {
String name;
int age;
public Person(String name, int age) {
this.born();
this.name = name;
this.age = age;
}
void born() {
}
}
super
super 關鍵字用於在子類中訪問父類的變數和方法。
class A {
protected String name = "大彬";
public void getName() {
System.out.println("父類:" + name);
}
}
public class B extends A {
@Override
public void getName() {
System.out.println(super.name);
super.getName();
}
public static void main(String[] args) {
B b = new B();
b.getName();
}
/**
* 大彬
* 父類:大彬
*/
}
在子類B中,我們重寫了父類的getName()
方法,如果在重寫的getName()
方法中我們要調用父類的相同方法,必須要通過super關鍵字顯式指出。
final, finally, finalize 的區別
- final 用於修飾屬性、方法和類, 分別表示屬性不能被重新賦值,方法不可被覆蓋,類不可被繼承。
- finally 是異常處理語句結構的一部分,一般以
try-catch-finally
出現,finally
代碼塊表示總是被執行。 - finalize 是Object類的一個方法,該方法一般由垃圾回收器來調用,當我們調用
System.gc()
方法的時候,由垃圾回收器調用finalize()
方法,回收垃圾,JVM並不保證此方法總被調用。
final關鍵字的作用?
- final 修飾的類不能被繼承。
- final 修飾的方法不能被重寫。
- final 修飾的變數叫常量,常量必須初始化,初始化之後值就不能被修改。
方法重載和重寫的區別?
同個類中的多個方法可以有相同的方法名稱,但是有不同的參數列表,這就稱為方法重載。參數列表又叫參數簽名,包括參數的類型、參數的個數、參數的順序,只要有一個不同就叫做參數列表不同。
重載是面向對象的一個基本特性。
public class OverrideTest {
void setPerson() { }
void setPerson(String name) {
//set name
}
void setPerson(String name, int age) {
//set name and age
}
}
方法的重寫描述的是父類和子類之間的。當父類的功能無法滿足子類的需求,可以在子類對方法進行重寫。方法重寫時, 方法名與形參列表必須一致。
如下代碼,Person為父類,Student為子類,在Student中重寫了dailyTask方法。
public class Person {
private String name;
public void dailyTask() {
System.out.println("work eat sleep");
}
}
public class Student extends Person {
@Override
public void dailyTask() {
System.out.println("study eat sleep");
}
}
介面與抽象類區別?
1、語法層面上的區別
- 抽象類可以有方法實現,而介面的方法中只能是抽象方法(Java 8 之後介面方法可以有預設實現);
- 抽象類中的成員變數可以是各種類型的,介面中的成員變數只能是public static final類型;
- 介面中不能含有靜態代碼塊以及靜態方法,而抽象類可以有靜態代碼塊和靜態方法(Java 8之後介面可以有靜態方法);
- 一個類只能繼承一個抽象類,而一個類卻可以實現多個介面。
2、設計層面上的區別
- 抽象層次不同。抽象類是對整個類整體進行抽象,包括屬性、行為,但是介面只是對類行為進行抽象。繼承抽象類是一種"是不是"的關係,而介面實現則是 "有沒有"的關係。如果一個類繼承了某個抽象類,則子類必定是抽象類的種類,而介面實現則是具備不具備的關係,比如鳥是否能飛。
- 繼承抽象類的是具有相似特點的類,而實現介面的卻可以不同的類。
門和警報的例子:
class AlarmDoor extends Door implements Alarm {
//code
}
class BMWCar extends Car implements Alarm {
//code
}
常見的Exception有哪些?
常見的RuntimeException:
ClassCastException
//類型轉換異常IndexOutOfBoundsException
//數組越界異常NullPointerException
//空指針ArrayStoreException
//數組存儲異常NumberFormatException
//數字格式化異常ArithmeticException
//數學運算異常
checked Exception:
NoSuchFieldException
//反射異常,沒有對應的欄位ClassNotFoundException
//類沒有找到異常IllegalAccessException
//安全許可權異常,可能是反射時調用了private方法
Error和Exception的區別?
Error:JVM 無法解決的嚴重問題,如棧溢出StackOverflowError
、記憶體溢出OOM
等。程式無法處理的錯誤。
Exception:其它因編程錯誤或偶然的外在因素導致的一般性問題。可以在代碼中進行處理。如:空指針異常、數組下標越界等。
運行時異常和非運行時異常(checked)的區別?
unchecked exception
包括RuntimeException
和Error
類,其他所有異常稱為檢查(checked)異常。
RuntimeException
由程式錯誤導致,應該修正程式避免這類異常發生。checked Exception
由具體的環境(讀取的文件不存在或文件為空或sql異常)導致的異常。必須進行處理,不然編譯不通過,可以catch或者throws。
throw和throws的區別?
-
throw:用於拋出一個具體的異常對象。
-
throws:用在方法簽名中,用於聲明該方法可能拋出的異常。子類方法拋出的異常範圍更加小,或者根本不拋異常。
通過故事講清楚NIO
下麵通過一個例子來講解下。
假設某銀行只有10個職員。該銀行的業務流程分為以下4個步驟:
1) 顧客填申請表(5分鐘);
2) 職員審核(1分鐘);
3) 職員叫保全去金庫取錢(3分鐘);
4) 職員列印票據,並將錢和票據返回給顧客(1分鐘)。
下麵我們看看銀行不同的工作方式對其工作效率到底有何影響。
首先是BIO方式。
每來一個顧客,馬上由一位職員來接待處理,並且這個職員需要負責以上4個完整流程。當超過10個顧客時,剩餘的顧客需要排隊等候。
一個職員處理一個顧客需要10分鐘(5+1+3+1)時間。一個小時(60分鐘)能處理6個顧客,一共10個職員,那就是只能處理60個顧客。
可以看到銀行職員的工作狀態並不飽和,比如在第1步,其實是處於等待中。
這種工作其實就是BIO,每次來一個請求(顧客),就分配到線程池中由一個線程(職員)處理,如果超出了線程池的最大上限(10個),就扔到隊列等待 。
那麼如何提高銀行的吞吐量呢?
思路就是:分而治之,將任務拆分開來,由專門的人負責專門的任務。
具體來講,銀行專門指派一名職員A,A的工作就是每當有顧客到銀行,他就遞上表格讓顧客填寫。每當有顧客填好表後,A就將其隨機指派給剩餘的9名職員完成後續步驟。
這種方式下,假設顧客非常多,職員A的工作處於飽和中,他不斷的將填好表的顧客帶到櫃臺處理。
櫃臺一個職員5分鐘能處理完一個顧客,一個小時9名職員能處理:9*(60/5)=108。
可見工作方式的轉變能帶來效率的極大提升。
這種工作方式其實就NIO的思路。
下圖是非常經典的NIO說明圖,mainReactor
線程負責監聽server socket,接收新連接,並將建立的socket分派給subReactor
subReactor
可以是一個線程,也可以是線程池,負責多路分離已連接的socket,讀寫網路數據。這裡的讀寫網路數據可類比顧客填表這一耗時動作,對具體的業務處理功能,其扔給worker線程池完成
可以看到典型NIO有三類線程,分別是mainReactor
線程、subReactor
線程、work
線程。
不同的線程乾專業的事情,最終每個線程都沒空著,系統的吞吐量自然就上去了。
那這個流程還有沒有什麼可以提高的地方呢?
可以看到,在這個業務流程裡邊第3個步驟,職員叫保全去金庫取錢(3分鐘)。這3分鐘櫃臺職員是在等待中度過的,可以把這3分鐘利用起來。
還是分而治之的思路,指派1個職員B來專門負責第3步驟。
每當櫃臺員工完成第2步時,就通知職員B來負責與保全溝通取錢。這時候櫃臺員工可以繼續處理下一個顧客。
當職員B拿到錢之後,通知顧客錢已經到櫃臺了,讓顧客重新排隊處理,當櫃臺職員再次服務該顧客時,發現該顧客前3步已經完成,直接執行第4步即可。
在當今web服務中,經常需要通過RPC或者Http等方式調用第三方服務,這裡對應的就是第3步,如果這步耗時較長,通過非同步方式將能極大降低資源使用率。
NIO+非同步的方式能讓少量的線程做大量的事情。這適用於很多應用場景,比如代理服務、api服務、長連接服務等等。這些應用如果用同步方式將耗費大量機器資源。
不過雖然NIO+非同步能提高系統吞吐量,但其並不能讓一個請求的等待時間下降,相反可能會增加等待時間。
最後,NIO基本思想總結起來就是:分而治之,將任務拆分開來,由專門的人負責專門的任務
BIO/NIO/AIO區別的區別?
同步阻塞IO : 用戶進程發起一個IO操作以後,必須等待IO操作的真正完成後,才能繼續運行。
同步非阻塞IO: 客戶端與伺服器通過Channel連接,採用多路復用器輪詢註冊的Channel
。提高吞吐量和可靠性。用戶進程發起一個IO操作以後,可做其它事情,但用戶進程需要輪詢IO操作是否完成,這樣造成不必要的CPU資源浪費。
非同步非阻塞IO: 非阻塞非同步通信模式,NIO的升級版,採用非同步通道實現非同步通信,其read和write方法均是非同步方法。用戶進程發起一個IO操作,然後立即返回,等IO操作真正的完成以後,應用程式會得到IO操作完成的通知。類似Future模式。
守護線程是什麼?
- 守護線程是運行在後臺的一種特殊進程。
- 它獨立於控制終端並且周期性地執行某種任務或等待處理某些發生的事件。
- 在 Java 中垃圾回收線程就是特殊的守護線程。
Java支持多繼承嗎?
java中,類不支持多繼承。介面才支持多繼承。介面的作用是拓展對象功能。當一個子介面繼承了多個父介面時,說明子介面拓展了多個功能。當一個類實現該介面時,就拓展了多個的功能。
Java不支持多繼承的原因:
- 出於安全性的考慮,如果子類繼承的多個父類裡面有相同的方法或者屬性,子類將不知道具體要繼承哪個。
- Java提供了介面和內部類以達到實現多繼承功能,彌補單繼承的缺陷。
如何實現對象克隆?
- 實現
Cloneable
介面,重寫clone()
方法。這種方式是淺拷貝,即如果類中屬性有自定義引用類型,只拷貝引用,不拷貝引用指向的對象。如果對象的屬性的Class也實現Cloneable
介面,那麼在克隆對象時也會克隆屬性,即深拷貝。 - 結合序列化,深拷貝。
- 通過
org.apache.commons
中的工具類BeanUtils
和PropertyUtils
進行對象複製。
同步和非同步的區別?
同步:發出一個調用時,在沒有得到結果之前,該調用就不返回。
非同步:在調用發出後,被調用者返回結果之後會通知調用者,或通過回調函數處理這個調用。
阻塞和非阻塞的區別?
阻塞和非阻塞關註的是線程的狀態。
阻塞調用是指調用結果返回之前,當前線程會被掛起。調用線程只有在得到結果之後才會恢復運行。
非阻塞調用指在不能立刻得到結果之前,該調用不會阻塞當前線程。
舉個例子,理解下同步、阻塞、非同步、非阻塞的區別:
同步就是燒開水,要自己來看開沒開;非同步就是水開了,然後水壺響了通知你水開了(回調通知)。阻塞是燒開水的過程中,你不能幹其他事情,必須在旁邊等著;非阻塞是燒開水的過程里可以乾其他事情。
Java8的新特性有哪些?
- Lambda 表達式:Lambda允許把函數作為一個方法的參數
- Stream API :新添加的Stream API(java.util.stream) 把真正的函數式編程風格引入到Java中
- 預設方法:預設方法就是一個在介面裡面有了一個實現的方法。
- Optional 類 :Optional 類已經成為 Java 8 類庫的一部分,用來解決空指針異常。
- Date Time API :加強對日期與時間的處理。
序列化和反序列化
- 序列化:把對象轉換為位元組序列的過程稱為對象的序列化.
- 反序列化:把位元組序列恢復為對象的過程稱為對象的反序列化.
什麼時候需要用到序列化和反序列化呢?
當我們只在本地 JVM 里運行下 Java 實例,這個時候是不需要什麼序列化和反序列化的,但當我們需要將記憶體中的對象持久化到磁碟,資料庫中時,當我們需要與瀏覽器進行交互時,當我們需要實現 RPC 時,這個時候就需要序列化和反序列化了.
前兩個需要用到序列化和反序列化的場景,是不是讓我們有一個很大的疑問? 我們在與瀏覽器交互時,還有將記憶體中的對象持久化到資料庫中時,好像都沒有去進行序列化和反序列化,因為我們都沒有實現 Serializable 介面,但一直正常運行.
下麵先給出結論:
只要我們對記憶體中的對象進行持久化或網路傳輸,這個時候都需要序列化和反序列化.
理由:
伺服器與瀏覽器交互時真的沒有用到 Serializable 介面嗎? JSON 格式實際上就是將一個對象轉化為字元串,所以伺服器與瀏覽器交互時的數據格式其實是字元串,我們來看來 String 類型的源碼:
public final class String
implements java.io.Serializable,Comparable<String>,CharSequence {
/\*\* The value is used for character storage. \*/
private final char value\[\];
/\*\* Cache the hash code for the string \*/
private int hash; // Default to 0
/\*\* use serialVersionUID from JDK 1.0.2 for interoperability \*/
private static final long serialVersionUID = -6849794470754667710L;
......
}
String 類型實現了 Serializable 介面,並顯示指定 serialVersionUID 的值.
然後我們再來看對象持久化到資料庫中時的情況,Mybatis 資料庫映射文件里的 insert 代碼:
<insert id="insertUser" parameterType="org.tyshawn.bean.User">
INSERT INTO t\_user(name,age) VALUES (#{name},#{age})
</insert>
實際上我們並不是將整個對象持久化到資料庫中,而是將對象中的屬性持久化到資料庫中,而這些屬性(如Date/String)都實現了 Serializable 介面。
實現序列化和反序列化為什麼要實現 Serializable 介面?
在 Java 中實現了 Serializable 介面後, JVM 在類載入的時候就會發現我們實現了這個介面,然後在初始化實例對象的時候就會在底層幫我們實現序列化和反序列化。
如果被寫對象類型不是String、數組、Enum,並且沒有實現Serializable介面,那麼在進行序列化的時候,將拋出NotSerializableException。源碼如下:
// remaining cases
if (obj instanceof String) {
writeString((String) obj, unshared);
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
} else {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
實現 Serializable 介面之後,為什麼還要顯示指定 serialVersionUID 的值?
如果不顯示指定 serialVersionUID,JVM 在序列化時會根據屬性自動生成一個 serialVersionUID,然後與屬性一起序列化,再進行持久化或網路傳輸. 在反序列化時,JVM 會再根據屬性自動生成一個新版 serialVersionUID,然後將這個新版 serialVersionUID 與序列化時生成的舊版 serialVersionUID 進行比較,如果相同則反序列化成功,否則報錯.
如果顯示指定了 serialVersionUID,JVM 在序列化和反序列化時仍然都會生成一個 serialVersionUID,但值為我們顯示指定的值,這樣在反序列化時新舊版本的 serialVersionUID 就一致了.
如果我們的類寫完後不再修改,那麼不指定serialVersionUID,不會有問題,但這在實際開發中是不可能的,我們的類會不斷迭代,一旦類被修改了,那舊