为了账号安全,请及时绑定邮箱和手机立即绑定

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

标签:
Java

一、背景

技术群里有一个老铁分享了一段 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” 这种写法会给出下面警告:
图片描述

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

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

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

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

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

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

图片描述

这里有修改的详细描述。

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

图片描述

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

2.1.2 看其他项目

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

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

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_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static boolean isBlank(java.lang.CharSequence);
    Code:
       0: aload_0
       1: ifnull        15
       4: aload_0
       5: invokeinterface #2,  1            // InterfaceMethod java/lang/CharSequence.length:()I
      10: dup
      11: istore_1
      12: ifne          17
      15: iconst_1
      16: ireturn
      17: iconst_0
      18: istore_2
      19: iload_2
      20: iload_1
      21: if_icmpge     45
      24: aload_0
      25: iload_2
      26: invokeinterface #3,  2            // InterfaceMethod java/lang/CharSequence.charAt:(I)C
      31: invokestatic  #4                  // Method java/lang/Character.isWhitespace:(C)Z
      34: ifne          39
      37: iconst_0
      38: ireturn
      39: iinc          2, 1
      42: goto          19
      45: iconst_1
      46: 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 String
    implements 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);
    }

    @Test
    public void test2() {
       // 报错 java.lang.UnsupportedOperationException
        MAP.put("c", 3);
        System.out.println(MAP);
    }
}

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

四、启示

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

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

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

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

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

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

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

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

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

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

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

五、写在最后

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

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

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

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

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

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


如果你觉得本文对你有帮助,欢迎点赞、转发、评论,你的支持是我创作的最大动力。
另外想学习,更多开发和避坑技巧,少走弯路,请关注我的专栏:《阿里巴巴Java 开发手册》详解专栏

点击查看更多内容
6人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
JAVA开发工程师
手记
粉丝
3857
获赞与收藏
1465

关注作者,订阅最新文章

阅读免费教程

感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消