字符串
Java 字符串就是 Unicode 字符序列。Java 没有内置的字符类型,而是在标准 Java 类库中提供了一个预定义类 String
。每个用双引号括起来的字符串都是 String 类的一个实例。可以通过直接赋值或者 new 操作符来创建字符串。
String str1 = ""; String str2 = "hello, this is a string."; String str3 = new String("create string with new");
String 类没有提供用于修改字符串的方法,所以我们将 String 类对象称为不可变字符串,它被声明为 final class,所有的属性也被定义为 final 的。但是我们可以修改字符串变量,让它指向另外一个字符串。
为了提高内存利用率,JVM 有一个字符串常量池,每次使用双引号定义字符串,JVM 会先到该常量池中来检测是否已经存在,存在则直接该对象的引用;否则在常量池中创建一个新增并返回该值的引用。
使用 new 创建字符串(new String("字符串");
)时,会直接在堆中创建该字符串并返回其引用。从 Java 6 开始,String 类提供了 intern()
方法,调用该方法时,JVM 去字符串常量池检测是否已存在该字符串,如果已经存在则直接返回引用;如果不存在则在常量池中添加并返回其引用。
Java 6 中,字符串常量池存在 PermGen 里,也就是(“永久代”),而这个空间是有限的,基本不会被 FullGC 之外的垃圾收集机制扫描。如果使用不当,经常会发生 OOM。在后续版本中,将字符串常量池放在了堆中,而且默认缓存大小也在不断扩大。在 Java 8 中永久代 PermGen 也被元数据区 MetaSpace 替代。
下边我们通过一些示例来验证一下:
String str1 = "hello"; String str2 = "hello"; String str3 = "hello" + "world"; String str4 = str2 + "world"; String str5 = new String("hello"); String str6 = new String("hello"); String str7 = str6.intern(); String str8 = new String("hello").intern(); System.out.println("str1 = str2, " + (str1 == str2)); System.out.println("str3 = str4, " + (str3 == str4)); System.out.println("str1 = str5, " + (str1 == str5)); System.out.println("str5 = str6, " + (str5 == str6)); System.out.println("str1 = str7, " + (str1 == str7)); System.out.println("str1 = str8, " + (str1 == str8)); String str9 = "hello"; str9 += "world"; System.out.println("str3 = str9, " + (str3 == str9));
字符串操作
-
长度
-
int length()
返回采用 UTF-16 编码表示的给定字符串所需要的代码单元数量。也即是 String 类内部 char 数组的长度。(char 数据类型是一个采用 UTF-16 编码表示 Unicode 代码点的代码单元) -
int codePointCount(int beginIndex, int endIndex)
表示字符串的实际长度,及代码点数。String str = "hello,\uD835\uDD5D\uD835\uDD60\uD835\uDD60\uD835\uDD5C"; System.out.println(str); System.out.println("length is: " + str.length()); System.out.println("code point count is: " + str.codePointCount(0, str.length()));
-
-
子串
-
String substring(int beginIndex)
-
String substring(int beginIndex, int endIndex)
String str = "hello, world!"; System.out.println(str.substring(1)); System.out.println(str.substring(0, 1)); System.out.println(str.substring(0, str.length() - 1)); System.out.println(str.substring(0, str.length() + 1));
-
-
拼接
可以直接使用
+
和+=
运算符来进行字符串的拼接,例如:-
String str = "hello" + " world!";
-
String str = "hello"; str += " world!";
-
String str = "hello"; str = str + " world!";
-
-
格式化
为了让字符串拼接更简洁直观,我们可以使用字符串格式化方法
String.format
-
%s
字符串 -
%c
字符类型 -
%b
布尔类型 -
%d
整数类型(十进制数) -
%x
整数类型(十六进制数) -
%o
整数类型(八进制数) -
%f
浮点类型 -
%a
浮点类型(十六进制数) -
%%
百分比类型 -
%n
换行
示例:
System.out.printf("hello, %s %n", "world"); System.out.printf("大写a:%c %n", 'A'); System.out.printf("100 > 50:%b %n", 100 > 50); System.out.printf("100除以2:%d %n", 100 / 2); System.out.printf("100的16进制数是:%x %n", 100); System.out.printf("100的8进制数是:%o %n", 100); System.out.printf("100元打8.5折扣是:%f 元%n", 50 * 0.85); System.out.printf("上述价格的16进制数是:%a %n", 50 * 0.85); System.out.printf("上面的折扣是%d%% %n", 85);
-
-
相等判断
-
equals
判断是否相等。 -
equalsIgnoreCase
不区分大小写判断是否相等。
-
-
前缀判断
"hello".startsWith("h")
-
后缀判断
"hello".endsWith("o")
-
包含判断
"hello".contains("ll")
-
查找
-
indexOf
从前边查找 -
lastIndexOf
从后边开始找
示例:
String str = "hello, world!"; System.out.println(str.indexOf("e")); System.out.println(str.indexOf('e')); System.out.println(str.indexOf(101)); System.out.println(str.indexOf("e", 2)); System.out.println(str.indexOf("l")); System.out.println(str.lastIndexOf("l")); System.out.println(str.lastIndexOf("l", 9));
-
-
查找替换
-
replace
-
replaceAll
示例:
System.out.println("hello, world!".replace('o', 'A')); System.out.println("hello, world!".replace("o", "OOO")); System.out.println("hello, world!".replaceAll("o", "OOO"));
-
-
去空格
System.out.println(" hello, world! ".trim());
-
大小写转换
-
System.out.println("Hello, world!".toUpperCase());
-
System.out.println("Hello, world!".toLowerCase());
-
-
空串和 Null 串
-
空串是一个长度为0且内容为空的 String 对象。
-
String 存放 null,表示没有任何对象与该变量关联。
-
String / StringBuilder / StringBuffer
String 在拼接过程中或操作不当时,可能会产生大量的中间对象。而 StringBuffer 就是为了解决这个问题而提供的一个类,StringBuffer 是线程安全的,如果没有线程安全的需要则使用 StringBuilder(Java 1.5 中新增)。
StringBuffer
和 StringBuilder
都继承自 AbstractStringBuilder 类,而 StringBuffer 类的所有方法都使用关键字 synchronized
来保证线程安全。它们的底层都是通过可修改的 char 数组(Java 9 以后改为 byte 数组实现)来实现修改。以下内容没有特别说明则均基于 Java 8。
StringBuffer,StringBuilder 在创建时,如果未指定容量,默认容量为 16。如果容量可预估,则最好在创建时指定合适的大小,这样可以避免多次扩容。扩容会产生多重开销:抛弃原有数组、创建新的数组、进行 arraycopy。
StringBuffer,StringBuilder 常用方法:
-
append
在字符串结尾追加 -
length
返回当前长度 -
setLength
设置字符串长度
示例:
String str1 = "hello" + " world" + "!"; System.out.println(str1); StringBuffer strB1 = new StringBuffer(); strB1.append("hello"); strB1.append(" world"); strB1.append("!"); System.out.println(strB1.toString()); StringBuilder strB2 = new StringBuilder(); strB2.append("hello"); strB2.append(" world"); strB2.append("!"); System.out.println(strB2.toString()); System.out.println("strB2 length is " + strB2.length()); strB2.setLength(strB2.length() - 1); System.out.println(strB2.toString()); strB2.setLength(strB2.length() + 10); System.out.println(strB2.toString());
JVM 对字符串的优化
现代 JVM 的实现是很智能的,编译时 JVM 对 String 操作进行一些优化以提高程序的运行效率。
示例1
String str = "hello" + ", " + "world!"; System.out.println(str);
JVM 优化后
String str = "hello, world!"; System.out.println(str);
示例2
String str1 = "hello"; String str2 = str1 + ", world!"; System.out.println(str2);
JVM 优化后
String str1 = "hello"; StringBuilder str2 = new StringBuilder(); str2.append(str1); str2.append(", world!"); System.out.println(str2.toString());
示例3
long start = System.currentTimeMillis(); String str = ""; for (int i = 0; i < 50000; i++) { str += i; } System.out.println(str.length()); System.out.println("耗时:" + (System.currentTimeMillis() - start) + "ms");
JVM 优化后
long start = System.currentTimeMillis(); String str = ""; for (int i = 0; i < 50000; i++) { StringBuilder tmp = new StringBuilder(); tmp.append(str); tmp.append(i); str = tmp.toString(); } System.out.println(str.length()); System.out.println("耗时:" + (System.currentTimeMillis() - start) + "ms");
for 循环经过优化后,虽然节省了空间,但是 StringBuilder 是在 for 循环内,每次都会创建。性能并不会提升,反而可能会下降。按下边实现方式改写代码后性能提升好几个数量级。
long start = System.currentTimeMillis(); StringBuilder str = new StringBuilder(); for (int i = 0; i < 50000; i++) { str.append(i); } System.out.println(str.length()); System.out.println("耗时:" + (System.currentTimeMillis() - start) + "ms");
说明:可以通过 javap -c 编译生成的class文件
反编译出字节码,然后来分析
应用练习
去掉字符串开头/结尾/中间的空格(不使用 trim 方法)
public String trimAll(String str) { StringBuilder tmp = new StringBuilder(); for (int i = 0; i < str.length(); i++) { char c = str.charAt(i); if (c == ' ') { continue; } tmp.append(c); } return tmp.toString(); }
反转字符串,比如输入 123,反转结果 321
public String reverse(String str) { StringBuilder tmp = new StringBuilder(); char[] chars = str.toCharArray(); for (int i = chars.length - 1; i >= 0; i--) { char c = chars[i]; tmp.append(c); } return tmp.toString(); }
常见面试问题
==
和 equals
的区别
-
==
对于基本类型来说是值比较;而对于引用类型比较的是引用,是否是指向同一个对象的引用。 -
equals
默认是引用比较,而 Integer、String 等包装类都重写了 equals 方法,改为了值比较。
所以对象都可以看作是继承自 Object
,我们来看一下 Object 的 equals 实现,如果自定义类未覆写 equals,调用对象实例的 equals 方法默认是引用比较。
public boolean equals(Object obj) { return (this == obj); }
我们再来看一下 Integer 类的 equals 方法
public boolean equals(Object obj) { if (obj instanceof Integer) { return value == ((Integer)obj).intValue(); } return false; }
下列代码的执行结果
String s1 = "hello" + ", world!"; String s2 = "hello"; s2 += ", world!"; String s3 = "hello, world!"; String s4 = s2.intern(); System.out.println(s1 == s2); System.out.println(s1 == s3); System.out.println(s1 == s4); System.out.println(s2 == s3); System.out.println(s2 == s4);
执行结果:
false true true false false
String / StringBuffer / StringBuilder 的区别
-
String 为不可变字符串;StringBuffer 和 StringBuilder 为字符串可变对象。
-
String 的 substring 等修改操作每次都会产生一个新的 String 对象;字符串拼接性能 String 低于 StringBuffer,而 StringBuffer 低于 StringBuilder。
-
StringBuffer 是线程安全的,StringBuilder 而线程不安全的。二者都是继承自 AbstractStringBuilder ,它们的唯一区别是 StringBuffer 的所有方法都使用了 synchronized 修饰符来保证线程安全。
String 对象的 intern 的作用
String 对象的 intern 方法用于字符串的显示排重。调用此方法时,JVM 去字符串常量池查找池中是否已经存在该字符串,如果已存在则直接返回它的引用;如果不存在则在池中先创建然后返回其引用。
String 不可变性的优点
-
字符串不可变,因此可以通过字符串常量池来实现,共享对象,从而节省空间,提高性能。
-
多线程安全,因为字符串不可变,所以当字符串被多个线程共享时不会存在线程安全问题。
-
适合做缓存的 Key,因为字符串不可变,因此它的哈希值也就不变;创建时它的哈希值就被缓存了,不需要重新计算,速度更快。
String 是否可以被继承
String 不能被继承。因为 String 被声明为 final,所以不能被继承。