写点什么

☕️【Java 技术之旅】带你一起探究 String 类不可变的特性

发布于: 2021 年 06 月 09 日
☕️【Java技术之旅】带你一起探究String类不可变的特性

前提介绍

在 Java 中 String 类的使用的频率可谓相当高。它是 Java 语言中的核心类,在 java.lang 包下,主要用于字符串的比较、查找、拼接等等操作。如果要深入理解一个类,最好的方法就是看看源码

什么是字符串

字符串是由引号所括起来的一系列字符序列

字符串类(String)

/** String 类源码 */public final class String      implements java.io.Serializable, Comparable<String>, CharSequence {    /** The value is used for character storage. */    private final char value[];
/** Cache the hash code for the string */ private int hash; // Default to 0 /** use serialVersionUID from JDK 1.0.2 for interoperability */ private static final long serialVersionUID = -6849794470754667710L;
/** * Class String is special cased within the Serialization Stream Protocol. * * A String instance is written into an ObjectOutputStream according to * <a href="{@docRoot}/../platform/serialization/spec/output.html"> * Object Serialization Specification, Section 6.2, "Stream Elements"</a> */ private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0]; ……}
复制代码


从源码中,可以看出以下几点:


  • String 类被 final 关键字修饰,表示 String 类不能被继承,且它的属性和方法都被 final 所修饰任何操作都会生成新对象

  • String:: subString(),String::concat() 等方法都会生成一个新的 String 对象,不会在原对象上进行操作从下面 String 源码部分中很容易得到上面的结论

  • String 类实现了 Serializable、CharSequence、 Comparable 接口

  • String 类的值是通过 char 数组存储的,并且 char 数组被 private 和 final 修饰,字符串一旦创建就不能再修改

String 不可变性

  • String 对象一旦被创建就是固定不变的了,对 String 对象的任何改变都不影响到原对象,相关的任何操作都会生成新的对象

  • String 不可变的表现就是当我们试图对一个已有的对象 "abcd" 赋值为 "abcde",String 会新创建一个对象


注意点

这个无法被修改仅仅是指引用地址不可被修改(也就是说栈里面的这个叫 value 的引用地址不可变,编译器不允许我们把 value 指向堆中的另一个地址),并不代表存储在堆中的这个数组本身的内容不可变。


那既然我们说 String 是不可变的,那显然仅仅靠 final 是远远不够的:


  1. char 数组是 private 的,并且 String 类没有对外提供修改这个数组的方法,所以它初始化之后外界没有有效的手段去改变它

  2. String 类被 final 修饰的,首先要讲 final 修饰类的作用,被 final 修饰的类不能被继承,类中的所有成员方法都会被隐式地指定为 final 方法。也就是不能拥有子类,成员方法也不能被重写。

  3. String 的所有方法里面,都很小心地避免去修改了 char 数组中的数据,涉及到对 char 数组中数据进行修改的操作全部都会重新创建一个 String 对象

比如 substring 方法:
public String substring(int beginIndex, int endIndex) {    if (beginIndex < 0) {        throw new StringIndexOutOfBoundsException(beginIndex);    }    if (endIndex > value.length) {        throw new StringIndexOutOfBoundsException(endIndex);    }    int subLen = endIndex - beginIndex;    if (subLen < 0) {        throw new StringIndexOutOfBoundsException(subLen);    }    return ((beginIndex == 0) && (endIndex == value.length)) ? this            : new String(value, beginIndex, subLen);}
复制代码

为什么要设计成不可变的呢?

String 被设计成不可变就是为了字符串常量池
  • 字符串常量池的定义

  • 大量频繁的创建字符串,将会极大程度地影响程序的性能,字符串的分配和其他对象分配一样,是需要消耗高昂的时间和空间的,而且字符串我们使用的非常多。JVM 为了提高性能和减少内存的开销,所以在实例化字符串的时候使用字符串常量池进行优化。

  • JVM 为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化:

  • 字符串开辟了一个字符串常量池 String Pool(HashSet 的 StringTable),可以理解为缓存区创建字符串常量时,首先检查字符串常量池中是否存在该字符串。

  • 池化思想其实在 Java 中并不少见,字符串常量池也是类似的思想,当创建字符串时,JVM 会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中


堆内存中只会创建一个 String 对象:


String str1 = "hello";String str2 = "hello";System.out.println(str1 == str2) // true 
复制代码



String 允许被改变,那如果我们修改了 str2 的内容为 good,那么 str1 也会被修改,显然这不是我们想要看见的结果


new String(“abc”)创建了几个对象?


  • 如果之前"abc"字符串没有使用过,毫无疑问是创建两个对象,堆中创建了一个 String 对象,字符串常量池创建了一个,一共两个。

  • 如果之前已经使用过了"abc"字符串,则不会再在字符串常量池创建对象,而是从字符串常量缓冲区中获取,只会在堆中创建一个 String 对象。


String s1 = "abc";String s2 = new String("abc");//s2这行代码,只会创建一个对象
复制代码
String 被设计成不可变就是为了安全
  • 作为最基础最常用的数据类型,String 被许多 Java 类库用来作为参数,如果 String 不是固定不变的,安全性考虑。字符串应用场景众多,设计成不可变性可以有效防止字符串被有意篡改。

  • String 被许多的 Java 类(库)用来当做参数,比如网络连接地址 URL,文件路径 path,还有反射机制所需要的 String 参数等,假若 String 不是固定不变的,将会引起各种安全隐患

  • 在多线程环境下,众所周知,多个线程同时想要修改同一个资源,是存在危险的,而 String 作为不可变对象,不能被修改,并且多个线程同时读同一个资源,是完全没有问题的,所以 String 是线程安全的

String 被设计成不可变就是为了效率

字符串不变性保证了 hash 码的唯一性,因此可以放心的进行缓存,这也是一种性能优化手段,意味着不必每次都取计算新的哈希码



String 真的不可变吗?

  • String 无非就是改变 char 数组 value 的内容,而 value 是私有属性,那么在 Java 中有没有某种手段可以访问类的私有属性呢?

  • 反射,使用反射可以直接修改 char 数组中的内容,当然,一般来说我们不这么做。

看下面代码

字符串的 replace

public String replace(char oldChar, char newChar) {    if (oldChar != newChar) {        int len = value.length;        int i = -1;        char[] val = value; /* avoid getfield opcode */        while (++i < len) {            if (val[i] == oldChar) {                break;            }        }        if (i < len) {            char buf[] = new char[len];            for (int j = 0; j < i; j++) {                buf[j] = val[j];            }            while (i < len) {                char c = val[i];                buf[i] = (c == oldChar) ? newChar : c;                i++;            }            //创建一个新的字符串返回            return new String(buf, true);        }    }    return this;}
复制代码


其他方法也是一样,无论是 sub、concat 还是 replace 操作都不是在原有的字符串上进行的,而是重新生成了一个新的字符串对象。

字符串拼接

字符串的拼接在 Java 中是很常见的操作,但是拼接字符串并不是简简单单地使用"+"号即可,还有一些要注意的点,否则会造成效率低下


public static void main(String[] args) throws Exception {    String s = "";    for (int i = 0; i < 10; i++) {        s+=i;    }    System.out.println(s);//0123456789}
复制代码


在循环内使用+=拼接字符串会有什么问题呢?我们反编译一下看看就知道了。



  • 其实反编译后,我们可以看到 String 类使用"+="拼接的底层其实是使用 StringBuilder,先初始化一个 StringBuilder 对象,然后使用 append()方法拼接,最后使用 toString()方法得到结果。

  • 问题在于如果在循环体内使用+=拼接,会创建很多临时的 StringBuilder 对象,拼接后再调用 toString()赋给原 String 对象。这会生成大量临时对象,严重影响性能。


所以在循环体内进行字符串拼接时,建议使用 StringBuilder 或者 StringBuffer 类,例子如下:


public static void main(String[] args) throws Exception {    StringBuilder s = new StringBuilder();    for (int i = 0; i < 10; i++) {        s.append(i);    }    System.out.println(s.toString());//0123456789}
复制代码


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);}
复制代码


StringBuilder 和 StringBuffer 的区别在于,StringBuffer 的方法都被 sync 关键字修饰,所以是线程安全的,而 StringBuilder 则是线程不安全的(效率高)。

总结

并不是因为 char 数组是 final 才导致 String 的不可变,而是为了把 String 设计成不可变才把 char 数组设置为 final 的


所有不可变类都完全遵守这些规则:


  • 不要提供 setter 方法(包括修改字段的方法和修改字段引用对象的方法)

  • 将类的所有字段定义为 final、private 的;

  • 不允许子类重写方法。简单的办法是将类声明为 final,更好的方法是将构造函数声明为私有的,通过工厂方法创建对象;

  • 如果类的字段是对可变对象的引用,不允许修改被引用对象。

发布于: 2021 年 06 月 09 日阅读数: 6
用户头像

我们始于迷惘,终于更高水平的迷惘。 2020.03.25 加入

🏆 【酷爱计算机技术、醉心开发编程、喜爱健身运动、热衷悬疑推理的”极客狂人“】 🏅 【Java技术领域,MySQL技术领域,APM全链路追踪技术及微服务、分布式方向的技术体系等】 🤝未来我们希望可以共同进步🤝

评论

发布
暂无评论
☕️【Java技术之旅】带你一起探究String类不可变的特性