1、 字符串类
字符串是我们在编程中最常使用的一种数据类型,Java中用类来描述字符串,其中最常用的字符串处理类是String,此外还有StringBuffer和StringBuilder。在本节,我们会了解每种字符串处理类的特点,以便能在应用时选择合适的字符串类型。字符串不属于8种基本数据类型,而是一种引用类型。String对象代表一组不可改变的Unicode字符序列,String类对象的内容一旦被初始化就不能再改变,对它的任何修改实际上又产生一个新的字符串。String类是final类型的类,不能被继承,所有的方法不允许被覆盖。StringBuffer对象代表一组可改变的Unicode字符序列,StringBuilder是JDK5.0版本后引入的字符串处理类,其中的方法与StringBuffer类的相同。他们都属于java.lang包,在使用的时候不需要用import语句导入。
1. String对象的创建
String是比较特殊的数据类型,它不属于基本数据类型,但是可以和使用基本数据类型一样直接赋值,也可以像引用数据类型一样,使用关键字new进行实例化。
String类型对象的实例化有两种方式:
–静态方式(常用):直接给变量赋值,
如:String s1 = "abc"; String s2 = "abc";
–动态方式:使用new运算符动态的给变量赋值,
如:String s3 = new String("abc"); String s4 = new String("abc");
这两种方式创建的字符串对象是有区别的。
区别在于:使用静态方式创建的字符串,如果前后两次创建的字符串内容相同,则在堆区的常量池中只会产生一个字符串对象,即两个引用指向同一块地址。而使用动态方式创建的字符串,不管前后两次创建的字符串内容是否相同,每创建一次,都会在堆内存中会产生出不同的对象,即两个引用指向不同的地址。
动态创建String对象时需要用到构造方法,String类的常用的构造方法如下,更多构造方法请直接查阅Java API。
- 初始化一个新创建的String 对象,它表示一个空字符序列。
String 变量名 = new String() ;
初始化一个新创建的String对象,表示一个空字符串(" ");注意空字符串与null的区别,空字符串表示String对象的内容为空,而null表示String类的变量不指向任何的String对象。
- 初始化一个新创建的 String对象,表示一个与该参数相同的字符序列。
String 变量名 = new String (String value) ;
- String(char chars[]) 使用一个字符数组创建一个String对象。
注意,当使用"+"运算符进行运算时,如果参与运算的有字符串,则"+"的含义为进行字符串连接,当参与运算的没有字符串时,则"+"的含义为算术运算符加号。例如:
String str1 = "hello ";
String str2 = "world";
System.out.println (str1 + str2); //输出结果为"hello world"
System.out.println(5 + 6 + 'A'); //输出结果为76
System.out.println(5 + 6 + "A"); //输出结果为11A
System.out.println(5 + "A" +6); //输出结果为5A6
2. String 对象的不可变性
任何一个String对象在创建之后都不能对它的内容作出任何改变。 String的不可变性具体讲就是每个String的实例会包含一个名为 private final char [] value的字符数组实例,一旦一个String类被实例后,这个value是不会被改变的,即具有不可变性(immutability)。String还包含一个hash字段,它的值取决于value的内容,value不变hash也不会改变的,所以String是一个不可变类。
String对于连接、获得子串和改变大小写等操作,如果返回值同原字符串不同,实际上是产生了一个新的String对象,在程序的任何地方,相同的字符串字面常量都是同一个对象,下面的代码会改变字符串s的内容吗?
String s = "Java";
s = "HTML";
答案是不会。第一条语句创建了一个内容为"Java"的String对象,并将其引用赋值给s。第二条语句创建了一个内容为"HTML"的新String对象,并将其引用赋值给s。赋值后第一个String对象仍然存在,但是不能再访问它,因为变量s现在指向了新的对象HTML,如图所示。
上图字符串是不可改变的,一旦创建,它们的内容不能修改
因为字符串在程序设计中是不可改变的,但同时又会被频繁地使用,所以Java虚拟机为了提高效率并节省内存,对具有相同字符串序列的字符串直接使用同一个实例。例如下面的语句:
Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
String str0 = "aa";
String str1 = "aa";
String str2 = new String("aa");
System.out.println(str0 == str1); // true
System.out.println(str0 == str2); // false
char[] value0 = (char[]) field.get(str0);
char[] value2 = (char[]) field.get(str2);
System.out.println(value0 == value2); // true
程序运行结果:
true
false
true
说明str0和str2是两个独立的对象,但是value字段是公用的。这段代码在内存图解如下:
3. 字符串的比较
String类提供了多种对字符串比较的方法,具体如表1-6所示。
表1-6 字符串比较方法
方法 | 含义 |
boolean equals(String) | 判断两个字符串对象的内容是否相等 |
boolean equalsIgnoreCase(String) | 比较两个字符串的内容是否相等,忽略大小 |
int compareTo(s1:String) | 返回一个大于0、等于0或者小于0的整数以表明这个字符串是大于、等于还是小于s1 |
int compareToIgnoreCase(String) | 除了不区分大小写外,其他都和compareTo是一样的 |
boolean regionMatches(int, String, int, int) | 如果这个字符串指定的子域精确匹配字符串s1中指定的子域则返回true |
boolean startsWith(String prefix) | 测试此字符串是否以指定的前缀开始 |
boolean endsWith(String suffix) | 测试此字符串是否以指定的后缀结束 |
例如,下面的语句先显示true,然后显示false.
String s1 = new String("welcome to Java");
String s2 = "welcome to Java";
String s3 = "welcome to C++";
System.out.println(s1.equals(s2)); //true
System.out.println(s1.equals(s3));//false
【例】字符串equals方法练习
String s1 = "abc";
String s2 = new String("abc");
String s3 = new String("abc");
String s4 = "abc";
System.out.println(s1.equals(s2)); //true
System.out.println(s1.equals(s4)); // true
System.out.println(s2.equals(s3)); //true
程序分析:equals方法用来比较两个字符串是否相等。
【例】重写equals()方法,判断不同的点,是否是坐标系上的同一点。
//点类
public class Point {
private int x; //x坐标
private int y;//y坐标
public Point(int x,int y){
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
@Override
public String toString() {
return "("+x+","+y+")";
}
//如果x,y的值相同,就认为是坐标上的同一点
public boolean equals(Object obj) {
if(obj == null){
return false;
}
if(this == obj){
return true;
}
if(!(obj instanceof Point)){
return false;
} else {
Point p = (Point)obj;
if(p.x==x&&p.y==y){
return true;
} else {
return false;
}
}
}
}
//测试类
public class TestPoint {
public static void main(String[] args) {
Point p1 = new Point(55,66);
Point p2 = new Point(55,66);
Point p3 = new Point(22,33);
System.out.println(p1.equals(p2));
System.out.println(p1==p2);
System.out.println(p1.equals(p3));
System.out.println(p1==p3);
}
}
程序运行结果:
true
false
false
false
程序分析:上例测试中main()方法中实例化了3个点,分别使用equals方法与==进行比较,因为Point类覆盖了父类的equals()方法,该方法判断如果两个Point对象的x坐标和y坐标相等,则返回true,所以第1行输出为true,"=="运算符比较引用数据类型时,判断两个引用是否指向同一个对象,p1,p2,p3都分别是不同的对象,所以第2行和第4行输出为false。
compareTo方法也可以用来对字符串进行比较。方法如下:
s1.compareTo(s2)
如果s1与s2相等,那么该方法返回值为0;如果按字典序(即以统一码的顺序)s1小于s2,那么方法返回值小于0;如果按字典序s1大于s2,方法返回值大于0。
方法compareTo返回的实际值是依据s1和s2从左到右数第一个不同字符之间的距离得出的,例如,假设s1为“abc”,s2为“abg”,那么s1.compareTo(s2)返回-4。首先比较的是s1与s2中第一个位置的字符(a与a)。因为它们相等,所以比较第二个位置的两个字符(b与b)。因为它们也相等,所以比较第三个位置的两个字符(c与g)。由于字符c比字符g小4,所以比较之后返回-4。如果使用像>、>=、<或<=这样的比较运算符比较两个字符串,就会发生错误,替代的方法就是使用s1.compareTo(s2)来进行比较。
如果两个字符串相等,equals方法返回true;如果不相等,方法返回false。compareTo方法会根据一个字符串是否等于、大于或小于另一个字符串,分别返回0、正整数或负整数。
4. 字符串与数组之间的转换
字符串不是数组,但是字符串可以转换成字节数组和字符数组,反之亦然。
(1)与字符数组之间的转换
为了将字符串转换成一个字符数组,可以使用toCharArray方法。例如,下述语句将字符串"Java "转换成一个字符数组:
Char[] chars= "Java".toCharArray();
因此chars[0]是'J',chars[1]是'a',chars[2]是'v',chars[3]是'a'。
还可以使用方法getChars(int srcBegin,int srcEnd,char[]dst,int dstBegin)将下标从srcBegin到srcEnd-1的子串复制到字符数组dst中下标从dstBegin开始的位置。例如,下面的代码功能是把字符串"CS3720"中下标从2到6-1的子串"3720"复制到字符数组dst中,赋值时下标从4开始的位置:
Char[] dst={'j','a','v','a','1','3','0','1'};
"CS3720".getChars(2,6,dst,4);
这样,dst就变成了{'j','a','v','a','3','7','2','0'}。
为了将一个字符数组转换成字符串,应该使用构造方法String(char[])或者方法valueOf(char[])。例如,下面的语句使用String构造方法把一个字符数组构造成一个字符串:
char[ ] chr = new char[]{'j','a','v','a'};
String str = new String( chr );
下面的语句使用valueOf方法把一个字符数组构造成一个字符串:
char[ ] chr = new char[]{'j','a','v','a'};
String str = String.valueOf( chr );
(2)与字节数组之间的转换
Java中能够把字符串转换为字节数组,有3种形式。
形式1:public byte[] getBytes() //方法定义:以默认编码把字符串转换为字节数组
形式2:public byte[] getBytes(Charset charset) //方法定义:按照指定的字符集把字符串转换为字节数组
形式3:public byte[] getBytes(String charsetName) //方法定义:按照指定的字符集把字符串转换为字节数组
【例】 使用平台的默认字符集,统计一个字符串所占用的字节数。
public class TestStringByte{
public static void main(String[] args) {
String str = "Java语言程序设计";
byte bytes[] = str.getBytes();
System.out.println(bytes.length);
}
}
程序运行结果:
16
程序分析:通过getBytes()方法把字符串转换成字节数组,字节数组的长度就是字符串所占的字节数。因为一个字符需要1~2个字节,或者更多,所以数节数与字符数不同。
【例】通过字节数组创建字符串对象
public class TestStringCharset {
public static void main(String[] args) {
byte[] bName = new byte[10];
String name1 = "张三";
try{
bName = name1.getBytes("utf-8"); //这种编码方式一个中文占三个字节
String name2 = new String(bName,"utf-8");
System.out.println("name2="+name2);
for(int i = 0;i< bName.length;i++){
System.out.print(bName[i]);
}
}catch (UnsupportedEncodingException e){
e.printStackTrace();
}
}
}
程序运行结果:
name2=张三
-27-68-96-28-72-119
程序分析:getBytes()是将一个字符串转化为一个字节数组。String的getBytes()方法是得到一个系统默认的编码格式的字节数组。将一个String类型的字符串中包含的字符转换成byte类型并且存入一个byte数组中。在Java中的所有数据底层都是字节,字节数据可以存入到byte数组。存储字符数据时(字符串就是字符数据),会先进行查表,然后将查询的结果写入设备,读取时也是先查表,把查到的内容打印到显示设备上,getBytes()是使用默认的字符集进行转换,getBytes(“utf-8”)是使用UTF-8编码表进行转换。
5. String中的常用方法
String的一些常用方法如表1-7所示。
String类的常用方法
方法 | 含义 |
byte[] getBytes(Charset charset) | 使用给定的 charset将此String 编码到byte 序列,并将结果存储到新的byte 数组 |
String[] split(String regex) | 根据给定正则表达式的匹配拆分此字符串。 |
String replace(char oldChar, char newChar) | 返回一个新的字符串,它是通过用newChar 替换此字符串中出现的所有oldChar 得到的 |
String toUpperCase() | 将String对象中的所有字符都转换为大写 |
String toLowerCase() | 将String对象中的所有字符都转换为小写 |
char charAt(int) | 返回指定索引处的 char 值 |
String substring(int begin) | 返回一个新字符串,该字符串是从begin开始的字符串的内容 |
String substring(int begin,int end) | 返回一个新字符串,该字符串是从begin开始到end-1结束的字符串的内容 |
int indexOf/lastIndexOf(char) | 返回指定字符在此字符串中第一次/最后一次出现处的索引。 |
int indexOf/lastIndexOf(char,int) | 从指定的索引开始搜索,返回在此字符串中第一次/最后一次出现指定字符处的索引 |
int indexOf/lastIndexOf(String) | 返回第一次出现的指定子字符串在此字符串中的索引 |
int indexOf/lastIndexOf(String,int) | 从指定的索引开始搜索,返回在此字符串中第一次/最后一次出现指定字符串处的索引 |
String trim() | 返回新的字符串,忽略前导空白和尾部空白 |
int length() | 返回此字符串的长度 |
String concat(String str) | 将指定字符串连接到此字符串的结尾 |
byte[] getBytes() | 使用平台的默认字符集将此String 编码为byte 序列,并将结果存储到一个新的byte 数组中 |
下面对常用的一些方法进行详细说明,为了便于说明,方法中使用的示例字符串为:
str="this is a test!";
(1)求长度
方法定义:public int length() 。
方法描述:获取字符串中的字符的个数。
例如:
str.length()
结果:
15
(2)获取字符串中的字符
方法定义:public char charAt(int index)。
方法描述:获取字符串中的第index个字符,从0开始。
例如:
str.charAt(3)
结果:
s
注意:是第4个字符。
【例】 统计一个字符串中字符e出现的次数。
public class TestString {
public static void main(String[] args) {
String s = "abecedkjkacedjkdseddklj";
int num = 0;
for(int i=0; i<s.length(); i++){
char c = s.charAt(i);
if(c == 'e'){
num++;
}
}
System.out.println("该字符串中字符e出现"+num+"次");
}
}
程序分析:程序中num的作用是用来记录字符'e'出现的次数。
(3)取子串
有两种形式:
形式1如下:
方法定义:public String substring(int beginIndex,int endIndex)。
方法描述:获取从beginIndex 开始到endIndex-1 结束的子串,包括beginIndex,不包括endIndex。
例如:
str.substring(1,4)
结果:
his
形式2如下:
方法定义:public String substring(int beginIndex)
方法描述:获取从beginIndex开始到结束的子串
例如:
str.substring(5)
结果:
is a test!
(4)定位字符或者字符串
indexOf方法有4种重载形式:
形式1如下:
方法定义:public int indexOf(int ch)
方法描述:定位参数所指定的字符。
例如:
str.indexOf('i')
结果:
2
形式2如下:
方法定义:public int indexOf(int ch,int index)
方法描述:从index开始定位参数所指定的字符。
例如:
str.indexOf('i',4)
结果:
5
形式3如下:
方法定义:public int indexOf(String str)
方法描述:定位参数所指定的字符串。
例如:
str.indexOf("is")
结果:
2
形式4如下:
方法定义:public int indexOf(String str,int index)
方法描述:从index开始定位str所指定的字符串。
例如:
str.indexOf("is",6)
结果:
-1表示没有找到
(5)替换字符和字符串
replace方法有3种重载形式:
形式1如下:
方法定义:public String replace(char c1,char c2)
方法描述:把字符串中的字符c1替换成字符c2
例如:
str.replace('i','I')
结果:
thIs Is a test!
形式2如下:
方法定义:public String replaceAll(String s1,String s2)
方法描述:把字符串中出现的所有的s1替换成s2
例如:
replaceAll("is","IS")
结果:
thIs IS a test!
形式3如下:
方法定义:public String replaceFirst(String s1,String s2)
方法描述:把字符串中的第一个s1替换成s2
例如:
replaceFirst("is","IS")
结果:
thIS is a test!
(6)比较字符串内容
根据是否需要考虑字符大小写,字符串比较有两种具体的实现方法:
方法1:public boolean equals(Object o)
方法描述:比较字符串内容是否与参数相同,区分大小写。
例如:
str.equals("this")
结果:
false
方法2:public boolean equalsIgnoreCase(Object o)
方法描述:比较是否与参数相同,不区分大写小。
例如:
str.equalsIgnoreCase("this")
结果:
false
(7)大小写转换
转换成大写:
方法定义:public String toUpperCase()
方法描述:把字符串中的所有字符都转换成大写。
例如:
str.toUpperCase()
结果:
THIS IS A TEST!
转换成小写:
方法定义:public String toLowerCase()
方法描述:把字符串中的所有字符都转换成小写。
例如:
str.toLowerCase()
结果:
this is a test!
(8)前缀和后缀
判断字符串是否以指定的参数开始或者结尾。
判断前缀:
方法定义:public boolean startsWith(String prefix)
方法描述:字符串是否以参数指定的子串为前缀。
例如:
str.startsWith("this")
结果:
true
判断后缀:
方法定义:public boolean endsWith(String suffix)
方法描述:字符串是否以参数指定的子串为后缀。
例如:
str.endsWith("this")
结果:
false
【例】判断一个字符串中子字符串出现的次数
import java.io.DataInputStream;
public class StringTest {
public static void main(String[] args) {
String str1 ="ab";//读入一行数据
String str2 = "abcedabsdabajab";
String str3 = str2.replace(str1,"");//把str2中的str1用空串替换,并赋给str3
int count = str2.length() - str3.length();//str2的长度减去str3的长度
count /= str1.length(); // 为什么需要除以长度数呢?分析一下吧
System.out.println(str1+"在"+str2+"中出现的次数为:"+count);
}
}
程序运行结果:
ab在abcedabsdabajab中出现的次数为:4
【例】使用字符串中的常用方法实现Email格式的判断。
public class EmailCheckException extends Exception {
public EmailCheckException(String msg) {
super(msg);
}
public class StringExercise {
public static void checkEmail(String email) throws EmailCheckException {
int len = email.length(); //email的长度
int begin = email.indexOf('@'); //从开始检索字符@在email中的位置
int end = email.lastIndexOf('@'); //从结尾检索字符@在email中的位置
int dot = email.indexOf('.', begin);//检索.是否在@后存在
//判断长度是否不超过20
if (email.length() > 20)
throw new EmailCheckException("Email长度不能大于20");
//判断是否唯一包含@
else if (begin != end)
throw new EmailCheckException("Email中含有多个@");
//判断@是否存在或在开头或在结尾
else if (begin == -1 || begin == 0 || begin == (len - 1))
throw new EmailCheckException("Email中没有@或@位置错误");
//判断@后是否有.
else if (dot == -1)
throw new EmailCheckException("@后缺少域分隔符");
//判断@是否在末尾
else if (dot == (len - 1))
throw new EmailCheckException("分隔符错误");
}
public static void main(String args[]) {
String email = args[0];
try {
checkEmail(email);
} catch (EmailCheckException e) {
e.printStackTrace();
}
}
}
}
程序分析:自定义了一个EmailCheckException异常类,当Email格式不符合要求时将抛出一个异常对象。程序中使用字符串的length()方法判断Email长度是否正确。通过indexOf()和lastIndexOf()分别从头和从尾查找@的位置,若从头查找和从尾查找到的@位置相等,说明字符串中只有一个@字符,否则存在多个@字符,则不是合法的Email格式。最后查找字符“.”的位置。若其位置在@字符后,且字符串中含有这个字符则是合法的Email格式。
6. StringBuffer和StringBuilder类
由于String对象的不可变性,所以一个字符串的内容如果经常需要变动,就不应该使用String,因为在变化的过程中实际上是不断创建对象的过程,这时候应该使用StringBuffer或者StringBuilder。
他们之间的集成关系树如下:
StringBuffer和StringBuilder用法基本相同。StringBuilder和 StringBuffer 之间的最大不同在于 StringBuilder 的方法不是线程安全的(不能同步访问)。由于 StringBuilder 相较于 StringBuffer 有速度优势,所以多数情况下建议使用 StringBuilder 类。
【例】StringBuilder常用方法及执行过程示意图如下:
public class RunoobTest{
public static void main(String args[]){
// 初始时指定缓冲器的长度
StringBuilder sb = new StringBuilder(10);
sb.append("Runoob..");
System.out.println(sb);
sb.append("!");
System.out.println(sb);
sb.insert(8, "Java");
System.out.println(sb);
sb.delete(5,8);
System.out.println(sb);
}
}
最终执行结果如下:
Runoob..
Runoob..!
Runoob..Java!
RunooJava!