Java17-入门基础知识-十-

111 阅读59分钟

Java17 入门基础知识(十)

原文:Beginning Java 17 Fundamentals

协议:CC BY-NC-SA 4.0

十七、格式化数据

在本章中,您将学习:

  • 如何格式化和解析日期和数字

  • 如何使用printf样式的格式

  • 如何创建使用自定义格式化程序的类

Java 提供了一组丰富的 API 来格式化数据。数据可能包括简单的值(如数字)或对象(如字符串、日期和其他类型的对象)。本章介绍了 Java 中不同类型值的格式化选项。本章中的所有示例程序都是清单 17-1 中声明的jdojo.format模块的成员。

// module-info.java
module jdojo.format {
    exports com.jdojo.format;
}

Listing 17-1The Declaration of a jdojo.format Module

格式化日期

日期时间 API 在第十六章中有所介绍。如果您正在编写与日期和时间相关的新代码,建议您使用日期-时间 API。但是,如果您需要使用使用旧方式格式化日期和时间的遗留代码,则提供本节内容。

在本节中,我们将讨论如何使用传统的日期 API 来格式化日期。我们还将讨论如何解析一个字符串来创建一个日期对象。您可以用预定义的格式或自己选择的格式来格式化日期。Java 库提供了两个类来格式化“java.text”包中的日期:

  • java.text.DateFormat

  • java.text.SimpleDateFormat

接下来的两节将向您展示如何以预定义和自定义的格式来格式化日期。

使用预定义的日期格式

使用DateFormat类使用预定义的格式来格式化日期。是一个abstract类。该类是抽象的,所以不能使用new操作符创建该类的实例。你可以调用它的一个getXxxInstance()方法,其中Xxx可以是DateDateTimeTime,来获取格式化程序对象,或者只是getInstance()。格式化的文本取决于两个因素:样式和区域设置。使用DateFormat类的format()方法来格式化日期和时间。格式化的样式决定了格式化文本中包含多少日期/时间信息,而区域设置决定了如何组合所有信息。DateFormat类将五种样式定义为常量:

  • DateFormat.DEFAULT

  • DateFormat.SHORT

  • DateFormat.MEDIUM

  • DateFormat.LONG

  • DateFormat.FULL

DEFAULT的格式与MEDIUM相同,除非你使用getInstance(),默认为SHORT。表 17-1 显示了美国地区相同日期的不同格式。

表 17-1

为区域设置(如美国)预定义的日期格式样式和格式化文本

|

风格

|

格式化日期示例

| | --- | --- | | DEFAULT | Mar 27, 2021 | | SHORT | 3/27/21 | | MEDIUM | Mar 27, 2021 | | LONG | March 27, 2021 | | FULL | Thursday, March 27, 2021 |

java.util.Locale类包含一些常见地区的常量。例如,对于语言为"fr"(法语)和国家代码为"FR"的地区,可以使用Locale.FRANCE。或者,你可以为法兰西创建一个Locale对象,如下所示:

Locale french FranceLocale = new Locale("fr", "FR") ;

要创建一个Locale,如果Locale类没有为那个国家声明一个常量,您需要使用一个两个字母的小写语言代码和一个两个字母的大写国家代码。语言代码和国家代码已在 ISO-639 代码和 ISO-3166 代码中列出。创建语言环境的更多示例如下:

Locale hindiIndiaLocale = new Locale("hi", "IN");
Locale bengaliIndiaLocale = new Locale("bn", "IN");
Locale thaiThailandLocale = new Locale("th", "TH");

Tip

使用Locale.getDefault()方法为您的系统获取默认的Locale

以下代码片段打印美国地区的长格式的当前日期:

Date today = new Date();
DateFormat formatter = DateFormat.getDateInstance(DateFormat.LONG, Locale.US);
String formattedDate = formatter.format(today);

System.out.println(formattedDate);
August 6, 2021

清单 17-2 中列出的程序默认以短格式和中格式显示地区日期(对于运行本例的 JVM 是 US)、法国和德国。程序打印当前日期。当你运行这个程序时,它将打印同一日期的不同格式。您可能会得到不同的输出,因为程序打印当前日期。

// PredefinedDateFormats.java
package com.jdojo.format;
import java.text.DateFormat;
import java.util.Date;
import java.util.Locale;
public class PredefinedDateFormats {
    public static void main(String[] args) {
        // Get the current date
        Date today = new Date();
        // Print date in the default locale format
        Locale defaultLocale = Locale.getDefault();
        printLocaleDetails(defaultLocale);
        printDate(defaultLocale, today);
        // Print date in French (France) format
        printLocaleDetails(Locale.FRANCE);
        printDate(Locale.FRANCE, today);
        // Print date in German (Germany) format. You could also use Locale.GERMANY
        // instead of new Locale ("de", "DE").
        Locale germanLocale = new Locale("de", "DE");
        printLocaleDetails(germanLocale);
        printDate(germanLocale, today);
    }
    public static void printLocaleDetails(Locale locale) {
        String languageCode = locale.getLanguage();
        String languageName = locale.getDisplayLanguage();
        String countryCode = locale.getCountry();
        String countryName = locale.getDisplayCountry();
        // Print the locale info
        System.out.println("Language: " + languageName + "("
                + languageCode + "); "
                + "Country: " + countryName
                + "(" + countryCode + ")");
    }
    public static void printDate(Locale locale, Date date) {
        // Format and print the date in SHORT style
        DateFormat formatter = DateFormat.getDateInstance(DateFormat.SHORT, locale);
        String formattedDate = formatter.format(date);
        System.out.println("SHORT: " + formattedDate);
        // Format and print the date in MEDIUM style
        formatter = DateFormat.getDateInstance(DateFormat.MEDIUM, locale);
        formattedDate = formatter.format(date);
        System.out.println("MEDIUM: " + formattedDate);
        // Print a blank line at the end
        System.out.println();
    }
}
Language: English(en); Country: United States(US)
SHORT: 1/24/21
MEDIUM: Jan 24, 2021
Language: French(fr); Country: France(FR)
SHORT: 24/01/21
MEDIUM: 24 janv. 2021
Language: German(de); Country: Germany(DE)
SHORT: 24.01.21
MEDIUM: 24.01.2021

Listing 17-2Using the Predefined Date Formats

使用自定义日期格式

如果你想使用定制的日期格式,使用SimpleDateFormat类。使用SimpleDateFormat类的格式是区分地区的。它的默认构造器使用默认区域设置和该区域设置的默认日期格式创建格式化程序。您可以使用其他构造器创建格式化程序,在这些构造器中您可以指定自己的日期格式和区域设置。一旦有了SimpleDateFormat类的对象,就可以调用它的format()方法来格式化日期。如果您想为后续的格式化更改日期格式,您可以使用applyPattern()方法,通过传递新的日期格式(或模式)作为参数。下面的代码片段向您展示了如何使用SimpleDateFormat类格式化日期:

// Create a formatter with a pattern dd/MM/yyyy.
SimpleDateFormat simpleFormatter = new SimpleDateFormat("dd/MM/yyyy");
// Get the current date
Date today = new Date();
// Format the current date
String formattedDate = simpleFormatter.format(today);
// Print the date
System.out.println("Today is (dd/MM/yyyy): " + formattedDate);
// Change the date format. Now month will be spelled fully.
simpleFormatter.applyPattern("MMMM dd, yyyy");
// Format the current date
formattedDate = simpleFormatter.format(today);
// Print the date
System.out.println("Today is (MMMM dd, yyyy): " + formattedDate);
Today is (dd/MM/yyyy): 06/08/2021
Today is (MMMM dd, yyyy): August 06, 2021

请注意,在您的计算机上运行这段代码时,输出会有所不同。它将使用默认的区域设置以这种格式打印当前日期。前面的输出是在美国地区。

表 17-2 中列出了用于创建日期和时间格式的字母及其含义。这些示例显示的日期是 2021 年 7 月 10 日下午 12:30:55。

表 17-2

用于格式化日期和时间的格式化符号列表

|

|

日期或时间组件

|

语句

|

例子

| | --- | --- | --- | --- | | G | 时代标志 | 文本 | 广告 | | y | 年 | 年 | 2021; Twenty-one | | Y | 基于周的年份 | 年 | 2021; Twenty-one | | M | 一年中的月份 | 月 | 七月;七月;07 | | w | 一年中的周 | 数字 | Twenty-eight | | W | 月中的周 | 数字 | Two | | D | 一年中的每一天 | 数字 | One hundred and ninety-one | | d | 一个月中的第几天 | 数字 | Ten | | F | 一个月中的星期几 | 数字 | Two | | E | 一周中的某一天 | 文本 | 周六;坐 | | a | AM/PM 标记 | 文本 | 首相 | | H | 一天中的小时(0–23) | 数字 | Twelve | | k | 一天中的小时数(1–24) | 数字 | Twelve | | K | 上午/下午的小时数(0–11) | 数字 | Zero | | h | 上午/下午的小时数(1–12) | 数字 | Twelve | | m | 小时中的分钟 | 数字 | Thirty | | s | 分钟秒 | 数字 | Fifty-five | | S | 毫秒 | 数字 | Nine hundred and seventy-eight | | z | 时区 | 通用时区 | 太平洋标准时间;PSTGMT-08:00 | | Z | 时区 | RFC 822 时区 | –0800 |

您可以在格式化的日期中嵌入文字。假设您将自己的出生日期(1969 年 9 月 19 日)存储在一个 date 对象中,现在您想将其打印为“我出生于 1969 年 9 月 19 日”。消息中的一些部分来自出生日期,而其他部分是文字,它们旨在按原样出现在消息中。在日期模式中,不能将字母(如 A–Z 和 A–Z)用作文字。您需要将它们放在单引号内,将其视为文字,而不是格式模式的一部分。首先,您需要一个Date对象来表示 1969 年 9 月 19 日。Date类的构造器采用年、月和日,已被弃用。让我们从GregorianCalendar类开始,使用它的getTime()方法获得一个Date对象。以下代码片段打印了这条消息:

// Create a GregorianCalendar object with September 19, 1969 as date
GregorianCalendar gc = new GregorianCalendar(1969, Calendar.SEPTEMBER,19);
// Get a Date object
Date birthDate = gc.getTime();
// Create the pattern. You must place literals inside single quotes
String pattern = "'I was born on the day' dd 'of the month' MMMM 'in' yyyy";
// Create a SimpleDateFormat with the pattern
SimpleDateFormat simpleFormatter = new SimpleDateFormat(pattern);
// Format and print the date
System.out.println(simpleFormatter.format(birthDate));
I was born on the Day 19 of the month September in 1969

解析日期

在前面几节中,您已经将日期对象转换为格式化文本。让我们看看如何将文本转换成Date对象。这是通过使用SimpleDateFormat类的parse()方法来完成的。parse()方法的签名如下:

Date parse(String text, ParsePosition startPos)

该方法有两个参数。第一个参数是要从中提取日期的文本。第二个是文本中字符的起始位置,从这里开始解析。文本中可以嵌入日期部分。例如,您可以从文本中提取两个日期,如“第一个日期是 1995 年 1 月 1 日,第二个日期是 2001 年 12 月 12 日”。因为解析器不知道日期在文本中的开始位置,所以您需要使用ParsePosition对象告诉它。它只是跟踪解析位置。对于ParsePosition类只有一个构造器,它采用一个int,这是解析开始的位置。在parse()方法成功之后,ParsePosition对象的索引被设置为所用日期文本的最后一个字符的索引加 1。请注意,该方法不使用所有传递的文本作为其第一个参数。它只使用创建日期对象所需的文本。

让我们从一个简单的例子开始。假设您有一个字符串"09/19/1969",它代表日期 1969 年 9 月 19 日。你想从这个字符串中得到一个Date对象。以下代码片段说明了这些步骤:

// Our text to be parsed
String text = "09/19/1969";
// Create a pattern for the date text "09/19/1969"
String pattern = "MM/dd/yyyy";
// Create a SimpleDateFormat object to represent this pattern
SimpleDateFormat simpleFormatter = new SimpleDateFormat(pattern);
// Since the date part in text "09/19/1969" start at index zero,
// we create a ParsePosition object with value zero
ParsePosition startPos = new ParsePosition(0);
// Parse the text
Date parsedDate = simpleFormatter.parse(text, startPos);
// Here, parsedDate will have September 19, 1969 as date and startPos current index
// will be set to 10, which you can get calling startPos.getIndex() method.

让我们解析更复杂的文本。如果上一个例子中的文本是"09/19/1969 Junk",您将得到相同的结果,因为在读取 1969 之后,解析器将不再查看文本中的任何字符。假设你有文本"XX01/01/1999XX12/31/2000XX"。文本中嵌入了两个日期。如何解析这两个日期?第一个日期的文本从索引 2 开始(前两个 x 的索引为 0 和 1)。一旦对第一个日期文本的解析完成,ParsePosition对象将指向文本中的第三个 X。您只需要将它的索引增加 2,指向第二个日期文本的第一个字符。以下代码片段说明了这些步骤:

// Our text to be parsed
String text = "XX01/01/1999XX12/31/2000XX";
// Create a pattern for our date text "09/19/1969"
String pattern = "MM/dd/yyyy";
// Create a SimpleDateFormat object to represent this pattern
SimpleDateFormat simpleFormatter = new SimpleDateFormat(pattern);
// Set the start index at 2
ParsePosition startPos = new ParsePosition(2);
// Parse the text to get the first date (January 1, 1999)
Date firstDate = simpleFormatter.parse(text, startPos);
// Now, startPos has its index set after the last character of the first date parsed.
// To set its index to the next date increment its index by 2.
int currentIndex = startPos.getIndex();
startPos.setIndex(currentIndex + 2);
// Parse the text to get the second date (December 31, 2000)
Date secondDate = simpleFormatter.parse(text, startPos);

留给读者的练习是编写一个程序,从文本“我出生于 1969 年 9 月 19 日”中提取Date对象中的日期。提取的日期应该是 1969 年 9 月 19 日。(提示:在前面的一个示例中,当您处理格式化日期对象时,已经有了该文本的模式。)

这里还有一个解析包含日期和时间的文本的例子。假设您有文本"2003-04-03 09:10:40.325",它以年-月-日hour:minute:second.millisecond的格式表示时间戳。您想要获得时间戳的时间部分。清单 17-3 展示了如何从这个文本中获取时间部分。

// ParseTimeStamp.java
package com.jdojo.format;
import java.util.Date;
import java.util.Calendar;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
public class ParseTimeStamp {
    public static void main(String[] args){
        String input = "2003-04-03 09:10:40.325";
        // Prepare the pattern
        String pattern = "yyyy-MM-dd HH:mm:ss.SSS" ;
        SimpleDateFormat sdf = new SimpleDateFormat(pattern);
        // Parse the text into a Date object
        Date dt = sdf.parse(input, new ParsePosition(0));
        System.out.println(dt);
        // Get the Calendar instance
        Calendar cal = Calendar.getInstance();
        // Set the time
        cal.setTime(dt);
        // Print time parts
        System.out.println("Hour:" + cal.get(Calendar.HOUR));
        System.out.println("Minute:" + cal.get(Calendar.MINUTE));
        System.out.println("Second:" + cal.get(Calendar.SECOND));
        System.out.println("Millisecond:" + cal.get(Calendar.MILLISECOND));

    }
}
Thu Apr 03 09:10:40 CST 2003
Hour:9
Minute:10
Second:40
Millisecond:325

Listing 17-3Parsing a Timestamp to Get Its Time Parts

格式化数字

在本节中,我们将讨论如何格式化数字。我们还将讨论如何解析一个字符串来创建一个Number对象。以下两个类可用于格式化和解析数字:

  • java.text.NumberFormat

  • java.text.DecimalFormat

NumberFormat类用于将数字格式化为特定地区的预定义格式。DecimalFormat类用于在特定的地区将数字格式化为您选择的格式。

使用预定义的数字格式

您可以使用NumberFormat类的getXxxInstance()方法来获取格式化程序对象的实例,其中Xxx可以替换为NumberCurrencyIntegerPercent,或者只替换为getInstance()。这些方法是重载的。如果不带任何参数调用它们,它们将返回默认区域设置的格式化程序对象。调用format()方法,将数字作为参数传递,以获得字符串形式的格式化数字。下面的代码片段向您展示了如何为不同的地区获取不同类型的数字格式化程序。它还向您展示了如何使用针对美国地区的货币格式化程序来格式化薪水。请注意,它只进行格式化,不进行货币转换:

// Get a number formatter for default locale
NumberFormat defaultFormatter = NumberFormat.getNumberInstance();
// Get a number formatter for French (France) locale
NumberFormat frenchFormatter = NumberFormat.getNumberInstance(Locale.FRENCH);
// Get a currency formatter for US
NumberFormat usCurrencyFormatter = NumberFormat.getCurrencyInstance(Locale.US);
double salary = 12590.90;
String str = usCurrencyFormatter.format(salary);
System.out.println("Salary in US currency: " + str);
Salary in US currency: $12,590.90

清单 17-4 展示了如何将数字格式化为当前地区(本例中默认地区为美国)和印度地区的默认格式。

// DefaultNumberFormatters.java
package com.jdojo.format;
import java.util.Locale;
import java.text.NumberFormat;
public class DefaultNumberFormatters {
    public static void main(String[] args){
        double value = 1566789.785 ;
        // Default locale
        printFormatted(Locale.getDefault(), value);
        // Indian locale
        // (Rupee is the Indian currency. Short form is Rs.)
        Locale indianLocale = new Locale("en", "IN");
        printFormatted(indianLocale, value);
    }
    public static void printFormatted(Locale locale, double value) {
        // Get number and currency formatter
        NumberFormat nf = NumberFormat.getInstance(locale);
        NumberFormat cf = NumberFormat.getCurrencyInstance(locale);
        System.out.println("Formatting value: " + value + " for locale: " + locale);
        System.out.println("Number: "   + nf.format(value));
        System.out.println("Currency: " + cf.format(value));
    }
}
Formatting value: 1566789.785 for locale: en_US
Number: 1,566,789.785
Currency: $1,566,789.78
Formatting value: 1566789.785 for locale: en_IN
Number: 1,566,789.785
Currency: Rs. 1,566,789.78

Listing 17-4Formatting Numbers Using Default Formats

使用自定义数字格式

要执行更高级的格式化,可以使用DecimalFormat类。它允许您提供自己的格式模式。一旦创建了一个DecimalFormat类的对象,就可以使用它的applyPattern()方法来改变格式模式。您可以为正数和负数指定不同的模式。这两种模式由分号分隔。

在格式化数字时,DecimalFormat类使用四舍五入模式。例如,如果您在数字格式中只指定了小数点后两位,则 12.745 将被舍入到 12.74,因为 5 在中间,4 是偶数;12.735 也将被舍入到 12.74,因为 5 在中间,第二个位置上最接近的偶数将是 4;12.746 将四舍五入为 12.75。清单 17-5 展示了DecimalFormat类的用法。

// DecimalFormatter.java
package com.jdojo.format;
import java.text.DecimalFormat;
public class DecimalFormatter {
    private static DecimalFormat formatter = new DecimalFormat();
    public static void main(String[] args) {
        formatNumber("##.##", 12.745);
        formatNumber("##.##", 12.746);
        formatNumber("0000.0000", 12.735);
        formatNumber("#.##", -12.735);
        // Positive and negative number format
        formatNumber("#.##;(#.##)", 12.735);
        formatNumber("#.##;(#.##)", -12.735);
    }
    public static void formatNumber(String pattern, double value) {
        // Apply the pattern
        formatter.applyPattern(pattern);
        // Format the number
        String formattedNumber = formatter.format(value);
        System.out.println("Number: " + value + ", Pattern: "
                + pattern + ", Formatted Number: "
                + formattedNumber);
    }
}
Number: 12.745, Pattern: ##.##, Formatted Number: 12.74
Number: 12.746, Pattern: ##.##, Formatted Number: 12.75
Number: 12.735, Pattern: 0000.0000, Formatted Number: 0012.7350
Number: -12.735, Pattern: #.##, Formatted Number: -12.73
Number: 12.735, Pattern: #.##;(#.##), Formatted Number: 12.73
Number: -12.735, Pattern: #.##;(#.##), Formatted Number: (12.73)

Listing 17-5Formatting Numbers

解析数字

您还可以使用DecimalFormat类的parse()方法将字符串解析为数字。parse()方法返回一个java.lang.Number类的对象。您可以使用xyzValue()方法获取原始值,其中xyz可以是bytedoublefloatintlongshort

清单 17-6 展示了使用DecimalFormat类来解析一个数字。注意,您也可以使用java.lang.Double类的parseDouble()方法将字符串解析为double值。但是,该字符串必须采用默认的数字格式。使用DecimalFormat类的parse()方法的优点是字符串可以是任何格式。

// ParseNumber.java
package com.jdojo.format;
import java.text.DecimalFormat;
import java.text.ParsePosition;
public class ParseNumber {
    public static void main(String[] args) {
        // Parse a string to decimal number
        String str = "XY4,123.983";
        String pattern = "#,###.###";
        DecimalFormat formatter = new DecimalFormat(pattern);
        // Create a ParsePosition object to specify the first digit of number
        // in the string. It is 4 in "XY4,123.983" with the index 2.
        ParsePosition pos = new ParsePosition(2);
        Number numberObject = formatter.parse(str, pos);
        double value = numberObject.doubleValue();
        System.out.println("Parsed Value is " + value);
    }
}
Parsed Value is 4123.983

Listing 17-6Parsing Numbers

printf 样式的格式

在这一节中,我们将讨论如何使用类似于 c 语言中的printf()函数所支持的printf样式的格式化来格式化对象和值。首先,我们将介绍 Java 中的printf样式格式化支持的一般思想,然后将介绍格式化所有类型的值的细节。

大局

java.util.Formatter类支持printf风格的格式,类似于 C 编程语言中的printf()函数所支持的格式。如果您熟悉 C、C++和 C#,您应该更容易理解本节中的讨论。在本节中,您将使用格式化字符串,如"%1$s""%1$4d"等。在你的代码中没有完整的解释它们的意思。你可能无法完全理解它们;你现在应该忽略它们。只需关注输出,并尝试了解Formatter类想要完成的更大的画面,而不是试图理解细节。我们将在下一节讨论细节。让我们从清单 17-7 中的一个简单例子开始。您可能会得到稍微不同的输出。

// PrintfTest.java
package com.jdojo.format;
import java.util.Date;
public class PrintfTest {
    public static void main(String[] args) {
        // Formatting strings
        System.out.printf("%1$s, %2$s, and %3$s %n", "Fu", "Hu", "Lo");
        System.out.printf("%3$s, %2$s, and %1$s %n", "Fu", "Hu", "Lo");
        // Formatting numbers
        System.out.printf("%1$4d, %2$4d, %3$4d %n", 1, 10, 100);
        System.out.printf("%1$4d, %2$4d, %3$4d %n", 10, 100, 1000);
        System.out.printf("%1$-4d, %2$-4d, %3$-4d %n", 1, 10, 100);
        System.out.printf("%1$-4d, %2$-4d, %3$-4d %n", 10, 100, 1000);
        // Formatting date and time
        Date dt = new Date();
        System.out.printf("Today is %tD %n", dt);
        System.out.printf("Today is %tF %n", dt);
        System.out.printf("Today is %tc %n", dt);
    }
}
Fu, Hu, and Lo
Lo, Hu, and Fu
1,  10,  100
10, 100, 1000
1,  10,  100
10, 100, 1000
Today is 08/06/21
Today is 2021-08-06
Today is Sun Aug 06 10:29:03 CDT 2021

Listing 17-7Using C’s printf-Style Formatting in Java

您一直在使用System.out.println()System.out.print()方法在标准输出上打印文本。实际上,System.outjava.io.PrintStream类的一个实例,它有println()print()实例方法。PrintStream类包含另外两个方法,format()printf(),它们可以用来将格式化的输出写到PrintStream实例中。这两种方法工作原理相同。清单 17-5 使用System.out.printf()方法将格式化文本打印到标准输出。

String类包含一个format()静态方法,它返回一个格式化的字符串。PrintStream类的format() / printf()方法和String类的format()静态方法的格式化行为是相同的。它们之间唯一的区别是PrintStream类中的format()printf()方法将格式化的输出写入输出流,而String类的format()方法将格式化的输出作为String返回。

PrintStream类的format()printf()方法和String类的format()方法是方便的方法。它们的存在是为了简化文本格式。然而,Formatter类完成了真正的工作。下面详细讨论一下Formatter类。您将在示例中使用这些方便的方法。一个Formatter用于格式化文本。格式化的文本可以写入以下目的地:

  • 可追加的(例如,StringBuffer、StringBuilder、Writer 等。)

  • 一个文件

  • 输出流

  • 打印流

下面的代码片段完成了与清单 17-7 中的代码相同的事情。这一次,您使用一个Formatter对象来格式化数据。当您调用Formatter对象的format()方法时,格式化的文本存储在StringBuilder对象中,您将它传递给Formatter对象的构造器。当您完成所有文本的格式化后,您调用StringBuildertoString()方法来获得整个格式化的文本:

// Create an Appendable data storage for our formatted output
StringBuilder sb = new StringBuilder();
// Create a Formatter that will store its output to the StringBuffer
Formatter fm = new Formatter(sb);
// Formatting strings
fm.format("%1$s, %2$s, and %3$s %n", "Fu", "Hu", "Lo");
fm.format("%3$s, %2$s, and %1$s %n", "Fu", "Hu", "Lo");
// Formatting numbers
fm.format("%1$4d, %2$4d, %3$4d %n", 1, 10, 100);
fm.format("%1$4d, %2$4d, %3$4d %n", 10, 100, 1000);
fm.format("%1$-4d, %2$-4d, %3$-4d %n", 1, 10, 100);
fm.format("%1$-4d, %2$-4d, %3$-4d %n", 10, 100, 1000);
// Formatting date and time
Date dt = new Date();
fm.format("Today is %tD %n", dt);
fm.format("Today is %tF %n", dt);
fm.format("Today is %tc %n", dt);
// Display the entire formatted string
System.out.println(sb.toString());

如果您想将所有格式化的文本写入一个文件,可以使用下面的代码片段。您将需要处理FileNotFoundException,如果指定的文件不存在,它可能会从Formatter类的构造器中抛出。当您使用完Formatter对象后,您将需要调用它的close()方法来关闭输出文件。注意示例代码中使用了一个try-with-resources块,所以格式化程序是自动关闭的:

import java.io.File;
import java.io.FileNotFoundException;
import java.util.Formatter;
...
// Create a Formatter that will write the output to the file C:\kishori\xyz.txt
try (Formatter fm = new Formatter(new File("C:\\kishori\\xyz.txt"))) {
    // Formatting strings
    fm.format("%1$s, %2$s, and %3$s %n", "Fu", "Hu", "Lo");
    fm.format("%3$s, %2$s, and %1$s %n", "Fu", "Hu", "Lo");
} catch (FileNotFoundException e) {
    e.printStackTrace();
}

Formatter类的format()方法被重载。其声明如下:

  • Formatter format(String format, Object... args)

  • Formatter format(Locale l, String format, Object... args)

第一个版本的format()方法使用默认的语言环境进行格式化。第二个版本允许您指定一个地区。PrintStream类的format() / printf()方法和String类的format()方法提供了相同的两个版本的format()方法,它们接受相同类型的参数。对Formatter类的format()方法的讨论同样适用于PrintStreamString类中的这些便利方法。

只要适用,Formatter类就使用特定于地区的格式。例如,如果您想要格式化一个十进制数,比如 12.89,在法国该数被格式化为 12,89(注意 12 和 89 之间的逗号),而在美国它被格式化为 12.89(注意 12 和 89 之间的点)。format()方法的 locale 参数用于将文本格式化为特定于语言环境的格式。下面的代码片段演示了特定于区域设置的格式的效果。请注意,对于相同的输入值,美国和法国的格式化输出有所不同:

System.out.printf(Locale.US, "In US: %1$.2f %n", 12.89);
System.out.printf(Locale.FRANCE, "In France: %1$.2f %n", 12.89);
Date dt = new Date();
System.out.printf(Locale.US, "In US: %tA %n", dt);
System.out.printf(Locale.FRANCE, "In France: %tA %n", dt);
In US: 12.89
In France: 12,89
In US: Friday
In France: vendredi

细节

使用Formatter格式化数据需要两种输入:

  • 格式字符串

  • 值的列表

格式字符串是定义输出外观的模板。它包含零个或多个固定文本和零个或多个嵌入的格式说明符。固定文本不会应用任何格式。格式说明符有两个用途。它在格式字符串中充当格式化数据的占位符,并指定应该如何格式化数据。

让我们考虑下面的例子。假设您想打印一个人的出生日期。以下是此类文本的一个示例:

January 16, 1970 is John's birthday.

Note

除非另有说明,本节中的所有输出均为美国语言环境。

以前的文本包含固定文本和格式化文本。固定文本应该出现在输出中。格式化的文本将取决于输入。您可以将之前的文本转换为模板,如下所示:

<month> <day>, <year> is <name>'s birthday.

您已经用尖括号中的占位符替换了可能不同的文本,例如,<month><day>等。您将需要四个输入值(月、日、年和名称)来使用前面的模板获得格式化的文本。例如,如果您将<month><day><year><name>的值分别提供为"January""16""1970""John",模板将生成

January 16, 1970 is John's birthday.

在本例中,您已经用实际值替换了模板中的占位符。您没有对实际值进行任何格式化。由Formatter类提供的格式以类似的方式工作。我们在这个例子中称之为占位符的叫做格式说明符。我们在这个例子中称之为模板的东西叫做格式字符串

格式说明符总是以百分号(%)开始。您可以将您的模板转换成一个格式字符串,它可以与Formatter类一起使用,如下所示:

%1$tB %1$td, %1$tY is %2$s's birthday.

在这个格式字符串中,“%1$tB""%1$td""%1$tY"%2$s"是四个格式说明符,而" "", ""is "'s birthday."是固定文本。

下面的代码片段使用这个格式字符串打印格式化文本。注意dob"John"是格式字符串的输入值。在这种情况下,输入值dob是包含出生日期的LocalDate类的一个实例:

LocalDate dob = LocalDate.of(1970, Month.JANUARY, 16);
System.out.printf("%1$tB %1$td, %1$tY is %2$s's birthday.", dob, "John");
January 16, 1970 is John's birthday.

格式说明符的一般语法如下:

%<argument-index$><flags><width><.precision><conversion>

除了%<conversion>部分,其他部分都是可选的。请注意,格式说明符的任何两个部分之间都没有空格。%(百分号)表示格式字符串中格式说明符的开始。如果要将%指定为格式字符串中固定文本的一部分,需要使用两个连续的%作为%%

<argument-index$>表示格式说明符引用的参数的索引。它由一个十进制格式的整数后跟一个$(美元符号)组成。第一个参数称为1$,第二个称为2$,依此类推。您可以在同一格式字符串内的不同格式说明符中多次引用同一个参数。

<flags>表示输出的格式。它是一组字符。<flags>的有效值取决于格式说明符引用的参数的数据类型。

<width >表示需要写入输出的最小字符数。

通常,<.precision >表示要写入输出的最大字符数。然而,它的确切含义因<conversion>的值而异。这是一个十进制数。它以一个点开始(.)。

<conversion>表示输出应该如何格式化。它的值取决于格式说明符引用的参数的数据类型。这是强制性的。

有两个特殊的格式说明符:%%%n%%格式说明符输出%(一个百分号),而%n输出一个特定于平台的换行符。下面的代码片段演示了这两个特殊格式说明符的用法:

System.out.printf("Interest rate is 10%%.%nJohn%nDonna");
Interest rate is 10%.
John
Donna

您没有为代码中的printf()方法提供任何参数,因为这两个特殊的格式说明符对任何参数都不起作用。注意输出中的两行新行是由格式字符串中的两个%n格式说明符生成的。

引用格式说明符中的参数

我们还没有讨论格式说明符的转换部分。对于本节的讨论,我们使用s作为格式说明符的转换字符。s转换将其参数格式化为字符串。最简单的形式是,您可以使用%s作为格式说明符。让我们考虑以下代码片段及其输出:

System.out.printf("%s, %s, and %s", "Ken", "Lola", "Matt");
Ken, Lola, and Matt

格式字符串中的格式说明符可以通过三种方式引用参数:

  • 普通索引

  • 显式索引

  • 相对索引

普通索引

当一个格式说明符没有指定一个参数索引值时(如在%s中),它被称为普通索引。在普通索引中,参数索引由格式字符串中格式说明符的索引确定。第一个没有参数索引的格式说明符的索引为 1,第二个格式说明符的索引为 2,依此类推。索引为 1 的格式说明符引用第一个参数;索引为 2 的格式说明符引用第二个参数;等等。图 17-1 显示了格式说明符和参数的索引。

img/323069_3_En_17_Fig1_HTML.png

图 17-1

格式字符串中格式说明符的索引和参数的索引

图 17-1 显示了在前面的例子中索引是如何映射的。指定的第一个%s格式是指第一个参数"Ken"。指定的第二个%s格式是指第二个参数"Lola"。而指定的第三个%s格式指的是第三个自变量"Matt"

如果参数的数量大于格式字符串中格式说明符的数量,多余的参数将被忽略。考虑下面的代码片段及其输出。它有三个格式说明符(三个%s)和四个参数。第四个参数"Lo"是一个额外的参数,被忽略:

System.out.printf("%s, %s, and %s", "Ken", "Lola", "Matt", "Lo");
Ken, Lola, and Matt

如果格式说明符引用了一个不存在的参数,就会抛出java.util.MissingFormatArgumentException。以下代码片段将引发此异常,因为参数的数量比格式说明符的数量少一个。有三个格式说明符,但只有两个参数:

// Compiles fine, but throws a runtime exception
System.out.printf("%s, %s, and %s", "Ken", "Lola");

注意,Formatter类的format()方法的最后一个参数是 var-args 参数。还可以将数组传递给 var-args 参数。下面的代码片段是有效的,尽管它使用了三个格式说明符和一个数组类型的参数。数组类型参数包含三个格式说明符的三个值:

String[] names = {"Ken", "Matt", "Lola"};
System.out.printf("%s, %s, and %s", names);
Ken, Matt, and Lola

以下代码片段也是有效的,因为它在数组类型参数中传递了四个值,但只有三个格式说明符:

String[] names = {"Ken", "Matt", "Lola", "Lo"};
System.out.printf("%s, %s, and %s", names);
Ken, Matt, and Lola

以下代码片段无效,因为它使用了只有两个元素和三个格式说明符的数组类型参数。当运行以下代码片段时,将抛出一个MissingFormatArgumentException:

String[] names = {"Ken", "Matt"};
System.out.printf("%s, %s, and %s", names); // Throws an exception

显式索引

当格式说明符显式指定参数索引时,称为显式索引。请注意,参数索引是在格式说明符中的%符号之后指定的。是十进制格式的整数,以$(美元符号)结尾。考虑下面的代码片段及其输出。它使用三种格式说明符,%1$s%2$s%3$s,这些说明符使用显式索引:

System.out.printf("%1$s, %2$s, and %3$s", "Ken", "Lola", "Matt");
Ken, Lola, and Matt

当格式说明符使用显式索引时,它可以使用参数的索引来引用参数列表中任何索引处的参数。考虑以下代码片段:

System.out.printf("%3$s, %1$s, and %2$s", "Lola", "Matt", "Ken");
Ken, Lola, and Matt

这段代码与之前的代码具有相同的输出。但是,在这种情况下,参数列表中的值的顺序不同。第一个格式说明符%3$s,引用第三个参数"Ken";第二个格式说明符%1$s,引用第一个参数"Lola";第三个格式说明符%2$s引用第二个参数"Matt"

允许使用显式索引多次引用同一个参数。也允许不引用格式字符串中的某些参数。在下面的代码片段中,"Lola"的第一个参数没有被引用,而"Ken"的第三个参数被引用了两次:

System.out.printf("%3$s, %2$s, and %3$s", "Lola", "Matt", "Ken");
Ken, Matt, and Ken

相对索引

还有第三种方法引用格式说明符中的参数,这种方法称为相对索引。在相对索引中,格式说明符使用与前一个格式说明符相同的参数。相对索引不使用参数索引值。相反,它使用<字符作为格式说明符中的标志。因为在相对索引中,格式说明符使用与前一个格式说明符相同的参数,所以它不能与第一个格式说明符一起使用,因为第一个格式说明符没有前一个格式说明符。考虑以下代码片段及其输出,它使用相对索引:

System.out.printf("%1$s, %<s, %<s, %2$s, and %<s", "Ken", "Matt");
Ken, Ken, Ken, Matt, and Matt

这段代码使用了五种格式说明符:%1$s%<s%<s%2$s%<s。它使用了两个参数:"Ken""Matt"。请注意,如果某些格式说明符使用相对索引,参数的数量可能会少于格式说明符的数量。%1$s的第一个格式说明符使用显式索引来引用第一个参数"Ken"%<s的第二个格式说明符使用相对索引(注意<标志);因此,它将使用与前面的格式说明符1$s相同的参数。这样,第一个和第二个格式说明符都使用第一个参数"Ken"。这一点通过将"Ken"显示为前两个名称的输出得到了证实。%<s的第三个格式说明符也使用相对索引。它将使用与前一个格式说明符(第二个格式说明符)相同的参数。因为第二个格式说明符使用了第一个参数"Ken",所以第三个也将使用相同的参数。这在将"Ken"显示为第三个名称的输出中得到确认。第四个%2$s格式说明符使用显式索引来使用"Matt"的第二个参数。%<s的第五个也是最后一个格式说明符使用相对索引,它将使用与其前一个格式说明符(第四个格式说明符)相同的参数。由于第四个格式说明符使用第二个参数"Matt",第五个格式说明符也将使用第二个参数"Matt"。这在将"Matt"显示为第五个名称的输出中得到确认。

以下语句将抛出一个MissingFormatArgumentException,因为它对第一个格式说明符使用了相对索引:

System.out.printf("%<s, %<s, %<s, %2$s, and %<s", "Ken", "Matt");

可以混合所有三种类型的索引来引用同一格式字符串中不同格式说明符内的参数。考虑以下语句及其输出:

System.out.printf("%1$s, %s, %<s, %s, and %<s", "Ken", "Matt");
Ken, Ken, Ken, Matt, and Matt

第一个格式说明符使用显式索引来使用第一个参数"Ken"。第二个和第四个格式说明符(都是%s)使用普通索引。第三和第五个格式说明符(都是%<s)使用相对索引。从相对索引规则中可以清楚地看出,第三和第五个格式说明符将分别使用与第二和第四个格式说明符相同的参数。第二个和第四个格式说明符将使用哪些参数?答案很简单。当您有一些使用普通索引和一些显式索引的格式说明符时,只是为了理解这个规则,忽略使用显式索引的格式说明符,并将使用普通索引的格式说明符编号为 1、2 等等。使用此规则,您可以将前面的语句视为与下面的语句相同:

System.out.printf("%1$s, %1$s, %<s, %2$s, and %<s", "Ken", "Matt");

请注意,您已经用%1$s替换了第一次出现的%s,用%2$s替换了第二次出现的%s,就好像它们使用了显式索引一样。这解释了前面语句生成的输出。

在格式说明符中使用标志

标志充当修饰符。他们修改格式化的输出。表 17-3 列出了可用于格式说明符的所有标志。

表 17-3

有效标志、它们的描述和用法示例的列表

|

|

描述

|

例子

| | --- | --- | --- | |   |   | 格式字符串 | 自变量 | 格式化文本 | | - | 结果是左对齐的。请注意,当您没有在格式说明符中使用-标志时,结果是右对齐的。 | "'%6s'" | "Ken" | '   Ken' | | "'%-6s'" | "Ken" | 'Ken   ' | | # | 根据格式说明符的转换部分,参数被格式化为替代形式。该示例显示了同一个十进制数 6270185 被格式化为十六进制格式。当使用#标志时,十六进制数以 0x 为前缀。 | "%x" | 6270185 | 5face9 | | "%#x" | 6270185 | 0x5face9 | | + | 结果包含一个代表正值的+符号。它仅适用于数值。 | "%d" | 105 | 105 | | "%+d" | 105 | +105 | | ' ' | 结果包含正值的前导空格。它仅适用于数值。 | "'%d'" | 105 | '105' | | "'% d'" | 105 | ' 105' | | 0 | 结果是零填充。它仅适用于数值。 | "'%6d'" | 105 | '  105' | | "'%06d'" | 105 | '000105' | | , | 结果包含特定于区域设置的分组分隔符。它仅适用于数值。例如,在美国地区,逗号被用作千位分隔符,而在法国地区,则使用空格。 | "%,d" | 89105 | 89,105``(US Locale) | | "%,d" | 89105 | 89 105``(France locale) | | ( | 负数的结果用括号括起来。它仅适用于数值。 | "%d" | -1969 | -1969 | | "%(d" | -1969 | (1969) | | < | 它导致先前格式说明符的参数被重用。它主要用于格式化日期和时间。 | "%s and %<s" | "Ken" | Ken and Ken |

标志的有效使用取决于其使用的上下文。根据被格式化的值,允许在一个格式说明符中使用多个标志。例如,格式说明符%1$,0(12d使用三个标志:,0(。如果-122899被这个格式说明符用作参数,它将输出(000122,899)。当我们在接下来的小节中讨论不同数据类型的格式时,将详细讨论使用每个标志的效果。

转换字符

不同的转换字符用于格式化不同数据类型的值。例如,s用于将值格式化为字符串。格式说明符中其他部分的有效值也由格式说明符引用的转换字符和参数的数据类型决定。基于数据类型的格式化类型可以大致分为四类:

  • 常规格式

  • 字符格式

  • 数字格式

  • 日期/时间格式

许多转换字符都有大写变体。比如S就是s的大写变体。大写变体将格式化输出转换为大写,就像调用了output.toUpperCase()方法一样,其中output是对格式化输出字符串的引用。以下语句及其输出演示了使用大写变体S的效果。注意,对于相同的输入值"Ken"s产生"Ken"S产生"KEN":

System.out.printf("%s and %<S", "Ken");
Ken and KEN

常规格式

常规格式可用于格式化任何数据类型的值。表 17-4 列出了通用格式类别下可用的转换。

表 17-4

常规格式的转换字符列表

|

转换

|

大写字母

不同的

|

描述

| | --- | --- | --- | | b | B | 它根据参数的值产生truefalse。它为一个null参数和一个值为假的布尔参数产生false。否则,就会产生true。 | | h | H | 它生成一个字符串,该字符串是参数的十六进制格式的哈希代码值。如果自变量为null,则产生"null"。 | | s | S | 它产生参数的字符串表示。如果参数是null,它产生一个"null"字符串。如果参数实现了Formattable接口,它就调用参数上的formatTo()方法,返回值就是结果。如果参数没有实现Formattable接口,那么将对参数调用toString()方法来获得结果。 |

通用格式的格式说明符的通用语法如下:

%<argument_index$><flags><width><.precision><conversion>

宽度表示要写入输出的最小字符数。如果参数的字符串表示形式的长度小于宽度值,结果将用空格填充。空格填充在参数值的左侧执行。如果使用了-标志,则向右执行空格填充。宽度值本身并不能决定结果的内容。宽度和精度的值共同决定了结果的最终内容。

精度表示要写入输出的最大字符数。在应用宽度之前,先将精度应用于参数。您需要理解在宽度之前应用精度的后果。如果精度小于参数的长度,参数将被截断到精度,并执行空格填充以使输出的长度与宽度值匹配。考虑以下代码片段:

System.out.printf("'%4.1s'", "Ken");
'   K'

参数是"Ken",,格式说明符是%4.1s,其中4是宽度,1是精度。首先,应用将值"Ken"截断为K的精度。现在,应用了宽度,这表明至少应该向输出中写入四个字符。但是,应用精度后,您只剩下一个字符。因此,K将用三个空格填充,以匹配宽度值 4。

考虑以下代码片段:

System.out.printf("'%1.4s'", "Ken");
'Ken'

参数值是"Ken",,格式说明符是%1.4s,其中1是宽度,4是精度。因为精度值 4 大于参数长度 3,所以精度没有影响。因为宽度值 1 小于应用精度后结果的宽度,所以宽度值对输出没有影响。

以下是使用布尔、字符串和哈希代码格式转换的几个示例。请注意,哈希代码格式转换(hH)以十六进制格式输出参数的哈希代码值。这些示例还演示了使用大写转换变量的效果:

// Boolean conversion
System.out.printf("'%b', '%5b', '%.3b'%n", true, false, true);
System.out.printf("'%b', '%5b', '%.3b'%n", "Ken", "Matt", "Lola");
System.out.printf("'%B', '%5B', '%.3B'%n", "Ken", "Matt", "Lola");
System.out.printf("%b %n", 1969);
System.out.printf("%b %n", new Object());
'true', 'false', 'tru'
'true', ' true', 'tru'
'TRUE', ' TRUE', 'TRU'
true
true
// String conversion
System.out.printf("'%s', '%5s', '%.3s'%n", "Ken", "Matt", "Lola");
System.out.printf("'%S', '%5S', '%.3S'%n", "Ken", "Matt", "Lola");
// Use '-' flag to left-justify the result. You must use width when you specify the '-' flag
System.out.printf("'%S', '%-5S', '%.3S'%n", "Ken", "Matt", "Lola");
System.out.printf("%s %n", 1969);
System.out.printf("%s %n", true);
System.out.printf("%s %n", new Object());
'Ken', ' Matt', 'Lol'
'KEN', ' MATT', 'LOL'
'KEN', 'MATT ', 'LOL'
1969
true
java.lang.Object@de6f34
// Hash Code conversion
System.out.printf("'%h', '%5h', '%.3h'%n", "Ken", "Matt", "Lola");
System.out.printf("'%H', '%5H', '%.3H'%n", "Ken", "Matt", "Lola");
System.out.printf("%h %n", 1969);
System.out.printf("%h %n", true);
System.out.printf("%h %n", new Object());
'12634', '247b34', '243'
'12634', '247B34', '243'
7b1
4cf
156ee8e

如果您将一个原始类型的值作为参数传递给Formatter类的format()方法(或PrintStream类的printf()方法),原始类型的值将使用自动装箱规则,使用适当类型的包装类转换为引用类型。例如,这种说法

System.out.println("%s", 1969);

被转换为

System.out.println("%s", new Integer(1969));

编写自定义格式化程序

Formatter类通过sS转换支持自定义格式。如果参数实现了java.util.Formattable接口,那么s转换会对参数调用formatTo()方法来获得格式化的结果。向formatTo()方法传递格式说明符中使用的Formatter对象、标志、宽度和精度值的引用。您可以在类的formatTo()方法中应用任何自定义逻辑来格式化您的类的对象。清单 17-8 包含了一个FormattablePerson类的代码,它实现了Formattable接口。

// FormattablePerson.java
package com.jdojo.format;
import java.util.Formattable;
import java.util.Formatter;
import java.util.FormattableFlags;
public class FormattablePerson implements Formattable {
    private String firstName = "Unknown";
    private String lastName = "Unknown";
    public FormattablePerson(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    /* Other code goes here... */
    @Override
    public void formatTo(Formatter formatter, int flags, int width, int precision) {
        String str = this.firstName + " " + this.lastName;
        int alternateFlagValue = FormattableFlags.ALTERNATE & flags;
        if (alternateFlagValue == FormattableFlags.ALTERNATE) {
            str = this.lastName + ", " + this.firstName;
        }
        // Check if uppercase variant of the conversion is being used
        int upperFlagValue = FormattableFlags.UPPERCASE & flags;
        if (upperFlagValue == FormattableFlags.UPPERCASE) {
            str = str.toUpperCase();
        }
        // Call the format() method of formatter argument,
        // so our result is stored in it and the caller will get it
        formatter.format(str);
    }
}

Listing 17-8Implementing a Custom Formatter Using the Formattable Interface

你的Formattable人有名字和姓氏。formatTo()方法内部的逻辑有意保持简单。你检查一下备用旗#。如果在格式说明符中使用了该标志,您可以将人名格式化为LastName, FirstName格式。如果没有使用替代标志,您可以将人名格式化为FirstName LastName格式。你也支持大写变体Ss的转换。如果使用了S转换,您将人名格式化为大写。您的逻辑不使用标志、宽度和精度的其他值。标志作为位掩码的int值传入。要检查是否传递了一个标志,您需要使用按位&操作符。按位&运算符中使用的操作数由java.util.FormattableFlags类中的常量定义。例如,要检查格式说明符是否使用左对齐-标志,您需要使用以下逻辑:

int leftJustifiedFlagValue = FormattableFlags.LEFT_JUSTIFY & flags;
if (leftJustifiedFlagValue == FormattableFlags.LEFT_JUSTIFY) {
    // Left-justified flag '-' is used
} else {
    // Left-justified flag '-' is not used
}

您可以使用字符串转换sS将您的FormattablePerson对象与格式说明符一起使用,如下所示:

FormattablePerson fp = new FormattablePerson("Ken", "Smith");
System.out.printf("%s %n", fp );
System.out.printf("%#s %n", fp );
System.out.printf("%S %n", fp );
System.out.printf("%#S %n", fp );
Ken Smith
Smith, Ken
KEN SMITH
SMITH, KEN

字符格式

字符格式可应用于char原始数据类型或Character对象的值。如果byteByteshortShortintInteger类型的值是有效的 Unicode 码位,也可以应用于这些值。您可以通过使用Character类的isValidCodePoint(int value)静态方法来测试一个整数值是否表示一个有效的 Unicode 码位。

字符格式化的转换字符为c。它的大写变体是C。字符格式不支持标志#和精度。标志-width在通用格式的上下文中具有相同的含义。以下代码片段演示了字符格式的使用:

System.out.printf("%c %n", 'a');
System.out.printf("%C %n", 'a');
System.out.printf("%C %n", 98);
System.out.printf("'%5C' %n", 100);
System.out.printf("'%-5C' %n", 100);
a
A
B
'    D'
'D    '

数字格式

数字格式可以大致分为两类:

  • 整数格式

  • 浮点数格式

格式化数值时,会自动应用许多特定于区域设置的格式。例如,用于数字格式的数字总是特定于区域设置的。如果带格式的数字包含小数分隔符或组分隔符,它们总是分别被替换为特定于区域设置的小数分隔符或组分隔符。以下代码片段显示了相同的数字1234567890在美国、印度和泰国这三个不同地区的不同格式:

Locale englishUS = new Locale ("en", "US");
Locale hindiIndia = new Locale ("hi", "IN");
Locale thaiThailand = new Locale ("th", "TH", "TH");
System.out.printf(englishUS, "%d %n", 1234567890);
System.out.printf(hindiIndia, "%d %n", 1234567890);
System.out.printf(thaiThailand, "%d %n", 1234567890);

img/323069_3_En_17_Figa_HTML.png

整数格式

整数格式化处理格式化整数。可以应用于byteByteshortShortintIntegerlongLongBigInteger的格式值。表 17-5 包含整数格式类别下可用的转换列表。

表 17-5

适用于 byte、Byte、short、Short、int、Integer、long、Long 和 BigInteger 数据类型的转换列表

|

转换

|

大写字母

不同的

|

描述

| | --- | --- | --- | | d |   | 它将参数格式化为特定于区域设置的十进制整数(基数为 10)。此转换不能使用#标志。 | | o |   | 它将参数格式化为基数为 8 的整数,没有任何本地化。如果此转换使用了#标志,输出总是以0(零)开始。(、+, ' ',,标志不能用于此转换。 | | x | X | 它将参数格式化为基数为 16 的整数,没有任何本地化。如果此转换使用了#标志,则输出总是以0x开始。当大写变量 X 与#标志一起使用时,输出总是以0X开始。(, +, ' ',,标志不能用于带有 byte、ByteshortShortintIntegerlongLong数据类型参数的转换。,标志不能与带有数据类型为BigInteger的参数的此转换一起使用。 |

整数格式的格式说明符的一般语法如下:

%<argument_index$><flags><width><conversion>

请注意,格式说明符中的精度部分不适用于整数格式。以下代码片段演示了使用带有各种标志的d转换来格式化整数:

System.out.printf("'%d' %n", 1969);
System.out.printf("'%6d' %n", 1969);
System.out.printf("'%-6d' %n", 1969);
System.out.printf("'%06d' %n", 1969);
System.out.printf("'%(d' %n", 1969);
System.out.printf("'%(d' %n", -1969);
System.out.printf("'% d' %n", 1969);
System.out.printf("'% d' %n", -1969);
System.out.printf("'%+d' %n", 1969);
System.out.printf("'%+d' %n", -1969);
'1969'
'  1969'
'1969  '
'001969'
'1969'
'(1969)'
' 1969'
'-1969'
'+1969'
'-1969'

当转换ox与数据类型为byteByteshort, ShortintIntegerlongLong的负参数一起使用时,参数值首先通过添加数字2N转换为无符号数,其中N是用于表示参数的数据类型值的位数。例如,如果参数数据类型是byte,它需要 8 位来存储值,则–X的参数值将通过向其添加 256 来转换为正的值–X + 256。结果包含值–X + 256的基数为 8 或基数为 16 的等效值。转换ox不会将负参数值转换为BigInteger参数类型的无符号值。考虑以下代码片段和输出:

byte b1 = 9;
byte b2 = -9;
System.out.printf("%o %n", b1);
System.out.printf("%o %n", b2);
11
367

转换o将 8 进制整数 11 输出为正的十进制整数 9。然而,当负十进制整数–9 用于o转换时,–9 被转换为正数-9 + 256 ( =247)。最终输出包含367,它是十进制247的八进制等效值。

下面的代码片段展示了关于intBigInteger参数类型的ox转换的更多示例:

System.out.printf("%o %n", 1969);
System.out.printf("%o %n", -1969);
System.out.printf("%o %n", new BigInteger("1969"));
System.out.printf("%o %n", new BigInteger("-1969"));
System.out.printf("%x %n", 1969);
System.out.printf("%x %n", -1969);
System.out.printf("%x %n", new BigInteger("1969"));
System.out.printf("%x %n", new BigInteger("-1969"));
System.out.printf("%#o %n", 1969);
System.out.printf("%#x %n", 1969);
System.out.printf("%#o %n", new BigInteger("1969"));
System.out.printf("%#x %n", new BigInteger("1969"));
3661
37777774117
3661
-3661
7b1
fffff84f
7b1
-7b1
03661
0x7b1
03661
0x7b1

浮点数格式

浮点数格式化处理格式化数字,它有一个整数部分和一个小数部分。可以应用于floatFloatdoubleDoubleBigDecimal数据类型的格式值。表 17-6 包含用于浮点数格式化的转换列表。

表 17-6

适用于 float、Float、double、Double 和 BigDecimal 数据类型的转换列表

|

转换

|

大写字母

不同的

|

描述

| | --- | --- | --- | | e | E | 它将参数格式化为特定于区域设置的计算机化科学记数法,例如 1.969919e+03。输出包含一个数字,后跟一个十进制分隔符,后跟指数部分。例如,如果精度为 6,1969.919 将被格式化为 1.969919e+03。精度是小数点后的位数。组分隔符标志不能用于此转换。 | | g | G | 它将参数格式化为特定于语言环境的通用科学符号。根据参数的值,它充当e转换或f转换。它根据精度值对参数值进行舍入。如果舍入后的值大于或等于 10-4 但小于 10 个精度,它会将该值格式化为使用了f转换。如果舍入后的值小于 10-4 或大于或等于 10 精度,它会像使用e转换一样格式化该值。请注意,结果中有效数字的总数等于精度值。默认情况下,使用 6 的精度。 | | f |   | 它将参数格式化为特定于区域设置的十进制格式。精度是小数点后的位数。该值根据指定的精度值进行舍入。 | | a | A | 它将参数格式化为十六进制指数形式。它不适用于BigDecimal类型的参数。 |

浮点数格式的格式说明符的一般语法如下:

%<argument_index$><flags><width><.precision><conversion>

精度有不同的含义。含义取决于转换字符。默认情况下,精度值为 6。对于ef转换,精度是小数点后的位数。对于g转换,精度是舍入后得到的量值的总位数。精度不适用于转换。

以下代码片段显示了如何使用默认精度(6)格式化浮点数:

System.out.printf("%e %n", 10.2);
System.out.printf("%f %n", 10.2);
System.out.printf("%g %n", 10.2);
System.out.printf("%e %n", 0.000002079);
System.out.printf("%f %n", 0.000002079);
System.out.printf("%g %n", 0.000002079);
System.out.printf("%a %n", 0.000002079);
1.020000e+01
10.200000
10.2000
2.079000e-06
0.000002
2.07900e-06
'1.97e+03'
0x1.1709e564a6d14p-19

以下代码片段显示了在浮点数格式化中使用widthprecision的效果:

System.out.printf("%.2e %n", 1969.27);
System.out.printf("%.2f %n", 1969.27);
System.out.printf("%.2g %n", 1969.27);
System.out.printf("'%8.2e' %n", 1969.27);
System.out.printf("'%8.2f' %n", 1969.27);
System.out.printf("'%8.2g' %n", 1969.27);
System.out.printf("'%10.2e' %n", 1969.27);
System.out.printf("'%10.2f' %n", 1969.27);
System.out.printf("'%10.2g' %n", 1969.27);
System.out.printf("'%-10.2e' %n", 1969.27);
System.out.printf("'%-10.2f' %n", 1969.27);
System.out.printf("'%-10.2g' %n", 1969.27);
System.out.printf("'%010.2e' %n", 1969.27);
System.out.printf("'%010.2f' %n", 1969.27);
System.out.printf("'%010.2g' %n", 1969.27);
1.97e+03
1969.27
2.0e+03
'1.97e+03'
' 1969.27'
' 2.0e+03'
'  1.97e+03'
'   1969.27'
'   2.0e+03'
'1.97e+03  '
'1969.27   '
'2.0e+03   '
'001.97e+03'
'0001969.27'
'0002.0e+03'

如果浮点转换的参数值是NaNInfinity,则输出分别包含字符串"NaN""Infinity"。以下代码片段显示了当浮点数的值为NaN或无穷大时浮点数的格式:

System.out.printf("%.2e %n", Double.NaN);
System.out.printf("%.2f %n", Double.POSITIVE_INFINITY);
System.out.printf("%.2g %n", Double.NEGATIVE_INFINITY);
System.out.printf("%(f %n", Double.POSITIVE_INFINITY);
System.out.printf("%(f %n", Double.NEGATIVE_INFINITY);
NaN
Infinity
-Infinity
Infinity
(Infinity)

格式化日期和时间

日期/时间格式化处理格式化日期、时间和日期/时间。可以应用于longLongjava.util.Calendarjava.util.Datejava.time.temporal.TemporalAccessor类型的格式值。一个long / Long类型的参数中的值被解释为自 1970 年 1 月 1 日午夜 UTC 以来经过的毫秒数。

Note

TemporalAccessor是 Java 8 中增加的一个接口。它是新的日期时间 API 的一部分。API 中所有指定某种日期和/或时间的类都是TemporalAccessor. LocalDateLocalTimeLocalDateTimeZonedDateTime都是TemporalAccessor的例子。参考第十六章了解更多关于使用新日期时间 API 的信息。

t转换字符用于格式化日期/时间值。它有一个大写的变体T。日期/时间格式的格式说明符的一般语法如下:

%<argument_index$><flags><width><conversion>

请注意,格式说明符中的精度部分不适用于日期/时间格式。对于日期/时间格式,转换是两个字符的序列。转换中的第一个字符总是tT。第二个字符称为转换后缀,它决定了日期/时间参数的格式。表格 17-7 至 17-9 列出了所有可与t / T数据/时间转换字符一起使用的转换后缀。

表 17-9

日期/时间格式的后缀字符列表

|

转换后缀

|

描述

| | --- | --- | | R | 它以 24 小时制格式将时间格式化为hour : minute。其效果与使用%tH:%tM作为格式说明符是一样的。例子有 1 1:2301:3521:30等。 | | T | 它以 24 小时制格式将时间格式化为hour:minute:second。其效果与使用%tH:%tM:%tS作为格式说明符是一样的。例子有11:23:1001:35:01, 21:30:34等。 | | r | 它以 12 小时制格式将时间格式化为hour:minute:second morning/afternoon marker。其效果与使用%tI:%tM:%tS %T p 作为格式说明符是一样的。上午/下午标记可以是特定于场所的。09:23:45 AM09:30:00 PM等。是美国地区的例子。 | | D | 它将日期格式化为%tm/%td/%ty,比如01/19/11。 | | F | 它将日期格式化为%tY-%tm-%td,比如2011-01-19。 | | c | 它将日期和时间格式化为%ta %tb %td %tT %tZ %tY,比如 Wed Jan 19 11:52:06 CST 201 1。 |

表 17-8

日期格式的后缀字符列表

|

转换后缀

|

描述

| | --- | --- | | B | 特定于区域设置的月份全名,如“January”、“February”等。对于美国地区。 | | b | 特定于区域设置的缩写月份名称,如“Jan”、“Feb”等。对于美国地区。 | | h | 同b。 | | A | 一周中某一天的特定于语言环境的全名,例如“Sunday""Monday"等。对于美国地区。 | | a | 一周中某一天的特定于语言环境的简称,如"Sun""Mon"等。对于美国地区。 | | C | 它将四位数年份除以 100,并将结果格式化为两位数。如果得到的数字是一位数,它会添加一个前导零。它忽略除以 100 的结果中的小数部分。有效值为 00–99。例如,如果四位数的年份是 2011 年,它将输出 20;如果四位数年份是 12,则输出 00。 | | Y | 至少是四位数的年份。如果年份少于四位数,它会添加前导零。比如年份是 789,就输出 0789;如果年份是 2021,则输出 2021;如果年份是 20189,则输出 20189。 | | y | 年份的最后两位数。如有必要,它会添加一个前导零。比如年份是 9,就输出 09;如果年份是 123,则输出 23;如果年份是 2011 年,它将输出 11。 | | j | 一年中三位数的一天。有效值为 000–366。 | | m | 两位数的月份。有效值为 01–13。需要特殊值 13 来支持农历。 | | d | 一个月中两位数的某一天。有效值为 01–31。 | | e | 一月中的某一天。有效值为 1–31。除了不在输出中添加前导零之外,它的行为与“d”相同。 |

表 17-7

时间格式的后缀字符列表

|

转换后缀

|

描述

| | --- | --- | | H | 24 小时制中一天中的两位数小时。有效值为 00–23。00 用于午夜。 | | I | 12 小时制中一天中的两位数小时。有效值为 01–12。01 值对应于早上或下午的一点钟。 | | k | 除了不在输出中添加前导零之外,它的行为与H后缀相同。有效值为 0–23。 | | l | 除了不添加前导零之外,它的行为与I后缀相同。有效值为 1–12。 | | M | 一小时内的两位数分钟。有效值为 00–59。 | | S | 一分钟内的两位数秒。有效值为 00–60。值 60 是支持闰秒所需的特殊值。 | | L | 一秒钟内的三位数毫秒。有效值为 000–999。 | | N | 一秒内的九位数纳秒。有效值为 000000000–99999999。纳秒值的精度取决于操作系统支持的精度。 | | p | 它以小写形式输出特定于地区的早晨或下午标记。例如,对于美国地区,它将输出"am""pm"。如果您想要大写的输出(例如,"AM""PM"用于美国地区),您需要使用大写的变体T作为转换字符。 | | z | 它从 GMT 输出数字时区偏移量(例如+0530)。 | | Z | 它是时区的字符串缩写(例如,CST、EST、IST 等)。). | | s | 它输出自 1970 年 1 月 1 日午夜 UTC 开始的纪元开始以来的秒数。 | | Q | 它输出自 1970 年 1 月 1 日午夜 UTC 开始的纪元开始以来的毫秒数。 |

只要适用,数据/时间格式就会应用本地化。以下代码片段格式化了相同的日期和时间,即美国、印度和泰国地区的 2014 年 1 月 25 日上午 11:48:16。注意在格式说明符中使用了<标志。它允许您使用以多种格式说明符保存日期和时间值的参数:

Locale englishUS = Locale.US;
Locale hindiIndia = new Locale ("hi", "IN");
Locale thaiThailand = new Locale ("th", "TH", "TH");
// Construct a LocalDateTime
LocalDateTime ldt = LocalDateTime.of(2014, Month.JANUARY, 25, 11, 48, 16);
System.out.printf(englishUS, "In US: %tB %<te, %<tY %<tT %<Tp%n", ldt);
System.out.printf(hindiIndia, "In India: %tB %<te, %<tY %<tT %<Tp%n", ldt);
System.out.printf(thaiThailand, "In Thailand: %tB %<te, %<tY %<tT %<Tp%n", ldt);

img/323069_3_En_17_Figb_HTML.png

以下代码片段将当前日期和时间格式化为默认区域设置(在本例中为 US)。运行代码时,您将得到不同的输出。它使用一个ZonedDateTime参数来保存当前日期/时间和时区:

ZonedDateTime currentTime = ZonedDateTime.now();
System.out.printf("%tA %<tB %<te, %<tY %n", currentTime);
System.out.printf("%TA %<TB %<te, %<tY %n", currentTime);
System.out.printf("%tD %n", currentTime);
System.out.printf("%tF %n", currentTime);
System.out.printf("%tc %n", currentTime);
System.out.printf("%Tc %n", currentTime);
Saturday August 21, 2021
SATURDAY AUGUST 21, 2021
08/21/21
2021-08-21
Sat Aug 21 20:52:19 EDT 2021
SAT AUG 21 20:52:19 EDT 2021

注意使用大写变体T作为转换字符的效果。它以大写字母格式化参数。大写字母的定义取决于所使用的语言环境。如果区域设置没有不同的大写和小写字母,当您使用Tt作为转换字符时,输出将是相同的。

摘要

DateFormat类用于使用预定义格式格式化传统日期和时间,而SimpleDateFormat类用于以自定义格式格式化传统日期和时间。

NumberFormat类用于将数字格式化为特定地区的预定义格式。DecimalFormat类用于在特定的地区以您选择的格式格式化一个数字。

您可以使用java.util.Formatter类来格式化字符串、数字和日期/时间,从而使用printf样式的格式。它让您将格式化的输出发送到StringBuilderStringBufferFileOutputStreamPrintStream等。您已经使用了System.out.format()System.out.printf()方法将格式化的输出发送到标准输出。使用静态的String.format()方法获得一个格式化的字符串。使用Formatter将格式化的输出发送到您选择的目的地。您可以实现Formattable接口,将自定义格式应用于该类的对象。

QUESTIONS AND EXERCISES

  1. 使用预定义的特定于语言环境的格式来格式化java.util.Date对象中的日期,您会使用什么类呢?

  2. 你会用什么类来格式化一个使用自定义格式的java.util.Date对象中的日期?

  3. 您将使用什么类来解析String对象中的日期以获得java.util.Date对象?

  4. 您会使用什么类来将一个数字格式化为预定义的特定于地区的格式?

  5. 你用什么类来定制一个数字的格式?

  6. 您将使用什么类来解析字符串中的double以获得一个数字?

  7. 假设下面代码片段中的new Date()表达式返回的当前日期是 1968 年 1 月 12 日,那么下面代码片段的输出会是什么?

    import java.text.SimpleDateFormat;
    import java.util.Date;
    ...
    SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy");
    String currDate = sdf.format(new Date());
    System.out.println(currDate);
    
    
  8. 您会使用什么方法将格式化输出打印到标准输出— System.out.println()System.out.printf()

  9. 输出布尔值、字符、整数、浮点数和字符串的格式说明符是什么?

  10. 下面的语句抛出一个MissingFormatArgumentException。描述异常背后的原因以及您将如何修复它:

```java
System.out.printf("%d %f", 1969);
System.out.printf("%d %f", 1969, 2017);

```

11. 编写以下语句的输出:

```java
System.out.printf("%s %s%n", "Ken", "Lu");
System.out.printf("%s %<s%n", "Ken", "Lu");
System.out.printf("%s %<s %2s%n", "Ken", "Lu");

```

12. 编写以下代码片段的输出:

```java
System.out.println(new DecimalFormat("##.##").format(12.675));
System.out.printf("%.2f%n", 12.675);
System.out.printf("%1.2f%n", 12.675);
System.out.printf("%2.2f%n", 12.675);
System.out.printf("%2.1f%n", 12.675);

```

13. 完成下面的代码片段,它将输出“我的生日是 1969 年 9 月 19 日星期五”。注意,您必须以大写形式输出星期几和月份的名称:

```java
LocalDate bDay = LocalDate.of(1969, 9, 19);
String format = /* Your code goes here*/;
System.out.printf(format, bDay);

```

14. 编写以下代码片段的输出:

```java
System.out.printf("%d%n", 16);
System.out.printf("%x%n", 10);
System.out.printf("%c%n", 'a');
System.out.printf("'%5C' %n", 'a');
System.out.printf("'%-5C' %n", 'a');

```

15. 编写以下代码片段的输出:

```java
System.out.printf("%s %<s %s %1$s %s%n", "Li", "Hu", "Xi");

```

十八、正则表达式

在本章中,您将学习:

  • 如何创建正则表达式

  • 如何在String类中使用方便的方法来执行基于正则表达式的查找和替换

  • 如何使用Pattern类编译正则表达式

  • 如何使用Matcher类来匹配一个正则表达式和一个输入字符串

  • 如何在正则表达式中使用组

  • 如何使用Matcher类执行高级查找和替换

本章中的所有示例程序都是清单 18-1 中声明的jdojo.regex模块的成员。

// module-info.java
module jdojo.regex {
    exports com.jdojo.regex;
}

Listing 18-1The Declaration of a jdojo.regex Module

什么是正则表达式?

正则表达式是描述字符序列中模式的一种方式。该模式可用于验证字符序列、搜索字符序列、用另一个字符序列替换匹配该模式的字符序列等。

先说个例子。假设你有一个字符串,它可能是一个电子邮件地址。如何确保字符串是有效的电子邮件地址格式?此时,您对电子邮件地址的存在不感兴趣。您只想验证它的格式。

您希望根据一些规则来验证字符串。例如,它必须包含一个@符号,该符号前面至少有一个字符,后面是域名。或者,您可以指定@符号之前的文本必须只包含字母、数字、下划线和连字符。域名必须包含一个点。您可能想要添加更多的验证。如果您只想检查字符串中的@字符,您可以通过调用email.indexOf('@')来完成,其中email是保存电子邮件地址的字符串的引用。如果要确保邮件字符串中只有一个@字符,就需要添加更多的逻辑。在这种情况下,您可能会有 20–50 行代码,甚至更多,这取决于您想要执行的验证的数量。

这就是正则表达式派上用场的地方。这将使您的电子邮件地址验证变得容易。只用一行代码就可以完成。听起来是不是好得不像真的?就在不久前,您被告知您可能最终会有 50 行代码。现在,您被告知只需一行代码就可以完成同样的任务。这是真的。它可以在一行代码中完成。在我们详细讨论如何做到这一点之前,让我们列出完成这项任务所需的步骤:

  • 为了验证这些类型的字符串,您需要识别您正在寻找的模式。例如,在最简单的电子邮件地址验证形式中,字符串应该由一些文本(至少一个字符)加上一个@符号,后跟一些域名文本。让我们暂时忽略任何其他细节。

  • 你需要一种方式来表达这种被认可的模式。正则表达式用于描述这种模式。

  • 你需要一个程序来匹配输入字符串的模式。这样的程序也被称为正则表达式引擎。

假设您想测试一个字符串的形式是否为X@X,其中X是任意字符。弦乐"a@a""b@f""3@h"都是这种形式。你可以在这里观察到一种模式。模式是“一个字符后面跟着@,后面跟着另一个字符。”用 Java 怎么表达这种模式?

在这种情况下,字符串".@."将代表您的正则表达式。在".@."中,圆点有着特殊的含义。它们代表任何字符。所有在正则表达式中有特殊含义的字符都被称为元字符。我们将在下一节讨论元字符。String类包含一个matches()方法。它将一个正则表达式作为参数,如果整个字符串匹配正则表达式,则返回true。否则返回false。这个方法的特征是

boolean matches(String regex)

清单 18-2 包含了说明String类的matches()方法用法的完整代码。

// RegexMatch.java
package com.jdojo.regex;
public class RegexMatch {
    public static void main(String[] args) {
        // Prepare a regular expression to represent a pattern
        String regex = ".@.";
        // Try matching many strings against the regular expression
        matchIt("a@k", regex);
        matchIt("webmaster@jdojo.com", regex);
        matchIt("r@j", regex);
        matchIt("a%N", regex);
        matchIt(".@.", regex);
    }
    public static void matchIt(String str, String regex) {
        // Test for pattern match
        boolean matched = str.matches(regex);
        System.out.printf("%s matched %s = %b%n", str, regex, matched);
    }
}

a@k matched .@. = true
webmaster@jdojo.com matched .@. = false
r@j matched .@. = true
a%N matched .@. = false
.@. matched .@. = true

Listing 18-2Matching a String Against a Pattern

需要注意的一些要点如下:

  • 正则表达式".@.""webmaster@jdojo.com"不匹配,因为点意味着只有一个字符,而String.matches()方法匹配正则表达式中的模式和整个字符串。注意,字符串"webmaster@jdojo.com"具有由.@.表示的模式;也就是一个字符后跟@和另一个字符。但是,模式匹配字符串的一部分,而不是整个字符串。"webmaster@jdojo.com""r@j"部分与该模式相匹配。我们给出了一些例子,在这些例子中,你可以在字符串中的任何地方匹配模式,而不是匹配整个字符串。

  • 如果要匹配字符串中的点字符,需要对正则表达式中的点进行转义。正则表达式".\\.."将匹配任何三个字符的字符串,其中中间的字符是点字符。比如方法调用"a.b".matches(".\\..")会返回true;方法调用"...".matches(".\\..")将返回true;方法调用"abc".matches(".\\..")"aa.ca".matches(".\\..")将返回false

您也可以用另一个字符串替换匹配的字符串。String类有两种方法来进行匹配替换:

  • String replaceAll(String regex, String replacementString)

  • String replaceFirst(String regex, String replacementString)

replaceAll()方法用指定的replacementString替换与指定的regex表示的模式匹配的字符串。它返回替换后的新字符串。使用replaceAll()方法的一些例子如下:

String regex = ".@.";
// newStr will contain "webmaste***dojo.com" String newStr = "webmaster@jdojo.com".replaceAll(regex,"***");
// newStr will contain "***"
newStr = "A@B".replaceAll(regex,"***");
// newStr will contain "***and***"
newStr = "A@BandH@G".replaceAll(regex,"***");
// newStr will contain "B%T" (same as the original string)
newStr = "B%T".replaceAll(regex,"***");

replaceFirst()方法用replacementString替换第一次出现的匹配。它返回替换后的新字符串。使用replaceFirst()方法的一些例子如下:

String regex = ".@.";
// newStr will contain "webmaste***dojo.com"
String newStr = "webmaster@jdojo.com".replaceFirst(regex, "***");
// newStr will contain "***"
newStr = "A@B".replaceFirst(regex, "***");
// newStr will contain "***andH@G"
newStr = "A@BandH@G".replaceFirst(regex, "***");
// newStr will contain "B%T" (same as the original string)
newStr = "B%T".replaceFirst(regex, "***");

元字符

元字符是具有特殊含义的字符。它们用在正则表达式中。有时元字符没有任何特殊含义,它们被视为普通字符。根据使用它们的上下文,它们被视为普通字符或元字符。Java 中正则表达式支持的元字符如下:

  • ( (a left parenthesis)

  • ) (a right parenthesis)

  • [ (a left bracket)

  • ] (a right bracket)

  • { (a left brace)

  • } (a right brace)

  • \ (a backslash)

  • ^ (a caret)

  • $ (a dollar sign)

  • | (a vertical bar)

  • ? (a question mark)

  • * (an asterisk)

  • + (an addition sign)

  • . (a dot or period)

  • < (a less-than sign)

  • > (a greater-than sign)

  • - (a hyphen)

  • = (an equal to sign)

  • ! (an exclamation mark)

字符类

元字符[](左右括号)用于指定正则表达式中的字符类。字符类是一组字符。正则表达式引擎将尝试匹配集合中的一个字符。请注意,在 Java 中,字符类与类结构或类文件没有关系。角色类别"[ABC]"将匹配角色ABC。例如,字符串"A@V""B@V""C@V"将匹配正则表达式"[ABC]@."。然而,字符串"H@V"将不匹配正则表达式"[ABC]@.",因为@前面没有ABC。作为另一个例子,字符串"man""men"将匹配正则表达式"m[ae]n"

当我们使用“匹配”这个词时,我们的意思是模式存在于一个字符串中。我们并不是说整个字符串都匹配这个模式。例如,"WEB@JDOJO.COM"匹配模式"[ABC]@.",因为@B.之前,即使字符串包含三个@符号,字符串"A@BAND@YEA@U"也匹配模式"[ABC]@."两次。第二个@不是匹配的一部分,因为它的前面是D,而不是AB,C

您还可以使用字符类指定字符范围。范围用连字符(-)表示。例如,正则表达式中的"[A-Z]"代表任意大写英文字母;"[0-9]"代表09之间的任意数字。如果在一个字符类的开头使用^,表示补语(意为不)。例如,"[^ABC]"表示除了AB,C以外的任何字符。字符类"[^A-Z]"代表除大写英文字母以外的任何字符。如果您在字符类中除了开头以外的任何地方使用^,它将失去其特殊含义(即补码的特殊含义),并且它只匹配一个^字符。例如,"[ABC^]"将匹配ABC^

您也可以在一个字符类中包含两个或多个范围。例如,"[a-zA-Z]"匹配从az的任意字符,AZ. "[a-zA-Z0-9]"匹配从az的任意字符(大写和小写)以及从09的任意数字。表 18-1 中列出了一些字符类别的例子。

表 18-1

字符类的示例

|

字符类

|

意义

|

种类

| | --- | --- | --- | | [abc] | 人物ab,c | 简单字符类 | | [^xyz] | 除了xyz之外的一个角色 | 补充还是否定 | | [a-p] | 字符ap | 范围 | | [a-cx-z] | 字符acxz,包括abcxyz | 联盟 | | [0-9&&[4-8]] | 两个范围的交集(45678) | 交集 | | [a-z&&[^aeiou]] | 所有小写字母减去元音。换句话说,一个小写字母,它不是元音。也就是全部小写辅音。 | 减法 |

预定义的字符类

表 18-2 中列出了一些常用的预定义字符类。

表 18-2

预定义正则表达式字符类的列表

|

预定义的字符类

|

意义

| | --- | --- | | . (a dot) | 任何字符(可能与行终止符匹配,也可能不匹配)。更多细节请参考java.util.regex.Pattern类的 API 文档中的“行终止符”一节。 | | \d | 一个数字。同[0-9]。 | | \D | 一个非数字。同[⁰-9]。 | | \s | 空白字符。同[ \t\n\x0B\f\r]。该列表包括一个空格、一个制表符、一个新行、一个垂直制表符、一个换页符和一个回车符。 | | \S | 非空白字符。同[^\s]。 | | \w | 一个单词字符。同[a-zA-Z_0-9]。该列表包括小写字母、大写字母、下划线和十进制数字。 | | \W | 非单词字符。同[^\w]。 |

如果您允许在电子邮件地址验证中使用所有的大写和小写字母、下划线和数字,那么只验证三个字符的电子邮件地址的正则表达式应该是"\w@\w"。现在,您在电子邮件地址验证过程中领先一步。不再只允许在电子邮件的第一部分使用ABC(用正则表达式[ABC]@.来表示),现在你允许任何单词字符作为第一部分和第二部分。

正则表达式的更多功能

到目前为止,您只看到了使用正则表达式的String类的三个方法。包java.util.regex包含三个类来支持正则表达式的完整版本。这些类别如下:

  • 模式

  • 制榫机

  • PatternSyntaxException

一个Pattern保存正则表达式的编译形式。正则表达式的编译形式是其专用的内存表示形式,以促进更快的字符串匹配。

一个Matcher将待匹配的字符串与一个Pattern,相关联,并执行实际的匹配。

一个PatternSyntaxException代表一个格式错误的正则表达式中的错误。

编译正则表达式

一个Pattern保存正则表达式的编译形式。它是不可改变的。可以分享。它没有public构造器。该类包含一个静态的compile()方法,该方法返回一个Pattern对象。compile()方法被重载:

  • static Pattern compile(String regex)

  • static Pattern compile(String regex, int flags)

以下代码片段将一个正则表达式编译成一个Pattern对象:

// Prepare a regular expression
String regex = "[a-z]@.";
// Compile the regular expression into a Pattern object
Pattern p = Pattern.compile(regex);

第二个版本的compile()方法允许您指定修改模式匹配方式的标志。flags参数是一个位掩码。这些标志被定义为Pattern类中的int常量,如表 18-3 所列。

表 18-3

模式类中定义的标志列表

|

|

描述

| | --- | --- | | Pattern.CANON_EQ | 启用规范等效。如果设置了此标志,则只有当两个字符的完全规范分解匹配时,它们才匹配。 | | Pattern.CASE_INSENSITIVE | 启用不区分大小写的匹配。此标志仅为 US-ASCII 字符集设置不区分大小写的匹配。对于 Unicode 字符集的不区分大小写匹配,UNICODE_CASE标志也应该与该标志一起设置。 | | Pattern.COMMENTS | 允许模式中有空白和注释。当设置了这个标志时,空白被忽略,以#开头的嵌入注释被忽略,直到一行结束。 | | Pattern.DOTALL | 启用 dotall 模式。默认情况下,表达式.(一个点)不匹配行终止符。设置此标志时,表达式匹配任何字符,包括行终止符。 | | Pattern.LITERAL | 启用模式的文字解析。当设置了该标志时,正则表达式中的字符被按字面意思处理。也就是说,元字符和转义序列没有特殊的含义。CASE_INSENSTIVEUNICODE_CASE标志在与此标志一起使用时保持其效果。 | | Pattern.MULTILINE | 启用多线模式。默认情况下,表达式^$匹配整个输入序列的开头和结尾。当该标志被设置时,它们分别在一个行结束符或输入序列结束符之后和之前匹配。 | | Pattern.UNICODE_CASE | 启用支持 Unicode 的大小写折叠。当该标志与CASE_INSENSITIVE标志一起设置时,根据 Unicode 标准执行不区分大小写的匹配。 | | Pattern.UNICODE_CHARACTER_CLASS | 启用预定义字符类和POSIX字符类的 Unicode 版本。设置该标志也具有设置UNICODE_CASE标志的效果。设置此标志时,(仅限 US-ASCII)预定义字符类和POSIX字符类符合 Unicode 技术标准#18: Unicode 正则表达式附录 C——兼容性属性。 | | Pattern.UNIX_LINES | 启用UNIX线条模式。设置该标志时,只有\n字符被识别为行结束符。 |

下面的代码片段编译了一个设置了CASE_INSENSTIVEDOTALL标志的正则表达式,因此匹配 US-ASCII 字符集时不区分大小写,表达式.(一个点)将匹配一个行结束符。例如,"A@\n"将由以下模式匹配:

// Prepare a regular expression
String regex = "[a-z]@.";
// Compile the regular expression into a Pattern object with
// the CASE_INSENSITIVE and DOTALL flags
Pattern p = Pattern.compile(regex, Pattern.CASE_INSENSITIVE | Pattern.DOTALL);

创建匹配器

通过解释保存在Pattern对象中的编译模式,Matcher类的一个实例用于对一系列字符执行匹配。它没有public构造器。Pattern类的matcher()方法用于获取Matcher类的实例。该方法将模式要匹配的字符串作为参数。下面的代码片段显示了如何获得一个Matcher:

// Create a Pattern object and compile it into a Pattern
String regex = "[a-z]@.";
Pattern p = Pattern.compile(regex);
// String to perform the match
String str = "abc@yahoo.com,123@cnn.com,ksharan@jdojo.com";
// Get a matcher object using Pattern object p for str
Matcher m = p.matcher(str);

此时,Matcher对象m已经将Pattern对象p中表示的模式与str中的字符序列相关联。它准备好开始匹配操作。通常,Matcher对象用于在字符序列中寻找匹配。比赛可能成功也可能失败。如果匹配成功,您可能有兴趣知道匹配的开始和结束位置以及匹配的文本。您可以查询一个Matcher对象来获得所有这些信息。

匹配模式

你需要使用Matcher的以下方法来对输入执行匹配:

  • find()

  • start()

  • end()

  • group()

find()方法用于在输入中寻找模式的匹配。如果查找成功,则返回true。否则,它返回false。对该方法的第一次调用从输入的开始处开始搜索模式。如果上一次对此方法的调用成功,则下一次对此方法的调用将在前一次匹配后开始搜索。通常,在一个while循环中调用find()方法来查找所有匹配。这是一个重载的方法。另一个版本的find()方法采用整数参数,这是开始查找匹配的偏移量。

start()方法返回前一个匹配的起始索引。通常,它在成功的find()方法调用之后使用。

end()方法返回匹配字符串中最后一个字符的索引加 1。因此,在成功调用find()方法之后,end()start()方法返回的值之间的差值将给出匹配字符串的长度。使用String类的substring()方法,可以得到如下匹配的字符串:

// Continued from previous fragment of code
if (m.find()) {
    // str is the string we are looking into
    String foundStr = str.substring(m.start(), m.end());
    System.out.println("Found string is:" + foundStr);
}

group()方法返回通过先前成功的find()方法调用找到的字符串。回想一下,您还可以通过使用匹配的开始和结束,使用String类的substring()方法来获取之前匹配的字符串。因此,前面的代码片段可以替换为以下代码:

if (m.find()) {
    String foundStr = m.group();
    System.out.println("Found text is:" + foundStr);
}

清单 18-3 说明了这些方法的使用。为了清楚起见,省略了对方法参数的验证。程序试图在不同的字符串中找到"[abc]@."模式。

// PatternMatcher.java
package com.jdojo.regex;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class PatternMatcher {
    public static void main(String[] args) {
        String regex = "[abc]@.";
        String source = "cric@jdojo.com is a valid email address";
        PatternMatcher.findPattern(regex, source);
        source = "kelly@jdojo.com is invalid";
        PatternMatcher.findPattern(regex, source);
        source = "a@band@yea@u";
        PatternMatcher.findPattern(regex, source);
        source = "There is an @ sign here";
        PatternMatcher.findPattern(regex, source);
    }
    public static void findPattern(String regex, String source) {
        // Compile regex into a Pattern object
        Pattern p = Pattern.compile(regex);
        // Get a Matcher object
        Matcher m = p.matcher(source);
        // Print regex and source text
        System.out.println("\nRegex: " + regex);
        System.out.println("Text: " + source);
        // Perform find
        boolean found = false;
        while (m.find()) {
            System.out.printf("Matched Text: %s, Start: %s, End: %s%n",
                    m.group(), m.start(), m.end());
            // We found at least one match. Set the found flag to true
            found = true;
        }
        if (!found) {
            // We did not find any match
            System.out.println("No match found");
        }
    }
}
Regex: [abc]@.
Text: cric@jdojo.com is a valid email address
Matched Text: c@j, Start: 3, End: 6
Regex: [abc]@.
Text: kelly@jdojo.com is invalid
No match found
Regex: [abc]@.
Text: a@band@yea@u
Matched Text: a@b, Start: 0, End: 3
Matched Text: a@u, Start: 9, End: 12
Regex: [abc]@.
Text: There is an @ sign here
No match found

Listing 18-3Using Pattern and Matcher Classes

查询匹配

在上一节中,我们向您展示了如何查询一个Matcher来获得一个匹配的状态(或细节)。获得这些状态的方法有start()end()group()groupCount()。匹配状态也可以表示为MatchResult的实例,它是一个接口。您可以使用MatchResult的以下方法来获取匹配状态:

  • int end()

  • int end(int group)

  • String group()

  • String group(int group)

  • int groupCount()

  • int start()

  • int start(int group)

如何获得MatchResult的实例?调用MatchertoMatchResult()方法得到匹配状态的副本:

Matcher m = /* get a matcher here */
while (m.find()) {
    MatchResult result = m.toMatchResult();
    // Use result here...
}

为什么要使用MatchResult而不是Matcher中的方法来获取匹配状态?原因有二:

  • MatchertoMatchResult()返回匹配状态的副本,这意味着Matcher的匹配状态的任何后续变化都不会影响MatchResult。在匹配过程中,您可以将所有匹配状态收集到MatchResult的实例中,然后在程序中进行分析。

  • A MatchResult是不可变的。如果您有处理器来处理匹配,您可以安全地将MatchResult实例传递给那些处理器。传递Matcher是不安全的,因为处理器可能会意外修改Matcher,这将在无意中影响您的程序。

Matcher类中有一些方法可以和MatchResult一起工作。我们将在本章后面介绍它们。现在,只要记住一个MatchResult包含一场比赛的细节的拷贝。

当心反斜线

当心在正则表达式中使用反斜杠。字符类\w(即反斜杠后跟一个w)代表一个单词字符。回想一下,反斜杠字符也被用作转义字符的一部分。因此,\w必须写成\\w作为字符串文字。您还可以使用反斜杠来消除元字符的特殊含义。例如,一个[标志着一个角色类的开始。匹配括号中数字的正则表达式是什么,例如,[1][5]等。?注意,正则表达式[0-9]将匹配任何数字。数字可以用括号括起来,也可以不括起来。你可以考虑用[[0-9]]。它不会给你任何错误;然而,它也不能完成这项工作。您也可以将一个字符类嵌入到另一个字符类中。比如可以写[a-z[0-9]],和[a-z0-9]一样。在这种情况下,[[0-9]]中的第一个[应该被视为普通字符,而不是元字符。必须使用反斜杠作为\[[0-9]\]。要将这个正则表达式写成字符串文字,需要在双引号中使用两个反斜杠作为"\\[[0-9]\\]]"

正则表达式中的量词

您还可以指定正则表达式中的字符与字符序列匹配的次数。如果您想匹配所有两位数的整数,那么您的正则表达式应该是\d\d,与[0-9][0-9]相同。匹配任意整数的正则表达式是什么?你无法用你目前所掌握的知识写出匹配任何整数的正则表达式。您需要能够使用正则表达式来表达“一位数或更多位数”的模式。量词的概念就来了。表 18-4 列出了量词及其含义。

表 18-4

量词及其意义

|

数量词

|

意义

| | --- | --- | | * | 零次或多次 | | + | 一次或多次 | | ? | 一次或根本没有 | | {m} | 恰好m次 | | {m, } | 至少m次 | | {m, n} | 至少m次,但不超过n次 |

值得注意的是,量词必须跟在它指定数量的字符或字符类之后。匹配任何整数的正则表达式是\d+,它将匹配一个或多个数字。这种匹配整数的解法正确吗?不,不是的。假设你的文本是“这是包含 10 和 120 的文本 123”。如果您对这个字符串运行您的模式\d+,它将匹配 123、10 和 120。注意123不是作为整数使用的;相反,它是单词text123的一部分。如果你在文本中寻找整数,那么text123中的 123 肯定不是整数。您希望匹配文本中构成一个单词的所有整数。

需要是发明之母。现在,您需要指定只在单词边界上执行匹配,而不是在嵌入了整数的文本中。这对于从先前的结果中排除整数 123 是必要的。下一节讨论使用元字符来匹配边界。

根据您在本节中学到的知识,让我们来改进您的电子邮件地址验证。在一个电子邮件地址中,必须有且只有一个@符号。要指定一个且仅一个字符,您可以在正则表达式中使用该字符一次,尽管您可以使用{1}作为量词。例如,X{1}X在正则表达式中的意思是一样的。你在这方面很好。然而,到目前为止,您的解决方案只支持在@符号前后有一个字符。实际上,电子邮件地址中的@符号前后可以有多个字符。您可以将验证电子邮件地址的模式指定为\w+@\w+,这意味着一个或多个单词字符、一个@符号和一个或多个单词字符。

匹配边界

到目前为止,您并不关心文本中模式匹配的位置。有时,您可能想知道匹配是否发生在行首。您可能对查找和替换特定的匹配感兴趣,只要该匹配是在单词中找到的,而不是作为任何单词的一部分。例如,您可能希望将字符串中的单词apple替换为单词orange。假设你的字符串是“我有一个苹果和五个菠萝”。当然,您不希望在这个字符串中用orange替换所有出现的apple。如果你这样做,你的新字符串将是“我有一个橘子和五个菠萝”。事实上,你希望新的字符串是“我有一个橘子和五个菠萝”。你想匹配单词apple作为一个独立的单词,而不是任何其他单词的一部分。

表 18-5 列出了所有可以在正则表达式中使用的边界匹配器。

表 18-5

正则表达式中的边界观察器列表

|

边界匹配器

|

意义

| | --- | --- | | ^ | 一行的开始 | | $ | 一行的结尾 | | \b | 单词边界 | | \B | 非单词边界 | | \A | 输入的开始 | | \G | 上一场比赛的结束 | | \Z | 输入的结尾,但最后一个终止符除外,如果有的话 | | \z | 输入的结束 |

在 Java 中,一个单词字符由[a-zA-Z_0-9]定义。字边界是零宽度匹配,可以匹配以下内容:

  • 在单词字符和非单词字符之间

  • 字符串的开头和一个单词字符

  • 一个单词字符和字符串的结尾

非单词边界也是零宽度匹配,它与单词边界相反。它与以下内容匹配:

  • 空字符串

  • 两个单词字符之间

  • 两个非单词字符之间

  • 在非单词字符和字符串的开头或结尾之间

匹配单词apple的正则表达式是\bapple\b,意思如下:单词边界、单词apple和单词边界。清单 18-4 演示了如何使用正则表达式匹配单词边界。

// MatchBoundary.java
package com.jdojo.regex;
public class MatchBoundary {
    public static void main(String[] args) {
        // Prepare regular expression. Use \\b to get \b inside the string literal.
        String regex = "\\bapple\\b";
        String replacementStr = "orange";
        String inputStr = "I have an apple and five pineapples";
        String newStr = inputStr.replaceAll(regex, replacementStr);
        System.out.printf("Regular Expression: %s%n", regex);
        System.out.printf("Input String: %s%n", inputStr);
        System.out.printf("Replacement String: %s%n", replacementStr);
        System.out.printf("New String: %s%n", newStr);
    }
}
Regular Expression: \bapple\b
Input String: I have an apple and five pineapples
Replacement String: orange
New String: I have an orange and five pineapples

Listing 18-4Matching a Word Boundary

有两个边界匹配器:^(一行的开始)和\A(输入的开始)。一个输入字符串可以由多行组成。在这种情况下,\A将匹配整个输入字符串的开头,而^将匹配输入中每一行的开头。例如,正则表达式"^The"将匹配一个the输入字符串,它位于任何一行的开头。

组和反向引用

您可以将多个字符作为一个组来使用,从而将它们视为一个单元。通过将一个或多个字符括在括号内,可以在正则表达式中创建组。(ab)ab(z)ab(ab)(xyz)(the((is)(is)))是组的例子。正则表达式中的每个组都有一个组号。组号从 1 开始。Matcher类有一个方法groupCount(),该方法返回与Matcher实例相关的模式中的组数。有一个特殊的群体叫 0 组(零)。它是指整个正则表达式。groupCount()方法不报告组 0。

每个组是如何编号的?正则表达式中的每个左括号标记一个新组的开始。表 18-6 列出了正则表达式中组编号的一些例子。注意,我们还列出了所有正则表达式的组 0,尽管它没有被Matcher类的groupCount()方法报告。列表中的最后一个示例显示存在组 0,即使正则表达式中没有显式组。

表 18-6

正则表达式中的组示例

|

正则表达式:AB(XY)

| | --- | | 由Matcher类的groupCount()方法报告的组数:1 | | 组号 | 群组文本 | | 0 | AB(XY) | | 1 | (XY) | | 正则表达式:(AB)(XY) | | 由Matcher类的groupCount()方法报告的组数:2 | | 组号 | 群组文本 | | 0 | (AB)(XY) | | 1 | (AB) | | 2 | (XY) | | 正则表达式:((A)((X)(Y))) | | 由Matcher类的groupCount()方法报告的组数:5 | | 组号 | 群组文本 | | 0 | ((A)((X)(Y))) | | 1 | ((A)((X)(Y))) | | 2 | (A) | | 3 | ((X)(Y)) | | 4 | (X) | | 5 | (Y) | | 正则表达式:ABXY | | 由Matcher类的groupCount()方法报告的组数:0 | | 组号 | 群组文本 | | 0 | ABXY |

您还可以在正则表达式中反向引用组号。假设您想要匹配以"ab"开头,然后是"xy",最后是"ab"的文本。你可以写一个正则表达式为"abxyab"。您也可以通过形成一个包含"ab"的组并将其反向引用为"(ab)xy\1"来获得相同的结果。这里,"\1"指的是组 1,在这种情况下是"(ab)"。可以用"\2"指代组 2,"\3"指代组 3,以此类推。正则表达式"(ab)xy\12"会如何解释?您已经使用"\12"作为组反向参考。正则表达式引擎足够聪明,可以检测到它只包含"(ab)xy\12"中的一个组。它使用"\1"作为第 1 组的后向引用,第 1 组是"(ab)",,第 2 组是普通字符。因此,正则表达式"(ab)xy\12""abxyab2"相同。如果正则表达式有 12 个或更多组,正则表达式中的\12 将指第 12 个组。

还可以通过在正则表达式中使用组号来获取匹配文本的一部分。Matcher类中的group()方法被重载。你已经看到了没有参数的group()方法。该方法的另一个版本将组号作为参数,并返回该组匹配的文本。假设您在输入文本中嵌入了电话号码。所有电话号码都是一个单词,长度为十位数。前三个数字是区号。正则表达式\b\d{10}\b将匹配输入文本中的所有电话号码。然而,要获得前三位数字(区号),您必须编写额外的代码。如果使用组构成正则表达式,则可以使用组号获得区号。将电话号码的前三个数字放在一个组中的正则表达式是\b(\d{3})\d{7}\b。如果m是对与该模式相关联的Matcher对象的引用,则在成功匹配后,m.group(1)将返回电话号码的前三位数字。您也可以使用m.group(0)来获取整个匹配的文本。清单 18-5 展示了在正则表达式中使用组来获取电话号码的区号部分。注意2339829与模式不匹配,因为它只有 7 个数字,而使用的模式只查找 10 个数字的电话号码。

// PhoneMatcher.java
package com.jdojo.regex;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class PhoneMatcher {
    public static void main(String[] args) {
        // Prepare a regular expression: A group of 3 digits followed by 7 digits.
        String regex = "\\b(\\d{3})\\d{7}\\b";
        // Compile the regular expression
        Pattern p = Pattern.compile(regex);
        String source = "3342449027, 2339829, and 6152534734";
        // Get the Matcher object
        Matcher m = p.matcher(source);
        // Start matching and display the found area codes
        while (m.find()) {
            String phone = m.group();
            String areaCode = m.group(1);
            System.out.printf("Phone: %s, Area Code: %s%n", phone, areaCode);
        }
    }
}
Phone: 3342449027, Area Code: 334
Phone: 6152534734, Area Code: 615

Listing 18-5Using Groups in Regular Expressions

组也用于格式化或用另一个字符串替换匹配的字符串。假设您想要将所有十位数的电话号码格式化为(xxx) xxx -xxxx,其中 x 表示一个数字。如您所见,电话号码分为三组:前三位、后三位和后四位。您需要使用三个组组成一个正则表达式,这样您就可以通过它们的组号来引用这三个匹配的组。正则表达式应该是\b(\d{3})(\d{3})(\d{4})\b。开头和结尾的\b表示您只对匹配单词边界的十位数感兴趣。以下代码片段说明了如何显示格式化的电话号码:

// Prepare the regular expression
String regex = "\\b(\\d{3})(\\d{3})(\\d{4})\\b";
// Compile the regular expression
Pattern p = Pattern.compile(regex);
String source = "3342449027, 2339829, and 6152534734";
// Get Matcher object
Matcher m = p.matcher(source);
// Start match and display formatted phone numbers
while (m.find()) {
    System.out.printf("Phone: %s, Formatted Phone: (%s) %s-%s%n",
            m.group(), m.group(1), m.group(2), m.group(3));
}
Phone: 3342449027, Formatted Phone: (334) 244-9027
Phone: 6152534734, Formatted Phone: (615) 253-4734

您也可以用格式化的电话号码替换输入文本中的所有十位数电话号码。您已经学习了如何使用String类的replaceAll()方法用另一个文本替换匹配的文本。Matcher类也有一个replaceAll()方法,它完成同样的事情。在用格式化的电话号码替换电话号码时,您面临的问题是获取匹配电话号码的匹配部分。在这种情况下,替换文本也包含匹配的文本。您事先不知道什么文本与模式匹配。团体来拯救你。$n,其中n为组号,内部替换文本为组n的匹配文本。例如,$1指第一个匹配的组。用格式化的电话号码替换电话号码的替换文本将是($1) $2-$3。清单 18-6 展示了在替换文本中引用组的技术。

// MatchAndReplace.java
package com.jdojo.regex;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class MatchAndReplace {
    public static void main(String[] args) {
        // Prepare the regular expression
        String regex = "\\b(\\d{3})(\\d{3})(\\d{4})\\b";
        String replacementText = "($1) $2-$3";
        String source = "3342449027, 2339829, and 6152534734";
        // Compile the regular expression
        Pattern p = Pattern.compile(regex);
        // Get Matcher object
        Matcher m = p.matcher(source);
        // Replace the phone numbers by formatted phone numbers
        String formattedSource = m.replaceAll(replacementText);
        System.out.printf("Text: %s%n", source );
        System.out.printf("Formatted Text: %s%n", formattedSource );
    }
}
Text: 3342449027, 2339829, and 6152534734
Formatted Text: (334) 244-9027, 2339829, and (615) 253-4734

Listing 18-6Back Referencing a Group in Replacement Text

您也可以通过使用String类获得相同的结果。你根本不需要使用PatternMatcher类。下面的代码片段说明了相同的概念,但是使用了String类。String类在内部使用PatternMatcher类来获得结果:

// Prepare the regular expression
String regex = "\\b(\\d{3})(\\d{3})(\\d{4})\\b";
String replacementText = "($1) $2-$3";
String source = "3342449027, 2339829, and 6152534734";
// Use replaceAll() method of the String class
String formattedSource = source.replaceAll(regex, replacementText)

Matcher类包含以下replaceAll()replaceFirst()方法:

  • 字符串替换全部(字符串替换)

  • 字符串 replaceAll(函数 replacer)

  • 字符串替换优先(字符串替换)

  • 字符串 replaceFirst(函数 replacer)

    提示replaceAll(Function<MatchResult,String> replacer)replaceFirst(Function<MatchResult,String> replacer)方法被添加到 Java 9 的Matcher类中。

正如我们在本节中解释的那样,replaceAll(String)replaceFirst(String)方法的工作原理与String类中同名的方法相同。其他版本以一个Function<MatchResult,String>作为参数。Function接受一个MatchResult并返回一个替换字符串。这些方法让您有机会使用您在Function中的逻辑来获得替换字符串。在执行查找和替换之前,这四种方法都首先重置匹配器。Function<T,R>java.util.function包中的一个接口。我们将在第二十章中详细讨论Function接口。

假设您想在一个输入字符串中查找十位数的电话号码,并且您想用区号 334 屏蔽所有的电话号码。比如一个电话号码是 3342449027,你想用(***) ***-****代替。您可以在Matcher类中使用新的replaceAll()方法来实现。清单 18-7 包含完整的程序。

// MaskAndFormat.java
package com.jdojo.regex;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class MaskAndFormat {
    public static void main(String[] args) {
        // Prepare the regular expression
        String regex = "\\b(\\d{3})(\\d{3})(\\d{4})\\b";
        String source = "3342449027, 2339829, and 6152534734";
        // Compile the regular expression
        Pattern p = Pattern.compile(regex);
        // Get Matcher object
        Matcher m = p.matcher(source);
        // Replace the phone numbers by formatted phone numbers
        String formattedSource = m.replaceAll(MaskAndFormat::mask);
        System.out.printf("Text: %s%n", source );
        System.out.printf("Formatted Text: %s%n", formattedSource );
    }
    private static String mask(MatchResult result) {
        String replacementText = "($1) $2-$3";
        String areaCode = result.group(1);
        if("334".equals(areaCode)) {
            replacementText = "(***) ***-****";
        }
        return replacementText;
    }
}
Text: 3342449027, 2339829, and 6152534734
Formatted Text: (***) ***-****, 2339829, and (615) 253-4734

Listing 18-7Using Logic to Mask or Format Phone Number Depending on the Area Code

注意以下语句在main()方法中的使用:

String formattedSource = m.replaceAll(MaskAndFormat::mask);

replaceAll()方法的参数是MaskAndFormat::mask,它是对MaskAndFormat类的mask()静态方法的方法引用。当找到匹配时,MatchResult被传递给mask()方法,从该方法返回的字符串被用作替换文本。注意您如何在mask()方法中用区号 334 屏蔽了电话号码。所有其他区号都使用与上一示例中相同的替换文字。

使用命名组

在一个大的正则表达式中使用组号很麻烦。Java 也支持命名组。您可以使用组名做任何事情,就像您在上一节中使用组号所做的那样:

  • 您可以命名一个组。

  • 您可以使用名称支持引用组。

  • 您可以在替换文本中引用组名。

  • 您可以使用组名获得匹配的文本。

和前面一样,您需要使用一对括号来创建一个组。开始括号后面是一个?和一个放在尖括号中的组名。定义命名组的格式如下:

(?<groupName>pattern)

群组名称必须仅由字母和数字组成:azAZ09。组名必须以字母开头。以下是使用三个命名组的正则表达式的示例。组名为areaCodeprefixlineNumber。正则表达式匹配一个十位数的电话号码:

\b(?<areaCode>\d{3})(?<prefix>\d{3})(?<lineNumber>\d{4})\b

您可以使用\k<groupName>反向引用名为groupName的组。电话号码中的区号和前缀部分使用相同的模式。您可以将前面反向引用areaCode组的正则表达式重写如下:

\b(?<areaCode>\d{3})\k<areaCode>(?<lineNumber>\d{4})\b

您可以在替换文本中引用一个命名组作为${groupName}。下面的代码片段显示了一个正则表达式,其中包含三个命名组以及使用它们的名称引用这三个组的替换文本:

String regex = "\\b(?<areaCode>\\d{3})(?<prefix>\\d{3})(?<lineNumber>\\d{4})\\b";
String replacementText = "(${areaCode}) ${prefix}-${lineNumber}";

当您命名一个组时,该组仍然会获得一个组号,如前一节所述。即使一个组有名称,您仍然可以通过它的组号来引用它。前面的代码片段重写如下,其中第三个组已被命名为lineNumber,在替换文本中使用其组号$3进行引用:

String regex = "\\b(?<areaCode>\\d{3})(?<prefix>\\d{3})(?<lineNumber>\\d{4})\\b";
String replacementText = "(${areaCode}) ${prefix}-$3";

匹配成功后,您可以使用Matcher类的group(String groupName)方法来获取该组的匹配文本。

清单 18-8 展示了如何在正则表达式中使用组名,以及如何在替换文本中使用组名。

// NamedGroups.java
package com.jdojo.regex;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class NamedGroups {
    public static void main(String[] args) {
        // Prepare the regular expression
        String regex =
            "\\b(?<areaCode>\\d{3})(?<prefix>\\d{3})(?<lineNumber>\\d{4})\\b";
        // Reference first two groups by names and the third one as its number
        String replacementText = "(${areaCode}) ${prefix}-$3";
        String source = "3342449027, 2339829, and 6152534734";
        // Compile the regular expression
        Pattern p = Pattern.compile(regex);
        // Get Matcher object
        Matcher m = p.matcher(source);
        // Replace the phone numbers by formatted phone numbers
        String formattedSource = m.replaceAll(replacementText);
        System.out.printf("Text: %s%n", source);
        System.out.printf("Formatted Text: %s%n", formattedSource);
    }
}
Text: 3342449027, 2339829, and 6152534734
Formatted Text: (334) 244-9027, 2339829, and (615) 253-4734

Listing 18-8Using Named Groups in Regular Expressions

在使用Matcher类的find()方法成功匹配之后,您可以使用它的start()end()方法来知道组的匹配边界。这些方法是重载的:

  • int start()

  • int start(int groupNumber)

  • int start(String groupName)

  • int end()

  • int end(int groupNumber)

  • int end(String groupName)

不带参数的方法返回前一个匹配的起点和终点。其他两组方法返回前一个匹配中一个组的开始和结束。下面的代码片段使用了前面的例子,在一个字符串中匹配一个十位数的电话号码。它打印每个成功匹配的每个组的开始:

// Prepare the regular expression
String regex = "\\b(?<areaCode>\\d{3})(?<prefix>\\d{3})(?<lineNumber>\\d{4})\\b";
String source = "3342449027, 2339829, and 6152534734";
System.out.println("Source Text: " + source);
// Compile the regular expression
Pattern p = Pattern.compile(regex);
// Get Matcher object
Matcher m = p.matcher(source);
while(m.find()) {
    String matchedText = m.group();
    int start1 = m.start("areaCode");
    int start2 = m.start("prefix");
    int start3 = m.start("lineNumber");
    System.out.printf("Matched Text: %s", matchedText);
    System.out.printf(". Area code start: %d", start1);
    System.out.printf(", Prefix start: %d", start2);
    System.out.printf(", Line Number start: %d%n", start3);
}
Source Text: 3342449027, 2339829, and 6152534734
Matched Text: 3342449027\. Area code start: 0, Prefix start: 3, Line Number start: 6
Matched Text: 6152534734\. Area code start: 25, Prefix start: 28, Line Number start: 31

重置匹配器

如果您已经完成了对输入文本的模式匹配,并且想要再次从输入文本的开头重新开始匹配,那么您需要使用Matcher类的reset()方法。在调用了reset()方法之后,下一个匹配模式的调用将从输入文本的开头开始。reset()方法被重载。另一个版本允许您将不同的输入文本与模式相关联。如果模式保持不变,这两个版本的reset()方法允许您重用任何现有的Matcher类实例。通过避免重新创建一个新的Matcher对象来执行相同模式的匹配,这增强了程序的性能。

电子邮件验证的最终结论

您现在已经学习了正则表达式的主要部分。您已经准备好完成您的电子邮件地址验证示例。请注意,我们只验证电子邮件地址的格式,而不验证它是否指向有效的电子邮件收件箱。您的电子邮件地址将根据以下规则进行验证:

  • 所有电子邮件地址都将采用name@domain的形式。

  • 名称部分必须以字母数字字符(a-z, A-Z, 0-9)开头。

  • 名称部分必须至少有一个字符。

  • 名称部分可以包含任何字母数字字符(a-z, A-Z, 0-9)、下划线、连字符或点号。

  • 域部分必须至少包含一个点。

  • 域部分中的点的前后必须至少有一个字母数字字符。

  • 您还应该能够使用组号来引用名称和域部分。这种验证表明,您将名称和域部分作为组放在正则表达式中。

以下正则表达式将根据这些规则匹配一个电子邮件地址。组 1 是名称部分,而组 2 是域部分:

([a-zA-Z0-9]+[\\w\\-.]*)@([a-zA-Z0-9]+\\.[a-zA-Z0-9\\-.]+)

添加的验证越多,正则表达式就越复杂。鼓励读者为电子邮件地址添加更多的验证,并相应地修改前面的正则表达式。这个正则表达式允许域部分有两个连续的点。你会如何阻止?

使用正则表达式查找并替换

查找和替换是正则表达式支持的一种非常强大的技术。有时您可能需要找到一个模式,并根据它匹配的文本替换它;也就是说,基于一些条件来决定替换文本。Java 正则表达式设计者看到了这种需求,他们在Matcher类中包含了两个方法,让您可以完成这项任务:

  • Matcher appendReplacement(StringBuffer sb, String replacement)

  • Matcher appendReplacement(StringBuilder sb, String replacement)

  • StringBuffer appendTail(StringBuffer sb)

  • StringBuffer appendTail(StringBuilder sb)

    提示Java 9 中增加了与StringBuilder一起工作的appendReplacement()appendTail()方法的版本。

考虑以下文本:

一列载有 125 名男女的火车正以每小时 100 英里的速度行驶。火车票价是每人 75 美元。”

您想要查找文本中的所有数字(例如,125、100 和 75)并替换它们,如下所示:

  • 一百乘以一百

  • “> 100”改为“超过 100”

  • “不足一百”改为“不足一百”

替换后,该案文应为:

一辆载有 100 多名男女的火车正以每小时 100 英里的速度行驶。火车票价每人不到 100 美元。”

要完成这项任务,您需要找到文本中嵌入的所有数字,将找到的数字与 100 进行比较,并决定替换文本。使用文本编辑器查找和替换文本时也会出现这种情况。文本编辑器突出显示您正在搜索的单词,您输入一个新单词,文本编辑器会为您进行替换。您也可以使用这两种方法创建一个在文本编辑器中找到的查找/替换程序。通常,这些方法与Matcher类的find()方法一起使用。下面概述了使用这两种方法完成文本查找和替换的步骤:

  1. 创建一个Pattern对象。

  2. 创建一个Matcher对象。

  3. 创建一个StringBuffer/StringBuilder对象来保存结果。

  4. 在循环中使用find()方法来匹配模式。

  5. 根据找到的匹配位置调用appendReplacement()appendTail()方法。

让我们通过编译正则表达式来创建一个Pattern。因为您想要查找所有的数字,所以您的正则表达式应该是\b\d+\b。注意第一个和最后一个\b。他们指出你只对单词边界上的数字感兴趣:

String regex = "\\b\\d+\\b"; Pattern p = Pattern.compile(regex);

通过将图案与文本相关联来创建一个Matcher:

String text = "A train carrying 125 men and women was traveling" +
              " at the speed of 100 miles per hour. The train" +
              " fare was 75 dollars per person.";
Matcher m = p.matcher(text);

创建一个StringBuilder来保存新文本:

StringBuilder sb = new StringBuilder();

开始在Matcher对象上使用find()方法来寻找匹配。当您第一次调用find()方法时,数字 125 将与模式匹配。此时,您希望根据匹配的文本准备替换文本,如下所示

String replacementText = "";
// Get the matched text. Recall that group() method returns the whole matched text
String matchedText = m.group();
// Convert the text into integer for comparison
int num = Integer.parseInt(matchedText);
// Prepare the replacement text
if (num == 100) {
    replacementText = "a hundred";
} else if (num < 100) {
    replacementText = "less than a hundred";
} else {
    replacementText = "more than a hundred";
}

现在,您将在Matcher对象上调用appendReplacement()方法,传递一个空的StringBuilderreplacementText作为参数。在本例中,replacementText有一个字符串"more than hundred",因为find()方法调用匹配数字 125:

m.appendReplacement(sb, replacementText);

知道appendReplacement()方法调用做什么是很有趣的。它检查是否有先前的匹配。因为这是对find()方法的第一次调用,所以没有先前的匹配。对于第一次匹配,它从输入文本的开头开始追加文本,直到匹配文本之前的字符。在您的情况下,以下文本被附加到StringBuilder。此时,StringBuilder中的文字是

"A train carrying "

现在,appendReplacement()方法将replacementText参数中的文本追加到StringBuilder中。这将把StringBuilder的内容改为

"A train carrying more than a hundred"

appendReplacement()方法还做了一件事。它将追加位置(即Matcher对象的内部状态)设置为第一个匹配文本之后的字符位置。在您的情况下,追加位置将被设置为 125 后面的字符,这是 125 后面的空格字符的位置。这就完成了第一个查找和替换步骤。

您将再次调用Matcher对象的find()方法。它会找到模式,也就是另一个数,是 100。您将使用与第一次匹配后相同的过程来计算替换文本的值。这一次,replacementText将包含字符串"a hundred"。您调用appendReplacement()方法如下:

m.appendReplacement(sb, replacementText);

同样,它检查是否有先前的匹配。由于这是对find()方法的第二次调用,它将找到一个先前的匹配,并将使用上一次appendReplacement()调用保存的追加位置作为起始位置。要追加的最后一个字符将是第二次匹配之前的字符。它还会将追加位置设置为数字 100 后面的字符位置。此时,StringBuilder包含以下文字:

"A train carrying more than a hundred men and women was traveling at the speed of a hundred"

第三次调用find()方法时,它会找到数字 75,替换后的StringBuilder内容如下。追加位置将被设置到数字75后面的字符位置:

"A train carrying more than a hundred men and women was traveling at the speed of a hundred miles per hour. The train fare was less than a hundred"

如果再次调用find()方法,它将找不到任何匹配。然而,StringBuilder不包含最后一个匹配之后的文本,也就是“dollars per person."”要追加最后一个匹配之后的文本,需要调用appendTail()方法。它从追加位置开始向StringBuilder追加文本,直到输入字符串结束。对此方法的调用

m.appendTail(sb);

StringBuilder修改成这样:

"A train carrying more than a hundred men and women was traveling at the speed of a hundred miles per hour. The train fare was less than a hundred dollars per person."

如果您在第二次调用appendReplacement()方法之后调用了appendTail()方法,那么StringBuilder的内容会是什么?完整的程序如清单 18-9 所示。

// AdvancedFindReplace.java
package com.jdojo.regex;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class AdvancedFindReplace {
    public static void main(String[] args) {
        String regex = "\\b\\d+\\b";
        StringBuilder sb = new StringBuilder();
        String text = "A train carrying 125 men and women was traveling at"
                + " the speed of 100 miles per hour. "
                + "The train fare was 75 dollars per person.";
        Pattern p = Pattern.compile(regex);
        Matcher m = p.matcher(text);
        while (m.find()) {
            String matchedText = m.group();
            // Convert the text into an integer for comparing
            int num = Integer.parseInt(matchedText);
            // Prepare the replacement text
            String replacementText;
            if (num == 100) {
                replacementText = "a hundred";
            } else if (num < 100) {
                replacementText = "less than a hundred";
            } else {
                replacementText = "more than a hundred";
            }
            m.appendReplacement(sb, replacementText);
        }
        // Append the tail
        m.appendTail(sb);
        // Display the old and new text
        System.out.printf("Old Text: %s%n", text);
        System.out.printf("New Text: %s%n", sb.toString());
    }
}
Old Text: A train carrying 125 men and women was traveling at the speed of 100 miles per hour. The train fare was 75 dollars per person.
New Text: A train carrying more than a hundred men and women was traveling at the speed of a hundred miles per hour. The train fare was less than a hundred dollars per person.

Listing 18-9Find-and-Replace Using Regular Expressions and appendReplacement() and appendTail() Methods

匹配结果流

Matcher类中有一个方法返回一个MatchResult流:

Stream<MatchResult> results()

Streams API 是一个庞大的主题,我们在第十六章中简单提到过。它允许您对数据流应用过滤-映射-归约操作。我们给出了一个使用results()方法来完成对Matcher类的讨论的例子。如果您在理解本节中的示例时有困难,请在阅读完 Streams API 之后重新阅读本节。

results()方法在一个元素属于MatchResult类型的流中返回匹配结果。您可以查询MatchResult获取比赛详情。results()方法不会重置匹配器。如果你想重用匹配器,不要忘记调用它的reset()方法来重置它到一个期望的位置。当您使用results()方法时,诸如计算匹配数、获取匹配列表和查找不同的区号等操作变得很容易。清单 18-10 展示了这种方法的一些有趣的用法。它在输入字符串中搜索十位数或七位数的电话号码。它获取所有格式化的匹配电话号码的列表。在第二个示例中,它在匹配结果中打印一组不同的区号。

// DistinctAreaCode.java
package com.jdojo.regex;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
public class DistinctAreaCode {
    public static void main(String[] args) {
        // A regex to match 7-digit or 10-digit phone numbers
        String regex = "\\b(\\d{3})?(\\d{3})(\\d{4})\\b";
        // An input string
        String source = "1, 3342229999, 2330001, 6159996666, 123, 3340909090";
        System.out.println("Input: " + source);
        // Create a matcher
        Matcher matcher = Pattern.compile(regex)
                .matcher(source);
        // Collect formatted phone numbers into a list
        List<String> phones = matcher.results()
                .map(mr -> (mr.group(1) == null ? "" : "(" + mr.group(1) + ") ")
                      + mr.group(2) + "-" + mr.group(3))
                .collect(toList());
        System.out.println("Phones: " + phones);
        // Reset the matcher, so we can reuse it from start
        matcher.reset();
        // Get distinct area codes
        Set<String> areaCodes = matcher.results()
                .filter(mr -> mr.group(1) != null)
                .map(mr -> mr.group(1))
                .collect(toSet());
        System.out.println("Distinct Area Codes: " + areaCodes);
    }
}
Input: 1, 3342229999, 2330001, 6159996666, 123, 3340909090
Phones: [(334) 222-9999, 233-0001, (615) 999-6666, (334) 090-9090]
Distinct Area Codes: [334, 615]

Listing 18-10Using the results() Method of the Matcher Class

main()方法中,下面的正则表达式将匹配七位数或十位数的电话号码:

// A regex to match 7-digit or 10-digit phone numbers
String regex = "\\b(\\d{3})?(\\d{3})(\\d{4})\\b";

您想将一个十位数的电话号码格式化为(xxx) xxx-xxxx,将一个七位数的电话号码格式化为xxx-xxxx。最后,您希望将所有格式化的电话号码收集到一个List<String>中。“Collect”是一个终端操作,它接受一个收集器作为参数;如示例所示,我们导入了两个方法,这两个方法提供了收集器 toList()和 toSet()。以下语句执行此操作:

 // Collect formatted phone numbers into a list
 List<String> phones = matcher.results()
                        .map(mr -> (mr.group(1) == null ? "" : "(" + mr.group(1) + ") ")
                                    + mr.group(2) + "-" + mr.group(3))
                        .collect(toList());

请注意map()方法的使用,它接受一个MatchResult并返回一个格式化的电话号码作为一个String。当匹配的是一个七位数的电话号码时,组 1 将是null。现在,您希望重用匹配器来查找十位数电话号码中不同的区号。您必须重置匹配器,以便下一个匹配从输入字符串的开头开始:

// Reset the matcher, so we can reuse it from start
matcher.reset();

MatchResult中的第一组包含区号。您需要过滤掉七位数的电话号码,并将 group 1 的值收集到一个Set<String>中,以获得一组不同的区号。下面的语句可以做到这一点:

// Get distinct area codes
Set<String> areaCodes = matcher.results()
                               .filter(mr -> mr.group(1) != null)
                               .map(mr -> mr.group(1))
                               .collect(toSet());

摘要

正则表达式是用作匹配某些文本的模式的字符序列。Java 通过java.util.regex包中的PatternMatcher类提供了对使用正则表达式的全面支持。在String类中有几种使用正则表达式的方便方法。

一个Pattern对象代表一个编译过的正则表达式。一个Matcher对象用于将一个Pattern与一个输入文本相关联,以搜索模式。Matcher类的find()方法用于在输入文本中查找模式的匹配。正则表达式允许您使用组。群组会自动从 1 到 n 编号。从左起的第一个群组编号为 1。存在包含整个正则表达式的特殊组 0。您也可以给组命名。您可以通过编号或名称来引用组。

Java 9 给Matcher类增加了一些有用的方法。replaceAll()replaceFirst()方法被重载;现在他们用一个Function<MatchResult,String>作为匹配结果的替换符,允许您使用任何逻辑来生成匹配的替换文本。results()方法返回一个Stream<MatchResult>,允许您将操作流式传输到匹配的结果。

QUESTIONS AND EXERCISES

  1. 什么是正则表达式?

  2. 什么是元字符?如何在正则表达式中将元字符作为普通字符使用?

  3. 你用什么类来编译一个模式?

  4. 你用什么类来匹配一个编译模式?

  5. 正则表达式"[aieou]"是什么意思?会和字符串"Hello"匹配吗?

  6. 编写一个正则表达式,它将匹配任何以小写辅音开头,后跟一个或多个小写元音,再后跟一个小写辅音的单词。例如,它应该匹配猫,狗,酷,小床,厄运,认为等。,但不是可乐,猫,鱼,冷等。

  7. 以下代码片段的输出会是什么:

    String source = "I saw the rat running.";
    String regex = "r..";
    Pattern p = Pattern.compile(regex);
    Matcher m = p.matcher(source);
    while(m.find()) {
        System.out.println(m.group());
    }
    
    
  8. 完成下面的代码片段,它将匹配输入中的两个单词— catcot。当代码运行时,它应该在两行上打印出catcot:

    String source = "cat camera can pen cow cab cot";
    String regex = /* Your code goes here */;
    Pattern p = Pattern.compile(regex);
    Matcher m = /* Your code goes here */;
    while(m.find()) {
        System.out.println(m.group());
    }
    
    
  9. 完成下面的代码片段,将所有以c开头的三个字母的单词替换为大写字母。代码应该打印"CAT camera CAN pen COW CAB COT"

    String source = "cat camera can pen cow cab cot";
    String regex = "/* You code goes here*/";
    Pattern p = Pattern.compile(regex);
    Matcher m = p.matcher(source);
    String str = m.replaceAll(mr -> mr.group().toUpperCase());
    System.out.println(str);
    
    
  10. 编写以下代码片段的输出:

```java
String source = "ABXXXABB";
String regex = "AB*";
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(source);
String str = m.replaceAll("Hello");
System.out.println(str);

```

11. 编写以下代码片段的输出:

```java
String source = "ABXXXABB";
String regex = "AB?";
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(source);
String str = m.replaceAll("Hello");
System.out.println(str);

```

12. 编写以下代码片段的输出:

```java
String source = "ABXXXABB";
String regex = "AB+";
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(source);
String str = m.replaceAll("Hello");
System.out.println(str);

```

13. 描述以下代码片段的意图并写出输出:

```java
String source = "I have 25 cents and 400 books.";
String regex = "\\b(\\d+)\\b";
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(source);
int sum = m.results()
           .mapToInt(mr -> Integer.parseInt(mr.group()))
           .sum();
System.out.println(sum);

```

14. 以下正则表达式中有多少个组:

```java
String regex = "\\b((\\d{3})(\\d{3})(\\d{4}))|((\\d{3})(\\d{4}))\\b";

```

15. 完成以下代码片段,以 xxx-xxxx 和(xxx) xxx-xxxx 格式打印七位数和十位数的电话号码。输出应该是"(334) 233-0908, 233-7656, 234, (617) 908-6547, unknown" :

```java
String source = "3342330908, 2337656, 234, 6179086547, unknown";
String regex = "/* Your code goes here*/";
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(source);
StringBuilder sb = new StringBuilder();
while(m.find()) {
    String replacement =
        m.group(1) != null ? /* Your code goes here*/;
    m.appendReplacement(sb, replacement);
}
m.appendTail(sb);
System.out.println(sb.toString());

```

16. 完成下面的代码片段,它将在单独的一行上打印源字符串中的每个单词:

```java
String source = "bug dug jug mug tug";
String regex = "/*your code goes here*/";
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(source);
while(m.find()) {
   System.out.println(m.group());
}

```

17. 下面的代码片段试图计算并打印问号(?)在输入字符串中。完成下面的代码片段,因此输出是3 :

```java
String source = "What? How? I do not know. Why?";
String regex = "/* Your code goes here */";
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(source);
long questionMarkCount = m.results().count();
System.out.println(questionMarkCount);

```