夯實Java基礎系列8:深入理解Java內部類及其實現原理

  • 2019 年 10 月 8 日
  • 筆記

目錄

- [Java 的兩大數據類型:](https://cloud.tencent.com/developer/article/write#java-%E7%9A%84%E4%B8%A4%E5%A4%A7%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B)
    - [內置數據類型](https://cloud.tencent.com/developer/article/write#%E5%86%85%E7%BD%AE%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B)      - [引用類型](https://cloud.tencent.com/developer/article/write#%E5%BC%95%E7%94%A8%E7%B1%BB%E5%9E%8B)      - [Java 常量](https://cloud.tencent.com/developer/article/write#java-%E5%B8%B8%E9%87%8F)  - [自動拆箱和裝箱(詳解)](https://cloud.tencent.com/developer/article/write#%E8%87%AA%E5%8A%A8%E6%8B%86%E7%AE%B1%E5%92%8C%E8%A3%85%E7%AE%B1%EF%BC%88%E8%AF%A6%E8%A7%A3%EF%BC%89)
    - [簡易實現](https://cloud.tencent.com/developer/article/write#%E7%AE%80%E6%98%93%E5%AE%9E%E7%8E%B0)      - [自動裝箱與拆箱中的「坑」](https://cloud.tencent.com/developer/article/write#%E8%87%AA%E5%8A%A8%E8%A3%85%E7%AE%B1%E4%B8%8E%E6%8B%86%E7%AE%B1%E4%B8%AD%E7%9A%84%E5%9D%91)      - [了解基本類型快取(常量池)的最佳實踐](https://cloud.tencent.com/developer/article/write#%E4%BA%86%E8%A7%A3%E5%9F%BA%E6%9C%AC%E7%B1%BB%E5%9E%8B%E7%BC%93%E5%AD%98%EF%BC%88%E5%B8%B8%E9%87%8F%E6%B1%A0%EF%BC%89%E7%9A%84%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5)      - [總結:](https://cloud.tencent.com/developer/article/write#%E6%80%BB%E7%BB%93%EF%BC%9A)  - [基本數據類型的存儲方式](https://cloud.tencent.com/developer/article/write#%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E7%9A%84%E5%AD%98%E5%82%A8%E6%96%B9%E5%BC%8F)
    - [存在棧中:](https://cloud.tencent.com/developer/article/write#%E5%AD%98%E5%9C%A8%E6%A0%88%E4%B8%AD%EF%BC%9A)      - [存在堆里](https://cloud.tencent.com/developer/article/write#%E5%AD%98%E5%9C%A8%E5%A0%86%E9%87%8C)  - [參考文章](https://cloud.tencent.com/developer/article/write#%E5%8F%82%E8%80%83%E6%96%87%E7%AB%A0)  - [微信公眾號](https://cloud.tencent.com/developer/article/write#%E5%BE%AE%E4%BF%A1%E5%85%AC%E4%BC%97%E5%8F%B7)
    - [Java技術江湖](https://cloud.tencent.com/developer/article/write#java%E6%8A%80%E6%9C%AF%E6%B1%9F%E6%B9%96)      - [個人公眾號:黃小斜](https://cloud.tencent.com/developer/article/write#%E4%B8%AA%E4%BA%BA%E5%85%AC%E4%BC%97%E5%8F%B7%EF%BC%9A%E9%BB%84%E5%B0%8F%E6%96%9C)
- Java基本數據類型

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到我的倉庫里查看

https://github.com/h2pl/Java-Tutorial

喜歡的話麻煩點下Star哈

文章首發於我的個人部落格:

www.how2playlife.com

本文是微信公眾號【Java技術江湖】的《夯實Java基礎系列博文》其中一篇,本文部分內容來源於網路,為了把本文主題講得清晰透徹,也整合了很多我認為不錯的技術部落格內容,引用其中了一些比較好的部落格文章,如有侵權,請聯繫作者。 該系列博文會告訴你如何從入門到進階,一步步地學習Java基礎知識,並上手進行實戰,接著了解每個Java知識點背後的實現原理,更完整地了解整個Java技術體系,形成自己的知識框架。為了更好地總結和檢驗你的學習成果,本系列文章也會提供每個知識點對應的面試題以及參考答案。

如果對本系列文章有什麼建議,或者是有什麼疑問的話,也可以關注公眾號【Java技術江湖】聯繫作者,歡迎你參與本系列博文的創作和修訂。

<!– more –>

Java 基本數據類型

變數就是申請記憶體來存儲值。也就是說,當創建變數的時候,需要在記憶體中申請空間。

記憶體管理系統根據變數的類型為變數分配存儲空間,分配的空間只能用來儲存該類型數據。

因此,通過定義不同類型的變數,可以在記憶體中儲存整數、小數或者字元。

Java 的兩大數據類型:

  • 內置數據類型
  • 引用數據類型
    • *

內置數據類型

Java語言提供了八種基本類型。六種數字類型(四個整數型,兩個浮點型),一種字元類型,還有一種布爾型。

byte:

  • byte 數據類型是8位、有符號的,以二進位補碼錶示的整數;
  • 最小值是 -128(-2^7);
  • 最大值是 127(2^7-1);
  • 默認值是 0;
  • byte 類型用在大型數組中節約空間,主要代替整數,因為 byte 變數佔用的空間只有 int 類型的四分之一;
  • 例子:byte a = 100,byte b = -50。

short:

  • short 數據類型是 16 位、有符號的以二進位補碼錶示的整數
  • 最小值是 -32768(-2^15);
  • 最大值是 32767(2^15 – 1);
  • Short 數據類型也可以像 byte 那樣節省空間。一個short變數是int型變數所佔空間的二分之一;
  • 默認值是 0;
  • 例子:short s = 1000,short r = -20000。

int:

  • int 數據類型是32位、有符號的以二進位補碼錶示的整數;
  • 最小值是 -2,147,483,648(-2^31);
  • 最大值是 2,147,483,647(2^31 – 1);
  • 一般地整型變數默認為 int 類型;
  • 默認值是 0 ;
  • 例子:int a = 100000, int b = -200000。

long:

  • long 數據類型是 64 位、有符號的以二進位補碼錶示的整數;
  • 最小值是 -9,223,372,036,854,775,808(-2^63);
  • 最大值是 9,223,372,036,854,775,807(2^63 -1);
  • 這種類型主要使用在需要比較大整數的系統上;
  • 默認值是 0L;
  • 例子: long a = 100000L,Long b = -200000L。 "L"理論上不分大小寫,但是若寫成"l"容易與數字"1"混淆,不容易分辯。所以最好大寫。

float:

  • float 數據類型是單精度、32位、符合IEEE 754標準的浮點數;
  • float 在儲存大型浮點數組的時候可節省記憶體空間;
  • 默認值是 0.0f;
  • 浮點數不能用來表示精確的值,如貨幣;
  • 例子:float f1 = 234.5f。

double:

  • double 數據類型是雙精度、64 位、符合IEEE 754標準的浮點數;
  • 浮點數的默認類型為double類型;
  • double類型同樣不能表示精確的值,如貨幣;
  • 默認值是 0.0d;
  • 例子:double d1 = 123.4。

boolean:

  • boolean數據類型表示一位的資訊;
  • 只有兩個取值:true 和 false;
  • 這種類型只作為一種標誌來記錄 true/false 情況;
  • 默認值是 false;
  • 例子:boolean one = true。

char:

  • char類型是一個單一的 16 位 Unicode 字元;
  • 最小值是 u0000(即為0);
  • 最大值是 uffff(即為65,535);
  • char 數據類型可以儲存任何字元;
  • 例子:char letter = 'A';。
//8位  byte bx = Byte.MAX_VALUE;  byte bn = Byte.MIN_VALUE;  //16位  short sx = Short.MAX_VALUE;  short sn = Short.MIN_VALUE;  //32位  int ix = Integer.MAX_VALUE;  int in = Integer.MIN_VALUE;  //64位  long lx = Long.MAX_VALUE;  long ln = Long.MIN_VALUE;  //32位  float fx = Float.MAX_VALUE;  float fn = Float.MIN_VALUE;  //64位  double dx = Double.MAX_VALUE;  double dn = Double.MIN_VALUE;  //16位  char cx = Character.MAX_VALUE;  char cn = Character.MIN_VALUE;  //1位  boolean bt = Boolean.TRUE;  boolean bf = Boolean.FALSE;

127 -128 32767 -32768 2147483647 -2147483648 9223372036854775807 -9223372036854775808 3.4028235E38 1.4E-45 1.7976931348623157E308 4.9E-324 ￿

true false

引用類型

  • 在Java中,引用類型的變數非常類似於C/C++的指針。引用類型指向一個對象,指向對象的變數是引用變數。這些變數在聲明時被指定為一個特定的類型,比如 Employee、Puppy 等。變數一旦聲明後,類型就不能被改變了。
  • 對象、數組都是引用數據類型。
  • 所有引用類型的默認值都是null。
  • 一個引用變數可以用來引用任何與之兼容的類型。
  • 例子:Site site = new Site("Runoob")。

Java 常量

常量在程式運行時是不能被修改的。

在 Java 中使用 final 關鍵字來修飾常量,聲明方式和變數類似:

final double PI = 3.1415927;

雖然常量名也可以用小寫,但為了便於識別,通常使用大寫字母表示常量。

字面量可以賦給任何內置類型的變數。例如:

byte a = 68;  char a = 'A'

自動拆箱和裝箱(詳解)

Java 5增加了自動裝箱與自動拆箱機制,方便基本類型與包裝類型的相互轉換操作。在Java 5之前,如果要將一個int型的值轉換成對應的包裝器類型Integer,必須顯式的使用new創建一個新的Integer對象,或者調用靜態方法Integer.valueOf()。

//在Java 5之前,只能這樣做 Integer value = new Integer(10); //或者這樣做 Integer value = Integer.valueOf(10); //直接賦值是錯誤的 //Integer value = 10;`

在Java 5中,可以直接將整型賦給Integer對象,由編譯器來完成從int型到Integer類型的轉換,這就叫自動裝箱。

//在Java 5中,直接賦值是合法的,由編譯器來完成轉換 Integer value = 10; 與此對應的,自動拆箱就是可以將包裝類型轉換為基本類型,具體的轉換工作由編譯器來完成。 //在Java 5 中可以直接這麼做 Integer value = new Integer(10); int i = value;

自動裝箱與自動拆箱為程式設計師提供了很大的方便,而在實際的應用中,自動裝箱與拆箱也是使用最廣泛的特性之一。自動裝箱和自動拆箱其實是Java編譯器提供的一顆語法糖(語法糖是指在電腦語言中添加的某種語法,這種語法對語言的功能並沒有影響,但是更方便程式設計師使用。通過可提高開發效率,增加程式碼可讀性,增加程式碼的安全性)。

簡易實現

在八種包裝類型中,每一種包裝類型都提供了兩個方法:

靜態方法valueOf(基本類型):將給定的基本類型轉換成對應的包裝類型;

實例方法xxxValue():將具體的包裝類型對象轉換成基本類型; 下面我們以int和Integer為例,說明Java中自動裝箱與自動拆箱的實現機制。看如下程式碼:

class Auto //code1 { public static void main(String[] args) { //自動裝箱 Integer inte = 10; //自動拆箱 int i = inte;

    //再double和Double來驗證一下      Double doub = 12.40;      double d = doub;    }

} 上面的程式碼先將int型轉為Integer對象,再講Integer對象轉換為int型,毫無疑問,這是可以正確運行的。可是,這種轉換是怎麼進行的呢?使用反編譯工具,將生成的Class文件在反編譯為Java文件,讓我們看看發生了什麼: class Auto//code2 { public static void main(String[] paramArrayOfString) { Integer localInteger = Integer.valueOf(10);

int i = localInteger.intValue();
Double localDouble = Double.valueOf(12.4D);  double d = localDouble.doubleValue();

} } 我們可以看到經過javac編譯之後,code1的程式碼被轉換成了code2,實際運行時,虛擬機運行的就是code2的程式碼。也就是說,虛擬機根本不知道有自動拆箱和自動裝箱這回事;在將Java源文件編譯為class文件的過程中,javac編譯器在自動裝箱的時候,調用了Integer.valueOf()方法,在自動拆箱時,又調用了intValue()方法。我們可以看到,double和Double也是如此。 實現總結:其實自動裝箱和自動封箱是編譯器為我們提供的一顆語法糖。在自動裝箱時,編譯器調用包裝類型的valueOf()方法;在自動拆箱時,編譯器調用了相應的xxxValue()方法。

自動裝箱與拆箱中的「坑」

在使用自動裝箱與自動拆箱時,要注意一些陷阱,為了避免這些陷阱,我們有必要去看一下各種包裝類型的源碼。

Integer源碼

public final class Integer extends Number implements Comparable<Integer> { private final int value;

/*Integer的構造方法,接受一個整型參數,Integer對象表示的int值,保存在value中*/   public Integer(int value) {          this.value = value;   }    /*equals()方法判斷的是:所代表的int型的值是否相等*/   public boolean equals(Object obj) {          if (obj instanceof Integer) {              return value == ((Integer)obj).intValue();          }          return false;  }    /*返回這個Integer對象代表的int值,也就是保存在value中的值*/   public int intValue() {          return value;   }     /**    * 首先會判斷i是否在[IntegerCache.low,Integer.high]之間    * 如果是,直接返回Integer.cache中相應的元素    * 否則,調用構造方法,創建一個新的Integer對象    */   public static Integer valueOf(int i) {      assert IntegerCache.high >= 127;      if (i >= IntegerCache.low && i <= IntegerCache.high)          return IntegerCache.cache[i + (-IntegerCache.low)];      return new Integer(i);   }    /**    * 靜態內部類,快取了從[low,high]對應的Integer對象    * low -128這個值不會被改變    * high 默認是127,可以改變,最大不超過:Integer.MAX_VALUE - (-low) -1    * cache 保存從[low,high]對象的Integer對象   */   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;          String integerCacheHighPropValue =              sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");          if (integerCacheHighPropValue != null) {              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);          }          high = h;            cache = new Integer[(high - low) + 1];          int j = low;          for(int k = 0; k < cache.length; k++)              cache[k] = new Integer(j++);      }        private IntegerCache() {}  }

} 以上是Oracle(Sun)公司JDK 1.7中Integer源碼的一部分,通過分析上面的程式碼,得到: 1)Integer有一個實例域value,它保存了這個Integer所代表的int型的值,且它是final的,也就是說這個Integer對象一經構造完成,它所代表的值就不能再被改變。 2)Integer重寫了equals()方法,它通過比較兩個Integer對象的value,來判斷是否相等。 3)重點是靜態內部類IntegerCache,通過類名就可以發現:它是用來快取數據的。它有一個數組,裡面保存的是連續的Integer對象。 (a) low:代表快取數據中最小的值,固定是-128。 (b) high:代表快取數據中最大的值,它可以被該改變,默認是127。high最小是127,最大是Integer.MAX_VALUE-(-low)-1,如果high超過了這個值,那麼cache 的長度就超過Integer.MAX_VALUE了,也就溢出了。 (c) cache[]:裡面保存著從low,high所對應的Integer對象,長度是high-low+1(因為有元素0,所以要加1)。 4)調用valueOf(int i)方法時,首先判斷i是否在low,high之間,如果是,則復用Integer.cachei-low。比如,如果Integer.valueOf(3),直接返回Integer.cache131;如果i不在這個範圍,則調用構造方法,構造出一個新的Integer對象。 5)調用intValue(),直接返回value的值。 通過3)和4)可以發現,默認情況下,在使用自動裝箱時,VM會復用-128,127之間的Integer對象。

Integer a1 = 1; Integer a2 = 1; Integer a3 = new Integer(1); //會列印true,因為a1和a2是同一個對象,都是Integer.cache129 System.out.println(a1 == a2); //false,a3構造了一個新的對象,不同於a1,a2 System.out.println(a1 == a3);

了解基本類型快取(常量池)的最佳實踐

//基本數據類型的常量池是-128到127之間。  // 在這個範圍中的基本數據類的包裝類可以自動拆箱,比較時直接比較數值大小。  public static void main(String[] args) {      //int的自動拆箱和裝箱只在-128到127範圍中進行,超過該範圍的兩個integer的 == 判斷是會返回false的。      Integer a1 = 128;      Integer a2 = -128;      Integer a3 = -128;      Integer a4 = 128;      System.out.println(a1 == a4);      System.out.println(a2 == a3);        Byte b1 = 127;      Byte b2 = 127;      Byte b3 = -128;      Byte b4 = -128;      //byte都是相等的,因為範圍就在-128到127之間      System.out.println(b1 == b2);      System.out.println(b3 == b4);        //      Long c1 = 128L;      Long c2 = 128L;      Long c3 = -128L;      Long c4 = -128L;      System.out.println(c1 == c2);      System.out.println(c3 == c4);        //char沒有負值      //發現char也是在0到127之間自動拆箱      Character d1 = 128;      Character d2 = 128;      Character d3 = 127;      Character d4 = 127;      System.out.println(d1 == d2);      System.out.println(d3 == d4);

結果

false true true true false true false true

    Integer i = 10;      Byte b = 10;      //比較Byte和Integer.兩個對象無法直接比較,報錯      //System.out.println(i == b);      System.out.println("i == b " + i.equals(b));      //答案是false,因為包裝類的比較時先比較是否是同一個類,不是的話直接返回false.      int ii = 128;      short ss = 128;      long ll = 128;      char cc = 128;      System.out.println("ii == bb " + (ii == ss));      System.out.println("ii == ll " + (ii == ll));      System.out.println("ii == cc " + (ii == cc));        結果      i == b false      ii == bb true      ii == ll true      ii == cc true        //這時候都是true,因為基本數據類型直接比較值,值一樣就可以。

總結:

通過上面的程式碼,我們分析一下自動裝箱與拆箱發生的時機:

(1)當需要一個對象的時候會自動裝箱,比如Integer a = 10;equals(Object o)方法的參數是Object對象,所以需要裝箱。

(2)當需要一個基本類型時會自動拆箱,比如int a = new Integer(10);算術運算是在基本類型間進行的,所以當遇到算術運算時會自動拆箱,比如程式碼中的 c == (a + b);

(3) 包裝類型 == 基本類型時,包裝類型自動拆箱;

需要注意的是:「==」在沒遇到算術運算時,不會自動拆箱;基本類型只會自動裝箱為對應的包裝類型,程式碼中最後一條說明的內容。

在JDK 1.5中提供了自動裝箱與自動拆箱,這其實是Java 編譯器的語法糖,編譯器通過調用包裝類型的valueOf()方法實現自動裝箱,調用xxxValue()方法自動拆箱。自動裝箱和拆箱會有一些陷阱,那就是包裝類型復用了某些對象。

(1)Integer默認復用了-128,127這些對象,其中高位置可以修改;

(2)Byte復用了全部256個對象-128,127;

(3)Short服用了-128,127這些對象;

(4)Long服用了-128,127;

(5)Character復用了0,127,Charater不能表示負數;

Double和Float是連續不可數的,所以沒法復用對象,也就不存在自動裝箱復用陷阱。

Boolean沒有自動裝箱與拆箱,它也復用了Boolean.TRUE和Boolean.FALSE,通過Boolean.valueOf(boolean b)返回的Blooean對象要麼是TRUE,要麼是FALSE,這點也要注意。

本文介紹了「真實的」自動裝箱與拆箱,為了避免寫出錯誤的程式碼,又從包裝類型的源碼入手,指出了各種包裝類型在自動裝箱和拆箱時存在的陷阱,同時指出了自動裝箱與拆箱發生的時機。

基本數據類型的存儲方式

上面自動拆箱和裝箱的原理其實與常量池有關。

存在棧中:

public void(int a) { int i = 1; int j = 1; } 方法中的i 存在虛擬機棧的局部變數表裡,i是一個引用,j也是一個引用,它們都指向局部變數表裡的整型值 1. int a是傳值引用,所以a也會存在局部變數表。

存在堆里

class A{ int i = 1; A a = new A(); } i是類的成員變數。類實例化的對象存在堆中,所以成員變數也存在堆中,引用a存的是對象的地址,引用i存的是值,這個值1也會存在堆中。可以理解為引用i指向了這個值1。也可以理解為i就是1.

3 包裝類對象怎麼存 其實我們說的常量池也可以叫對象池。 比如String a= new String("a").intern()時會先在常量池找是否有「a"對象如果有的話直接返回「a"對象在常量池的地址,即讓引用a指向常量」a"對象的記憶體地址。 public native String intern(); Integer也是同理。

下圖是Integer類型在常量池中查找同值對象的方法。

public static Integer valueOf(int i) {      if (i >= IntegerCache.low && i <= IntegerCache.high)          return IntegerCache.cache[i + (-IntegerCache.low)];      return new Integer(i);  }  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;          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];          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() {}  }

所以基本數據類型的包裝類型可以在常量池查找對應值的對象,找不到就會自動在常量池創建該值的對象。

而String類型可以通過intern來完成這個操作。

JDK1.7後,常量池被放入到堆空間中,這導致intern()函數的功能不同,具體怎麼個不同法,且看看下面程式碼,這個例子是網上流傳較廣的一個例子,分析圖也是直接粘貼過來的,這裡我會用自己的理解去解釋這個例子:

[java] view plain copy  String s = new String("1");  s.intern();  String s2 = "1";  System.out.println(s == s2);    String s3 = new String("1") + new String("1");  s3.intern();  String s4 = "11";  System.out.println(s3 == s4);  輸出結果為:    [java] view plain copy  JDK1.6以及以下:false false  JDK1.7以及以上:false true

JDK1.6查找到常量池存在相同值的對象時會直接返回該對象的地址。

JDK 1.7後,intern方法還是會先去查詢常量池中是否有已經存在,如果存在,則返回常量池中的引用,這一點與之前沒有區別,區別在於,如果在常量池找不到對應的字元串,則不會再將字元串拷貝到常量池,而只是在常量池中生成一個對原字元串的引用。

那麼其他字元串在常量池找值時就會返回另一個堆中對象的地址。

下一節詳細介紹String以及相關包裝類。

具體請見:https://blog.csdn.net/a724888/article/details/80042298

關於Java面向對象三大特性,請參考:

https://blog.csdn.net/a724888/article/details/80033043

參考文章

https://www.runoob.com/java/java-basic-datatypes.html

https://www.cnblogs.com/zch1126/p/5335139.html

https://blog.csdn.net/jreffchen/article/details/81015884

https://blog.csdn.net/yuhongye111/article/details/31850779