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

前言

只有光頭才能變強

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

0、final關鍵字

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

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

final能夠修飾類、方法、成員變量mysql

  • 當final修飾類的時候,說明該類不能被繼承
  • 當final修飾方法的時候,說明該方法不能被重寫面試

    • 在早期,可能使用final修飾的方法,編譯器針對這些方法的全部調用都轉成內嵌調用,這樣提升效率(但到如今通常咱們不會去管這事了,編譯器和JVM都愈來愈聰明瞭)
  • 當final修飾成員變量時,有兩種狀況:redis

    • 若是修飾的是基本類型,說明這個變量的所表明數值永不能變(不能從新賦值)!
    • 若是修飾的是引用類型,該變量所的引用不能變,但引用所表明的對象內容是可變的!

值得一說的是:並非被final修飾的成員變量就必定是編譯期常量了。好比說咱們能夠寫出這樣的代碼:private final int java3y = new Randon().nextInt(20);算法

你有沒有這樣的編程經驗,在編譯器寫代碼時,某個場景下必定要將變量聲明爲final,不然會出現編譯不經過的狀況。爲何要這樣設計?

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

  • 外部類實例變量
  • 方法或做用域內的局部變量
  • 方法的參數
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下)!數據庫

若是切換到jdk1.8編譯環境下,能夠經過編譯的~編程

下面咱們首先來講一下顯示聲明爲final的緣由:爲了保持內部外部數據一致性api

  • Java只是實現了capture-by-value形式的閉包,也就是匿名函數內部會從新拷貝一份自由變量,而後函數外部和函數內部就有兩份數據。
  • 要想實現內部外部數據一致性目的,只能要求兩處變量不變。JDK8以前要求使用final修飾,JDK8聰明些了,可使用effectively final的方式
爲何僅僅針對方法中的參數限制final,而訪問外部類的屬性就能夠隨意

內部類中是保存着一個指向外部類實例的引用,內部類訪問外部類的成員變量都是經過這個引用。

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

那當你在匿名內部類裏面嘗試改變外部基本類型的變量的值的時候,或者改變外部引用變量的指向的時候,表面上看起來好像都成功了,但實際上並不會影響到外部的變量。因此,Java爲了避免讓本身看起來那麼奇怪,才加了這個final的限制。

參考資料:

1、char和varchar的區別

  1. char是固定長度,varchar長度可變。varchar:若是原先存儲的位置沒法知足其存儲的需求,就須要一些額外的操做,根據存儲引擎的不一樣,有的會採用拆分機制,有的採用分頁機制
  2. char和varchar的存儲字節由具體的字符集來決定(以前寫錯了);
  3. char是固定長度,長度不夠的狀況下,用空格代替。varchar表示的是實際長度的數據類型

選用考量:

  • 若是字段長度較和字符間長度相近甚至是相同的長度,會採用char字符類型

2、多個線程順序打印問題

三個線程分別打印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筆試題就出了..

3、生產者和消費者

在很多的面經都能看到它的身影哈~~~基本都是要求可以手寫代碼的。

其實邏輯並不難,歸納起來就兩句話:

  • 從生產者角度:若是公共隊列滿了(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的代碼了,會簡單一丟丟。能夠參考:

4、算法[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);
    }

}

參考資料:

5、多線程下的HashMap

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

結論:

  • put()的時候致使的多線程數據不一致(丟失數據)
  • resize()操做會致使環形鏈表

    • jdk1.8已解決環鏈的問題(聲明兩對指針,維護兩個連鏈表)
  • fail-fast機制,對當前HashMap同時進行刪除/修改會拋出ConcurrentModificationException異常

參考資料:

6、Spring和Springboot區別

1、SpringBoot是可以建立出獨立的Spring應用程序的
2、簡化Spring配置
  • Spring因爲其繁瑣的配置,一度被人成爲「配置地獄」,各類XML、Annotation配置,讓人眼花繚亂,並且若是出錯了也很難找出緣由。
  • Spring Boot項目就是爲了解決配置繁瑣的問題,最大化的實現convention over configuration(約定大於配置)。

    • 提供一系列的依賴包來把其它一些工做作成開箱即用其內置一個’Starter POM’,對項目構建進行了高度封裝,最大化簡化項目構建的配置。
3、嵌入式Tomcat,Jetty容器,無需部署WAR包

7、G1和CMS

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

  • G1是一個有整理內存過程的垃圾收集器,不會產生不少內存碎片

    • CMS採用的是標記清除垃圾回收算法,可能會產生很多的內存碎片
  • G1的Stop The World(STW)更可控,G1在停頓時間上添加了預測機制,用戶能夠指按期望停頓時間

拓展閱讀:

8、海量數據解決方案

海量數據的處理也是一個常常考的知識點,不管在面試仍是在筆試中都是比較常見的。有幸讀了下面的文章,摘錄了一些解決海量數據的思路:

  • Bloom filter布隆過濾器

    • 適用範圍:能夠用來實現數據字典,進行數據的判重,或者集合求交集
  • Hashing

    • 適用範圍:快速查找,刪除的基本數據結構,一般須要總數據量能夠放入內存
  • bit-map

    • 適用範圍:可進行數據的快速查找,判重,刪除,通常來講數據範圍是int的10倍如下
    • 適用範圍:海量數據前n大,而且n比較小,堆能夠放入內存
  • 雙層桶劃分----其實本質上就是【分而治之】的思想,重在「分」的技巧上!

    • 適用範圍:第k大,中位數,不重複或重複的數字
  • 數據庫索引

    • 適用範圍:大數據量的增刪改查
  • 倒排索引(Inverted index)

    • 適用範圍:搜索引擎,關鍵字查詢
  • 外排序

    • 適用範圍:大數據的排序,去重
  • trie樹

    • 適用範圍:數據量大,重複多,可是數據種類小能夠放入內存
  • 分佈式處理 mapreduce

    • 適用範圍:數據量大,可是數據種類小能夠放入內存

詳細可參考原文:

9、冪等性

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。Java技術 羣討論:742919422。公衆號還有 海量的視頻資源哦,關注便可免費領取。
相關文章
相關標籤/搜索