面渣逆襲:Java基礎五十三問,快來看看有沒有你不會的!

大家好,我是老三, 面渣逆襲 系列繼續。這節我們回到夢開始的地方——Java基礎,其實過了萌新階段,面試問基礎就問的不多,但是保不齊突然問一下。想一下,總不能張口高併發、閉口分佈式,結果什麼是面向對象,說不清,那多少有點魔幻。所以趕緊來看看,這些基礎有沒有你不會的!

Java概述

1.什麼是Java?

下輩子還學Java

PS:碎慫Java,有啥好介紹的。哦,面試啊。

Java是一門面向對象的編程語言,不僅吸收了C++語言的各種優點,還摒棄了C++裏難以理解的多繼承、指針等概念,因此Java語言具有功能強大和簡單易用兩個特徵。Java語言作爲靜態面向對象編程語言的優秀代表,極好地實現了面向對象理論,允許程序員以優雅的思維方式進行復雜的編程 。

2.Java語言有哪些特點?

Java語言有很多優秀(可吹)的特點,以下幾個是比較突出的:

Java語言特點

  • 面向對象(封裝,繼承,多態);
  • 平臺無關性,平臺無關性的具體表現在於,Java 是“一次編寫,到處運行(Write Once,Run any Where)”的語言,因此採用 Java 語言編寫的程序具有很好的可移植性,而保證這一點的正是 Java 的虛擬機機制。在引入虛擬機之後,Java 語言在不同的平臺上運行不需要重新編譯。
  • 支持多線程。C++ 語言沒有內置的多線程機制,因此必須調用操作系統的多線程功能來進行多線程程序設計,而 Java 語言卻提供了多線程支持;
  • 編譯與解釋並存;

3.JVM、JDK 和 JRE 有什麼區別?

JVM:Java Virtual Machine,Java虛擬機,Java程序運行在Java虛擬機上。針對不同系統的實現(Windows,Linux,macOS)不同的JVM,因此Java語言可以實現跨平臺。

JRE: Java 運⾏時環境。它是運⾏已編譯 Java 程序所需的所有內容的集合,包括 Java 虛擬機(JVM),Java 類庫,Java 命令和其他的⼀些基礎構件。但是,它不能⽤於創建新程序。

JDK: Java Development Kit,它是功能⻬全的 Java SDK。它擁有 JRE 所擁有的⼀切,還有編譯器(javac)和⼯具(如 javadoc 和 jdb)。它能夠創建和編譯程序。

簡單來說,JDK包含JRE,JRE包含JVM。

JDK、JRE、JVM關係

4.說說什麼是跨平臺性?原理是什麼

所謂跨平臺性,是指Java語言編寫的程序,一次編譯後,可以在多個系統平臺上運行。

實現原理:Java程序是通過Java虛擬機在系統平臺上運行的,只要該系統可以安裝相應的Java虛擬機,該系統就可以運行java程序。

5.什麼是字節碼?採用字節碼的好處是什麼?

所謂的字節碼,就是Java程序經過編譯之類產生的.class文件,字節碼能夠被虛擬機識別,從而實現Java程序的跨平臺性。

Java 程序從源代碼到運行主要有三步:

  • 編譯:將我們的代碼(.java)編譯成虛擬機可以識別理解的字節碼(.class)
  • 解釋:虛擬機執行Java字節碼,將字節碼翻譯成機器能識別的機器碼
  • 執行:對應的機器執行二進制機器碼

Java程序執行過程

只需要把Java程序編譯成Java虛擬機能識別的Java字節碼,不同的平臺安裝對應的Java虛擬機,這樣就可以可以實現Java語言的平臺無關性。

6.爲什麼說 Java 語言“編譯與解釋並存”?

高級編程語言按照程序的執行方式分爲編譯型解釋型兩種。

簡單來說,編譯型語言是指編譯器針對特定的操作系統將源代碼一次性翻譯成可被該平臺執行的機器碼;解釋型語言是指解釋器對源程序逐行解釋成特定平臺的機器碼並立即執行。

比如,你想讀一本外國的小說,你可以找一個翻譯人員幫助你翻譯,有兩種選擇方式,你可以先等翻譯人員將全本的小說(也就是源碼)都翻譯成漢語,再去閱讀,也可以讓翻譯人員翻譯一段,你在旁邊閱讀一段,慢慢把書讀完。

Java 語言既具有編譯型語言的特徵,也具有解釋型語言的特徵,因爲 Java 程序要經過先編譯,後解釋兩個步驟,由 Java 編寫的程序需要先經過編譯步驟,生成字節碼(\*.class 文件),這種字節碼必須再經過JVM,解釋成操作系統能識別的機器碼,在由操作系統執行。因此,我們可以認爲 Java 語言編譯解釋並存。

編譯與解釋

基礎語法

7.Java有哪些數據類型?

定義:Java語言是強類型語言,對於每一種數據都定義了明確的具體的數據類型,在內存中分配了不同大小的內存空間。

Java語言數據類型分爲兩種:基本數據類型引用數據類型

Java數據類型

基本數據類型:

  • 數值型
    • 整數類型(byte、short、long)
    • 浮點類型(float、long)
  • 字符型(char)
  • 布爾型(boolean)

Java基本數據類型範圍和默認值:

基本類型 位數 字節 默認值
int 32 4 0
short 16 2 0
long 64 8 0L
byte 8 1 0
char 16 2 'u0000'
float 32 4 0f
double 64 8 0d
boolean 1 false

引用數據類型:

  • 類(class)
  • 接口(interface)
  • 數組([])

8.自動類型轉換、強制類型轉換?看看這幾行代碼?

Java 所有的數值型變量可以相互轉換,當把一個表數範圍小的數值或變量直接賦給另一個表數範圍大的變量時,可以進行自動類型轉換;反之,需要強制轉換。

Java自動類型轉換方向

這就好像,小杯裏的水倒進大杯沒問題,但大杯的水倒進小杯就不行了,可能會溢出。

float f=3.4,對嗎?

不正確。3.4 是單精度數,將雙精度型(double)賦值給浮點型(float)屬於下轉型(down-casting,也稱爲窄化)會造成精度損失,因此需要強制類型轉換float f =(float)3.4; 或者寫成 float f =3.4F

short s1 = 1; s1 = s1 + 1;對嗎?short s1 = 1; s1 += 1;對嗎?

對於 short s1 = 1; s1 = s1 + 1;編譯出錯,由於 1 是 int 類型,因此 s1+1 運算結果也是 int型,需要強制轉換類型才能賦值給 short 型。

而 short s1 = 1; s1 += 1;可以正確編譯,因爲 s1+= 1;相當於 s1 = (short(s1 + 1);其中有隱含的強制類型轉換。

9.什麼是自動拆箱/封箱?

  • 裝箱:將基本類型用它們對應的引用類型包裝起來;
  • 拆箱:將包裝類型轉換爲基本數據類型;

Java可以自動對基本數據類型和它們的包裝類進行裝箱和拆箱。

裝箱和拆箱

舉例:

Integer i = 10;  //裝箱
int n = i;   //拆箱

10.&和&&有什麼區別?

&運算符有兩種用法:短路與邏輯與

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

&&之所以稱爲短路運算是因爲,如果&&左邊的表達式的值是 false,右邊的表達式會被直接短路掉,不會進行運算。很多時候我們可能都需要用&&而不是&。

例如在驗證用戶登錄時判定用戶名不是 null 而且不是空字符串,應當寫爲 username != null &&!username.equals(""),二者的順序不能交換,更不能用&運算符,因爲第一個條件如果不成立,根本不能進行字符串的 equals 比較,否則會產生 NullPointerException 異常。

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

11.switch 是否能作用在 byte/long/String上?

Java5 以前 switch(expr)中,expr 只能是 byte、short、char、int。

從 Java 5 開始,Java 中引入了枚舉類型, expr 也可以是 enum 類型。

從 Java 7 開始,expr還可以是字符串(String),但是長整型(long)在目前所有的版本中都是不可以的。

12.break ,continue ,return 的區別及作用?

  • break 跳出整個循環,不再執行循環(結束當前的循環體)
  • continue 跳出本次循環,繼續執行下次循環(結束正在執行的循環 進入下一個循環條件)
  • return 程序返回,不再執行下面的代碼(結束當前的方法 直接返回)

break 、continue 、return

13.用最有效率的方法計算2乘以8?

2 << 3。位運算,數字的二進制位左移三位相當於乘以2的三次方。

14.說說自增自減運算?看下這幾個代碼運行結果?

在寫代碼的過程中,常見的一種情況是需要某個整數類型變量增加 1 或減少 1,Java 提供了一種特殊的運算符,用於這種表達式,叫做自增運算符(++)和自減運算符(--)。

++和--運算符可以放在變量之前,也可以放在變量之後。

當運算符放在變量之前時(前綴),先自增/減,再賦值;當運算符放在變量之後時(後綴),先賦值,再自增/減。

例如,當 b = ++a 時,先自增(自己增加 1),再賦值(賦值給 b);當 b = a++ 時,先賦值(賦值給 b),再自增(自己增加 1)。也就是,++a 輸出的是 a+1 的值,a++輸出的是 a 值。

用一句口訣就是:“符號在前就先加/減,符號在後就後加/減”。

看一下這段代碼運行結果?

int i  = 1;
i = i++;
System.out.println(i);

答案是1。有點離譜對不對。

對於JVM而言,它對自增運算的處理,是會先定義一個臨時變量來接收i的值,然後進行自增運算,最後又將臨時變量賦給了值爲2的i,所以最後的結果爲1。

相當於這樣的代碼:

int i = 1;
int temp = i;
i++;
i = temp;
System.out.println(i);

這段代碼會輸出什麼?

int count = 0;
for(int i = 0;i < 100;i++)
{
    count = count++;
}
System.out.println("count = "+count);

答案是0。

和上面的題目一樣的道理,同樣是用了臨時變量,count實際是等於臨時變量的值。

int autoAdd(int count)
{
    int temp = count;
    count = coutn + 1;
    return temp;
}

PS:筆試面試可能會碰到的奇葩題,開發這麼寫,見一次吊一次。

面向對象

15.⾯向對象和⾯向過程的區別?

  • ⾯向過程 :面向過程就是分析出解決問題所需要的步驟,然後用函數把這些步驟一步一步實現,使用的時候再一個一個的一次調用就可以。
  • ⾯向對象 :面向對象,把構成問題的事務分解成各個對象,而建立對象的目的也不是爲了完成一個個步驟,而是爲了描述某個事件在解決整個問題的過程所發生的行爲。 目的是爲了寫出通用的代碼,加強代碼的重用,屏蔽差異性。

用一個比喻:面向過程是編年體;面向對象是紀傳體。

面向對象和麪向過程的區別

16.面向對象有哪些特性

面向對象三大特徵

  • 封裝

    封裝把⼀個對象的屬性私有化,同時提供⼀些可以被外界訪問的屬性的⽅法。

  • 繼承

    繼承是使⽤已存在的類的定義作爲基礎創建新的類,新類的定義可以增加新的屬性或新的方法,也可以繼承父類的屬性和方法。通過繼承可以很方便地進行代碼複用。

關於繼承有以下三個要點:

  1. ⼦類擁有⽗類對象所有的屬性和⽅法(包括私有屬性和私有⽅法),但是⽗類中的私有屬性和⽅法⼦類是⽆法訪問,只是擁有。

  2. ⼦類可以擁有⾃⼰屬性和⽅法,即⼦類可以對⽗類進⾏擴展。

  3. ⼦類可以⽤⾃⼰的⽅式實現⽗類的⽅法。

  • 多態

    所謂多態就是指程序中定義的引⽤變量所指向的具體類型和通過該引⽤變量發出的⽅法調⽤在編程時並不確定,⽽是在程序運⾏期間才確定,即⼀個引⽤變量到底會指向哪個類的實例對象,該引⽤變量發出的⽅法調⽤到底是哪個類中實現的⽅法,必須在由程序運⾏期間才能決定。

    在 Java 中有兩種形式可以實現多態:繼承(多個⼦類對同⼀⽅法的重寫)和接⼝(實現接⼝並覆蓋接⼝中同⼀⽅法)。

17.重載(overload)和重寫(override)的區別?

方法的重載和重寫都是實現多態的方式,區別在於前者實現的是編譯時的多態性,而後者實現的是運行時的多態性。

  • 重載發生在一個類中,同名的方法如果有不同的參數列表(參數類型不同、參數個數不同或者二者都不同)則視爲重載;

  • 重寫發生在子類與父類之間,重寫要求子類被重寫方法與父類被重寫方法有相同的返回類型,比父類被重寫方法更好訪問,不能比父類被重寫方法聲明更多的異常(里氏代換原則)。

方法重載的規則:

  1. 方法名一致,參數列表中參數的順序,類型,個數不同。
  2. 重載與方法的返回值無關,存在於父類和子類,同類中。
  3. 可以拋出不同的異常,可以有不同修飾符。

18.訪問修飾符public、private、protected、以及不寫(默認)時的區別?

Java中,可以使用訪問控制符來保護對類、變量、方法和構造方法的訪問。Java 支持 4 種不同的訪問權限。

  • default (即默認,什麼也不寫): 在同一包內可見,不使用任何修飾符。可以修飾在類、接口、變量、方法。
  • private : 在同一類內可見。可以修飾變量、方法。注意:不能修飾類(外部類)
  • public : 對所有類可見。可以修飾類、接口、變量、方法
  • protected : 對同一包內的類和所有子類可見。可以修飾變量、方法。注意:不能修飾類(外部類)

訪問修飾符和可見性

19.this關鍵字有什麼作用?

this是自身的一個對象,代表對象本身,可以理解爲:指向對象本身的一個指針

this的用法在Java中大體可以分爲3種:

  1. 普通的直接引用,this相當於是指向當前對象本身

  2. 形參與成員變量名字重名,用this來區分:

public Person(String name,int age){
    this.name=name;
    this.age=age;
}
  1. 引用本類的構造函數

20.抽象類(abstract class)和接口(interface)有什麼區別?

  1. 接⼝的⽅法默認是 public ,所有⽅法在接⼝中不能有實現(Java 8 開始接⼝⽅法可以有默認實現),⽽抽象類可以有⾮抽象的⽅法。

  2. 接⼝中除了 static 、 final 變量,不能有其他變量,⽽抽象類中則不⼀定。

  3. ⼀個類可以實現多個接⼝,但只能實現⼀個抽象類。接⼝⾃⼰本身可以通過 extends 關鍵字擴展多個接⼝。

  4. 接⼝⽅法默認修飾符是 public ,抽象⽅法可以有 public 、 protected 和 default 這些修飾符(抽象⽅法就是爲了被重寫所以不能使⽤ private 關鍵字修飾!)。

  5. 從設計層⾯來說,抽象是對類的抽象,是⼀種模板設計,⽽接⼝是對⾏爲的抽象,是⼀種⾏爲的規範。

  1. 在 JDK8 中,接⼝也可以定義靜態⽅法,可以直接⽤接⼝名調⽤。實現類和實現是不可以調⽤的。如果同時實現兩個接⼝,接⼝中定義了⼀樣的默認⽅法,則必須重寫,不然會報錯。

  2. jdk9 的接⼝被允許定義私有⽅法 。

總結⼀下 jdk7~jdk9 Java 中接⼝的變化:

  1. 在 jdk 7 或更早版本中,接⼝⾥⾯只能有常量變量和抽象⽅法。這些接⼝⽅法必須由選擇實現接⼝的類實現。

  2. jdk 8 的時候接⼝可以有默認⽅法和靜態⽅法功能。

  3. jdk 9 在接⼝中引⼊了私有⽅法和私有靜態⽅法。

21.成員變量與局部變量的區別有哪些?

  1. 從語法形式上看:成員變量是屬於類的,⽽局部變量是在⽅法中定義的變量或是⽅法的參數;成員變量可以被 public , private , static 等修飾符所修飾,⽽局部變量不能被訪問控制修飾符及 static 所修飾;但是,成員變量和局部變量都能被 final 所修飾。

  2. 從變量在內存中的存儲⽅式來看:如果成員變量是使⽤ static 修飾的,那麼這個成員變量是屬於類的,如果沒有使⽤ static 修飾,這個成員變量是屬於實例的。對象存於堆內存,如果局部變量類型爲基本數據類型,那麼存儲在棧內存,如果爲引⽤數據類型,那存放的是指向堆內存對象的引⽤或者是指向常量池中的地址。

  3. 從變量在內存中的⽣存時間上看:成員變量是對象的⼀部分,它隨着對象的創建⽽存在,⽽局部變量隨着⽅法的調⽤⽽⾃動消失。

  4. 成員變量如果沒有被賦初值:則會⾃動以類型的默認值⽽賦值(⼀種情況例外:被 final 修飾的成員變量也必須顯式地賦值),⽽局部變量則不會⾃動賦值。

22.靜態變量和實例變量的區別?靜態方法、實例方法呢?

靜態變量和實例變量的區別?

靜態變量: 是被 static 修飾符修飾的變量,也稱爲類變量,它屬於類,不屬於類的任何一個對象,一個類不管創建多少個對象,靜態變量在內存中有且僅有一個副本。

實例變量: 必須依存於某一實例,需要先創建對象然後通過對象才能訪問到它。靜態變量可以實現讓多個對象共享內存。

靜態⽅法和實例⽅法有何不同?

類似地。

靜態方法:static修飾的方法,也被稱爲類方法。在外部調⽤靜態⽅法時,可以使⽤"類名.⽅法名"的⽅式,也可以使⽤"對象名.⽅法名"的⽅式。靜態方法裏不能訪問類的非靜態成員變量和方法。

實例⽅法:依存於類的實例,需要使用"對象名.⽅法名"的⽅式調用;可以訪問類的所有成員變量和方法。

24.final關鍵字有什麼作用?

final表示不可變的意思,可用於修飾類、屬性和方法:

  • 被final修飾的類不可以被繼承

  • 被final修飾的方法不可以被重寫

  • 被final修飾的變量不可變,被final修飾的變量必須被顯式第指定初始值,還得注意的是,這裏的不可變指的是變量的引用不可變,不是引用指向的內容的不可變。

    例如:

            final StringBuilder sb = new StringBuilder("abc");
            sb.append("d");
            System.out.println(sb);  //abcd
    

    一張圖說明:

    final修飾變量

25.final、finally、finalize的區別?

  • final 用於修飾變量、方法和類:final修飾的類不可被繼承;修飾的方法不可被重寫;修飾的變量不可變。

  • finally 作爲異常處理的一部分,它只能在 try/catch 語句中,並且附帶一個語句塊表示這段語句最終一定被執行(無論是否拋出異常),經常被用在需要釋放資源的情況下,System.exit (0) 可以阻斷 finally 執行。

  • finalize 是在 java.lang.Object 裏定義的方法,也就是說每一個對象都有這麼個方法,這個方法在 gc 啓動,該對象被回收的時候被調用。

    一個對象的 finalize 方法只會被調用一次,finalize 被調用不一定會立即回收該對象,所以有可能調用 finalize 後,該對象又不需要被回收了,然後到了真正要被回收的時候,因爲前面調用過一次,所以不會再次調用 finalize 了,進而產生問題,因此不推薦使用 finalize 方法。

26.==和 equals 的區別?

== : 它的作⽤是判斷兩個對象的地址是不是相等。即,判斷兩個對象是不是同⼀個對象(基本數據類型==比較的是值,引⽤數據類型==比較的是內存地址)。

equals() : 它的作⽤也是判斷兩個對象是否相等。但是這個“相等”一般也分兩種情況:

  • 默認情況:類沒有覆蓋 equals() ⽅法。則通過 equals() 比較該類的兩個對象時,等價於通過“==”比較這兩個對象,還是相當於比較內存地址。

  • 自定義情況:類覆蓋了 equals() ⽅法。我們平時覆蓋的 equals()方法一般是比較兩個對象的內容是否相同,自定義了一個相等的標準,也就是兩個對象的值是否相等。

舉個例⼦,Person,我們認爲兩個人的編號和姓名相同,就是一個人:

public class Person {
    private String no;
    private String name;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Person)) return false;
        Person person = (Person) o;
        return Objects.equals(no, person.no) &&
                Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(no, name);
    }
}

27.hashCode與 equals?

這個也是面試常問——“你重寫過 hashcode 和 equals 麼,爲什麼重寫 equals 時必須重寫hashCode ⽅法?”

什麼是HashCode?

hashCode() 的作⽤是獲取哈希碼,也稱爲散列碼;它實際上是返回⼀個 int 整數,定義在 Object 類中, 是一個本地⽅法,這個⽅法通常⽤來將對象的內存地址轉換爲整數之後返回。

public native int hashCode();

哈希碼主要在哈希表這類集合映射的時候用到,哈希表存儲的是鍵值對(key-value),它的特點是:能根據“鍵”快速的映射到對應的“值”。這其中就利⽤到了哈希碼!

爲什麼要有 hashCode?

上面已經講了,主要是在哈希表這種結構中用的到。

例如HashMap怎麼把key映射到對應的value上呢?用的就是哈希取餘法,也就是拿哈希碼和存儲元素的數組的長度取餘,獲取key對應的value所在的下標位置。詳細可見:面渣逆襲:Java集合連環三十問

爲什麼重寫 quals 時必須重寫 hashCode ⽅法?

如果兩個對象相等,則 hashcode ⼀定也是相同的。兩個對象相等,對兩個對象分別調⽤ equals⽅法都返回 true。反之,兩個對象有相同的 hashcode 值,它們也不⼀定是相等的 。因此,equals ⽅法被覆蓋過,則 hashCode ⽅法也必須被覆蓋。

hashCode() 的默認⾏爲是對堆上的對象產⽣獨特值。如果沒有重寫 hashCode() ,則該class 的兩個對象⽆論如何都不會相等(即使這兩個對象指向相同的數據)

爲什麼兩個對象有相同的 hashcode值,它們也不⼀定是相等的?

因爲可能會碰撞, hashCode() 所使⽤的散列算法也許剛好會讓多個對象傳回相同的散列值。越糟糕的散列算法越容易碰撞,但這也與數據值域分佈的特性有關(所謂碰撞也就是指的是不同的對象得到相同的 hashCode )。

28.Java是值傳遞,還是引用傳遞?

Java語言是值傳遞。Java 語言的方法調用只支持參數的值傳遞。當一個對象實例作爲一個參數被傳遞到方法中時,參數的值就是對該對象的引用。對象的屬性可以在被調用過程中被改變,但對對象引用的改變是不會影響到調用者的。

JVM 的內存分爲堆和棧,其中棧中存儲了基本數據類型和引用數據類型實例的地址,也就是對象地址。

而對象所佔的空間是在堆中開闢的,所以傳遞的時候可以理解爲把變量存儲的對象地址給傳遞過去,因此引用類型也是值傳遞。

Java引用數據值傳遞示意圖

29.深拷貝和淺拷貝?

  • 淺拷貝:僅拷貝被拷貝對象的成員變量的值,也就是基本數據類型變量的值,和引用數據類型變量的地址值,而對於引用類型變量指向的堆中的對象不會拷貝。
  • 深拷貝:完全拷貝一個對象,拷貝被拷貝對象的成員變量的值,堆中的對象也會拷貝一份。

例如現在有一個order對象,裏面有一個products列表,它的淺拷貝和深拷貝的示意圖:

淺拷貝和深拷貝示意圖

因此深拷貝是安全的,淺拷貝的話如果有引用類型,那麼拷貝後對象,引用類型變量修改,會影響原對象。

淺拷貝如何實現呢?

Object類提供的clone()方法可以非常簡單地實現對象的淺拷貝。

深拷貝如何實現呢?

  • 重寫克隆方法:重寫克隆方法,引用類型變量單獨克隆,這裏可能會涉及多層遞歸。
  • 序列化:可以先講原對象序列化,再反序列化成拷貝對象。

30.Java 創建對象有哪幾種方式?

Java中有以下四種創建對象的方式:

Java創建對象的四種方式

  • new創建新對象
  • 通過反射機制
  • 採用clone機制
  • 通過序列化機制

前兩者都需要顯式地調用構造方法。對於clone機制,需要注意淺拷貝和深拷貝的區別,對於序列化機制需要明確其實現原理,在Java中序列化可以通過實現Externalizable或者Serializable來實現。

常用類

String

31.String 是 Java 基本數據類型嗎?可以被繼承嗎?

String是Java基本數據類型嗎?

不是。Java 中的基本數據類型只有8個:byte、short、int、long、float、double、char、boolean;除了基本類型(primitive type),剩下的都是引用類型(reference type)。

String是一個比較特殊的引用數據類型。

String 類可以繼承嗎?

不行。String 類使用 final 修飾,是所謂的不可變類,無法被繼承。

32.String和StringBuilder、StringBuffer的區別?

  • String:String 的值被創建後不能修改,任何對 String 的修改都會引發新的 String 對象的生成。
  • StringBuffer:跟 String 類似,但是值可以被修改,使用 synchronized 來保證線程安全。
  • StringBuilder:StringBuffer 的非線程安全版本,性能上更高一些。

33.String str1 = new String("abc")和String str2 = "abc" 和 區別?

兩個語句都會去字符串常量池中檢查是否已經存在 “abc”,如果有則直接使用,如果沒有則會在常量池中創建 “abc” 對象。

堆與常量池中的String

但是不同的是,String str1 = new String("abc") 還會通過 new String() 在堆裏創建一個 "abc" 字符串對象實例。所以後者可以理解爲被前者包含。

String s = new String("abc")創建了幾個對象?

很明顯,一個或兩個。如果字符串常量池已經有“abc”,則是一個;否則,兩個。

當字符創常量池沒有 “abc”,此時會創建如下兩個對象:

  • 一個是字符串字面量 "abc" 所對應的、字符串常量池中的實例
  • 另一個是通過 new String() 創建並初始化的,內容與"abc"相同的實例,在堆中。

34.String不是不可變類嗎?字符串拼接是如何實現的?

String的確是不可變的,“+”的拼接操作,其實是會生成新的對象。

例如:

String a = "hello ";
String b = "world!";
String ab = a + b;

jdk1.8之前,a和b初始化時位於字符串常量池,ab拼接後的對象位於堆中。經過拼接新生成了String對象。如果拼接多次,那麼會生成多箇中間對象。

內存如下:

jdk1.8之前的字符串拼接

Java8時JDK對“+”號拼接進行了優化,上面所寫的拼接方式會被優化爲基於StringBuilder的append方法進行處理。Java會在編譯期對“+”號進行處理。

下面是通過javap -verbose命令反編譯字節碼的結果,很顯然可以看到StringBuilder的創建和append方法的調用。

stack=2, locals=4, args_size=1
     0: ldc           #2                  // String hello
     2: astore_1
     3: ldc           #3                  // String world!
     5: astore_2
     6: new           #4                  // class java/lang/StringBuilder
     9: dup
    10: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
    13: aload_1
    14: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    17: aload_2
    18: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    21: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
    24: astore_3
    25: return

也就是說其實上面的代碼其實相當於:

String a = "hello ";
String b = "world!";
StringBuilder sb = new StringBuilder();
sb.append(a);
sb.append(b);
String ab = sb.toString();

此時,如果再籠統的回答:通過加號拼接字符串會創建多個String對象,因此性能比StringBuilder差,就是錯誤的了。因爲本質上加號拼接的效果最終經過編譯器處理之後和StringBuilder是一致的。

當然,循環裏拼接還是建議用StringBuilder,爲什麼,因爲循環一次就會創建一個新的StringBuilder對象,大家可以自行實驗。

35.intern方法有什麼作用?

JDK源碼裏已經對這個方法進行了說明:

     * <p>
     * When the intern method is invoked, if the pool already contains a
     * string equal to this {@code String} object as determined by
     * the {@link #equals(Object)} method, then the string from the pool is
     * returned. Otherwise, this {@code String} object is added to the
     * pool and a reference to this {@code String} object is returned.
     * <p>

意思也很好懂:

  • 如果當前字符串內容存在於字符串常量池(即equals()方法爲true,也就是內容一樣),直接返回字符串常量池中的字符串
  • 否則,將此String對象添加到池中,並返回String對象的引用

Integer

36.Integer a= 127,Integer b = 127;Integer c= 128,Integer d = 128;,相等嗎?

答案是a和b相等,c和d不相等。

  • 對於基本數據類型==比較的值
  • 對於引用數據類型==比較的是地址

Integer a= 127這種賦值,是用到了Integer自動裝箱的機制。自動裝箱的時候會去緩存池裏取Integer對象,沒有取到纔會創建新的對象。

如果整型字面量的值在-128到127之間,那麼自動裝箱時不會new新的Integer對象,而是直接引用緩存池中的Integer對象,超過範圍 a1==b1的結果是false

    public static void main(String[] args) {
        Integer a = 127;
        Integer b = 127;
        Integer b1 = new Integer(127);
        System.out.println(a == b); //true
        System.out.println(b==b1);  //false

        Integer c = 128;
        Integer d = 128;
        System.out.println(c == d);  //false
    }

什麼是Integer緩存?

因爲根據實踐發現大部分的數據操作都集中在值比較小的範圍,因此 Integer 搞了個緩存池,默認範圍是 -128 到 127,可以根據通過設置JVM-XX:AutoBoxCacheMax=來修改緩存的最大值,最小值改不了。

實現的原理是int 在自動裝箱的時候會調用Integer.valueOf,進而用到了 IntegerCache。

Integer.valueOf

很簡單,就是判斷下值是否在緩存範圍之內,如果是的話去 IntegerCache 中取,不是的話就創建一個新的Integer對象。

IntegerCache是一個靜態內部類, 在靜態塊中會初始化好緩存值。

 private static class IntegerCache {
     ……
     static {
            //創建Integer對象存儲
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);
         ……
     } 
 }

37.String怎麼轉成Integer的?原理?

PS:這道題印象中在一些面經中出場過幾次。

String轉成Integer,主要有兩個方法:

  • Integer.parseInt(String s)
  • Integer.valueOf(String s)

不管哪一種,最終還是會調用Integer類內中的parseInt(String s, int radix)方法。

拋去一些邊界之類的看看核心代碼:

public static int parseInt(String s, int radix)
                throws NumberFormatException
    {

        int result = 0;
        //是否是負數
        boolean negative = false;
        //char字符數組下標和長度
        int i = 0, len = s.length();
        ……
        int digit;
        //判斷字符長度是否大於0,否則拋出異常
        if (len > 0) {
            …… 
            while (i < len) {
                // Accumulating negatively avoids surprises near MAX_VALUE
                //返回指定基數中字符表示的數值。(此處是十進制數值)
                digit = Character.digit(s.charAt(i++),radix);
                //進制位乘以數值
                result *= radix;        
                result -= digit;
            }
        } 
        //根據上面得到的是否負數,返回相應的值
        return negative ? result : -result;
    }

去掉枝枝蔓蔓(當然這些枝枝蔓蔓可以去看看,源碼cover了很多情況),其實剩下的就是一個簡單的字符串遍歷計算,不過計算方式有點反常規,是用負的值累減。

parseInt示意圖

Object

38.Object 類的常見方法?

Object 類是一個特殊的類,是所有類的父類,也就是說所有類都可以調用它的方法。它主要提供了以下 11 個方法,大概可以分爲六類:

Object類的方法

對象比較

  • public native int hashCode() :native方法,用於返回對象的哈希碼,主要使用在哈希表中,比如JDK中的HashMap。
  • public boolean equals(Object obj):用於比較2個對象的內存地址是否相等,String類對該方法進行了重寫用戶比較字符串的值是否相等。

對象拷貝

  • protected native Object clone() throws CloneNotSupportedException:naitive方法,用於創建並返回當前對象的一份拷貝。一般情況下,對於任何對象 x,表達式 x.clone() != x 爲true,x.clone().getClass() == x.getClass() 爲true。Object本身沒有實現Cloneable接口,所以不重寫clone方法並且進行調用的話會發生CloneNotSupportedException異常。

對象轉字符串:

  • public String toString():返回類的名字@實例的哈希碼的16進制的字符串。建議Object所有的子類都重寫這個方法。

多線程調度:

  • public final native void notify():native方法,並且不能重寫。喚醒一個在此對象監視器上等待的線程(監視器相當於就是鎖的概念)。如果有多個線程在等待只會任意喚醒一個。
  • public final native void notifyAll():native方法,並且不能重寫。跟notify一樣,唯一的區別就是會喚醒在此對象監視器上等待的所有線程,而不是一個線程。
  • public final native void wait(long timeout) throws InterruptedException:native方法,並且不能重寫。暫停線程的執行。注意:sleep方法沒有釋放鎖,而wait方法釋放了鎖 。timeout是等待時間。
  • public final void wait(long timeout, int nanos) throws InterruptedException:多了nanos參數,這個參數表示額外時間(以毫微秒爲單位,範圍是 0-999999)。 所以超時的時間還需要加上nanos毫秒。
  • public final void wait() throws InterruptedException:跟之前的2個wait方法一樣,只不過該方法一直等待,沒有超時時間這個概念

反射:

  • public final native Class<?> getClass():native方法,用於返回當前運行時對象的Class對象,使用了final關鍵字修飾,故不允許子類重寫。

垃圾回收:

  • protected void finalize() throws Throwable :通知垃圾收集器回收對象。

異常處理

39.Java 中異常處理體系?

Java的異常體系是分爲多層的。

Java異常體系

Throwable 是 Java 語言中所有錯誤或異常的基類。 Throwable 又分爲ErrorException,其中Error是系統內部錯誤,比如虛擬機異常,是程序無法處理的。Exception是程序問題導致的異常,又分爲兩種:

  • CheckedException受檢異常:編譯器會強制檢查並要求處理的異常。
  • RuntimeException運行時異常:程序運行中出現異常,比如我們熟悉的空指針、數組下標越界等等

40.異常的處理方式?

針對異常的處理主要有兩種方式:

異常處理

  • 遇到異常不進行具體處理,而是繼續拋給調用者 (throw,throws)

拋出異常有三種形式,一是 throw,一個 throws,還有一種系統自動拋異常。

throws 用在方法上,後面跟的是異常類,可以跟多個;而 throw 用在方法內,後面跟的是異常對象。

  • try catch 捕獲異常

在catch語句塊中補貨發生的異常,並進行處理。

       try {
            //包含可能會出現異常的代碼以及聲明異常的方法
        }catch(Exception e) {
            //捕獲異常並進行處理
        }finally {                                                       }
            //可選,必執行的代碼
        }

try-catch捕獲異常的時候還可以選擇加上finally語句塊,finally語句塊不管程序是否正常執行,最終它都會必然執行。

41.三道經典異常處理代碼題

題目1

public class TryDemo {
    public static void main(String[] args) {
        System.out.println(test());
    }
    public static int test() {
        try {
            return 1;
        } catch (Exception e) {
            return 2;
        } finally {
            System.out.print("3");
        }
    }
}

執行結果:31。

try、catch。finally 的基礎用法,在 return 前會先執行 finally 語句塊,所以是先輸出 finally 裏的 3,再輸出 return 的 1。

題目2

public class TryDemo {
    public static void main(String[] args) {
        System.out.println(test1());
    }
    public static int test1() {
        try {
            return 2;
        } finally {
            return 3;
        }
    }
}

執行結果:3。

try 返回前先執行 finally,結果 finally 裏不按套路出牌,直接 return 了,自然也就走不到 try 裏面的 return 了。

finally 裏面使用 return 僅存在於面試題中,實際開發這麼寫要挨吊的。

題目3

public class TryDemo {
    public static void main(String[] args) {
        System.out.println(test1());
    }
    public static int test1() {
        int i = 0;
        try {
            i = 2;
            return i;
        } finally {
            i = 3;
        }
    }
}

執行結果:2。

大家可能會以爲結果應該是 3,因爲在 return 前會執行 finally,而 i 在 finally 中被修改爲 3 了,那最終返回 i 不是應該爲 3 嗎?

但其實,在執行 finally 之前,JVM 會先將 i 的結果暫存起來,然後 finally 執行完畢後,會返回之前暫存的結果,而不是返回 i,所以即使 i 已經被修改爲 3,最終返回的還是之前暫存起來的結果 2。

I/O

42.Java 中 IO 流分爲幾種?

流按照不同的特點,有很多種劃分方式。

  • 按照流的流向分,可以分爲輸入流輸出流
  • 按照操作單元劃分,可以劃分爲字節流字符流
  • 按照流的角色劃分爲節點流處理流

Java Io流共涉及40多個類,看上去雜亂,其實都存在一定的關聯, Java I0流的40多個類都是從如下4個抽象類基類中派生出來的。

  • InputStream/Reader: 所有的輸入流的基類,前者是字節輸入流,後者是字符輸入流。
  • OutputStream/Writer: 所有輸出流的基類,前者是字節輸出流,後者是字符輸出流。

IO-操作方式分類-圖片來源參考[2]

IO流用到了什麼設計模式?

其實,Java的IO流體系還用到了一個設計模式——裝飾器模式

InputStream相關的部分類圖如下,篇幅有限,裝飾器模式就不展開說了。

Java IO流用到裝飾器模式

43.既然有了字節流,爲什麼還要有字符流?

其實字符流是由 Java 虛擬機將字節轉換得到的,問題就出在這個過程還比較耗時,並且,如果我們不知道編碼類型就很容易出現亂碼問題。

所以, I/O 流就乾脆提供了一個直接操作字符的接口,方便我們平時對字符進行流操作。如果音頻文件、圖片等媒體文件用字節流比較好,如果涉及到字符的話使用字符流比較好。

44.BIO、NIO、AIO?

BIO、NIO、AIO

BIO(blocking I/O) : 就是傳統的IO,同步阻塞,服務器實現模式爲一個連接一個線程,即客戶端有連接請求時服務器端就需要啓動一個線程進行處理,如果這個連接不做任何事情會造成不必要的線程開銷,可以通過連接池機制改善(實現多個客戶連接服務器)。

BIO、NIO、AIO

BIO方式適用於連接數目比較小且固定的架構,這種方式對服務器資源要求比較高,併發侷限於應用中,JDK1.4 以前的唯一選擇,程序簡單易理解。

NIO :全稱 java non-blocking IO,是指 JDK 提供的新 API。從JDK1.4開始,Java 提供了一系列改進的輸入/輸出的新特性,被統稱爲NIO(即New IO)。

NIO是同步非阻塞的,服務器端用一個線程處理多個連接,客戶端發送的連接請求會註冊到多路複用器上,多路複用器輪詢到連接有IO請求就進行處理:

NIO線程

NIO的數據是面向緩衝區Buffer的,必須從Buffer中讀取或寫入。

所以完整的NIO示意圖:

NIO完整示意圖

可以看出,NIO的運行機制:

  • 每個Channel對應一個Buffer。
  • Selector對應一個線程,一個線程對應多個Channel。
  • Selector會根據不同的事件,在各個通道上切換。
  • Buffer是內存塊,底層是數據。

AIO:JDK 7 引入了 Asynchronous I/O,是異步不阻塞的 IO。在進行 I/O 編程中,常用到兩種模式:Reactor 和 Proactor。Java 的 NIO 就是 Reactor,當有事件觸發時,服務器端得到通知,進行相應的處理,完成後才通知服務端程序啓動線程去處理,一般適用於連接數較多且連接時間較長的應用。

PS:關於同步阻塞IO、同步不阻塞IO、異步不阻塞IO的相關概念可以查看:面試字節,被操作系統問掛了

序列化

45.什麼是序列化?什麼是反序列化?

什麼是序列化,序列化就是把Java對象轉爲二進制流,方便存儲和傳輸。

所以反序列化就是把二進制流恢復成對象

序列化和反序列化

類比我們生活中一些大件物品的運輸,運輸的時候把它拆了打包,用的時候再拆包組裝。

Serializable接口有什麼用?

這個接口只是一個標記,沒有具體的作用,但是如果不實現這個接口,在有些序列化場景會報錯,所以一般建議,創建的JavaBean類都實現 Serializable。

serialVersionUID 又有什麼用?

serialVersionUID 就是起驗證作用。

private static final long serialVersionUID = 1L;

我們經常會看到這樣的代碼,這個 ID 其實就是用來驗證序列化的對象和反序列化對應的對象ID 是否一致。

這個 ID 的數字其實不重要,無論是 1L 還是 IDE自動生成的,只要序列化時候對象的 serialVersionUID 和反序列化時候對象的 serialVersionUID 一致的話就行。

如果沒有顯示指定 serialVersionUID ,則編譯器會根據類的相關信息自動生成一個,可以認爲是一個指紋。

所以如果你沒有定義一個 serialVersionUID, 結果序列化一個對象之後,在反序列化之前把對象的類的結構改了,比如增加了一個成員變量,則此時的反序列化會失敗。

因爲類的結構變了,所以 serialVersionUID 就不一致。

Java 序列化不包含靜態變量?

序列化的時候是不包含靜態變量的。

如果有些變量不想序列化,怎麼辦?

對於不想進行序列化的變量,使用transient關鍵字修飾。

transient 關鍵字的作用是:阻止實例中那些用此關鍵字修飾的的變量序列化;當對象被反序列化時,被 transient 修飾的變量值不會被持久化和恢復。transient 只能修飾變量,不能修飾類和方法。

46.說說有幾種序列化方式?

Java序列化方式有很多,常見的有三種:

Java常見序列化方式

  • Java對象流列化 :Java原生序列化方法即通過Java原生流(InputStream和OutputStream之間的轉化)的方式進行轉化,一般是對象輸出流 ObjectOutputStream和對象輸入流ObjectI叩utStream
  • Json序列化:這個可能是我們最常用的序列化方式,Json序列化的選擇很多,一般會使用jackson包,通過ObjectMapper類來進行一些操作,比如將對象轉化爲byte數組或者將json串轉化爲對象。
  • ProtoBuff序列化:ProtocolBuffer是一種輕便高效的結構化數據存儲格式,ProtoBuff序列化對象可以很大程度上將其壓縮,可以大大減少數據傳輸大小,提高系統性能。

泛型

47.Java 泛型瞭解麼?什麼是類型擦除?介紹一下常用的通配符?

什麼是泛型?

Java 泛型(generics)是 JDK 5 中引入的一個新特性, 泛型提供了編譯時類型安全檢測機制,該機制允許程序員在編譯時檢測到非法的類型。泛型的本質是參數化類型,也就是說所操作的數據類型被指定爲一個參數。

List<Integer> list = new ArrayList<>();

list.add(12);
//這裏直接添加會報錯
list.add("a");
Class<? extends List> clazz = list.getClass();
Method add = clazz.getDeclaredMethod("add", Object.class);
//但是通過反射添加,是可以的
add.invoke(list, "kl");

System.out.println(list);

泛型一般有三種使用方式:泛型類泛型接口泛型方法

泛型類、泛型接口、泛型方法

1.泛型類

//此處T可以隨便寫爲任意標識,常見的如T、E、K、V等形式的參數常用於表示泛型
//在實例化泛型類時,必須指定T的具體類型
public class Generic<T>{

    private T key;

    public Generic(T key) {
        this.key = key;
    }

    public T getKey(){
        return key;
    }
}

如何實例化泛型類:

Generic<Integer> genericInteger = new Generic<Integer>(123456);

2.泛型接口

class GeneratorImpl<T> implements Generator<T>{
    @Override
    public T method() {
        return null;
    }
}

實現泛型接口,指定類型:

class GeneratorImpl<T> implements Generator<String>{
    @Override
    public String method() {
        return "hello";
    }
}

3.泛型方法

   public static < E > void printArray( E[] inputArray )
   {
         for ( E element : inputArray ){
            System.out.printf( "%s ", element );
         }
         System.out.println();
    }

使用:

// 創建不同類型數組: Integer, Double 和 Character
Integer[] intArray = { 1, 2, 3 };
String[] stringArray = { "Hello", "World" };
printArray( intArray  );
printArray( stringArray  );

泛型常用的通配符有哪些?

常用的通配符爲: T,E,K,V,?

  • ? 表示不確定的 java 類型
  • T (type) 表示具體的一個 java 類型
  • K V (key value) 分別代表 java 鍵值中的 Key Value
  • E (element) 代表 Element

什麼是泛型擦除?

所謂的泛型擦除,官方名叫“類型擦除”。

Java 的泛型是僞泛型,這是因爲 Java 在編譯期間,所有的類型信息都會被擦掉。

也就是說,在運行的時候是沒有泛型的。

例如這段代碼,往一羣貓裏放條狗:

LinkedList<Cat> cats = new LinkedList<Cat>();
LinkedList list = cats;  // 注意我在這裏把範型去掉了,但是list和cats是同一個鏈表!
list.add(new Dog());  // 完全沒問題!

因爲Java的範型只存在於源碼裏,編譯的時候給你靜態地檢查一下範型類型是否正確,而到了運行時就不檢查了。上面這段代碼在JRE(Java運行環境)看來和下面這段沒區別:

LinkedList cats = new LinkedList();  // 注意:沒有範型!
LinkedList list = cats;
list.add(new Dog());

爲什麼要類型擦除呢?

主要是爲了向下兼容,因爲JDK5之前是沒有泛型的,爲了讓JVM保持向下兼容,就出了類型擦除這個策略。

註解

48.說一下你對註解的理解?

Java註解本質上是一個標記,可以理解成生活中的一個人的一些小裝扮,比如戴什麼什麼帽子,戴什麼眼鏡。

Java註解和帽子

註解可以標記在類上、方法上、屬性上等,標記自身也可以設置一些值,比如帽子顏色是綠色。

有了標記之後,我們就可以在編譯或者運行階段去識別這些標記,然後搞一些事情,這就是註解的用處。

例如我們常見的AOP,使用註解作爲切點就是運行期註解的應用;比如lombok,就是註解在編譯期的運行。

註解生命週期有三大類,分別是:

  • RetentionPolicy.SOURCE:給編譯器用的,不會寫入 class 文件
  • RetentionPolicy.CLASS:會寫入 class 文件,在類加載階段丟棄,也就是運行的時候就沒這個信息了
  • RetentionPolicy.RUNTIME:會寫入 class 文件,永久保存,可以通過反射獲取註解信息

所以我上文寫的是解析的時候,沒寫具體是解析啥,因爲不同的生命週期的解析動作是不同的。

像常見的:

Override註解

就是給編譯器用的,編譯器編譯的時候檢查沒問題就over了,class文件裏面不會有 Override 這個標記。

再比如 Spring 常見的 Autowired ,就是 RUNTIME 的,所以在運行的時候可以通過反射得到註解的信息,還能拿到標記的值 required 。

Autowired註解

反射

49.什麼是反射?應用?原理?

什麼是反射?

我們通常都是利用new方式來創建對象實例,這可以說就是一種“正射”,這種方式在編譯時候就確定了類型信息。

而如果,我們想在時候動態地獲取類信息、創建類實例、調用類方法這時候就要用到反射

通過反射你可以獲取任意一個類的所有屬性和方法,你還可以調用這些方法和屬性。

反射最核心的四個類:

Java反射相關類

反射的應用場景?

一般我們平時都是在在寫業務代碼,很少會接觸到直接使用反射機制的場景。

但是,這並不代表反射沒有用。相反,正是因爲反射,你才能這麼輕鬆地使用各種框架。像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射機制。

像Spring裏的很多 註解 ,它真正的功能實現就是利用反射。

就像爲什麼我們使用 Spring 的時候 ,一個@Component註解就聲明瞭一個類爲 Spring Bean 呢?爲什麼通過一個 @Value註解就讀取到配置文件中的值呢?究竟是怎麼起作用的呢?

這些都是因爲我們可以基於反射操作類,然後獲取到類/屬性/方法/方法的參數上的註解,註解這裏就有兩個作用,一是標記,我們對註解標記的類/屬性/方法進行對應的處理;二是註解本身有一些信息,可以參與到處理的邏輯中。

反射的原理?

我們都知道Java程序的執行分爲編譯和運行兩步,編譯之後會生成字節碼(.class)文件,JVM進行類加載的時候,會加載字節碼文件,將類型相關的所有信息加載進方法區,反射就是去獲取這些信息,然後進行各種操作。

JDK1.8新特性

JDK已經出到17了,但是你迭代你的版本,我用我的8。JDK1.8的一些新特性,當然現在也不新了,其實在工作中已經很常用了。

50.JDK1.8都有哪些新特性?

JDK1.8有不少新特性,我們經常接觸到的新特性如下:

JDK1.8主要新特性

  • 接口默認方法:Java 8允許我們給接口添加一個非抽象的方法實現,只需要使用 default關鍵字修飾即可

  • Lambda 表達式和函數式接口:Lambda 表達式本質上是一段匿名內部類,也可以是一段可以傳遞的代碼。Lambda 允許把函數作爲一個方法的參數(函數作爲參數傳遞到方法中),使用 Lambda 表達式使代碼更加簡潔,但是也不要濫用,否則會有可讀性等問題,《Effective Java》作者 Josh Bloch 建議使用 Lambda 表達式最好不要超過3行。

  • Stream API:用函數式編程方式在集合類上進行復雜操作的工具,配合Lambda表達式可以方便的對集合進行處理。

    Java8 中處理集合的關鍵抽象概念,它可以指定你希望對集合進行的操作,可以執行非常複雜的查找、過濾和映射數據等操作。使用Stream API 對集合數據進行操作,就類似於使用 SQL 執行的數據庫查詢。也可以使用 Stream API 來並行執行操作。

    簡而言之,Stream API 提供了一種高效且易於使用的處理數據的方式。

  • 日期時間API:Java 8 引入了新的日期時間API改進了日期時間的管理。

  • Optional 類:用來解決空指針異常的問題。很久以前 Google Guava 項目引入了 Optional 作爲解決空指針異常的一種方式,不贊成代碼被 null 檢查的代碼污染,期望程序員寫整潔的代碼。受Google Guava的鼓勵,Optional 現在是Java 8庫的一部分。

51.Lambda 表達式瞭解多少?

Lambda 表達式本質上是一段匿名內部類,也可以是一段可以傳遞的代碼。

比如我們以前使用Runnable創建並運行線程:

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Thread is running before Java8!");
            }
        }).start();

這是通過內部類的方式來重寫run方法,使用Lambda表達式,還可以更加簡潔:

new Thread( () -> System.out.println("Thread is running since Java8!") ).start();

當然不是每個接口都可以縮寫成 Lambda 表達式。只有那些函數式接口(Functional Interface)才能縮寫成 Lambda 表示式。

所謂函數式接口(Functional Interface)就是隻包含一個抽象方法的聲明。針對該接口類型的所有 Lambda 表達式都會與這個抽象方法匹配。

Java8有哪些內置函數式接口?

JDK 1.8 API 包含了很多內置的函數式接口。其中就包括我們在老版本中經常見到的 ComparatorRunnable,Java 8 爲他們都添加了 @FunctionalInterface 註解,以用來支持 Lambda 表達式。

除了這兩個之外,還有Callable、Predicate、Function、Supplier、Consumer等等。

52.Optional瞭解嗎?

Optional是用於防範NullPointerException

可以將 Optional 看做是包裝對象(可能是 null, 也有可能非 null)的容器。當我們定義了 一個方法,這個方法返回的對象可能是空,也有可能非空的時候,我們就可以考慮用 Optional 來包裝它,這也是在 Java 8 被推薦使用的做法。

Optional<String> optional = Optional.of("bam");

optional.isPresent();           // true
optional.get();                 // "bam"
optional.orElse("fallback");    // "bam"

optional.ifPresent((s) -> System.out.println(s.charAt(0)));     // "b"

53.Stream 流用過嗎?

Stream 流,簡單來說,使用 java.util.Stream 對一個包含一個或多個元素的集合做各種操作。這些操作可能是 中間操作 亦或是 終端操作。 終端操作會返回一個結果,而中間操作會返回一個 Stream 流。

Stream流一般用於集合,我們對一個集合做幾個常見操作:

List<String> stringCollection = new ArrayList<>();
stringCollection.add("ddd2");
stringCollection.add("aaa2");
stringCollection.add("bbb1");
stringCollection.add("aaa1");
stringCollection.add("bbb3");
stringCollection.add("ccc");
stringCollection.add("bbb2");
stringCollection.add("ddd1");
  • Filter 過濾
stringCollection
    .stream()
    .filter((s) -> s.startsWith("a"))
    .forEach(System.out::println);

// "aaa2", "aaa1"

  • Sorted 排序
stringCollection
    .stream()
    .sorted()
    .filter((s) -> s.startsWith("a"))
    .forEach(System.out::println);

// "aaa1", "aaa2"
  • Map 轉換
stringCollection
    .stream()
    .map(String::toUpperCase)
    .sorted((a, b) -> b.compareTo(a))
    .forEach(System.out::println);

// "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"
  • Match 匹配
// 驗證 list 中 string 是否有以 a 開頭的, 匹配到第一個,即返回 true
boolean anyStartsWithA =
    stringCollection
        .stream()
        .anyMatch((s) -> s.startsWith("a"));

System.out.println(anyStartsWithA);      // true

// 驗證 list 中 string 是否都是以 a 開頭的
boolean allStartsWithA =
    stringCollection
        .stream()
        .allMatch((s) -> s.startsWith("a"));

System.out.println(allStartsWithA);      // false

// 驗證 list 中 string 是否都不是以 z 開頭的,
boolean noneStartsWithZ =
    stringCollection
        .stream()
        .noneMatch((s) -> s.startsWith("z"));

System.out.println(noneStartsWithZ);      // true
  • Count 計數

count 是一個終端操作,它能夠統計 stream 流中的元素總數,返回值是 long 類型。

// 先對 list 中字符串開頭爲 b 進行過濾,讓後統計數量
long startsWithB =
    stringCollection
        .stream()
        .filter((s) -> s.startsWith("b"))
        .count();

System.out.println(startsWithB);    // 3
  • Reduce

Reduce 中文翻譯爲:減少、縮小。通過入參的 Function,我們能夠將 list 歸約成一個值。它的返回類型是 Optional 類型。

Optional<String> reduced =
    stringCollection
        .stream()
        .sorted()
        .reduce((s1, s2) -> s1 + "#" + s2);

reduced.ifPresent(System.out::println);
// "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"

以上是常見的幾種流式操作,還有其它的一些流式操作,可以幫助我們更便捷地處理集合數據。

Java Stream流

簡單事情重複做,重複事情認真做,認真事情有創造性地做。

我是三分惡,一個能文能武的程序員,點贊關注不迷路,咱們下期見!



參考:

[1]. Java 基礎高頻面試題(2021年最新版)

[2].2.7w字!Java基礎面試題/知識點總結!(2021 最新版)

[3].面試題系列第8篇:談談String、StringBuffer、StringBuilder的區別?

[4].面試題系列第2篇:new String()創建幾個對象?有你不知道的

[5].面試題系列第6篇:JVM字符串常量池及String的intern方法詳解?

[6]. 2W字,52道Java熱點必考題,含答案,圖文並茂

[7]. BIO、NIO、AIO、Netty面試題(總結最全面的面試題!!!)

[8]. Java基礎知識面試題(2020最新版)

[9]. Java基礎面試題(2021最新版)

[10]. 乾貨 | Java8 新特性教程

[11].面向對象和麪向過程分別是什麼?

[12]. 《瘋狂Java講義》

[13].3. 彤哥說netty系列之Java BIO NIO AIO進化史

[14].什麼是泛型擦除?

[15].學會反射後,我被錄取了(乾貨)


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章