Java面试宝典————基础篇
參考原文:《Java面試題全集(上)》
1.Java中的基本數據類型有哪些?
類型:byte short int long float double boolean char
字節:1? ? ? ? 2? ? ? 4? ? ?8? ? ? 4? ? ? ? 8? ? ? ? ? ?1? ? ? ? ??2
2.面向對象的特征有哪些?
抽象、繼承、封裝、多態。
????? ? 抽象是將一類對象的共同特征總結出來的過程。包括數據抽象和行為抽象兩方面。抽象只關注對象有哪些屬性和行為,并不關注這些行為的細節是什么。
????? ? 繼承是從已有類得到繼承信息創建新類的過程。提供繼承信息的類被稱為父類;得到繼承信息的類被稱為子類。
????? ? 封裝是把數據和操作數據的方法綁定起來,對數據的訪問只能通過已定義的接口。將具體的實現細節對調用者隱藏掉。
????? ? 多態是指允許不同的子類型的對象對同一消息做出不同的響應。工廠方法模式中就是充分利用了多態的特性,結合一些具體的實現細節,比如繼承、方法重載等,根據里式替換原則,在調用父類的地方使用子類對象,可以實現更多樣的行為。
3.訪問修飾符有哪些?區別是什么?
4.String是基本數據類型嗎?對String類型的理解。
String不是基本類型,是引用類型,基本類型只有八個。Java中類型包括基本類型、引用類型 和 枚舉類型。
String類是java.lang包下的重要組成。
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {java程序中的所有字符串字面量(如“abc”)都作為此類的實例。
字符串是常亮:意味著它們的值在創建之后不能更改。字符串緩沖區StringBuffer支持可變的字符串。因為String對象是不可變的,所以可以共享。
由于String內部實際上是一個char數組,因此:
String str = "abc";等效于: char data[] = {'a', 'b', 'c'}; String str = new String(data);String的equals方法
public boolean equals(Object anObject)參數是String類型且與此字符串表示相同字符序列,結果為true。即比較的是內容,而不是內存地址。
下面這句代碼在內存中創建了幾個對象?
String s1 = new String("abc");//創建了兩個對象,分別保存在常量池和堆內存中new進堆內存,常量池一個對象“abc”,堆內存一個對象“abc”的一個副本。這樣是創建了兩個對象。
內存中有一個常量池,當我們創建常量時,系統先看常量池中有沒有這個常量,如果有,則不創建對象,直接引用這個常量。
另外,java語言中的“+”字符串拼接符號,是通過StringBuilder和append來實現的,而且是每一個“+”都會創建一個StringBuilder對象,因此效率非常低。但是這里注意,通過+拼接雖然是通過在創建StringBuilder來實現,但是最終生成的字符串并不是存儲在堆內存中,而是仍然保存在常量池中,測試代碼如下:
String s = "abc" + "d"; String s1 = "abcd"; System.out.println(s == s1); // true5.float f = 3.4;是否正確?
答:不正確。3.4是雙精度數,將雙精度型(double)賦值給浮點型(float)屬于向下轉型(down-casting,也稱為窄化)會造成精度損失,因此需要強制類型轉換float f = (float)3.4;或者寫成float f = 3.4f; ?。
6.int和Integer有什么區別?
答:Java語言雖然是面向對象的編程語言,但是為了操作方便,依然引入了int等并不是表示對象的八種基本數據類型。讓int等基本數據類型可以像其他對象那樣完成某些動作,Java將八大基本數據類型都配備了封裝類,Integer就是int的封裝類。并且添加了自動拆裝箱特性,開發者可以輕松的實現int與Integer之間的轉換。
下面一個面試題:
Integer f1 = 100, f2 = 100, f3 = 150, f4 = 150; System.out.println(f1 == f2);// true System.out.println(f3 == f4);// false解析:
當一個int類型賦值給Integer對象的時候會隱式地調用valueOf方法,而valueOf方法內部又使用了一個叫IntegerCache 的靜態內部類。這個類完成了一系列靜態功能,使得:如果整型字面量的值在 -128到127之間,那么不會new新的Integer對象,而是直接引用常量池中的Integer對象。
7. &和&& 的區別?
答:&&是“短路與”邏輯運算符,左面的表達式如果是false那么程序不會繼續判斷右面的表達式的值,而&不管左面的值是否為false都會完成兩遍的邏輯計算。在大多數場合下我們需要的是&&而不是&。
8.Math.round(11.5) 等于多少?Math.round(-11.5)等于多少?
答:Math.round(11.5)的返回值是12,Math.round(-11.5)的返回值是-11。四舍五入的原理是在參數上加0.5然后進行下取整。
9.switch 是否能作用在byte 上,是否能作用在long 上,是否能作用在String上?
答:在Java 5以前,switch(expr)中,expr只能是byte、short、char、int。從Java 5開始,Java中引入了枚舉類型,expr也可以是enum類型,從Java 7開始,expr還可以是字符串(String),但是長整型(long)在目前所有的版本中都是不可以的。
10.用最有效率的方法計算2乘以8?
答: 2 << 3(左移3位相當于乘以2的3次方,右移3位相當于除以2的3次方)。
11.數組有沒有length()方法?String有沒有length()方法?
答:數組沒有length()方法,有length 的屬性。String 有length()方法。JavaScript中,獲得字符串的長度是通過length屬性得到的。
12.構造器(constructor)是否可被重寫(override)?
答:構造器不能被繼承,因此不能被重寫,但可以被重載。
13.請說明一下equals()方法和hash code的關系,實現equals()方法的重寫。
答:如果兩個對象x和y滿足x.equals(y),那么他們的哈希碼應當相同。
Java對于?equals方法和hashCode方法是這樣規定的:
(1)如果兩個對象相同(equals方法返回true),那么它們的hashCode值一定要相同; (2)如果兩個對象的hashCode相同,它們并不一定相同。equals()方法的特性:
自反性(x.equals(x)必須返回true); 對稱性(x.equals(y)返回true時,y.equals(x)也必須返回true); 傳遞性(x.equals(y)和y.equals(z)都返回true時,x.equals(z)也必須返回true); 一致性(當x和y引用的對象信息沒有被修改時,多次調用x.equals(y)應該得到同樣的返回值); 對于任何非null值的引用x,x.equals(null)必須返回false。equals()方法和hashCode()方法重寫:
//經典方式(17和31散列碼思想) public class User {private String name;private int age;private String passport;//getters and setters, constructor@Overridepublic boolean equals(Object o) {if (o == this) return true;if (!(o instanceof User)) {return false;}User user = (User) o;return user.name.equals(name) &&user.age == age &&user.passport.equals(passport);}//Idea from effective Java : Item 9@Overridepublic int hashCode() {int result = 17;result = 31 * result + name.hashCode();result = 31 * result + age;result = 31 * result + passport.hashCode();return result;} } //JDK7+ import java.util.Objects;public class User {private String name;private int age;private String passport;//getters and setters, constructor@Overridepublic boolean equals(Object o) {if (o == this) return true;if (!(o instanceof User)) {return false;}User user = (User) o;return age == user.age &&Objects.equals(name, user.name) &&Objects.equals(passport, user.passport);}@Overridepublic int hashCode() {return Objects.hash(name, age, passport);} }14.是否可以繼承String類?
答:String 類是final類,不可以被繼承。繼承String本身就是一個錯誤的行為,對String類型最好的重用方式是關聯關系(Has-A)和依賴關系(Use-A)而不是繼承關系(Is-A)。
15.當一個對象被當做參數傳遞到一個方法后,此方法可改變這個對象的屬性,并可返回變化后的結果,那么這里到底是值傳遞還是引用傳遞?
答:是值傳遞。Java語言的方法調用只支持參數的值傳遞。不論是基本類型(int)、特殊的引用類型(String)還是Object類型,Java在傳參時都是將參數拷貝一份傳入方法體。
public class Demo {public static void main(String[] args) {int i = 0;change(i);// 將變量直接傳入System.out.println(i);// 0 調用方}public static void change(int i){i = 10;} } public class Demo {public static void main(String[] args) {A a = new A();System.out.println(a.age);// 0 change(a);System.out.println(a.age);// 10 Wrapper類A將age進行封裝}public static void change(A a){a.age = 10;} }class A {public int age; }Object類型的參數可以被方法內部的邏輯改變,但是基本類型和String無法被改變。
理由是,首先Java中的方法調用是采用值傳遞的方式,Java會將參數拷貝一份傳入方法體,不論這個參數是什么類型。基本類型變量存儲在棧中,拷貝一份后,其內存地址不同,改變了拷貝變量的數據不會影響原來的內存地址中的數據,因此不會改變;
引用類型的變量在內存中我們需要看作是兩部分:引用和對象。而引用也存儲在棧中,對象則存儲在堆中。
String變量,傳參時,會將棧中的引用拷貝一份傳入方法體,雖然引用指向了一個地址,方法體中對這個地址內的數據進行修改,但是別忘了,String本身是不可變對象,對String變量的修改只會生成新的變量,所以原來的內存地址還是存放未改變的變量。
Object變量,同樣的,傳參時依然是將參數的引用拷貝一份,這個拷貝的引用和原來的引用雖然在棧中存放的地址(引用的地址)不同了,但他們都指向著堆中的同一塊內存地址,因此在方法體中修改引用的值,實際上還是對原來地址中的對象進行修改,所以原來引用指向的堆內存中的變量是會被改變的。
綜上,其實我們確實可以說Java中是只有值傳遞的,但是對于Object類型的傳參,或許也可以說是引用傳遞吧。
16.抽象類與接口異同?
同: 1.抽象類和接口可以定義引用,但是不能直接實例化。 2.一個類繼承了抽象類或接口,除非是其本身也是抽象類,否則,必須實現全部抽象方法。 異: 1.抽象類中可以定義構造器,可以有抽象方法和具體方法,而接口中不能定義構造器,方法也必須全是抽象方法。 2.抽象類的成員可以由四種訪問權限修飾,但是接口中的成員全是public 3.抽象類中可以定義成員變量,接口中定義的成員變量實際上都是常量。 4.有抽象方法,一定是抽象類,但反過來抽象類,不一定有抽象方法。 5.繼承關系:接口可以繼承接口,抽象類可以實現接口,接口不能繼承抽象類17.靜態嵌套類(Static Nested Class)與內部類(Inner Class)的區別?
18.抽象方法能否被static修飾?能否被Native修飾?能否被synchronized修飾?
答:都不能。抽象方法需要子類重寫,static修飾的類靜態類無法被重寫。Native是用來修飾本地方法的修飾符,不屬于Java代碼。synchronized和方法的實現細節有關,抽象方法沒有方法體,即沒有實現細節,因此無法被synchronized修飾。
19.靜態方法能否調用非靜態方法?
答:不可以,靜態方法只能訪問靜態成員,因為非靜態方法的調用首先要創建對象,在調用靜態方法時,可能對象并沒有被正確的初始化。
20.如何實現對象克隆?
《Java常用設計模式————原型模式(二)之深拷貝與淺拷貝》
21.GC是什么?為什么要有GC?
GC意思是垃圾收集,C語言等不存在GC的概念,編程人員需要手動回收內存,忘記或者錯誤的內存回收都會導致程序的不穩定甚至崩潰。Java提供了GC功能可以自動檢測對象是否超過作用域從而達到自動回收內存的目的,因此,Java語言也沒有提供釋放已分配內存的顯式操作方法。Java程序員不用擔心回收內存的問題,但是依然可以調用System.gc()或Runtime.getRuntime().gc()。垃圾回收可以有效的防止內存泄漏。
補充:垃圾回收機制有很多種,包括:分代復制垃圾回收、標記垃圾回收、增量垃圾回收等方式。標準的Java進程既有棧又有堆。棧保存了原始型局部變量,堆保存了要創建的對象。Java平臺對堆內存回收和再利用的基本算法被稱為標記和清除,但是Java對其進行了改進,采用“分代式垃圾收集”。這種方法會跟Java對象的生命周期將堆內存劃分為不同的區域,在垃圾收集過程中,可能會將對象移動到不同區域:
- 伊甸園(Eden):這是對象最初誕生的區域,并且對大多數對象來說,這里是它們唯一存在過的區域。
- 幸存者樂園(Survivor):從伊甸園幸存下來的對象會被挪到這里。
- 終身頤養園(Tenured):這是足夠老的幸存對象的歸宿。年輕代收集(Minor-GC)過程是不會觸及這個地方的。當年輕代收集不能把對象放進終身頤養園時,就會觸發一次完全收集(Major-GC),這里可能還會牽扯到壓縮,以便為大對象騰出足夠的空間。
?
與垃圾回收相關的JVM參數:
- -Xms / -Xmx — 堆的初始大小 / 堆的最大大小
- -Xmn — 堆中年輕代的大小
- -XX:-DisableExplicitGC — 讓System.gc()不產生任何作用
- -XX:+PrintGCDetails — 打印GC的細節
- -XX:+PrintGCDateStamps — 打印GC操作的時間戳
- -XX:NewSize / XX:MaxNewSize — 設置新生代大小/新生代最大大小
- -XX:NewRatio — 可以設置老生代和新生代的比例
- -XX:PrintTenuringDistribution — 設置每次新生代GC后輸出幸存者樂園中對象年齡的分布
- -XX:InitialTenuringThreshold / -XX:MaxTenuringThreshold:設置老年代閥值的初始值和最大值
- -XX:TargetSurvivorRatio:設置幸存區的目標使用率
22.接口、抽象類和普通類之間的繼承和實現關系是怎樣的?
答:接口可以繼承接口,而且支持多重繼承。抽象類可以實現(implements)接口,抽象類可繼承具體類也可以繼承抽象類。
23.一個”.java”源文件中是否可以包含多個類(不是內部類)?有什么限制?
答:可以有多個類,但是一個java文件只能有一個public class 而且這個public class的類名必須與java文件名一致。
24.說出下面代碼的執行結果:
class A {static {System.out.print("1");}public A() {System.out.print("2");} }class B extends A {static {System.out.print("a");}public B() {System.out.print("b");} }public class Box {public static void main(String[] args) {A ab = new B();ab = new B();} }答:1a2b2b。new創建對象時,構造器的調用順序是:先初始化靜態成員,然后調用父類構造器,再初始化非靜態成員,最后調用自身構造器。
25.如何將String轉成基本數據類型?
// String轉intint num = Integer.parseInt("100");// String轉double或floatdouble num2 = Double.parseDouble("100.5");// String轉charchar ch = "中".charAt(0);// String轉booleanboolean flag = Boolean.parseBoolean("true");補充:將基本數據類型轉成String可以通過String.valueOf()或者通過加號拼接空字符串“”,兩種方式來實現。
?
26.怎樣將GB2312編碼的字符串轉換為ISO-8859-1編碼的字符串?
?
String s1 = "你好"; String s2 = new String(s1.getBytes("GB2312"), "ISO-8859-1");27.String對象的char數組有參構造應該如何使用?
String構造器:public String(char value[]) ,允許傳入一個char數組,但是大家是否寫過這樣的代碼:
String myName = new String({'一','個','叉','數','組'});答案是肯定沒有,因為這樣的代碼是不符合Java語法規范的。必須要將char數組聲明才可以傳入String構造器,如下:
char[] chs = {'一','個','叉','數','組'}; String myName = new String(chs);而且,通過char數組初始化String也只有這樣一種寫法,因為如下的寫法是沒有任何意義的:
String myName = new String(new char[5]);代碼中的myName變量,既不等于空字符串,也無法對其修改。(實際上,new String(new char[0]).equals("")為true)
28.日期和時間:
- 如何獲取年月日、時分秒?
- 如何獲取從1970年1月1日0時0分0秒到現在的毫秒值?
- 如何獲取某月的最后一天?
- 如何格式化日期?
答:
問題一:創建java.util.Calendar實例,調用其get()方法傳入不同的參數即可獲取參數對應的值。
Java8中可使用java.time。LocalDateTime來獲取,代碼如下:
public class DateTimeTest {public static void main(String[] args) {Calendar cal = Calendar.getInstance();System.out.println(cal.get(Calendar.YEAR));System.out.println(cal.get(Calendar.MONTH)); // 0 - 11System.out.println(cal.get(Calendar.DATE));System.out.println(cal.get(Calendar.HOUR_OF_DAY));System.out.println(cal.get(Calendar.MINUTE));System.out.println(cal.get(Calendar.SECOND));// Java 8LocalDateTime dt = LocalDateTime.now();System.out.println(dt.getYear());System.out.println(dt.getMonthValue()); // 1 - 12System.out.println(dt.getDayOfMonth());System.out.println(dt.getHour());System.out.println(dt.getMinute());System.out.println(dt.getSecond());} }問題2:以下方法均可獲取該毫秒值。
Calendar.getInstance().getTimeInMillis(); System.currentTimeMillis(); Clock.systemDefaultZone().millis(); // Java 8問題3:代碼如下所示:
Calendar time = Calendar.getInstance(); time.getActualMaximum(Calendar.DAY_OF_MONTH);問題4:利用java.text.DateFormat的子類(如SimpleDateFormat)中的format(Date) 方法可將日期格式化。Java8中可以用java.time.format.DateTimeFormatter來格式化時間日期,代碼如下:
import java.text.SimpleDateFormat; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.Date;class DateFormatTest {public static void main(String[] args) {SimpleDateFormat oldFormatter = new SimpleDateFormat("yyyy/MM/dd");Date date1 = new Date();System.out.println(oldFormatter.format(date1));// Java 8DateTimeFormatter newFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");LocalDate date2 = LocalDate.now();System.out.println(date2.format(newFormatter));} }補充:java的時間日期api一直以來都是被詬病的東西,為了解決這一問題,Java8中引入了新的日期時間api,其中包括LocalDate、LocalTime、LocalDateTime、Clock、Instant等類,這些類的設計都使用了“不變模式”,因此是線程安全的。如果不理解這些內容,可以參考轉載這位大神的另一篇文章《關于Java并發編程的總結和思考》
29、打印昨天的當前時刻
答:
import java.util.Calendar; class YesterdayCurrent {public static void main(String[] args){Calendar cal = Calendar.getInstance();cal.add(Calendar.DATE, -1);System.out.println(cal.getTime());} }在Java8中,可以用下面的代碼實現相同的功能:
import java.time.LocalDateTime; public class YestodayCuttent {public static void main(String[] args) {LocalDateTime now = LocalDateTime.now();LocalDateTime yestoday = now.minusDays(2);System.out.println(yestoday);} }30、比較一下Java和JavaScript
標準回答:一些標準的回答點有(一下特性除非特殊說明,否則都是先說Java后說JavaScript):
面向對象和基于對象;編譯和解釋;強類型變量與弱類型變量;代碼格式不一樣。
非標準回答:Java與JavaScript最重要的區別是Java是靜態語言,JavaScript是動態語言。目前的編程語言發展趨勢是函數式語言和動態語言。在Java中class 是一等公民,而JavaScriptfunction是一等公民,因此JavaScript支持函數式編程,可以使用Lambda函數和閉包,當然Java8也開始支持函數式編程,提供了對Lambda表達式以及函數式接口的支持。
31、什么時候用斷言(assert)?
答:斷言在軟件開發中是一種常用的調試方式,很多開發語言都支持這種機制。一般來說,斷言用于保證程序最基本、關鍵的正確性。斷言檢查通常在開發和測試時開啟。為了保證程序的執行效率,在軟件發布后斷言檢查通常是關閉的。斷言是一種包含布爾表達式的語句,在執行這個語句時假設該表達式為true,如果表達式的值為false,那么系統會報告一個AssertionError。代碼實例如下:
assert(a > 0); // throws an AssertionError if a <= 0斷言可以有兩種書寫格式:
assert Expression1;
assert Expression1 : Expression2;
Expression1應該總是產生一個布爾值。
Expression2可以是得出一個值的任意表達式;這個值用于生成顯示更多的調試信息的字符串消息。
啟動斷言檢查略,非常簡單,請參考網上教程。
注意:斷言不應該以任何方式改變程序的狀態,簡單的說,如果希望在不滿足某些條件時阻止代碼的執行,就可以考慮用斷言來阻止它。
32、Error和Exception有什么區別?
答:Error表示系統級的錯誤和程序不必處理的異常,是恢復不是不可能但很困難的情況下的一種嚴重問題;比如內存溢出,不可能指望程序能處理這樣的情況;Exception表示需要捕捉或者需要程序進行處理的異常,是一種設計或實現問題;也就是說,它表示如果程序運行正常,從不會發生的情況。
面試題:2005年摩托羅拉的面試中曾經問過這么一個問題“If a process reports a stack overflow run-time error, what’s the most possible cause?”,給了四個選項a. lack of memory; b. write on an invalid memory space; c. recursive function calling; d. array index out of boundary. Java程序在運行時也可能會遭遇StackOverflowError,這是一個無法恢復的錯誤,只能重新修改代碼了,這個面試題的答案是c。如果寫了不能迅速收斂的遞歸,則很有可能引發棧溢出的錯誤,如下所示:
class StackOverflowErrorTest {public static void main(String[] args) {main(null);} }提示:用遞歸編寫程序時一定要牢記兩點:1.遞歸公式;2.收斂條件(什么時候就不再繼續遞歸)
33、try{}里有一個return語句,那么緊跟在這個try后的finally{}里的代碼會不會被執行,什么時候執行,在return前還是后?
答:會執行,在return之前執行。
注意:在finally中改變返回值的做法是不好的,因為如果存在finally代碼塊,try中的return語句不會立刻返回調用者,而是記錄下返回值待finally代碼塊執行完畢之后再向調用者返回其值,然后如果在finally中修改了返回值,就會返回修改之后的值。顯然,在finally中返回或者修改返回值會對程序造成很大的困擾。
34、Java語言如何進行異常處理?關鍵字throws、throw、try、catch、finally分別如何使用?
答:Java通過面向對象的方式進行異常處理,把各種不同的異常進行分類,并提供良好的接口。在Java中每個異常都是一個對象,它是Throwable類或其子類的實例。當一個方法出現異常后便拋出一個異常對象,該對象中包含有異常信息,調用這個對象的方法可以捕獲到這個異常并可以對其進行處理。Java的異常處理通過5個關鍵字來實現:
try、catch、throw、throws、finally。
一般情況下使用try來執行一段程序,如果系統會拋出(throw)一個異常對象,可以通過它的類型來捕獲(catch)它,或通過總是執行代碼塊(finally)來處理;try用來指定一塊預防所有異常的程序;catch子句緊跟在try塊后面,用來指定你想要捕獲的異常類型;throw語句用來明確拋出一個異常;throws用來聲明一個方法可能拋出的各種異常(當然聲明異常時允許無病呻吟);finally為確保一段代碼不管發生什么異常狀況都要被執行;try語句可以嵌套,每當遇到一個try語句,異常的結構就會被放入異常棧中,直到所有的try語句都完成。如果下一級的try語句沒有對某種異常進行處理,異常棧就會執行出棧操作,直到遇到有處理這種異常的try語句或者最終將異常拋給JVM。
35、運行時異常與受檢異常有何異同?
答:異常表示程序運行過程中可能出現的非正常狀態,運行時異常表示虛擬機的通常操作中可能遇到的異常,是一種常見運行錯誤,只要程序設計的沒有問題通常就不會發生。受檢異常跟程序運行的上下文環境有關,即使程序設計無誤,仍然可能因使用的問題而引發。Java編譯器要求方法必須聲明拋出可能發生的受檢異常,但是并不要求必須聲明拋出未被捕獲的運行時異常。
36、列出一些常見的異常
答:
- NullPointerException(空指針異常)
- ClassCastException(類轉換異常)
- IllegalArgumentException(非法參數異常)
- IndexOutOfBoundException(下標越界異常)
37、簡單說明finalize
答:finallize是Object類中定義的方法,Java中允許使用finallize()方法在垃圾收集器將對象從內存中清除出去之前做必要的清理工作。這個方法是由垃圾收集器在銷毀對象時調用的,通過重寫finallize()方法可以整理系統資源或者執行其他清理工作。
38、List、Set、Map是否繼承自Collection接口?
答:List、Set是,Map不是。Map是鍵值對映射容器,與List和Set有明顯的區別,而Set存儲的零散的元素且不允許有重復元素(數學中的集合也是如此),List是線性結構的容器,適用于按數值索引訪問元素的情形。
39、闡述ArrayList、Vector、LinkedList的存儲性能和特性。
答:ArrayList和Vector都是使用數組方式存儲數據,此數組元素數大于實際存儲的數據以便增加和插入元素,它們都允許直接按序號索引元素,但是插入元素涉及數組元素移動等內存操作,所以索引數據快而插入數據慢,Vector中的方法由于添加了synchronized修飾,因此Vector是線性安全的容器,但性能上較ArrayList差,因此已經是Java中的遺留容器。LinkedList使用雙向鏈表實現存儲(將內存中零散的內存單元通過附加的引用關聯起來,內存的利用率更高),按序號索引數據需要進行前向和后向遍歷,但是插入數據時只需要記錄本項的前后項即可,所以插入速度較快。Vector屬于遺留容器(Java早期的版本中提供的容器,除此之外,HashTable、Dictionary、BitSet、Stack、Properties都是遺留容器),已經不推薦使用,但是由于ArrayList和LinkedList都是非線程安全的,如果遇到多個線程操作同一個容器的場景,則可以通過工具類Collections中的synchronizedList方法將其轉換成線程安全的容器后再使用(這是對裝潢模式的應用,將已有對象傳入另一個類的構造器中創建新的對象來增強實現)。
補充:遺留容器中的Properties類和Stack類在設計上有嚴重的問題,Properties是一個鍵和值都是字符串的特殊鍵值對映射,在設計上應該是關聯一個Hashtable并將其兩個泛型參數設置為String 類型,但是JavaApi中的Properties直接繼承了Hashtable,這很明顯是對繼承的濫用。這里復用代碼的方式應該是Has-A關系而不是Is-A關系,另一方面容器都屬于工具類,繼承工具類本身就是一個錯誤的做法,使用工具類最好的方式是Has-A關系(關聯)或Use-A關系(依賴)。同理,Stack類繼承Vector也是不正確的。
40、Collection和Collections的區別?
答:Collection是一個接口,它是Set、List等容器的父接口;Collections是個一個工具類,提供了一系列的靜態方法來輔助容器操作,這些方法包括對容器的搜索、排序、線程安全化等等。
41、List、Map、Set三個接口存取元素時,各有什么特點?
答:List以特定索引來存取元素,可以有重復元素。Set不能存放重復元素(用對象的equals()方法來區分元素是否重復)。Map保存鍵值對(key-value pair)映射,映射關系可以是一對一或多對一。Set和Map容器都有基于哈希存儲和排序樹的兩種實現版本,基于哈希存儲的版本理論存取時間復雜度為O(1),而基于排序樹版本的實現在插入或刪除元素時會按照元素或元素的鍵(key)構成排序樹從而達到排序和去重的效果。
42、TreeMap和TreeSet在排序時如何比較元素?Collections工具類中的sort()方法如何比較元素?
答:TreeSet要求存放的對象所屬的類必須實現Comparable接口,該接口提供了比較元素的compareTo()方法,當插入元素時會回調該方法比較元素的大小。TreeMap要求存放的鍵值對映射的鍵必須實現Comparable接口從而根據鍵對元素進行排序。Collections工具類的sort方法有兩種重載的形式,第一種要求傳入的待排序容器中存放的對象比較實現Comparable接口以實現元素的比較;第二種不強制性的要求容器中的元素必須可比較,但是要求傳入第二個參數,參數是Comparator接口的子類型(需要重寫compare方法實現元素的比較),相當于一個臨時定義的排序規則,其實就是通過接口注入比較元素大小的算法,也是對回調模式的應用(Java中對函數式編程的支持)。
例子1:
public class Student implements Comparable<Student> {private String name; // 姓名private int age; // 年齡public Student(String name, int age) {this.name = name;this.age = age;}@Overridepublic String toString() {return "Student [name=" + name + ", age=" + age + "]";}@Overridepublic int compareTo(Student o) {return this.age - o.age; // 比較年齡(年齡的升序)}} import java.util.Set; import java.util.TreeSet;class Test01 {public static void main(String[] args) {Set<Student> set = new TreeSet<>(); // Java 7的鉆石語法(構造器后面的尖括號中不需要寫類型)set.add(new Student("Hao LUO", 33));set.add(new Student("XJ WANG", 32));set.add(new Student("Bruce LEE", 60));set.add(new Student("Bob YANG", 22));for(Student stu : set) {System.out.println(stu);} // 輸出結果: // Student [name=Bob YANG, age=22] // Student [name=XJ WANG, age=32] // Student [name=Hao LUO, age=33] // Student [name=Bruce LEE, age=60]} }例子2:
public class Student {private String name; // 姓名private int age; // 年齡public Student(String name, int age) {this.name = name;this.age = age;}/*** 獲取學生姓名*/public String getName() {return name;}/*** 獲取學生年齡*/public int getAge() {return age;}@Overridepublic String toString() {return "Student [name=" + name + ", age=" + age + "]";}} import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List;class Test02 {public static void main(String[] args) {List<Student> list = new ArrayList<>(); // Java 7的鉆石語法(構造器后面的尖括號中不需要寫類型)list.add(new Student("Hao LUO", 33));list.add(new Student("XJ WANG", 32));list.add(new Student("Bruce LEE", 60));list.add(new Student("Bob YANG", 22));// 通過sort方法的第二個參數傳入一個Comparator接口對象// 相當于是傳入一個比較對象大小的算法到sort方法中// 由于Java中沒有函數指針、仿函數、委托這樣的概念// 因此要將一個算法傳入一個方法中唯一的選擇就是通過接口回調Collections.sort(list, new Comparator<Student> () {@Overridepublic int compare(Student o1, Student o2) {return o1.getName().compareTo(o2.getName()); // 比較學生姓名}});for(Student stu : list) {System.out.println(stu);} // 輸出結果: // Student [name=Bob YANG, age=22] // Student [name=Bruce LEE, age=60] // Student [name=Hao LUO, age=33] // Student [name=XJ WANG, age=32]} }43、Thread類的sleep()方法和對象的wait()方法都可以讓線程暫停執行,它們有什么區別?
答:sleep()方法(休眠)是線程類(Thread)的靜態方法,調用此方法會讓當前線程暫停執行指定的時間,將執行機會(CPU)讓給其他線程,但是對象的鎖依然保持,因此休眠時間結束后會自動恢復(線程回到就緒狀態,請參考第66題中的線程狀態轉換圖)。wait()是Object類的方法,調用對象的wait()方法導致當前線程放棄對象的鎖(線程暫停執行),進入對象的等待池(wait pool),只有調用對象的notify()方法(或notifyAll()方法)時才能喚醒等待池中的線程進入等鎖池(lock pool),如果線程重新獲得對象的鎖就可以進入就緒狀態。
補充:可能不少人對什么是進程,什么是線程還比較模糊,對于為什么需要多線程編程也不是特別理解。簡單的說:進程是具有一定獨立功能的程序關于某個數據集合上的一次運行活動,是操作系統進行資源分配和調度的一個獨立單位;線程是進程的一個實體,是CPU調度和分派的基本單位,是比進程更小的能獨立運行的基本單位。線程的劃分尺度小于進程,這使得多線程程序的并發性高;進程在執行時通常擁有獨立的內存單元,而線程之間可以共享內存。使用多線程的編程通常能夠帶來更好的性能和用戶體驗,但是多線程的程序對于其他程序是不友好的,因為它可能占用了更多的CPU資源。當然,也不是線程越多,程序的性能就越好,因為線程之間的調度和切換也會浪費CPU時間。時下很時髦的Node.js就采用了單線程異步I/O的工作模式。
44、線程的sleep()方法和yield()方法有什么區別?
答:
① sleep()方法給其他線程運行機會時不考慮線程的優先級,因此會給低優先級的線程以運行的機會;yield()方法只會給相同優先級或更高優先級的線程以運行的機會;
② 線程執行sleep()方法后轉入阻塞(blocked)狀態,而執行yield()方法后轉入就緒(ready)狀態;
③ sleep()方法聲明拋出InterruptedException,而yield()方法沒有聲明任何異常;
④ sleep()方法比yield()方法(跟操作系統CPU調度相關)具有更好的可移植性。
45、當一個線程進入一個對象的synchronized方法A之后,其它線程是否可進入此對象的synchronized方法B?
答:不能。其它線程只能訪問該對象的非同步方法,同步方法則不能進入。因為非靜態方法上的synchronized修飾符要求執行方法時要獲得對象的鎖,如果已經進入A方法說明對象鎖已經被取走,那么試圖進入B方法的線程就只能在等鎖池(注意不是等待池哦)中等待對象的鎖。
46、請說出與線程同步以及線程調度相關的方法。
答:
- wait():使一個線程處于等待(阻塞)狀態,并且釋放所持有的對象的鎖;
- sleep():使一個正在運行的線程處于睡眠狀態,是一個靜態方法,調用此方法要處理InterruptedException異常;
- notify():喚醒一個處于等待狀態的線程,當然在調用此方法的時候,并不能確切的喚醒某一個等待狀態的線程,而是由JVM確定喚醒哪個線程,而且與優先級無關;
- notityAll():喚醒所有處于等待狀態的線程,該方法并不是將對象的鎖給所有線程,而是讓它們競爭,只有獲得鎖的線程才能進入就緒狀態;
提示:關于Java多線程和并發編程的問題,參考另一篇文章《關于Java并發編程的總結和思考》。
補充:Java 5通過Lock接口提供了顯式的鎖機制(explicit lock),增強了靈活性以及對線程的協調。Lock接口中定義了加鎖(lock())和解鎖(unlock())的方法,同時還提供了newCondition()方法來產生用于線程之間通信的Condition對象;此外,Java 5還提供了信號量機制(semaphore),信號量可以用來限制對某個共享資源進行訪問的線程的數量。在對資源進行訪問之前,線程必須得到信號量的許可(調用Semaphore對象的acquire()方法);在完成對資源的訪問后,線程必須向信號量歸還許可(調用Semaphore對象的release()方法)。
下面的例子演示了100個線程同時向一個銀行賬戶中存入1元錢,在沒有使用同步機制和使用同步機制情況下的執行情況。
- 銀行賬戶類:
- 存錢線程類:
- 測試類:
在沒有同步的情況下,執行結果通常是顯示賬戶余額在10元以下,出現這種狀況的原因是,當一個線程A試圖存入1元的時候,另外一個線程B也能夠進入存款的方法中,線程B讀取到的賬戶余額仍然是線程A存入1元錢之前的賬戶余額,因此也是在原來的余額0上面做了加1元的操作,同理線程C也會做類似的事情,所以最后100個線程執行結束時,本來期望賬戶余額為100元,但實際得到的通常在10元以下(很可能是1元哦)。解決這個問題的辦法就是同步,當一個線程對銀行賬戶存錢時,需要將此賬戶鎖定,待其操作完成后才允許其他的線程進行操作,代碼有如下幾種調整方案:
- 在銀行賬戶的存款(deposit)方法上同步(synchronized)關鍵字
- 在線程調用存款方法時對銀行賬戶進行同步
- 通過Java 5顯示的鎖機制,為每個銀行賬戶創建一個鎖對象,在存款操作進行加鎖和解鎖的操作
按照上述三種方式對代碼進行修改后,重寫執行測試代碼Test01,將看到最終的賬戶余額為100元。當然也可以使用Semaphore或CountdownLatch來實現同步。
47、編寫多線程程序有幾種實現方式?
答:Java 5以前實現多線程有兩種實現方法:一種是繼承Thread類;另一種是實現Runnable接口。兩種方式都要通過重寫run()方法來定義線程的行為,推薦使用后者,因為Java中的繼承是單繼承,一個類有一個父類,如果繼承了Thread類就無法再繼承其他類了,顯然使用Runnable接口更為靈活。
補充:Java 5以后創建線程還有第三種方式:實現Callable接口,該接口中的call方法可以在線程執行結束時產生一個返回值,代碼如下所示:
import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future;class MyTask implements Callable<Integer> {private int upperBounds;public MyTask(int upperBounds) {this.upperBounds = upperBounds;}@Overridepublic Integer call() throws Exception {int sum = 0; for(int i = 1; i <= upperBounds; i++) {sum += i;}return sum;}}class Test {public static void main(String[] args) throws Exception {List<Future<Integer>> list = new ArrayList<>();ExecutorService service = Executors.newFixedThreadPool(10);for(int i = 0; i < 10; i++) {list.add(service.submit(new MyTask((int) (Math.random() * 100))));}int sum = 0;for(Future<Integer> future : list) {// while(!future.isDone()) ;sum += future.get();}System.out.println(sum);} }48、synchronized關鍵字的用法?
答:synchronized關鍵字可以將對象或者方法標記為同步,以實現對對象和方法的互斥訪問,可以用synchronized(對象) { … }定義同步代碼塊,或者在聲明方法時將synchronized作為方法的修飾符。在第60題的例子中已經展示了synchronized關鍵字的用法。
49、舉例說明同步和異步。
答:如果系統中存在臨界資源(資源數量少于競爭資源的線程數量的資源),例如正在寫的數據以后可能被另一個線程讀到,或者正在讀的數據可能已經被另一個線程寫過了,那么這些數據就必須進行同步存取(數據庫操作中的排他鎖就是最好的例子)。當應用程序在對象上調用了一個需要花費很長時間來執行的方法,并且不希望讓程序等待方法的返回時,就應該使用異步編程,在很多情況下采用異步途徑往往更有效率。事實上,所謂的同步就是指阻塞式操作,而異步就是非阻塞式操作。
50、啟動一個線程是調用run()還是start()方法?
答:啟動一個線程是調用start()方法,使線程所代表的虛擬處理機處于可運行狀態,這意味著它可以由JVM 調度并執行,這并不意味著線程就會立即運行。run()方法是線程啟動后要進行回調(callback)的方法。
51、什么是線程池(thread pool)?
答:在面向對象編程中,創建和銷毀對象是很費時間的,因為創建一個對象要獲取內存資源或者其它更多資源。在Java中更是如此,虛擬機將試圖跟蹤每一個對象,以便能夠在對象銷毀后進行垃圾回收。所以提高服務程序效率的一個手段就是盡可能減少創建和銷毀對象的次數,特別是一些很耗資源的對象創建和銷毀,這就是”池化資源”技術產生的原因。線程池顧名思義就是事先創建若干個可執行的線程放入一個池(容器)中,需要的時候從池中獲取線程不用自行創建,使用完畢不需要銷毀線程而是放回池中,從而減少創建和銷毀線程對象的開銷。
Java 5+中的Executor接口定義一個執行線程的工具。它的子類型即線程池接口是ExecutorService。要配置一個線程池是比較復雜的,尤其是對于線程池的原理不是很清楚的情況下,因此在工具類Executors面提供了一些靜態工廠方法,生成一些常用的線程池,如下所示:
- newSingleThreadExecutor:創建一個單線程的線程池。這個線程池只有一個線程在工作,也就是相當于單線程串行執行所有任務。如果這個唯一的線程因為異常結束,那么會有一個新的線程來替代它。此線程池保證所有任務的執行順序按照任務的提交順序執行。
- newFixedThreadPool:創建固定大小的線程池。每次提交一個任務就創建一個線程,直到線程達到線程池的最大大小。線程池的大小一旦達到最大值就會保持不變,如果某個線程因為執行異常而結束,那么線程池會補充一個新線程。
- newCachedThreadPool:創建一個可緩存的線程池。如果線程池的大小超過了處理任務所需要的線程,那么就會回收部分空閑(60秒不執行任務)的線程,當任務數增加時,此線程池又可以智能的添加新線程來處理任務。此線程池不會對線程池大小做限制,線程池大小完全依賴于操作系統(或者說JVM)能夠創建的最大線程大小。
- newScheduledThreadPool:創建一個大小無限的線程池。此線程池支持定時以及周期性執行任務的需求。
- newSingleThreadExecutor:創建一個單線程的線程池。此線程池支持定時以及周期性執行任務的需求。
第60題的例子中演示了通過Executors工具類創建線程池并使用線程池執行線程的代碼。如果希望在服務器上使用線程池,強烈建議使用newFixedThreadPool方法來創建線程池,這樣能獲得更好的性能。
52、線程的基本狀態以及狀態之間的關系?或線程 的生命周期?
答:其中Running表示運行狀態,Runnable表示就緒狀態(萬事俱備,只欠CPU),Blocked表示阻塞狀態,阻塞狀態又有多種情況,可能是因為調用wait()方法進入等待池,也可能是執行同步方法或同步代碼塊進入等鎖池,或者是調用了sleep()方法或join()方法等待休眠或其他線程結束,或是因為發生了I/O中斷。
53、簡述synchronized 和java.util.concurrent.locks.Lock的異同?
答:Lock是Java 5以后引入的新的API,和關鍵字synchronized相比主要相同點:Lock 能完成synchronized所實現的所有功能;主要不同點:Lock有比synchronized更精確的線程語義和更好的性能,而且不強制性的要求一定要獲得鎖。synchronized會自動釋放鎖,而Lock一定要求程序員手工釋放,并且最好在finally 塊中釋放(這是釋放外部資源的最好的地方)。
54、Java中如何實現序列化,有什么意義?
答:序列化就是一種用來處理對象流的機制,所謂對象流也就是將對象的內容進行流化。可以對流化后的對象進行讀寫操作,也可將流化后的對象傳輸于網絡之間。序列化是為了解決對象流讀寫操作時可能引發的問題(如果不進行序列化可能會存在數據亂序的問題)。
要實現序列化,需要讓一個類實現Serializable接口,該接口是一個標識性接口,標注該類對象是可被序列化的,然后使用一個輸出流來構造一個對象輸出流并通過writeObject(Object)方法就可以將實現對象寫出(即保存其狀態);如果需要反序列化則可以用一個輸入流建立對象輸入流,然后通過readObject方法從流中讀取對象。序列化除了能夠實現對象的持久化之外,還能夠用于對象的深度克隆。
55、Java中有幾種類型的流?
答:字節流和字符流。字節流繼承于InputStream、OutputStream,字符流繼承于Reader、Writer。在java.io 包中還有許多其他的流,主要是為了提高性能和使用方便。關于Java的I/O需要注意的有兩點:一是兩種對稱性(輸入和輸出的對稱性,字節和字符的對稱性);二是兩種設計模式(適配器模式和裝潢模式)。另外Java中的流不同于C#的是它只有一個維度一個方向。
面試題?- 編程實現文件拷貝。(這個題目在筆試的時候經常出現,下面的代碼給出了兩種實現方案)
import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.channels.FileChannel;public final class MyUtil {private MyUtil() {throw new AssertionError();}public static void fileCopy(String source, String target) throws IOException {try (InputStream in = new FileInputStream(source)) {try (OutputStream out = new FileOutputStream(target)) {byte[] buffer = new byte[4096];int bytesToRead;while((bytesToRead = in.read(buffer)) != -1) {out.write(buffer, 0, bytesToRead);}}}}public static void fileCopyNIO(String source, String target) throws IOException {try (FileInputStream in = new FileInputStream(source)) {try (FileOutputStream out = new FileOutputStream(target)) {FileChannel inChannel = in.getChannel();FileChannel outChannel = out.getChannel();ByteBuffer buffer = ByteBuffer.allocate(4096);while(inChannel.read(buffer) != -1) {buffer.flip();outChannel.write(buffer);buffer.clear();}}}} }注意:上面用到Java 7的TWR,使用TWR后可以不用在finally中釋放外部資源 ,從而讓代碼更加優雅。
56、寫一個方法,輸入一個文件名和一個字符串,統計這個字符串在這個文件中出現的次數。
import java.io.BufferedReader; import java.io.FileReader;public final class MyUtil {// 工具類中的方法都是靜態方式訪問的因此將構造器私有不允許創建對象(絕對好習慣)private MyUtil() {throw new AssertionError();}/*** 統計給定文件中給定字符串的出現次數* * @param filename 文件名* @param word 字符串* @return 字符串在文件中出現的次數*/public static int countWordInFile(String filename, String word) {int counter = 0;try (FileReader fr = new FileReader(filename)) {try (BufferedReader br = new BufferedReader(fr)) {String line = null;while ((line = br.readLine()) != null) {int index = -1;while (line.length() >= word.length() && (index = line.indexOf(word)) >= 0) {counter++;line = line.substring(index + word.length());}}}} catch (Exception ex) {ex.printStackTrace();}return counter;}}57、如何用Java代碼列出一個目錄下所有的文件?
答:如果只要求列出當前文件夾下的文件,代碼如下所示:
import java.io.File;class Test12 {public static void main(String[] args) {File f = new File("/Users/Hao/Downloads");for(File temp : f.listFiles()) {if(temp.isFile()) {System.out.println(temp.getName());}}} }如果需要對文件夾繼續展開,代碼如下所示:
import java.io.File;class Test12 {public static void main(String[] args) {showDirectory(new File("/Users/Hao/Downloads"));}public static void showDirectory(File f) {_walkDirectory(f, 0);}private static void _walkDirectory(File f, int level) {if(f.isDirectory()) {for(File temp : f.listFiles()) {_walkDirectory(temp, level + 1);}}else {for(int i = 0; i < level - 1; i++) {System.out.print("\t");}System.out.println(f.getName());}} }在Java 7中可以使用NIO.2的API來做同樣的事情,代碼如下所示:
class ShowFileTest {public static void main(String[] args) throws IOException {Path initPath = Paths.get("/Users/Hao/Downloads");Files.walkFileTree(initPath, new SimpleFileVisitor<Path>() {@Overridepublic FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {System.out.println(file.getFileName().toString());return FileVisitResult.CONTINUE;}});} }58、用Java的套接字編程實現一個多線程的回顯(echo)服務器。
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket;public class EchoServer {private static final int ECHO_SERVER_PORT = 6789;public static void main(String[] args) { try(ServerSocket server = new ServerSocket(ECHO_SERVER_PORT)) {System.out.println("服務器已經啟動...");while(true) {Socket client = server.accept();new Thread(new ClientHandler(client)).start();}} catch (IOException e) {e.printStackTrace();}}private static class ClientHandler implements Runnable {private Socket client;public ClientHandler(Socket client) {this.client = client;}@Overridepublic void run() {try(BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream()));PrintWriter pw = new PrintWriter(client.getOutputStream())) {String msg = br.readLine();System.out.println("收到" + client.getInetAddress() + "發送的: " + msg);pw.println(msg);pw.flush();} catch(Exception ex) {ex.printStackTrace();} finally {try {client.close();} catch (IOException e) {e.printStackTrace();}}}} }注意:上面的代碼使用了Java 7的TWR語法,由于很多外部資源類都間接的實現了AutoCloseable接口(單方法回調接口),因此可以利用TWR語法在try結束的時候通過回調的方式自動調用外部資源類的close()方法,避免書寫冗長的finally代碼塊。此外,上面的代碼用一個靜態內部類實現線程的功能,使用多線程可以避免一個用戶I/O操作所產生的中斷影響其他用戶對服務器的訪問,簡單的說就是一個用戶的輸入操作不會造成其他用戶的阻塞。當然,上面的代碼使用線程池可以獲得更好的性能,因為頻繁的創建和銷毀線程所造成的開銷也是不可忽視的。
下面是一段回顯客戶端測試代碼:
import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.Socket; import java.util.Scanner;public class EchoClient {public static void main(String[] args) throws Exception {Socket client = new Socket("localhost", 6789);Scanner sc = new Scanner(System.in);System.out.print("請輸入內容: ");String msg = sc.nextLine();sc.close();PrintWriter pw = new PrintWriter(client.getOutputStream());pw.println(msg);pw.flush();BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream()));System.out.println(br.readLine());client.close();} }如果希望用NIO的多路復用套接字實現服務器,代碼如下所示。NIO的操作雖然帶來了更好的性能,但是有些操作是比較底層的,對于初學者來說還是有些難于理解。
import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Iterator;public class EchoServerNIO {private static final int ECHO_SERVER_PORT = 6789;private static final int ECHO_SERVER_TIMEOUT = 5000;private static final int BUFFER_SIZE = 1024;private static ServerSocketChannel serverChannel = null;private static Selector selector = null; // 多路復用選擇器private static ByteBuffer buffer = null; // 緩沖區public static void main(String[] args) {init();listen();}private static void init() {try {serverChannel = ServerSocketChannel.open();buffer = ByteBuffer.allocate(BUFFER_SIZE);serverChannel.socket().bind(new InetSocketAddress(ECHO_SERVER_PORT));serverChannel.configureBlocking(false);selector = Selector.open();serverChannel.register(selector, SelectionKey.OP_ACCEPT);} catch (Exception e) {throw new RuntimeException(e);}}private static void listen() {while (true) {try {if (selector.select(ECHO_SERVER_TIMEOUT) != 0) {Iterator<SelectionKey> it = selector.selectedKeys().iterator();while (it.hasNext()) {SelectionKey key = it.next();it.remove();handleKey(key);}}} catch (Exception e) {e.printStackTrace();}}}private static void handleKey(SelectionKey key) throws IOException {SocketChannel channel = null;try {if (key.isAcceptable()) {ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();channel = serverChannel.accept();channel.configureBlocking(false);channel.register(selector, SelectionKey.OP_READ);} else if (key.isReadable()) {channel = (SocketChannel) key.channel();buffer.clear();if (channel.read(buffer) > 0) {buffer.flip();CharBuffer charBuffer = CharsetHelper.decode(buffer);String msg = charBuffer.toString();System.out.println("收到" + channel.getRemoteAddress() + "的消息:" + msg);channel.write(CharsetHelper.encode(CharBuffer.wrap(msg)));} else {channel.close();}}} catch (Exception e) {e.printStackTrace();if (channel != null) {channel.close();}}} } import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.nio.charset.CharsetEncoder;public final class CharsetHelper {private static final String UTF_8 = "UTF-8";private static CharsetEncoder encoder = Charset.forName(UTF_8).newEncoder();private static CharsetDecoder decoder = Charset.forName(UTF_8).newDecoder();private CharsetHelper() {}public static ByteBuffer encode(CharBuffer in) throws CharacterCodingException{return encoder.encode(in);}public static CharBuffer decode(ByteBuffer in) throws CharacterCodingException{return decoder.decode(in);} }59、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允許應用程序代碼把這些事件逐個拉出來,而不用提供在解析器方便時從解析器中接收事件的處理程序。
60、你在項目中哪些地方用到了XML?
答:XML的主要作用有兩個方面:數據交換和信息配置。在做數據交換時,XML將數據用標簽組裝成起來,然后壓縮打包加密后通過網絡傳送給接收者,接收解密與解壓縮后再從XML文件中還原相關信息進行處理,XML曾經是異構系統間交換數據的事實標準,但此項功能幾乎已經被JSON(JavaScript?Object?Notation)取而代之。當然,目前很多軟件仍然使用XML來存儲配置信息,我們在很多項目中通常也會將作為配置信息的硬代碼寫在XML文件中,Java的很多框架也是這么做的,而且這些框架都選擇了dom4j作為處理XML的工具,因為Sun公司的官方API實在不怎么好用。
補充:現在有很多時髦的軟件(如Sublime)已經開始將配置文件書寫成JSON格式,我們已經強烈的感受到XML的另一項功能也將逐漸被業界拋棄。
61、闡述JDBC操作數據庫的步驟。
答:下面的代碼以連接本機的Oracle數據庫為例,演示JDBC操作數據庫的步驟。
- 加載驅動:
- 創建連接:
- 創建語句:
- 執行語句:
- 處理結果:
- 關閉資源:
提示:關閉外部資源的順序應該和打開的順序相反,也就是說先關閉ResultSet、再關閉Statement、在關閉Connection。上面的代碼只關閉了Connection(連接),雖然通常情況下在關閉連接時,連接上創建的語句和打開的游標也會關閉,但不能保證總是如此,因此應該按照剛才說的順序分別關閉。此外,第一步加載驅動在JDBC 4.0中是可以省略的(自動從類路徑中加載驅動),但是我們建議保留。?
62、Statement和PreparedStatement有什么區別?哪個性能更好?
答:與Statement相比,①PreparedStatement接口代表預編譯的語句,它主要的優勢在于可以減少SQL的編譯錯誤并增加SQL的安全性(減少SQL注射攻擊的可能性);②PreparedStatement中的SQL語句是可以帶參數的,避免了用字符串連接拼接SQL語句的麻煩和不安全;③當批量處理SQL或頻繁執行相同的查詢時,PreparedStatement有明顯的性能上的優勢,由于數據庫可以將編譯優化后的SQL語句緩存起來,下次執行相同結構的語句時就會很快(不用再次編譯和生成執行計劃)。
補充:為了提供對存儲過程的調用,JDBC API中還提供了CallableStatement接口。存儲過程(Stored Procedure)是數據庫中一組為了完成特定功能的SQL語句的集合,經編譯后存儲在數據庫中,用戶通過指定存儲過程的名字并給出參數(如果該存儲過程帶有參數)來執行它。雖然調用存儲過程會在網絡開銷、安全性、性能上獲得很多好處,但是存在如果底層數據庫發生遷移時就會有很多麻煩,因為每種數據庫的存儲過程在書寫上存在不少的差別。
63、使用JDBC操作數據庫時,如何提升讀取數據的性能?如何提升更新數據的性能?
答:要提升讀取數據的性能,可以指定通過結果集(ResultSet)對象的setFetchSize()方法指定每次抓取的記錄數(典型的空間換時間策略);要提升更新數據的性能可以使用PreparedStatement語句構建批處理,將若干SQL語句置于一個批處理中執行。
64、在進行數據庫編程時,連接池有什么作用?
答:由于創建連接和釋放連接都有很大的開銷(尤其是數據庫服務器不在本地時,每次建立連接都需要進行TCP的三次握手,釋放連接需要進行TCP四次握手,造成的開銷是不可忽視的),為了提升系統訪問數據庫的性能,可以事先創建若干連接置于連接池中,需要時直接從連接池獲取,使用結束時歸還連接池而不必關閉連接,從而避免頻繁創建和釋放連接所造成的開銷,這是典型的用空間換取時間的策略(浪費了空間存儲連接,但節省了創建和釋放連接的時間)。池化技術在Java開發中是很常見的,在使用線程時創建線程池的道理與此相同。基于Java的開源數據庫連接池主要有:C3P0、Proxool、DBCP、BoneCP、Druid等。
補充:在計算機系統中時間和空間是不可調和的矛盾,理解這一點對設計滿足性能要求的算法是至關重要的。大型網站性能優化的一個關鍵就是使用緩存,而緩存跟上面講的連接池道理非常類似,也是使用空間換時間的策略。可以將熱點數據置于緩存中,當用戶查詢這些數據時可以直接從緩存中得到,這無論如何也快過去數據庫中查詢。當然,緩存的置換策略等也會對系統性能產生重要影響,對于這個問題的討論已經超出了這里要闡述的范圍。
65、什么是DAO模式?
答:DAO(Data Access Object)顧名思義是一個為數據庫或其他持久化機制提供了抽象接口的對象,在不暴露底層持久化方案實現細節的前提下提供了各種數據訪問操作。在實際的開發中,應該將所有對數據源的訪問操作進行抽象化后封裝在一個公共API中。用程序設計語言來說,就是建立一個接口,接口中定義了此應用程序中將會用到的所有事務方法。在這個應用程序中,當需要和數據源進行交互的時候則使用這個接口,并且編寫一個單獨的類來實現這個接口,在邏輯上該類對應一個特定的數據存儲。DAO模式實際上包含了兩個模式,一是Data Accessor(數據訪問器),二是Data Object(數據對象),前者要解決如何訪問數據的問題,而后者要解決的是如何用對象封裝數據。
66、事務的ACID是指什么?
答:
- 原子性(Atomic):事務中各項操作,要么全做要么全不做,任何一項操作的失敗都會導致整個事務的失敗;
- 一致性(Consistent):事務結束后系統狀態是一致的;
- 隔離性(Isolated):并發執行的事務彼此無法看到對方的中間狀態;
- 持久性(Durable):事務完成后所做的改動都會被持久化,即使發生災難性的失敗。通過日志和同步備份可以在故障發生后重建數據。
補充:關于事務,在面試中被問到的概率是很高的,可以問的問題也是很多的。首先需要知道的是,只有存在并發數據訪問時才需要事務。當多個事務訪問同一數據時,可能會存在5類問題,包括3類數據讀取問題(臟讀、不可重復讀和幻讀)和2類數據更新問題(第1類丟失更新和第2類丟失更新)。
臟讀(Dirty Read):A事務讀取B事務尚未提交的數據并在此基礎上操作,而B事務執行回滾,那么A讀取到的數據就是臟數據。
| T1 | ? | 開始事務 |
| T2 | 開始事務 | ? |
| T3 | ? | 查詢賬戶余額為1000元 |
| T4 | ? | 取出500元余額修改為500元 |
| T5 | 查詢賬戶余額為500元(臟讀) | ? |
| T6 | ? | 撤銷事務余額恢復為1000元 |
| T7 | 匯入100元把余額修改為600元 | ? |
| T8 | 提交事務 | ? |
不可重復讀(Unrepeatable Read):事務A重新讀取前面讀取過的數據,發現該數據已經被另一個已提交的事務B修改過了。
| T1 | ? | 開始事務 |
| T2 | 開始事務 | ? |
| T3 | ? | 查詢賬戶余額為1000元 |
| T4 | 查詢賬戶余額為1000元 | ? |
| T5 | ? | 取出100元修改余額為900元 |
| T6 | ? | 提交事務 |
| T7 | 查詢賬戶余額為900元(不可重復讀) | ? |
幻讀(Phantom Read):事務A重新執行一個查詢,返回一系列符合查詢條件的行,發現其中插入了被事務B提交的行。
| T1 | ? | 開始事務 |
| T2 | 開始事務 | ? |
| T3 | 統計總存款為10000元 | ? |
| T4 | ? | 新增一個存款賬戶存入100元 |
| T5 | ? | 提交事務 |
| T6 | 再次統計總存款為10100元(幻讀) | ? |
第1類丟失更新:事務A撤銷時,把已經提交的事務B的更新數據覆蓋了。
| T1 | 開始事務 | ? |
| T2 | ? | 開始事務 |
| T3 | 查詢賬戶余額為1000元 | ? |
| T4 | ? | 查詢賬戶余額為1000元 |
| T5 | ? | 匯入100元修改余額為1100元 |
| T6 | ? | 提交事務 |
| T7 | 取出100元將余額修改為900元 | ? |
| T8 | 撤銷事務 | ? |
| T9 | 余額恢復為1000元(丟失更新) | ? |
第2類丟失更新:事務A覆蓋事務B已經提交的數據,造成事務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 | 不允許 | 不允許 | 不允許 | 不允許 | 不允許 |
需要說明的是,事務隔離級別和數據訪問的并發性是對立的,事務隔離級別越高并發性就越差。所以要根據具體的應用來確定合適的事務隔離級別,這個地方沒有萬能的原則。
67、JDBC中如何進行事務處理?
答:Connection提供了事務處理的方法,通過調用setAutoCommit(false)可以設置手動提交事務;當事務完成后用commit()顯式提交事務;如果在事務處理過程中發生異常則通過rollback()進行事務回滾。除此之外,從JDBC 3.0中還引入了Savepoint(保存點)的概念,允許通過代碼設置保存點并讓事務回滾到指定的保存點。
68、JDBC能否處理Blob和Clob?
答: Blob是指二進制大對象(Binary Large Object),而Clob是指大字符對象(Character Large Objec),因此其中Blob是為存儲大的二進制數據而設計的,而Clob是為存儲大的文本數據而設計的。JDBC的PreparedStatement和ResultSet都提供了相應的方法來支持Blob和Clob操作。下面的代碼展示了如何使用JDBC操作LOB:
下面以MySQL數據庫為例,創建一個張有三個字段的用戶表,包括編號(id)、姓名(name)和照片(photo),建表語句如下:
下面的Java代碼向數據庫中插入一條記錄:
import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.SQLException;class JdbcLobTest {public static void main(String[] args) {Connection con = null;try {// 1. 加載驅動(Java6以上版本可以省略)Class.forName("com.mysql.jdbc.Driver");// 2. 建立連接con = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123456");// 3. 創建語句對象PreparedStatement ps = con.prepareStatement("insert into tb_user values (default, ?, ?)");ps.setString(1, "駱昊"); // 將SQL語句中第一個占位符換成字符串try (InputStream in = new FileInputStream("test.jpg")) { // Java 7的TWRps.setBinaryStream(2, in); // 將SQL語句中第二個占位符換成二進制流// 4. 發出SQL語句獲得受影響行數System.out.println(ps.executeUpdate() == 1 ? "插入成功" : "插入失敗");} catch(IOException e) {System.out.println("讀取照片失敗!");}} catch (ClassNotFoundException | SQLException e) { // Java 7的多異常捕獲e.printStackTrace();} finally { // 釋放外部資源的代碼都應當放在finally中保證其能夠得到執行try {if(con != null && !con.isClosed()) {con.close(); // 5. 釋放數據庫連接 con = null; // 指示垃圾回收器可以回收該對象}} catch (SQLException e) {e.printStackTrace();}}} }69、簡述正則表達式及其用途。
答:在編寫處理字符串的程序時,經常會有查找符合某些復雜規則的字符串的需要。正則表達式就是用于描述這些規則的工具。換句話說,正則表達式就是記錄文本規則的代碼。
說明:計算機誕生初期處理的信息幾乎都是數值,但是時過境遷,今天我們使用計算機處理的信息更多的時候不是數值而是字符串,正則表達式就是在進行字符串匹配和處理的時候最為強大的工具,絕大多數語言都提供了對正則表達式的支持。
70、Java中是如何支持正則表達式操作的?
答:Java中的String類提供了支持正則表達式操作的方法,包括:matches()、replaceAll()、replaceFirst()、split()。此外,Java中可以用Pattern類表示正則表達式對象,它提供了豐富的API進行各種正則表達式操作,請參考下面面試題的代碼。
面試題:?- 如果要從字符串中截取第一個英文左括號之前的字符串,例如:北京市(朝陽區)(西城區)(海淀區),截取結果為:北京市,那么正則表達式怎么寫?
import java.util.regex.Matcher; import java.util.regex.Pattern;class RegExpTest {public static void main(String[] args) {String str = "北京市(朝陽區)(西城區)(海淀區)";Pattern p = Pattern.compile(".*?(?=\\()");Matcher m = p.matcher(str);if(m.find()) {System.out.println(m.group());}} }說明:上面的正則表達式中使用了懶惰匹配和前瞻,如果不清楚這些內容,推薦讀一下網上很有名的《正則表達式30分鐘入門教程》。?
71、獲得一個類的類對象有哪些方式?
答:
- 方法1:類型.class,例如:String.class
- 方法2:對象.getClass(),例如:”hello”.getClass()
- 方法3:Class.forName(),例如:Class.forName(“java.lang.String”)
72、如何通過反射創建對象?
答:
- 方法1:通過類對象調用newInstance()方法,例如:String.class.newInstance()
- 方法2:通過類對象的getConstructor()或getDeclaredConstructor()方法獲得構造器(Constructor)對象并調用其newInstance()方法創建對象,例如:String.class.getConstructor(String.class).newInstance(“Hello”);
73、如何通過反射獲取和設置對象私有字段的值?
答:可以通過類對象的getDeclaredField()方法字段(Field)對象,然后再通過字段對象的setAccessible(true)將其設置為可以訪問,接下來就可以通過get/set方法來獲取/設置字段的值了。下面的代碼實現了一個反射的工具類,其中的兩個靜態方法分別用于獲取和設置私有字段的值,字段可以是基本類型也可以是對象類型且支持多級對象操作,例如ReflectionUtil.get(dog, “owner.car.engine.id”);可以獲得dog對象的主人的汽車的引擎的ID號。
import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.List;/*** 反射工具類* @author 駱昊**/ public class ReflectionUtil {private ReflectionUtil() {throw new AssertionError();}/*** 通過反射取對象指定字段(屬性)的值* @param target 目標對象* @param fieldName 字段的名字* @throws 如果取不到對象指定字段的值則拋出異常* @return 字段的值*/public static Object getValue(Object target, String fieldName) {Class<?> clazz = target.getClass();String[] fs = fieldName.split("\\.");try {for(int i = 0; i < fs.length - 1; i++) {Field f = clazz.getDeclaredField(fs[i]);f.setAccessible(true);target = f.get(target);clazz = target.getClass();}Field f = clazz.getDeclaredField(fs[fs.length - 1]);f.setAccessible(true);return f.get(target);}catch (Exception e) {throw new RuntimeException(e);}}/*** 通過反射給對象的指定字段賦值* @param target 目標對象* @param fieldName 字段的名稱* @param value 值*/public static void setValue(Object target, String fieldName, Object value) {Class<?> clazz = target.getClass();String[] fs = fieldName.split("\\.");try {for(int i = 0; i < fs.length - 1; i++) {Field f = clazz.getDeclaredField(fs[i]);f.setAccessible(true);Object val = f.get(target);if(val == null) {Constructor<?> c = f.getType().getDeclaredConstructor();c.setAccessible(true);val = c.newInstance();f.set(target, val);}target = val;clazz = target.getClass();}Field f = clazz.getDeclaredField(fs[fs.length - 1]);f.setAccessible(true);f.set(target, value);}catch (Exception e) {throw new RuntimeException(e);}}}74、如何通過反射調用對象的方法?
import java.lang.reflect.Method;class MethodInvokeTest {public static void main(String[] args) throws Exception {String str = "hello";Method m = str.getClass().getMethod("toUpperCase");System.out.println(m.invoke(str)); // HELLO} }75、簡述一下面向對象的”六原則一法則”。
答:
- 單一職責原則:一個類只做它該做的事情。(單一職責原則想表達的就是”高內聚”,寫代碼最終極的原則只有六個字”高內聚、低耦合”,就如同葵花寶典或辟邪劍譜的中心思想就八個字”欲練此功必先自宮”,所謂的高內聚就是一個代碼模塊只完成一項功能,在面向對象中,如果只讓一個類完成它該做的事,而不涉及與它無關的領域就是踐行了高內聚的原則,這個類就只有單一職責。我們都知道一句話叫”因為專注,所以專業”,一個對象如果承擔太多的職責,那么注定它什么都做不好。這個世界上任何好的東西都有兩個特征,一個是功能單一,好的相機絕對不是電視購物里面賣的那種一個機器有一百多種功能的,它基本上只能照相;另一個是模塊化,好的自行車是組裝車,從減震叉、剎車到變速器,所有的部件都是可以拆卸和重新組裝的,好的乒乓球拍也不是成品拍,一定是底板和膠皮可以拆分和自行組裝的,一個好的軟件系統,它里面的每個功能模塊也應該是可以輕易的拿到其他系統中使用的,這樣才能實現軟件復用的目標。)
- 開閉原則:軟件實體應當對擴展開放,對修改關閉。(在理想的狀態下,當我們需要為一個軟件系統增加新功能時,只需要從原來的系統派生出一些新類就可以,不需要修改原來的任何一行代碼。要做到開閉有兩個要點:①抽象是關鍵,一個系統中如果沒有抽象類或接口系統就沒有擴展點;②封裝可變性,將系統中的各種可變因素封裝到一個繼承結構中,如果多個可變因素混雜在一起,系統將變得復雜而換亂,如果不清楚如何封裝可變性,可以參考《設計模式精解》一書中對橋梁模式的講解的章節。)
- 依賴倒轉原則:面向接口編程。(該原則說得直白和具體一些就是聲明方法的參數類型、方法的返回類型、變量的引用類型時,盡可能使用抽象類型而不用具體類型,因為抽象類型可以被它的任何一個子類型所替代,請參考下面的里氏替換原則。)
里氏替換原則:任何時候都可以用子類型替換掉父類型。(關于里氏替換原則的描述,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、內存、硬盤、顯卡、聲卡各種設備需要相互配合才能很好的工作,但是如果這些東西都直接連接到一起,計算機的布線將異常復雜,在這種情況下,主板作為一個調停者的身份出現,它將各個設備連接在一起而不需要每個設備之間直接交換數據,這樣就減小了系統的耦合度和復雜度,如下圖所示。迪米特法則用通俗的話來將就是不要和陌生人打交道,如果真的需要,找一個自己的朋友,讓他替你和陌生人打交道。)
76、簡述一下你了解的設計模式。
答:所謂設計模式,就是一套被反復使用的代碼設計經驗的總結(情境中一個問題經過證實的一個解決方案)。使用設計模式是為了可重用代碼、讓代碼更容易被他人理解、保證代碼可靠性。設計模式使人們可以更加簡單方便的復用成功的設計和體系結構。將已證實的技術表述成設計模式也會使新系統開發者更加容易理解其設計思路。
在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系統中都使用裝潢模式)等,反正基本原則就是揀自己最熟悉的、用得最多的作答,以免言多必失。
77、用Java寫一個單例類。
- 餓漢式單例
- 懶漢式單例
?注意:實現一個單例有兩點注意事項,①將構造器私有,不允許外界通過構造器創建對象;②通過公開的靜態方法向外界返回類的唯一實例。這里有一個問題可以思考:Spring的IoC容器可以為普通的類創建單例,它是怎么做到的呢?
78、什么是UML?
答:UML是統一建模語言(Unified Modeling Language)的縮寫,它發表于1997年,綜合了當時已經存在的面向對象的建模語言、方法和過程,是一個支持模型化和軟件系統開發的圖形化語言,為軟件開發的所有階段提供模型化和可視化支持。使用UML可以幫助溝通與交流,輔助應用設計和文檔的生成,還能夠闡釋系統的結構和行為。
79、UML中有哪些常用的圖?
答:UML定義了多種圖形化的符號來描述軟件系統部分或全部的靜態結構和動態結構,包括:用例圖(use case diagram)、類圖(class diagram)、時序圖(sequence diagram)、協作圖(collaboration diagram)、狀態圖(statechart diagram)、活動圖(activity diagram)、構件圖(component diagram)、部署圖(deployment diagram)等。在這些圖形化符號中,有三種圖最為重要,分別是:用例圖(用來捕獲需求,描述系統的功能,通過該圖可以迅速的了解系統的功能模塊及其關系)、類圖(描述類以及類與類之間的關系,通過該圖可以快速了解系統)、時序圖(描述執行特定任務時對象之間的交互關系以及執行順序,通過該圖可以了解對象能接收的消息也就是說對象能夠向外界提供的服務)。
80、用Java寫一個折半查找。
答:折半查找,也稱二分查找、二分搜索,是一種在有序數組中查找某一特定元素的搜索算法。搜素過程從數組的中間元素開始,如果中間元素正好是要查找的元素,則搜素過程結束;如果某一特定元素大于或者小于中間元素,則在數組大于或小于中間元素的那一半中查找,而且跟開始一樣從中間元素開始比較。如果在某一步驟數組已經為空,則表示找不到指定的元素。這種搜索算法每一次比較都使搜索范圍縮小一半,其時間復雜度是O(logN)。
import java.util.Comparator;public class MyUtil {public static <T extends Comparable<T>> int binarySearch(T[] x, T key) {return binarySearch(x, 0, x.length- 1, key);}// 使用循環實現的二分查找public static <T> int binarySearch(T[] x, T key, Comparator<T> comp) {int low = 0;int high = x.length - 1;while (low <= high) {int mid = (low + high) >>> 1;int cmp = comp.compare(x[mid], key);if (cmp < 0) {low= mid + 1;}else if (cmp > 0) {high= mid - 1;}else {return mid;}}return -1;}// 使用遞歸實現的二分查找private static<T extends Comparable<T>> int binarySearch(T[] x, int low, int high, T key) {if(low <= high) {int mid = low + ((high -low) >> 1);if(key.compareTo(x[mid])== 0) {return mid;}else if(key.compareTo(x[mid])< 0) {return binarySearch(x,low, mid - 1, key);}else {return binarySearch(x,mid + 1, high, key);}}return -1;} }?說明:上面的代碼中給出了折半查找的兩個版本,一個用遞歸實現,一個用循環實現。需要注意的是計算中間位置時不應該使用(high+ low) / 2的方式,因為加法運算可能導致整數越界,這里應該使用以下三種方式之一:low + (high – low) / 2或low + (high – low) >> 1或(low + high) >>> 1(>>>是邏輯右移,是不帶符號位的右移)
總結
以上是生活随笔為你收集整理的Java面试宝典————基础篇的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux窗帘文件夹命令,窗帘面板和电机
- 下一篇: Java 多线程 —— ThreadLo