Java 中 String 对 null 对象的容错处理!

简介: 最近在读《Thinking in Java》,看到这样一段话:

作者:肖汉松 blog.xiaohansong.com/2016/03/13/null-in-java-string/


最近在读《Thinking in Java》,看到这样一段话:


_Primitives that are fields in a class are automatically initialized to zero, as noted in the Everything Is an Object chapter. _


_But the object references are initialized to null, and if you try to call methods for any of them, you’ll get an exception-a runtime error. _


Conveniently, you can still print a null reference without throwing an exception.


大意是:原生类型会被自动初始化为 0,但是对象引用会被初始化为 null,如果你尝试调用该对象的方法,就会抛出空指针异常。通常,你可以打印一个 null 对象而不会抛出异常。


第一句相信大家都会容易理解,这是类型初始化的基础知识,但是第二句就让我很疑惑:为什么打印一个 null 对象不会抛出异常?带着这个疑问,我开始了解惑之旅。下面我将详细阐述我解决这个问题的思路,并且深入 JDK 源码找到问题的答案。


解决问题的过程


可以发现,其实这个问题有几种情况,所以我们分类讨论各种情况,看最后能不能得到答案。


首先,我们把这个问题分解为三个小问题,逐一解决。


第一个问题

直接打印 null 的 String 对象,会得到什么结果?

String s = null;  
System.out.print(s);  • 1

运行的结果是

null

果然如书上说的没有抛出异常,而是打印了null。显然问题的线索在于print函数的源码中。我们找到print的源码:

public void print(String s) {  
    if (s == null) {  
        s = "null";  
    }  
    write(s);  
}  

看到源码才发现原来就只是加了一句判断而已,简单粗暴,可能你对 JDK 的简单实现有点失望了。放心,第一个问题只是开胃菜而已,大餐还在后面。

第二个问题

打印一个 null 的非 String 对象,例如说 Integer:

Integer i = null;  
System.out.print(i);  

运行的结果不出意料:

null

我们再去看看print的源码:

public void print(Object obj) {     write(String.valueOf(obj)); }

有点不一样的了,看来秘密藏在valueOf里面。

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

看到这里,我们终于发现了打印 null 对象不会抛出异常的秘密。print方法对 String 对象和非 String 对象分开进行处理。


String 对象:直接判断是否为 null,如果为 null 给 null 对象赋值为”null”。


非 String 对象:通过调用String.valueOf方法,如果是 null 对象,就返回”null”,否则调用对象的toString方法。


通过上面的处理,可以保证打印 null 对象不会出错。


到这里,本文就应该结束了。

什么?说好的大餐呢?上面还不够塞牙缝呢。

开玩笑啦。下面我们来探讨第三个问题。


第三个问题(隐藏的大餐)

null 对象与字符串拼接会得到什么结果?

String s = null;  
s = s + "!";  
System.out.print(s);'  • 1
• 2

结果可能你也猜到了:

null!

为什么呢?跟踪代码运行可以发现,这回跟print没有什么关系。但是上面的代码就调用了print函数,不是它会是谁呢?+的嫌疑最大,但是+又不是函数,我们怎么看到它的源代码?这种情况,唯一的解释就是编译器动了手脚,天网恢恢,疏而不漏,找不到源代码,我们可以去看看编译器生成的字节码。

L0  
 LINENUMBER 27 L0  
 ACONST_NULL  
 ASTORE 1  
L1  
 LINENUMBER 28 L1  
 NEW java/lang/StringBuilder  
 DUP  
 INVOKESPECIAL java/lang/StringBuilder. ()V  
 ALOAD 1  
 INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;  
 LDC "!"  
 INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;  
 INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;  
 ASTORE 1  
L2  
 LINENUMBER 29 L2  
 GETSTATIC java/lang/System.out : Ljava/io/PrintStream;  
 ALOAD 1  
 INVOKEVIRTUAL java/io/PrintStream.print (Ljava/lang/String;)V  

看了上面的字节码是不是一头雾水?这里我们就要扯开话题,来侃侃+字符串拼接的原理了。


编译器对字符串相加会进行优化,首先实例化一个StringBuilder,然后把相加的字符串按顺序append,最后调用toString返回一个String对象。不信你们看看上面的字节码是不是出现了StringBuilder。详细的解释参考这篇文章 Java细节:字符串的拼接。

String s = "a" + "b";  

等价于

StringBuilder sb = new StringBuilder();  
sb.append("a");  
sb.append("b");  
String s = sb.toString();  • 1
• 2
• 3

再回到我们的问题,现在我们知道秘密在StringBuilder.append函数的源码中。

//针对 String 对象  
public AbstractStringBuilder append(String str) {  
    if (str == null)  
        return appendNull();  
    int len = str.length();  
    ensureCapacityInternal(count + len);  
    str.getChars(0, len, value, count);  
    count += len;  
    return this;  
}  
//针对非 String 对象  
public AbstractStringBuilder append(Object obj) {  
    return append(String.valueOf(obj));  
}  
private AbstractStringBuilder appendNull() {  
    int c = count;  
    ensureCapacityInternal(c + 4);  
    final char[] value = this.value;  
    value[c++] = 'n';  
    value[c++] = 'u';  
    value[c++] = 'l';  
    value[c++] = 'l';  
    count = c;  
    return this;  
}  

现在我们恍然大悟,append函数如果判断对象为 null,就会调用appendNull,填充”null”。


总结

上面我们讨论了三个问题,由此引出 Java 中 String 对 null 对象的容错处理。上面的例子没有覆盖所有的处理情况,算是抛砖引玉。


如何让程序中的 null 对象在我们的控制之中,是我们编程的时候需要时刻注意的事情。


相关文章
|
21天前
|
设计模式 网络协议 数据可视化
Java 设计模式之状态模式:让对象的行为随状态优雅变化
状态模式通过封装对象的状态,使行为随状态变化而改变。以订单为例,将待支付、已支付等状态独立成类,消除冗长条件判断,提升代码可维护性与扩展性,适用于状态多、转换复杂的场景。
199 0
|
3月前
|
缓存 安全 Java
Java反射机制:动态操作类与对象
Java反射机制是运行时动态操作类与对象的强大工具,支持获取类信息、动态创建实例、调用方法、访问字段等。它在框架开发、依赖注入、动态代理等方面有广泛应用,但也存在性能开销和安全风险。本文详解反射核心API、实战案例及性能优化策略,助你掌握Java动态编程精髓。
|
3月前
|
存储 人工智能 JavaScript
Java从作用域到对象高级应用​
本内容详细讲解了JavaScript中的作用域类型(函数作用域、块作用域、全局作用域)、作用域链、垃圾回收机制、闭包、变量提升、函数参数、数组方法、内置构造函数、对象高级知识、原型链、对象赋值、深浅拷贝、递归、异常处理及this指向等内容,全面覆盖JS核心概念与编程技巧。
40 0
|
4月前
|
存储 Java
Java对象的内存布局
在HotSpot虚拟机中,Java对象的内存布局分为三部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。对象头包含Mark Word、Class对象指针及数组长度;实例数据存储对象的实际字段内容;对齐填充用于确保对象大小为8字节的整数倍。
|
5月前
|
Java 数据库连接 API
Java 对象模型现代化实践 基于 Spring Boot 与 MyBatis Plus 的实现方案深度解析
本文介绍了基于Spring Boot与MyBatis-Plus的Java对象模型现代化实践方案。采用Spring Boot 3.1.2作为基础框架,结合MyBatis-Plus 3.5.3.1进行数据访问层实现,使用Lombok简化PO对象,MapStruct处理对象转换。文章详细讲解了数据库设计、PO对象实现、DAO层构建、业务逻辑封装以及DTO/VO转换等核心环节,提供了一个完整的现代化Java对象模型实现案例。通过分层设计和对象转换,实现了业务逻辑与数据访问的解耦,提高了代码的可维护性和扩展性。
205 1
|
5月前
|
前端开发 Java 数据库连接
java bo 对象详解_全面解析 java 中 PO,VO,DAO,BO,POJO 及 DTO 等几种对象类型
Java开发中常见的六大对象模型(PO、VO、DAO、BO、POJO、DTO)各有侧重,共同构建企业级应用架构。PO对应数据库表结构,VO专为前端展示设计,DAO封装数据访问逻辑,BO处理业务逻辑,POJO是简单的Java对象,DTO用于层间数据传输。它们在三层架构中协作:表现层使用VO,业务层通过BO调用DAO处理PO,DTO作为数据传输媒介。通过在线商城的用户管理模块示例,展示了各对象的具体应用。最佳实践包括保持分层清晰、使用工具类转换对象,并避免过度设计带来的类膨胀。理解这些对象模型的区别与联系。
356 1
|
6月前
|
Java
深入JavaSE:详解Java对象的比较。
总的来说,Java对象的比较就像海洋生物的比较,有外在的,有内在的,有面对所有情况的,也有针对特殊情况的。理解并掌握这些比较方式,就能更好地驾驭Java的世界,游刃有余地操作Java对象。
115 12
|
7月前
|
编解码 JavaScript 前端开发
【Java进阶】详解JavaScript的BOM(浏览器对象模型)
总的来说,BOM提供了一种方式来与浏览器进行交互。通过BOM,你可以操作窗口、获取URL、操作历史、访问HTML文档、获取浏览器信息和屏幕信息等。虽然BOM并没有正式的标准,但大多数现代浏览器都实现了相似的功能,因此,你可以放心地在你的JavaScript代码中使用BOM。
204 23
|
7月前
|
Java 数据安全/隐私保护
Java 类和对象
本文介绍了Java编程中类和对象的基础知识,作为面向对象编程(OOP)的核心概念。类是对象的蓝图,定义实体类型;对象是具体实例,包含状态和行为。通过示例展示了如何创建表示汽车的类及其实例,并说明了构造函数、字段和方法的作用。同时,文章还探讨了访问修饰符的使用,强调封装的重要性,如通过getter和setter控制字段访问。最后总结了类与对象的关系及其在Java中的应用,并建议进一步学习继承等概念。
159 1

热门文章

最新文章