3.6 字符串


String

从概念上讲,Java字符串就是Unicode字符序列。例如,字符串"Java\u2122"由5个Unicode字符J、a、v、a和?组成。Java没有内置的字符串类型,而是在标准Java类库中提供了一个预定义类,很自然地叫做String。每个用双引号括起来的字符串都是String类的一个实例:

String e=" ";     //an empty string 
String greeting="Hello";

程序示例

public class HuangZiHanTest
{  
   public static void main(String[] args)
   {
	   String huangzihan_e=" ";     //an empty string 
	   String huangzihan_greeting="huangzihan_Hello";
	   System.out.println(huangzihan_e);
	   System.out.println(huangzihan_greeting);
	   System.out.println("Java\u2122");
   }
}

运行结果

 
huangzihan_Hello
Java?

子串

substring方法

String类的substring方法可以从一个较大的字符串提取出一个子串。例如:

String greeting ="Hello";
String s = greeting.substring(0,3);

程序示例

public class HuangZiHanTest
{  
   public static void main(String[] args)
   {
	   String huangzihan_greeting ="huangzihan_Hello";
	   String huangzihan_s = huangzihan_greeting.substring(0,10);
	   System.out.println(huangzihan_s);
   }
}

运行结果

huangzihan

创建一个由字符“huangzihan”组成的字符串。

注释

类似于C和C++,Java字符串中的代码单元和代码点从0开始计数。

substring方法的第二个参数是不想复制的第一个位置。这里要复制位置为0、1、2、3、4、5、6、7、8、9(从0到9,包括0和9)的字符。在substring中从0开始计数,直到9为止,但不包含9。

substring的工作方式有一个优点:容易计算子串的长度。字符串s.substring(a,b)的长度为b-a。例如,子串“huangzihan”的长度为10-0=10。

拼接

+号连接(拼接)

与绝大多数程序设计语言一样,Java语言允许使用 + 号连接(拼接)两个字符串。

String expletive="Expletive";
String PG13="deleted";
String message=expletive+PG13;

上述代码将“Expletivedeleted”赋给变量message(注意,单词之间没有空格,+号完全按照给定的次序将两个字符串拼接起来)。

程序示例

public class HuangZiHanTest
{  
   public static void main(String[] args)
   {
	   String huangzihan_expletive="huang";
	   String huangzihan_PG13="zihan";
	   String huangzihan_message=huangzihan_expletive+huangzihan_PG13;
	   System.out.println(huangzihan_message);
   }
}

运行结果

huangzihan

字符串与非字符串拼接

当将一个字符串与一个非字符串的值进行拼接时,后者会转换成字符串。例如:

int age=13;
String rating="PG"+age;

rating设置为“PG13”。这种特性通常用在输出语句中。例如:

System.out.println("The answer is "+answer);

这是一条合法的语句,并且会打印出所希望的结果(因为单词is后面加了一个空格,输出时也会加上这个空格)。

程序示例

public class HuangZiHanTest
{  
   public static void main(String[] args)
   {
	   int huangzihan_age=13;
	   String huangzihan_rating="帅"+huangzihan_age;
	   System.out.println("黄子涵是"+huangzihan_rating);
   }
}

运行结果

黄子涵是帅13

静态join方法

如果需要把多个字符串放在一起,用一个界定符分隔,可以使用静态join方法:

String all=String.join("/","S","M","L","XL");
//all is the string "S/M/L/XL"

程序示例

public class HuangZiHanTest
{  
   public static void main(String[] args)
   {
	   String huangzihan_all=String.join("/","huang","zi","han");
	   //all is the string "S/M/L/XL"
	   System.out.println(huangzihan_all);
   }
}

运行结果

huang/zi/han

repeat方法

在Java 11中,还提供了一个repeat方法:

String repeated="Java".repeat(3);   //repeated is "JavaJavaJava"

程序示例

public class HuangZiHanTest
{  
   public static void main(String[] args)
   {
	   String huangzihan_repeated="Huangzihan".repeat(3);   //repeated is "HuangzihanHuangzihanHuangzihan"
	   System.out.println(huangzihan_repeated);
   }
}

运行结果

HuangzihanHuangzihanHuangzihan

不可变字符串

修改字符串中某个字符

String类没有提供修改字符串中某个字符的方法。如果希望将greeting的内容修改为“Help!”,不能直接将greeting的最后两个位置的字符修改为'p'和'!'。对于C程序员来说,这会让他们茫然无措。如何修改这个字符串呢?在Java中实现这项操作非常容易。可以提取想要保留的子串,再与希望替换的字符拼接:

greeting = greeting.substring(0,3)+"p!";

上面这条语句将greeting变量的当前值修改为“Help!”。

由于不能修改Java字符串中的单个字符,所以在Java文档中将String类对象称为是不可变的(immutable),如同数字3永远是数字3一样,字符串“Hello”永远包含字符Hell0的代码单元序列。你不能修改这些值,不过,可以修改字符串变量greeting,让它引用另外一个字符串,这就如同可以让原本存放3的数值变量改成存放4一样。

程序示例

public class HuangZiHanTest
{  
   public static void main(String[] args)
   {
	   String huangzihan_greeting ="黄子涵_huangzihan";
	   huangzihan_greeting = huangzihan_greeting.substring(0,3)+"是帅哥!";
	   System.out.println(huangzihan_greeting);
   }
}

运行结果

黄子涵是帅哥!

字符串共享

这样做是否会降低运行效率呢?看起来好像修改一个代码单元要比从头创建一个新字符串更加简洁。答案是:也对,也不对。的确,通过拼接“Hel”和“p!”来创建一个新字符串的效率确实不高。但是,不可变字符串却有一个优点:编译器可以让字符串共享

为了弄清具体的工作方式,可以想象将各种字符串存放在公共的存储池中。字符串变量指向存储池中相应的位置。如果复制一个字符串变量,原始字符串与复制的字符串共享相同的字符。

总而言之,Java的设计者认为共享带来的高效率远远胜过于提取子串、拼接字符串所带来的低效率。可以看看你自己的程序,我们发现:大多数情况下都不会修改字符串,而只是需要对字符串进行比较(有一种例外情况,将来自于文件或键盘的单个字符或较短的字符串组装成字符串。)。

检测字符串是否相等

equals方法

可以使用equals方法检测两个字符串是否相等。对于表达式:

s.equals(t)

如果字符串s与字符串t相等,则返回true;否则,返回false。需要注意的是,st可以是字符串变量,也可以是字符串字面量。例如,以下表达式是合法的:

"Hello".equals(greeting)

程序示例

public class HuangZiHanTest
{  
   public static void main(String[] args)
   {
	   String shuaige="是帅哥!";
	   boolean huangzihan="黄子涵".equals(shuaige);
	   System.out.println(huangzihan);
   }
}

运行结果

false

equalsIgnoreCase方法

要想检测两个字符串是否相等,而不区分大小写,可以使用equalsIgnoreCase方法。

"Hello".equalsIgnoreCase("hello")

程序示例

public class HuangZiHanTest
{  
   public static void main(String[] args)
   {
	   boolean huangzihan;
	   huangzihan="黄子涵".equalsIgnoreCase("是帅哥!");
	   System.out.println(huangzihan);
	   System.out.println();
	   System.out.println("你这让我很伤心!");
   }
}

运行结果

false

你这让我很伤心!

不要使用==检测字符串是否相等

一定不要使用==运算符检测两个字符串是否相等!这个运算符只能够确定两个字符串是否存放在同一个位置上。当然,如果字符串在同一个位置上,它们必然相等。但是,完全有可能将内容相同的多个字符串副本放置在不同的位置上。

可能将内容相同的多个字符串副本放置在不同的位置上。

String greeting ="Hello";  //initialize greeting to a string

if(greeting=="Hello") ...
//probably true 
if(greeting.substring(0,3) == "Hel") ...
//probably false

如果虚拟机始终将相同的字符串共享,就可以使用==运算符检测是否相等。但实际上只有字符串字面量是共享的,而+或substring等操作得到的字符串并不共享。因此,千万不要使用==运算符测试字符串的相等性,以免在程序中出现这种最糟糕的bug,看起来这种bug就像随机产生的间歇性错误。

程序示例

public class HuangZiHanTest
{  
   public static void main(String[] args)
   {
	   String huangzihan_greeting ="黄子涵";  //initialize greeting to a string

	   if(huangzihan_greeting=="黄子涵")
		   System.out.println("黄子涵");
	   //probably true 
	   if(huangzihan_greeting.substring(0,2) == "黄子涵") 
		   System.out.println("肯定");
	   //probably false
	   if(huangzihan_greeting.substring(0,3) == "黄子涵") 
		   System.out.println("是帅哥!");
	   //probably false
   }
}

运行结果

黄子涵
是帅哥!

空串与Null串

检查字符串是否为空

空串 " " 是长度为0的字符串。可以调用以下代码检查一个字符串是否为空:

if(str.length() == 0)
if(str.equals(" "))

程序示例

public class HuangZiHanTest
{  
   public static void main(String[] args)
   {
	   String huangzihan="";
	   if(huangzihan.length() == 0)
		   System.out.print("黄子涵");
	   if(huangzihan.equals(""))
		   System.out.print("是帅哥!");
   }
}

运行结果

黄子涵是帅哥!

空串

空串是一个Java对象,有自己的串长度(0)和内容(空)。不过,String变量还可以存放一个特殊的值,名为null,表示目前没有任何对象与该变量关联。要检查一个字符串是否为null,要使用以下条件:

这里不是很懂!!!

if(str == null)

有时要检查一个字符串既不是null也不是空串,这种情况下就需要使用以下条件:

if(str != null && str.length() !=0) 

首先要检查str不为null

程序示例

public class HuangZiHanTest
{  
   public static void main(String[] args)
   {
	   String huang="huangzihan";
	   String zihan="";
	   if(zihan == null)   //这里有问题!!!
		   System.out.print("黄子涵");
	   if(huang != null && huang.length() !=0)
		   System.out.print("是帅哥!");
   }
}

运行结果

是帅哥!

码点与代码单元

代码单元

Java字符串由char值序列组成。char数据类型是一个采用UTF-16编码表示Unicode码点的代码单元。最常用的Unicode字符使用一个代码单元就可以表示,而辅助字符需要一对代码单元表示。

length方法

length方法将返回采用UTF-16编码表示给定字符串所需要的代码单元数量。例如:

String greeting="Hello";
int n=greeting.length();  //is 5

程序示例

public class HuangZiHanTest
{  
   public static void main(String[] args)
   {
	   String huangzihan_greeting="黄子涵是帅哥!";
	   int huangzihan_n=huangzihan_greeting.length();  //is 5
	   System.out.println(huangzihan_n);
   }
}

运行结果

7

码点数量

要想得到实际的长度,即码点数量,可以调用:

int cpCount = greeting.codePointCount(0,greeting.length());

程序示例

public class HuangZiHanTest
{  
   public static void main(String[] args)
   {
	   String greeting="黄子涵是帅哥!";
	   int cpCount = greeting.codePointCount(0,greeting.length());
	   System.out.println(cpCount);
   }
}

运行结果

7

s.charAt(n)

调用s.charAt(n)将返回位置n的代码单元,n介于0 ~ s.length()-1之间。例如:

char first = greeting.charAt(0);   //first is 'H'
char last = greeting.charAt(4);    //last is 'o'

程序示例

public class HuangZiHanTest
{  
   public static void main(String[] args)
   {
	   String huangzihan_greeting="黄子涵是帅哥!";
	   char huangzihan_first = huangzihan_greeting.charAt(0);   //first is 'H'
	   char huangzihan_last = huangzihan_greeting.charAt(6);    //last is 'o'
	   System.out.println(huangzihan_first);
	   System.out.println(huangzihan_last);
   }
}

运行结果

黄
!

第i个码点(这里不太懂!!!)

要想得到第i个码点,应该使用下列语句

int index = greeting.offsetByCodePoints(0,i);
int cp = greeting.codePointAt(index);

程序示例

public class HuangZiHanTest
{  
   public static void main(String[] args)
   {
	   String huangzihan_greeting="黄子涵是帅哥!";
	   int i=6;
	   int huangzihan_index = huangzihan_greeting.offsetByCodePoints(0,i);
	   System.out.println(huangzihan_index);
	   int huangzihan_cp = huangzihan_greeting.codePointAt(huangzihan_index);
	   System.out.println(huangzihan_cp);
   }
}

运行结果

6
65281

注释

不要以为可以忽略包含U+FFFF以上代码单元的奇怪字符,喜欢emoji表情符号的用户可能会在字符串中加入类似(U+1F37A,啤酒杯)的字符。

遍历字符串

如果想要遍历一个字符串,并且依次查看每一个码点,可以使用下列语句:

int cp = sentence.codePointAt(i); if(Character.isSupplementaryCodePoint(cp))
i += 2;
else i++;

isSupplementaryCodePoint()方法

描述

确定指定的字符(Unicode码点)是否在补充字符范围内。

参数

码点要测试的字符(Unicode码点)

返回值

如果指定的代码点介于MIN_SUPPLEMENTARY_CODE_POINT和MAX_CODE_POINT (含)之间,则为true;否则为false。

程序示例

运行结果

40644
23376
28085
26159
24069
21733
65281

40644
23376
28085
26159
24069
21733
65281

反向遍历

可以使用下列语句实现反向遍历:

i--;
if(Character.isSurrogate(sentence.charAt(i)))i--;
int cp=sentence.codePointAt(i);

程序示例

public class HuangZiHanTest
{  
   public static void main(String[] args)
   {
	   int codePoint0=0;
	   int codePoint1=1;
	   int codePoint2=2;
	   int codePoint3=3;
	   int codePoint4=4;
	   int codePoint5=5;
	   int codePoint6=6; 
	   
	   String sentence="黄子涵是帅哥!";
	   int huangzihan_cp0=sentence.charAt(codePoint0);
	   int huangzihan_cp1=sentence.charAt(codePoint1);
	   int huangzihan_cp2=sentence.charAt(codePoint2);
	   int huangzihan_cp3=sentence.charAt(codePoint3);
	   int huangzihan_cp4=sentence.charAt(codePoint4);
	   int huangzihan_cp5=sentence.charAt(codePoint5);
	   int huangzihan_cp6=sentence.charAt(codePoint6);
	   
	   System.out.println(huangzihan_cp0);   //黄的Unicode编码是\u9ec4,对应的十进制是40644
	   System.out.println(huangzihan_cp1);   //子的Unicode编码是\u5b50,对应的十进制是23376
	   System.out.println(huangzihan_cp2);   //涵的Unicode编码是\u6db5,对应的十进制是28085
	   System.out.println(huangzihan_cp3);   //是的Unicode编码是\u662f,对应的十进制是26159
	   System.out.println(huangzihan_cp4);   //帅的Unicode编码是\u5e05,对应的十进制是24069
	   System.out.println(huangzihan_cp5);   //哥的Unicode编码是\u54e5,对应的十进制是21733
	   System.out.println(huangzihan_cp6);   //!的Unicode编码是\uff01,对应的十进制是65281
	   
	   System.out.println(); 
	   
	   for(int codePoint=6;codePoint>0;codePoint--)
	   {	   
		   int huangzihan=sentence.charAt(codePoint);
		   System.out.println(huangzihan);
	   }
   }
}

运行结果

40644
23376
28085
26159
24069
21733
65281

65281
21733
24069
26159
28085
23376

isSurrogate()方法

描述

确定给定的字符值是否是Unicode代理代码单元。

这些值本身并不表示字符,而是用于表示UTF-16编码中的补充字符。

字符值是代理代码单元当且仅当它是低代理代码单元还是高代理代码单元时。

参数

ch要测试的char值

返回值

如果char值介于MIN_SURROGATE和MAX_SURROGATE之间,则返回true;否则为false。

codePoints方法

显然,这很麻烦。更容易的办法是使用codePoints方法,它会生成一个int值的“流”,每个int值对应一个码点。可以将它转换为一个数组,再完成遍历。

int[] codePoints = str.codePoints().toArray();

程序示例

import java.util.stream.IntStream;

public class HuangZiHanTest
{  
   public static void main(String[] args)
   {   
	   String huangzihan="黄子涵是帅哥!";
	   IntStream huangzihanCodePoints=huangzihan.codePoints();
	   int[] huangzihzanToArray = huangzihan.codePoints().toArray();
	   System.out.println(huangzihanCodePoints);
	   System.out.println(huangzihzanToArray);
   }
}

运行结果

java.util.stream.IntPipeline$Head@1fb3ebeb
[I@548c4f57

码点数组转换为字符串

反之,要把一个码点数组转换为一个字符串,可以使用构造器

String str=new String(codePoints,0,codePoints.length);

程序示例

public class HuangZiHanTest
{  
   public static void main(String[] args)
   {   
	   char[] huangzihanCodePoints ={'黄','子','涵','是','帅','哥','!'};
	   String huangzihan=new String(huangzihanCodePoints,0,huangzihanCodePoints.length);
	   System.out.println(huangzihan);
   }
}

运行结果

黄子涵是帅哥!

注释

虚拟机不一定把字符串实现为代码单元序列。在Java 9中,只包含单字节代码单元的字符串使用byte数组实现,所有其他字符串使用char数组。

String API

Java中的String类包含了50多个方法。令人惊讶的是它们绝大多数都很有用,可以想见使用的频率非常高。下面的API注释汇总了一部分最常用的方法。

本书中给出的API注释可以帮助你理解Java应用编程接口(API)。每一个API的注释都以类名开始,如java.lang.String。类名之后是一个或多个方法的名字、解释和参数描述。

在这里,一般不列出某个类的所有方法,而是选择一些最常用的方法,并以简洁的方式给予描述。完整的方法列表请参看联机文档。

这里还会列出所介绍的类的版本号。如果某个方法是在这个版本之后添加的,就会给出一个单独的版本号。

阅读联机API文档

正如前面所看到的,String类包含许多方法。而且,在标准库中有几千个类,方法数量更加惊人。要想记住所有的类和方法是一件不太不可能的事情。因此,学会使用联机API文档十分重要,从中可以查阅标准类库中的所有类和方法。可以从Oracle下载API文档,并保存在本地。也可以在浏览器中访问https://docs.oracle.com/javase/10/docs/api/overview-summary.html

在Java 10中,API文档有一个搜索框(见图(Java API文档))。较老的版本则有一些窗框,分别包含包列表和类列表。仍然可以点击Frames菜单项得到这些列表。例如,要获得有关String类方法的更多信息,可以在搜索框中键入“String”,选择类型java.lang.String,或者在窗框中找到String链接,然后点击这个链接。你会看到这个类的描述,如图(String类的描述)所示。

接下来,向下滚动,直到看见按字母顺序排列的所有方法的小结(请参看图(String类方法的小结))。点击任何一个方法名便可以查看这个方法的详细描述(参见图(一个String方法的详细描述))。例如,如果点击compare TolgnoreCase链接,就会看到compareToIgnoreCase方法的描述。

构建字符串

有些时候,需要由较短的字符串构建字符串,例如,按键或来自文件中的单词。如果采用字符串拼接的方式来达到这个目的,效率会比较低。每次拼接字符串时,都会构建一个新的String对象,既耗时,又浪费空间。使用StringBuilder类就可以避免这个问题的发生。

构建空字符串构建器

如果需要用许多小段的字符串来构建一个字符串,那么应该按照下列步骤进行。首先,构建一个空的字符串构建器:

StringBuilder builder = new StringBuilder();

append方法

当每次需要添加一部分内容时,就调用append方法。

builder.append(ch);    //appends a single character 
builder.append(str);   //appends a string

toString方法

在字符串构建完成时就调用toString方法,将可以得到一个String对象,其中包含了构建器中的字符序列。

String completedString = builder.toString();

程序示例

public class HuangZiHanTest
{  
   public static void main(String[] args)
   {   
	   StringBuilder HuangzihaBuilder=new StringBuilder();
	   char HuangzihanCharY='Y';
	   char HuangzihanCharE='E';
	   char HuangzihanCharS='S';
	   char HuangzihanChar='!';
	   String HuangzihanString="黄子涵是帅哥!";
	   HuangzihaBuilder.append(HuangzihanCharY);
	   HuangzihaBuilder.append(HuangzihanCharE);
	   HuangzihaBuilder.append(HuangzihanCharS);
	   HuangzihaBuilder.append(HuangzihanChar);
	   HuangzihaBuilder.append(HuangzihanString);
	   String HuangzihanToString=HuangzihaBuilder.toString();
	   System.out.println(HuangzihanToString);
   }
}

运行结果

YES!黄子涵是帅哥!

注释

StringBuilder类在Java 5中引入。这个类的前身是StringBuffer,它的效率稍有些低,但允许采用多线程的方式添加或删除字符。如果所有字符串编辑操作都在单个线程中执行(通常都是这样),则应该使用StringBuilder。这两个类的API是一样的。