细微之处见真章之StringUtils的isBlank函数细节解读

article/2025/9/19 16:25:42

一、背景

技术群里有一个老铁分享了一段 commons-lang 的 StringUtils 工具类的代码:

public static boolean isBlank(final CharSequence cs) {int strLen;if (cs == null || (strLen = cs.length()) == 0) {return true;}for (int i = 0; i < strLen; i++) {if (Character.isWhitespace(cs.charAt(i)) == false) {return false;}}return true;
}

得出的结论是:

老外的代码风格和咱们真的不一样,人家判断某个布尔值是否等于 false,居然这么写,咱们都是取反判断的。

真的是这样吗?

从这段代码中我们还发现,人家的参数用 final 修饰.

What are you 弄啥哩?

平凡之处见真章,本文将以这个简单的问题入手,带着大家熟悉反编译和反汇编,带着大家分析问题。

二、布尔判断问题

2.1 真的是老外都这么写?

2.1.1 拉最新版本

那么真的老外都是这么写的吗?我们拉取 commons-lang 最新版的代码,发现并非如此。

master 分支 commitId 为 fe44a99852719ff842ff5 的源码:

public static boolean isBlank(final CharSequence cs) {int strLen = length(cs);if (strLen == 0) {return true;}for (int i = 0; i < strLen; i++) {if (!Character.isWhitespace(cs.charAt(i))) {return false;}}return true;
}

已经改成取反的方式了。

那么问题来了,大家可以想想,为啥改了呢?

很显然,源码改成这种写法应该是这种写法更好,否则没必要改啊,对吧。

那么为啥这种写法更好呢?

我们可以借助 IDEA 的检查工具。

其实如果平时你写代码的时候能够关注 IDEA 的警告,就会发现 “条件 == false” 这种写法会给出下面警告:

spring警告

因此我们可知道, IDEA 不推荐这种写法,认为另外一种写法是更简化的形式。

那么我们如何知道作者的用意呢?

直接拉源码,查看该函数或者该类的修改历史即可。

spring源码

可以从修改历史的提交注释中找到原因。

可以看出修改原因为, 根据 IDEA 提示进行重构,在 #276 编号的 PR 中引入进来的。

我们可以到该项目的 pull requests 中修饰该编号:

https://github.com/apache/commons-lang/pull/276

spring源码

这里有修改的详细描述。

另外我们在研究这个问题的时候又有了新的发现:

spring源码

我们发现 overlay 函数在此次提交时,将 StringBuilder 拼接的字符串的方式改为了直接用加号拼接,大家可以思考下为什么。可以评论区给出自己的看法。

2.1.2 看其他项目

我们还可以用专栏里强力推荐的 codota 查看其他外国的知名开源项目有没有这种写法。

发现有很多类似的写法,包括 spring-framework:

spring源码

2.2 研究两者的差别

为了更好地研究这个问题,咱们自己写一个字符串工具类,Copy一下代码:

public class StringUtils {public static boolean isBlank(final CharSequence cs) {int strLen;if (cs == null || (strLen = cs.length()) == 0) {return true;}for (int i = 0; i < strLen; i++) {if (Character.isWhitespace(cs.charAt(i)) == false) {return false;}}return true;}
}

对该类进行编译,然后通过 IDEA 自带的反编译工具进行反编译,得到下面的代码:

public class StringUtils {public StringUtils() {}public static boolean isBlank(final CharSequence cs) {int strLen;if (cs != null && (strLen = cs.length()) != 0) {for(int i = 0; i < strLen; ++i) {if (!Character.isWhitespace(cs.charAt(i))) {return false;}}return true;} else {return true;}}
}

我们看到反编译后的代码,还是对 Character.isWhitespace 的判断取反。

我们可以查反汇编代码:

public class com.chujianyun.libs.commons.lang3.StringUtils {public com.chujianyun.libs.commons.lang3.StringUtils();Code:0: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: returnpublic static boolean isBlank(java.lang.CharSequence);Code:0: aload_01: ifnull        154: aload_05: invokeinterface #2,  1            // InterfaceMethod java/lang/CharSequence.length:()I10: dup11: istore_112: ifne          1715: iconst_116: ireturn17: iconst_018: istore_219: iload_220: iload_121: if_icmpge     4524: aload_025: iload_226: invokeinterface #3,  2            // InterfaceMethod java/lang/CharSequence.charAt:(I)C31: invokestatic  #4                  // Method java/lang/Character.isWhitespace:(C)Z34: ifne          3937: iconst_038: ireturn39: iinc          2, 142: goto          1945: iconst_146: ireturn
}

我们可以看到 isBlank 反编译的代码的 31 行处,调用 java.lang.Character#isWhitespace(char) 返回了 boolean 值。

 31: invokestatic  #4                  // Method java/lang/Character.isWhitespace:(C)Z

JVMS 2.3.4 节对 boolean 类型有如下描述:

JVM 中用 1 表示 true , 0 表示 false。

Java 编程语言中 boolean 类型的值会被编译器编译成 JVM 所需的整数类型。

因此面执行的结果为 0 或者 1 。

然后执行 ifne

  34: ifne          39

ifne success if and only if value ≠ 0

只有值不等于 0 则为成功

如果值为 1 则跳转到 39 行,将局部变量表索引为 2 的变量即 i 加一,然后和 strLen 比较,然后…

如果值为 0 即上述结果为 false ,则执行

iconst_0  // 将常量 0 压如操作数栈
ireturn  // 将栈顶元素作为返回弹出

即等价于 return false。

然后我们将代码改成另外一种形式:

public class StringUtils {public static boolean isBlank(final CharSequence cs) {int strLen;if (cs == null || (strLen = cs.length()) == 0) {return true;}for (int i = 0; i < strLen; i++) {if (!Character.isWhitespace(cs.charAt(i)) ) {// 这里不同return false;}}return true;}
}

发现编译后的反编译代码相同,反汇编后的代码也相同(此处就不再重复贴出代码了)。

因此可以得出一个结论,两种写法编译后的字节码相同。

都是通过 ifne 判断上面表达式的boolean 结果来决定执行再次循环或者返回的逻辑。

三、final 参数问题

参数声明为 final 的目的是啥呢?

JLS 4.12.4 final variables 讲到:

变量可以声明为 final。 final 变量只能被赋值一次。

一个 final 变量,除非之前该变量是明确未被赋值,否则再次赋值会报编译时错误。

一旦 final 变量被赋值,那么它就是始终保持同一个值。

如果 final 类型的变量持有一个对象的引用,对象的状态可以由对象提供的函数修改,但是变量总是引用相同的对象。

这个原则同样适用于数组,因为数组包含多个对象;如果一个 final 变量持有数组对象,数组的元素可以修改,但这个变量引用同一个数组对象。

也有一些变量虽然不声明为 final ,也会被认为 effectively final(和 final 等效)。

局部变量声明时即初始化,如果满足以下几种情况,则为 effectively final

  • 没有声明为 final。
  • 它永远不会出现在赋值表达式的左侧。 (注意:局部变量声明符包含初始化但不能是赋值表达式。)
  • 它永远不会作为前缀或后缀递增或递减运算符的操作数出现。

2 局部变量声明时如果没有初始化,如果满足以下几种情况,则为 effectively final

  • 没有声明为 final
  • 当它出现在赋值表达式的左边时,它肯定是未赋值的,而且在赋值之前也没有明确赋值;
    也就是说,它绝对是未赋值的,也不是绝对赋值在赋值表达式的右边(§16(明确赋值))。
  • 它永远不会作为前缀或后缀递增或递减运算符的操作数出现。

3 方法、构造器、lambda 或异常的参数被视作有初始化器的局部变量,目的是为了判断这些参数是否为 effectively final 的。

另外Java 语言手册还有这样一段描述:

如果变量是 effectively final ,那么为其添加 final 修饰符不会有任何错误。一个合法的 final 局部变量或者参数删除 final 修饰符,会变成 effectively final。

有了这些知识储备之后,我们再看这个问题就简单多了。

因为 lambda 表达式和匿名内部类中使用的变量要求是 final 或 effectively final类型。

报错

从语言角度

只要满足以上条件,参数上可以不显式声明 final, 也可以在 lambda 表达式或者匿名内部类中使用。

报错

报错

显式声明还有一个好处是,在函数内部引用不能发生改变。

报错

从功能角度

从功能角度来讲, isBlank 函数是判断该字符序列是否为空字符串、null 或者包含空格。

因此参数传入后不希望也不需要在函数内部对引用进行修改。

因此显式加上 final 声明更稳妥。

so ,问题解决了??

No, 上面讲到如果final 变量持有对象的引用,如果不允许修改对象的属性怎么办

可以使用不可变对象。如 String。

那么不可变对象是如何实现的呢?

我们以 String 为例:

public final class Stringimplements java.io.Serializable, Comparable<String>, CharSequence {/** The value is used for character storage. */private final char value[];// 将参数字符串追加到当前字符串后public String concat(String str) {int otherLen = str.length();if (otherLen == 0) {return this;}int len = value.length;char buf[] = Arrays.copyOf(value, len + otherLen);str.getChars(buf, len);return new String(buf, true);}// 其他属性和函数略
}

建议大家自行思考 String 是如何实现不可变的,这个面试中也可能会问到。

  • 存储字符数组的 value 成员变量用 final 修饰,赋值后引用不能改变。
  • 所有修改对象的属性或状态的方法返回的都是新的字符串对象。

因此我们编写不可变对象时可以参考这种思路。

那么如果引用不可变也不允许改变对象的属性怎么办?

此时可以 final + 不可变对象一起起作用。

public class MapTest {private static final Map<String, Integer> MAP;static {Map<String, Integer> data = new HashMap<>();data.put("a", 1);data.put("b", 2);MAP = MapUtils.unmodifiableMap(data);}@Testpublic void test2() {// 报错 java.lang.UnsupportedOperationExceptionMAP.put("c", 3);System.out.println(MAP);}
}

这样,引用不可变,map 的值也不可修改。

四、启示

本文内容并不难,但是希望通过本问向大家传达一些理念。

实践是检验真理的标准。没实践不要轻易下结论。我们在下结论之前进行对比,进行调研,不要看到孤立的例子就立马下结论。

学习时要多动手。大家学习技术时要尽量自己写简单的DEMO 验证自己的想法,可以调试细节。

善用工具。 本文用到的 codota 是编程利器,还有很多超好用的插件在本的博客中或专栏里有专门的推荐。 IDEA 的语法警告、错误提示是我们养成好的编程习惯,避免犯错的极佳助手。 GIT 也是我们学习源码的重要工具。

更多以好用的 IDEA 插件和好用的效率工具可以看这篇文章。

善用反编译和反汇编。通过反编译可以破解一些语法糖,通过反汇编可以从字节码层面学习知识。可以透过源码看到更本质的东西,推荐大家去重点掌握。

细微之处见真章。有些看似简单的问题背后隐藏着很多可学的知识,然而很多人会忽略这些问题。面试中一些简单问题,能否回答的全面,回答的有深度,都是一个人专业是否扎实的表现。

看源码。看源码有很多思维和方法。比如以设计者的角度学习源码;比如通过设计模式的角度学源码;比如通过调试学源码等等,专栏有专门章节详细介绍。在这里提醒大家的是,看源码一定要多思考。

思考它为什么这么写,不这么写行不行?这点很重要,比如本文提到的 为啥源码某个版本 if 条件 用 == false 判断,为啥参数带 final 等等。可以将知识串起来,加深对知识的理解。

Java 语言规范 和 Java 虚拟机规范是最权威的参考。很多人习惯看博客来学习知识。在这里我更希望大家转向从 Java 的语言和虚拟机层面来学习知识,而《Java 语言规范》和 《Java 虚拟机规范》则是官方出的权威参考。

是什么?为什么?怎么做? 这是一个非常重要的思维方式。然而很多人喜欢记忆结论。导致记住容易遗忘,记住不会用。

五、写在最后

发现很多人学习技术总是喜欢强调努力,强调多看书,多看源码。

就我个人而言,更喜欢大家如果自己的学习效果不是特别满意,多去学习和运用一些新的思维和方法。

因为新的思维和方法对技术的提升速度影响更大。

多看书也没错,但是看什么书?怎么看?多看源码也没错,看哪些源码?怎么看?有哪些思维和方法?

这些才是问题的关键,使用不同的方法看不同的内容,最后效果的差距也非常大。

总之希望大家学习时不要忽略基础,希望大家多探索一些好的方法,能够从更深的层面去学习和理解源码。


如果你觉得本文对你有帮助,欢迎点赞、转发、评论,你的支持是我创作的最大动力。


http://chatgpt.dhexx.cn/article/5u4WQYsJ.shtml

相关文章

字符串判空,isBlank 和 isEmpty 到底选那个?

字符串的判空&#xff0c;日常开发是经常要做的一种校验&#xff0c;common-lang包帮我们做了一些字符串判空的封装 <dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId> </dependency> 并对外提…

sbrk() 函数是干什么的?

brk和sbrk主要的工作是实现虚拟内存到内存的映射. 在GNU C中,内存分配是这样的: 每个进程可访问的虚拟内存空间为3G&#xff0c;但在程序编译时&#xff0c;不可能也没必要为程序分配这么大的空间&#xff0c;只分配并不大的数据段空间&#xff0c;程序中动态分配的空间就是从这…

ORACLE如何修改默认端口号

oracle在安装的的时候&#xff0c;除了可以默认端口号&#xff0c;也可以填入自定义的端口号。但是项目上会出现这样的情况&#xff0c;项目在进展时期&#xff0c;需要对数据库安全升级&#xff0c;首先要修改数据库的默认端口号。那如何来设置oracle的默认端口号&#xff0c;…

Oracle如何更改端口号

Oracle默认端口号为8080&#xff0c;因为Tomcat端口默认也是8080所以需要把Oracle端口更改一下&#xff0c;不建议更改Tomcat端口号 运行cmd命令行操作 查看Oracle版本&#xff1a; 链接Oracle数据库&#xff1a; conn system/root as sysdba&#xff0c;system/root用户名和…

Oracle 19c中默认端口

Oracle官网https://docs.oracle.com/en/database/oracle/oracle-database/19/rilin/port-numbers-and-protocols-of-oracle-components.html#GUID-D168F70C-BECE-4F9A-B616-D9103A35F1FFhttps://www.cndba.cn/hbhe0316/article/87226https://www.cndba.cn/hbhe0316/article/872…

oracle 数据库改端口

Oracle 11g修改默认端口1521为其他值 1、修改listener.ora 打开文件D:\app\Administrator\product\11.2.0\dbhome_1\NETWORK\ADMIN\listener.ora&#xff0c;修改PORT 后的数值&#xff0c;如下图 修改listener.ora文件 2、重启TNSListener服务 在Windows服务中重启OracleOr…

如何查看oracle的服务端口号

查看 oracle服务端口的步骤&#xff1a; 按住WinR键&#xff0c;弹出DOS命令窗口。 输入cmd命令&#xff0c;点击确定。 弹出的DOS窗口中&#xff0c;i输入 lsnrctl status 命令&#xff0c;并点击Enter键。 找到窗口中&#xff0c;以下文字‘监听端点概要 (DESCRIPTION…

JUC是什么?

JUC表示什么 JUC是java.util.concurrent包的缩写&#xff0c;其包结构如下。 JUC框架结构 JUC是包的简称&#xff0c;JUC可能也是Java核心里最难的一块儿&#xff0c;JUC指的是Java的并发工具包&#xff0c;里边提供了各种各样的控制同步和线程通信的工具类。学习JUC之前&a…

JUC总结系列篇 (二) : 对线程的理解和使用总结

文章内容&#xff1a; 一.为什么需要多线程 二.线程的创建 三.线程的方法sleep(),run(),wait(),yeid(),join(),interrupt()等方法归纳总结 四.线程的状态及其转换 五.线程的交替执行案例 六.多个线程依次执行案例 七.多线程并发带来的线程安全问题 一.为什么需要多线程&#x…

JUC源码系列-ReentrantReadWriteLock

继承关系 ReadLock和WriteLock是ReentrantReadWriteLock的两个内部类&#xff0c;Lock的上锁和释放锁都是通过AQS来实现的。 AQS定义了独占模式的acquire()和release()方法&#xff0c;共享模式的acquireShared()和releaseShared()方法。 还定义了抽象方法tryAcquire()、tryA…

了解JUC

高级技术之 JUC 高并发编程 内容概览 1 、什么是JUC2 、Lock接口3 、线程间通信4 、集合的线程安全5 、多线程锁6 、Callable接口7 、JUC三大辅助类: CountDownLatch CyclicBarrier Semaphore8 、读写锁: ReentrantReadWriteLock9 、阻塞队列10 、ThreadPool线程池11 、Fork/…

juc系列(1)---进程,线程,并行,并发

目录 概述进程线程关系并发并行&#xff1a;同步异步&#xff1a;对比 概述 进程 程序由指令和数据组成&#xff0c;但这些指令要运行&#xff0c;数据要读写&#xff0c;就必须将指令加载至CPU,数据加载至内 存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加…

JUC系列(九) CAS 与锁的理解

&#x1f4e3; &#x1f4e3; &#x1f4e3; &#x1f4e2;&#x1f4e2;&#x1f4e2; ☀️☀️你好啊&#xff01;小伙伴&#xff0c;我是小冷。是一个兴趣驱动自学练习两年半的的Java工程师。 &#x1f4d2; 一位十分喜欢将知识分享出来的Java博主⭐️⭐️⭐️&#xff0c;擅…

JUC系列(四)

1、CAS 1.1、没有CAS之前&#xff0c;保证线程安全的方式 多线程环境不使用原子类保证线程安全&#xff08;基本数据类型&#xff09; public class T3 {volatile int number 0;//读取public int getNumber(){return number;}//写入加锁保证原子性public synchronized void…

1、什么是juc

1、juc简介 在java中&#xff0c;线程部分是重点&#xff0c;juc就是java.util.concurrent工具包的简称。这是一个处理线程的工具包&#xff0c;从jdk1.5开始出现 2、进程与线程 进程&#xff1a;指在系统中正在运行的一个应用程序&#xff1b;程序一旦运行就是进程&#xff1…

JUC系列(六) 线程池

&#x1f4e3; &#x1f4e3; &#x1f4e3; &#x1f4e2;&#x1f4e2;&#x1f4e2; ☀️☀️你好啊&#xff01;小伙伴&#xff0c;我是小冷。是一个兴趣驱动自学练习两年半的的Java工程师。 &#x1f4d2; 一位十分喜欢将知识分享出来的Java博主⭐️⭐️⭐️&#xff0c;擅…

JUC系列(五)

1、ThreadLocal 1.1、什么是ThreadLocal 线程局部变量。 1.2、ThreadLocal的作用以及可以为什么保证线程安全&#xff1f; 多线程访问同一个共享变量的时候容易出现并发问题&#xff0c;特别是多个线程对一个变量进行写入的时候&#xff0c;为了保证线程安全&#xff0c;一…

JUC系列(二)

1、聊一聊Java“锁” 1.1、乐观锁和悲观锁 悲观锁&#xff1a;认为自己在使用数据的时候一定有别的线程来修改数据&#xff0c;因此在获取数据的时候会先加锁&#xff0c;确保数据不会被别的线程修改。synchronized关键字和Lock的实现类都是悲观锁。适合写操作多的场景&#…

JUC概述

JUC是什么&#xff1f; JUC就是java.util.concurrent,java.util.concurrent.atomic和java.util.concurrent.locks三个工具类包&#xff0c;它们是处理线程的工具包&#xff0c;最开始出现是从JDK 1.5开始出现。&#xff08;JUC就是java.util.concurrent工具类的首字母&#xf…

JUC系列一:什么是JUC

前言&#xff1a;笔记整理参考尚硅谷周阳老师在B站上的JUC教程&#xff0c;万分感谢周阳老师。有兴趣的朋友可以在B站搜索周阳老师的视频教程&#xff0c;绝对让你受益匪浅&#xff0c;期望未来也能成为像周阳老师那样的人O(∩_∩)O哈哈~。 JUC B站视频地址 https://www.bilib…