在Java开发中,String类绝对是我们接触最频繁的类之一。无论是日常的字符串拼接、比较,还是复杂的文本处理,都离不开它的身影。但很多开发者在使用String时,往往只停留在表面,对其底层实现和核心特性一知半解,很容易写出低效甚至错误的代码。今天这篇文章,就带大家全面剖析Java String类,从底层原理到实战用法,帮你彻底搞懂它。
String类最核心的特性就是不可变性,即一旦一个String对象被创建,它的值就无法被修改。很多同学可能会有疑问:我们平时写的String str = "abc"; str = "def";不是修改了字符串吗?其实不然,这里的“修改”本质上是让str变量重新指向了一个新的String对象"def",而原来的"abc"对象并没有被改变,最终会被垃圾回收器回收。
那么String的不可变性是如何实现的呢?我们来看JDK源码(以JDK 8为例):

从源码可以看出,String类被final修饰,意味着它不能被继承;同时,存储字符串数据的value数组也是final修饰的,这保证了value数组的引用地址无法被修改。虽然value数组本身是可以修改的(比如通过反射),但Java官方并未提供这样的接口,因此从开发者的角度来看,String对象就是不可变的。
不可变性带来了很多优势:首先是线程安全,由于String对象无法被修改,多线程环境下使用无需担心线程安全问题;其次是可以实现字符串常量池复用,减少内存占用;此外,不可变性还让String对象的哈希值可以被缓存,提升了哈希表等集合的性能。
Java中创建String对象主要有两种方式,这两种方式的底层实现和内存分配完全不同,也是面试中的高频考点。
当我们使用String str = "abc";这种方式创建对象时,JVM会先去字符串常量池中查找是否存在"abc"这个字符串。如果存在,直接将常量池中的对象引用赋值给str;如果不存在,就会在常量池中创建一个"abc"对象,再将引用赋值给str。
当使用String str = new String("abc");创建对象时,JVM会先在堆内存中创建一个String对象,然后去字符串常量池中查找是否存在"abc"。如果常量池中没有"abc",会先在常量池中创建一个"abc"对象,再将堆内存中对象的value数组指向常量池中的value数组;最后将堆内存中对象的引用赋值给str。也就是说,这种方式至少会创建一个对象,最多会创建两个对象(常量池不存在时)。
我们通过一个简单的例子来验证:

这里要注意:==运算符比较的是对象的引用地址,而String类重写了equals方法,比较的是字符串的实际内容。因此,在比较字符串内容时,一定要使用equals方法,而不是==。
String类提供了大量实用的方法,下面介绍几个开发中最常用的方法及使用注意事项。
字符串拼接是最常见的操作,我们可以使用+运算符或concat方法。需要注意的是,由于String的不可变性,每次拼接都会创建一个新的String对象,效率较低。
示例:
String str1 = "a";
String str2 = str1 + "b"; // 创建新对象"ab"
String str3 = str2.concat("c"); // 创建新对象"abc"
如果需要大量拼接字符串,建议使用StringBuilder(非线程安全,效率高)或StringBuffer(线程安全,效率稍低),它们的底层是可变的字符数组,不会频繁创建新对象。
indexOf方法用于查找指定字符或字符串在当前字符串中的索引位置,若不存在则返回-1;contains方法用于判断当前字符串是否包含指定字符序列,其底层其实就是调用indexOf方法实现的。
示例:
String str = "Hello Java"; System.out.println(str.indexOf("Java")); // 6 System.out.println(str.indexOf("Python")); // -1 System.out.println(str.contains("Java")); // true
substring方法用于截取字符串的一部分,有两个重载方法:substring(int beginIndex)(从beginIndex开始截取到末尾)和substring(int beginIndex, int endIndex)(截取[beginIndex, endIndex)区间的字符串,左闭右开)。
示例:
String str = "Hello Java";
System.out.println(str.substring(6)); // Java
System.out.println(str.substring(0, 5)); // Hello
toLowerCase和toUpperCase方法分别用于将字符串转换为小写和大写;valueOf方法是静态方法,用于将其他类型(如int、boolean、Object等)转换为String类型,非常常用。
示例:

如前所述,由于String的不可变性,频繁使用+运算符拼接字符串会创建大量临时对象,占用大量内存,甚至可能导致内存溢出。在循环中拼接字符串时,一定要使用StringBuilder或StringBuffer。
很多新手会习惯性地使用==比较两个字符串的内容,但实际上==比较的是引用地址。只有当两个字符串对象指向同一个引用时,==才会返回true,否则返回false。正确的做法是使用equals方法,或者使用Objects.equals方法(可以避免空指针异常)。
当调用一个为null的String对象的方法时,会抛出NullPointerException。因此,在使用String对象之前,一定要做好空值判断。推荐使用StringUtils.isEmpty(str)(需要导入org.apache.commons.lang3.StringUtils包),它可以同时判断字符串是否为null或空字符串。
String类是Java中最基础也最重要的类之一,掌握它的核心特性(不可变性)、创建方式(常量池vs堆内存)和常用方法,是写出高效、健壮代码的基础。同时,要注意避开频繁拼接、误用==、忽略空值判断等常见坑。希望通过本文的讲解,你能对Java String类有更深入的理解,并在实际开发中灵活运用。