文章目錄
Java面試總結(jié)匯總,整理了包括Java基礎(chǔ)知識,集合容器,并發(fā)編程,JVM,常用開源框架Spring,MyBatis,數(shù)據(jù)庫,中間件等,包含了作為一個Java工程師在面試中需要用到或者可能用到的絕大部分知識。歡迎大家閱讀,本人見識有限,寫的博客難免有錯誤或者疏忽的地方,還望各位大佬指點,在此表示感激不盡。文章持續(xù)更新中… Java概述何為編程編程就是讓計算機為解決某個問題而使用某種程序設(shè)計語言編寫程序代碼,并最終得到結(jié)果的過程。 為了使計算機能夠理解人的意圖,人類就必須要將需解決的問題的思路、方法、和手段通過計算機能夠理解的形式告訴計算機,使得計算機能夠根據(jù)人的指令一步一步去工作,完成某種特定的任務(wù)。這種人和計算機之間交流的過程就是編程。 什么是JavaJava是一門面向?qū)ο缶幊陶Z言,不僅吸收了C++語言的各種優(yōu)點,還摒棄了C++里難以理解的多繼承、指針等概念,因此Java語言具有功能強大和簡單易用兩個特征。Java語言作為靜態(tài)面向?qū)ο缶幊陶Z言的代表,極好地實現(xiàn)了面向?qū)ο罄碚?,允許程序員以優(yōu)雅的思維方式進行復(fù)雜的編程 。 jdk1.5之后的三大版本
JVM、JRE和JDK的關(guān)系JVM JRE 如果想要運行一個開發(fā)好的Java程序,計算機中只需要安裝JRE即可。 JDK JVM&JRE&JDK關(guān)系圖 什么是跨平臺性?原理是什么所謂跨平臺性,是指java語言編寫的程序,一次編譯后,可以在多個系統(tǒng)平臺上運行。 實現(xiàn)原理:Java程序是通過java虛擬機在系統(tǒng)平臺上運行的,只要該系統(tǒng)可以安裝相應(yīng)的java虛擬機,該系統(tǒng)就可以運行java程序。 Java語言有哪些特點簡單易學(xué)(Java語言的語法與C語言和C++語言很接近) 面向?qū)ο螅ǚ庋b,繼承,多態(tài)) 平臺無關(guān)性(Java虛擬機實現(xiàn)平臺無關(guān)性) 支持網(wǎng)絡(luò)編程并且很方便(Java語言誕生本身就是為簡化網(wǎng)絡(luò)編程設(shè)計的) 支持多線程(多線程機制使應(yīng)用程序在同一時間并行執(zhí)行多項任) 健壯性(Java語言的強類型機制、異常處理、垃圾的自動收集等) 安全性 什么是字節(jié)碼?采用字節(jié)碼的最大好處是什么字節(jié)碼:Java源代碼經(jīng)過虛擬機編譯器編譯后產(chǎn)生的文件(即擴展為.class的文件),它不面向任何特定的處理器,只面向虛擬機。 采用字節(jié)碼的好處: Java語言通過字節(jié)碼的方式,在一定程度上解決了傳統(tǒng)解釋型語言執(zhí)行效率低的問題,同時又保留了解釋型語言可移植的特點。所以Java程序運行時比較高效,而且,由于字節(jié)碼并不專對一種特定的機器,因此,Java程序無須重新編譯便可在多種不同的計算機上運行。 先看下java中的編譯器和解釋器: Java中引入了虛擬機的概念,即在機器和編譯程序之間加入了一層抽象的虛擬機器。這臺虛擬的機器在任何平臺上都提供給編譯程序一個的共同的接口。編譯程序只需要面向虛擬機,生成虛擬機能夠理解的代碼,然后由解釋器來將虛擬機代碼轉(zhuǎn)換為特定系統(tǒng)的機器碼執(zhí)行。在Java中,這種供虛擬機理解的代碼叫做字節(jié)碼(即擴展為.class的文件),它不面向任何特定的處理器,只面向虛擬機。每一種平臺的解釋器是不同的,但是實現(xiàn)的虛擬機是相同的。Java源程序經(jīng)過編譯器編譯后變成字節(jié)碼,字節(jié)碼由虛擬機解釋執(zhí)行,虛擬機將每一條要執(zhí)行的字節(jié)碼送給解釋器,解釋器將其翻譯成特定機器上的機器碼,然后在特定的機器上運行,這就是上面提到的Java的特點的編譯與解釋并存的解釋。 Java源代碼---->編譯器---->jvm可執(zhí)行的Java字節(jié)碼(即虛擬指令)---->jvm---->jvm中解釋器----->機器可執(zhí)行的二進制機器碼---->程序運行。
什么是Java程序的主類?應(yīng)用程序和小程序的主類有何不同?一個程序中可以有多個類,但只能有一個類是主類。在Java應(yīng)用程序中,這個主類是指包含main()方法的類。而在Java小程序中,這個主類是一個繼承自系統(tǒng)類JApplet或Applet的子類。應(yīng)用程序的主類不一定要求是public類,但小程序的主類要求必須是public類。主類是Java程序執(zhí)行的入口點。 Java應(yīng)用程序與小程序之間有那些差別?簡單說應(yīng)用程序是從主線程啟動(也就是main()方法)。applet小程序沒有main方法,主要是嵌在瀏覽器頁面上運行(調(diào)用init()線程或者run()來啟動),嵌入瀏覽器這點跟flash的小游戲類似。 Java和C++的區(qū)別我知道很多人沒學(xué)過C++,但是面試官就是沒事喜歡拿咱們Java和C++比呀!沒辦法?。?!就算沒學(xué)過C++,也要記下來!
Oracle JDK 和 OpenJDK 的對比
基礎(chǔ)語法數(shù)據(jù)類型Java有哪些數(shù)據(jù)類型定義:Java語言是強類型語言,對于每一種數(shù)據(jù)都定義了明確的具體的數(shù)據(jù)類型,在內(nèi)存中分配了不同大小的內(nèi)存空間。 分類
Java基本數(shù)據(jù)類型圖 switch 是否能作用在 byte 上,是否能作用在 long 上,是否能作用在 String 上在 Java 5 以前,switch(expr)中,expr 只能是 byte、short、char、int。從 Java5 開始,Java 中引入了枚舉類型,expr 也可以是 enum 類型,從 Java 7 開始,expr 還可以是字符串(String),但是長整型(long)在目前所有的版本中都是不可以的。 用最有效率的方法計算 2 乘以 82 << 3(左移 3 位相當(dāng)于乘以 2 的 3 次方,右移 3 位相當(dāng)于除以 2 的 3 次方)。 Math.round(11.5) 等于多少?Math.round(-11.5)等于多少Math.round(11.5)的返回值是 12,Math.round(-11.5)的返回值是-11。四舍五入的原理是在參數(shù)上加 0.5 然后進行下取整。 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;相當(dāng)于 s1 = (short(s1 + 1);其中有隱含的強制類型轉(zhuǎn)換。 編碼Java語言采用何種編碼方案?有何特點?Java語言采用Unicode編碼標(biāo)準(zhǔn),Unicode(標(biāo)準(zhǔn)碼),它為每個字符制訂了一個唯一的數(shù)值,因此在任何的語言,平臺,程序都可以放心的使用。 注釋什么Java注釋定義:用于解釋說明程序的文字 分類
作用 在程序中,尤其是復(fù)雜的程序中,適當(dāng)?shù)丶尤胱⑨尶梢栽黾映绦虻目勺x性,有利于程序的修改、調(diào)試和交流。注釋的內(nèi)容在程序編譯的時候會被忽視,不會產(chǎn)生目標(biāo)代碼,注釋的部分不會對程序的執(zhí)行結(jié)果產(chǎn)生任何影響。 注意事項:多行和文檔注釋都不能嵌套使用。 訪問修飾符訪問修飾符 public,private,protected,以及不寫(默認(rèn))時的區(qū)別定義:Java中,可以使用訪問修飾符來保護對類、變量、方法和構(gòu)造方法的訪問。Java 支持 4 種不同的訪問權(quán)限。 分類 private : 在同一類內(nèi)可見。使用對象:變量、方法。 注意:不能修飾類(外部類) 訪問修飾符圖 運算符&和&&的區(qū)別&運算符有兩種用法:(1)按位與;(2)邏輯與。 &&運算符是短路與運算。邏輯與跟短路與的差別是非常巨大的,雖然二者都要求運算符左右兩端的布爾值都是true 整個表達式的值才是 true。&&之所以稱為短路運算,是因為如果&&左邊的表達式的值是 false,右邊的表達式會被直接短路掉,不會進行運算。 注意:邏輯或運算符(|)和短路或運算符(||)的差別也是如此。 關(guān)鍵字Java 有沒有 gotogoto 是 Java 中的保留字,在目前版本的 Java 中沒有使用。 final 有什么用?用于修飾類、屬性和方法;
final finally finalize區(qū)別
this關(guān)鍵字的用法this是自身的一個對象,代表對象本身,可以理解為:指向?qū)ο蟊旧淼囊粋€指針。 this的用法在java中大體可以分為3種: 1.普通的直接引用,this相當(dāng)于是指向當(dāng)前對象本身。 2.形參與成員名字重名,用this來區(qū)分:
3.引用本類的構(gòu)造函數(shù) 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關(guān)鍵字的用法super可以理解為是指向自己超(父)類對象的一個指針,而這個超類指的是離自己最近的一個父類。 super也有三種用法: 1.普通的直接引用 與this類似,super相當(dāng)于是指向當(dāng)前對象的父類的引用,這樣就可以用super.xxx來引用父類的成員。 2.子類中的成員變量或方法與父類中的成員變量或方法同名時,用super進行區(qū)分
3.引用父類構(gòu)造函數(shù) 3、引用父類構(gòu)造函數(shù)
this與super的區(qū)別
static存在的主要意義static的主要意義是在于創(chuàng)建獨立于具體對象的域變量或者方法。以致于即使沒有創(chuàng)建對象,也能使用屬性和調(diào)用方法! static關(guān)鍵字還有一個比較關(guān)鍵的作用就是 用來形成靜態(tài)代碼塊以優(yōu)化程序性能。static塊可以置于類中的任何地方,類中可以有多個static塊。在類初次被加載的時候,會按照static塊的順序來執(zhí)行每個static塊,并且只會執(zhí)行一次。 為什么說static塊可以用來優(yōu)化程序性能,是因為它的特性:只會在類加載的時候執(zhí)行一次。因此,很多時候會將一些只需要進行一次的初始化操作都放在static代碼塊中進行。 static的獨特之處1、被static修飾的變量或者方法是獨立于該類的任何對象,也就是說,這些變量和方法不屬于任何一個實例對象,而是被類的實例對象所共享。
2、在該類被第一次加載的時候,就會去加載被static修飾的部分,而且只在類第一次使用時加載并進行初始化,注意這是第一次用就要初始化,后面根據(jù)需要是可以再次賦值的。 3、static變量值在類加載的時候分配空間,以后創(chuàng)建類對象的時候不會重新分配。賦值的話,是可以任意賦值的! 4、被static修飾的變量或者方法是優(yōu)先于對象存在的,也就是說當(dāng)一個類加載完畢之后,即便沒有創(chuàng)建對象,也可以去訪問。 static應(yīng)用場景因為static是被類的實例對象所共享,因此如果某個成員變量是被所有對象所共享的,那么這個成員變量就應(yīng)該定義為靜態(tài)變量。 因此比較常見的static應(yīng)用場景有:
static注意事項1、靜態(tài)只能訪問靜態(tài)。 2、非靜態(tài)既可以訪問非靜態(tài)的,也可以訪問靜態(tài)的。 流程控制語句break ,continue ,return 的區(qū)別及作用break 跳出總上一層循環(huán),不再執(zhí)行循環(huán)(結(jié)束當(dāng)前的循環(huán)體) continue 跳出本次循環(huán),繼續(xù)執(zhí)行下次循環(huán)(結(jié)束正在執(zhí)行的循環(huán) 進入下一個循環(huán)條件) return 程序返回,不再執(zhí)行下面的代碼(結(jié)束當(dāng)前的方法 直接返回) 在 Java 中,如何跳出當(dāng)前的多重嵌套循環(huán)在Java中,要想跳出多重循環(huán),可以在外面的循環(huán)語句前定義一個標(biāo)號,然后在里層循環(huán)體的代碼中使用帶有標(biāo)號的break 語句,即可跳出外層循環(huán)。例如: public static void main(String[] args) { ok: for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; j++) { System.out.println('i=' + i + ',j=' + j); if (j == 5) { break ok; } } }}
面向?qū)ο?/h2> 面向?qū)ο蠛兔嫦蜻^程的區(qū)別 |
參數(shù) | 抽象類 | 接口 |
---|---|---|
聲明 | 抽象類使用abstract關(guān)鍵字聲明 | 接口使用interface關(guān)鍵字聲明 |
實現(xiàn) | 子類使用extends關(guān)鍵字來繼承抽象類。如果子類不是抽象類的話,它需要提供抽象類中所有聲明的方法的實現(xiàn) | 子類使用implements關(guān)鍵字來實現(xiàn)接口。它需要提供接口中所有聲明的方法的實現(xiàn) |
構(gòu)造器 | 抽象類可以有構(gòu)造器 | 接口不能有構(gòu)造器 |
訪問修飾符 | 抽象類中的方法可以是任意訪問修飾符 | 接口方法默認(rèn)修飾符是public。并且不允許定義為 private 或者 protected |
多繼承 | 一個類最多只能繼承一個抽象類 | 一個類可以實現(xiàn)多個接口 |
字段聲明 | 抽象類的字段聲明可以是任意的 | 接口的字段默認(rèn)都是 static 和 final 的 |
備注:Java8中接口中引入默認(rèn)方法和靜態(tài)方法,以此來減少抽象類和接口之間的差異。
現(xiàn)在,我們可以為接口提供默認(rèn)實現(xiàn)的方法了,并且不用強制子類來實現(xiàn)它。
接口和抽象類各有優(yōu)缺點,在接口和抽象類的選擇上,必須遵守這樣一個原則:
不能,定義抽象類就是讓其他類繼承的,如果定義為 final 該類就不能被繼承,這樣彼此就會產(chǎn)生矛盾,所以 final 不能修飾抽象類
new關(guān)鍵字,new創(chuàng)建對象實例(對象實例在堆內(nèi)存中),對象引用指向?qū)ο髮嵗▽ο笠么娣旁跅?nèi)存中)。一個對象引用可以指向0個或1個對象(一根繩子可以不系氣球,也可以系一個氣球);一個對象可以有n個引用指向它(可以用n條繩子系住一個氣球)
變量:在程序執(zhí)行的過程中,在某個范圍內(nèi)其值可以發(fā)生改變的量。從本質(zhì)上講,變量其實是內(nèi)存中的一小塊區(qū)域
成員變量:方法外部,類內(nèi)部定義的變量
局部變量:類的方法中的變量。
成員變量和局部變量的區(qū)別
作用域
成員變量:針對整個類有效。
局部變量:只在某個范圍內(nèi)有效。(一般指的就是方法,語句體內(nèi))
存儲位置
成員變量:隨著對象的創(chuàng)建而存在,隨著對象的消失而消失,存儲在堆內(nèi)存中。
局部變量:在方法被調(diào)用,或者語句被執(zhí)行的時候存在,存儲在棧內(nèi)存中。當(dāng)方法調(diào)用完,或者語句結(jié)束后,就自動釋放。
生命周期
成員變量:隨著對象的創(chuàng)建而存在,隨著對象的消失而消失
局部變量:當(dāng)方法調(diào)用完,或者語句結(jié)束后,就自動釋放。
初始值
成員變量:有默認(rèn)初始值。
局部變量:沒有默認(rèn)初始值,使用前必須賦值。
使用原則
在使用變量時需要遵循的原則為:就近原則
首先在局部范圍找,有就使用;接著在成員位置找。
Java程序在執(zhí)行子類的構(gòu)造方法之前,如果沒有用super()來調(diào)用父類特定的構(gòu)造方法,則會調(diào)用父類中“沒有參數(shù)的構(gòu)造方法”。因此,如果父類中只定義了有參數(shù)的構(gòu)造方法,而在子類的構(gòu)造方法中又沒有用super()來調(diào)用父類中特定的構(gòu)造方法,則編譯時將發(fā)生錯誤,因為Java程序在父類中找不到?jīng)]有參數(shù)的構(gòu)造方法可供執(zhí)行。解決辦法是在父類里加上一個不做事且沒有參數(shù)的構(gòu)造方法。
幫助子類做初始化工作。
主要作用是完成對類對象的初始化工作??梢詧?zhí)行。因為一個類即使沒有聲明構(gòu)造方法也會有默認(rèn)的不帶參數(shù)的構(gòu)造方法。
名字與類名相同;
沒有返回值,但不能用void聲明構(gòu)造函數(shù);
生成類的對象時自動執(zhí)行,無需調(diào)用。
靜態(tài)變量: 靜態(tài)變量由于不屬于任何實例對象,屬于類的,所以在內(nèi)存中只會有一份,在類的加載過程中,JVM只為靜態(tài)變量分配一次內(nèi)存空間。
實例變量: 每次創(chuàng)建對象,都會為每個對象分配成員變量內(nèi)存空間,實例變量是屬于實例對象的,在內(nèi)存中,創(chuàng)建幾次對象,就有幾份成員變量。
static變量也稱作靜態(tài)變量,靜態(tài)變量和非靜態(tài)變量的區(qū)別是:靜態(tài)變量被所有的對象所共享,在內(nèi)存中只有一個副本,它當(dāng)且僅當(dāng)在類初次加載時會被初始化。而非靜態(tài)變量是對象所擁有的,在創(chuàng)建對象的時候被初始化,存在多個副本,各個對象擁有的副本互不影響。
還有一點就是static成員變量的初始化順序按照定義的順序進行初始化。
靜態(tài)方法和實例方法的區(qū)別主要體現(xiàn)在兩個方面:
由于靜態(tài)方法可以不通過對象進行調(diào)用,因此在靜態(tài)方法里,不能調(diào)用其他非靜態(tài)變量,也不可以訪問非靜態(tài)變量成員。
方法的返回值是指我們獲取到的某個方法體中的代碼執(zhí)行后產(chǎn)生的結(jié)果?。ㄇ疤崾窃摲椒赡墚a(chǎn)生結(jié)果)。返回值的作用:接收出結(jié)果,使得它可以用于其他的操作!
在Java中,可以將一個類的定義放在另外一個類的定義內(nèi)部,這就是內(nèi)部類。內(nèi)部類本身就是類的一個屬性,與其他屬性定義方式一致。
內(nèi)部類可以分為四種:成員內(nèi)部類、局部內(nèi)部類、匿名內(nèi)部類和靜態(tài)內(nèi)部類。
定義在類內(nèi)部的靜態(tài)類,就是靜態(tài)內(nèi)部類。
public class Outer { private static int radius = 1; static class StaticInner { public void visit() { System.out.println('visit outer static variable:' + radius); } }}
靜態(tài)內(nèi)部類可以訪問外部類所有的靜態(tài)變量,而不可訪問外部類的非靜態(tài)變量;靜態(tài)內(nèi)部類的創(chuàng)建方式,new 外部類.靜態(tài)內(nèi)部類()
,如下:
Outer.StaticInner inner = new Outer.StaticInner();inner.visit();
定義在類內(nèi)部,成員位置上的非靜態(tài)類,就是成員內(nèi)部類。
public class Outer { private static int radius = 1; private int count =2; class Inner { public void visit() { System.out.println('visit outer static variable:' + radius); System.out.println('visit outer variable:' + count); } }}
成員內(nèi)部類可以訪問外部類所有的變量和方法,包括靜態(tài)和非靜態(tài),私有和公有。成員內(nèi)部類依賴于外部類的實例,它的創(chuàng)建方式外部類實例.new 內(nèi)部類()
,如下:
Outer outer = new Outer();Outer.Inner inner = outer.new Inner();inner.visit();
定義在方法中的內(nèi)部類,就是局部內(nèi)部類。
public class Outer { private int out_a = 1; private static int STATIC_b = 2; public void testFunctionClass(){ int inner_c =3; class Inner { private void fun(){ System.out.println(out_a); System.out.println(STATIC_b); System.out.println(inner_c); } } Inner inner = new Inner(); inner.fun(); } public static void testStaticFunctionClass(){ int d =3; class Inner { private void fun(){ // System.out.println(out_a); 編譯錯誤,定義在靜態(tài)方法中的局部類不可以訪問外部類的實例變量 System.out.println(STATIC_b); System.out.println(d); } } Inner inner = new Inner(); inner.fun(); }}
定義在實例方法中的局部類可以訪問外部類的所有變量和方法,定義在靜態(tài)方法中的局部類只能訪問外部類的靜態(tài)變量和方法。局部內(nèi)部類的創(chuàng)建方式,在對應(yīng)方法內(nèi),new 內(nèi)部類()
,如下:
public static void testStaticFunctionClass(){ class Inner { } Inner inner = new Inner(); }
匿名內(nèi)部類就是沒有名字的內(nèi)部類,日常開發(fā)中使用的比較多。
public class Outer { private void test(final int i) { new Service() { public void method() { for (int j = 0; j < i; j++) { System.out.println('匿名內(nèi)部類' ); } } }.method(); } } //匿名內(nèi)部類必須繼承或?qū)崿F(xiàn)一個已有的接口 interface Service{ void method();}
除了沒有名字,匿名內(nèi)部類還有以下特點:
匿名內(nèi)部類創(chuàng)建方式:
new 類/接口{ //匿名內(nèi)部類實現(xiàn)部分}
我們?yōu)槭裁匆褂脙?nèi)部類呢?因為它有以下優(yōu)點:
局部內(nèi)部類和匿名內(nèi)部類訪問局部變量的時候,為什么變量必須要加上final呢?它內(nèi)部原理是什么呢?
先看這段代碼:
public class Outer { void outMethod(){ final int a =10; class Inner { void innerMethod(){ System.out.println(a); } } }}
以上例子,為什么要加final呢?是因為生命周期不一致, 局部變量直接存儲在棧中,當(dāng)方法執(zhí)行結(jié)束后,非final的局部變量就被銷毀。而局部內(nèi)部類對局部變量的引用依然存在,如果局部內(nèi)部類要調(diào)用局部變量時,就會出錯。加了final,可以確保局部內(nèi)部類使用的變量與外層的局部變量區(qū)分開,解決了這個問題。
public class Outer { private int age = 12; class Inner { private int age = 13; public void print() { int age = 14; System.out.println('局部變量:' + age); System.out.println('內(nèi)部類變量:' + this.age); System.out.println('外部類變量:' + Outer.this.age); } } public static void main(String[] args) { Outer.Inner in = new Outer().new Inner(); in.print(); }}
運行結(jié)果:
局部變量:14內(nèi)部類變量:13外部類變量:12
構(gòu)造器不能被繼承,因此不能被重寫,但可以被重載。
方法的重載和重寫都是實現(xiàn)多態(tài)的方式,區(qū)別在于前者實現(xiàn)的是編譯時的多態(tài)性,而后者實現(xiàn)的是運行時的多態(tài)性。
重載:發(fā)生在同一個類中,方法名相同參數(shù)列表不同(參數(shù)類型不同、個數(shù)不同、順序不同),與方法返回值和訪問修飾符無關(guān),即重載的方法不能根據(jù)返回類型進行區(qū)分
重寫:發(fā)生在父子類中,方法名、參數(shù)列表必須相同,返回值小于等于父類,拋出的異常小于等于父類,訪問修飾符大于等于父類(里氏代換原則);如果父類方法訪問修飾符為private則子類中就不是重寫。
== : 它的作用是判斷兩個對象的地址是不是相等。即,判斷兩個對象是不是同一個對象。(基本數(shù)據(jù)類型 == 比較的是值,引用數(shù)據(jù)類型 == 比較的是內(nèi)存地址)
equals() : 它的作用也是判斷兩個對象是否相等。但它一般有兩種使用情況:
情況1:類沒有覆蓋 equals() 方法。則通過 equals() 比較該類的兩個對象時,等價于通過“==”比較這兩個對象。
情況2:類覆蓋了 equals() 方法。一般,我們都覆蓋 equals() 方法來兩個對象的內(nèi)容相等;若它們的內(nèi)容相等,則返回 true (即,認(rèn)為這兩個對象相等)。
舉個例子:
public class test1 { public static void main(String[] args) { String a = new String('ab'); // a 為一個引用 String b = new String('ab'); // b為另一個引用,對象的內(nèi)容一樣 String aa = 'ab'; // 放在常量池中 String bb = 'ab'; // 從常量池中查找 if (aa == bb) // true System.out.println('aa==bb'); if (a == b) // false,非同一對象 System.out.println('a==b'); if (a.equals(b)) // true System.out.println('aEQb'); if (42 == 42.0) { // true System.out.println('true'); } }}
說明:
HashSet如何檢查重復(fù)
兩個對象的 hashCode() 相同,則 equals() 也一定為 true,對嗎?
hashCode和equals方法的關(guān)系
面試官可能會問你:“你重寫過 hashcode 和 equals 么,為什么重寫equals時必須重寫hashCode方法?”
hashCode()介紹
hashCode() 的作用是獲取哈希碼,也稱為散列碼;它實際上是返回一個int整數(shù)。這個哈希碼的作用是確定該對象在哈希表中的索引位置。hashCode() 定義在JDK的Object.java中,這就意味著Java中的任何類都包含有hashCode()函數(shù)。
散列表存儲的是鍵值對(key-value),它的特點是:能根據(jù)“鍵”快速的檢索出對應(yīng)的“值”。這其中就利用到了散列碼!(可以快速找到所需要的對象)
為什么要有 hashCode
我們以“HashSet 如何檢查重復(fù)”為例子來說明為什么要有 hashCode:
當(dāng)你把對象加入 HashSet 時,HashSet 會先計算對象的 hashcode 值來判斷對象加入的位置,同時也會與其他已經(jīng)加入的對象的 hashcode 值作比較,如果沒有相符的hashcode,HashSet會假設(shè)對象沒有重復(fù)出現(xiàn)。但是如果發(fā)現(xiàn)有相同 hashcode 值的對象,這時會調(diào)用 equals()方法來檢查 hashcode 相等的對象是否真的相同。如果兩者相同,HashSet 就不會讓其加入操作成功。如果不同的話,就會重新散列到其他位置。(摘自我的Java啟蒙書《Head first java》第二版)。這樣我們就大大減少了 equals 的次數(shù),相應(yīng)就大大提高了執(zhí)行速度。
hashCode()與equals()的相關(guān)規(guī)定
如果兩個對象相等,則hashcode一定也是相同的
兩個對象相等,對兩個對象分別調(diào)用equals方法都返回true
兩個對象有相同的hashcode值,它們也不一定是相等的
因此,equals 方法被覆蓋過,則 hashCode 方法也必須被覆蓋
hashCode() 的默認(rèn)行為是對堆上的對象產(chǎn)生獨特值。如果沒有重寫 hashCode(),則該 class 的兩個對象無論如何都不會相等(即使這兩個對象指向相同的數(shù)據(jù))
對象的相等 比的是內(nèi)存中存放的內(nèi)容是否相等而 引用相等 比較的是他們指向的內(nèi)存地址是否相等。
是值傳遞。Java 語言的方法調(diào)用只支持參數(shù)的值傳遞。當(dāng)一個對象實例作為一個參數(shù)被傳遞到方法中時,參數(shù)的值就是對該對象的引用。對象的屬性可以在被調(diào)用過程中被改變,但對對象引用的改變是不會影響到調(diào)用者的
首先回顧一下在程序設(shè)計語言中有關(guān)將參數(shù)傳遞給方法(或函數(shù))的一些專業(yè)術(shù)語。按值調(diào)用(call by value)表示方法接收的是調(diào)用者提供的值,而按引用調(diào)用(call by reference)表示方法接收的是調(diào)用者提供的變量地址。一個方法可以修改傳遞引用所對應(yīng)的變量值,而不能修改傳遞值調(diào)用所對應(yīng)的變量值。 它用來描述各種程序設(shè)計語言(不只是Java)中方法參數(shù)傳遞方式。
Java程序設(shè)計語言總是采用按值調(diào)用。也就是說,方法得到的是所有參數(shù)值的一個拷貝,也就是說,方法不能修改傳遞給它的任何參數(shù)變量的內(nèi)容。
下面通過 3 個例子來給大家說明
example 1
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);}
結(jié)果:
a = 20b = 10num1 = 10num2 = 20
解析:
在swap方法中,a、b的值進行交換,并不會影響到 num1、num2。因為,a、b中的值,只是從 num1、num2 的復(fù)制過來的。也就是說,a、b相當(dāng)于num1、num2 的副本,副本的內(nèi)容無論怎么修改,都不會影響到原件本身。
通過上面例子,我們已經(jīng)知道了一個方法不能修改一個基本數(shù)據(jù)類型的參數(shù),而對象引用作為參數(shù)就不一樣,請看 example2.
example 2
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) { // 將數(shù)組的第一個元素變?yōu)? array[0] = 0; }
結(jié)果:
10
解析:
array 被初始化 arr 的拷貝也就是一個對象的引用,也就是說 array 和 arr 指向的時同一個數(shù)組對象。 因此,外部對引用對象的改變會反映到所對應(yīng)的對象上。
通過 example2 我們已經(jīng)看到,實現(xiàn)一個改變對象參數(shù)狀態(tài)的方法并不是一件難事。理由很簡單,方法得到的是對象引用的拷貝,對象引用及其他的拷貝同時引用同一個對象。
很多程序設(shè)計語言(特別是,C++和Pascal)提供了兩種參數(shù)傳遞的方式:值調(diào)用和引用調(diào)用。有些程序員(甚至本書的作者)認(rèn)為Java程序設(shè)計語言對對象采用的是引用調(diào)用,實際上,這種理解是不對的。由于這種誤解具有一定的普遍性,所以下面給出一個反例來詳細(xì)地闡述一下這個問題。
example 3
public class Test { public static void main(String[] args) { // TODO Auto-generated method stub Student s1 = new Student('小張'); Student s2 = new Student('小李'); Test.swap(s1, s2); System.out.println('s1:' + s1.getName()); System.out.println('s2:' + s2.getName()); } public static void swap(Student x, Student y) { Student temp = x; x = y; y = temp; System.out.println('x:' + x.getName()); System.out.println('y:' + y.getName()); }}
結(jié)果:
x:小李y:小張s1:小張s2:小李
解析:
交換之前:
交換之后:
通過上面兩張圖可以很清晰的看出: 方法并沒有改變存儲在變量 s1 和 s2 中的對象引用。swap方法的參數(shù)x和y被初始化為兩個對象引用的拷貝,這個方法交換的是這兩個拷貝
總結(jié)
Java程序設(shè)計語言對對象采用的不是引用調(diào)用,實際上,對象引用是按值傳遞的。
下面再總結(jié)一下Java中方法參數(shù)的使用情況:
值傳遞:指的是在方法調(diào)用時,傳遞的參數(shù)是按值的拷貝傳遞,傳遞的是值的拷貝,也就是說傳遞后就互不相關(guān)了。
引用傳遞:指的是在方法調(diào)用時,傳遞的參數(shù)是按引用進行傳遞,其實傳遞的引用的地址,也就是變量所對應(yīng)的內(nèi)存空間的地址。傳遞的是值的引用,也就是說傳遞前和傳遞后都指向同一個引用(也就是同一個內(nèi)存空間)。
剛開始的時候 JavaAPI 所必需的包是 java 開頭的包,javax 當(dāng)時只是擴展 API 包來說使用。然而隨著時間的推移,javax 逐漸的擴展成為 Java API 的組成部分。但是,將擴展從 javax 包移動到 java 包將是太麻煩了,最終會破壞一堆現(xiàn)有的代碼。因此,最終決定 javax 包將成為標(biāo)準(zhǔn)API的一部分。
所以,實際上java和javax沒有區(qū)別。這都是一個名字。
Java Io流共涉及40多個類,這些類看上去很雜亂,但實際上很有規(guī)則,而且彼此之間存在非常緊密的聯(lián)系, Java I0流的40多個類都是從如下4個抽象類基類中派生出來的。
按操作方式分類結(jié)構(gòu)圖:
按操作對象分類結(jié)構(gòu)圖:
簡答
詳細(xì)回答
Socket
和 ServerSocket
相對應(yīng)的 SocketChannel
和 ServerSocketChannel
兩種不同的套接字通道實現(xiàn),兩種通道都支持阻塞和非阻塞兩種模式。阻塞模式使用就像傳統(tǒng)中的支持一樣,比較簡單,但是性能和可靠性都不好;非阻塞模式正好與之相反。對于低負(fù)載、低并發(fā)的應(yīng)用程序,可以使用同步阻塞I/O來提升開發(fā)速率和更好的維護性;對于高負(fù)載、高并發(fā)的(網(wǎng)絡(luò))應(yīng)用,應(yīng)使用 NIO 的非阻塞模式來開發(fā)JAVA反射機制是在運行狀態(tài)中,對于任意一個類,都能夠知道這個類的所有屬性和方法;對于任意一個對象,都能夠調(diào)用它的任意一個方法和屬性;這種動態(tài)獲取的信息以及動態(tài)調(diào)用對象的方法的功能稱為java語言的反射機制。
靜態(tài)編譯和動態(tài)編譯
反射是框架設(shè)計的靈魂。
在我們平時的項目開發(fā)過程中,基本上很少會直接使用到反射機制,但這不能說明反射機制沒有用,實際上有很多設(shè)計、開發(fā)都與反射機制有關(guān),例如模塊化的開發(fā),通過反射去調(diào)用對應(yīng)的字節(jié)碼;動態(tài)代理設(shè)計模式也采用了反射機制,還有我們?nèi)粘J褂玫?Spring/Hibernate 等框架也大量使用到了反射機制。
舉例:①我們在使用JDBC連接數(shù)據(jù)庫時使用Class.forName()通過反射加載數(shù)據(jù)庫的驅(qū)動程序;②Spring框架也用到很多反射機制,最經(jīng)典的就是xml的配置模式。Spring 通過 XML 配置模式裝載 Bean 的過程:1) 將程序內(nèi)所有 XML 或 Properties 配置文件加載入內(nèi)存中; 2)Java類里面解析xml或properties里面的內(nèi)容,得到對應(yīng)實體類的字節(jié)碼字符串以及相關(guān)的屬性信息; 3)使用反射機制,根據(jù)這個字符串獲得某個類的Class實例; 4)動態(tài)配置實例的屬性
1.通過new對象實現(xiàn)反射機制 2.通過路徑實現(xiàn)反射機制 3.通過類名實現(xiàn)反射機制
public class Student { private int id; String name; protected boolean sex; public float score;}
public class Get { //獲取反射機制三種方式 public static void main(String[] args) throws ClassNotFoundException { //方式一(通過建立對象) Student stu = new Student(); Class classobj1 = stu.getClass(); System.out.println(classobj1.getName()); //方式二(所在通過路徑-相對路徑) Class classobj2 = Class.forName('fanshe.Student'); System.out.println(classobj2.getName()); //方式三(通過類名) Class classobj3 = Student.class; System.out.println(classobj3.getName()); }}
網(wǎng)絡(luò)編程的面試題可以查看我的這篇文章重學(xué)TCP/IP協(xié)議和三次握手四次揮手,內(nèi)容不僅包括TCP/IP協(xié)議和三次握手四次揮手的知識,還包括計算機網(wǎng)絡(luò)體系結(jié)構(gòu),HTTP協(xié)議,get請求和post請求區(qū)別,session和cookie的區(qū)別等,歡迎大家閱讀。
字符串常量池位于堆內(nèi)存中,專門用來存儲字符串常量,可以提高內(nèi)存的使用率,避免開辟多塊空間存儲相同的字符串,在創(chuàng)建字符串時 JVM 會首先檢查字符串常量池,如果該字符串已經(jīng)存在池中,則返回它的引用,如果不存在,則實例化一個字符串放到池中,并返回其引用。
不是。Java 中的基本數(shù)據(jù)類型只有 8 個 :byte、short、int、long、float、double、char、boolean;除了基本類型(primitive type),剩下的都是引用類型(referencetype),Java 5 以后引入的枚舉類型也算是一種比較特殊的引用類型。
這是很基礎(chǔ)的東西,但是很多初學(xué)者卻容易忽視,Java 的 8 種基本數(shù)據(jù)類型中不包括 String,基本數(shù)據(jù)類型中用來描述文本數(shù)據(jù)的是 char,但是它只能表示單個字符,比如 ‘a(chǎn)’,‘好’ 之類的,如果要描述一段文本,就需要用多個 char 類型的變量,也就是一個 char 類型數(shù)組,比如“你好” 就是長度為2的數(shù)組 char[] chars = {‘你’,‘好’};
但是使用數(shù)組過于麻煩,所以就有了 String,String 底層就是一個 char 類型的數(shù)組,只是使用的時候開發(fā)者不需要直接操作底層數(shù)組,用更加簡便的方式即可完成對字符串的使用。
不變性:String 是只讀字符串,是一個典型的 immutable 對象,對它進行任何操作,其實都是創(chuàng)建一個新的對象,再把引用指向該對象。不變模式的主要作用在于當(dāng)一個對象需要被多線程共享并頻繁訪問時,可以保證數(shù)據(jù)的一致性。
常量池優(yōu)化:String 對象創(chuàng)建之后,會在字符串常量池中進行緩存,如果下次創(chuàng)建同樣的對象時,會直接返回緩存的引用。
final:使用 final 來定義 String 類,表示 String 類不能被繼承,提高了系統(tǒng)的安全性。
簡單來說就是String類利用了final修飾的char類型數(shù)組存儲字符,源碼如下圖所以:
/** The value is used for character storage. */private final char value[];
我覺得如果別人問這個問題的話,回答不可變就可以了。 下面只是給大家看兩個有代表性的例子:
1) String不可變但不代表引用不可以變
String str = 'Hello';str = str + ' World';System.out.println('str=' + str);
結(jié)果:
str=Hello World
解析:
實際上,原來String的內(nèi)容是不變的,只是str由原來指向'Hello'的內(nèi)存地址轉(zhuǎn)為指向'Hello World'的內(nèi)存地址而已,也就是說多開辟了一塊內(nèi)存區(qū)域給'Hello World'字符串。
2) 通過反射是可以修改所謂的“不可變”對象
// 創(chuàng)建字符串'Hello World', 并賦給引用sString s = 'Hello World';System.out.println('s = ' + s); // Hello World// 獲取String類中的value字段Field valueFieldOfString = String.class.getDeclaredField('value');// 改變value屬性的訪問權(quán)限valueFieldOfString.setAccessible(true);// 獲取s對象上的value屬性的值char[] value = (char[]) valueFieldOfString.get(s);// 改變value所引用的數(shù)組中的第5個字符value[5] = '_';System.out.println('s = ' + s); // Hello_World
結(jié)果:
s = Hello Worlds = Hello_World
解析:
用反射可以訪問私有成員, 然后反射出String對象中的value屬性, 進而改變通過獲得的value引用改變數(shù)組的結(jié)構(gòu)。但是一般我們不會這么做,這里只是簡單提一下有這個東西。
String 類是 final 類,不可以被繼承。
不一樣,因為內(nèi)存的分配方式不一樣。String str='i'的方式,java 虛擬機會將其分配到常量池中;而 String str=new String(“i”) 則會被分到堆內(nèi)存中。
兩個對象,一個是靜態(tài)區(qū)的'xyz',一個是用new創(chuàng)建在堆上的對象。
String str1 = 'hello'; //str1指向靜態(tài)區(qū)String str2 = new String('hello'); //str2指向堆上的對象String str3 = 'hello';String str4 = new String('hello');System.out.println(str1.equals(str2)); //trueSystem.out.println(str2.equals(str4)); //trueSystem.out.println(str1 == str3); //trueSystem.out.println(str1 == str2); //falseSystem.out.println(str2 == str4); //falseSystem.out.println(str2 == 'hello'); //falsestr2 = str1;System.out.println(str2 == 'hello'); //true
使用 StringBuilder 或者 stringBuffer 的 reverse() 方法。
示例代碼:
// StringBuffer reverseStringBuffer stringBuffer = new StringBuffer();stringBuffer. append('abcdefg');System. out. println(stringBuffer. reverse()); // gfedcba// StringBuilder reverseStringBuilder stringBuilder = new StringBuilder();stringBuilder. append('abcdefg');System. out. println(stringBuilder. reverse()); // gfedcba
數(shù)組沒有 length()方法 ,有 length 的屬性。String 有 length()方法。JavaScript中,獲得字符串的長度是通過 length 屬性得到的,這一點容易和 Java 混淆。
HashMap 內(nèi)部實現(xiàn)是通過 key 的 hashcode 來確定 value 的存儲位置,因為字符串是不可變的,所以當(dāng)創(chuàng)建字符串時,它的 hashcode 被緩存下來,不需要再次計算,所以相比于其他對象更快。
可變性
String類中使用字符數(shù)組保存字符串,private final char value[],所以string對象是不可變的。StringBuilder與StringBuffer都繼承自AbstractStringBuilder類,在AbstractStringBuilder中也是使用字符數(shù)組保存字符串,char[] value,這兩種對象都是可變的。
線程安全性
String中的對象是不可變的,也就可以理解為常量,線程安全。AbstractStringBuilder是StringBuilder與StringBuffer的公共父類,定義了一些字符串的基本操作,如expandCapacity、append、insert、indexOf等公共方法。StringBuffer對方法加了同步鎖或者對調(diào)用的方法加了同步鎖,所以是線程安全的。StringBuilder并沒有對方法進行加同步鎖,所以是非線程安全的。
性能
每次對String 類型進行改變的時候,都會生成一個新的String對象,然后將指針指向新的String 對象。StringBuffer每次都會對StringBuffer對象本身進行操作,而不是生成新的對象并改變對象引用。相同情況下使用StirngBuilder 相比使用StringBuffer 僅能獲得10%~15% 左右的性能提升,但卻要冒多線程不安全的風(fēng)險。
對于三者使用的總結(jié)
如果要操作少量的數(shù)據(jù)用 = String
單線程操作字符串緩沖區(qū) 下操作大量數(shù)據(jù) = StringBuilder
多線程操作字符串緩沖區(qū) 下操作大量數(shù)據(jù) = StringBuffer
裝箱:將基本類型用它們對應(yīng)的引用類型包裝起來;
拆箱:將包裝類型轉(zhuǎn)換為基本數(shù)據(jù)類型;
Java 是一個近乎純潔的面向?qū)ο缶幊陶Z言,但是為了編程的方便還是引入了基本數(shù)據(jù)類型,但是為了能夠?qū)⑦@些基本數(shù)據(jù)類型當(dāng)成對象操作,Java 為每一個基本數(shù)據(jù)類型都引入了對應(yīng)的包裝類型(wrapper class),int 的包裝類就是 Integer,從 Java 5 開始引入了自動裝箱/拆箱機制,使得二者可以相互轉(zhuǎn)換。
Java 為每個原始類型提供了包裝類型:
原始類型: boolean,char,byte,short,int,long,float,double
包裝類型:Boolean,Character,Byte,Short,Integer,Long,F(xiàn)loat,Double
對于對象引用類型:==比較的是對象的內(nèi)存地址。
對于基本數(shù)據(jù)類型:==比較的是值。
如果整型字面量的值在-128到127之間,那么自動裝箱時不會new新的Integer對象,而是直接引用常量池中的Integer對象,超過范圍 a1==b1的結(jié)果是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}
|