上一篇:近5年常考Java面試題及答案整理(二)
68、Java中如何實現序列化,有什么意義?
答:序列化就是一種用來處理對象流的機制,所謂對象流也就是將對象的內容進行流化。可以對流化后的對象進行讀寫操作,也可將流化后的對象傳輸於網絡之間。序列化是為了解決對象流讀寫操作時可能引發的問題(如果不進行序列化可能會存在數據亂序的問題)。
要實現序列化,需要讓一個類實現Serializable接口,該接口是一個標識性接口,標注該類對象是可被序列化的,然后使用一個輸出流來構造一個對象輸出流並通過writeObject(Object)方法就可以將實現對象寫出(即保存其狀態);如果需要反序列化則可以用一個輸入流建立對象輸入流,然后通過readObject方法從流中讀取對象。序列化除了能夠實現對象的持久化之外,還能夠用於對象的深度克隆(可以參考第29題)。
69、Java中有幾種類型的流?
答:字節流和字符流。字節流繼承於InputStream、OutputStream,字符流繼承於Reader、Writer。在 java.io 包中還有許多其他的流,主要是為了提高性能和使用方便。關於Java的I/O需要注意的有兩點:一是兩種對稱性(輸入和輸出的對稱性,字節和字符的對稱性);二是兩種設計模式(適配器模式和裝潢模式)。另外Java中的流不同於C#的是它只有一個維度一個方向。
面試題 - 編程實現文件拷貝。(這個題目在筆試的時候經常出現,下面的代碼給出了兩種實現方案)
1 import java.io.FileInputStream; 2 import java.io.FileOutputStream; 3 import java.io.IOException; 4 import java.io.InputStream; 5 import java.io.OutputStream; 6 import java.nio.ByteBuffer; 7 import java.nio.channels.FileChannel; 8 9 public final class MyUtil { 10 11 private MyUtil() { 12 throw new AssertionError(); 13 } 14 15 public static void fileCopy(String source, String target) throws IOException { 16 try (InputStream in = new FileInputStream(source)) { 17 try (OutputStream out = new FileOutputStream(target)) { 18 byte[] buffer = new byte[4096]; 19 int bytesToRead; 20 while((bytesToRead = in.read(buffer)) != -1) { 21 out.write(buffer, 0, bytesToRead); 22 } 23 } 24 } 25 } 26 27 public static void fileCopyNIO(String source, String target) throws IOException { 28 try (FileInputStream in = new FileInputStream(source)) { 29 try (FileOutputStream out = new FileOutputStream(target)) { 30 FileChannel inChannel = in.getChannel(); 31 FileChannel outChannel = out.getChannel(); 32 ByteBuffer buffer = ByteBuffer.allocate(4096); 33 while(inChannel.read(buffer) != -1) { 34 buffer.flip(); 35 outChannel.write(buffer); 36 buffer.clear(); 37 } 38 } 39 } 40 } 41 }
注意:上面用到Java 7的TWR,使用TWR后可以不用在finally中釋放外部資源 ,從而讓代碼更加優雅。
70、寫一個方法,輸入一個文件名和一個字符串,統計這個字符串在這個文件中出現的次數。
答:代碼如下:
1 import java.io.BufferedReader; 2 import java.io.FileReader; 3 4 public final class MyUtil { 5 6 // 工具類中的方法都是靜態方式訪問的因此將構造器私有不允許創建對象(絕對好習慣) 7 private MyUtil() { 8 throw new AssertionError(); 9 } 10 11 /** 12 * 統計給定文件中給定字符串的出現次數 13 * 14 * @param filename 文件名 15 * @param word 字符串 16 * @return 字符串在文件中出現的次數 17 */ 18 public static int countWordInFile(String filename, String word) { 19 int counter = 0; 20 try (FileReader fr = new FileReader(filename)) { 21 try (BufferedReader br = new BufferedReader(fr)) { 22 String line = null; 23 while ((line = br.readLine()) != null) { 24 int index = -1; 25 while (line.length() >= word.length() && (index = line.indexOf(word)) >= 0) { 26 counter++; 27 line = line.substring(index + word.length()); 28 } 29 } 30 } 31 } catch (Exception ex) { 32 ex.printStackTrace(); 33 } 34 return counter; 35 } 36 37 }
71、如何用Java代碼列出一個目錄下所有的文件?
答:
如果只要求列出當前文件夾下的文件,代碼如下所示:
1 import java.io.File; 2 3 class Test12 { 4 5 public static void main(String[] args) { 6 File f = new File("/Users/nnngu/Downloads"); 7 for(File temp : f.listFiles()) { 8 if(temp.isFile()) { 9 System.out.println(temp.getName()); 10 } 11 } 12 } 13 }
如果需要對文件夾繼續展開,代碼如下所示:
1 import java.io.File; 2 3 class Test12 { 4 5 public static void main(String[] args) { 6 showDirectory(new File("/Users/nnngu/Downloads")); 7 } 8 9 public static void showDirectory(File f) { 10 _walkDirectory(f, 0); 11 } 12 13 private static void _walkDirectory(File f, int level) { 14 if(f.isDirectory()) { 15 for(File temp : f.listFiles()) { 16 _walkDirectory(temp, level + 1); 17 } 18 } 19 else { 20 for(int i = 0; i < level - 1; i++) { 21 System.out.print("\t"); 22 } 23 System.out.println(f.getName()); 24 } 25 } 26 }
在Java 7中可以使用NIO.2的API來做同樣的事情,代碼如下所示:
1 class ShowFileTest { 2 3 public static void main(String[] args) throws IOException { 4 Path initPath = Paths.get("/Users/nnngu/Downloads"); 5 Files.walkFileTree(initPath, new SimpleFileVisitor<Path>() { 6 7 @Override 8 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) 9 throws IOException { 10 System.out.println(file.getFileName().toString()); 11 return FileVisitResult.CONTINUE; 12 } 13 14 }); 15 } 16 }
72、用Java的套接字編程實現一個多線程的回顯(echo)服務器。
答:
1 import java.io.BufferedReader; 2 import java.io.IOException; 3 import java.io.InputStreamReader; 4 import java.io.PrintWriter; 5 import java.net.ServerSocket; 6 import java.net.Socket; 7 8 public class EchoServer { 9 10 private static final int ECHO_SERVER_PORT = 6789; 11 12 public static void main(String[] args) { 13 try(ServerSocket server = new ServerSocket(ECHO_SERVER_PORT)) { 14 System.out.println("服務器已經啟動..."); 15 while(true) { 16 Socket client = server.accept(); 17 new Thread(new ClientHandler(client)).start(); 18 } 19 } catch (IOException e) { 20 e.printStackTrace(); 21 } 22 } 23 24 private static class ClientHandler implements Runnable { 25 private Socket client; 26 27 public ClientHandler(Socket client) { 28 this.client = client; 29 } 30 31 @Override 32 public void run() { 33 try(BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream())); 34 PrintWriter pw = new PrintWriter(client.getOutputStream())) { 35 String msg = br.readLine(); 36 System.out.println("收到" + client.getInetAddress() + "發送的: " + msg); 37 pw.println(msg); 38 pw.flush(); 39 } catch(Exception ex) { 40 ex.printStackTrace(); 41 } finally { 42 try { 43 client.close(); 44 } catch (IOException e) { 45 e.printStackTrace(); 46 } 47 } 48 } 49 } 50 51 }
注意:上面的代碼使用了Java 7的TWR語法,由於很多外部資源類都間接的實現了AutoCloseable接口(單方法回調接口),因此可以利用TWR語法在try結束的時候通過回調的方式自動調用外部資源類的close()方法,避免書寫冗長的finally代碼塊。此外,上面的代碼用一個靜態內部類實現線程的功能,使用多線程可以避免一個用戶I/O操作所產生的中斷影響其他用戶對服務器的訪問,簡單的說就是一個用戶的輸入操作不會造成其他用戶的阻塞。當然,上面的代碼使用線程池可以獲得更好的性能,因為頻繁的創建和銷毀線程所造成的開銷也是不可忽視的。
下面是一段回顯客戶端測試代碼:
1 import java.io.BufferedReader; 2 import java.io.InputStreamReader; 3 import java.io.PrintWriter; 4 import java.net.Socket; 5 import java.util.Scanner; 6 7 public class EchoClient { 8 9 public static void main(String[] args) throws Exception { 10 Socket client = new Socket("localhost", 6789); 11 Scanner sc = new Scanner(System.in); 12 System.out.print("請輸入內容: "); 13 String msg = sc.nextLine(); 14 sc.close(); 15 PrintWriter pw = new PrintWriter(client.getOutputStream()); 16 pw.println(msg); 17 pw.flush(); 18 BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream())); 19 System.out.println(br.readLine()); 20 client.close(); 21 } 22 }
如果希望用NIO的多路復用套接字實現服務器,代碼如下所示。NIO的操作雖然帶來了更好的性能,但是有些操作是比較底層的,對於初學者來說還是有些難於理解。
1 import java.io.IOException; 2 import java.net.InetSocketAddress; 3 import java.nio.ByteBuffer; 4 import java.nio.CharBuffer; 5 import java.nio.channels.SelectionKey; 6 import java.nio.channels.Selector; 7 import java.nio.channels.ServerSocketChannel; 8 import java.nio.channels.SocketChannel; 9 import java.util.Iterator; 10 11 public class EchoServerNIO { 12 13 private static final int ECHO_SERVER_PORT = 6789; 14 private static final int ECHO_SERVER_TIMEOUT = 5000; 15 private static final int BUFFER_SIZE = 1024; 16 17 private static ServerSocketChannel serverChannel = null; 18 private static Selector selector = null; // 多路復用選擇器 19 private static ByteBuffer buffer = null; // 緩沖區 20 21 public static void main(String[] args) { 22 init(); 23 listen(); 24 } 25 26 private static void init() { 27 try { 28 serverChannel = ServerSocketChannel.open(); 29 buffer = ByteBuffer.allocate(BUFFER_SIZE); 30 serverChannel.socket().bind(new InetSocketAddress(ECHO_SERVER_PORT)); 31 serverChannel.configureBlocking(false); 32 selector = Selector.open(); 33 serverChannel.register(selector, SelectionKey.OP_ACCEPT); 34 } catch (Exception e) { 35 throw new RuntimeException(e); 36 } 37 } 38 39 private static void listen() { 40 while (true) { 41 try { 42 if (selector.select(ECHO_SERVER_TIMEOUT) != 0) { 43 Iterator<SelectionKey> it = selector.selectedKeys().iterator(); 44 while (it.hasNext()) { 45 SelectionKey key = it.next(); 46 it.remove(); 47 handleKey(key); 48 } 49 } 50 } catch (Exception e) { 51 e.printStackTrace(); 52 } 53 } 54 } 55 56 private static void handleKey(SelectionKey key) throws IOException { 57 SocketChannel channel = null; 58 59 try { 60 if (key.isAcceptable()) { 61 ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel(); 62 channel = serverChannel.accept(); 63 channel.configureBlocking(false); 64 channel.register(selector, SelectionKey.OP_READ); 65 } else if (key.isReadable()) { 66 channel = (SocketChannel) key.channel(); 67 buffer.clear(); 68 if (channel.read(buffer) > 0) { 69 buffer.flip(); 70 CharBuffer charBuffer = CharsetHelper.decode(buffer); 71 String msg = charBuffer.toString(); 72 System.out.println("收到" + channel.getRemoteAddress() + "的消息:" + msg); 73 channel.write(CharsetHelper.encode(CharBuffer.wrap(msg))); 74 } else { 75 channel.close(); 76 } 77 } 78 } catch (Exception e) { 79 e.printStackTrace(); 80 if (channel != null) { 81 channel.close(); 82 } 83 } 84 } 85 86 }
1 import java.nio.ByteBuffer; 2 import java.nio.CharBuffer; 3 import java.nio.charset.CharacterCodingException; 4 import java.nio.charset.Charset; 5 import java.nio.charset.CharsetDecoder; 6 import java.nio.charset.CharsetEncoder; 7 8 public final class CharsetHelper { 9 private static final String UTF_8 = "UTF-8"; 10 private static CharsetEncoder encoder = Charset.forName(UTF_8).newEncoder(); 11 private static CharsetDecoder decoder = Charset.forName(UTF_8).newDecoder(); 12 13 private CharsetHelper() { 14 } 15 16 public static ByteBuffer encode(CharBuffer in) throws CharacterCodingException{ 17 return encoder.encode(in); 18 } 19 20 public static CharBuffer decode(ByteBuffer in) throws CharacterCodingException{ 21 return decoder.decode(in); 22 } 23 }
73、XML文檔定義有幾種形式?它們之間有何本質區別?解析XML文檔有哪幾種方式?
答:XML文檔定義分為DTD和Schema兩種形式,二者都是對XML語法的約束,其本質區別在於Schema本身也是一個XML文件,可以被XML解析器解析,而且可以為XML承載的數據定義類型,約束能力較之DTD更強大。對XML的解析主要有DOM(文檔對象模型,Document Object Model)、SAX(Simple API for XML)和StAX(Java 6中引入的新的解析XML的方式,Streaming API for XML),其中DOM處理大型文件時其性能下降的非常厲害,這個問題是由DOM樹結構占用的內存較多造成的,而且DOM解析方式必須在解析文件之前把整個文檔裝入內存,適合對XML的隨機訪問(典型的用空間換取時間的策略);SAX是事件驅動型的XML解析方式,它順序讀取XML文件,不需要一次全部裝載整個文件。當遇到像文件開頭,文檔結束,或者標簽開頭與標簽結束時,它會觸發一個事件,用戶通過事件回調代碼來處理XML文件,適合對XML的順序訪問;顧名思義,StAX把重點放在流上,實際上StAX與其他解析方式的本質區別就在於應用程序能夠把XML作為一個事件流來處理。將XML作為一組事件來處理的想法並不新穎(SAX就是這樣做的),但不同之處在於StAX允許應用程序代碼把這些事件逐個拉出來,而不用提供在解析器方便時從解析器中接收事件的處理程序。
74、你在項目中哪些地方用到了XML?
答:XML的主要作用有兩個方面:數據交換和信息配置。在做數據交換時,XML將數據用標簽組裝成起來,然后壓縮打包加密后通過網絡傳送給接收者,接收解密與解壓縮后再從XML文件中還原相關信息進行處理,XML曾經是異構系統間交換數據的事實標准,但此項功能幾乎已經被JSON(JavaScript Object Notation)取而代之。當然,目前很多軟件仍然使用XML來存儲配置信息,我們在很多項目中通常也會將作為配置信息的硬代碼寫在XML文件中,Java的很多框架也是這么做的,而且這些框架都選擇了dom4j作為處理XML的工具,因為Sun公司的官方API實在不怎么好用。
補充:現在有很多時髦的軟件(如Sublime)已經開始將配置文件書寫成JSON格式,我們已經強烈的感受到XML的另一項功能也將逐漸被業界拋棄。
75、闡述JDBC操作數據庫的步驟。
答:下面的代碼以連接本機的Oracle數據庫為例,演示JDBC操作數據庫的步驟。
加載驅動。
創建連接。
創建語句。
1 PreparedStatement ps = con.prepareStatement("select * from emp where sal between ? and ?"); 2 ps.setInt(1, 1000); 3 ps.setInt(2, 3000);
執行語句。
ResultSet rs = ps.executeQuery();
處理結果。
1 while(rs.next()) { 2 System.out.println(rs.getInt("empno") + " - " + rs.getString("ename")); 3 }
關閉資源。
1 finally { 2 if(con != null) { 3 try { 4 con.close(); 5 } catch (SQLException e) { 6 e.printStackTrace(); 7 } 8 } 9 }
提示:關閉外部資源的順序應該和打開的順序相反,也就是說先關閉ResultSet、再關閉Statement、在關閉Connection。上面的代碼只關閉了Connection(連接),雖然通常情況下在關閉連接時,連接上創建的語句和打開的游標也會關閉,但不能保證總是如此,因此應該按照剛才說的順序分別關閉。此外,第一步加載驅動在JDBC 4.0中是可以省略的(自動從類路徑中加載驅動),但是我們建議保留。
76、Statement和PreparedStatement有什么區別?哪個性能更好?
答:與Statement相比,①PreparedStatement接口代表預編譯的語句,它主要的優勢在於可以減少SQL的編譯錯誤並增加SQL的安全性(減少SQL注射攻擊的可能性);②PreparedStatement中的SQL語句是可以帶參數的,避免了用字符串連接拼接SQL語句的麻煩和不安全;③當批量處理SQL或頻繁執行相同的查詢時,PreparedStatement有明顯的性能上的優勢,由於數據庫可以將編譯優化后的SQL語句緩存起來,下次執行相同結構的語句時就會很快(不用再次編譯和生成執行計划)。
補充:為了提供對存儲過程的調用,JDBC API中還提供了CallableStatement接口。存儲過程(Stored Procedure)是數據庫中一組為了完成特定功能的SQL語句的集合,經編譯后存儲在數據庫中,用戶通過指定存儲過程的名字並給出參數(如果該存儲過程帶有參數)來執行它。雖然調用存儲過程會在網絡開銷、安全性、性能上獲得很多好處,但是存在如果底層數據庫發生遷移時就會有很多麻煩,因為每種數據庫的存儲過程在書寫上存在不少的差別。
77、使用JDBC操作數據庫時,如何提升讀取數據的性能?如何提升更新數據的性能?
答:要提升讀取數據的性能,可以指定通過結果集(ResultSet)對象的setFetchSize()方法指定每次抓取的記錄數(典型的空間換時間策略);要提升更新數據的性能可以使用PreparedStatement語句構建批處理,將若干SQL語句置於一個批處理中執行。
78、在進行數據庫編程時,連接池有什么作用?
答:由於創建連接和釋放連接都有很大的開銷(尤其是數據庫服務器不在本地時,每次建立連接都需要進行TCP的三次握手,釋放連接需要進行TCP四次握手,造成的開銷是不可忽視的),為了提升系統訪問數據庫的性能,可以事先創建若干連接置於連接池中,需要時直接從連接池獲取,使用結束時歸還連接池而不必關閉連接,從而避免頻繁創建和釋放連接所造成的開銷,這是典型的用空間換取時間的策略(浪費了空間存儲連接,但節省了創建和釋放連接的時間)。池化技術在Java開發中是很常見的,在使用線程時創建線程池的道理與此相同。基於Java的開源數據庫連接池主要有:C3P0、Proxool、DBCP、BoneCP、Druid等。
補充:在計算機系統中時間和空間是不可調和的矛盾,理解這一點對設計滿足性能要求的算法是至關重要的。大型網站性能優化的一個關鍵就是使用緩存,而緩存跟上面講的連接池道理非常類似,也是使用空間換時間的策略。可以將熱點數據置於緩存中,當用戶查詢這些數據時可以直接從緩存中得到,這無論如何也快過去數據庫中查詢。當然,緩存的置換策略等也會對系統性能產生重要影響,對於這個問題的討論已經超出了這里要闡述的范圍。
79、什么是DAO模式?
答:DAO(Data Access Object)顧名思義是一個為數據庫或其他持久化機制提供了抽象接口的對象,在不暴露底層持久化方案實現細節的前提下提供了各種數據訪問操作。在實際的開發中,應該將所有對數據源的訪問操作進行抽象化后封裝在一個公共API中。用程序設計語言來說,就是建立一個接口,接口中定義了此應用程序中將會用到的所有事務方法。在這個應用程序中,當需要和數據源進行交互的時候則使用這個接口,並且編寫一個單獨的類來實現這個接口,在邏輯上該類對應一個特定的數據存儲。DAO模式實際上包含了兩個模式,一是Data Accessor(數據訪問器),二是Data Object(數據對象),前者要解決如何訪問數據的問題,而后者要解決的是如何用對象封裝數據。
80、事務的ACID是指什么?
答:
- 原子性(Atomic):事務中各項操作,要么全做要么全不做,任何一項操作的失敗都會導致整個事務的失敗;
- 一致性(Consistent):事務結束后系統狀態是一致的;
- 隔離性(Isolated):並發執行的事務彼此無法看到對方的中間狀態;
- 持久性(Durable):事務完成后所做的改動都會被持久化,即使發生災難性的失敗。通過日志和同步備份可以在故障發生后重建數據。
補充:關於事務,在面試中被問到的概率是很高的,可以問的問題也是很多的。首先需要知道的是,只有存在並發數據訪問時才需要事務。當多個事務訪問同一數據時,可能會存在5類問題,包括3類數據讀取問題(臟讀、不可重復讀和幻讀)和2類數據更新問題(第1類丟失更新和第2類丟失更新)。
臟讀(Dirty Read):A事務讀取B事務尚未提交的數據並在此基礎上操作,而B事務執行回滾,那么A讀取到的數據就是臟數據。

不可重復讀(Unrepeatable Read):事務A重新讀取前面讀取過的數據,發現該數據已經被另一個已提交的事務B修改過了。

幻讀(Phantom Read):事務A重新執行一個查詢,返回一系列符合查詢條件的行,發現其中插入了被事務B提交的行。

第1類丟失更新:事務A撤銷時,把已經提交的事務B的更新數據覆蓋了。
| 時間 | 取款事務A | 轉賬事務B |
|---|---|---|
| T1 | 開始事務 | |
| T2 | 開始事務 | |
| T3 | 查詢賬戶余額為1000元 | |
| T4 | 查詢賬戶余額為1000元 | |
| T5 | 匯入100元修改余額為1100元 | |
| T6 | 提交事務 | |
| T7 | 取出100元將余額修改為900元 | |
| T8 | 撤銷事務 | |
| T9 | 余額恢復為1000元(丟失更新) |
第2類丟失更新:事務A覆蓋事務B已經提交的數據,造成事務B所做的操作丟失。
| 時間 | 轉賬事務A | 取款事務B |
|---|---|---|
| T1 | 開始事務 | |
| T2 | 開始事務 | |
| T3 | 查詢賬戶余額為1000元 | |
| T4 | 查詢賬戶余額為1000元 | |
| T5 | 取出100元將余額修改為900元 | |
| T6 | 提交事務 | |
| T7 | 匯入100元將余額修改為1100元 | |
| T8 | 提交事務 | |
| T9 | 查詢賬戶余額為1100元(丟失更新) |
數據並發訪問所產生的問題,在有些場景下可能是允許的,但是有些場景下可能就是致命的,數據庫通常會通過鎖機制來解決數據並發訪問問題,按鎖定對象不同可以分為表級鎖和行級鎖;按並發事務鎖定關系可以分為共享鎖和獨占鎖,具體的內容大家可以自行查閱資料進行了解。
直接使用鎖是非常麻煩的,為此數據庫為用戶提供了自動鎖機制,只要用戶指定會話的事務隔離級別,數據庫就會通過分析SQL語句然后為事務訪問的資源加上合適的鎖,此外,數據庫還會維護這些鎖通過各種手段提高系統的性能,這些對用戶來說都是透明的(就是說你不用理解,事實上我確實也不知道)。ANSI/ISO SQL 92標准定義了4個等級的事務隔離級別,如下表所示:
| 隔離級別 | 臟讀 | 不可重復讀 | 幻讀 | 第一類丟失更新 | 第二類丟失更新 |
|---|---|---|---|---|---|
| READ UNCOMMITED | 允許 | 允許 | 允許 | 不允許 | 允許 |
| READ COMMITTED | 不允許 | 允許 | 允許 | 不允許 | 允許 |
| REPEATABLE READ | 不允許 | 不允許 | 允許 | 不允許 | 不允許 |
| SERIALIZABLE | 不允許 | 不允許 | 不允許 | 不允許 | 不允許 |
需要說明的是,事務隔離級別和數據訪問的並發性是對立的,事務隔離級別越高並發性就越差。所以要根據具體的應用來確定合適的事務隔離級別,這個地方沒有萬能的原則。
81、JDBC中如何進行事務處理?
答:Connection提供了事務處理的方法,通過調用setAutoCommit(false)可以設置手動提交事務;當事務完成后用commit()顯式提交事務;如果在事務處理過程中發生異常則通過rollback()進行事務回滾。除此之外,從JDBC 3.0中還引入了Savepoint(保存點)的概念,允許通過代碼設置保存點並讓事務回滾到指定的保存點。

82、JDBC能否處理Blob和Clob?
答: Blob是指二進制大對象(Binary Large Object),而Clob是指大字符對象(Character Large Objec),因此其中Blob是為存儲大的二進制數據而設計的,而Clob是為存儲大的文本數據而設計的。JDBC的PreparedStatement和ResultSet都提供了相應的方法來支持Blob和Clob操作。下面的代碼展示了如何使用JDBC操作LOB:
下面以MySQL數據庫為例,創建一個張有三個字段的用戶表,包括編號(id)、姓名(name)和照片(photo),建表語句如下:
1 create table tb_user 2 ( 3 id int primary key auto_increment, 4 name varchar(20) unique not null, 5 photo longblob 6 );
下面的Java代碼向數據庫中插入一條記錄:
1 import java.io.FileInputStream; 2 import java.io.IOException; 3 import java.io.InputStream; 4 import java.sql.Connection; 5 import java.sql.DriverManager; 6 import java.sql.PreparedStatement; 7 import java.sql.SQLException; 8 9 class JdbcLobTest { 10 11 public static void main(String[] args) { 12 Connection con = null; 13 try { 14 // 1. 加載驅動(Java6以上版本可以省略) 15 Class.forName("com.mysql.jdbc.Driver"); 16 // 2. 建立連接 17 con = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123456"); 18 // 3. 創建語句對象 19 PreparedStatement ps = con.prepareStatement("insert into tb_user values (default, ?, ?)"); 20 ps.setString(1, "郭靖"); // 將SQL語句中第一個占位符換成字符串 21 try (InputStream in = new FileInputStream("test.jpg")) { // Java 7的TWR 22 ps.setBinaryStream(2, in); // 將SQL語句中第二個占位符換成二進制流 23 // 4. 發出SQL語句獲得受影響行數 24 System.out.println(ps.executeUpdate() == 1 ? "插入成功" : "插入失敗"); 25 } catch(IOException e) { 26 System.out.println("讀取照片失敗!"); 27 } 28 } catch (ClassNotFoundException | SQLException e) { // Java 7的多異常捕獲 29 e.printStackTrace(); 30 } finally { // 釋放外部資源的代碼都應當放在finally中保證其能夠得到執行 31 try { 32 if(con != null && !con.isClosed()) { 33 con.close(); // 5. 釋放數據庫連接 34 con = null; // 指示垃圾回收器可以回收該對象 35 } 36 } catch (SQLException e) { 37 e.printStackTrace(); 38 } 39 } 40 } 41 }
83、簡述正則表達式及其用途。
答:在編寫處理字符串的程序時,經常會有查找符合某些復雜規則的字符串的需要。正則表達式就是用於描述這些規則的工具。換句話說,正則表達式就是記錄文本規則的代碼。
說明:計算機誕生初期處理的信息幾乎都是數值,但是時過境遷,今天我們使用計算機處理的信息更多的時候不是數值而是字符串,正則表達式就是在進行字符串匹配和處理的時候最為強大的工具,絕大多數語言都提供了對正則表達式的支持。
84、Java中是如何支持正則表達式操作的?
答:Java中的String類提供了支持正則表達式操作的方法,包括:matches()、replaceAll()、replaceFirst()、split()。此外,Java中可以用Pattern類表示正則表達式對象,它提供了豐富的API進行各種正則表達式操作,請參考下面面試題的代碼。
面試題: - 如果要從字符串中截取第一個英文左括號之前的字符串,例如:北京市(朝陽區)(西城區)(海淀區),截取結果為:北京市,那么正則表達式怎么寫?
1 import java.util.regex.Matcher; 2 import java.util.regex.Pattern; 3 4 class RegExpTest { 5 6 public static void main(String[] args) { 7 String str = "北京市(朝陽區)(西城區)(海淀區)"; 8 Pattern p = Pattern.compile(".*?(?=\\()"); 9 Matcher m = p.matcher(str); 10 if(m.find()) { 11 System.out.println(m.group()); 12 } 13 } 14 }
說明:上面的正則表達式中使用了懶惰匹配和前瞻,如果不清楚這些內容,推薦讀一下網上很有名的《正則表達式30分鍾入門教程》。
85、獲得一個類的類對象有哪些方式?
答:
- 方法1:類型.class,例如:String.class
- 方法2:對象.getClass(),例如:"hello".getClass()
- 方法3:Class.forName(),例如:Class.forName("java.lang.String")
86、如何通過反射創建對象?
答:
- 方法1:通過類對象調用newInstance()方法,例如:String.class.newInstance()
- 方法2:通過類對象的getConstructor()或getDeclaredConstructor()方法獲得構造器(Constructor)對象並調用其newInstance()方法創建對象,例如:String.class.getConstructor(String.class).newInstance("Hello");
87、如何通過反射獲取和設置對象私有字段的值?
答:可以通過類對象的getDeclaredField()方法獲得字段(Field)對象,然后再通過字段對象的setAccessible(true)將其設置為可以訪問,接下來就可以通過get/set方法來獲取/設置字段的值了。下面的代碼實現了一個反射的工具類,其中的兩個靜態方法分別用於獲取和設置私有字段的值,字段可以是基本類型也可以是對象類型且支持多級對象操作,例如ReflectionUtil.get(dog, "owner.car.engine.id");可以獲得dog對象的主人的汽車的引擎的ID號。
1 import java.lang.reflect.Constructor; 2 import java.lang.reflect.Field; 3 import java.lang.reflect.Modifier; 4 import java.util.ArrayList; 5 import java.util.List; 6 7 /** 8 * 反射工具類 9 * @author nnngu 10 * 11 */ 12 public class ReflectionUtil { 13 14 private ReflectionUtil() { 15 throw new AssertionError(); 16 } 17 18 /** 19 * 通過反射取對象指定字段(屬性)的值 20 * @param target 目標對象 21 * @param fieldName 字段的名字 22 * @throws 如果取不到對象指定字段的值則拋出異常 23 * @return 字段的值 24 */ 25 public static Object getValue(Object target, String fieldName) { 26 Class<?> clazz = target.getClass(); 27 String[] fs = fieldName.split("\\."); 28 29 try { 30 for(int i = 0; i < fs.length - 1; i++) { 31 Field f = clazz.getDeclaredField(fs[i]); 32 f.setAccessible(true); 33 target = f.get(target); 34 clazz = target.getClass(); 35 } 36 37 Field f = clazz.getDeclaredField(fs[fs.length - 1]); 38 f.setAccessible(true); 39 return f.get(target); 40 } 41 catch (Exception e) { 42 throw new RuntimeException(e); 43 } 44 } 45 46 /** 47 * 通過反射給對象的指定字段賦值 48 * @param target 目標對象 49 * @param fieldName 字段的名稱 50 * @param value 值 51 */ 52 public static void setValue(Object target, String fieldName, Object value) { 53 Class<?> clazz = target.getClass(); 54 String[] fs = fieldName.split("\\."); 55 try { 56 for(int i = 0; i < fs.length - 1; i++) { 57 Field f = clazz.getDeclaredField(fs[i]); 58 f.setAccessible(true); 59 Object val = f.get(target); 60 if(val == null) { 61 Constructor<?> c = f.getType().getDeclaredConstructor(); 62 c.setAccessible(true); 63 val = c.newInstance(); 64 f.set(target, val); 65 } 66 target = val; 67 clazz = target.getClass(); 68 } 69 70 Field f = clazz.getDeclaredField(fs[fs.length - 1]); 71 f.setAccessible(true); 72 f.set(target, value); 73 } 74 catch (Exception e) { 75 throw new RuntimeException(e); 76 } 77 } 78 79 }
88、如何通過反射調用對象的方法?
答:請看下面的代碼:
1 import java.lang.reflect.Method; 2 3 class MethodInvokeTest { 4 5 public static void main(String[] args) throws Exception { 6 String str = "hello"; 7 Method m = str.getClass().getMethod("toUpperCase"); 8 System.out.println(m.invoke(str)); // HELLO 9 } 10 }
89、簡述一下面向對象的"六原則一法則"。
答:
- 單一職責原則:一個類只做它該做的事情。(單一職責原則想表達的就是"高內聚",寫代碼最終極的原則只有六個字"高內聚、低耦合",就如同葵花寶典或辟邪劍譜的中心思想就八個字"欲練此功必先自宮",所謂的高內聚就是一個代碼模塊只完成一項功能,在面向對象中,如果只讓一個類完成它該做的事,而不涉及與它無關的領域就是踐行了高內聚的原則,這個類就只有單一職責。我們都知道一句話叫"因為專注,所以專業",一個對象如果承擔太多的職責,那么注定它什么都做不好。這個世界上任何好的東西都有兩個特征,一個是功能單一,好的相機絕對不是電視購物里面賣的那種一個機器有一百多種功能的,它基本上只能照相;另一個是模塊化,好的自行車是組裝車,從減震叉、剎車到變速器,所有的部件都是可以拆卸和重新組裝的,好的乒乓球拍也不是成品拍,一定是底板和膠皮可以拆分和自行組裝的,一個好的軟件系統,它里面的每個功能模塊也應該是可以輕易的拿到其他系統中使用的,這樣才能實現軟件復用的目標。)
- 開閉原則:軟件實體應當對擴展開放,對修改關閉。(在理想的狀態下,當我們需要為一個軟件系統增加新功能時,只需要從原來的系統派生出一些新類就可以,不需要修改原來的任何一行代碼。要做到開閉有兩個要點:①抽象是關鍵,一個系統中如果沒有抽象類或接口系統就沒有擴展點;②封裝可變性,將系統中的各種可變因素封裝到一個繼承結構中,如果多個可變因素混雜在一起,系統將變得復雜而混亂,如果不清楚如何封裝可變性,可以參考《設計模式精解》一書中對橋梁模式的講解的章節。)
- 依賴倒轉原則:面向接口編程。(該原則說得直白和具體一些就是聲明方法的參數類型、方法的返回類型、變量的引用類型時,盡可能使用抽象類型而不用具體類型,因為抽象類型可以被它的任何一個子類型所替代,請參考下面的里氏替換原則。)
里氏替換原則:任何時候都可以用子類型替換掉父類型。(關於里氏替換原則的描述,Barbara Liskov女士的描述比這個要復雜得多,但簡單的說就是能用父類型的地方就一定能使用子類型。里氏替換原則可以檢查繼承關系是否合理,如果一個繼承關系違背了里氏替換原則,那么這個繼承關系一定是錯誤的,需要對代碼進行重構。例如讓貓繼承狗,或者狗繼承貓,又或者讓正方形繼承長方形都是錯誤的繼承關系,因為你很容易找到違反里氏替換原則的場景。需要注意的是:子類一定是增加父類的能力而不是減少父類的能力,因為子類比父類的能力更多,把能力多的對象當成能力少的對象來用當然沒有任何問題。) - 接口隔離原則:接口要小而專,絕不能大而全。(臃腫的接口是對接口的污染,既然接口表示能力,那么一個接口只應該描述一種能力,接口也應該是高度內聚的。例如,琴棋書畫就應該分別設計為四個接口,而不應設計成一個接口中的四個方法,因為如果設計成一個接口中的四個方法,那么這個接口很難用,畢竟琴棋書畫四樣都精通的人還是少數,而如果設計成四個接口,會幾項就實現幾個接口,這樣的話每個接口被復用的可能性是很高的。Java中的接口代表能力、代表約定、代表角色,能否正確的使用接口一定是編程水平高低的重要標識。)
- 合成聚合復用原則:優先使用聚合或合成關系復用代碼。(通過繼承來復用代碼是面向對象程序設計中被濫用得最多的東西,因為所有的教科書都無一例外的對繼承進行了鼓吹從而誤導了初學者,類與類之間簡單的說有三種關系,Is-A關系、Has-A關系、Use-A關系,分別代表繼承、關聯和依賴。其中,關聯關系根據其關聯的強度又可以進一步划分為關聯、聚合和合成,但說白了都是Has-A關系,合成聚合復用原則想表達的是優先考慮Has-A關系而不是Is-A關系復用代碼,原因嘛可以自己從百度上找到一萬個理由,需要說明的是,即使在Java的API中也有不少濫用繼承的例子,例如Properties類繼承了Hashtable類,Stack類繼承了Vector類,這些繼承明顯就是錯誤的,更好的做法是在Properties類中放置一個Hashtable類型的成員並且將其鍵和值都設置為字符串來存儲數據,而Stack類的設計也應該是在Stack類中放一個Vector對象來存儲數據。記住:任何時候都不要繼承工具類,工具是可以擁有並可以使用的,而不是拿來繼承的。)
- 迪米特法則:迪米特法則又叫最少知識原則,一個對象應當對其他對象有盡可能少的了解。(迪米特法則簡單的說就是如何做到"低耦合",門面模式和調停者模式就是對迪米特法則的踐行。對於門面模式可以舉一個簡單的例子,你去一家公司洽談業務,你不需要了解這個公司內部是如何運作的,你甚至可以對這個公司一無所知,去的時候只需要找到公司入口處的前台美女,告訴她們你要做什么,她們會找到合適的人跟你接洽,前台的美女就是公司這個系統的門面。再復雜的系統都可以為用戶提供一個簡單的門面,Java Web開發中作為前端控制器的Servlet或Filter不就是一個門面嗎,瀏覽器對服務器的運作方式一無所知,但是通過前端控制器就能夠根據你的請求得到相應的服務。調停者模式也可以舉一個簡單的例子來說明,例如一台計算機,CPU、內存、硬盤、顯卡、聲卡各種設備需要相互配合才能很好的工作,但是如果這些東西都直接連接到一起,計算機的布線將異常復雜,在這種情況下,主板作為一個調停者的身份出現,它將各個設備連接在一起而不需要每個設備之間直接交換數據,這樣就減小了系統的耦合度和復雜度,如下圖所示。迪米特法則用通俗的話來將就是不要和陌生人打交道,如果真的需要,找一個自己的朋友,讓他替你和陌生人打交道。)


90、簡述一下你了解的設計模式。
答:所謂設計模式,就是一套被反復使用的代碼設計經驗的總結(情境中一個問題經過證實的一個解決方案)。使用設計模式是為了可重用代碼、讓代碼更容易被他人理解、保證代碼可靠性。設計模式使人們可以更加簡單方便的復用成功的設計和體系結構。將已證實的技術表述成設計模式也會使新系統開發者更加容易理解其設計思路。
在GoF的《Design Patterns: Elements of Reusable Object-Oriented Software》中給出了三類(創建型[對類的實例化過程的抽象化]、結構型[描述如何將類或對象結合在一起形成更大的結構]、行為型[對在不同的對象之間划分責任和算法的抽象化])共23種設計模式,包括:Abstract Factory(抽象工廠模式),Builder(建造者模式),Factory Method(工廠方法模式),Prototype(原始模型模式),Singleton(單例模式);Facade(門面模式),Adapter(適配器模式),Bridge(橋梁模式),Composite(合成模式),Decorator(裝飾模式),Flyweight(享元模式),Proxy(代理模式);Command(命令模式),Interpreter(解釋器模式),Visitor(訪問者模式),Iterator(迭代子模式),Mediator(調停者模式),Memento(備忘錄模式),Observer(觀察者模式),State(狀態模式),Strategy(策略模式),Template Method(模板方法模式), Chain Of Responsibility(責任鏈模式)。
面試被問到關於設計模式的知識時,可以揀最常用的作答,例如:
- 工廠模式:工廠類可以根據條件生成不同的子類實例,這些子類有一個公共的抽象父類並且實現了相同的方法,但是這些方法針對不同的數據進行了不同的操作(多態方法)。當得到子類的實例后,開發人員可以調用基類中的方法而不必考慮到底返回的是哪一個子類的實例。
- 代理模式:給一個對象提供一個代理對象,並由代理對象控制原對象的引用。實際開發中,按照使用目的的不同,代理可以分為:遠程代理、虛擬代理、保護代理、Cache代理、防火牆代理、同步化代理、智能引用代理。
- 適配器模式:把一個類的接口變換成客戶端所期待的另一種接口,從而使原本因接口不匹配而無法在一起使用的類能夠一起工作。
- 模板方法模式:提供一個抽象類,將部分邏輯以具體方法或構造器的形式實現,然后聲明一些抽象方法來迫使子類實現剩余的邏輯。不同的子類可以以不同的方式實現這些抽象方法(多態實現),從而實現不同的業務邏輯。
除此之外,還可以講講上面提到的門面模式、橋梁模式、單例模式、裝潢模式(Collections工具類和I/O系統中都使用裝潢模式)等,反正基本原則就是揀自己最熟悉的、用得最多的作答,以免言多必失。
91、用Java寫一個單例類。
答:
- 餓漢式單例
1 public class Singleton { 2 private Singleton(){} 3 private static Singleton instance = new Singleton(); 4 public static Singleton getInstance(){ 5 return instance; 6 } 7 }
- 懶漢式單例
1 public class Singleton { 2 private static Singleton instance = null; 3 private Singleton() {} 4 public static synchronized Singleton getInstance(){ 5 if (instance == null) instance = new Singleton(); 6 return instance; 7 } 8 }
注意:實現一個單例有兩點注意事項,①將構造器私有,不允許外界通過構造器創建對象;②通過公開的靜態方法向外界返回類的唯一實例。這里有一個問題可以思考:Spring的IoC容器可以為普通的類創建單例,它是怎么做到的呢?
92、什么是UML?
答:UML是統一建模語言(Unified Modeling Language)的縮寫,它發表於1997年,綜合了當時已經存在的面向對象的建模語言、方法和過程,是一個支持模型化和軟件系統開發的圖形化語言,為軟件開發的所有階段提供模型化和可視化支持。使用UML可以幫助溝通與交流,輔助應用設計和文檔的生成,還能夠闡釋系統的結構和行為。
93、UML中有哪些常用的圖?
答:UML定義了多種圖形化的符號來描述軟件系統部分或全部的靜態結構和動態結構,包括:用例圖(use case diagram)、類圖(class diagram)、時序圖(sequence diagram)、協作圖(collaboration diagram)、狀態圖(statechart diagram)、活動圖(activity diagram)、構件圖(component diagram)、部署圖(deployment diagram)等。在這些圖形化符號中,有三種圖最為重要,分別是:用例圖(用來捕獲需求,描述系統的功能,通過該圖可以迅速的了解系統的功能模塊及其關系)、類圖(描述類以及類與類之間的關系,通過該圖可以快速了解系統)、時序圖(描述執行特定任務時對象之間的交互關系以及執行順序,通過該圖可以了解對象能接收的消息也就是說對象能夠向外界提供的服務)。
用例圖:

類圖:

時序圖:

94、用Java寫一個冒泡排序。
答:冒泡排序幾乎是個程序員都寫得出來,但是面試的時候如何寫一個逼格高的冒泡排序卻不是每個人都能做到,下面提供一個參考代碼:
1 import java.util.Comparator; 2 3 /** 4 * 排序器接口(策略模式: 將算法封裝到具有共同接口的獨立的類中使得它們可以相互替換) 5 * @author nnngu 6 * 7 */ 8 public interface Sorter { 9 10 /** 11 * 排序 12 * @param list 待排序的數組 13 */ 14 public <T extends Comparable<T>> void sort(T[] list); 15 16 /** 17 * 排序 18 * @param list 待排序的數組 19 * @param comp 比較兩個對象的比較器 20 */ 21 public <T> void sort(T[] list, Comparator<T> comp); 22 }
1 import java.util.Comparator; 2 3 /** 4 * 冒泡排序 5 * 6 * @author nnngu 7 * 8 */ 9 public class BubbleSorter implements Sorter { 10 11 @Override 12 public <T extends Comparable<T>> void sort(T[] list) { 13 boolean swapped = true; 14 for (int i = 1, len = list.length; i < len && swapped; ++i) { 15 swapped = false; 16 for (int j = 0; j < len - i; ++j) { 17 if (list[j].compareTo(list[j + 1]) > 0) { 18 T temp = list[j]; 19 list[j] = list[j + 1]; 20 list[j + 1] = temp; 21 swapped = true; 22 } 23 } 24 } 25 } 26 27 @Override 28 public <T> void sort(T[] list, Comparator<T> comp) { 29 boolean swapped = true; 30 for (int i = 1, len = list.length; i < len && swapped; ++i) { 31 swapped = false; 32 for (int j = 0; j < len - i; ++j) { 33 if (comp.compare(list[j], list[j + 1]) > 0) { 34 T temp = list[j]; 35 list[j] = list[j + 1]; 36 list[j + 1] = temp; 37 swapped = true; 38 } 39 } 40 } 41 } 42 }
95、用Java寫一個折半查找。
答:折半查找,也稱二分查找、二分搜索,是一種在有序數組中查找某一特定元素的搜索算法。搜素過程從數組的中間元素開始,如果中間元素正好是要查找的元素,則搜素過程結束;如果某一特定元素大於或者小於中間元素,則在數組大於或小於中間元素的那一半中查找,而且跟開始一樣從中間元素開始比較。如果在某一步驟數組已經為空,則表示找不到指定的元素。這種搜索算法每一次比較都使搜索范圍縮小一半,其時間復雜度是O(logN)。
1 import java.util.Comparator; 2 3 public class MyUtil { 4 5 public static <T extends Comparable<T>> int binarySearch(T[] x, T key) { 6 return binarySearch(x, 0, x.length- 1, key); 7 } 8 9 // 使用循環實現的二分查找 10 public static <T> int binarySearch(T[] x, T key, Comparator<T> comp) { 11 int low = 0; 12 int high = x.length - 1; 13 while (low <= high) { 14 int mid = (low + high) >>> 1; 15 int cmp = comp.compare(x[mid], key); 16 if (cmp < 0) { 17 low= mid + 1; 18 } 19 else if (cmp > 0) { 20 high= mid - 1; 21 } 22 else { 23 return mid; 24 } 25 } 26 return -1; 27 } 28 29 // 使用遞歸實現的二分查找 30 private static<T extends Comparable<T>> int binarySearch(T[] x, int low, int high, T key) { 31 if(low <= high) { 32 int mid = low + ((high - low) >> 1); 33 if(key.compareTo(x[mid])== 0) { 34 return mid; 35 } 36 else if(key.compareTo(x[mid])< 0) { 37 return binarySearch(x,low, mid - 1, key); 38 } 39 else { 40 return binarySearch(x,mid + 1, high, key); 41 } 42 } 43 return -1; 44 } 45 }
說明:上面的代碼中給出了折半查找的兩個版本,一個用遞歸實現,一個用循環實現。需要注意的是計算中間位置時不應該使用(high+ low) / 2的方式,因為加法運算可能導致整數越界,這里應該使用以下三種方式之一:low + (high - low) / 2或low + (high – low) >> 1或(low + high) >>> 1(>>>是邏輯右移,是不帶符號位的右移)
