天天看點

精選21道Java後端面試題,看完你也能唬住面試官拿30K{}是預編譯處理,${}是字元串替換。

微信公衆号:慕容千語的架構筆記。歡迎關注一起進步。

1. 如何用數組實作隊列?

用數組實作隊列時要注意 溢出 現象,這時我們可以采用循環數組的方式來解決,即将數組收尾相接。使用front指針指向隊列首位,tail指針指向隊列末位。

2. 内部類通路局部變量的時候,為什麼變量必須加上final修飾?

因為生命周期不同。局部變量在方法結束後就會被銷毀,但内部類對象并不一定,這樣就會導緻内部類引用了一個不存在的變量。

是以編譯器會在内部類中生成一個局部變量的拷貝,這個拷貝的生命周期和内部類對象相同,就不會出現上述問題。

但這樣就導緻了其中一個變量被修改,兩個變量值可能不同的問題。為了解決這個問題,編譯器就要求局部變量需要被final修飾,以保證兩個變量值相同。

在JDK8之後,編譯器不要求内部類通路的局部變量必須被final修飾,但局部變量值不能被修改(無論是方法中還是内部類中),否則會報編譯錯誤。利用javap檢視編譯後的位元組碼可以發現,編譯器已經加上了final。

3. long s = 499999999 * 499999999 在上面的代碼中,s的值是多少?

根據代碼的計算結果,s的值應該是-1371654655,這是由于Java中右側值的計算預設是int類型。

4. NIO相關,Channels、Buffers、Selectors

NIO(Non-blocking IO)為所有的原始類型提供(Buffer)緩存支援,字元集編碼解碼解決方案。 Channel :一個新的原始I/O 抽象。 支援鎖和記憶體映射檔案的檔案通路接口。提供多路(non-bloking) 非阻塞式的高伸縮性網絡I/O 。

IO NIO
面向流 面向緩沖
阻塞IO 非阻塞IO
選擇器

流與緩沖

Java NIO和IO之間第一個最大的差別是,IO是面向流的,NIO是面向緩沖區的。 Java IO面向流意味着每次從流中讀一個或多個位元組,直至讀取所有位元組,它們沒有被緩存在任何地方。此外,它不能前後移動流中的資料。如果需要前後移動從流中讀取的資料,需要先将它緩存到一個緩沖區。

Java NIO的緩沖導向方法略有不同。資料讀取到一個它稍後處理的緩沖區,需要時可在緩沖區中前後移動。這就增加了處理過程中的靈活性。但是,還需要檢查是否該緩沖區中包含所有您需要處理的資料。而且,需確定當更多的資料讀入緩沖區時,不要覆寫緩沖區裡尚未處理的資料。

阻塞與非阻塞IO

Java IO的各種流是阻塞的。這意味着,當一個線程調用read() 或 write()時,該線程被阻塞,直到有一些資料被讀取,或資料完全寫入。該線程在此期間不能再幹任何事情了。 Java NIO的非阻塞模式,是線程向某通道發送請求讀取資料,僅能得到目前可用的資料,如果目前沒有資料可用時,就什麼都不會擷取,當然它不會保持線程阻塞。是以直至資料變的可以讀取之前,該線程可以繼續做其他的事情。 非阻塞寫也是如此。是以一個單獨的線程現在可以管理多個輸入和輸出通道。

選擇器(Selectors)

Java NIO 的 選擇器允許一個單獨的線程來監視多個輸入通道,你可以注冊多個通道使用一個選擇器,然後使用一個單獨的線程來“選擇”通道:這些通道裡已經有可以處理的輸入,或者選擇已準備寫入的通道。這種選擇機制,使得一個單獨的線程很容易來管理多個通道。

5. 反射的用途

Java反射機制可以讓我們在編譯期(Compile Time)之外的運作期(Runtime)檢查類,接口,變量以及方法的資訊。反射還可以讓我們在運作期執行個體化對象,調用方法,通過調用get/set方法擷取變量的值。同時我們也可以通過反射來擷取泛型資訊,以及注解。還有更進階的應用–動态代理和動态類加載(ClassLoader.loadclass())。

下面列舉一些比較重要的方法:

  • getFields:擷取所有 public 的變量。
  • getDeclaredFields:擷取所有包括 private , protected 權限的變量。
  • setAccessible:設定為 true 可以跳過Java權限檢查,進而通路private權限的變量。
  • getAnnotations:擷取注解,可以用在類和方法上。

擷取方法的泛型參數:

method = Myclass.class.getMethod("setStringList", List.class);

Type[] genericParameterTypes = method.getGenericParameterTypes();

for(Type genericParameterType : genericParameterTypes){
    if(genericParameterType instanceof ParameterizedType){
        ParameterizedType aType = (ParameterizedType) genericParameterType;
        Type[] parameterArgTypes = aType.getActualTypeArguments();
        for(Type parameterArgType : parameterArgTypes){
            Class parameterArgClass = (Class) parameterArgType;
            System.out.println("parameterArgClass = " + parameterArgClass);
        }
    }
}           

動态代理:

//Main.java
public static void main(String[] args) {
    HelloWorld helloWorld=new HelloWorldImpl();
    InvocationHandler handler=new HelloWorldHandler(helloWorld);

    //建立動态代理對象
    HelloWorld proxy=(HelloWorld)Proxy.newProxyInstance(
            helloWorld.getClass().getClassLoader(),
            helloWorld.getClass().getInterfaces(),
            handler);
    proxy.sayHelloWorld();
}

//HelloWorldHandler.java
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    Object result = null;
    //調用之前
    doBefore();
    //調用原始對象的方法
    result=method.invoke(obj, args);
    //調用之後
    doAfter();
    return result;
}           

通過反射擷取方法注解的參數:

Class aClass = TheClass.class;
Annotation[] annotations = aClass.getAnnotations();

for(Annotation annotation : annotations){
   if(annotation instanceof MyAnnotation){
       MyAnnotation myAnnotation = (MyAnnotation) annotation;
       System.out.println("name: " + myAnnotation.name());
       System.out.println("value: " + myAnnotation.value());
   }
}           

非靜态内部類能定義靜态方法嗎?

public class OuterClass{
    private static float f = 1.0f;

    class InnerClass{
        public static float func(){return f;}
    }
}           

以上代碼會出現編譯錯誤,因為隻有靜态内部類才能定義靜态方法。

6. Lock 和 Synchronized 有什麼差別?

使用方法的差別

  • Synchronized:在需要同步的對象中加入此控制,synchronized可以加在方法上,也可以加在特定代碼塊中,括号中表示需要鎖的對象。
  • Lock:需要顯示指定起始位置和終止位置。一般使用ReentrantLock類做為鎖,多個線程中必須要使用一個ReentrantLock類做為對象才能保證鎖的生效。且在加鎖和解鎖處需要通過lock()和unlock()顯示指出。是以一般會在finally塊中寫unlock()以防死鎖。

性能的差別

synchronized是托管給JVM執行的,而lock是java寫的控制鎖的代碼。在Java1.5中,synchronize是性能低效的。因為這是一個重量級操作,需要調用操作接口,導緻有可能加鎖消耗的系統時間比加鎖以外的操作還多。相比之下使用Java提供的Lock對象,性能更高一些。但是到了Java1.6,發生了變化。synchronize在語義上很清晰,可以進行很多優化,有适應自旋,鎖消除,鎖粗化,輕量級鎖,偏向鎖等等。導緻在Java1.6上synchronize的性能并不比Lock差。

  • Synchronized:采用的是CPU悲觀鎖機制,即線程獲得的是獨占鎖。獨占鎖意味着 其他線程隻能依靠阻塞來等待線程釋放鎖。而在CPU轉換線程阻塞時會引起線程上下文切換,當有很多線程競争鎖的時候,會引起CPU頻繁的上下文切換導緻效率很低。
  • Lock:用的是樂觀鎖方式。所謂樂觀鎖就是,每次不加鎖而是假設沒有沖突而去完成某項操作,如果因為沖突失敗就重試,直到成功為止。樂觀鎖實作的機制就是CAS操作。我們可以進一步研究ReentrantLock的源代碼,會發現其中比較重要的獲得鎖的一個方法是compareAndSetState。這裡其實就是調用的CPU提供的特殊指令。

ReentrantLock:具有更好的可伸縮性:比如時間鎖等候、可中斷鎖等候、無塊結構鎖、多個條件變量或者鎖投票。

7. float 變量如何與 0 比較?

folat類型的還有double類型的,這些小數類型在趨近于0的時候直接等于0的可能性很小,一般都是無限趨近于0,是以不能用==來判斷。應該用|x-0|

//用程式表示就是

fabs(x) < 0.00001f           

8. 如何建立非靜态内部類?

内部類在聲明的時候必須是 Outer.Inner a,就像int a 一樣,至于靜态内部類和非靜态内部類new的時候有點差別:

  • Outer.Inner a = new Outer().new Inner()(非靜态,先有Outer對象才能 new 内部類)
  • Outer.Inner a = new Outer.Inner()(靜态内部類)

9. Java辨別符命名規則

可以包含:字母、數字、$、_(下劃線),不可用數字開頭,不能是 Java 的關鍵字和保留字。

11. 你知道哪些JDK中用到的設計模式?

  • 裝飾模式:java.io
  • 單例模式:Runtime類
  • 簡單工廠模式:Integer.valueOf方法
  • 享元模式:String常量池、Integer.valueOf(int i)、Character.valueOf(char c)
  • 疊代器模式:Iterator
  • 職責鍊模式:ClassLoader的雙親委派模型
  • 解釋器模式:正規表達式java.util.regex.Pattern

12. ConcurrentHashMap如何保證線程安全

JDK 1.7及以前:

ConcurrentHashMap允許多個修改操作并發進行,其關鍵在于使用了鎖分離技術。它使用了多個鎖來控制對hash表的不同部分進行的修改。ConcurrentHashMap内部使用段(Segment)來表示這些不同的部分,每個段其實就是一個小的hash table,它們有自己的鎖。隻要多個修改操作發生在不同的段上,它們就可以并發進行。

JDK 1.8:

Segment雖保留,但已經簡化屬性,僅僅是為了相容舊版本。

插入時使用CAS算法:unsafe.compareAndSwapInt(this, valueOffset, expect, update)。 CAS(Compare And Swap)意思是如果valueOffset位置包含的值與expect值相同,則更新valueOffset位置的值為update,并傳回true,否則不更新,傳回false。插入時不允許key或value為null

與Java8的HashMap有相通之處,底層依然由“數組”+連結清單+紅黑樹;

底層結構存放的是TreeBin對象,而不是TreeNode對象;

CAS作為知名無鎖算法,那ConcurrentHashMap就沒用鎖了麼?當然不是,當hash值與連結清單的頭結點相同還是會synchronized上鎖,鎖連結清單。

13. i++在多線程環境下是否存在問題,怎麼解決?

雖然遞增操作++i是一種緊湊的文法,使其看上去隻是一個操作,但這個操作并非原子的,因而它并不會作為一個不可分割的操作來執行。實際上,它包含了三個獨立的操作:讀取count的值,将值加1,然後将計算結果寫入count。這是一個“讀取 - 修改 - 寫入”的操作序列,并且其結果狀态依賴于之前的狀态。是以在多線程環境下存在問題。

要解決自增操作在多線程環境下線程不安全的問題,可以選擇使用Java提供的原子類,如AtomicInteger或者使用synchronized同步方法。

14. new與newInstance()的差別

new是一個關鍵字,它是調用new指令建立一個對象,然後調用構造方法來初始化這個對象,可以使用帶參數的構造器

newInstance()是Class的一個方法,在這個過程中,是先取了這個類的不帶參數的構造器Constructor,然後調用構造器的newInstance方法來建立對象。

Class.newInstance不能帶參數,如果要帶參數需要取得對應的構造器,然後調用該構造器的Constructor.newInstance(Object … initargs)方法

15. 你了解哪些JDK1.8的新特性?

  • 接口的預設方法和靜态方法,JDK8允許我們給接口添加一個非抽象的方法實作,隻需要使用default關鍵字即可。也可以定義被static修飾的靜态方法。
  • 對HashMap進行了改進,當單個桶的元素個數大于6時就會将實作改為紅黑樹實作,以避免構造重複的hashCode的攻擊
  • 多并發進行了優化。如ConcurrentHashMap實作由分段加鎖、鎖分離改為CAS實作。
  • JDK8拓寬了注解的應用場景,注解幾乎可以使用在任何元素上,并且允許在同一個地方多次使用同一個注解
  • Lambda表達式

16. 你用過哪些JVM參數?

  • Xms 堆最小值
  • Xmx 堆最大值
  • Xmn: 新生代容量
  • XX:SurvivorRatio 新生代中Eden與Surivor空間比例
  • Xss 棧容量
  • XX:PermSize 方法區初始容量
  • XX:MaxPermSize 方法區最大容量
  • XX:+PrintGCDetails 收集器日志參數

17. 如何打破 ClassLoader 雙親委托?

重寫loadClass()方法。

18. hashCode() && equals()

hashcode() 傳回該對象的哈希碼值,支援該方法是為哈希表提供一些優點,例如,java.util.Hashtable 提供的哈希表。

在 Java 應用程式執行期間,在同一對象上多次調用 hashCode 方法時,必須一緻地傳回相同的整數,前提是對象上 equals 比較中所用的資訊沒有被修改(equals預設傳回對象位址是否相等)。如果根據 equals(Object)方法,兩個對象是相等的,那麼在兩個對象中的每個對象上調用 hashCode 方法都必須生成相同的整數結果。

以下情況不是必需的:如果根據 equals(java.lang.Object) 方法,兩個對象不相等,那麼在兩個對象中的任一對象上調用 hashCode 方法必定會生成不同的整數結果。但是,程式員應該知道,為不相等的對象生成不同整數結果可以提高哈希表的性能。

實際上,由 Object 類定義的 hashCode 方法确實會針對不同的對象傳回不同的整數。(這一般是通過将該對象的内部位址轉換成一個整數來實作的,但是 JavaTM 程式設計語言不需要這種實作技巧I。)

hashCode的存在主要是用于查找的快捷性,如 Hashtable,HashMap等,hashCode 是用來在散列存儲結構中确定對象的存儲位址的;

如果兩個對象相同,就是适用于 equals(java.lang.Object) 方法,那麼這兩個對象的 hashCode 一定要相同;

如果對象的 equals 方法被重寫,那麼對象的 hashCode 也盡量重寫,并且産生 hashCode 使用的對象,一定要和 equals 方法中使用的一緻,否則就會違反上面提到的第2點;

兩個對象的hashCode相同,并不一定表示兩個對象就相同,也就是不一定适用于equals(java.lang.Object) 方法,隻能夠說明這兩個對象在散列存儲結構中,如Hashtable,他們“存放在同一個籃子裡”。

19. Thread.sleep() & Thread.yield()

sleep()和yield()都會釋放CPU。

sleep()使目前線程進入停滞狀态,是以執行sleep()的線程在指定的時間内肯定不會執行;yield()隻是使目前線程重新回到可執行狀态,是以執行yield()的線程有可能在進入到可執行狀态後馬上又被執行。

sleep()可使優先級低的線程得到執行的機會,當然也可以讓同優先級和高優先級的線程有執行的機會;yield()隻能使同優先級的線程有執行的機會。

20. #{}和${}的差別是什麼?

{}是預編譯處理,${}是字元串替換。

Mybatis在處理#{}時,會将sql中的#{}替換為?号,調用PreparedStatement的set方法來指派;

Mybatis在處理${}時,就是把${}替換成變量的值。

使用#{}可以有效的防止SQL注入,提高系統安全性。

21. 通常一個Xml映射檔案,都會寫一個Dao接口與之對應,請問,這個Dao接口的工作原理是什麼?Dao接口裡的方法,參數不同時,方法能重載嗎?

Dao接口,就是人們常說的Mapper接口,接口的全限名,就是映射檔案中的namespace的值,接口的方法名,就是映射檔案中MappedStatement的id值,接口方法内的參數,就是傳遞給sql的參數。Mapper接口是沒有實作類的,當調用接口方法時,接口全限名+方法名拼接字元串作為key值,可唯一定位一個MappedStatement,舉例:com.mybatis3.mappers.StudentDao.findStudentById,可以唯一找到namespace為com.mybatis3.mappers.StudentDao下面id = findStudentById的MappedStatement。在Mybatis中,每一個、、、标簽,都會被解析為一個MappedStatement對象。

Dao接口裡的方法,是不能重載的,因為是全限名+方法名的儲存和尋找政策。

Dao接口的工作原理是JDK動态代理,Mybatis運作時會使用JDK動态代理為Dao接口生成代理proxy對象,代理對象proxy會攔截接口方法,轉而執行MappedStatement所代表的sql,然後将sql執行結果傳回。

歡迎關注微信公衆号:慕容千語的架構筆記 一起學習提升