JAVA常量池,一篇文章就足够入門了。(含圖解)

河海哥yyds 2022-01-08 06:28:26 阅读数:419

java 常量 一篇 篇文章 文章

前言

一直在《深入理解JVM》對常量池只有一個淺薄的了解,之前也遇到過這種題目,今天還是要挑出來進行一次全方比特的了解。

常量池分類

常量池大體可以分為:靜態常量池,運行時常量池。

  • 靜態常量池 存在於class文件中,比如經常使用的javap -verbose中,常量池總是在最前面把?

  • 運行時常量池呢,就是在class文件被加載進了內存之後,常量池保存在了方法區中,通常說的常量池 值的是運行時常量池。所以呢,討論的都是運行時常量池

字符串常量池

最最最流行的、最典型的就是字符串了

典型範例:

String a = "abc";
String b = new String("abc");
System.out.println(a == b);
----*----
結果:false

這裏寫圖片描述
這個是第一個需要理解的地方,a指向哪片內存,b又指向哪片內存呢?對象儲存在堆中,這個是不用質疑的,而a作為字面量一開始儲存在了class文件中,之後運行期,轉存至方法區中。它們兩個就不是同一個地方存儲的。知道了它之後我們就可以通過實例直接進一步了解了

實例

 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

分析:
1、s1 = = s2 很容易可以判斷出來。s1 和 s2 都指向了方法區常量池中的Hello。
2、s1 = = s3 這裏要注意一下,因為做+號的時候,會進行優化,自動生成Hello賦值給s3,所以也是true
3、s1 = = s4 s4是分別用了常量池中的字符串和存放對象的堆中的字符串,做+的時候會進行動態調用,最後生成的仍然是一個String對象存放在堆中。
這裏寫圖片描述
4、s1 = = s9 在JAVA9中,因為用的是動態調用,所以返回的是一個新的String對象。所以s9和s4,s5這三者都不是指向同一塊內存
這裏寫圖片描述
5、s1 = = s6 為啥s1 和 s6地址相等呢? 歸功於intern方法,這個方法首先在常量池中查找是否存在一份equal相等的字符串如果有的話就返回該字符串的引用,沒有的話就將它加入到字符串常量池中,所以存在於class中的常量池並非固定不變的,可以用intern方法加入新的

需要注意的特例

1、常量拼接

 public static final String a = "123";
public static final String b = "456";
public static void main(String[] args)
{
String c = "123456";
String d = a + b;
System.out.println(c == d);
}
------反編譯結果-------
0: ldc #2 // String 123456
2: astore_1
3: ldc #2 // String 123456
5: astore_2
6: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;

我們可以發現,對於final類型的常量它們已經在編譯中被確定下來,自動執行了+號,把它們拼接了起來,所以就相當於直接”123” + “456”;

2、static 靜態代碼塊

 public static final String a;
public static final String b;
static {
a = "123";
b = "456";
}
public static void main(String[] args)
{
String c = "123456";
String d = a + b;
System.out.println(c == d);
}
------反編譯結果-------
3: getstatic #3 // Field a:Ljava/lang/String;
6: getstatic #4 // Field b:Ljava/lang/String;
9: invokedynamic #5, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;

上個例子是在編譯期間,就已經確定了a和b,但是在這段代碼中,編譯期static不執行的,a和b的值是未知的,static代碼塊,在初始化的時候被執行,初始化屬於類加載的一部分,屬於運行期。看看反編譯的結果,很明顯使用的是indy指令,動態調用返回String類型對象。一個在堆中一個在方法區常量池中,自然是不一樣的。

包裝類的常量池技術(緩存)

簡單介紹

相信學過java的同學都知道自動裝箱和自動拆箱,自動裝箱常見的就是valueOf這個方法,自動拆箱就是intValue方法。在它們的源碼中有一段神秘的代碼值得我們好好看看。除了兩個包裝類Long和Double 沒有實現這個緩存技術,其它的包裝類均實現了它。

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 =
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() {}
}

分析:我們可以看到從-128~127的數全部被自動加入到了常量池裏面,意味著這個段的數使用的常量值的地址都是一樣的。一個簡單的實例

Integer i1 = 40;
Integer i2 = 40;
Double i3 = 40.0Double i4 = 40.0;
System.out.println("i1=i2 " + (i1 == i2));
System.out.println("i3=i4 " + (i3 == i4));
-----結果----
true
false

原理如下:
1、== 這個運算在不出現算數運算符的情况下 不會自動拆箱,所以i1 和 i 2它們不是數值進行的比較,仍然是比較地址是否指向同一塊內存

2、它們都在常量池中存儲著,類似於這樣
這裏寫圖片描述

3、編譯階段已經將代碼轉變成了調用valueOf方法,使用的是常量池,如果超過了範圍則創建新的對象

 2: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;

複雜實例[-128~127]

 Integer i1 = 40;
Integer i2 = 40;
Integer i3 = 0;
Integer i4 = new Integer(40);
Integer i5 = new Integer(40);
Integer i6 = new Integer(0);
System.out.println("i1=i2 " + (i1 == i2));
System.out.println("i1=i2+i3 " + (i1 == i2 + i3));
System.out.println("i1=i4 " + (i1 == i4));
System.out.println("i4=i5 " + (i4 == i5));
System.out.println("i4=i5+i6 " + (i4 == i5 + i6));
System.out.println("40=i5+i6 " + (40 == i5 + i6));
----結果----
(1)i1=i2 true
(2)i1=i2+i3 true
(3)i1=i4 false
(4)i4=i5 false
(5)i4=i5+i6 true
(6)40=i5+i6 true

它們的內存分布大概如下
這裏寫圖片描述
注意點
1、當出現運算符的時候,Integer不可能直接用來運算,所以會進行一次拆箱成為基本數字進行比較

2、==這個符號,既可以比較普通基本類型,也可以比較內存地址看比較的是什麼了

分析:
(1)號成立不用多說
(2)號成立是因為運算符自動拆箱
(3)(4)號是因為內存地址不同
(5)(6)號都是自動拆箱的結果

PS:equals方法比較的時候不會處理數據之間的轉型,比如Double類型和Integer類型。

超過範圍

假設一下,如果超出了這個範圍之後呢?正如前文所言,所有的都將成為新的對象

 Integer i1 = 400;
Integer i2 = 400;
Integer i3 = 0;
Integer i4 = new Integer(400);
Integer i5 = new Integer(400);
Integer i6 = new Integer(0);
Integer i7 = 1;
Integer i8 = 2;
Integer i9 = 3;
System.out.println("i1=i2 " + (i1 == i2));
System.out.println("i1=i2+i3 " + (i1 == i2 + i3));
System.out.println("i1=i4 " + (i1 == i4));
System.out.println("i4=i5 " + (i4 == i5));
System.out.println("i4=i5+i6 " + (i4 == i5 + i6));
System.out.println("400=i5+i6 " + (400 == i5 + i6));
----結果----
i1=i2 false
i1=i2+i3 true
i1=i4 false
i4=i5 false
i4=i5+i6 true
400=i5+i6 true

總結

關於常量池部分的總結到這裏,通過實際的例子和繪圖來熟悉了下字符串常量池和包裝類的常量池的使用。其中還包括了裝箱和拆箱的小知識。收獲還是豐厚的,終於明白了常量池的內容了。~happy-。-,如有筆誤,還望糾正

版权声明:本文为[河海哥yyds]所创,转载请带上原文链接,感谢。 https://gsmany.com/2022/01/202201080628261631.html