提到 String、StringBuffer 和 StringBuilder,就不得不谈及它们的历史,在了解它们的历史之后,我们对它们的理解将更上一级台阶!
发展历史
String 与 StringBuffer 的出现
String 和 StringBuffer 在 Java1.0 中就已经有了,目前也一直存在于各个 Java 版本之中,但是 StringBuilder 是在 Java5 中才被引入。我们都知道,String 是 Java 中的字符串类,是不可修改的,在 Java1.0 的时候,若要对字符串进行大量修改,应当使用 StringBuffer,它是可修改的,同时,当时的开发人员考虑到多个线程对一个字符串的修改可能出现线程不安全的问题,于是让 StringBuffer 在拥有可修改字符串的功能的情况下,又给它加上了线程安全的机制。看到这里是不是觉得还挺好,挺正常的?但是要知道一个前提,那就是在 Java5 之前的 Java 在处理字符串的速度上一直被别人诟病,原因出在哪里?原因就在于这个 StringBuffer 上面。
被人诟病的 StringBuffer
StringBuffer 本来是为了实现大量修改字符串的功能而出现的,但却因为 Java 的开发人员给它加了个线程安全的功能,导致它执行效率极大地下降。这个线程安全的功能的实现并不是像我们现在用的方法,当时只是保证没有异常抛出,程序可以正常运行下去而已。在 Java 中,要实现字符串的相加,用加法运算符将两个字符串相加即可。但在这个过程中,Java5 之前是有 String 自动隐含地转换成 StringBuffer,再进行操作这一个步骤的(毕竟 String 类不可直接修改)。只要有这些步骤,就可以实现字符串的修改,但是呢,StringBuffer 有个线程安全的功能,它会在上面提到的步骤中还额外的执行一些功能,以保证线程的安全,而且,这里实现线程安全的方式和我们现在用锁的方式是不一样的!它这里的实现线程安全的方式极为繁琐且复杂,这就大大降低了 StringBuffer 的执行效率,以至于后来被广大程序员诟病。
StringBuilder 的出现
我们仔细地想一下,实际上也并没有多少地方需要在修改字符串的同时保证线程安全,就算有,我们给它加个锁就行。基于这种想法,在 StringBuffer 出现 10 年之后,Java 的开发人员回过头看这个问题,才发现 StringBuffer 的实现是多么的愚蠢,于是后来在 Java5 就有了 StringBuilder。StringBuilder 同样可以快速高效地修改字符串,同时不是线程安全的。虽然它不是线程安全的,但是它的执行效率却比 StringBuffer 要高上了不少。在 Java5 之后的版本中,字符串相加隐含的转化过程中,不再将 String 转化为 StringBuffer,而是转化成 StringBuilder。
历史故事讲完了,下面正式开始讲解它们各自的用法和特性。
String 类
String 类是 Java 用来存储字符串的内置类,在 String 初始化之后,其值就不可改变。
创建字符串
创建字符串的方式有两种,直接创建和使用 new 关键字来创建
直接创建
String str = "Java";
直接创建的 String 类的数据存储在公共的常量池中(Java 的常量优化机制),即直接创建的相同值的不同 String 类的引用相同。
String str1 = "Java";
String str2 = "J" + "a" + "v" + "a";
System.out.println(str1 == str2); // Output: true
但下面这种情况要注意一下,它和上面的不同:
String str1 = "Java";
String str2 = "Java";
String str3 = str2 + "";
System.out.println(str1 == str2); // Output: true
System.out.println(str2 == str3); // Output: false
为什么 str2 与 str3 不相等呢?实际上 str2 + "" 这一过程中,编译器会隐含地将 str2 转换成 StringBuilder(Java5 之前为 StringBuffer),然后再与 "" 相加,此过程会产生一个新的 StringBuilder 类,就是转换而来的那个。这个新产生的 StringBuilder 再转换为 String 类并赋值给变量 str3,因此,str3 的引用位置是 String 对象的堆上(与 new 关键字创建的 String 类相同),故与 str2 不相等。
new 关键字创建
String str = new String("Java");
通过 new 关键字创建的 String 和其他一般的类的创建一样,数据是存储在 String 类的对象的堆上,即通过 new 关键字创建的相同值的不同 String 类的引用不同。
String str1 = new String("Java");
String str2 = new String("Java");
System.out.println(str1 == str2); // Output: false
通过 new 关键字创建的 String 类会调用 String 类的构造方法,String 类的构造方法有 11 种,除了通过字符串来创建 String 类之外,我们还可以用字符数组等方式来创建。
char[] s = {'J', 'a', 'v', 'a'};
String str = new String(s);
格式化字符串
在 Java 中,格式化字符串的方法有很多,比如有 C 语言风格的 printf,也有 Java 风格的 String.format。
System.out.printf("%f, %.2f, %s", Math.E, Math.PI, "Java");
虽然说是 C 语言风格,但还是略微有一点点不同,比如说,用 %d 来输出浮点数在 C 语言里面编译是不会报错的,尽管输出的结果不对,但是这在 Java 里面是会报错的。
用 String 类的 format 方法也能格式化字符串,它可以看作是强化了的格式化字符串工具。
它既可以像 printf 那样格式化字符串:
System.out.println(String.format("%f, %.2f, %s", Math.E, Math.PI, "Java"));
也有它自己独特的方式:%[index]$[flag][type]
[index] 表示参数的索引,从 1 开始,因为 0 是格式化字符串;
[flag] 表示格式化的标识,有以下几种:
- - :左对齐(默认是右对齐的),和长度限定一起使用;
- + :正数前加正号;
- 0 :不够长度用 0 来补齐;
- # :对非十进制的数前加上标识;
- , :对于十进制整数每隔三位加一个逗号分隔符;
- ( :若为负数,则用括号将其括起来;
- (空格字符):正数前空一格,负数没有变化;
- m.nf :对于浮点数,输出长度为 m 位,保留小数点后 n 位;
- e/E :以科学计数法表示浮点数;
- g/G :根据情况智能选择以普通形式或者科学计数法输出浮点数;
- a/A :输出带有效位数和指数的十六进制浮点数;
int i = 1234567890;
System.out.println(String.format("%,d", i)); // Output: 1,234,567,890
System.out.println(String.format("%16d", i)); // Output: 1234567890
System.out.println(String.format("%016d", i)); // Output: 0000001234567890
System.out.println(String.format("%-16d", i)); // Output: 1234567890
System.out.println(String.format("% d", i)); // Output: 1234567890
System.out.println(String.format("% d", -i)); // Output: -1234567890
System.out.println(String.format("%+d", i)); // Output: +1234567890
System.out.println(String.format("%(d", -i)); // Output: (1234567890)
System.out.println(String.format("%#x", i)); // Output: 0x499602d2
System.out.println(String.format("%x", i)); // Output: 499602d2
System.out.println(String.format("%o", i)); // Output: 11145401322
System.out.println(String.format("%b", i)); // Output: true
double d = 3.1415926;
System.out.println(String.format("%12.5f", d)); // Output: 3.14159
System.out.println(String.format("%g", d)); // Output: 3.14159
System.out.println(String.format("%e", d)); // Output: 3.141593e+00
System.out.println(String.format("%a", d)); // Output: 0x1.921fb4d12d84ap1
[type] 标识格式化的类型,有 d(整数)、f(浮点数)、s(字符串)、c(字符)、b(布尔值) 等。注意,这里的整数包括 int 类型和 long 类型,浮点数包括 float 类型和 double 类型。除了上面的基本类型外,还有几种特殊的:x(十六进制)、o(八进制)。
int i = 1;
long l = 1l;
float f = 1.f;
double d = 1.;
char c = 'a';
String str = "Java";
String format = String.format("%6$s %5$c %4$f %3$f %2$d %1$d", i, l, f, d, c, str);
System.out.println(format); // Output: Java a 1.000000 1.000000 1 1
关于 Java 中输出整数的二进制的方法:
System.out.println(Integer.toBinaryString(123)); // Output: 1111011
输出二进制不能用格式化输出了,但可以用 Integer 类的 toBinaryString 方法将其转化为二进制形式的字符串,然后再输出即可。
这里再补充一些 Java 和其他语言不同的地方(方便 C/C++ 和 Python 的人熟悉 Java):
- C/C++ 的整数可以用单引号进行分隔,但是 Java 和 Python 不可以;
- Java 可以通过逗号格式字符串来达到整数每隔三位分隔的效果,C/C++ 和 Python 不可以;
- C/C++ 和 Python 可以将参数作为精度值并进行输出,而 Java 不可以;
- C/C++、Java 和 Python 在十六进制、十进制、二进制上表示方法一样,但在八进制上 Python 与 C/C++ 和 Java 表示方法不同;
C/C++:
int num = 1'234'567'890;
int num = 0xabc; // 十六进制
int num = 123; // 十进制
int num = 0123; // 八进制
int num = 0b101; // 二进制
double d = 3.14;
long long l = 112358;
printf("%lf, %lld", d, f); // Output: 3.14, 112358
printf("%*.*f", 6, 3, 3.1415926); // Output: 3.142
Java:
int num1 = 0xabc; // 十六进制
int num2 = 123; // 十进制
int num3 = 0123; // 八进制
int num4 = 0b101; // 二进制
int num = 1234567890;
System.out.println(String.format("%,d", num)); // Output: 1,234,567,890
Python:(下面的类型提示语法是为了让读者更容易理解)
num: int = 0xabc # 十六进制
num: int = 123 # 十进制
num: int = 0o123 # 八进制
num: int = 0b101 # 二进制
print('%*.*f' % (6, 3, 3.1415926)) # Output: 3.142
String 类的各种方法
常用方法
方法名 | 方法描述 |
int length() | 返回字符串的长度 |
char charAt(int index) | 返回字符串中索引为 index 处的字符 |
int indexOf(char c) | 返回第一个字符 c 的索引,若没有则返回 -1 |
String concat(String str) | 返回原字符串和 str 连接后的新字符串 |
String[] split(char c) | 返回以字符 c 进行分割得到的字符串数组 |
boolean equals(Object anObject) | 将字符串与 anObject 进行比较,并返回比较结果 |
boolean startsWith(String str) | 测试字符串是否以 str 开头,并返回测试结果 |
boolean endsWith(String str) | 测试字符串是否以 str 结尾,并返回测试结果 |
boolean contains(String str) | 判断字符串中是否包含 str,并返回判断结果 |
String substring(int start, int end) | 截取字符串,返回索引为 start 和索引为 end(不含)之间的字符串 |
String format(String fmt, Object... args) | 返回格式化后的字符串 |
String 与 char
众所周知,String 的值是不可改变的,但是下面的代码又是为什么呢?
String str = "Java";
System.out.println(str); // Output: Java
str = "java";
System.out.println(str); // Output: java
上面的代码表面看上去 String 类型的变量 str 的值被改变,其实没有,因为 Java 中的变量都只是对象的引用。原来值为 "Java" 的 String 类型对象实际上还存在于内存中,str = "java"; 只不过是将变量 str 的引用改到了常量 "java" 上面去了。
实际上,String 就是一个 char 类型的数组,且 String 封装的这个 char 数组是用 final 关键字修饰的,String 本身也被 final 修饰,因此无法被改变。所以 String 类型的方法只能返回一个修改原 String 的、新的 String 类型的变量,而无法对原值进行修改。要产生一个新的 String 必然要开辟新的内存,这将花费不少的时间,因此只有在对 String 有少量修改的需求情况下,才使用 String 类,若要大量修改,那么还是需要 StringBuffer 类和 StringBuilder 类。
StringBuffer 与 StringBuilder
StringBuffer 和 StringBuilder 与 String 最大的不同之处在于,它们可以大量且频繁地修改字符串的值而不产生新的字符串。StringBuffer 和 StringBuilder 最大的不同之处在于,StringBuffer 是线程安全的,而 StringBuilder 不是线程安全的,但 StringBuilder 相较于 StringBuffer 有速度优势,绝大多数情况下,推荐使用 StringBuilder。
继承结构
String 是直接继承自 CharSequence,而 StringBuffer 和 StringBuilder 继承自 AbstractStringBuilder,AbstractStringBuilder 又同时继承自 CharSequence 和 Appendable。
基本用法
这里以 StringBuilder 为例,介绍它的基本用法,StringBuffer 的用法与之类似,不同的地方会指出来的。
下面是 StringBuilder 的常用方法:
方法名 | 方法描述 |
append(String str) | 在字符串的后面添加字符串 str |
insert(int start, String str) | 在索引为 start 的位置插入字符串 str |
delete(int start, int end) | 将索引为 start 和索引为 end 之间的字符串删除 |
replace(int start, int end, String str) | 将索引为 start 和索引为 end 之间的字符串替换为字符串 str |
reverse() | 反转字符串本身 |
StringBuilder sb = new StringBuilder(10);
sb.append("Java"); // sb: Java
sb.insert(1, "java"); // sb: Jjavaava
sb.delete(2, 3); // sb: Jjvaava
sb.replace(4, 5, "JAVA"); // sb: JjvaJAVAva
sb.reverse(); // sb: avAVAJavjJ
与 String 的区别
在调用 String 类的 concat 方法(字符串相加)时,实际上是将两个字符串相加并得到一个新的 String 类并返回,而并非对原 String 进行修改,而 StringBuffer 和 StringBuilder 是对自身的值直接进行修改的,速度和内存的消耗谁更多显而易见。
concat 方法与加法运算符
String.concat 方法和使用加号来连接字符串的结果都是得到连接后的字符串,但是这两者有什么区别吗?
String str1 = "Ja" + "va";
String str2 = "Ja".concat("va");
System.out.println(str1 == str2); // Output: false
无疑,str1 和 str2 的值是相等,但是两者的引用地址不同,从前面的知识我们可以知道,str1 的内存地址在公共的常量池中,上面的代码就说明 str2 的内存地址在 String 类的堆上。concat 方法实际上先复制了原来的字符串后再与新的字符串拼接,然后返回了一个新的字符串对象,所以地址不在公共常量池中,与 new 创建的字符串类似。
除 concat 和加法运算符之外,还有一种方式可以实现字符串的拼接,那就是 StringBuffer 类或 StringBuilder 类的 append 方法,实际上,加法运算符拼接字符串,转换成 StringBuffer 或 StringBuilder 后再进行字符串拼接的操作就是使用 append 方法进行拼接。也就是说,加法拼接字符串的底层实际是调用 append 方法。
适用场景
String、StringBuffer 和 StringBuilder 用处不同,各有各的适用场景。总结如下:
类名称 | 是否可修改 | 线程是否安全 | 相对速度比较 |
String | 否 | \ | 缓慢 |
StringBuffer | 是 | 是 | 一般 |
StringBuilder | 是 | 否 | 快速 |
综上所述,当我们不需要对字符串做太多修改的时候,我们就选择 String 类,当我们需要对字符串进行大量且频繁的修改时,我们就选择 StringBuilder 类,除非遇到了需要线程安全的情况。不过,就算遇到了需要线程安全的情况,仍然推荐使用 StringBuilder,因为 StringBuffer 的线程安全,仅仅是保证 Jvm 不抛出异常,顺利地往下执行而已,它并不能保证逻辑正确和调用顺序正确。在大多数时候,我们需要的是锁。但也可以偷懒使用 StringBuffer。
从 Java5 之后,用加号来连接字符串的时候,都会隐含地调用 StringBuilder 类,因此,大部分情况下用加号连接字符串的操作已经没有太多的性能损失,但并非绝对的。如果在有循环的情况下,编译器可能无法完全做到智能地替换,这个时候我们还是自己手动使用 StringBuilder 类比较好。