昨天晚上十一点多吧,我在公司楼下抽烟(别学哈),我们组小李跑过来问我一句特别“基础但坑死人的”问题:哥,toString()、String.valueOf()、还有那个强转(String)xx,不都是把东西变成字符串么?我当时困得要死,就回他一句:不一样,差老远了,你别瞎用,不然线上日志能把你坑哭。然后他不信,我就掏电脑给他演示了一波。
先说obj.toString(),这个东西吧,就是“问对象你自己咋描述你自己”。前提是:对象不为null。你要是obj可能为 null,还硬来:
Object obj = null;
System.out.println(obj.toString()); // 直接 NPE,别问
所以我一般脑子里对toString()的第一反应是:它不是“转换”,它是“调用方法”。而且输出长啥样完全看这个类有没有重写toString(),没重写就那种com.xxx.User@1a2b3c,看着就烦。
然后String.valueOf(x),这个东西我跟小李说,像是“官方出品的兜底转换器”,尤其是对null友好。它的关键点就俩: 1)参数是对象时,null不会炸,它会给你字符串"null"(注意是四个字母那种,不是空)。 2)参数是基本类型时,它有一堆重载,走的是更直接的路径,不会装箱(这点在热路径上还挺香的)。
你看这个小例子,小李当时看完就“哦…原来我一直以为一样”:
public class Demo1 {
staticclass User {
privatefinal String name;
User(String name) { this.name = name; }
@Override
public String toString() {
return"User{name='" + name + "'}";
}
}
public static void main(String[] args) {
User u1 = new User("dong");
User u2 = null;
System.out.println(u1.toString()); // User{name='dong'}
System.out.println(String.valueOf(u1)); // User{name='dong'}
// System.out.println(u2.toString()); // NPE
System.out.println(String.valueOf(u2)); // "null"
int n = 42;
System.out.println(String.valueOf(n)); // "42"
}
}
然后我顺手又补了一刀:你们平时写日志是不是爱这么搞:
System.out.println("req=" + obj);
你以为你在拼接字符串,其实 JVM 背后也是在做String.valueOf(obj)这套(所以obj为 null 的时候,打印出来就是req=null,不会 NPE)。小李当时就说,怪不得我有时候日志是null,我还以为是我业务里手动写的……不是,是它帮你兜底了。
接着说第三个:(String) xx强转。这个最容易被误解,很多人以为“强转=转换”,其实强转只是“我保证它本来就是这个类型,你按这个类型看它”。说白了,它不会帮你把Integer变成"123",它只会在你骗 JVM 的时候狠狠干你一巴掌(ClassCastException)。
我当时给小李举了个很“现实”的场景:从Map里拿东西、或者从Object参数里拿字符串,最容易写出强转。
import java.util.HashMap;
import java.util.Map;
publicclass Demo2 {
public static void main(String[] args) {
Map<String, Object> ctx = new HashMap<>();
ctx.put("traceId", "abc-001");
ctx.put("retry", 3);
String traceId = (String) ctx.get("traceId"); // 没问题,本来就是 String
System.out.println(traceId);
// 这句就很经典:你以为是 "3",其实它是 Integer
try {
String retry = (String) ctx.get("retry"); // 直接 ClassCastException
System.out.println(retry);
} catch (ClassCastException e) {
System.out.println("cast boom: " + e);
}
// 你如果真想要字符串:用 valueOf
String retry2 = String.valueOf(ctx.get("retry")); // "3"
System.out.println(retry2);
}
}
所以你看差别一下就出来了:
toString():对象得活着(非 null),而且结果取决于类咋写。
String.valueOf():更像“安全转换”,null 也能处理,基本类型还更顺。
(String)xx:根本不是转换,是“类型断言”,断错了就炸。
我还顺口跟小李说了个细节,很多人会在一些框架里踩:比如你写 JSON、日志、链路参数,一堆地方都用Object装着,最后你想拿字符串。你要的是“可读字符串”,九成应该是String.valueOf(x);你要的是“我确认它就是 String”,那才用强转。强转特别像你在门口跟保安说“我就是本楼的”,你真是的话放行,你不是的话……就尴尬了。
再来个我自己线上爱用的小工具,算是把这仨的关系落到代码里,省得每次脑子打结:
public class Strings {
private Strings() {}
// 业务里常见:null 当成空串处理(别老打印 "null" 影响体验)
public static String nullToEmpty(Object x) {
return x == null ? "" : String.valueOf(x);
}
// 业务里常见:只接受真 String,否则给默认值(避免 ClassCastException)
public static String asStringOrDefault(Object x, String def) {
return (x instanceof String) ? (String) x : def;
}
}
你看这就很“工程化”了:要“展示”用valueOf,要“断言类型”用instanceof + 强转,要“对象自述”你再去toString(),而且一般我还会提醒一句:你自定义类不重写toString(),你就是在跟未来的自己过不去……日志里全是对象地址,排查起来真想掀桌子。
哦对了,小李最后又问我一句:那我是不是以后都用String.valueOf()就完事了?我说也别极端哈。比如你已经确定拿到的是String,那强转或者直接声明成 String 就行;比如你在调试对象内部结构,toString()重写得好就特别爽。但你要处理“不确定类型 + 可能 null + 想要可读字符串”,String.valueOf()真的是最稳的那个。
行了我先不说了,我这会儿又被拉去看一个线上告警,日志里一堆"null",我怀疑又是谁把空串当成了……算了先去看下 Kibana。