面試準備:Java常見面試題彙總(三)


前兩篇,參考:
面試準備:Java常見面試題彙總(一)
面試準備:Java常見面試題彙總(二)

83. Java泛型瞭解麼?什麼是類型擦除?介紹一下常用的通配符?

Java 泛型(generics)是 JDK 5 中引入的一個新特性, 泛型提供了編譯時類型安全檢測機制,該機制允許在編譯時檢測到非法的類型。泛型的本質是參數化類型,也就是說所操作的數據類型被指定爲一個參數。

Java的泛型是僞泛型,這是因爲Java在編譯期間,所有的泛型信息都會被擦掉,這也就是通常所說類型擦除 。 Java編譯時期生成的字節碼的時候會去掉泛型,寫入一個特定類型參數。這個過程成爲類型擦除。

通配符沒啥區別,只不過是編碼時的一種約定俗成的東西。
常用的通配符爲: T,E,K,V,?

  • ? 表示不確定的 java 類型
  • T (type) 表示具體的一個java類型
  • K V (key value) 分別代表java鍵值中的Key Value
  • E (element) 代表Element

其他知識點,參考:Java泛型類型擦除以及類型擦除帶來的問題

84. 包裝類的常量池技術有了解過嗎?

常量池(constant_pool)是在編譯期被確定,並被保存在已編譯的class文件中的一些數據。除了包含代碼中所定義的各種基本類型(如 int、long等)和對象型(如 String 及數組)的常量值外,還包含一些以文本形式出現的符號引用(Class中的常量池中數據會在加載的方式放進方法區中的運行時常量池中)。

Java 基本類型的包裝類的大部分都實現了常量池技術,即 Byte,Short,Integer,Long,Character,Boolean;前面 4 種包裝類默認創建了數值[-128,127] 的相應類型的緩存數據,Character創建了數值在[0,127]範圍的緩存數據,Boolean 直接返回True Or False。如果超出對應範圍會去創建新的對象,否則直接在常量池中拿到對象

比如下面的代碼:

	public static void main(String[] args) {
        Integer a = 10;
        Integer b = 10;
        Integer c = 200;
        Integer d = 200;
        System.out.println(a==b);//true
        System.out.println(c==d);//false
		//new 肯定是創建一個新的對象
		Integer i1=new Integer(1);  
	    Integer i2=new Integer(1);  
		//i1,i2分別位於堆中不同的內存空間  
	   System.out.println(i1==i2);//輸出false  
    }

會被編譯成爲:

	public static void main(String[] args) {
        Integer a = Integer.valueOf(10);
        Integer b = Integer.valueOf(10);
        Integer c = Integer.valueOf(200);
        Integer d = Integer.valueOf(200);
        System.out.println(a == b);
        System.out.println(c == d);
    }

其中valueOf()方法的實現如下:

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

85. 在 Java 中定義一個不做事且沒有參數的構造方法的作用?

Java 程序在執行子類的構造方法之前,如果沒有用 super()來調用父類特定的構造方法,則會調用父類中“沒有參數的構造方法”。因此,如果父類中只定義了有參數的構造方法,而在子類的構造方法中又沒有用 super()來調用父類中特定的構造方法,則編譯時將發生錯誤,因爲 Java 程序在父類中找不到沒有參數的構造方法可供執行。解決辦法是在父類里加上一個不做事且沒有參數的構造方法。

86. 成員變量與局部變量的區別有哪些?

  1. 成員變量能由public private protect修飾,而局部變量不行。
  2. 成員變量依附於對象,跟隨對象的生命週期,它也存儲在堆內存中;
    局部變量依附於方法,跟隨方法棧幀的生命週期,存在於棧內存的局部變量表中。
  3. 成員變量如果沒有被賦初值:則會自動以類型的默認值而賦值,而局部變量則不會自動賦值。

87. 構造方法作用?有哪些特性?

主要作用是完成對類對象的初始化工作。

特性:

  1. 名字與類名相同。
  2. 沒有返回值,但不能用 void 聲明構造函數。
  3. 生成類的對象時自動執行,無需調用。

88. 在調用子類構造方法之前會先調用父類無參構造方法,其目的是?

子類繼承了父類的一些屬性,調用父類構造方法幫助子類做初始化工作。

89. Object類提供了哪些方法?

public final native Class<?> getClass()//native方法,用於返回當前運行時對象的Class對象,使用了final關鍵字修飾,故不允許子類重寫。

public native int hashCode() //native方法,用於返回對象的哈希碼,主要使用在哈希表中,比如JDK中的HashMap。
public boolean equals(Object obj)//用於比較2個對象的內存地址是否相等,String類對該方法進行了重寫用戶比較字符串的值是否相等。

protected native Object clone() throws CloneNotSupportedException//naitive方法,用於創建並返回當前對象的一份拷貝。一般情況下,對於任何對象 x,表達式 x.clone() != x 爲true,x.clone().getClass() == x.getClass() 爲true。Object本身沒有實現Cloneable接口,所以不重寫clone方法並且進行調用的話會發生CloneNotSupportedException異常。

public String toString()//返回類的名字@實例的哈希碼的16進制的字符串。建議Object所有的子類都重寫這個方法。

public final native void notify()//native方法,並且不能重寫。喚醒一個在此對象監視器上等待的線程(監視器相當於就是鎖的概念)。如果有多個線程在等待只會任意喚醒一個。

public final native void notifyAll()//native方法,並且不能重寫。跟notify一樣,唯一的區別就是會喚醒在此對象監視器上等待的所有線程,而不是一個線程。

public final native void wait(long timeout) throws InterruptedException//native方法,並且不能重寫。暫停線程的執行。注意:sleep方法沒有釋放鎖,而wait方法釋放了鎖 。timeout是等待時間。

public final void wait(long timeout, int nanos) throws InterruptedException//多了nanos參數,這個參數表示額外時間(以毫微秒爲單位,範圍是 0-999999)。 所以超時的時間還需要加上nanos毫秒。

public final void wait() throws InterruptedException//跟之前的2個wait方法一樣,只不過該方法一直等待,沒有超時時間這個概念

protected void finalize() throws Throwable { }//實例被垃圾回收器回收的時候觸發的操作

90. 獲取用鍵盤輸入常用的兩種方法?

方法 1:通過 Scanner

Scanner input = new Scanner(System.in);
String s  = input.nextLine();
input.close();

方法 2:通過 BufferedReader

BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
String s = input.readLine();

91. 使用 try-with-resources?

需要繼承closeable,重寫close方法。

public final class Scanner implements Iterator<String>, Closeable {...}
try (Scanner scanner = new Scanner(new File("test.txt"))) {
    while (scanner.hasNext()) {
        System.out.println(scanner.nextLine());
    }
} catch (FileNotFoundException fnfe) {
    fnfe.printStackTrace();
}

92. 既然有了字節流,爲什麼還要有字符流?

不管是文件讀寫還是網絡發送接收,信息的最小存儲單元都是字節,字符流也是基於字節流封裝的,主要作用就是提高開發效率。並且,如果我們不知道編碼類型就很容易出現亂碼問題(因爲字節流在處理時是逐個字節讀取,在讀取漢字時會出現亂碼問題)。所以, I/O 流就乾脆提供了一個直接操作字符的接口,方便我們平時對字符進行流操作。如果音頻文件、圖片等媒體文件用字節流比較好,如果涉及到字符的話使用字符流比較好。

93. 浮點數怎麼比較?

浮點數之間的等值判斷,基本數據類型不能用==來比較,包裝數據類型不能用 equals 來判斷。

float a = 1.0f - 0.9f;
float b = 0.9f - 0.8f;
System.out.println(a);// 0.100000024
System.out.println(b);// 0.099999964
System.out.println(a == b);// false

使用使用 BigDecimal 來定義浮點數的值,再進行浮點數的運算操作。

BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");
BigDecimal x = a.subtract(b);// 0.1
BigDecimal y = b.subtract(c);// 0.1
System.out.println(x.equals(y));// true 

94. 說一說自己對於 synchronized 關鍵字的瞭解?

synchronized關鍵字解決的是多個線程之間訪問資源的互斥和同步。

在 Java 早期版本中,synchronized屬於重量級鎖,效率低下,因爲監視器鎖(monitor)是依賴於底層的操作系統的 Mutex Lock 來實現的,Java 的線程是映射到操作系統的原生線程之上的。如果要掛起或者喚醒一個線程,都需要操作系統幫忙完成,而操作系統實現線程之間的切換時需要從用戶態轉換到內核態,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高,這也是爲什麼早期的 synchronized 效率低的原因。

JDK1.6對synchronized的實現引入了大量的優化,比如鎖升級的方式。

95. 講一下volatile關鍵字?

在 JDK1.2 之前,Java的內存模型實現總是從主存(即共享內存)讀取變量,是不需要進行特別的注意的。而在當前的 Java 內存模型下,線程可以把變量保存本地內存(比如機器的寄存器)中,而不是直接在主存中進行讀寫。這就可能造成一個線程在主存中修改了一個變量的值,而另外一個線程還繼續使用它在寄存器中的變量值的拷貝,造成數據的不一致。

volatile 關鍵字的主要作用就是保證變量的可見性然後還有一個作用是防止指令重排序。

96. 簡單介紹一下 AtomicInteger 類的原理?

AtomicInteger 線程安全原理簡單分析

AtomicInteger 類的部分源碼:

    // setup to use Unsafe.compareAndSwapInt for updates(更新操作時提供“比較並替換”的作用)
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;

AtomicInteger 類主要利用 CAS (compare and swap) + volatile 和 native 方法來保證原子操作,從而避免 synchronized 的高開銷,執行效率大爲提升。

CAS的原理是拿期望的值和原本的一個值作比較,如果相同則更新成新的值。UnSafe 類的 objectFieldOffset() 方法是一個本地方法,這個方法是用來拿到“原來的值”的內存地址,返回值是 valueOffset。另外 value 是一個volatile變量,在內存中可見,因此 JVM 可以保證任何時刻任何線程總能拿到該變量的最新值。

97. 簡單介紹一下AQS ?

參考:Java併發編程實戰——理解AbstractQueuedSynchronizer(AQS)的模版方法模式

AQS是一個用來構建鎖和同步器的框架,它的核心思想是判斷一個共享資源有沒有被佔用,如果沒有被佔用,則將其設置爲佔用狀態;否則實現一套阻塞等待、排隊、喚醒的機制。

具體來說,就是通過一個被volatile修飾過的int類型的值和CAS機制來控制同步狀態。然後通過模版方法模式來重寫AQS提供的方法,比如說共享或者獨佔的同步方法,也可以去重寫等待隊列。

一般來說類似ReentrantLock或者CountDownLatch都是實現了一個Sync(繼承自AQS)來使用AQS的模版方法的。
在這裏插入圖片描述

98. 比較 HashSet、LinkedHashSet 和 TreeSet 三者的異同?

HashSet 是 Set 接口的主要實現類 ,HashSet 的底層是 HashMap,線程不安全的,可以存儲 null 值;

LinkedHashSet 是 HashSet 的子類,能夠按照添加的順序遍歷;

TreeSet 底層使用紅黑樹,能夠按照添加元素的順序進行遍歷,排序的方式有自然排序和定製排序。

99. HashSet 如何檢查重複?

當你把對象加入HashSet時,HashSet 會先計算對象的hashcode值來判斷對象加入的位置,同時也會與其他加入的對象的 hashcode 值作比較,如果沒有相符的 hashcode,HashSet 會假設對象沒有重複出現。但是如果發現有相同 hashcode 值的對象,這時會調用equals()方法來檢查 hashcode 相等的對象是否真的相同。如果兩者相同,HashSet 就不會讓加入操作成功。

100. 什麼是快速失敗(fail-fast)?

快速失敗(fail-fast) 是 Java 集合的一種錯誤檢測機制。在使用迭代器對集合進行遍歷的時候,我們在多線程下操作非安全失敗(fail-safe)的集合類可能就會觸發 fail-fast 機制,導致拋出 ConcurrentModificationException 異常。 另外,在單線程下,如果在遍歷過程中對集合對象的內容進行了修改的話也會觸發 fail-fast 機制。

101. 什麼是安全失敗(fail-safe)呢?

採用安全失敗機制的集合容器,在遍歷時不是直接在集合內容上訪問的,而是先複製原有集合內容,在拷貝的集合上進行遍歷。所以,在遍歷過程中對原集合所作的修改並不能被迭代器檢測到,故不會拋 ConcurrentModificationException 異常

102. 線程池大小如何確定?

如果我們設置的線程池數量太小的話,如果同一時間有大量任務/請求需要處理,可能會導致大量的請求/任務在任務隊列中排隊等待執行,甚至會出現任務隊列滿了之後任務/請求無法處理的情況,或者大量任務堆積在任務隊列導致 OOM。這樣很明顯是有問題的! CPU 根本沒有得到充分利用。

但是,如果我們設置線程數量太大,大量線程可能會同時在爭取 CPU 資源,這樣會導致大量的上下文切換,從而增加線程的執行時間,影響了整體執行效率。

有一個簡單並且適用面比較廣的公式:

  • CPU 密集型任務(N+1): 這種任務消耗的主要是 CPU 資源,可以將線程數設置爲 N(CPU 核心數)+1,比 CPU 核心數多出來的一個線程是爲了防止線程偶發的缺頁中斷,或者其它原因導致的任務暫停而帶來的影響。一旦任務暫停,CPU 就會處於空閒狀態,而在這種情況下多出來的一個線程就可以充分利用 CPU 的空閒時間。
  • I/O 密集型任務(2N): 這種任務應用起來,系統會用大部分的時間來處理 I/O 交互,而線程在處理 I/O 的時間段內不會佔用 CPU 來處理,這時就可以將 CPU 交出給其它線程使用。因此在 I/O 密集型任務的應用中,我們可以多配置一些線程,具體的計算方法是 2N。

103. 樂觀鎖的缺點?

1 ABA 問題
如果一個變量V初次讀取的時候是A值,並且在準備賦值的時候檢查到它仍然是A值,那我們就能說明它的值沒有被其他線程修改過了嗎?很明顯是不能的,因爲在這段時間它的值可能被改爲其他值,然後又改回A,那CAS操作就會誤認爲它從來沒有被修改過。這個問題被稱爲CAS操作的 "ABA"問題。

JDK 1.5 以後的 AtomicStampedReference 類就提供了此種能力,其中的 compareAndSet 方法就是首先檢查當前引用是否等於預期引用,並且當前標誌是否等於預期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設置爲給定的更新值。

2 循環時間長開銷大
自旋CAS會給CPU帶來非常大的執行開銷。
如果JVM能支持處理器提供的pause指令那麼效率會有一定的提升,pause指令有兩個作用,第一它可以延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零。第二它可以避免在退出循環的時候因內存順序衝突(memory order violation)而引起CPU流水線被清空(CPU pipeline flush),從而提高CPU的執行效率。

3 只能保證一個共享變量的原子操作
CAS 只對單個共享變量有效,當操作涉及跨多個共享變量時 CAS 無效。但是從 JDK 1.5開始,提供了AtomicReference類來保證引用對象之間的原子性,你可以把多個變量放在一個對象裏來進行 CAS 操作。所以我們可以使用鎖或者利用AtomicReference類把多個共享變量合併成一個共享變量來操作。

104. 爲什麼要將永久代 (PermGen) 替換爲元空間 (MetaSpace) 呢?

  1. 整個永久代有一個 JVM 本身設置固定大小上限,無法進行調整,而元空間使用的是直接內存,受本機可用內存的限制
  2. 永久代回收的條件比較苛刻,所以容易發生溢出。雖然元空間仍舊可能溢出,但是比原來出現的機率會更小。
  3. 元空間裏面存放的是類的元數據,這樣加載多少類的元數據就不由 MaxPermSize 控制了, 而由系統的實際可用空間來控制,這樣能加載的類就更多了。
  4. 永久帶本身只是HotSpot實現的方法區,在 JDK8,合併 HotSpot 和 JRockit 的代碼時, JRockit 從來沒有一個叫永久代的東西, 合併之後就沒有必要額外的設置這麼一個永久代的地方了。

105. 描述Java 對象的創建過程?

  • Step1:類加載檢查

虛擬機遇到一條 new 指令時,首先將去檢查這個符號引用代表的類是否已被加載過、解析和初始化過。如果沒有,那必須先執行相應的類加載過程。


  • Step2:分配內存

在類加載檢查通過後,接下來虛擬機將爲新生對象分配內存。對象所需的內存大小在類加載完成後便可確定,爲對象分配空間的任務等同於把一塊確定大小的內存從 Java 堆中劃分出來


  • Step3:初始化零值

內存分配完成後,虛擬機需要將分配到的內存空間都初始化爲零值(不包括對象頭),這一步操作保證了對象的實例字段在 Java 代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。


  • Step4:設置對象頭

初始化零值完成之後,虛擬機要對對象進行必要的設置,例如這個對象是那個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的 GC 分代年齡等信息。 這些信息存放在對象頭中。 另外,根據虛擬機當前運行狀態的不同,如是否啓用偏向鎖等,對象頭會有不同的設置方式。

Hotspot 虛擬機的對象頭包括兩部分信息,第一部分用於存儲對象自身的運行時數據(哈希碼、GC 分代年齡、鎖狀態標誌等等),另一部分是類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是那個類的實例。


  • Step5:執行 init 方法
    在上面工作都完成之後,從虛擬機的視角來看,一個新的對象已經產生了,但從 Java 程序的視角來看,對象創建纔剛開始,<init>方法還沒有執行,所有的字段都還爲零。所以一般來說,執行 new 指令之後會接着執行 <init> 方法,把對象按照程序員的意願進行初始化,這樣一個真正可用的對象纔算完全產生出來。

106. Java分配內存的方式?

在創建對象的第二步就是分配對象的內存:

分配方式有 “指針碰撞” 和 “空閒列表” 兩種,選擇那種分配方式由 Java 堆是否規整決定,而 Java 堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定。

選擇以上兩種方式中的哪一種,取決於 Java 堆內存是否規整。而 Java 堆內存是否規整,取決於 GC 收集器的算法是"標記-清除",還是"標記-整理"(也稱作"標記-壓縮"),值得注意的是,複製算法內存也是規整的。

內存分配併發問題
在創建對象的時候有一個很重要的問題,就是線程安全,因爲在實際開發過程中,創建對象是很頻繁的事情,作爲虛擬機來說,必須要保證線程是安全的,通常來講,虛擬機採用兩種方式來保證線程安全:

  1. CAS+失敗重試: CAS 是樂觀鎖的一種實現方式。所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操作,如果因爲衝突失敗就重試,直到成功爲止。虛擬機採用 CAS 配上失敗重試的方式保證更新操作的原子性。
  2. TLAB: 爲每一個線程預先在 Eden 區分配一塊內存,JVM 在給線程中的對象分配內存時,首先在 TLAB 分配,當對象大於 TLAB 中的剩餘內存或 TLAB 的內存已用盡時,再採用上述的 CAS 進行內存分配。

107. 簡單說明 強/軟/弱/虛引用?

JDK1.2 之前,Java 中引用的定義很傳統:如果 reference 類型的數據存儲的數值代表的是另一塊內存的起始地址,就稱這塊內存代表一個引用。

JDK1.2 以後,Java 對引用的概念進行了擴充,將引用分爲強引用、軟引用、弱引用、虛引用四種(引用強度逐漸減弱)

  1. 強引用(StrongReference

以前我們使用的大部分引用實際上都是強引用,這是使用最普遍的引用。如果一個對象具有強引用,那就類似於必不可少的生活用品,垃圾回收器絕不會回收它。當內存空間不足,Java 虛擬機寧願拋出 OutOfMemoryError 錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足問題。

  1. 軟引用(SoftReference)

如果一個對象只具有軟引用,那就類似於可有可無的生活用品。如果內存空間足夠,垃圾回收器就不會回收它,如果內存空間不足了,就會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就可以被程序使用。軟引用可用來實現內存敏感的高速緩存

3.弱引用(WeakReference)

如果一個對象只具有弱引用,那就類似於可有可無的生活用品。弱引用與軟引用的區別在於:只具有弱引用的對象擁有更短暫的生命週期。在垃圾回收器線程掃描它所管轄的內存區域的過程中,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存不過,由於垃圾回收器是一個優先級很低的線程, 因此不一定會很快發現那些只具有弱引用的對象

  1. 虛引用(PhantomReference)

"虛引用"顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用並不會決定對象的生命週期。如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收。

虛引用主要用來跟蹤對象被垃圾回收的活動。

特別注意,在程序設計中一般很少使用弱引用與虛引用,使用軟引用的情況較多,這是因爲軟引用可以加速 JVM 對垃圾內存的回收速度,可以維護系統的運行安全,防止內存溢出(OutOfMemory)等問題的產生。

108. 爲什麼ThreadLocal要使用弱引用?

既然上面的問題我們可以得出,弱引用只要被GC掃描到,那麼就會被回收。那麼ThreadLocal是如何工作的呢?

我們注意下面這一段代碼:

public void ceateThreadLocal(){
	//此時local對創建的對象是強引用
	ThreadLocal<String> local=new ThreadLocal<>();
	//以一個鍵值對的形式<local,"123">//線程的成員屬性存入map
	local.set("123");
	...
}

這一段代碼,此時創建的對象ThreadLocal被兩個地方引用:

  1. local的強引用
  2. 鍵值對的弱引用(ThreadLocalMap的鍵就是ThreadLocal)

由於強引用的存在,那麼在棧幀還在Java虛擬機棧裏面的時候,ThreadLocal對象是不會被回收的,內存不夠了只會拋出OOM錯誤。

而當棧幀一旦出棧,方法就結束了,強引用也消失了,只剩一個弱飲用,那麼下一次GC掃描的時候,就會將這個弱引用回收。雖然GC掃描線程優先級低,還是有一定的輔助避免內存泄漏的作用;但是正由於它的優先級低,所以還是存在內存泄漏的風險的。

109. 如何判斷一個常量是廢棄常量?

假如在常量池中存在字符串 “abc”,如果當前沒有任何 String 對象引用該字符串常量的話,就說明常量 “abc” 就是廢棄常量,如果這時發生內存回收的話而且有必要的話,“abc” 就會被系統清理出常量池

110. 如何判斷一個類是無用的類?

  1. 該類所有的實例都已經被回收,也就是 Java 堆中不存在該類的任何實例。
  2. 加載該類的 ClassLoader 已經被回收。
  3. 該類對應的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

虛擬機可以對滿足上述 3 個條件的無用類進行回收,這裏說的僅僅是“可以”,而並不是和對象一樣不使用了就會必然被回收。

111. 一個類的生命週期?

加載->驗證->準備->解析->初始化->使用->卸載。

其中前五個步驟就是類加載的過程,在Java常見面試題彙總(一)說過:問題23:描述Java類加載機制?
最後一個卸載,就是問題110. 如何判斷一個類是無用的類?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章