第一篇:Java回顧之I/O
第二篇:Java回顧之網絡通信
第三篇:Java回顧之多線程
第四篇:Java回顧之多線程同步
第五篇:Java回顧之集合
第六篇:Java回顧之序列化
第七篇:Java回顧之反射
這兩天,無意間在網上翻到一本關於Java面試解惑的文章集,里面提到了很多基礎的概念,但一不留神,還是可能會“掉到坑里”。里面的文章寫的很不錯,大家可以通過下面的地址下載:http://zangweiren.iteye.com/blog/241218
在看上述文章的時候,隨手寫了一些測試代碼,以便加深理解。這也就是這篇文章的來源了。
類的初始化順序
在Java中,類里面可能包含:靜態變量,靜態初始化塊,成員變量,初始化塊,構造函數。在類之間可能存在着繼承關系,那么當我們實例化一個對象時,上述各部分的加載順序是怎樣的?
首先來看代碼:
1 class Parent 2 { 3 public static StaticVarible staticVarible= new StaticVarible("父類-靜態變量1"); 4 public StaticVarible instVarible= new StaticVarible("父類-成員變量1"); 5 6 static 7 { 8 System.out.println("父類-靜態塊"); 9 } 10 11 { 12 System.out.println("父類-初始化塊"); 13 } 14 15 public static StaticVarible staticVarible2= new StaticVarible("父類-靜態變量2"); 16 public StaticVarible instVarible2= new StaticVarible("父類-成員變量2"); 17 18 public Parent() 19 { 20 System.out.println("父類-實例構造函數"); 21 } 22 } 23 24 class Child extends Parent 25 { 26 public static StaticVarible staticVarible= new StaticVarible("子類-靜態變量1"); 27 public StaticVarible instVarible= new StaticVarible("子類-成員變量1"); 28 29 static 30 { 31 System.out.println("子類-靜態塊"); 32 } 33 34 public Child() 35 { 36 System.out.println("子類-實例構造函數"); 37 } 38 39 { 40 System.out.println("子類-初始化塊"); 41 } 42 43 public static StaticVarible staticVarible2= new StaticVarible("子類-靜態變量2"); 44 public StaticVarible instVarible2= new StaticVarible("子類-成員變量2"); 45 46 47 } 48 49 class StaticVarible 50 { 51 public StaticVarible(String info) 52 { 53 System.out.println(info); 54 } 55 }
然后執行下面的語句:
1 Child child = new Child();
輸出結果如下:
父類-靜態變量1 父類-靜態塊 父類-靜態變量2 子類-靜態變量1 子類-靜態塊 子類-靜態變量2 父類-成員變量1 父類-初始化塊 父類-成員變量2 父類-實例構造函數 子類-成員變量1 子類-初始化塊 子類-成員變量2 子類-實例構造函數
結論
從上述結果可以看出,在實例化一個對象時,各部分的加載順序如下:
父類靜態成員/父類靜態初始化塊 -> 子類靜態成員/子類靜態初始化塊 -> 父類成員變量/父類初始化塊 -> 父類構造函數 -> 子類成員變量/子類初始化塊 -> 子類構造函數
和String相關的一些事兒
首先,我們聊一聊Java中堆和棧的事兒。
- 棧:存放基本類型,包括char/byte/short/int/long/float/double/boolean
- 堆:存放引用類型,同時一般會在棧中保留一個指向它的指針,垃圾回收判斷一個對象是否可以回收,就是判斷棧中是否有指針指向堆中的對象。
String作為一種特殊的數據類型,它不完全等同於基本類型,也不是全部的引用類型,許多面試題都有它的身影。
String類型變量的存儲結構
String的存儲結構分為兩部分,我們以String a = "abc";為例,描述String類型的存儲方式:
1)在棧中創建一個char數組,值分為是'a','b','c'。
2)在堆中創建一個String對象。
Java中的字符串池
為了節省空間和資源,JVM會維護一個字符串池,或者說會緩存一部分曾經出現過的字符串。
例如下面的代碼:
1 String v1 = "ab"; 2 String v2 = "ab";
實際上,v1==v2,因為JVM在v1聲明后,已經對“ab”進行了緩存。
那么JVM對字符串進行緩存的依據是什么?我們來看下面的代碼,非常有意思:
1 public class StringTest { 2 public static final String constValue = "ab"; 3 public static final String staticValue; 4 5 static 6 { 7 staticValue="ab"; 8 } 9 10 public static void main(String[] args) 11 { 12 String v1 = "ab"; 13 String v2 = "ab"; 14 System.out.println("v1 == v2 : " + (v1 == v2)); 15 String v3 = new String("ab"); 16 System.out.println("v1 == v3 : " + (v1 == v3)); 17 String v4 = "abcd"; 18 String v5 = "ab" + "cd"; 19 System.out.println("v4 == v5 : " + (v4 == v5)); 20 String v6 = v1 + "cd"; 21 System.out.println("v4 == v6 : " + (v4 == v6)); 22 String v7 = constValue + "cd"; 23 System.out.println("v4 == v7 : " + (v4 == v7)); 24 String v8 = staticValue + "cd"; 25 System.out.println("v4 == v8 : " + (v4 == v8)); 26 String v9 = v4.intern(); 27 System.out.println("v4 == v9 :" + (v4 == v9)); 28 String v10 = new String(new char[]{'a','b','c','d'}); 29 String v11 = v10.intern(); 30 System.out.println("v4 == v11 :" + (v4 == v11)); 31 System.out.println("v10 == v11 :" + (v10 == v11)); 32 } 33 }
請注意它的輸出結果:
v1 == v2 : true v1 == v3 : false v4 == v5 : true v4 == v6 : false v4 == v7 : true v4 == v8 : false v4 == v9 :true v4 == v11 :true v10 == v11 :false
我們會發現,並不是所有的判斷都返回true,這似乎和我們上面的說法有矛盾了。其實不然,因為
結論
1. JVM只能緩存那些在編譯時可以確定的常量,而非運行時常量。
上述代碼中的constValue屬於編譯時常量,而staticValue則屬於運行時常量。
2. 通過使用 new方式創建出來的字符串,JVM緩存的方式是不一樣的。
所以上述代碼中,v1不等同於v3。
String的這種設計屬於享元模式嗎?
這個話題比較有意思,大部分講設計模式的文章,在談到享元時,一般就會拿String來做例子,但它屬於享元模式嗎?
字符串與享元的關系,大家可以參考下面的文章:http://www.cnblogs.com/winter-cn/archive/2012/01/21/2328388.html
字符串的反轉輸出
這種情況下,一般會將字符串看做是字符數組,然后利用反轉數組的方式來反轉字符串。
眼花繚亂的方法調用
有繼承關系結構中的方法調用
繼承是面向對象設計中的常見方式,它可以有效的實現”代碼復用“,同時子類也有重寫父類方法的自由,這就對到底是調用父類方法還是子類方法帶來了麻煩。
來看下面的代碼:
1 public class PropertyTest { 2 3 public static void main(String[] args) 4 { 5 ParentDef v1 = new ParentDef(); 6 ParentDef v2 = new ChildDef(); 7 ChildDef v3 = new ChildDef(); 8 System.out.println("=====v1====="); 9 System.out.println("staticValue:" + v1.staticValue); 10 System.out.println("value:" + v1.value); 11 System.out.println("=====v2====="); 12 System.out.println("staticValue:" + v2.staticValue); 13 System.out.println("value:" + v2.value); 14 System.out.println("=====v3====="); 15 System.out.println("staticValue:" + v3.staticValue); 16 System.out.println("value:" + v3.value); 17 } 18 } 19 20 class ParentDef 21 { 22 public static final String staticValue = "父類靜態變量"; 23 public String value = "父類實例變量"; 24 } 25 26 class ChildDef extends ParentDef 27 { 28 public static final String staticValue = "子類靜態變量"; 29 public String value = "子類實例變量"; 30 }
輸出結果如下:
=====v1===== staticValue:父類靜態變量 value:父類實例變量 =====v2===== staticValue:父類靜態變量 value:父類實例變量 =====v3===== staticValue:子類靜態變量 value:子類實例變量
結論
對於調用父類方法還是子類方法,只與變量的聲明類型有關系,與實例化的類型沒有關系。
到底是值傳遞還是引用傳遞
對於這個話題,我的觀點是值傳遞,因為傳遞的都是存儲在棧中的內容,無論是基本類型的值,還是指向堆中對象的指針,都是值而非引用。並且在值傳遞的過程中,JVM會將值復制一份,然后將復制后的值傳遞給調用方法。
按照這種方式,我們來看下面的代碼:
1 public class ParamTest { 2 3 public void change(int value) 4 { 5 value = 10; 6 } 7 8 public void change(Value value) 9 { 10 Value temp = new Value(); 11 temp.value = 10; 12 value = temp; 13 } 14 15 public void add(int value) 16 { 17 value += 10; 18 } 19 20 public void add(Value value) 21 { 22 value.value += 10; 23 } 24 25 public static void main(String[] args) 26 { 27 ParamTest test = new ParamTest(); 28 Value value = new Value(); 29 int v = 0; 30 System.out.println("v:" + v); 31 System.out.println("value.value:" + value.value); 32 System.out.println("=====change====="); 33 test.change(v); 34 test.change(value); 35 System.out.println("v:" + v); 36 System.out.println("value.value:" + value.value); 37 value = new Value(); 38 v = 0; 39 System.out.println("=====add====="); 40 test.add(v); 41 test.add(value); 42 System.out.println("v:" + v); 43 System.out.println("value.value:" + value.value); 44 } 45 } 46 47 class Value 48 { 49 public int value; 50 }
它的輸出結果:
v:0 value.value:0 =====change===== v:0 value.value:0 =====add===== v:0 value.value:10
我們看到,在調用change方法時,即使我們傳遞進去的是指向對象的指針,但最終對象的屬性也沒有變,這是因為在change方法體內,我們新建了一個對象,然后將”復制過的指向原對象的指針“指向了“新對象”,並且對新對象的屬性進行了調整。但是“復制前的指向原對象的指針”依然是指向“原對象”,並且屬性沒有任何變化。
final/finally/finalize的區別
final可以修飾類、成員變量、方法以及方法參數。使用final修飾的類是不可以被繼承的,使用final修飾的方法是不可以被重寫的,使用final修飾的變量,只能被賦值一次。
使用final聲明變量的賦值時機:
1)定義聲明時賦值
2)初始化塊或靜態初始化塊中
3)構造函數
來看下面的代碼:
1 class FinalTest 2 { 3 public static final String staticValue1 = "靜態變量1"; 4 public static final String staticValue2; 5 6 static 7 { 8 staticValue2 = "靜態變量2"; 9 } 10 11 public final String value1 = "實例變量1"; 12 public final String value2; 13 public final String value3; 14 15 { 16 value2 = "實例變量2"; 17 } 18 19 public FinalTest() 20 { 21 value3 = "實例變量3"; 22 } 23 }
finally一般是和try...catch放在一起使用,主要用來釋放一些資源。
我們來看下面的代碼:
1 public class FinallyTest { 2 3 public static void main(String[] args) 4 { 5 finallyTest1(); 6 finallyTest2(); 7 finallyTest3(); 8 } 9 10 private static String finallyTest1() 11 { 12 try 13 { 14 throw new RuntimeException(); 15 } 16 catch(Exception ex) 17 { 18 ex.printStackTrace(); 19 } 20 finally 21 { 22 System.out.println("Finally語句被執行"); 23 } 24 try 25 { 26 System.out.println("Hello World"); 27 return "Hello World"; 28 } 29 catch(Exception ex) 30 { 31 ex.printStackTrace(); 32 } 33 finally 34 { 35 System.out.println("Finally語句被執行"); 36 } 37 return null; 38 } 39 40 private static void finallyTest2() 41 { 42 int i = 0; 43 for (i = 0; i < 3; i++) 44 { 45 try 46 { 47 if (i == 2) break; 48 System.out.println(i); 49 } 50 finally 51 { 52 System.out.println("Finally語句被執行"); 53 } 54 } 55 } 56 57 private static Test finallyTest3() 58 { 59 try 60 { 61 return new Test(); 62 } 63 finally 64 { 65 System.out.println("Finally語句被執行"); 66 } 67 } 68 }
執行結果如下:
java.lang.RuntimeException at sample.interview.FinallyTest.finallyTest1(FinallyTest.java:16) at sample.interview.FinallyTest.main(FinallyTest.java:7) Finally語句被執行 Hello World Finally語句被執行 0 Finally語句被執行 1 Finally語句被執行 Finally語句被執行 Test實例被創建 Finally語句被執行
注意在循環的過程中,對於某一次循環,即使調用了break或者continue,finally也會執行。
finalize則主要用於釋放資源,在調用GC方法時,該方法就會被調用。
來看下面的示例:
1 class FinalizeTest 2 { 3 protected void finalize() 4 { 5 System.out.println("finalize方法被調用"); 6 } 7 8 public static void main(String[] args) 9 { 10 FinalizeTest test = new FinalizeTest(); 11 test = null; 12 Runtime.getRuntime().gc(); 13 } 14 }
執行結果如下:
finalize方法被調用
關於基本類型的一些事兒
基本類型供分為9種,包括byte/short/int/long/float/double/boolean/void,每種基本類型都對應一個“包裝類”,其他一些基本信息如下:
1. 基本類型:byte 二進制位數:8 2. 包裝類:java.lang.Byte 3. 最小值:Byte.MIN_VALUE=-128 4. 最大值:Byte.MAX_VALUE=127 5. 基本類型:short 二進制位數:16 6. 包裝類:java.lang.Short 7. 最小值:Short.MIN_VALUE=-32768 8. 最大值:Short.MAX_VALUE=32767 9. 基本類型:int 二進制位數:32 10. 包裝類:java.lang.Integer 11. 最小值:Integer.MIN_VALUE=-2147483648 12. 最大值:Integer.MAX_VALUE=2147483647 13. 基本類型:long 二進制位數:64 14. 包裝類:java.lang.Long 15. 最小值:Long.MIN_VALUE=-9223372036854775808 16. 最大值:Long.MAX_VALUE=9223372036854775807 17. 基本類型:float 二進制位數:32 18. 包裝類:java.lang.Float 19. 最小值:Float.MIN_VALUE=1.4E-45 20. 最大值:Float.MAX_VALUE=3.4028235E38 21. 基本類型:double 二進制位數:64 22. 包裝類:java.lang.Double 23. 最小值:Double.MIN_VALUE=4.9E-324 24. 最大值:Double.MAX_VALUE=1.7976931348623157E308 25. 基本類型:char 二進制位數:16 26. 包裝類:java.lang.Character 27. 最小值:Character.MIN_VALUE=0 28. 最大值:Character.MAX_VALUE=65535
關於基本類型的一些結論(來自《Java面試解惑》)
- 未帶有字符后綴標識的整數默認為int類型;未帶有字符后綴標識的浮點數默認為double類型。
- 如果一個整數的值超出了int類型能夠表示的范圍,則必須增加后綴“L”(不區分大小寫,建議用大寫,因為小寫的L與阿拉伯數字1很容易混淆),表示為long型。
- 帶有“F”(不區分大小寫)后綴的整數和浮點數都是float類型的;帶有“D”(不區分大小寫)后綴的整數和浮點數都是double類型的。
- 編譯器會在編譯期對byte、short、int、long、float、double、char型變量的值進行檢查,如果超出了它們的取值范圍就會報錯。
- int型值可以賦給所有數值類型的變量;long型值可以賦給long、float、double類型的變量;float型值可以賦給float、double類型的變量;double型值只能賦給double類型變量。
關於基本類型之間的轉換
下面的轉換是無損精度的轉換:
- byte->short
- short->int
- char->int
- int->long
- float->double
下面的轉換是會損失精度的:
- int->float
- long->float
- long->double
除此之外的轉換,是非法的。
和日期相關的一些事兒
Java中,有兩個類和日期相關,一個是Date,一個是Calendar。我們來看下面的示例:
1 public class DateTest { 2 3 public static void main(String[] args) throws ParseException 4 { 5 test1(); 6 test2(); 7 test3(); 8 } 9 10 private static void test1() throws ParseException 11 { 12 Date date = new Date(); 13 System.out.println(date); 14 DateFormat sf = new SimpleDateFormat("yyyy-MM-dd"); 15 System.out.println(sf.format(date)); 16 String formatString = "2013-05-12"; 17 System.out.println(sf.parse(formatString)); 18 } 19 20 private static void test2() 21 { 22 Date date = new Date(); 23 System.out.println("Year:" + date.getYear()); 24 System.out.println("Month:" + date.getMonth()); 25 System.out.println("Day:" + date.getDate()); 26 System.out.println("Hour:" + date.getHours()); 27 System.out.println("Minute:" + date.getMinutes()); 28 System.out.println("Second:" + date.getSeconds()); 29 System.out.println("DayOfWeek:" + date.getDay()); 30 } 31 32 private static void test3() 33 { 34 Calendar c = Calendar.getInstance(); 35 System.out.println(c.getTime()); 36 System.out.println(c.getTimeZone()); 37 System.out.println("Year:" + c.get(Calendar.YEAR)); 38 System.out.println("Month:" + c.get(Calendar.MONTH)); 39 System.out.println("Day:" + c.get(Calendar.DATE)); 40 System.out.println("Hour:" + c.get(Calendar.HOUR)); 41 System.out.println("HourOfDay:" + c.get(Calendar.HOUR_OF_DAY)); 42 System.out.println("Minute:" + c.get(Calendar.MINUTE)); 43 System.out.println("Second:" + c.get(Calendar.SECOND)); 44 System.out.println("DayOfWeek:" + c.get(Calendar.DAY_OF_WEEK)); 45 System.out.println("DayOfMonth:" + c.get(Calendar.DAY_OF_MONTH)); 46 System.out.println("DayOfYear:" + c.get(Calendar.DAY_OF_YEAR)); 47 } 48 }
輸出結果如下:
Sat May 11 13:44:34 CST 2013 2013-05-11 Sun May 12 00:00:00 CST 2013 Year:113 Month:4 Day:11 Hour:13 Minute:44 Second:35 DayOfWeek:6 Sat May 11 13:44:35 CST 2013 sun.util.calendar.ZoneInfo[id="Asia/Shanghai",offset=28800000,dstSavings=0,useDaylight=false,transitions=19,lastRule=null] Year:2013 Month:4 Day:11 Hour:1 HourOfDay:13 Minute:44 Second:35 DayOfWeek:7 DayOfMonth:11 DayOfYear:131
需要注意的是,Date中的getxxx方法已經變成deprecated了,因此我們盡量使用calendar.get方法來獲取日期的細節信息。
另外,注意DateFormat,它不僅可以對日期的輸出進行格式化,而且可以逆向操作,將符合Format的字符串轉換為日期類型。