在众多的编程语言里面,字符串都被广泛的使用。在Java中字符串属于对象,语言提供了String类来创建和操作字符串。
字符串String简单知识
Java提供两种方式来定义字符串,例如:
1 | 定义字符使用单引号,定义字符串使用双引号; |
通过对String源码的查看:
1 | public final class String |
从上面的代码我们可以得出两点结论:
- Java中的String类被final修饰。在Java中被final修饰的类不允许被继承,并且成员方法默认被final修饰。在早期的JVM的版本,被final修饰的方法会被转为内嵌调用借此来提升执行效率,但是从Java1.5/6之后,这种方式就被取消了。在之后的版本里,final修饰类只是为了不让类被继承。
- String类是通过char数组保存字符串的。
对字符串的每一次操作,例如连接子串都会重新创建一个新的String对象。我们可以从String中的concat方法源码中可以看出这一点,代码如下:
1 | public String concat(String str) { |
当被连接的子串的长度为0时,直接返回自身,连接一个长度不为0的子串,通过char数组的系列操作,重新生成一个新的String对象。
所以在此要注意对String类对象的任何改变都不影响到原对象,相关的任何change操作都会生成新的对象。
深入理解字符串String
上面写了两种定义字符串的方式,不知道大家知道这两种方式的区别和联系么?
1 | // 直接赋值 |
你能直接说出上面的执行结果么?如果不能请继续往下看,能的话也请继续往下看。
具体的结果如下:
1 | false |
在class文件中有一部分来存储编译期间生成的字面常量以及符号引用,这部分叫做class文件常量池,在运行期间对应着方法区的运行时常量池。在上述的代码中String str1 = “hello world”;和String str2 = new String(“hello world”);都在编译期生成了字面常量和符号引用,运行期间字面常量”hello world”都被存储在运行时常量池。JVM执行引擎会在运行时常量池中查找是否存在相同的字面常量,若有则直接将引用指向已经存在的字面常量;否则在运行时常量池中开辟一个新的空间来存储该字面量,并将引用指向该字面常量,通过这种方式来把String对象跟引用绑定。
通过new关键字生成对象这个过程是在堆heap中进行的,而在堆进行对象生成过程中,不会有检查对象是否已经存在这个行为。因此通过new来创建对象,创建出来的一定是新的对象,即在内存中有着新的内存地址,但字符串的内容是相同的。
下面是Java中不同变量在内存中存放的位置:
变量 | 内存位置 |
---|---|
new出来的对象 | heap 堆 |
局部变量、基本数据类型 | stack 栈 |
静态变量、字符串、常量 | data segment 数据区 |
代码 | code segment 代码区 |
String、StringBuffer、StringBuilder的区别
为什么已经存在了String了,还会出现StringBuffer、StringBuilder?
如果一个字符串需要连接10000次其他的字符串,实现代码如下:
1 | public class Main { |
上述代码不断的new字符串对象,前面已经说了重要的一点对String类对象的任何改变都不影响到原对象,相关的任何change操作都会生成新的对象。,这种代码将会有多大的内存消耗。这个时候想必大家已经有了点答案。我将上述的代码稍微的修改一下:
1 | public class Main { |
两部分代码看似只有一点差异,其实两者的内存消耗有着天大的差别。我们通过javap命令来反编译.class文件。具体内容如下:
1 | D:\work\javaLearn\out\production\javaLearn>javap -c Main |
从上面反编译出来的字节码中可以看出一点门道:string+=”hello”的操作事实上会自动被JVM优化成StringBuilder类的append操作。
那么有人会问既然有了StringBuilder类,为什么还需要StringBuffer类?查看源代码便一目了然,事实上,StringBuilder和StringBuffer类拥有的成员属性以及成员方法基本相同,区别是StringBuffer类的成员方法前面多了一个关键字:synchronized,不用多说,这个关键字是在多线程访问时起到安全保护作用的,也就是说StringBuffer是线程安全的。
我们来看下面的代码:
1 | public class Main { |
用javap命令来反编译.class文件:
1 | D:\work\javaLearn\out\production\javaLearn>javap -c Main |
str1在编译之后就被直接赋值为”I love you”;str5却没有什么操作。综上所述我们可以得出一些结论:
- 对于直接通过加号相连字符串效率高,因为编译器直接确定了它的值。就像上面的”I “+”love “+”you”;的字符串相加,在编译期间就被优化成了”I love you“。
- 对于间接相加的,形如str2 + str3 + str4;编译期不会进行优化。
- 对于执行效率来说StringBuilder > StringBuffer > String,但这个也不是绝对的。比如String str = “hello”+ “world”的效率就比 StringBuilder st = new StringBuilder().append(“hello”).append(“world”)要高。但是,当字符串相加的操作或者字符改动的情况较少的时候,采用String肯定是比较好的;当字符串的操作较多的时候推荐使用StringBuilder,如果考虑到线程安全问题,无疑采用StringBuffer是最合适的。
常见的字符串相关的面试题
- 下面的代码输出的结果是什么?结果是true,它String b = “hello” + 2; 被编译器优化成了String b = “hello2”; 所以运行时字符串a和b指向同一个对象。
1
2
3String a = "hello2";
String b = "hello" + 2;
System.out.println((a == b)); - 下面的代码输出的结果是什么?输出结果为:false。由于有符号引用的存在,所以 String c = b + 2;不会在编译期间被优化,不会把b+2当做字面常量来处理的,通过StringBuilder生成了一个新的对象,因此这种方式生成的对象事实上是保存在堆上的。
1
2
3
4String a = "hello2";
String b = "hello";
String c = b + 2;
System.out.println((a == c)); - 下面的代码输出的结果是什么?输出结果为:true。对于被final修饰的变量,会在class文件常量池中保存一个副本,也就是说不会通过连接而进行访问,对final变量的访问在编译期间都会直接被替代为真实的值。那么String c = b + 2;在编译期间就会被优化成:String c = “hello” + 2;
1
2
3
4String a = "hello2";
final String b = "hello";
String c = b + 2;
System.out.println((a == c));
字符串的故事就暂时说到这里,后续有的话就继续更新。