13

Java 字符串连接运算符干了什么?

 3 years ago
source link: https://segmentfault.com/a/1190000023010265
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

和其他多数程序设计语言一样,Java 语言允许使用 + 连接两个字符串。

String name = "stephen";
String foo = "Hey, " + name;

当我们将一个字符串和一个非字符串的值进行拼接时,并不会报错:

String name = "Stephen";
int age = 25;
String foo = name + age; // 结果为 Stephen25

其原因是当 + 运算符左右两边有一个值是字符串时,会将另一个值尝试转化为字符串。

字符串转换机制

我们在了解字符串连接运算符前,先了解一下字符串转换机制(String Conversion)。

Any type may be converted to type String by string conversion.

如果值 x 是基本数据类型 T,那么在字符串转换前,首先会将其转换成一个引用值,举几个例子:

• 如果 T 是 boolean 类型的,那么就会用 new Boolean(x) 封装一下;

• 如果 T 是 char 类型的,那么就会用 new Character(x) 封装一下;

• 如果 T 是 byte、short、int 类型的,那么就会用 new Integer(x) 封装一下;

我们知道,对于基本数据类型,Java 都对应有一个包装类(比如 int 类型对应有 Integer 对象),这样操作以后,每个基础数据类型的值 x 都变成了一个对象的引用。

为什么这么做?为了统一对待,当我们把基础数据类型转换成对应的包装类的一个实例后,所有的值都是统一的对象引用。

此时才开始真正进行字符串转换。我们需要考虑两种情况:空值和非空值。

如果此时的值 x 是 null ,那么最终的字符串转换结果就是一个字符串 null

否则就会 调用这个对象的 toString() 的无参方法

前者很好理解,后者我们一起来看看:

在 Java 所有的父类 Object 中,有一个重要的方法就是 toString 方法,它返回表示对象值的一个字符串。在 Object 类中对 toString 的定义如下:

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

该方法返回对象的类名和散列码。如果类没有重写 toString 方法,默认就会调用它的父类的 toString 方法,而此时我们的值 x 统一都是对象值,所以一定有 toString 方法可以调用并打印出值(也有个特殊,如果调用 toString 返回的值是一个 null 值,那么就会用字符串 null 代替)。

字符串连接符

+ 运算符左右两边参与运算的表达式的值有一个为字符串时,那么在程序运行时会对另一个值进行 字符串转换

这里需要注意的是 + 运算符同时作为算术运算符,在含有多个值参与运算的时候,要留意优先级,比如下面这个例子:

String a = 1 + 2 + " equals 3";
String b = "12 eqauls " + 1 + 2;

变量 a 的结果是 3 equals 3 ,变量 b 的结果是 12 equals 12

有些人这里可能会有疑问,解释一下,第一种情况根据运算优先级是先计算 1+2 那么此时的 + 运算符是算术运算符,所以结果是 3,然后再和 " equals 3" 运算,又因为 3 + " equals 3" 有一个值为字符串,所以 + 运算符是字符串连接运算符。

在运行时,Java 编译器一般会使用类似 StringBuffer/StringBuilder 这样带缓冲区的方式来减少通过执行表达式时创建的中间 String 对象的数量,从而提高程序性能。

我们可以用 Java 自带的反汇编工具 javap 简单的看一下:

假设有如下这段代码:

public class Demo {
    public static void main(String[] args) {
        int i = 10;
        String words = "stephen" + i;
    }
}

然后编译,再反汇编一下:

javac Demo.java
javap -c Demo

可以得到如下内容:

Compiled from "Demo.java"
public class Demo {
  public Demo();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: bipush        10
       2: istore_1
       3: new           #2                  // class java/lang/StringBuilder
       6: dup
       7: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V
      10: ldc           #4                  // String stephen
      12: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      15: iload_1
      16: invokevirtual #6                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
      19: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      22: astore_2
      23: return
}

我们可以发现,Java 编译器在执行字符串连接运算符所在表达式的时候,会先创建一个 StringBuilder 对象,然后将运算符左边的字符串 stephen 拼接(append)上去,接着在拼接右边的整型 10 ,然后调用 StringBuilder 的 toString 方法返回结果。

如果我们拼接的是一个对象呢?

public class Demo {
    public static void main(String[] args) {
        Demo obj = new Demo();
        String words = obj + "stephen";
    }

    @Override
    public String toString() {
        return "App{}";
    }
}

一样的做法,我们会发现此时 Method java/lang/StringBuilder.append:(Ljava/lang/Object;) 也就是 StringBuilder 调用的是 append(Object obj) 这个方法,我们查看 StringBuilder 类的 append 方法:

public StringBuilder append(Object obj) {
    return append(String.valueOf(obj));
}

String.valueOf(obj) 的实现代码如下:

public static String valueOf(Object obj) {
    return (obj == null) ? "null" : obj.toString();
}

也就是会调用对象的 toString() 方法。

可能到这里大家会有一个疑问:上面不是说字符串转换对于基本类型是先转换成对应的包装类,然后调用它的 toString 方法吗,这边怎么都是调用 StringBuilder 的 append 方法了呢?

实现方式不同,其实是本质上是一样的,只不过为了提高性能(减少创建中间字符串等的损耗),Java 编译器采用 StringBuilder 来做。感兴趣的可以自己去追踪下 Integer 包装类的 toString 方法,其实和 StringBuilder 的 append(int i) 方法的代码是几乎一样的。

参考链接

String Concatenation Operator +

String Conversion


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK