Java里的加号是一个被赋予了两重职责的运算符:既能做算术加法,也能做字符串拼接。正因为如此,当它和关系运算符(比如大于、小于、等于)出现在同一个表达式里时,求值顺序和类型转换就容易让人犯迷糊。最经典的例子是System.out.println(1 + 2 + "abc")输出3abc,而System.out.println("abc" + 1 + 2)却输出abc12。同样一个加号,结果完全不同,问题的根源就在运算符优先级、结合性以及字符串拼接的触发时机上。本文就把这几个知识点串起来讲清楚。

一、加号的双重身份:什么时候做加法,什么时候做拼接
Java语言规范里对加号的行为定义得很明确:当加号两边的操作数中至少有一个是String类型时,执行字符串拼接;只有当两个操作数都是数值类型(int、long、double等)时,才执行算术加法。这个判断不是看整个表达式,而是看当前这一次加法运算的两个操作数。
理解了这一点,再看1 + 2 + "abc"就好解释了。加法运算符是左结合的,表达式等价于(1 + 2) + "abc"。先算1 + 2,两个操作数都是int,做算术加法得到3,然后3 + "abc"中有一个String参与,触发拼接,结果就是3abc。反过来,"abc" + 1 + 2等价于("abc" + 1) + 2,第一步就产生了字符串abc1,第二步继续拼接,得到abc12。
public class PlusDemo {
public static void main(String[] args) {
System.out.println(1 + 2 + "abc"); // 输出 3abc
System.out.println("abc" + 1 + 2); // 输出 abc12
System.out.println("abc" + (1 + 2)); // 输出 abc3,括号改变了求值顺序
System.out.println(1 + 2 + 'a'); // 输出 100,char参与算术加法被提升为int
}
}上面最后一行也值得注意:char类型在与数值做加法时会被提升为int,'a'的编码值是97,所以结果是100而不是3a。如果想拼接字符,需要显式让字符串出现在前面,或者用String.valueOf转换。
二、关系运算符与字符串拼接混合时的优先级陷阱
Java中关系运算符(<、>、<=、>=)的优先级高于加法运算符,而相等运算符==和!=的优先级低于加法。这就带来一个常见的坑:把比较结果和字符串拼接混在一起时,你以为的求值顺序和实际执行的可能不一样。
举个例子,x + y > z会先算加法吗?不会。因为>的优先级更高,编译器会怎么处理?实际上这里要先澄清:优先级高的先结合,但求值上x + y作为>的操作数,需要先完成加法才能比较。真正容易出问题的是拼接和比较写在同一层的情况,比如"结果:" + a == b。由于+的优先级高于==,表达式实际是("结果:" + a) == b,也就是先拼接出一个字符串,再和b做引用比较,几乎永远返回false,而且如果b不是String类型还会直接编译报错。
public class PriorityDemo {
public static void main(String[] args) {
int a = 10, b = 5;
// 危险写法:实际执行的是 ("a+b=" + a) == b,编译直接报错
// System.out.println("a+b=" + a == b);
// 正确写法:用括号明确优先级
System.out.println("a+b=" + (a == b)); // 输出 a+b=false
System.out.println("比较结果:" + (a > b)); // 输出 比较结果:true
}
}所以经验法则是:只要表达式中同时出现字符串拼接和关系运算,就应该用括号把比较部分括起来,避免依赖优先级的隐式规则。这不仅是避免bug,也让代码可读性更好。
三、类型兼容性与自动类型提升
字符串拼接虽然看起来什么类型都能接,但背后依赖的是编译器生成的转换逻辑。编译器遇到String + 其他类型时,会调用StringBuilder的append方法(或StringConcatFactory),把非字符串操作数转换为其字符串表示。基本类型直接转,对象则调用其toString方法,如果为null会输出字面量null而不是抛异常。
数值类型之间的加法则遵循自动类型提升规则:byte、short、char在参与算术运算前都会被提升为int,如果有long、float、double参与则继续提升到更宽的类型。这解释了为什么byte b1 = 1; byte b2 = 2; byte b3 = b1 + b2;会编译报错——右边的结果是int,需要强制转换才能赋回byte。
public class TypeDemo {
public static void main(String[] args) {
byte b1 = 10, b2 = 20;
// byte b3 = b1 + b2; // 编译错误:int不能自动转byte
byte b3 = (byte) (b1 + b2); // 正确:显式强转
System.out.println("b3=" + b3);
Object obj = null;
System.out.println("值=" + obj); // 输出 值=null,不抛NPE
char c = 'A';
System.out.println(c + 1); // 输出 66,int算术
System.out.println("" + c + 1); // 输出 A1,字符串拼接
}
}另一个类型兼容性问题是布尔值。关系运算的结果是boolean,而boolean在Java中不能参与算术运算,也不能与数值隐式转换。但boolean可以参与字符串拼接,"flag=" + true是完全合法的。这一点和C语言差别很大,从C转过来的开发者尤其要注意。
四、字符串比较的正确姿势:equals与编译期优化
既然谈到了关系运算符和字符串,就必须说说==比较String的问题。==比较的是引用地址,两个内容相同的字符串字面量因为常量池的存在可能返回true,但运行时拼接或new出来的字符串几乎一定是false。判断内容相等必须用equals方法。
public class EqualsDemo {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "hello";
String s3 = "hel" + "lo"; // 编译期常量折叠,进常量池
String s4 = "hel" + getLo(); // 运行期拼接,新对象
System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // true,常量折叠
System.out.println(s1 == s4); // false,运行期生成
System.out.println(s1.equals(s4)); // true,内容相等
}
static String getLo() {
return "lo";
}
}编译器会对纯字面量的拼接做常量折叠优化,"hel" + "lo"在class文件里直接就是"hello"。而一旦拼接中有变量参与,就会在运行期通过StringBuilder生成新对象。这也说明依赖==判断字符串相等是一种碰运气的行为,生产代码中一律使用equals。
五、实用建议与总结
针对这类表达式的编写,可以归纳几条实践原则:第一,拼接和比较混合时永远使用括号,把比较逻辑显式括起来;第二,需要拼接数值计算结果时,把算式括起来再拼接,如"sum=" + (a + b);第三,避免在循环中用加号反复拼接字符串,改用StringBuilder或String.join,性能差距在大数据量下非常明显;第四,字符串内容比较永远用equals,需要忽略大小写时用equalsIgnoreCase。
总结一下核心知识点:加号只有在两个操作数都是数值时才做加法,任一操作数是String就转为拼接;运算从左到右按优先级和结合性逐步求值,括号是最可靠的优先级控制手段;算术运算会触发byte、short、char到int的自动提升;==比较字符串比较的是引用而非内容。把这些规则记牢,再遇到1 + 2 + "x"这类表达式就不会再有疑惑了。
Java运算符优先级字符串拼接类型转换修改时间:2026-09-14 01:00:55