面试篇总结[JAVA基础]—字符串

1.String、StringBuilder和StringBuffer各自特点

1.1 String类型

摘抄自源码注释的String简介

The String class represents character strings. All string literals in Java programs, such as “abc”, are implemented as instances of this class.
Strings are constant; their values cannot be changed after they are created. String buffers support mutable strings. Because String objects are immutable they can be shared. .
For example:
String str = “abc”;
String类表示字符字符串。Java程序中的所有字符串字面值,如"abc",都是这个类的实例。 字符串是常量;它们的值在创建后不能更改。字符串缓冲区支持可变字符串。因为String对象是不可变的,所以它们可以被共享。
例如:
String str = “abc”;

上面提到的字符串缓冲区是指什么?

在Java中,字符串是不可变的,这意味着一旦创建了一个字符串对象,它的值就不能被修改。但是,有时候我们需要对字符串进行频繁的修改,比如在循环中拼接字符串。为了支持这种情况,Java提供了StringBuffer和StringBuilder这两个类,它们都是可变的字符串缓冲区,可以动态地修改字符串的内容。

在JAVA语言规范(The Java Language Specification, Java SE 21 Edition)中,有一段对于字符串字面量的代码例子:

package testPackage;
class Test {
 public static void main(String[] args) {
 String hello = "Hello", lo = "lo";
 System.out.println(hello == "Hello");
 System.out.println(Other.hello == hello);
 System.out.println(other.Other.hello == hello);
 System.out.println(hello == ("Hel"+"lo"));
 System.out.println(hello == ("Hel"+lo));
 System.out.println(hello == ("Hel"+lo).intern());
 }
}
package testPackage;
class Other { static String hello = "Hello"; }
package other;
public class Other { public static String hello = "Hello"; }

这段程序的输出结果:

true
true
true
true
false
true

This example illustrates six points:
从上面例子可以得出6点结论:

  • String literals in the same class and package represent references to the same String object (§4.3.1).
    同一类和包中的字符串字面量表示对同一 String 对象的引用(§4.3.1)。
  • String literals in different classes in the same package represent references to the same String object.
    同一包中不同类中的字符串字面量表示对同一 String 对象的引用。
  • String literals in different classes in different packages likewise represent references to the same String object.
    不同包中不同类中的字符串字面量同样表示对同一 String 对象的引用。
  • Strings concatenated from constant expressions (§15.29) are computed at compile time and then treated as if they were literals.
    由常量表达式(§15.29)连接的字符串在编译时计算,然后被视为字面量。
    这句话的意思是,当字符串是由常量表达式连接而成时,在编译时就会计算这个连接操作的结果,然后将结果视为一个字符串字面量。这意味着编译器会在编译阶段将这些连接的字符串计算出来,而不是在运行时才进行计算。
  • Strings computed by concatenation at run time are newly created and therefore distinct.
    在运行时通过连接计算的字符串是新创建的,因此是不同的。
    即,除了表达式是常量表达式,在运行时通过字符串连接操作生成的新字符串会被视为一个新的对象,即使它们的内容与已有的字符串相同也是如此。这是因为字符串是不可变的,因此每次执行连接操作时都会创建一个新的字符串对象来保存连接后的结果。
  • The result of explicitly interning a computed string is the same String object as any pre-existing string literal with the same contents.
    明确地对通过计算得到的字符串进行内部化的结果是与具有相同内容的任何现有字符串字面量相同的 String 对象。
    这句话的意思是,如果你明确调用了字符串的 intern 方法,将一个通过计算得到的字符串进行内部化(intern),那么得到的结果将会是一个与已经存在的具有相同内容的字符串字面量相同的 String 对象。也就是说,如果字符串已经存在于常量池中(因为它是一个字符串字面量),那么调用 intern 方法后会返回常量池中已经存在的对象的引用。

在JAVA语言规范还有一段对于字符串连接运算符 + 的规范描述:

If only one operand expression is of type String, then string conversion (§5.1.11) is performed on the other operand to produce a string at run time.
如果只有一个操作数表达式的类型是 String,那么在运行时会对另一个操作数进行字符串转换(参见§5.1.11),以生成一个字符串。
The result of string concatenation is a reference to a String object that is the concatenation of the two operand strings. The characters of the left-hand operand precede the characters of the right-hand operand in the newly created string.
字符串连接的结果是一个指向 String 对象的引用,该对象是两个操作数字符串的连接。左操作数的字符在新创建的字符串中位于右操作数的字符之前。
The String object is newly created (§12.5) unless the expression is a constant expression (§15.29).
该 String 对象是新创建的(参见§12.5),除非该表达式是一个常量表达式(参见§15.29)。
An implementation may choose to perform conversion and concatenation in one step to avoid creating and then discarding an intermediate String object. To increase the performance of repeated string concatenation, a Java compiler may use the StringBuffer class or a similar technique to reduce the number of intermediate String objects that are created by evaluation of an expression.
为了避免创建然后丢弃中间的 String 对象,一个实现可以选择在一步中执行转换和连接。为了提高重复字符串连接的性能,Java 编译器可以使用 StringBuffer 类或类似的技术来减少通过评估表达式创建的中间 String 对象的数量。
For primitive types, an implementation may also optimize away the creation of a wrapper object by converting directly from a primitive type to a string.
对于基本类型,实现也可以通过直接将基本类型转换为字符串来优化避免创建包装对象。

结合上文总结一下String的特点:

  1. 不可变:Java中的String类字符串是不可变的,一旦创建就不能更改。这意味着字符串可以被共享。

  2. 字符串字面量的共享:字符串字面量同样表示对同一String对象的引用。就是一样的字符串指向的是同一个实例对象地址。

    字符串字面量的共享?
    字符串字面量的共享与字符串常量密切相关。在Java中,字符串常量池是一种特殊的内存区域,用于存储字符串字面量。当编译器在代码中遇到字符串字面量时,它会首先检查常量池中是否已经存在相同内容的字符串。
    如果存在,则返回常量池中的字符串引用,而不是创建一个新的字符串对象。这就是为什么在相同类、相同包或不同包中的不同类中使用相同字符串字面量时,它们实际引用的是常量池中的同一个String对象。
    这种共享机制可以节省内存空间,并提高性能,因为不需要为相同的字符串内容创建多个对象。但需要注意的是,通过new关键字创建的字符串对象不会被存储在常量池中,因此它们不会享受这种共享机制。

  3. 字符串连接运算符"+"规则:

    • 如果只有一个操作数的表达式是字符串类型,则另一个操作数将在运行时进行字符串转换。
    • 字符串连接的结果是一个指向String对象的引用,该对象是两个操作数字符串的连接。
    • 除非表达式是常量表达式,否则将新创建String对象。

    常量表达式是指在Java中可以在编译时确定并且不会出现突然中断的表达式。它可以由以下内容组成:

    • 原始类型的字面量和字符串字面量
    • 到原始类型和String类型的转换
    • 一元操作符 +, -, ~, 和 !
    • 乘法操作符 *, /, 和 %
      其实就是定义的带指定值的变量。

为什么把字符串设置为不可变?

  1. 这种不可变新使得‘String’在多线程下更加安全,无需担心线程安全问题。
  2. 很多数据都是字符串存储或者交互的,对于某些情况(密码、网络协议、文件路径等)保证String对象不可变很重要,防止恶意篡改。
  3. 提高性能,由于String是不可变的,保证了hashcode的唯一性,于是在创建对象时其hashcode就可以放心的缓存了,不需要重新计算。

在Java中,当你使用字符串作为HashMap或者其他哈希表的键时,哈希表会根据键的hashCode来确定存储位置。如果两个字符串在逻辑上相等(内容相同),那么它们的hashCode也必须相等。
因为String是不可变的,所以在创建String对象时,它的hashCode可以被预先计算并缓存起来,而不必每次使用时都重新计算。这意味着如果你有两个相同内容的字符串,它们的hashCode会是相同的,因为它们实际上是同一个对象的引用。这种设计使得在哈希表中查找和操作字符串变得更加高效。
相反,如果字符串是可变的,那么在修改字符串时可能会导致hashCode的变化,这会影响到哈希表的性能,因为它需要重新计算并更新哈希表中的存储位置。
因此,String的不可变性确保了在哈希表等数据结构中使用字符串时的性能和稳定性。

  1. 节省内存:字符串常量池的设计,多个String引用对象指向同一个地址。

把字符串设计为不可变的原因很简单,安全(线程安全、数据安全)且性能高,但是,如果强行修改String的值也是有方法的,可以通过反射改变String对象的值,当然这种方法是不推荐的。

1.2 StringBuilder

上源码注释

A mutable sequence of characters. This class provides an API compatible with StringBuffer, but with no guarantee of synchronization. This class is designed for use as a drop-in replacement for StringBuffer in places where the string buffer was being used by a single thread (as is generally the case). Where possible, it is recommended that this class be used in preference to StringBuffer as it will be faster under most implementations.
StringBuilder是一个可变的字符序列,它提供了与StringBuffer兼容的API,但没有同步的保证。这个类被设计成可以在单个线程中使用,可以作为StringBuffer的替代品。在大多数情况下,建议使用StringBuilder代替StringBuffer,因为它在大多数实现下都会更快。
The principal operations on a StringBuilder are the append and insert methods, which are overloaded so as to accept data of any type. Each effectively converts a given datum to a string and then appends or inserts the characters of that string to the string builder. The append method always adds these characters at the end of the builder; the insert method adds the characters at a specified point.
StringBuilder的主要操作是append和insert方法,这两个方法被重载以接受任何类型的数据。每个方法都会将给定的数据转换为字符串,然后将该字符串的字符追加或插入到字符串构建器中。append方法总是将这些字符添加到构建器的末尾;insert方法在指定位置添加字符。
For example, if z refers to a string builder object whose current contents are “start”, then the method call z.append(“le”) would cause the string builder to contain “startle”, whereas z.insert(4, “le”) would alter the string builder to contain “starlet”.
In general, if sb refers to an instance of a StringBuilder, then sb.append(x) has the same effect as sb.insert(sb.length(), x).
例如,如果z引用一个当前内容为"start"的字符串构建器对象,那么方法调用z.append("le")会导致字符串构建器包含"startle",而z.insert(4, "le")会将字符串构建器改为包含"starlet"。
通常情况下,如果sb是一个StringBuilder的实例,那么sb.append(x)的效果与sb.insert(sb.length(), x)相同。
Every string builder has a capacity. As long as the length of the character sequence contained in the string builder does not exceed the capacity, it is not necessary to allocate a new internal buffer. If the internal buffer overflows, it is automatically made larger.
每个StringBuilder都有一个容量。只要字符串构建器中包含的字符序列的长度不超过容量,就不需要分配新的内部缓冲区。如果内部缓冲区溢出,它会自动变大。
Instances of StringBuilder are not safe for use by multiple threads. If such synchronization is required then it is recommended that StringBuffer be used.
Unless otherwise noted, passing a null argument to a constructor or method in this class will cause a NullPointerException to be thrown.
StringBuilder的实例不适合多线程使用。如果需要同步,建议使用StringBuffer。
除非另有说明,在这个类的构造函数或方法中传递一个null参数将导致抛出NullPointerException。
API Note:
StringBuilder implements Comparable but does not override equals. Thus, the natural ordering of StringBuilder is inconsistent with equals. Care should be exercised if StringBuilder objects are used as keys in a SortedMap or elements in a SortedSet. See Comparable, SortedMap, or SortedSet for more information.
API注意事项:
StringBuilder实现了Comparable接口,但没有重写equals方法。因此,StringBuilder的自然排序与equals方法不一致。如果StringBuilder对象用作SortedMap中的键或SortedSet中的元素,则应注意。有关更多信息,请参阅Comparable、SortedMap或SortedSet。

总结一下StringBuilder:

  1. 可变性:StringBuilder对象的内容可以动态修改,包括追加、插入、删除等操作

  2. 非线程安全

  3. 性能高:在单线程环境下,通常比StringBuffer性能好

  4. 没有重写equals方法

  5. 容量:当字符序列长度超过容量时,会自动扩容

  6. 可链式操作:StringBuilder的方法通常返回StringBuilder自身,可以进行链式调用,简化代码。

    链式操作是一种编程风格,允许在一个对象上连续调用多个方法,而不是每次调用都需要单独处理对象。对于StringBuilder来说,它的方法通常会返回自身的引用(即StringBuilder对象),这样就可以在同一个语句中连续调用多个方法。例如:
    StringBuilder sb = new StringBuilder(); sb.append("Hello").append(" ").append("world").append("!");
    在同一行代码中连续调用多次append方法来构建字符串,这就是可链式操作。

1.StringBuffer

A thread-safe, mutable sequence of characters. A string buffer is like a String, but can be modified. At any point in time it contains some particular sequence of characters, but the length and content of the sequence can be changed through certain method calls.
StringBuffer是Java中的一个类,用于处理可变的字符序列。它类似于String,但可以被修改。在任何时刻,它包含某个特定的字符序列,但是序列的长度和内容可以通过某些方法调用进行更改。
String buffers are safe for use by multiple threads. The methods are synchronized where necessary so that all the operations on any particular instance behave as if they occur in some serial order that is consistent with the order of the method calls made by each of the individual threads involved.
StringBuffer是线程安全的,可以被多个线程安全使用。方法在必要时进行同步,以便某个特定实例上的所有操作表现得好像它们发生在与每个涉及的单个线程所做的方法调用顺序一致的某个串行顺序中。
As of release JDK 5, this class has been supplemented with an equivalent class designed for use by a single thread, StringBuilder. The StringBuilder class should generally be used in preference to this one, as it supports all of the same operations but it is faster, as it performs no synchronization.
从JDK 5版本开始,StringBuffer类已经补充了一个设计用于单线程使用的等效类StringBuilder。一般应优先使用StringBuilder类,因为它支持所有相同的操作,但速度更快,因为它不进行同步。

其它的内容和StingBuilder大同小异
StringBuffer的特点总结如下:

  1. 可变性:StringBuffer对象的内容可以动态修改,包括追加、插入、删除等操作
  2. 线程安全
  3. 性能较低:在单线程环境下,通常比StringBuilder性能低
  4. 没有重写equals方法
  5. 可链式操作:可以进行链式调用,简化代码。

2.String、StringBuilder和StringBuffer对比

String、StringBuilder和StringBuffer是Java中处理字符串的三种主要方式。它们之间的主要区别如下:

  1. 可变性:
    • String:不可变的,一旦创建就不能被修改。
    • StringBuilder:可变的,可以通过方法调用来修改字符串的内容。
    • StringBuffer:可变的,可以通过方法调用来修改字符串的内容。
  2. 线程安全性:
    • String:不是线程安全的。——使用反射修改
    • StringBuilder:不是线程安全的,适用于单线程环境。
    • StringBuffer:是线程安全的,适用于多线程环境。
  3. 性能:
    • String:由于不可变性,每次对字符串进行操作都会创建一个新的字符串对象,性能较差。
    • StringBuilder:由于可变性,并且不是线程安全的,性能较好,适合在单线程环境中频繁操作字符串。
    • StringBuffer:性能比StringBuilder差,因为它是线程安全的,需要进行额外的同步操作。
  4. 使用场景:
    • String:适用于字符串不经常变化的情况,如字符串常量、枚举等。
    • StringBuilder:适用于单线程环境下需要频繁进行字符串操作的情况。
    • StringBuilder:适用于单线程环境下需要频繁进行字符串操作的情况。

2.面试中字符串常问问题

  1. String类型定义的字符串可以修改吗?为什么要这样设计
  2. String、StringBuffer和StringBuilder类的区别
  3. 给你一段代码,让你判断创建了多少个对象或者输出结果

大概说一下答案:

  1. String类型定义的字符串可以修改吗?为什么要这样设计
    String类型是不可变的,它被final修饰,意味着当你修改一个‘String’对象的值时,实际上是创建了一个新的‘String’对象,让引用对象指向了新的实例对象。
    当然,如果想强行改变一个String类型的字符串,可以,使用反射。
    为什么要把String设计为不可变呢?
    1. 这种不可变新使得‘String’在多线程下更加安全,无需担心线程安全问题。
    2. 很多数据都是字符串存储或者交互的,对于某些情况(密码、网络协议、文件路径等)保证String对象不可变很重要,防止恶意篡改。
    3. 提高性能,由于String是不可变的,保证了hashcode的唯一性,于是在创建对象时其hashcode就可以放心的缓存了,不需要重新计算。
    4. 节省内存:字符串常量池的设计,多个String引用对象指向同一个地址。

  2. String、StringBuffer和StringBuilder类的区别
    String:对象不可变,如果需要拼接字符串,每次进行拼接时都需要生成一个新的字符串对象,然后将指针指向新对象,在大量拼接情况下,对性能影响较大
    StringBuilder:对象可变但线程不安全,需要大量拼接时使用
    StringBuffer:对象可变并且线程安全,需要大量拼接并且要求线程安全情况下使用
    大量字符串拼接时,一般来说,修改字符串时性能高->低:StringBuilder>StringBuffer>String

  3. 给你一段代码,让你判断创建了多少个对象或者输出结果
    这个问题,一要了解字符串常量池是怎么运行的,二要了解编译器对字符串拼接的优化规则

    • 字符串常量池
      字符串常量对象存储在字符串常量池中,每次新建一个对象,都先查找字符串常量池,如果找到一样的字符串,只需要把引用对象地址指向这个字符串实例即可。如果找不到,在字符串常量池新建一个新的字符串实例,再把引用对象地址指向该实例。
    • 编译器对字符串常量的拼接优化
      1. javac编译对字符串常量直接相加表达式进行优化,如String c=“a”+“b”,会被优化成“ab”,只在字符串常量池生成一个对象
      2. 常量表达式中的字符串拼接,如 final String c = “c”; String result =“b” + c;,也会被优化为 “bc”,因为确定了c不可再赋值,只在字符串常量池生成一个对象。
      3. 如果是变量和常量相加,会new一个String对象,然后引用对象指向新new的对象地址。举例,String b = “b”;String a=b+“c”,会生成一个新的字符串对象,a指向新的字符串对象地址。
    • String中的intern()方法返回的是引用地址。

    理解上面几个规则,就可以快速判断字符串创建的对象以及指向地址的问题:

    public static void main(String[] args) {
        String a = new String("a");
        String b = "a";
        String c = "ad";
        String d= b+"d";
        String e = "a"+"d";
        final String f = "a";
        String g = f+"d";
        String intern = c.intern();
        String intern1 = a.intern();
        System.out.println(a == b);//new 会创建一个新的对象,a指向改对象地址,false
        System.out.println(c == d);// 变量+常量会生成一个新对象,d指向新对象,false
        System.out.println(c == e);//两个常量相加,会自动优化”+“,变成ad,true
        System.out.println(g == e);//final修饰的f,确认不可以再赋值,f+"d"自动优化,true
        System.out.println(intern1==intern);//intern()方法返回的是引用地址,false
    } 
    
  • 11
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值