前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >震惊,99.9% 的同学没有真正理解字符串的不可变性

震惊,99.9% 的同学没有真正理解字符串的不可变性

作者头像
明明如月学长
发布2022-09-17 09:02:52
2370
发布2022-09-17 09:02:52
举报
文章被收录于专栏:明明如月的技术专栏
在这里插入图片描述
在这里插入图片描述

一、你以为的常识

1.1 不可变性的理解

稍有些基础的同学都知道 Java 中 String 字符串是“不可变”的,想要使用“可变字符串”可以使用 StringBuilderStringBuffer

大多数讲字符串不可变性的文章大同小异。

不可变的定义:

An immutable object is an object whose internal state remains constant after it has been entirely created. This means that once the object has been assigned to a variable, we can neither update the reference nor mutate the internal state by any means. – 《Why String is Immutable in Java?》 所谓不可变对象,即对象创建之后内部状态保持不变。换句话说,一旦对象被赋值给一个变量,将不再允许通过任何方式改变引用、修改内部状态。

1.2 不可变性的实现

String “不可变性”的保障:

  • (1) String 类被 final ,导致不继承;
  • (2) 存储 String 的字符的 char 数组为 final 则引用不可改变。
  • (3) 所有修改方法(如 concat)都会返回一个新的字符串对象。
代码语言:javascript
复制
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

//省略其他

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

}

1.3 不可变性的好处

1.3.1 节省内存

由于字符串的不可变性,不同的字符串变量可以引用同一个示例来实现节省堆内存的目的。

代码语言:javascript
复制
String s1 = "明明如月学长";
String s2 = "明明如月学长");
String s3 = new String("明明如月学长");
assertThat(s1 == s2).isTrue();
assertThat(s1 == s3).isFalse();
在这里插入图片描述
在这里插入图片描述

1.3.2 更安全

字符串的不可变性保证了安全性。 请看下面的示例代码,先执行参数的合法性检查,然后执行一些次要的认为,最后执行重要的任务:

代码语言:javascript
复制
void criticalMethod(String userName) {
    //1 执行安全检查
    if (!isAlphaNumeric(userName)) {
        throw new SecurityException(); 
    }
	
    //2 执行一些次要的任务
    initializeDatabase();
	
    //3 重要的任务
    connection.executeUpdate("UPDATE Customers SET Status = 'Active' " +
      " WHERE UserName = '" + userName + "'");
}

假如字符串可变,在第一步安全检查通过后字符串发生修改,代码运行可能出现不符合预期的结果,比如造成 SQL 注入等。

字符串的不可变性也保证了多线程访问时的现成安全性。

1.3.3 hashCode 缓存

大家可以看到 String 的 hashCode 的计算和构成字符串的字符有关,由于 String 的不可变性就可以将 hashCode 缓存起来。源码中也可以看出计算过之后,下次调用 hashCode 直接返回。

代码语言:javascript
复制
 /**
     * Returns a hash code for this string. The hash code for a
     * {@code String} object is computed as
     *      * s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
     * 
     * using {@code int} arithmetic, where {@code s[i]} is the
     * ith character of the string, {@code n} is the length of
     * the string, and {@code ^} indicates exponentiation.
     * (The hash value of the empty string is zero.)
     *
     * @return  a hash code value for this object.
     */
    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;
    }

二、怀疑人生

2.1 质疑

不知道你是否真正认真思考过,字符串真的不可变吗? 即使字符串类用 final 修饰,字符串值字符数组也用 final 修饰,所有修改方法都返回新的字符串对象,那么值一定无法修改吗? 答案是否定的!! 我们可以用反射来修改字符串对象的值。

2.2 验证

代码语言:javascript
复制
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        String name ="明明如月学长 admin";
        System.out.println("name 修改前:"+name+", hashCode:"+name.hashCode());
        String newName = "明明如月学长";
        System.out.println("newName:"+newName+", hashCode:"+newName.hashCode());
        replace(name,newName);
        System.out.println("name 修改后: "+name+", hashCode:"+name.hashCode());
    }


    private static  void replace(String name,String newName) throws NoSuchFieldException, IllegalAccessException {
    // 去掉私有
        Field value = String.class.getDeclaredField("value");
        value.setAccessible(true);

   // 去掉 final
        Field mod = Field.class.getDeclaredField("modifiers");
        mod.setAccessible(true);
        mod.setInt(value, value.getModifiers() & ~Modifier.FINAL);

       // 直接替换 value 字符数组
        value.set(name, newName.toCharArray());
    }

输出结果:

name 修改前:明明如月学长 admin, hashCode:557981902 newName:明明如月学长, hashCode:-292262689 name 修改后: 明明如月学长, hashCode:557981902

2.3 带来的问题

如果字符串的值可以修改,程序就可能出现不符合预期的行为,

代码语言:javascript
复制
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.concurrent.TimeUnit;

public class StringDemo {

    public static void main(String[] args) throws InterruptedException {

        String name ="明明如月学长 admin";

        new Thread(()->{
            try {
                mockExecute(name);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        TimeUnit.SECONDS.sleep(2);
        new Thread(()->{
            try {
                mockInject(name);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();
        TimeUnit.SECONDS.sleep(10);


    }

private  static synchronized   void mockInject(String name) throws InterruptedException, NoSuchFieldException, IllegalAccessException {
        // sleep 让 子线程执行到 wait
        TimeUnit.SECONDS.sleep(1);

        // 检查前使用合规的名称
        System.out.println("mockInject: 去掉 admin,绕过校验");
        replace(name,"明明如月学长");
        StringDemo.class.notifyAll();
        System.out.println("mockInject: 恢复 admin");
        // 检查后换成不合规的名称
        replace(name,"明明如月学长 admin");
        System.out.println("mockInject: 恢复 admin 完毕");
        StringDemo.class.notifyAll();
    }

private static  void replace(String name,String newName) throws NoSuchFieldException, IllegalAccessException {
        Field value = String.class.getDeclaredField("value");
        value.setAccessible(true);

        Field mod = Field.class.getDeclaredField("modifiers");
        mod.setAccessible(true);
        mod.setInt(value, value.getModifiers() & ~Modifier.FINAL);
        value.set(name, newName.toCharArray());

    }

private static synchronized  void mockExecute(String name) throws InterruptedException {
        System.out.println("mockExecute: [1] name.contains(\"admin\"):"+name.contains("admin"));
        StringDemo.class.wait();
        //1 参数检查
        if(name.contains("admin")){
            throw new IllegalArgumentException("参数检查失败");
        }
        System.out.println("mockExecute: 不含 admin 关键字,参数检查通过");
        System.out.println("mockExecute: [2] name.contains(\"admin\"):"+name.contains("admin"));
        //2 执行次要任务
        System.out.println("mockExecute: 执行次要任务");

        //3 执行重要人物
        System.out.println("mockExecute: 执行重要任务");
    }
}

这里简单使用 wait/notify 来模拟多线程情况下字符串修改带来的问题。

输出的结果:

mockExecute: [1] name.contains(“admin”):true mockInject: 去掉 admin,绕过校验 mockInject: 恢复 admin mockInject: 恢复 admin 完毕 mockExecute: 不含 admin 关键字,参数检查通过 mockExecute: [2] name.contains(“admin”):false mockExecute: 执行次要任务 mockExecute: 执行重要任务

三、总结

字符串的不可变性是指通过 String 的方法来修改字符串都会产出新的字符串队形。但并非指字符串的字符一定无法被修改,我们可以通过反射一样可以对字符串的“状态/值” 进行修改。

正常情况下不会有人去这么做,否则会产出很多不出乎意料的 BUG。

通过本文想提醒大家,尽信书不如无书,对于看到的知识要有自己的思考。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022/07/31 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、你以为的常识
    • 1.1 不可变性的理解
      • 1.2 不可变性的实现
        • 1.3 不可变性的好处
          • 1.3.1 节省内存
          • 1.3.2 更安全
          • 1.3.3 hashCode 缓存
      • 二、怀疑人生
        • 2.1 质疑
          • 2.2 验证
            • 2.3 带来的问题
            • 三、总结
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档