最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會員登陸 & 注冊

三天吃透Java面試八股文(2023最新整理)

2023-06-05 16:20 作者:politmirror  | 我要投稿

作者:程序員大彬
鏈接:https://zhuanlan.zhihu.com/p/608060989
來源:知乎
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。

Java的特點

Java是一門面向?qū)ο蟮木幊陶Z言。面向?qū)ο蠛兔嫦蜻^程的區(qū)別參考下一個問題。

Java具有平臺獨立性和移植性。

  • Java有一句口號:Write once, run anywhere,一次編寫、到處運行。這也是Java的魅力所在。而實現(xiàn)這種特性的正是Java虛擬機JVM。已編譯的Java程序可以在任何帶有JVM的平臺上運行。你可以在windows平臺編寫代碼,然后拿到linux上運行。只要你在編寫完代碼后,將代碼編譯成.class文件,再把class文件打成Java包,這個jar包就可以在不同的平臺上運行了。

Java具有穩(wěn)健性。

  • Java是一個強類型語言,它允許擴展編譯時檢查潛在類型不匹配問題的功能。Java要求顯式的方法聲明,它不支持C風(fēng)格的隱式聲明。這些嚴格的要求保證編譯程序能捕捉調(diào)用錯誤,這就導(dǎo)致更可靠的程序。

  • 異常處理是Java中使得程序更穩(wěn)健的另一個特征。異常是某種類似于錯誤的異常條件出現(xiàn)的信號。使用try/catch/finally語句,程序員可以找到出錯的處理代碼,這就簡化了出錯處理和恢復(fù)的任務(wù)。

Java是如何實現(xiàn)跨平臺的?

Java是通過JVM(Java虛擬機)實現(xiàn)跨平臺的。

JVM可以理解成一個軟件,不同的平臺有不同的版本。我們編寫的Java代碼,編譯后會生成.class 文件(字節(jié)碼文件)。Java虛擬機就是負責(zé)將字節(jié)碼文件翻譯成特定平臺下的機器碼,通過JVM翻譯成機器碼之后才能運行。不同平臺下編譯生成的字節(jié)碼是一樣的,但是由JVM翻譯成的機器碼卻不一樣。

只要在不同平臺上安裝對應(yīng)的JVM,就可以運行字節(jié)碼文件,運行我們編寫的Java程序。

因此,運行Java程序必須有JVM的支持,因為編譯的結(jié)果不是機器碼,必須要經(jīng)過JVM的翻譯才能執(zhí)行。

本文內(nèi)容已經(jīng)整理到大廠面試手冊了,手冊內(nèi)容包含計算機基礎(chǔ)、Java基礎(chǔ)、多線程、JVM、數(shù)據(jù)庫、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服務(wù)、設(shè)計模式、架構(gòu)、校招社招分享等高頻面試題,非常實用,有小伙伴靠著這份手冊拿過字節(jié)offer~

需要的小伙伴可以自行下載:

大廠高頻面試題總結(jié)mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd

Java 與 C++ 的區(qū)別

  • Java 是純粹的面向?qū)ο笳Z言,所有的對象都繼承自 java.lang.Object,C++ 兼容 C ,不但支持面向?qū)ο笠仓С置嫦蜻^程。

  • Java 通過虛擬機從而實現(xiàn)跨平臺特性, C++ 依賴于特定的平臺。

  • Java 沒有指針,它的引用可以理解為安全指針,而 C++ 具有和 C 一樣的指針。

  • Java 支持自動垃圾回收,而 C++ 需要手動回收。

  • Java 不支持多重繼承,只能通過實現(xiàn)多個接口來達到相同目的,而 C++ 支持多重繼承。

JDK/JRE/JVM三者的關(guān)系

JVM

英文名稱(Java Virtual Machine),就是我們耳熟能詳?shù)?Java 虛擬機。Java 能夠跨平臺運行的核心在于 JVM 。



所有的java程序會首先被編譯為.class的類文件,這種類文件可以在虛擬機上執(zhí)行。也就是說class文件并不直接與機器的操作系統(tǒng)交互,而是經(jīng)過虛擬機間接與操作系統(tǒng)交互,由虛擬機將程序解釋給本地系統(tǒng)執(zhí)行。

針對不同的系統(tǒng)有不同的 jvm 實現(xiàn),有 Linux 版本的 jvm 實現(xiàn),也有Windows 版本的 jvm 實現(xiàn),但是同一段代碼在編譯后的字節(jié)碼是一樣的。這就是Java能夠跨平臺,實現(xiàn)一次編寫,多處運行的原因所在。

JRE

英文名稱(Java Runtime Environment),就是Java 運行時環(huán)境。我們編寫的Java程序必須要在JRE才能運行。它主要包含兩個部分,JVM 和 Java 核心類庫。



JRE是Java的運行環(huán)境,并不是一個開發(fā)環(huán)境,所以沒有包含任何開發(fā)工具,如編譯器和調(diào)試器等。

如果你只是想運行Java程序,而不是開發(fā)Java程序的話,那么你只需要安裝JRE即可。

JDK

英文名稱(Java Development Kit),就是 Java 開發(fā)工具包

學(xué)過Java的同學(xué),都應(yīng)該安裝過JDK。當(dāng)我們安裝完JDK之后,目錄結(jié)構(gòu)是這樣的



可以看到,JDK目錄下有個JRE,也就是JDK中已經(jīng)集成了 JRE,不用單獨安裝JRE。

另外,JDK中還有一些好用的工具,如jinfo,jps,jstack等。



最后,總結(jié)一下JDK/JRE/JVM,他們?nèi)叩年P(guān)系

JRE = JVM + Java 核心類庫

JDK = JRE + Java工具 + 編譯器 + 調(diào)試器



Java程序是編譯執(zhí)行還是解釋執(zhí)行?

先看看什么是編譯型語言和解釋型語言。

編譯型語言

在程序運行之前,通過編譯器將源程序編譯成機器碼可運行的二進制,以后執(zhí)行這個程序時,就不用再進行編譯了。

優(yōu)點:編譯器一般會有預(yù)編譯的過程對代碼進行優(yōu)化。因為編譯只做一次,運行時不需要編譯,所以編譯型語言的程序執(zhí)行效率高,可以脫離語言環(huán)境獨立運行。

缺點:編譯之后如果需要修改就需要整個模塊重新編譯。編譯的時候根據(jù)對應(yīng)的運行環(huán)境生成機器碼,不同的操作系統(tǒng)之間移植就會有問題,需要根據(jù)運行的操作系統(tǒng)環(huán)境編譯不同的可執(zhí)行文件。

總結(jié):執(zhí)行速度快、效率高;依靠編譯器、跨平臺性差些。

代表語言:C、C++、Pascal、Object-C以及Swift。

解釋型語言

定義:解釋型語言的源代碼不是直接翻譯成機器碼,而是先翻譯成中間代碼,再由解釋器對中間代碼進行解釋運行。在運行的時候才將源程序翻譯成機器碼,翻譯一句,然后執(zhí)行一句,直至結(jié)束。

優(yōu)點:

  1. 有良好的平臺兼容性,在任何環(huán)境中都可以運行,前提是安裝了解釋器(如虛擬機)。

  2. 靈活,修改代碼的時候直接修改就可以,可以快速部署,不用停機維護。

缺點:每次運行的時候都要解釋一遍,性能上不如編譯型語言。

總結(jié):解釋型語言執(zhí)行速度慢、效率低;依靠解釋器、跨平臺性好。

代表語言:JavaScript、Python、Erlang、PHP、Perl、Ruby。

對于Java這種語言,它的源代碼會先通過javac編譯成字節(jié)碼,再通過jvm將字節(jié)碼轉(zhuǎn)換成機器碼執(zhí)行,即解釋運行 和編譯運行配合使用,所以可以稱為混合型或者半編譯型。

面向?qū)ο蠛兔嫦蜻^程的區(qū)別?

面向?qū)ο蠛兔嫦蜻^程是一種軟件開發(fā)思想。

  • 面向過程就是分析出解決問題所需要的步驟,然后用函數(shù)按這些步驟實現(xiàn),使用的時候依次調(diào)用就可以了。

  • 面向?qū)ο笫前褬?gòu)成問題事務(wù)分解成各個對象,分別設(shè)計這些對象,然后將他們組裝成有完整功能的系統(tǒng)。面向過程只用函數(shù)實現(xiàn),面向?qū)ο笫怯妙悓崿F(xiàn)各個功能模塊。

以五子棋為例,面向過程的設(shè)計思路就是首先分析問題的步驟:

1、開始游戲,2、黑子先走,3、繪制畫面,4、判斷輸贏,5、輪到白子,6、繪制畫面,7、判斷輸贏,8、返回步驟2,9、輸出最后結(jié)果。 把上面每個步驟用分別的函數(shù)來實現(xiàn),問題就解決了。

而面向?qū)ο蟮脑O(shè)計則是從另外的思路來解決問題。整個五子棋可以分為:

  1. 黑白雙方

  2. 棋盤系統(tǒng),負責(zé)繪制畫面

  3. 規(guī)則系統(tǒng),負責(zé)判定諸如犯規(guī)、輸贏等。

黑白雙方負責(zé)接受用戶的輸入,并告知棋盤系統(tǒng)棋子布局發(fā)生變化,棋盤系統(tǒng)接收到了棋子的變化的信息就負責(zé)在屏幕上面顯示出這種變化,同時利用規(guī)則系統(tǒng)來對棋局進行判定。

面向?qū)ο笥心男┨匦裕?/h1>

面向?qū)ο笏拇筇匦裕悍庋b,繼承,多態(tài),抽象

1、封裝就是將類的信息隱藏在類內(nèi)部,不允許外部程序直接訪問,而是通過該類的方法實現(xiàn)對隱藏信息的操作和訪問。 良好的封裝能夠減少耦合。

2、繼承是從已有的類中派生出新的類,新的類繼承父類的屬性和行為,并能擴展新的能力,大大增加程序的重用性和易維護性。在Java中是單繼承的,也就是說一個子類只有一個父類。

3、多態(tài)是同一個行為具有多個不同表現(xiàn)形式的能力。在不修改程序代碼的情況下改變程序運行時綁定的代碼。實現(xiàn)多態(tài)的三要素:繼承、重寫、父類引用指向子類對象。

  • 靜態(tài)多態(tài)性:通過重載實現(xiàn),相同的方法有不同的參數(shù)列表,可以根據(jù)參數(shù)的不同,做出不同的處理。

  • 動態(tài)多態(tài)性:在子類中重寫父類的方法。運行期間判斷所引用對象的實際類型,根據(jù)其實際類型調(diào)用相應(yīng)的方法。

4、抽象。把客觀事物用代碼抽象出來。

面向?qū)ο缶幊痰牧笤瓌t?

  • 對象單一職責(zé):我們設(shè)計創(chuàng)建的對象,必須職責(zé)明確,比如商品類,里面相關(guān)的屬性和方法都必須跟商品相關(guān),不能出現(xiàn)訂單等不相關(guān)的內(nèi)容。這里的類可以是模塊、類庫、程序集,而不單單指類。

  • 里式替換原則:子類能夠完全替代父類,反之則不行。通常用于實現(xiàn)接口時運用。因為子類能夠完全替代基(父)類,那么這樣父類就擁有很多子類,在后續(xù)的程序擴展中就很容易進行擴展,程序完全不需要進行修改即可進行擴展。比如IA的實現(xiàn)為A,因為項目需求變更,現(xiàn)在需要新的實現(xiàn),直接在容器注入處更換接口即可.

  • 迪米特法則,也叫最小原則,或者說最小耦合。通常在設(shè)計程序或開發(fā)程序的時候,盡量要高內(nèi)聚,低耦合。當(dāng)兩個類進行交互的時候,會產(chǎn)生依賴。而迪米特法則就是建議這種依賴越少越好。就像構(gòu)造函數(shù)注入父類對象時一樣,當(dāng)需要依賴某個對象時,并不在意其內(nèi)部是怎么實現(xiàn)的,而是在容器中注入相應(yīng)的實現(xiàn),既符合里式替換原則,又起到了解耦的作用。

  • 開閉原則:開放擴展,封閉修改。當(dāng)項目需求發(fā)生變更時,要盡可能的不去對原有的代碼進行修改,而在原有的基礎(chǔ)上進行擴展。

  • 依賴倒置原則:高層模塊不應(yīng)該直接依賴于底層模塊的具體實現(xiàn),而應(yīng)該依賴于底層的抽象。接口和抽象類不應(yīng)該依賴于實現(xiàn)類,而實現(xiàn)類依賴接口或抽象類。

  • 接口隔離原則:一個對象和另外一個對象交互的過程中,依賴的內(nèi)容最小。也就是說在接口設(shè)計的時候,在遵循對象單一職責(zé)的情況下,盡量減少接口的內(nèi)容。

簡潔版

  • 單一職責(zé):對象設(shè)計要求獨立,不能設(shè)計萬能對象。

  • 開閉原則:對象修改最小化。

  • 里式替換:程序擴展中抽象被具體可以替換(接口、父類、可以被實現(xiàn)類對象、子類替換對象)

  • 迪米特:高內(nèi)聚,低耦合。盡量不要依賴細節(jié)。

  • 依賴倒置:面向抽象編程。也就是參數(shù)傳遞,或者返回值,可以使用父類類型或者接口類型。從廣義上講:基于接口編程,提前設(shè)計好接口框架。

  • 接口隔離:接口設(shè)計大小要適中。過大導(dǎo)致污染,過小,導(dǎo)致調(diào)用麻煩。

數(shù)組到底是不是對象?

先說說對象的概念。對象是根據(jù)某個類創(chuàng)建出來的一個實例,表示某類事物中一個具體的個體。

對象具有各種屬性,并且具有一些特定的行為。站在計算機的角度,對象就是內(nèi)存中的一個內(nèi)存塊,在這個內(nèi)存塊封裝了一些數(shù)據(jù),也就是類中定義的各個屬性。

所以,對象是用來封裝數(shù)據(jù)的。

java中的數(shù)組具有java中其他對象的一些基本特點。比如封裝了一些數(shù)據(jù),可以訪問屬性,也可以調(diào)用方法。

因此,可以說,數(shù)組是對象。

也可以通過代碼驗證數(shù)組是對象的事實。比如以下的代碼,輸出結(jié)果為java.lang.Object。

Class clz = int[].class;System.out.println(clz.getSuperclass().getName());

由此,可以看出,數(shù)組類的父類就是Object類,那么可以推斷出數(shù)組就是對象。

Java的基本數(shù)據(jù)類型有哪些?

  • byte,8bit

  • char,16bit

  • short,16bit

  • int,32bit

  • float,32bit

  • long,64bit

  • double,64bit

  • boolean,只有兩個值:true、false,可以使?用 1 bit 來存儲

簡單類型booleanbytecharshortIntlongfloatdouble二進制位數(shù)18161632643264包裝類BooleanByteCharacterShortIntegerLongFloatDouble

在Java規(guī)范中,沒有明確指出boolean的大小。在《Java虛擬機規(guī)范》給出了單個boolean占4個字節(jié),和boolean數(shù)組1個字節(jié)的定義,具體 還要看虛擬機實現(xiàn)是否按照規(guī)范來,因此boolean占用1個字節(jié)或者4個字節(jié)都是有可能的。

為什么不能用浮點型表示金額?

由于計算機中保存的小數(shù)其實是十進制的小數(shù)的近似值,并不是準確值,所以,千萬不要在代碼中使用浮點數(shù)來表示金額等重要的指標。

建議使用BigDecimal或者Long來表示金額。

什么是值傳遞和引用傳遞?

  • 值傳遞是對基本型變量而言的,傳遞的是該變量的一個副本,改變副本不影響原變量。

  • 引用傳遞一般是對于對象型變量而言的,傳遞的是該對象地址的一個副本,并不是原對象本身,兩者指向同一片內(nèi)存空間。所以對引用對象進行操作會同時改變原對象。

java中不存在引用傳遞,只有值傳遞。即不存在變量a指向變量b,變量b指向?qū)ο蟮倪@種情況。

了解Java的包裝類型嗎?為什么需要包裝類?

Java 是一種面向?qū)ο笳Z言,很多地方都需要使用對象而不是基本數(shù)據(jù)類型。比如,在集合類中,我們是無法將 int 、double 等類型放進去的。因為集合的容器要求元素是 Object 類型。

為了讓基本類型也具有對象的特征,就出現(xiàn)了包裝類型。相當(dāng)于將基本類型包裝起來,使得它具有了對象的性質(zhì),并且為其添加了屬性和方法,豐富了基本類型的操作。

自動裝箱和拆箱

Java中基礎(chǔ)數(shù)據(jù)類型與它們對應(yīng)的包裝類見下表:

原始類型包裝類型booleanBooleanbyteBytecharCharacterfloatFloatintIntegerlongLongshortShortdoubleDouble

裝箱:將基礎(chǔ)類型轉(zhuǎn)化為包裝類型。

拆箱:將包裝類型轉(zhuǎn)化為基礎(chǔ)類型。

當(dāng)基礎(chǔ)類型與它們的包裝類有如下幾種情況時,編譯器會自動幫我們進行裝箱或拆箱:

  • 賦值操作(裝箱或拆箱)

  • 進行加減乘除混合運算 (拆箱)

  • 進行>,<,==比較運算(拆箱)

  • 調(diào)用equals進行比較(裝箱)

  • ArrayList、HashMap等集合類添加基礎(chǔ)類型數(shù)據(jù)時(裝箱)

示例代碼:

Integer x = 1; // 裝箱 調(diào)? Integer.valueOf(1)int y = x; // 拆箱 調(diào)? X.intValue()

下面看一道常見的面試題:

Integer a = 100;Integer b = 100;System.out.println(a == b);Integer c = 200;Integer d = 200;System.out.println(c == d);

輸出:

truefalse

為什么第三個輸出是false?看看 Integer 類的源碼就知道啦。

public static Integer valueOf(int i) { ? ?if (i >= IntegerCache.low && i <= IntegerCache.high) ? ? ? ?return IntegerCache.cache[i + (-IntegerCache.low)]; ? ?return new Integer(i);}

Integer c = 200; 會調(diào)用 調(diào)?Integer.valueOf(200)。而從Integer的valueOf()源碼可以看到,這里的實現(xiàn)并不是簡單的new Integer,而是用IntegerCache做一個cache。

private static class IntegerCache { ? ?static final int low = -128; ? ?static final int high; ? ?static final Integer cache[]; ? ?static { ? ? ? ?// high value may be configured by property ? ? ? ?int h = 127; ? ? ? ?String integerCacheHighPropValue = ? ? ? ? ? ?sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); ? ? ? ?if (integerCacheHighPropValue != null) { ? ? ? ? ? ?try { ? ? ? ? ? ? ? ?int i = parseInt(integerCacheHighPropValue); ? ? ? ? ? ? ? ?i = Math.max(i, 127); ? ? ? ? ? ? ? ?// Maximum array size is Integer.MAX_VALUE ? ? ? ? ? ? ? ?h = Math.min(i, Integer.MAX_VALUE - (-low) -1); ? ? ? ? ? ?} catch( NumberFormatException nfe) { ? ? ? ? ? ? ? ?// If the property cannot be parsed into an int, ignore it. ? ? ? ? ? ?} ? ? ? ?} ? ? ? ?high = h; ? ?} ? ?...}

這是IntegerCache靜態(tài)代碼塊中的一段,默認Integer cache 的下限是-128,上限默認127。當(dāng)賦值100給Integer時,剛好在這個范圍內(nèi),所以從cache中取對應(yīng)的Integer并返回,所以a和b返回的是同一個對象,所以==比較是相等的,當(dāng)賦值200給Integer時,不在cache 的范圍內(nèi),所以會new Integer并返回,當(dāng)然==比較的結(jié)果是不相等的。

String 為什么不可變?

先看看什么是不可變的對象。

如果一個對象,在它創(chuàng)建完成之后,不能再改變它的狀態(tài),那么這個對象就是不可變的。不能改變狀態(tài)的意思是,不能改變對象內(nèi)的成員變量,包括基本數(shù)據(jù)類型的值不能改變,引用類型的變量不能指向其他的對象,引用類型指向的對象的狀態(tài)也不能改變。

接著來看Java8 String類的源碼:

public final class String ? ?implements java.io.Serializable, Comparable<String>, CharSequence { ? ?/** The value is used for character storage. */ ? ?private final char value[]; ? ?/** Cache the hash code for the string */ ? ?private int hash; // Default to 0}

從源碼可以看出,String對象其實在內(nèi)部就是一個個字符,存儲在這個value數(shù)組里面的。

value數(shù)組用final修飾,final 修飾的變量,值不能被修改。因此value不可以指向其他對象。

String類內(nèi)部所有的字段都是私有的,也就是被private修飾。而且String沒有對外提供修改內(nèi)部狀態(tài)的方法,因此value數(shù)組不能改變。

所以,String是不可變的。

那為什么String要設(shè)計成不可變的?

主要有以下幾點原因:

  1. 線程安全。同一個字符串實例可以被多個線程共享,因為字符串不可變,本身就是線程安全的。

  2. 支持hash映射和緩存。因為String的hash值經(jīng)常會使用到,比如作為 Map 的鍵,不可變的特性使得 hash 值也不會變,不需要重新計算。

  3. 出于安全考慮。網(wǎng)絡(luò)地址URL、文件路徑path、密碼通常情況下都是以String類型保存,假若String不是固定不變的,將會引起各種安全隱患。比如將密碼用String的類型保存,那么它將一直留在內(nèi)存中,直到垃圾收集器把它清除。假如String類不是固定不變的,那么這個密碼可能會被改變,導(dǎo)致出現(xiàn)安全隱患。

  4. 字符串常量池優(yōu)化。String對象創(chuàng)建之后,會緩存到字符串常量池中,下次需要創(chuàng)建同樣的對象時,可以直接返回緩存的引用。

既然我們的String是不可變的,它內(nèi)部還有很多substring, replace, replaceAll這些操作的方法。這些方法好像會改變String對象?怎么解釋呢?

其實不是的,我們每次調(diào)用replace等方法,其實會在堆內(nèi)存中創(chuàng)建了一個新的對象。然后其value數(shù)組引用指向不同的對象。

為何JDK9要將String的底層實現(xiàn)由char[]改成byte[]?

主要是為了節(jié)約String占用的內(nèi)存。

在大部分Java程序的堆內(nèi)存中,String占用的空間最大,并且絕大多數(shù)String只有Latin-1字符,這些Latin-1字符只需要1個字節(jié)就夠了。

而在JDK9之前,JVM因為String使用char數(shù)組存儲,每個char占2個字節(jié),所以即使字符串只需要1字節(jié),它也要按照2字節(jié)進行分配,浪費了一半的內(nèi)存空間。

到了JDK9之后,對于每個字符串,會先判斷它是不是只有Latin-1字符,如果是,就按照1字節(jié)的規(guī)格進行分配內(nèi)存,如果不是,就按照2字節(jié)的規(guī)格進行分配,這樣便提高了內(nèi)存使用率,同時GC次數(shù)也會減少,提升效率。

不過Latin-1編碼集支持的字符有限,比如不支持中文字符,因此對于中文字符串,用的是UTF16編碼(兩個字節(jié)),所以用byte[]和char[]實現(xiàn)沒什么區(qū)別。

String, StringBuffer 和 StringBuilder區(qū)別

1. 可變性

  • String 不可變

  • StringBuffer 和 StringBuilder 可變

2. 線程安全

  • String 不可變,因此是線程安全的

  • StringBuilder 不是線程安全的

  • StringBuffer 是線程安全的,內(nèi)部使用 synchronized 進行同步

什么是StringJoiner?

StringJoiner是 Java 8 新增的一個 API,它基于 StringBuilder 實現(xiàn),用于實現(xiàn)對字符串之間通過分隔符拼接的場景。

StringJoiner 有兩個構(gòu)造方法,第一個構(gòu)造要求依次傳入分隔符、前綴和后綴。第二個構(gòu)造則只要求傳入分隔符即可(前綴和后綴默認為空字符串)。

StringJoiner(CharSequence delimiter, CharSequence prefix, CharSequence suffix)StringJoiner(CharSequence delimiter)

有些字符串拼接場景,使用 StringBuffer 或 StringBuilder 則顯得比較繁瑣。

比如下面的例子:

List<Integer> values = Arrays.asList(1, 3, 5);StringBuilder sb = new StringBuilder("(");for (int i = 0; i < values.size(); i++) { sb.append(values.get(i)); if (i != values.size() -1) { sb.append(","); }}sb.append(")");

而通過StringJoiner來實現(xiàn)拼接List的各個元素,代碼看起來更加簡潔。

List<Integer> values = Arrays.asList(1, 3, 5);StringJoiner sj = new StringJoiner(",", "(", ")");for (Integer value : values) { sj.add(value.toString());}

另外,像平時經(jīng)常使用的Collectors.joining(","),底層就是通過StringJoiner實現(xiàn)的。

源碼如下:

public static Collector<CharSequence, ?, String> joining( ? ?CharSequence delimiter,CharSequence prefix,CharSequence suffix) { ? ?return new CollectorImpl<>( ? ? ? ? ? ?() -> new StringJoiner(delimiter, prefix, suffix), ? ? ? ? ? ?StringJoiner::add, StringJoiner::merge, ? ? ? ? ? ?StringJoiner::toString, CH_NOID);}


String 類的常用方法有哪些?

  • indexOf():返回指定字符的索引。

  • charAt():返回指定索引處的字符。

  • replace():字符串替換。

  • trim():去除字符串兩端空白。

  • split():分割字符串,返回一個分割后的字符串?dāng)?shù)組。

  • getBytes():返回字符串的 byte 類型數(shù)組。

  • length():返回字符串長度。

  • toLowerCase():將字符串轉(zhuǎn)成小寫字母。

  • toUpperCase():將字符串轉(zhuǎn)成大寫字符。

  • substring():截取字符串。

  • equals():字符串比較。

new String("dabin")會創(chuàng)建幾個對象?

使用這種方式會創(chuàng)建兩個字符串對象(前提是字符串常量池中沒有 "dabin" 這個字符串對象)。

  • "dabin" 屬于字符串字面量,因此編譯時期會在字符串常量池中創(chuàng)建一個字符串對象,指向這個 "dabin" 字符串字面量;

  • 使用 new 的方式會在堆中創(chuàng)建一個字符串對象。

什么是字符串常量池?

字符串常量池(String Pool)保存著所有字符串字面量,這些字面量在編譯時期就確定。字符串常量池位于堆內(nèi)存中,專門用來存儲字符串常量。在創(chuàng)建字符串時,JVM首先會檢查字符串常量池,如果該字符串已經(jīng)存在池中,則返回其引用,如果不存在,則創(chuàng)建此字符串并放入池中,并返回其引用。

String最大長度是多少?

String類提供了一個length方法,返回值為int類型,而int的取值上限為2^31 -1。

所以理論上String的最大長度為2^31 -1。

達到這個長度的話需要多大的內(nèi)存嗎

String內(nèi)部是使用一個char數(shù)組來維護字符序列的,一個char占用兩個字節(jié)。如果說String最大長度是2^31 -1的話,那么最大的字符串占用內(nèi)存空間約等于4GB。

也就是說,我們需要有大于4GB的JVM運行內(nèi)存才行。

那String一般都存儲在JVM的哪塊區(qū)域呢?

字符串在JVM中的存儲分兩種情況,一種是String對象,存儲在JVM的堆棧中。一種是字符串常量,存儲在常量池里面。

什么情況下字符串會存儲在常量池呢?

當(dāng)通過字面量進行字符串聲明時,比如String s = "程序新大彬";,這個字符串在編譯之后會以常量的形式進入到常量池。

那常量池中的字符串最大長度是2^31-1嗎

不是的,常量池對String的長度是有另外限制的。。Java中的UTF-8編碼的Unicode字符串在常量池中以CONSTANT_Utf8類型表示。

CONSTANT_Utf8_info { ? ?u1 tag; ? ?u2 length; ? ?u1 bytes[length];}

length在這里就是代表字符串的長度,length的類型是u2,u2是無符號的16位整數(shù),也就是說最大長度可以做到2^16-1 即 65535。

不過javac編譯器做了限制,需要length < 65535。所以字符串常量在常量池中的最大長度是65535 - 1 = 65534。

最后總結(jié)一下:

String在不同的狀態(tài)下,具有不同的長度限制。

  • 字符串常量長度不能超過65534

  • 堆內(nèi)字符串的長度不超過2^31-1

Object常用方法有哪些?

Java面試經(jīng)常會出現(xiàn)的一道題目,Object的常用方法。下面給大家整理一下。

Object常用方法有:toString()equals()、hashCode()、clone()等。

toString

默認輸出對象地址。

public class Person { ? ?private int age; ? ?private String name; ? ?public Person(int age, String name) { ? ? ? ?this.age = age; ? ? ? ?this.name = name; ? ?} ? ?public static void main(String[] args) { ? ? ? ?System.out.println(new Person(18, "程序員大彬").toString()); ? ?} ? ?//output ? ?//me.tyson.java.core.Person@4554617c}

可以重寫toString方法,按照重寫邏輯輸出對象值。

public class Person { ? ?private int age; ? ?private String name; ? ?public Person(int age, String name) { ? ? ? ?this.age = age; ? ? ? ?this.name = name; ? ?} ? ?@Override ? ?public String toString() { ? ? ? ?return name + ":" + age; ? ?} ? ?public static void main(String[] args) { ? ? ? ?System.out.println(new Person(18, "程序員大彬").toString()); ? ?} ? ?//output ? ?//程序員大彬:18}

equals

默認比較兩個引用變量是否指向同一個對象(內(nèi)存地址)。

public class Person { ? ?private int age; ? ?private String name; ? ?public Person(int age, String name) { ? ? ? this.age = age; ? ? ? this.name = name; ? ?} ? ?public static void main(String[] args) { ? ? ? ?String name = "程序員大彬"; ? ? ? ?Person p1 = new Person(18, name); ? ? ? ?Person p2 = new Person(18, name); ? ? ? ?System.out.println(p1.equals(p2)); ? ?} ? ?//output ? ?//false}

可以重寫equals方法,按照age和name是否相等來判斷:

public class Person { ? ?private int age; ? ?private String name; ? ?public Person(int age, String name) { ? ? ? ?this.age = age; ? ? ? ?this.name = name; ? ?} ? ?@Override ? ?public boolean equals(Object o) { ? ? ? ?if (o instanceof Person) { ? ? ? ? ? ?Person p = (Person) o; ? ? ? ? ? ?return age == p.age && name.equals(p.name); ? ? ? ?} ? ? ? ?return false; ? ?} ? ?public static void main(String[] args) { ? ? ? ?String name = "程序員大彬"; ? ? ? ?Person p1 = new Person(18, name); ? ? ? ?Person p2 = new Person(18, name); ? ? ? ?System.out.println(p1.equals(p2)); ? ?} ? ?//output ? ?//true}

hashCode

將與對象相關(guān)的信息映射成一個哈希值,默認的實現(xiàn)hashCode值是根據(jù)內(nèi)存地址換算出來。

public class Cat { ? ?public static void main(String[] args) { ? ? ? ?System.out.println(new Cat().hashCode()); ? ?} ? ?//out ? ?//1349277854}

clone

Java賦值是復(fù)制對象引用,如果我們想要得到一個對象的副本,使用賦值操作是無法達到目的的。Object對象有個clone()方法,實現(xiàn)了對

象中各個屬性的復(fù)制,但它的可見范圍是protected的。

protected native Object clone() throws CloneNotSupportedException;

所以實體類使用克隆的前提是:

  • 實現(xiàn)Cloneable接口,這是一個標記接口,自身沒有方法,這應(yīng)該是一種約定。調(diào)用clone方法時,會判斷有沒有實現(xiàn)Cloneable接口,沒有實現(xiàn)Cloneable的話會拋異常CloneNotSupportedException。

  • 覆蓋clone()方法,可見性提升為public。

public class Cat implements Cloneable { ? ?private String name; ? ?@Override ? ?protected Object clone() throws CloneNotSupportedException { ? ? ? ?return super.clone(); ? ?} ? ?public static void main(String[] args) throws CloneNotSupportedException { ? ? ? ?Cat c = new Cat(); ? ? ? ?c.name = "程序員大彬"; ? ? ? ?Cat cloneCat = (Cat) c.clone(); ? ? ? ?c.name = "大彬"; ? ? ? ?System.out.println(cloneCat.name); ? ?} ? ?//output ? ?//程序員大彬}

getClass

返回此 Object 的運行時類,常用于java反射機制。

public class Person { ? ?private String name; ? ?public Person(String name) { ? ? ? ?this.name = name; ? ?} ? ?public static void main(String[] args) { ? ? ? ?Person p = new Person("程序員大彬"); ? ? ? ?Class clz = p.getClass(); ? ? ? ?System.out.println(clz); ? ? ? ?//獲取類名 ? ? ? ?System.out.println(clz.getName()); ? ?} ? ?/** ? ? * class com.tyson.basic.Person ? ? * com.tyson.basic.Person ? ? */}

wait

當(dāng)前線程調(diào)用對象的wait()方法之后,當(dāng)前線程會釋放對象鎖,進入等待狀態(tài)。等待其他線程調(diào)用此對象的notify()/notifyAll()喚醒或者等待超時時間wait(long timeout)自動喚醒。線程需要獲取obj對象鎖之后才能調(diào)用 obj.wait()。

notify

obj.notify()喚醒在此對象上等待的單個線程,選擇是任意性的。notifyAll()喚醒在此對象上等待的所有線程。

講講深拷貝和淺拷貝?

淺拷貝:拷?對象和原始對象的引?類型引用同?個對象。

以下例子,Cat對象里面有個Person對象,調(diào)用clone之后,克隆對象和原對象的Person引用的是同一個對象,這就是淺拷貝。

public class Cat implements Cloneable { ? ?private String name; ? ?private Person owner; ? ?@Override ? ?protected Object clone() throws CloneNotSupportedException { ? ? ? ?return super.clone(); ? ?} ? ?public static void main(String[] args) throws CloneNotSupportedException { ? ? ? ?Cat c = new Cat(); ? ? ? ?Person p = new Person(18, "程序員大彬"); ? ? ? ?c.owner = p; ? ? ? ?Cat cloneCat = (Cat) c.clone(); ? ? ? ?p.setName("大彬"); ? ? ? ?System.out.println(cloneCat.owner.getName()); ? ?} ? ?//output ? ?//大彬}

深拷貝:拷貝對象和原始對象的引用類型引用不同的對象。

以下例子,在clone函數(shù)中不僅調(diào)用了super.clone,而且調(diào)用Person對象的clone方法(Person也要實現(xiàn)Cloneable接口并重寫clone方法),從而實現(xiàn)了深拷貝。可以看到,拷貝對象的值不會受到原對象的影響。

public class Cat implements Cloneable { ? ?private String name; ? ?private Person owner; ? ?@Override ? ?protected Object clone() throws CloneNotSupportedException { ? ? ? ?Cat c = null; ? ? ? ?c = (Cat) super.clone(); ? ? ? ?c.owner = (Person) owner.clone();//拷貝Person對象 ? ? ? ?return c; ? ?} ? ?public static void main(String[] args) throws CloneNotSupportedException { ? ? ? ?Cat c = new Cat(); ? ? ? ?Person p = new Person(18, "程序員大彬"); ? ? ? ?c.owner = p; ? ? ? ?Cat cloneCat = (Cat) c.clone(); ? ? ? ?p.setName("大彬"); ? ? ? ?System.out.println(cloneCat.owner.getName()); ? ?} ? ?//output ? ?//程序員大彬}

兩個對象的hashCode()相同,則 equals()是否也一定為 true?

equals與hashcode的關(guān)系:

  1. 如果兩個對象調(diào)用equals比較返回true,那么它們的hashCode值一定要相同;

  2. 如果兩個對象的hashCode相同,它們并不一定相同。

hashcode方法主要是用來提升對象比較的效率,先進行hashcode()的比較,如果不相同,那就不必在進行equals的比較,這樣就大大減少了equals比較的次數(shù),當(dāng)比較對象的數(shù)量很大的時候能提升效率。

為什么重寫 equals 時一定要重寫 hashCode?

之所以重寫equals()要重寫hashcode(),是為了保證equals()方法返回true的情況下hashcode值也要一致,如果重寫了equals()沒有重寫hashcode(),就會出現(xiàn)兩個對象相等但hashcode()不相等的情況。這樣,當(dāng)用其中的一個對象作為鍵保存到hashMap、hashTable或hashSet中,再以另一個對象作為鍵值去查找他們的時候,則會查找不到。

Java創(chuàng)建對象有幾種方式?

Java創(chuàng)建對象有以下幾種方式:

  • 用new語句創(chuàng)建對象。

  • 使用反射,使用Class.newInstance()創(chuàng)建對象。

  • 調(diào)用對象的clone()方法。

  • 運用反序列化手段,調(diào)用java.io.ObjectInputStream對象的readObject()方法。

說說類實例化的順序

Java中類實例化順序:

  1. 靜態(tài)屬性,靜態(tài)代碼塊。

  2. 普通屬性,普通代碼塊。

  3. 構(gòu)造方法。

public class LifeCycle { ? ?// 靜態(tài)屬性 ? ?private static String staticField = getStaticField(); ? ?// 靜態(tài)代碼塊 ? ?static { ? ? ? ?System.out.println(staticField); ? ? ? ?System.out.println("靜態(tài)代碼塊初始化"); ? ?} ? ?// 普通屬性 ? ?private String field = getField(); ? ?// 普通代碼塊 ? ?{ ? ? ? ?System.out.println(field); ? ? ? ?System.out.println("普通代碼塊初始化"); ? ?} ? ?// 構(gòu)造方法 ? ?public LifeCycle() { ? ? ? ?System.out.println("構(gòu)造方法初始化"); ? ?} ? ?// 靜態(tài)方法 ? ?public static String getStaticField() { ? ? ? ?String statiFiled = "靜態(tài)屬性初始化"; ? ? ? ?return statiFiled; ? ?} ? ?// 普通方法 ? ?public String getField() { ? ? ? ?String filed = "普通屬性初始化"; ? ? ? ?return filed; ? ?} ? ?public static void main(String[] argc) { ? ? ? ?new LifeCycle(); ? ?} ? ?/** ? ? * ? ? ?靜態(tài)屬性初始化 ? ? * ? ? ?靜態(tài)代碼塊初始化 ? ? * ? ? ?普通屬性初始化 ? ? * ? ? ?普通代碼塊初始化 ? ? * ? ? ?構(gòu)造方法初始化 ? ? */}

equals和==有什么區(qū)別?

  • 對于基本數(shù)據(jù)類型,==比較的是他們的值?;緮?shù)據(jù)類型沒有equal方法;

  • 對于復(fù)合數(shù)據(jù)類型,==比較的是它們的存放地址(是否是同一個對象)。equals()默認比較地址值,重寫的話按照重寫邏輯去比較。

常見的關(guān)鍵字有哪些?

static

static可以用來修飾類的成員方法、類的成員變量。

static變量也稱作靜態(tài)變量,靜態(tài)變量和非靜態(tài)變量的區(qū)別是:靜態(tài)變量被所有的對象所共享,在內(nèi)存中只有一個副本,它當(dāng)且僅當(dāng)在類初次加載時會被初始化。而非靜態(tài)變量是對象所擁有的,在創(chuàng)建對象的時候被初始化,存在多個副本,各個對象擁有的副本互不影響。

以下例子,age為非靜態(tài)變量,則p1打印結(jié)果是:Name:zhangsan, Age:10;若age使用static修飾,則p1打印結(jié)果是:Name:zhangsan, Age:12,因為static變量在內(nèi)存只有一個副本。

public class Person { ? ?String name; ? ?int age; ? ? ? ?public String toString() { ? ? ? ?return "Name:" + name + ", Age:" + age; ? ?} ? ? ? ?public static void main(String[] args) { ? ? ? ?Person p1 = new Person(); ? ? ? ?p1.name = "zhangsan"; ? ? ? ?p1.age = 10; ? ? ? ?Person p2 = new Person(); ? ? ? ?p2.name = "lisi"; ? ? ? ?p2.age = 12; ? ? ? ?System.out.println(p1); ? ? ? ?System.out.println(p2); ? ?} ? ?/**Output ? ? * Name:zhangsan, Age:10 ? ? * Name:lisi, Age:12 ? ? *///~}

static方法一般稱作靜態(tài)方法。靜態(tài)方法不依賴于任何對象就可以進行訪問,通過類名即可調(diào)用靜態(tài)方法。

public class Utils { ? ?public static void print(String s) { ? ? ? ?System.out.println("hello world: " + s); ? ?} ? ?public static void main(String[] args) { ? ? ? ?Utils.print("程序員大彬"); ? ?}}

靜態(tài)代碼塊只會在類加載的時候執(zhí)行一次。以下例子,startDate和endDate在類加載的時候進行賦值。

class Person ?{ ? ?private Date birthDate; ? ?private static Date startDate, endDate; ? ?static{ ? ? ? ?startDate = Date.valueOf("2008"); ? ? ? ?endDate = Date.valueOf("2021"); ? ?} ? ?public Person(Date birthDate) { ? ? ? ?this.birthDate = birthDate; ? ?}}

靜態(tài)內(nèi)部類

在靜態(tài)方法里,使用?靜態(tài)內(nèi)部類依賴于外部類的實例,也就是說需要先創(chuàng)建外部類實例,才能用這個實例去創(chuàng)建非靜態(tài)內(nèi)部類。?靜態(tài)內(nèi)部類不需要。

public class OuterClass { ? ?class InnerClass { ? ?} ? ?static class StaticInnerClass { ? ?} ? ?public static void main(String[] args) { ? ? ? ?// 在靜態(tài)方法里,不能直接使用OuterClass.this去創(chuàng)建InnerClass的實例 ? ? ? ?// 需要先創(chuàng)建OuterClass的實例o,然后通過o創(chuàng)建InnerClass的實例 ? ? ? ?// InnerClass innerClass = new InnerClass(); ? ? ? ?OuterClass outerClass = new OuterClass(); ? ? ? ?InnerClass innerClass = outerClass.new InnerClass(); ? ? ? ?StaticInnerClass staticInnerClass = new StaticInnerClass(); ? ? ? ?outerClass.test(); ? ?} ? ? ? ?public void nonStaticMethod() { ? ? ? ?InnerClass innerClass = new InnerClass(); ? ? ? ?System.out.println("nonStaticMethod..."); ? ?}}

final

  1. 基本數(shù)據(jù)類型用final修飾,則不能修改,是常量;對象引用用final修飾,則引用只能指向該對象,不能指向別的對象,但是對象本身可以修改。

  2. final修飾的方法不能被子類重寫

  3. final修飾的類不能被繼承。

this

this.屬性名稱指訪問類中的成員變量,可以用來區(qū)分成員變量和局部變量。如下代碼所示,this.name訪問類Person當(dāng)前實例的變量。

/** * @description: * @author: 程序員大彬 * @time: 2021-08-17 00:29 */public class Person { ? ?String name; ? ?int age; ? ?public Person(String name, int age) { ? ? ? ?this.name = name; ? ? ? ?this.age = age; ? ?}}

this.方法名稱用來訪問本類的方法。以下代碼中,this.born()調(diào)用類 Person 的當(dāng)前實例的方法。

/** * @description: * @author: 程序員大彬 * @time: 2021-08-17 00:29 */public class Person { ? ?String name; ? ?int age; ? ?public Person(String name, int age) { ? ? ? ?this.born(); ? ? ? ?this.name = name; ? ? ? ?this.age = age; ? ?} ? ?void born() { ? ?}}

super

super 關(guān)鍵字用于在子類中訪問父類的變量和方法。

class A { ? ?protected String name = "大彬"; ? ?public void getName() { ? ? ? ?System.out.println("父類:" + name); ? ?}}public class B extends A { ? ?@Override ? ?public void getName() { ? ? ? ?System.out.println(super.name); ? ? ? ?super.getName(); ? ?} ? ?public static void main(String[] args) { ? ? ? ?B b = new B(); ? ? ? ?b.getName(); ? ?} ? ?/** ? ? * 大彬 ? ? * 父類:大彬 ? ? */}

在子類B中,我們重寫了父類的getName()方法,如果在重寫的getName()方法中我們要調(diào)用父類的相同方法,必須要通過super關(guān)鍵字顯式指出。

final, finally, finalize 的區(qū)別

  • final 用于修飾屬性、方法和類, 分別表示屬性不能被重新賦值,方法不可被覆蓋,類不可被繼承。

  • finally 是異常處理語句結(jié)構(gòu)的一部分,一般以try-catch-finally出現(xiàn),finally代碼塊表示總是被執(zhí)行。

  • finalize 是Object類的一個方法,該方法一般由垃圾回收器來調(diào)用,當(dāng)我們調(diào)用System.gc()方法的時候,由垃圾回收器調(diào)用finalize()方法,回收垃圾,JVM并不保證此方法總被調(diào)用。

final關(guān)鍵字的作用?

  • final 修飾的類不能被繼承。

  • final 修飾的方法不能被重寫。

  • final 修飾的變量叫常量,常量必須初始化,初始化之后值就不能被修改。

方法重載和重寫的區(qū)別?

同個類中的多個方法可以有相同的方法名稱,但是有不同的參數(shù)列表,這就稱為方法重載。參數(shù)列表又叫參數(shù)簽名,包括參數(shù)的類型、參數(shù)的個數(shù)、參數(shù)的順序,只要有一個不同就叫做參數(shù)列表不同。

重載是面向?qū)ο蟮囊粋€基本特性。

public class OverrideTest { ? ?void setPerson() { } ? ? ? ?void setPerson(String name) { ? ? ? ?//set name ? ?} ? ? ? ?void setPerson(String name, int age) { ? ? ? ?//set name and age ? ?}}

方法的重寫描述的是父類和子類之間的。當(dāng)父類的功能無法滿足子類的需求,可以在子類對方法進行重寫。方法重寫時, 方法名與形參列表必須一致。

如下代碼,Person為父類,Student為子類,在Student中重寫了dailyTask方法。

public class Person { ? ?private String name; ? ? ? ?public void dailyTask() { ? ? ? ?System.out.println("work eat sleep"); ? ?}}public class Student extends Person { ? ?@Override ? ?public void dailyTask() { ? ? ? ?System.out.println("study eat sleep"); ? ?}}

接口與抽象類區(qū)別?

1、語法層面上的區(qū)別

  • 抽象類可以有方法實現(xiàn),而接口的方法中只能是抽象方法(Java 8 之后接口方法可以有默認實現(xiàn));

  • 抽象類中的成員變量可以是各種類型的,接口中的成員變量只能是public static final類型;

  • 接口中不能含有靜態(tài)代碼塊以及靜態(tài)方法,而抽象類可以有靜態(tài)代碼塊和靜態(tài)方法(Java 8之后接口可以有靜態(tài)方法);

  • 一個類只能繼承一個抽象類,而一個類卻可以實現(xiàn)多個接口。

2、設(shè)計層面上的區(qū)別

  • 抽象層次不同。抽象類是對整個類整體進行抽象,包括屬性、行為,但是接口只是對類行為進行抽象。繼承抽象類是一種"是不是"的關(guān)系,而接口實現(xiàn)則是 "有沒有"的關(guān)系。如果一個類繼承了某個抽象類,則子類必定是抽象類的種類,而接口實現(xiàn)則是具備不具備的關(guān)系,比如鳥是否能飛。

  • 繼承抽象類的是具有相似特點的類,而實現(xiàn)接口的卻可以不同的類。

門和警報的例子:

class AlarmDoor extends Door implements Alarm { ? ?//code}class BMWCar extends Car implements Alarm { ? ?//code}

常見的Exception有哪些?

常見的RuntimeException:

  1. ClassCastException //類型轉(zhuǎn)換異常

  2. IndexOutOfBoundsException //數(shù)組越界異常

  3. NullPointerException //空指針

  4. ArrayStoreException //數(shù)組存儲異常

  5. NumberFormatException //數(shù)字格式化異常

  6. ArithmeticException //數(shù)學(xué)運算異常

checked Exception:

  1. NoSuchFieldException //反射異常,沒有對應(yīng)的字段

  2. ClassNotFoundException //類沒有找到異常

  3. IllegalAccessException //安全權(quán)限異常,可能是反射時調(diào)用了private方法

Error和Exception的區(qū)別?

Error:JVM 無法解決的嚴重問題,如棧溢出StackOverflowError、內(nèi)存溢出OOM等。程序無法處理的錯誤。

Exception:其它因編程錯誤或偶然的外在因素導(dǎo)致的一般性問題??梢栽诖a中進行處理。如:空指針異常、數(shù)組下標越界等。

運行時異常和非運行時異常(checked)的區(qū)別?

unchecked exception包括RuntimeExceptionError類,其他所有異常稱為檢查(checked)異常。

  1. RuntimeException由程序錯誤導(dǎo)致,應(yīng)該修正程序避免這類異常發(fā)生。

  2. checked Exception由具體的環(huán)境(讀取的文件不存在或文件為空或sql異常)導(dǎo)致的異常。必須進行處理,不然編譯不通過,可以catch或者throws。

throw和throws的區(qū)別?

  • throw:用于拋出一個具體的異常對象。

  • throws:用在方法簽名中,用于聲明該方法可能拋出的異常。子類方法拋出的異常范圍更加小,或者根本不拋異常。

通過故事講清楚NIO

下面通過一個例子來講解下。

假設(shè)某銀行只有10個職員。該銀行的業(yè)務(wù)流程分為以下4個步驟:

1) 顧客填申請表(5分鐘);

2) 職員審核(1分鐘);

3) 職員叫保安去金庫取錢(3分鐘);

4) 職員打印票據(jù),并將錢和票據(jù)返回給顧客(1分鐘)。

下面我們看看銀行不同的工作方式對其工作效率到底有何影響。

首先是BIO方式。

每來一個顧客,馬上由一位職員來接待處理,并且這個職員需要負責(zé)以上4個完整流程。當(dāng)超過10個顧客時,剩余的顧客需要排隊等候。

一個職員處理一個顧客需要10分鐘(5+1+3+1)時間。一個小時(60分鐘)能處理6個顧客,一共10個職員,那就是只能處理60個顧客。

可以看到銀行職員的工作狀態(tài)并不飽和,比如在第1步,其實是處于等待中。

這種工作其實就是BIO,每次來一個請求(顧客),就分配到線程池中由一個線程(職員)處理,如果超出了線程池的最大上限(10個),就扔到隊列等待 。

那么如何提高銀行的吞吐量呢?

思路就是:分而治之,將任務(wù)拆分開來,由專門的人負責(zé)專門的任務(wù)。

具體來講,銀行專門指派一名職員A,A的工作就是每當(dāng)有顧客到銀行,他就遞上表格讓顧客填寫。每當(dāng)有顧客填好表后,A就將其隨機指派給剩余的9名職員完成后續(xù)步驟。

這種方式下,假設(shè)顧客非常多,職員A的工作處于飽和中,他不斷的將填好表的顧客帶到柜臺處理。

柜臺一個職員5分鐘能處理完一個顧客,一個小時9名職員能處理:9*(60/5)=108。

可見工作方式的轉(zhuǎn)變能帶來效率的極大提升。

這種工作方式其實就NIO的思路。

下圖是非常經(jīng)典的NIO說明圖,mainReactor線程負責(zé)監(jiān)聽server socket,接收新連接,并將建立的socket分派給subReactor

subReactor可以是一個線程,也可以是線程池,負責(zé)多路分離已連接的socket,讀寫網(wǎng)絡(luò)數(shù)據(jù)。這里的讀寫網(wǎng)絡(luò)數(shù)據(jù)可類比顧客填表這一耗時動作,對具體的業(yè)務(wù)處理功能,其扔給worker線程池完成

可以看到典型NIO有三類線程,分別是mainReactor線程、subReactor線程、work線程。

不同的線程干專業(yè)的事情,最終每個線程都沒空著,系統(tǒng)的吞吐量自然就上去了。




那這個流程還有沒有什么可以提高的地方呢?

可以看到,在這個業(yè)務(wù)流程里邊第3個步驟,職員叫保安去金庫取錢(3分鐘)。這3分鐘柜臺職員是在等待中度過的,可以把這3分鐘利用起來。

還是分而治之的思路,指派1個職員B來專門負責(zé)第3步驟。

每當(dāng)柜臺員工完成第2步時,就通知職員B來負責(zé)與保安溝通取錢。這時候柜臺員工可以繼續(xù)處理下一個顧客。

當(dāng)職員B拿到錢之后,通知顧客錢已經(jīng)到柜臺了,讓顧客重新排隊處理,當(dāng)柜臺職員再次服務(wù)該顧客時,發(fā)現(xiàn)該顧客前3步已經(jīng)完成,直接執(zhí)行第4步即可。

在當(dāng)今web服務(wù)中,經(jīng)常需要通過RPC或者Http等方式調(diào)用第三方服務(wù),這里對應(yīng)的就是第3步,如果這步耗時較長,通過異步方式將能極大降低資源使用率。

NIO+異步的方式能讓少量的線程做大量的事情。這適用于很多應(yīng)用場景,比如代理服務(wù)、api服務(wù)、長連接服務(wù)等等。這些應(yīng)用如果用同步方式將耗費大量機器資源。

不過雖然NIO+異步能提高系統(tǒng)吞吐量,但其并不能讓一個請求的等待時間下降,相反可能會增加等待時間。

最后,NIO基本思想總結(jié)起來就是:分而治之,將任務(wù)拆分開來,由專門的人負責(zé)專門的任務(wù)

BIO/NIO/AIO區(qū)別的區(qū)別?

同步阻塞IO : 用戶進程發(fā)起一個IO操作以后,必須等待IO操作的真正完成后,才能繼續(xù)運行。

同步非阻塞IO: 客戶端與服務(wù)器通過Channel連接,采用多路復(fù)用器輪詢注冊的Channel。提高吞吐量和可靠性。用戶進程發(fā)起一個IO操作以后,可做其它事情,但用戶進程需要輪詢IO操作是否完成,這樣造成不必要的CPU資源浪費。

異步非阻塞IO: 非阻塞異步通信模式,NIO的升級版,采用異步通道實現(xiàn)異步通信,其read和write方法均是異步方法。用戶進程發(fā)起一個IO操作,然后立即返回,等IO操作真正的完成以后,應(yīng)用程序會得到IO操作完成的通知。類似Future模式。

守護線程是什么?

  • 守護線程是運行在后臺的一種特殊進程。

  • 它獨立于控制終端并且周期性地執(zhí)行某種任務(wù)或等待處理某些發(fā)生的事件。

  • 在 Java 中垃圾回收線程就是特殊的守護線程。

Java支持多繼承嗎?

java中,類不支持多繼承。接口才支持多繼承。接口的作用是拓展對象功能。當(dāng)一個子接口繼承了多個父接口時,說明子接口拓展了多個功能。當(dāng)一個類實現(xiàn)該接口時,就拓展了多個的功能。

Java不支持多繼承的原因:

  • 出于安全性的考慮,如果子類繼承的多個父類里面有相同的方法或者屬性,子類將不知道具體要繼承哪個。

  • Java提供了接口和內(nèi)部類以達到實現(xiàn)多繼承功能,彌補單繼承的缺陷。

如何實現(xiàn)對象克隆?

  • 實現(xiàn)Cloneable接口,重寫 clone() 方法。這種方式是淺拷貝,即如果類中屬性有自定義引用類型,只拷貝引用,不拷貝引用指向的對象。如果對象的屬性的Class也實現(xiàn) Cloneable 接口,那么在克隆對象時也會克隆屬性,即深拷貝。

  • 結(jié)合序列化,深拷貝。

  • 通過org.apache.commons中的工具類BeanUtilsPropertyUtils進行對象復(fù)制。

同步和異步的區(qū)別?

同步:發(fā)出一個調(diào)用時,在沒有得到結(jié)果之前,該調(diào)用就不返回。

異步:在調(diào)用發(fā)出后,被調(diào)用者返回結(jié)果之后會通知調(diào)用者,或通過回調(diào)函數(shù)處理這個調(diào)用。

阻塞和非阻塞的區(qū)別?

阻塞和非阻塞關(guān)注的是線程的狀態(tài)。

阻塞調(diào)用是指調(diào)用結(jié)果返回之前,當(dāng)前線程會被掛起。調(diào)用線程只有在得到結(jié)果之后才會恢復(fù)運行。

非阻塞調(diào)用指在不能立刻得到結(jié)果之前,該調(diào)用不會阻塞當(dāng)前線程。

舉個例子,理解下同步、阻塞、異步、非阻塞的區(qū)別:
同步就是燒開水,要自己來看開沒開;異步就是水開了,然后水壺響了通知你水開了(回調(diào)通知)。阻塞是燒開水的過程中,你不能干其他事情,必須在旁邊等著;非阻塞是燒開水的過程里可以干其他事情。

Java8的新特性有哪些?

  • Lambda 表達式:Lambda允許把函數(shù)作為一個方法的參數(shù)

  • Stream API :新添加的Stream API(java.util.stream) 把真正的函數(shù)式編程風(fēng)格引入到Java中

  • 默認方法:默認方法就是一個在接口里面有了一個實現(xiàn)的方法。

  • Optional 類 :Optional 類已經(jīng)成為 Java 8 類庫的一部分,用來解決空指針異常。

  • Date Time API :加強對日期與時間的處理。

Java8 新特性總結(jié)

序列化和反序列化

  • 序列化:把對象轉(zhuǎn)換為字節(jié)序列的過程稱為對象的序列化.

  • 反序列化:把字節(jié)序列恢復(fù)為對象的過程稱為對象的反序列化.

什么時候需要用到序列化和反序列化呢?

當(dāng)我們只在本地 JVM 里運行下 Java 實例,這個時候是不需要什么序列化和反序列化的,但當(dāng)我們需要將內(nèi)存中的對象持久化到磁盤,數(shù)據(jù)庫中時,當(dāng)我們需要與瀏覽器進行交互時,當(dāng)我們需要實現(xiàn) RPC 時,這個時候就需要序列化和反序列化了.

前兩個需要用到序列化和反序列化的場景,是不是讓我們有一個很大的疑問? 我們在與瀏覽器交互時,還有將內(nèi)存中的對象持久化到數(shù)據(jù)庫中時,好像都沒有去進行序列化和反序列化,因為我們都沒有實現(xiàn) Serializable 接口,但一直正常運行.

下面先給出結(jié)論:

只要我們對內(nèi)存中的對象進行持久化或網(wǎng)絡(luò)傳輸,這個時候都需要序列化和反序列化.

理由:

服務(wù)器與瀏覽器交互時真的沒有用到 Serializable 接口嗎? JSON 格式實際上就是將一個對象轉(zhuǎn)化為字符串,所以服務(wù)器與瀏覽器交互時的數(shù)據(jù)格式其實是字符串,我們來看來 String 類型的源碼:

public final class String ? ?implements java.io.Serializable,Comparable<String>,CharSequence { ? ?/\*\* The value is used for character storage. \*/ ? ?private final char value\[\]; ? ?/\*\* Cache the hash code for the string \*/ ? ?private int hash; // Default to 0 ? ?/\*\* use serialVersionUID from JDK 1.0.2 for interoperability \*/ ? ?private static final long serialVersionUID = -6849794470754667710L; ? ?...... }

String 類型實現(xiàn)了 Serializable 接口,并顯示指定 serialVersionUID 的值.

然后我們再來看對象持久化到數(shù)據(jù)庫中時的情況,Mybatis 數(shù)據(jù)庫映射文件里的 insert 代碼:

<insert id="insertUser" parameterType="org.tyshawn.bean.User"> ? ?INSERT INTO t\_user(name,age) VALUES (#{name},#{age}) </insert>

實際上我們并不是將整個對象持久化到數(shù)據(jù)庫中,而是將對象中的屬性持久化到數(shù)據(jù)庫中,而這些屬性(如Date/String)都實現(xiàn)了 Serializable 接口。

實現(xiàn)序列化和反序列化為什么要實現(xiàn) Serializable 接口?

在 Java 中實現(xiàn)了 Serializable 接口后, JVM 在類加載的時候就會發(fā)現(xiàn)我們實現(xiàn)了這個接口,然后在初始化實例對象的時候就會在底層幫我們實現(xiàn)序列化和反序列化。

如果被寫對象類型不是String、數(shù)組、Enum,并且沒有實現(xiàn)Serializable接口,那么在進行序列化的時候,將拋出NotSerializableException。源碼如下:

// remaining casesif (obj instanceof String) { ? ?writeString((String) obj, unshared);} else if (cl.isArray()) { ? ?writeArray(obj, desc, unshared);} else if (obj instanceof Enum) { ? ?writeEnum((Enum<?>) obj, desc, unshared);} else if (obj instanceof Serializable) { ? ?writeOrdinaryObject(obj, desc, unshared);} else { ? ?if (extendedDebugInfo) { ? ? ? ?throw new NotSerializableException( ? ? ? ? ? ?cl.getName() + "\n" + debugInfoStack.toString()); ? ?} else { ? ? ? ?throw new NotSerializableException(cl.getName()); ? ?}}

實現(xiàn) Serializable 接口之后,為什么還要顯示指定 serialVersionUID 的值?

如果不顯示指定 serialVersionUID,JVM 在序列化時會根據(jù)屬性自動生成一個 serialVersionUID,然后與屬性一起序列化,再進行持久化或網(wǎng)絡(luò)傳輸. 在反序列化時,JVM 會再根據(jù)屬性自動生成一個新版 serialVersionUID,然后將這個新版 serialVersionUID 與序列化時生成的舊版 serialVersionUID 進行比較,如果相同則反序列化成功,否則報錯.

如果顯示指定了 serialVersionUID,JVM 在序列化和反序列化時仍然都會生成一個 serialVersionUID,但值為我們顯示指定的值,這樣在反序列化時新舊版本的 serialVersionUID 就一致了.

如果我們的類寫完后不再修改,那么不指定serialVersionUID,不會有問題,但這在實際開發(fā)中是不可能的,我們的類會不斷迭代,一旦類被修改了,那舊對象反序列化就會報錯。 所以在實際開發(fā)中,我們都會顯示指定一個 serialVersionUID。

static 屬性為什么不會被序列化?

因為序列化是針對對象而言的,而 static 屬性優(yōu)先于對象存在,隨著類的加載而加載,所以不會被序列化.

看到這個結(jié)論,是不是有人會問,serialVersionUID 也被 static 修飾,為什么 serialVersionUID 會被序列化? 其實 serialVersionUID 屬性并沒有被序列化,JVM 在序列化對象時會自動生成一個 serialVersionUID,然后將我們顯示指定的 serialVersionUID 屬性值賦給自動生成的 serialVersionUID.

transient關(guān)鍵字的作用?

Java語言的關(guān)鍵字,變量修飾符,如果用transient聲明一個實例變量,當(dāng)對象存儲時,它的值不需要維持。

也就是說被transient修飾的成員變量,在序列化的時候其值會被忽略,在被反序列化后, transient 變量的值被設(shè)為初始值, 如 int 型的是 0,對象型的是 null。

什么是反射?

動態(tài)獲取的信息以及動態(tài)調(diào)用對象的方法的功能稱為Java語言的反射機制。

在運行狀態(tài)中,對于任意一個類,能夠知道這個類的所有屬性和方法。對于任意一個對象,能夠調(diào)用它的任意一個方法和屬性。

反射有哪些應(yīng)用場景呢?

  1. JDBC連接數(shù)據(jù)庫時使用Class.forName()通過反射加載數(shù)據(jù)庫的驅(qū)動程序

  2. Eclispe、IDEA等開發(fā)工具利用反射動態(tài)解析對象的類型與結(jié)構(gòu),動態(tài)提示對象的屬性和方法

  3. Web服務(wù)器中利用反射調(diào)用了Sevlet的service方法

  4. JDK動態(tài)代理底層依賴反射實現(xiàn)

講講什么是泛型?

Java泛型是JDK 5中引?的?個新特性, 允許在定義類和接口的時候使?類型參數(shù)。聲明的類型參數(shù)在使?時?具體的類型來替換。

泛型最?的好處是可以提?代碼的復(fù)?性。以List接口為例,我們可以將String、 Integer等類型放?List中, 如不?泛型, 存放String類型要寫?個List接口, 存放Integer要寫另外?個List接口, 泛型可以很好的解決這個問題。

如何停止一個正在運行的線程?

有幾種方式。

1、使用線程的stop方法

使用stop()方法可以強制終止線程。不過stop是一個被廢棄掉的方法,不推薦使用。

使用Stop方法,會一直向上傳播ThreadDeath異常,從而使得目標線程解鎖所有鎖住的監(jiān)視器,即釋放掉所有的對象鎖。使得之前被鎖住的對象得不到同步的處理,因此可能會造成數(shù)據(jù)不一致的問題。

2、使用interrupt方法中斷線程,該方法只是告訴線程要終止,但最終何時終止取決于計算機。調(diào)用interrupt方法僅僅是在當(dāng)前線程中打了一個停止的標記,并不是真的停止線程。

接著調(diào)用 Thread.currentThread().isInterrupted()方法,可以用來判斷當(dāng)前線程是否被終止,通過這個判斷我們可以做一些業(yè)務(wù)邏輯處理,通常如果isInterrupted返回true的話,會拋一個中斷異常,然后通過try-catch捕獲。

3、設(shè)置標志位

設(shè)置標志位,當(dāng)標識位為某個值時,使線程正常退出。設(shè)置標志位是用到了共享變量的方式,為了保證共享變量在內(nèi)存中的可見性,可以使用volatile修飾它,這樣的話,變量取值始終會從主存中獲取最新值。

但是這種volatile標記共享變量的方式,在線程發(fā)生阻塞時是無法完成響應(yīng)的。比如調(diào)用Thread.sleep() 方法之后,線程處于不可運行狀態(tài),即便是主線程修改了共享變量的值,該線程此時根本無法檢查循環(huán)標志,所以也就無法實現(xiàn)線程中斷。

因此,interrupt() 加上手動拋異常的方式是目前中斷一個正在運行的線程最為正確的方式了。

什么是跨域?

簡單來講,跨域是指從一個域名的網(wǎng)頁去請求另一個域名的資源。由于有同源策略的關(guān)系,一般是不允許這么直接訪問的。但是,很多場景經(jīng)常會有跨域訪問的需求,比如,在前后端分離的模式下,前后端的域名是不一致的,此時就會發(fā)生跨域問題。

那什么是同源策略呢?

所謂同源是指"協(xié)議+域名+端口"三者相同,即便兩個不同的域名指向同一個ip地址,也非同源。

同源策略限制以下幾種行為:

1. Cookie、LocalStorage 和 IndexDB 無法讀取 2. DOM 和 Js對象無法獲得 3. AJAX 請求不能發(fā)送

為什么要有同源策略

舉個例子,假如你剛剛在網(wǎng)銀輸入賬號密碼,查看了自己的余額,然后再去訪問其他帶顏色的網(wǎng)站,這個網(wǎng)站可以訪問剛剛的網(wǎng)銀站點,并且獲取賬號密碼,那后果可想而知。因此,從安全的角度來講,同源策略是有利于保護網(wǎng)站信息的。

跨域問題怎么解決呢?

嗯,有以下幾種方法:

CORS,跨域資源共享

CORS(Cross-origin resource sharing),跨域資源共享。CORS 其實是瀏覽器制定的一個規(guī)范,瀏覽器會自動進行 CORS 通信,它的實現(xiàn)主要在服務(wù)端,通過一些 HTTP Header 來限制可以訪問的域,例如頁面 A 需要訪問 B 服務(wù)器上的數(shù)據(jù),如果 B 服務(wù)器 上聲明了允許 A 的域名訪問,那么從 A 到 B 的跨域請求就可以完成。

@CrossOrigin注解

如果項目使用的是Springboot,可以在Controller類上添加一個 @CrossOrigin(origins ="*") 注解就可以實現(xiàn)對當(dāng)前controller 的跨域訪問了,當(dāng)然這個標簽也可以加到方法上,或者直接加到入口類上對所有接口進行跨域處理。注意SpringMVC的版本要在4.2或以上版本才支持@CrossOrigin。

nginx反向代理接口跨域

nginx反向代理跨域原理如下: 首先同源策略是瀏覽器的安全策略,不是HTTP協(xié)議的一部分。服務(wù)器端調(diào)用HTTP接口只是使用HTTP協(xié)議,不會執(zhí)行JS腳本,不需要同源策略,也就不存在跨越問題。

nginx反向代理接口跨域?qū)崿F(xiàn)思路如下:通過nginx配置一個代理服務(wù)器(域名與domain1相同,端口不同)做跳板機,反向代理訪問domain2接口,并且可以順便修改cookie中domain信息,方便當(dāng)前域cookie寫入,實現(xiàn)跨域登錄。

// proxy服務(wù)器server { ? ?listen ? ? ? 81; ? ?server_name ?www.domain1.com; ? ?location / { ? ? ? ?proxy_pass ? http://www.domain2.com:8080; ?#反向代理 ? ? ? ?proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名 ? ? ? ?index ?index.html index.htm; ? ? ? ? ? ? ? ?add_header Access-Control-Allow-Origin http://www.domain1.com; ? ?}}

這樣我們的前端代理只要訪問 http:www.domain1.com:81/*就可以了。

通過jsonp跨域

通常為了減輕web服務(wù)器的負載,我們把js、css,img等靜態(tài)資源分離到另一臺獨立域名的服務(wù)器上,在html頁面中再通過相應(yīng)的標簽從不同域名下加載靜態(tài)資源,這是瀏覽器允許的操作,基于此原理,我們可以通過動態(tài)創(chuàng)建script,再請求一個帶參網(wǎng)址實現(xiàn)跨域通信。

設(shè)計接口要注意什么?

  1. 接口參數(shù)校驗。接口必須校驗參數(shù),比如入?yún)⑹欠裨试S為空,入?yún)㈤L度是否符合預(yù)期。

  2. 設(shè)計接口時,充分考慮接口的可擴展性。思考接口是否可以復(fù)用,怎樣保持接口的可擴展性。

  3. 串行調(diào)用考慮改并行調(diào)用。比如設(shè)計一個商城首頁接口,需要查商品信息、營銷信息、用戶信息等等。如果是串行一個一個查,那耗時就比較大了。這種場景是可以改為并行調(diào)用的,降低接口耗時。

  4. 接口是否需要防重處理。涉及到數(shù)據(jù)庫修改的,要考慮防重處理,可以使用數(shù)據(jù)庫防重表,以唯一流水號作為唯一索引。

  5. 日志打印全面,入?yún)⒊鰠?,接口耗時,記錄好日志,方便甩鍋。

  6. 修改舊接口時,注意兼容性設(shè)計

  7. 異常處理得當(dāng)。使用finally關(guān)閉流資源、使用log打印而不是e.printStackTrace()、不要吞異常等等

  8. 是否需要考慮限流。限流為了保護系統(tǒng),防止流量洪峰超過系統(tǒng)的承載能力。

過濾器和攔截器有什么區(qū)別?

1、實現(xiàn)原理不同。

過濾器和攔截器底層實現(xiàn)不同。過濾器是基于函數(shù)回調(diào)的,攔截器是基于Java的反射機制(動態(tài)代理)實現(xiàn)的。一般自定義的過濾器中都會實現(xiàn)一個doFilter()方法,這個方法有一個FilterChain參數(shù),而實際上它是一個回調(diào)接口。

2、使用范圍不同

過濾器實現(xiàn)的是 javax.servlet.Filter 接口,而這個接口是在Servlet規(guī)范中定義的,也就是說過濾器Filter的使用要依賴于Tomcat等容器,導(dǎo)致它只能在web程序中使用。而攔截器是一個Spring組件,并由Spring容器管理,并不依賴Tomcat等容器,是可以單獨使用的。攔截器不僅能應(yīng)用在web程序中,也可以用于Application、Swing等程序中。

3、使用的場景不同。

因為攔截器更接近業(yè)務(wù)系統(tǒng),所以攔截器主要用來實現(xiàn)項目中的業(yè)務(wù)判斷的,比如:日志記錄、權(quán)限判斷等業(yè)務(wù)。而過濾器通常是用來實現(xiàn)通用功能過濾的,比如:敏感詞過濾、響應(yīng)數(shù)據(jù)壓縮等功能。

4、觸發(fā)時機不同。

過濾器Filter是在請求進入容器后,但在進入servlet之前進行預(yù)處理,請求結(jié)束是在servlet處理完以后。

攔截器 Interceptor 是在請求進入servlet后,在進入Controller之前進行預(yù)處理的,Controller 中渲染了對應(yīng)的視圖之后請求結(jié)束。

5、攔截的請求范圍不同

請求的執(zhí)行順序是:請求進入容器 -> 進入過濾器 -> 進入 Servlet -> 進入攔截器 -> 執(zhí)行控制器。可以看到過濾器和攔截器的執(zhí)行時機也是不同的,過濾器會先執(zhí)行,然后才會執(zhí)行攔截器,最后才會進入真正的要調(diào)用的方法。

參考鏈接:segmentfault.com/a/1190

對接第三方接口要考慮什么?

嗯,需要考慮以下幾點:

  1. 確認接口對接的網(wǎng)絡(luò)協(xié)議,是https/http或者自定義的私有協(xié)議等。

  2. 約定好數(shù)據(jù)傳參、響應(yīng)格式(如application/json),弱類型對接強類型語言時要特別注意

  3. 接口安全方面,要確定身份校驗方式,使用token、證書校驗等

  4. 確認是否需要接口調(diào)用失敗后的重試機制,保證數(shù)據(jù)傳輸?shù)淖罱K一致性。

  5. 日志記錄要全面。接口出入?yún)?shù),以及解析之后的參數(shù)值,都要用日志記錄下來,方便定位問題(甩鍋)。

參考:blog.csdn.net/gzt198811

后端接口性能優(yōu)化有哪些方法?

有以下這些方法:

1、優(yōu)化索引。給where條件的關(guān)鍵字段,或者order by后面的排序字段,加索引。

2、優(yōu)化sql語句。比如避免使用select *、批量操作、避免深分頁、提升group by的效率等

3、避免大事務(wù)。使用@Transactional注解這種聲明式事務(wù)的方式提供事務(wù)功能,容易造成大事務(wù),引發(fā)其他的問題。應(yīng)該避免在事務(wù)中一次性處理太多數(shù)據(jù),將一些跟事務(wù)無關(guān)的邏輯放到事務(wù)外面執(zhí)行。

4、異步處理。剝離主邏輯和副邏輯,副邏輯可以異步執(zhí)行,異步寫庫。比如用戶購買的商品發(fā)貨了,需要發(fā)短信通知,短信通知是副流程,可以異步執(zhí)行,以免影響主流程的執(zhí)行。

5、降低鎖粒度。在并發(fā)場景下,多個線程同時修改數(shù)據(jù),造成數(shù)據(jù)不一致的情況。這種情況下,一般會加鎖解決。但如果鎖加得不好,導(dǎo)致鎖的粒度太粗,也會非常影響接口性能。

6、加緩存。如果表數(shù)據(jù)量非常大的話,直接從數(shù)據(jù)庫查詢數(shù)據(jù),性能會非常差。可以使用Redismemcached提升查詢性能,從而提高接口性能。

7、分庫分表。當(dāng)系統(tǒng)發(fā)展到一定的階段,用戶并發(fā)量大,會有大量的數(shù)據(jù)庫請求,需要占用大量的數(shù)據(jù)庫連接,同時會帶來磁盤IO的性能瓶頸問題?;蛘邤?shù)據(jù)庫表數(shù)據(jù)非常大,SQL查詢即使走了索引,也很耗時。這時,可以通過分庫分表解決。分庫用于解決數(shù)據(jù)庫連接資源不足問題,和磁盤IO的性能瓶頸問題。分表用于解決單表數(shù)據(jù)量太大,sql語句查詢數(shù)據(jù)時,即使走了索引也非常耗時問題。

8、避免在循環(huán)中查詢數(shù)據(jù)庫。循環(huán)查詢數(shù)據(jù)庫,非常耗時,最好能在一次查詢中獲取所有需要的數(shù)據(jù)。

為什么在阿里巴巴Java開發(fā)手冊中強制要求使用包裝類型定義屬性呢?

嗯,以布爾字段為例,當(dāng)我們沒有設(shè)置對象的字段的值的時候,Boolean類型的變量會設(shè)置默認值為null,而boolean類型的變量會設(shè)置默認值為false。

也就是說,包裝類型的默認值都是null,而基本數(shù)據(jù)類型的默認值是一個固定值,如boolean是false,byte、short、int、long是0,float是0.0f等。

舉一個例子,比如有一個扣費系統(tǒng),扣費時需要從外部的定價系統(tǒng)中讀取一個費率的值,我們預(yù)期該接口的返回值中會包含一個浮點型的費率字段。當(dāng)我們?nèi)〉竭@個值得時候就使用公式:金額*費率=費用 進行計算,計算結(jié)果進行劃扣。

如果由于計費系統(tǒng)異常,他可能會返回個默認值,如果這個字段是Double類型的話,該默認值為null,如果該字段是double類型的話,該默認值為0.0。

如果扣費系統(tǒng)對于該費率返回值沒做特殊處理的話,拿到null值進行計算會直接報錯,阻斷程序。拿到0.0可能就直接進行計算,得出接口為0后進行扣費了。這種異常情況就無法被感知。

那我可以對0.0做特殊判斷,如果是0就阻斷報錯,這樣是否可以呢?

不對,這時候就會產(chǎn)生一個問題,如果允許費率是0的場景又怎么處理呢?

使用基本數(shù)據(jù)類型只會讓方案越來越復(fù)雜,坑越來越多。

這種使用包裝類型定義變量的方式,通過異常來阻斷程序,進而可以被識別到這種線上問題。如果使用基本數(shù)據(jù)類型的話,系統(tǒng)可能不會報錯,進而認為無異常。

因此,建議在POJO和RPC的返回值中使用包裝類型。

參考鏈接:mp.weixin.qq.com/s/O_jC

8招讓接口性能提升100倍

池化思想

如果你每次需要用到線程,都去創(chuàng)建,就會有增加一定的耗時,而線程池可以重復(fù)利用線程,避免不必要的耗時。

比如TCP三次握手,它為了減少性能損耗,引入了Keep-Alive長連接,避免頻繁的創(chuàng)建和銷毀連接。

拒絕阻塞等待

如果你調(diào)用一個系統(tǒng)B的接口,但是它處理業(yè)務(wù)邏輯,耗時需要10s甚至更多。然后你是一直阻塞等待,直到系統(tǒng)B的下游接口返回,再繼續(xù)你的下一步操作嗎?這樣顯然不合理。

參考IO多路復(fù)用模型。即我們不用阻塞等待系統(tǒng)B的接口,而是先去做別的操作。等系統(tǒng)B的接口處理完,通過事件回調(diào)通知,我們接口收到通知再進行對應(yīng)的業(yè)務(wù)操作即可。

遠程調(diào)用由串行改為并行

比如設(shè)計一個商城首頁接口,需要查商品信息、營銷信息、用戶信息等等。如果是串行一個一個查,那耗時就比較大了。這種場景是可以改為并行調(diào)用的,降低接口耗時。

鎖粒度避免過粗

在高并發(fā)場景,為了防止超賣等情況,我們經(jīng)常需要加鎖來保護共享資源。但是,如果加鎖的粒度過粗,是很影響接口性能的。

不管你是synchronized加鎖還是redis分布式鎖,只需要在共享臨界資源加鎖即可,不涉及共享資源的,就不必要加鎖。

耗時操作,考慮放到異步執(zhí)行

耗時操作,考慮用異步處理,這樣可以降低接口耗時。比如用戶注冊成功后,短信郵件通知,是可以異步處理的。

使用緩存

把要查的數(shù)據(jù),提前放好到緩存里面,需要時,直接查緩存,而避免去查數(shù)據(jù)庫或者計算的過程。

提前初始化到緩存

預(yù)取思想很容易理解,就是提前把要計算查詢的數(shù)據(jù),初始化到緩存。如果你在未來某個時間需要用到某個經(jīng)過復(fù)雜計算的數(shù)據(jù),才實時去計算的話,可能耗時比較大。這時候,我們可以采取預(yù)取思想,提前把將來可能需要的數(shù)據(jù)計算好,放到緩存中,等需要的時候,去緩存取就行。這將大幅度提高接口性能。


三天吃透Java面試八股文(2023最新整理)的評論 (共 條)

分享到微博請遵守國家法律
阿合奇县| 新巴尔虎右旗| 黔南| 库尔勒市| 营口市| 临沂市| 汝阳县| 广昌县| 黑龙江省| 武强县| 满城县| 葫芦岛市| 益阳市| 宜宾县| 丰宁| 禹城市| 桓台县| 巩义市| 浦县| 鹤岗市| 上栗县| 荣昌县| 女性| 巩义市| 开平市| 金秀| 西城区| 彭山县| 留坝县| 灵寿县| 邵武市| 慈溪市| 泸州市| 浪卡子县| 乌鲁木齐市| 池州市| 南郑县| 信宜市| 辽宁省| 西充县| 大渡口区|