阅读本文你可以解决的问题
- 为什么 String 类型要用 final 修饰?
- == 和 equals 的区别是什么?
- String 和 StringBuilder、StringBuffer 有什么区别?
- String 的 intern() 方法有什么含义?
- String 类型在 JVM(Java 虚拟机)中是如何存储的?编译器对 String 做了哪些优化?
String a=new String(“abc”);一共创建了多少个对象?
== 和 equals 的区别
- == 对于基本数据类型来说,是用于比较 “值”是否相等的
- 而对于引用类型来说,是用于比较引用地址是否相同的。
先看Object的equals源码
public boolean equals(Object obj) {
return (this == obj);
}
可以看出,Object 中的 equals() 方法其实就是 ==,而 String 重写了 equals() 方法把它修改成比较两个字符串的值是否相等。
public boolean equals(Object anObject) {
// 对象引用相同直接返回 true
if (this == anObject) {
return true;
}
// 判断需要对比的值是否为 String 类型,如果不是则直接返回 false
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
// 把两个字符串都转换为 char 数组对比
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
// 循环比对两个字符串的每一个字符
while (n-- != 0) {
// 如果其中有一个字符不相等就 true false,否则继续对比
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
其他重要方法:
- indexOf():查询字符串首次出现的下标位置
- lastIndexOf():查询字符串最后出现的下标位置
- contains():查询字符串中是否包含另一个字符串
- toLowerCase():把字符串全部转换成小写
- toUpperCase():把字符串全部转换成大写
- length():查询字符串的长度
- trim():去掉字符串首尾空格
- replace():替换字符串中的某些字符
- split():把字符串分割并返回字符串数组
- join():把字符串数组转为字符串
为什么 String 类型要用 final 修饰
String有两处比较重要的地方使用final修饰
public final class String implements ...{
private final char value[];
...
- 在类名使用final是为了String不给继承,保证其"不可变性"
- 而来value使用final是为了保证value字段的内存地址不被修改
先看第二点,value用final修饰,编译器不允许我把value指向堆区另一个地址。但如果我直接对数组元素动手,分分钟搞定。
final String[] value = {"a", "b"};
value[0] = "666";
//编译器允许,修改数组的值,且内存地址没变
System.out.println(value);
所以为防止这种情况,String是不能被继承,外部也无法对value进行操作修改,保证其"不可变性"。
什么叫"不可变性":
String str = "abc";
//二次修改内容其实并不是在原本的对象进行修改,而是重新创建对象重新指向一个新的内存地址
str = "abcd";
为什么要这样做,为了安全性考虑,举个例子:
HashMap<String, Object> map = new HashMap<>();
String key1 = "1";
String key2 = "2";
map.put(key1, "a");
map.put(key2, "b");
//如果我此时改变key1的值为key2的值,若没有"不可变性"的保护,map就会出现2个相同的key但值不一样的对象
key1 = "2";
//所以实际上虽然改变了key1的值,但是map中的key内存地址是没有变化的,自然没有问题
System.out.println(map);
另外在java里面字符串是放在常量池,下面2个变量其实都是指向同一个内存地址,这样做可以节省内存空间提高效率,之说以能实现这个特性就是因为其不可变性
String key1 = "1";
String key2 = "1";
String与JVM
String 常见的创建方式有两种,new String() 的方式和直接赋值的方式,直接赋值的方式会先去字符串常量池中查找是否已经有此值,如果有则把引用地址直接指向此值,否则会先在常量池中创建,然后再把引用指向此值;而 new String() 的方式一定会先在堆上创建一个字符串对象,然后再去常量池中查询此字符串的值是否已经存在,如果不存在会先在常量池中创建此字符串,然后把引用的值指向此字符串
String s1 = new String("Java");
String s2 = s1.intern();//返回该字符串常量池地址
String s3 = "Java";
System.out.println(s1 == s2); // false s1是对象内存地址 s2是常量池地址
System.out.println(s2 == s3); // true
//编译器会自动将"Ja"+"va" 直接编译成了 "Java"
String s1 = "Ja" + "va";
String s2 = "Java";
System.out.println(s1 == s2); //true
所以编译器会对字符串片段进行优化,节省空间,它们在 JVM 存储的位置,如下图所示
还有一个常见问题,String a=new String(“abc”);一共创建了多少个对象?
- 如之前已经声明过String xx="abc";那就只创建了一个对象 new String
- 如果之前没有声明过String xx="abc";那就创建了"abc"与 new String
如果你搞懂常量池的原理就明白答案了
String 和 StringBuilder、StringBuffer 的区别
因为 String 类型是不可变的,所以在字符串拼接的时候如果使用 String 的话性能会很低,因此我们就需要使用另一个数据类型 StringBuffer,它提供了 append 和 insert 方法可用于字符串的拼接,它使用 synchronized 来保证线程安全,如下源码所示:
@Override
public synchronized StringBuffer append(Object obj) {
toStringCache = null;
super.append(String.valueOf(obj));
return this;
}
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
由于使用了synchronized,所以性能会受到影响,所有在jdk1.5新增一个一个StringBuilder,两者功能相似但它没有使用 synchronized 来修饰
结论:
- StringBuilder 性能高,线程不安全
- StringBuffer 线程安全,因为核心方法都是用synchronized修饰