String不得不知

参考文章

cm's blogs

String

String并不是基本数据类型,而是一个不可变的对象

””、null、new String()

  • null 表示string还没有new ,也就是说对象的引用还没有创建,也没有分配内存空间

  • ””、new String()则说明了已经new了,只不过内部为空,但是它创建了对象的引用,且分配内存空间的。

  • 一个空玻璃杯,你不能说它里面什么都没有,因为里面有空气,当然也可以把它弄成真空,null与" "、new String()的区别就象真空与空气一样。

字符串池

  • 创建一个字符串对象时,首先检查字符串池,如果有,则不再创建,直接返回引用

  • 若没有则创建然后放入到字符串池,并返回新建字符串引用。

  • 提高效率,减少了内存空间的占用 。

  • 所以在使用字符串的过程中,推荐使用直接赋值(即String s=”aa”),而非 new String(”aa”)。

String和StringBuffer

  • String和StringBuffer,它们可以储存和操作字符串,即包含多个字符的字符数据。

  • String类表示内容不可改变的字符串。而StringBuffer类表示内容可以被修改的字符串。

  • 当你知道字符数据要改变的时候你就可以使用StringBuffer。

StringBuffer与StringBuilder

  • StringBuffer和StringBuilder类都表示内容可以被修改的字符串,StringBuilder是线程不安全的,运行效率高

  • 如果一个字符串变量是在方法里面定义,这种情况只可能有一个线程访问它,不存在不安全的因素了,则用StringBuilder。

  • 如果要在类里面定义成员变量,并且这个类的实例对象会在多线程环境下使用,那么最好用StringBuffer。

你应该知道的不可变

Q: String s = "Hello";s = s + " world!";这两行代码执行后,原始的String对象中的内容到底变了没有?

A: 没有。因为String被设计成不可变(immutable)类,所以它的所有对象都是不可变对象。在这段代码中,s原先指向一个String对象,内容是 "Hello",然后我们对s进行了+操作,那么s所指向的那个对象是否发生了改变呢?答案是没有。这时,s不指向原来那个对象了,而指向了另一个 String对象,内容为"Hello world!",原来那个对象还存在于内存之中,只是s这个引用变量不再指向它了。

Q: 是否可以继承String类?

A: String类是final类故不可以继承。

hashCode神奇的因子 “31”

先来看看hashcode()的源码

 /**
     * Returns a hash code for this string. The hash code for a
     * {@code String} object is computed as
     * <blockquote><pre>
     * s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
     * </pre></blockquote>
     * using {@code int} arithmetic, where {@code s[i]} is the
     * <i>i</i>th character of the string, {@code n} is the length of
     * the string, and {@code ^} indicates exponentiation.
     * (The hash value of the empty string is zero.)
     *
     * @return  a hash code value for this object.
     */
    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;
    }

公式推导

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

假设 数组长度 n=3
i=0 -> h = 31 * 0 + val[0]
i=1 -> h = 31 * (31 * 0 + val[0]) + val[1]
i=2 -> h = 31 * (31 * (31 * 0 + val[0]) + val[1]) + val[2]
h = 31x31x31x0 + 31x31xval[0] + 31xval[1] + val[2]
h = 31^(n-1)xval[0] + 31^(n-2)xval[1] + val[2]

网上流传的原因

  • 原文地址 为什么选择31作为乘子

  • 第一,31是一个不大不小的质数,是作为 hashCode 乘子的优选质数之一。另外一些相近的质数,比如37、41、43等等,也都是不错的选择。那么为啥偏偏选中了31呢?请看第二个原因。

  • 31可以被 JVM 优化,31 * i = (i << 5) - i

上面两个原因中,第一个需要解释一下,第二个比较简单,就不说了。下面我来解释第一个理由。一般在设计哈希算法时,会选择一个特殊的质数。至于为啥选择质数,我想应该是可以降低哈希算法的冲突率。至于原因,这个就要问数学家了,我几乎可以忽略的数学水平解释不了这个原因。上面说到,31是一个不大不小的质数,是优选乘子。那为啥同是质数的2和101(或者更大的质数)就不是优选乘子呢,分析如下。

这里先分析质数2。首先,假设 n = 6,然后把质数2和 n 带入上面的计算公式。并仅计算公式中次数最高的那一项,结果是2^5 = 32,是不是很小。所以这里可以断定,当字符串长度不是很长时,用质数2做为乘子算出的哈希值,数值不会很大。也就是说,哈希值会分布在一个较小的数值区间内,分布性不佳,最终可能会导致冲突率上升。

上面说了,质数2做为乘子会导致哈希值分布在一个较小区间内,那么如果用一个较大的大质数101会产生什么样的结果呢?根据上面的分析,我想大家应该可以猜出结果了。就是不用再担心哈希值会分布在一个小的区间内了,因为101^5 = 10,510,100,501。但是要注意的是,这个计算结果太大了。如果用 int 类型表示哈希值,结果会溢出,最终导致数值信息丢失。尽管数值信息丢失并不一定会导致冲突率上升,但是我们暂且先认为质数101(或者更大的质数)也不是很好的选择。最后,我们再来看看质数31的计算结果: 31^5 = 28629151,结果值相对于3210,510,100,501来说。是不是很nice,不大不小。

posted @ 2021-06-27 17:06  沉梦匠心  阅读(48)  评论(0)    收藏  举报