面渣逆襲:Java基礎五十三問,快來看看有沒有你不會的!
大家好,我是老三, 面渣逆襲 系列繼續。這節我們回到夢開始的地方——Java基礎,其實過了萌新階段,面試問基礎就問的不多,但是保不齊突然問一下。想一下,總不能張口高並發、閉口分散式,結果什麼是面向對象,說不清,那多少有點魔幻。所以趕緊來看看,這些基礎有沒有你不會的!
Java概述
1.什麼是Java?
PS:碎慫Java,有啥好介紹的。哦,面試啊。
Java是一門面向對象的程式語言,不僅吸收了C++語言的各種優點,還摒棄了C++里難以理解的多繼承、指針等概念,因此Java語言具有功能強大和簡單易用兩個特徵。Java語言作為靜態面向對象程式語言的優秀代表,極好地實現了面向對象理論,允許程式設計師以優雅的思維方式進行複雜的編程 。
2.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。
4.說說什麼是跨平台性?原理是什麼
所謂跨平台性,是指Java語言編寫的程式,一次編譯後,可以在多個系統平台上運行。
實現原理:Java程式是通過Java虛擬機在系統平台上運行的,只要該系統可以安裝相應的Java虛擬機,該系統就可以運行java程式。
5.什麼是位元組碼?採用位元組碼的好處是什麼?
所謂的位元組碼,就是Java程式經過編譯之類產生的.class文件,位元組碼能夠被虛擬機識別,從而實現Java程式的跨平台性。
Java 程式從源程式碼到運行主要有三步:
- 編譯:將我們的程式碼(.java)編譯成虛擬機可以識別理解的位元組碼(.class)
- 解釋:虛擬機執行Java位元組碼,將位元組碼翻譯成機器能識別的機器碼
- 執行:對應的機器執行二進位機器碼
只需要把Java程式編譯成Java虛擬機能識別的Java位元組碼,不同的平台安裝對應的Java虛擬機,這樣就可以可以實現Java語言的平台無關性。
6.為什麼說 Java 語言「編譯與解釋並存」?
高級程式語言按照程式的執行方式分為編譯型和解釋型兩種。
簡單來說,編譯型語言是指編譯器針對特定的作業系統將源程式碼一次性翻譯成可被該平台執行的機器碼;解釋型語言是指解釋器對源程式逐行解釋成特定平台的機器碼並立即執行。
比如,你想讀一本外國的小說,你可以找一個翻譯人員幫助你翻譯,有兩種選擇方式,你可以先等翻譯人員將全本的小說(也就是源碼)都翻譯成漢語,再去閱讀,也可以讓翻譯人員翻譯一段,你在旁邊閱讀一段,慢慢把書讀完。
Java 語言既具有編譯型語言的特徵,也具有解釋型語言的特徵,因為 Java 程式要經過先編譯,後解釋兩個步驟,由 Java 編寫的程式需要先經過編譯步驟,生成位元組碼(\*.class
文件),這種位元組碼必須再經過JVM,解釋成作業系統能識別的機器碼,在由作業系統執行。因此,我們可以認為 Java 語言編譯與解釋並存。
基礎語法
7.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 所有的數值型變數可以相互轉換,當把一個表數範圍小的數值或變數直接賦給另一個表數範圍大的變數時,可以進行自動類型轉換;反之,需要強制轉換。
這就好像,小杯里的水倒進大杯沒問題,但大杯的水倒進小杯就不行了,可能會溢出。
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 程式返回,不再執行下面的程式碼(結束當前的方法 直接返回)
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.面向對象有哪些特性
-
封裝
封裝把⼀個對象的屬性私有化,同時提供⼀些可以被外界訪問的屬性的⽅法。
-
繼承
繼承是使⽤已存在的類的定義作為基礎創建新的類,新類的定義可以增加新的屬性或新的方法,也可以繼承父類的屬性和方法。通過繼承可以很方便地進行程式碼復用。
關於繼承有以下三個要點:
-
⼦類擁有⽗類對象所有的屬性和⽅法(包括私有屬性和私有⽅法),但是⽗類中的私有屬性和⽅法⼦類是⽆法訪問,只是擁有。
-
⼦類可以擁有⾃⼰屬性和⽅法,即⼦類可以對⽗類進⾏擴展。
-
⼦類可以⽤⾃⼰的⽅式實現⽗類的⽅法。
-
多態
所謂多態就是指程式中定義的引⽤變數所指向的具體類型和通過該引⽤變數發出的⽅法調⽤在編程時並不確定,⽽是在程式運⾏期間才確定,即⼀個引⽤變數到底會指向哪個類的實例對象,該引⽤變數發出的⽅法調⽤到底是哪個類中實現的⽅法,必須在由程式運⾏期間才能決定。
在 Java 中有兩種形式可以實現多態:繼承(多個⼦類對同⼀⽅法的重寫)和接⼝(實現接⼝並覆蓋接⼝中同⼀⽅法)。
17.重載(overload)和重寫(override)的區別?
方法的重載和重寫都是實現多態的方式,區別在於前者實現的是編譯時的多態性,而後者實現的是運行時的多態性。
-
重載發生在一個類中,同名的方法如果有不同的參數列表(參數類型不同、參數個數不同或者二者都不同)則視為重載;
-
重寫發生在子類與父類之間,重寫要求子類被重寫方法與父類被重寫方法有相同的返回類型,比父類被重寫方法更好訪問,不能比父類被重寫方法聲明更多的異常(里氏代換原則)。
方法重載的規則:
- 方法名一致,參數列表中參數的順序,類型,個數不同。
- 重載與方法的返回值無關,存在於父類和子類,同類中。
- 可以拋出不同的異常,可以有不同修飾符。
18.訪問修飾符public、private、protected、以及不寫(默認)時的區別?
Java中,可以使用訪問控制符來保護對類、變數、方法和構造方法的訪問。Java 支援 4 種不同的訪問許可權。
- default (即默認,什麼也不寫): 在同一包內可見,不使用任何修飾符。可以修飾在類、介面、變數、方法。
- private : 在同一類內可見。可以修飾變數、方法。注意:不能修飾類(外部類)
- public : 對所有類可見。可以修飾類、介面、變數、方法
- protected : 對同一包內的類和所有子類可見。可以修飾變數、方法。注意:不能修飾類(外部類)。
19.this關鍵字有什麼作用?
this是自身的一個對象,代表對象本身,可以理解為:指向對象本身的一個指針。
this的用法在Java中大體可以分為3種:
-
普通的直接引用,this相當於是指向當前對象本身
-
形參與成員變數名字重名,用this來區分:
public Person(String name,int age){
this.name=name;
this.age=age;
}
- 引用本類的構造函數
20.抽象類(abstract class)和介面(interface)有什麼區別?
-
接⼝的⽅法默認是 public ,所有⽅法在接⼝中不能有實現(Java 8 開始接⼝⽅法可以有默認實現),⽽抽象類可以有⾮抽象的⽅法。
-
接⼝中除了 static 、 final 變數,不能有其他變數,⽽抽象類中則不⼀定。
-
⼀個類可以實現多個接⼝,但只能實現⼀個抽象類。接⼝⾃⼰本身可以通過 extends 關鍵字擴展多個接⼝。
-
接⼝⽅法默認修飾符是 public ,抽象⽅法可以有 public 、 protected 和 default 這些修飾符(抽象⽅法就是為了被重寫所以不能使⽤ private 關鍵字修飾!)。
-
從設計層⾯來說,抽象是對類的抽象,是⼀種模板設計,⽽接⼝是對⾏為的抽象,是⼀種⾏為的規範。
在 JDK8 中,接⼝也可以定義靜態⽅法,可以直接⽤接⼝名調⽤。實現類和實現是不可以調⽤的。如果同時實現兩個接⼝,接⼝中定義了⼀樣的默認⽅法,則必須重寫,不然會報錯。
jdk9 的接⼝被允許定義私有⽅法 。
總結⼀下 jdk7~jdk9 Java 中接⼝的變化:
-
在 jdk 7 或更早版本中,接⼝⾥⾯只能有常量變數和抽象⽅法。這些接⼝⽅法必須由選擇實現接⼝的類實現。
-
jdk 8 的時候接⼝可以有默認⽅法和靜態⽅法功能。
-
jdk 9 在接⼝中引⼊了私有⽅法和私有靜態⽅法。
21.成員變數與局部變數的區別有哪些?
-
從語法形式上看:成員變數是屬於類的,⽽局部變數是在⽅法中定義的變數或是⽅法的參數;成員變數可以被 public , private , static 等修飾符所修飾,⽽局部變數不能被訪問控制修飾符及 static 所修飾;但是,成員變數和局部變數都能被 final 所修飾。
-
從變數在記憶體中的存儲⽅式來看:如果成員變數是使⽤ static 修飾的,那麼這個成員變數是屬於類的,如果沒有使⽤ static 修飾,這個成員變數是屬於實例的。對象存於堆記憶體,如果局部變數類型為基本數據類型,那麼存儲在棧記憶體,如果為引⽤數據類型,那存放的是指向堆記憶體對象的引⽤或者是指向常量池中的地址。
-
從變數在記憶體中的⽣存時間上看:成員變數是對象的⼀部分,它隨著對象的創建⽽存在,⽽局部變數隨著⽅法的調⽤⽽⾃動消失。
-
成員變數如果沒有被賦初值:則會⾃動以類型的默認值⽽賦值(⼀種情況例外:被 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
一張圖說明:
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 的記憶體分為堆和棧,其中棧中存儲了基本數據類型和引用數據類型實例的地址,也就是對象地址。
而對象所佔的空間是在堆中開闢的,所以傳遞的時候可以理解為把變數存儲的對象地址給傳遞過去,因此引用類型也是值傳遞。
29.深拷貝和淺拷貝?
- 淺拷貝:僅拷貝被拷貝對象的成員變數的值,也就是基本數據類型變數的值,和引用數據類型變數的地址值,而對於引用類型變數指向的堆中的對象不會拷貝。
- 深拷貝:完全拷貝一個對象,拷貝被拷貝對象的成員變數的值,堆中的對象也會拷貝一份。
例如現在有一個order對象,裡面有一個products列表,它的淺拷貝和深拷貝的示意圖:
因此深拷貝是安全的,淺拷貝的話如果有引用類型,那麼拷貝後對象,引用類型變數修改,會影響原對象。
淺拷貝如何實現呢?
Object類提供的clone()方法可以非常簡單地實現對象的淺拷貝。
深拷貝如何實現呢?
- 重寫克隆方法:重寫克隆方法,引用類型變數單獨克隆,這裡可能會涉及多層遞歸。
- 序列化:可以先講原對象序列化,再反序列化成拷貝對象。
30.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 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對象。如果拼接多次,那麼會生成多個中間對象。
記憶體如下:
在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。
很簡單,就是判斷下值是否在快取範圍之內,如果是的話去 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了很多情況),其實剩下的就是一個簡單的字元串遍歷計算,不過計算方式有點反常規,是用負的值累減。
Object
38.Object 類的常見方法?
Object 類是一個特殊的類,是所有類的父類,也就是說所有類都可以調用它的方法。它主要提供了以下 11 個方法,大概可以分為六類:
對象比較:
- 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的異常體系是分為多層的。
Throwable
是 Java 語言中所有錯誤或異常的基類。 Throwable 又分為Error
和Exception
,其中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流用到了什麼設計模式?
其實,Java的IO流體系還用到了一個設計模式——裝飾器模式。
InputStream相關的部分類圖如下,篇幅有限,裝飾器模式就不展開說了。
43.既然有了位元組流,為什麼還要有字元流?
其實字元流是由 Java 虛擬機將位元組轉換得到的,問題就出在這個過程還比較耗時,並且,如果我們不知道編碼類型就很容易出現亂碼問題。
所以, I/O 流就乾脆提供了一個直接操作字元的介面,方便我們平時對字元進行流操作。如果音頻文件、圖片等媒體文件用位元組流比較好,如果涉及到字元的話使用字元流比較好。
44.BIO、NIO、AIO?
BIO(blocking I/O) : 就是傳統的IO,同步阻塞,伺服器實現模式為一個連接一個執行緒,即客戶端有連接請求時伺服器端就需要啟動一個執行緒進行處理,如果這個連接不做任何事情會造成不必要的執行緒開銷,可以通過連接池機制改善(實現多個客戶連接伺服器)。
BIO方式適用於連接數目比較小且固定的架構,這種方式對伺服器資源要求比較高,並發局限於應用中,JDK1.4 以前的唯一選擇,程式簡單易理解。
NIO :全稱 java non-blocking IO,是指 JDK 提供的新 API。從JDK1.4開始,Java 提供了一系列改進的輸入/輸出的新特性,被統稱為NIO(即New IO)。
NIO是同步非阻塞的,伺服器端用一個執行緒處理多個連接,客戶端發送的連接請求會註冊到多路復用器上,多路復用器輪詢到連接有IO請求就進行處理:
NIO的數據是面向緩衝區Buffer的,必須從Buffer中讀取或寫入。
所以完整的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原生流(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註解本質上是一個標記,可以理解成生活中的一個人的一些小裝扮,比如戴什麼什麼帽子,戴什麼眼鏡。
註解可以標記在類上、方法上、屬性上等,標記自身也可以設置一些值,比如帽子顏色是綠色。
有了標記之後,我們就可以在編譯或者運行階段去識別這些標記,然後搞一些事情,這就是註解的用處。
例如我們常見的AOP,使用註解作為切點就是運行期註解的應用;比如lombok,就是註解在編譯期的運行。
註解生命周期有三大類,分別是:
- RetentionPolicy.SOURCE:給編譯器用的,不會寫入 class 文件
- RetentionPolicy.CLASS:會寫入 class 文件,在類載入階段丟棄,也就是運行的時候就沒這個資訊了
- RetentionPolicy.RUNTIME:會寫入 class 文件,永久保存,可以通過反射獲取註解資訊
所以我上文寫的是解析的時候,沒寫具體是解析啥,因為不同的生命周期的解析動作是不同的。
像常見的:
就是給編譯器用的,編譯器編譯的時候檢查沒問題就over了,class文件裡面不會有 Override 這個標記。
再比如 Spring 常見的 Autowired ,就是 RUNTIME 的,所以在運行的時候可以通過反射得到註解的資訊,還能拿到標記的值 required 。
反射
49.什麼是反射?應用?原理?
什麼是反射?
我們通常都是利用new
方式來創建對象實例,這可以說就是一種「正射」,這種方式在編譯時候就確定了類型資訊。
而如果,我們想在時候動態地獲取類資訊、創建類實例、調用類方法這時候就要用到反射。
通過反射你可以獲取任意一個類的所有屬性和方法,你還可以調用這些方法和屬性。
反射最核心的四個類:
反射的應用場景?
一般我們平時都是在在寫業務程式碼,很少會接觸到直接使用反射機制的場景。
但是,這並不代表反射沒有用。相反,正是因為反射,你才能這麼輕鬆地使用各種框架。像 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有不少新特性,我們經常接觸到的新特性如下:
-
介面默認方法: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 包含了很多內置的函數式介面。其中就包括我們在老版本中經常見到的 Comparator 和 Runnable,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"
以上是常見的幾種流式操作,還有其它的一些流式操作,可以幫助我們更便捷地處理集合數據。
簡單事情重複做,重複事情認真做,認真事情有創造性地做。
我是三分惡,一個能文能武的程式設計師,點贊、關注不迷路,咱們下期見!
參考:
[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].學會反射後,我被錄取了(乾貨)