编程知识 cdmana.com

我一个5年的Java程序员居然倒在了String面试上……

本文是我和xqnode联合创作,已收录至我们的GitHub,欢迎大家给个Star:https://github.com/nxJava/nx_java

我们会持续更新,欢迎监督!

微信搜索:武哥聊编程,关注这个Java菜鸟~

今天跟大家聊聊Java中的String,这里面门道还挺多的,由于本人能力有限,如果有写的不对的地方,欢迎大家给我指正,谢谢支持~


1. 看看源码

大家都知道, String 被声明为 final,因此它不可被继承。(Integer 等包装类也不能被继承)。我们先来看看 String 的源码。

在 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[];
}

在 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 不可变。

2. 不可变有什么好处呢

2.1 可以缓存 hash 值

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

2.2 String Pool 的使用

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

2.3 安全性

String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 的那一方以为现在连接的是其它主机,而实际情况却不一定是。

2.4 线程安全

String 不可变性天生具备线程安全,可以在多个线程中安全地使用。

3. 再来深入了解一下 String

3.1 “+” 连接符

字符串对象可以使用“+”连接其他对象,其中字符串连接是通过 StringBuilder(或 StringBuffer)类及其 append 方法实现的,对象转换为字符串是通过 toString 方法实现的。可以通过反编译验证一下:

/**
 * 测试代码
 */
public class Test {
    
    public static void main(String[] args) {
    
        int i = 10;
        String s = "abc";
        System.out.println(s + i);
    }
}

/**
 * 反编译后
 */
public class Test {
    
    public static void main(String args[]) {
        //删除了默认构造函数和字节码
        byte byte0 = 10;      
        String s = "abc";      
        System.out.println((new StringBuilder()).append(s).append(byte0).toString());
    }
}

由上可以看出,Java中使用"+"连接字符串对象时,会创建一个StringBuilder()对象,并调用append()方法将数据拼接,最后调用toString()方法返回拼接好的字符串。那这个 “+” 的效率怎么样呢?

3.2 “+”连接符的效率

使用“+”连接符时,JVM会隐式创建StringBuilder对象,这种方式在大部分情况下并不会造成效率的损失,不过在进行大量循环拼接字符串时则需要注意。比如:

String s = "abc";
for (int i=0; i<10000; i++) {
    
    s += "abc";
}

这样由于大量StringBuilder创建在堆内存中,肯定会造成效率的损失,所以在这种情况下建议在循环体外创建一个StringBuilder对象调用append()方法手动拼接(如上面例子如果使用手动拼接运行时间将缩小到1/200左右)。

与此之外还有一种特殊情况,也就是当"+"两端均为编译期确定的字符串常量时,编译器会进行相应的优化,直接将两个字符串常量拼接好,例如:

System.out.println("Hello" + "World");

/**
 * 反编译后
 */
System.out.println("HelloWorld");

4. 字符串常量

4.1 为什么使用字符串常量?

JVM为了提高性能和减少内存的开销,在实例化字符串的时候进行了一些优化:使用字符串常量池。每当创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。由于String字符串的不可变性,常量池中一定不存在两个相同的字符串

4.2 实现字符串常量池的基础

  • 实现该优化的基础是因为字符串是不可变的,可以不用担心数据冲突进行共享。

  • 运行时实例创建的全局字符串常量池中有一个表,总是为池中每个唯一的字符串对象维护一个引用,这就意味着它们一直引用着字符串常量池中的对象,所以,在常量池中的这些字符串不会被垃圾收集器回收。

我们来看个小例子,了解下不同的方式创建的字符串在内存中的位置:

String string1 = "abc";   // 常量池
String string2 = "abc";     // 常量池
String string3 = new String("abc");  // 堆内存

5. String类常见的面试题

5.1 判断字符串s1和s2是否相等

public static void main(String[] args) {
    
    String s1 = "123";
    String s2 = "123";
    String s3 = "1234";
    String s4 = "12" + "34";
    String s5 = s1 + "4";
    String s6 = new String("1234");
    System.out.println(s1 == s2);   // true
    System.out.println(s1.equals(s2));  //true
    System.out.println(s3 == s4);   //true
    System.out.println(s3 == s5);   // false
    System.out.println(s3.equals(s5)); //true
    System.out.println(s3 == s6);   // false
}

解析:

  • s1和s2:

    String s1 = "123";先是在字符串常量池创建了一个字符串常量“123”,“123”常量是有地址值,地址值赋值给s1。接着声明 String s2=“123”,由于s1已经在方法区的常量池创建字符串常量"123",进入常量池规则:如果常量池中没有这个常量,就创建一个,如果有就不再创建了,故直接把常量"123"的地址值赋值给s2,所以s1==s2为true。

    由于String类重写了equals方法,s1.equals(s2)比较的是字符串的内容,s1和s2的内容都是"123",故s1.equals(s2)为true。

  • s3和s4:

    s3创建了一个新的字符串"1234",s4是两个新的字符串"12"和"34"通过"+“符号连接所得,根据Java中常量优化机制, “12” 和"34"两个字符串常量在编译期就连接创建了字符串"1234”,由于字符串"1234"在常量池中存在,故直接把"1234"在常量池的地址赋值给s4,所以s3==s4为true。

  • s3和s5:

    s5是由一个变量s1连接一个新的字符串"4",首先会在常量池创建字符串"4",然后进行"+“操作,根据字符串的串联规则,s5会在堆内存中创建StringBuilder(或StringBuffer)对象,通过append方法拼接s1和字符串常量"4”,此时拼接成的字符串"1234"是StringBuilder(或StringBuffer)类型的对象,通过调用toString方法转成String对象"1234",所以s5此时实际指向的是堆内存中的"1234"对象,堆内存中对象的地址和常量池中对象的地址不一致,故s3==s5为false。

    看下JDK8的API文档里的解释:

    不管是常量池还是堆,只要是使用equals比较字符串,都是比较字符串的内容,所以s3.equals(s5)为true。

    Java常量优化机制:给一个变量赋值,如果等于号的右边是常量,并且没有一个变量,那么就会在编译阶段计算该表达式的结果,然后判断该表达式的结果是否在左边类型所表示范围内,如果在,那么就赋值成功,如果不在,那么就赋值失败。但是注意如果一旦有变量参与表达式,那么就不会有编译期间的常量优化机制

  • s3和s6:

    String s6 = new String("1234");在堆内存创建一个字符串对象,s6指向这个堆内存的对象地址,而s3指向的是字符串常量池的"1234"对象的地址,故s3==s6为false。

5.2 创建多少个字符串对象?

String s0 = "123";
String s1 = new String("123"); 
String s2 = new String("1" + "2");
String s3 = new String("12") + "3";

解析:

  • s0:

    符串常量池对象:“123”,1个;

    共1个。

  • s1:

    字符串常量池对象:“123”,1个;

    堆对象:new String(“123”),1个;

    共2个。

  • s2:

    字符串常量池对象:“1”、“2”、“12”,3个;

    堆对象:new String(“12”),1个

    共4个。

  • s3:

    字符串常量池对象:“12”、“3”、“123”,3个,

    堆对象:new String(“12”),1个;

    共4个。

总结:

new String()是在堆内存创建新的字符串对象,其构造参数中可传入字符串,此字符串一般会在常量池中先创建出来,new String()创建的字符串是参数字符串的副本,看下API中关于String构造器的解释:

所以new String()的方式创建字符串百分百会产生一个新的字符串对象,而类似于"123"这样的字符串对象则需要在创建之前看常量池中有没有,有的话就不创建,没有则创建新的对象。 "+"操作符连接字符串常量的时候会在编译器直接生成连接后的字符串,若该字符串在常量池已经存在,则不会创建新的字符串;连接变量的话则涉及StringBuilder等字符串构建器的创建,会在堆内存生成新的字符串对象。


如果觉得有帮助,希望老铁们来个三连击,给更多的人看到这篇文章

1、关注我的原创微信公众号「武哥聊编程」,专注于Java、数据结构和算法、微服务、中间件等技术分享,保证你看完有所收获。

2、给俺点个赞呗,可以让更多的人看到这篇文章,顺便激励下我继续写作,嘻嘻。

作者info

【作者】:武哥
【公众号】:武哥聊编程。欢迎大家关注~
【作者简介】:同济大学,硕士。先后在华为、科大讯飞、拼多多采坑。一个自学 Java 的菜鸟,期待你的关注。

点赞是对我最大的鼓励
↓↓↓↓↓↓

版权声明
本文为[武哥聊编程]所创,转载请带上原文链接,感谢
https://blog.csdn.net/eson_15/article/details/106680589

Scroll to Top