java基础之String漫谈(一)

1. 导读

String类也是日常开发中经常用到的类, 今天主要分享下我在看String源码时想到的4个问题:
1.1 String为什么是不可变的; 为什么要设计成不可变的;
1.2 hashCode; 为什么是31;

2. String为什么是不可变的;

    public final class String
    implements java.io.Serializable, Comparable, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

这是String源码的前三行代码, 这三行代码给出四个关键信息:
.1 String类是被final修饰的, 不可被继承;
.2 String字符串的底层实现是基于char数组的;
.3 char数组也是被final修饰的, 他的引用是不可被修改的;
.4 value[]被private修饰, 寻遍整个String类, 没有提供value[]公共的getter 和 setter方法, 那么
他的值是不可被外部修改的;
基于以上四点, 我们可以找到String是不可变的原因: String类不可被继承, 那么就不存在引用到StringChild这种子类, 换言之任何一个声明的String类型引用, 他引用到的对象一定是String对象, 而不会是其他对象;
当一个String对象被创建时, 其底层的value[]被赋值, 被final修饰, 其引用始终指向同一个内存地址, 并且String没有提供修改value的方法, 可以认为value[]数组的值是不可被更改的(如果你非要说通过反射可以修改, 那我只能说反射这种方式不在正常的考虑范围内);

划重点:
.1 基于String整个类, 被final修饰不可继承, 那么String类就是不可变类;
.2 基于String对象, 底层的value[]被final修饰, 不对外提供修改value[]的方法, value[]引用不变其值不变, 那么整个String对象也就是不可变的;

3. String为什么设计成不可变

上面说明了为什么String是不可变的, 但是为什么要把String设计成不可变的呢? 说不准有人需要个性化定制呢?

我想了下, String类的作者可能有已下几方面的考虑:
.1 线程安全: 我们都知道因为共享变量可能会被多个线程修改, 会引起线程安全问题, 把String设计成不可变以后, 就不需要考虑多线程的安全性了, 因为每个线程只有读的权限;

.2 关注下源码中的hash:

    /** Cache the hash code for the string */
    private int hash; // Default to 0

hash的作用是将当前String对象的hash值缓存起来, 因为String是不可变的, 因而缓存的hash也是不变的; 那么把hash起来有啥用呢? 举个HashMap的例子, 当String作为HashMap的key时, 多次调用String::hashCode时, 只会在第一次计算hash, 后面所有的调用都会取这个Key缓存的hash, 大大提高了效率;

.3 在JVM设计时, 对String有个优化处理:设计了一个String常量池, “”声明的String对象会先去常量池寻找, 不存在则放入常量池, 反之则取常量池中的String对象;假设10K个"str"的引用, 不需要在堆中new 10K个String对象, 而只需要将引用指向同一个String对象即可;
String::intern方法做的也是同样的事情, 关于JVM对这种设计的变迁以及优缺点放到分享intern方法时再说;

.4 在使用int时, 我们是去考虑int是否可变的, 因为在设计时, int类型就已经具有了不可变性了, 所以在设计Integer这个封装类时, Integer也被设计成了不可变类(后面会有专门分享, 这里不展开);
而String在java世界中的受欢迎程度和基本数据类型没什么差别, 甚至一些java的底层设计都是基于String的, 比如反射的类路径等等; 而且想像一下前一秒还在对String判空, 下一秒调用时抛出了NPE, 这种时候你会不会问候下String的设计者;
从语言设计层面考虑, String在设计之初就是希望把他当做基本数据类型来使用, 那么不可变性是必须的;

.5 在java的其他设计中也使用了String, 还是拿HashMap举例, 有个节点A的key是"1", value是2; 假设String是可变的, A的key变成了"2", 这时插入("2", 1)的数据, 会将原本的数据覆盖, 但根据插入之初的值, 应该新增一个节点的;
甚至HashMap中有两个节点A("1", 2), B("3", 1), 假设String可变, A节点变成了("3", 2); 使用"1"去获取时发现数据不存在, 使用"3"获取时得到的是错误数据;
上述两种情况明显违背了HashMap设计的初衷, HashMap希望将数据缓存起来, 再次获取时可以取到正确的值, 这是基于key的不可变实现的; 所以了世界的和平, String的设计者就打断熊孩子的念想, 把String设计成了不可变;
划重点:
.1 String在JAVA语言设计时就约定了不可变;
.2 String的不可变性保证了线程安全;
.3 String的不可变性提供了不变的hash, 实现了hash的懒加载, 提升了效率;
.4 JVM的String常量池是基于String的不可变性实现的;
.5 String的不可变性维护了世界的和平, 防止JAVA底层的一些设计出问题(也有可能String的作者觉得自己的设计非常棒, 防止艺术被污染, 把String设计成了不可变);

4. hashCode以及为什么是31

    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;
    }

通过hashCode的源码, 我们可以看到:
.1 这是对Object::hashCode的重写;
.2 实现了懒加载, 只有在第一次调用时才会计算String对象的hash值, 往后所有的调用都会返回缓存的hash值;
.3 String::hashCode的算法是:
value[0]31^(n-1) + value[1]31^(n-2) + ... + valuen-1;
.4 31? String::hashCode的算法中31十分显眼, 在看源码的时候会想为什么用31?
4.1 31*h可以被JVM优化成 (h << 5) - h; 众所周知, JAVA中的位运算的速度比乘法运算要快; 这是一个效率优化的考虑; 那么这又引出了另一个问题: 63 或者 15都可以被JVM优化, 还是没有解答为什么选用31;
4.2 String的hashCode其实用到了一个字符串散列化的算法----djb2; 这是由Daniel J. Bernstein提出字符串散列算法; 感兴趣的可以去
wiki;

这个算法用大白话讲就是字符串不断得乘33, 所以又被叫做"Time33"算法;看到这里, 是不是发现String::hashCode只是把33替换成了31, 本质还是"Time33";

划重点:
.1 String::hashCode同一个String对象只会计算一次hash;
.2 使用了"Time33"算法来重写Object::hashCode;

好了本次分享就到这里, 上面表述有什么问题, 欢迎指正; 若还有String的问题也欢迎一起探讨, 感谢阅读;

你可能感兴趣的:(java基础之String漫谈(一))