和其他多数程序设计语言一样, 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
来源: https://segmentfault.com/a/1190000023010265