onlyliuxin / coding2017

218 stars 643 forks source link

为什么Java的String类要设计成final的 #507

Open kailuncen opened 7 years ago

kailuncen commented 7 years ago

大家可以探讨一下

hashRui commented 7 years ago

密码是string类型的吧 如果不设置成final 在传输的过程中可能会有安全性的问题 之前看过一篇博客有说到过

Kilien commented 7 years ago

https://www.zhihu.com/question/31345592

yangzhm commented 7 years ago

我也认为是为了安全,对于String来说,相同的字符串指向了同一块内存,如果不是final的其中一个修改,其他的都会被修改掉。

SJshenjian commented 7 years ago

在Map<String,Object>键值对,String实现了equals与hashCode方法,可以放心的用来做Key

SmilePoorCat commented 7 years ago

感觉String在java中就是一种特殊的基本类型,虽然是引用地址,但是每个String的地址唯一也就不像引用了,所以,如果没有个final限制,想想都可怕

KevinLanK commented 7 years ago

String的不可变,是从某种意义上弥补StringBuilder的缺陷吧,String本身是用StringBuilder实现的,然后Builder是线程非安全的,而对应的StringBuffer是线程安全的,使用Builder的原因可能是因为Builder的速度比Buffer快将近一倍,所以用Builder实现,然后用线程池并且不可变的方式弥补缺陷。当然,在反射面前,final也没有什么安全的。

kailuncen commented 7 years ago

补充一个自己的回答。

凯伦说,公众号ID: KailunTalk,努力写出最优质的技术文章,欢迎关注探讨。

1. 前言

最近看到几个有趣的关于Java核心类String的问题。

  1. String类是如何实现其不可变的特性的,设计成不可变的好处在哪里。
  2. 为什么不推荐使用+号的方式去形成新的字符串,推荐使用StringBuilder或者StringBuffer呢。

翻阅了网上的一些博客和stackoverflow,结合自己的理解做一个汇总。

2. String类是如何实现不可变的

String类的一大特点,就是使用Final类修饰符。

A class can be declared final if its definition is complete and no subclasses are desired or required.

Because a final class never has any subclasses, the methods of a final class are never overridden .

Java SE 7 官方手册中的定义如上,如果你认为这个类已经定义完全并且不需要任何子类的话,可以将这个类声明为Final,Final类中的方法将永远不会被重写。

在Java中,String是被设计成一个不可变(immutable)类,一旦创建完后,字符串本身是无法通过正常手段被修改的。

private final char value[];      // 一旦初始化后,引用不能被修改

public String substring(int beginIndex, int endIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        if (endIndex > value.length) {
            throw new StringIndexOutOfBoundsException(endIndex);
        }
        int subLen = endIndex - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        return ((beginIndex == 0) && (endIndex == value.length)) ? this
                : new String(value, beginIndex, subLen);
    }

选了substring方法来做一个代表,其他常见的涉及String操作的方法都是类似,如果你操作后的内容会和目前String中的内容不一致的话,那么都是重新创建一个新的String类返还,不会让你去修改内部的内容。

将String类设计成Final类,能够避免其方法被子类重写,从而破坏了它本身方法的实现,进而破坏了不可变的特性。

2.1 String类设计成不可变的好处

我们都不是Java语言的设计者,不知道其为何一定要设计成不可变,试着做一些猜想。

  1. 可以实现多个变量引用JVM内存中的同一个字符串实例。见后文String Pool的介绍。
  2. 安全性,String类的用途实在太广了,如果可以随意修改的,是不是很恐怖。
  3. 性能,String大量运用在哈希的处理中,由于String的不可变性,可以只计算一次哈希值,然后缓存在内部,后续直接取就好了。如果String类是可变的话,在进行哈希处理的时候,需要进行大量的哈希值的重新计算。

这是结合个人理解和stackoverflow上看的汇总,我们来看看Java语言的爸爸James Gosling是怎么说的。

From a strategic point of view, they tend to more often be trouble free. And there are usually things you can do with immutables that you can't do with mutable things, such as cache the result. If you pass a string to a file open method, or if you pass a string to a constructor for a label in a user interface, in some APIs (like in lots of the Windows APIs) you pass in an array of characters. The receiver of that object really has to copy it, because they don't know anything about the storage lifetime of it. And they don't know what's happening to the object, whether it is being changed under their feet.

You end up getting almost forced to replicate the object because you don't know whether or not you get to own it. And one of the nice things about immutable objects is that the answer is, "Yeah, of course you do." Because the question of ownership, who has the right to change it, doesn't exist.

One of the things that forced Strings to be immutable was security. You have a file open method. You pass a String to it. And then it's doing all kind of authentication checks before it gets around to doing the OS call. If you manage to do something that effectively mutated the String, after the security check and before the OS call, then boom, you're in. But Strings are immutable, so that kind of attack doesn't work. That precise example is what really demanded that Strings be immutable.

这是James Gosling在2001年5月的一次访谈中,谈到了不可变类和String,大意就是 他会更倾向于使用不可变类,它能够缓存结果,当你在传参的时候,使用不可变类不需要去考虑谁可能会修改其内部的值,这个问题不存在的。如果使用可变类的话,可能需要每次记得重新拷贝出里面的值,性能会有一定的损失。

老爷子还说了,迫使String类设计成不可变的另一个原因是安全,当你在调用其他方法,比如调用一些系统级操作之前,可能会有一系列校验,如果是可变类的话,可能在你校验过后,其内部的值被改变了,可能引起严重的系统崩溃问题,这是迫使String类设计成不可变类的重要原因。

2.2 String Pool

上文说了,设计成不可变后,可以多个变量引用JVM上同一块地址,可以节省内存空间,相同的字符串不用重复占用Heap区域空间。

String test1 = "abc";
String test2 = "abc";

通常我们平时在使用字符串是,都是通过这种方式使用,那么JVM中的大致存储就是如下图所示。

两个变量同时引用了String Pool中的abc,如果String类是可变的话,也就不能存在String Pool这样的设计了。 在平时我们还会通过new关键字来生成String,那么新创建的String是否也会和上文中的示例一样共享同一个字符串地址呢。

        String test1 = "abc";
        String test2 = "abc";
        String test3 = new String("abc");

答案是不会,使用new关键字会在堆区在创建出一个字符串,所以使用new来创建字符串还是很浪费内存的,内存结构如下图所示。

2.3 不推荐使用+来拼装字符串的原因。

首先我们来看这一段代码,应该是之前写代码比较常见的。

String test1 = "abc";
String test2 = "abc";
String test3 = test1 + test2;

test3通过test1和test2拼接而成,我们看一下这个过程中的字节码。

从以上图我们可以看到,目前的JDK7的做法是,会通过新建StringBuilder的方式来完成这个+号的操作。这是目前的一个底层字节码的实现,那么是不是没有使用StringBuilder或者StringBuffer的必要了呢。还是有的,看下一个例子。

String test2 = "abc";
String test3 = "abc";

for (int i = 0; i < 5; i++) {
    test3 += test2;
}

在上述代码中,我们还是使用+号进行拼接,但这次我们加了一个循环,看一下字节码有什么变化。 

每次循环都会创建一个StringBuilder,在末尾再调用toString返还回去,效率很低。继续看下一个例子,我们直接使用StringBuilder,来做拼接。

String test2 = "abc";
// 使用StringBuilder进行拼接
StringBuilder test4 = new StringBuilder("abc");
for (int i = 0; i < 5; i++) {
    test4.append(test2);
}

每次循环体中只会调用之前创建的StringBuilder的append方法进行拼接,效率大大提高。

至于StringBuilder 的内部实现,诸位有兴趣可以自己再去看一下,本质上也是一个char数组上的操作,和StringBuffer的区别在于,StringBuffer是有做同步处理的,而StringBuilder没有。

3. 总结

本文主要探讨了String类设计为Final修饰和不可变类的原因,以及为何在日常工作中不推荐使用+号进行字符串拼接。

BlindingDark commented 7 years ago

将方法或类声明为final主要目的是:确保它们不会再子类中改变语义。String类是final类,这意味着不允许任何人定义String的子类。换言之,如果有一个String的引用,它引用的一定是一个String对象,而不可能是其他类的对象。——《Java核心技术 卷I》

作者:知乎用户 链接:https://www.zhihu.com/question/31345592/answer/51639967 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

引用一段。这段说的很清楚了。final类,意味着不允许你去对 String 进行瞎搞。为啥不让你瞎搞?因为瞎搞能改变大家都作为默认用法的东西,就完蛋了。

一个好的设计思想应该是简洁的。如果连这种问题也需要长篇大论,那真的是本末倒置。

(虽然我也在长篇大论23333) 如果上面的话还是不能帮助你理解,那么我可以用一些简洁的的代码来说明,相信你看了就会明白。

假如你现在写了一个方法,判断字符串是不是为 "yes",如果是就打印出来。

void myPrint(String someString) {
    if ("yes".equals(someString)) {
        System.out.print(someString);
    }
}

这个方法在各种情况下都没问题是吧。

假如从明天开始 String 不再是 final,那么熊孩子就可以这么搞。

class MyString extends String {
    private char value[];
    public String(String original) {
        this.value = null; // 重写构造方法,使得 equals 失效
        this.hash = -1111;
    }
}

// ... main
    String myString = new MyString("yes");
    myPrint(myString); // false
//

这不就全乱套了。 这还叫好的。如果他再来一个多线程……


String myString = new MyString("no");

new Thread(new Runnable() {
    @Override
    public void run() {
        myPrint(myString); // "能不能打印出来看心情"
    }
}).start();

myString.value = "yes".toCharArray(); // 因为这里直接改了值。

你 debug 吧。能气死你。

所以,设计成 final 是不让你搞一个“可变的 String”或者“奇怪的 String”,你不能继承,就不能改写,一劳永逸。 最后如果所有的东西都是不可变的,就算你继承重写还是不可变的,那该多好?反正我用了只有不可变值的语言之后是回不去了。