0%

String类小demo

关于string类的一些小demo。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package cn.itcast.String.demo;

public class StringDemo {

/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
/*
* String类的特点;
* 字符串对象一旦被初始化就不会被改变。
*/
StringDemo2();
}

public static void StringDemo2() {
// TODO Auto-generated method stub

String s = "abc";//创建一个字符串对象在常量池中
String s1 = new String("abc");//创建两个对象一个new,一个字符串对象在堆内存中。
System.out.println(s==s1);//,比较地址值,是同一个字符串指向不同的引用,所以是false
System.out.println(s.equals(s1));//true//String类的中equals复写了object中的equals方法,建立了String类自己
//判断字符串对象是否相同的依据,实际上就是比较字符串的内容(不是地址值)。
// System.out.println("s="+s);
// System.out.println("s1="+s1);
}

/**
* 演示字符串定义的第一中方式,并明确字符串常量池特点,
* 池中没有就建立,有就直接用。
*/
private static void StringDemo1() {
String s = "abc";//"abc"存储在字符串常量池中。
// s = "nba";
String s1 = "abc";
System.out.println(s==s1);//true ==比较地址值
// System.out.println("s="+s);
}

}

再看一段代码,我们看看String的intern()方法:

1
2
3
4
5
6
7
8
9
String ss = "abc";
String s = new String("abc");
String s1 = new String("abc").intern();

System.out.println(ss == s);
System.out.println(ss == s1);
//打印结果
false
true

为什么会是一个false一个true呢?一个String实例str调用intern()方法时,java会查找常量池中是否有相同unicode的字符串常量,如果有就返回它的引用,如果没有就则在常量池中增加一个unicode等于str的字符串并返回它的引用。
那上面的代码为什么会打印出不同的结果呢?
字面量和运行时常量池
JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化,为了减少在JVM中创建的字符串常量,字符串类维护了一个字符串常量池。
在JVM运行时区域的方法中,有一块区域是运行时常量池,主要用来存储编译期生成的各种字面量符号引用
比如java的反编译,在java代码被javac编译后,文件结构中是包含一部分Constant pool的,比如如下代码:

1
2
3
public static void main(String[] args) {
String ss = "abc";
}

经过编译后,在常量池有如下几个重要的内容:

1
2
#21 = Utf8  ss
#22 = Utf8 abc

ss是符号引用,而abc就是我们提到的字面量,class文件中的常量池部分的内容,会在运行期间被运行时常量池加载进去。
new String究竟创建了几个对象
String ss = new String(“abc”);创建了几个对象,我们可以知道的信息时s和abc被加载到class文件的常量池中,然后在类加载阶段,这两个常量会进入常量池。不过这个”进入常量池”阶段,并不会直接把所有类中定义的常量全部加载进来,而是会做个比较,如果需要加到字符串常量池中的字符串已经存在,那就不需要再把字符串字面量加载进来了。
所以,我们说”若常量池中已经存在”abc”,则直接引用,也就是说此时只会创建一个对象”说的就是这个字符串字面量在字符串池中被创建的过程。
再看看运行期间,new String(“abc”);执行的时候是要在java堆中创建一个字符串的,而这个对象所对应的字符串字面量是保存在字符串常量池中的。但是 String s = new String(“abc”);对象的符号引用s是保存在java虚拟机的栈上的,它保存的是堆中刚刚创建出来的字符串对象的引用。
所以对于如下代码:

1
2
3
4
String ss = new String("abc");
String s = new String("abc");
//输出false
System.out.println(ss == s);

输出false,因为 == 比较的是两个对象的地址值。但是使用equals()方法做比较就是比较字符串的字面量了,就会得到true(实际上Object中的equals方法的默认实现也是比较地址值,只不过String类复写了Object类中的equals方法,有自己的实现,这点要搞清楚,equals方法通常在需要比较两个对象的时候调用,一般这个时候我们都会复写它)。
常量池的对象时在编译期就确定好了的,在类被加载的时候创建的,如果类被加载时,该字符串常量在常量池中已经有了,这一步就省略了。堆中的对象时在运行期才确定的,在代码执行期到new的时候创建的。

运行时常量池的动态扩展
编译期生成的各种字面量和符号引用是运行时常量池中比较重要的一部分来源,但并不是全部。那么还有一种情况,可以在运行期间向运行时常量池中增加常量,这就是String的intern()方法。
String类的对象调用intern()方法时,java查找常量池中是否有相同unicode的字符串常量,若有,则返回其引用;若没有则在常量池中增加一个Unicode等于str的字符串并返回它的引用。
intern()方法有两个作用,第一个是将字符串字面量放入常量池,第二个是返回这个常量的引用。
再看这个例子:

1
2
3
4
5
6
7
8
9
String ss = "abc";
String s = new String("abc");
String s1 = new String("abc").intern();

System.out.println(ss == s);
System.out.println(ss == s1);
//运行结果
false
true

可以简单理解为String ss = “abc”;和String s1 = new String(“abc”).intern();做的事是一样的(实际有区别,暂时不展开),都是定义一个字符串对象,然后将其字面量保存在常量池中,并把这个字面量的引用返回给定好的对象引用。
对于String s1 = new String(“abc”).intern();在不调用intern的情况下,s1是指向JVM在堆中创建的那个对象的引用,但执行了intern方法,s1将指向字符串常量池中的字符串常量。由于ss和s1都是字符串常量池中的字面量的引用,所以ss == s1,但是s的引用是堆中的对象,ss != s。
intern的正确用法
常量池是要保存已确定的字面量值。也就是说,对于字符串的拼接,纯字面量和字面量的拼接,会把拼接结果作为常量保存到字符串常量池。如果在字符串的拼接中,有一个参数是非字面量,而是一个变量的话,整个拼接操作会被编译成StringBuilder.append,这种情况编译器是无法知道其确定值的。只有在运行期间才能确定。
有这个特性,intern就派上用场了,很多时候,程序中得到的字符串是只有在运行期才能确定的,在编译器间是无法确定的,那么也就没办法在编译期间被加入到常量池中。
这时候,对于那种可能被经常使用的字符串,使用intern进行定义,每次JVM运行代这段代码的时候会直接返回常量池中该字面量的引用,这样就可以减少大量字符串对象的创建了。
为什么java把字符串设计成不可变的

1
2
String s  ="abc";
String s1 = "abc";

只会在堆中的常量池建立一个对象。如果可变的话,当两个引用指向同一个字符串时,对其中一个做修改就会影响另外一个。
缓存hash码
java中会经常用到字符串的hash码(hashcode),例如,在hashmap中,字符串的不变性能保证其hashcode永远保持一致,这样就可以避免一些不必要的麻烦,也意味着每次在使用一个字符串的hashcode的时候不用重新计算一次,这样更加高效。