写点什么

阿里面试 问我字符串

用户头像
java金融
关注
发布于: 2020 年 11 月 28 日

引言

众所周知在 java 里面除了8种基本数据类型的话,还有一种特殊的类型 String,这个类型是我们每天搬砖都基本上要使用它。

String 类型可能是 Java 中应用最频繁的引用类型,但它的性能问题却常常被忽略。高效的使用字符串,可以提升系统的整体性能。当然,要做到高效使用字符串,需要深入了解其特性。

String 类

我们可以看下 String 类的源码:

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

从源码上我们是不是可以发现String类是被final关键字所修饰的,String类的数据是通过char[] 数组来存储的。数组也是被final修饰的所以String 对象是不可被更改的。接下来我们再看看 String 的一些方法:像 concat、replace、substring 等都是返回了一个新的new String感兴趣的可以去看看 String 的一些常见方法。当我们执行这些方法之后最原始的字符串是没有改变的,都是返回新的字符串。

 public static void main(String[] args) {        String str = new String("java金融");        String str1 = str.substring(0, 4);        String str2 = str.concat("公众号");        String str3 = str.replace("java金融", "关注:【java金融】");        // 还有其他的方法        System.out.println(str1);        System.out.println(str2);        System.out.println(str3);        System.out.println(str);    }
复制代码

输出结果

javajava金融公众号关注:【java金融】java金融
复制代码

所以我们只要记住一点:“String对象一旦被创建就是固定不变的了,

String对象的任何改变都不影响到原对象,相关的任何change操作都会生成新的对象”。

字符串常量池

JVM中,为了减少字符串对象的重复创建,维护了一块特殊的内存空间,这块内存就被称为全局字符串常量池(string pool也有叫做string literal pool)。

字符串常量池的位置

字符串常量池所在的位置也是跟不同的jdk版本有关系的。

  • JDK6及之前字符串常量池存放在方法区, 此时hotspot虚拟机对方法区的实现为永久代。

  • JDK7字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是hotspot中的永久代。

  • JDK8 hotspot移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆里只不过把方法区的实现从永久代变成了元空间(Metaspace) 。

String# intern

String::intern()是一个本地方法,它的作用是如果字符串常量池中已经包含一个等于此 String 对象的字符串,则返回代表池中这个字符串的 String 对象的引用;

否则,会将此 String 对象包含的字符串添加到常量池中,并且返回此 String 对象的引用。


上述定义出自《深入理解 Java 虚拟机:JVM 高级特性与最佳实践(第 3 版)》我们知道了这个 String::intern()这个方法的作用下面来看几道并没有什么用的题目看看你是否都能够回答对?

        String str2 = new String("java") + new String("金融"); // 1         str2.intern(); // 2         String str1 = "java金融"; // 3         System.out.println(str2 == str1);
复制代码

这个代码在JDK6中输出结果是false,在jdk7输出是true

为何会因为不同的jdk版本输出结果不一样,因为不同版本字符串常量池的位置发生了变化。

下面来分析下为何会产生这种差异。

字符串虽然不属于基本数据类型但是它也可以想基本类型一样,直接通过字面量来赋值,同时也是可以通过new 来生成字符串对象。通过字面量赋值的方式和new 的方式 生成字符串还是有区别的。

  • 字面量赋值:通过字面量赋值(使用双引号声明出来的String)会先去常量池中查找是否已经有相同的字符串,如果已经存在栈中的引用直接指向该字符串,如果不存在就在常量中生成一个字符串再将栈中的引用指向该字符串。

  • new 的方式创建:而通过new的方式创建字符串时,就直接在堆中生成一个字符串的对象栈中的引用指向该对象。对于堆中的字符串对象,可以通过 intern() 方法来将字符串添加的常量池中,并返回指向该常量的引用。

jdk6 结果是false,是因为常量池是在永久代的 Perm 区和 java 堆是两个区域。所以两个区域的对象地址比较是不同的。

JDK7结果是true, 这个原因主要是从JDK 7及以后,HotSpot 将常量池从永久代移到了堆,正因为如此,JDK7 及以后的intern方法在实现上发生了比较大的改变,JDK7及以后,intern 方法还是会先去查询常量池中是否有已经存在,如果存在,则返回常量池中的引用,这一点与之前没有区别,区别在于如果在常量池找不到对应的字符串则不会再将字符串拷贝到常量池,而只是在常量池中生成一个对原字符串的引用。所以为什么返回 true 是因为执行完标号为 1 的时候常量池中没有"java 金融"对象的,接下来标号为 2 的时候 会在常量池生成一个“java 金融”的对象会直接存一个对堆中“*java 金融*”的引用,标号为 3:进行字面量赋值的时候常量池已经存在了所以直接返回该引用。所以都是指向堆中的字符串返回 true

如果把 3 行代码放到第一行上面结果又不一样了,感兴趣的可以动手试一试并且分析下原因哦。

string 常见性能优化

使用+号拼接字符串

字符串拼接是我们平时在代码中使用最频繁的了。

  • +号拼接静态字符串

   String str = "关注"+"公众号:"+"java金融";
复制代码

我们可以通过反编译查看下上述代码:

 public static void main(java.lang.String[]);    descriptor: ([Ljava/lang/String;)V    flags: ACC_PUBLIC, ACC_STATIC    Code:      stack=1, locals=2, args_size=1         0: ldc           #2                  // String 关注公众号:java金融         2: astore_1         3: return      LineNumberTable:        line 11: 0        line 12: 3}
复制代码

我们可以发现编译器直接帮我们优化了,直接生成了一个字符串“关注公众号:java 金融” 并没有生成中间变量的String实例。如果我们上述代码稍微变化下

   public static void main(String[] args) {        String str ="关注";        String str1 = str + "公众号:java金融";    }
stack=2, locals=3, args_size=1 0: ldc #2 // String 关注 2: astore_1 3: new #3 // class java/lang/StringBuilder 6: dup 7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V 10: aload_1 11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 14: ldc #6 // String 公众号:java金融 16: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 19: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 22: astore_2 23: return LineNumberTable: line 11: 0 line 12: 3 line 13: 23
复制代码

从反编译代码中我们会发现生成了StringBuilder对象来进行追加。

  • 所以String + 拼接变量的时候底层是通过StringBuilder来实现的,我们循环操作拼接字符串的时候也应当使用StringBuilder替代+,否则的话每一次循环都会创建

一个StringBuilder 对象。

  • 对于静态字符串的拼接操作,Java 在编译时会进行彻底的优化,会把多个拼接字符串在编译时合成一个单独的长字符串。

常见字符串经典面试题

关于字符串最常见的面试题,面试宝典常见的题目。

String s = new String("xyz") 创建了多少个实例?

一般的回答都会是 2 个,(一个是“xyz”,一个是指向“xyz”的引用对象 s)

答案并没有那么简单哦,可以看看大佬的回答还是非常精彩的。

连接地址https://www.iteye.com/blog/rednaxelafx-774673(文末第一个参考地址)

结束

  • 由于自己才疏学浅,难免会有纰漏,假如你发现了错误的地方,还望留言给我指出来,我会对其加以修正。

  • 如果你觉得文章还不错,你的转发、分享、赞赏、点赞、留言就是对我最大的鼓励。

  • 感谢您的阅读,十分欢迎并感谢您的关注。

https://www.iteye.com/blog/rednaxelafx-774673

https://www.zhihu.com/question/36908414/answer/69724311

https://www.cnblogs.com/paddix/p/5326863.html

https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.html

https://www.jianshu.com/p/6bee67a7f6ce

https://juejin.im/post/6844903741032759310


用户头像

java金融

关注

公众号:【java金融】 2018.05.15 加入

专注 Java 领域相关技术:java基础、 Java SE、Java 并发、JVM、分布式、中间件、微服务、Spring、mysql、oracle等技术。

评论

发布
暂无评论
阿里面试 问我字符串