天天看點

一個疏忽損失慘重!就因為把int改成Integer,第2天被辭了

1 故事背景

一個程式員就因為改了生産環境上的一個方法參數,把int型改成了Integer類型,因為涉及到錢,結果上線之後公司損失慘重,程式員被辭退了。信不信繼續往下看。先來看一段代碼:

public static void main(String[] args) {

        Integer a = Integer.valueOf(100);
        Integer b = 100;

        Integer c = Integer.valueOf(129);
        Integer d = 129;

        System.out.println("a==b:" + (a==b));
        System.out.println("c==d:" + (c==d));
}
           

大家猜它的運作結果是什麼?在運作完程式後,我們才發現有些不對,得到了一個意想不到的運作結果,如下圖所示。

一個疏忽損失慘重!就因為把int改成Integer,第2天被辭了

看到這個運作結果,有人就一定會問,為什麼是這樣?之是以得到這樣的結果,是因為Integer用到的享元模式。來看Integer的源碼。

public final class Integer extends Number implements Comparable<Integer> {

        ...

        public static Integer valueOf(int i) {

                        if (i >= IntegerCache.low && i <= IntegerCache.high)
                                return IntegerCache.cache[i + (-IntegerCache.low)];
                        return new Integer(i);

        }

        ...

}
           

再繼續進入到IntegerCache的源碼來看low和high的值:

private static class IntegerCache {
  // 最小值
  static final int low = -128;
  // 最大值,支援自定義
  static final int high;
  // 緩存數組
  static final Integer cache[];

  static {
    // 最大值可以通過屬性配置來改變
    int h = 127;
    String integerCacheHighPropValue =
      sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
    // 如果設定了對應的屬性,則使用該值
    if (integerCacheHighPropValue != null) {
      try {
        int i = parseInt(integerCacheHighPropValue);
        i = Math.max(i, 127);
        // 最大數組大小為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];
    int j = low;
    // 将low-high範圍内的值全部執行個體化并存入數組中當緩存使用
    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() {}
}
           

由上可知,Integer源碼中的valueOf()方法做了一個條件判斷,如果目标值在-128 - 127,則直接從緩存中取值,否則建立對象。其實,Integer第一次使用的時候就會初始化緩存,其中範圍最小值為-128,最大值預設是127。接着會把low至high中所有的資料初始化存入資料中,預設就是将-128 - 127總共256個數循環執行個體化存入cache數組中。準确的說應該是将這256個對象在記憶體中的位址存進數組中。這裡又有人會問了,那為什麼預設是-128 - 127,怎麼不是-200 - 200或者是其他值呢?那JDK為何要這樣做呢?

在Java API 中是這樣解釋的:

Returns an Integer instance representing the specified int value. If a new Integer instance is not required, this method should generally be used in preference to the constructor Integer(int), as this method is likely to yield significantly better space and time performance by caching frequently requested values. This method will always cache values in the range -128 to 127, inclusive, and may cache other values outside of this range

大緻意思是:

128~127的資料在int範圍内是使用最頻繁的,為了減少頻繁建立對象帶來的記憶體消耗,這裡其實是用到了享元模式,以提高空間和時間性能。

JDK增加了這一預設的範圍并不是不可變,我們在使用前可以通過設定-Djava.lang.Integer.IntegerCache.high=xxx或者設定-XX:AutoBoxCacheMax=xxx來修改緩存範圍,如下圖:

一個疏忽損失慘重!就因為把int改成Integer,第2天被辭了

後來,我又找到一個比較靠譜的解釋:

實際上,在Java 5中首次引入此功能時,範圍固定為-127到+127。後來在Java 6中,範圍的最大值映射到java.lang.Integer.IntegerCache.high,VM參數允許我們設定高位數。根據我們的應用用例,它可以靈活地調整性能。應該從-127到127選擇這個數字範圍的原因應該是什麼。這被認為是廣泛使用的整數範圍。在程式中首次使用Integer必須花費額外的時間來緩存執行個體。

Java Language Specification 的原文解釋如下:

Ideally, boxing a given primitive value p, would always yield an identical reference. In practice, this may not be feasible using existing implementation techniques. The rules above are a pragmatic compromise. The final clause above requires that certain common values always be boxed into indistinguishable objects. The implementation may cache these, lazily or eagerly. For other values, this formulation disallows any assumptions about the identity of the boxed values on the programmer's part. This would allow (but not require) sharing of some or all of these references. This ensures that in most common cases, the behavior will be the desired one, without imposing an undue performance penalty, especially on small devices. Less memory-limited implementations might, for example, cache all char and short values, as well as int and long values in the range of -32K to +32K.

2 關于Integer和int的比較

\1) 由于Integer變量實際上是對一個Integer對象的引用,是以兩個通過new生成的Integer變量永遠是不相等的(因為new生成的是兩個對象,其記憶體位址不同)。

Integer i = new Integer(100);
Integer j = new Integer(100);
System.out.print(i == j); //false
           

\2) Integer變量和int變量比較時,隻要兩個變量的值是向等的,則結果為true(因為包裝類Integer和基本資料類型int比較時,java會自動拆包裝為int,然後進行比較,實際上就變為兩個int變量的比較)

Integer i = new Integer(100);
int j = 100;
System.out.print(i == j); //true
           

\3) 非new生成的Integer變量和new Integer()生成的變量比較時,結果為false。(因為 ①當變量值在-128 - 127之間時,非new生成的Integer變量指向的是java常量池中的對象,而new Integer()生成的變量指向堆中建立的對象,兩者在記憶體中的位址不同;②當變量值在-128 - 127之間時,非new生成Integer變量時,java API中最終會按照new Integer(i)進行處理(參考下面第4條),最終兩個Interger的位址同樣是不相同的)

Integer i = new Integer(100);
Integer j = 100;
System.out.print(i == j); //false
           

\4) 對于兩個非new生成的Integer對象,進行比較時,如果兩個變量的值在區間-128到127之間,則比較結果為true,如果兩個變量的值不在此區間,則比較結果為false

3 擴充知識

在JDK中,這樣的應用不止int,以下包裝類型也都應用了享元模式,對數值做了緩存,隻是緩存的範圍不一樣,具體如下表所示:

一個疏忽損失慘重!就因為把int改成Integer,第2天被辭了

4 使用享元模式實作資料庫連接配接池

public class ConnectionPool {

    private Vector<Connection> pool;

    private String url = "jdbc:mysql://localhost:3306/test";
    private String username = "root";
    private String password = "root";
    private String driverClassName = "com.mysql.jdbc.Driver";
    private int poolSize = 100;

public ConnectionPool() {

        pool = new Vector<Connection>(poolSize);

        try{
            Class.forName(driverClassName);
            for (int i = 0; i < poolSize; i++) {
                Connection conn = DriverManager.getConnection(url,username,password);
                pool.add(conn);
            }
        }catch (Exception e){
            e.printStackTrace();
        }

    }

    public synchronized Connection getConnection(){
        if(pool.size() > 0){
            Connection conn = pool.get(0);
            pool.remove(conn);
            return conn;
        }
        return null;
    }

    public synchronized void release(Connection conn){
        pool.add(conn);
}

}