面霸篇,從面試題作為切入點提升大家的 Java 内功,所謂根基不牢,地動山搖。隻有紮實的基礎,才是寫出寫好代碼。
拒絕知識碎片化
碼哥在 《Redis 系列》的開篇 Redis 為什麼這麼快中說過:學習一個技術,通常隻接觸了零散的技術點,沒有在腦海裡建立一個完整的知識架構和架構體系,沒有系統觀。這樣會很吃力,而且會出現一看好像自己會,過後就忘記,一臉懵逼。
我們需要一個系統觀,清晰完整的去學習技術,同時也不能埋頭苦幹,過于死磕某個細節。
系統觀其實是至關重要的,從某種程度上說,在解決問題時,擁有了系統觀,就意味着你能有依據、有章法地定位和解決問題。
跟着「碼哥」一起來提綱挈領,梳理一個相對完整的 Java 開發技術能力圖譜,将基礎夯實。
點選下方關注我
目錄
- Java 平台的了解
- JVM、JRE、JDK關系
- Java 是解釋執行麼?
- 采用位元組碼的好處
- 基礎文法
- JDK 1.8 之後有哪些新特性
- 構造器是否可以重寫
- wait() 和 sleep 差別
- &和&&的差別
- Java 有哪些資料類型?
- this 關鍵字的用法
- super 關鍵字的用法
- 成員變量與局部變量的差別有哪些
- 動态代理是基于什麼原理
- int 與 Integer 差別
- 面向對象
- 面向對象四大特性
- 什麼是多态機制?
- Java語言是如何實作多态的?
- 重載與重寫
- == 和 equals 的差別是什麼
- 為什麼要有 hashcode
- 面向對象的基本原則
- 說下 Exception 與 Error 差別?
- Exception
- Error
- JVM 如何處理異常?
- NoClassDefFoundError 和 ClassNotFoundException 差別?
- Java 常見異常有哪些?
- final、finally、finalize 有什麼差別?
- 強引用、軟引用、弱引用、虛引用
- String、StringBuilder、StringBuffer 有什麼差別?
- String
- StringBuilder
- StringBuffer
- HashMap 使用 String 作為 key有什麼好處
- 接口和抽象類有什麼差別?
- 接口
- 抽象類
- 值傳遞
- 基本資料類型
- 對象引用類型
- 值傳遞和引用傳遞有什麼差別?
Java 平台的了解
碼老濕,你是怎麼了解 Java 平台呢?
Java 是一種面向對象的語言,有兩個明顯特性:
- 跨平台能力:一次編寫,到處運作(Write once,run anywhere);
- 垃圾收集:
Java 通過位元組碼和 Java 虛拟機(JVM)這種跨平台的抽象,屏蔽了作業系統和硬體的細節,這也是實作「一次編譯,到處執行」的基礎。
Java 通過垃圾收集器(Garbage Collector)回收配置設定記憶體,大部分情況下,程式員不需要自己操心記憶體的配置設定和回收。
最常見的垃圾收集器,如 SerialGC、Parallel GC、 CMS、 G1 等,對于适用于什麼樣的工作負載最好也心裡有數。
JVM、JRE、JDK關系
碼老濕,能說下 JVM、JRE 和 JDK 的關系麼?
JVM Java Virtual Machine
是 Java 虛拟機,Java 程式需要運作在虛拟機上,不同的平台有自己的虛拟機,是以Java語言可以實作跨平台。
JRE Java Runtime Environment
包括 Java 虛拟機和 Java 程式所需的核心類庫等。
核心類庫主要是
java.lang
包:包含了運作Java程式必不可少的系統類,如基本資料類型、基本數學函數、字元串處理、線程、異常處理類等,系統預設加載這個包
如果想要運作一個開發好的 Java 程式,計算機中隻需要安裝 JRE 即可。
JDK Java Development Kit
是提供給 Java 開發人員使用的,其中包含了Java 的開發工具,也包括了JRE。
是以安裝了JDK,就無需再單獨安裝JRE了。其中的開發工具:編譯工具(javac.exe),打包工具(jar.exe) 等。
Java 是解釋執行麼?
碼老濕,Java 是解釋執行的麼?
這個說法不太準确。
我們開發的 Java 的源代碼,首先通過 Javac 編譯成為位元組碼(bytecode),在運作時,通過 Java 虛拟機(JVM)内嵌的解釋器将位元組碼轉換成為最終的機器碼。
但是常見的 JVM,比如我們大多數情況使用的 Oracle JDK 提供的 Hotspot JVM,都提供了
JIT(Just-In-Time)
編譯器。
也就是通常說的動态編譯器,JIT 能夠在運作時将熱點代碼編譯成機器碼,這種情況下部分熱點代碼就屬于編譯執行,而不是解釋執行了。
采用位元組碼的好處
什麼是位元組碼?采用位元組碼的好處是什麼?
位元組碼:Java源代碼經過虛拟機編譯器編譯後産生的檔案(即擴充為.class的檔案),它不面向任何特定的處理器,隻面向虛拟機。
采用位元組碼的好處:
衆所周知,我們通常把 Java 分為編譯期和運作時。這裡說的 Java 的編譯和 C/C++ 是有着不同的意義的,Javac 的編譯,編譯 Java 源碼生成“.class”檔案裡面實際是位元組碼,而不是可以直接執行的機器碼。Java 通過位元組碼和 Java 虛拟機(JVM)這種跨平台的抽象,屏蔽了作業系統和硬體的細節,這也是實作“一次編譯,到處執行”的基礎。
基礎文法
JDK 1.8 之後有哪些新特性
接口預設方法:Java8允許我們給接口添加一個非抽象的方法實作,隻需要使用default關鍵字即可。
Lambda表達式和函數式接口:Lambda表達式本質上是一段匿名内部類,也可以是一段可以傳遞的代碼。
Lambda允許把函數作為一個方法的參數(函數作為參數傳遞到方法中),使用Lambda表達式使代碼更加簡潔,但是也不要濫用,否則會有可讀性等問題,《EffectiveJava》作者JoshBloch建議使用Lambda表達式最好不要超過3行。
StreamAPI:用函數式程式設計方式在集合類上進行複雜操作的工具,配合Lambda表達式可以友善的對集合進行處理。
Java8中處理集合的關鍵抽象概念,它可以指定你希望對集合進行的操作,可以執行非常複雜的查找、過濾和映射資料等操作。
使用StreamAPI對集合資料進行操作,就類似于使用SQL執行的資料庫查詢。也可以使用StreamAPI來并行執行操作。
簡而言之,StreamAPI提供了一種高效且易于使用的處理資料的方式。
方法引用:方法引用提供了非常有用的文法,可以直接引用已有Java類或對象(執行個體)的方法或構造器。
與lambda聯合使用,方法引用可以使語言的構造更緊湊簡潔,減少備援代碼。
日期時間API:Java8引入了新的日期時間API改進了日期時間的管理。Optional類:著名的NullPointerException是引起系統失敗最常見的原因。
很久以前GoogleGuava項目引入了Optional作為解決空指針異常的一種方式,不贊成代碼被null檢查的代碼污染,期望程式員寫整潔的代碼。
受GoogleGuava的鼓勵,Optional現在是Java8庫的一部分。
新工具:新的編譯工具,如:Nashorn引擎jjs、類依賴分析器jdeps。
構造器是否可以重寫
Constructor不能被override(重寫),但是可以overload(重載),是以你可以看到⼀個類中有多個構造函數的情況。
wait() 和 sleep 差別
來源不同:sleep()來自Thread類,wait()來自Object類。
對于同步鎖的影響不同:sleep()不會該表同步鎖的行為,如果目前線程持有同步鎖,那麼sleep是不會讓線程釋放同步鎖的。
wait()會釋放同步鎖,讓其他線程進入synchronized代碼塊執行。
使用範圍不同:sleep()可以在任何地方使用。wait()隻能在同步控制方法或者同步控制塊裡面使用,否則會抛IllegalMonitorStateException。
恢複方式不同:兩者會暫停目前線程,但是在恢複上不太一樣。sleep()在時間到了之後會重新恢複;
wait()則需要其他線程調用同一對象的notify()/nofityAll()才能重新恢複。
&和&&的差別
&
運算符有兩種用法:
- 按位與;
- 邏輯與。
&&
運算符是短路與運算。邏輯與跟短路與的差别是非常巨大的,雖然二者都要求運算符左右兩端的布爾值都是true 整個表達式的值才是 true。
&&
之是以稱為短路運算,是因為如果&&左邊的表達式的值是 false,右邊的表達式會被直接短路掉,不會進行運算。
注意:邏輯或運算符(|)和短路或運算符(||)的差别也是如此。
Java 有哪些資料類型?
Java語言是強類型語言,對于每一種資料都定義了明确的具體的資料類型,在記憶體中配置設定了不同大小的記憶體空間。
分類
- 基本資料類型
- 數值型
- 整數類型(byte,short,int,long)
- 浮點類型(float,double)
- 字元型(char)
- 布爾型(boolean)
- 數值型
- 引用資料類型
- 類(class)
- 接口(interface)
- 數組([])
this 關鍵字的用法
this是自身的一個對象,代表對象本身,可以了解為:指向對象本身的一個指針。
this的用法在java中大體可以分為3種:
- 普通的直接引用,this相當于是指向目前對象本身。
- 形參與成員名字重名,用this來區分:
public Person(String name, int age) {
this.name = name;
this.age = age;
}
- 引用本類的構造函數
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;
}
}
super 關鍵字的用法
super可以了解為是指向自己超(父)類對象的一個指針,而這個超類指的是離自己最近的一個父類。
super也有三種用法:
- 普通的直接引用:與this類似,super相當于是指向目前對象的父類的引用,這樣就可以用super.xxx來引用父類的成員。
- 子類中的成員變量或方法與父類中的成員變量或方法同名時,用super進行區分
lass Person{ protected String name; public Person(String name) { this.name = name; } } class Student extends Person{ private String name; public Student(String name, String name1) { super(name); this.name = name1; } public void getInfo(){ System.out.println(this.name); //Child System.out.println(super.name); //Father } } public class Test { public static void main(String[] args) { Student s1 = new Student("Father","Child"); s1.getInfo(); } }
- 引用父類構造函數;
成員變量與局部變量的差別有哪些
變量:在程式執行的過程中,在某個範圍内其值可以發生改變的量。從本質上講,變量其實是記憶體中的一小塊區域。
成員變量:方法外部,類内部定義的變量。
局部變量:類的方法中的變量。
差別如下:
作用域
成員變量:針對整個類有效。 局部變量:隻在某個範圍内有效。(一般指的就是方法,語句體内)
存儲位置
成員變量:随着對象的建立而存在,随着對象的消失而消失,存儲在堆記憶體中。
局部變量:在方法被調用,或者語句被執行的時候存在,存儲在棧記憶體中。當方法調用完,或者語句結束後,就自動釋放。
生命周期
成員變量:随着對象的建立而存在,随着對象的消失而消失 局部變量:當方法調用完,或者語句結束後,就自動釋放。
初始值
成員變量:有預設初始值。
局部變量:沒有預設初始值,使用前必須指派。
動态代理是基于什麼原理
基于反射實作
反射機制是 Java 語言提供的一種基礎功能,賦予程式在運作時自省(introspect,官方用語)的能力。通過反射我們可以直接操作類或者對象,比如擷取某個對象的類定義,擷取類聲明的屬性和方法,調用方法或者構造對象,甚至可以運作時修改類定義。
碼老濕,他的使用場景是什麼?
AOP 通過(動态)代理機制可以讓開發者從這些繁瑣事項中抽身出來,大幅度提高了代碼的抽象程度和複用度。
包裝 RPC 調用:通過代理可以讓調用者與實作者之間解耦。比如進行 RPC 調用,架構内部的尋址、序列化、反序列化等,對于調用者往往是沒有太大意義的,通過代理,可以提供更加友善的界面。
int 與 Integer 差別
Java 是一個近乎純潔的面向對象程式設計語言,但是為了程式設計的友善還是引入了基本資料類型,但是為了能夠将這些基本資料類型當成對象操作,Java 為每一個基本資料類型都引入了對應的包裝類型(wrapper class),int 的包裝類就是 Integer,從 Java 5 開始引入了自動裝箱/拆箱機制,使得二者可以互相轉換。
Java 為每個原始類型提供了包裝類型:
- 原始類型: boolean,char,byte,short,int,long,float,double。
- 包裝類型:Boolean,Character,Byte,Short,Integer,Long,Float,Double。
int 是我們常說的整形數字,是 Java 的 8 個原始資料類型(Primitive Types,boolean、byte 、short、char、int、float、double、long)之一。Java 語言雖然号稱一切都是對象,但原始資料類型是例外。
Integer 是 int 對應的包裝類,它有一個 int 類型的字段存儲資料,并且提供了基本操作,比如數學運算、int 和字元串之間轉換等。在 Java 5 中,引入了自動裝箱和自動拆箱功能(boxing/unboxing),Java 可以根據上下文,自動進行轉換,極大地簡化了相關程式設計。
Integer a= 127 與 Integer b = 127相等嗎
對于對象引用類型:比較的是對象的記憶體位址。 對于基本資料類型:比較的是值。
大部分資料操作都是集中在有限的、較小的數值範圍,因而,在 Java 5 中新增了靜态工廠方法 valueOf,在調用它的時候會利用一個緩存機制,帶來了明顯的性能改進。按照 Javadoc,這個值預設緩存是 -128 到 127 之間。
如果整型字面量的值在-128到127之間,那麼自動裝箱時不會new新的Integer對象,而是直接引用常量池中的Integer對象,超過範圍 a1==b1的結果是false。
public static void main(String[] args) {
Integer a = new Integer(3);
Integer b = 3; // 将3自動裝箱成Integer類型
int c = 3;
System.out.println(a == b); // false 兩個引用沒有引用同一對象
System.out.println(a == c); // true a自動拆箱成int類型再和c比較
System.out.println(b == c); // true
Integer a1 = 128;
Integer b1 = 128;
System.out.println(a1 == b1); // false
Integer a2 = 127;
Integer b2 = 127;
System.out.println(a2 == b2); // true
}
面向對象
面向對象與面向過程的差別是什麼?
面向過程
優點:性能比面向對象高,因為類調用時需要執行個體化,開銷比較大,比較消耗資源;比如單片機、嵌入式開發、Linux/Unix等一般采用面向過程開發,性能是最重要的因素。
缺點:沒有面向對象易維護、易複用、易擴充
面向對象
優點:易維護、易複用、易擴充,由于面向對象有封裝、繼承、多态性的特性,可以設計出低耦合的系統,使系統更加靈活、更加易于維護
缺點:性能比面向過程低
面向過程是具體化的,流程化的,解決一個問題,你需要一步一步的分析,一步一步的實作。
面向對象是模型化的,你隻需抽象出一個類,這是一個封閉的盒子,在這裡你擁有資料也擁有解決問題的方法。需要什麼功能直接使用就可以了,不必去一步一步的實作,至于這個功能是如何實作的,管我們什麼事?我們會用就可以了。
面向對象的底層其實還是面向過程,把面向過程抽象成類,然後封裝,友善我們使用的就是面向對象了。
面向對象程式設計因為其具有豐富的特性(封裝、抽象、繼承、多态),可以實作很多複雜的設計思路,是很多設計原則、設計模式等編碼實作的基礎。
面向對象四大特性
碼老濕,如何了解面向對象的四大特性?
抽象
抽象是将一類對象的共同特征總結出來構造類的過程,包括資料抽象和行為抽象兩方面。抽象隻關注對象有哪些屬性和行為,并不關注這些行為的細節是什麼。
另外,抽象是一個寬泛的設計思想,開發者能不能設計好代碼,抽象能力也至關重要。
很多設計原則都展現了抽象這種設計思想,比如基于接口而非實作程式設計、開閉原則(對擴充開放、對修改關閉)、代碼解耦(降低代碼的耦合性)等。
在面對複雜系統的時候,人腦能承受的資訊複雜程度是有限的,是以我們必須忽略掉一些非關鍵性的實作細節。
封裝
把一個對象的屬性私有化,同時提供一些可以被外界通路的屬性的方法,如果屬性不想被外界通路,我們大可不必提供方法給外界通路。
通過封裝,隻需要暴露必要的方法給調用者,調用者不必了解背後的業務細節,用錯的機率就減少。
繼承
使用已存在的類的定義作為基礎建立新類的技術,新類的定義可以增加新的資料或新的功能,也可以用父類的功能,但不能選擇性地繼承父類。
通過使用繼承我們能夠非常友善地複用以前的代碼,需要注意的是,過度使用繼承,層級深就會導緻代碼可讀性和可維護性變差。
關于繼承如下 3 點請記住:
- 子類擁有父類非 private 的屬性和方法。
- 子類可以擁有自己屬性和方法,即子類可以對父類進行擴充。
- 子類可以用自己的方式實作父類的方法。(以後介紹)。
多态
所謂多态就是指程式中定義的引用變量所指向的具體類型和通過該引用變量發出的方法調用在程式設計時并不确定,而是在程式運作期間才确定。
即一個引用變量到底會指向哪個類的執行個體對象,該引用變量發出的方法調用到底是哪個類中實作的方法,必須在由程式運作期間才能決定。
在Java中有兩種形式可以實作多态:繼承(多個子類對同一方法的重寫)和接口(實作接口并覆寫接口中同一方法)。
多态也是很多設計模式、設計原則、程式設計技巧的代碼實作基礎,比如政策模式、基于接口而非實作程式設計、依賴倒置原則、裡式替換原則、利用多态去掉冗長的 if-else 語句等等。
什麼是多态機制?
所謂多态就是指程式中定義的引用變量所指向的具體類型和通過該引用變量發出的方法調用在程式設計時并不确定,而是在程式運作期間才确定,即一個引用變量倒底會指向哪個類的執行個體對象,該引用變量發出的方法調用到底是哪個類中實作的方法,必須在由程式運作期間才能決定。
因為在程式運作時才确定具體的類,這樣,不用修改源程式代碼,就可以讓引用變量綁定到各種不同的類實作上,進而導緻該引用調用的具體方法随之改變,即不修改程式代碼就可以改變程式運作時所綁定的具體代碼,讓程式可以選擇多個運作狀态,這就是多态性。
多态分為編譯時多态和運作時多态。
其中編輯時多态是靜态的,主要是指方法的重載,它是根據參數清單的不同來區分不同的函數,通過編輯之後會變成兩個不同的函數,在運作時談不上多态。
而運作時多态是動态的,它是通過動态綁定來實作的,也就是我們所說的多态性。
Java語言是如何實作多态的?
Java實作多态有三個必要條件:繼承、重寫、向上轉型。
繼承:在多态中必須存在有繼承關系的子類和父類。
重寫:子類對父類中某些方法進行重新定義,在調用這些方法時就會調用子類的方法。
向上轉型:在多态中需要将子類的引用賦給父類對象,隻有這樣該引用才能夠具備技能調用父類的方法和子類的方法。
隻有滿足了上述三個條件,我們才能夠在同一個繼承結構中使用統一的邏輯實作代碼處理不同的對象,進而達到執行不同的行為。
重載與重寫
方法的重載和重寫都是實作多态的方式,差別在于前者實作的是編譯時的多态性,而後者實作的是運作時的多态性。
重載:發生在同一個類中,方法名相同參數清單不同(參數類型不同、個數不同、順序不同),與方法傳回值和通路修飾符無關,即重載的方法不能根據傳回類型進行區分。
重寫:發生在父子類中,方法名、參數清單必須相同,傳回值小于等于父類,抛出的異常小于等于父類,通路修飾符大于等于父類(裡氏代換原則);如果父類方法通路修飾符為private則子類中就不是重寫。
== 和 equals 的差別是什麼
== : 它的作用是判斷兩個對象的位址是不是相等。即,判斷兩個對象是不是同一個對象。(基本資料類型 == 比較的是值,引用資料類型 == 比較的是記憶體位址)。
equals() : 它的作用也是判斷兩個對象是否相等。但它一般有兩種使用情況:
- 類沒有覆寫 equals() 方法。則通過 equals() 比較該類的兩個對象時,等價于通過“==”比較這兩個對象。
- 類覆寫了 equals() 方法。一般,我們都覆寫 equals() 方法來兩個對象的内容相等;若它們的内容相等,則傳回 true (即,認為這兩個對象相等)。
為什麼重寫equals時必須重寫hashCode方法?
如果兩個對象相等,則hashcode一定也是相同的
兩個對象相等,對兩個對象分别調用equals方法都傳回true
兩個對象有相同的hashcode值,它們也不一定是相等的.
是以,equals 方法被覆寫過,則 hashCode 方法也必須被覆寫
為什麼要有 hashcode
我們以“HashSet 如何檢查重複”為例子來說明為什麼要有 hashCode:
當你把對象加入 HashSet 時,HashSet 會先計算對象的 hashcode 值來判斷對象加入的位置,同時也會與其他已經加入的對象的 hashcode 值作比較,如果沒有相符的hashcode,HashSet會假設對象沒有重複出現。
但是如果發現有相同 hashcode 值的對象,這時會調用 equals()方法來檢查 hashcode 相等的對象是否真的相同。
如果兩者相同,HashSet 就不會讓其加入操作成功。
如果不同的話,就會重新散列到其他位置。這樣我們就大大減少了 equals 的次數,相應就大大提高了執行速度。
面向對象的基本原則
碼老濕,什麼是 SOLID?
這是面向對象程式設計的一種設計原則,對于每一種設計原則,我們需要掌握它的設計初衷,能解決哪些程式設計問題,有哪些應用場景。
- 單一職責原則 SRP(Single Responsibility Principle) 類的功能要單一,不能包羅萬象,跟雜貨鋪似的。
- 開放封閉原則 OCP(Open-Close Principle) 一個子產品對于拓展是開放的,對于修改是封閉的,想要增加功能熱烈歡迎,想要修改,哼,一萬個不樂意。
- 裡式替換原則 LSP(the Liskov Substitution Principle LSP) 子類可以替換父類出現在父類能夠出現的任何地方。比如你能代表你爸去你外婆家幹活。哈哈~~(其實多态就是一種這個原則的一種實作)。
- 接口分離原則ISP(the Interface Segregation Principle ISP) 設計時采用多個與特定客戶類有關的接口比采用一個通用的接口要好。就比如一個手機擁有打電話,看視訊,玩遊戲等功能,把這幾個功能拆分成不同的接口,比在一個接口裡要好的多。
- 依賴倒置原則DIP(the Dependency Inversion Principle DIP) :高層子產品(high-level modules)不要依賴低層子產品(low-level)。高層子產品和低層子產品應該通過抽象(abstractions)來互相依賴。除此之外,抽象(abstractions)不要依賴具體實作細節(details),具體實作細節(details)依賴抽象(abstractions)。
- 抽象不應該依賴于具體實作,具體實作應該依賴于抽象。就是你出國要說你是中國人,而不能說你是哪個村子的。
- 比如說中國人是抽象的,下面有具體的xx省,xx市,xx縣。你要依賴的抽象是中國人,而不是你是xx村的。
- 所謂高層子產品和低層子產品的劃分,簡單來說就是,在調用鍊上,調用者屬于高層,被調用者屬于低層。
- Tomcat 就是高層子產品,我們編寫的 Web 應用程式代碼就是低層子產品。Tomcat 和應用程式代碼之間并沒有直接的依賴關系,兩者都依賴同一個「抽象」,也就是 Servlet 規範。
- Servlet 規範不依賴具體的 Tomcat 容器和應用程式的實作細節,而 Tomcat 容器和應用程式依賴 Servlet 規範。
碼老濕,接口隔離與單一職責有什麼差別?
單一職責側重點是子產品、類、接口的設計思想。
接口隔離原則側重于接口設計,提供了一種判斷接口職責是否單一的标準。
說下 Exception 與 Error 差別?
碼老濕,他們的相同點是什麼呀?
Exception 和 Error 都是繼承了 Throwable 類,在 Java 中隻有 Throwable 類型的執行個體才可以被抛出(throw)或者捕獲(catch),它是異常處理機制的基本組成類型。
Exception 和 Error 展現了 Java 平台設計者對不同異常情況的分類。
異常使用規範:
- 盡量不要捕獲類似 Exception 這樣的通用異常,而是應該捕獲特定異常
- 不要生吞(swallow)異常。這是異常進行中要特别注意的事情,因為很可能會導緻非常難以診斷的詭異情況。
Exception
Exception 是程式正常運作中,可以預料的意外情況,可能并且應該被捕獲,進行相應處理。
就好比開車去洗三溫暖,前方道路施工,禁止通行。但是我們換條路就可以解決。
Exception 又分為可檢查(checked)異常和不檢查(unchecked)異常,可檢查異常在源代碼裡必須顯式地進行捕獲處理,這是編譯期檢查的一部分。
不檢查異常就是所謂的運作時異常,類似 NullPointerException、ArrayIndexOutOfBoundsException 之類,通常是可以編碼避免的邏輯錯誤,具體根據需要來判斷是否需要捕獲,并不會在編譯期強制要求。
Checked Exception 的假設是我們捕獲了異常,然後恢複程式。但是,其實我們大多數情況下,根本就不可能恢複。
Checked Exception 的使用,已經大大偏離了最初的設計目的。Checked Exception 不相容 functional 程式設計,如果你寫過 Lambda/Stream 代碼,相信深有體會。
Error
此類錯誤一般表示代碼運作時 JVM 出現問題。通常有 Virtual MachineError(虛拟機運作錯誤)、NoClassDefFoundError(類定義錯誤)等。
比如 OutOfMemoryError:記憶體不足錯誤;StackOverflowError:棧溢出錯誤。此類錯誤發生時,JVM 将終止線程。
絕大多數導緻程式不可恢複,這些錯誤是不受檢異常,非代碼性錯誤。是以,當此類錯誤發生時,應用程式不應該去處理此類錯誤。按照Java慣例,我們是不應該實作任何新的Error子類的!
比如開車去洗三溫暖,老王出車禍了。無法洗了,隻能去醫院。
JVM 如何處理異常?
在一個方法中如果發生異常,這個方法會建立一個異常對象,并轉交給 JVM,該異常對象包含異常名稱,異常描述以及異常發生時應用程式的狀态。
建立異常對象并轉交給 JVM 的過程稱為抛出異常。可能有一系列的方法調用,最終才進入抛出異常的方法,這一系列方法調用的有序清單叫做調用棧。
JVM 會順着調用棧去查找看是否有可以處理異常的代碼,如果有,則調用異常處理代碼。
當 JVM 發現可以處理異常的代碼時,會把發生的異常傳遞給它。如果 JVM 沒有找到可以處理該異常的代碼塊,JVM 就會将該異常轉交給預設的異常處理器(預設處理器為 JVM 的一部分),預設異常處理器列印出異常資訊并終止應用程式。
NoClassDefFoundError 和 ClassNotFoundException 差別?
NoClassDefFoundError 是一個 Error 類型的異常,是由 JVM 引起的,不應該嘗試捕獲這個異常。
引起該異常的原因是 JVM 或 ClassLoader 嘗試加載某類時在記憶體中找不到該類的定義,該動作發生在運作期間,即編譯時該類存在,但是在運作時卻找不到了,可能是變異後被删除了等原因導緻;
ClassNotFoundException 是一個受查異常,需要顯式地使用 try-catch 對其進行捕獲和處理,或在方法簽名中用 throws 關鍵字進行聲明。
當使用 Class.forName, ClassLoader.loadClass 或 ClassLoader.findSystemClass 動态加載類到記憶體的時候,通過傳入的類路徑參數沒有找到該類,就會抛出該異常;
另一種抛出該異常的可能原因是某個類已經由一個類加載器加載至記憶體中,另一個加載器又嘗試去加載它。
Java 常見異常有哪些?
java.lang.IllegalAccessError:違法通路錯誤。當一個應用試圖通路、修改某個類的域(Field)或者調用其方法,但是又違反域或方法的可見性聲明,則抛出該異常。
java.lang.InstantiationError:執行個體化錯誤。當一個應用試圖通過Java的new操作符構造一個抽象類或者接口時抛出該異常.
java.lang.OutOfMemoryError:記憶體不足錯誤。當可用記憶體不足以讓Java虛拟機配置設定給一個對象時抛出該錯誤。
java.lang.StackOverflowError:堆棧溢出錯誤。當一個應用遞歸調用的層次太深而導緻堆棧溢出或者陷入死循環時抛出該錯誤。
java.lang.ClassCastException:類造型異常。假設有類A和B(A不是B的父類或子類),O是A的執行個體,那麼當強制将O構造為類B的執行個體時抛出該異常。該異常經常被稱為強制類型轉換異常。
java.lang.ClassNotFoundException:找不到類異常。當應用試圖根據字元串形式的類名構造類,而在周遊CLASSPAH之後找不到對應名稱的class檔案時,抛出該異常。
java.lang.ArithmeticException:算術條件異常。譬如:整數除零等。
java.lang.ArrayIndexOutOfBoundsException:數組索引越界異常。當對數組的索引值為負數或大于等于數組大小時抛出。
final、finally、finalize 有什麼差別?
除了名字相似,他們毫無關系!!!
- final可以修飾類、變量、方法,修飾類表示該類不能被繼承、修飾方法表示該方法不能被重寫、修飾變量表示該變量是一個常量不能被重新指派。
- finally一般作用在try-catch代碼塊中,在處理異常的時候,通常我們将一定要執行的代碼方法finally代碼塊中,表示不管是否出現異常,該代碼塊都會執行,一般用來存放一些關閉資源的代碼。
- finalize是一個方法,屬于Object類的一個方法,而Object類是所有類的父類,Java 中允許使用 finalize()方法在垃圾收集器将對象從記憶體中清除出去之前做必要的清理工作。
final 有什麼用?
用于修飾類、屬性和方法;
- 被final修飾的類不可以被繼承
- 被final修飾的方法不可以被重寫
- 被final修飾的變量不可以被改變,被final修飾不可變的是變量的引用,而不是引用指向的内容,引用指向的内容是可以改變的。
try-catch-finally 中,如果 catch 中 return 了,finally 還會執行嗎?
答:會執行,在 return 前執行。
注意:在 finally 中改變傳回值的做法是不好的,因為如果存在 finally 代碼塊,try中的 return 語句不會立馬傳回調用者,而是記錄下傳回值待 finally 代碼塊執行完畢之後再向調用者傳回其值,然後如果在 finally 中修改了傳回值,就會傳回修改後的值。
顯然,在 finally 中傳回或者修改傳回值會對程式造成很大的困擾,C#中直接用編譯錯誤的方式來阻止程式員幹這種龌龊的事情,Java 中也可以通過提升編譯器的文法檢查級别來産生警告或錯誤。
public static int getInt() {
int a = 10;
try {
System.out.println(a / 0);
a = 20;
} catch (ArithmeticException e) {
a = 30;
return a;
/*
* return a 在程式執行到這一步的時候,這裡不是return a 而是 return 30;這個傳回路徑就形成了
* 但是呢,它發現後面還有finally,是以繼續執行finally的内容,a=40
* 再次回到以前的路徑,繼續走return 30,形成傳回路徑之後,這裡的a就不是a變量了,而是常量30
*/
} finally {
a = 40;
}
return a;
}
執行結果:30。
public static int getInt() {
int a = 10;
try {
System.out.println(a / 0);
a = 20;
} catch (ArithmeticException e) {
a = 30;
return a;
} finally {
a = 40;
//如果這樣,就又重新形成了一條傳回路徑,由于隻能通過1個return傳回,是以這裡直接傳回40
return a;
}
}
執行結果:40。
強引用、軟引用、弱引用、虛引用
強引用、軟引用、弱引用、幻象引用有什麼差別?具體使用場景是什麼?
不同的引用類型,主要展現的是對象不同的可達性(reachable)狀态和對垃圾收集的影響。
強引用
通過new 建立的對象就是強引用,強引用指向一個對象,就表示這個對象還活着,垃圾回收不會去收集。
軟引用
是一種相對強引用弱化一些的引用,隻有當 JVM 認為記憶體不足時,才會去試圖回收軟引用指向的對象。
JVM 會確定在抛出 OutOfMemoryError 之前,清理軟引用指向的對象。
軟引用通常用來實作記憶體敏感的緩存,如果還有空閑記憶體,就可以暫時保留緩存,當記憶體不足時清理掉,這樣就保證了使用緩存的同時,不會耗盡記憶體。
弱引用
ThreadlocalMap中的 key 就是用了弱引用,因為ThreadlocalMap 被thread 對象持有,是以如果是強引用的話,隻有當thread結束時才能被回收,而弱引用則可以在使用完後立即回收,不必等待thread結束。
虛引用
“虛引用”顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用并不會決定對象的生命周期。如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。
虛引用主要用來跟蹤對象被垃圾回收器回收的活動。虛引用與軟引用和弱引用的一個差別在于:虛引用必須和引用隊列 (ReferenceQueue)聯合使用。
當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象的記憶體之前,把這個虛引用加入到與之 關聯的引用隊列中。
String、StringBuilder、StringBuffer 有什麼差別?
可變性
String類中使用字元數組儲存字元串,
private final char value[]
,是以string對象是不可變的。StringBuilder與StringBuffer都繼承自AbstractStringBuilder類,在AbstractStringBuilder中也是使用字元數組儲存字元串,char[] value,這兩種對象都是可變的。
線程安全性
String中的對象是不可變的,也就可以了解為常量,線程安全。AbstractStringBuilder是StringBuilder與StringBuffer的公共父類,定義了一些字元串的基本操作,如expandCapacity、append、insert、indexOf等公共方法。
StringBuffer對方法加了同步鎖或者對調用的方法加了同步鎖,是以是線程安全的。StringBuilder并沒有對方法進行加同步鎖,是以是非線程安全的。
性能
每次對String 類型進行改變的時候,都會生成一個新的String對象,然後将指針指向新的String 對象。
StringBuffer每次都會對StringBuffer對象本身進行操作,而不是生成新的對象并改變對象引用。相同情況下使用StirngBuilder 相比使用StringBuffer 僅能獲得10%~15% 左右的性能提升,但卻要冒多線程不安全的風險。
對于三者使用的總結
如果要操作少量的資料用 = String
單線程操作字元串緩沖區 下操作大量資料 = StringBuilder
多線程操作字元串緩沖區 下操作大量資料 = StringBuffer
String
String 是 Java 語言非常基礎和重要的類,提供了構造和管理字元串的各種基本邏輯。它是典型的 Immutable 類,被聲明成為 final class,所有屬性也都是 final 的。
也由于它的不可變性,類似拼接、裁剪字元串等動作,都會産生新的 String 對象。
StringBuilder
StringBuilder 是 Java 1.5 中新增的,在能力上和 StringBuffer 沒有本質差別,但是它去掉了線程安全的部分,有效減小了開銷,是絕大部分情況下進行字元串拼接的首選。
StringBuffer
StringBuffer 是為解決上面提到拼接産生太多中間對象的問題而提供的一個類,我們可以用 append 或者 add 方法,把字元串添加到已有序列的末尾或者指定位置。StringBuffer 本質是一個線程安全的可修改字元序列,它保證了線程安全,也随之帶來了額外的性能開銷,是以除非有線程安全的需要,不然還是推薦使用它的後繼者,也就是 StringBuilder。
HashMap 使用 String 作為 key有什麼好處
HashMap 内部實作是通過 key 的 hashcode 來确定 value 的存儲位置,因為字元串是不可變的,是以當建立字元串時,它的 hashcode 被緩存下來,不需要再次計算,是以相比于其他對象更快。
接口和抽象類有什麼差別?
抽象類是用來捕捉子類的通用特性的。接口是抽象方法的集合。
接口和抽象類各有優缺點,在接口和抽象類的選擇上,必須遵守這樣一個原則:
- 行為模型應該總是通過接口而不是抽象類定義,是以通常是優先選用接口,盡量少用抽象類。
- 選擇抽象類的時候通常是如下情況:需要定義子類的行為,又要為子類提供通用的功能。
相同點
- 接口和抽象類都不能執行個體化
- 都位于繼承的頂端,用于被其他實作或繼承
- 都包含抽象方法,其子類都必須覆寫這些抽象方法
接口
接口定義了協定,是面向對象程式設計(封裝、繼承多态)基礎,通過接口我們能很好的實作單一職責、接口隔離、内聚。
- 不能執行個體化;
- 不能包含任何非常量成員,任何 field 都是隐含着 public static final 的意義;
- 同時,沒有非靜态方法實作,也就是說要麼是抽象方法,要麼是靜态方法。
Java8 中接口中引入預設方法和靜态方法,并且不用強制子類來實作它。以此來減少抽象類和接口之間的差異。
抽象類
抽象類是不能執行個體化的類,用 abstract 關鍵字修飾 class,其目的主要是代碼重用。
從設計層面來說,抽象類是對類的抽象,是一種模闆設計,接口是行為的抽象,是一種行為的規範。
除了不能執行個體化,形式上和一般的 Java 類并沒有太大差別。
可以有一個或者多個抽象方法,也可以沒有抽象方法。抽象類大多用于抽取相關 Java 類的共用方法實作或者是共同成員變量,然後通過繼承的方式達到代碼複用的目的。
碼老濕,抽象類能用 final 修飾麼?
不能,定義抽象類就是讓其他類繼承的,如果定義為 final 該類就不能被繼承,這樣彼此就會産生沖突,是以 final 不能修飾抽象類
值傳遞
當一個對象被當作參數傳遞到一個方法後,此方法可改變這個對象的屬性,并可傳回變化後的結果,那麼這裡到底是值傳遞還是引用傳遞?
是值傳遞。
Java 語言的方法調用隻支援參數的值傳遞。當一個對象執行個體作為一個參數被傳遞到方法中時,參數的值就是對該對象的引用。
對象的屬性可以在被調用過程中被改變,但對對象引用的改變是不會影響到調用者的。
為什麼 Java 隻有值傳遞?
首先回顧一下在程式設計語言中有關将參數傳遞給方法(或函數)的一些專業術語。按值調用(call by value)表示方法接收的是調用者提供的值,而按引用調用(call by reference)表示方法接收的是調用者提供的變量位址。
一個方法可以修改傳遞引用所對應的變量值,而不能修改傳遞值調用所對應的變量值。
它用來描述各種程式設計語言(不隻是Java)中方法參數傳遞方式。
Java程式設計語言總是采用按值調用。也就是說,方法得到的是所有參數值的一個拷貝,也就是說,方法不能修改傳遞給它的任何參數變量的内容。
基本資料類型
例子如下:
public static void main(String[] args) {
int num1 = 10;
int num2 = 20;
swap(num1, num2);
System.out.println("num1 = " + num1);
System.out.println("num2 = " + num2);
}
public static void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
System.out.println("a = " + a);
System.out.println("b = " + b);
}
執行結果:
a = 20
b = 10
num1 = 10
num2 = 20
解析:
在swap方法中,a、b的值進行交換,并不會影響到 num1、num2。
因為,a、b中的值,隻是從 num1、num2 的複制過來的。
也就是說,a、b相當于num1、num2 的副本,副本的内容無論怎麼修改,都不會影響到原件本身。
對象引用類型
public static void main(String[] args) {
int[] arr = { 1, 2, 3, 4, 5 };
System.out.println(arr[0]);
change(arr);
System.out.println(arr[0]);
}
public static void change(int[] array) {
// 将數組的第一個元素變為0
array[0] = 0;
}
結果:
1
0
解析:
array 被初始化 arr 的拷貝也就是一個對象的引用,也就是說 array 和 arr 指向的時同一個數組對象。 是以,外部對引用對象的改變會反映到所對應的對象上。
通過 example2 我們已經看到,實作一個改變對象參數狀态的方法并不是一件難事。理由很簡單,方法得到的是對象引用的拷貝,對象引用及其他的拷貝同時引用同一個對象。
很多程式設計語言(特别是,C++和Pascal)提供了兩種參數傳遞的方式:值調用和引用調用。
有些程式員認為Java程式設計語言對對象采用的是引用調用,實際上,這種了解是不對的。
值傳遞和引用傳遞有什麼差別?
值傳遞:指的是在方法調用時,傳遞的參數是按值的拷貝傳遞,傳遞的是值的拷貝,也就是說傳遞後就互不相關了。
引用傳遞:指的是在方法調用時,傳遞的參數是按引用進行傳遞,其實傳遞的引用的位址,也就是變量所對應的記憶體空間的位址。傳遞的是值的引用,也就是說傳遞前和傳遞後都指向同一個引用(也就是同一個記憶體空間)。