麵霸篇:高頻 Java 基礎問題(核心卷一)

碼哥位元組發表於2021-11-16

麵霸篇,從面試題作為切入點提升大家的 Java 內功,所謂根基不牢,地動山搖。只有紮實的基礎,才是寫出寫好程式碼。

拒絕知識碎片化

碼哥在 《Redis 系列》的開篇 Redis 為什麼這麼快中說過:學習一個技術,通常只接觸了零散的技術點,沒有在腦海裡建立一個完整的知識框架和架構體系,沒有系統觀。這樣會很吃力,而且會出現一看好像自己會,過後就忘記,一臉懵逼。

我們需要一個系統觀,清晰完整的去學習技術,同時也不能埋頭苦幹,過於死磕某個細節。

系統觀其實是至關重要的,從某種程度上說,在解決問題時,擁有了系統觀,就意味著你能有依據、有章法地定位和解決問題。

跟著「碼哥」一起來提綱挈領,梳理一個相對完整的 Java 開發技術能力圖譜,將基礎夯實。

點選下方關注我

[toc]

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()才能重新恢復。

&和&&的區別

&運算子有兩種用法:

  1. 按位與;
  2. 邏輯與。

&&運算子是短路與運算。邏輯與跟短路與的差別是非常巨大的,雖然二者都要求運算子左右兩端的布林值都是true 整個表示式的值才是 true。

&&之所以稱為短路運算,是因為如果&&左邊的表示式的值是 false,右邊的表示式會被直接短路掉,不會進行運算。

注意:邏輯或運算子(|)和短路或運算子(||)的差別也是如此。

Java 有哪些資料型別?

Java語言是強型別語言,對於每一種資料都定義了明確的具體的資料型別,在記憶體中分配了不同大小的記憶體空間。

分類

  • 基本資料型別

    • 數值型

      • 整數型別(byte,short,int,long)
      • 浮點型別(float,double)
    • 字元型(char)
    • 布林型(boolean)
  • 引用資料型別

    • 類(class)
    • 介面(interface)
    • 陣列([])

this 關鍵字的用法

this是自身的一個物件,代表物件本身,可以理解為:指向物件本身的一個指標。

this的用法在java中大體可以分為3種:

  1. 普通的直接引用,this相當於是指向當前物件本身。
  2. 形參與成員名字重名,用this來區分:
public Person(String name, int age) {
    this.name = name;
    this.age = age;
}
  1. 引用本類的建構函式
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也有三種用法:

  1. 普通的直接引用:與this類似,super相當於是指向當前物件的父類的引用,這樣就可以用super.xxx來引用父類的成員。
  2. 子類中的成員變數或方法與父類中的成員變數或方法同名時,用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();
     
        }
    }
  3. 引用父類建構函式;

成員變數與區域性變數的區別有哪些

變數:在程式執行的過程中,在某個範圍內其值可以發生改變的量。從本質上講,變數其實是記憶體中的一小塊區域。

成員變數:方法外部,類內部定義的變數。

區域性變數:類的方法中的變數。

區別如下:

作用域

成員變數:針對整個類有效。 區域性變數:只在某個範圍內有效。(一般指的就是方法,語句體內)

儲存位置

成員變數:隨著物件的建立而存在,隨著物件的消失而消失,儲存在堆記憶體中。

區域性變數:在方法被呼叫,或者語句被執行的時候存在,儲存在棧記憶體中。當方法呼叫完,或者語句結束後,就自動釋放。

生命週期

成員變數:隨著物件的建立而存在,隨著物件的消失而消失 區域性變數:當方法呼叫完,或者語句結束後,就自動釋放。

初始值

成員變數:有預設初始值。

區域性變數:沒有預設初始值,使用前必須賦值。

動態代理是基於什麼原理

基於反射實現

反射機制是 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 點請記住:

  1. 子類擁有父類非 private 的屬性和方法。
  2. 子類可以擁有自己屬性和方法,即子類可以對父類進行擴充套件。
  3. 子類可以用自己的方式實現父類的方法。(以後介紹)。

多型

所謂多型就是指程式中定義的引用變數所指向的具體型別和通過該引用變數發出的方法呼叫在程式設計時並不確定,而是在程式執行期間才確定。

即一個引用變數到底會指向哪個類的例項物件,該引用變數發出的方法呼叫到底是哪個類中實現的方法,必須在由程式執行期間才能決定。

在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程式設計語言對物件採用的是引用呼叫,實際上,這種理解是不對的。

值傳遞和引用傳遞有什麼區別?

值傳遞:指的是在方法呼叫時,傳遞的引數是按值的拷貝傳遞,傳遞的是值的拷貝,也就是說傳遞後就互不相關了。

引用傳遞:指的是在方法呼叫時,傳遞的引數是按引用進行傳遞,其實傳遞的引用的地址,也就是變數所對應的記憶體空間的地址。傳遞的是值的引用,也就是說傳遞前和傳遞後都指向同一個引用(也就是同一個記憶體空間)。

相關文章