一、String 类
想要理解一个类,最好的方法就是看这个类的实现源代码,来看一下 String 类的源码:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence
{
/** The value is used for character storage. */
private final char value[];
/** The offset is the first index of the storage that is used. */
private final int offset;
/** The count is the number of characters in the String. */
private final int count;
/** Cache the hash code for the string */
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
........
}
从下面能够看出几点:
1)String 类是 final 类,也即意味着 String 类不能被继承,并且它的成员办法都默认为 final 办法。在 Java 中,被 final 润饰的类是不容许被继承的,并且该类中的成员办法都默认为 final 办法。
2)下面列举出了 String 类中所有的成员属性,从下面能够看出 String 类其实是通过 char 数组来保留字符串的。
上面再持续看 String 类的一些办法实现:
public String substring(int beginIndex, int endIndex) {if (beginIndex < 0) {throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > count) {throw new StringIndexOutOfBoundsException(endIndex);
}
if (beginIndex > endIndex) {throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
}
return ((beginIndex == 0) && (endIndex == count)) ? this :
new String(offset + beginIndex, endIndex - beginIndex, value);
}
public String concat(String str) {int otherLen = str.length();
if (otherLen == 0) {return this;}
char buf[] = new char[count + otherLen];
getChars(0, count, buf, 0);
str.getChars(0, otherLen, buf, count);
return new String(0, count + otherLen, buf);
}
public String replace(char oldChar, char newChar) {if (oldChar != newChar) {
int len = count;
int i = -1;
char[] val = value; /* avoid getfield opcode */
int off = offset; /* avoid getfield opcode */
while (++i < len) {if (val[off + i] == oldChar) {break;}
}
if (i < len) {char buf[] = new char[len];
for (int j = 0 ; j < i ; j++) {buf[j] = val[off+j];
}
while (i < len) {char c = val[off + i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(0, len, buf);
}
}
return this;
}
从下面的三个办法能够看出,无论是 sub 操、concat 还是 replace 操作都不是在原有的字符串上进行的,而是从新生成了一个新的字符串对象。也就是说进行这些操作后,最原始的字符串并没有被扭转。
在这里要永远记住一点:“String 对象一旦被创立就是固定不变的了,对 String 对象的任何扭转都不影响到原对象,相干的任何 change 操作都会生成新的对象”。
二、字符串常量池
咱们晓得字符串的调配和其余对象调配一样,是须要耗费昂扬的工夫和空间的,而且字符串咱们应用的十分多。JVM 为了进步性能和缩小内存的开销,在实例化字符串的时候进行了一些优化:应用字符串常量池 。 每当咱们创立字符串常量时,JVM 会首先查看字符串常量池,如果该字符串曾经存在常量池中,那么就间接返回常量池中的实例援用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。因为 String 字符串的不可变性咱们能够非常必定常量池中肯定不存在两个雷同的字符串(这点对了解下面至关重要)。
Java 中的常量池,实际上分为两种状态:动态常量池 和运行时常量池 。
所谓 动态常量池 ,即 *.class 文件中的常量池,class 文件中的常量池不仅仅蕴含字符串(数字) 字面量,还蕴含类、办法的信息,占用 class 文件绝大部分空间。
而 运行时常量池,则是 jvm 虚拟机在实现类装载操作后,将 class 文件中的常量池载入到内存中,并保留在办法区中,咱们常说的常量池,就是指办法区中的运行时常量池。
来看上面的程序:
String a = "chenssy";
String b = "chenssy";
a、b 和字面上的 chenssy 都是指向 JVM 字符串常量池中的 ”chenssy” 对象,他们指向同一个对象。
String c = new String("chenssy");
new 关键字肯定会产生一个对象 chenssy(留神这个 chenssy 和下面的 chenssy 不同),同时这个对象是存储在堆中。所以下面应该产生了两个对象:保留在栈中的 c 和保留堆中 chenssy。然而在 Java 中基本就不存在两个齐全截然不同的字符串对象。故堆中的 chenssy 应该是援用字符串常量池中 chenssy。所以 c、chenssy、池 chenssy 的关系应该是:c—>chenssy—> 池 chenssy。整个关系如下:
通过下面的图咱们能够十分清晰的意识他们之间的关系。所以咱们批改内存中的值,他变动的是所有。
总结:尽管 a、b、c、chenssy 是不同的对象,然而从 String 的内部结构咱们是能够了解下面的。String c = new String(“chenssy”); 尽管 c 的内容是创立在堆中,然而他的外部 value 还是指向 JVM 常量池的 chenssy 的 value,它结构 chenssy 时所用的参数仍然是 chenssy 字符串常量。
上面再来看几个例子:
例子 1:
/**
* 采纳字面值的形式赋值
*/
public void test1(){
String str1="aaa";
String str2="aaa";
System.out.println("===========test1============");
System.out.println(str1==str2);//true 能够看出 str1 跟 str2 是指向同一个对象
}
执行上述代码,后果为:true。
剖析:当执行 String str1=”aaa” 时,JVM 首先会去字符串池中查找是否存在 ”aaa” 这个对象,如果不存在,则在字符串池中创立 ”aaa” 这个对象,而后将池中 ”aaa” 这个对象的援用地址返回给字符串常量 str1,这样 str1 会指向池中 ”aaa” 这个字符串对象;如果存在,则不创立任何对象,间接将池中 ”aaa” 这个对象的地址返回,赋给字符串常量。当创立字符串对象 str2 时,字符串池中曾经存在 ”aaa” 这个对象,间接把对象 ”aaa” 的援用地址返回给 str2,这样 str2 指向了池中 ”aaa” 这个对象,也就是说 str1 和 str2 指向了同一个对象,因而语句 System.out.println(str1 == str2)输入:true。
例子 2:
/**
* 采纳 new 关键字新建一个字符串对象
*/
public void test2(){String str3=new String("aaa");
String str4=new String("aaa");
System.out.println("===========test2============");
System.out.println(str3==str4);//false 能够看出用 new 的形式是生成不同的对象
}
执行上述代码,后果为:false。
剖析:采纳 new 关键字新建一个字符串对象时,JVM 首先在字符串池中查找有没有 ”aaa” 这个字符串对象,如果有,则不在池中再去创立 ”aaa” 这个对象了,间接在堆中创立一个 ”aaa” 字符串对象,而后将堆中的这个 ”aaa” 对象的地址返回赋给援用 str3,这样,str3 就指向了堆中创立的这个 ”aaa” 字符串对象;如果没有,则首先在字符串池中创立一个 ”aaa” 字符串对象,而后再在堆中创立一个 ”aaa” 字符串对象,而后将堆中这个 ”aaa” 字符串对象的地址返回赋给 str3 援用,这样,str3 指向了堆中创立的这个 ”aaa” 字符串对象。当执行 String str4=new String(“aaa”)时,因为采纳 new 关键字创建对象时,每次 new 进去的都是一个新的对象,也即是说援用 str3 和 str4 指向的是两个不同的对象,因而语句 System.out.println(str3 == str4)输入:false。
例子 3:
/**
* 编译期确定
*/
public void test3(){
String s0="helloworld";
String s1="helloworld";
String s2="hello"+"world";
System.out.println("===========test3============");
System.out.println(s0==s1); //true 能够看出 s0 跟 s1 是指向同一个对象
System.out.println(s0==s2); //true 能够看出 s0 跟 s2 是指向同一个对象
}
执行上述代码,后果为:true、true。
剖析:因为例子中的 s0 和 s1 中的 ”helloworld”都是字符串常量,它们在编译期就被确定了,所以 s0==s1 为 true;而 ”hello”和 ”world”也都是字符串常量,当一个字符串由多个字符串常量连贯而成时,它本人必定也是字符串常量,所以 s2 也同样在编译期就被解析为一个字符串常量,所以 s2 也是常量池中 ”helloworld”的一个援用。所以咱们得出 s0==s1==s2。
例子 4:
/**
* 编译期无奈确定
*/
public void test4(){
String s0="helloworld";
String s1=new String("helloworld");
String s2="hello" + new String("world");
System.out.println("===========test4============");
System.out.println(s0==s1); //false
System.out.println(s0==s2); //false
System.out.println(s1==s2); //false
}
执行上述代码,后果为:false、false、false。
剖析:用 new String() 创立的字符串不是常量,不能在编译期就确定,所以 new String() 创立的字符串不放入常量池中,它们有本人的地址空间。
s0 还是常量池中 ”helloworld”的援用,s1 因为无奈在编译期确定,所以是运行时创立的新对象 ”helloworld”的援用,s2 因为有后半局部 new String(”world”)所以也无奈在编译期确定,所以也是一个新创建对象 ”helloworld”的援用。
例子 5:
/**
* 持续 - 编译期无奈确定
*/
public void test5(){
String str1="abc";
String str2="def";
String str3=str1+str2;
System.out.println("===========test5============");
System.out.println(str3=="abcdef"); //false
}
执行上述代码,后果为:false。
剖析:因为 str3 指向堆中的 ”abcdef” 对象,而 ”abcdef” 是字符串池中的对象,所以后果为 false。JVM 对 String str=”abc” 对象放在常量池中是在编译时做的,而 String str3=str1+str2 是在运行时刻能力晓得的。new 对象也是在运行时才做的。而这段代码总共创立了 5 个对象,字符串池中两个、堆中三个。+ 运算符会在堆中建设来两个 String 对象,这两个对象的值别离是 ”abc” 和 ”def”,也就是说从字符串池中复制这两个值,而后在堆中创立两个对象,而后再建设对象 str3, 而后将 ”abcdef” 的堆地址赋给 str3。
步骤:
1)栈中开拓一块两头寄存援用 str1,str1 指向池中 String 常量 ”abc”。
2)栈中开拓一块两头寄存援用 str2,str2 指向池中 String 常量 ”def”。
3)栈中开拓一块两头寄存援用 str3。
4)str1 + str2 通过 StringBuilder 的最初一步 toString()办法还原一个新的 String 对象 ”abcdef”,因而堆中开拓一块空间寄存此对象。
5)援用 str3 指向堆中 (str1 + str2) 所还原的新 String 对象。
6)str3 指向的对象在堆中,而常量 ”abcdef” 在池中,输入为 false。
例子 6:
/**
* 编译期优化
*/
public void test6(){
String s0 = "a1";
String s1 = "a" + 1;
System.out.println("===========test6============");
System.out.println((s0 == s1)); //result = true
String s2 = "atrue";
String s3= "a" + "true";
System.out.println((s2 == s3)); //result = true
String s4 = "a3.4";
String s5 = "a" + 3.4;
System.out.println((s4 == s5)); //result = true
}
执行上述代码,后果为:true、true、true。
剖析:在程序编译期,JVM 就将常量字符串的 ”+” 连贯优化为连贯后的值,拿 ”a” + 1 来说,经编译器优化后在 class 中就曾经是 a1。在编译期其字符串常量的值就确定下来,故下面程序最终的后果都为 true。
/**
* 编译期无奈确定
*/
public void test7(){
String s0 = "ab";
String s1 = "b";
String s2 = "a" + s1;
System.out.println("===========test7============");
System.out.println((s0 == s2)); //result = false
}
执行上述代码,后果为:false。
剖析:JVM 对于字符串援用,因为在字符串的 ”+” 连贯中,有字符串援用存在,而援用的值在程序编译期是无奈确定的,即 ”a” + s1 无奈被编译器优化,只有在程序运行期来动态分配并将连贯后的新地址赋给 s2。所以下面程序的后果也就为 false。
例子 8:
/**
* 比拟字符串常量的“+”和字符串援用的“+”的区别
*/
public void test8(){
String test="javalanguagespecification";
String str="java";
String str1="language";
String str2="specification";
System.out.println("===========test8============");
System.out.println(test == "java" + "language" + "specification");
System.out.println(test == str + str1 + str2);
}
执行上述代码,后果为:true、false。
剖析:为什么呈现下面的后果呢?这是因为,字符串字面量拼接操作是在 Java 编译器编译期间就执行了,也就是说编译器编译时,间接把 ”java”、”language” 和 ”specification” 这三个字面量进行 ”+” 操作失去一个 ”javalanguagespecification” 常量,并且间接将这个常量放入字符串池中,这样做实际上是一种优化,将 3 个字面量合成一个,防止了创立多余的字符串对象。而字符串援用的 ”+” 运算是在 Java 运行期间执行的,即 str + str2 + str3 在程序执行期间才会进行计算,它会在堆内存中从新创立一个拼接后的字符串对象。总结来说就是:字面量 ”+” 拼接是在编译期间进行的,拼接后的字符串寄存在字符串池中;而字符串援用的 ”+” 拼接运算切实运行时进行的,新创建的字符串寄存在堆中。
对于间接相加字符串,效率很高,因为在编译器便确定了它的值,也就是说形如 ”I”+”love”+”java”; 的字符串相加,在编译期间便被优化成了 ”Ilovejava”。对于间接相加(即蕴含字符串援用),形如 s1+s2+s3; 效率要比间接相加低,因为在编译器不会对援用变量进行优化。
例子 9:
/**
* 编译期确定
*/
public void test9(){
String s0 = "ab";
final String s1 = "b";
String s2 = "a" + s1;
System.out.println("===========test9============");
System.out.println((s0 == s2)); //result = true
}
执行上述代码,后果为:true。
剖析:和例子 7 中惟一不同的是 s1 字符串加了 final 润饰,对于 final 润饰的变量,它在编译时被解析为常量值的一个本地拷贝存储到本人的常量池中或嵌入到它的字节码流中。所以此时的 ”a” + s1 和 ”a” + “b” 成果是一样的。故下面程序的后果为 true。
例子 10:
/**
* 编译期无奈确定
*/
public void test10(){
String s0 = "ab";
final String s1 = getS1();
String s2 = "a" + s1;
System.out.println("===========test10============");
System.out.println((s0 == s2)); //result = false
}
private static String getS1() {return "b";}
执行上述代码,后果为:false。
剖析:这外面尽管将 s1 用 final 润饰了,然而因为其赋值是通过办法调用返回的,那么它的值只能在运行期间确定,因而 s0 和 s2 指向的不是同一个对象,故下面程序的后果为 false。
三、总结
1.String 类初始化后是不可变的(immutable)
String 应用 private final char value[]来实现字符串的存储,也就是说 String 对象创立之后,就不能再批改此对象中存储的字符串内容,就是因为如此,才说 String 类型是不可变的 (immutable)。程序员不能对已有的不可变对象进行批改。咱们本人也能够创立不可变对象,只有在接口中不提供批改数据的办法就能够。
然而,String 类对象的确有编辑字符串的性能,比方 replace()。这些编辑性能是通过创立一个新的对象来实现的,而不是对原有对象进行批改。比方:
s = s.replace("World", "Universe");
上面对 s.replace()的调用将创立一个新的字符串 ”Hello Universe!”,并返回该对象的援用。通过赋值,援用 s 将指向该新的字符串。如果没有其余援用指向原有字符串 ”Hello World!”,原字符串对象将被垃圾回收。
2. 援用变量与对象
A aa;
这个语句申明一个类 A 的援用变量 aa[咱们经常称之为句柄],而对象个别通过 new 创立。所以 aa 仅仅是一个援用变量,它不是对象。
3. 创立字符串的形式
创立字符串的形式归纳起来有两类:
(1)应用 ”” 引号创立字符串;
(2)应用 new 关键字创立字符串。
联合下面例子,总结如下:
(1)独自应用 ”” 引号创立的字符串都是常量, 编译期就曾经确定存储到 String Pool 中;
(2)应用 new String(“”)创立的对象会存储到 heap 中, 是运行期新创建的;
new 创立字符串时首先查看池中是否有雷同值的字符串,如果有,则拷贝一份到堆中,而后返回堆中的地址;如果池中没有,则在堆中创立一份,而后返回堆中的地址(留神,此时不须要从堆中复制到池中,否则,将使得堆中的字符串永远是池中的子集,导致节约池的空间)!
(3)应用只蕴含常量的字符串连接符如 ”aa” + “aa” 创立的也是常量, 编译期就能确定, 曾经确定存储到 String Pool 中;
(4)应用蕴含变量的字符串连接符如 ”aa” + s1 创立的对象是运行期才创立的, 存储在 heap 中;
4. 应用 String 不肯定创建对象
在执行到双引号蕴含字符串的语句时,如 String a = “123”,JVM 会先到常量池里查找,如果有的话返回常量池里的这个实例的援用,否则的话创立一个新实例并置入常量池里。所以,当咱们在应用诸如 String str = “abc”;的格局定义对象时,总是想当然地认为,创立了 String 类的对象 str。放心陷阱!对象可能并没有被创立!而可能只是指向一个先前曾经创立的对象。只有通过 new()办法能力保障每次都创立一个新的对象。
5. 应用 new String,肯定创建对象
在执行 String a = new String(“123”)的时候,首先走常量池的路线取到一个实例的援用,而后在堆上创立一个新的 String 实例,走以下构造函数给 value 属性赋值,而后把实例援用赋值给 a:
public String(String original) {
int size = original.count;
char[] originalValue = original.value;
char[] v;
if (originalValue.length > size) {
// The array representing the String is bigger than the new
// String itself. Perhaps this constructor is being called
// in order to trim the baggage, so make a copy of the array.
int off = original.offset;
v = Arrays.copyOfRange(originalValue, off, off+size);
} else {
// The array representing the String is the same
// size as the String, so no point in making a copy.
v = originalValue;
}
this.offset = 0;
this.count = size;
this.value = v;
}
从中咱们能够看到,尽管是新创建了一个 String 的实例,然而 value 是等于常量池中的实例的 value,即是说没有 new 一个新的字符数组来寄存 ”123″。
6. 对于 String.intern()
intern 办法应用:一个初始为空的字符串池,它由类 String 单独保护。当调用 intern 办法时,如果池曾经蕴含一个等于此 String 对象的字符串(用 equals(oject)办法确定),则返回池中的字符串。否则,将此 String 对象增加到池中,并返回此 String 对象的援用。
它遵循以下规定:对于任意两个字符串 s 和 t,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true。
String.intern();
再补充介绍一点:存在于.class 文件中的常量池,在运行期间被 jvm 装载,并且能够裁减。String 的 intern()办法就是裁减常量池的一个办法;当一个 String 实例 str 调用 intern()办法时,java 查找常量池中是否有雷同 unicode 的字符串常量,如果有,则返回其援用,如果没有,则在常量池中减少一个 unicode 等于 str 的字符串并返回它的援用。
/**
* 对于 String.intern()
*/
public void test11(){
String s0 = "kvill";
String s1 = new String("kvill");
String s2 = new String("kvill");
System.out.println("===========test11============");
System.out.println(s0 == s1); //false
System.out.println("**********");
s1.intern(); // 尽管执行了 s1.intern(), 但它的返回值没有赋给 s1
s2 = s2.intern(); // 把常量池中 "kvill" 的援用赋给 s2
System.out.println(s0 == s1); //flase
System.out.println(s0 == s1.intern() ); //true// 阐明 s1.intern()返回的是常量池中 "kvill" 的援用
System.out.println(s0 == s2); //true
}
运行后果:false、false、true、true。
7. 对于 equals 和 ==
(1)对于 ==,如果作用于根本数据类型的变量(byte,short,char,int,long,float,double,boolean),则间接比拟其存储的 ” 值 ” 是否相等;如果作用于援用类型的变量(String),则比拟的是所指向的对象的地址(即是否指向同一个对象)。
(2)equals 办法是基类 Object 中的办法,因而对于所有的继承于 Object 的类都会有该办法。在 Object 类中,equals 办法是用来比拟两个对象的援用是否相等,即是否指向同一个对象。
(3)对于 equals 办法,留神:equals 办法不能作用于根本数据类型的变量。如果没有对 equals 办法进行重写,则比拟的是援用类型的变量所指向的对象的地址;而 String 类对 equals 办法进行了重写,用来比拟指向的字符串对象所存储的字符串是否相等。其余的一些类诸如 Double,Date,Integer 等,都对 equals 办法进行了重写用来比拟指向的对象所存储的内容是否相等。
/**
* 对于 equals 和 ==
*/
public void test12(){
String s1="hello";
String s2="hello";
String s3=new String("hello");
System.out.println("===========test12============");
System.out.println(s1 == s2); //true, 示意 s1 和 s2 指向同一对象,它们都指向常量池中的 "hello" 对象
//flase, 示意 s1 和 s3 的地址不同,即它们别离指向的是不同的对象,s1 指向常量池中的地址,s3 指向堆中的地址
System.out.println(s1 == s3);
System.out.println(s1.equals(s3)); //true, 示意 s1 和 s3 所指向对象的内容雷同
}
8.String 相干的 +:
String 中的 + 罕用于字符串的连贯。看上面一个简略的例子:
/**
* String 相干的 +
*/
public void test13(){
String a = "aa";
String b = "bb";
String c = "xx" + "yy" + a + "zz" + "mm" + b;
System.out.println("===========test13============");
System.out.println(c);
}
编译运行后,次要字节码局部如下:
public static main([Ljava/lang/String;)V
L0
LINENUMBER 5 L0
LDC "aa"
ASTORE 1
L1
LINENUMBER 6 L1
LDC "bb"
ASTORE 2
L2
LINENUMBER 7 L2
NEW java/lang/StringBuilder
DUP
LDC "xxyy"
INVOKESPECIAL java/lang/StringBuilder.<init> (Ljava/lang/String;)V
ALOAD 1
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
LDC "zz"
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
LDC "mm"
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 2
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
ASTORE 3
L3
LINENUMBER 8 L3
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
ALOAD 3
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L4
LINENUMBER 9 L4
RETURN
L5
LOCALVARIABLE args [Ljava/lang/String; L0 L5 0
LOCALVARIABLE a Ljava/lang/String; L1 L5 1
LOCALVARIABLE b Ljava/lang/String; L2 L5 2
LOCALVARIABLE c Ljava/lang/String; L3 L5 3
MAXSTACK = 3
MAXLOCALS = 4
}
显然,通过字节码咱们能够得出如下几点论断:
(1).String 中应用 + 字符串连接符进行字符串连贯时,连贯操作最开始时如果都是字符串常量,编译后将尽可能多的间接将字符串常量连接起来,造成新的字符串常量参加后续连贯(通过反编译工具 jd-gui 也能够不便的间接看出);
(2). 接下来的字符串连贯是从左向右顺次进行,对于不同的字符串,首先以最右边的字符串为参数创立 StringBuilder 对象,而后顺次对左边进行 append 操作,最初将 StringBuilder 对象通过 toString()办法转换成 String 对象(留神:两头的多个字符串常量不会主动拼接)。
也就是说String c = “xx” + “yy ” + a + “zz” + “mm” + b; 本质上的实现过程是:String c = new StringBuilder(“xxyy “).append(a).append(“zz”).append(“mm”).append(b).toString();
由此得出结论:当应用 + 进行多个字符串连贯时,实际上是产生了一个 StringBuilder 对象和一个 String 对象。
9.String 的不可变性导致字符串变量应用 + 号的代价:
String s = "a" + "b" + "c";
String s1 = "a";
String s2 = "b";
String s3 = "c";
String s4 = s1 + s2 + s3;
剖析:变量 s 的创立等价于 String s = “abc”; 由下面例子可知编译器进行了优化,这里只创立了一个对象。由下面的例子也能够晓得 s4 不能在编译期进行优化,其对象创立相当于:
StringBuilder temp = new StringBuilder();
temp.append(a).append(b).append(c);
String s = temp.toString();
由下面的剖析后果,可就不难推断出 String 采纳连贯运算符(+)效率低下起因剖析,形如这样的代码:
public class Test {public static void main(String args[]) {
String s = null;
for(int i = 0; i < 100; i++) {s += "a";}
}
}
每做一次 + 就产生个 StringBuilder 对象,而后 append 后就扔掉。下次循环再达到时从新产生个 StringBuilder 对象,而后 append 字符串,如此循环直至完结。如果咱们间接采纳 StringBuilder 对象进行 append 的话,咱们能够节俭 N – 1 次创立和销毁对象的工夫。所以对于在循环中要进行字符串连贯的利用,个别都是用 StringBuffer 或 StringBulider 对象来进行 append 操作。
10.String、StringBuffer、StringBuilder 的区别
(1)可变与不可变:String 是 不可变字符串对象 ,StringBuilder 和 StringBuffer 是 可变字符串对象(其外部的字符数组长度可变)。
(2)是否多线程平安:String 中的对象是不可变的,也就能够了解为常量,显然 线程平安 。StringBuffer 与 StringBuilder 中的办法和性能齐全是等价的,只是 StringBuffer 中的办法大都采纳了 synchronized 关键字进行润饰,因而是 线程平安 的,而 StringBuilder 没有这个润饰,能够被认为是 非线程平安 的。
(3)String、StringBuilder、StringBuffer 三者的执行效率:
StringBuilder > StringBuffer > String 当然这个是绝对的,不肯定在所有状况下都是这样。比方 String str = “hello”+ “world” 的效率就比 StringBuilder st = new StringBuilder().append(“hello”).append(“world”)要高。因而,这三个类是各有利弊,该当依据不同的状况来进行抉择应用:
当字符串相加操作或者改变较少的状况下,倡议应用 String str=”hello” 这种模式;
当字符串相加操作较多的状况下,倡议应用 StringBuilder,如果采纳了多线程,则应用 StringBuffer。
11.String 中的 final 用法和了解
final StringBuffer a = new StringBuffer("111");
final StringBuffer b = new StringBuffer("222");
a=b;// 此句编译不通过
final StringBuffer a = new StringBuffer("111");
a.append("222");// 编译通过
可见,final 只对援用的 ” 值 ”(即内存地址)无效,它迫使援用只能指向初始指向的那个对象,扭转它的指向会导致编译期谬误。至于它所指向的对象的变动,final 是不负责的。
12. 对于 String str = new String(“abc”)创立了多少个对象?
这个问题在很多书籍上都有说到比方《Java 程序员面试宝典》,包含很多国内大公司口试面试题都会遇到,大部分网上流传的以及一些面试书籍上都说是 2 个对象,这种说法是全面的。
首先必须弄清楚创建对象的含意,创立是什么时候创立的?这段代码在运行期间会创立 2 个对象么?毫无疑问不可能,用 javap - c 反编译即可失去 JVM 执行的字节码内容:
很显然,new 只调用了一次,也就是说只创立了一个对象。而这道题目让人混同的中央就是这里,这段代码在运行期间的确只创立了一个对象,即在堆上创立了 ”abc” 对象。而为什么大家都在说是 2 个对象呢,这外面要廓清一个概念,该段代码执行过程和类的加载过程是有区别的。在类加载的过程中,的确在运行时常量池中创立了一个 ”abc” 对象,而在代码执行过程中的确只创立了一个 String 对象。
因而,这个问题如果换成 String str = new String(“abc”)波及到几个 String 对象?正当的解释是 2 个。
集体感觉在面试的时候如果遇到这个问题,能够向面试官询问分明”是这段代码执行过程中创立了多少个对象还是波及到多少个对象“再依据具体的来进行答复。
13. 字符串池的优缺点:
字符串池的长处就是防止了雷同内容的字符串的创立,节俭了内存,省去了创立雷同字符串的工夫,同时晋升了性能;另一方面,字符串池的毛病就是就义了 JVM 在常量池中遍历对象所须要的工夫,不过其工夫老本相比而言比拟低。
四、综合实例
package com.spring.test;
public class StringTest {public static void main(String[] args) {
/**
* 情景一:字符串池
* JAVA 虚拟机 (JVM) 中存在着一个字符串池,其中保留着很多 String 对象;
* 并且能够被共享应用,因而它进步了效率。* 因为 String 类是 final 的,它的值一经创立就不可扭转。* 字符串池由 String 类保护,咱们能够调用 intern()办法来拜访字符串池。*/
String s1 = "abc";
//↑ 在字符串池创立了一个对象
String s2 = "abc";
//↑ 字符串 pool 曾经存在对象“abc”(共享), 所以创立 0 个对象,累计创立一个对象
System.out.println("s1 == s2 :"+(s1==s2));
//↑ true 指向同一个对象,System.out.println("s1.equals(s2) :" + (s1.equals(s2)));
//↑ true 值相等
//↑------------------------------------------------------over
/**
* 情景二:对于 new String("")
*
*/
String s3 = new String("abc");
//↑ 创立了两个对象,一个寄存在字符串池中,一个存在与堆区中;//↑ 还有一个对象援用 s3 寄存在栈中
String s4 = new String("abc");
//↑ 字符串池中曾经存在“abc”对象,所以只在堆中创立了一个对象
System.out.println("s3 == s4 :"+(s3==s4));
//↑false s3 和 s4 栈区的地址不同,指向堆区的不同地址;System.out.println("s3.equals(s4) :"+(s3.equals(s4)));
//↑true s3 和 s4 的值雷同
System.out.println("s1 == s3 :"+(s1==s3));
//↑false 寄存的地区多不同,一个栈区,一个堆区
System.out.println("s1.equals(s3) :"+(s1.equals(s3)));
//↑true 值雷同
//↑------------------------------------------------------over
/**
* 情景三:* 因为常量的值在编译的时候就被确定 (优化) 了。* 在这里,"ab" 和 "cd" 都是常量,因而变量 str3 的值在编译时就能够确定。* 这行代码编译后的成果等同于:String str3 = "abcd";
*/
String str1 = "ab" + "cd"; // 1 个对象
String str11 = "abcd";
System.out.println("str1 = str11 :"+ (str1 == str11));
//↑------------------------------------------------------over
/**
* 情景四:* 局部变量 str2,str3 存储的是存储两个扣留字符串对象 (intern 字符串对象) 的地址。*
* 第三行代码原理(str2+str3):* 运行期 JVM 首先会在堆中创立一个 StringBuilder 类,* 同时用 str2 指向的扣留字符串对象实现初始化,* 而后调用 append 办法实现对 str3 所指向的扣留字符串的合并,* 接着调用 StringBuilder 的 toString()办法在堆中创立一个 String 对象,* 最初将刚生成的 String 对象的堆地址寄存在局部变量 str3 中。*
* 而 str5 存储的是字符串池中 "abcd" 所对应的扣留字符串对象的地址。* str4 与 str5 地址当然不一样了。*
* 内存中实际上有五个字符串对象:* 三个扣留字符串对象、一个 String 对象和一个 StringBuilder 对象。*/
String str2 = "ab"; // 1 个对象
String str3 = "cd"; // 1 个对象
String str4 = str2+str3;
String str5 = "abcd";
System.out.println("str4 = str5 :" + (str4==str5)); // false
//↑------------------------------------------------------over
/**
* 情景五:* JAVA 编译器对 string + 根本类型 / 常量 是当成常量表达式间接求值来优化的。* 运行期的两个 string 相加,会产生新的对象的,存储在堆 (heap) 中
*/
String str6 = "b";
String str7 = "a" + str6;
String str67 = "ab";
System.out.println("str7 = str67 :"+ (str7 == str67));
//↑str6 为变量,在运行期才会被解析。final String str8 = "b";
String str9 = "a" + str8;
String str89 = "ab";
System.out.println("str9 = str89 :"+ (str9 == str89));
//↑str8 为常量变量,编译期会被优化
//↑------------------------------------------------------over
}
}
运行后果:
s1 == s2 : true
s1.equals(s2) : true
s3 == s4 : false
s3.equals(s4) : true
s1 == s3 : false
s1.equals(s3) : true
str1 = str11 : true
str4 = str5 : false
str7 = str67 : false
str9 = str89 : true
欢送关注公众号【码农开花】一起学习成长
我会始终分享 Java 干货,也会分享收费的学习材料课程和面试宝典
回复:【计算机】【设计模式】【面试】有惊喜哦