剛剛經歷過秋招,看了大量的面經,順便將常見的Java常考知識點總結了一下,並根據被問到的頻率大致做了一個標注。一顆星表示知識點需要了解,被問到的頻率不高,面試時起碼能說個差不多。兩顆星表示被問到的頻率較高或對理解Java有着重要的作用,建議熟練掌握。三顆星表示被問到的頻率非常高,建議深入理解並熟練掌握其相關知識,方便面試時拓展(方便裝逼),給面試官留下個好印象。
推薦閱讀:一文搞懂所有Java集合面試題
- JVM、JRE及JDK的關系 **
- JAVA語言特點 **
- JAVA和C++的區別 **
- Java的基本數據類型 **
- 隱式(自動)類型轉換和顯示(強制)類型轉換 **
- 自動裝箱與拆箱 **
- String(不是基本數據類型)
- switch 是否能作用在 byte 上,是否能作用在 long 上,是否能作用在 String 上 *
- Java語言采用何種編碼方案?有何特點?*
- 訪問修飾符 **
- 運算符 *
- 關鍵字
- 面向對象和面向過程的區別 **
- 面向對象三大特性(封裝、繼承、多態) ***
- 面向對象五大基本原則是什么 **
- 抽象類和接口的對比 ***
- 在Java中定義一個不做事且沒有參數的構造方法的作用 *
- 在調用子類構造方法之前會先調用父類沒有參數的構造方法,其目的是 *
- 一個類的構造方法的作用是什么?若一個類沒有聲明構造方法,改程序能正確執行嗎?為什么? *
- 構造方法有哪些特性? **
- 變量 **
- 內部類 **
- 重寫與重載 ***
- == 和 equals 的區別 ***
- hashCode 與 equals(為什么重寫equals方法后,hashCode方法也必須重寫) ***
- Java 中是值傳遞還是引用傳遞,還是兩者共存 **
- IO流 *
- BIO,NIO,AIO 有什么區別? **
- 反射 ***
- JAVA異常 ***
- JAVA注解 **
- JAVA泛型 ***
- JAVA序列化 **
- 深拷貝與淺拷貝 ***
- 常見的Object方法 ***
JVM、JRE及JDK的關系 **
JDK(Java Development Kit)是針對Java開發員的產品,是整個Java的核心,包括了Java運行環境JRE、Java工具和Java基礎類庫。
Java Runtime Environment(JRE)是運行JAVA程序所必須的環境的集合,包含JVM標准實現及Java核心類庫。
JVM是Java Virtual Machine(Java虛擬機)的縮寫,是整個java實現跨平台的最核心的部分,能夠運行以Java語言寫作的軟件程序。
簡單來說就是JDK是Java的開發工具,JRE是Java程序運行所需的環境,JVM是Java虛擬機.它們之間的關系是JDK包含JRE和JVM,JRE包含JVM.
JAVA語言特點 **
- Java是一種面向對象的語言
- Java通過Java虛擬機實現了平台無關性,一次編譯,到處運行
- 支持多線程
- 支持網絡編程
- 具有較高的安全性和可靠性
JAVA和C++的區別 **
面試時記住前四個就行了
- Java 通過虛擬機從而實現跨平台特性,但是 C++ 依賴於特定的平台。
- Java 沒有指針,它的引用可以理解為安全指針,而 C++ 具有和 C 一樣的指針。
- Java 支持自動垃圾回收,而 C++ 需要手動回收。
- Java 不支持多重繼承,只能通過實現多個接口來達到相同目的,而 C++ 支持多重繼承。
- Java 不支持操作符重載,雖然可以對兩個 String 對象執行加法運算,但是這是語言內置支持的操作,不屬於操
作符重載,而 C++ 可以。 - Java 的 goto 是保留字,但是不可用,C++ 可以使用 goto。
Java的基本數據類型 **
注意
String
不是基本數據類型
類型 | 關鍵字 | 包裝器類型 | 占用內存(字節)(重要) | 取值范圍 | 默認值 |
---|---|---|---|---|---|
字節型 | byte | Byte | 1 | -128(-2^7) ~ 127(2^7-1) | 0 |
短整型 | short | Short | 2 | -2^15 ~ 2^15-1 | 0 |
整型 | int | Integer | 4 | -2^31 ~ 2^31-1 | 0 |
長整型 | long | Long | 8 | -2^63 ~ 2^63-1 | 0L |
單精度浮點型 | float | Float | 4 | 3.4e-45 ~ 1.4e38 | 0.0F |
雙精度浮點型 | double | Double | 8 | 4.9e-324 ~ 1.8e308 | 0.0D |
字符型 | char | Character | 2 | '\u0000' | |
布爾型 | boolean | Boolean | 1 | true/flase | flase |
隱式(自動)類型轉換和顯示(強制)類型轉換 **
- 隱式(自動)類型轉換:從存儲范圍小的類型到存儲范圍大的類型。
byte
→short(char)
→int
→long
→float
→double
- 顯示(強制)類型轉換:從存儲范圍大的類型到存儲范圍小的類型。
double
→float
→long
→int
→short(char)
→byte
。該類類型轉換很可能存在精度的損失。
看一個經典的代碼
short s = 1;
s = s + 1;
這是會報錯的,因為1是int
型,s+1
會自動轉換為int
型,將int
型直接賦值給short
型會報錯。
做一下修改即可避免報錯
short s = 1;
s = (short)(s + 1);
或這樣寫,因為s += 1
會自動進行強制類型轉換
short s = 1;
s += 1;
自動裝箱與拆箱 **
-
裝箱:將基本類型用包裝器類型包裝起來
-
拆箱:將包裝器類型轉換為基本類型
這個地方有很多易混淆的地方,但在面試中問到的頻率一般,筆試的選擇題中經常出現,還有一個
String
創建對象和這個比較像,很容易混淆,在下文可以看到 -
下面這段代碼的輸出結果是什么?
public class Main { public static void main(String[] args) { Integer a = 100; Integer b = 100; Integer c = 128; Integer d = 128; System.out.println(a==b); System.out.println(c==d); } }
true false
很多人看到這個結果會很疑惑,為什么會是一個
true
一個flase
.其實從源碼中可以很容易找到原因.首先找到Integer
方法中的valueOf
方法public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); }
可以看到當不滿足
if
語句中的條件,就會重新創建一個對象返回,那結果必然不相等。繼續打開IntegerCache
可以看到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; cache = new Integer[(high - low) + 1]; int j = low; for(int k = 0; k < cache.length; k++) cache[k] = new Integer(j++); // range [-128, 127] must be interned (JLS7 5.1.7) assert IntegerCache.high >= 127; } private IntegerCache() {} }
代碼挺長,大概說的就是在通過
valueOf
方法創建Integer
對象的時候,如果數值在[-128,127]之間,便返回指向IntegerCache.cache
中已經存在的對象的引用;否則創建一個新的Integer
對象。所以上面代碼中a
與b
相等,c
與d
不相等。 -
在看下面的代碼會輸出什么
public class Main { public static void main(String[] args) { Double a = 1.0; Double b = 1.0; Double c = 2.0; Double d = 2.0; System.out.println(a==b); System.out.println(c==d); } }
flase flase
采用同樣的方法,可以看到
Double
的valueOf
方法,每次返回都是重新new
一個新的對象,所以上面代碼中的結果都不想等。public static Double valueOf(double d) { return new Double(d); }
-
最后再看這段代碼的輸出結果
public class Main { public static void main(String[] args) { Boolean a = false; Boolean b = false; Boolean c = true; Boolean d = true; System.out.println(a==b); System.out.println(c==d); } }
true true
老方法繼續看
valueOf
方法public static Boolean valueOf(boolean b) { return (b ? TRUE : FALSE); }
再看看
TRUE
和FALSE
是個什么東西,是兩個靜態成員屬性。public static final Boolean TRUE = new Boolean(true); public static final Boolean FALSE = new Boolean(false);
**說下結論 **:Integer
、Short
、Byte
、Character
、Long
這幾個類的valueOf
方法的實現是類似的。Double
、Float
的valueOf
方法的實現是類似的。然后是Boolean
的valueOf
方法是單獨一組的。
-
Integer i = new Integer(xxx)
和Integer i =xxx
的區別這兩者的區別主要是第一種會觸發自動裝箱,第二者不會
最后看看下面這段程序的輸出結果
public class Main { public static void main(String[] args) { Integer a = 1; Integer b = 2; Integer c = 3; Long g = 3L; int int1 = 12; int int2 = 12; Integer integer1 = new Integer(12); Integer integer2 = new Integer(12); Integer integer3 = new Integer(1); System.out.println("c==(a+b) ->"+ (c==(a+b))); System.out.println("g==(a+b) ->" + (g==(a+b))); System.out.println( "c.equals(a+b) ->" + (c.equals(a+b))); System.out.println( "g.equals(a+b) ->" + (g.equals(a+b))); System.out.println("int1 == int2 -> " + (int1 == int2)); System.out.println("int1 == integer1 -> " + (int1 == integer1)); System.out.println("integer1 == integer2 -> " + (integer1 == integer2)); System.out.println("integer3 == a1 -> " + (integer3 == a)); } }
c==(a+b) ->true g==(a+b) ->true c.equals(a+b) ->true g.equals(a+b) ->false int1 == int2 -> true int1 == integer1 -> true integer1 == integer2 -> false integer3 == a1 -> false
下面簡單解釋這些結果。
1.當 "=="運算符的兩個操作數都是包裝器類型的引用,則是比較指向的是否是同一個對象,而如果其中有一個操作數是表達式(即包含算術運算)則比較的是數值(即會觸發自動拆箱的過程)。所以
c==a+b
,g==a+b
為true
。2.而對於
equals
方法會先觸發自動拆箱過程,再觸發自動裝箱過程。也就是說a+b,會先各自調用intValue
方法,得到了加法運算后的數值之后,便調用Integer.valueOf
方法,再進行equals
比較。所以c.equals(a+b)
為true
。而對於g.equals(a+b)
,a+b
會先拆箱進行相加運算,在裝箱進行equals
比較,但是裝箱后為Integer
,g
為Long
,所以g.equals(a+b)
為false
。3.
int1 == int2
為true
無需解釋,int1 == integer1
,在進行比較時,integer1
會先進行一個拆箱操作變成int
型在進行比較,所以int1 == integer1
為true
。4.
integer1 == integer2
->false
。integer1
和integer2
都是通過new
關鍵字創建的,可以看成兩個對象,所以integer1 == integer2
為false
。integer3 == a1
->false
,integer3
是一個對象類型,而a1
是一個常量它們存放內存的位置不一樣,所以integer3 == a1
為false
,具體原因可學習下java的內存模型。
String(不是基本數據類型)
String的不可變性 ***
在 Java 8 中,String
內部使用 char
數組存儲數據。並且被聲明為final
,因此它不可被繼承。
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
}
為什么Strin
g`要設計成不可變的呢(不可變性的好處):
1.可以緩存 hash
值()
因為 String
的hash
值經常被使用,例如 String
用做 HashMap
的 key
。不可變的特性可以使得 hash
值也不可變,
因此只需要進行一次計算。
2.常量池優化
String
對象創建之后,會在字符串常量池中進行緩存,如果下次創建同樣的對象時,會直接返回緩存的引用。
3.線程安全
String
不可變性天生具備線程安全,可以在多個線程中安全地使用。
字符型常量和字符串常量的區別 *
- 形式上: 字符常量是單引號引起的一個字符 字符串常量是雙引號引起的若干個字符
- 含義上: 字符常量相當於一個整形值(ASCII值),可以參加表達式運算 字符串常量代表一個地址值(該字符串在內存中存放位置)
- 占內存大小 字符常量占兩個字節 字符串常量占若干個字節(至少一個字符結束標志)
什么是字符串常量池?*
字符串常量池位於堆內存中,專門用來存儲字符串常量,可以提高內存的使用率,避免開辟多塊空間存儲相同的字符串,在創建字符串時 JVM 會首先檢查字符串常量池,如果該字符串已經存在池中,則返回它的引用,如果不存在,則實例化一個字符串放到池中,並返回其引用。
String 類的常用方法都有那些?**
面試時一般不會問,但面試或筆試寫字符串相關的算法題經常會涉及到,還是得背一背(以下大致是按使用頻率優先級排序)
length()
:返回字符串長度charAt()
:返回指定索引處的字符substring()
:截取字符串trim()
:去除字符串兩端空白split()
:分割字符串,返回一個分割后的字符串數組。replace()
:字符串替換。indexOf()
:返回指定字符的索引。toLowerCase()
:將字符串轉成小寫字母。toUpperCase()
:將字符串轉成大寫字符。
String和StringBuffer、StringBuilder的區別是什么?***
1.可變性
String
不可變,StringBuilder
和StringBuffer
是可變的
2.線程安全性
String
由於是不可變的,所以線程安全。StringBuffer
對方法加了同步鎖或者對調用的方法加了同步鎖,所以是線程安全的。 StringBuilder
並沒有對方法進行加同步鎖,所以是非線程安全的。
3.性能
StringBuilder
> StringBuffer
> String
為了方便記憶,總結如下
是否可變 | 是否安全 | 性能 | |
---|---|---|---|
String | 不可變 | 安全 | 低 |
StringBuilder | 可變 | 不安全 | 高 |
StringBuffer | 可變 | 安全 | 較高 |
switch 是否能作用在 byte 上,是否能作用在 long 上,是否能作用在 String 上 *
switch
可以作用於char
byte
short
int
及它們對應的包裝類型,switch
不可作用於long
double
float
boolean
及他們的包裝類型。在 JDK1.5之后可以作用於枚舉類型,在JDK1.7之后可作用於String
類型。
Java語言采用何種編碼方案?有何特點?*
Java語言采用Unicode編碼標准,它為每個字符制訂了一個唯一的數值,因此在任何的語言,平台,程序都可以放心的使用。
訪問修飾符 **
在Java編程語言中有四種權限訪問控制符,這四種訪問權限的控制符能夠控制類中成員的可見性。其中類有兩種public
、default
。而方法和變量有 4 種:public
、default
、protected
、private
。
-
public : 對所有類可見。使用對象:類、接口、變量、方法
-
protected : 對同一包內的類和所有子類可見。使用對象:變量、方法。 注意:不能修飾類(外部類)。
-
default : 在同一包內可見,不使用任何修飾符。使用對象:類、接口、變量、方法。
-
private : 在同一類內可見。使用對象:變量、方法。 注意:不能修飾類(外部類)
修飾符 當前類 同包內 子類(同包) 其他包 public Y Y Y Y protected Y Y Y N default Y Y Y N private Y N N N
運算符 *
-
&&和&
&&
和&
都可以表示邏輯與,但他們是有區別的,共同點是他們兩邊的條件都成立的時候最終結果才是true
;不同點是&&
只要是第一個條件不成立為false
,就不會再去判斷第二個條件,最終結果直接為false
,而&
判斷的是所有的條件。 -
||和|
||
和|
都表示邏輯或,共同點是只要兩個判斷條件其中有一個成立最終的結果就是true
,區別是||
只要滿足第一個條件,后面的條件就不再判斷,而|
要對所有的條件進行判斷。
關鍵字
static關鍵字 ***
static
關鍵字的主要用途就是方便在沒有創建對象時調用方法和變量和優化程序性能
1.static變量(靜態變量)
用static
修飾的變量被稱為靜態變量,也被稱為類變量,可以直接通過類名來訪問它。靜態變量被所有的對象共享,在內存中只有一個副本,僅當在類初次加載時會被初始化,而非靜態變量在創建對象的時候被初始化,並且存在多個副本,各個對象擁有的副本互不影響。
2.static方法(靜態方法)
static
方法不依賴於任何對象就可以進行訪問,在static
方法中不能訪問類的非靜態成員變量和非靜態成員方法,因為非靜態成員方法/變量都是必須依賴具體的對象才能夠被調用,但是在非靜態成員方法中是可以訪問靜態成員方法/變量的。
public class Main {
public static String s1 = "s1";//靜態變量
String s2 = "s2";
public void fun1(){
System.out.println(s1);
System.out.println(s2);
}
public static void fun2(){
System.out.println(s1);
System.out.println(s2);//此處報錯,靜態方法不能調用非靜態變量
}
}
3.static代碼塊(靜態代碼塊)
靜態代碼塊的主要用途是可以用來優化程序的性能,因為它只會在類加載時加載一次,很多時候會將一些只需要進行一次的初始化操作都放在static
代碼塊中進行。如果程序中有多個static
塊,在類初次被加載的時候,會按照static
塊的順序來執行每個static
塊。
public class Main {
static {
System.out.println("hello,word");
}
public static void main(String[] args) {
Main m = new Main();
}
}
4.可以通過this訪問靜態成員變量嗎?(可以)
this
代表當前對象,可以訪問靜態變量,而靜態方法中是不能訪問非靜態變量,也不能使用this
引用。
5.初始化順序
靜態變量和靜態語句塊優先於實例變量和普通語句塊,靜態變量和靜態語句塊的初始化順序取決於它們在代碼中的順序。如果存在繼承關系的話,初始化順序為父類中的靜態變量和靜態代碼塊——子類中的靜態變量和靜態代碼塊——父類中的實例變量和普通代碼塊——父類的構造函數——子類的實例變量和普通代碼塊——子類的構造函數
final 關鍵字 ***
final
關鍵字主要用於修飾類,變量,方法。
- 類:被
final
修飾的類不可以被繼承 - 方法:被
final
修飾的方法不可以被重寫 - 變量:被
final
修飾的變量是基本類型,變量的數值不能改變;被修飾的變量是引用類型,變量便不能在引用其他對象,但是變量所引用的對象本身是可以改變的。
public class Main {
int a = 1;
public static void main(String[] args) {
final int b = 1;
b = 2;//報錯
final Main m = new Main();
m.a = 2;//不報錯,可以改變引用類型變量所指向的對象
}
}
final finally finalize區別 ***
final
主要用於修飾類,變量,方法finally
一般作用在try-catch
代碼塊中,在處理異常的時候,通常我們將一定要執行的代碼方法finally
代碼塊
中,表示不管是否出現異常,該代碼塊都會執行,一般用來存放一些關閉資源的代碼。finalize
是一個屬於Object
類的一個方法,該方法一般由垃圾回收器來調用,當我們調用System.gc()
方法的時候,由垃圾回收器調用finalize()
,回收垃圾,但Java語言規范並不保證inalize
方法會被及時地執行、而且根本不會保證它們會被執行。
this關鍵字 **
重點掌握前三種即可
1.this
關鍵字可用來引用當前類的實例變量。主要用於形參與成員名字重名,用this
來區分。
public Person(String name, int age) {
this.name = name;
this.age = age;
}
2.this
關鍵字可用於調用當前類方法。
public class Main {
public void fun1(){
System.out.println("hello,word");
}
public void fun2(){
this.fun1();//this可省略
}
public static void main(String[] args) {
Main m = new Main();
m.fun2();
}
}
3.this()
可以用來調用當前類的構造函數。(注意:this()
一定要放在構造函數的第一行,否則編譯不通過)
class Person{
private String name;
private int age;
public Person() {
}
public Person(String name) {
this.name = name;
}
public Person(String name, int age) {
this(name);
this.age = age;
}
}
4.this
關鍵字可作為調用方法中的參數傳遞。
5.this
關鍵字可作為參數在構造函數調用中傳遞。
6.this
關鍵字可用於從方法返回當前類的實例。super
super關鍵字 **
1.super
可以用來引用直接父類的實例變量。和this
類似,主要用於區分父類和子類中相同的字段
2.super
可以用來調用直接父類構造函數。(注意:super()
一定要放在構造函數的第一行,否則編譯不通過)
3.super
可以用來調用直接父類方法。
public class Main {
public static void main(String[] args) {
Child child = new Child("Father","Child");
child.test();
}
}
class Father{
protected String name;
public Father(String name) {
this.name = name;
}
public void Say(){
System.out.println("hello,child");
}
}
class Child extends Father{
private String name;
public Child(String name1, String name2) {
super(name1); //調用直接父類構造函數
this.name = name2;
}
public void test(){
System.out.println(this.name);
System.out.println(super.name); //引用直接父類的實例變量
super.Say(); //調用直接父類方法
}
}
this與super的區別 **
-
相同點:
super()
和this()
都必須在構造函數的第一行進行調用,否則就是錯誤的this()
和super()
都指的是對象,所以,均不可以在static
環境中使用。
-
不同點:
super()
主要是對父類構造函數的調用,this()
是對重載構造函數的調用super()
主要是在繼承了父類的子類的構造函數中使用,是在不同類中的使用;this()
主要是在同一類的不同構造函數中的使用
break ,continue ,return 的區別及作用 **
break
結束當前的循環體continue
結束本次循環,進入下一次循環return
結束當前方法
面向對象和面向過程的區別 **
-
面向過程
優點:性能比面向對象高,因為類調用時需要實例化,開銷比較大,比較消耗資源。
缺點:沒有面向對象易維護、易復用、易擴展
-
面向對象
優點:易維護、易復用、易擴展,由於面向對象有封裝、繼承、多態性的特性,可以設計出低耦合的系統,使系統更加靈活、更加易於維護
缺點:性能比面向過程低
面向對象三大特性(封裝、繼承、多態) ***
-
封裝
封裝就是隱藏對象的屬性和實現細節,僅對外公開接口,控制在程序中屬性的讀和修改的訪問級別。
-
繼承
繼承就是子類繼承父類的特征和行為,使得子類對象(實例)具有父類的實例域和方法,或子類從父類繼承方法,使得子類具有父類相同的行為。
-
多態(重要)
多態是同一個行為具有多個不同表現形式或形態的能力。這句話不是很好理解,可以看這個解釋,在Java語言中,多態就是指程序中定義的引用變量所指向的具體類型和通過該引用變量發出的方法調用在編程時並不確定,而是在程序運行期間才確定,即一個引用變量倒底會指向哪個類的實例對象,該引用變量發出的方法調用到底是哪個類中實現的方法,必須在由程序運行期間才能決定。
在Java中實現多態的三個必要條件:繼承、重寫、向上轉型。繼承和重寫很好理解,向上轉型是指在多態中需要將子類的引用賦給父類對象。
public class Main { public static void main(String[] args) { Person person = new Student(); //向上轉型 person.run(); } } class Person { public void run() { System.out.println("Person"); } } class Student extends Person { //繼承 @Override public void run() { //重載 System.out.println("Student"); } }
運行結果為
Student
面向對象五大基本原則是什么 **
-
單一職責原則(Single-Resposibility Principle)
一個類,最好只做一件事,只有一個引起它的變化。單一職責原則可以看做是低耦合、高內聚在面向對象原則上的引申,將職責定義為引起變化的原因,以提高內聚性來減少引起變化的原因。
-
開放封閉原則(Open-Closed principle)
軟件實體應該是可擴展的,而不可修改的。也就是,對擴展開放,對修改封閉的。
-
里氏替換原則 (Liskov-Substituion Principle)
子類必須能夠替換其基類。這一思想體現為對繼承機制的約束規范,只有子類能夠替換基類時,才能保證系統在運行期內識別子類,這是保證繼承復用的基礎。在父類和子類的具體行為中,必須嚴格把握繼承層次中的關系和特征,將基類替換為子類,程序的行為不會發生任何變化。同時,這一約束反過來則是不成立的,子類可以替換基類,但是基類不一定能替換子類。
-
依賴倒置原則(Dependecy-Inversion Principle)
依賴於抽象。具體而言就是高層模塊不依賴於底層模塊,二者都同依賴於抽象;抽象不依賴於具體,具體依賴於抽象。
-
接口隔離原則(Interface-Segregation Principle)
使用多個小的專門的接口,而不要使用一個大的總接口。
抽象類和接口的對比 ***
在Java語言中,abstract class
和interface
是支持抽象類定義的兩種機制。抽象類:用來捕捉子類的通用特性的。接口:抽象方法的集合。
相同點:
- 接口和抽象類都不能實例化
- 都包含抽象方法,其子類都必須覆寫這些抽象方法
不同點:
類型 | 抽象類 | 接口 |
---|---|---|
定義 | abstract class | Interface |
實現 | extends(需要提供抽象類中所有聲明的方法的實現) | implements(需要提供接口中所有聲明的方法的實現) |
繼承 | 抽象類可以繼承一個類和實現多個接口;子類只可以繼承一個抽象類 | 接口只可以繼承接口(一個或多個);子類可以實現多個接口 |
訪問修飾符 | 抽象方法可以有public、protected和default這些修飾符 | 接口方法默認修飾符是public。你不可以使用其它修飾符 |
構造器 | 抽象類可以有構造器 | 接口不能有構造器 |
字段聲明 | 抽象類的字段聲明可以是任意的 | 接口的字段默認都是 static 和 final 的 |
在Java中定義一個不做事且沒有參數的構造方法的作用 *
Java程序存在繼承,在執行子類的構造方法時,如果沒有用super()
來調用父類特定的構造方法,則會調用父類中“沒有參數的構造方法”。如果父類只定義了有參數的構造函數,而子類的構造函數沒有用super
調用父類那個特定的構造函數,就會出錯。
在調用子類構造方法之前會先調用父類沒有參數的構造方法,其目的是 *
幫助子類做初始化工作。
一個類的構造方法的作用是什么?若一個類沒有聲明構造方法,改程序能正確執行嗎?為什么? *
主要作用是完成對類對象的初始化工作。可以執行。因為一個類即使沒有聲明構造方法也會有默認的不帶參數的構造方法。
構造方法有哪些特性? **
- 方法名稱和類同名
- 不用定義返回值類型
- 不可以寫
retrun
語句 - 構造方法可以被重載
變量 **
-
類變量:獨立於方法之外的變量,用
static
修飾。 -
實例變量:獨立於方法之外的變量,不過沒有
static
修飾。 -
局部變量:類的方法中的變量。
-
成員變量:成員變量又稱全局變量,可分為類變量和實例變量,有
static
修飾為類變量,沒有static
修飾為實例變量。類變量 實例變量 局部變量 定義位置 類中,方法外 類中,方法外 方法中 初始值 有默認初始值 有默認初始值 無默認初始值 存儲位置 方法區 堆 棧 生命周期 類何時被加載和卸載 實例何時被創建及銷毀 方法何時被調用及結束調用
內部類 **
內部類包括這四種:成員內部類、局部內部類、匿名內部類和靜態內部類
-
成員內部類
1.成員內部類定義為位於另一個類的內部,成員內部類可以無條件訪問外部類的所有成員屬性和成員方法(包括
private
成員和靜態成員)。class Outer{ private double a = 0; public static int b =1; public Outer(double a) { this.a = a; } class Inner { //內部類 public void fun() { System.out.println(a); System.out.println(b); } } }
2.當成員內部類擁有和外部類同名的成員變量或者方法時,即默認情況下訪問的是成員內部類的成員。如果要訪問外部類的同名成員,需要以下面的形式進行訪問:外部類.
this
.成員變量3.在外部類中如果要訪問成員內部類的成員,必須先創建一個成員內部類的對象,再通過指向這個對象的引用來訪問。
4.成員內部類是依附外部類而存在的,如果要創建成員內部類的對象,前提是必須存在一個外部類的對象。創建成員內部類對象的一般方式如下:
class Outter{ private double a = 0; public static int b =1; public Outter(){} public Outter(double a) { this.a = a; Inner inner = new Inner(); inner.fun(); //調用內部類的方法 } class Inner { //內部類 int b = 2; public void fun() { System.out.println(a); System.out.println(b); //訪問內部類的b System.out.println(Outter.this.b);//訪問外部類的b } } } public class Main{ public static void main(String[] args) { Outter outter = new Outter(); Outter.Inner inner = outter.new Inner(); //創建內部類的對象 } }
-
局部內部類
局部內部類是定義在一個方法或者一個作用域里面的類,它和成員內部類的區別在於局部內部類的訪問僅限於方法內或者該作用域內。定義在實例方法中的局部類可以訪問外部類的所有變量和方法,定義在靜態方法中的局部類只能訪問外部類的靜態變量和方法。
class Outter { private int outter_a = 1; private static int static_b = 2; public void test1(){ int inner_c =3; class Inner { private void fun(){ System.out.println(outter_a); System.out.println(static_b); System.out.println(inner_c); } } Inner inner = new Inner(); //創建局部內部類 inner.fun(); } public static void test2(){ int inner_d =3; class Inner { private void fun(){ System.out.println(outter_a); //編譯錯誤,定義在靜態方法中的局部類不可以訪問外部類的實例變量 System.out.println(static_b); System.out.println(inner_d); } } Inner inner = new Inner(); inner.fun(); } }
-
匿名內部類
匿名內部類只沒有名字的內部類,在日常開發中使用較多。使用匿名內部類的前提條件:必須繼承一個父類或實現一個接口。
interface Person { public void fun(); } class Demo { public static void main(String[] args) { new Person() { public void fun() { System.out.println("hello,word"); } }.fun(); } }
-
靜態內部類
靜態內部類也是定義在另一個類里面的類,只不過在類的前面多了一個關鍵字
static
。靜態內部類是不需要依賴於外部類的,並且它不能使用外部類的非static
成員變量或者方法,這點很好理解,因為在沒有外部類的對象的情況下,可以創建靜態內部類的對象,如果允許訪問外部類的非static
成員就會產生矛盾,因為外部類的非static成員必須依附於具體的對象。class Outter { int a = 1; static int b = 2; public Outter() { } static class Inner { public Inner() { System.out.println(a);//報錯,靜態內部類不能訪問非靜態變量 System.out.println(b); } } } public class Main{ public static void main(String[] args) { Outter.Inner inner = new Outter.Inner(); } }
-
內部類的優點:
- 內部類不為同一包的其他類所見,具有很好的封裝性;
- 匿名內部類可以很方便的定義回調。
- 每個內部類都能獨立的繼承一個接口的實現,所以無論外部類是否已經繼承了某個(接口的)實現,對於內部類都沒有影響。
- 內部類有效實現了“多重繼承”,優化 java 單繼承的缺陷。
-
局部內部類和匿名內部類訪問局部變量的時候,為什么變量必須要加上
final
?public class Main { public static void main(String[] args) { } public void fun(final int b) { final int a = 10; new Thread(){ public void run() { System.out.println(a); System.out.println(b); }; }.start(); } }
對於變量
a
可以從生命周期的角度理解,局部變量直接存儲在棧中,當方法執行結束后,非final
的局部變量就被銷毀,而局部內部類對局部變量的引用依然存在,如果局部內部類要調用沒有final
修飾的局部變量時,就會造成生命周期不一致出錯。對於變量
b
,其實是將fun
方法中的變量b
以參數的形式對匿名內部類中的拷貝(變量b
的拷貝)進行賦值初始化。在run
方法中訪問的變量b
根本就不是test
方法中的局部變量b
,而是一個拷貝值,所以不存在生命周期不一致的問題,但如果在run
方法中修改變量b
的值會導致數據不一致,所以需要加final
修飾。
重寫與重載 ***
重載和重寫的區別
- 重載:發生在同一個類中,方法名相同參數列表不同(參數類型不同、個數不同、順序不同),與方法返回值和訪問修飾符無關,即重載的方法不能根據返回類型進行區分。
- 重寫:發生在父子類中,方法名、參數列表必須相同,返回值小於等於父類,拋出的異常小於等於父類,訪問修飾符大於等於父類(里氏代換原則);如果父類方法訪問修飾符為
private
則子類中就不是重寫。
構造器(constructor)是否可被重寫(override)
構造器可以被重載,不能被重寫
重載的方法能否根據返回類型進行區分?為什么?
不能,因為調用時不能指定類型信息,編譯器不知道你要調用哪個函數。
== 和 equals 的區別 ***
-
==
對於基本數據類型,
==
比較的是值;對於引用數據類型,==
比較的是內存地址。 -
eauals
對於沒有重寫
equals
方法的類,equals
方法和==
作用類似;對於重寫過equals
方法的類,equals
比較的是值。
hashCode 與 equals(為什么重寫equals方法后,hashCode方法也必須重寫) ***
-
equals
先看下
String
類中重寫的equals
方法。public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }
從源碼中可以看到:
equals
方法首先比較的是內存地址,如果內存地址相同,直接返回true
;如果內存地址不同,再比較對象的類型,類型不同直接返回false
;類型相同,再比較值是否相同;值相同返回true
,值不同返回false
。總結一下,equals
會比較內存地址、對象類型、以及值,內存地址相同,equals
一定返回true
;對象類型和值相同,equals
方法一定返回true
。- 如果沒有重寫
equals
方法,那么equals
和==
的作用相同,比較的是對象的地址值。
-
hashCode
hashCode
方法返回對象的散列碼,返回值是int
類型的散列碼。散列碼的作用是確定該對象在哈希表中的索引位置。關於
hashCode
有一些約定:- 兩個對象相等,則
hashCode
一定相同。 - 兩個對象有相同的
hashCode
值,它們不一定相等。 hashCode()
方法默認是對堆上的對象產生獨特值,如果沒有重寫hashCode()
方法,則該類的兩個對象的hashCode
值肯定不同
- 兩個對象相等,則
-
為什么重寫
equals
方法后,hashCode
方法也必須重寫- 根據規定,兩個對象相等,
hashCode
值也許相同,所以重寫equals
方法后,hashCode
方法也必須重寫(面試官肯定不是想聽這個答案) hashCode
在具有哈希機制的集合中起着非常關鍵的作用,比如HashMap
、HashSet
等。以HashSet
為例,HashSet
的特點是存儲元素時無序且唯一,在向HashSet
中添加對象時,首相會計算對象的HashCode
值來確定對象的存儲位置,如果該位置沒有其他對象,直接將該對象添加到該位置;如果該存儲位置有存儲其他對象(新添加的對象和該存儲位置的對象的HashCode
值相同),調用equals
方法判斷兩個對象是否相同,如果相同,則添加對象失敗,如果不相同,則會將該對象重新散列到其他位置。所以重寫equals
方法后,hashCode
方法不重寫的話,會導致所有對象的HashCode
值都不相同,都能添加成功,那么HashSet
中會出現很多重復元素,HashMap
也是同理(因為HashSet
的底層就是通過HashMap
實現的),會出現大量相同的Key
(HashMap
中的key
是唯一的,但不同的key
可以對應相同的value
)。所以重寫equals
方法后,hashCode
方法也必須重寫。同時因為兩個對象的hashCode
值不同,則它們一定不相等,所以先計算對象的hashCode
值可以在一定程度上判斷兩個對象是否相等,提高了集合的效率。總結一下,一共兩點:第一,在HashSet
等集合中,不重寫hashCode
方法會導致其功能出現問題;第二,可以提高集合效率。
- 根據規定,兩個對象相等,
Java 中是值傳遞還是引用傳遞,還是兩者共存 **
這是一個很容易搞混又很難解釋清楚的問題,先說結論,Java中只有值傳遞
先看這樣一段代碼
public class Main{
public static void main(String[] args) {
int a = 1;
printValue(a);
System.out.println("a:" + a);
}
public static void printValue(int b){
b = 2;
System.out.println("b:"+ b);
}
}
輸出
b:2
a:1
可以看到將a
的值傳到printValue
方法中,並將其值改為2。但方法調用結束后,a
的值還是1,並未發生改變,所以這種情況下為值傳遞。
再看這段代碼
public class Main{
public static void main(String[] args) {
Preson p = new Preson();
p.name = "zhangsan";
printValue(p);
System.out.println("p.name: " + p.name);
}
public static void printValue(Preson q){
q.name = "lisi";
System.out.println("q.name: "+ q.name);
}
}
class Preson{
public String name;
}
輸出結果
q.name: lisi
p.name: lisi
在將p
傳入printValue
方法后,方法調用結束,p
的name
屬性竟然被改變了!所以得出結論,參數為基本類型為值傳遞,參數為引用類型為時為引用傳遞。這個結論是錯誤的,下面來看看判斷是值傳遞還是值傳遞的關鍵是什么,先看定義
- 值傳遞:是指在調用函數時將實際參數復制一份傳遞到函數中,這樣在函數中如果對參數進行修改,將不會影響到實際參數。
- 引用傳遞:是指在調用函數時將實際參數的地址直接傳遞到函數中,那么在函數中對參數所進行的修改,將影響到實際參數。
從定義中可以明顯看出,區分是值傳遞還是引用傳遞主要是看向方法中傳遞的是實際參數的副本還是實際參數的地址。上面第一個例子很明顯是值傳遞,其實第二個例子中向printValue
方法中傳遞的是一個引用的副本,只是這個副本引用和原始的引用指向的同一個對象,所以副本引用修改過對象屬性后,通過原始引用查看對象屬性肯定也是被修改過的。換句話說,printValue
方法中修改的是副本引用指向的對象的屬性,不是引用本身,如果修改的是引用本身,那么原始引用肯定不受影響。看下面這個例子
public class Main{
public static void main(String[] args) {
Preson p = new Preson();
p.name = "zhangsan";
printValue(p);
System.out.println("p.name: " + p.name);
}
public static void printValue(Preson q){
q = new Preson();
q.name = "lisi";
System.out.println("q.name: "+ q.name);
}
}
class Preson{
public String name;
}
輸出結果
q.name: lisi
p.name: zhangsan
可以看到將p
傳入printValue
方法后,printValue
方法調用結束后,p
的屬性name
沒有改變,這是因為在printValue
方法中並沒有改變副本引用q
所指向的對象,而是改變了副本引用q
本身,將副本引用q
指向了另一個對象並對這個對象的屬性進行修改,所以原始引用p
所指向的對象不受影響。所以證明Java中只存在值傳遞。
IO流 *
Java IO流主要可以分為輸入流和輸出流。按照照操作單元划分,可以划分為字節流和字符流。按照流的角色划分為節點流和處理流。
Java I0流的40多個類都是從4個抽象類基類中派生出來的。
- InputStream:字節輸入流
- Reader:字符輸入流
- OutputStream:字節輸出流
- Writer:字符輸出流
BIO,NIO,AIO 有什么區別? **
-
BIO (Blocking I/O):服務器實現模式為一個連接一個線程,即客戶端有連接請求時服務器就需要啟動一個線程進行處理,如果這個連接不做任何事情會造成不必要的線程開銷,可以通過線程池機制來改善。BIO方式適用於連接數目比較小且固定的架構,這種方式對服務端資源要求比較高,並發局限於應用中,在jdk1.4以前是唯一的io
-
NIO (New I/O):服務器實現模式為一個請求一個線程,即客戶端發送的連接請求都會注冊到多路復用器上,多路復用器輪詢到連接有IO請求時才啟動一個線程進行處理。NIO方式適用於連接數目多且連接比較短(輕操作)的架構,比如聊天服務器,並發局限於應用中,編程比較復雜,jdk1,4開始支持
-
AIO (Asynchronous I/O):服務器實現模式為一個有效請求一個線程,客戶端的IO請求都是由操作系統先完成了再通知服務器用其啟動線程進行處理。AIO方式適用於連接數目多且連接比較長(重操作)的架構,比如相冊服務器,充分調用OS參與並發操作,編程比較復雜,jdk1.7開始支持。
這些概念看着比較枯燥,可以從這個經典的燒開水的例子去理解
**BIO **:來到廚房,開始燒水NIO,並坐在水壺面前一直等着水燒開。
NIO:來到廚房,開AIO始燒水,但是我們不一直坐在水壺前面等,而是做些其他事,然后每隔幾分鍾到廚房看一下水有沒有燒開。
AIO:來到廚房,開始燒水,我們不一直坐在水壺前面等,而是在水壺上面裝個開關,水燒開之后它會通知我。
反射 ***
JAVA反射機制是在運行狀態中,對於任意一個類,都能夠知道這個類的所有屬性和方法;對於任意一個對象,都能夠調用它的任意一個方法和屬性;這種動態獲取的信息以及動態調用對象的方法的功能稱為java語言的反射機制。
Java獲取Class對象的三種方式
class Person {
public String name = "zhangsan";
public Person() {
}
}
public class Main{
public static void main(String[] args) throws ClassNotFoundException {
//方式1
Person p1 = new Person();
Class c1 = p1.getClass();
//方式2
Class c2 = Person.class;
//方式3可能會拋出ClassNotFoundException異常
Class c3 = Class.forName("com.company");
}
}
因為在一個類在 JVM 中只會有一個 Class
實例,所以對c1
、c2
、c3
進行equals
比較時返回的都是true
。
反射優缺點:
- 優點:運行期類型的判斷,動態加載類,提高代碼靈活度。
- 缺點:性能比直接的java代碼要慢很多。
反射應用場景:
- Java的很多框架都用到了反射,例如
Spring
中的xml的配置模式等 - 動態代理設計模式也采用了反射機制
JAVA異常 ***
異常主要分為Error
和Exception
兩種
- Error:
Error
類以及他的子類的實例,代表了JVM本身的錯誤。錯誤不能被程序員通過代碼處理。 - EXception:
Exception
以及他的子類,代表程序運行時發送的各種不期望發生的事件。可以被Java異常處理機制使用,是異常處理的核心。
異常框圖
除了以上的分類,異常還能分為非檢查異常和檢查異常
- 非檢查異常(unckecked exception):該類異常包括運行時異常(RuntimeException極其子類)和錯誤(Error)。編譯器不會進行檢查並且不要求必須處理的異常,也就說當程序中出現此類異常時,即使我們沒有
try-catch
捕獲它,也沒有使用throws
拋出該異常,編譯也會正常通過。因為這樣的異常發生的原因很可能是代碼寫的有問題。 - 檢查異常(checked exception):除了
Error
和RuntimeException
的其它異常。這是編譯器要求必須處理的異常。這樣的異常一般是由程序的運行環境導致的。因為程序可能被運行在各種未知的環境下,而程序員無法干預用戶如何使用他編寫的程序,所以必須處理這些異常。
下面來看下
try{}catch(){}finally{}
和return
之間的“恩恩怨怨”,這里有些亂,面試時問的也不是很多,實在記不住就算啦。還是先看代碼猜結果。
public class Main{
public static void main(String[] args) {
int a = test1();
System.out.println(a);
int b = test2();
System.out.println(b);
int c = test3();
System.out.println(c);
int d = test4();
System.out.println(d);
int e = test5();
System.out.println(e);
}
public static int test1(){
int a = 1;
try{
a = 2;
return a;
}catch(Exception e){
System.out.println("hello,test1");
a = 3;
}finally{
a = 4;
}
return a;
}
//輸出 2
public static int test2(){
int a = 1;
try{
a = 2;
return a;
}catch(Exception e){
System.out.println("hello,test2");
a = 3;
return a;
}finally{
a = 4;
}
}
//輸出 2
public static int test3(){
int a = 1;
try{
a = 2/0;
return a;
}catch(Exception e){
System.out.println("hello,test3");
a = 3;
}finally{
a = 4;
}
return a;
}
//輸出 hello,test3
// 4
public static int test4(){
int a = 1;
try{
a = 2/0;
return a;
}catch(Exception e){
System.out.println("hello,test4");
a = 3;
return a;
}finally{
a = 4;
}
}
//輸出 hello,test4
// 3
public static int test5(){
int a = 1;
try{
a = 2/0;
return a;
}catch(Exception e){
a = 3;
return a;
}finally{
a = 4;
return a;
}
}
//輸出 4
}
如果沒有仔細的研究過,應該好多會猜錯,下面總結下規律。
- 從前三個例子可以看出如果
try{}
中的代碼沒有異常,catch(){}
代碼塊中的代碼不會執行。所以如果try{}
和catch(){}
都含有return
時,無異常執行try{}
中的return
,存在異常執行catch(){}
的return
。 - 不管任何情況,就算
try{}
或catch(){}
中含有return
,finally{}
中的代碼一定會執行,那么為什么test1
、test2
、test3
中的結果不是4呢,因為雖然finally
是在return
后面的表達式運算之后執行的,但此時並沒有返回運算之后的值,而是把值保存起來,不管finally
對該值做任何的改變,返回的值都不會改變,依然返回保存起來的值。也就是說方法的返回值是在finally
運算之前就確定了的。 - 如果
return
的數據是引用數據類型,而在finally
中對該引用數據類型的屬性值的改變起作用,try
中的return
語句返回的就是在finally
中改變后的該屬性的值。這個不理解可以看看上面提到的Java的值傳遞的問題。 - 如果
finally{}
中含有return
,會導致程序提前退出,不在執行try{}
或catch(){}
中的return
。所以test5
返回的值是4。
最后總計一下try{}catch(){}finally{}
的執行順序。
- 先執行
try
中的語句,包括return
后面的表達式; - 有異常時,執行
catch
中的語句,包括return
后面的表達式,無異常跳過catch
語句; - 然后執行
finally
中的語句,如果finally
里面有return
語句,執行return
語句,程序結束; finally{}
中沒有return
時,無異常執行try
中的return
,如果有異常時則執行catch
中的return
。前兩步執行的return
只是確定返回的值,程序並未結束,finally{}
執行之后,最后將前兩步確定的return
的返回值返回。
JAVA注解 **
面試問的不多,但是在使用框架開發時會經常使用,但東西太多了,這里只是簡單介紹下概念。
Annotation
注解可以看成是java中的一種標記記號,用來給java中的類,成員,方法,參數等任何程序元素添加一些額外的說明信息,同時不改變程序語義。注解可以分為三類:基本注解,元注解,自定義注解
-
標准注解
- @Deprecated:該注解用來說明程序中的某個元素(類,方法,成員變量等)已經不再使用,如果使用的話的編譯器會給出警告。
- @SuppressWarnings(value=“”):用來抑制各種可能出現的警告。
- @Override:用來說明子類方法覆蓋了父類的方法,保護覆蓋方法的正確使用
-
元注解(元注解也稱為元數據注解,是對注解進行標注的注解,元注解更像是一種對注解的規范說明,用來對定義的注解進行行為的限定。例如說明注解的生存周期,注解的作用范圍等)
- @Target(value=“ ”):該注解是用來限制注解的使用范圍的,即該注解可以用於哪些程序元素。
- @Retention(value=“ ”):用於說明注解的生存周期
- @Documnent:用來說明指定被修飾的注解可以被javadoc.exe工具提取進入文檔中,所有使用了該注解進行標注的類在生成API文檔時都在包含該注解的說明。
- @Inherited:用來說明使用了該注解的父類,其子類會自動繼承該注解。
- @Repeatable:java1.8新出的元注解,如果需要在給程序元素使用相同類型的注解,則需將該注解標注上。
-
自定義注解:用@Interface來聲明注解。
JAVA泛型 ***
Java 泛型是 JDK 5 中引入的一個新特性, 泛型提供了編譯時類型安全檢測機制,該機制允許程序員在編譯時檢測到非法的類型。泛型的本質是參數化類型,也就是說所操作的數據類型被指定為一個參數。
-
泛型擦除(這是面試考察泛型時經常問到的問題)
Java的泛型基本上都是在編譯器這個層次上實現的,在生成的字節碼中是不包含泛型中的類型信息的,使用泛型的時候加上類型參數,在編譯器編譯的時候會去掉,這個過程成為類型擦除。看下面代碼
public class Main{ public static void main(String[] args) { ArrayList<Integer> arrayList1 = new ArrayList<>(); ArrayList<String> arrayList2 = new ArrayList<>(); System.out.println(arrayList1.getClass() == arrayList2.getClass()); } }
輸出結果
true
可以看到
ArrayList<Integer>
和ArrayList<String>
的原始類型是相同,在編譯成字節碼文件后都會變成List
,JVM看到的只有List
,看不到泛型信息,這就是泛型的類型擦除。在看下面這段代碼public class Main{ public static void main(String[] args) throws Exception { ArrayList<Integer> arrayList = new ArrayList<>(); arrayList.add(1); arrayList.getClass().getMethod("add", Object.class).invoke(arrayList, "a"); System.out.println(arrayList.get(0)); System.out.println(arrayList.get(1)); } }
輸出
1 a
可以看到通過反射進行
add
操作,ArrayList<Integer>
竟然可以存儲字符串,這是因為在反射就是在運行期調用的add
方法,在運行期泛型信息已經被擦除。 -
既然存在類型擦除,那么Java是如何保證在
ArrayList<Integer>
添加字符串會報錯呢?Java編譯器是通過先檢查代碼中泛型的類型,然后在進行類型擦除,再進行編譯。
JAVA序列化 **
-
序列化:將對象寫入到IO流中
-
反序列化:從IO流中恢復對象
-
序列化的意義:將Java對象轉換成字節序列,這些字節序列更加便於通過網絡傳輸或存儲在磁盤上,在需要時可以通過反序列化恢復成原來的對象。
-
實現方式:
- 實現Serializable接口
- 實現Externalizable接口
-
序列化的注意事項:
- 對象的類名、實例變量會被序列化;方法、類變量、
transient
實例變量都不會被序列化。 - 某個變量不被序列化,可以使用
transient
修飾。 - 序列化對象的引用類型成員變量,也必須是可序列化的,否則,會報錯。
- 反序列化時必須有序列化對象的
class
文件。
- 對象的類名、實例變量會被序列化;方法、類變量、
深拷貝與淺拷貝 ***
-
深拷貝:對基本數據類型進行值傳遞,對引用數據類型,創建一個新的對象,並復制其內容,兩個引用指向兩個對象,但對象內容相同。
-
淺拷貝:對基本數據類型進行值傳遞,對引用數據類型復制一個引用指向原始引用的對象,就是復制的引用和原始引用指向同一個對象。
具體區別看下圖
-
深拷貝的實現方式
-
重載
clone
方法public class Main{ public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, CloneNotSupportedException { Address s = new Address("天津"); Person p = new Person("張三", 23, s); System.out.println("克隆前的地址:" + p.getAddress().getName()); Person cloneP = (Person) p.clone(); cloneP.getAddress().setName("北京"); System.out.println("克隆后的地址:" + cloneP.getAddress().getName()); } } class Address implements Cloneable { private String city; public Address(String name){ this.city = name; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } public String getName() { return city; } public void setName(String name) { this.city = name; } } class Person implements Cloneable{ private String name; private int age; private Address address; public Person(String name, int age, Address address){ this.name = name; this.age = age; this.address = address; } @Override public Object clone() throws CloneNotSupportedException { Person person = (Person) super.clone(); person.address = (Address)address.clone(); return person; } 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; } public Address getAddress() { return address; } public void setAddress(Address address) { this.address = address; } }
輸出
克隆前的地址:天津 克隆后的地址:北京
其實就是
Person
類和Address
類都要重寫clone
方法,這里面需要注意的一點是super.clone()
為淺克隆,所以在在Person
類中重寫clone
方法時,address
對象需要調用address.clone()
重新賦值,因為address
類型為引用類型。 -
序列化
public class Main{ public static void main(String[] args) throws IOException, ClassNotFoundException { Address s = new Address("天津"); Person p = new Person("張三", 23, s); System.out.println("克隆前的地址:" + p.getAddress().getName()); Person cloneP = (Person) p.deepClone(); cloneP.getAddress().setName("北京"); System.out.println("克隆后的地址:" + cloneP.getAddress().getName()); } } class Address implements Serializable{ private String city; public Address(String name){ this.city = name; } public String getName() { return city; } public void setName(String name) { this.city = name; } } class Person implements Serializable{ private String name; private int age; private Address address; public Person(String name, int age, Address address){ this.name = name; this.age = age; this.address = address; } public Object deepClone() throws IOException, ClassNotFoundException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(this); ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bis); return ois.readObject(); } 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; } public Address getAddress() { return address; } public void setAddress(Address address) { this.address = address; } }
輸出
克隆前的地址:天津 克隆后的地址:北京
-
常見的Object方法 ***
這些方法都很重要,面試經常會問到,要結合其他知識將這些方法理解透徹
Object clone()
:創建與該對象的類相同的新對象boolean equals(Object)
:比較兩對象是否相等void finalize()
:當垃圾回收器確定不存在對該對象的更多引用時,對象垃圾回收器調用該方法Class getClass()
:返回一個對象運行時的實例類int hashCode()
:返回該對象的散列碼值void notify()
:喚醒等待在該對象的監視器上的一個線程void notifyAll()
:喚醒等待在該對象的監視器上的全部線程String toString()
:返回該對象的字符串表示void wait()
:在其他線程調用此對象的notify()
方法或notifyAll()
方法前,導致當前線程等待