文章目录
第十八章 字符串
字符串操作毫无疑问是计算机程序设计中最常见的行为之一。
1. 字符串的不可变
String 对象是不可变的。查看 JDK 文档你就会发现, String 类中每一个看起来会修改 String 值的方法,实际上都是创建了一个全新的 String 对象,以包含修改后的字符串内容。而最初的 String 对象则丝毫未动。
public class Immutable {
public static String upcase(String s) {
return s.toUpperCase();
}
public static void main(String[] args) {
String q = "howdy";
System.out.println(q);
String qq = upcase(q);
System.out.println(qq);
System.out.println(q);
}
}
输出:
howdy
HOWDY
howdy
当把 q 传递给 upcase() 方法时,实际传递的是引用的一个拷贝。其实,每当把 String 对象作为方法的参数时,都会复制一份引用,而该引用所指向的对象其实一直待在单一的物理位置上,从未动过。
—PS:q 还是那个皮蛋
2. + 的重载与 StringBuilder
重载的意思是,一个操作符在用于特定的类时,被赋予了特殊的意义(用于 String 的 + 与 += 是 Java 中仅有的两个重载过的操作符,Java 不允许程序员重载任何其他的操作符 )。
—PS:+ 在数值是相加,在字符串是相连。+ 的重载与方法重载是两码事
操作符 + 可以用来连接 String :
public class Concatenation {
public static void main(String[] args) {
String mango = "mango";
String s = "abc" + mango + "def" + 47;
System.out.println(s);
}
}
输出:
abcmangodef47
可以想象一下,这段代码是这样工作的: String 可能有一个 append() 方法,它会生成一个新的 String 对象,以包含“abc”与 mango 连接后的字符串。该对象会再创建另一个新的 String 对象,然后与“def”相连,生成另一个新的对象,依此类推。 这种方式当然是可行的,但是为了生成最终的 String 对象,会产生一大堆需要垃圾回收的中间对象。
用 JDK 自带的 javap 工具来反编译以上代码,发现编译器自动引入了 java.lang.StringBuilder 类,就因为它更高效。
在这里,编译器创建了一个 StringBuilder 对象,用于构建最终的 String ,并对每个字符串调用 了一次 append() 方法,共计 4 次。最后调用 toString() 生成结果,也许你会觉得可以随意使用 String 对象,反正编译器会自动为你做性能优化。
但是从下面这段代码的反编译结果来看,显式的使用 StringBuilder 会节省很多资源。
public class WhitherStringBuilder {
public String implicit(String[] fields) {
String result = "";
for (String field : fields) {
// PS:反编译后,每次循环都会创建一个 StringBuilder 对象
result += field;
}
return result;
}
public String explicit(String[] fields) {
// PS:反编译后,explicit() 不仅循环部分的代码更简短、更简单,只会创建一个 StringBuilder 对象
StringBuilder result = new StringBuilder();
for (String field : fields) {
result.append(field);
}
return result.toString();
}
}
显式地创建 StringBuilder 还允许你预先为其指定大小。如果你已经知道最终字符串的大概长度,那预先指定 StringBuilder 的大小可以避免频繁地重新分配缓冲。
—PS:简单的字符串连接可以直接用 + ,涉及到循环时最好用 StringBuilder
StringBuilder 提供了丰富而全面的方法,包括 insert() 、 replace() 、 substring() 、delete() ,甚至还有 reverse() ,但是最常用的还是 append() 和 toString() 。
3. 意外递归
如果你希望 toString() 打印出类的内存地址,也许你会考虑使用 this 关键字:
@Override
public String toString() {
return " InfiniteRecursion address: " + this + "\n";
}
当你创建了 InfiniteRecursion 对象,并将其打印出来的时候,你会得到一串很长的异常信息。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1cc4F9yp-1688095585633)(img/181.png)]
其实,当运行到如下代码时:
"InfiniteRecursion address: " + this
编译器发现一个 String 对象后面跟着一个 “+”,而 “+” 后面的对象不是 String ,于是编译器试着将 this 转换成一个 String 。它怎么转换呢?正是通过调用 this 上的 toString() 方法,于是就发生了递归调用。
—PS:this 的 toString() 还是这个方法,自己调自己
如果你真的想要打印对象的内存地址,应该调用 super.toString() 方法。
4. 字符串操作
推荐一个大佬的文章:Java 字符串常见的操作
当需要改变字符串的内容时, String 类的方法都会返回一个新的 String 对 象。同时,如果内容不改变, String 方法只是返回原始对象的一个引用而已。这可以节约存储空间以及避免额外的开销。
5. 格式化输出
5.1 printf()
printf() 并不使用重载的 + 操作符来连接引号内的字符串或字符串变量,而是使用特殊的占位符来表示数据将来的位置。而且它还将插入格式化字符串的参数,以逗号分隔,排成一行。
控制符 | 说明 |
---|---|
%d | 整数 |
%8d | 整数,右对齐,输出长度为8位 |
%-8d | 整数,左对齐,输出长度为8位 |
%f | 浮点数 |
%8f | 浮点数,右对齐,输出长度为8位,有 - 为左对齐 |
%.2f | 浮点数,精确到百分位 |
%8.3f | 浮点数,精确到千分位,输出长度为8位(算上小数点) |
%s | 字符串,与 %d 类似 |
%n | 换行符 |
public class StrTest {
public static void main(String[] args) {
System.out.printf("%d%n", 100);
for (int i = 0; i < 5; i++) {
System.out.printf("%4d", i);
}
System.out.println();
for (int i = 0; i < 5; i++) {
System.out.printf("%-4d", i);
}
System.out.println();
System.out.printf("%f%n", 8d);
System.out.printf("%.2f%n", 8d);
System.out.printf("%8.3f%n", 8d);
System.out.printf("%-8.3f%n", 8d);
System.out.printf("%s%n", "a1");
System.out.printf("不装了,%s有%d套房%n", "小悦", 66);
}
}
输出:
100
0 1 2 3 4
0 1 2 3 4
8.000000
8.00
8.000
8.000
a1
不装了,小悦有66套房
5.2 System.out.format()
Java SE5 引入了 format() 方法,可用于 PrintStream 或者 PrintWriter 对象,其中也包括 System.out 对象。format() 和 printf() 是等价的,String 类也有一个 static format() 方法,可以格式化字符串。
public class SimpleFormat {
public static void main(String[] args) {
int x = 5;
double y = 5.332542;
System.out.println("Row 1: [" + x + " " + y + "]");
System.out.format("Row 1: [%d %f]%n", x, y);
System.out.printf("Row 1: [%d %f]%n", x, y);
System.out.println(String.format("Row 1: [%d %f]%n", x, y));
}
}
输出:
Row 1: [5 5.332542]
Row 1: [5 5.332542]
Row 1: [5 5.332542]
Row 1: [5 5.332542]
5.3 Formatter 类
在 Java 中,所有的格式化功能都是由 java.util.Formatter 类处理的。可以将 Formatter 看做一个翻译器,它将你的格式化字符串与数据翻译成需要的结果。当你创建一个 Formatter 对象时,需要向其构造器传递一些信息,告诉它最终的结果将向哪里输出:
import java.io.PrintStream;
import java.util.Formatter;
public class Turtle {
private String name;
private Formatter f;
public Turtle(String name, Formatter f) {
this.name = name;
this.f = f;
}
public void move(int x, int y) {
f.format("%s The Turtle is at (%d,%d)%n",
name, x, y);
}
public static void main(String[] args) {
// PS:创建一个输出流
PrintStream outAlias = System.out;
// PS: Formatter 的重载构造器支持输出到多个路径,
// 不过最常用的还是 PrintStream() 、 OutputStream 和 File 。
Turtle tommy = new Turtle("Tommy", new Formatter(System.out));
Turtle terry = new Turtle("Terry", new Formatter(outAlias));
tommy.move(0, 0);
terry.move(4, 8);
tommy.move(3, 4);
terry.move(2, 5);
tommy.move(3, 3);
terry.move(3, 3);
}
}
输出:
Tommy The Turtle is at (0,0)
Terry The Turtle is at (4,8)
Tommy The Turtle is at (3,4)
Terry The Turtle is at (2,5)
Tommy The Turtle is at (3,3)
Terry The Turtle is at (3,3)
5.3.1 格式化修饰符
在插入数据时,如果想要优化空格与对齐,你需要更精细复杂的格式修饰符。以下是其通用语法:
%[argument_index$][flags][width][.precision]conversion
argument_index: 可选,是一个十进制整数,用于表明参数在参数列表中的位置。第一个参数由 “1 " 引用,第二个参数由 " 2 " 引用,第二个参数由 "2 "引用,第二个参数由"2” 引用,依此类推。
flags: 可选,用来控制输出格式
width: 可选,是一个正整数,表示输出的最小长度
precision:可选,用来限定输出字符数
conversion:必须,用来表示如何格式化参数的字符
推荐大佬文章,里面有详细说明:String.format()方法使用说明
5.3.2 Formatter 转换
类型 | 含义 |
---|---|
d | 整型(十进制) |
c | Unicode字符 |
b | Boolean值 |
s | String |
f | 浮点数(十进制) |
e | 浮点数(科学计数) |
x | 整型(十六进制) |
h | 散列码(十六进制) |
% | 字面值“%” |
对于 boolean 基本类型或 Boolean 对象,其转换结果是对应的 true 或 false 。
但是,对其他类型的参数,只要该参数不为 null ,其转换结果永远都是 true 。
5.4 String.format()
String.format() 是一个 static 方法,它接受与 Formatter.format() 方法一样的参数,但返回一个 String 对象。当你只需使用一次 format() 方法的时候, String.format() 用起来很方便。其实在 String.format() 内部,它也是创建了一个 Formatter 对象,然后将你传入的参数转给 Formatter 。
6. 自我学习总结
-
String 对象是不可变的
-
操作符重载就是在不同场合有不同的作用,例如,+ 除了数值相加,还可以用于字符串连接,+ 与 += 是 Java 中仅有的两个重载操作符
-
简单的字符串连接可以使用 + ,这是因为编译器自动优化为了 StringBuilder 去处理,涉及到复杂的、繁多的字符串连接时,还是显式的使用 StringBuilder 最好
-
StringBuilder 提供了很多方法,insert() 、 replace() 、 substring() 、delete() ,甚至还有 reverse() ,但是最常用的还是 append() 和 toString()
-
字符串操作很多返回的是 String 对象,所以可以链式的调用方法,例如:s.trim().replace(“a”,“b”).substring(0,1).toLowerCase();
-
字符串格式化输出的方式有:
- System.out.printf()
- System.out.format()
- Formatter 类 format()
- String.format()
以上都遵循下面的规则:
控制符 | 说明 |
---|---|
%d | 整数 |
%8d | 整数,右对齐,输出长度为8位 |
%-8d | 整数,左对齐,输出长度为8位 |
%f | 浮点数 |
%8f | 浮点数,右对齐,输出长度为8位,有 - 为左对齐 |
%.2f | 浮点数,精确到百分位 |
%8.3f | 浮点数,精确到千分位,输出长度为8位(算上小数点) |
%s | 字符串,与 %d 类似 |
%n | 换行符 |
(图网,侵删)