面試八股文之 Java 基礎
一.介紹 Java
java 是一門「開源的跨平臺的面向對象的」計算機語言。
跨平臺是因為 java 的 class 文件是運行在虛擬機上的,其實跨平臺的,而「虛擬機是不同平臺有不同版本」,所以說 java 是跨平臺的.
面向對象有幾個特點:
1.「封裝」
- 兩層含義:一層含義是把對象的屬性和行為看成一個密不可分的整體,將這兩者'封裝'在一個不可分割的「獨立單元」(即對象)中
- 另一層含義指'信息隱藏,把不需要讓外界知道的信息隱藏起來,有些對象的屬性及行為允許外界用戶知道或使用,但不允許更改,而另一些屬性或行為,則不允許外界知曉,或只允許使用對象的功能,而盡可能「隱藏對象的功能實現細節」。
「優點」:
- 良好的封裝能夠「減少耦合」,符合程序設計追求'高內聚,低耦合'
- 「類內部的結構可以自由修改」
- 可以對成員變量進行更「精確的控制」
- 「隱藏信息」實現細節
2.「繼承」
繼承就是子類繼承父類的特征和行為,使得子類對象(實例)具有父類的實例域和方法,或子類從父類繼承方法,使得子類具有父類相同的行為。
「優點」:
- 提高類代碼的「復用性」
- 提高了代碼的「維護性」
3.「多態」
- 「方法重載」:在一個類中,允許多個方法使用同一個名字,但方法的參數不同,完成的功能也不同。
- 「對象多態」:子類對象可以與父類對象進行轉換,而且根據其使用的子類不同完成的功能也不同(重寫父類的方法)。
多態是同一個行為具有多個不同表現形式或形態的能力。Java語言中含有方法重載與對象多態兩種形式的多態:
「優點」
「消除類型之間的耦合關系」
「可替換性」
「可擴充性」
「接口性」
「靈活性」
「簡化性」
二.Java 有哪些數據類型?
java 主要有兩種數據類型
1.「基本數據類型」
- byte,short,int,long屬于數值型中的整數型
- float,double屬于數值型中的浮點型
- char屬于字符型
- boolean屬于布爾型
基本數據有「八個」,
2.「引用數據類型」
引用數據類型有「三個」,分別是類,接口和數組
三.接口和抽象類有什么區別?
1.接口是抽象類的變體,「接口中所有的方法都是抽象的」。而抽象類是聲明方法的存在而不去實現它的類。
2.接口可以多繼承,抽象類不行。
3.接口定義方法,不能實現,默認是 「public abstract」,而抽象類可以實現部分方法。
4.接口中基本數據類型為 「public static final」 并且需要給出初始值,而抽類象不是的。
四.重載和重寫什么區別?
重寫:
1.參數列表必須「完全與被重寫的方法」相同,否則不能稱其為重寫而是重載.
2.「返回的類型必須一直與被重寫的方法的返回類型相同」,否則不能稱其為重寫而是重載。
3.訪問「修飾符的限制一定要大于被重寫方法的訪問修飾符」
4.重寫方法一定「不能拋出新的檢查異?;蛘弑缺恢貙懛椒ㄉ昝鞲訉挿旱臋z查型異?!埂?/p>
重載:
1.必須具有「不同的參數列表」;
2.可以有不同的返回類型,只要參數列表不同就可以了;
3.可以有「不同的訪問修飾符」;
4.可以拋出「不同的異常」;
五.常見的異常有哪些?
- NullPointerException 空指針異常
- ArrayIndexOutOfBoundsException 索引越界異常
- InputFormatException 輸入類型不匹配
- SQLException SQL異常
- IllegalArgumentException 非法參數
- NumberFormatException 類型轉換異常 等等....
六.異常要怎么解決?
Java標準庫內建了一些通用的異常,這些類以Throwable為頂層父類。
Throwable又派生出「Error類和Exception類」。
錯誤:Error類以及他的子類的實例,代表了JVM本身的錯誤。錯誤不能被程序員通過代碼處理,Error很少出現。因此,程序員應該關注Exception為父類的分支下的各種異常類。
異常:Exception以及他的子類,代表程序運行時發送的各種不期望發生的事件??梢员籎ava異常處理機制使用,是異常處理的核心。
處理方法:
1.「try()catch(){}」
- try{
- // 程序代碼
- }catch(ExceptionName e1){
- //Catch 塊
- }
2.「throw」
throw 關鍵字作用是拋出一個異常,拋出的時候是拋出的是一個異常類的實例化對象,在異常處理中,try 語句要捕獲的是一個異常對象,那么此異常對象也可以自己拋出
3.「throws」
定義一個方法的時候可以使用 throws 關鍵字聲明。使用 throws 關鍵字聲明的方法表示此方法不處理異常,而交給方法調用處進行處理。
七.arrayList 和 linkedList 的區別?
1.ArrayList 是實現了基于「數組」的,存儲空間是連續的。LinkedList 基于「鏈表」的,存儲空間是不連續的。(LinkedList 是雙向鏈表)
2.對于「隨機訪問」 get 和 set ,ArrayList 覺得優于 LinkedList,因為 LinkedList 要移動指針。
3.對于「新增和刪除」操作 add 和 remove ,LinedList 比較占優勢,因為 ArrayList 要移動數據。
4.同樣的數據量 LinkedList 所占用空間可能會更小,因為 ArrayList 需要「預留空間」便于后續數據增加,而 LinkedList 增加數據只需要「增加一個節點」
八.hashMap 1.7 和 hashMap 1.8 的區別?
只記錄「重點」
九.hashMap 線程不安全體現在哪里?
在 「hashMap1.7 中擴容」的時候,因為采用的是頭插法,所以會可能會有循環鏈表產生,導致數據有問題,在 1.8 版本已修復,改為了尾插法
在任意版本的 hashMap 中,如果在「插入數據時多個線程命中了同一個槽」,可能會有數據覆蓋的情況發生,導致線程不安全。
十. hashMap 線程不安全怎么解決?
- 給 hashMap 「直接加鎖」,來保證線程安全
- 使用 「hashTable」,比方法一效率高,其實就是在其方法上加了 synchronized 鎖
- 使用 「concurrentHashMap」 , 不管是其 1.7 還是 1.8 版本,本質都是「減小了鎖的粒度,減少線程競爭」來保證高效.
十一.concurrentHashMap 1.7 和 1.8 有什么區別
只記錄「重點」
十二.介紹 hashset
上圖是 set 家族整體的結構,set 繼承于 Collection 接口,是一個「不允許出現重復元素,并且無序的集合」.
HashSet 是「基于 HashMap 實現」的,底層「采用 HashMap 來保存元素」
元素的哈希值是通過元素的 hashcode 方法 來獲取的, HashSet 首先判斷兩個元素的哈希值,如果哈希值一樣,接著會比較 equals 方法 如果 equls 結果為 true ,HashSet 就視為同一個元素。如果 equals 為 false 就不是同一個元素。
十三.什么是泛型?
泛型:「把類型明確的工作推遲到創建對象或調用方法的時候才去明確的特殊的類型」
十四.泛型擦除是什么?
因為泛型其實只是在編譯器中實現的而虛擬機并不認識泛型類項,所以要在虛擬機中將泛型類型進行擦除。也就是說,「在編譯階段使用泛型,運行階段取消泛型,即擦除」。擦除是將泛型類型以其父類代替,如String 變成了Object等。其實在使用的時候還是進行帶強制類型的轉化,只不過這是比較安全的轉換,因為在編譯階段已經確保了數據的一致性。
十五.說說進程和線程的區別?
「進程是系統資源分配和調度的基本單位」,它能并發執行較高系統資源的利用率.
「線程」是「比進程更小」的能獨立運行的基本單位,創建、銷毀、切換成本要小于進程,可以減少程序并發執行時的時間和空間開銷,使得操作系統具有更好的并發性。
十六.volatile 有什么作用?
「1.保證內存可見性」
- 可見性是指線程之間的可見性,一個線程修改的狀態對另一個線程是可見的。也就是一個線程修改的結果,另一個線程馬上就能看到。
「2.禁止指令重排序」
- cpu 是和緩存做交互的,但是由于 cpu 運行效率太高,所以會不等待當前命令返回結果從而繼續執行下一個命令,就會有亂序執行的情況發生
十七.什么是包裝類?
為什么需要包裝類?「Java 中有 8 個基本類型,分別對應的 8 個包裝類」
- byte -- Byte
- boolean -- Boolean
- short -- Short
- char -- Character
- int -- Integer
- long -- Long
- float -- Float
- double -- Double
「為什么需要包裝類」:
- 基本數據類型方便、簡單、高效,但泛型不支持、集合元素不支持
- 不符合面向對象思維
- 包裝類提供很多方法,方便使用,如 Integer 類 toHexString(int i)、parseInt(String s) 方法等等
十八.Integer a = 1000,Integer b = 1000,a==b 的結果是什么?那如果 a,b 都為1,結果又是什么?
Integer a = 1000,Integer b = 1000,a==b 結果為「false」
Integer a = 1,Integer b = 1,a==b 結果為「true」
這道題主要考察 Integer 包裝類緩存的范圍,「在-128~127之間會緩存起來」,比較的是直接緩存的數據,在此之外比較的是對象
十九.JMM 是什么?
JMM 就是 「Java內存模型」(java memory model)。因為在不同的硬件生產商和不同的操作系統下,內存的訪問有一定的差異,所以會造成相同的代碼運行在不同的系統上會出現各種問題。所以java內存模型(JMM)「屏蔽掉各種硬件和操作系統的內存訪問差異,以實現讓java程序在各種平臺下都能達到一致的并發效果」。
Java內存模型規定所有的變量都存儲在主內存中,包括實例變量,靜態變量,但是不包括局部變量和方法參數。每個線程都有自己的工作內存,線程的工作內存保存了該線程用到的變量和主內存的副本拷貝,線程對變量的操作都在工作內存中進行。「線程不能直接讀寫主內存中的變量」。
每個線程的工作內存都是獨立的,「線程操作數據只能在工作內存中進行,然后刷回到主存」。這是 Java 內存模型定義的線程基本工作方式。
二十.創建對象有哪些方式
有「五種創建對象的方式」
1.new關鍵字
- Person p1 = new Person();
2.Class.newInstance
- Person p1 = Person.class.newInstance();
3.Constructor.newInstance
- Constructor<Person> constructor = Person.class.getConstructor();
- Person p1 = constructor.newInstance();
4.clone
- Person p1 = new Person();
- Person p2 = p1.clone();
5.反序列化
- Person p1 = new Person();
- byte[] bytes = SerializationUtils.serialize(p1);
- Person p2 = (Person)SerializationUtils.deserialize(bytes);
二十一.講講單例模式懶漢式吧
直接貼代碼
- // 懶漢式
- public class Singleton {
- // 延遲加載保證多線程安全
- Private volatile static Singleton singleton;
- private Singleton(){}
- public static Singleton getInstance(){
- if(singleton == null){
- synchronized(Singleton.class){
- if(singleton == null){
- singleton = new Singleton();
- }
- }
- }
- return singleton;
- }
- }
- 使用 volatile 是「防止指令重排序,保證對象可見」,防止讀到半初始化狀態的對象
- 第一層if(singleton == null) 是為了防止有多個線程同時創建
- synchronized 是加鎖防止多個線程同時進入該方法創建對象
- 第二層if(singleton == null) 是防止有多個線程同時等待鎖,一個執行完了后面一個又繼續執行的情況
二十二.volatile 有什么作用
1.「保證內存可見性」
當一個被volatile關鍵字修飾的變量被一個線程修改的時候,其他線程可以立刻得到修改之后的結果。當一個線程向被volatile關鍵字修飾的變量「寫入數據」的時候,虛擬機會「強制它被值刷新到主內存中」。當一個線程「讀取」被volatile關鍵字修飾的值的時候,虛擬機會「強制要求它從主內存中讀取」。
2.「禁止指令重排序」
指令重排序是編譯器和處理器為了高效對程序進行優化的手段,cpu 是與內存交互的,而 cpu 的效率想比內存高很多,所以 cpu 會在不影響最終結果的情況下,不等待返回結果直接進行后續的指令操作,而 volatile 就是給相應代碼加了「內存屏障」,在屏障內的代碼禁止指令重排序。
二十三.怎么保證線程安全?
1.synchronized關鍵字
可以用于代碼塊,方法(靜態方法,同步鎖是當前字節碼對象;實例方法,同步鎖是實例對象)
2.lock鎖機制
- Lock lock = new ReentrantLock();
- lock. lock();
- try {
- System. out. println("獲得鎖");
- } catch (Exception e) {
- } finally {
- System. out. println("釋放鎖");
- lock. unlock();
- }
二十四.synchronized 鎖升級的過程
在 Java1.6 之前的版本中,synchronized 屬于重量級鎖,效率低下,「鎖是」 cpu 一個「總量級的資源」,每次獲取鎖都要和 cpu 申請,非常消耗性能。
在 「jdk1.6 之后」 Java 官方對從 JVM 層面對 synchronized 較大優化,所以現在的 synchronized 鎖效率也優化得很不錯了,Jdk1.6 之后,為了減少獲得鎖和釋放鎖所帶來的性能消耗,引入了偏向鎖和輕量級鎖,「增加了鎖升級的過程」,由無鎖->偏向鎖->自旋鎖->重量級鎖.
增加鎖升級的過程主要是「減少用戶態到核心態的切換,提高鎖的效率,從 jvm 層面優化鎖」
二十五.cas 是什么?
cas 叫做 CompareAndSwap,「比較并交換」,很多地方使用到了它,比如鎖升級中自旋鎖就有用到,主要是「通過處理器的指令來保證操作的原子性」,它主要包含三個變量:
- 「1.變量內存地址」
- 「2.舊的預期值 A」
- 「3.準備設置的新值 B」
當一個線程需要修改一個共享變量的值,完成這個操作需要先取出共享變量的值,賦給 A,基于 A 進行計算,得到新值 B,在用預期原值 A 和內存中的共享變量值進行比較,「如果相同就認為其他線程沒有進行修改」,而將新值寫入內存。
「CAS的缺點」
- 「CPU開銷比較大」:在并發量比較高的情況下,如果許多線程反復嘗試更新某一個變量,卻又一直更新不成功,又因為自旋的時候會一直占用CPU,如果CAS一直更新不成功就會一直占用,造成CPU的浪費。
- 「ABA 問題」:比如線程 A 去修改 1 這個值,修改成功了,但是中間 線程 B 也修改了這個值,但是修改后的結果還是 1,所以不影響 A 的操作,這就會有問題??梢杂谩赴姹咎枴箒斫鉀Q這個問題。
- 「只能保證一個共享變量的原子性」
二十六.聊聊 ReentrantLock 吧
ReentrantLock 意為「可重入鎖」,說起 ReentrantLock 就不得不說 AQS ,因為其底層就是「使用 AQS 去實現」的。
ReentrantLock有兩種模式,一種是公平鎖,一種是非公平鎖。
- 公平模式下等待線程入隊列后會嚴格按照隊列順序去執行
- 非公平模式下等待線程入隊列后有可能會出現插隊情況
「公平鎖」
第一步:「獲取狀態的 state 的值」
- 如果 state=0 即代表鎖沒有被其它線程占用,執行第二步。
- 如果 state!=0 則代表鎖正在被其它線程占用,執行第三步。
第二步:「判斷隊列中是否有線程在排隊等待」
- 如果不存在則直接將鎖的所有者設置成當前線程,且更新狀態 state 。
- 如果存在就入隊。
第三步:「判斷鎖的所有者是不是當前線程」
- 如果是則更新狀態 state 的值。
- 如果不是,線程進入隊列排隊等待。
「非公平鎖」
獲取狀態的 state 的值
- 如果 state=0 即代表鎖沒有被其它線程占用,則設置當前鎖的持有者為當前線程,該操作用 CAS 完成。
- 如果不為0或者設置失敗,代表鎖被占用進行下一步。
此時「獲取 state 的值」
- 如果是,則給state+1,獲取鎖
- 如果不是,則進入隊列等待
- 如果是0,代表剛好線程釋放了鎖,此時將鎖的持有者設為自己
- 如果不是0,則查看線程持有者是不是自己
二十七.多線程的創建方式有哪些?
1、「繼承Thread類」,重寫run()方法
- public class Demo extends Thread{
- //重寫父類Thread的run()
- public void run() {
- }
- public static void main(String[] args) {
- Demo d1 = new Demo();
- Demo d2 = new Demo();
- d1.start();
- d2.start();
- }
- }
2.「實現Runnable接口」,重寫run()
- public class Demo2 implements Runnable{
- //重寫Runnable接口的run()
- public void run() {
- }
- public static void main(String[] args) {
- Thread t1 = new Thread(new Demo2());
- Thread t2 = new Thread(new Demo2());
- t1.start();
- t2.start();
- }
- }
3.「實現 Callable 接口」
- public class Demo implements Callable<String>{
- public String call() throws Exception {
- System.out.println("正在執行新建線程任務");
- Thread.sleep(2000);
- return "結果";
- }
- public static void main(String[] args) throws InterruptedException, ExecutionException {
- Demo d = new Demo();
- FutureTask<String> task = new FutureTask<>(d);
- Thread t = new Thread(task);
- t.start();
- //獲取任務執行后返回的結果
- String result = task.get();
- }
- }
4.「使用線程池創建」
- public class Demo {
- public static void main(String[] args) {
- Executor threadPool = Executors.newFixedThreadPool(5);
- for(int i = 0 ;i < 10 ; i++) {
- threadPool.execute(new Runnable() {
- public void run() {
- //todo
- }
- });
- }
- }
- }
二十八.線程池有哪些參數?
「1.corePoolSize」:「核心線程數」,線程池中始終存活的線程數。
「2.maximumPoolSize」: 「最大線程數」,線程池中允許的最大線程數。
「3.keepAliveTime」: 「存活時間」,線程沒有任務執行時最多保持多久時間會終止。
「4.unit」: 「單位」,參數keepAliveTime的時間單位,7種可選。
「5.workQueue」: 一個「阻塞隊列」,用來存儲等待執行的任務,均為線程安全,7種可選。
「6.threadFactory」: 「線程工廠」,主要用來創建線程,默及正常優先級、非守護線程。
「7.handler」:「拒絕策略」,拒絕處理任務時的策略,4種可選,默認為AbortPolicy。
二十九.線程池的執行流程?
判斷線程池中的線程數「是否大于設置的核心線程數」
- 如果「沒有滿」,則「放入隊列」,等待線程空閑時執行任務
- 如果隊列已經「滿了」,則判斷「是否達到了線程池設置的最大線程數」
- 如果「沒有達到」,就「創建新線程」來執行任務
- 如果已經「達到了」最大線程數,則「執行指定的拒絕策略」
- 如果「小于」,就「創建」一個核心線程來執行任務
- 如果「大于」,就會「判斷緩沖隊列是否滿了」
三十.線程池的拒絕策略有哪些?
- 「AbortPolicy」:直接丟棄任務,拋出異常,這是默認策略
- 「CallerRunsPolicy」:只用調用者所在的線程來處理任務
- 「DiscardOldestPolicy」:丟棄等待隊列中最舊的任務,并執行當前任務
- 「DiscardPolicy」:直接丟棄任務,也不拋出異常
三十一.介紹一下四種引用類型?
「強引用 StrongReference」
- Object obj = new Object();
- //只要obj還指向Object對象,Object對象就不會被回收
垃圾回收器不會回收被引用的對象,哪怕內存不足時,JVM 也會直接拋出 OutOfMemoryError,除非賦值為 null。
- 「軟引用 SoftReference」
軟引用是用來描述一些非必需但仍有用的對象。在內存足夠的時候,軟引用對象不會被回收,只有在內存不足時,系統則會回收軟引用對象,如果回收了軟引用對象之后仍然沒有足夠的內存,才會拋出內存溢出異常。
- 「弱引用 WeakReference」
弱引用的引用強度比軟引用要更弱一些,無論內存是否足夠,只要 JVM 開始進行垃圾回收,那些被弱引用關聯的對象都會被回收。
- 「虛引用 PhantomReference」
虛引用是最弱的一種引用關系,如果一個對象僅持有虛引用,那么它就和沒有任何引用一樣,它隨時可能會被回收,在 JDK1.2 之后,用 PhantomReference 類來表示,通過查看這個類的源碼,發現它只有一個構造函數和一個 get() 方法,而且它的 get() 方法僅僅是返回一個null,也就是說將永遠無法通過虛引用來獲取對象,虛引用必須要和 ReferenceQueue 引用隊列一起使用,NIO 的堆外內存就是靠其管理。
三十二.深拷貝、淺拷貝是什么?
淺拷貝并不是真的拷貝,只是「復制指向某個對象的指針」,而不復制對象本身,新舊對象還是共享同一塊內存。
深拷貝會另外「創造一個一模一樣的對象」,新對象跟原對象不共享內存,修改新對象不會改到原對象。
三十三.聊聊 ThreadLocal 吧
ThreadLocal其實就是「線程本地變量」,他會在每個線程都創建一個副本,那么在線程之間訪問內部副本變量就行了,做到了線程之間互相隔離。
- ThreadLocal 有一個「靜態內部類 ThreadLocalMap」,ThreadLocalMap 又包含了一個 Entry 數組,「Entry 本身是一個弱引用」,他的 key 是指向 ThreadLocal 的弱引用,「弱引用的目的是為了防止內存泄露」,如果是強引用那么除非線程結束,否則無法終止,可能會有內存泄漏的風險。
- 但是這樣還是會存在內存泄露的問題,假如 key 和 ThreadLocal 對象被回收之后,entry 中就存在 key 為 null ,但是 value 有值的 entry 對象,但是永遠沒辦法被訪問到,同樣除非線程結束運行?!附鉀Q方法就是調用 remove 方法刪除 entry 對象」。
三十四.一個對象的內存布局是怎么樣的?
對象內存布局
「1.對象頭」: 對象頭又分為 「MarkWord」 和 「Class Pointer」 兩部分。
- 「MarkWord」:包含一系列的標記位,比如輕量級鎖的標記位,偏向鎖標記位,gc記錄信息等等。
- 「ClassPointer」:用來指向對象對應的 Class 對象(其對應的元數據對象)的內存地址。在 32 位系統占 4 字節,在 64 位系統中占 8 字節。
「2.Length」:只在數組對象中存在,用來記錄數組的長度,占用 4 字節
「3.Instance data」: 對象實際數據,對象實際數據包括了對象的所有成員變量,其大小由各個成員變量的大小決定。(這里不包括靜態成員變量,因為其是在方法區維護的)
「4.Padding」:Java 對象占用空間是 8 字節對齊的,即所有 Java 對象占用 bytes 數必須是 8 的倍數,是因為當我們從磁盤中取一個數據時,不會說我想取一個字節就是一個字節,都是按照一塊兒一塊兒來取的,這一塊大小是 8 個字節,所以為了完整,padding 的作用就是補充字節,「保證對象是 8 字節的整數倍」。