面霸篇:高頻 Java 基礎問題(核心卷一)
從面試題作為切入點提升大家的 Java 內(nèi)功,所謂根基不牢,地動山搖。只有扎實的基礎,才是寫出寫好代碼。
拒絕知識碎片化
碼哥在 《Redis 系列》的開篇 Redis 為什么這么快中說過:學習一個技術,通常只接觸了零散的技術點,沒有在腦海里建立一個完整的知識框架和架構體系,沒有系統(tǒng)觀。這樣會很吃力,而且會出現(xiàn)一看好像自己會,過后就忘記,一臉懵逼。
我們需要一個系統(tǒng)觀,清晰完整的去學習技術,同時也不能埋頭苦干,過于死磕某個細節(jié)。
跟著「碼哥」一起來提綱挈領,梳理一個相對完整的 Java 開發(fā)技術能力圖譜,將基礎夯實。
萬字總結,建議收藏。面試不慌,加薪有望。
Java 平臺的理解
碼老濕,你是怎么理解 Java 平臺呢?
Java 是一種面向對象的語言,有兩個明顯特性:
跨平臺能力:一次編寫,到處運行(Write once,run anywhere);
垃圾收集:
Java 通過字節(jié)碼和 Java 虛擬機(JVM)這種跨平臺的抽象,屏蔽了操作系統(tǒng)和硬件的細節(jié),這也是實現(xiàn)「一次編譯,到處執(zhí)行」的基礎。
Java 通過垃圾收集器(Garbage Collector)回收分配內(nèi)存,大部分情況下,程序員不需要自己操心內(nèi)存的分配和回收。
最常見的垃圾收集器,如 SerialGC、Parallel GC、 CMS、 G1 等,對于適用于什么樣的工作負載最好也心里有數(shù)。
JVM、JRE、JDK 關系
碼老濕,能說下 JVM、JRE 和 JDK 的關系么?
JVM Java Virtual Machine 是 Java 虛擬機,Java 程序需要運行在虛擬機上,不同的平臺有自己的虛擬機,因此 Java 語言可以實現(xiàn)跨平臺。
JRE Java Runtime Environment包括 Java 虛擬機和 Java 程序所需的核心類庫等。
核心類庫主要是 java.lang 包:包含了運行 Java 程序必不可少的系統(tǒng)類,如基本數(shù)據(jù)類型、基本數(shù)學函數(shù)、字符串處理、線程、異常處理類等,系統(tǒng)缺省加載這個包
如果想要運行一個開發(fā)好的 Java 程序,計算機中只需要安裝 JRE 即可。
JDK Java Development Kit是提供給 Java 開發(fā)人員使用的,其中包含了 Java 的開發(fā)工具,也包括了 JRE。
所以安裝了 JDK,就無需再單獨安裝 JRE 了。其中的開發(fā)工具:編譯工具(javac.exe),打包工具(jar.exe) 等。
Java 是解釋執(zhí)行么?
碼老濕,Java 是解釋執(zhí)行的么?
這個說法不太準確。
我們開發(fā)的 Java 的源代碼,首先通過 Javac 編譯成為字節(jié)碼(bytecode),在運行時,通過 Java 虛擬機(JVM)內(nèi)嵌的解釋器將字節(jié)碼轉換成為最終的機器碼。
但是常見的 JVM,比如我們大多數(shù)情況使用的 Oracle JDK 提供的 Hotspot JVM,都提供了 JIT(Just-In-Time)編譯器。
也就是通常說的動態(tài)編譯器,JIT 能夠在運行時將熱點代碼編譯成機器碼,這種情況下部分熱點代碼就屬于編譯執(zhí)行,而不是解釋執(zhí)行了。
采用字節(jié)碼的好處
什么是字節(jié)碼?采用字節(jié)碼的好處是什么?
字節(jié)碼:Java 源代碼經(jīng)過虛擬機編譯器編譯后產(chǎn)生的文件(即擴展為.class 的文件),它不面向任何特定的處理器,只面向虛擬機。
采用字節(jié)碼的好處:
眾所周知,我們通常把 Java 分為編譯期和運行時。這里說的 Java 的編譯和 C/C++ 是有著不同的意義的,Javac 的編譯,編譯 Java 源碼生成“.class”文件里面實際是字節(jié)碼,而不是可以直接執(zhí)行的機器碼。Java 通過字節(jié)碼和 Java 虛擬機(JVM)這種跨平臺的抽象,屏蔽了操作系統(tǒng)和硬件的細節(jié),這也是實現(xiàn)“一次編譯,到處執(zhí)行”的基礎。
基礎語法
JDK 1.8 之后有哪些新特性
接口默認方法:Java8 允許我們給接口添加一個非抽象的方法實現(xiàn),只需要使用 default 關鍵字即可。
Lambda 表達式和函數(shù)式接口:Lambda 表達式本質(zhì)上是一段匿名內(nèi)部類,也可以是一段可以傳遞的代碼。
Lambda 允許把函數(shù)作為一個方法的參數(shù)(函數(shù)作為參數(shù)傳遞到方法中),使用 Lambda 表達式使代碼更加簡潔,但是也不要濫用,否則會有可讀性等問題,《EffectiveJava》作者 JoshBloch 建議使用 Lambda 表達式最好不要超過 3 行。
StreamAPI:用函數(shù)式編程方式在集合類上進行復雜操作的工具,配合 Lambda 表達式可以方便的對集合進行處理。
Java8 中處理集合的關鍵抽象概念,它可以指定你希望對集合進行的操作,可以執(zhí)行非常復雜的查找、過濾和映射數(shù)據(jù)等操作。
使用 StreamAPI 對集合數(shù)據(jù)進行操作,就類似于使用 SQL 執(zhí)行的數(shù)據(jù)庫查詢。也可以使用 StreamAPI 來并行執(zhí)行操作。
簡而言之,StreamAPI 提供了一種高效且易于使用的處理數(shù)據(jù)的方式。
方法引用:方法引用提供了非常有用的語法,可以直接引用已有 Java 類或對象(實例)的方法或構造器。
與 lambda 聯(lián)合使用,方法引用可以使語言的構造更緊湊簡潔,減少冗余代碼。
日期時間 API:Java8 引入了新的日期時間 API 改進了日期時間的管理。
Optional 類:著名的 NullPointerException 是引起系統(tǒng)失敗最常見的原因。
很久以前 GoogleGuava 項目引入了 Optional 作為解決空指針異常的一種方式,不贊成代碼被 null 檢查的代碼污染,期望程序員寫整潔的代碼。
受 GoogleGuava 的鼓勵,Optional 現(xiàn)在是 Java8 庫的一部分。
新工具:新的編譯工具,如:Nashorn 引擎 jjs、類依賴分析器 jdeps。
構造器是否可以重寫
Constructor 不能被 override(重寫),但是可以 overload(重載),所以你可以看到⼀個類中有多個構造函數(shù)的情況。
wait() 和 sleep 區(qū)別
來源不同:sleep()來自 Thread 類,wait()來自 Object 類。
對于同步鎖的影響不同:sleep()不會該表同步鎖的行為,如果當前線程持有同步鎖,那么 sleep 是不會讓線程釋放同步鎖的。
wait()會釋放同步鎖,讓其他線程進入 synchronized 代碼塊執(zhí)行。
使用范圍不同:sleep()可以在任何地方使用。wait()只能在同步控制方法或者同步控制塊里面使用,否則會拋 IllegalMonitorStateException。
恢復方式不同:兩者會暫停當前線程,但是在恢復上不太一樣。sleep()在時間到了之后會重新恢復;
wait()則需要其他線程調(diào)用同一對象的 notify()/nofityAll()才能重新恢復。
&和&&的區(qū)別
&運算符有兩種用法:
- 按位與;
- 邏輯與。
&&運算符是短路與運算。邏輯與跟短路與的差別是非常巨大的,雖然二者都要求運算符左右兩端的布爾值都是 true 整個表達式的值才是 true。
&&之所以稱為短路運算,是因為如果&&左邊的表達式的值是 false,右邊的表達式會被直接短路掉,不會進行運算。
注意:邏輯或運算符(|)和短路或運算符(||)的差別也是如此。
Java 有哪些數(shù)據(jù)類型?
Java 語言是強類型語言,對于每一種數(shù)據(jù)都定義了明確的具體的數(shù)據(jù)類型,在內(nèi)存中分配了不同大小的內(nèi)存空間。
分類
基本數(shù)據(jù)類型
- 整數(shù)類型(byte,short,int,long)
- 浮點類型(float,double)
- 數(shù)值型
- 字符型(char)
- 布爾型(boolean)
引用數(shù)據(jù)類型
- 類(class)
- 接口(interface)
- 數(shù)組([])
this 關鍵字的用法
this 是自身的一個對象,代表對象本身,可以理解為:指向對象本身的一個指針。
this 的用法在 java 中大體可以分為 3 種:
- 普通的直接引用,this 相當于是指向當前對象本身。
- 形參與成員名字重名,用 this 來區(qū)分:
- public Person(String name, int age) {
- this.name = name;
- this.age = age;
- }
引用本類的構造函數(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 關鍵字的用法
super 可以理解為是指向自己超(父)類對象的一個指針,而這個超類指的是離自己最近的一個父類。
super 也有三種用法:
1.普通的直接引用:與 this 類似,super 相當于是指向當前對象的父類的引用,這樣就可以用 super.xxx 來引用父類的成員。
2.子類中的成員變量或方法與父類中的成員變量或方法同名時,用 super 進行區(qū)分
- class Person{
- protected String name;
- public Person(String name) {
- this.name = name;
- }
- }
- class Student extends Person{
- private String name;
- public Student(String name, String name1) {
- super(name);
- this.name = name1;
- }
- public void getInfo(){
- System.out.println(this.name); //Child
- System.out.println(super.name); //Father
- }
- }
- public class Test {
- public static void main(String[] args) {
- Student s1 = new Student("Father","Child");
- s1.getInfo();
- }
- }
3.引用父類構造函數(shù);
成員變量與局部變量的區(qū)別有哪些變量:在程序執(zhí)行的過程中,在某個范圍內(nèi)其值可以發(fā)生改變的量。從本質(zhì)上講,變量其實是內(nèi)存中的一小塊區(qū)域。
成員變量:方法外部,類內(nèi)部定義的變量。
局部變量:類的方法中的變量。
區(qū)別如下:
作用域
成員變量:針對整個類有效。局部變量:只在某個范圍內(nèi)有效。(一般指的就是方法,語句體內(nèi))
存儲位置
成員變量:隨著對象的創(chuàng)建而存在,隨著對象的消失而消失,存儲在堆內(nèi)存中。
局部變量:在方法被調(diào)用,或者語句被執(zhí)行的時候存在,存儲在棧內(nèi)存中。當方法調(diào)用完,或者語句結束后,就自動釋放。
生命周期
成員變量:隨著對象的創(chuàng)建而存在,隨著對象的消失而消失 局部變量:當方法調(diào)用完,或者語句結束后,就自動釋放。
初始值
成員變量:有默認初始值。
局部變量:沒有默認初始值,使用前必須賦值。
動態(tài)代理是基于什么原理
基于反射實現(xiàn)
反射機制是 Java 語言提供的一種基礎功能,賦予程序在運行時自省(introspect,官方用語)的能力。通過反射我們可以直接操作類或者對象,比如獲取某個對象的類定義,獲取類聲明的屬性和方法,調(diào)用方法或者構造對象,甚至可以運行時修改類定義。
碼老濕,他的使用場景是什么?
AOP 通過(動態(tài))代理機制可以讓開發(fā)者從這些繁瑣事項中抽身出來,大幅度提高了代碼的抽象程度和復用度。
包裝 RPC 調(diào)用:通過代理可以讓調(diào)用者與實現(xiàn)者之間解耦。比如進行 RPC 調(diào)用,框架內(nèi)部的尋址、序列化、反序列化等,對于調(diào)用者往往是沒有太大意義的,通過代理,可以提供更加友善的界面。
int 與 Integer 區(qū)別
Java 是一個近乎純潔的面向對象編程語言,但是為了編程的方便還是引入了基本數(shù)據(jù)類型,但是為了能夠將這些基本數(shù)據(jù)類型當成對象操作,Java 為每一個基本數(shù)據(jù)類型都引入了對應的包裝類型(wrapper class),int 的包裝類就是 Integer,從 Java 5 開始引入了自動裝箱/拆箱機制,使得二者可以相互轉換。
Java 為每個原始類型提供了包裝類型:
- 原始類型: boolean,char,byte,short,int,long,float,double。
- 包裝類型:Boolean,Character,Byte,Short,Integer,Long,F(xiàn)loat,Double。
int 是我們常說的整形數(shù)字,是 Java 的 8 個原始數(shù)據(jù)類型(Primitive Types,boolean、byte 、short、char、int、float、double、long)之一。Java 語言雖然號稱一切都是對象,但原始數(shù)據(jù)類型是例外。
Integer 是 int 對應的包裝類,它有一個 int 類型的字段存儲數(shù)據(jù),并且提供了基本操作,比如數(shù)學運算、int 和字符串之間轉換等。在 Java 5 中,引入了自動裝箱和自動拆箱功能(boxing/unboxing),Java 可以根據(jù)上下文,自動進行轉換,極大地簡化了相關編程。
Integer a= 127 與 Integer b = 127 相等嗎
對于對象引用類型:==比較的是對象的內(nèi)存地址。對于基本數(shù)據(jù)類型:==比較的是值。
大部分數(shù)據(jù)操作都是集中在有限的、較小的數(shù)值范圍,因而,在 Java 5 中新增了靜態(tài)工廠方法 valueOf,在調(diào)用它的時候會利用一個緩存機制,帶來了明顯的性能改進。按照 Javadoc,這個值默認緩存是 -128 到 127 之間。
如果整型字面量的值在-128 到 127 之間,那么自動裝箱時不會 new 新的 Integer 對象,而是直接引用常量池中的 Integer 對象,超過范圍 a1==b1 的結果是 false。
- public static void main(String[] args) {
- Integer a = new Integer(3);
- Integer b = 3; // 將3自動裝箱成Integer類型
- int c = 3;
- System.out.println(a == b); // false 兩個引用沒有引用同一對象
- System.out.println(a == c); // true a自動拆箱成int類型再和c比較
- System.out.println(b == c); // true
- Integer a1 = 128;
- Integer b1 = 128;
- System.out.println(a1 == b1); // false
- Integer a2 = 127;
- Integer b2 = 127;
- System.out.println(a2 == b2); // true
- }
面向對象
面向對象與面向過程的區(qū)別是什么?
面向過程
優(yōu)點:性能比面向對象高,因為類調(diào)用時需要實例化,開銷比較大,比較消耗資源;比如單片機、嵌入式開發(fā)、Linux/Unix 等一般采用面向過程開發(fā),性能是最重要的因素。
缺點:沒有面向對象易維護、易復用、易擴展
面向對象
優(yōu)點:易維護、易復用、易擴展,由于面向對象有封裝、繼承、多態(tài)性的特性,可以設計出低耦合的系統(tǒng),使系統(tǒng)更加靈活、更加易于維護
缺點:性能比面向過程低
面向過程是具體化的,流程化的,解決一個問題,你需要一步一步的分析,一步一步的實現(xiàn)。
面向對象是模型化的,你只需抽象出一個類,這是一個封閉的盒子,在這里你擁有數(shù)據(jù)也擁有解決問題的方法。需要什么功能直接使用就可以了,不必去一步一步的實現(xiàn),至于這個功能是如何實現(xiàn)的,管我們什么事?我們會用就可以了。
面向對象的底層其實還是面向過程,把面向過程抽象成類,然后封裝,方便我們使用的就是面向對象了。
面向對象編程因為其具有豐富的特性(封裝、抽象、繼承、多態(tài)),可以實現(xiàn)很多復雜的設計思路,是很多設計原則、設計模式等編碼實現(xiàn)的基礎。
面向對象四大特性
碼老濕,如何理解面向對象的四大特性?
抽象
抽象是將一類對象的共同特征總結出來構造類的過程,包括數(shù)據(jù)抽象和行為抽象兩方面。抽象只關注對象有哪些屬性和行為,并不關注這些行為的細節(jié)是什么。
另外,抽象是一個寬泛的設計思想,開發(fā)者能不能設計好代碼,抽象能力也至關重要。
很多設計原則都體現(xiàn)了抽象這種設計思想,比如基于接口而非實現(xiàn)編程、開閉原則(對擴展開放、對修改關閉)、代碼解耦(降低代碼的耦合性)等。
在面對復雜系統(tǒng)的時候,人腦能承受的信息復雜程度是有限的,所以我們必須忽略掉一些非關鍵性的實現(xiàn)細節(jié)。
封裝
把一個對象的屬性私有化,同時提供一些可以被外界訪問的屬性的方法,如果屬性不想被外界訪問,我們大可不必提供方法給外界訪問。
通過封裝,只需要暴露必要的方法給調(diào)用者,調(diào)用者不必了解背后的業(yè)務細節(jié),用錯的概率就減少。
繼承
使用已存在的類的定義作為基礎建立新類的技術,新類的定義可以增加新的數(shù)據(jù)或新的功能,也可以用父類的功能,但不能選擇性地繼承父類。
通過使用繼承我們能夠非常方便地復用以前的代碼,需要注意的是,過度使用繼承,層級深就會導致代碼可讀性和可維護性變差。
關于繼承如下 3 點請記?。?/p>
- 子類擁有父類非 private 的屬性和方法。
- 子類可以擁有自己屬性和方法,即子類可以對父類進行擴展。
- 子類可以用自己的方式實現(xiàn)父類的方法。(以后介紹)。
多態(tài)
所謂多態(tài)就是指程序中定義的引用變量所指向的具體類型和通過該引用變量發(fā)出的方法調(diào)用在編程時并不確定,而是在程序運行期間才確定。
即一個引用變量到底會指向哪個類的實例對象,該引用變量發(fā)出的方法調(diào)用到底是哪個類中實現(xiàn)的方法,必須在由程序運行期間才能決定。
在 Java 中有兩種形式可以實現(xiàn)多態(tài):繼承(多個子類對同一方法的重寫)和接口(實現(xiàn)接口并覆蓋接口中同一方法)。
多態(tài)也是很多設計模式、設計原則、編程技巧的代碼實現(xiàn)基礎,比如策略模式、基于接口而非實現(xiàn)編程、依賴倒置原則、里式替換原則、利用多態(tài)去掉冗長的 if-else 語句等等。
什么是多態(tài)機制?
所謂多態(tài)就是指程序中定義的引用變量所指向的具體類型和通過該引用變量發(fā)出的方法調(diào)用在編程時并不確定,而是在程序運行期間才確定,即一個引用變量倒底會指向哪個類的實例對象,該引用變量發(fā)出的方法調(diào)用到底是哪個類中實現(xiàn)的方法,必須在由程序運行期間才能決定。
因為在程序運行時才確定具體的類,這樣,不用修改源程序代碼,就可以讓引用變量綁定到各種不同的類實現(xiàn)上,從而導致該引用調(diào)用的具體方法隨之改變,即不修改程序代碼就可以改變程序運行時所綁定的具體代碼,讓程序可以選擇多個運行狀態(tài),這就是多態(tài)性。
多態(tài)分為編譯時多態(tài)和運行時多態(tài)。
其中編輯時多態(tài)是靜態(tài)的,主要是指方法的重載,它是根據(jù)參數(shù)列表的不同來區(qū)分不同的函數(shù),通過編輯之后會變成兩個不同的函數(shù),在運行時談不上多態(tài)。
而運行時多態(tài)是動態(tài)的,它是通過動態(tài)綁定來實現(xiàn)的,也就是我們所說的多態(tài)性。
Java 語言是如何實現(xiàn)多態(tài)的?
Java 實現(xiàn)多態(tài)有三個必要條件:繼承、重寫、向上轉型。
繼承:在多態(tài)中必須存在有繼承關系的子類和父類。
重寫:子類對父類中某些方法進行重新定義,在調(diào)用這些方法時就會調(diào)用子類的方法。
向上轉型:在多態(tài)中需要將子類的引用賦給父類對象,只有這樣該引用才能夠具備技能調(diào)用父類的方法和子類的方法。
只有滿足了上述三個條件,我們才能夠在同一個繼承結構中使用統(tǒng)一的邏輯實現(xiàn)代碼處理不同的對象,從而達到執(zhí)行不同的行為。
重載與重寫
方法的重載和重寫都是實現(xiàn)多態(tài)的方式,區(qū)別在于前者實現(xiàn)的是編譯時的多態(tài)性,而后者實現(xiàn)的是運行時的多態(tài)性。
重載:發(fā)生在同一個類中,方法名相同參數(shù)列表不同(參數(shù)類型不同、個數(shù)不同、順序不同),與方法返回值和訪問修飾符無關,即重載的方法不能根據(jù)返回類型進行區(qū)分。
重寫:發(fā)生在父子類中,方法名、參數(shù)列表必須相同,返回值小于等于父類,拋出的異常小于等于父類,訪問修飾符大于等于父類(里氏代換原則);如果父類方法訪問修飾符為 private 則子類中就不是重寫。
== 和 equals 的區(qū)別是什么
== : 它的作用是判斷兩個對象的地址是不是相等。即,判斷兩個對象是不是同一個對象。(基本數(shù)據(jù)類型 == 比較的是值,引用數(shù)據(jù)類型 == 比較的是內(nèi)存地址)。
equals() : 它的作用也是判斷兩個對象是否相等。但它一般有兩種使用情況:
類沒有覆蓋 equals() 方法。則通過 equals() 比較該類的兩個對象時,等價于通過“==”比較這兩個對象。
類覆蓋了 equals() 方法。一般,我們都覆蓋 equals() 方法來兩個對象的內(nèi)容相等;若它們的內(nèi)容相等,則返回 true (即,認為這兩個對象相等)。
為什么重寫 equals 時必須重寫 hashCode 方法?
如果兩個對象相等,則 hashcode 一定也是相同的
兩個對象相等,對兩個對象分別調(diào)用 equals 方法都返回 true
兩個對象有相同的 hashcode 值,它們也不一定是相等的.
因此,equals 方法被覆蓋過,則 hashCode 方法也必須被覆蓋
為什么要有 hashcode
我們以“HashSet 如何檢查重復”為例子來說明為什么要有 hashCode:
當你把對象加入 HashSet 時,HashSet 會先計算對象的 hashcode 值來判斷對象加入的位置,同時也會與其他已經(jīng)加入的對象的 hashcode 值作比較,如果沒有相符的 hashcode,HashSet 會假設對象沒有重復出現(xiàn)。
但是如果發(fā)現(xiàn)有相同 hashcode 值的對象,這時會調(diào)用 equals()方法來檢查 hashcode 相等的對象是否真的相同。
如果兩者相同,HashSet 就不會讓其加入操作成功。
如果不同的話,就會重新散列到其他位置。這樣我們就大大減少了 equals 的次數(shù),相應就大大提高了執(zhí)行速度。
面向對象的基本原則
碼老濕,什么是 SOLID?
這是面向對象編程的一種設計原則,對于每一種設計原則,我們需要掌握它的設計初衷,能解決哪些編程問題,有哪些應用場景。
- 單一職責原則 SRP(Single Responsibility Principle) 類的功能要單一,不能包羅萬象,跟雜貨鋪似的。
- 開放封閉原則 OCP(Open-Close Principle) 一個模塊對于拓展是開放的,對于修改是封閉的,想要增加功能熱烈歡迎,想要修改,哼,一萬個不樂意。
- 里式替換原則 LSP(the Liskov Substitution Principle LSP) 子類可以替換父類出現(xiàn)在父類能夠出現(xiàn)的任何地方。比如你能代表你爸去你姥姥家干活。哈哈~~(其實多態(tài)就是一種這個原則的一種實現(xiàn))。
- 接口分離原則 ISP(the Interface Segregation Principle ISP) 設計時采用多個與特定客戶類有關的接口比采用一個通用的接口要好。就比如一個手機擁有打電話,看視頻,玩游戲等功能,把這幾個功能拆分成不同的接口,比在一個接口里要好的多。
- 依賴倒置原則 DIP(the Dependency Inversion Principle DIP) :高層模塊(high-level modules)不要依賴低層模塊(low-level)。高層模塊和低層模塊應該通過抽象(abstractions)來互相依賴。除此之外,抽象(abstractions)不要依賴具體實現(xiàn)細節(jié)(details),具體實現(xiàn)細節(jié)(details)依賴抽象(abstractions)。
- 抽象不應該依賴于具體實現(xiàn),具體實現(xiàn)應該依賴于抽象。就是你出國要說你是中國人,而不能說你是哪個村子的。
- 比如說中國人是抽象的,下面有具體的 xx 省,xx 市,xx 縣。你要依賴的抽象是中國人,而不是你是 xx 村的。
- 所謂高層模塊和低層模塊的劃分,簡單來說就是,在調(diào)用鏈上,調(diào)用者屬于高層,被調(diào)用者屬于低層。
- Tomcat 就是高層模塊,我們編寫的 Web 應用程序代碼就是低層模塊。Tomcat 和應用程序代碼之間并沒有直接的依賴關系,兩者都依賴同一個「抽象」,也就是 Servlet 規(guī)范。
- Servlet 規(guī)范不依賴具體的 Tomcat 容器和應用程序的實現(xiàn)細節(jié),而 Tomcat 容器和應用程序依賴 Servlet 規(guī)范。
碼老濕,接口隔離與單一職責有什么區(qū)別?
單一職責側重點是模塊、類、接口的設計思想。
接口隔離原則側重于接口設計,提供了一種判斷接口職責是否單一的標準。
Exception 與 Error 區(qū)別?
碼老濕,他們的相同點是什么呀?
Exception 和 Error 都是繼承了 Throwable 類,在 Java 中只有 Throwable 類型的實例才可以被拋出(throw)或者捕獲(catch),它是異常處理機制的基本組成類型。
Exception 和 Error 體現(xiàn)了 Java 平臺設計者對不同異常情況的分類。
異常使用規(guī)范:
- 盡量不要捕獲類似 Exception 這樣的通用異常,而是應該捕獲特定異常
- 不要生吞(swallow)異常。這是異常處理中要特別注意的事情,因為很可能會導致非常難以診斷的詭異情況。
Exception
Exception是程序正常運行中,可以預料的意外情況,可能并且應該被捕獲,進行相應處理。
就好比開車去洗桑拿,前方道路施工,禁止通行。但是我們換條路就可以解決。
Exception 又分為可檢查(checked)異常和不檢查(unchecked)異常,可檢查異常在源代碼里必須顯式地進行捕獲處理,這是編譯期檢查的一部分。
不檢查異常就是所謂的運行時異常,類似 NullPointerException、ArrayIndexOutOfBoundsException 之類,通常是可以編碼避免的邏輯錯誤,具體根據(jù)需要來判斷是否需要捕獲,并不會在編譯期強制要求。
Checked Exception 的假設是我們捕獲了異常,然后恢復程序。但是,其實我們大多數(shù)情況下,根本就不可能恢復。
Checked Exception 的使用,已經(jīng)大大偏離了最初的設計目的。Checked Exception 不兼容 functional 編程,如果你寫過 Lambda/Stream 代碼,相信深有體會。
Error
此類錯誤一般表示代碼運行時 JVM 出現(xiàn)問題。通常有 Virtual MachineError(虛擬機運行錯誤)、NoClassDefFoundError(類定義錯誤)等。
比如 OutOfMemoryError:內(nèi)存不足錯誤;StackOverflowError:棧溢出錯誤。此類錯誤發(fā)生時,JVM 將終止線程。
絕大多數(shù)導致程序不可恢復,這些錯誤是不受檢異常,非代碼性錯誤。因此,當此類錯誤發(fā)生時,應用程序不應該去處理此類錯誤。按照 Java 慣例,我們是不應該實現(xiàn)任何新的 Error 子類的!
比如開車去洗桑拿,老王出車禍了。無法洗了,只能去醫(yī)院。
JVM 如何處理異常?
在一個方法中如果發(fā)生異常,這個方法會創(chuàng)建一個異常對象,并轉交給 JVM,該異常對象包含異常名稱,異常描述以及異常發(fā)生時應用程序的狀態(tài)。
創(chuàng)建異常對象并轉交給 JVM 的過程稱為拋出異常??赡苡幸幌盗械姆椒ㄕ{(diào)用,最終才進入拋出異常的方法,這一系列方法調(diào)用的有序列表叫做調(diào)用棧。
JVM 會順著調(diào)用棧去查找看是否有可以處理異常的代碼,如果有,則調(diào)用異常處理代碼。
當 JVM 發(fā)現(xiàn)可以處理異常的代碼時,會把發(fā)生的異常傳遞給它。如果 JVM 沒有找到可以處理該異常的代碼塊,JVM 就會將該異常轉交給默認的異常處理器(默認處理器為 JVM 的一部分),默認異常處理器打印出異常信息并終止應用程序。
NoClassDefFoundError 和 ClassNotFoundException
NoClassDefFoundError 是一個 Error 類型的異常,是由 JVM 引起的,不應該嘗試捕獲這個異常。
引起該異常的原因是 JVM 或 ClassLoader 嘗試加載某類時在內(nèi)存中找不到該類的定義,該動作發(fā)生在運行期間,即編譯時該類存在,但是在運行時卻找不到了,可能是變異后被刪除了等原因導致;
ClassNotFoundException 是一個受查異常,需要顯式地使用 try-catch 對其進行捕獲和處理,或在方法簽名中用 throws 關鍵字進行聲明。
當使用 Class.forName, ClassLoader.loadClass 或 ClassLoader.findSystemClass 動態(tài)加載類到內(nèi)存的時候,通過傳入的類路徑參數(shù)沒有找到該類,就會拋出該異常;
另一種拋出該異常的可能原因是某個類已經(jīng)由一個類加載器加載至內(nèi)存中,另一個加載器又嘗試去加載它。
Java 常見異常有哪些?
java.lang.IllegalAccessError:違法訪問錯誤。當一個應用試圖訪問、修改某個類的域(Field)或者調(diào)用其方法,但是又違反域或方法的可見性聲明,則拋出該異常。
java.lang.InstantiationError:實例化錯誤。當一個應用試圖通過 Java 的 new 操作符構造一個抽象類或者接口時拋出該異常.
java.lang.OutOfMemoryError:內(nèi)存不足錯誤。當可用內(nèi)存不足以讓 Java 虛擬機分配給一個對象時拋出該錯誤。
java.lang.StackOverflowError:堆棧溢出錯誤。當一個應用遞歸調(diào)用的層次太深而導致堆棧溢出或者陷入死循環(huán)時拋出該錯誤。
java.lang.ClassCastException:類造型異常。假設有類 A 和 B(A 不是 B 的父類或子類),O 是 A 的實例,那么當強制將 O 構造為類 B 的實例時拋出該異常。該異常經(jīng)常被稱為強制類型轉換異常。
java.lang.ClassNotFoundException:找不到類異常。當應用試圖根據(jù)字符串形式的類名構造類,而在遍歷 CLASSPAH 之后找不到對應名稱的 class 文件時,拋出該異常。
java.lang.ArithmeticException:算術條件異常。譬如:整數(shù)除零等。
java.lang.ArrayIndexOutOfBoundsException:數(shù)組索引越界異常。當對數(shù)組的索引值為負數(shù)或大于等于數(shù)組大小時拋出。
final、finally、finalize 區(qū)別?
除了名字相似,他們毫無關系!!!
- final 可以修飾類、變量、方法,修飾類表示該類不能被繼承、修飾方法表示該方法不能被重寫、修飾變量表示該變量是一個常量不能被重新賦值。
- finally 一般作用在 try-catch 代碼塊中,在處理異常的時候,通常我們將一定要執(zhí)行的代碼方法 finally 代碼塊中,表示不管是否出現(xiàn)異常,該代碼塊都會執(zhí)行,一般用來存放一些關閉資源的代碼。
- finalize 是一個方法,屬于 Object 類的一個方法,而 Object 類是所有類的父類,Java 中允許使用 finalize()方法在垃圾收集器將對象從內(nèi)存中清除出去之前做必要的清理工作。
final 有什么用?
用于修飾類、屬性和方法;
- 被 final 修飾的類不可以被繼承
- 被 final 修飾的方法不可以被重寫
- 被 final 修飾的變量不可以被改變,被 final 修飾不可變的是變量的引用,而不是引用指向的內(nèi)容,引用指向的內(nèi)容是可以改變的。
try-catch-finally 中,如果 catch 中 return 了,finally 還會執(zhí)行嗎?
答:會執(zhí)行,在 return 前執(zhí)行。
注意:在 finally 中改變返回值的做法是不好的,因為如果存在 finally 代碼塊,try 中的 return 語句不會立馬返回調(diào)用者,而是記錄下返回值待 finally 代碼塊執(zhí)行完畢之后再向調(diào)用者返回其值,然后如果在 finally 中修改了返回值,就會返回修改后的值。
顯然,在 finally 中返回或者修改返回值會對程序造成很大的困擾,C#中直接用編譯錯誤的方式來阻止程序員干這種齷齪的事情,Java 中也可以通過提升編譯器的語法檢查級別來產(chǎn)生警告或錯誤。
- public static int getInt() {
- int a = 10;
- try {
- System.out.println(a / 0);
- a = 20;
- } catch (ArithmeticException e) {
- a = 30;
- return a;
- /*
- * return a 在程序執(zhí)行到這一步的時候,這里不是return a 而是 return 30;這個返回路徑就形成了
- * 但是呢,它發(fā)現(xiàn)后面還有finally,所以繼續(xù)執(zhí)行finally的內(nèi)容,a=40
- * 再次回到以前的路徑,繼續(xù)走return 30,形成返回路徑之后,這里的a就不是a變量了,而是常量30
- */
- } finally {
- a = 40;
- }
- return a;
- }
執(zhí)行結果:30。
- public static int getInt() {
- int a = 10;
- try {
- System.out.println(a / 0);
- a = 20;
- } catch (ArithmeticException e) {
- a = 30;
- return a;
- } finally {
- a = 40;
- //如果這樣,就又重新形成了一條返回路徑,由于只能通過1個return返回,所以這里直接返回40
- return a;
- }
- }
執(zhí)行結果:40。
強引用、軟引用、弱引用、虛引用
強引用、軟引用、弱引用、幻象引用有什么區(qū)別?具體使用場景是什么?
不同的引用類型,主要體現(xiàn)的是對象不同的可達性(reachable)狀態(tài)和對垃圾收集的影響。
強引用
通過 new 創(chuàng)建的對象就是強引用,強引用指向一個對象,就表示這個對象還活著,垃圾回收不會去收集。
軟引用
是一種相對強引用弱化一些的引用,只有當 JVM 認為內(nèi)存不足時,才會去試圖回收軟引用指向的對象。
JVM 會確保在拋出 OutOfMemoryError 之前,清理軟引用指向的對象。
軟引用通常用來實現(xiàn)內(nèi)存敏感的緩存,如果還有空閑內(nèi)存,就可以暫時保留緩存,當內(nèi)存不足時清理掉,這樣就保證了使用緩存的同時,不會耗盡內(nèi)存。
弱引用
ThreadlocalMap 中的 key 就是用了弱引用,因為 ThreadlocalMap 被 thread 對象持有,所以如果是強引用的話,只有當 thread 結束時才能被回收,而弱引用則可以在使用完后立即回收,不必等待 thread 結束。
虛引用
“虛引用”顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用并不會決定對象的生命周期。如果一個對象僅持有虛引用,那么它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。
虛引用主要用來跟蹤對象被垃圾回收器回收的活動。虛引用與軟引用和弱引用的一個區(qū)別在于:虛引用必須和引用隊列 (ReferenceQueue)聯(lián)合使用。
當垃圾回收器準備回收一個對象時,如果發(fā)現(xiàn)它還有虛引用,就會在回收對象的內(nèi)存之前,把這個虛引用加入到與之關聯(lián)的引用隊列中。
String 篇章
可變性
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% 左右的性能提升,但卻要冒多線程不安全的風險。
對于三者使用的總結
如果要操作少量的數(shù)據(jù)用 = String
單線程操作字符串緩沖區(qū)下操作大量數(shù)據(jù) = StringBuilder
多線程操作字符串緩沖區(qū)下操作大量數(shù)據(jù) = StringBuffer
String
String 是 Java 語言非?;A和重要的類,提供了構造和管理字符串的各種基本邏輯。它是典型的 Immutable 類,被聲明成為 final class,所有屬性也都是 final 的。
也由于它的不可變性,類似拼接、裁剪字符串等動作,都會產(chǎn)生新的 String 對象。
StringBuilder
StringBuilder 是 Java 1.5 中新增的,在能力上和 StringBuffer 沒有本質(zhì)區(qū)別,但是它去掉了線程安全的部分,有效減小了開銷,是絕大部分情況下進行字符串拼接的首選。
StringBuffer
StringBuffer 是為解決上面提到拼接產(chǎn)生太多中間對象的問題而提供的一個類,我們可以用 append 或者 add 方法,把字符串添加到已有序列的末尾或者指定位置。
StringBuffer 本質(zhì)是一個線程安全的可修改字符序列,它保證了線程安全,也隨之帶來了額外的性能開銷,所以除非有線程安全的需要,不然還是推薦使用它的后繼者,也就是 StringBuilder。
HashMap 使用 String 作為 key 有什么好處
HashMap 內(nèi)部實現(xiàn)是通過 key 的 hashcode 來確定 value 的存儲位置,因為字符串是不可變的,所以當創(chuàng)建字符串時,它的 hashcode 被緩存下來,不需要再次計算,所以相比于其他對象更快。
接口和抽象類有什么區(qū)別?
抽象類是用來捕捉子類的通用特性的。接口是抽象方法的集合。
接口和抽象類各有優(yōu)缺點,在接口和抽象類的選擇上,必須遵守這樣一個原則:
- 行為模型應該總是通過接口而不是抽象類定義,所以通常是優(yōu)先選用接口,盡量少用抽象類。
- 選擇抽象類的時候通常是如下情況:需要定義子類的行為,又要為子類提供通用的功能。
相同點
- 接口和抽象類都不能實例化
- 都位于繼承的頂端,用于被其他實現(xiàn)或繼承
- 都包含抽象方法,其子類都必須覆寫這些抽象方法
接口接口定義了協(xié)議,是面向對象編程(封裝、繼承多態(tài))基礎,通過接口我們能很好的實現(xiàn)單一職責、接口隔離、內(nèi)聚。
- 不能實例化;
- 不能包含任何非常量成員,任何 field 都是隱含著 public static final 的意義;
- 同時,沒有非靜態(tài)方法實現(xiàn),也就是說要么是抽象方法,要么是靜態(tài)方法。
Java8 中接口中引入默認方法和靜態(tài)方法,并且不用強制子類來實現(xiàn)它。以此來減少抽象類和接口之間的差異。
抽象類
抽象類是不能實例化的類,用 abstract 關鍵字修飾 class,其目的主要是代碼重用。
從設計層面來說,抽象類是對類的抽象,是一種模板設計,接口是行為的抽象,是一種行為的規(guī)范。
除了不能實例化,形式上和一般的 Java 類并沒有太大區(qū)別。
可以有一個或者多個抽象方法,也可以沒有抽象方法。抽象類大多用于抽取相關 Java 類的共用方法實現(xiàn)或者是共同成員變量,然后通過繼承的方式達到代碼復用的目的。
碼老濕,抽象類能用 final 修飾么?
不能,定義抽象類就是讓其他類繼承的,如果定義為 final 該類就不能被繼承,這樣彼此就會產(chǎn)生矛盾,所以 final 不能修飾抽象類
值傳遞
當一個對象被當作參數(shù)傳遞到一個方法后,此方法可改變這個對象的屬性,并可返回變化后的結果,那么這里到底是值傳遞還是引用傳遞?
是值傳遞。
Java 語言的方法調(diào)用只支持參數(shù)的值傳遞。當一個對象實例作為一個參數(shù)被傳遞到方法中時,參數(shù)的值就是對該對象的引用。
對象的屬性可以在被調(diào)用過程中被改變,但對對象引用的改變是不會影響到調(diào)用者的。
為什么 Java 只有值傳遞?
首先回顧一下在程序設計語言中有關將參數(shù)傳遞給方法(或函數(shù))的一些專業(yè)術語。按值調(diào)用(call by value)表示方法接收的是調(diào)用者提供的值,而按引用調(diào)用(call by reference)表示方法接收的是調(diào)用者提供的變量地址。
一個方法可以修改傳遞引用所對應的變量值,而不能修改傳遞值調(diào)用所對應的變量值。
它用來描述各種程序設計語言(不只是 Java)中方法參數(shù)傳遞方式。
Java 程序設計語言總是采用按值調(diào)用。也就是說,方法得到的是所有參數(shù)值的一個拷貝,也就是說,方法不能修改傳遞給它的任何參數(shù)變量的內(nèi)容。
基本數(shù)據(jù)類型
例子如下:
- 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);
- }
執(zhí)行結果:
- a = 20
- b = 10
- num1 = 10
- num2 = 20
解析:
在 swap 方法中,a、b 的值進行交換,并不會影響到 num1、num2。
因為,a、b 中的值,只是從 num1、num2 的復制過來的。
也就是說,a、b 相當于 num1、num2 的副本,副本的內(nèi)容無論怎么修改,都不會影響到原件本身。
對象引用類型
- 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;
- }
結果:
- 1
- 0
解析:
array 被初始化 arr 的拷貝也就是一個對象的引用,也就是說 array 和 arr 指向的時同一個數(shù)組對象。因此,外部對引用對象的改變會反映到所對應的對象上。
通過 example2 我們已經(jīng)看到,實現(xiàn)一個改變對象參數(shù)狀態(tài)的方法并不是一件難事。理由很簡單,方法得到的是對象引用的拷貝,對象引用及其他的拷貝同時引用同一個對象。
很多程序設計語言(特別是,C++和 Pascal)提供了兩種參數(shù)傳遞的方式:值調(diào)用和引用調(diào)用。
有些程序員認為 Java 程序設計語言對對象采用的是引用調(diào)用,實際上,這種理解是不對的。
值傳遞和引用傳遞有什么區(qū)別?
值傳遞:指的是在方法調(diào)用時,傳遞的參數(shù)是按值的拷貝傳遞,傳遞的是值的拷貝,也就是說傳遞后就互不相關了。
引用傳遞:指的是在方法調(diào)用時,傳遞的參數(shù)是按引用進行傳遞,其實傳遞的引用的地址,也就是變量所對應的內(nèi)存空間的地址。傳遞的是值的引用,也就是說傳遞前和傳遞后都指向同一個引用(也就是同一個內(nèi)存空間)。
本文轉載自微信公眾號「碼哥字節(jié)」
【編輯推薦】