简介
可以证明,字符串操作是计算机程序设计中最常见的行为
不可变String
String对象是不可变的,通过JDK文档可以发现,每一个看似会修改String值的操作,实际上都是创建了一个新的String对象,以包含修改后的字符串内容,而最初的String对象则丝毫未动。
重载"+" 与StringBuilder
String对象是不可变的额,你可以给String对象加任意多的别名。因为String具有只读特性,所以指向它的任何引用都不可能改变它的值,因此,也就不会对其他的引用有什么影响。
不可变性会带来一定的效率问题。为String对象重载的”+“操作符就是一个例子,用于String的”+“与”+=“被重载为用来连接两个字符串
看下面代码
public static void main(String[] args) {
String str = "Hello ";
String str2 = str + "World";
System.out.println(str2);
}
上面代码一开始声明了一个Hello字符串,后面与World字符串进行了连接赋值给了str2.所以最后会打印Hello World的字段
现在看看代码到底是如何工作的
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String Hello
2: astore_1
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: aload_1
11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
14: ldc #6 // String World
16: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: astore_2
23: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
26: aload_2
27: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
30: return
}
上面是对源代码进行反编译后的汇编语句。我们暂时不必读懂,可以参考后面的注释。我们可以发现所谓的String字符串拼接实际上就是创建一个空的StringBuilder对象,并将对所有通过”+“进行拼接的字符串进行append操作,最后通过toString方法将拼接好的字符串赋值给str2引用。
注意:虽然编译器会将字符串的拼接优化为StringBuilder的操作,但是这不意味着我们可以滥用”+“,这会我们带来不少的资源浪费。现在看下面的代码:
public void f1(){
String result="";
for (int i =0 ; i<100 ; i++){
result += "Hello ";
}
}
public void f2(){
StringBuilder result= new StringBuilder();
for (int i =0 ; i<100 ; i++){
result.append("Hello ");
}
}
上面代码中,前者通过”+=“来循环拼接字符串,这意味着每一次循环,都会创建一个新的SringBuilder对象,这会极大浪费系统的性能,虽然垃圾回收机制可以回收存储资源,但是创建对象是非常浪费资源的是比较慢的,所以相比之下,后者的使用效率会更高。
String上的操作
方法 | 参数 | 应用 |
---|---|---|
length | 字符串长度 | |
charAt() | Int索引 | 获取String中该索引位置上的char |
getChars(),getBytes() | 要复制部分的起点和终点的索引 | 复制char或byte到一个目标数组中 |
toCharArray() | 生成一个char[],包含String的所有字符 | |
equals(),equalsIgnoreCase() | 与之比较的String | 比较两个String是否相同 |
contains() | 要搜索的CharSequeue | 如果该String包含参数的内容则返回true |
regionMatcher() | 该String的偏移量 | 表示比较的区域是否相等 |
startsWith | 可能的起始String | 该String是否以参数String为开始 |
endsWith | 可能的结束String | 该String是否以参数String为结束 |
indexOf(),lastIndexOf() | String | 如果包含参数Sring返回下标,如果不包含,返回-1 |
substring() | 起始索引+终点索引下标 | 截取目标位置区间的字符串 |
concat() | 要连接地额字符串 | 连接两个字符串 |
replace() | String,String | 用参数String替换符合标准的String |
trim() | 压缩空白字符 | |
valueOf() | 基本类型或数组等 | 将各种基本类型转换为String |
格式化输出
Formatter
在java中所有新的格式化功能都由java.util.Formatter类处理。可以将这个类当作一个翻译器,它将你的格式化字符串与数据翻译成需要的结果。当你创建一个Formatter对象的时候,需要向其构造器传递一些信息,告诉它最终的结果将向哪里输出:
public static void main(String[] args) {
Formatter formatter = new Formatter(System.out);
formatter.format("The %s is %d RMB","apple",30);
//The apple is 30 RMB
}
格式化说明符
在插入数据时,如果想要控制空格与之对其,需要更加精细复杂的格式修饰符,以下是抽象的语法
%[argument_index$][flags][width][.precision]conversion
%[][对齐方向][最小宽度][精度(小数点精确到几位或者字符串最多有几位)]类型转换符
% -10.2s
类型转换符
符号 | 含义 |
---|---|
d | 整数(十进制) |
c | 字符 |
b | 布尔值 |
s | String |
f | 浮点数 |
e | 浮点数 |
x | 整数(十六进制) |
h | 散列码(十六进制) |
% |
String.format()
String中也提供了格式化的功能,这个方法是静态的,与Formatter的format具有相同的作用
System.out.println(String.format("The %s is %d RMB","apple",30));
正则表达式
正则表达式是一种灵活而强大的文本处理工具。通过正则表达式我们就能以编程的方式构造复杂的文本模式,并对输入的字符串进行搜索。
一般来说,正则表达式就是以某种方式来描述字符串,因此,你可以说”如果一个字符串包含这样那样的东西,那么它就是我要找的“
创建正则表达式
字符 | 意义 |
---|---|
\t | 制表符 |
\n | 换行符 |
\r | 回车 |
\f | 换页 |
\e | 转义 |
. | 任意字符 |
[abc] | 包含a、b、c中任意字符 |
[^abc] | 不包含a、b、c中任意字符 |
[a-zA-Z] | 所有大小写英文字母 |
[abc[hij]] | 等于[abchij] |
[a-z&&[hij]] | 取前后交集 |
\s | 空白符(空格、制表、换行、换页和回车) |
\S | 非空白符 |
\d | 数字[0-9] |
\D | 非数字 |
\w | [a-zA-Z0-9] |
\W | [^a-zA-Z0-9] |
边界匹配符 | 含义 |
---|---|
^ | 一行的起始 |
$ | 一行的结束 |
\b | 词的边界 |
\B | 非词的边界 |
\G | 前一个匹配的结束 |
量词
量词 | 匹配 |
---|---|
X? | 一个或零个X |
X* | 零个或多个X |
X+ | 一个或多个X |
X{n} | 恰好n个X |
X{n,} | 至少n个X |
X{n,m} | n个到m个X |
量词中的贪婪型、勉强型和占有型
量词描述了一个模式吸收输入文本的方式
贪婪型
贪婪型就是我们前面提到的量词表中的形式,贪婪型会根据表达式去尽最大范围去匹配
String test = "a<tr>aava </tr>abb ";
String reg = "<.+>";
System.out.println(test.replaceAll(reg, "###"));//a###abb
通过前面正则符号的学习我们可以知道”.“代表任意字符,而”+“则代表一个或多个字符,组合起来就表示匹配一个或多个任意字符。我们看到例子中的正则表达式”<.+>“表示会匹配一对尖括号<>,尖括号中间会有一个或多个任意字符。所以,通常来说我们会认为例子中的符号会被匹配到量词,因为有两个尖括号是符合条件的。但是事实上我们看到输出结果只匹配到了一个。所以我们现在可以理解贪婪型中的尽最大范围匹配是什么含义了
勉强型
勉强型就是在贪婪型的基础上增加了”?“符号,通过前面的学习我们知道”?”代表0个或1个符号,但是如果前面已经出现了量词,那它将不再作为原来的含义使用。
勉强型的含义刚好以贪婪型相反,它会近最小的匹配返回去匹配
String test = "a<tr>aava </tr>abb ";
String reg = "<.+?>";
System.out.println(test.replaceAll(reg, "###"));//a###aava ###abb
在上面例子中我们发现例子中的test字符串被匹配到了两次,这不就是我们希望的结果吗,事实上,勉强型就是会在遇到匹配的结果就不再扩大范围,直接完成对象匹配,然后完成对应操作后再往后继续重新开始匹配
独占型
独占型比较相比前两者难理解,其实独占型与贪婪型基本上是一样的,都是尽最大范围去匹配,只是独占型没有回退功能,所以如果一旦由于把范围扩的太大,会造成原先匹配到的对象都丢失的情况。
这样说比较抽象。我们对正则匹配的方式进行深度的说明
下面图中绿色代表匹配,黄色代表不匹配
通过上面的图我们可以发现到第四步,我们的“.++”把后面所有的字符都给匹配掉了,所以在第五步时,我们的“>”没有可以匹配的字符了,这时候就会匹配失败,如果时我们的独占型,就会到此结束。但是如果是贪婪型,将会把之前匹配完成的结果进行回退,一直退到符合匹配条件的或者全部都不匹配为止。
CharSequeue
CharSequeue是一个从CharBuffer、String、StringBuffer,StringBuilder类之中抽象出来的字符序列的一般化定义。这些类都实现了该接口。大多数正则表达式操作都会接收这个接口作为参数。由于这个接口在1.8之后有大量的方法体,所以有需要可以自己去看看源码
Pattern和Matcher
一般来说,比起功能有限的String类,我们更愿意构造功能强大的正则表达式对象。只需要导入java.util.regex包,然后用static Pattern.compile()方法来编译正则表达式即可。它会根据你的String类型的正则摆动式生成一个Pattern对象。接下来,把你想要检索的字符串传入Pattern的matcher()方法。该方法会生成一个Macther对象,它会有许多的功能可用。
我们看下面的代码
public static void main(String[] args) {
Pattern pattern = Pattern.compile("<.+?>");
Matcher matcher = pattern.matcher("<tr>");
System.out.println(matcher.matches());//true
}
组
组是用括号划分的正则表达式,可以根据组的编号来引用某个组。组号为0表示整个表达式,组号为1表示被第一对括号括起的组,依此推类
看下面表达式
A(B(C))D
组0为ABCD,组1为BC,组2为C
Matcher对象提供一系列方法用来获取与组相关的信息
public static void main(String[] args) {
Pattern pattern = Pattern.compile("j(av)a");
Matcher matcher = pattern.matcher("123javaabbbjacccjavaddd");
System.out.println(matcher.find());//查找多个匹配//true
System.out.println(matcher.start());//前一次匹配到的组的起始位置//3
System.out.println(matcher.end());//前一次匹配到的组的结束位置//7
System.out.println(matcher.group());//前一次匹配到的第0组的内容//java
System.out.println(matcher.group(1));//前一次匹配到的第1组的内容//av
}
下面介绍一下Matcher对象常用的一些方法
- find():查找多个匹配,每次调用查找一次,每次只找一个匹配项,但下次查找会从前一次匹配的结束位置的后一位开始查找,假设我们要查找ava,目标字段是avava那只能find到一次
- lookingAt():只在正则表达式与输入开始处就开始匹配时才会成功。举个例子:表达式ava,遇到java会匹配失败,但是遇上ava或者avaj都能够匹配成功
- matches():只在正则表达式与输入开始处就开始匹配并且表达式和输入完全匹配时才会成功。举个例子:表达式ava,遇到java和avaj会匹配失败,只有遇上ava才能够匹配成功
注意,如果我们希望像上面代码示例中一样获取组信息,需要先调用上面三个匹配方法,否则就会异常。
Pattern标记
Pattern类的compile方法还有另一个版本,它接受一个标记参数以调整匹配的行为
Pattern Pattern.compile(String regex,int flag)
其中的flag来自以下Pattern类中的常量:
编译标记 | 效果 |
---|---|
Pattern.CANON_EQ | |
Pattern.CASE_INSENSITIVE | 匹配忽略大小写 |
Pattern.COMMENTS | 忽略空格和以#为开始的注释 |
Pattern.DOTALL | "."匹配所有字符包括终结符,默认不匹配终结符 |
Pattern…MULTILINE | 多行模式下,^和$代表一行的始末,默认则是字符串的始末 |
Pattern.UNICODE_CASE | |
Pattern.UNIX_LINES |
扫描输入
我们通常通过Scanner对象完成对各种输入流的类型读写,否则什么都要自己通过分解String然后进行各种类型的parse会是一件很大的工程。
Scanner stdin = new Scanner(System.in);
int i = stdin.nextInt();
...
这里只做一点简单的额介绍,有需要可以自己去看一下源码有哪些方法可用
Scanner定界符
默认情况下,Scanner根据空白字符对输入进行分词,但是我们也可以使用正则表达式指定我们需要的定界符。
public static void main(String[] args) {
Scanner scanner = new Scanner("12, 42, 22");
scanner.useDelimiter(",\\s*");
while (scanner.hasNextInt()){
System.out.println(scanner.nextInt());
}
//12
//42
//22
}
通过上面例子我们能够看出原字符串中是以“, ”作为分割符的,所以我们通过useDelimiter(",\s*")指定了新的分界符号。
正则表达式扫描
Scanner除了扫描基本类型以外,还能够使用自定义的正则表达式进行扫描。这在扫描复杂数据时非常有用,下面例子将扫描一个防火墙日志文件中记录的威胁数据
public class StringDemo5 {
static String threatData =
"58.27.82.161@02/10/2020\n" +
"58.27.82.161@02/10/2020\n" +
"[Next log session with different data format]\n"+
"58.27.82.161@02/10/2020\n" +
"58.27.82.161@02/10/2020\n" +
"58.27.82.161@02/10/2020\n" +
"[Next log session with different data format]";
public static void main(String[] args) {
Scanner scanner = new Scanner(threatData);
String pattern = "(\\d+[.]\\d+[.]\\d+[.]\\d+)@(\\d{2}/\\d{2}/\\d{4})";
while (scanner.hasNext(pattern)||scanner.hasNext()){
if(!scanner.hasNext(pattern)){
scanner.next();
continue;
}
scanner.next(pattern);
MatchResult result = scanner.match();
String ip = result.group(1);
String date = result.group(2);
System.out.println("Threat on "+date+" from "+ip);
}
/*Threat on 02/10/2020 from 58.27.82.161
Threat on 02/10/2020 from 58.27.82.161
Threat on 02/10/2020 from 58.27.82.161
Threat on 02/10/2020 from 58.27.82.161
Threat on 02/10/2020 from 58.27.82.161*/
}
}
上面代码中,我们希望从数据中筛选出我们需要的威胁记录,并从中提取出ip和时间。注意,hasNext(pattern)和scanner.next(pattern)仅仅针对下一个输入分词进行匹配,也就说,根据我们的正则表达式,在第三行数据会匹配失败,此时不会往下继续匹配,需要通过next()跳到下一个分词