再使用Java的过程中,String类是我们最常用的类之一,String有哪些特性,应该怎样用?

如何理解String的不可变性?

首先,理解一下什么是不可变对象,都知道String是不可变的,那么如何理解不可变?
解答:我们可以这样理解不可变对象,如果一个对象,在它创建完成之后,不能再改变它的状态,那么这个对象就是不可变的,不能改变状态的意思就是,不呢改变对象内的成员变量,包括基本数据类型的不能改变,引用类型的变量不能指向其它的对象,引用类型指向的对象的状态也不能改变。
然后,再Java中,如何才能实现不可变?

  1. 属性定义为private final
  2. 类声明为final不可变
  3. 对于private属性不提供设值方法

为什么String是不可变

区分对象和对象的引用

对于Java初学者, 对于String是不可变对象总是存有疑惑。看下面代码:

String s  = "ABCabc";
System.out.println("s = " + s);
s = "123456";
System.out.println("s = " + s);

输出:
s = ABCabc
s = 123456

首先创建一个String对象s,然后让s的值为“ABCabc”, 然后又让s的值为“123456”。 从打印结果可以看出,s的值确实改变了。那么怎么还说String对象是不可变的呢? 其实这里存在一个误区: s只是一个String对象的引用,并不是对象本身。对象在内存中是一块内存区,成员变量越多,这块内存区占的空间越大。引用只是一个4字节的数据,里面存放了它所指向的对象的地址,通过这个地址可以访问对象。

也就是说,s只是一个引用,它指向了一个具体的对象,当s=“123456”; 这句代码执行过之后,又创建了一个新的对象“123456”, 而引用s重新指向了这个心的对象,原来的对象“ABCabc”还在内存中存在,并没有改变。内存结构如下图所示:
79122720161128180652068432546324.png

那么回归正题,String为什么不可变?
看一下String的源码:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    private final char value[];
    private int hash; 
    private static final long serialVersionUID = -6849794470754667710L;
    private static final ObjectStreamField[] serialPersistentFields =new ObjectStreamField[0];
}

通过源码可以看出来,String的值存储再value数组中,定义为final类型不可变。除此之外还有一个hash成员变量,是该String对象的哈希值的缓存,这个成员变量也和本文的讨论无关。在Java中,数组也是对象。所以value也只是一个引用,它指向一个真正的数组对象。其实执行了String s = “ABCabc”; 这句代码之后,真正的内存布局应该是这样的:
791227201611281807330841497226988.png
value,offset和count这三个变量都是private的,并且没有提供setValue, setOffset和setCount等公共方法来修改这些值,所以在String类的外部无法修改String。也就是说一旦初始化就不能修改, 并且在String类的外部不能访问这三个成员。此外,value,offset和count这三个变量都是final的, 也就是说在String类内部,一旦这三个值初始化了, 也不能被改变。所以可以认为String对象是不可变的了。
那么在String中,明明存在一些方法,调用他们可以得到改变后的值。这些方法包括substring, replace, replaceAll, toLowerCase等。例如如下代码:

String a = "ABCabc";
System.out.println("a = " + a);
a = a.replace('A', 'a');
System.out.println("a = " + a);
输出为: 
a = ABCabc
a = aBCabc

那么a的值看似改变了,其实也是同样的误区。再次说明,a只是一个引用, 不是真正的字符串对象,在调用a.replace('A', 'a')时,方法内部创建了一个新的String对象,并把这个新的对象重新赋给了引用a。String中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;
    }

StringBuilder和StringBuffer

运行速度

正常情况:StringBuilder>StringBuffer>String
详细分析:其中先看一下String,String是一个final不可变不可被继承的字符串常量,看一个简单的例子

String str = "abc";
System.out.println(str);
str = str + "de";
System.out.println(str);

这段代码中,首先会创建一个String对象str,将abc赋值给str,然后运行到第三行,JVM会再创建一个新的str对象,并将原有str的值和de加起来再赋值给新的str。而第一个创建的str对象被JVM的垃圾回收机制(GC)回收掉。所以str实际上并没有被更改,即String对象一旦创建就不可更改。所以Java中对String对象进行的操作实际上是一个不断创建并回收对象的过程,因此在运行速度上很慢。
后两者是可更改的,它们只能通过构造函数来建立对象,且对象被建立以后将在内存中分配内存空间,并初始保存一个null,通过append方法向StringBuffer和StringBuilder中赋值。StringBuilder和StringBuffer的对象是变量,对变量的操作是直接对该对象就行更改,因此不会进行反复的创建和回收。所以在运行速度上比较快。

线程安全

StringBuilder(非线程安全)

而StringBuilder的方法没有该关键字修饰,所以不能保证线程安全性。是JDK1.5新增的,该类提供一个与StringBuffer兼容的 API,但不能保证同步,所以在性能上较高。该类被设计用作 StringBuffer 的一个简易替换,用在字符串缓冲区被单个线程使用的时候(这种情况很普遍)。如果可能,建议优先采用该类,因为在大多数实现中,它比 StringBuffer 要快。两者的方法基本相同。

StringBuffer(线程安全的)

StringBuffer中大部分方法由synchronized关键字修饰,在必要时可对方法进行同步,因此任意特定实例上的所有操作就好像是以串行顺序发生的,该顺序与所涉及的每个线程进行的方法调用顺序一致,所以是线程安全的。类似于 String 的字符串缓冲区,但不能修改。虽然在任意时间点上它都包含某种特定的字符序列,但通过某些方法调用可以改变该序列的长度和内容。StringBuffer 上的主要操作是 append 和 insert 方法,可重载这些方法,以接受任意类型的数据。每个方法都能有效地将给定的数据转换成字符串,然后将该字符串的字符追加或插入到字符串缓冲区中。append 方法始终将这些字符添加到缓冲区的末端;而 insert 方法则在指定的点添加字符。

使用场景
  • String:适用于少量的字符串操作。
  • StringBuilder:适用于单线程下在字符串缓冲区进行大量操作。
  • StringBuffer:适用于多线程下在字符串缓冲区进行大量操作。


标题:Java中的String详解
作者:XxwGit
地址:http://xxwgit.cn/solo/articles/2020/01/15/1579073513135.html