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

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

字符串String简单知识

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

定义字符使用单引号,定义字符串使用双引号;

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

通过对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

    /** 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方法源码中可以看出这一点,代码如下:

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

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

// 直接赋值
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);

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

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次其他的字符串,实现代码如下:

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操作都会生成新的对象。,这种代码将会有多大的内存消耗。这个时候想必大家已经有了点答案。我将上述的代码稍微的修改一下:

public class Main {

    public static void main(String[] args){
        
        String string = "";
        for(int i=0;i<10000;i++){
            string += "hello";
        }
    }

}

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

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是线程安全的。

我们来看下面的代码:

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文件:

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是最合适的。

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

  1. 下面的代码输出的结果是什么?
String a = "hello2";   
String b = "hello" + 2;   
System.out.println((a == b));

结果是true,它String b = "hello" + 2; 被编译器优化成了String b = "hello2"; 所以运行时字符串a和b指向同一个对象。

  1. 下面的代码输出的结果是什么?
String a = "hello2";    
String b = "hello";       
String c = b + 2;       
System.out.println((a == c));

输出结果为:false。由于有符号引用的存在,所以 String c = b + 2;不会在编译期间被优化,不会把b+2当做字面常量来处理的,通过StringBuilder生成了一个新的对象,因此这种方式生成的对象事实上是保存在堆上的。

  1. 下面的代码输出的结果是什么?
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;

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

相关文章

此处评论已关闭