【面试题】在面试中问到的 String 相关

281 阅读7分钟

这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战

常见面试题

  • 为什么 String 类型要用 final 修饰?
  • == 和 equals 的区别是什么?
  • String 和 StringBuilder、StringBuffer 有什么区别?
  • new String("abc") 创建了几个对象?
  • String 的 intern() 方法有什么含义?

1、为什么 String 类型要用 final 修饰?

从 String 类的源码我们可以看出 String 是被 final 修饰的不可继承类(Integer 等包装类也不能被继承)。

在 Java 8 中,String 内部使用 char 数组存储数据。

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
    ...
}

在 Java 9 之后,String 类的实现改用 byte 数组存储字符串,同时使用 coder 来标识使用了哪种编码。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    
    /** The value is used for character storage. */
    private final byte[] value;

    /** The identifier of the encoding used to encode the bytes in {@code value}. */
    private final byte coder;
    ...
}

value 数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组。并且 String 内部没有改变 value 数组的方法,因此可以保证 String 不可变。

不可变的好处

1、String Pool 的需要

如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。

2、缓存 hash 值

因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。

3、安全性

Java 在每个服务级别都提供了一个安全的环境,而 String 在维护整个安全方面至关重要。字符串已被广泛用作许多Java 类的参数,例如用于打开网络连接、在Java 中读取文件和目录以及用于打开数据库连接。如果 String 不是不可变的,那么它可能会带来严重的安全问题,例如

  • 用户可能已授权访问系统中的特定文件,但在身份验证后,他/她可以将 PATH 更改为其他内容,这可能会导致严重的安全问题。
  • 在连接到数据库或网络中的任何其他机器时,改变 String 值可能会带来安全威胁。
  • 可变字符串也可能导致反射中的安全问题,因为参数是字符串。

4、线程安全

由于 String 是不可变的,它可以安全地在多个线程之间共享,这在多线程编程环境中非常重要,并且可以避免 Java 中的任何同步问题。不变性还使 String 实例线程安全,这意味着您不需要在外部同步 String 操作。关于 String 的另一个需要注意的重点是 SubString 引起的内存泄漏,这不是线程相关的问题,而是需要注意的事情。

String 不可变或 final 的缺点

由于 String 是不可变的,它会产生大量的临时使用和抛出对象,这给垃圾收集器带来了压力。Java 设计者已经考虑过,将 String 字面量存储在池中是他们减少 String 垃圾的解决方案。但是仍然必须小心创建 String 对象。此外,平均而言,Java 应用程序会生成过多的垃圾。由于String pool位于Java Heap的PermGen Space,与Java Heap相比非常有限,过多的String文字会很快填满这个空间,导致java.lang.OutOfMemoryError: PermGen Space。

2、== 和 equals 的区别是什么?

== 对于基本数据类型来说,是用于比较 “值”是否相等的;而对于引用类型来说,是用于比较引用地址是否相同的。

3、String 和 StringBuilder、StringBuffer 有什么区别?

1. 可变性

  • String 不可变
  • StringBuffer 和 StringBuilder 可变

2. 线程安全

  • String 不可变,因此是线程安全的
  • StringBuilder 不是线程安全的
  • StringBuffer 是线程安全的,内部使用 synchronized 进行同步

StackOverflow : String, StringBuffer, and StringBuilder

4、new String("abc") 创建了几个对象?

使用这种方式一共会创建两个字符串对象(前提是 String Pool 中还没有 "abc" 字符串对象)。

  • "abc" 属于字符串字面量,因此编译时期会在 String Pool 中创建一个字符串对象,指向这个 "abc" 字符串字面量;
  • 而使用 new 的方式会在堆中创建一个字符串对象。

blog.csdn.net/wo541075754…

5、String 的 intern() 方法有什么含义?

这个intern方法经历了几代的优化,其作用就是,如果常量池中不存在这个字符串,就将字符串放入常量池,如果存在直接返回常量池中的这个字符串引用。利用intern方法能大大提高性能,例如在fastjson这个序列化库,就用了很多这个机制。因为字段名基本不变,所以通过将字段名动态缓存到常量池对性能提升很明显。 对于这个常量池:

  • 由于 Java 6 中使用固定的内存大小(PermGen)因此不要使用 String.intern() 方法
  • Java7 和 8 在堆内存中实现字符串池。这以为这字符串池的内存限制等于应用程序的内存限制。 在 Java 7 和 8 中使用 -XX:StringTableSize 来设置字符串池 Map 的大小。它是固定的,因为它使用 HashMap 实现。近似于你应用单独的字符串个数(你希望保留的)并且设置池的大小为最接近的质数并乘以 2 (减少碰撞的可能性)。它是的 String.intern 可以使用相同(固定)的时间并且在每次插入时消耗更小的内存(同样的任务,使用java WeakHashMap将消耗4-5倍的内存)。
  • 在 Java 6 和 7(Java7u40以前) 中 -XX:StringTableSize 参数的值是 1009。Java7u40 以后这个值调整为 60013 (Java 8 中使用相同的值)

深入解析String#intern

拓展1:不可变对象

为了满足不可变对象,Java语言要求遵守以下5条原则:

  1. 类内部所有的字段都是final修饰的
  2. 类内部所有的字段都是私有的,也就是private修饰
  3. 类不能够被集合和拓展
  4. 类不能对外提供那些能够修改内部状态的方法,setter方法也不行
  5. 类内部的字段如果是引用,也就是说可以指向可变对象,但我们不能获取这个对象

从源码可以看出,String满足不可变对象的5条原则,源码解析:

  1. String类被final修饰,说明String类绝不可能被继承了,——也就是任何对String的操作方法,都不会被继承覆写。
  2. String 中保存数据的是一个char的数组value,同样也是被final修饰,——也就是value一旦被赋值,内存地址是绝对无法修改的
  3. value的权限是私有的,外部绝对访问不到
  4. String也没有开放出可以对value进行赋值的方法

综上,value一旦产生,内存地址就根本无法被修改。

拓展2:String常量池(String pool)

常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。 例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。 (1)节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。 (2)节省运行时间:比较字符串时,== 比equals()快。对于两个引用变量,只用 ==判断引用是否相等,也就可以判断实际值是否相等。

在 Java 7 之前,String Pool 被放在运行时常量池中,它属于永久代。而在 Java 7,String Pool 被移到堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。

详细参考:触摸java常量池

拓展3:String真的不可变吗?

既然String是不可变的,好像内部还有很多substring, replace, replaceAll这些操作的方法。好像都是对String对象改变了,解释起来也很简单,我们每次的replace这些操作,其实就是在堆内存中创建了一个新的对象。然后我们的value指向不同的对象罢了。

在jdk1.8中,可通过反射改变 String:

String str = "Tyron";
System.out.println("反射前:" + str);
try {
    Field field = String.class.getDeclaredField("value");
    field.setAccessible(true);
    char[] value = (char[]) field.get(str);
    value[0] = 't';
	System.out.println("反射后:" + str);
} catch (NoSuchFieldException | IllegalAccessException e) {
	e.printStackTrace();
}

但在 Java9之后运行均会报错。

stackoverflow.com/questions/4…

参考

Why String is immutable or final in Java?

JDK核心JAVA源码解析

Java中的String为什么是不可变的? -- String源码分析

CS-Notes-String