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


    在这里插入图片描述

    一、你以为的常识

    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)都会返回一个新的字符串对象。
    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    1.3 不可变性的好处

    1.3.1 节省内存

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

    String s1 = "明明如月学长";
    String s2 = "明明如月学长");
    String s3 = new String("明明如月学长");
    assertThat(s1 == s2).isTrue();
    assertThat(s1 == s3).isFalse();
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在这里插入图片描述

    1.3.2 更安全

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

    void criticalMethod(String userName) {
        //1 执行安全检查
        if (!isAlphaNumeric(userName)) {
            throw new SecurityException(); 
        }
    	
        //2 执行一些次要的任务
        initializeDatabase();
    	
        //3 重要的任务
        connection.executeUpdate("UPDATE Customers SET Status = 'Active' " +
          " WHERE UserName = '" + userName + "'");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

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

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

    1.3.3 hashCode 缓存

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

     /**
         * 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; }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    二、怀疑人生

    2.1 质疑

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

    2.2 验证

    
        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());
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    输出结果:

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

    2.3 带来的问题

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

    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: 执行重要任务");
        }
    }
    
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76

    这里简单使用 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。

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


    创作不易,如果本文对你有帮助,欢迎点赞、收藏加关注,你的支持和鼓励,是我创作的最大动力。
    在这里插入图片描述

  • 相关阅读:
    C //例 7.13 有一个3*4的矩阵,求所有元素中的最大值。
    eggjs controller层调用controller层解决方案
    用ffmpeg删除视频的音轨,让视频静音
    [单片机框架][bsp层][N32G4FR][bsp_pwm] pwm配置和使用
    Hbuilder出现 CR LF
    25.leetcode---只出现一次的数字(Java版)
    Redis分布式缓存(四)| 分片集群搭建、散列插槽、集群伸缩、故障转移、与SpringBoot集成分片集群
    Generate Label from Click
    【ESP32】Arduino C语言语法总结
    【MySQL Router】第 1 章 通用信息
  • 原文地址:https://blog.csdn.net/w605283073/article/details/126045777