本篇文章主要记录一下String类中一些常见问题,不定时更新。
常量池
常量是指被final修饰的变量,值一旦确定就无法改变,类似于C语言的Const。
常量池主要用来存放两大类常量:字面量和符号引用量,字面量相当于Java语言的常量,如文本字符串,声明为final的常量等,符号引用包括以下三种
类和接口的全限定名
字段名称和描述符
方法名称和描述符
class文件中的常量池中的内容会在类加载后进入方法区的运行时常量池。相对于常量池,运行时常量池的重要特征是具有动态性,java并不要求常量只有在编译器才会产生,运行期间也可以将新的常量存放入池中,这种特性用的最多的String类中的intern()方法。
常量池是为了避免频繁的创建和销毁对象造成系统性能的浪费,实现了对象的共享。
==号比较基本数据类型,就是比值,比较引用数据类型比较的是内存中存放的地址。
下面通过具体例子来讲解一下:
public static void main(String[] args) {
String s1 = "hello";
String s2 = new String(s1);
char[] str = {'h','e','l','l','o'};
String s3 = new String(str);
System.out.println("s1的hashCode:" + s1.hashCode());
System.out.println("s2的hashCode:"+ s2.hashCode());
System.out.println("s3的hashCode:" + s3.hashCode());
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));
System.out.println(s1 == s3);
System.out.println(s1.equals(s3));
System.out.println(s2 == s3);
System.out.println(s2.equals(s3));
System.out.println("---------------------------");
System.out.println(s1 == s2.intern());
}
结果如下
我们来分析一下==和equals以及intern()
上面已经说过 == 比较的是内存地址,equals 比较的是内容,这里我需要特别说明一下:hahs值不等于内存地址!!如果你没有重写hashcode()方法,那么JVM默认的算法是用内存地址经过散列得到hash值,如果你重写了hashCode()方法就用你重写后的。这里在补充一点,你在程序中看到的“内存”地址,不能称为真正的物理内存地址,都是虚拟内存地址。
有了上面的基础知识,对于equals()的结果应该都没有什么疑问。那么 == 结果和intern()是为什么呢?
首先简单介绍一下JVM中的常量池,常量池是以表的形式存在的,具体如下。
这里我们主要关注CONSTANT_Utf8_info 和CONSTANT_String_info,具体细节后续补充,这里只需要知道CONSTANT_String_info中的常量其实都是对CONSTANT_Utf8_info的引用,而在创建String对象时都会先查看String常量池是否存在该对象,没有的话会现在常量池创建该对象。
看上图,s1 == s2 比较的是s1 和s2的地址,很明显一个在常量池,一个在堆中,因此不等。而intern()方法会返回常量池的引用,因此s1 == s2.intern(),因为他们地址都是常量池的对象地址。
还有一个通过字节数组创建的对象没有说,下面看String类的源码。
因此他们的内存地址也不同,即==结果为false。
下面来看hashcode()为什么相同
同时需要注意
equals()方法
看到这里,不禁会想到,hashcode()为什么要重写?貌似没什么用?因为hashcode()重写后也不代表内存地址,==用不上,equals里面也没用。这其实要设计到Java中关于hashcode的规定
两个对象相等,hashcode一定相等
两个对象不等,hashcode不一定不等
hashcode相等,两个对象不一定相等
hashcode不等,两个对象一定不等
为什么会有这种规定呢?,主要是使用一些集合容器时,为了加快hash表的操作,需要先比较hashcode值,如果不重写hashcode,可能两个内容相同的对象的hashcode不一样(因为默认的hashcode是根据内存计算的)。也就是会出现equals相等而hashcode不等的情况,这样你在使用HashMap之类的容器时就会产生一些不符合预期的结果。
Interger类中的缓存
Java5为Integer的操作引入了一个新的特性,用来节省内存和提高性能。整型对象在内部实现中通过使用相同的对象引用实现了缓存和重用。
上面的规则默认适用于整数区间 -128 到 +127(这个整数区间可以通过启动应用的虚拟机参数修改:-XX:AutoBoxCacheMax)。这种Integer缓存策略仅在自动装箱(autoboxing)的时候有用,使用构造器创建的Integer对象不能被缓存。Java 编译器把原始类型自动转换为封装类的过程称为自动装箱(autoboxing),这相当于调用 valueOf 方法。
// IntegerCache,Integer类的内部类,注意它的属性都是定义为static final
private static class IntegerCache {
//缓存的下界,-128,不可变
static final int low = -128;
//缓存上界,暂为null
static final int high;
//缓存的整型数组
static final Integer cache[];
static {
// 缓存上界,可以通过JVM参数来配置
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);
//最大的数组值是Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low));
}
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() {
}
}
-XX:AutoBoxCacheMax这个参数是设置Integer缓存上限的参数,在JVM初始化期间java.lang.Integer.IntegerCache.high属性可以被设置和保存在私有的系统属性sun.misc.VM class中。理论上讲,当系统需要频繁使用Integer时,或者说堆内存中存在大量的Integer对象时,可以考虑提高Integer缓存上限,避免JVM重复创造对象,提高内存的使用率,减少GC的频率,从而提高系统的性能。
理论归理论,这个参数能否提高系统系统关键还是要看堆中Integer对象到底有多少、以及Integer的创建的方式。如果堆中的Integer对象很少,重新设置这个参数并不会提高系统的性能。即使堆中存在大量的Integer对象,也要看Integer对象时如何产生的。
大部分Integer对象通过Integer.valueOf()产生。说明代码里存在大量的拆箱与装箱操作。这时候设置这个参数会系统性能有所提高。
大部分Integer对象通过反射,new产生。这时候Integer对象的产生大部分不会走valueOf()方法,所以设置这个参数也是无济于事。
常见的使用缓冲池的包装类还有Byte、Short、Long
Byte,Short,Long 的缓存池范围默认都是: -128 到 127。可以看出,Byte的所有值都在缓存区中,用它生成的相同值对象都是相等的。
同时Character 对象也有CharacterCache 缓存 池,范围是 0 到 127
Integer的缓存上限可以通过Java虚拟机参数修改,Byte、Short、Long、Character的缓存则没法修改。同时注意浮点类型的数据Java中没有缓存。