String 为啥用 final 修饰?(String 为什么设计成不可变的呢?)
1、安全
- 引发安全问题,譬如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接,或者在 socket 编程中,主机名和端口都是以字符串的形式传入。因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变字符串指向的对象的值,造成安全漏洞
- 保证线程安全,在并发场景下,多个线程同时读写资源时,会引竞态条件,由于 String 是不可变的,不会引发线程的问题而保证了线程
- HashCode,当 String 被创建出来的时候,hashcode 也会随之被缓存,hashcode 的计算与 value 有关,若 String 可变,那么 hashcode 也会随之变化,针对于 Map、Set 等容器,他们的键值需要保证唯一性和一致性,因此,String 的不可变性使其比其他对象更适合当容器的键值。
「2、高效」
第二个好处是高效,以 JVM 中的字符串常量池来举例,如下两个变量:
String s1 = "Java"; String s2 = "Java";
- 只有字符串是不可变时,我们才能实现字符串常量池,字符串常量池可以为我们缓存字符串,提高程序的运行效率,如下图所示:
如果 String 是可变的,那当 s1 的值修改之后,s2 的值也跟着改变了,这样就和我们预期的结果不相符了,因此也就没有办法实现字符串常量池的功能了。
== 和 equals 方法的区别?
「== 对于基本数据类型来说,是用于比较 "值" 是否相等的;而对于引用类型来说,是用于比较引用地址是否相同的」。比如:
int a = 1; int b = 2; System.out.println(a==b); // 输出 true Integer a = 128; Integer b = 128; System.out.println(a==b); // 输出 false
再到 equals 方法,查看源码我们可以知道 Object 中也有 equals () 方法,源码如下:
public boolean equals(Object obj) { return (this == obj); }
不难看出,Object 的 equals 其实就是 "=="。而 String 中的 equals 重写了 Object 中的,把它修改成比较两个字符串的值是否相等。源码如下:
public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String) anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }
String、StringBuffer、StringBuilder 的区别?
先上一张三者的继承关系,首先 String 直接继承于 CharSequence,StringBuilder 和 StringBuffer 间接继承于 CharSequence,中间还多了个 AbstractStringBuilder。内部都是用一个 char 数组实现,虽然它们都与字符串相关,但是其处理机制不同:
- String:是不可改变的量,也就是创建后就不能在修改了。
- StringBuffer:是一个可变字符串序列,它与 String 一样,在内存中保存的都是一个有序的字符串序列(char 类型的数组),不同点是 StringBuffer 对象的值都是可变的。
- StringBuilder:与 StringBuffer 类基本相同,都是可变字符换字符串序列,不同点是 StringBuffer 是线程安全的,StringBuilder 是线程不安全的。
「使用场景」:
- 使用 String 类的场景:在字符串不经常变化的场景中可以使用 String 类,例如常量的声明、少量的变量运算。
- 使用 StringBuffer 类的场景:在频繁进行字符串运算(如拼接、替换、删除等),并且运行在多线程环境中,则可以考虑使用 StringBuffer,例如 XML 解析、HTTP 参数解析和封装。
- 使用 StringBuilder 类的场景:在频繁进行字符串运算(如拼接、替换、和删除等),并且运行在单线程的环境中,则可以考虑使用 StringBuilder,如 SQL 语句的拼装、JSON 封装等。
JVM 对 String 做了那些优化?
String 常见的创建方式有两种,new String () 的方式和直接赋值的方式。
- 「直接赋值会先去字符串常量池中查找是否已经有此值,如果有则把引用地址直接指向此值,否则会先在常量池中创建,然后再把引用指向此值」。
- 「new String () 的方式一定会先在堆上创建一个字符串对象,然后再去常量池中查询此字符串的值是否已经存在,如果不存在会先在常量池中创建此字符串,然后把引用的值指向此字符串」
比如下面代码:
String s1 = new String("Java"); String s2 = s1.intern(); String s3 = "Java"; System.out.println(s1 == s2); // 输出 false System.out.println(s2 == s3); // 输出 true
除此以外,JVM 还会对 String 字符串做一些优化,例如以下代码:
String s1 = "Ja" + "va"; String s2 = "Java"; System.out.println(s1 == s2);
虽然 s1 拼接了多个字符串,但对比的结果却是 true,使用反编译工具,看到的结果如下:
Compiled from "StringExample.java" public class com.lagou.interview.StringExample { public com.lagou.interview.StringExample(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 3: 0 public static void main(java.lang.String[]); Code: 0: ldc #2 // String Java 2: astore_1 3: ldc #2 // String Java 5: astore_2 6: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 9: aload_1 10: aload_2 11: if_acmpne 18 14: iconst_1 15: goto 19 18: iconst_0 19: invokevirtual #4 // Method java/io/PrintStream.println:(Z)V 22: return LineNumberTable: line 5: 0 line 6: 3 line 7: 6 line 8: 22 }
从编译代码 #2 可以看出,代码 "Ja"+"va" 被直接编译成了 "Java" ,所以 s1==s2 的结果才是 true,这就是编译器对字符串优化的结果。