【探究为什么String类是不可变类型:String类仿写】
01.介绍
我们都知道String类是不可变类型,但很少人思考为什么它是不可变类型,然后我抱着这个想法去搜索了一下
搜索结果
1.因为String类是被final修饰的2.因为String类里的 char value[],是被value修饰的
其中大多数都是说因为char value[] 加了final 所以String类是不可变类型
这个是String源代码
可以看出确实如上面描述的那样,可是我依然不理解为什么这样就是不可变类型,可能是我比较愚笨,所以采用了最朴素的方法来证明它是不可变的,那就是仿写一个,看看加不加final的区别
02.仿写
class 与 value都不加final
class A{
private char[] val;
public A(){
this.val=new A(new char[]{' '}).val;
}
public A(char[] ss){
this.val=ss;
}
public A(A ss){
this.val=ss.val;
}
public void change(A ss){
this.val=ss.val;
}
public char[] tostring() {
return this.val;
}
}
public class TestString {
public static void main(String[] args) {
A b = new A(new char[]{'s'});
A a = new A(b);
test1(a);
System.out.println(a.tostring());
}
static void test1(A a){
A b = new A(new char[]{'a','b'});
a.change(b);
}
}
这里我们那 A类模拟String类,val数组模拟value数组,change()方法模拟 String直接赋值,tostring方法模拟toString方法
结果
ab
解释
在我们class与val都不加final的情况下,我们先创建了一个 A对象a并附初值为's',然后我们调用test1()方法 传入对象a 然后我们改变对象a为"ab",这一步我们模拟的是 String a = "s",调用方法test1() 然后a="ab",我们都知道这时我们打印 主函数的String a对象结果肯定是"s" 因为是String是不可变类型,但这里我们 A类a对象 则打印了ab 说明此时A类是可变类型
val加final,class不加final
class A{
final private char[] val;
public A(){
this.val=new A(new char[]{' '}).val;
}
public A(char[] ss){
this.val=ss;
}
public A(A ss){
this.val=ss.val;
}
// public void change(A ss){
// this.val=ss.val;
//// return new A(ss); //注意:此时这段代码报错 因为此时val不能再被改变 所以不能用this.val
// }
public A change(A ss){
return new A(ss); //改用这个代码
}
public char[] tostring() {
return this.val;
}
}
public class TestString {
public static void main(String[] args) {
A b = new A(new char[]{'s'});
A a = new A(b);
test1(a);
System.out.println(a.tostring());
}
static void test1(A a){
A b = new A(new char[]{'a','b'});
a = a.change(b);
}
}
结果
s
解释
这里我们把val用final修饰了 也就代表此时val数组的地址不能再被改变 所以不能用 this.val,所以我们此时change()方法的策略是 当赋新值时 返回一个新的A类,所以我们的test1()方法里的a = a.change(b);这个代码等价于 a="ab",我们再来看此时的结果,发现确实 现在输出的值是 's'了 现在是不可变类型了。
这时我才你会想问那么 对class加final的作用是什么?下面来解释
val加final,class不加final 会出现的问题
class A{
final private char[] val;
public A(){
this.val=new A(new char[]{' '}).val;
}
public A(char[] ss){
this.val=ss;
}
public A(A ss){
this.val=ss.val;
}
public A change(A ss){
return new A(ss);
}
public A toUp(A ss){
for (int i=0;i<ss.val.length;i++) {
if (97<=ss.val[i]&&ss.val[i]<=122){
ss.val[i]= (char) (ss.val[i]-32);
}
}
return ss;
}
public char[] tostring() {
return this.val;
}
}
class B extends A{
final private char[] val;
public B(){
this.val=new B(new char[]{' '}).val;
}
public B(char[] ss){
this.val=ss;
}
public B(B ss){
this.val=ss.val;
}
@Override
public A toUp(A ss){
return ss;
}
@Override
public char[] tostring() {
return this.val;
}
}
public class TestString {
public static void main(String[] args) {
// A a = new A(new char[]{'s','a'});
// A aa = a; // 没使用多态
// aa = aa.toUp(aa);
// System.out.println(aa.tostring());
B b = new B(new char[]{'s','a'});
A aa = b; // 使用多态
aa = aa.toUp(aa);
System.out.println(aa.tostring());
}
}
toUp方法 是把小写字母变为大写,但是我们在A的子类B中重写这个方法 使其直接返回 也就是使这个方法失效
结果
SA 没使用多态的答案sa 使用多态的答案
可以看出 此时我们创建了一个子类B,并且使用多态 成功改变了父类A中的改变大小写的方法
解释
如果我们不对类加final,则说明此类的可以继承,也就是有子类,有子类就说明可以重写父类方法,而此时我们用多态 把子类对象赋值给父类对象,此时父类对象调用被重写的方法 就会导致 父类原有法方法被改变,就像这个例子里的toUp方法,很显然我们不希望出现这种事,所以String类 用final修饰class是很有必要的。
03.总结
综上我们明白了为什么 java类是不可变类型 且为什么 java类与java的成员变量value为什么一定要用final修饰。
再来谈谈这样实现的其他好处
第一个好处:String类的设计还可以保证线程安全,首先因为成员变量 value被final修饰 也就是不可变 所以多个线程操作公共String不会出现线程安全问题,你可能会问 String类型有replace,substring 等方法可以改变值啊,但其实不会,因为这些方法和我上面举例的toUp()方法一样会返回一个新的String对象 也就不可能出现 线程不安全的情况。
第二个好处:因为string类在java中被大量应用,所以它的内存占用优化就显得很重要,而string的不可变性才使得jvm可以实现字符串常量池,创建String对象之前 jvm会先检查字符串常量池中是否存在该对象,若存在则直接返回其引用,否则新建一个对象并缓存进常量池,再返回其引用。
第三个好处:因为==private final char value[]==中final修饰的是字符数组,也就是final只是限制了其引用 并没有限制其值 所以我们可以对value里的值进行修改,进而实现了如replace,substring等方法,同时用private修饰 也防止了在类外 String对象直接操作value[]数组。