【Java】幾道常見的秋招面試題

Java3y發表於2018-10-23

前言

只有光頭才能變強

Redis目前還在看,今天來分享一下我在秋招看過(遇到)的一些面試題(相對比較常見的)

0、final關鍵字

簡要說一下final關鍵字,final可以用來修飾什麼?

這題我是在真實的面試中遇到的,當時答得不太好,現在來整理一下吧。

final可以修飾類、方法、成員變數

  • 當final修飾類的時候,說明該類不能被繼承
  • 當final修飾方法的時候,說明該方法不能被重寫
    • 在早期,可能使用final修飾的方法,編譯器針對這些方法的所有呼叫都轉成內嵌呼叫,這樣提高效率(但到現在一般我們不會去管這事了,編譯器和JVM都越來越聰明瞭)
  • 當final修飾成員變數時,有兩種情況:
    • 如果修飾的是基本型別,說明這個變數的所代表數值永不能變(不能重新賦值)!
    • 如果修飾的是引用型別,該變數所的引用不能變,但引用所代表的物件內容是可變的!

值得一說的是:並不是被final修飾的成員變數就一定是編譯期常量了。比如說我們可以寫出這樣的程式碼:private final int java3y = new Randon().nextInt(20);

你有沒有這樣的程式設計經驗,在編譯器寫程式碼時,某個場景下一定要將變數宣告為final,否則會出現編譯不通過的情況。為什麼要這樣設計?

在編寫匿名內部類的時候就可能會出現這種情況,匿名內部類可能會使用到的變數:

  • 外部類例項變數
  • 方法或作用域內的區域性變數
  • 方法的引數

class Outer {


    // string:外部類的例項變數
    String string = "";


    //ch:方法的引數
    void outerTest(final char ch) {

        // integer:方法內區域性變數
        final Integer integer = 1;
        new Inner() {
            void innerTest() {
                System.out.println(string);
                System.out.println(ch);
                System.out.println(integer);
            }
        };

    }
    public static void main(String[] args) {
        new Outer().outerTest(' ');
    }
    class Inner {
    }
}

複製程式碼

其中我們可以看到:方法或作用域內的區域性變數和方法引數都要顯示使用final關鍵字來修飾(在jdk1.7下)!

【Java】幾道常見的秋招面試題

如果切換到jdk1.8編譯環境下,可以通過編譯的~

【Java】幾道常見的秋招面試題

下面我們首先來說一下顯示宣告為final的原因:為了保持內部外部資料一致性

  • Java只是實現了capture-by-value形式的閉包,也就是匿名函式內部會重新拷貝一份自由變數,然後函式外部和函式內部就有兩份資料。
  • 要想實現內部外部資料一致性目的,只能要求兩處變數不變。JDK8之前要求使用final修飾,JDK8聰明些了,可以使用effectively final的方式

為什麼僅僅針對方法中的引數限制final,而訪問外部類的屬性就可以隨意

內部類中是儲存著一個指向外部類例項的引用,內部類訪問外部類的成員變數都是通過這個引用。

  • 在內部類修改了這個引用的資料,外部類獲取時拿到的資料是一致的!

那當你在匿名內部類裡面嘗試改變外部基本型別的變數的值的時候,或者改變外部引用變數的指向的時候,表面上看起來好像都成功了,但實際上並不會影響到外部的變數。所以,Java為了不讓自己看起來那麼奇怪,才加了這個final的限制。

參考資料:

一、char和varchar的區別

  1. char是固定長度,varchar長度可變。varchar:如果原先儲存的位置無法滿足其儲存的需求,就需要一些額外的操作,根據儲存引擎的不同,有的會採用拆分機制,有的採用分頁機制
  2. char和varchar的儲存位元組由具體的字符集來決定(之前寫錯了);
  3. char是固定長度,長度不夠的情況下,用空格代替。varchar表示的是實際長度的資料型別

選用考量:

  • 如果欄位長度較和字元間長度相近甚至是相同的長度,會採用char字元型別

二、多個執行緒順序列印問題

三個執行緒分別列印A,B,C,要求這三個執行緒一起執行,列印n次,輸出形如“ABCABCABC....”的字串。

原博主給出了4種方式,我認為訊號量這種方式比較簡單和容易理解,我這裡貼上一下(具體的可到原博主下學習)..


public class PrintABCUsingSemaphore {
    private int times;
    private Semaphore semaphoreA = new Semaphore(1);
    private Semaphore semaphoreB = new Semaphore(0);
    private Semaphore semaphoreC = new Semaphore(0);

    public PrintABCUsingSemaphore(int times) {
        this.times = times;
    }

    public static void main(String[] args) {
        PrintABCUsingSemaphore printABC = new PrintABCUsingSemaphore(10);

        // 非靜態方法引用  x::toString   和() -> x.toString() 是等價的!
        new Thread(printABC::printA).start();
        new Thread(printABC::printB).start();
        new Thread(printABC::printC).start();

        /*new Thread(() -> printABC.printA()).start();
        new Thread(() -> printABC.printB()).start();
        new Thread(() -> printABC.printC()).start();
*/
    }

    public void printA() {
        try {
            print("A", semaphoreA, semaphoreB);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void printB() {
        try {
            print("B", semaphoreB, semaphoreC);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void printC() {
        try {
            print("C", semaphoreC, semaphoreA);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private void print(String name, Semaphore current, Semaphore next)
            throws InterruptedException {
        for (int i = 0; i < times; i++) {
            current.acquire();
            System.out.print(name);
            next.release();
        }
    }
}


複製程式碼

2018年9月14日18:15:36 yy筆試題就出了..

三、生產者和消費者

在不少的面經都能看到它的身影哈~~~基本都是要求能夠手寫程式碼的。

其實邏輯並不難,概括起來就兩句話:

  • 如果生產者的佇列滿了(while迴圈判斷是否滿),則等待。如果生產者的佇列沒滿,則生產資料並喚醒消費者進行消費。
  • 如果消費者的佇列空了(while迴圈判斷是否空),則等待。如果消費者的佇列沒空,則消費資料並喚醒生產者進行生產。

基於原作者的程式碼,我修改了部分並給上我認為合適的註釋(下面附上了原作者出處,感興趣的同學可到原文學習)

生產者:


import java.util.Random;
import java.util.Vector;
import java.util.concurrent.atomic.AtomicInteger;

public class Producer implements Runnable {

    // true--->生產者一直執行,false--->停掉生產者
    private volatile boolean isRunning = true;

    // 公共資源
    private final Vector sharedQueue;

    // 公共資源的最大數量
    private final int SIZE;

    // 生產資料
    private static AtomicInteger count = new AtomicInteger();

    public Producer(Vector sharedQueue, int SIZE) {
        this.sharedQueue = sharedQueue;
        this.SIZE = SIZE;
    }

    @Override
    public void run() {
        int data;
        Random r = new Random();

        System.out.println("start producer id = " + Thread.currentThread().getId());
        try {
            while (isRunning) {
                // 模擬延遲
                Thread.sleep(r.nextInt(1000));

                // 當佇列滿時阻塞等待
                while (sharedQueue.size() == SIZE) {
                    synchronized (sharedQueue) {
                        System.out.println("Queue is full, producer " + Thread.currentThread().getId()
                                + " is waiting, size:" + sharedQueue.size());
                        sharedQueue.wait();
                    }
                }

                // 佇列不滿時持續創造新元素
                synchronized (sharedQueue) {
                    // 生產資料
                    data = count.incrementAndGet();
                    sharedQueue.add(data);

                    System.out.println("producer create data:" + data + ", size:" + sharedQueue.size());
                    sharedQueue.notifyAll();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
            Thread.currentThread().interrupted();
        }
    }

    public void stop() {
        isRunning = false;
    }
}
複製程式碼

消費者:


import java.util.Random;
import java.util.Vector;

public class Consumer implements Runnable {

    // 公共資源
    private final Vector sharedQueue;

    public Consumer(Vector sharedQueue) {
        this.sharedQueue = sharedQueue;
    }

    @Override
    public void run() {

        Random r = new Random();

        System.out.println("start consumer id = " + Thread.currentThread().getId());
        try {
            while (true) {
                // 模擬延遲
                Thread.sleep(r.nextInt(1000));

                // 當佇列空時阻塞等待
                while (sharedQueue.isEmpty()) {
                    synchronized (sharedQueue) {
                        System.out.println("Queue is empty, consumer " + Thread.currentThread().getId()
                                + " is waiting, size:" + sharedQueue.size());
                        sharedQueue.wait();
                    }
                }
                // 佇列不空時持續消費元素
                synchronized (sharedQueue) {
                    System.out.println("consumer consume data:" + sharedQueue.remove(0) + ", size:" + sharedQueue.size());
                    sharedQueue.notifyAll();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
            Thread.currentThread().interrupt();
        }
    }
}
複製程式碼

Main方法測試:


import java.util.Vector;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Test2 {


    public static void main(String[] args) throws InterruptedException {

        // 1.構建記憶體緩衝區
        Vector sharedQueue = new Vector();
        int size = 4;

        // 2.建立執行緒池和執行緒
        ExecutorService service = Executors.newCachedThreadPool();
        Producer prodThread1 = new Producer(sharedQueue, size);
        Producer prodThread2 = new Producer(sharedQueue, size);
        Producer prodThread3 = new Producer(sharedQueue, size);
        Consumer consThread1 = new Consumer(sharedQueue);
        Consumer consThread2 = new Consumer(sharedQueue);
        Consumer consThread3 = new Consumer(sharedQueue);
        service.execute(prodThread1);
        service.execute(prodThread2);
        service.execute(prodThread3);
        service.execute(consThread1);
        service.execute(consThread2);
        service.execute(consThread3);

        // 3.睡一會兒然後嘗試停止生產者(結束迴圈)
        Thread.sleep(10 * 1000);
        prodThread1.stop();
        prodThread2.stop();
        prodThread3.stop();

        // 4.再睡一會兒關閉執行緒池
        Thread.sleep(3000);

        // 5.shutdown()等待任務執行完才中斷執行緒(因為消費者一直在執行的,所以會發現程式無法結束)
        service.shutdown();


    }
}
複製程式碼

另外,上面原文中也說了可以使用阻塞佇列來實現消費者和生產者。這就不用我們手動去寫wait/notify的程式碼了,會簡單一丟丟。可以參考:

四、演算法[1]

我現在需要實現一個棧,這個棧除了可以進行普通的push、pop操作以外,還可以進行getMin的操作,getMin方法被呼叫後,會返回當前棧的最小值,你會怎麼做呢?你可以假設棧裡面存的都是int整數

解決方案:

  • 使用一個min變數來記住最小值,每次push的時候,看看是否需要更新min。
    • 如果被pop出去的是min,第二次pop的時候,只能遍歷一下棧內元素,重新找到最小值。
    • 總結:pop的時間複雜度是O(n),push是O(1),空間是O(1)
  • 使用輔助棧來儲存最小值。如果當前要push的值比輔助棧的min值要小,那在輔助棧push的值是最小值
    • 總結:push和pop的時間複雜度都是O(1),空間是O(n)。典型以空間換時間的例子。

import java.util.ArrayList;
import java.util.List;

public class MinStack {

    private List<Integer> data = new ArrayList<Integer>();
    private List<Integer> mins = new ArrayList<Integer>();

    public void push(int num) {
        data.add(num);
        if (mins.size() == 0) {
            // 初始化mins
            mins.add(num);
        } else {
            // 輔助棧mins每次push當時最小值
            int min = getMin();
            if (num >= min) {
                mins.add(min);
            } else {
                mins.add(num);
            }
        }
    }

    public int pop() {
        // 棧空,異常,返回-1
        if (data.size() == 0) {
            return -1;
        }
        // pop時兩棧同步pop
        mins.remove(mins.size() - 1);
        return data.remove(data.size() - 1);
    }

    public int getMin() {
        // 棧空,異常,返回-1
        if (mins.size() == 0) {
            return -1;
        }
        // 返回mins棧頂元素
        return mins.get(mins.size() - 1);
    }

}
複製程式碼

繼續優化:

  • 棧為空的時候,返回-1很可能會帶來歧義(萬一人家push進去的值就有-1呢?),這邊我們可以使用Java Exception來進行優化
  • 演算法的空間優化:上面的程式碼我們可以發現:data棧和mins棧的元素個數總是相等的,mins棧中儲存幾乎都是最小的值(此部分是重複的!)
    • 所以我們可以這樣做:當push的時候,如果比min棧的值要小的,才放進mins棧。同理,當pop的時候,如果pop的值是mins的最小值,mins才出棧,否則mins不出棧!
    • 上述做法可以一定避免mins輔助棧有相同的元素!

但是,如果一直push的值是最小值,那我們的mins輔助棧還是會有大量的重複元素,此時我們可以使用索引(mins輔助棧儲存的是最小值索引,非具體的值)!

最終程式碼:


import java.util.ArrayList;
import java.util.List;


public class MinStack {

    private List<Integer> data = new ArrayList<Integer>();
    private List<Integer> mins = new ArrayList<Integer>();

    public void push(int num) throws Exception {
        data.add(num);
        if(mins.size() == 0) {
            // 初始化mins
            mins.add(0);
        } else {
            // 輔助棧mins push最小值的索引
            int min = getMin();
            if (num < min) {
                mins.add(data.size() - 1);
            }
        }
    }

    public int pop() throws Exception {
        // 棧空,丟擲異常
        if(data.size() == 0) {
            throw new Exception("棧為空");
        }
        // pop時先獲取索引
        int popIndex = data.size() - 1;
        // 獲取mins棧頂元素,它是最小值索引
        int minIndex = mins.get(mins.size() - 1);
        // 如果pop出去的索引就是最小值索引,mins才出棧
        if(popIndex == minIndex) {
            mins.remove(mins.size() - 1);
        }
        return data.remove(data.size() - 1);
    }

    public int getMin() throws Exception {
        // 棧空,丟擲異常
        if(data.size() == 0) {
            throw new Exception("棧為空");
        }
        // 獲取mins棧頂元素,它是最小值索引
        int minIndex = mins.get(mins.size() - 1);
        return data.get(minIndex);
    }

}
複製程式碼

參考資料:

五、多執行緒下的HashMap

眾所周知,HashMap不是一個執行緒安全的類。但有可能在面試的時候會被問到:如果在多執行緒環境下使用HashMap會有什麼現象發生呢??

結論:

  • put()的時候導致的多執行緒資料不一致(丟失資料)
  • resize()操作會導致環形連結串列
    • jdk1.8已解決環鏈的問題(宣告兩對指標,維護兩個連連結串列)
  • fail-fast機制,對當前HashMap同時進行刪除/修改會丟擲ConcurrentModificationException異常

參考資料:

六、Spring和Springboot區別

一、SpringBoot是能夠建立出獨立的Spring應用程式的

二、簡化Spring配置

  • Spring由於其繁瑣的配置,一度被人成為“配置地獄”,各種XML、Annotation配置,讓人眼花繚亂,而且如果出錯了也很難找出原因。
  • Spring Boot專案就是為了解決配置繁瑣的問題,最大化的實現convention over configuration(約定大於配置)。
    • 提供一系列的依賴包來把其它一些工作做成開箱即用其內建一個’Starter POM’,對專案構建進行了高度封裝,最大化簡化專案構建的配置。

三、嵌入式Tomcat,Jetty容器,無需部署WAR包

七、G1和CMS

G1收集器的設計目標是取代CMS收集器,它同CMS相比,在以下方面表現的更出色:

  • G1是一個有整理記憶體過程的垃圾收集器,不會產生很多記憶體碎片
    • CMS採用的是標記清除垃圾回收演算法,可能會產生不少的記憶體碎片
  • G1的Stop The World(STW)更可控,G1在停頓時間上新增了預測機制,使用者可以指定期望停頓時間

擴充閱讀:

八、海量資料解決方案

海量資料的處理也是一個經常考的知識點,無論在面試還是在筆試中都是比較常見的。有幸讀了下面的文章,摘錄了一些解決海量資料的思路:

  • Bloom filter布隆過濾器
    • 適用範圍:可以用來實現資料字典,進行資料的判重,或者集合求交集
  • Hashing
    • 適用範圍:快速查詢,刪除的基本資料結構,通常需要總資料量可以放入記憶體
  • bit-map
    • 適用範圍:可進行資料的快速查詢,判重,刪除,一般來說資料範圍是int的10倍以下
    • 適用範圍:海量資料前n大,並且n比較小,堆可以放入記憶體
  • 雙層桶劃分----其實本質上就是【分而治之】的思想,重在“分”的技巧上!
    • 適用範圍:第k大,中位數,不重複或重複的數字
  • 資料庫索引
    • 適用範圍:大資料量的增刪改查
  • 倒排索引(Inverted index)
    • 適用範圍:搜尋引擎,關鍵字查詢
  • 外排序
    • 適用範圍:大資料的排序,去重
  • trie樹
    • 適用範圍:資料量大,重複多,但是資料種類小可以放入記憶體
  • 分散式處理 mapreduce
    • 適用範圍:資料量大,但是資料種類小可以放入記憶體

詳細可參考原文:

九、冪等性

9.1HTTP冪等性

昨天去做了一套筆試題,經典的HTTP中get/post的區別。今天回來搜了一下,發現跟之前的理解有點出入

如果一個人一開始就做Web開發,很可能把HTML對HTTP協議的使用方式,當成HTTP協議的唯一的合理使用方式。從而犯了以偏概全的錯誤

單純以HTTP協議規範來說,可能我們之前總結出的GET/POST區別就沒用了。(但通讀完整篇文章,我個人認為:如果面試中有GET/POST區別,還是預設以Web開發場景下來回答較好,這也許是面試官想要的答案)

參考資料:


其中也學習到了冪等性這麼一個概念,於是也做做筆記吧~~~

Methods can also have the property of “idempotence” in that (aside from error or expiration issues) the side-effects of N > 0 identical requests is the same as for a single request.

從定義上看,HTTP方法的冪等性是指一次和多次請求某一個資源應該具有同樣的副作用

  • 這裡簡單說一下“副作用”的意思:指當你傳送完一個請求以後,網站上的資源狀態沒有發生修改,即認為這個請求是無副作用的

HTTP的GET/POST/DELETE/PUT方法冪等的情況:

  • GET是冪等的,無副作用
    • 比如我想要獲得訂單ID為2的訂單:http://localhost/order/2,使用GET多次獲取,這個ID為2的訂單(資源)是不會發生變化的!
  • DELETE/PUT是冪等的,有副作用
    • 比如我想要刪除或者更新ID為2的訂單:http://localhost/order/2,使用PUT/DELETE多次請求,這個ID為2的訂單(資源)只會發生一次變化(是有副作用的)!但繼續多次重新整理請求,訂單ID為2的最終狀態都是一致的
  • POST是非冪等的,有副作用的
    • 比如我想要建立一個名稱叫3y的訂單:http://localhost/order,使用POST多次請求,此時可能就會建立多個名稱為3y的訂單,這個訂單(資源)是會多次變化的,每次請求的資源狀態都會變化

題外話:

HTTP協議本身是一種面向資源的應用層協議,但對HTTP協議的使用實際上存在著兩種不同的方式:一種是RESTful的,它把HTTP當成應用層協議,比較忠實地遵守了HTTP協議的各種規定(充分利用了HTTP的方法);另一種是SOA的,它並沒有完全把HTTP當成應用層協議,而是把HTTP協議作為了傳輸層協議,然後在HTTP之上建立了自己的應用層協議

參考資料:

9.2介面冪等性

在查閱資料的時候,可以發現很多部落格都講了介面的冪等性。從上面我們也可以看出,POST方法是非冪等的。但我們可以通過一些手段來令POST方法的介面變成是冪等的。

說了那麼多,那介面設計成冪等的好處是什麼????

舉個例子說一下非冪等的壞處:

  • 3y大一的時候是要搶體育課的,但學校的搶課系統做得賊爛(延遲很高)。我想要搶到課,就開了10多個Chrome標籤頁去搶(即使某個Chrome標籤頁崩了,我還有另外的Chrome標籤頁是可用的)。我想搶到乒乓球或者羽毛球。
  • 搶課時間一到,我就輪著點選我要想搶的乒乓球或者羽毛球。如果系統設計得不好,這個請求是非冪等的(或者說事務控制得不好),我手速足夠快&&網路足夠好,那我很可能搶到了多次乒乓球或者羽毛球的課程了。(這是不合理的,一個人只能選一門課,而我搶到了多門或者多次重複的課)
  • 涉及到商城的應用場景可能就是:使用者下了多個重複的訂單了

如果我的搶課介面是冪等的話,那就不會出現這個問題了。因為冪等是多次請求某一個資源應該具有同樣的副作用。

  • 在資料庫後臺最多隻會有一條記錄,不存在搶到多門課的現象了。

說白了,設計冪等性介面就是為了防止重複提交的(資料庫出現多條重複的資料)!

網上有博主也分享了幾條常見解決重複提交的方案:

  1. 同步鎖(單執行緒,在叢集可能會失效)
  2. 分散式鎖如redis(實現複雜)
  3. 業務欄位加唯一約束(簡單)
  4. 令牌表+唯一約束(簡單推薦)---->實現冪等介面的一種手段
  5. mysql的insert ignore或者on duplicate key update(簡單)
  6. 共享鎖+普通索引(簡單)
  7. 利用MQ或者Redis擴充套件(排隊)
  8. 其他方案如多版本控制MVCC 樂觀鎖 悲觀鎖 狀態機等。。

參考資料:

最後

如果以上有理解錯的地方,或者說有更好的理解方式,希望大家不吝在評論區下留言。共同進步!

如果想看更多的原創技術文章,歡迎大家關注我的微信公眾號:Java3y。公眾號還有海量的視訊資源哦,關注即可免費領取。

可能感興趣的連結:

相關文章