1 年经验面试说说:String、StringBuffer、StringBuilder

简介: JavaSE 基础题目了,可以说字符串所要了解的内容还是非常多的,其中涉及字符串可变、字符串拼接、字符串安全、字符串内存位置等等。


1、String


String 是 Java 定义的一个字符串类型类,源码(JDK11,本篇所有源码环境都是 11 )如下:


image.png


这里说明一点,Java 在不同版本对 String 源码做了点修改,具体改动如下图。


image.png


改动最大的莫过于将存储字符串的 char 类型数组改成了 byte 类型。那这是为什么呢!


J3:节省 String 占用的内存。


Java 程序语言是按照 Unicode 编码标准存储字符串的,而我们都知道 UTF - 8 编码占用两个及以上的字节个数、ISO-8859-1 编码则是单字节编码只占一个字节。在大部分的时候计算机任然使用的是 ISO-8859-1 编码,所以在存储像字母时,则会白白浪费一个字节的空间,也正是这个原因,Java 才会将 char 改成 byte。


那多了的一个属性 code 是干啥?


J3:标识字符串编码方式


源码:


image.png


coder 属性默认有 0 和 1 两个值。如果 String 判断字符串只包含了 Latin-1,则 coder 属性值为 0 ,反之则为 1


0 代表Latin-1(单字节编码)。

1 代表 UTF-16 编码。

另外 String 类是被 final 修饰的,表示最终类即不可被继承。而且内部存储字符串值的数组属性也是被 final 修饰表明 String 类型变量一旦被定义赋值,则值不可修改(下面会解释不可修改这个点)。


image.png


以上介绍了 String 的基本情况,那再来说说它在 JVM 中的内存布局。


JVM 内部划分为两个组件和两个系统(《Java 虚拟机运行时数据区》):


两个子系统为:


  • Class Loader(类装载子系统)
  • Execution Engine(执行引擎)


两个组件为:


  • Runtime Data Area(运行时数据区)
  • Native Interface(本地接口)


String 所涉及的区为 Runtime Data Area(运行时数据区) ,在该区中 String 类型的字

符串常量存放区域倒是因为 JDK 版本的不一样而略有不同。


  • JDK1.6 及以前字符串常量都存放在方法区的字符串常量池中。
  • JDK1.7 及以后字符串常量池被移到了堆中,所以字符串常量自然就存放在堆中了。


那下面来看看几行代码:

public class StringTest {
    public static void main(String[] args) {
        // 直接赋值一个字符串常量值
        String name = "J3";
        String name1 = "J3";
        // 创建一个 String 对象赋值
        String name2 = new String("J3");
        System.out.println("name 重新赋值前:" + name);
        // name 和 name1 是否相等
        System.out.println("name 和 name1 是否相等:" + (name == name1));
        // 给 name 重新赋值
        name = "刘亦菲";
        System.out.println("name 重新赋值后:" + name);
        System.out.println("name2 赋值:" + name2);
        // name 和 name1 是否相等
        System.out.println("name 和 name1 是否相等:" + (name == name1));
    }
}


image.png

上面代码的 5,6,8 行代码都是给变量赋值,体现在 JVM 中的效果如图:


image.png


紧接着 13 行代码体现图如下:


image.png


结合上图,当字符串常量池中出现相同的字符串时,JVM 不会再生成对应的字符串而时将已经存在的字符串地址赋给变量,从而在字符串常量池中相同的字符串只会存在一份。当栈中变量重新赋值字符串时,则会将变量引用指向新创建的常量池中字符串地址,而常量池原先的值是不会改变的,所以 String 类型变量重新赋值只是变量指向的地址变化,不是值变化。


2、StringBuffer 与 StringBuilder


类继承图:


image.png


StringBuffer 是一个字符串可变的序列,通过其提供的方法可以改变这个字符串对象的字符串序列。


StringBuilder 是从 JDK1.5 开始出现的,功能和 StringBuffer 类似,不同点是 StringBuffer 线程安全,StringBuilder 线程不安全。


这里有两个点,字符串可变和线程安全。


1、字符串可变


String 类型字符串不可变是因为内部存储值的属性是被 final 修饰,所以其值不可变。如果在进行字符串拼接的时候,字符串常量值不会在原来的字符串后面添加字符串,而是重新生成一个拼接后的字符串放到字符串常量池中。


看如下代码:

String name3 = "J3" + "-西行";



继续结合上图,效果如下:


image.png


由图可发现,原来的字符串其实是不会改变,而是重新在字符串常量池中生一个新字符串,这就是 String 字符串不可变的真正地方。


而 StringBuffer 字符串可变是体现在什么地方,咱上源码。


StringBuffer # append

@Override
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    // 调用父类(AbstractStringBuilder)拼接字符串方法
    super.append(str);
    return this;
}


既然调用了父类方法,那点进去瞧瞧。


AbstractStringBuilder # append

public AbstractStringBuilder append(String str) {
    // 拼接字符串为空,那就直接拼接一个空字符串
    if (str == null)
        return appendNull();
    // 获取拼接的字符串长度
    int len = str.length();
    // (重点代码)扩容!!!将原始 char 数组扩容到可以容纳拼接后字符串的长度
    ensureCapacityInternal(count + len);
    // 真正开始字符串拼接
    str.getChars(0, len, value, count);
    // 重新计算字符串长度
    count += len;
    // 返回字符串对象
    return this;
}


在 StringBuffer 和 StringBuilder 中,存储字符串的数组还是 char 类型,并且没有被 final 修饰,所以其指向的 char 类型数组引用可以重新赋值。


那现在来关注一下重点代码,扩容。


AbstractStringBuilder # ensureCapacityInternal

private void ensureCapacityInternal(int minimumCapacity) {
    // overflow-conscious code
    // 如果合并后的字符串长度,大于原始字符串长度,才开始扩容
    if (minimumCapacity - value.length > 0) {
        // 数组扩容,底层调用 System.arraycopy 方法,原理是生成一个新的数组,将原始数组中的内容移动到新数组中,最终赋值给 value
        value = Arrays.copyOf(value,
                newCapacity(minimumCapacity));
    }
}


扩容代码仅仅只是一个开始,保证最后字符串拼接的时候不会导致 char 类型数组溢出,那最后只剩下字符串拼接了,上代码。


AbstractStringBuilder # getChars

public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
    // 各种字符串长度校验
    if (srcBegin < 0) {
        throw new StringIndexOutOfBoundsException(srcBegin);
    }
    if (srcEnd > value.length) {
        throw new StringIndexOutOfBoundsException(srcEnd);
    }
    if (srcBegin > srcEnd) {
        throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
    }
    // 最终走到这里,将 待拼接字符串:value,赋值到 目标数组:dst 中,完成拼接。
    System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}


以上就是 Java 提供的可变字符串内部原理,总结一下可变原因。


内部存放字符串值的 char 类型数组没有被修饰成 final。

实现了一套可扩容的数组机制。


2、线程安全问题


在字符串可变问题上,我已经贴出了可变字符串类型中的一个重要字符串拼接方法(StringBuffer # append)源代码,其中方法上被 synchronized 修饰了,这就是其是一个线程安全的字符串拼接类。


大家可以仔细留意一下,只要是涉及改变字符串内容的方法,都被 synchronized 修饰了,以此来保证线程安全。


why?为什么,这是为什么?


说一下我的理解:保证字符串拼接出的结果和我们预期的一样,系统中可变字符串对象引用可以被多个方法所执行,而他们都想进行字符串拼接,那同一时刻多个方法调用同一个可变字符串对象进行字符串拼接,我们能得到预期的结果嘛,显然是不能的,所以 Java 就在方法的开头加了一把锁(synchronized)谁能第一个锁住这个对象,那就谁先来执行字符串拼接。


而对资源进行加锁与解锁毕竟是要有点开销的,所以 StringBuffer 在字符串拼接的时候效率就会有点损耗 StringBuilder 则不会,因为它内部拼接方法没有加锁,但这也是它线程不安全的原因。


3、我的面试答案

面试官你好,对于这个问题我说一下我的理解:


Java 中常用的字符串类型就莫过于 String 类型了,它是一个被 final 修饰的类,表示类不可被继承。其中存储字符串的数组属性同样也被 final 修饰这也是其字符串不可变的原因,并在不同的 JDK 版本中存储字符串的数组类型也是不一样。


在 JDK1.8 及以前存储字符串的数组类型为 char 之后则是改成了 byte 类型,究其原因则是为了节省字符串占用空间。我们都知道 Java 的字符串编码规则是按 Unicode 编码,Unicode 只是一个规范(其实现有 ISO-8859-1、UTF-8 等),如果 char 类型在 ISO-8859-1 字符编码中字母类型只占 1 个字节,而 UTF-8 则会占用 2 个字节,这就造成了空间浪费。


而 StringBuffer 和 StringBuilder 是属于字符串可变类,内部存储字符的是一个 char 类型数组并没有被 final 修饰,且其内部实现了一套可变的数组代码,这就使得其可以在 char 数组中进行扩容添加字符。


对于可变字符串类 StringBuffer 和 StringBuilder 两者功能基本一样,只不过两者在拼接字符串的时候考虑的使用环境不同。StringBuffer 类在线程安全和不安全环境都可以使用,因为其内部拼接方法都被 synchronized 修饰了,使其变成了一个线程安全方法,但效率有点损耗;StringBuilder 类内部拼接方法则没有保证线程安全未被 synchronized 修饰,所以其只能在线程安全环境下使用,也正是其未被 synchronized 修饰,所以在字符串拼接的时候效率比 StringBuffer 高一点。


到这里,内心窃喜,没有被难倒。


今天的内容到这里就结束了,关注我,我们下期见。


目录
相关文章
|
2月前
|
Java
【Java基础面试三十一】、String a = “abc“; ,说一下这个过程会创建什么,放在哪里?
这篇文章解释了在Java中声明`String a = "abc";`时,JVM会检查常量池中是否存在"abc"字符串,若不存在则存入常量池,然后引用常量池中的"abc"给变量a。
|
2月前
|
Java
【Java基础面试三十二】、new String(“abc“) 是去了哪里,仅仅是在堆里面吗?
这篇文章解释了Java中使用`new String("abc")`时,JVM会将字符串直接量"abc"存入常量池,并在堆内存中创建一个新的String对象,该对象会指向常量池中的字符串直接量。
|
2月前
|
安全 Java API
【Java字符串操作秘籍】StringBuffer与StringBuilder的终极对决!
【8月更文挑战第25天】在Java中处理字符串时,经常需要修改字符串,但由于`String`对象的不可变性,频繁修改会导致内存浪费和性能下降。为此,Java提供了`StringBuffer`和`StringBuilder`两个类来操作可变字符串序列。`StringBuffer`是线程安全的,适用于多线程环境,但性能略低;`StringBuilder`非线程安全,但在单线程环境中性能更优。两者基本用法相似,通过`append`等方法构建和修改字符串。
53 1
|
29天前
|
安全 Java
String、StringBuffer、StringBuilder的区别
这篇文章讨论了Java中String、StringBuffer和StringBuilder的区别。String是不可变的,每次操作都会产生新的对象,效率低且浪费内存。StringBuilder可以在原字符串基础上进行操作,不开辟额外内存,弥补了String的缺陷。StringBuffer和StringBuilder类似,但StringBuffer的方法是线程安全的。文章还列举了StringBuffer的常用方法,并提供了使用示例代码。最后总结了这三者的主要区别。
String、StringBuffer、StringBuilder的区别
|
12天前
|
canal 安全 索引
(StringBuffer和StringBuilder)以及回文串,字符串经典习题
(StringBuffer和StringBuilder)以及回文串,字符串经典习题
31 5
|
1月前
|
安全 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版)
|
1月前
|
安全 Java
Java StringBuffer 和 StringBuilder 类详解
在 Java 中,`StringBuffer` 和 `StringBuilder` 用于操作可变字符串,支持拼接、插入、删除等功能。两者的主要区别在于线程安全性和性能:`StringBuffer` 线程安全但较慢,适用于多线程环境;`StringBuilder` 非线程安全但更快,适合单线程环境。选择合适的类取决于具体的应用场景和性能需求。通常,在不需要线程安全的情况下,推荐使用 `StringBuilder` 以获得更好的性能。
|
2月前
|
Java
【Java基础面试二十六】、说一说String和StringBuffer有什么区别
这篇文章区分了Java中的String和StringBuffer类:String是不可变类,一旦创建字符序列就不能改变;而StringBuffer代表可变的字符串,可以通过其方法修改字符序列,最终可以通过`toString()`方法转换为String对象。
【Java基础面试二十六】、说一说String和StringBuffer有什么区别
|
2月前
|
安全 Java API
Java系类 之 String、StringBuffer和StringBuilder类的区别
这篇文章讨论了Java中`String`、`StringBuffer`和`StringBuilder`三个类的区别,其中`String`是不可变的,而`StringBuffer`是线程安全的可变字符串类,`StringBuilder`是非线程安全的可变字符串类,通常在单线程环境下性能更优。
Java系类 之 String、StringBuffer和StringBuilder类的区别
|
2月前
|
安全 Java
【Java基础面试二十七】、说一说StringBuffer和StringBuilder有什么区别
这篇文章介绍了Java中StringBuffer和StringBuilder的区别:StringBuffer是线程安全的,而StringBuilder是非线程安全的,因此在单线程环境下优先推荐使用StringBuilder以获得更好的性能。