為什么是Java SE 5?
目前已經到了JDK-8u74了,JDK7的主版本已經于2015年4月停止公開更新。
那為什么還要來說Java/JDK5呢?
Java SE在1.4(2002)趨于成熟,隨著越來越多應用于開發企業應用,許多框架誕生于這個時期或走向成熟。
Java SE 5.0的發布(2004)在語法層面增加了很多特性,讓開發更高效,代碼更整潔。
- 自動裝箱/拆箱、泛型、注解、for循環增強、枚舉、可變參數等新特性讓你的小手指少敲了不少代碼,可以寫更優雅的實現;
- API提供并發庫大大減少并發編程的難度;
- 虛擬機層面改進了內存模型,增加虛擬機監控和管理相關的api和工具等等。
但是,<font color=red>語法層面的改變對應于JVM卻沒有多大變化,只是編譯器在編譯字節碼時偷偷做了手腳。</font>
所以我們應該了解下到底編譯器干了啥壞事,有助于寫更合理的代碼,少踩坑,掉陷阱里也得知道怎么掉的。
另外原因,目前從各種各樣的項目代碼看,其實多數開發人員常用的還是Java SE 5.0 的特性,甚至習慣用Java SE 1.4及以前的語法特性。
學java也有幾年了,許多特性也知道個一二,但是要寫下來,還是得查閱不少文章,很多東西欠缺完整性和系統性。
碼農寫文章(更合理說是整理資料)也是一個學習的過程。
學習一門語言,一旦實際應用于實際開發中,了解背后的原理和理念,深入了解語言的特點,有好處沒壞處。
注:javac XXXXX.java 編譯命令
javap -c XXXXX 反編譯命令
-c 反編譯
-s 輸出內部類型簽名 需要看方法簽名時 要加上這個參數
-v 輸出附加信息 會輸出比較多信息 包括常量表 line number table 等信息, 但沒有-s的輸出內容
一、自動裝箱/拆箱
1、包裝類型(存在于Java 1.5之前)
Java中,類型分成兩大類,基本類型(Primitive Type)和引用類型(Reference Type)。基本類型是內定的,有確定的取值范圍,值占有確定的內存空間。
有八大基本類型,分成兩個浮點類型(float、double),五個整型(byte, short, int, long,char), 一個布爾型(boolean)。
沒看錯char也是整型,在語言規范中說明,char是一個16bit無符號整形,用來表示一個UTF-16編碼的單元(在Java5中對應Unicode4.0,Java8中對應Unicode6.2)。
基本類型的值不是對象,最基本的對象(Object)方法(toString, hashCode, getClass, equals等)也不能調用。
為了把基本類型當引用類型來用,具備對象的特質,JDK中定義了各種基本類型相對應的包裝類。
所謂裝箱,就是將基本類型的值包裝成(轉換-conversion)對應的包裝類型的對象,拆箱,就是講包裝類型的對象,轉換成基本類型的值。
裝箱和拆箱:
Integer i = 100;
int j = new Integer(250);
基本類型 | 大小 | 數值范圍 | 包裝類型 | 默認值 |
---|---|---|---|---|
boolean | --- | true, false | Boolean | false |
byte | 1字節(8bit) | -2^7 -- 2^7-1 | Byte | 0 |
char | 2字節(16bit) | \u0000--\uffff | Character | \u0000 |
short | 2字節(16bit) | -2^15 -- 2^15-1 | Short | 0 |
int | 4字節(32bit) | -2^31 -- 2^31-1 | Integer | 0 |
long | 8字節(64bit) | -2^63 -- 2^63-1 | Long | 0 |
float | 4字節(32bit) | IEEE754 | Float | 0.0f |
double | 8字節(64bit) | IEEE754 | Double | 0.0d |
2、自動裝箱/拆箱背后
前面說了,語法特性的改變并沒改變JVM的實現方式,那么我們可以看看背后編譯器到底干了啥事情。
下面代碼和編譯后的反編譯結果:
public void boxUnBox(){
Integer i = 100;
int j = new Integer(250);
}

反編譯結果可以看到,以上代碼實際等同于以下代碼的編譯結果:
public void boxUnBox(){
Integer i = Integer. valueOf(100);
Integer t = new Integer(250);
int j = t .intValue();
}
八大基本類型的裝箱操作都調用的是valueOf方法,拆箱操作調用各自賭贏的xxxValue()方法,有興趣可以試試。
3、注意==比較的陷阱
在java中,計算類型的運算符,
先來看下比較的代碼編譯結果:
public void boxUnBoxCMP(){
Integer i = 100;
int j = new Integer(250);
if(j == i ){}
Integer h = new Integer(100);
Integer k = new Integer(100);
if(h == k ){}
}

==第一個紅框==是if(j == i ) 的反編譯代碼
從上面的反編譯結果可以看出,包裝類型的單目運算符計算其實是需要通過拆箱=>計算=>裝箱實現的,
而雙目運算符的運算也是需要將包裝類型轉換成基本類型,然后再參與運算。
但是,== 的比較要牢記它的本質,如果==比較兩邊都是引用類型,那么比較的是引用地址,如果其中一邊是基本類型,那么非引用類型的值將轉換成基本類型再做比較。
==第二個紅框==中是引用比較,沒有轉換。
4、Cache帶來的坑
我們看看自動裝箱的valueOf的代碼吧
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache. high)
return IntegerCache.cache[i + (-IntegerCache. low)];
return new Integer(i);
}
一眼就可以看到IntegerCache這個玩意,完整代碼(JDK1.8的代碼)如下:
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
// 根據配置獲取緩存最大值,最大值配置范圍 127 < h < Integer.MAX_VALUE-129
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt( integerCacheHighPropValue);
i = Math. max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math. min(i, Integer.MAX_VALUE - (- low) -1);
} catch( NumberFormatException nfe ) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h ;
cache = new Integer[(high - low) + 1];// 也許有人會疑惑為什么會有個+1,其實就是0這個數占了個坑
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k ] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache. high >= 127;
}
private IntegerCache() {}
}
IntegerCache的意思就是將low到high的值先緩存起來,low恒定是-128, high默認是127,可以配置成127<= high <= Interger.MAX_VALUE-129
注意緩存的是Integer對象,所以是引用對象。既然是引用對象,那么==的比較就會有問題了。
public static void trap(){
Integer i = 100;
Integer k = 100;
if(i == k ){System.out.println( "i == k");}
Integer j = 500;
Integer h = 500;
if(j == h ){System.out.println( "j == h");}else { System.out.println("j != h" );}
}
輸出結果是什么呢?
i == k
j != h
因為i和k都是從IntegerCache中取得的緩存對象,引用是一樣的,j和h沒有緩存,必須valueOf必須重新new一個Integer對象,所以引用是不等的。
類型Byte、Short、Long和Integer類似,只是沒有可配置的最大緩存值,Byte所有值都被緩存了,所以不存在==的坑。
Character緩存的是0~127。
Float和Double沒有緩沖,也沒辦法緩存。
5、建議
- 不會參與運算的用包裝, 比如數據庫自增的記錄ID,用Long類型
- 參與運算的,如果計算復雜,盡量先轉成基本類型,計算后再轉回對應的包裝類對象;特別是頻繁的單目運算符,如循環中的自增自減
- 參與比較,注意包裝類的cache坑
- 記得所有集合中只能存對象類型,基本類型都是經過裝箱/拆箱的
舉個不好的例子吧:
public static Long bad(List<Integer> list){
Long sum = 0L;
for(Integer i : list ){
if(i % 2 == 0 ){
sum += i;
} else {
sum += i * 2;
}
}
return sum ;
}
有興趣的童鞋可以反編譯看下,類似于以下代碼完成的事情:
public static Long badOrigin(List<Integer> list){
Long sum = Long. valueOf(0L);
Iterator<Integer> it = list.iterator();
Integer value = null;
long sumTmp = 0L;
while(it .hasNext()){
value = it.next();
if(value .intValue() % 2 == 0){
sumTmp = sum.longValue();
sumTmp = sumTmp+ value.intValue();
sum = Long. valueOf(sumTmp);
} else {
sumTmp = sum.longValue();
sumTmp = sumTmp+ value.intValue()*2;
sum = Long. valueOf(sumTmp);
}
}
return sum ;
}
按照建議來,可以改成以下代碼:
public static Long good(List <Integer> list ){
long sum = 0L;
int value = 0;
for(Integer i : list ){
value = i.intValue();
if(value % 2 == 0 ){
sum += value;
} else {
sum += value * 2;
}
}
return sum ;
}
以上反編譯下看看字節碼,是不是清爽多了^^
二、for循環增強
for循環增強也是1.5里的一個語法糖,讓大家寫for循環更加便利,再加上IDE的代碼模板,非常方便
1、先看看List的for循環增強怎么寫:
public void iteratorForeach(){
List<String> list = new ArrayList<String>();
for (String str : list ) {
}
}
反編譯結果如下,可以看出,其實就是調用Iterable接口的iterator方法,獲得一個迭代器(Iterator), 利用迭代器進行遍歷所有數據。
從這里也可以推出,只要實現Iterable接口的類型,都可以在for循環增強中使用:

比如自己實現一個只有add方法,只能通過iterator遍歷的List:
public void myListForeach(){
MyList<String> myList = new MyList<>();
for (String str : myList ) {
}
}
public static class MyList<V> implements Iterable<V>{
private List<V> datas = new ArrayList<>();
public void add(V data ){
datas.add( data);
}
@Override
public Iterator<V> iterator() {
final Iterator<V> it = datas .iterator();
return new Iterator<V>() {
@Override
public boolean hasNext() {
return it .hasNext();
}
@Override
public V next() {
return it .next();
}
};
}
}
2、再看看數組類型的for循環增強怎么寫:
public void arrayForeach(){
String[] strs = new String[10];
for (String str : strs ) {
}
System. out.println();
String str = null ;
for(int i = 0; i < strs .length ; i ++){ //傳統for循環寫法
str = strs[ i];
}
}
跟傳統for循環相比,數組的for增強循環更加簡潔,從反編譯代碼中也可以看出,用到的指令序列基本上是一樣的。

3、不適應的地方
這么好的東西什么情況下用不了呢? 主要是for增強循環中沒能得到下標也沒能得到iterator對象引用導致的。
第一種是數組或者List集合類型,需要用到下標的情況;
第二種是需要調用到Iterator接口的remove方法的情況;
三、可變參數
Java SE 5.0中增加了可變參數特性,對于以往用數組表示的參數可以調整到最后一個參數,作為可變參數定義,
調用方省去顯示創建數組,可空數組可以直接可以省略:
public static void varargs(String s, String... ss) {
}
public static void main(String[] args) {
varargs("aaa" );
varargs("aaa" , "bbb" );
varargs("aaa" , "bbb" , "ccc" , "ddd" );
varargs("", null) ;
varargs("aaa" , new String[]{"abc", "ccc", "ddd" });
}
可變參數背后編譯器也是創建一個數組來傳遞參數的,可以方編譯以上代碼, varargs的方法簽名中第二個參數就是一個string數組:

==注意事項:==
- 不能有多個可變參數,并且只能是最后一個參數;
- 因為可變參數是由數組實現的,調用方忽略可變參數時,可變參數為空數組;但是既然是數組,就可以設置成null,所以要注意空判斷;
- 如果被調用的方法,既匹配了可變參數方法,有匹配了固定參數方法,固定參數方法將被調用;
- 盡量避免可變參數方法的重載(overload):
- 可變參數類型與前一個參數的類型一樣時,與只有可變參數類型方法重載沖突,會導致調用不明確;
- 可變參數類型不同,但可變參數為空時,可以省略,或者設置成null,都會導致被調用方法不明確;
- 可變參數類型是基本類型或包裝類型,重載會因為自動裝箱/拆箱導致調用不明確
- override的方法參數類型和形式必須一致,不能將可變參數改成數組,雖然背后實現是一樣的;
/**不能有多個可變參數,并且只能是最后一個參數**/
public static void varargs10(Object ... objs, String abc){ //編譯出錯
}
public static void varargs11(String abc, Object ... objs){
}
/**因為可變參數是由數組實現的,調用方忽略可變參數時,可變參數為長度為0的數組;但是既然是數組,就可以設置成null,所以要注意null判斷;**/
public static void varargs2Test(){
varargs2();
varargs2(null); //NullPointerException
}
public static void varargs2(String...strs){
//strs 可能為null, 應該做 strs是否為空的判斷
for (String str : strs ) {
}
}
/**如果被調用的方法,既匹配了可變參數方法,有匹配了固定參數方法,固定參數方法將被調用;**/
public static void varargs3Test(){
varargs3(11, 22); //varargs30
}
public static void varargs3(int i, int j ){
System. out.println("varargs30" );
}
public static void varargs3(int i , int... arr){
System. out.println("varargs31" );
}
/**可變參數類型與前一個參數的類型一樣時,與只有可變參數類型方法重載沖突,會導致調用不明確;**/
public static void varargs4Test(){
varargs4("abc" , "def" , "ijk" ); //編譯出錯
}
public static void varargs4(String...strs){
}
public static void varargs4(String str, String... strs){
}
/**可變參數類型不同,但可變參數為空時,可以省略,或者設置成null,都會導致被調用方法不明確;**/
public static void varargs5Test(){
varargs5(); //編譯出錯
varargs5("abc" , null); //編譯出錯
}
public static void varargs5(String str, String... strs){
}
public static void varargs5(String str, Integer... datas){
}
/**可變參數類型是基本類型或包裝類型,重載會因為自動裝箱/拆箱導致調用不明確**/
public static void varargs6Test(){
varargs6("abc" , 1, 2, 3); //編譯出錯
}
public static void varargs6(String str, int... datas){
}
public static void varargs6(String str, Integer... datas){
}
/**override的方法參數類型和形式必須一致,不能將可變參數改成數組,雖然背后實現是一樣的**/
public static void varargs7Test(){
Sub sub = new Sub();
Base base = sub;
base.varargs7( "abc", "def" );
base.varargs7();
sub.varargs7(); //編譯錯誤
sub.varargs7("abc" , "def" ); //編譯錯誤
}
public static interface Base {
public void varargs7(String...strs );
}
public static class Sub implements Base{
@Override
public void varargs7(String[] strs) {
System. out.println("varargs7" );
}
}
四、StringBuilder和字符串+(非1.5特性,順便提一下而已)
JDK 5.0中增加了StringBuilder, 基本上和StringBuffer一樣,但去掉了所有synchronized同步關鍵字。
性能上StringBuilder優于StringBuffer, 所以非并發情況下使用StringBuilder沒商量。
Java中對象沒有參與運算符運算的可能,也沒有提供像C++那樣重載運算符語法支持,不要被String的+操作欺騙了。
Java1.4中,字符串的+操作在編譯器生成的字節碼可以看到使用的是StringBuffer進行append,
Java5.0中,+操作改成StringBuilder的append:
public static void sbTest(String s1, String s2){
String str = s1 +s2 ;
}
