String类为什么是final的

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

 

 

大白话解释就是:String很多实用的特性,比如说“不可变性”,是工程师精心设计的艺术品!艺术品易碎!用final就是拒绝继承,防止世界被熊孩子破坏,维护世界和平!

1. 什么是不可变?
String不可变很简单,如下图,给一个已有字符串"abcd"第二次赋值成"abcedl",不是在原内存地址上修改数据,而是重新指向一个新对象,新地址。

 

2. String为什么不可变?
翻开JDK源码,java.lang.String类起手前三行,是这样写的:

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    /** String本质是个char数组. 而且用final关键字修饰.*/
    private final char value[];
	...
	...
}

首先String类是用final关键字修饰,这说明String不可继承。再看下面,String类的主力成员字段value是个char[ ]数组,而且是用final修饰的。final修饰的字段创建以后就不可改变。

有的人以为故事就这样完了,其实没有。因为虽然value是不可变,也只是value这个引用地址不可变。挡不住Array数组是可变的事实。Array的数据结构看下图,


也就是说Array变量只是stack上的一个引用,数组的本体结构在heap堆。String类里的value用final修饰,只是说stack里的这个叫value的引用地址不可变。没有说堆里array本身数据不可变。看下面这个例子,

final int[] value={1,2,3}
int[] another={4,5,6};
value=another;    //编译器报错,final不可变

value用final修饰,编译器不允许我把value指向堆区另一个地址。但如果我直接对数组元素动手,分分钟搞定。

final int[] value={1,2,3};
value[2]=100;  //这时候数组里已经是{1,2,100}

或者 @刘硕 评论说喜欢更粗暴的反射直接改,也是可以的。

final int[] array={1,2,3};
Array.set(array,2,100); //数组也被改成{1,2,100}

 

所以String是不可变,关键是因为SUN公司的工程师,在后面所有String的方法里很小心的没有去动Array里的元素,没有暴露内部成员字段。private final char value[]这一句里,private的私有访问权限的作用都比final大。而且设计师还很小心地把整个String设成final禁止继承,避免被其他人继承后破坏。所以String是不可变的关键都在底层的实现,而不是一个final。考验的是工程师构造数据类型,封装数据的功力。

3. 不可变有什么好处?
这个最简单地原因,就是为了安全。看下面这个场景(有评论反应例子不够清楚,现在完整地写出来),一个函数appendStr( )在不可变的String参数后面加上一段“bbb”后返回。appendSb( )负责在可变的StringBuilder后面加“bbb”。

class Test{
    //不可变的String
    public static String appendStr(String s){
        s+="bbb";
        return s;
    }

    //可变的StringBuilder
    public static StringBuilder appendSb(StringBuilder sb){
        return sb.append("bbb");
    }

    public static void main(String[] args){
        //String做参数
        String s=new String("aaa");
        String ns=Test.appendStr(s);
        System.out.println("String aaa >>> "+s.toString());

        //StringBuilder做参数
        StringBuilder sb=new StringBuilder("aaa");
        StringBuilder nsb=Test.appendSb(sb);
        System.out.println("StringBuilder aaa >>> "+sb.toString());
    }
}

//Output: 
//String aaa >>> aaa
//StringBuilder aaa >>> aaabbb 

如果程序员不小心像上面例子里,直接在传进来的参数上加"bbb",因为Java对象参数传的是引用,所以可变的的StringBuffer参数就被改变了。可以看到变量sb在Test.appendSb(sb)操作之后,就变成了"aaabbb"。有的时候这可能不是程序员的本意。所以String不可变的安全性就体现在这里。

再看下面这个HashSet用StringBuilder做元素的场景,问题就更严重了,而且更隐蔽。

class Test{
    public static void main(String[] args){
        HashSet<StringBuilder> hs=new HashSet<StringBuilder>();
        StringBuilder sb1=new StringBuilder("aaa");
        StringBuilder sb2=new StringBuilder("aaabbb");
        hs.add(sb1);
        hs.add(sb2);    //这时候HashSet里是{"aaa","aaabbb"}

        StringBuilder sb3=sb1;
        sb3.append("bbb");  //这时候HashSet里是{"aaabbb","aaabbb"}
        System.out.println(hs);
    }
}
//Output:
//[aaabbb, aaabbb]

StringBuilder型变量sb1和sb2分别指向了堆内的字面量"aaa"和"aaabbb"。把他们都插入一个HashSet。到这一步没问题。但如果后面我把变量sb3也指向sb1的地址,再改变sb3的值,因为StringBuilder没有不可变性的保护,sb3直接在原先"aaa"的地址上改。导致sb1的值也变了。这时候,HashSet上就出现了两个相等的键值"aaabbb"。破坏了HashSet键值的唯一性。所以千万不要用可变类型做HashMap和HashSet键值。

还有一个大家都知道,就是在并发场景下,多个线程同时读一个资源,是不会引发竟态条件的。只有对资源做写操作才有危险。不可变对象不能被写,所以线程安全

最后别忘了String另外一个字符串常量池的属性。像下面这样字符串one和two都用字面量"something"赋值。它们其实都指向同一个内存地址。

String one = "someString";
String two = "someString";

 


这样在大量使用字符串的情况下,可以节省内存空间,提高效率。但之所以能实现这个特性,String的不可变性是最基本的一个必要条件。要是内存里字符串内容能改来改去,这么做就完全没有意义了。

 

------------------------------------------------------
我的笔记栈 http://ciaoshen.com (笔记向,非教程)

<think>嗯,用户问为什么Java的Stringfinal的,还有String的存储大小限制。我需要先理解这两个问题,然后根据提供的引用资料来回答。 首先,关于String被设计为final的原因。我记得引用[1]和[2]提到过,Stringfinal的,不能被继承。这样做的好处可能有安全性,比如防止子覆盖方法导致不可预期的行为。比如哈希表之的数据结构依赖String的不可变性,如果String可变,可能会破坏哈希表的键值。另外,不可变性还能让字符串常量池有效工作,多个引用可以共享同一个字符串,节省内存。还有线程安全的问题,因为不可变对象天生是线程安全的,不需要同步。 然后关于存储大小限制。引用[2]提到在JDK1.9之前是char数组,之后改为byte数组加编码标记。不过数组的最大长度应该是Integer.MAX_VALUE,也就是大约2^31-1,但实际可能受虚拟机限制,比如最大数组长度可能稍小一些,比如有的JVM实现可能限制为Integer.MAX_VALUE - 8。另外,String内部使用byte数组,如果使用Latin1编码每个字符占1字节,而UTF-16的话可能占2或4字节,这会影响实际存储的字符数量。不过用户可能想道的是理论上String能有多长,比如字符数,但实际可能会抛出OutOfMemoryError,或者超过数组长度限制的错误。 需要确认引用中的信息。引用[1]说从JDK9开始用byte数组加编码标记,引用[2]中的代码示例显示char数组,但可能是在JDK8的情况。因此,存储结构的变化可能影响存储大小,但基本还是受数组长度限制。 可能还需要补充一些例子,比如当尝试创建非常大的字符串时,可能会遇到内存不足的问题,或者超过数组长度限制的错误,比如抛出java.lang.OutOfMemoryError: Java heap space或者java.lang.NegativeArraySizeException之的异常。 然后用户的问题可能还涉及到设计决策的原因,比如为什么选择用final,除了安全性、不可变性的好处,还有性能优化,比如字符串常量池的存在,因为字符串不可变,所以可以缓存哈希值,提高效率。 总结下来,回答的结构应该是先解释为什么Stringfinal,列举几个主要原因,每个原因结合引用中的内容。然后解释存储大小限制,包括理论上的数组长度限制和实际中的内存限制,可能还要提到不同JDK版本的变化。</think>### Java String设计为final的原因及存储大小限制分析 #### 一、String设计为final的主要原因 1. **安全性保障** String作为Java核心,广泛用于文件路径、网络连接、哈希表键值等场景。若可被继承,恶意子可能通过覆写方法破坏安全性,例如在`equals()`中注入非法逻辑[^1][^4]。 2. **不可变性的核心实现** final修饰确保String的不可变性: - **线程安全**:无需同步即可多线程共享[^1][^4] - **哈希值缓存**:`hash`字段可预计算并复用,提高哈希表性能[^2] - **字符串常量池优化**:相同字面量共享同一内存,减少重复创建[^1][^4] 3. **性能优化基础** 不可变性使编译器可进行字符串常量折叠(如`"a"+"b"`优化为`"ab"`),同时避免因内容修改引发的意外行为[^1][^4]。 #### 二、字符串存储大小限制 1. **理论限制** String内部使用`byte[]`存储数据(JDK9+),数组最大长度为`Integer.MAX_VALUE - 8`(约21亿),实际限制取决于具体JVM实现[^1][^2]。 2. **实际限制** - **内存约束**:字符串占用内存需满足`byte[]长度 × 元素字节数 + 对象头开销`,超大字符串会触发`OutOfMemoryError` - **编码影响**:Latin1编码时每个字符占1字节,UTF-16编码时基本字符占2字节,扩展字符占4字节[^1] 3. **典型错误示例** ```java // 尝试创建超长字符串 char[] arr = new char[Integer.MAX_VALUE]; // 抛出java.lang.OutOfMemoryError String s = new String(arr); ``` #### 三、设计延伸验证 ```java // 验证字符串不可变性 String s1 = "Hello"; String s2 = s1.concat(" World"); System.out.println(s1); // 输出"Hello"(原值未改变) System.out.println(s2); // 输出"Hello World" ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值