字符串

  • 前言
  • 字符串的不可变
  • + 的重载与 StringBuilder
  • 格式化输出
  • Formatter 类
  • 正则表达式
  • 创建正则表达式
  • Pattern 和 Matcher


前言

字符串操作毫无疑问是计算机程序设计中最常见的行为之一。

字符串的不可变

String 对象是不可变的。查看 JDK 文档你就会发现,String 类中每一个看起来会修改 String 值的方法,实际上都是创建了一个全新的 String 对象,以包含修改后的字符串内容。而最初的 String 对象则丝毫未动。

public class Str {
    public static void main(String[] args) {
        String str = "hello";
        System.out.println(str);
        String str2 = append(str);
        System.out.println(str2);
        System.out.println(str);
    }
    private static String append(String s){
        return s + "world";
    }
}

调用append()方法时,实际传递的是引用的一个拷贝。其实,每当把 String 对象作为方法的参数时,都会复制一份引用,而该引用所指向的对象其实一直待在单一的物理位置上,从未动过。一旦 append()运行结束,s 就消失了。当然了,append()的返回值,其实是最终结果的引用。这足以说明,append()返回的引用已经指向了一个新的对象,而 str 仍然在原来的位置。

+ 的重载与 StringBuilder

String 对象是不可变的,所以指向它的任何引用都不可能修改它的值,因此,也就不会影响到其他引用。不可变性会带来一定的效率问题。为 String 对象重载的 + 操作符就是一个例子。重载的意思是,一个操作符在用于特定的类时,被赋予了特殊的意义(用于 String++= 是 Java 中仅有的两个重载过的操作符,Java 不允许程序员重载任何其他的操作符 )。

操作符 + 可以用来连接 String

public class Str {
    public static void main(String[] args) {
        String s = "hello";
        String str = s + " world";
        System.out.println(str);
        /** Output:
         *  hello world
         */
    }
}

我们可以通过-javap来反编译以上代码,来了解它的底层:

//先编译
javac 类名.java
//反编译
javap -c 类型

java 逗号换成顿号_正则表达式


如果你有汇编语言的经验,以上代码应该很眼熟。即使你完全不了解汇编语言也无需担心)。需要重点注意的是:编译器自动引入了 java.lang.StringBuilder 类。虽然源代码中并没有使用 StringBuilder 类,但是编译器却自作主张地使用了它,就因为它更高效。编译器创建了一个 StringBuilder 对象,调用了两次append() 方法,最后调用 toString() 生成结果。

或许,你会觉得可以随意使用 String 对象,反正编译器会自动为你做性能优化。下面通过两个例子String对象和StringBuilder对象来展示它们的区别。

public class Str {
    public static void main(String[] args) {
        String s = "";
        for (int i = 0; i < 5; i++) {
            s+="";
        }
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < 5; i++) {
            stringBuilder.append("");
        }
    }
}

java 逗号换成顿号_后端_02

通过反编译可以看到,在循环内,使用操作符 + 此时每循环一次就会创建一个新的 StringBuilder 对象。而使用StringBuilder 对象追加元素时,反编译的内容更短,且只创建了一个对象。

java 逗号换成顿号_正则表达式_03


StringBuilder 提供了丰富而全面的方法,包括 insert()replace()substring(),甚至还有reverse(),但是最常用的还是 append()toString()StringBuilder是 Java SE5 引入的,在这之前用的是 StringBuffer。后者是线程安全的,因此开销也会大些。使用 StringBuilder 进行字符串操作更快一点。

格式化输出

在长久的等待之后,Java SE5 终于推出了 C 语言中 printf() 风格的格式化输出这一功能。这不仅使得控制输出的代码更加简单,同时也给与Java开发者对于输出格式与排列更强大的控制能力。

转义符

含义

%n

换行,相当于 \n

%c

单个字符

%d

十进制整数

%u

无符号十进制数

%f

十进制浮点数

%o

八进制数

%x

整型(十六进制数)

%s

字符串

%%

输出百分号

%b

Boolean值

C 语言的 printf() 并不像 Java 那样连接字符串,它使用一个简单的格式化字符串,加上要插入其中的值,然后将其格式化输出。 printf() 并不使用重载的 + 操作符(C语言没有重载)来连接引号内的字符串或字符串变量,而是使用特殊的占位符来表示数据将来的位置。而且它还将插入格式化字符串的参数,以逗号分隔,排成一行。例如:

public class Str {
    public static void main(String[] args) {
        System.out.printf("我是%s", "张三");
        /** Output:
         *  我是张三
         */
    }
}

Java SE5 引入了 format() 方法,可用于 PrintStream 或者 PrintWriter 对象,其中也包括 System.out 对象。format() 方法模仿了 C 语言的 printf()。如果你比较怀旧的话,也可以使用 printf()。以下是一个简单的示例:

public class Str {
    public static void main(String[] args) {
        System.out.printf("我是%s%n", "张三");
        System.out.format("我是%s%n", "张三");
        System.out.println("我是张三");
        /** Output:
         *  我是张三
         *  我是张三
         *  我是张三
         */
    }
}

可以看到,format()printf() 是等价的,它们只需要一个简单的格式化字符串,加上一串参数即可,每个参数对应一个格式修饰符。

Formatter 类

Java 中,所有的格式化功能都是由 java.util.Formatter 类处理的。可以将 Formatter 看做一个翻译器,它将你的格式化字符串与数据翻译成需要的结果。当你创建一个 Formatter 对象时,需要向其构造器传递一些信息,告诉它最终的结果将向哪里输出:

public class Str {
    public static void main(String[] args) {
        Formatter formatter = new Formatter(System.out);
        formatter.format("%s%n","张三");
        formatter.format("%c%n",'张');
        formatter.format("%b%n",'张');
        formatter.format("%d%n",1);
        
        /** Output:
         *  张三
         *  张
         *  true
         *  1
         */
    }
}

一定要注意类型匹配,否则会触发异常。用到了 %b 转换,只要该参数不为 null,其转换结果永远都是 true。还有许多不常用的类型转换与格式修饰符选项,你可以在 JDK 文档中的 Formatter 类部分找到它们。

String 类也有一个 static format() 方法,可以格式化字符串,其实在 String.format() 内部,它也是创建了一个 Formatter 对象,然后将你传入的参数转给 Formatter。这样的代码更清晰易读。

public class Str {
    public static void main(String[] args) {
        String format = String.format("我是%s", "张三");
        System.out.println(format);
        /** Output:
         *  我是张三
         */
    }
}

正则表达式

正则表达式是一种强大而灵活的文本处理工具。使用正则表达式,我们能够以编程的方式,构造复杂的文本模式,并对输入 String 进行搜索。正则表达式提供了一种完全通用的方式,能够解决各种 String 处理相关的问题:匹配、选择、编辑以及验证。

例如,要找一个数字,它可能有一个负号在最前面,那你就写一个负号加上一个问号,就像这样:

-?

在正则表达式中,用 \d 表示一位数字。Java中 \\ 的意思是“我要插入一个正则表达式的反斜线,所以其后的字符具有特殊的意义。例如,如果你想表示一位数字,那么正则表达式应该是 \\d;换行符和制表符之类的东西只需要使用单反斜线:\n\t;要表示“一个或多个之前的表达式”,应该使用 +;用竖线 | 表示或操作。应用正则表达式最简单的途径,就是利用 String 类内建的功能,如下所示:

public class Str {
    public static void main(String[] args) {
        System.out.println("+183".matches("-?\\d+"));
        System.out.println("+183".matches("(-|\\+)?\\d+"));
        /** Output:
         *  false
         *  true
         */
    }
}

除此之外还有:split() 方法,其功能是“将字符串从正则表达式匹配的地方切开。”;replaceAll()方法和replaceFirst()方法可以进行匹配替换操作,replaceFirst()方法只替换第一个匹配元素:

public class Str {
    public static void main(String[] args) {
        String string = "This is string";
        //非单词字符进行切割
        System.out.println(Arrays.toString(string.split("\\W+")));
        //T开头的字母替换为located
        System.out.println(string.replaceAll("T\\w+", "located"));
        //第一个T开头的字母替换为located
        System.out.println(string.replaceFirst("T\\w+", "located"));
        /** Output:
         *  [This, is, string]
         *  located is string
         *  located is string
         */
    }
}

创建正则表达式

我们首先从正则表达式可能存在的构造集中选取一个很有用的子集,以此开始学习正则表达式。正则表达式的完整构造子列表,请参考JDK文档 java.util.regex 包中的 Pattern类。

表达式

含义

B

指定字符B

\xhh

十六进制值为0xhh的字符

\uhhhh

十六进制表现为0xhhhh的Unicode字符

\t

制表符Tab

\n

换行符

\r

回车

\f

换页

\e

转义(Escape)

当你学会了使用字符类(character classes)之后,正则表达式的威力才能真正显现出来。以下是一些创建字符类的典型方式,以及一些预定义的类:

表达式

含义

.

任意字符

[abc]

包含a、b或c的任何字符(和a b c作用相同)

[^abc]

除a、b和c之外的任何字符(否定)

[a-zA-Z]

从a到z或从A到Z的任何字符(范围)

[abc[hij]]

a、b、c、h、i、j中的任意字符(与a b c h i j作用相同)(合并)

[a-z&&[hij]]

任意h、i或j(交)

\s 空白符

(空格、tab、换行、换页、回车)

\S

非空白符([^\s])

\d

数字([0-9])

\D

非数字([^0-9])

\w

词字符([a-zA-Z_0-9])

\W

非词字符([^\w])

下面是不同的边界匹配符:

边界匹配符

含义

^

一行的开始

$

一行的结束

\b

词的边界

\B

非词的边界

\G

前一个匹配的结束

下面展示了,h开头,d结尾,中间任意字符的一个简单的正则表达式:

public class Str {
    public static void main(String[] args) {
        System.out.println("hello world".matches("^h(.*)d$"));
        /** Output:
         *  true
         */
    }
}

我们的目的并不是编写最难理解的正则表达式,而是尽量编写能够完成任务的、最简单以及最必要的正则表达式。一旦真正开始使用正则表达式了,你就会发现,在编写新的表达式之前,你通常会参考代码中已经用到的正则表达式。

Pattern 和 Matcher

比起功能有限的 String 类,我们更愿意构造功能强大的正则表达式对象。只需导入 java.util.regex包,然后用 static Pattern.compile() 方法来编译你的正则表达式即可。它会根据你的 String 类型的正则表达式生成一个 Pattern 对象。接下来,把你想要检索的字符串传入 Pattern 对象的 matcher() 方法。matcher() 方法会生成一个 Matcher 对象。使用 Matcher 上的方法,我们将能够判断各种不同类型的匹配是否成功:

boolean matches() 
boolean lookingAt() 
boolean find() 
boolean find(int start)

Matcher.find() 方法可用来在 CharSequence 中查找多个匹配。例如:

public class Str {
    public static void main(String[] args) {
        // 按指定模式在字符串查找
        String line = "2022 One World One Family";
        String pattern = "\\w+.*";

        // 创建 Pattern 对象
        Pattern r = Pattern.compile(pattern);
        // 现在创建 matcher 对象
        Matcher m = r.matcher(line);
        while (m.find()) {
            System.out.println("Found value: " + m.group());
        } else {
            System.out.println("NO MATCH");
        }
        /** Output:
         *  Found value: 2022 One World One Family
         */
    }
}

在匹配操作成功之后,start() 返回先前匹配的起始位置的索引,而 end() 返回所匹配的最后字符的索引加一的值。匹配操作失败之后(或先于一个正在进行的匹配操作去尝试)调用 start()end() 将会产生 IllegalStateException

public class Str {
    public static void main(String[] args) {
        // 按指定模式在字符串查找
        String line = "2022 One World One Family";
        String pattern = "\\w+.*";

        // 创建 Pattern 对象
        Pattern r = Pattern.compile(pattern);
        // 现在创建 matcher 对象
        Matcher m = r.matcher(line);
        while (m.find()) {
            System.out.println("start:"+ m.start());
            System.out.println("end:"+ m.end());
        }
        /** Output:
         *  start:0
         *  end:25
         */
    }
}

通过 reset() 方法,可以将现有的 Matcher 对象应用于一个新的字符序列:

public class Str {
    public static void main(String[] args) {
        // 按指定模式在字符串查找
        String line = "2022 One World One Family";
        String pattern = "\\w+.*";

        // 创建 Pattern 对象
        Pattern r = Pattern.compile(pattern);
        // 现在创建 matcher 对象
        Matcher m = r.matcher(line);
        while (m.find()) {
            System.out.println("old: "+m.group());
            m.reset("hello world");
            while(m.find()) {
                System.out.println("reset: "+m.group());
            }
        }
        /** Output:
         *  old: 2022 One World One Family
         *  reset: hello world
         */
    }
}

Java 引入正则表达式(J2SE1.4)和 Scanner 类(Java SE5)之前,分割字符串的唯一方法是使用 StringTokenizer 来分词。不过,现在有了正则表达式和 Scanner,我们可以使用更加简单、更加简洁的方式来完成同样的工作了。基本上,我们可以放心地说,StringTokenizer 已经可以废弃不用了。