常量池
首先来说一下常量池的概念:
- 常量池是Java的一项技术,在八种基础数据类型(byte、short、int、long、float、double、char、boolean)中,除了float和double都实现了常量池技术。
- 常量池把经常用到的数据存放在某块内存中,避免频繁的数据创建与销毁,实现数据共享,提高系统性能。
- 在JDK版本1.7后,字符串常量池被实现在Java堆内存中。
String
String的定义和初始化
我们知道String有两种方式进行初始化:
1.String s1 = “hello”;
2.String s2 = new String(“hello”);
问题1:
我们知道s1、s2最后都指向了一个"hello"的字符串,但是如果在上述代码的基础上,我们System.out.println(s1 == s2);会得到什么呢?
结果是false。
我们先来看一下第一行代码
String s1 = "hello";
在这段代码中,我们直接通过双引号“”声明了字符串,在这种声明方式下,虚拟机首先会到堆内存中的字符串常量池中查找该字符串是否已存在,如果存在,会直接返回该引用,如果不存在,则会在堆内存中创建该字符串对象,然后到字符串常量池中注册该字符串。需要注意的是,字符串常量池中
注册只是字符串对象的内存地址,也就是指向字符串对象的引用。
在本例中,虚拟机首先会到字符串常量池中查找是否有“hello”字符串对应的引用,发现没有后,会在堆内存中创建“hello”字符串对象(设内存地址0x0001),然后到字符串常量池中注册地址为0x0001的"hello”对象,也就是添加指向0x0001的引用,最后把字符串对象返回给s1。
ps:字符串常量池底层是用HashTable实现的, 存储的是键值对。
我们再来看一下第二行代码
String s2 = new String("hello");
在使用new关键字创建字符串对象的这种声明方式的时候,JVM将不会查询字符串常量池,会直接在堆内存中创建一个字符串对象,并返回给所属变量。同时,这个字符串对象也不会在字符串常量池中注册。
综上,我们可以得出结果:s1和s2指向的是两个完全不同的对象,他们在堆里的地址并不相等,所以s1 == s2的时候会返回false。
问题二:
如果代码的顺序是:
String s1 = new String(“hello”);
String s2 = “hello”;
System.out.prinln(s1== s2);
那输出的是什么呢?
答案是false。在第一句中,没有访问常量池,直接在内存中创建了一个"hello"的字符串对象。在第二句中,首先访问常量池,但我们在第一句中并没有注册"hello"的引用到常量池中,所以会发现常量池没有“hello”字符串对应的引用。然后也会在堆内存中新建一个字符串对象,然后注册到字符串常量池中。所以s1 == s2自然是false了。
问题三:
String s1 = new String(“hello”);
String s2 = “hello”;
System.out.prinln(s1.equals(s2));
答案是true,String类重写了equals(),比较的是两者的值是否相等,在上述例子中,s1和s2的值都是“hello”,所以输出的是true。
总结:
当用new关键字创建字符串对象时, 不会查询字符串常量池; 当用双引号直接声明字符串对象时, 虚拟机将会查询字符串常量池. 说白了就是: 字符串常量池提供了字符串的复用功能, 除非我们要显式创建新的字符串对象, 否则对同一个字符串虚拟机只会维护一份拷贝。
String 的拼接
-
String s1 = new String(“hello”) + new String(“world”);
-
String s2 = “hello” + “world”
;
第一行代码String s1 = new String(“hello”) + new String(“world”);的执行过程是这样的:
1.依次在内存中创建"hello"和"world"两个字符串对象
2.然后把它们拼接起来(底层使用StringBuilder实现)
3.在拼接完成后会产生新的"hello world"对象,这时变量s1指向新对象"hello wold"
问题一:
String s1 = “hello”;
String s2 = “world”;
String s3 = s1 + s2;
String s4 = “hello world”;
System.out.println(s3 == s4);
我们知道,在执行完第一行代码和第二行代码后,堆内存中分别有"hello"的字符串对象和"world"的字符串对象,并且它们都在字符串常量池中注册了。
那执行第三句代码 String s3 = s1 + s2; 时,JVM又是怎么处理的呢?(其实就是不知道s1 + s2在创建完新字符串“hello world”后是否会在字符串常量池中注册,即不知道使用双引号的形式还是用new关键字的形式进行声明)
其实在s3的拼接过程中,会创建一个StringBuilder对象,然后将s1 ,s2分别append到StringBuilder中,也就是执行了两次append操作,最后调用StringBuilder的toString()获得字符串hello world并存放到s3。而StringBuilder的toString()方法是通过new的方式,将StringBuilder转换成String的。
也就是说,字符串常量池中并没有存储"hello world"的引用,所以s3和s4指向的不是同一个字符串对象,输出false。
问题二:
String s1 = new String(“hello”);
String s2 = new String(“world”);
String s3 = s1 + s2;
String s4 = “hello world”;
System.out.println(s3 == s4);
答案仍然会是false,因为用new形式声明的字符串在拼接的时候依旧是用StringBuilder进行拼接。
StringBuilder、StringBuffer
底层实现
String类是通过char类型数组实现的。但这个数组使用了final进行修饰。
StringBuilder、StringBuilder两者都继承自AbstractStringBuilder类,也是通过char类型数组实现的,但并没有用final进行修饰。
StringBuilder和StringBuffer的区别
通过StringBuilder和StringBuffer继承自同一个父类这点,可以推断出他两的方法都是差不多的,事实上,只不过StringBuffer在方法上添加了synchronized关键字,证明它的方法绝大多数都是线程同步方法,也就是说在多线程的情况下,我们可以使用StringBuffer来保证线程安全,在单线程环境下我们可以使用StringBuilder来获得更高的效率。
String 和StringBuilder的区别
-
对于String,范式涉及到返回参数类型为String类型的方法,在返回的时候都会通过new关键字创建一个新的字符串对象;
如subString()、concat()都是最终返回一个new关键字创建的新的字符串对象 -
而对于StringBuilder,大多数方法都会返回StringBuilder对象自身。如append()、replace();
-
在拼接方面,当String类拼接字符串时,每次都会生成一个新的StringBuilder对象,然后调用两次append()方法把字符串拼接好,最后通过StringBuilder的toString()方法new出一个新的字符串对象。而StringBuilder拼接字符串,除了一开始创建了StringBuilder对象,运行时都没有创建其他任何对象,每次只调用一次append()方法。所以从拼接大量字符串的效率上讲,StringBuilder比String快的多。
-
StringBuilder.append()方法会先判断当前数组长度+需要添加的字符串长度是否够装,如果不够装就扩容(扩容时还有复制内容到新数组中),然后追加内容到value数组的最后方。所以在StringBuilder的append方法中,不会创建新的StringBuilder对象,而String通过“+”拼接是会产生新的String对象。
其它
- String是不可变的(修改String时,不会在原有的内存地址修改,而是重新指向一个新对象),String用final修饰,不可继承,String本质上是个final的char[]数组,所以char[]数组的内存地址不会被修改,而且String也没有对外暴露修改char[]数组的方法.不可变性可以保证线程安全以及字符串串常量池的实现.频繁的增删操作是不建议使用String的.
- StringBuffer是线程安全的,多线程建议使用这个.
- StringBuilder是非线程安全的,单线程使用这个更快.