Java内关于字符串的相关处理能供很是强大,但只是一个String类的话还不够,因为并没有想象的那般高效,所以Java还有StringBuilder和StringBuffer两个引用类型,用以帮助处理字符串。但我们得一步一步来,由浅及深地去了解。
(一)String类
①String类类对象地实例化
如果要实例化一个String类对象,那么就必然要看下构造方法了,但是身为官方提供地类,它地构造方法可太多了,所以我们挑常见地几个说明一下,如果是无参构造方法,那么会构造一个空的字符串,形如“”;如果传入的参数是一个字符串,那么会构造一个与这个字符串相同的字符串;如果传入的参数是一个字符数组,那么会按照字符数组内的字符,依次排列,构造成一个字符串。
依照上面的构造方法,我们实际使用效果如下:
String string1 = new String(); // string1值为""
String string2 = new String("123"); // string2值为"123"
String string3 = new String({'a','b','c'}); // string3值为"abc"
==================================================================
String string1 = ""; // string1值为""
String string2 = "123"; // string2值为"123"
String string3 = {'a','b','c'}; // string3值为"abc"
分界线上下的内容是完全等价的。此外我们还需要注意,String是引用变量,它本身不保存这些字符串的值,他们只是这些字符串的管理者。所以当我们暂时不想,或者没有字符串给String变量赋值的话,我们可以暂时赋值null,表示这个变量为空,方式如下:
String string = null;
②字符串的比较
字符串之间的比较跟基本数据类型的比较不大一样,因为String是引用类型,对应变量内只存储了对象的地址,并不保存对象本身,因此如果直接采用逻辑判断运算符进行逻辑判断的话,只会对变量内存储的对象地址进行比较,这往往会导致与我们所想要实现的结果有所不同,试看下面的代码:
public class Main {
public static void main(String[] args) {
String test1 = new String("123");
String test2 = new String("123");
System.out.println(test1 == test2); // output:false
System.out.println(test1.equals(test2)); // output:true
System.out.println(test1.compareTo(test2)); // output:0
}
}
可以看到,test1和test2都是String类型的变量,他们内部都存储了值为“123”的字符串对象地址,这两个字符串都是独立生成的,因此地址必然是不一样的,因此输出结果为false;但如果我们用String类内重写过的Object方法equals去比较两个字符串的值是否相等时,输出结果为true。此外equals只能判断两个字符串是否相等,并不能将两个字符串进行比较,这时我们可以调用String类的compareTo方法,将两个字符串进行比较,当test1大于test2时,返回值大于0;test1等于test2时,返回0;当test1小于test2时,返回值小于0,因为上面test1和test2内对应字符串的值是相同的,所以返回值为0。
话说回来,String类内存储字符串的方式,实际上有两个成员变量,一个是字符数组,一个是哈希值,哈希值我不懂,但是字符数组的存储跟C语言似乎是相同的,包括String类的charAt方法,也是直接访问对应字符数组下表来访问的。
③字符串的查找、转换、替换等功能
这部分主要使用的是String类已有的方法,并且十分简单,查找可以通过indexOf、charAt,这两个方法可以查找单个字符、也可以查找一个字符串。
字符串的转化,分情况,数字转字符串,可以使用对应包装类的toString方法,字符串转数字可以用valueOf方法,字符串大小写的转换可以用toUpperCase和toLowerCase方法。甚至于还有字符串转数组的方法,用toCharArray方法即可实现字符串转化为字符数组的功能。格式化的字符串的输出转化方式,可以使用format方法;字符串内的替换可以用replaceAll或者replaceFirst方法;字符串内的分割可以用split方法;字符串内的截取可以用substring方法。最后还有一个trim方法,这个方法主要作用就是去除一个字符串左右两边非空字符外的空字符。
④String类并不会修改字符串
String类的类对象,对字符串的所有操作,都不会改变字符串本身,这一点从String类的源代码内也可以看到,String类内的字符数组value,它是由private final修饰的,这说明value内存储的字符串对象地址是不可改变的;此外String类内,我们也并没有看到value的set和get方法,而由于value是private修饰的,所以如果没有对应get和set方法,我们是无法实现对String类对象内存储的字符数组进行修改的。
那有人说了,前面不是由字符串的转化、替换功能的方法吗?这些转化和替换,不就是对字符串进行修改吗?对,也不对,因为String类内,这些方法本质上都是产生了一个新的字符串,而不再是原来的字符串了。所以不难想到,这种不可修改的模式,会导致很多系统资源的浪费。
由于String类,是无法对字符串进行修改的,每次对字符串进行修改的操作,结果就是会产生一个新的字符串,所以我们在对String类进行使用的时候要尽量避免对字符串进行修改。
而为了解决这种资源浪费问题,Java也提供了StringBuffer和StringBuilder两个类,这两个类是基本一样的,唯一区别就是Buffer只允许单线程运行,实际使用到底使用哪一个需要具体问题具体分析,而使用这两个类对字符串进行操作时,字符串本身是允许被进行修改的,修改的同事也不会产生新的字符串,从而避免了系统资源的浪费。
(二)异常
①概念
对于程序来说,异常就是程序并未按预期一样去运行,这就是异常,那么程序为什么会没按预期进行运行呢?单单从代码角度来说,大致会分为两种情况,一个是敲代码的人有问题,一个是代码运行过程中给定的数据有问题。敲代码的人有问题,意味这代码本身就可能存在逻辑上的漏洞, 而给定的数据有问题,可能是给出了一个超过我们预期范围的数据。
而对于异常的反应,我们有两种处理的思路,一种是先预防,也就是操作之前,做好充分的检查于准备(LBYL,事前防御),另一种是出现问题再解决问题,即先操作,后处理(EAFP,事后处理),而对于Java来说,它的核心处理思路是按EAFP来的,所以提供了不少关键字,包括try、catch、fianlly、throw、throws,应用这些关键字,即可实现对异常的处理。
②异常的抛出与捕获
在程序出现异常时,需要抛出对应异常信息给程序调用者,这个过程在Java内是默认进行的,即Java本身对于常见的一些错误,已经预先提供了对应的异常信息抛出,那么针对一些特定的程序,能否按我们自己想法,抛出一个包含对应异常信息的自定义异常呢?
答案是肯定的,我们可以自定义一个类,然后让这个类继承Exception类,实例化这个类后,我们就可以将之抛出,定义的代码如下:
class TestIndexOfBoundException extends Exception {
public TestIndexOfBoundException() {
super();
}
public TestIndexOfBoundException(String word) {
super(word);
}
}
我们首先定义了一个TestIndexOfBoundException类,然后让它继承了Exception父类,这样,这个类就被认为是一个异常了,然后我们写出了两个构造方法,一个带参数,一个不带参数,带参数的构造方法传入的参数是一个字符串,虽然后我们调用父类的构造方法,将这个字符串传入,在父类那一侧,父类重载选择了带字符串参数的构造方法,从而将我们想要提示的异常信息提示出来。
有了自定义的异常了,那么我们如何将之抛出呢?答案很简单,我们只需要使用throw、try和catch关键字即可实现抛出与抓取的过程,具体代码如下:
public class Main {
public static void main(String[] args) {
java.util.Scanner input = new java.util.Scanner(System.in);
int num = -1;
try {
num = input.nextInt();
if (num == 0) {
throw new TestIndexOfBoundException("error! num != 0");
} else {
num = 1;
}
} catch (TestIndexOfBoundException e) {
e.printStackTrace();
}
}
}
// result:
// TestIndexOfBoundException: error! num != 0
// at Main.main(Main.java:9)
这段代码含义是,如果我们输入的值为0,就会抛出一个异常,并给出对应提示,如果输入非0值,就会让变量num的值变为1。如果我们希望不管这段异常发生不发生,后续代码都必须要执行下去,那么我们可以是用finally关键字去实现,实现也很简单,上面代码修改后变为下面的形式:
public class Main {
public static void main(String[] args) {
java.util.Scanner input = new java.util.Scanner(System.in);
int num = -1;
try {
num = input.nextInt();
if (num == 0) {
throw new TestIndexOfBoundException("error! num != 0");
} else {
num = 1;
}
} catch (TestIndexOfBoundException e) {
e.printStackTrace();
} finally {
System.out.println("123");
}
}
}
// result:
// TestIndexOfBoundException: error! num != 0
// at Main.main(Main.java:9)
// 123
我们可以发现,即使抛出了异常,我们finally内,打印“123”的语句仍然被执行了。
我们都知道方法的调用往往是多层的,比如我们这样一个语句:
System.out.println(java.util.Arrays.toString(array));
我们的方法嵌套在其他的方法内,假如这里可能会发生一个异常,我们不希望它直接在Arrays类的方法直接处理,而希望在Main类的方法内处理,即希望由方法的调用者去处理这个异常,那么我们就需要在可能发生异常的方法上声明这个异常,表明这个异常由调用者去处理。上述代码可以进一步更改为:
public class Main {
public static void main(String[] args) throws TestIndexOfBoundException {
java.util.Scanner input = new java.util.Scanner(System.in);
int num = -1;
try {
num = input.nextInt();
if (num == 0) {
throw new TestIndexOfBoundException("error! num != 0");
} else {
num = 1;
}
} catch (TestIndexOfBoundException e) {
e.printStackTrace();
} finally {
System.out.println("123");
}
}
}
// result:
// TestIndexOfBoundException: error! num != 0
// at Main.main(Main.java:9)
// 123
这样,我们就实现了对异常的声明。
③throw的特性
只能写在方法体内;
一旦抛出异常,后续程序就不再执行;
抛出的异常必须是Exception或者其子类对象;
如果抛出的是RunTimeException或者其子类,我们可以不处理,交由JVM处理;但如果是编译时异常,则必须进行处理,否则编译将无法通过。