目錄剛剛經歷過秋招,看了大量的面經,順便將常見的Java常考知識點總結了一下,并根據被問到的頻率大致做了一個標注,一顆星表示知識點需要了解,被問到的頻率不高,面試時起碼能說個差不多,兩顆星表示被問到的頻率較高或對理解Java有著重要的作用,建議熟練掌握,三顆星表示被問到的頻率非常高,建議深入理解并熟練掌握其相關知識,方便面試時拓展(方便裝逼),給面試官留下個好印象,
- JVM、JRE及JDK的關系 **
- JAVA語言特點 **
- JAVA和C++的區別 **
- Java的基本資料型別 **
- 隱式(自動)型別轉換和顯示(強制)型別轉換 **
- 自動裝箱與拆箱 **
- String(不是基本資料型別)
- String的不可變性 ***
- 字符型常量和字串常量的區別 *
- 什么是字串常量池?*
- String 類的常用方法都有那些?**
- String和StringBuffer、StringBuilder的區別是什么?***
- switch 是否能作用在 byte 上,是否能作用在 long 上,是否能作用在 String 上 *
- Java語言采用何種編碼方案?有何特點?*
- 訪問修飾符 **
- 運算子 *
- 關鍵字
- static關鍵字 ***
- final 關鍵字 ***
- final finally finalize區別 ***
- this關鍵字 **
- super關鍵字 **
- this與super的區別 **
- break ,continue ,return 的區別及作用 **
- 面向物件和面向程序的區別 **
- 面向物件三大特性(封裝、繼承、多型) ***
- 面向物件五大基本原則是什么 **
- 抽象類和介面的對比 ***
- 在Java中定義一個不做事且沒有引數的構造方法的作用 *
- 在呼叫子類構造方法之前會先呼叫父類沒有引數的構造方法,其目的是 *
- 一個類的構造方法的作用是什么?若一個類沒有宣告構造方法,改程式能正確執行嗎?為什么? *
- 構造方法有哪些特性? **
- 變數 **
- 內部類 **
- 重寫與多載 ***
- 多載和重寫的區別
- 構造器(constructor)是否可被重寫(override)
- 多載的方法能否根據回傳型別進行區分?為什么?
- == 和 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 = https://www.cnblogs.com/zydybaby/archive/2020/11/23/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[];
}
為什么String`要設計成不可變的呢(不可變性的好處):
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和==的作用相同,比較的是物件的地址值,
-
hashCodehashCode方法回傳物件的散列碼,回傳值是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=https://www.cnblogs.com/zydybaby/archive/2020/11/23/“”):用來抑制各種可能出現的警告,
- @Override:用來說明子類方法覆寫了父類的方法,保護覆寫方法的正確使用
-
元注解(元注解也稱為元資料注解,是對注解進行標注的注解,元注解更像是一種對注解的規范說明,用來對定義的注解進行行為的限定,例如說明注解的生存周期,注解的作用范圍等)
- @Target(value=https://www.cnblogs.com/zydybaby/archive/2020/11/23/“ ”):該注解是用來限制注解的使用范圍的,即該注解可以用于哪些程式元素,
- @Retention(value=https://www.cnblogs.com/zydybaby/archive/2020/11/23/“ ”):用于說明注解的生存周期
- @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()方法前,導致當前執行緒等待
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/226505.html
標籤:其他
下一篇:前端-CSS
