概述
String 对于日常代码来说是一个使用频率很高的对象,因为它的一些使用数据和基本数据类型有些相似,所以很容易把String 混淆为基本数据类型。
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; /** Cache the hash code for the string */ private int hash; // Default to 0 public String() { this.value = new char[0]; } public String(String original) { this.value = original.value; this.hash = original.hash; } public String(char value[]) { this.value = Arrays.copyOf(value, value.length); } }
打开Jdk 1.8 String的源码 我们可以看到String 是一个被 final修饰的类,并且它的本质是 使用字符数组来存储的数据 。
那么为何String要被声明成final类型呢,原因简单的说有两点:
- 安全
-
class Test { //不可变的String public String changeStr(String s) { s += "bbb"; return s; } // 可变的StringBuilder public StringBuilder changeSb(StringBuilder sb) { return sb.append("bbb"); } public static void main(String[] args) { //String做参数 String s = new String("aaa"); String ns = changeStr(s); System.out.println("String aaa >>> " + s.toString()); // StringBuilder做参数 StringBuilder sb = new StringBuilder("aaa"); StringBuilder nsb = changeSb(sb); System.out.println("StringBuilder aaa >>> " + sb.toString()); } } // Output: //String aaa >>> aaa //StringBuilder aaa >>> aaabbb
如果程序员不小心像上面例子里,直接在传进来的参数上加”bbb”,因为Java对象参数传的是引用,所以可变的的StringBuffer参数就被改变了。 可以看到变量sb在changeSb(sb)操作之后,就变成了”aaabbb”。有的时候这可能不是程序员的本意。所以String不可变的安全性就体现在这里。
基本数据类型传递与引用传递区别详解
当传递方法参数类型为基本数据类型时,方法不会修改基本数据类型的参数。(值拷贝)
当传递方法参数类型为引用数据类型时,方法会修改引用数据类型的参数所指向对象的值。(堆的值)
所以正是因为不可变性,String对于方法中形参值更多的是像基本数据类型属于值拷贝,正是由于String不可变的安全特征,它可以用来作为Map键值的唯一性。并且String也是使用最频繁的对象,也避免开发者的错误使用和继承,工程师们设计了完美的String类供大家使用。
- 效率/性能
在jdk 1.7之后,方法区的字符串常量池移至堆中
常量池是为了避免频繁的创建和销毁对象而影响系统性能。例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中,常量池的优点就是数据共享,例如下面代码中,我们定义了2个String类型的字符串,那么在字符串常量池中,只会存在1份”java”字面量对象。(new String(“”)情况另外下面会继续分析) -
String a="java"; String b="java";
这样在大量使用字符串的情况下,可以节省内存空间,提高效率。但之所以能实现这个特性,String的不可变性是最基本的一个必要条件。要是内存里字符串内容能改来改去,这么做就完全没有意义了。
String 的使用方式
相信很多 JAVA 程序员都做做类似 String s = new String(“abc”)这个语句创建了几个对象的题目。 这种题目主要就是为了考察程序员对字符串对象的常量池掌握与否。上述的语句中是创建了2个对象,第一个对象是”abc”字符串存储在常量池中,第二个对象在JAVA Heap中的 String 对象。
使用 ” ” 双引号创建 : String s1 = “first”; 使用字符串连接符拼接 : String s2=”se”+”cond”; 使用字符串加引用拼接 : String s12=”first”+s2; 使用new String(“”)创建 : String s3 = new String(“three”); 使用new String(“”)拼接 : String s4 = new String(“fo”)+”ur”; 使用new String(“”)拼接 : String s5 = new String(“fo”)+new String(“ur”);
解析
Java 会确保一个字符串常量只有一个拷贝。
- s1 : 中的”first” 是字符串常量,在编译期就被确定了,先检查字符串常量池中是否含有”first”字符串,若没有则添加”first”到字符串常量池中,并且直接指向它。所以s1直接指向字符串常量池的”first”对象。
- s2 : “se”和”cond”也都是字符串常量,当一个字符串由多个字符串常量连接而成时,它自己肯定也是字符串常量,所以s2也同样在编译期就被解析为一个字符串常量,并且s2是常量池中”second”的一个引用。
- s12 : JVM对于字符串引用,由于在字符串的”+”连接中,有字符串引用存在,而引用的值在程序编译期是无法确定的,即
("first"+s2)
无法被编译器优化,只有在程序运行期来动态分配使用StringBuilder
连接后的新String对象赋给s12。
(编译器创建一个StringBuilder
对象,并调用append()
方法,最后调用toString()
创建新String
对象,以包含修改后的字符串内容) - s3 : 用new String() 创建的字符串不是常量,不能在编译期就确定,所以new String() 创建的字符串不放入常量池中,它们有自己的地址空间。
但是”three”字符串常量在编译期也会被加入到字符串常量池(如果不存在的话) - s4 : 同样不能在编译期确定,但是”fo”和”ur”这两个字符串常量也会添加到字符串常量池中,并且在堆中创建String对象。(字符串常量池并不会存放”four”这个字符串)
- s5 : 原理同s4。
String 的拼接
因为String对象是不可变的。String类中每一个看起来会修改String值的方法,实际上都是创建一个StringBuilder
对象,并调用append()
方法,最后调用toString()
创建新String
对象,以包含修改后的字符串内容。
Java中仅有的重载运算符
在Java中,唯一被重载的运算符就是字符串的拼接相关的。+,+=。除此之外,Java设计者不允许重载其他的运算符。
引用拼接
public class StringConcat { String a = "hello"; String b = "moto"; String result = a + b + "2018"; }
当Java编译器遇到字符串拼接的时候,会创建一个StringBuilder
对象,后面的拼接,实际上是调用StringBuilder
对象的append()
方法。
(1)”hello” + “moto” 首先创建新的StringBuilder
对象,使用append()
添加”hello”和”moto”;
(2)append()
拼接”2018”;
(3)引用result 指向最终生成的String。
因为有字符串引用存在,而引用的值在程序编译期是无法确定的。另外小提示 : “hello”、”moto”和”2018”都会添加到字符串常量池中(如果没有的话),因为它们都是编译期确定的字符串常量,但是最后的”hellomoto2018”并不会添加到字符串常量池。
有兴趣的可以尝试拼接null。即String a=null;
字符串常量拼接
但是如果是下面这种拼接情况 :
public class StringConcat { String result = "hello" + "moto" + "2018"; }
“hello”、”moto”、”2018”都是字符串常量,当一个字符串由多个字符串常量连接而成时,它自己肯定也是字符串常量,所以result也同样在编译期就被解析为一个字符串常量。
final引用拼接
public class StringConcat { final String a = "hello"; final String b = "moto"; String result = a + b + "2018"; }
和引用拼接中唯一不同的是a和b这两个局部变量加了final修饰。
对于final修饰的局部变量,它在编译时被解析为常量值的一个本地拷贝存储到自己的常量池中或嵌入到它的字节码流中。
所以此时的(a + b + “2018”)和(“hello” + “moto” + “2018”)效果是一样的。