所谓的常量池技术,可以理解是一种缓存技术,在常量池中存缓存好对象的话,那么程序在执行过程中就不需要重复的创建出来同一个对象,因为常量池里面已经有了,直接将引用指向常量池中的对象即可。需要使用的时候,拿来即用,开袋即食。因为反复创建出相同的对象不仅在创建的时间上面的开销比较大,也是对于内存空间的一种浪费,所以想到了使用常量池技术。
Java 里面的常量池技术有多种,本文重要讨论的是 String 类型的常量池思想,以及基本类型的包装类的常量池思想。
常量池中放置的都是引用类型,也就是各种对象,确切的说是在程序开发过程中,可能会大量重复使用的对象,提前在常量池中创建好,一方面减去了程序运行过程中,重复创建对象的时间,另一方面,减少了重复创建对象的空间。
在 Java 中创建出来的字符串对象
存在的位置有两个:
存在的位置也就是:堆内存中和字符串常量池中;
注意:JDK 1.7 的时候,字符串常量池在方法区中,JDK 1.8 的时候,字符串常量池放在了堆中;重点不是字符串常量池放在了哪里,重点是创建出来的字符串对象有没有放在字符串常量池中;
下面的两张图中表示了 常量池在 JDK 不同版本中的存在位置:
使用 new String("hello");
创建出来的对象是在堆内存空间的,也就是没有放在字符串常量池中。使用 String s = "hello";
是放在字符串常量池中的;
在堆内存创建出来的对象:每次都执行 new String(“hello”);执行了多少次的 new 就会创建出来多少个对象,因为在对象存在堆中没有存放到字符串常量池进行优化;
使用 String s = "hello";
创建出来的字符串对象放置在了字符串常量池中,再次有 String s1 = "hello";
不会在字符串常量池中创建出来新的对象,这样子可以对于频繁的创建出来的相同的字符串对象进行复用,是一种优化的策略;
小案例:
String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing";//常量池中的对象
String str4 = str1 + str2; //在堆上创建的新的对象
String str5 = "string";//常量池中的对象
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false
1、在堆中一定创建一个新的对象
2、检查字符串常量池中是不是存在和 new 出来一样的字符串
3、字符串常量池中存在和 new 出来的字符串一样的字符串,那么字符串常量池中就不需要创建了,否则字符串常量池中需要创建爱你和 new 一样的字符串;
创建出来一个或者两个对象;
一个的解释:首先一定会在堆内存中创建出来一个 String 对象;但是由于字符串常量池中存在了 “abc” 这个字符串所以字符串常量池中就不创建了;
两个的解释:首先肯定在堆内存中创建出来一个 String 对象,此时发现字符串常量池中没有 “abc” 这个对象,所以在字符串常量池中也进行创建一个对象;
重点关注创建出来的对象有没有放置到字符串常量池中,放到了字符串常量池中的 String 对象是不会重复创建的,尽管多次创建但是都会指向一个地址,所以比较引用地址的时候显示的是 true。为了内存的合理利用,但是没有放在字符串常量池中的 String 对象,是会被重复创建的,所以比较引用地址的时候显示的是你false;
简单的代码实例如下:
Integer i1 = 33; // 在缓存
Integer i2 = 33;// 在缓存
System.out.println(i1 == i2);// 输出 true
Integer i11 = 333;// 不在缓存
Integer i22 = 333;// 不在缓存
System.out.println(i11 == i22);// 输出 false
上面的四种基本类型的包装类创建出来的缓存数据分别是:-128 - 127
对于基本类型的包装类型:
Integer i1 = 40;// 在缓存
Integer i2 = new Integer(40);// 不在缓存
System.out.println(i1==i2); // false
Integer i1 = 40; 创建出来的对象是在常量池中,但是 Integer i2 = new Integer(40); 创建出来的对象是在堆内存中的,所以返回的数据就是 false;
下面再看一段代码,由于自动装箱以及拆箱的操作使得判断的情况和String不一样:
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);// true
System.out.println(i1 == i2 + i3);//true
System.out.println(i1 == i4);// false
System.out.println(i4 == i5);// false
System.out.println(i4 == i5 + i6);// true 堆创建 但是对象没法比较所以拆箱了,拆箱了比较的是具体的数值,不是引用地址了
System.out.println(40 == i5 + i6);// true 堆创建 但是对象没法比较所以拆箱了,拆箱了比较的是具体的数值,不是引用地址了
i1 , i2 , i3 都是常量池中的对象,i4 , i5 , i6 是堆中的对象。
i4 == i5 + i6 为什么是 true 呢?因为, i5 和 i6 会进行自动拆箱操作,进行数值相加,即 i4 == 40 。 Integer 对象无法与数值进行直接比较,所以 i4 自动拆箱转为 int 值 40,最终这条语句转为 40 == 40 进行数值比较
Byte: -2 ^7 --- 2 ^ 7 - 1
Short -2 ^ 15 ---2 ^ 15 - 1
Integer -2 ^ 31 ---2 ^ 31 - 1
Long -2 ^ 63 ---2 ^ 63 - 1
因为没有实现常量池技术,所以比较的结果就是 false
Double i3 = 1.2;
Double i4 = 1.2;
System.out.println(i3 == i4);// 输出 false
在源代码中缓存了基本的 ASCII 的全部字符 0 到 127 ,缓存了 128 个字符
private static class CharacterCache {
private CharacterCache(){}
// 整个 ASCII 表进行了缓存,因为这些字符使用的频率比较高
static final Character cache[] = new Character[127 + 1];
static {
for (int i = 0; i < cache.length; i++)
cache[i] = new Character((char)i);
}
}
所谓的 ASCII 就是将常见的 127 个字符进行编码,使用数字和符号进行一一的对应,给这个常见的 127 个字符使用唯一的数字编码对应;
使用 char 定义数据的时候,可以定义数字比如:char ch = 65; 这个字符就是 65 对应的那个符号,输出的话,也是 65 对应的符号;
@Test
public void testChar() {
char testChar = 65;
System.out.println(testChar); // A 65对应的 ASCII 字符就是A
}
char 数据类型的前 127 个字符就是 ASCII 字符;也就是说 char 类型里面包含了 ASCII 里面的 127 的字符;可以直接定义 char ch = 数字;
或者 char ch = ' ';
前者是根据字符的编号寻找对应确定的字符,后者是直接定义到指定的字符;
只是缓存了 true 和 false 两种数据
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}