本文基于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. 题解 P5082 【成绩】

    随机跳题跳到了这一题,一看是个红题,本蒟蒻就 艰难地思考起来 高兴地写起来 这题实在不能用数组,用了数组就RE 一开始就卡在这上面了 说实话,这道题真的 很难 不算很难,只要照着公式往上面套就行了 废 ...

  2. Dubbo服务的搭建

    dubbo框架主要作用是基于RPC的远程调用服务管理,但是注册中心是用的zookeeper,搭建dubbo,首先要安装zookeeper,配置zookeeper... 实现功能如图所示:(存在2个系统 ...

  3. 【dp】拔河比赛

    01背包:感谢ZCK大佬 题目描述 学校举行拔河比赛,所有的人被分成了两组,每个人必须(且只能够)在其中的一组,要求两个组的人数相差不能超过1,且两个组内的所有人体重加起来尽可能地接近. 输入 输入中 ...

  4. axure笔记--变量值在页面之间的传递

    fx     先给局部变量赋值,再添加到上面,即给全局变量赋值. 实现页面跳转: 1.打开链接,选择要跳转的下个页面---确定 2.打开那个下一个跳转的页面,要得到上个页面的值,需要到页面交互---页 ...

  5. C++代码学习之一:组合模式例子

    #include"AbstractFile.h" void AbstractFile::add(AbstractFile*) { } void AbstractFile::remo ...

  6. mysql启动错误排查-无法申请足够内存

    一般情况下mysql的启动错误还是很容易排查的,但是今天我们就来说一下不一般的情况.拿到一台服务器,安装完mysql后进行启动,启动错误如下: 有同学会说,哥们儿你是不是buffer pool设置太大 ...

  7. ORACLE 检索某列包含特定字符串的数据表工具存储过程

    使用示例: delete APPS.FIND_RESULT; set serveroutput ondeclare     v_ret varchar(200);begin     apps.sp_f ...

  8. sqlserver查看死锁进程工具脚本p_lockinfo

    /* -- 处理死锁 -- 查看当前进程,或死锁进程,并能自动杀掉死进程 -- 因为是针对死的,所以如果有死锁进程,只能查看死锁进程 -- 当然,你可以通过参数控制,不管有没有死锁,都只查看死锁进程 ...

  9. C#学习基础概念二十五问

    C#学习基础概念二十五问 1.静态变量和非静态变量的区别?2.const 和 static readonly 区别?3.extern 是什么意思?4.abstract 是什么意思?5.internal ...

  10. 检测SQLserver数据库链接是否正常

    select * From [数据库链接名].master.dbo.sysdatabases where name='数据库名' and status<>512