java基础——String的不可变性

String & Immutable

1.概念

String 类被 final 修饰,类似的被 final 修饰的类还有 Integer、Double等等包装类。我们小学二年级时候都学过,被 final 修饰的类是不可被继承的,在 Java 中,被 final 修饰的类被称为 “不可变类”。好奇的同学可能会问了,1.这些类为什么需要被 final 修饰呢?2.为什么不能改变呢,不可变有什么好处?我们以 String 为例,进行探讨。

2.不可变 (immutable)

如果你阅读了 String 方法的源码,你知道 String 的核心是它的成员变量 value

private final char value[];

String 的所有方法都围绕着这个 char 类型的数组。

我们小学二年级时候学过,被 final 修饰的成员变量有几个特点

  1. 如果修饰的是实例成员变量,则需要在构造函数执行完毕前完成对成员变量的初始化。如果是静态成员变量,则需要在类初始化完毕前完成初始化。
  2. 变量的值不能变。如果修饰的是引用类型,值代表的就是对象中存储的内存地址;如果是基本数据类型,则变量的值不能改变。

value 是被 final 修饰的、实例的、引用类型的成员变量。就意味着,需要在创建 String 对象时对数组进行初始化。且完成初始化后,对象的内存地址不能被改变。

public class StringTest2 {
    private final char[] value;
    
    public StringTest2(){
        value = new char[]{1,2,3};
    }
    
    public void update(){
        value[2] = 100;
    }
}

虽然 value 的内存地址无法改变,但根据上面例子看来数组中的值是可以改变的。但设计 String 的工程师 1.将 value 数组私有化(private),2.没有对外提供任何修改 value 数组元素的方法( 没有 setter 方法),3.在源码中也没有任一地方对 value 数组的值进行了修改。

例如 String.replace 方法,实际不修改 value 数组,而是将 value 数组复制一份,在新的数组上修改后,将新数组返回。测试代码如下:

String str = "test";
String replace = str.replace('t', 'f');
System.out.println(replace);
System.out.println(str);

//output
fesf
test

加上文章开头说的,设计者用 final 修饰 String 类,使 String 类不能被继承,不能拥有子类。 也就不能通过继承的方式实现多态,改变 String 中方法的实现。我们通过下面的例子了解

public class Son{
    @Override
    public String toString() {
        return "哈哈哈";
    }
}

public void sout(String str){
        System.out.println(str);
}

我们假设 Son 类是 String 的子类,当调用下方 sout 方法时,需要传入 String 类型的对象。了解多态的同学知道,多态就是父类的引用指向子类的对象。我们可以利用多态,向 sout 方法中传入 Son 的实例,那么结果就是 “哈哈哈” 而非输出 String 对象的值了。

可见设计 String 的工程师是多么的用心良苦,为了让我们知道,String 这个类应该是不可变的(immutable)。

为了避免 String 中的方法被扩展 / 修改,为了保证 String 的不可变性,所以将 String 用 final 修饰以表明 String 不可变。这也就回答了文章开头的第一个问题。下面我们来解答第二个问题:为什么把 String 设计成不可变,不可变有什么好处。

3.为什么把 String 设计成不可变

  • 安全

    我们可以通过以下例子去理解不可变的安全性。

    1. 不可变类的实例作为 HashMap 的 key 值
    class Student{
        private String name;
        private Integer id;
    
        public Student(String name, Integer id) {
            this.name = name;
            this.id = id;
        }
    
        //getter.. 篇幅原因,此处省略
        //setter.. 篇幅原因,此处省略
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Student)) return false;
            Student student = (Student) o;
            return Objects.equals(getName(), student.getName()) &&
                    Objects.equals(getId(), student.getId());
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(getName(), getId());
        }
    }
    
    public static void main(String[] args) {
            HashMap<String,Integer> test1 = new HashMap<>();
            HashMap<Student,Integer> test2 = new HashMap<>();
    
            test1.put("1",1);
            test1.put("2",1);
    
            Student student1 = new Student("thd",1);
            Student student2 = new Student("djw",2);
            test2.put(student1,1);
            test2.put(student2,2);
            student2.setName("thd");
            student2.setId(1);
    
            System.out.println(student1.hashCode());
            System.out.println(student2.hashCode());
            System.out.println(student1.equals(student2));
            System.out.println(test2.get(student2));
    }
    
    //output
    3559762
    3559762
    true
    1
    

    当 HashMap 中的 key 值为可变类时,如果按照以上的操作,将 student2 的信息改变为与 student1 相同后。通过 hashMap.get 方法获取 value 值,导致信息不匹配错误。(所以在开发中经常使用不可变字符串作为 HashMap 的 key 值)。相反的,不可变类的实例存入 HashMap 中,不会再被外部通过任何方式修改,也就保证了 key 的唯一性。

    2.线程安全:因为不可变类的信息是无法被外部修改的,所以在多线程中不存在对不可变类进行 写 操作,只有读操作,所以不存在线程安全问题。

    3.在网络连接和数据库连接时,字符串常常作为参数。例如,网络连接地址 URL、文件路径 Path。(url 以字符串形式传入后端后,后端可能会对 url 进行字符串操作,string 的不可变性确保了 url 不会被改变)

  • 效率

    1.基于 String 的不可变性,Java 提出了 字符串常量池。实际开发中我们经常需要操作字符串,如果没有字符串常量池,系统中会存在大量的重复的字符串对象,浪费了很多内存空间。有了字符串常量池,可以提高效率并且减少内存分配,方便重复使用。如果 String 是可变的,那么也就没有 字符串常量 的概念,字符串常量池也就无法提出。我们通过下面的例子了解这一点:

    String str = "123";
    str = "456";
    String str1 = "123";
    

    如果 String 是可变的,那么如下图:

    在这里插入图片描述

    str 是栈中指向堆中对象的引用,如果 String 是可变的,那么 str 指向的 “123” 对象的值变为 “456”,前后是同一个对象,地址不变。这时 str1 需要指向一个值为 “123” 的对象,在堆中找不到,只能再次创建一个对象。

    但如果 String 是不可变的,那么如下图:

    在这里插入图片描述

    不再需要创建新的 String 对象,在程序中出现过的字符串对象都保存在字符串常量池中统一管理。

    2.String 对象中私有化封装了一个 hash 用于缓存哈希值,String对象的哈希码被频繁地使用, 比如在 hashMap 等容器中。String 不可变性保证了 hashCode 的唯一性,因此可以放心地进行缓存.这也是一种性能优化手段,意味着不必每次都去计算新的哈希码。

    /** Cache the hash code for the string */
        private int hash; // Default to 0
    

    3.每次对字符串进行增删改查之前其实 jvm 需要检查一下这个String对象的安全性,就是通过 hashcode,当设计成不可变对象时候,就保证了每次增删改查的hashcode的唯一性,也就可以放心的操作。

4.总结

1.如何让一个类不可变

  • 使用 final 修饰类,使得类不能被继承
  • 将成员变量私有化
  • 使用 final 修饰成员变量
  • 在构造函数中完成对成员变量的初始化
  • 不对外提供修改成员变量的方法
  • 当类中含有可变的属性时,为了防止属性被修改,getter 方法不应该返回该对象的引用而应该返回一个包含与对象相同内容的新对象
final class Student1{
    //immutable field
    private final String name;
    //mutable field
    private final Date date;

    public Student1(String name,Date date) {
        this.name = name;
        this.date = date;
    }

    public String getName(){
        return name;
    }

    public Date getDate(){
        return new Date(this.date.getTime());
    }
}

2.不可变有什么好处

  • 安全
    1. 可以作为 HashMap 的 key 值
    2. 线程安全
  • 高效
    1. 可以缓存 hashCode,每次使用时不需要再次计算
    2. 可以实现缓存池
  • 10
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值