字符串

深入探讨Java中字符串操作与正则表达式的应用,包括String的不可变性、拼接效率、格式化输出、正则表达式基础及高级用法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


可以证明,字符串操作是计算机程序设计中最常见的行为。

不可变String

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

当把String对象传递给某个方法时,实际传递的是引用的一个拷贝。其实,每当把String对象作为方法的参数时,都会复制一份引用,而该引用所指的对象其实一直待在单一的物理位置上,从未动过。String的这种行为方式其实正是我们想要的。 难道你真的希望方法能改变其参数吗?对于一个方法而言,参数是为该方法提供信息的,而不是想让该方法改变自己的。正是有了这种保障,才使得代码易于编写与阅读。

重载“+”与StringBuilder

不可变性会带来一定的效率问题。为String对象重载的“+”操作符就是一个例子。重载的意思是,一个操作符在应用于特定的类时,被赋予了特殊的意义(用于String的“+”与“+=”是Java中仅有的两个重载过的操作符,而Java并不允许程序员重载任何操作符)。操作符“+”可以用来连接String:

public class Test {
	public static void main(String[] args) {
		String str = "mango";
		String s = "abc"+str+7;
		System.out.println(s);
	}
	//输出:abcmango7
}

在这个例子中,编译器创建了一个StringBuilder对象,用以构造最终的String,并为每个字符串调用一次StringBuilder的append()方法,总共三次。最后调用toString()生成结果,井存为s。

这种工作方式当然也行得通,但是为了生成最终的String,此方式会产生一大堆需要垃圾回收的中间对象。我猜想,Java设计师一开始就是这么做的(这也是软件设计中的一个教训除非你用代码将系统实现,并让它动起来,否则你无法真正了解它会有什么问题),然后他们发现其性能相当糟糕。如果想看看以上代码到底是如何工作的,可以用JDK自带的工具javap来反编译以上代码。命令如下:

javap -c Test

String的操作

以下是 String对象具备的一些基本方法。重载的方法归纳在同一行中:
在这里插入图片描述
从这个表中可以看出,当需要改变字符串的内容时,String类的方法都会返回一个新的String对象。同时,如果内容没有发生改变,String的方法只是返回指向原对象的引用而已。这可以节约存储空问以及避免额外的开销。

格式化输出

Formatter类

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

public class Test {
	public static void main(String[] args) {
		Formatter f = new Formatter(System.out);
		f.format("%s ssss","1");
	}
	//输出:1 ssss
}

上例所有的输出都指定到System.out。Formatter的构造器经过重载可以接受多种输出目的地,不过最常用的还是PrintStream()、OutputStream和File。

格式化说明符

在插入数据时,如果想要控制空格与对齐,你需要更精细复杂的格式修饰符。以下是其抽象的语法:

%[argument_index$] [flags] [width] [precision] converfon

最常见的应用是控制一个域的最小尺寸,这可以通过指定width,来实现。Formatter对象通过在必要时添加空格,来确保一个域至少达到某个长度。在默认的情况下,数据是右对齐,不过可以通过使用“-”标志来改变对齐方向。

与width相对的是precision,它用来指明最大尺寸。width可以应用于各种类型的数据转换,并且其行为方式都一样。precision则不然,不是所有类型的数据都能使用precision,而且应用于不同类型的数据转换时,precision的意义也不同。在将precision应用于String时,它表示打印String时输出字符的最大数量。而在将precision应用于浮点数时,它表示小数部分要显示出来的位数(默认是6位小数),如果小数位数过多则舍入,太少则在尾部补零。由于整数没有小数部分,所以precision无法应用于整数,如果你对整数应用precision,则会触发异常。例:

public class Test {
	public static void main(String[] args) {
		Formatter f = new Formatter(System.out);
		f.format("%-15s %5s %10s\n", "Item","Qty","Price");
		f.format("%-15s %5s %10s\n", "----","---","-----");
		f.format("%-15.15s %5d %10.2fs\n", "Jack's Magic Be",4,4.25);
		f.format("%-15.15s %5d %10.2fs\n", "Princess",3,5.10);
	}
	//输出:
	//Item              Qty      Price
	//----              ---      -----
	//Jack's Magic Be     4       4.25s
	//Princess            3       5.10s
}

正如所见,通过相当简清的语法,Formatter提供了对空格与对齐的强大控制能力。在该程序中,为了恰当地控制间隔,格式化字符串被重复地利用了多遍。

Formatter转换

下面的表格包含了最常用的类型转换:
在这里插入图片描述
例:

public class Test {
	public static void main(String[] args) {
		Formatter f = new Formatter(System.out);
		char c = 'a';
		f.format("s: %s\n",c);
		//f.format("s: %d\n",c);
		f.format("s: %c\n",c);
		f.format("s: %b\n",c);
		//f.format("s: %f\n",c);
		//f.format("s: %e\n",c);
		//f.format("s: %x\n",c);
		f.format("s: %h\n",c);
	}
	//输出:
	//s: a
	//s: a
	//s: true
	//s: 61
}

被注释的代码表示,针对相应类型的变量,这些转换是无效的。如果执行这些转换,则会触发异常。

注意,程序中的每个变量都用到了b转换。虽然它对各种类型都是合法的,但其行为却不一定与你想象的一致。对于boolean基本类型或Boolean对象,其转换结果是对应的true或false但是,对其他类型的参数,只要该参数不为null,那转换的结果就永远都是true。即使是数字0,转换结果依然为true。所以,将b应用于非布尔类型的对象时请格外小心。

还有许多不常用的类型转换与格式修饰符选项,你可以在JDK文档中的Formatter类部分找到它们。

String.format()

String.format()是一个Static方法,它接受与Formattet.format()方法一样的参数,但返回一个String对象。当你只需使用format()方法一次的时候,String.fomat()用起来很方便。例如:

class Exce extends Exception{
	public Exce(int i , int ii,String str){
		super(String.format("t%d q%d %s", i,ii,str));
	}
}
public class Test {
	public static void main(String[] args) {
		try {
			throw new Exce(3,7,"NONONO");
		} catch (Exception e) {
			System.out.println(e);
		}
	}
	//输出:thinkinjava.Exce: t3 q7 NONONO	
}

正则表达式

正则表达式是一种强大而灵活的文本处理工具。使用正则表达式,我们能够以编程的方式,构造复杂的文本模式,并对输入的字符串进行搜索。一旦找到了匹配这些模式的部分,你就能够随心所欲地对它们进行处理。初学正则表达式时,其语法是一个难点,但它确实是一种简洁、动态的语言。正则表达式提供了一种完全通用的方式,能够解决各种字符串处理相关的问题:匹配、选择、编輯以及验证。

基础

一般来说,正则表达式就是以某种方式来描述字符串,因此你可以说:“如果一个字符串含有这些东西,那么它就是我正在找的东西。”例如,要找一个数字,它可能有一个负号在最前面,那你就写一个负号加上一个问号,就像这样:

-?

要描述一个整数,你可以说它有一位或多位阿拉伯数字。在正则表达式中,用\d表示一位数字。如果在其他语言中使用过正则表达式,那你立刻就能发现Java对反斜线\的不同处理。 在其他语言中\表示“我想要在正则表达式中插入一个普通的(字面上的)反斜线,请不要给它任何特殊的意义。”而在Java中,\的意思是“我要插入一个正则表达式的反斜线,所以其后的字符具有特殊的意义。例如,如果你想表示一位数字,那么正则表达式应该是\d。如果你想插入一个普通的反斜线,则应该这样\\。不过换行和制表符之类的东西只需使用单反斜线:\n\t。

要表示“一个或多个之前的表达式”,应该使用+。所以,如果要表示“可能有一个负号,后面跟着一位或多位数字”,可以这样:

-?\d+

应用正则表达式的最简单的途径,就是利用String类内建的功能。例如你可以检查一个String是否匹配如上所述的正则表达式:

public class Test {
	public static void main(String[] args) {
		System.out.println("-1234".matches("-?\\d+"));
		System.out.println("1234".matches("-?\\d+"));
		System.out.println("+1234".matches("-?\\d+"));
		System.out.println("+1234".matches("(-|\\+)?\\d+"));
	}
	//输出:
	//true
	//true
	//false
	//true
}

前两个字符串满足对应的正则表达式,匹配成功。第三个字符串开头有一个+,它也是一个合法的整数,但与对应的正则表达式却不匹配。因此,我们的正则表达式应该描述为:“可能以一个加号或减号开头”。在正则表达式中,括号有着将表达式分组的效果,而竖直线 l 则表示或操作。也就是:

(-|\+)?

这个正则表达式表示字符串的起始字符可能是一个-或+,或二者皆没有(因为后面跟着修饰符?)。因为字符+在正则表达式中有特殊的意义,所以必须使用\将其转义,使之成为表达式中的一个普通字符。

split()

Strtng类还自带了一个非常有用的正则表达式工具split()方法,其功能是“将字符串从正则表达式匹配的地方切开”。例:

public class Test {
	public static void main(String[] args) {
		String str = "Are you ok ?";
		System.out.println(Arrays.toString(str.split(" ")));
		System.out.println(Arrays.toString(str.split("\\W+")));
	}
	//输出:
	//[Are, you, ok, ?]
	//[Are, you, ok]
}

首先看第一个语句,注意这里用的是普通的字符作为正则表达式,其中并不包含任何特殊的字符。因此第一个split()只是按空格来划分字符串。第二个split()用到了\W+,它的意思是非单词字符(如果W小写,\w则表示一 个单词字符)。通过第二个例子可以看到,它将标点字符删除了。

String.split()还有一个重载的版本,它允许你限制字符串分割的次数。

替换

String类自带的最后一个正则表达式工具是“替换”。你可以只替换正则表达式第一个匹配的子串,或是替换所有匹配的地方。

public class Test {
	public static void main(String[] args) {
		String str = "Are you ok ?";
		System.out.println(str.replaceFirst(" ","-"));
		System.out.println(str.replaceAll(" ","-"));
		System.out.println(str.replaceAll("o|a","-"));
	}
	//输出:
	//Are-you ok ?
	//Are-you-ok-?
	//Are y-u -k ?
}

第一个表达式要匹配的是以空格开头的,并且只替换掉第一个匹配的部分,第二个表达式则是替换所有匹配的部分。第三个表达式要匹配的是两个字符中的任意一个,因为它们以竖直线分隔表示“或”,并且替换所有匹配的部分。

创建正则表达式

我们首先从正则表达式可能存在的构造集中选取一个很有用的子集,以此开始学习正则表达式。正则表达式的完整构造子列表请参考JDK文挡java.util.regex包中的Pattern类。
在这里插入图片描述
当你学会了使用字符类(character classes)之后,正则表达式的威力才能真正显现出来。以下是一些创建字符类的典型方式,以及一些预定义的类:
在这里插入图片描述
这里只列出了部分常用的表达式,你应该将JDK文档中java.util.regexx.Pattern那一页加入浏览器书签中,以便在需要的时候方便査询。
在这里插入图片描述
例:

public class Test {
	public static void main(String[] args) {
		String str = "Rudolph";
		System.out.println(str.matches("[rR]udolph"));
		System.out.println(str.matches("[rR][aeiou][a-z]ol.*"));
		System.out.println(str.matches("R.*"));
	}
	//输出:
	//true
	//true
	//true
}

量词

量词描述了一个模式吸收输入文本的方式:

  1. 贪婪型:量词总是贪婪的,除非有其他的选项被设置。贪婪表达式会为所有可能的模式发现尽可能多的匹配。导致此问题的一个典型理由就是假定我们的模式仅能匹配第一个可能的字符组,如果它是贪婪的,那么它就会继续往下匹配。

  2. 勉强型:用问号来指定,这个量词匹配满足模式所需的最少字符数。因此也称作懒惰的、最少匹配的、非食装的,或不贪婪的。

  3. 占有型:目前,这种类型的量词只有在Java语言中才可用,并且也更高级,因此我们大概不会立刻用到它。当正则表达式被应用于字符串时,它会产生相当多的状态,以便在匹配失败时可以回溯。而“占有的”量词并不保存这些中间状态,因此它们可以防止回溯。它们常常用子防止正则表达式失控,因此可以使正则表达式执行起来更有效。

在这里插入图片描述
应该非常清楚地意识到,表达式X通常必须要用圆括号括起来,以便它能够按照我们期望的效果去执行。例如:
aoc+
看起来它似乎应该匹配1个或多个abc序列,如果我们把它应用于输入字特串abcabcabc,则实际上会获得3个匹配。然而,这个表达式实际上表示的是:匹配ab,后面跟随1个或多个c。要表明匹配1个或多个完整的abc字符串,我们必须这样表示:
(abc)+

Pattern和Matcher

一般来说,比起功能有限的String类,我们更愿意构造功能强大的正则表达式对象。只需导入java.util.regex包,然后用static Pattern.complie()方法来编译你的正则表达式即可。它会根据你的String类型的正则表达式生成一个Pattern对象。接下来把你想要检索的字符串传入Pattern对象的matcher()方法。matcher()方法会生成一个Matcher对象,它有很多功能可用(可以参考java.util.regext.Matcher的JDK文档)。

作为第一个示例,下面的类可以用来测试正则表达式,看看它们能否匹配一个输入字符串。

public class Test {
	public static void main(String[] args) {
		Pattern p = Pattern.compile("a");
		Matcher m = p.matcher("abcabcabc");
		while(m.find()){
			System.out.println("Match:"+m.group()+",position:"+m.start()+"-"+m.end());
		}
	}
	//输出:
	//Match:a,position:0-1
	//Match:a,position:3-4
	//Match:a,position:6-7
}

Pattern对象表示编译后的正则表达式。从这个例子中可以看到,我们使用已编译的Pattern对象上的matcher()方法,加上一个输入字符串,从而共同构造了一个Matcher对象。同时Pattem类还提供了static方法:

static boolean matches(String regex. CharSequence input)

该方法用以检査regex是否匹配整个CharSeqence类型的input参数。编译后的Mattern对象还提供了split()方法,它从匹配了regex的地方分割输入字符串,返回分割后的子字符串String数组。

find()

Matcher.find()方法可用来在CharSequence中查找多个匹配,如上例while的条件。

组(Groups)

组是用括号划分的正则表达式,可以根据组的编号来引用某个组。组号为0表示整个表达式,组号1表示被第一对括号括起的组,依此类推。因此在下面这个表达式:

A(B(C))D

中有三个组,组0是ABCD,组1是BC,组2是C。

Matcher对象提供了一系列方法,用以获取与组相关的信息:public int groupCount()返回该匹配器的模式中的分组数目,第0组不包括在内。public String group()返回前一次匹配操作(例如find())的第0组(整个匹配)。public String group(int group)返回在前一次匹配操作期间指定的组号,如果匹配成功,但是指定的组没有匹配输入字符串的任何部分,则将会返回null。

start()和end()

public int start(int group)返回在前一次匹配操作中寻找到的组的起始索引。public int end(int group)返回在前一次匹配操作中寻找到的组的最后一个字符索引加一的值。

Pattern

Pattern类的compile()方法还有另一个版本,它接受一个标记参数数,以调整匹配的行为:

Pattern Pattern.compile(String regex,int flag)

其中的flag来自以下Pattern类中的常量:
在这里插入图片描述
在这些标记中,Pattern.CASE_INSENSITIVE、Pattern.MULTILINE以及Pattern.COMMENTS(对声明或文档有用)特别有用。请注意,你可以直接在正则表达式中使用其中的大多数标记,只需要将上表中括号括起的字符插入到正则表达式中,你希望它起作用的位置即可,你还可以通过“或”(|)操作符组合多个标记的功能:

public class Test {
	public static void main(String[] args) {
		
		Pattern p = Pattern.compile("^java",Pattern.CASE_INSENSITIVE|Pattern.MULTILINE);
		Matcher m = p.matcher("java has regex\nJava has regex\nJava has pretty good regular expressions");
		while(m.find()){
			System.out.println(m.group());
		}
	}
	//输出:
	//java
	//Java
	//Java
}

reset()

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

public class Test {
	public static void main(String[] args) {
		Pattern p = Pattern.compile("^java",Pattern.CASE_INSENSITIVE|Pattern.MULTILINE);
		Matcher m = p.matcher("java has regex\nJava has regex\nJava has pretty good regular expressions");
		while(m.find()){
			System.out.println(m.group());
		}
		m.reset("java a \n Java b");
		while(m.find()){
			System.out.println(m.group());
		}
	}
	//输出:
	//java
	//Java
	//Java
	//java
}

输入扫描

到日前为止,从文件或标准输入读取数据还是一件相当痛普的事情。终于,Java SE5新增了Scanner类,它可以大大减轻扫描输入的工作负担:

public class Test {
	public static void main(String[] args) {
		Scanner s = new Scanner(System.in);
		System.out.println("What you name?");
		String name = s.nextLine();
		System.out.format("Hi %s",name);
	}
	//输出:
	//What you name?
	//输入:楼主
	//Hi 楼主
}

Scanner的构造器可以接受任何类型的输入对象,包括File对象、InputStream、String或者Readable对象。Readable是Java SE5中新加入的一个接口,表示“具有read()方法的某种东西”。有了Scanner,所有的输入、分词以及翻译的操作都隐藏在不同类型的next方法中。普通的next()方法返回下一个String。所有的基本类型(除char之外)都有对应的next()方法,包括BigDecimal和BigInteger。所有的next方法,只有在找到一个完整的分词之后才会返回。Scanner还有相应的hasNext方法,用以判断下一个输入分词是否所需的类型。

Scanner定界符

在默认的情况下,Scanner根据空白字符对输入进行分词,但是你可以用正则表达式指定自己所需的定界符:

public class Test {
	public static void main(String[] args) {
		Scanner s = new Scanner("12 13 14 15");
		s.useDelimiter("\\s* \\s*");
		while(s.hasNext()){
			System.out.println(s.nextInt());
		}
	}
	//输出
	//12
	//13
	//14
	//15
}

同样的技术也可以用来读取逗号分隔的文件。我们可以用useDelimiter()来设置定界符,同时还有一个delimiter()方法,用来返回当前正在作为定界符使用的Pattern对象。


  1. 本文来源《Java编程思想(第四版)》
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值