【Java原理探索】带你探究String类不可变的特性 | Java开发实战

简介: 【Java原理探索】带你探究String类不可变的特性 | Java开发实战

前提介绍


在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 会新创建一个对象


image.png

注意点


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


image.png

那既然我们说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


image.png

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 数组中的内容,当然,一般来说我们不这么做。


看下面代码

image.png

字符串的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
}
复制代码


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

image.png

  • 其实反编译后,我们可以看到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,更好的方法是将构造函数声明为私有的,通过工厂方法创建对象;
  • 如果类的字段是对可变对象的引用,不允许修改被引用对象。








相关文章
|
5天前
|
安全 Java API
【Java面试题汇总】Java基础篇——String+集合+泛型+IO+异常+反射(2023版)
String常量池、String、StringBuffer、Stringbuilder有什么区别、List与Set的区别、ArrayList和LinkedList的区别、HashMap底层原理、ConcurrentHashMap、HashMap和Hashtable的区别、泛型擦除、ABA问题、IO多路复用、BIO、NIO、O、异常处理机制、反射
【Java面试题汇总】Java基础篇——String+集合+泛型+IO+异常+反射(2023版)
|
4天前
|
存储 安全 Java
Java——String类详解
String 是 Java 中的一个类,用于表示字符串,属于引用数据类型。字符串可以通过多种方式定义,如直接赋值、创建对象、传入 char 或 byte 类型数组。直接赋值会将字符串存储在串池中,复用相同的字符串以节省内存。String 类提供了丰富的方法,如比较(equals() 和 compareTo())、查找(charAt() 和 indexOf())、转换(valueOf() 和 format())、拆分(split())和截取(substring())。此外,还介绍了 StringBuilder 和 StringJoiner 类,前者用于高效拼接字符串,后者用于按指定格式拼接字符串
11 1
Java——String类详解
|
1天前
|
安全 Java API
JAVA并发编程JUC包之CAS原理
在JDK 1.5之后,Java API引入了`java.util.concurrent`包(简称JUC包),提供了多种并发工具类,如原子类`AtomicXX`、线程池`Executors`、信号量`Semaphore`、阻塞队列等。这些工具类简化了并发编程的复杂度。原子类`Atomic`尤其重要,它提供了线程安全的变量更新方法,支持整型、长整型、布尔型、数组及对象属性的原子修改。结合`volatile`关键字,可以实现多线程环境下共享变量的安全修改。
|
1天前
|
安全 Java
Java StringBuffer 和 StringBuilder 类详解
在 Java 中,`StringBuffer` 和 `StringBuilder` 用于操作可变字符串,支持拼接、插入、删除等功能。两者的主要区别在于线程安全性和性能:`StringBuffer` 线程安全但较慢,适用于多线程环境;`StringBuilder` 非线程安全但更快,适合单线程环境。选择合适的类取决于具体的应用场景和性能需求。通常,在不需要线程安全的情况下,推荐使用 `StringBuilder` 以获得更好的性能。
|
3天前
|
机器学习/深度学习 人工智能 安全
python和Java的区别以及特性
Python:适合快速开发、易于维护、学习成本低、灵活高效。如果你需要快速上手,写脚本、数据处理、做点机器学习,Python就是你的首选。 Java:适合大型项目、企业级应用,性能要求较高的场景。它类型安全、跨平台能力强,而且有丰富的生态,适合更复杂和规模化的开发。
14 3
|
1天前
|
Java 索引
Java String 类详解
Java 中的 `String` 类用于表示不可变的字符序列,是 Java 标准库 `java.lang` 包的一部分。字符串对象一旦创建,其内容不可更改,修改会生成新对象。
|
28天前
|
API 索引
String类下常用API
String类下常用API
32 1
|
28天前
for循环和String类下方法的一个练习题
for循环和String类下方法的一个练习题
42 1
|
30天前
|
Java API 索引
【Java基础面试二十四】、String类有哪些方法?
这篇文章列举了Java中String类的常用方法,如`charAt()`、`substring()`、`split()`、`trim()`、`indexOf()`、`lastIndexOf()`、`startsWith()`、`endsWith()`、`toUpperCase()`、`toLowerCase()`、`replaceFirst()`和`replaceAll()`,并建议面试时展示对这些方法的熟悉度,同时深入理解部分方法的源码实现。
【Java基础面试二十四】、String类有哪些方法?
|
29天前
|
存储 SQL Java
Java 系类之 Java StringBuffer类详解
这篇文章详细介绍了Java中的StringBuffer类的使用,包括其构造方法、追加字符串、替换字符、反转字符串和删除字符串的方法,并提供了相应的示例代码。