面向面试编程-String源码篇

185 阅读5分钟

阅读本文你可以解决的问题

  • 为什么 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 存储的位置,如下图所示

img

还有一个常见问题,String a=new String(“abc”);一共创建了多少个对象?

  1. 如之前已经声明过String xx="abc";那就只创建了一个对象 new String
  2. 如果之前没有声明过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修饰