面試題總結(二)
12. 工廠模式和建造者模式的區別
-
工廠模式一般都是創建一個產品,注重的是把這個產品創建出來,而不關心這個產品的組成部分。從代碼上看,工廠模式就是一個方法,用這個方法來生產出產品
-
建造者模式也是創建一個產品,但是不僅要把這個產品創建出來,還要關心這個產品的組成細節,組成過程。從代碼上看,建造者模式在創建產品的時候,這個產品有很多方法,建造者模式會根據這些相同的方法按不同的執行順序建造出不同組成細節的產品
13. 深拷貝和淺拷貝
-
淺拷貝:復制對象時只復制對象本身,包括基本數據類型的屬性,但是不會復制引用數據類型屬性指向的對象,即拷貝對象的與原對象的引用數據類型的屬性指向同一個對象
淺拷貝沒有達到完全復制,即原對象與克隆對象之間有關系,會相互影響
-
深拷貝:復制一個新的對象,引用數據類型指向對象會拷貝新的一份,不再指向原有引用對象的地址
深拷貝達到了完全復制的目的,即原對象與克隆對象之間不會相互影響
14. 泛型知識
Java泛型深度解析以及面試題_周將的博客-CSDN博客
Java泛型是在JDK5引入的新特性,它提供了編譯時類型安全檢測機制。該機制允許程序員在編譯時檢測到非法的類型,泛型的本質是參數類型。
1?? 使用泛型的好處
-
泛型可以增強編譯時錯誤檢測,減少因類型問題引發的運行時異常。
-
泛型可以避免類型轉換。
-
泛型可以泛型算法,增加代碼復用性。
2?? Java中泛型的分類
-
泛型類:它的定義格式是
class name<T1, T2, ..., Tn>,如下, 返回一個對象中包含了code和一個data, data是一個對象,我們不能固定它是什么類型,這時候就用T泛型來代替,大大增加了代碼的復用性。
public class Result<T> {
private T data;
private int code;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
-
泛型接口:和泛型類使用相似
-
泛型方法:它的定義是
[public] [static] <T> 返回值類型 方法名(T 參數列表),只有在前面加<T>這種的才能算是泛型方法,比如上面的setData方法雖然有泛型,但是不能算泛型方法
3?? 常見的泛型參數
-
K 鍵
-
V 值
-
N 數字
-
T 類型
-
E 元素
-
S, U, V 等,泛型聲明的多個類型
4?? 鉆石運算符Diamond
鉆石操作符是在 java 7 中引入的,可以讓代碼更易讀,但它不能用于匿名的內部類。在 java 9 中, 它可以與匿名的內部類一起使用,從而提高代碼的可讀性。
-
JDK7以下版本需要
Box<Integer> box = new Box<Integer>(); -
JDK7及以上版本
Box<Integer> integerBox1 = new Box<>();
5?? 受限類型參數
-
它的作用是對泛型變量的范圍作出限制,格式:
單一限制:
<U extends Number>多種限制:
<U extends A & B & C> -
多種限制的時候,類必須寫在第一個
6?? 通配符
通配符用?標識,分為受限制的通配符和不受限制的通配符,它使代碼更加靈活,廣泛運用于框架中。
比如List<Number>和List<Integer>是沒有任何關系的。如果我們將print方法中參數列表部分的List聲明為List<Number> list, 那么編譯是不會通過的,但是如果我們將List定義為List<? extends Number> list或者List<?> list,那么在編譯的時候就不會報錯了。


-
受限制的通配符:語法為
<? extends XXX>,它可以擴大兼容的范圍(XXX以及它的子類)比如上面例子中print中如果改為List<Number>,雖然它能存儲Integer和Double等類型的元素,但是作為參數傳遞的時候,它只能接受List<Number>這一種類型。如果聲明為List<? extends Number> list就不一樣了,相當于擴大了類型的范圍,使得代碼更加的靈活,代碼復用性更高。
<? super T>和extends一樣,只不過extends是限定了上限,而super是限定了下限 -
非受限制的通配符:不適用關鍵字extends或者super。比如上面print參數列表聲明為List<?> list也可以解決問題。
?代表了未知類型。所以所有的類型都可以理解為List<?>的子類。它的使用場景一般是泛型類中的方法不依賴于類型參數的時候,比如list.size(), 遍歷集合等,這樣的話并不關心List中元素的具體類型。
7?? 泛型中的PECS原則
PECS原則的全拼是"Producer Extends Consumer Super"。
當需要
頻繁取值,而不需要寫值則使用上界通配符
? extends T作為數據結構泛型。=相反,當需要
頻繁寫值,而不需要取值則使用下屆通配符
? super T作為數據結構泛型。
案例分析:創建Apple,Fruit兩個類,其中Apple是Fruit的子類
public class PECS {
ArrayList<? extends Fruit> exdentFurit;
ArrayList<? super Fruit> superFurit;
Apple apple = new Apple();
private void test() {
Fruit a1 = exdentFurit.get(0);
Fruit a2 = superFurit.get(0); //Err1
exdentFurit.add(apple); //Err2
superFurit.add(apple);
}
}
其中Err1和Err2行處報錯,因為這些操作并不符合PECS原則,逐一分析:
-
Err1
使用? super T規定泛型的數據結構,其存儲的值是T的父類,而這里superFruit.get()的對象為Fruit的父類對象,而指向該對象的引用類型為Fruit,父類缺少子類中的一些信息,這顯然是不對的,因此編譯器直接禁止在使用? super T泛型的數據結構中進行取值,只能進行寫值,正是開頭所說的CS原則。 -
Err2
使用? extends T規定泛型的數據結構,其存儲的值是T的子類,這里exdentFruit.add()也就是向其中添加Fruit的子類對象,而Fruit可以有多種子類對象,因此當我們進行寫值時,我們并不知道其中存儲的到底是哪個子類,因此寫值操作必然會出現問題,所以編譯器接禁止在使用? extends T泛型的數據結構中進行寫,只能進行取值,正是開頭所說的PE原則。
8?? 類型擦除
-
類型擦除作用:因為Java中的泛型實在JDK1.5之后新加的特性,為了兼容性,在虛擬機中運行時是不存在泛型的,所以Java泛型是一種偽泛型,類型擦除就保證了泛型不在運行時候出現。
-
場景:編譯器會把泛型類型中所有的類型參數替換為它們的上(下)限,如果沒有對類型參數做出限制,那么就替換為Object類型。因此,編譯出的字節碼僅僅包含了常規類,接口和方法。
-
在必要時插入類型轉換以保持類型安全。
-
生成橋方法以在擴展泛型時保持多態性
-
Bridge Methods 橋方法
當編譯一個擴展參數化類的類,或一個實現了參數化接口的接口時,編譯器有可能因此要創建一個合成方法,名為橋方法。它是類型擦除過程中的一部分。下面對橋方法代碼驗證一下:
-
public class Node<T> {
T t;
public Node(T t) {
this.t = t;
}
public void set(T t) {
this.t = t;
}
}
class MyNode extends Node<String> {
public MyNode(String s) {
super(s);
}
@Override
public void set(String s) {
super.set(s);
} - 上面
Node<T>是一個泛型類型,沒有聲明上下限,所以在類型擦除后會變為Object類型。而MyNode類已經聲明了實際類型參數為String類型,這樣在調用父類set方法的時候就會出現不匹配的情況,所以虛擬機在編譯的時候為我們生成了一個橋方法,我們通過javap -c MyNode.class查看字節碼文件,看到確實為我們生成了一個橋方法

15. Java泛型的原理?什么是泛型擦除機制?
-
Java的泛型是JDK5新引入的特性,為了向下兼容,虛擬機其實是不支持泛型,所以Java實現的是一種偽泛型機制,也就是說Java在編譯期擦除了所有的泛型信息,這樣Java就不需要產生新的類型到字節碼,所有的泛型類型最終都是一種原始類型,在Java運行時根本就不存在泛型信息。
-
類型擦除其實在類常量池中保存了泛型信息,運行時還能拿到信息,比如Gson的TypeToken的使用。
-
泛型算法實現的關鍵:利用受限類型參數。
16. Java編譯器具體是如何擦除泛型的
-
檢查泛型類型,獲取目標類型
-
擦除類型變量,并替換為限定類型
-
如果泛型類型的類型變量沒有限定,則用Object作為原始類型
-
如果有限定,則用限定的類型作為原始類型
-
如果有多個限定(T extends Class1&Class2),則使用第一個邊界Class1作為原始類
-
在必要時插入類型轉換以保持類型安全
-
生成橋方法以在擴展時保持多態性
17. Array數組中可以用泛型嗎?
不能,簡單的來講是因為如果可以創建泛型數組,泛型擦除會導致編譯能通過,但是運行時會出現異常。所以如果禁止創建泛型數組,就可以避免此類問題。
18. PESC原則&限定通配符和非限定通配符
-
如果你只需要從集合中獲得類型T , 使用<? extends T>通配符
-
如果你只需要將類型T放到集合中, 使用<? super T>通配符
-
如果你既要獲取又要放置元素,則不使用任何通配符。例如
List<String> -
<?>非限定通配符既不能存也不能取, 一般使用非限定通配符只有一個目的,就是為了靈活的轉型。其實List<?> 等于 List<? extends Object>。
19. Java中List<?>和List<Object>的區別
雖然他們都會進行類型檢查,實質上卻完全不同。List<?> 是一個未知類型的List,而List<Object>其實是任意類型的List。你可以把List<String>, List<Integer>賦值給List<?>,卻不能把List<String>賦值給List<Object>。
20. for循環和forEach效率問題
遍歷ArrayList測試
這里向ArrayList中插入10000000條數據,分別用for循環和for each循環進行遍歷測試
package for循環效率問題;
import java.util.ArrayList;
public class Test {
public static void main(String[] args) {
ArrayList<Integer> arrayList = new ArrayList<>();
for (int i = 0; i < 10000000; i++) {
arrayList.add(i);
}
int x = 0;
//for循環遍歷
long forStart = System.currentTimeMillis();
for (int i = 0; i < arrayList.size(); i++) {
x = arrayList.get(i);
}
long forEnd = System.currentTimeMillis();
System.out.println("for循環耗時" + (forEnd - forStart) + "ms");
//for-each遍歷
long forEachStart = System.currentTimeMillis();
for (int i : arrayList) {
x = i;
}
long forEachEnd = System.currentTimeMillis();
System.out.println("foreach耗時" + (forEachEnd - forEachStart) + "ms");
}
}
根據執行結果,可以看到for循環速度更快一點,但是差別不太大
我們反編譯class文件看看
package for循環效率問題;
import java.util.ArrayList;
import java.util.Iterator;
public class Test {
public Test() {
}
public static void main(String[] args) {
ArrayList<Integer> arrayList = new ArrayList();
int x;
for(x = 0; x < 10000000; ++x) {
arrayList.add(x);
}
int x = false;
long forStart = System.currentTimeMillis();
for(int i = 0; i < arrayList.size(); ++i) {
x = (Integer)arrayList.get(i);
}
long forEnd = System.currentTimeMillis();
System.out.println("for循環耗時" + (forEnd - forStart) + "ms");
long forEachStart = System.currentTimeMillis();
int i;
for(Iterator var9 = arrayList.iterator();
var9.hasNext();
i = (Integer)var9.next()) {
}
long forEachEnd = System.currentTimeMillis();
System.out.println("foreach耗時" + (forEachEnd - forEachStart) + "ms");
}
}
可以看到增強for循環本質上就是使用iterator迭代器進行遍歷
遍歷LinkedList測試
這里向LinkedList中插入測試10000條數據進行遍歷測試,實驗中發現如果循環次數太大,for循環直接卡死;
package for循環效率問題;
import java.util.LinkedList;
public class Test2 {
public static void main(String[] args) {
LinkedList<Integer> linkedList = new LinkedList<>();
for (int i = 0; i < 10000; i++) {
linkedList.add(i);
}
int x = 0;
//for循環遍歷
long forStart = System.currentTimeMillis();
for (int i = 0; i < linkedList.size(); i++) {
x = linkedList.get(i);
}
long forEnd = System.currentTimeMillis();
System.out.println("for循環耗時" + (forEnd - forStart) + "ms");
//for-each遍歷
long forEachStart = System.currentTimeMillis();
for (int i : linkedList) {
x = i;
}
long forEachEnd = System.currentTimeMillis();
System.out.println("foreach耗時" + (forEachEnd - forEachStart) + "ms");
}
}
根據結果可以看到,遍歷LinkedList時for each速度遠遠大于for循環速度
反編譯class文件的源碼
package for循環效率問題;
import java.util.Iterator;
import java.util.LinkedList;
public class Test2 {
public Test2() {
}
public static void main(String[] args) {
LinkedList<Integer> linkedList = new LinkedList();
int x;
for (x = 0; x < 10000; ++x) {
linkedList.add(x);
}
int x = false;
long forStart = System.currentTimeMillis();
for (int i = 0; i < linkedList.size(); ++i) {
x = (Integer) linkedList.get(i);
}
long forEnd = System.currentTimeMillis();
System.out.println("for循環耗時" + (forEnd - forStart) + "ms");
long forEachStart = System.currentTimeMillis();
int i;
for (Iterator var9 = linkedList.iterator(); var9.hasNext(); i = (Integer) var9.next()) {
}
long forEachEnd = System.currentTimeMillis();
System.out.println("foreach耗時" + (forEachEnd - forEachStart) + "ms");
}
}
總結:
1?? 區別:
-
for 循環就是按順序遍歷,隨機訪問元素
-
for each循環本質上是使用iterator迭代器遍歷,順序鏈表訪問元素;
2?? 性能比對:
-
對于arraylist底層為數組類型的結構,使用for循環遍歷比使用foreach循環遍歷稍快一些,但相差不大
-
對于linkedlist底層為單鏈表類型的結構,使用for循環每次都要從第一個元素開始遍歷,速度非常慢;使用foreach可以直接讀取當前結點,速度比for快很多
3?? 原理接釋:
-
ArrayList數組類型結構對隨機訪問比較快,而for循環中的get()方法,采用的即是隨機訪問的方法,因此在ArrayList里,for循環較快
-
順序表a
用for循環,從a[0]開始直接讀到元素,接著直接讀a[1](順序表的優點,隨機訪問)
用foreach,得到a[0]-a[2]的全部地址放入隊列,按順序取出隊里里的地址來訪問元素 - LinkedList鏈表形結構對順序訪問比較快,iterator中的next()方法,采用的即是順序訪問的方法,因此在LinkedList里,使用iterator較快
單鏈表b
用for循環,從a[0]開始讀元素、然后通過a[0]的next讀到a[1]元素、通過a[0]的next的next讀到a[2]元素,以此類推,性能影響較大,慎用!用foreach,得到a[0]-a[2]的全部地址放入隊列,按順序取出隊里里的地址來訪問元素;
16. NIO、BIO、AIO
阻塞IO 和 非阻塞IO
IO操作分為兩個部分,即發起IO請求和實際IO操作,阻塞IO和非阻塞IO的區別就在于第二個步驟是否阻塞
-
若發起IO請求后請求線程一直等待實際IO操作完成,則為阻塞IO
-
若發起IO請求后請求線程返回而不會一直等待,則為非阻塞IO
同步IO 和 異步IO
IO操作分為兩個部分,即發起IO請求和實際IO操作,同步IO和異步IO的區別就在于第一個步驟是否阻塞
-
若實際IO操作阻塞請求進程,即請求進程需要等待或輪詢查看IO操作是否就緒,則為同步IO
-
若實際IO操作不阻塞請求進程,而是由操作系統來進行實際IO操作并將結果返回,則為異步IO
NIO、BIO、AIO
BIO表示同步阻塞式IO,服務器實現模式為一個連接一個線程,即客戶端有連接請求時服務器端就需要啟動一個線程進行處理,如果這個連接不做任何事情會造成不必要的線程開銷,當然可以通過線程池機制改善。
NIO表示同步非阻塞IO,服務器實現模式為一個請求一個線程,即客戶端發送的連接請求都會注冊到多路復用器上,多路復用器輪詢到連接有I/O請求時才啟動一個線程進行處理。
AIO表示異步非阻塞IO,服務器實現模式為一個有效請求一個線程,客戶端的I/O請求都是由操作系統先完成IO操作后再通知服務器應用來啟動線程進行處理。
17. 什么是反射
反射是在運行狀態中,對于任意一個類,都能夠知道這個類的所有屬性和方法;對于任意一個對象,都能夠調用它的任意一個方法和屬性;這種動態獲取的信息以及動態調用對象的方法的功能稱為 Java 語言的反射機制。
反射實現了把java類中的各種結構法、屬性、構造器、類名)映射成一個個的Java對象
優點:可以實現動態創建對象和編譯,體現了很大的靈活性
缺點:對性能有影響,使用反射本質上是一種接釋操作,慢于直接執行java代碼
應用場景:
-
JDBC中,利用反射動態加載了數據庫驅動程序。
-
Web服務器中利用反射調用了Sevlet的服務方法。
-
Eclispe等開發工具利用反射動態刨析對象的類型與結構,動態提示對象的屬性和方法。
-
很多框架都用到反射機制,注入屬性,調用方法,如Spring。
18. 序列化&反序列化
Java基礎學習總結——Java對象的序列化和反序列化 - 孤傲蒼狼 - 博客園 (cnblogs.com)
1?? 什么是序列化?
序列化是指將Java對象轉化為字節序列的過程,而反序列化則是將字節序列轉化為Java對象的過程
2?? 為什么需要序列化?
我們知道不同線程/進程進行遠程通信時可以相互發送各種數據,包括文本圖片音視頻等,Java對象不能直接傳輸,所以需要轉化為二進制序列傳輸,所以需要序列化
3?? 序列化的用途?
-
把對象的字節序列永久地保存到硬盤上,通常存放在一個文件中
在很多應用中,需要對某些對象進行序列化,讓它們離開內存空間,入住物理硬盤,以便長期保存。比如最常見的是Web服務器中的Session對象,當有10萬用戶并發訪問,就有可能出現10萬個Session對象,內存可能吃不消,于是Web容器就會把一些seesion先序列化到硬盤中,等要用了,再把保存在硬盤中的對象還原到內存中
-
在網絡上傳送對象的字節序列
當兩個進程在進行遠程通信時,彼此可以發送各種類型的數據。無論是何種類型的數據,都會以二進制序列的形式在網絡上傳送。發送方需要把這個Java對象轉換為字節序列,才能在網絡上傳送;接收方則需要把字節序列再恢復為Java對象
4?? JDK類庫中的序列化API
-
java.io.ObjectOutputStream代表對象輸出流,它的writeObject(Object obj)方法可對參數指定的obj對象進行序列化,把得到的字節序列寫到一個目標輸出流中 -
java.io.ObjectInputStream代表對象輸入流,它的readObject()方法從一個源輸入流中讀取字節序列,再把它們反序列化為一個對象,并將其返回
只有實現了Serializable和Externalizable接口的類的對象才能被序列化。Externalizable接口繼承自Serializable接口,實現Externalizable接口的類完全由自身來控制序列化的行為,而僅實現Serializable接口的類可以 采用默認的序列化方式
對象序列化包括如下步驟:
-
創建一個對象輸出流,它可以包裝一個其他類型的目標輸出流,如文件輸出流
-
通過對象輸出流的writeObject()方法寫對象
對象反序列化的步驟如下:
-
創建一個對象輸入流,它可以包裝一個其他類型的源輸入流,如文件輸入流
-
通過對象輸入流的readObject()方法讀取對象
5?? serialVersionUID的作用
serialVersionUID: 字面意思上是序列化的版本號,凡是實現Serializable接口的類都有一個表示序列化版本標識符的靜態變量
如果實現Serializable接口的類如果類中沒有添加serialVersionUID,那么就會出現警告提示
serialVersionUID有兩種生成方式:
-
采用Add default serial version ID方式生成的serialVersionUID是1L,例如:
private static final long serialVersionUID = 1L;
2.采用Add generated serial version ID這種方式生成的serialVersionUID是根據類名,接口名,方法和屬性等來生成的,例如:
private static final long serialVersionUID = 4603642343377807741L;
19. 動態代理是什么?有哪些應用?
當想要給實現了某個接口的類中的方法,加一些額外的處理。比如說加日志,加事務等??梢越o這個類創建一個代理,故名思議就是創建一個新的類,這個類不僅包含原來類方法的功能,而且還在原來的基礎上添加了額外處理的新類。這個代理類并不是定義好的,是動態生成的。具有解耦意義,靈活,擴展性強。
應用:
-
Spring的AOP
-
加事務
-
加權限
-
加日志
20. 怎么實現動態代理
在java的java.lang.reflect包下提供了一個Proxy類和一個InvocationHandler接口,通過這個類和這個接口可以生成JDK動態代理類和動態代理對象
-
java.lang.reflect.Proxy是所有動態代理的父類。它通過靜態方法newProxyInstance()來創建動態代理的class對象和實例。
-
每一個動態代理實例都有一個關聯的InvocationHandler。通過代理實例調用方法,方法調用請求會被轉發給InvocationHandler的invoke方法。
-
首先定義一個
IncocationHandler處理器接口實現類,實現其invoke()方法 -
通過
Proxy.newProxyInstance生成代理類對象
package demo3;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class ProxyInvocationHandler implements InvocationHandler {
//定義真實角色
private Rent host;
//真實角色set方法
public void setHost(Rent host) {
this.host = host;
}
/**
生成代理類方法
1. 類加載器,為當前類即可
2. 代理類實現的接口
3. 處理器接口對象
**/
public Object getProxy() {
return Proxy.newProxyInstance(this.getClass().getClassLoader(),
host.getClass().getInterfaces(), this);
}
//處理代理實例,并返回結果
//方法在此調用
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//調用真實角色方法,相當于調用rent()方法
Object result = method.invoke(host, args);
//附加方法
seeHouse();
contract();
fare();
return result;
}
//看房
public void seeHouse() {
System.out.println("中介帶你看房");
}
//簽合同
public void contract() {
System.out.println("租賃合同");
}
//收中介費
public void fare() {
System.out.println("收中介費");
}
}
package demo3;
public class Client {
public static void main(String[] args) {
//真實角色:房東
Host host = new Host();
//處理器接口對象
ProxyInvocationHandler handler = new ProxyInvocationHandler();
//設置要代理的真實角色
handler.setHost(host);
//動態生成代理類
Rent proxy = (Rent) handler.getProxy();
//調用方法
proxy.rent();
}
}
21. 如何實現對象克?。?/em>
有兩種方式:
1.、實現Cloneable接口并重寫Object類中的clone()方法;
-
protected Object clone() throws CloneNotSupportedException {
test_paper paper = (test_paper) super.clone();
paper.date = (Date) date.clone();
return paper;
} -
2、實現Serializable接口,通過對象的序列化和反序列化實現克隆,可以實現真正的深度克隆
-
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Date;
@SuppressWarnings("all")
public class Client {
public static void main(String[] args) throws Exception {
Date date = new Date();
String name = "zsr";
test_paper paper1 = new test_paper(name, date);
//通過序列化和反序列化來實現深克隆
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream obs = new ObjectOutputStream(bos);
obs.writeObject(paper1);
byte a[] = bos.toByteArray();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(a));
test_paper paper3 = (test_paper) ois.readObject();//獲取到新對象
paper3.getDate().setDate(1000);//改變非基本類型屬性
System.out.println(paper1);
System.out.println(paper3);
}
}

浙公網安備 33010602011771號