Java中的String到底是什么?(知识扩展+考点分析+源码分析)

背景知识

现在几乎所有的Java面试题中都是涉及到String的问题,也是很基本的问题,对于String的知识点,我们可能只是知道:String 不属于基础类型、String不可变、String有这三个类:String、StringBuffer、StringBuilder等等。但是String是如何实现的?它有哪些重要的方法?

源码分析

对于现在的主流JDK版本JDK1.8,String的内部存储结构是char数组,源码如下:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    // 用于存储字符串的值
    private final char value[];
    // 缓存字符串的 hash code
    private int hash; // Default to 0
    // ......其他内容
}

String源码中的几个核心方法如下:

1. 多构造的方法

// String 为参数的构造方法
public String(String original) {
    this.value = original.value;
    this.hash = original.hash;
}
// char[] 为参数构造方法
public String(char value[]) {
    this.value = Arrays.copyOf(value, value.length);
}
// StringBuffer 为参数的构造方法
public String(StringBuffer buffer) {
    synchronized(buffer) {
        this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
    }
}
// StringBuilder 为参数的构造方法
public String(StringBuilder builder) {
    this.value = Arrays.copyOf(builder.getValue(), builder.length());
}

2. 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;
}

String通过重写Object中的equals()方法实现相等的比较,在比较时首先通过instanceof判断是否为String类型,是,则返回true;否则,返回false;
instanceof的使用方式如下:

Object oString = "123";
Object oInt = 123;
System.out.println(oString instanceof String); // 返回 true
System.out.println(oInt instanceof String); // 返回 false

进行String类型判断的时候,会循环对比两个字符串的每一个字符,当所有的字符都相等是返回true,否则返回false。

此外,equalsIgnoreCase()和equals()方式比较类似,但是区别是前者比较的时候忽略字符串的大小写。

3. compareTo()比较两个字符串

compareTo()方法比较两个字符串返回的结果是int型值。

public int compareTo(String anotherString) {
    int len1 = value.length;
    int len2 = anotherString.value.length;
    // 获取到两个字符串长度最短的那个 int 值
    int lim = Math.min(len1, len2);
    char v1[] = value;
    char v2[] = anotherString.value;
    int k = 0;
    // 对比每一个字符
    while (k < lim) {
        char c1 = v1[k];
        char c2 = v2[k];
        if (c1 != c2) {
            // 有字符不相等就返回差值
            return c1 - c2;
        }
        k++;
    }
    return len1 - len2;
}

源码中可以显示出,compareTo()循环比较所有的字符,两个字符串中出现不同字符时,return char1-char2。比如,两个字符串分别存储的是 1 和 2,返回的值是 -1;如果存储的是 1 和 1,则返回的值是 0 ,如果存储的是 2 和 1,则返回的值是 1。compareToIgnoreCase(),用于忽略大小写后比较两个字符串。

compareTo() 方法和 equals() 方法的不同点:
1.equals() 可以接收一个 Object 类型的参数,而 compareTo() 只能接收一个 String 类型的参数;

2.equals() 返回值为 Boolean,而 compareTo() 的返回值则为 int。
它们都可以用于两个字符串的比较,当 equals() 方法返回 true 时,或者是 compareTo() 方法返回 0 时,则表示两个字符串完全相同。

4.其他常用方法

  • indexOf():查询字符串首次出现的下标位置
  • lastIndexOf():查询字符串最后出现的下标位置
  • contains():查询字符串中是否包含另一个字符串
  • toLowerCase():把字符串全部转换成小写
  • toUpperCase():把字符串全部转换成大写
  • length():查询字符串的长度
  • trim():去掉字符串首尾空格
  • replace():替换字符串中的某些字符
  • split():把字符串分割并返回字符串数组
  • join():把字符串数组转为字符串

考点分析

String主要是针对源码进行考察,非常考验一个人的基础知识。对于基础知识的问题可能还有以下方面:

  • 为什么 String 类型要用 final 修饰?
  • == 和 equals 的区别是什么?
  • String 和 StringBuilder、StringBuffer 有什么区别?
  • String 的 intern() 方法有什么含义?
  • String 类型在 JVM(Java 虚拟机)中是如何存储的?编译器对 String 做了哪些优化?

知识扩展

1. 为什么 String 类型要用 final 修饰?

public final class String 
 implements java.io.Serializable,  Comparable<String>, CharSequence 
 { //...... }

从源码进行分析,String是被final修饰的不可继承类。
这样做的好处时什么呢?
Java语言之父James Gosling说过,他更倾向于使用final,因为它可以缓存结果,传参数的时候不用考虑它的修改变动;如果可变的话,那么需要不断地传参,这样就会造成性能的损失

James Gosling还说过另一个原因是安全,在实际过程中,比如你调用系统级操作指令之前,可能需要校验。如果是可变类的话,校验过后,内部值就改变了,这样就可能造成系统的崩溃问题。

总结来说:final修饰的优势是:
1.安全
2.高效

从JVM的角度来讲,只有字符串是不可变时,我们才能实现字符串常量池,字符串常量池可以为我们缓存字符串,提高程序的运行效率。
Java中的String到底是什么?(知识扩展+考点分析+源码分析)_第1张图片
试想一下如果 String 是可变的,那当 s1 的值修改之后,s2 的值也跟着改变了,这样就和我们预期的结果不相符了,因此也就没有办法实现字符串常量池的功能了

2.== 和 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;
}

3.String 和 StringBuilder、StringBuffer 有什么区别?

因为String类型不可变,因此String性能比较低,因此StringBuilder的出现,它提供了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,它同样提供了 append 和 insert 的拼接方法,但它没有使用 synchronized 来修饰,因此在性能上要优于 StringBuffer,所以在非并发操作的环境下可使用 StringBuilder 来进行字符串拼接。

4.String 类型在 JVM(Java 虚拟机)中是如何存储的?编译器对 String 做了哪些优化?

String的创建方式有两种:

  1. new String()的方式:先在堆上创建一个字符串对象,然后再去常量池中查询此字符串是否存在,如果不存在就就在常量池直接创建,然后把引用的值指向此字符串。
  2. 直接赋值:直接赋值先去字符串常量池中查找是否已经有此值,如果有则把引用地址直接指向此值,否则会先在常量值中创建,然后再把引用指向此值。
    源码如下:
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的存储方式如下图所示:
Java中的String到底是什么?(知识扩展+考点分析+源码分析)_第2张图片
Tips:JDK 1.7 之后把永生代换成的元空间,把字符串常量池从方法区移到了 Java 堆上。

总结

此文从String源码入手,阐述了String的构造方法、equals()方法和compareTo()方法,其中 equals() 重写了 Object 的 equals() 方法,把引用对比改成了字符串值对比,也介绍了 final 修饰 String 的好处,可以提高效率和增强安全性,最后还介绍了 String 和 JVM 的一些执行细节。

谢谢大家的欣赏!!!

你可能感兴趣的:(Java)