Java 基礎(chǔ)
一、Java 概述
1.什么是 Java?
Java 是一門面向?qū)ο蟮木幊陶Z言,不僅吸收了 C++語言的各種優(yōu)點,還摒棄了 C++里難以理解的多繼承、指針等概念,因此 Java 語言具有功能強大和簡單易用兩個特征。Java 語言作為靜態(tài)面向?qū)ο缶幊陶Z言的優(yōu)秀代表,極好地實現(xiàn)了面向?qū)ο罄碚摚试S程序員以優(yōu)雅的思維方式進行復(fù)雜的編程 。
2.Java 語言有哪些特點?
Java 語言有很多優(yōu)秀(可吹)的特點,以下幾個是比較突出的:
- 面向?qū)ο螅ǚ庋b,繼承,多態(tài));
- 平臺無關(guān)性,平臺無關(guān)性的具體表現(xiàn)在于,Java 是“一次編寫,到處運行(Write Once,Run any Where)”的語言,因此采用 Java 語言編寫的程序具有很好的可移植性,而保證這一點的正是 Java 的虛擬機機制。在引入虛擬機之后,Java 語言在不同的平臺上運行不需要重新編譯。
- 支持多線程。C++ 語言沒有內(nèi)置的多線程機制,因此必須調(diào)用操作系統(tǒng)的多線程功能來進行多線程程序設(shè)計,而 Java 語言卻提供了多線程支持;
- 編譯與解釋并存;
3.JDK、JRE、JVM 三者之間的關(guān)系?
- JVM:Java Virtual Machine,Java 虛擬機,Java 程序運行在 Java 虛擬機上。針對不同系統(tǒng)的實現(xiàn)(Windows,Linux,macOS)不同的 JVM,因此 Java 語言可以實現(xiàn)跨平臺。
- JRE:Java 運?時環(huán)境。它是運?已編譯 Java 程序所需的所有內(nèi)容的集合,包括 Java 虛擬機(JVM),Java 類庫,Java 命令和其他的?些基礎(chǔ)構(gòu)件。但是,它不能?于創(chuàng)建新程序。
- JDK: Java Development Kit,它是功能?全的 Java SDK。它擁有 JRE 所擁有的?切,還有編譯器(javac)和?具(如 javadoc 和 jdb)。它能夠創(chuàng)建和編譯程序。
簡單來說,JDK 包含 JRE,JRE 包含 JVM。
- JDK = JRE + 多種Java開發(fā)工具
- JRE = JVM + 各種類庫
- 這三者的關(guān)系是一層層的嵌套關(guān)系,
JDK > JRE > JVM
4.說說什么是跨平臺性?原理是什么
所謂跨平臺性,是指 Java 語言編寫的程序,一次編譯后,可以在多個系統(tǒng)平臺上運行。
實現(xiàn)原理:Java 程序是通過 Java 虛擬機在系統(tǒng)平臺上運行的,只要該系統(tǒng)可以安裝相應(yīng)的 Java 虛擬機,該系統(tǒng)就可以運行 java 程序。
5.什么是字節(jié)碼?采用字節(jié)碼的好處是什么?
所謂的字節(jié)碼,就是 Java 程序經(jīng)過編譯之類產(chǎn)生的.class 文件,字節(jié)碼能夠被虛擬機識別,從而實現(xiàn) Java 程序的跨平臺性。
Java 程序從源代碼到運行主要有三步:
- 編譯:將我們的代碼(.java)編譯成虛擬機可以識別理解的字節(jié)碼(.class)
- 解釋:虛擬機執(zhí)行 Java 字節(jié)碼,將字節(jié)碼翻譯成機器能識別的機器碼
-
執(zhí)行:對應(yīng)的機器執(zhí)行二進制機器碼
6.為什么說 Java 語言“編譯與解釋并存”?
高級編程語言按照程序的執(zhí)行方式分為編譯型和解釋型兩種。
簡單來說,編譯型語言是指編譯器針對特定的操作系統(tǒng)將源代碼一次性翻譯成可被該平臺執(zhí)行的機器碼;解釋型語言是指解釋器對源程序逐行解釋成特定平臺的機器碼并立即執(zhí)行。
比如,你想讀一本外國的小說,你可以找一個翻譯人員幫助你翻譯,有兩種選擇方式,你可以先等翻譯人員將全本的小說(也就是源碼)都翻譯成漢語,再去閱讀,也可以讓翻譯人員翻譯一段,你在旁邊閱讀一段,慢慢把書讀完。
Java 語言既具有編譯型語言的特征,也具有解釋型語言的特征,因為 Java 程序要經(jīng)過先編譯,后解釋兩個步驟,由 Java 編寫的程序需要先經(jīng)過編譯步驟,生成字節(jié)碼(\*.class
文件),這種字節(jié)碼必須再經(jīng)過 JVM,解釋成操作系統(tǒng)能識別的機器碼,在由操作系統(tǒng)執(zhí)行。因此,我們可以認為 Java 語言編譯與解釋并存。
二、基礎(chǔ)語法
7.Java 有哪些數(shù)據(jù)類型?
定義:Java 語言是強類型語言,對于每一種數(shù)據(jù)都定義了明確的具體的數(shù)據(jù)類型,在內(nèi)存中分配了不同大小的內(nèi)存空間。
Java 語言數(shù)據(jù)類型分為兩種:基本數(shù)據(jù)類型和引用數(shù)據(jù)類型。
基本數(shù)據(jù)類型:
- 數(shù)值型
- 整數(shù)類型(byte、short、long)
- 浮點類型(float、long)
- 字符型(char)
- 布爾型(boolean)
Java 基本數(shù)據(jù)類型范圍和默認值:
基本類型 | 位數(shù) | 字節(jié) | 默認值 |
---|---|---|---|
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 |
引用數(shù)據(jù)類型:
- 類(class)
- 接口(interface)
- 數(shù)組([])
8.自動類型轉(zhuǎn)換、強制類型轉(zhuǎn)換?看看這幾行代碼?
Java 所有的數(shù)值型變量可以相互轉(zhuǎn)換,當把一個表數(shù)范圍小的數(shù)值或變量直接賦給另一個表數(shù)范圍大的變量時,可以進行自動類型轉(zhuǎn)換;反之,需要強制轉(zhuǎn)換。
這就好像,小杯里的水倒進大杯沒問題,但大杯的水倒進小杯就不行了,可能會溢出。
float f=3.4
,對嗎?
不正確。3.4 是單精度數(shù),將雙精度型(double)賦值給浮點型(float)屬于下轉(zhuǎn)型(down-casting,也稱為窄化)會造成精度損失,因此需要強制類型轉(zhuǎn)換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 運算結(jié)果也是 int 型,需要強制轉(zhuǎn)換類型才能賦值給 short 型。
而 short s1 = 1; s1 += 1;
可以正確編譯,因為 s1+= 1;相當于 s1 = (short(s1 + 1);其中有隱含的強制類型轉(zhuǎn)換。
9.什么是自動拆箱/封箱?
- 裝箱:將基本類型用它們對應(yīng)的引用類型包裝起來;
- 拆箱:將包裝類型轉(zhuǎn)換為基本數(shù)據(jù)類型;
Java 可以自動對基本數(shù)據(jù)類型和它們的包裝類進行裝箱和拆箱。
舉例:
Integer i = 10; //裝箱
int n = i; //拆箱
10.&和&&有什么區(qū)別?
&運算符有兩種用法:短路與、邏輯與。
&&運算符是短路與運算。邏輯與跟短路與的差別是非常巨大的,雖然二者都要求運算符左右兩端的布爾值都是 true 整個表達式的值才是 true。
&&之所以稱為短路運算是因為,如果&&左邊的表達式的值是 false,右邊的表達式會被直接短路掉,不會進行運算。很多時候我們可能都需要用&&而不是&。
例如:在驗證用戶登錄時判定用戶名不是 null 而且不是空字符串,應(yīng)當寫為username != null &&!username.equals("")
,二者的順序不能交換,更不能用&運算符,因為第一個條件如果不成立,根本不能進行字符串的 equals
比較,否則會產(chǎn)生 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 的區(qū)別及作用?
- break 跳出整個循環(huán),不再執(zhí)行循環(huán)(結(jié)束當前的循環(huán)體)
- continue 跳出本次循環(huán),繼續(xù)執(zhí)行下次循環(huán)(結(jié)束正在執(zhí)行的循環(huán) 進入下一個循環(huán)條件)
- return 程序返回,不再執(zhí)行下面的代碼(結(jié)束當前的方法 直接返回)
13.用最有效率的方法計算 2 乘以 8?
2 << 3
。**位運算,**數(shù)字的二進制位左移三位相當于乘以 2 的三次方。
14.說說自增自減運算?看下這幾個代碼運行結(jié)果?
在寫代碼的過程中,常見的一種情況是需要某個整數(shù)類型變量增加 1 或減少 1,Java 提供了一種特殊的運算符,用于這種表達式,叫做自增運算符(++)和自減運算符(–)。
++和–運算符可以放在變量之前,也可以放在變量之后。
當運算符放在變量之前時(前綴),先自增/減,再賦值;當運算符放在變量之后時(后綴),先賦值,再自增/減。
例如,當 b = ++a
時,先自增(自己增加 1),再賦值(賦值給 b);當 b = a++
時,先賦值(賦值給 b),再自增(自己增加 1)。也就是,++a 輸出的是 a+1 的值,a++輸出的是 a 值。
用一句口訣就是:“符號在前就先加/減,符號在后就后加/減”。
看一下這段代碼運行結(jié)果?
int i = 1;
i = i++;
System.out.println(i);
答案是 1。有點離譜對不對。
對于 JVM 而言,它對自增運算的處理,是會先定義一個臨時變量來接收 i 的值,然后進行自增運算,最后又將臨時變量賦給了值為 2 的 i,所以最后的結(jié)果為 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;
}
三、面向?qū)ο?/h3>
15.解釋下什么是面向?qū)ο??面向?qū)ο蠛兔嫦蜻^程的區(qū)別?
- 面向?qū)ο螅菏擒浖_發(fā)方法,一種編程范式,面向?qū)ο笫且环N對現(xiàn)實世界理解和抽象的方法,是計算機編程技術(shù)發(fā)展到一定階段后的產(chǎn)物。例如:現(xiàn)實中的事物都抽象為“對象”。每個對象是唯一的,且都可以擁有它的屬性與行為。
- ?向過程 :面向過程就是分析出解決問題所需要的步驟,然后用函數(shù)把這些步驟一步一步實現(xiàn),使用的時候再一個一個的一次調(diào)用就可以。
- 面向?qū)ο螅好嫦驅(qū)ο螅褬?gòu)成問題的事務(wù)分解成各個對象,而建立對象的目的也不是為了完成一個個步驟,而是為了描述某個事件在解決整個問題的過程所發(fā)生的行為。目的是為了寫出通用的代碼,加強代碼的重用,屏蔽差異性。
16.面向?qū)ο蟮娜筇匦裕?/h4>
- 封裝:封裝把?個對象的屬性私有化,同時提供?些可以被外界訪問的屬性的?法。
- 繼承:繼承是使?已存在的類的定義作為基礎(chǔ)創(chuàng)建新的類,新類的定義可以增加新的屬性或新的方法,也可以繼承父類的屬性和方法。通過繼承可以很方便地進行代碼復(fù)用。
關(guān)于繼承有以下三個要點:
- ?類擁有?類對象所有的屬性和?法(包括私有屬性和私有?法),但是?類中的私有屬性和?法?類是?法訪問,只是擁有。
- ?類可以擁有??屬性和?法,即?類可以對?類進?擴展。
- ?類可以???的?式實現(xiàn)?類的?法
- 多態(tài):是同一個行為具有多個不同表現(xiàn)形式或形態(tài)的能力。
在 Java 中有兩種形式可以實現(xiàn)多態(tài):繼承(多個?類對同??法的重寫)和接?(實現(xiàn)接?并覆蓋接?中同??法)
17.重載(overload)和重寫(override)的區(qū)別?
方法的重載和重寫都是實現(xiàn)多態(tài)的方式,區(qū)別在于前者實現(xiàn)的是編譯時的多態(tài)性,而后者實現(xiàn)的是運行時的多態(tài)性。
- 重載發(fā)生在一個類中,同名的方法如果有不同的參數(shù)列表(參數(shù)類型不同、參數(shù)個數(shù)不同或者二者都不同)則視為重載
- 重寫發(fā)生在子類與父類之間,重寫要求子類被重寫方法與父類被重寫方法有相同的返回類型,比父類被重寫方法更好訪問,不能比父類被重寫方法聲明更多的異常(里氏代換原則)。
方法重載的規(guī)則:
- 方法名一致,參數(shù)列表中參數(shù)的順序,類型,個數(shù)不同。
- 重載與方法的返回值無關(guān),存在于父類和子類,同類中。
- 可以拋出不同的異常,可以有不同修飾符。
18.訪問修飾符 public、private、protected、以及不寫(默認)時的區(qū)別?
Java 中,可以使用訪問控制符來保護對類、變量、方法和構(gòu)造方法的訪問。Java 支持 4 種不同的訪問權(quán)限。
- default (即默認,什么也不寫): 在同一包內(nèi)可見,不使用任何修飾符??梢孕揎椩陬悺⒔涌?、變量、方法。
- private : 在同一類內(nèi)可見??梢孕揎椬兞俊⒎椒?。注意:不能修飾類(外部類)
- public : 對所有類可見??梢孕揎楊?、接口、變量、方法
- protected : 對同一包內(nèi)的類和所有子類可見??梢孕揎椬兞?、方法。注意:不能修飾類(外部類)。
19.this 關(guān)鍵字有什么作用?
this 是自身的一個對象,代表對象本身,可以理解為:指向?qū)ο蟊旧淼囊粋€指針。
this 的用法在 Java 中大體可以分為 3 種:
- 普通的直接引用,this 相當于是指向當前對象本身
- 形參與成員變量名字重名,用 this 來區(qū)分:
public Person(String name,int age){
this.name=name;
this.age=age;
}
- 引用本類的構(gòu)造函數(shù)
20.抽象類(abstract class)和接口(interface)有什么區(qū)別?
- 接?的?法默認是 public ,所有?法在接?中不能有實現(xiàn)(Java 8 開始接??法可以有默認實現(xiàn)),?抽象類可以有?抽象的?法。
- 接?中除了 static 、 final 變量,不能有其他變量,?抽象類中則不?定。
- ?個類可以實現(xiàn)多個接?,但只能實現(xiàn)?個抽象類。接???本身可以通過 extends 關(guān)鍵字擴展多個接?。
- 接??法默認修飾符是 public ,抽象?法可以有 public 、 protected 和 default 這些修飾符(抽象?法就是為了被重寫所以不能使? private 關(guān)鍵字修飾?。?/li>
- 從設(shè)計層?來說,抽象是對類的抽象,是?種模板設(shè)計,?接?是對?為的抽象,是?種?為的規(guī)范。
- 在 JDK8 中,接?也可以定義靜態(tài)?法,可以直接?接?名調(diào)?。實現(xiàn)類和實現(xiàn)是不可以調(diào)?的。如果同時實現(xiàn)兩個接?,接?中定義了?樣的默認?法,則必須重寫,不然會報錯。
- jdk9 的接?被允許定義私有?法 。
總結(jié)?下 jdk7~jdk9 Java 中接?的變化:
- 在 jdk 7 或更早版本中,接???只能有常量變量和抽象?法。這些接??法必須由選擇實現(xiàn)接?的類實現(xiàn)。
- jdk 8 的時候接?可以有默認?法和靜態(tài)?法功能。
- jdk 9 在接?中引?了私有?法和私有靜態(tài)?法。
參數(shù) | 抽象類 | 接口 |
---|---|---|
默認的方法實現(xiàn) | 它可以有默認的方法實現(xiàn) | 接口完全是抽象的。它根本不存在方法的實現(xiàn) |
實現(xiàn) | 子類使用extends關(guān)鍵字來繼承抽象類。如果子類不是抽象類的話,它需要提供抽象類中所有聲明的方法的實現(xiàn)。 | 子類使用關(guān)鍵字implements來實現(xiàn)接口。它需要提供接口中所有聲明的方法的實現(xiàn) |
構(gòu)造器 | 抽象類可以有構(gòu)造器 | 接口不能有構(gòu)造器 |
與正常Java類的區(qū)別 | 除了你不能實例化抽象類之外,它和普通Java類沒有任何區(qū)別 | 接口是完全不同的類型 |
訪問修飾符 | 抽象方法可以有public、protected和default這些修飾符 | 接口方法默認修飾符是public。你不可以使用其它修飾符。 |
main方法 | 抽象方法可以有main方法并且我們可以運行它 | 接口沒有main方法,因此我們不能運行它。(java8以后接口可以有default和static方法,所以可以運行main方法) |
多繼承 | 抽象方法可以繼承一個類和實現(xiàn)多個接口 | 接口只可以繼承一個或多個其它接口 |
速度 | 它比接口速度要快 | 接口是稍微有點慢的,因為它需要時間去尋找在類中實現(xiàn)的方法。 |
添加新方法 | 如果你往抽象類中添加新的方法,你可以給它提供默認的實現(xiàn)。因此你不需要改變你現(xiàn)在的代碼。 | 如果你往接口中添加方法,那么你必須改變實現(xiàn)該接口的類。 |
21.成員變量與局部變量的區(qū)別有哪些?
-
從語法形式上看:成員變量是屬于類的,?局部變量是在?法中定義的變量或是?法的參數(shù);成員變量可以被 public , private , static 等修飾符所修飾,?局部變量不能被訪問控制修飾符及 static 所修飾;但是,成員變量和局部變量都能被 final 所修飾。
-
從變量在內(nèi)存中的存儲?式來看:如果成員變量是使? static 修飾的,那么這個成員變量是屬于類的,如果沒有使? static 修飾,這個成員變量是屬于實例的。對象存于堆內(nèi)存,如果局部變量類型為基本數(shù)據(jù)類型,那么存儲在棧內(nèi)存,如果為引?數(shù)據(jù)類型,那存放的是指向堆內(nèi)存對象的引?或者是指向常量池中的地址。
-
從變量在內(nèi)存中的?存時間上看:成員變量是對象的?部分,它隨著對象的創(chuàng)建?存在,?局部變量隨著?法的調(diào)???動消失。
-
成員變量如果沒有被賦初值:則會?動以類型的默認值?賦值(?種情況例外:被 final 修飾的成員變量也必須顯式地賦值),?局部變量則不會?動賦值。
22.靜態(tài)變量和實例變量的區(qū)別?靜態(tài)方法、實例方法呢?
靜態(tài)變量和實例變量的區(qū)別?
-
靜態(tài)變量: 是被 static 修飾符修飾的變量,也稱為類變量,它屬于類,不屬于類的任何一個對象,一個類不管創(chuàng)建多少個對象,靜態(tài)變量在內(nèi)存中有且僅有一個副本。
-
實例變量: 必須依存于某一實例,需要先創(chuàng)建對象然后通過對象才能訪問到它。靜態(tài)變量可以實現(xiàn)讓多個對象共享內(nèi)存。
靜態(tài)?法和實例?法有何不同?
-
靜態(tài)方法:static 修飾的方法,也被稱為類方法。在外部調(diào)?靜態(tài)?法時,可以使?"類名.?法名"的?式,也可以使?"對象名.?法名"的?式。靜態(tài)方法里不能訪問類的非靜態(tài)成員變量和方法。
-
實例?法:依存于類的實例,需要使用"對象名.?法名"的?式調(diào)用;可以訪問類的所有成員變量和方法。
24.final 關(guān)鍵字有什么作用?
final 表示不可變的意思,可用于修飾類、屬性和方法:
- 被 final 修飾的類不可以被繼承
- 被 final 修飾的方法不可以被重寫
- 被 final 修飾的變量不可變,被 final 修飾的變量必須被顯式第指定初始值,還得注意的是,這里的不可變指的是變量的引用不可變,不是引用指向的內(nèi)容的不可變。
例如:
final StringBuilder sb = new StringBuilder("abc");
sb.append("d");
System.out.println(sb); //abcd
一張圖說明:
25.final、finally、finalize 的區(qū)別?
-
final 用于修飾變量、方法和類:final 修飾的類不可被繼承;修飾的方法不可被重寫;修飾的變量不可變。
-
finally 作為異常處理的一部分,它只能在
try/catch
語句中,并且附帶一個語句塊表示這段語句最終一定被執(zhí)行(無論是否拋出異常),經(jīng)常被用在需要釋放資源的情況下,System.exit (0)
可以阻斷 finally 執(zhí)行。
finalize 是在 java.lang.Object
里定義的方法,也就是說每一個對象都有這么個方法,這個方法在 gc
啟動,該對象被回收的時候被調(diào)用。
一個對象的 finalize 方法只會被調(diào)用一次,finalize 被調(diào)用不一定會立即回收該對象,所以有可能調(diào)用 finalize 后,該對象又不需要被回收了,然后到了真正要被回收的時候,因為前面調(diào)用過一次,所以不會再次調(diào)用 finalize 了,進而產(chǎn)生問題,因此不推薦使用 finalize 方法。
26.==和 equals 的區(qū)別?
==
: 它的作?是判斷兩個對象的地址是不是相等。即,判斷兩個對象是不是同?個對象(基本數(shù)據(jù)類型 ==
比較的是值,引?數(shù)據(jù)類型 ==
比較的是內(nèi)存地址)。
equals()
: 它的作?也是判斷兩個對象是否相等。但是這個“相等”一般也分兩種情況:
- 默認情況:類沒有覆蓋 equals() ?法。則通過 equals() 比較該類的兩個對象時,等價于通過“
==
”比較這兩個對象,還是相當于比較內(nèi)存地址。 - 自定義情況:類覆蓋了 equals() ?法。我們平時覆蓋的 equals()方法一般是比較兩個對象的內(nèi)容是否相同,自定義了一個相等的標準,也就是兩個對象的值是否相等。
舉個例?,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 整數(shù),定義在 Object 類中, 是一個本地?法,這個?法通常?來將對象的內(nèi)存地址轉(zhuǎn)換為整數(shù)之后返回。
public native int hashCode();
哈希碼主要在哈希表這類集合映射的時候用到,哈希表存儲的是鍵值對(key-value),它的特點是:能根據(jù)“鍵”快速的映射到對應(yīng)的“值”。這其中就利?到了哈希碼!
為什么要有 hashCode?
上面已經(jīng)講了,主要是在哈希表這種結(jié)構(gòu)中用的到。
例如 HashMap 怎么把 key 映射到對應(yīng)的 value 上呢?用的就是哈希取余法,也就是拿哈希碼和存儲元素的數(shù)組的長度取余,獲取 key 對應(yīng)的 value 所在的下標位置。
為什么重寫 quals 時必須重寫 hashCode ?法?
如果兩個對象相等,則 hashcode ?定也是相同的。兩個對象相等,對兩個對象分別調(diào)用equals ?法都返回 true。反之,兩個對象有相同的 hashcode 值,它們也不?定是相等的 。因此,equals
?法被覆蓋過,則 hashCode
?法也必須被覆蓋。
hashCode() 的默認?為是對堆上的對象產(chǎn)?獨特值。如果沒有重寫 hashCode() ,則該 class 的兩個對象?論如何都不會相等(即使這兩個對象指向相同的數(shù)據(jù))
為什么兩個對象有相同的 hashcode 值,它們也不?定是相等的?
因為可能會碰撞, hashCode() 所使?的散列算法也許剛好會讓多個對象傳回相同的散列值。越糟糕的散列算法越容易碰撞,但這也與數(shù)據(jù)值域分布的特性有關(guān)(所謂碰撞也就是指的是不3同的對象得到相同的 hashCode )。
28.Java 是值傳遞,還是引用傳遞?
Java 語言是值傳遞。Java 語言的方法調(diào)用只支持參數(shù)的值傳遞。當一個對象實例作為一個參數(shù)被傳遞到方法中時,參數(shù)的值就是對該對象的引用。對象的屬性可以在被調(diào)用過程中被改變,但對對象引用的改變是不會影響到調(diào)用者的。
JVM 的內(nèi)存分為堆和棧,其中棧中存儲了基本數(shù)據(jù)類型和引用數(shù)據(jù)類型實例的地址,也就是對象地址。
而對象所占的空間是在堆中開辟的,所以傳遞的時候可以理解為把變量存儲的對象地址給傳遞過去,因此引用類型也是值傳遞。
29.深拷貝和淺拷貝?
- 淺拷貝:僅拷貝被拷貝對象的成員變量的值,也就是基本數(shù)據(jù)類型變量的值,和引用數(shù)據(jù)類型變量的地址值,而對于引用類型變量指向的堆中的對象不會拷貝。
- 深拷貝:完全拷貝一個對象,拷貝被拷貝對象的成員變量的值,堆中的對象也會拷貝一份。
例如現(xiàn)在有一個 order 對象,里面有一個 products 列表,它的淺拷貝和深拷貝的示意圖:
因此深拷貝是安全的,淺拷貝的話如果有引用類型,那么拷貝后對象,引用類型變量修改,會影響原對象。
淺拷貝如何實現(xiàn)呢?
Object 類提供的 clone()方法可以非常簡單地實現(xiàn)對象的淺拷貝。
深拷貝如何實現(xiàn)呢?
- 重寫克隆方法:重寫克隆方法,引用類型變量單獨克隆,這里可能會涉及多層遞歸。
- 序列化:可以先講原對象序列化,再反序列化成拷貝對象。
30.Java 創(chuàng)建對象有哪幾種方式?
Java 中有以下四種創(chuàng)建對象的方式:
- new 創(chuàng)建新對象
- 通過反射機制
- 采用 clone 機制
- 通過序列化機制
前兩者都需要顯式地調(diào)用構(gòu)造方法。對于 clone 機制,需要注意淺拷貝和深拷貝的區(qū)別,對于序列化機制需要明確其實現(xiàn)原理,在 Java 中序列化可以通過實現(xiàn) Externalizable 或者 Serializable 來實現(xiàn)。
四、常用類
1) String
31.String 是 Java 基本數(shù)據(jù)類型嗎?可以被繼承嗎?
String 是 Java 基本數(shù)據(jù)類型嗎?
不是。Java 中的基本數(shù)據(jù)類型只有 8 個:byte、short、int、long、float、double、char、boolean;除了基本類型(primitive type),剩下的都是引用類型(reference type)。
String 是一個比較特殊的引用數(shù)據(jù)類型。
String 類可以繼承嗎?
不行。String 類使用 final 修飾,是所謂的不可變類,無法被繼承。
32.String 和 StringBuilder、StringBuffer 的區(qū)別?
-
String
:String 的值被創(chuàng)建后不能修改,任何對 String 的修改都會引發(fā)新的 String 對象的生成。 -
StringBuffer
:跟 String 類似,但是值可以被修改,使用 synchronized 來保證線程安全。 -
StringBuilder
:StringBuffer 的非線程安全版本,性能上更高一些。
33.String str1 = new String(“abc”)和 String str2 = “abc” 和 區(qū)別?
兩個語句都會去字符串常量池中檢查是否已經(jīng)存在 “abc”,如果有則直接使用,如果沒有則會在常量池中創(chuàng)建 “abc” 對象。
但是不同的是,String str1 = new String(“abc”) 還會通過 new String() 在堆里創(chuàng)建一個 “abc” 字符串對象實例。所以后者可以理解為被前者包含。
String s = new String(“abc”)創(chuàng)建了幾個對象?
很明顯,一個或兩個。如果字符串常量池已經(jīng)有“abc”,則是一個;否則,兩個。
當字符創(chuàng)常量池沒有 “abc”,此時會創(chuàng)建如下兩個對象:
- 一個是字符串字面量 “abc” 所對應(yīng)的、字符串常量池中的實例
- 另一個是通過 new String() 創(chuàng)建并初始化的,內(nèi)容與"abc"相同的實例,在堆中。
34.String 不是不可變類嗎?字符串拼接是如何實現(xiàn)的?
String 的確是不可變的,“+”的拼接操作,其實是會生成新的對象。
例如:
String a = "hello ";
String b = "world!";
String ab = a + b;
在jdk1.8 之前
,a 和 b 初始化時位于字符串常量池,ab 拼接后的對象位于堆中。經(jīng)過拼接新生成了 String 對象。如果拼接多次,那么會生成多個中間對象。
內(nèi)存如下:
在Java8 時JDK 對“+”號拼接進行了優(yōu)化,上面所寫的拼接方式會被優(yōu)化為基于 StringBuilder 的 append 方法進行處理。Java 會在編譯期對“+”號進行處理。
下面是通過 javap -verbose 命令反編譯字節(jié)碼的結(jié)果,很顯然可以看到 StringBuilder 的創(chuàng)建和 append 方法的調(diào)用。
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();
此時,如果再籠統(tǒng)的回答:通過加號拼接字符串會創(chuàng)建多個 String 對象,因此性能比 StringBuilder 差,就是錯誤的了。因為本質(zhì)上加號拼接的效果最終經(jīng)過編譯器處理之后和 StringBuilder 是一致的。
當然,循環(huán)里拼接還是建議用 StringBuilder,為什么,因為循環(huán)一次就會創(chuàng)建一個新的 StringBuilder 對象,大家可以自行實驗。
35.intern 方法有什么作用?
JDK 源碼里已經(jīng)對這個方法進行了說明:
* <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>
意思也很好懂:
- 如果當前字符串內(nèi)容存在于字符串常量池(即 equals()方法為 true,也就是內(nèi)容一樣),直接返回字符串常量池中的字符串
- 否則,將此 String 對象添加到池中,并返回 String 對象的引用
2) Integer
36.Integer a= 127,Integer b = 127;Integer c= 128,Integer d = 128;,相等嗎?
答案是 a 和 b 相等,c 和 d 不相等。
- 對于基本數(shù)據(jù)類型==比較的值
- 對于引用數(shù)據(jù)類型==比較的是地址
Integer a= 127 這種賦值,是用到了 Integer 自動裝箱的機制。自動裝箱的時候會去緩存池里取 Integer 對象,沒有取到才會創(chuàng)建新的對象。
如果整型字面量的值在-128 到 127 之間,那么自動裝箱時不會 new 新的 Integer 對象,而是直接引用緩存池中的 Integer 對象,超過范圍 a1==b1 的結(jié)果是 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 緩存?
因為根據(jù)實踐發(fā)現(xiàn)大部分的數(shù)據(jù)操作都集中在值比較小的范圍,因此 Integer 搞了個緩存池,默認范圍是 -128 到 127,可以根據(jù)通過設(shè)置JVM-XX:AutoBoxCacheMax=來修改緩存的最大值,最小值改不了。
實現(xiàn)的原理是 int 在自動裝箱的時候會調(diào)用 Integer.valueOf,進而用到了 IntegerCache。
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
很簡單,就是判斷下值是否在緩存范圍之內(nèi),如果是的話去 IntegerCache 中取,不是的話就創(chuàng)建一個新的 Integer 對象。
IntegerCache 是一個靜態(tài)內(nèi)部類, 在靜態(tài)塊中會初始化好緩存值。
private static class IntegerCache {
……
static {
//創(chuàng)建Integer對象存儲
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
……
}
}
37.String 怎么轉(zhuǎn)成 Integer 的?原理?
String 轉(zhuǎn)成 Integer,主要有兩個方法:
- Integer.parseInt(String s)
- Integer.valueOf(String s)
不管哪一種,最終還是會調(diào)用 Integer 類內(nèi)中的parseInt(String s, int radix)方法。
拋去一些邊界之類的看看核心代碼:
public static int parseInt(String s, int radix)
throws NumberFormatException
{
int result = 0;
//是否是負數(shù)
boolean negative = false;
//char字符數(shù)組下標和長度
int i = 0, len = s.length();
……
int digit;
//判斷字符長度是否大于0,否則拋出異常
if (len > 0) {
……
while (i < len) {
// Accumulating negatively avoids surprises near MAX_VALUE
//返回指定基數(shù)中字符表示的數(shù)值。(此處是十進制數(shù)值)
digit = Character.digit(s.charAt(i++),radix);
//進制位乘以數(shù)值
result *= radix;
result -= digit;
}
}
//根據(jù)上面得到的是否負數(shù),返回相應(yīng)的值
return negative ? result : -result;
}
去掉枝枝蔓蔓(當然這些枝枝蔓蔓可以去看看,源碼 cover 了很多情況),其實剩下的就是一個簡單的字符串遍歷計算,不過計算方式有點反常規(guī),是用負的值累減。
3) Object
38.Object 類的常見方法?
Object 類是一個特殊的類,是所有類的父類,也就是說所有類都可以調(diào)用它的方法。它主要提供了以下 11 個方法,大概可以分為六類:
對象比較:
-
public native int hashCode()
:native 方法,用于返回對象的哈希碼,主要使用在哈希表中,比如 JDK 中的 HashMap。 -
public boolean equals(Object obj)
:用于比較 2 個對象的內(nèi)存地址是否相等,String 類對該方法進行了重寫用戶比較字符串的值是否相等。
對象拷貝:
-
protected native Object clone() throws CloneNotSupportedException
:naitive 方法,用于創(chuàng)建并返回當前對象的一份拷貝。一般情況下,對于任何對象 x,表達式x.clone() != x
為 true,x.clone().getClass() == x.getClass()
為 true。Object 本身沒有實現(xiàn) Cloneable 接口,所以不重寫 clone 方法并且進行調(diào)用的話會發(fā)生 CloneNotSupportedException 異常。
對象轉(zhuǎn)字符串:
-
public String toString()
:返回類的名字@實例的哈希碼的 16 進制的字符串。建議 Object 所有的子類都重寫這個方法。
多線程調(diào)度:
-
public final native void notify()
:native 方法,并且不能重寫。喚醒一個在此對象監(jiān)視器上等待的線程(監(jiān)視器相當于就是鎖的概念)。如果有多個線程在等待只會任意喚醒一個。 -
public final native void notifyAll()
:native 方法,并且不能重寫。跟 notify 一樣,唯一的區(qū)別就是會喚醒在此對象監(jiān)視器上等待的所有線程,而不是一個線程。 -
public final native void wait(long timeout) throws InterruptedException
:native 方法,并且不能重寫。暫停線程的執(zhí)行。注意:sleep 方法沒有釋放鎖,而 wait 方法釋放了鎖 。timeout 是等待時間。 -
public final void wait(long timeout, int nanos) throws InterruptedException
:多了 nanos 參數(shù),這個參數(shù)表示額外時間(以毫微秒為單位,范圍是 0-999999)。所以超時的時間還需要加上 nanos 毫秒。 -
public final void wait() throws InterruptedException
:跟之前的 2 個 wait 方法一樣,只不過該方法一直等待,沒有超時時間這個概念
反射:
-
public final native Class<?> getClass()
:native 方法,用于返回當前運行時對象的 Class 對象,使用了 final 關(guān)鍵字修飾,故不允許子類重寫。
垃圾回收:
-
protected void finalize() throws Throwable
:通知垃圾收集器回收對象。
五、異常處理
39.Java 中異常處理體系?
Java 的異常體系是分為多層的。Throwable
是 Java 語言中所有錯誤或異常的基類。Throwable 又分為Error
和Exception
,其中 Error 是系統(tǒng)內(nèi)部錯誤,比如虛擬機異常,是程序無法處理的。Exception
是程序問題導(dǎo)致的異常,又分為兩種:
- CheckedException 受檢異常:編譯器會強制檢查并要求處理的異常。
- RuntimeException 運行時異常:程序運行中出現(xiàn)異常,比如我們熟悉的空指針、數(shù)組下標越界等等
40.異常的處理方式?
針對異常的處理主要有兩種方式:
-
遇到異常不進行具體處理,而是繼續(xù)拋給調(diào)用者 (throw,throws)
拋出異常有三種形式,一是 throw,一個 throws,還有一種系統(tǒng)自動拋異常。
throws 用在方法上,后面跟的是異常類,可以跟多個;而 throw 用在方法內(nèi),后面跟的是異常對象。
- try catch 捕獲異常
在 catch 語句塊中補貨發(fā)生的異常,并進行處理。
try {
//包含可能會出現(xiàn)異常的代碼以及聲明異常的方法
}catch(Exception e) {
//捕獲異常并進行處理
}finally { }
//可選,必執(zhí)行的代碼
}
try-catch 捕獲異常的時候還可以選擇加上 finally 語句塊,finally 語句塊不管程序是否正常執(zhí)行,最終它都會必然執(zhí)行。
41.三道經(jīng)典異常處理代碼題
題目 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");
}
}
}
執(zhí)行結(jié)果:31。
try、catch。finally 的基礎(chǔ)用法,在 return 前會先執(zhí)行 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;
}
}
}
執(zhí)行結(jié)果:3。
try 返回前先執(zhí)行 finally,結(jié)果 finally 里不按套路出牌,直接 return 了,自然也就走不到 try 里面的 return 了。
finally 里面使用 return 僅存在于面試題中,實際開發(fā)這么寫要挨吊的。
題目 3
ublic 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;
}
}
}
執(zhí)行結(jié)果:2。
大家可能會以為結(jié)果應(yīng)該是 3,因為在 return 前會執(zhí)行 finally,而 i 在 finally 中被修改為 3 了,那最終返回 i 不是應(yīng)該為 3 嗎?
但其實,在執(zhí)行 finally 之前,JVM 會先將 i 的結(jié)果暫存起來,然后 finally 執(zhí)行完畢后,會返回之前暫存的結(jié)果,而不是返回 i,所以即使 i 已經(jīng)被修改為 3,最終返回的還是之前暫存起來的結(jié)果 2。
六、I/O
42.Java 中 IO 流分為幾種?
流按照不同的特點,有很多種劃分方式。
- 按照流的流向分,可以分為輸入流和輸出流;
- 按照操作單元劃分,可以劃分為字節(jié)流和字符流;
- 按照流的角色劃分為節(jié)點流和處理流
Java Io 流共涉及 40 多個類,看上去雜亂,其實都存在一定的關(guān)聯(lián), Java I0 流的 40 多個類都是從如下 4 個抽象類基類中派生出來的。
-
InputStream/Reader
: 所有的輸入流的基類,前者是字節(jié)輸入流,后者是字符輸入流。 -
OutputStream/Writer
: 所有輸出流的基類,前者是字節(jié)輸出流,后者是字符輸出流。
IO 流用到了什么設(shè)計模式?
其實,Java 的 IO 流體系還用到了一個設(shè)計模式——裝飾器模式。
InputStream 相關(guān)的部分類圖如下,篇幅有限,裝飾器模式就不展開說了。
43.既然有了字節(jié)流,為什么還要有字符流?
其實字符流是由 Java 虛擬機將字節(jié)轉(zhuǎn)換得到的,問題就出在這個過程還比較耗時,并且,如果我們不知道編碼類型就很容易出現(xiàn)亂碼問題。
所以, I/O 流就干脆提供了一個直接操作字符的接口,方便我們平時對字符進行流操作。如果音頻文件、圖片等媒體文件用字節(jié)流比較好,如果涉及到字符的話使用字符流比較好。
44.BIO、NIO、AIO?
BIO
(blocking I/O) :就是傳統(tǒng)的 IO,同步阻塞,服務(wù)器實現(xiàn)模式為一個連接一個線程,即客戶端有連接請求時服務(wù)器端就需要啟動一個線程進行處理,如果這個連接不做任何事情會造成不必要的線程開銷,可以通過連接池機制改善(實現(xiàn)多個客戶連接服務(wù)器)。
BIO 方式適用于連接數(shù)目比較小且固定的架構(gòu),這種方式對服務(wù)器資源要求比較高,并發(fā)局限于應(yīng)用中,JDK1.4 以前的唯一選擇,程序簡單易理解。
NIO
:全稱 java non-blocking IO,是指 JDK 提供的新 API。從 JDK1.4 開始,Java 提供了一系列改進的輸入/輸出的新特性,被統(tǒng)稱為 NIO(即 New IO)。
NIO 是同步非阻塞的,服務(wù)器端用一個線程處理多個連接,客戶端發(fā)送的連接請求會注冊到多路復(fù)用器上,多路復(fù)用器輪詢到連接有 IO 請求就進行處理:
NIO 的數(shù)據(jù)是面向緩沖區(qū) Buffer的,必須從 Buffer 中讀取或?qū)懭搿?/p>
所以完整的 NIO 示意圖:
可以看出,NIO 的運行機制:
- 每個 Channel 對應(yīng)一個 Buffer。
- Selector 對應(yīng)一個線程,一個線程對應(yīng)多個 Channel。
- Selector 會根據(jù)不同的事件,在各個通道上切換。
- Buffer 是內(nèi)存塊,底層是數(shù)據(jù)。
AIO
:JDK 7 引入了 Asynchronous I/O,是異步不阻塞的 IO。在進行 I/O 編程中,常用到兩種模式:Reactor 和 Proactor。Java 的 NIO 就是 Reactor,當有事件觸發(fā)時,服務(wù)器端得到通知,進行相應(yīng)的處理,完成后才通知服務(wù)端程序啟動線程去處理,一般適用于連接數(shù)較多且連接時間較長的應(yīng)用。
七、序列化
45.什么是序列化?什么是反序列化?
什么是序列化,序列化就是把 Java 對象轉(zhuǎn)為二進制流,方便存儲和傳輸。
所以反序列化就是把二進制流恢復(fù)成對象。
類比我們生活中一些大件物品的運輸,運輸?shù)臅r候把它拆了打包,用的時候再拆包組裝。
Serializable 接口有什么用?
這個接口只是一個標記,沒有具體的作用,但是如果不實現(xiàn)這個接口,在有些序列化場景會報錯,所以一般建議,創(chuàng)建的 JavaBean 類都實現(xiàn) Serializable。
serialVersionUID 又有什么用?
serialVersionUID 就是起驗證作用。
private static final long serialVersionUID = 1L;
我們經(jīng)常會看到這樣的代碼,這個 ID 其實就是用來驗證序列化的對象和反序列化對應(yīng)的對象 ID 是否一致。
這個 ID 的數(shù)字其實不重要,無論是 1L 還是 IDE 自動生成的,只要序列化時候?qū)ο蟮?serialVersionUID 和反序列化時候?qū)ο蟮?serialVersionUID 一致的話就行。
如果沒有顯示指定 serialVersionUID ,則編譯器會根據(jù)類的相關(guān)信息自動生成一個,可以認為是一個指紋。
所以如果你沒有定義一個 serialVersionUID, 結(jié)果序列化一個對象之后,在反序列化之前把對象的類的結(jié)構(gòu)改了,比如增加了一個成員變量,則此時的反序列化會失敗。
因為類的結(jié)構(gòu)變了,所以 serialVersionUID 就不一致。
Java 序列化不包含靜態(tài)變量?
序列化的時候是不包含靜態(tài)變量的。
如果有些變量不想序列化,怎么辦?
對于不想進行序列化的變量,使用transient
關(guān)鍵字修飾。
transient
關(guān)鍵字的作用是:阻止實例中那些用此關(guān)鍵字修飾的的變量序列化;當對象被反序列化時,被 transient
修飾的變量值不會被持久化和恢復(fù)。transient
只能修飾變量,不能修飾類和方法。
46.說說有幾種序列化方式?
Java 序列化方式有很多,常見的有三種:
- Java 對象流列化 :Java 原生序列化方法即通過 Java 原生流(InputStream 和 OutputStream 之間的轉(zhuǎn)化)的方式進行轉(zhuǎn)化,一般是對象輸出流 ObjectOutputStream和對象輸入流ObjectI叩utStream。
- Json 序列化:這個可能是我們最常用的序列化方式,Json 序列化的選擇很多,一般會使用 jackson 包,通過 ObjectMapper 類來進行一些操作,比如將對象轉(zhuǎn)化為 byte 數(shù)組或者將 json 串轉(zhuǎn)化為對象。
- ProtoBuff 序列化:ProtocolBuffer 是一種輕便高效的結(jié)構(gòu)化數(shù)據(jù)存儲格式,ProtoBuff 序列化對象可以很大程度上將其壓縮,可以大大減少數(shù)據(jù)傳輸大小,提高系統(tǒng)性能。
八、泛型
47.Java 泛型了解么?什么是類型擦除?介紹一下常用的通配符?
什么是泛型?
Java 泛型(generics)是 JDK 5 中引入的一個新特性, 泛型提供了編譯時類型安全檢測機制,該機制允許程序員在編譯時檢測到非法的類型。泛型的本質(zhì)是參數(shù)化類型,也就是說所操作的數(shù)據(jù)類型被指定為一個參數(shù)。
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);
泛型一般有三種使用方式:泛型類、泛型接口、泛型方法。
- 泛型類:
//此處T可以隨便寫為任意標識,常見的如T、E、K、V等形式的參數(shù)常用于表示泛型
//在實例化泛型類時,必須指定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);
- 泛型接口 :
class GeneratorImpl<T> implements Generator<T>{
@Override
public T method() {
return null;
}
}
實現(xiàn)泛型接口,指定類型:
class GeneratorImpl<T> implements Generator<String>{
@Override
public String method() {
return "hello";
}
}
- 泛型方法 :
public static < E > void printArray( E[] inputArray )
{
for ( E element : inputArray ){
System.out.printf( "%s ", element );
}
System.out.println();
}
使用:
// 創(chuàng)建不同類型數(shù)組: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 的范型只存在于源碼里,編譯的時候給你靜態(tài)地檢查一下范型類型是否正確,而到了運行時就不檢查了。上面這段代碼在 JRE(Java運行環(huán)境)看來和下面這段沒區(qū)別:
LinkedList cats = new LinkedList(); // 注意:沒有范型!
LinkedList list = cats;
list.add(new Dog());
為什么要類型擦除呢?
主要是為了向下兼容,因為 JDK5 之前是沒有泛型的,為了讓 JVM 保持向下兼容,就出了類型擦除這個策略。
九、注解
48.說一下你對注解的理解?
Java 注解本質(zhì)上是一個標記,可以理解成生活中的一個人的一些小裝扮,比如戴什么什么帽子,戴什么眼鏡。
注解可以標記在類上、方法上、屬性上等,標記自身也可以設(shè)置一些值,比如帽子顏色是綠色。
有了標記之后,我們就可以在編譯或者運行階段去識別這些標記,然后搞一些事情,這就是注解的用處。
例如我們常見的 AOP,使用注解作為切點就是運行期注解的應(yīng)用;比如 lombok,就是注解在編譯期的運行。
注解生命周期有三大類,分別是:
- RetentionPolicy.SOURCE:給編譯器用的,不會寫入 class 文件
- RetentionPolicy.CLASS:會寫入 class 文件,在類加載階段丟棄,也就是運行的時候就沒這個信息了
- RetentionPolicy.RUNTIME:會寫入 class 文件,永久保存,可以通過反射獲取注解信息
所以我上文寫的是解析的時候,沒寫具體是解析啥,因為不同的生命周期的解析動作是不同的。
像常見的:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
就是給編譯器用的,編譯器編譯的時候檢查沒問題就 over 了,class 文件里面不會有 Override 這個標記。
再比如 Spring 常見的 Autowired ,就是 RUNTIME 的,所以在運行的時候可以通過反射得到注解的信息,還能拿到標記的值 required 。
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
boolean required() default true;
}
十、反射
49.什么是反射?應(yīng)用?原理?
什么是反射?
我們通常都是利用new
方式來創(chuàng)建對象實例,這可以說就是一種“正射”,這種方式在編譯時候就確定了類型信息。
而如果,我們想在時候動態(tài)地獲取類信息、創(chuàng)建類實例、調(diào)用類方法這時候就要用到反射。
通過反射你可以獲取任意一個類的所有屬性和方法,你還可以調(diào)用這些方法和屬性。
反射最核心的四個類:
反射的應(yīng)用場景?
一般我們平時都是在在寫業(yè)務(wù)代碼,很少會接觸到直接使用反射機制的場景。
但是,這并不代表反射沒有用。相反,正是因為反射,你才能這么輕松地使用各種框架。像編輯器輸入對象名稱提示對象屬性和方法、 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射機制。
像 Spring 里的很多 注解 ,它真正的功能實現(xiàn)就是利用反射。
就像為什么我們使用 Spring 的時候 ,一個@Component注解就聲明了一個類為 Spring Bean 呢?為什么通過一個 @Value注解就讀取到配置文件中的值呢?究竟是怎么起作用的呢?
這些都是因為我們可以基于反射操作類,然后獲取到類/屬性/方法/方法的參數(shù)上的注解,注解這里就有兩個作用,一是標記,我們對注解標記的類/屬性/方法進行對應(yīng)的處理;二是注解本身有一些信息,可以參與到處理的邏輯中。
反射的原理?
我們都知道 Java 程序的執(zhí)行分為編譯和運行兩步,編譯之后會生成字節(jié)碼(.class)文件,JVM 進行類加載的時候,會加載字節(jié)碼文件,將類型相關(guān)的所有信息加載進方法區(qū),反射就是去獲取這些信息,然后進行各種操作。
十一、JDK1.8 新特性
50.JDK1.8 都有哪些新特性?
JDK1.8 有不少新特性,我們經(jīng)常接觸到的新特性如下:
-
接口默認方法:Java 8 允許我們給接口添加一個非抽象的方法實現(xiàn),只需要使用 default 關(guān)鍵字修飾即可
-
Lambda 表達式和函數(shù)式接口:Lambda 表達式本質(zhì)上是一段匿名內(nèi)部類,也可以是一段可以傳遞的代碼。Lambda 允許把函數(shù)作為一個方法的參數(shù)(函數(shù)作為參數(shù)傳遞到方法中),使用 Lambda 表達式使代碼更加簡潔,但是也不要濫用,否則會有可讀性等問題,《Effective Java》作者 Josh Bloch 建議使用 Lambda 表達式最好不要超過 3 行。
-
Stream API:用函數(shù)式編程方式在集合類上進行復(fù)雜操作的工具,配合 Lambda 表達式可以方便的對集合進行處理。
-
Java8 中處理集合的關(guān)鍵抽象概念,它可以指定你希望對集合進行的操作,可以執(zhí)行非常復(fù)雜的查找、過濾和映射數(shù)據(jù)等操作。使用 Stream API 對集合數(shù)據(jù)進行操作,就類似于使用 SQL 執(zhí)行的數(shù)據(jù)庫查詢。也可以使用 Stream API 來并行執(zhí)行操作。
簡而言之,Stream API 提供了一種高效且易于使用的處理數(shù)據(jù)的方式。
-
日期時間 API:Java 8 引入了新的日期時間 API 改進了日期時間的管理。
-
Optional 類:用來解決空指針異常的問題。很久以前 Google Guava 項目引入了 Optional 作為解決空指針異常的一種方式,不贊成代碼被 null 檢查的代碼污染,期望程序員寫整潔的代碼。受 Google Guava 的鼓勵,Optional 現(xiàn)在是 Java 8 庫的一部分。
51.Lambda 表達式了解多少?
Lambda 表達式本質(zhì)上是一段匿名內(nèi)部類,也可以是一段可以傳遞的代碼。
比如我們以前使用 Runnable 創(chuàng)建并運行線程:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Thread is running before Java8!");
}
}).start();
這是通過內(nèi)部類的方式來重寫 run 方法,使用 Lambda 表達式,還可以更加簡潔:
new Thread( () -> System.out.println("Thread is running since Java8!") ).start();
當然不是每個接口都可以縮寫成 Lambda 表達式。只有那些函數(shù)式接口(Functional Interface)才能縮寫成 Lambda 表示式。
所謂函數(shù)式接口(Functional Interface)就是只包含一個抽象方法的聲明。針對該接口類型的所有 Lambda 表達式都會與這個抽象方法匹配。
Java8 有哪些內(nèi)置函數(shù)式接口?
JDK 1.8 API 包含了很多內(nèi)置的函數(shù)式接口。其中就包括我們在老版本中經(jīng)常見到的 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 對一個包含一個或多個元素的集合做各種操作。這些操作可能是 中間操作 亦或是 終端操作。終端操作會返回一個結(jié)果,而中間操作會返回一個 Stream 流。
Stream 流一般用于集合,我們對一個集合做幾個常見操作:文章來源:http://www.zghlxwxcb.cn/news/detail-481264.html
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 轉(zhuǎn)換
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 計數(shù)
count 是一個終端操作,它能夠統(tǒng)計 stream 流中的元素總數(shù),返回值是 long 類型。
// 先對 list 中字符串開頭為 b 進行過濾,讓后統(tǒng)計數(shù)量
long startsWithB =
stringCollection
.stream()
.filter((s) -> s.startsWith("b"))
.count();
System.out.println(startsWithB); // 3
-
Reduce
Reduce 中文翻譯為:減少、縮小。通過入?yún)⒌?Function,我們能夠?qū)?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"
以上是常見的幾種流式操作,還有其它的一些流式操作,可以幫助我們更便捷地處理集合數(shù)據(jù)。文章來源地址http://www.zghlxwxcb.cn/news/detail-481264.html
到了這里,關(guān)于Java 八股文-基礎(chǔ)篇的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!