【Java基础】面试官:Java 对象是值传递还是引用传递?

2023-11-09 17:55:00 浏览数 (2)

导读

  • String、StringBuffer、StringBuilder 的区别以及如何正确使用
  • 如何理解 String 不可变?
  • String、StringBuffer、StringBuilder 在 JVM 内存中的分布,以及面试常问:Java 不同字符类会创建几个对象?

版本及环境

  • JDK 运行版本:JDK8

String、StringBuffer、StringBuilder

  • String 是不可变字符串,StringBuffer、StringBuilder 是可变字符串。
  • StringBuffer 是线程安全的,StringBuilder 是线程非安全的。StringBuilder 具有 StringBuffer 一切能力,当不涉及多线程安全时,优先使用 StringBuilder,没有同步操作,性能更好。

如何理解 String 不可变

  • 即无法通过引用地址修改 String 对象的值。
代码语言:Java复制
// String 类
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
    ....
}
1、value[] 数组是 final 类型,表示 value 的引用地址不可变,但是 Arrary 数组是可变的
    final char[] charArray = {'a', 'b', 'c', 'd', 'e'};

    char ch = 'z';
    charArray[0] = ch;
    System.out.println(charArray);
2、但是由于 value[] 数组私有,且没有提供外部访问能力,因此无法修改

String 不可变的好处

String HashCode缓存

  • String的HashCode在比如HashMap等容器当中都有使用,String的不变性保证了HashCode的不变性,不必每次去计算新的HashCode,这也是Map喜欢将String作为Key的原因,这是一种性能考虑。

安全性

  • String被许多的Java类(库)用来当做参数,例如 网络连接地址URL,文件路径path,还有反射机制所需要的String参数等, 假若String不是固定不变的,将会引起各种安全隐患,比如使用的url被修改。

可变 StringBuilder vs StringBuffer 与线程安全

  • StringBuilder、StringBuffer 都是可变的,StringBuffer 是线程安全的,StringBuilder 是线程非安全的。StringBuilder 具有 StringBuffer 一切能力,当不涉及多线程安全是,优先使用 StringBuilder,没有同步操作,性能更好。
代码语言:Java复制
    // 继承 AbstractStringBuilder 实现不同
    @Override
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }
    
    @Override
    public StringBuilder append(String str) {
        super.append(str);
        return this;
    }

字符常量池

  • Java 内部维护了一个字符常量池,当创建一个字符 String 时,如果已经在字符常量池中存在,则使用该存在该变量,而不会创建新的。

思考一下

  • 下面的代码你可以说出它们的创建过程,以及在 JVM 内存中的分配?
代码语言:Java复制
        // 在常量池中
        String str1 = "abc";

        // 在堆上
        String str2 = new String("abc");

        // 字符串常量   号的重载 编译时合并成 abcdef 字符串
        String str3 = "abc"   "def";

        String str4 = "abc"   new String("def");

        String str41 = new String("def")   "abc";

        String str5 = new String("abc")   new String("def");

        String str6 = "defabc";

""、new String()、new StringBuffer()、new StringBuilder() 与字符串常量池

  • 从上面可以看出,""、new String()、StringBuffer()、StringBuilder() 创建字符串的过程和结果并不一样,我们分为以下几种情况讨论:

"" 和 new String()

代码语言:Java复制
String str1 = "abc";
String str2 = "abc";
// 直接在字符串常量池中查找,如果存在直接使用,不存在创建,即str1、str2指向常量池中的同一个值

String str2 = new String("abc");
// 在堆中创建一个 String 对象,判断 abc 在字符串常量是否存在,存在char数组直接指向,不存在创建新的字符串常量再指向
// 上述检查常量池是否有相同Unicode的字符串常量时,使用的方法是String中的intern()方法

StringBuffer() 和 StringBuilder()

  • 两者都是在堆中创建对象,并不涉及字符串常量池,char 数组维护在堆的对象中。

拓展: 当两个字符串连接时,JVM 底层是如何实现的

代码语言:Java复制
        // 字符串常量   号的重载 编译时优化合并成 abcdef 字符串
        String str3 = "abc"   "def";

        String str4 = "abc"   new String("def");

        String str41 = new String("def")   "abc";

        String str5 = new String("abc")   new String("def");
        // 上面三种情况,都会先创建一个 StringBuilder 对象再进行字符串拼接,最后调用toString()方法创建一个 String 对象
        // 注意: 在这里创建 String 时,并没有在字符串常量池中创建,而是直接指向了 StringBuilder 的 value 数组
        // @Override
        // public String toString() {
        // Create a copy, don't share the array
        //    return new String(value, 0, count);
        // }
        
        String str6 = "defabc";
        // 因此,上述 String str6 = "defabc" 会在字符串常量池中创建 "defabc" 常量

字节码分析

完整源码

代码语言:Java复制
public class MainTest {
    public static void main(String[] args) throws InterruptedException {
        // 在常量池中
        String str1 = "abc";

        // 在堆上
        String str2 = new String("abc");

        // 字符串常量   号的重载 编译时合并成 abcdef 字符串
        String str3 = "abc"   "def";

        String str4 = "abc"   new String("def");

        String str41 = new String("def")   "abc";

        String str5 = new String("abc")   new String("def");

        String str6 = "defabc";
    }
}

字节码

  • 使用 javap 命令查看字节码
代码语言:c复制
// javap -c .MainTest.class
Compiled from "MainTest.java"
public class org.example.MainTest {
  public org.example.MainTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]) throws java.lang.InterruptedException;
    Code:
---------------------------------------------String str1 = "abc"; --------------------------------------------------------------------------
       0: ldc           #2                  // String abc
       2: astore_1
---------------------------------------------String str2 = new String("abc");---------------------------------------------------------------  
       3: new           #3                  // class java/lang/String // 创建 String 对象
       6: dup
       7: ldc           #2                  // String abc
       9: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
      12: astore_2
---------------------------------------------String str3 = "abc"   "def";-------------------------------------------------------------------      
      13: ldc           #5                  // String abcdef  编译时优化为 "abcdef"
      15: astore_3
---------------------------------------------String str4 = "abc"   new String("def");-------------------------------------------------------
      16: new           #6                  // class java/lang/StringBuilder // 创建 StringBuilder
      19: dup
      20: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V
      23: ldc           #2                  // String abc
      25: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; //拼接 abc
      28: new           #3                  // class java/lang/String 创建 String 对象(def)
      31: dup
      32: ldc           #9                  // String def
      34: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
      37: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; //拼接 def 
      40: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; // 调用 toString 方法生成一个新 String 对象
      43: astore        4
--------------------------------------------------------------------------------------------------------------------------------------------
      45: new           #6                  // class java/lang/StringBuilder
      48: dup
      49: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V
      52: new           #3                  // class java/lang/String
      55: dup
      56: ldc           #9                  // String def
      58: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
      61: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      64: ldc           #2                  // String abc
      66: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      69: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      72: astore        5
      74: new           #6                  // class java/lang/StringBuilder
      77: dup
      78: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V
      81: new           #3                  // class java/lang/String
      84: dup
      85: ldc           #2                  // String abc
      87: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
      90: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      93: new           #3                  // class java/lang/String
      96: dup
      97: ldc           #9                  // String def
      99: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
     102: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
     105: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
     108: astore        6
     110: ldc           #11                 // String defabc
     112: astore        7
     114: return
}

总结

  • 当用于 url、路径地址 等需要保证不可以变的使用 String,可变的场景下需要保证线程安全使用 StringBuffer,不需要保证线程安全使用 StringBuilder,减少同步加锁开销。
  • 常见面试题:不同使用字符串的方式会创建多少个对象:
代码语言:java复制
案例一:
String str3 = "abc"   "def";

编译阶段优化为 “abcdef” 在常量池创建一个对象 “abcdef” 

案例二:
String str4 = "abc"   new String("def")

"abc" 在常量池创建一个对象
new String("def")  在堆中创建一个 String 对象,常量池创建对象 "def"
创建一个 StringBuilder 对象用于拼接两个字符串,最后调用 toString() 方法创建一个 String 对象

// 即在不考虑字符串常量池已存在常量字符串的情况,需要创建 5 个对象
// 再思考一下:其实还有一个强引用对象 str4 对创建字符串的强引用

个人简介

0 人点赞