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
美团面试:String 为什么 不可变 ?(90%答错了,尼恩来一个绝世答案)
45岁老架构师尼恩分享Java面试心得,涵盖String不可变性、字符串常量池、面试技巧等内容。尼恩强调,掌握深层技术原理,如String不可变性的真正原因,可在面试中脱颖而出,赢得高薪Offer。此外,尼恩还提供了大量技术资源和面试指导,帮助求职者提升技术水平,顺利通过大厂面试。
|
2月前
|
安全
String、StringBuffer、StringBuilder的区别
String 由 char[] 数组构成,使用了 final 修饰,对 String 进行改变时每次都会新生成一个 String 对象,然后把指针指向新的引用对象。 StringBuffer可变并且线程安全;有一定缓冲区容量,字符串大小没超过容量,不会重新分配新的容量,适合多线程操作字符串; StringBuiler可变并且线程不安全。速度比StringBuffer更快,适合单线程操作字符串。 操作少量字符数据用 String;单线程操作大量数据用 StringBuilder;多线程操作大量数据用 StringBuffer
|
4月前
|
安全 Java
String、StringBuffer、StringBuilder的区别
这篇文章讨论了Java中String、StringBuffer和StringBuilder的区别。String是不可变的,每次操作都会产生新的对象,效率低且浪费内存。StringBuilder可以在原字符串基础上进行操作,不开辟额外内存,弥补了String的缺陷。StringBuffer和StringBuilder类似,但StringBuffer的方法是线程安全的。文章还列举了StringBuffer的常用方法,并提供了使用示例代码。最后总结了这三者的主要区别。
String、StringBuffer、StringBuilder的区别
|
3月前
|
canal 安全 索引
(StringBuffer和StringBuilder)以及回文串,字符串经典习题
(StringBuffer和StringBuilder)以及回文串,字符串经典习题
50 5
|
3月前
|
存储 安全 Java
String、StringBuffer 和 StringBuilder 的区别
【10月更文挑战第21天】String、StringBuffer 和 StringBuilder 都有各自的特点和适用场景。了解它们之间的区别,可以帮助我们在编程中更合理地选择和使用这些类,从而提高程序的性能和质量。还可以结合具体的代码示例和实际应用场景,进一步深入分析它们的性能差异和使用技巧,使对它们的理解更加全面和深入。
74 0
|
5月前
|
存储 Java
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
|
2月前
|
存储 缓存 算法
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
本文介绍了多线程环境下的几个关键概念,包括时间片、超线程、上下文切换及其影响因素,以及线程调度的两种方式——抢占式调度和协同式调度。文章还讨论了减少上下文切换次数以提高多线程程序效率的方法,如无锁并发编程、使用CAS算法等,并提出了合理的线程数量配置策略,以平衡CPU利用率和线程切换开销。
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
|
2月前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
2月前
|
存储 缓存 Java
大厂面试必看!Java基本数据类型和包装类的那些坑
本文介绍了Java中的基本数据类型和包装类,包括整数类型、浮点数类型、字符类型和布尔类型。详细讲解了每种类型的特性和应用场景,并探讨了包装类的引入原因、装箱与拆箱机制以及缓存机制。最后总结了面试中常见的相关考点,帮助读者更好地理解和应对面试中的问题。
82 4
|
3月前
|
算法 Java 数据中心
探讨面试常见问题雪花算法、时钟回拨问题,java中优雅的实现方式
【10月更文挑战第2天】在大数据量系统中,分布式ID生成是一个关键问题。为了保证在分布式环境下生成的ID唯一、有序且高效,业界提出了多种解决方案,其中雪花算法(Snowflake Algorithm)是一种广泛应用的分布式ID生成算法。本文将详细介绍雪花算法的原理、实现及其处理时钟回拨问题的方法,并提供Java代码示例。
120 2