手记

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

一、背景

技术群里有一个老铁分享了一段 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人推荐
随时随地看视频
慕课网APP