Java中CharSequence和String的确切区别是什么?

110
我看了这篇前一篇文章。除了String实现CharSequence并且String是一个字符序列之外,有没有人能说出CharSequence和String之间的确切区别?例如:
CharSequence obj = "hello";
String str = "hello";
System.out.println("output is : " + obj + "  " + str);

当将“hello”分配给 obj ,然后再次分配给 str 时会发生什么?

12
听起来可能有点像RTFM,但是这是真诚的建议:亲自查看String的源代码可以非常有趣并且是一次学习经历。 - Miquel
1
你在哪里可以找到Java源代码?我以为它都是专有的? - aaronsnoswell
4
OpenJDK是开源的。你可以在http://hg.openjdk.java.net/jdk7u/jdk7u/jdk/file/6069fe8ffead/src/share/classes/java/lang/String.java找到一个版本的`String`类。但即使在那之前,Sun JDK也会附带一个名为src.zip的文件,其中包含了大部分的源代码。查看这些代码是可能且有用的,不过根据许可证的规定,修改它们可能涉及其他问题。 - MvG
对于源代码,多家公司通过自己的网站提供访问各种开源项目,包括OpenJDK。只需使用搜索引擎查询“java string class source code”等内容,即可在这些提供商中获得多个结果。 - Basil Bourque
天啊,Oracle不再发布JDK的源代码了吗? - ggb667
8个回答

105

常见差异

除了 String 之外,还有几个实现了 CharSequence 接口的类。其中包括:

  • StringBuilder,用于可变长度的字符序列,可进行修改
  • CharBuffer,用于固定长度的低级别字符序列,可进行修改

任何接受 CharSequence 的方法都可以同样适用于上述所有类型。只有接受 String 的方法需要进行转换。因此,在不关心内部细节的所有地方使用 CharSequence 作为参数类型是明智的。但是,如果您实际上返回一个 String,则应将其用作返回类型,因为这样可以避免在调用方法实际需要 String 的情况下对返回值进行转换。

还要注意,地图应该使用 String 作为键类型,而不是 CharSequence,因为地图键不能更改。换句话说,有时不可变的特性对于 String 是必需的。

特定代码片段

至于您贴出的代码:只需编译它,并使用 javap -v 查看 JVM 字节码。您会注意到,objstr 都是对同一常量对象的引用。由于 String 是不可变的,因此这种共享是可以的。

String+ 运算符被编译为各种 StringBuilder.append 调用。因此,它相当于

System.out.println(
  (new StringBuilder())
  .append("output is : ")
  .append((Object)obj)
  .append(" ")
  .append(str)
  .toString()
)

我必须承认,我的编译器 javac 1.6.0_33 编译 + obj 时使用的是StringBuilder.append(Object) 而不是 StringBuilder.append(CharSequence),这让我有点惊讶。前者可能涉及调用对象的toString()方法,而后者应该可以以更高效的方式实现。另一方面,String.toString()只是返回字符串本身,因此几乎没有什么损失。因此,StringBuilder.append(String) 可能会更有效率,因为少了一个方法调用。


98
一个是接口(CharSequence),而另一个是该接口的具体实现(String)。
CharSequence animal = "cat"  // `String` object presented as the interface `CharSequence`.

就像 ArrayList 是一个 List,而 HashMap 是一个 Map,同样地,String 是一个 CharSequence
作为一个接口,通常情况下我们更常见到的是 CharSequence 而不是 String,但是由于一些扭曲的历史原因,这个接口在实现之后的几年才被定义。所以在旧的 API 中我们经常看到 String,而在新的 API 中我们更倾向于使用 CharSequence 来定义参数和返回类型。

详细信息

现在我们知道,通常一个API/框架应该主要关注导出接口,其次是具体类。但我们并不总是如此了解这个教训。
在Java中,首先出现的是String类。只有后来才出现了一个面向前端的接口CharSequence。
扭曲的历史
了解一点历史可能有助于理解。
在早期,由于互联网/网络狂热推动了整个行业,Java被急于上市,超前了一些时间。一些库的设计并不像应该那样周全。字符串处理就是其中之一。
此外,Java是最早面向生产的非学术面向对象编程(OOP)环境之一。在此之前,唯一成功的现实世界中的OOP实现是一些有限版本的SmallTalk,然后是Objective-CNeXTSTEP/OpenStep。因此,还有许多实践经验需要学习。
Java从String类和StringBuffer类开始。但是这两个类没有关联,它们之间没有继承关系,也没有接口关系。后来,Java团队意识到在与字符串相关的实现之间应该有一个统一的联系,使它们可以互换使用。在Java 4中,团队添加了CharSequence接口,并在String和String Buffer上实现了该接口,同时添加了另一个实现CharBuffer。后来在Java 5中,他们添加了StringBuilder,它基本上是StringBuffer的非同步版本,因此速度更快一些。
所以这些面向字符串的类有点混乱,对于学习来说也有点困惑。许多库和接口都是用来接收和返回String对象的。现在这样的库通常应该预期CharSequence。但是(a) String似乎仍然占据着主导地位,(b) 在混合使用各种CharSequence实现时可能存在一些微妙的技术问题。回顾过去,我们可以看到所有这些字符串相关的事情本可以处理得更好,但现在我们就是这样。
理想情况下,Java应该从一个接口和/或超类开始,这些接口和/或超类在我们现在使用String的许多地方都可以使用,就像我们在CollectionList接口代替ArrayListLinkedList实现的地方一样。
接口与类的关键区别在于CharSequence是一个接口,而不是一个实现。这意味着你不能直接实例化一个CharSequence,而是实例化实现该接口的类之一。
例如,这里我们有一个看起来像是CharSequence的x,但实际上是一个StringBuilder对象。
CharSequence x = new StringBuilder( "dog" );  // Looks like a `CharSequence` but is actually a `StringBuilder` instance.

当使用字符串字面量时,这一点就不那么明显了。请记住,当你看到源代码中只有引号包围的字符时,编译器会将其转换为一个字符串对象。
CharSequence y = "cat";  // Looks like a `CharSequence` but is actually a `String` instance.

字面量与构造函数

关于"cat"new String("cat")之间有一些微妙的区别,可以在这个其他问题中讨论,但在这里并不重要。

类图

这个类图可能会对你有所帮助。我标注了它们出现的Java版本,以展示这些类和接口经历了多少变化。

diagram showing the various string-related classes and interfaces as of Java 8

文本块

除了添加更多的Unicode字符,包括大量的表情符号,近年来在处理文本方面,Java并没有发生太多变化。直到出现了文本块

文本块是一种更好地处理多行字符串字面量或字符转义的新方法。这将使编写嵌入式代码字符串(如HTML、XML、SQL或JSON)更加方便。

引用JEP 378的话:

文本块是一个多行字符串字面量,避免了大多数转义序列的需要,自动以可预测的方式格式化字符串,并在需要时给开发人员对格式的控制。

文本块功能并没有引入新的数据类型。文本块只是一种用于编写String字面量的新语法。文本块生成一个String对象,就像传统的字面量语法一样。文本块生成一个String对象,该对象也是一个CharSequence对象,如上所述。

SQL示例

再次引用JSR 378...

使用"一维"字符串字面量。

String query = "SELECT \"EMP_ID\", \"LAST_NAME\" FROM \"EMPLOYEE_TB\"\n" +
               "WHERE \"CITY\" = 'INDIANAPOLIS'\n" +
               "ORDER BY \"EMP_ID\", \"LAST_NAME\";\n";

使用一个“二维”的文本块
String query = """
               SELECT "EMP_ID", "LAST_NAME" FROM "EMPLOYEE_TB"
               WHERE "CITY" = 'INDIANAPOLIS'
               ORDER BY "EMP_ID", "LAST_NAME";
               """;

文本块可在Java 15及更高版本中找到,参见Java 15JEP 378: 文本块

首次在Java 13中进行了预览,参见JEP 355: 文本块(预览)。然后在Java 14中再次进行了预览,参见JEP 368: 文本块(第二次预览)

这项工作之前是JEP 326: 原始字符串字面量(预览)。这些概念经过重新设计,形成了文本块功能。

字符串模板

字符串模板通过将文字文本与嵌入式表达式和模板处理器相结合,产生特定的结果,以补充Java现有的字符串字面量和文本块。

查看JEP 430: 字符串模板(预览版)
此功能正在Java 21中进行预览。

2
老派的好学校 - Yugerten
1
太棒了,我读了你的其他答案,想知道那个人是怎么知道这么多的,所有的都很长而且解释得很清楚,难道你找到了一种将信息下载到大脑中的方法吗 :) - Abhinav Chauhan

22

CharSequence 是一个契约(interface),而 String 是这个契约的一个 implementation

public final class String extends Object 
    implements Serializable, Comparable<String>, CharSequence

documentationCharSequence的内容如下:

CharSequence是一个可读的char值序列。该接口提供了对许多不同类型的char序列的统一只读访问。char值表示基本多文种平面(BMP)中的字符或代理项。有关详细信息,请参阅Unicode字符表示。


12

除了String实现CharSequence接口并且String是字符序列外,你的代码中还发生了几件事情。

CharSequence obj = "hello";

这创建了一个String文字,即"hello",它是一个String对象。作为一个String,它实现了CharSequence接口,因此也是一个CharSequence。(例如,您可以阅读这篇有关在Java中编写接口的文章)。

下一行:

String str = "hello";

这里稍微复杂了一些。在Java中,String文字字符串会被存储在一个池子里(即被interned),因此这行代码上的"hello"和第一行上的"hello"是同一个对象(identity)。因此,这行代码只是将相同的String文字字符串赋给了str

此时,objstr都是指向"hello"这个String文字字符串的引用,因此它们equals==,并且它们都是StringCharSequence类型。

我建议您测试此代码,展示我刚才所写的内容:

public static void main(String[] args) {
    CharSequence obj = "hello";
    String str = "hello";
    System.out.println("Type of obj: " + obj.getClass().getSimpleName());
    System.out.println("Type of str: " + str.getClass().getSimpleName());
    System.out.println("Value of obj: " + obj);
    System.out.println("Value of str: " + str);
    System.out.println("Is obj a String? " + (obj instanceof String));
    System.out.println("Is obj a CharSequence? " + (obj instanceof CharSequence));
    System.out.println("Is str a String? " + (str instanceof String));
    System.out.println("Is str a CharSequence? " + (str instanceof CharSequence));
    System.out.println("Is \"hello\" a String? " + ("hello" instanceof String));
    System.out.println("Is \"hello\" a CharSequence? " + ("hello" instanceof CharSequence));
    System.out.println("str.equals(obj)? " + str.equals(obj));
    System.out.println("(str == obj)? " + (str == obj));
}

3

我知道这似乎是显而易见的,但CharSequence是一个接口,而String是一个具体类 :)

java.lang.String是该接口的实现...


2
考虑使用UTF-8编码。在UTF-8中,Unicode代码点由一个或多个字节构成。封装UTF-8字节数组的类可以实现CharSequence接口,但绝不是String。当允许使用CharSequence时,您可以传递实现CharSequence的UTF-8包装类,但不能将UTF-8字节数组传递到期望String的位置。在我的项目中,我正在开发一个名为CBTF8Field(压缩二进制传输格式 - 八位)的类,以提供对xml的数据压缩,并希望使用CharSequence接口来实现从CBTF8字节数组到/从字符数组(UTF-16)和字节数组(UTF-8)的转换。
我来这里的原因是要完全理解子序列合同。

2
从Java API的CharSequence中:
引用:

CharSequence是一个可读的字符序列。该接口提供了对许多不同类型的字符序列的统一只读访问。

然后,String, CharBufferStringBuffer使用此接口保持所有方法名称的一致性。

1
在charSequence中,你没有像String那样很有用的方法。如果你不想查看文档,请键入: obj. 和 str.
看看你的编译器提供了哪些方法。这对我来说是基本区别。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接