名师讲坛:Java开发实战经典(第2版)
上QQ阅读APP看书,第一时间看更新

5.7 String类

本书一直使用String声明一个字符串,相信读者对这个类型应该不是很陌生,但是从之前本书的要求来看,读者不难发现,String声明的时候单词的首字母大写,所以String本身是一个类的定义。但是此类在使用时却有很多的要求,而且此类在Java中也算是一个比较特殊的类,那么本节将为读者讲解String类的作用以及使用上的一些限制。

5.7.1 实例化String类对象

对于String可以采用直接赋值的方式进行操作,如下面的代码。

【例5.16】直接为String赋值

程序执行结果:

在String类的使用上还有另外一种形式的实例化方法,就是直接调用String类中的构造方法,String类存在以下的构造方法:

所以以上的范例,也可以通过如下的代码进行编写。

【例5.17】使用第2种String的实例化方式

程序执行结果:

可以发现上面两段代码的使用效果类似,两种方法都可以使用,而两者具体的区别在后面将有所讲解。

5.7.2 String对象的内容比较

对于基本数据类型(例如int型变量)可以通过“==”进行内容的比较,如下面代码。

【例5.18】使用“==”进行内容的比较

程序执行结果:

可以发现,两个数字的比较结果相等,下面,按照以上的程序思路进行比较两个字符串的操作。

【例5.19】使用“==”比较字符串的内容

程序执行结果:

从程序运行结果中可以发现,虽然以上程序中String的内容都一样,但是比较的结果却是有的相等,有的不相等,这是为什么呢?下面通过图5-11为读者说明。

图5-11 String对象的声明

从图5-11中可以清楚地发现,每个String对象的内容实际上都是保存在堆内存之中的,而且堆中的内容是相等的。但是对str1和str2来说,其内容分别保存在了不同的空间,所以即便内容相等,但是地址的值也是不相等的,“==”是用来进行数值的比较,所以str1和str2比较不相等。那么str2和str3呢?从程序中可以发现str2和str3指向了同一个空间,是同一个地址,所以最终结果str2和str3的地址值是相等的,同理,str1和str3的地址值也不相等,所以返回了false。从上面的结果中可以清楚地发现“==”是用来进行地址值比较的。

那么既然无法使用“==”进行判断,那该如何去判断两个字符串的内容是否相等呢?此时,就可以利用String中专门提供的方法(String是一个类,则会存在各种方法):

提示

equals()方法暂时变化一下形式。

如果读者查询Java Doc文档,可以发现,String类中定义的equals()方法为:public boolean equals(Object str),但是此处列出的是public boolean equals(String str),只是先将参数的类型改变,而之所以这样改变,主要是因为还没有为读者讲解Object类,而Object类在第6章中会为读者讲解。

同时equals()方法是会区分字符串大小写字母的,也就是说“H”与“h”比较的结果为false。

【例5.20】使用equals()方法对String的内容进行比较

程序执行结果:

因为equals()方法的作用是将内容进行比较,所以此处的返回结果都是true。在实际的开发之中,由于字符串的地址是不好确定的,所以不要使用“==”比较,所有的比较都要通过equals()方法完成。

常见面试题分析:请解释String类中“==”和equals()方法比较的区别。

(1)“==”是Java提供的关系运算符,主要的功能是进行数值相等判断的,如果用在了String对象上表示的是内存地址数值的比较。

(2)equals()方法是由String提供的一个方法,此方法专门负责进行字符串内容的比较。

5.7.3 String类两种对象实例化方式的区别

String有两种实例化方式,一种是通过直接赋值的方式,另外一种是使用标准的new调用构造方法完成实例化,那么两种方式有什么区别?该使用哪种更好呢?

如果要想对以上问题进行解释,首先必须明白一个重要概念,一个字符串就是一个String类的匿名对象,匿名对象就是已经开辟了堆内存空间的并可以直接使用的对象。

【例5.21】验证一个字符串就是String的匿名对象

程序执行结果:

从程序的运行结果可以发现,一个字符串确实可以调用String类中的方法,也就证明了一个字符串就是一个String类的匿名对象。

提示

避免NullPointerException的比较操作。

在实际的工作开发中会出现这样一种情况:假设现在用户需要输入某一个字符串,而后判断该输入的内容是否为指定内容,如果用户输入信息还可以判断,而当用户不输入信息时如果处理不当就会出现NullPointerException,如下面代码所示。

实例1:观察问题

此时由于没有输入数据所以input的内容为null,而null对象调用方法的结果将直接导致“NullPointerException”,而这样的问题可以通过一些代码的变更来帮助用户回避。

实例2:回避NullPointerException问题

此时的程序直接利用了字符串常量来调用了equals()方法,因为字符串常量是一个String类的匿名对象,所以该对象永远不可能是null,所以将不会出现NullPointerException,特别需要提醒读者的是,equals()方法内部实际上也存在有null的检查,这一点有兴趣的读者可以打开Java类的源代码来观察,或者通过本书中第6章的内容来学习。

所以,对于以下的代码:

实际上就是把一个在堆中开辟好的堆内存空间的使用权给了str1这个对象,如图5-12所示。

图5-12 直接赋值实例化String类对象

实际上使用此种方式还有另外一个好处,那就是,如果一个字符串已经被一个名称所引用,则以后再有相同的字符串声明的时候,不会再重新开辟空间。如下面代码所示,观察代码的输出结果。

【例5.22】采用直接赋值的方式声明多个String对象,并且内容相同,观察地址比较

程序执行结果:

从程序的运行结果可以发现,所有的字符串都使用了“==”进行了比较,所得到的结果都是true,那么也就是说这3个字符串指向的堆内存地址空间都是同一个,所以,String使用直接赋值的方式之后,只要是以后声明的字符串内容相同,则不会再开辟新的内存空间。如图5-13所示。

技术穿越:String类所采用的设计模式为共享设计模式。

在JVM的底层实际上会存在有一个对象池(不一定只保存String对象),当代码之中使用了直接赋值的方式定义了一个String类对象时,会将此字符串对象所使用的匿名对象入池保存,而后如果后续还有其他String类对象也采用了直接赋值的方式,并且设置了同样内容的时候,那么将不会开辟新的堆内存空间,而是使用已有的对象进行引用的分配,从而继续使用。

关于共享设计模式的简单解释:这就好比在家中准备的工具箱一样,如果有一天需要用到螺丝刀,发现家里没有,那么肯定要去买一把新的,但是使完之后不可能丢掉,会将其放到工具箱中以备下次需要好继续使用,而这个工具箱中的工具肯定可以为家庭中的每一个成员服务。

下面介绍使用new String()的方式实例化String对象。

【例5.23】使用new String()的方式实例化String对象

一个字符串就是一个String类的匿名对象,如果使用new关键字的话,不管如何都会再开辟一个新的空间,但是此时,此空间的内容还是“hello”,所以上面的代码实际上是开辟了两个内存空间,但真正使用的只是一个使用关键字new开辟的空间,另一个是垃圾空间,如图5-14所示。

图5-13 使用直接赋值的方式为String实例化

图5-14 new String方式实例化对象

通过以上的两种实现方式比较,读者应该很清楚的知道哪种方式更合适,所以本书强烈建议读者,对于字符串的操作就采用直接赋值的方式完成,而不要采用构造方法传递字符串的方式完成,当然,在String类中也存在一些其他的构造方法,这点随后读者可以看到。

常见面试题分析:请解释String类的两种对象实例化方式的区别。

(1)直接赋值(String str="字符串";):只会开辟一块堆内存空间,并且会自动保存在对象池之中以供下次重复使用;

(2)构造方法(String str=new String("字符串")):会开辟两块堆内存空间,其中有一块空间将成为垃圾,并且不会自动入池,但是用户可以使用intern()方法手工入池。

在日后的所有开发之中,String对象的实例化永远都采用直接赋值的方式完成。

5.7.4 字符串的内容不可改变

在使用String类进行操作的时候还有一个特性是特别重要的,那就是字符串的内容一旦声明则不可改变。下面通过一段代码进行介绍。

【例5.24】修改字符串的内容

程序执行结果:

从程序的运行结果发现String对象的内容可以修改,但是内容真的是修改了吗?下面通过内存的分配图说明字符串不可更改的含义,如图5-15所示。

图5-15 字符串内容的修改

从图5-15中可以清楚地发现,一个String对象内容的改变实际上是通过内存地址的“断开-连接”变化完成的,而本身字符串中的内容并没有任何的变化。

注意

String在开发中的不正确应用。

读者日后在程序的开发中一定要明确地记住字符串的内容不可改变这一重要特征,所以对于以下的程序代码,在开发中一定要尽可能回避。

实例:需要注意的代码

以上的代码通过循环修改了100次str1的内容,就意味着字符串的指向要“断开-连接”100次,此程序代码的性能是很低的。当然,对于以上的需求在Java中也有相应的解决方式,可以使用StringBuffer类完成,StringBuffer类的内容将在Java常用类库章节中为读者介绍。

5.7.5 String类中常用方法

String在所有的项目开发里面都一定要使用到,那么在String类里面提供了一系列的功能操作方法。除了之前所介绍的两个方法(equals()、intern())之外,还提供有大量的其他操作方法。这些方法可以通过Java Doc文档查阅,如图5-16所示。考虑到String类在实际的工作之中使用非常的广泛,建议读者尽可能将所有讲解过的方法都背下来,并且希望读者将以下所讲解的每一个方法的名称、返回值类型、参数的类型及个数、方法的作用全部记下来。

图5-16 String类的Java Doc说明

技术穿越:文档很重要。

每一位开发者都不可能将Java的全部方法都记下,所以优秀的开发者一定是会参考文档的,在本书中没有为读者使用中文的Java文档,这样做的目的也是为了日后的其他技术文档接轨,对于文档,可以这么讲:每一门技术都会提供文档,所以读者在本章一定要具备文档的查询能力。

对于每一个文档的内容而言,它都包含有以下几个主要组成部分。

(1)第一部分:类的定义以及相关的继承结构。

(2)第二部分:类的一些简短的说明。

(3)第三部分:类的组成结构。

①类之中的成员(Field Summary)。

②类中的构造方法(Constructor Summary)。

③类中的普通方法(Method Summary)。

(4)第四部分:对每一个成员、构造方法、普通方法的作用进行详细说明,包括参数的作用。

为了方便读者理解,表5-3列出了String类常用的操作方法。

表5-3 String类常用操作方法

下面通过具体的应用范例来讲解String类中常用方法的基本使用。

1.字符串与字符数组的转换

字符串可以使用toCharrArray()方法变成一个字符数组,也可以使用String类的构造方法把一个字符数组,变为一个字符串。

【例5.25】验证以上的方法

程序执行结果:

程序在一开始将一个字符串变为一个字符数组,字符串的长度就是转换之后字符数组的行数,也可以把一个字符数组变为字符串,可以将字符数组全部转换或部分转换。

2.从字符串中取出指定位置的字符

可以直接使用String类中的charAt()方法取出字符串指定位置的字符。

【例5.26】验证以上方法

程序执行结果:

3.字符串与byte数组的转换

字符串可以通过getBytes()方法将String变为一个byte数组,之后可以通过String的构造方法将一个字节数组重新变为字符串。

【例5.27】验证以上代码

程序执行结果:

在以后的IO操作中经常会遇到字符串与byte数组或是与char数组之间的转换操作,可以发现,byte数组与char数组的转换代码风格是很相似的。

4.取得一个字符串的长度

在String中使用length()方法取得字符串的长度。

【例5.28】验证以上方法

程序执行结果:

以上代码调用了length()取得了字符串的长度。

注意

length与length()。

许多初学者经常对“length”和“length()”两者的关系搞不清楚,“在数组操作中,使用length取得数组的长度,但是操作的最后没有‘()’。而字符串调用length()是一个方法,只要是方法后面都有‘()’”。

5.查找一个指定的字符串是否存在

在String中使用indexOf()方法,可以返回指定的字符串的位置,如果不存在则返回-1。

【例5.29】验证以上方法

程序执行结果:

从程序的运行结果可以发现,如果查找到了指定的字符串,则会返回此字符串的位置,如果没有查找到,则返回-1。

提示

可以使用JDK 1.5后提供的contains()方法。

从JDK 1.5开始,String类对于判断字符串是否存在的方法提供有contains()(public boolean contains(String str)),此方法直接返回boolean型数据。

实例:使用contains()方法判断子字符串是否存在

程序执行结果:

contains()方法相较indexOf()方法会更加简单一些,但使用何种方法还是看个人的编程习惯。

6.去掉左右空格

在实际的系统开发中,用户输入的数据中可能含有大量的空格,此时,使用trim()方法可以去掉字符串的左、右空格。

【例5.30】验证以上方法

程序执行结果:

从程序的运行结果可以发现,字符串中左、右两边的空格都被清除掉了。

7.字符串截取

在String中提供了两个substring()方法,一个是从指定位置截取到字符串结尾,另一个是截取指定范围的内容。

【例5.31】验证以上方法

程序执行结果:

8.按照指定的字符串拆分字符串

在String中通过split()方法可以进行字符串的拆分操作,拆分的数据将以字符串数组的形式返回。

【例5.32】验证以上方法

程序执行结果:

本程序是根据空格进行了字符串的拆分,如果在使用split()方法时只设置一个空字符串“""”,那么就表示按照每一个字符进行拆分。

注意

要避免正则表达式的影响,可以进行转义操作。

实际上split()方法的字符串拆分能否正常进行都与正则表达式的操作有关,所以有些时候会出现无法拆分的情况,例如:现在给一个IP地址(192.168.1.2),那么肯定首先想到的是根据“.”拆分,而如果直接使用“.”是不可能正常拆分的。

实例1:错误的拆分操作

此时是不能够正常执行的,而要想正常执行,必须对要拆分的“.”进行转义,在Java中转义要使用“\\”(“\\”表示一个“\”)描述。

实例2:正确的拆分操作

程序执行结果:

此时的程序已经实现了字符串的拆分操作。而关于正则表达式的内容将在本书第11章为读者讲解。

9.字符串的大小写转换

在用户输入信息时,有时需要统一输入数据的大小写,那么此时就可以使用toUpperCase()和toLowerCase()两个方法完成大小写的转换操作。

【例5.33】验证以上方法

程序执行结果:

10.判断是否以指定的字符串开头或结尾

在String中使用startsWith()方法可以判断字符串是否以指定的内容开头,使用endsWith()方法可以判断字符串是否以指定的内容结尾。

【例5.34】验证以上方法

程序执行结果:

11.不区分大小写进行字符串比较

在String中可以通过equals()方法进行字符串内容的比较,但是这种比较方法是区分大小写的比较,如果要完成不区分大小写的比较则可以使用equalsIgnoreCase()方法。

【例5.35】验证以上方法

程序执行结果:

从程序运行结果可以发现,equals()方法在比较的时候是区分大小写的。

12.将一个指定的字符串,替换成其他的字符串

使用String的replaceAll()方法,可以将字符串的指定内容进行替换。

【例5.36】验证以上方法

程序执行结果:

上面列举了String类的一些常用的操作方法,而其中所涉及的replaceAll()、split()等方法还需要在第11章讲解的正则表达式中进一步介绍。