1. 程式人生 > >Java基礎2——深入理解基本資料型別與常量池

Java基礎2——深入理解基本資料型別與常量池

基本資料型別與常量池

基本資料型別

  Java中的基本資料型別只有8個:byte(1位元組=8位)、short(2位元組)、int(4字 節)、long(8位元組)、float(4位元組)、double(8位元組)、char(1字 節)、boolean(1位)。
  除了以上8種基本資料型別,其餘的都是引用資料型別。
  對應的包裝類分別是:Byte、Short、Integer、Long、Float、Double、 Character、Boolean。

JVM的記憶體區域組成

java把記憶體分兩種:一種是棧記憶體,另一種是堆記憶體

  1. 在函式中定義的基本型別變數(即基本型別的區域性變數)和物件的引用變數(即物件的變數名
    )都在函式的棧記憶體中分配;
  2. 堆記憶體用來存放由new建立的物件和陣列以及物件的例項變數(即全域性變數)

  在函式(程式碼塊)中定義一個變數時,java就在棧中為這個變數分配記憶體空間,當超過變數的作用域後,java會自動釋放掉為該變數所分配的記憶體空間;在堆中分配的記憶體由java虛擬機器的自動垃圾回收器來管理。

堆和棧的優缺點

堆的優勢:是可以動態分配記憶體大小,生存期也不必事先告訴編譯器,因為它是在執行時動態分配記憶體的。
缺點:就是要在執行時動態分配記憶體,存取速度較慢;
棧的優勢:是存取速度比堆要快,僅次於直接位於CPU中的暫存器。另外,棧資料可以共享。
缺點:是存在棧中的資料大小與生存期必須是確定的,缺乏靈活性。
方法區


  方法區中主要儲存所有物件資料共享區域,儲存靜態變數和普通方法、靜態方法、常量、字串常量(嚴格說存放在常量池,堆和棧都有)等類資訊,、說白了就是儲存類的模版。(方法區包含所有的class和static變數

自動拆箱和裝箱

  自動裝箱就是Java自動將原始型別值轉換成對應的物件,比如將int的變數轉換成Integer物件,這個過程叫做裝箱,反之將Integer物件轉換成int型別值,這個過程叫做拆箱。
  因為這裡的裝箱和拆箱是自動進行的非人為轉換,所以就稱作為自動裝箱和拆箱。
原始類
byte,short,char,int,long,float,double和boolean
對應的封裝類Byte,Short,Character,Integer,Long,Float,Double,Boolean。

賦值

在Java 1.5以前我們需要手動地進行轉換才行,而現在所有的轉換都是由編譯器來完成。

//before autoboxing
Integer iObject = Integer.valueOf(3);
Int iPrimitive = iObject.intValue()

//after java5
Integer iObject = 3; //autobxing - primitive to wrapper conversion
int iPrimitive = iObject; //unboxing - object to primitive conversion

方法呼叫

public static Integer show(Integer iParam){
   System.out.println("autoboxing example - method invocation i: " + iParam);
   return iParam;
}

//autoboxing and unboxing in method invocation
show(3); //autoboxing
int result = show(3); //unboxing because return type of method is Integer

物件相等比較

public class AutoboxingTest {

    public static void main(String args[]) {

        // Example 1: == comparison pure primitive – no autoboxing
        int i1 = 1;
        int i2 = 1;
        System.out.println("i1==i2 : " + (i1 == i2)); // true

        // Example 2: equality operator mixing object and primitive
        Integer num1 = 1; // autoboxing
        int num2 = 1;
        System.out.println("num1 == num2 : " + (num1 == num2)); // true

        // Example 3: special case - arises due to autoboxing in Java
        Integer obj1 = 1; // autoboxing will call Integer.valueOf()
        Integer obj2 = 1; // same call to Integer.valueOf() will return same
                            // cached Object

        System.out.println("obj1 == obj2 : " + (obj1 == obj2)); // true

        // Example 4: equality operator - pure object comparison
        Integer one = new Integer(1); // no autoboxing
        Integer anotherOne = new Integer(1);
        System.out.println("one == anotherOne : " + (one == anotherOne)); // false

    }

}

Output:
i1==i2 : true
num1 == num2 : true
obj1 == obj2 : true
one == anotherOne : false

  值得注意的是第三個小例子,這是一種極端情況。obj1和 obj2的初始化都發生了自動裝箱操作。但是處於節省記憶體的考慮,JVM會快取-128到 127的 Integer物件。因為 obj1和 obj2實際上是同一個物件。所以使用”==“比較返回 true。
  另一個需要避免的問題就是混亂使用物件和原始資料值,一個具體的例子就是當我們在一個原始資料值與一個物件進行比較時,如果這個物件沒有進行初始化或者為Null,在自動拆箱過程中obj.xxxValue,會丟擲NullPointerException。


public class Test {

	//基本資料型別的常量池是-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);


	    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));
	    //這時候都是true,因為基本資料型別直接比較值,值一樣就可以。

	}
}

基本資料型別的儲存方式

**上面自動拆箱和裝箱的原理其實與常量池有關**。
存在棧中:
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.

包裝類物件怎麼存
其實我們說的常量池也可以叫物件池。
比如String a= new String("a").intern()時會先在常量池找是否有“a"物件如果有的話直接返回“a"物件在常量池的地址,即讓引用a指向常量”a"物件的記憶體地址。
public native String intern();
Integer也是同理
--------------------- 
作者:How 2 Play Life 
來源:CSDN 
原文:https://blog.csdn.net/a724888/article/details/80048774 
版權宣告:本文為博主原創文章,轉載請附上博文連結!

基本資料型別之間轉換

自動型別轉換: 容量小的型別自動轉換為容量大的資料型別,如下圖:在這裡插入圖片描述

  1. 多種型別的資料混合運算時,系統先自動將所有資料轉換成容量最大的那種 資料型別,再進行計算。
  2. 當把任何基本型別的值和字串值進行連線運算時(+),基本型別的值將自 動轉化為字串型別。當把任何基本型別的值和字串值進行連線運算時(+),基本型別的值將自動轉化為字串型別。

強制型別轉換: 自動型別轉換的逆過程,將容量大的轉換為容量小的資料類 型。

  1. 使用時要加上強制轉換符“()”,但可能造成精度降低或溢位。
  2. 字串不能直接轉換為基本型別,需要藉助基本型別對應的包裝類來實現。
public class Test {
	public static void main(String[] args){
		// java中的型別的自動轉化
		byte b1 = 4;
		int x1 = 3;
		x1 = x1 + b1;
		System.out.println(x1);//x=7
		//java中的強制轉化
		byte b2 =3;
		b2 = b2+1;// 編譯錯誤,需要強轉
		b2 = (byte)(b2+1);
		System.out.println(b2);//cannot convert from int to byte
	
		char ch = 1;
		char ch2 = 'a';
		ch = ch + ch2;//編譯失敗
		ch2 = (char) (ch + ch2);
		char ch3 = 1+'a';
		int ch4 = 1+'a';
		System.out.println("ch3:"+ch3+"    ch4:"+ch4);//ch3:b    ch4:98
		System.out.println(ch2);//b
		
		int i1 = 1;
		int i2 = 2;
		i1 = i1 + i2;
		System.out.println(i1);//3
		
		short s =4;
		s = s+5;//編譯失敗
		s = (short)(s+5);//需要強轉
		s+=5;//編譯成功,做自動轉化

	}
	public static void main(String[] args){
		Integer a = new Integer(3);//手動建立物件,不會引用常量池中的物件;
		Integer a2 = new Integer(0);//手動建立物件,不會引用常量池中的物件;
		Integer b = 3;//將3自動裝箱成為Integer型別
		int c = 3;
		System.out.println(a==b);// false 兩個引用沒有引用同一個物件(易錯)
		System.out.println(a==c);// true 自動拆箱然後在和C進行比較
		System.out.println(a+a2==c);//true java的數學計算實在記憶體棧裡面,java會對a和a2進行拆箱操作
		
		Integer f1 = 100, f2 = 100, f3 = 150, f4 = 150;
		System.out.println(f1==f2);//true
		System.out.println(f3==f4);//false
		//自動裝箱時-128到127引用常量池中的物件,否則新建物件
	}

java常量池

新增連結描述
在這裡插入圖片描述

  • 程式計數器是jvm執行程式的流水線,存放一些跳轉指令。

  • 本地方法棧是jvm呼叫作業系統方法所使用的棧。

  • 虛擬機器棧是jvm執行java程式碼所使用的棧。

  • 方法區存放了一些常量、靜態變數、類資訊等,可以理解成class檔案在記憶體中的存放位置。

  • 虛擬機器堆是jvm執行java程式碼所使用的堆。

  Java中的常量池,實際上分為兩種形態:靜態常量池和執行時常量池。
   所謂靜態常量池,即*.class檔案中的常量池,class檔案中的常量池不僅僅包含字串(數字)字面量,還包含類、方法的資訊,佔用class檔案絕大部分空間。這種常量池主要用於存放兩大類常量:字面量(Literal)和符號引用量(Symbolic References),字面量相當於Java語言層面常量的概念,如文字字串,宣告為final的常量值等,符號引用則屬於編譯原理方面的概念,包括瞭如下三種類型的常量:
  類和介面的全限定名
  欄位名稱和描述符
  方法名稱和描述符
   而執行時常量池,則是jvm虛擬機器在完成類裝載操作後,將class檔案中的常量池載入到記憶體中,並儲存在方法區中,我們常說的常量池,就是指方法區中的執行時常量池。執行時常量池相對於CLass檔案常量池的另外一個重要特徵是具備動態性,Java語言並不要求常量一定只有編譯期才能產生,也就是並非預置入CLass檔案中常量池的內容才能進入方法區執行時常量池,執行期間也可能將新的常量放入池中,這種特性被開發人員利用比較多的就是String類的intern()方法
   String的intern()方法會查詢在常量池中是否存在一份equal相等的字串,如果有則返回該字串的引用,如果沒有則新增自己的字串進入常量池。

常量池的好處
   常量池是為了避免頻繁的建立和銷燬物件而影響系統性能,其實現了物件的共享。例如字串常量池,在編譯階段就把所有的字串文字放到一個常量池中。
(1)節省記憶體空間:常量池中所有相同的字串常量被合併,只佔用一個空間。
(2)節省執行時間:比較字串時,比equals()快。對於兩個引用變數,只用判斷引用是否相等,也就可以判斷實際值是否相等。

String與常量池

String s1 = "Hello";
String s2 = "Hello";
String s3 = "Hel" + "lo";
String s4 = "Hel" + new String("lo");
String s5 = new String("Hello");
String s6 = s5.intern();
String s7 = "H";
String s8 = "ello";
String s9 = s7 + s8;
          
System.out.println(s1 == s2);  // true
System.out.println(s1 == s3);  // true
System.out.println(s1 == s4);  // false
System.out.println(s1 == s9);  // false
System.out.println(s4 == s5);  // false
System.out.println(s1 == s6);  // true

  首先說明一點,在java 中,直接使用= =操作符比較的是兩個字串的引用地址,並不是比較內容,比較內容請用String.equals()。
   s1 = = s2這個非常好理解,s1、s2在賦值時,均使用的字串字面量,說白話點,就是直接把字串寫死,在編譯期間,這種字面量會直接放入class檔案的常量池中,從而實現複用,載入執行時常量池後,s1、s2指向的是同一個記憶體地址,所以相等。
  s1 = = s3這個地方有個坑,s3雖然是動態拼接出來的字串,但是所有參與拼接的部分都是已知的字面量,在編譯期間,這種拼接會被優化,編譯器直接幫你拼好,因此String s3 = “Hel” + “lo”;在class檔案中被優化成String s3 = “Hello”,所以s1 = = s3成立。只有使用引號包含文字的方式建立的String物件之間使用“+”連線產生的新物件才會被加入字串池中
  s1 == s4當然不相等,s4雖然也是拼接出來的,但new String(“lo”)這部分不是已知字面量,是一個不可預料的部分,編譯器不會優化,必須等到執行時才可以確定結果,結合字串不變定理,鬼知道s4被分配到哪去了,所以地址肯定不同。對於所有包含new方式新建物件(包括null)的“+”連線表示式,它所產生的新物件都不會被加入字串池中
在這裡插入圖片描述
  s1 == s9也不相等,道理差不多,雖然s7、s8在賦值的時候使用的字串字面量,但是拼接成s9的時候,s7、s8作為兩個變數,都是不可預料的,編譯器畢竟是編譯器,不可能當直譯器用,不能在編譯期被確定,所以不做優化,只能等到執行時,在堆中建立s7、s8拼接成的新字串,在堆中地址不確定,不可能與方法區常量池中的s1地址相同。
在這裡插入圖片描述
  s4 = = s5已經不用解釋了,絕對不相等,二者都在堆中,但地址不同。
  s1 == s6這兩個相等完全歸功於intern方法,s5在堆中,內容為Hello ,intern方法會嘗試將Hello字串新增到常量池中,並返回其在常量池中的地址,因為常量池中已經有了Hello字串,所以intern方法直接返回地址;而s1在編譯期就已經指向常量池了,因此s1和s6指向同一地址,相等。
特例:

public static final String A = "ab"; // 常量A
public static final String B = "cd"; // 常量B
public static void main(String[] args) {
     String s = A + B;  // 將兩個常量用+連線對s進行初始化 
     String t = "abcd";   
    if (s == t) {   
         System.out.println("s等於t,它們是同一個物件");   
     } else {   
         System.out.println("s不等於t,它們不是同一個物件");   
     }   
 } 
輸出:s等於t,它們是同一個物件在這裡插入程式碼片

  A和B都是常量,值是固定的,因此s的值也是固定的,它在類被編譯時就已經確定了。也就是說:String s=A+B; 等同於:String s=“ab”+“cd”;

public static final String A; // 常量A
public static final String B;    // 常量B
static {   
     A = "ab";   
     B = "cd";   
 }   
 public static void main(String[] args) {   
    // 將兩個常量用+連線對s進行初始化   
     String s = A + B;   
     String t = "abcd";   
    if (s == t) {   
         System.out.println("s等於t,它們是同一個物件");   
     } else {   
         System.out.println("s不等於t,它們不是同一個物件");   
     }   
 } 
s不等於t,它們不是同一個物件

  A和B雖然被定義為常量,但是它們都沒有馬上被賦值。在運算出s的值之前,他們何時被賦值,以及被賦予什麼樣的值,都是個變數。因此A和B在被賦值之前,性質類似於一個變數。那麼s就不能在編譯期被確定,而只能在執行時被建立了。

結論:

  • 必須要關注編譯期的行為,才能更好的理解常量池。
  • 執行時常量池中的常量,基本來源於各個class檔案中的常量池。
  • 程式執行時,除非手動向常量池中新增常量(比如呼叫intern方法),否則jvm不會自動新增常量到常量池。