Java学习系列文章第二篇:字符串

在众多的编程语言里面,字符串都被广泛的使用。在Java中字符串属于对象,语言提供了String类来创建和操作字符串。

字符串String简单知识

Java提供两种方式来定义字符串,例如:

1
2
3
4
5
6
定义字符使用单引号,定义字符串使用双引号;

// 直接赋值
String str1 = "hello world";
// 构造方法
String str2 = new String("hello world");

通过对String源码的查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
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

/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;

……
}

从上面的代码我们可以得出两点结论:

  1. Java中的String类被final修饰。在Java中被final修饰的类不允许被继承,并且成员方法默认被final修饰。在早期的JVM的版本,被final修饰的方法会被转为内嵌调用借此来提升执行效率,但是从Java1.5/6之后,这种方式就被取消了。在之后的版本里,final修饰类只是为了不让类被继承。
  2. String类是通过char数组保存字符串的。

对字符串的每一次操作,例如连接子串都会重新创建一个新的String对象。我们可以从String中的concat方法源码中可以看出这一点,代码如下:

1
2
3
4
5
6
7
8
9
10
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}

当被连接的子串的长度为0时,直接返回自身,连接一个长度不为0的子串,通过char数组的系列操作,重新生成一个新的String对象。
所以在此要注意对String类对象的任何改变都不影响到原对象,相关的任何change操作都会生成新的对象。

深入理解字符串String

上面写了两种定义字符串的方式,不知道大家知道这两种方式的区别和联系么?

1
2
3
4
5
6
7
8
9
10
11
12
// 直接赋值
String str1 = "hello world";
// 构造方法
String str2 = new String("hello world");

String str3 = "hello world";

String str4 = new String("hello world");

System.out.println(str1==str2);
System.out.println(str1==str3);
System.out.println(str2==str4);

你能直接说出上面的执行结果么?如果不能请继续往下看,能的话也请继续往下看。
具体的结果如下:

1
2
3
false
true
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
2
3
4
5
6
7
8
9
10
11
public class Main {

public static void main(String[] args){

String string = "";
for(int i=0;i<10000;i++){
string = string.concat("hello");
}
}

}

上述代码不断的new字符串对象,前面已经说了重要的一点对String类对象的任何改变都不影响到原对象,相关的任何change操作都会生成新的对象。,这种代码将会有多大的内存消耗。这个时候想必大家已经有了点答案。我将上述的代码稍微的修改一下:

1
2
3
4
5
6
7
8
9
10
11
public class Main {

public static void main(String[] args){

String string = "";
for(int i=0;i<10000;i++){
string += "hello";
}
}

}

两部分代码看似只有一点差异,其实两者的内存消耗有着天大的差别。我们通过javap命令来反编译.class文件。具体内容如下:

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
D:\work\javaLearn\out\production\javaLearn>javap -c Main
Compiled from "Main.java"
public class Main {
public Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public static void main(java.lang.String[]);
Code:
0: ldc #2 // String
2: astore_1
3: iconst_0
4: istore_2
5: iload_2
6: sipush 10000
9: if_icmpge 38
12: new #3 // class java/lang/StringBuilder
15: dup
16: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
19: aload_1
20: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
23: ldc #6 // String hello
25: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
31: astore_1
32: iinc 2, 1
35: goto 5
38: return
}

从上面反编译出来的字节码中可以看出一点门道:string+=”hello”的操作事实上会自动被JVM优化成StringBuilder类的append操作。

那么有人会问既然有了StringBuilder类,为什么还需要StringBuffer类?查看源代码便一目了然,事实上,StringBuilder和StringBuffer类拥有的成员属性以及成员方法基本相同,区别是StringBuffer类的成员方法前面多了一个关键字:synchronized,不用多说,这个关键字是在多线程访问时起到安全保护作用的,也就是说StringBuffer是线程安全的。

我们来看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
public class Main {
public static void main(String[] args){

String str1 = "I "+"love "+"you";
String str2 = "I ";
String str3 = "love ";
String str4 = "you ";

String str5 = str2 + str3 + str4;
}
}

用javap命令来反编译.class文件:

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
D:\work\javaLearn\out\production\javaLearn>javap -c Main
Compiled from "Main.java"
public class Main {
public Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public static void main(java.lang.String[]);
Code:
0: ldc #2 // String I love you
2: astore_1
3: ldc #3 // String I
5: astore_2
6: ldc #4 // String love
8: astore_3
9: ldc #5 // String you
11: astore 4
13: new #6 // class java/lang/StringBuilder
16: dup
17: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V
20: aload_2
21: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: aload_3
25: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: aload 4
30: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
33: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
36: astore 5
38: return
}

str1在编译之后就被直接赋值为”I love you”;str5却没有什么操作。综上所述我们可以得出一些结论:

  1. 对于直接通过加号相连字符串效率高,因为编译器直接确定了它的值。就像上面的”I “+”love “+”you”;的字符串相加,在编译期间就被优化成了”I love you“。
  2. 对于间接相加的,形如str2 + str3 + str4;编译期不会进行优化。
  3. 对于执行效率来说StringBuilder > StringBuffer > String,但这个也不是绝对的。比如String str = “hello”+ “world”的效率就比 StringBuilder st = new StringBuilder().append(“hello”).append(“world”)要高。但是,当字符串相加的操作或者字符改动的情况较少的时候,采用String肯定是比较好的;当字符串的操作较多的时候推荐使用StringBuilder,如果考虑到线程安全问题,无疑采用StringBuffer是最合适的。

    常见的字符串相关的面试题

  4. 下面的代码输出的结果是什么?
    1
    2
    3
    String a = "hello2";   
    String b = "hello" + 2;   
    System.out.println((a == b));
    结果是true,它String b = “hello” + 2; 被编译器优化成了String b = “hello2”; 所以运行时字符串a和b指向同一个对象。
  5. 下面的代码输出的结果是什么?
    1
    2
    3
    4
    String a = "hello2";    
    String b = "hello";
    String c = b + 2;
    System.out.println((a == c));
    输出结果为:false。由于有符号引用的存在,所以 String c = b + 2;不会在编译期间被优化,不会把b+2当做字面常量来处理的,通过StringBuilder生成了一个新的对象,因此这种方式生成的对象事实上是保存在堆上的。
  6. 下面的代码输出的结果是什么?
    1
    2
    3
    4
    String a = "hello2";
    final String b = "hello";
    String c = b + 2;
    System.out.println((a == c));
    输出结果为:true。对于被final修饰的变量,会在class文件常量池中保存一个副本,也就是说不会通过连接而进行访问,对final变量的访问在编译期间都会直接被替代为真实的值。那么String c = b + 2;在编译期间就会被优化成:String c = “hello” + 2;

字符串的故事就暂时说到这里,后续有的话就继续更新。

  • 作者: Sam
  • 发布时间: 2018-06-29 21:44:13
  • 最后更新: 2019-12-09 23:03:26
  • 文章链接: https://ydstudios.gitee.io/post/9d613e12.html
  • 版权声明: 本网所有文章除特别声明外, 禁止未经授权转载,违者依法追究相关法律责任!