原标题:Java中的缓存设计
之所以写这篇博客,是因为在学习JavaSE的时候遇到了一个问题,由解决问题延伸到该类问题的总结,大概讲讲解决这个问题的思路:
一: java中基本类型对于的包装类的缓存设计:
查看Integer的源代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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() {}
}
从上面截取的源代码来看,Integer类中是有一个静态内部类的,这个静态内部类的作用是一个常量数组,里面放的是256个Integer对象的地址,而且这256个Integer对象的值是从-128到127的!这就是java对Integer类做的缓存设计!这个静态数组在.class文件加载的时候就在方法区中创建了!!每当我们要使用基本类型的自动包装来初始化对象的时候,程序会先去遍历这个方法区中常量池中的静态数组中是不是有值一样的对象,如果有的话,就把当前对象地址返回给需要的引用,从而不必再花时间和空间去堆中创建新对象了!如果在常量池中没有找到同值的对象,那么程序就会在堆中开辟一个新的对象并返回地址给需要的引用!这就解释了如下代码的结果:
测试源码:
1
2
3
4
5
6
7
8
9
public class IntegerBoxing {
public static void main(String[] args) {
Integer i = 1 ; //1在缓存中,所以直接把缓存中值为1的对象地址给i
Integer i1 = 1 ; //同上理
System.out.println(i = i1); //答案 true
Integer m = 220 ; //220没有在 [-128 , 127]区间内,所以在堆中新开辟一个空间
Integer n = 220 ; //同理
System.out.println(m == n); //false
2.通过上面对Integer类的探究,我们直接看看其他的基本类型是不是有缓存设计,如果有的话是如何设计的:
Byte类的源码中的部分:
1
2
3
4
5
6
7
8
private static class ByteCache {
private ByteCache(){}
static final Byte cache[] = new Byte[-(- 128 ) + 127 + 1 ];
static {
for ( int i = 0 ; i < cache.length; i++)
cache[i] = new Byte(( byte )(i - 128 ));
}
}
Short的源代码是怎么设计缓存的
1
2
3
4
5
6
7
8
private static class ShortCache {
private ShortCache(){}
static final Short cache[] = new Short[-(- 128 ) + 127 + 1 ];
static {
for ( int i = 0 ; i < cache.length; i++)
cache[i] = new Short(( short )(i - 128 ));
}
}
Long类型的源代码设计,如下
1
2
3
4
5
6
7
8
private static class LongCache {
private LongCache(){}
static final Long cache[] = new Long[-(- 128 ) + 127 + 1 ];
static {
for ( int i = 0 ; i < cache.length; i++)
cache[i] = new Long(i - 128 );
}
}
Float类型,Double类型
在查看了Float和Double的源代码,确实里面没有缓存设计!!!这要注意和前面的区分了
Character类型是有缓存的,我们这就去看看源代码的设计
1
2
3
4
5
6
7
8
private static class CharacterCache {
private CharacterCache(){}
static final Character cache[] = new Character[ 127 + 1 ];
static {
for ( int i = 0 ; i < cache.length; i++)
cache[i] = new Character(( char )i);
}
}
Boolean类型的源代码中有缓存设计,代码如下:
1
2
3
4
5
public static final Boolean TRUE = new Boolean( true ); //缓存值为true
...
public static final Boolean FALSE = new Boolean( false ); //缓存值为:false
看出Boolean类型是有 2 个缓存值的
================
好了,到这儿我们就把基本类型的包装类的缓存设计带来的“==”问题解决完了。之所以有缓存设计是因为这些是我们经常用的数据对象,设定合理的缓存,可以大大提高我们代码执行速度(不用再开辟空间而耗时间了)和大大节约内存空间(不用重复new对象)
二.String类型的缓存设计
1.首先去看看源代码
1
2
3
4
5
6
7
private final char value[];
...
public String() {
this .value = new char [ 0 ];
}
从源代码看出,java在设计String类的时候是有缓存的!缓存的值在常量池里
2.String类型的缓存理解例子
测试代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class StringDemo {
private static String getXx(){
return "AB" ;
}
public static void main(String[] args) {
String str1 = "ABCD" ; //@1
String str2 = "A" + "B" + "C" + "D" ; //@2
String str3 = "AB" + "CD" ; //@3
String str4 = new String( "ABCD" ); //@4
String temp = "AB" ; //@5
String str5 = temp + "CD" ; //@6
String str6 = getXx()+ "CD" ; //@7
System.out.println(str1 == str2); //@8 true
System.out.println(str3 == str2); //@9 true
System.out.println(str3 == str4); //@10 false
System.out.println(str4 == str5); //@11 false
System.out.println(str5 == str6); //@12 false
}
}
这儿要区别一下编译时常量和运行时常量,编译时常量表示的是编译器在编译代码的时候就可把可以确定的常量值;而运行是常量是指运行的时候才可以确定的常量的值(也就是程序运行的时候,比如调用构造方法的时候才可以确定的常量值)。一旦常量值被确定就会被放入常量池中从当缓存的字面量!
1
2
3
4
5
6
7
8
9
10
11
12
解析:
1 ). @1 和 @2 和 @3 是编译时常量,也就是编译的时候就已经确定了的常量,在字节码文件加载的时候,
就开始把这些确定了的常量放入常量池。编译的时候就把“ABCD”字面量放入了常量池,在程序运
行的时候, @1 操作会把常量池中的值为“ABCD”对象的地址赋给str1; @2 操作也是把常量池中的值
为“ABCD”对象的地址赋给str2; @3 操作也一样,所以str1 = str2 = str3
2 )str4是 new 的对象,直接在堆中开辟空间,并把地址赋给str4,所以str4和前面的 3 个引用地址都
是不一样的
3 )str5是运行时常量,运行的时候,会在堆中开辟一个空间,其中的值是有常量池中的“CD”
和“AB”做字符串的串联操作得到的,新地址在堆中,所以str5是和str4以及前面 3 个引用地址都是
不一样的
4 )str6是运行是常量,只有运行时调用getXx()方法才会在堆中创建对象返回地址给str6,所以
str6与前面的 5 个地址都是不同的!!
三.equals方法在8总基本类型的包装类和String类中的体现
1.所有类都继承Object类,equals方法是该类原生的方法,如果子类没有重写该方法,那么调用该方法就是比较的是两个对象的地址是否相同!其中Object类该方法api如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public boolean equals(Object obj)
指示其他某个对象是否与此对象“相等”。
equals 方法在非空对象引用上实现相等关系:
自反性:对于任何非空引用值 x,x.equals(x) 都应返回
true 。
对称性:对于任何非空引用值 x 和 y,当且仅当
y.equals(x) 返回 true 时,x.equals(y) 才应返回
true 。
传递性:对于任何非空引用值 x、y 和 z,如果
x.equals(y) 返回 true ,并且 y.equals(z) 返回
true ,那么 x.equals(z) 应返回 true 。
一致性:对于任何非空引用值 x 和 y,多次调用
x.equals(y) 始终返回 true 或始终返回 false ,前提是对象上
equals 比较中所用的信息没有被修改。
对于任何非空引用值 x,x.equals( null ) 都应返回
false 。
Object 类的 equals 方法实现对象上差别可能性最大的相等关系;即,对于任何非空引用值
x 和 y,当且仅当 x 和 y 引用同一个对象时,此方法才返回 true (x == y 具有值 true )。
注意:当此方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。
2.查看包装类的api发现其中的equals方法是被重写了的,equals方法是比较的是对象的值是否相等,而非对象的地址。具体如下:
1
2
3
4
public boolean equals(Object obj)
比较此对象与指定对象。当且仅当参数不为 null ,并且是一个与该对象包含相同 int 值的Integer
对象时,结果为 true 。
3.查看String类的api发现其中的equals方法也是被重写了的,equals方法比较的是字符串的值是否相等,而非地址是否相等,如下:
1
2
3
4
public boolean equals(Object anObject)
将此字符串与指定的对象比较。当且仅当该参数不为 null ,并且是与此对象表示相同字符序列的
String 对象时,结果才为 true 。
责任编辑: