本篇文章帶給大家的內容是常見的幾道秋招java面試題分析。有一定的參考價值,有需要的朋友可以參考一下,希望對你們有幫助。
前言
只有光頭才能變強
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下)!
如果切換到jdk1.8編譯環境下,可以透過編譯的~
下面我們先來說一下顯示宣告為final的原因:為了保持內部外部資料一致性
Java只是實作了capture-by-value形式的閉包,也就是匿名函數內部會重新拷貝一份自由變數,然後函數外部和函數內部就有兩份資料。
要實現內部外部資料一致性目的,只能要求兩處變數不變。 JDK8之前要求使用final修飾,JDK8聰明了,可以使用effectively final的方式
為什麼僅僅針對方法中的參數限制final,而訪問外部類別的屬性就可以隨意
內部類別中是保存著一個指向外部類別實例的參考,內部類別存取外部類別的成員變數都是透過這個引用。
在內部類別修改了這個引用的數據,外部類別再取得時拿到的數據是一致的!
那當你在匿名內部類別裡面嘗試改變外部基本類型的變數的值的時候,或是改變外部引用變數的指向的時候,表面上看起來好像都成功了,但其實不會影響到外部的變數。所以,Java為了不讓自己看起來那麼奇怪,才增加了這個final的限制。
參考資料:
java為什麼匿名內部類別的參數引用時final? https://www.zhihu.com/question/21395848
一、char與varchar的差異
char是固定長度,varchar長度可變。 varchar:如果原先儲存的位置無法滿足其儲存的需求,就需要一些額外的操作,根據儲存引擎的不同,有的會採用分割機制,有的採用分頁機制。
char的存储方式是:英文字符占1个字节,汉字占用2个字节;varchar的存储方式是:英文和汉字都占用2个字节,两者的存储数据都非unicode的字符数据。
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(); } } }
作者:cheergoivan
链接:https://www.jianshu.com/p/40078ed436b4
來源:简书
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(); } }
作者:我没有三颗心脏
链接:https://www.jianshu.com/p/3f0cd7af370d
來源:简书
另外,上面原文中也说了可以使用阻塞队列来实现消费者和生产者。这就不用我们手动去写wait/notify
的代码了,会简单一丢丢。可以参考:
使用阻塞队列解决生产者-消费者问题:https://www.cnblogs.com/chenpi/p/5553325.html
四、算法[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); } }
参考资料:
【面試現場】如何實現可以取得最小值的堆疊?
作者:channingbreeze 來源: 互聯網偵察
五、多執行緒下的HashMap
#眾所周知,HashMap不是一個執行緒安全的類別。但有可能在面試的時候會被問到:如果在多執行緒環境下使用HashMap會有什麼現象發生呢? ?
結論:
put()
的時候導致的多執行緒資料不一致(遺失資料)
#resize()
操作會導致環形鍊錶
#jdk1.8已解決環鏈的問題(宣告兩對指針,維護兩個連鍊錶)
fail-fast機制,同時對目前HashMap同時進行刪除/修改會拋出ConcurrentModificationException例外
#參考資料:
談談HashMap執行緒不安全的體現:http://www.importnew.com/22011.html
jdk1.8 hashmap多執行緒put不會造成死迴圈:https://blog.csdn.net/qq_27007251/article/details/71403647
#f六、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在停頓時間上增加了預測機制,使用者可以指定期望停頓時間。
拓展閱讀:
G1 垃圾收集器介紹:https://javadoop.com/post/g1
八、海量資料解決方案
海量資料的處理也是一個經常考的知識點,無論在面試或筆試中都是比較常見的。有幸讀了下面的文章,摘錄了一些解決海量資料的想法:
Bloom filter布隆過濾器
適用範圍:可以用來實作資料字典,進行資料的判重,或是集合求交集
Hashing
適用範圍:資料量大,重複多,但是資料種類小可以放入記憶體
分散式處理mapreduce
適用範圍:資料量大,但是資料種類小可以放入記憶體
詳細可參考原文:
十道大量資料處理面試題目與十個方法大總結:https://blog.csdn.net/v_JULY_v/article/details/6279498
#九、冪等性
9.1HTTP冪等性
昨天去做了一套筆試題,經典的HTTP中get/post
的差別。今天回來搜了一下,發現跟之前的理解有點出入。
如果一個人一開始就做Web開發,很可能把HTML對HTTP協定的使用方式,當成HTTP協定的唯一合理的使用方式。從而犯了以偏概全的錯誤
單純以HTTP協定規範來說,可能我們之前總結出的GET/POST
差異就沒用了。 (但通讀完整篇文章,我個人認為:如果面試中有GET/POST
區別,還是預設以Web開發場景下來回答較好,這也許是面試官想要的答案)
參考資料:
GET和POST有什麼不同?及為什麼網路上的多數答案都是錯的。 http://www.cnblogs.com/nankezhishi/archive/2012/06/09/getandpost.html
其中也學習到了冪等性這麼一個概念,所以也做做筆記吧~~~
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之上建立了自己的應用層協定
參考資料:
#HTTP冪等性http://www.cnblogs.com/weidagang2046/archive/2011/06/04/2063696. html#!comments
如何理解RESTful的冪等性http://blog.720ui.com/2016/restful_idempotent/
#淺談HTTP中Get與Post的區別http://www.cnblogs.com/hyddd/archive/2009/03/31/1426026.html
HTTP 請求中POST 和GET 請求的區別? https://www.zhihu.com/question/27622127/answer/37676304
9.2介面冪等性
#查閱資料的時候,可以發現很多部落格都講了介面的冪等性。從上面我們也可以看出,POST
方法是非冪等的。但我們可以用一些手段來令POST
方法的介面變成是冪等的。
說了那麼多,那介面設計成冪等的好處是什麼? ? ? ?
舉個例子說一下非冪等的壞處:
3y大一的時候是要搶體育課的,但學校的搶課系統做得賊爛(延遲很高)。我想要搶到課,就開了10多個Chrome標籤頁去搶(即使某個Chrome標籤頁崩了,我還有另外的Chrome標籤頁是可用的)。我想搶到乒乓球或羽毛球。
搶課時間一到,我就輪著點擊我要想搶的乒乓球或羽毛球。如果系統設計得不好,這個請求是非冪等的(或者說事務控制得不好),我手速足夠快&&網絡足夠好,那我很可能搶到了多次乒乓球或者羽毛球的課程了。 (這是不合理的,一個人只能選一門課,而我搶到了多門或多次重複的課)
涉及到商城的應用場景可能就是:使用者下了多個重複的訂單了
如果我的搶課介面是冪等的話,那就不會出現這個問題了。因為冪等是多次請求某一個資源應該有同樣的副作用。
在資料庫後台最多只會有一筆記錄,不存在搶到多門課的現象了。
說白了,設計冪等性介面就是為了防止重複提交的(資料庫出現多條重複的資料)!
網路上有部落客也分享了幾個常見的解決重複提交方案:
同步鎖定(單線程,在叢集可能會失效)
分散式鎖定如redis(實作複雜)
業務欄位加唯一約束(簡單)
令牌表唯一約束(簡單推薦)---->實作冪等介面的一種手段
mysql的insert ignore或on duplicate key update(簡單)
共享鎖定普通索引(簡單)
#利用MQ或Redis擴充(排隊)
其他方案如多版本控制MVCC 樂觀鎖悲觀鎖狀態機等。 。
參考資料:
分散式系統介面冪等性http://blog.brucefeng.info/post/api-idempotent
如何避免下重複訂單https://www.jianshu.com/p/e618cc818432
關於介面冪等性的總結https: //www.jianshu.com/p/6eba27f8fb03
#使用資料庫唯一鍵實作交易冪等性http://www.caosh.me/be-tech/idempotence-using- unique-key/
API介面非冪等性問題及使用redis實作簡單的分散式鎖定https://blog.csdn.net/rariki/article/details/50783819
最後
如果以上有理解錯的地方,或是有更好的理解方式,希望大家不吝在留言區下留言。共同進步!
以上是常見的幾道秋招java面試題分析的詳細內容。更多資訊請關注PHP中文網其他相關文章!