本文基于JDK1.8,首发于公众号:Plus技术栈

让我们从一段代码开始

System.out.println("a" + "b" == "ab");
System.out.println(new String("ab") == "ab");

上述代码中,第一行结果为True,第二行结果为False。两者结果不同的原因在于Java中的==符号判断的是对象是否相等,其实质上是比较两者的内存地址,很显然第一行两边指向同一对象,而第二行指向不同对象。

我们都知道String中判断字符串的字面值是否相等要用equals()方法而不是==,那么String类中equals()究竟是如何实现的呢?

String类会在后续文章中分为几个篇章来讲。本篇文章中主要深入String类中有关哈希的部分,主要有以下几个要点:

  • equals()方法的实现

  • 哈希值、字面值与内存地址之间的关系

  • 哈希碰撞与生日攻击

equals()方法的实现

equals的实现

String类属于“值类“。程序员在比较字符串时,希望知道它们在逻辑上是否相等,而不是想了解它们是否指向同一个对象.

值类仅仅是一个表示值的类,具有自己特有的“逻辑相等“概念(不同于对象等同的概念),因此为了满足比较的需求,String类需要覆盖Object类的equals()方法。

每个值至多只存在一个对象的“值类”不需要覆盖equals方法。如枚举类型

String的equals()实现如下:

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

逻辑很简单,如果比较的两个字符串指向同一对象或者每个位置上的字符相等,则返回true。以上实现遵守了Object的规范[JavaSE6]:

  • 自反性。对于任何非null的引用值x,x.equals(x)必须返回true。
  • 对称性。对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true。
  • 传递性。对于任何非null的引用值x、y和z,如果x.equals(y)返回true,并且y.equals(z)也返回true,那么x.equals(z)d也必须返回true。
  • 一致性。对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致地返回true,或者一致地返回false

好吧,以上4条规范又让我想起大学被数学分析支配的恐惧。虽然恐惧,但是我们不能忽视这四条规定,否则我们的程序可能会莫名其妙的崩溃。

hashcode的实现

那么,在覆盖了Object.equals()方法之后是否就可以进行比较了呢?回答当然是不!

覆盖equals时总要覆盖hashCode

经常看源码的同学会发现,很多类中都有hash相关的变量,本篇文章涉及的String类中也有hashcode()方法,那么哈希到底是什么呢?

所谓哈希(hash),就是将不同的输入映射成独一无二的、固定长度的值(又称"哈希值")。它是软件常见运算之一。如果我们在覆盖了equals()方法之后没有覆盖hashcode()将会导致String类无法结合所有基于散列的集合一起正常运作,这样的集合包括HashMap、HashSet和Hashtable。

无法运作的原因在于我们违反了一条关键约定:相等的对象必须具有相等的散列码(hash code)

一起看看String类中的hashcode()方法:

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;
        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
    hash = h;
    }
    return h;
}

一个String实例的hash值只与其内容有关。从代码实现来看,String类计算哈希值实际上就是通过公式(s代表字符串,n代表字符串长度):

s[0]31^(n-1) + s[1]31^(n-2) + ... + s[n-1]

计算而来,即对字符串的每个取31的n-1次幂并累加。那么这个31是怎么来的呢

在Java Effective中有提及:

之所以选择31,是因为它是一个奇素数。如果乘数是偶数,并且乘法溢出的话,信息就会丢失,因为与2相乘等价于移位运算。使用素数的好处并不明显,但是习惯上都使用素数来计算散列结果。31有个很好的特性。即用移位和减法来代替乘法,可以得到更好的性能:31 * i == (i << 5) - i。现代的VM可以自动完成这种优化。

一个好的散列函数通常倾向于“为不相等的对象产生不相等的散列码“。理想情况下,散列函数应该把集合中不相等的实例均匀地分布到所有可能的散列值上。

31是一个奇素数,如果使用非奇素数,非奇素数代表着乘以偶数会导致较少的位包含“变化”信息,即若低位变为零,乘完之后还是零。用偶数会失去一点“可变性”。结果是可能的哈希值分布较差。

除此之外。31可以保证比较少的哈希碰撞, 31i=32i-i=(i<<5)-i,这种位移与减法结合的计算相比一般的运算快很多。

哈希值、字面值与内存地址之间的关系

在hashcode()函数的源码实现可以看到,一个字符串实例的hash值仅与其内容有关。那么是否可以理解内容相同的两个字符串的哈希值一定相等呢?请看以下代码:

int a =  "Aa".hashCode();
int b =  "BB".hashCode();

a与b的值相等,都是2112。上面说到,哈希值就是不同的输入映射成独一无二的、固定长度的值。如果不同的输入得到了相同的哈希值,就发生了“哈希碰撞“(collision)。

像上面这种情况就发生了哈希碰撞,除了这种简单字符串之外,"柳柴"与"柴柕","志捘"与"崇몈"也会导致碰撞。两者的hashcode值分别为851553和786017。

我们可以总结一下:

  • 哈希值相同的字符串,字面值不一定相同。
  • 字面值相同的字符串,哈希值一定相同。
  • 字面值相同的字符串,内存地址不一定相同。

哈希碰撞和与生日攻击

防止哈希碰撞

前面说了那么多的哈希碰撞,究竟我们应该如何防止哈希碰撞?

防止哈希碰撞的最有效方法,就是扩大哈希值的取值空间。

16个二进制位的哈希值,产生碰撞的可能性是 65536 分之一。也就是说,如果有65537个用户,就一定会产生碰撞。哈希值的长度扩大到32个二进制位,碰撞的可能性就会下降到 4,294,967,296 分之一。

更长的哈希值意味着更大的存储空间、更多的计算,将影响性能和成本。开发者必须做出抉择,在安全与成本之间找到平衡。

下面就介绍,如何在满足安全要求的前提下,找出哈希值的最短长度。

生日攻击

哈希碰撞的概率取决于两个因素(假设哈希函数是可靠的,每个值的生成概率都相同)。

  • 取值空间的大小(即哈希值的长度)。
  • 整个生命周期中,哈希值的计算次数。

这个问题在数学上早有原型,叫做"生日问题"(birthday problem):一个班级需要有多少人,才能保证每个同学的生日都不一样?

答案很出人意料。如果至少两个同学生日相同的概率不超过5%,那么这个班只能有7个人。事实上,一个23人的班级有50%的概率,至少两个同学生日相同;50人班级有97%的概率,70人的班级则是99.9%的概率(计算方法见后文)。

这意味着,如果哈希值的取值空间是365,只要计算23个哈希值,就有50%的可能产生碰撞。也就是说,哈希碰撞的可能性,远比想象的高。哈希碰撞所需耗费的计算次数,跟取值空间的平方根是一个数量级

这种利用哈希空间不足够大,而制造碰撞的攻击方法,就被称为生日攻击(birthday attack)。

有关哈希碰撞和生日攻击的知识与推导就不在这里详细描述了,大家感兴趣的可以去找找资料。

String源码分析(1)--哈希篇的更多相关文章

  1. (转)Java中的String为什么是不可变的? -- String源码分析

    背景:被问到很基础的知识点  string  自己答的很模糊 Java中的String为什么是不可变的? -- String源码分析 ps:最好去阅读原文 Java中的String为什么是不可变的 什 ...

  2. 鸿蒙内核源码分析(忍者ninja篇) | 都忍者了能不快吗 | 百篇博客分析OpenHarmony源码 | v61.02

    百篇博客系列篇.本篇为: v61.xx 鸿蒙内核源码分析(忍者ninja篇) | 都忍者了能不快吗 | 51.c.h.o 编译构建相关篇为: v50.xx 鸿蒙内核源码分析(编译环境篇) | 编译鸿蒙 ...

  3. 鸿蒙内核源码分析(汇编传参篇) | 如何传递复杂的参数 | 百篇博客分析OpenHarmony源码 | v23.02

    百篇博客系列篇.本篇为: v23.xx 鸿蒙内核源码分析(汇编传参篇) | 如何传递复杂的参数 | 51.c.h .o 硬件架构相关篇为: v22.xx 鸿蒙内核源码分析(汇编基础篇) | CPU在哪 ...

  4. 鸿蒙内核源码分析(用栈方式篇) | 程序运行场地谁提供的 | 百篇博客分析OpenHarmony源码 | v20.04

    百篇博客系列篇.本篇为: v20.xx 鸿蒙内核源码分析(用栈方式篇) | 程序运行场地谁提供的 | 51.c.h .o 精读内核源码就绕不过汇编语言,鸿蒙内核有6个汇编文件,读不懂它们就真的很难理解 ...

  5. 鸿蒙内核源码分析(内存主奴篇) | 皇上和奴才如何相处 | 百篇博客分析OpenHarmony源码 | v10.04

    百篇博客系列篇.本篇为: v10.xx 鸿蒙内核源码分析(内存主奴篇) | 皇上和奴才如何相处 | 51.c.h .o 前因后果相关篇为: v08.xx 鸿蒙内核源码分析(总目录) | 百万汉字注解 ...

  6. MyBatis源码分析之环境准备篇

    前言 之前一段时间写了[Spring源码分析]系列的文章,感觉对Spring的原理及使用各方面都掌握了不少,趁热打铁,开始下一个系列的文章[MyBatis源码分析],在[MyBatis源码分析]文章的 ...

  7. v80.01 鸿蒙内核源码分析(内核态锁篇) | 如何实现快锁Futex(下) | 百篇博客分析OpenHarmony源码

    百篇博客分析|本篇为:(内核态锁篇) | 如何实现快锁Futex(下) 进程通讯相关篇为: v26.08 鸿蒙内核源码分析(自旋锁) | 当立贞节牌坊的好同志 v27.05 鸿蒙内核源码分析(互斥锁) ...

  8. spring源码分析之spring-core总结篇

    1.spring-core概览 spring-core是spring框架的基石,它为spring框架提供了基础的支持. spring-core从源码上看,分为6个package,分别是asm,cgli ...

  9. string源码分析 ——转载 http://blogs.360.cn/360cloud/2012/11/26/linux-gcc-stl-string-in-depth/

    1. 问题提出 最近在我们的项目当中,出现了两次与使用string相关的问题. 1.1. 问题1:新代码引入的Bug 前一段时间有一个老项目来一个新需求,我们新增了一些代码逻辑来处理这个新需求.测试阶 ...

随机推荐

  1. Hibernate 中批量处理数据

    一.批量处理操作 批量处理数据是指在一个事务场景中处理大量数据.在应用程序中难以避免进行批量操作,Hibernate提供了以下方式进行批量处理数据: (1)使用HQL进行批量操作     数据库层面 ...

  2. non-JRMP server at remote endpoint

    #在相应的domain的domain.xml文件添加下面红色设置,并重启domain <admin-service system-jmx-connector-name="system& ...

  3. (3)zabbix用户管理

    登陆zabbix 默认账号:Admin,密码:zabbix,这是一个超级管理员.登陆之后在右下角可以看到“connected as Admin“(中文版:连接为Admin). zabbix组介绍 我们 ...

  4. Redis数据库(二)

    1. Redis数据库持久化 redis提供了两种数据备份方式,一种是RDB,另外一种是AOF,以下将详细介绍这两种备份策略. 面试: 1.1  配置文件详解备份方式 [root@localhost ...

  5. 纯 CSS 创作一个表达怀念童年心情的条纹彩虹心特效

    效果预览 在线演示 按下右侧的"点击预览"按钮可以在当前页面预览,点击链接可以全屏预览. https://codepen.io/comehope/pen/QxbmxJ 可交互视频教 ...

  6. 我的Python分析成长之路2

    2018-12-29 一.python数据类型: 1.数字 int(整形) float(浮点型) complex(复数型) 2.布尔值(bool)     真或假 True or False 3.字符 ...

  7. python基础——9(迭代器、生成器)

    一.迭代器 1.概念 器:包含了多个值的容器 迭代:循环反馈(一次从容器中取出一个值) 迭代器:从装有多个值的容器中一次取出一个值给外界 s = 'abcdef' ls = [1,2,3,4,5] 遍 ...

  8. HTML5 移动端web

    概述 HTML5 提供了很多新的功能,主要有: 新的 HTML 元素,例如 section, nav, header, footer, article 等 用于绘画的 Canvas 元素 用于多媒体播 ...

  9. luogu3755 [CQOI2017]老C的任务

    扫描线水题. #include <algorithm> #include <iostream> #include <cstdio> using namespace ...

  10. IOS 自动布局-UIStackPanel和UIGridPanel(三)

    在这一篇了我将继续讲解UIGridPanel. 在iphone的app里面可以经常看到一些九宫格布局的应用,做过html开发的对这类布局应该是很熟悉的.在IOS中要实现这样的布局方法还是蛮多的,但是我 ...