麪霸篇,從面試題作爲切入點提升大家的 Java 內功,所謂根基不牢,地動山搖。只有紮實的基礎,纔是寫出寫好代碼。
拒絕知識碎片化
碼哥在 《Redis 系列》的開篇 Redis 爲什麼這麼快中說過:學習一個技術,通常只接觸了零散的技術點,沒有在腦海裏建立一個完整的知識框架和架構體系,沒有系統觀。這樣會很喫力,而且會出現一看好像自己會,過後就忘記,一臉懵逼。
我們需要一個系統觀,清晰完整的去學習技術,同時也不能埋頭苦幹,過於死磕某個細節。
系統觀其實是至關重要的,從某種程度上說,在解決問題時,擁有了系統觀,就意味着你能有依據、有章法地定位和解決問題。
跟着「碼哥」一起來提綱挈領,梳理一個相對完整的 Java 開發技術能力圖譜,將基礎夯實。
點擊下方關注我
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程序設計語言對對象採用的是引用調用,實際上,這種理解是不對的。
值傳遞和引用傳遞有什麼區別?
值傳遞:指的是在方法調用時,傳遞的參數是按值的拷貝傳遞,傳遞的是值的拷貝,也就是說傳遞後就互不相關了。
引用傳遞:指的是在方法調用時,傳遞的參數是按引用進行傳遞,其實傳遞的引用的地址,也就是變量所對應的內存空間的地址。傳遞的是值的引用,也就是說傳遞前和傳遞後都指向同一個引用(也就是同一個內存空間)。