抛砖引玉:为何java.lang.String被声明为final

面试曾被问及java.lang.String为何被声明为final,不假思索地回答,当然是为了不能让client改变String的内置方法,得到了否定的答案。确实,这仅仅是表象。查阅了资料,总结一下其根本原因。

文章目录

    • @[toc]
  • final关键字
  • 可变(mutable)还是不可变(immutable)
  • String特殊在哪
    • 性能优化
    • 线程安全
    • 安全性

final关键字

final——故名思议,即编译期间可以确定的可以确定的量,定义之后,这个值就不再改变,也就是我们常说的常量。表现就是,被final修饰的对象(实际是引用)不能重新指定引用,被final修饰的基本类型的值也不能被改变。

而被final修饰的class,表示其不可被继承,我们将不能被覆写(Override)其中的方法。同理,被final修饰的方法,也不能被覆写。

可变(mutable)还是不可变(immutable)

我们说一个Class是不是immutable,说的是其对象是否会改变实际的内容。没有不可变性的保护,这个对象在共享时会出现严重问题。

StringBuffer buffer = new StringBuffer("aaaa");
StringBuffer bufferBack = buffer;
bufferBack.append("bb");  //buffer所指的内容也改变了
String str = "aaaa";
String strBack = str;
strBack.concat("bbb");	  //str的内容没有变化

查看String的源码可以知道,实际存储String的是一个数组value[],这个引用被指定为final。

public final class String implements java.io.Serializable, Comparable, CharSequence {

    private final char value[];

    private int hash;
    
    ...
}

但是这个地址里的值我们却可以改变,从这个意义上来说,String是mutable的。这个在String源码里就有体现——Array.copyOf就是对数组进行内存复制的操作。这个操作,在堆上开辟了一个区域的空间并调用native方法System.arraycopy进行内存拷贝。所以,value[]所引用的内容是有可能被改变的。

String
public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
}

所以,String的设计哲学是immutable的,解读是String的实际内容只希望在需要时被改变,而需不需要由设计者来决定,或者说一旦内存分配后,内容就不再改变。SUN工程师在编写String时不会随意去改变上面这个value[]的内容,并且也不暴露内部细节,还小心翼翼地把String声明为final,防止其他人改变,维护了这个不可变性,这是为什么?下一节我们再讨论。

这里附上concat的源码,管中窥豹,可以略见String的设计哲学:

public String concat(String str) {
        int otherLen = str.length();
        if (otherLen == 0) {
            return this;
        }
        int len = value.length;
        //重新开辟了一片空间
        //并进行复制,保证了原来的地址空间没有被改变
        char buf[] = Arrays.copyOf(value, len + otherLen);
        str.getChars(buf, len);
        return new String(buf, true);
    }

String特殊在哪

试想如果我们可以继承String,并对内部的方法进行覆写,可以预见的是String的子类的行为将会是不可控的——至少我们无法要求这个derived class不去改变内部实际存储的字符数组value[],以及hashcode所使用到的hash。

性能优化

String的广泛使用,导致对它的性能优化是不可忽略的。

Now when you make a class immutable, you know in advance that, this class is not going to change once created. This guarantee open path for many performance optimization e.g. caching.

在性能上,由于immutable的保证,可以做很多文章,比如:

  1. 可以把hashCode()的计算结果直接cache下来,而不用每次都进行计算,毕竟String一经创建就不再改变。这在String作为key的HashMap中能带来可观的性能提升。

    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            //只在未计算过hash值时进行计算
            char val[] = value;
    
            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }
    
  2. 由于String是immutable的,在**常量池(constant pool)**中下面的语句才可能得以优化成共用一片内存区域:

    String a = "aaa";
    String b = "aaa";
    

线程安全

事实上不止是多线程共享,在对象的共享上,哪怕是单线程的,如果如第二节开头的例子中的StringBuffer类的行为一样,会出现很多令人迷惑的问题。毕竟在Java中,我们所使用的都是引用。为此,良好设计的String类,应该是immutable的。

Since it was expected that String will be used widely, making it Immutable means no external synchronization, means much cleaner code involving sharing of String between multiple threads. This single feature, makes already complicate, confusing and error prone concurrency coding much easier. Because String is immutable and we just share it between threads, it result in more readable code.

安全性

还有的观点提到,String作为很多重要的基础框架使用的类,它的行为应该是可以预测的,immutable就是一个极为有利的保证。

Another reason for making String final or Immutable was driven by the fact that it was heavily used in class loading mechanism. As String been not Immutable, an attacker can take advantage of this fact and a request to load standard Java classes e.g. java.io.Reader can be changed to malicious class com.unknown.DataStolenReader. By keeping String final and immutable, we can at least be sure that JVM is loading correct classes.

大致意思是可以防止攻击者篡改String的行为来影响JVM对类的加载。 试想,若String不是final,于是ClassLoader的一个方法接受了你改写的HackedString作为参数,而你的HackedString改写了一些行为,去动态地改变自己的值,然后ClassLoader却对此全然无知,这是多态的魅力,也是Hacker的最爱。举个例子吧:

public class HackedString extend String {
	...
	
	private void justChange() {
		char[] hackChars = "com.unkown.DataStolenReader".toCharArray();
		System.arraycopy(hackChars, 0, this.value, 0, hackChars.length());
	}
	
	@Override
	private void someMethodDerivedFromString() {
		justChange();
	}
}

public abstract class ClassLoader {
	...
	
	public Class loadClass(String name) {
		...
		//也许someMethodDerivedFromString()被调用了,
		//不幸的是, 它已经被篡改了
		className.someMethodDerivedFromString();
	}
}


String s = new HackedString("java.io.Reader");
new ClassLoad().loadClass(s); //并不会得到你想要的Reader

Java has clear goal in terms of providing a secure environment at every level of service and String is critical in those whole security stuff. String has been widely used as parameter for many Java classes, e.g. for opening network connection, you can pass host and port as String, for reading files in Java you can pass path of files and directory as String and for opening database connection, you can pass database URL as String. If String was not immutable, a user might have granted to access a particular file in system, but after authentication he can change the PATH to something else, this could cause serious security issues. Similarly, while connecting to database or any other machine in network, mutating String value can pose security threats. Mutable strings could also cause security problem in Reflection as well, as the parameters are strings.

这是说String在一些重要的类的大量使用,比如网络、IO,导致String一旦是mutable的,就会留下很多漏洞。总而言之,String的在各种JDK系统层面的API中大量运用,导致了其特殊地位,索性让其的行为固定下来,不能随意改变。

#总结
最后,总结一下:java.lang.String之所以是final,是因为设计者很小心翼翼地保证了String的不可变性,并且不想让其他人改变这个性质。看到一句话,说的也挺好的。

The string is VVS Laxman of Java, i.e. very very special class. I have not seen a single Java program which is written without using String. That’s why a solid understanding of String is very important for a Java developer.


[1]: 在java中String类为什么要设计成final? - 知乎讨论
[2]: Semantics of immutable in Java
[3]: Why String Class is final
[4]: Why String Class is made Immutable or Final in Java - 5 Reasons

你可能感兴趣的:(Java,Java,String,面试)