在前面的Java字符串小节,我们就已经接触了String类,但并未提及String类相关的操作,现在有了面向对象相关前置知识,我们知道了类下面可以有相关的操作,作为Java语言的内置类,String类也为我们预先定义了很多好用的方法,本小节我们将介绍String类的常用方法,并结合示例辅助理解。
1. String 对象的创建String对象的创建有两种方式。
第1 种方式就是我们最常见的创建字符串的方式:
String str1 = "Hello, 夜猫编程";代码块1
第 2 种方式是对象实例化的方式,使用new关键字,并将要创建的字符串作为构造参数:
String str2 = new String("Hello, Java");代码块1
如果调用 String 类的无参构造方法,则会创建一个空字符串:
String str3 = new String();代码块1
此处的str3就是一个空字符串。但注意,这种方式很少使用。
2. 获取字符串长度可以使用length()方法来获取字符串的长度。例如:
实例演示
public class StringMethod1 {public static void main(String[] args) {// 创建String对象strString str = "hello world!";// 调用对象下length()方法,并使用int类型变量接收返回结果int length = str.length();System.out.println("str的长度为:" + length);}}123456789
可查看在线运行效果
运行结果:
str1的长度为:12代码块1
注意,hello world!中的空格也算一个字符。
3. 字符串查找3.1 获取指定位置字符可以使用char charAt(int index)方法获取字符串指定位置的字符。它接收一个整型的index参数,指的是索引位置,那什么是索引位置呢?例如,有一字符串I love Java,其每个字符的索引如下图所示:
可以从图示中看出,索引下标从0开始。假如我们要获取字符J,则为方法传入参数7即可:
实例演示
public class StringMethod2 {public static void main(String[] args) {String str = "I love Java";char c = str.charAt(7);System.out.println("索引位置为7的字符为:" + c);}}1234567
可查看在线运行效果
运行结果:
3.2 查找字符串位置索引位置为7的字符为:J代码块1
这里介绍查找字符串位置的两个方法:
indexOf() 获取字符或子串在字符串中第一次出现的位置。
lasIndexOf() 获取字符或子串在字符串中最后一次出现的位置。
这里的子串指的就是字符串中的连续字符组成的子序列。例如,字符串Hello就是字符串Hello Java的子串。
indexOf()有多个重载方法,这里我们只演示其中最常用的两个。
获取字符在字符串中第一次出现的位置:
实例演示
public class StringMethod2 {public static void main(String[] args) {String str = "I love Java, I love imooc!";int i = str.indexOf('a');System.out.println("字符a在字符串str第一次出现的位置为:" + i);}}1234567
可查看在线运行效果
运行结果:
字符a在字符串str第一次出现的位置为:8代码块1
获取子串在字符串中第一次出现的位置:
实例演示
public class StringDemo2 {public static void main(String[] args) {String str = "I love Java, I love imooc!";int i = str.indexOf("love");System.out.println("子串love在字符串str第一次出现的位置为:" + i);}}1234567
可查看在线运行效果
运行结果:
子串love在字符串str第一次出现的位置为:2代码块1
关于lastIndexOf(),我们也只演示最常用的两个重载方法。
获取字符在字符串中最后一次出现的位置:
实例演示
public class StringMethod2 {public static void main(String[] args) {String str = "I love Java, I love imooc!";int i = str.lastIndexOf('e');System.out.println("字符e在字符串str最后一次出现的位置为:" + i);}}1234567
可查看在线运行效果
运行结果:
字符e在字符串str最后一次出现的位置为:18代码块1
获取子串在字符串中最后一次出现的位置:
实例演示
public class StringMethod2 {public static void main(String[] args) {String str = "I love Java, I love imooc!";int i = str.lastIndexOf("I love");System.out.println("字串I love在字符串str最后一次出现的位置为:" + i);}}1234567
可查看在线运行效果
运行结果:
字串I love在字符串str最后一次出现的位置为:13代码块1
需要特别注意的是,以上方法的参数都是区分大小写的。这也就意味着,你永远无法在I love Java中查找到字符E。如果没有查找,上述方法都会返回一个整型值:-1。我们来看以下示例:
实例演示
public class StringMethod2 {public static void main(String[] args) {String str = "I love Java";int i = str.indexOf('E');System.out.println(i);}}1234567
可查看在线运行效果
运行结果:
4. 字符串截取-1代码块1
字符串的截取也称为获取子串,在实际开发中经常用到,可以使用substring()方法来获取子串,String类中有两个重载的实例方法:
String substring(int beginIndex) 获取从beginIndex位置开始到结束的子串。
String substring(int beginIndex, int endIndex) 获取从beginIndex位置开始到endIndex位置的子串(不包含endIndex位置字符)。
关于这两个方法的使用,我们来看一个实例:
实例演示
public class StringMethod3 {public static void main(String[] args) {String str = "I love Java";String substring = str.substring(2);String substring1 = str.substring(2, 6);System.out.println("从索引位置2到结束的子串为:" + substring);System.out.println("从索引位置2到索引位置6的子串为:" + substring1);}}123456789
可查看在线运行效果
运行结果:
从索引位置2到结束的子串为:love Java从索引位置2到索引位置6的子串为:love代码块12
要特别注意,方法签名上有两个参数的substring(int beginIndex, int endIndex)方法,截取的子串不包含endIndex位置的字符。
5. 字符串切割5.1 切割为字串数组String[] split(String regex)方法可将字符串切割为子串,其参数regex是一个正则表达式分隔符,返回字符串数组。例如,我们使用空格作为分隔符来切割I love Java字符串,结果将返回含有3个元素的字符串数组:
实例演示
public class StringMethod4 {public static void main(String[] args) {String str1 = "I love Java";// 将字符串str1以空格分隔,并将分割结果赋值给strArr数组String[] strArr = str1.split(" ");// 遍历数组,打印每一个元素for (String str: strArr) {System.out.print(str + '\t');}}}12345678910111213
可查看在线运行效果
运行结果:
I love Java代码块1
注意,有几种特殊的分隔符:* ^ : | . \,要使用转义字符转义。例如:
// 以*切割String str2 = "I*love*Java";String[] strArr2 = str2.split("\\*");// 以\切割String str3 = "I\\love\\Java";String[] strArr4 = str3.split("\\\\");// 以|切割String str4 = "I|love|Java";String[] strArr4 = str4.split("\\|");代码块1234567891011
另外,还有一个重载方法String[] split(String regex, int limit),其第二个参数limit用以控制正则匹配被应用的次数,因此会影响结果的长度,此处不再一一举例介绍。
5.2 切割为 byte 数组在实际工作中,网络上的数据传输就是使用二进制字节数据。因此字符串和字节数组之间的相互转换也很常用。
我们可以使用getBytes()方法将字符串转换为byte数组。例如:
实例演示
public class StringMethod4 {public static void main(String[] args) {String str2 = "我喜欢Java";System.out.println("将字符串转换为byte数组:");// 将字符串转换为字节数组byte[] ascii = str2.getBytes();// 遍历字节数组,打印每个元素for (byte aByte : ascii) {System.out.print(aByte + "\t");}}}123456789101112
可查看在线运行效果
运行结果:
将字符串转换为byte数组:-26 -120 -111 -27 -106 -100 -26 -84 -94 74 97 118 97代码块12
将字节数组转换为字符串的方法很简单,直接实例化一个字符串对象,将字节数组作为构造方法的参数即可:
6. 字符串大小写转换// 此处的ascii为上面通过字符串转换的字节数组String s = new String(ascii);代码块12
字符串的大小写转换有两个方法:
toLowerCase() 将字符串转换为小写
toUpperCase() 将字符串转换为大写
我们来看一个实例:
实例演示
public class StringMethod5 {public static void main(String[] args) {String str = "HELLO world";String s = str.toLowerCase();System.out.println("字符串str为转换为小写后为:" + s);String s1 = s.toUpperCase();System.out.println("字符串s为转换为大写后为:" + s1);}}123456789
可查看在线运行效果
运行结果:
字符串str为转换为小写后为:hello world字符串s为转换为大写后为:HELLO WORLD代码块12
试想,如果想把字符串HELLO world中的大小写字母互换,该如何实现呢?
这里可以结合字符串切割方法以及字符串连接来实现:
实例演示
public class StringMethod5 {public static void main(String[] args) {String str = "HELLO world";// 先切割为数组String[] strArr = str.split(" ");// 将数组中元素转换大小写并连接为一个新的字符串String result = strArr[0].toLowerCase() + " " + strArr[1].toUpperCase();System.out.println("字符串str的大小写互换后为:" + result);}}12345678910
可查看在线运行效果
运行结果:
字符串str的大小写互换后为:hello WORLD代码块1
当然,实现方式不止一种,你可以结合所学写出更多的方式。
7. 字符串比较String类提供了boolean equals(Object object)方法来比较字符串内容是否相同,返回一个布尔类型的结果。
需要特别注意的是,在比较字符串内容是否相同时,必须使用equals()方法而不能使用==运算符。我们来看一个示例:
实例演示
public class StringMethod6 {public static void main(String[] args) {// 用两种方法创建三个内容相同的字符串String str1 = "hello";String str2 = "hello";String str3 = new String("hello");System.out.println("使用equals()方法比较str1和str2的结果为:" + str1.equals(str2));System.out.println("使用==运算符比较str1和str2的结果为:" + (str1 == str2));System.out.println("使用==运算符比较str1和str2的结果为:" + (str1 == str2));System.out.println("使用==运算符比较str1和str3的结果为:" + (str1 == str3));}}123456789101112
可查看在线运行效果
运行结果:
使用equals()方法比较str1和str2的结果为:true使用==运算符比较str1和str2的结果为:true使用equals()方法比较str1和str3的结果为:true使用==运算符比较str1和str3的结果为:false代码块1234
代码中三个字符串str1,str2和str3的内容都是hello,因此使用equals()方法对它们进行比较,其结果总是为true。
注意观察执行结果,其中使用==运算符比较str1和str2的结果为true,但使用==运算符比较的str1和str3的结果为false。这是因为==运算符比较的是两个变量的地址而不是内容。
要探究其原因,就要理解上述创建字符串的代码在计算机内存中是如何执行的。下面我们通过图解的形式来描述这三个变量是如何在内存中创建的。
当执行String str1 = "hello;"语句时,会在内存的栈空间中创建一个str1,在常量池中创建一个"hello",并将str1指向hello。
当执行String str2 = "hello";语句时,栈空间中会创建一个str2,由于其内容与str1相同,会指向常量池中的同一个对象。所以str1与str2指向的地址是相同的,这就是==运算符比较str1和str2的结果为true的原因。
当执行String str3 = new String("hello");语句时,使用了new关键字创建字符串对象,由于对象的实例化操作是在内存的堆空间进行的,此时会在栈空间创建一个str3,在堆空间实例化一个内容为hello的字符串对象,并将str3地址指向堆空间中的hello,这就是==运算符比较str1和str3的结果为false的原因。
StringBuilder上一节,我们学习了 Java 的 String 类,并介绍了其常用方法。本小节我们来介绍字符串的另外一个类:StringBuilder,我们将会了解到 StringBuilder 与 String 的差异,StringBuilder 的使用场景,也会介绍与 StringBuilder 类对应的 StringBuffer 类,StringBuilder 的使用方法以及其常用方法是本小节的重点学习内容。
1. StringBuilder 概述1.1 什么是 StringBuilder与 String 相似,StringBuilder 也是一个与字符串相关的类,Java 官方文档给 StringBuilder 的定义是:可变的字符序列。
1.2 为什么需要 StringBuilder在 Java 字符串的学习中,我们知道了字符串具有不可变性,当频繁操作字符串时候,会在常量池中产生很多无用的数据(回忆图示)。
而 StringBuilder 与 String 不同,它具有可变性。相较 String 类不会产生大量无用数据,性能上会大大提高。
因此对于需要频繁操作字符串的场景,建议使用 Stringbuilder 类来代替 String 类。
2. StringBuffer 概述2.1 定义了解了 StringBuilder 类 ,StringBuffer 也是不得不提的一个类,Java 官方文档给出的定义是:线程安全的可变字符序列。
2.2 与前者的区别StringBuffer 是 StringBuilder 的前身,在早期的 Java 版本中应用非常广泛,它是 StringBuilder 的线程安全版本(线程我们将在后面的小节中介绍),但实现线程安全的代价是执行效率的下降。
你可以对比 StringBuilder 和 StringBuffer 的接口文档,它们的接口基本上完全一致。为了提升我们代码的执行效率,在如今的实际开发中 StringBuffer 并不常用。因此本小节的重点在 StringBuilder 的学习。
3. StringBuilder 的常用方法3.1 构造方法StringBuilder 类提供了如下 4 个构造方法:
StringBuilder() 构造一个空字符串生成器,初始容量为 16 个字符;
StringBuilder(int catpacity) 构造一个空字符串生成器,初始容量由参数 capacity 指定;
StringBuilder(CharSequence seq) 构造一个字符串生成器,该生成器包含与指定的 CharSequence 相同的字符。;
StringBuilder(String str) 构造初始化为指定字符串内容的字符串生成器。
其中第 4 个构造方法最为常用,我们可以使用 StringBuilder 这样初始化一个内容为 hello 的字符串:
3.2 成员方法StringBuilder str = new StringBuilder("Hello");代码块1
StringBuilder 类下面也提供了很多与 String 类相似的成员方法,以方便我们对字符串进行操作。下面我们将举例介绍一些常用的成员方法。
3.2.1 字符串连接可以使用 StringBuilder 的 StringBuilder append(String str) 方法来实现字符串的连接操作。
我们知道,String 的连接操作是通过 + 操作符完成连接的:
String str1 = "Hello";String str2 = "World";String str3 = str1 + " " + str2;代码块123
如下是通过 StringBuilder 实现的字符串连接示例:
实例演示
public class ConnectString1 {public static void main(String[] args) {// 初始化一个内容为 Hello 的字符串生成器StringBuilder str = new StringBuilder("Hello");// 调用append()方法进行字符串的连接str.append(" ");str.append("World");System.out.println(str);}}12345678910
可查看在线运行效果
运行结果:
Hello World代码块1
由于 append() 方法返回的是一个 StringBuilder 类型,我们可以实现链式调用。例如,上述连续两个 append() 方法的调用语句,可以简化为一行语句:
str.append(" ").append("World");代码块1
如果你使用 IDE 编写如上连接字符串的代码,可能会有下面这样的提示(IntelliJ idea 的代码截图):
提示内容说可以将 StringBuilder 类型可以替换为 String 类型,也就是说可以将上边地代码改为:
String str = "Hello" + " " + "World";代码块1
这样写并不会导致执行效率的下降,这是因为 Java 编译器在编译和运行期间会自动将字符串连接操作转换为 StringBuilder 操作或者数组复制,间接地优化了由于 String 的不可变性引发的性能问题。
值得注意的是,append() 的重载方法有很多,可以实现各种类型的连接操作。例如我们可以连接 char 类型以及 float 类型,实例如下:
实例演示
public class ConnectString2 {public static void main(String[] args) {StringBuilder str = new StringBuilder("小明的身高为");str.append(':').append(172.5f);System.out.println(str);}}1234567
可查看在线运行效果
运行结果:
小明的身高为:172.5代码块1
上面代码里连续的两个 append() 方法分别调用的是重载方法 StringBuilder append(char c) 和 StringBuilder append(float f)。
3.2.2 获取容量可以使用 int capacity() 方法来获取当前容量,容量指定是可以存储的字符数(包含已写入字符),超过此数将进行自动分配。注意,容量与长度(length)不同,长度指的是已经写入字符的长度。
例如,构造方法 StringBuilder() 构造一个空字符串生成器,初始容量为 16 个字符。我们可以获取并打印它的容量,实例如下:
实例演示
public class GetCapacity {public static void main(String[] args) {// 调用StringBuilder的无参构造方法,生成一个str对象StringBuilder str = new StringBuilder();System.out.println("str的初始容量为:" + str.capacity());// 循环执行连接操作for (int i = 0; i 16; i ++) {str.append(i);}System.out.println("连接操作后,str的容量为" + str.capacity());}}可查看在线运行效果
运行结果:
3.2.3 字符串替换str的初始容量为:16连接操作后,str的容量为34代码块12
可以使用 StringBuilder replace(int start, int end, String str) 方法,来用指定字符串替换从索引位置 start 开始到 end 索引位置结束(不包含 end)的子串。实例如下:
实例演示
public class StringReplace {public static void main(String[] args) {// 初始化一个内容为 Hello 的字符串生成器StringBuilder str = new StringBuilder("Hello World!");// 调用字符串替换方法,将 World 替换为 Javastr.replace(6, 11, "Java");// 打印替换后的字符串System.out.println(str);}}12345678910
可查看在线运行效果
运行结果:
Hello Java!代码块1
也可使用 StringBuilder delete(int start, int end) 方法,先来删除索引位置 start 开始到 end 索引位置(不包含 end)的子串,再使用 StringBuilder insert(int offset, String str) 方法,将字符串插入到序列的 offset 索引位置。同样可以实现字符串的替换,例如:
3.2.4 字符串截取StringBuilder str = new StringBuilder("Hello World!");str.delete(6, 11);str.insert(6, "Java");代码块123
可以使用 StringBuilder substring(int start) 方法来进行字符串截取,例如,我们想截取字符串的后三个字符,实例如下:
实例演示
public class StringSub {public static void main(String[] args) {StringBuilder str = new StringBuilder("你好,欢迎来到夜猫编程");String substring = str.substring(7);System.out.println("str截取后子串为:" + substring);}}1234567
可查看在线运行效果
运行结果:
str截取后子串为:夜猫编程代码块1
如果我们想截取示例中的” 欢迎 “二字,可以使用重载方法 StringBuilder substring(int start, int end) 进行截取:
3.2.5 字符串反转String substring = str.substring(3, 5);代码块1
可以使用 StringBuildr reverse() 方法,对字符串进行反转操作,例如:
实例演示
public class StringReverse {public static void main(String[] args) {StringBuilder str = new StringBuilder("Hello Java");System.out.println("str经过反转操作后为:" + str.reverse());}}123456
可查看在线运行效果
运行结果:
Java Scanner 类str经过反转操作后为:avaJ olleH代码块1
一直以来,我们都使用System.out.println()方法向屏幕打印内容,那么如何接收输入的内容呢?本小节所学习的Scanner类就可以实现对输入内容的接收。在本小节,我们将学习Scanner类的定义,如何使用Scanner类以及其常用方法,在学完这些基础知识后,我们会在最后学习一个比较有趣的实例程序。
1. 定义Scanner是一个简单的文本扫描器,可以解析基础数据类型和字符串。
它位于java.util包下,因此如果要使用此类,必须使用import语句导入:
2. Scanner 对象创建import java.util.Scanner;代码块1
想要使用Scanner类就要了解如何创建对象,我们可以使用如下代码创建一个扫描器对象:
Scanner scanner = new Scanner(System.in);代码块1
构造方法的参数System.in表示允许用户从系统中读取内容。本小节,我们的示例代码中都将使用这个构造方法。
Tips:System.in是一个InputStream类型,Scanner类还有很多接收其他类型的构造方法。这里不详细介绍。
3. 常用方法3.1 next()及其同伴方法想要获取用户的输入,只有对象是不行的,还要配合它的实例方法。此时配合Scanner类中的next()方法及其同伴方法可以获取指定类型的输入。
3.1.1 next() 方法next()方法的返回值是字符串类型,可以使用此方法,将用户输入的内容扫描为字符串。我们来看一个示例,获取并打印用户输入的内容:
import java.util.Scanner;public class ScannerDemo1 {public static void main(String[] args) {// 创建扫描器对象Scanner scanner = new Scanner(System.in);System.out.println("请输入一段内容,输入回车结束:");// 可以将用户输入的内容扫描为字符串String str = scanner.next();// 打印输出System.out.println("您输入的内容为:" + str);// 关闭扫描器scanner.close();}}代码块123456789101112131415
在代码中我们注意到,在代码块的最后调用了close()方法,这个方法用于关闭当前扫描器,就和电脑的开关机一样,使用电脑前要开机,而当用不到的时候最好关掉。
编译执行代码,屏幕将会提示:
请输入一段内容,输入回车结束:代码块1
接下来我们按照提示输入内容,然后输入回车结束输入:
3.1.2 同伴方法那什么是同伴方法呢?这里的同伴方法指的是Scanner类中以next单词开头的方法。我们举例来看几个同伴方法及其作用:
nextLine() :返回输入回车之前的所有字符;
nextInt() :将输入内容扫描为int类型;
nextFloat() :将输入内容扫描为float类型。
这里的nextLine() 方法也可以获取字符串。我们来看一下它和next()方法的差异:
next()方法只有扫描到有效字符后才会结束输入;而nextLine()方法可以直接使用回车结束输入。
另外,next()方法会自动去掉空白(例如回车、空格等),也不能得到带有空格的字符串;nextLine()方法可以得到空白和带有空格的字符串。
我们再来看一个示例,获取用户输入的姓名、年龄和身高,并且打印输出:
import java.util.Scanner;public class ScannerDemo2 {public static void main(String[] args) {// 创建扫描器对象Scanner scanner = new Scanner(System.in);System.out.println("请输入您的姓名:");// 将第一行输入扫描为字符串String name = scanner.nextLine();System.out.println("请输入您的年龄:");// 将第二行输入扫描为int类型int age = scanner.nextInt();System.out.println("请输入您的身高:");// 将第三行输入扫描为float类型float height = scanner.nextFloat();// 打印扫描器所扫描的值System.out.println("您的姓名为:" + name);System.out.println("您的年龄为:" + age);System.out.println("您的身高为:" + height);// 关闭扫描器scanner.close();}}代码块1234567891011121314151617181920212223242526
编译执行代码,按照提示输入对应内容,直到程序完整运行:
请输入您的姓名:三井 寿请输入您的年龄:19请输入您的身高:183您的姓名为:三井 寿您的年龄为:19您的身高为:183代码块123456789
Tips:上面代码中,如果使用next()方法代替nextLine()方法来获取姓名字符串,是无法得到我们输入的“三井 寿”这个字符串的,这是因为next()方法不能获取带有空格的字符串。
要特别注意的是:Scanner 类读到的内容,只与输入顺序有关,和终端上显示的顺序无关,因此类似于下面的这种输入,是读不到空格的,执行代码的流程如下:
3.2 hasNext()及其同伴方法hasNext()方法的返回值是一个布尔类型,如果输入中包含数据的输入,则返回true。否则返回false。通常用来做输入内容的验证。
它的同伴方法是以hasNext单词开头的方法,诸如hasNextLine()、hasNextInt()等方法。例如,上面的代码中,我们可以对应加入hasNext同伴方法结合条件判断语句,来提升代码的稳定性:
4. 实例int age;if (scanner.hasNextInt()) {age = scanner.nextInt();} else {System.out.println("不是int类型");}float height;if (scanner.hasNextFloat()) {height = scanner.nextFloat();} else {System.out.println("不是float类型");}代码块12345678910111213
前面我们已经对Scanner类的基本用法有了一定的了解,下面我们来实现一个示例程序,这个程序用于估算一个人的体脂率,这里事先给出体脂率估算公式:
参数a = 腰围(cm)×0.74参数b = 体重(kg)× 0.082 + 44.74脂肪重量(kg)= a - b体脂率 =(脂肪重量 ÷ 体重)× 100%。代码块1234
从公式中我们可以看出,想要得到最终的体脂率,参数a(腰围)和参数 b(体重)是需要用户手动输入的,公式部分只需要使用算数运算符实现即可。下面是程序代码:
import java.util.Scanner;public class GetBodyFat {public static void main(String[] args) {// 初始化腰围float waistline = 0f;// 初始化体重float weight = 0f;// 声明浮点型参数a,b,bodyFatWeight(脂肪重量)float a, b, bodyFatWeight;Scanner scanner = new Scanner(System.in);System.out.println("请输入您的腰围(cm):");if (scanner.hasNextFloat()) {waistline = scanner.nextFloat();}System.out.println("请输入您的体重(kg):");if (scanner.hasNextFloat()) {weight = scanner.nextFloat();}// 计算参数a 公式:参数a = 腰围(cm)× 0.74a = waistline * 0.74f;// 计算参数b 公式:参数b = 体重(kg)× 0.082 + 44.74b = weight * 0.082f + 44.74f;// 计算脂肪重量bodyFatWeight = a - b;// 计算体脂率 =(脂肪重量 ÷ 体重)×100%。float result = bodyFatWeight / weight * 100;System.out.println("您的体脂率为" + result + "%");}}代码块123456789101112131415161718192021222324252627282930
编译运行代码,按照提示输入,将估算出你的体脂含量:
请输入您的腰围(cm):70请输入您的体重(kg):50您的体脂率为5.919998%代码块12345
执行代码的流程如下:
Java 异常处理Java 的异常处理是 Java 语言的一大重要特性,也是提高代码健壮性的最强大方法之一。当我们编写了错误的代码时,编译器在编译期间可能会抛出异常,有时候即使编译正常,在运行代码的时候也可能会抛出异常。本小节我们将介绍什么是异常、Java 中异常类的架构、如何进行异常处理、如何自定义异常、什么是异常链、如何使用异常链等内容。
1. 什么是异常异常就是程序上的错误,我们在编写程序的时候经常会产生错误,这些错误划分为编译期间的错误和运行期间的错误。
下面我们来看几个常见的异常案例。
如果语句漏写分号,程序在编译期间就会抛出异常,实例如下:
public class Hello {public static void main(String[] args) {System.out.println("Hello World!")}}代码块12345
运行结果:
$$ javac Hello.javaHello.java:3: 错误: 需要';'System.out.println("Hello World!")^1 个错误代码块12345
运行过程:
由于代码的第 3 行语句漏写了分号,Java 编译器给出了明确的提示。
static 关键字写成了 statci,实例如下:
Hello.java:2: 错误: 需要 标识符public statci void main(String[] args) {^1 个错误代码块1234
当数组下标越界,程序在编译阶段不会发生错误,但在运行时会抛出异常。实例如下:
public class ArrayOutOfIndex {public static void main(String[] args) {int[] arr = {1, 2, 3};System.out.println(arr[3]);}}代码块123456
运行结果:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3at ArrayOutOfIndex.main(ArrayOutOfIndex.java:4)代码块12
运行过程:
2. Java 异常类架构在 Java 中,通过 Throwable 及其子类来描述各种不同类型的异常。如下是 Java 异常类的架构图(不是全部,只展示部分类):
2.1 Throwable 类Throwable 位于 java.lang 包下,它是 Java 语言中所有错误(Error)和异常(Exception)的父类。
Throwable 包含了其线程创建时线程执行堆栈的快照,它提供了 printStackTrace() 等接口用于获取堆栈跟踪数据等信息。
主要方法:
fillInStackTrace: 用当前的调用栈层次填充 Throwable 对象栈层次,添加到栈层次任何先前信息中;
getMessage:返回关于发生的异常的详细信息。这个消息在 Throwable 类的构造函数中初始化了;
getCause:返回一个 Throwable 对象代表异常原因;
getStackTrace:返回一个包含堆栈层次的数组。下标为 0 的元素代表栈顶,最后一个元素代表方法调用堆栈的栈底;
printStackTrace:打印 toString() 结果和栈层次到 System.err,即错误输出流。
2.2 Error 类Error 是 Throwable 的一个直接子类,它可以指示合理的应用程序不应该尝试捕获的严重问题。这些错误在应用程序的控制和处理能力之外,编译器不会检查 Error,对于设计合理的应用程序来说,即使发生了错误,本质上也无法通过异常处理来解决其所引起的异常状况。
常见 Error:
AssertionError:断言错误;
VirtualMachineError:虚拟机错误;
UnsupportedClassVersionError:Java 类版本错误;
OutOfMemoryError :内存溢出错误。
2.3 Exception 类Exception 是 Throwable 的一个直接子类。它指示合理的应用程序可能希望捕获的条件。
Exception 又包括 Unchecked Exception(非检查异常)和 Checked Exception(检查异常)两大类别。
2.3.1 Unchecked Exception (非检查异常)Unchecked Exception 是编译器不要求强制处理的异常,包含 RuntimeException 以及它的相关子类。我们编写代码时即使不去处理此类异常,程序还是会编译通过。
常见非检查异常:
NullPointerException:空指针异常;
ArithmeticException:算数异常;
ArrayIndexOutOfBoundsException:数组下标越界异常;
ClassCastException:类型转换异常。
2.3.2 Checked Exception(检查异常)Checked Exception 是编译器要求必须处理的异常,除了 RuntimeException 以及它的子类,都是 Checked Exception 异常。我们在程序编写时就必须处理此类异常,否则程序无法编译通过。
常见检查异常:
IOException:IO 异常
SQLException:SQL 异常
3. 如何进行异常处理在 Java 语言中,异常处理机制可以分为两部分:
抛出异常:当一个方法发生错误时,会创建一个异常对象,并交给运行时系统处理;
捕获异常:在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器。
Java 通过 5 个关键字来实现异常处理,分别是:throw、throws、try、catch、finally。
异常总是先抛出,后捕获的。下面我们将围绕着 5 个关键字来详细讲解如何抛出异常以及如何捕获异常。
4. 抛出异常4.1 实例我们先来看一个除零异常的实例代码:
public class ExceptionDemo1 {// 打印 a / b 的结果public static void divide(int a, int b) {System.out.println(a / b);}public static void main(String[] args) {// 调用 divide() 方法divide(2, 0);}}代码块1234567891011
运行结果:
Exception in thread "main" java.lang.ArithmeticException: / by zeroat ExceptionDemo1.divide(ExceptionDemo1.java:4)at ExceptionDemo1.main(ExceptionDemo1.java:9)代码块123
运行过程:
我们知道 0 是不能用作除数的,由于 divide() 方法中除数 b 为 0,所以代码将停止执行并显示了相关的异常信息,此信息为堆栈跟踪,上面的运行结果告诉我们:main 线程发生了类型为 ArithmeticException 的异常,显示消息为 by zero,并且提示了可能发生异常的方法和行号。
4.2 throw上面的实例中,程序在运行时引发了错误,那么如何来显示抛出(创建)异常呢?
我们可以使用 throw 关键字来抛出异常,throw 关键字后面跟异常对象,改写上面的实例代码:
public class ExceptionDemo2 {// 打印 a / b 的结果public static void divide(int a, int b) {if (b == 0) {// 抛出异常throw new ArithmeticException("除数不能为零");}System.out.println(a / b);}public static void main(String[] args) {// 调用 divide() 方法divide(2, 0);}}代码块123456789101112131415
运行结果:
Exception in thread "main" java.lang.ArithmeticException: 除数不能为零at ExceptionDemo2.divide(ExceptionDemo2.java:5)at ExceptionDemo2.main(ExceptionDemo2.java:12)代码块123
运行过程:
代码在运行时同样引发了错误,但显示消息为 “除数不能为零”。我们看到 divide() 方法中加入了条件判断,如果调用者将参数 b 设置为 0 时,会使用 throw 关键字来抛出异常,throw 后面跟了一个使用 new 关键字实例化的算数异常对象,并且将消息字符串作为参数传递给了算数异常的构造函数。
我们可以使用 throw 关键字抛出任何类型的 Throwable 对象,它会中断方法,throw 语句之后的所有内容都不会执行。除非已经处理抛出的异常。异常对象不是从方法中返回的,而是从方法中抛出的。
4.3 throws可以通过 throws 关键字声明方法要抛出何种类型的异常。如果一个方法可能会出现异常,但是没有能力处理这种异常,可以在方法声明处使用 throws 关键字来声明要抛出的异常。例如,汽车在运行时可能会出现故障,汽车本身没办法处理这个故障,那就让开车的人来处理。
throws 用在方法定义时声明该方法要抛出的异常类型,如下是伪代码:
public void demoMethod() throws Exception1, Exception2, ... ExceptionN {// 可能产生异常的代码}代码块123
throws 后面跟的异常类型列表可以有一个也可以有多个,多个则以 , 分割。当方法产生异常列表中的异常时,将把异常抛向方法的调用方,由调用方处理。
throws 有如下使用规则:
如果方法中全部是非检查异常(即 Error、RuntimeException 以及的子类),那么可以不使用 throws 关键字来声明要抛出的异常,编译器能够通过编译,但在运行时会被系统抛出;
如果方法中可能出现检查异常,就必须使用 throws 声明将其抛出或使用 try catch 捕获异常,否则将导致编译错误;
当一个方法抛出了异常,那么该方法的调用者必须处理或者重新抛出该异常;
当子类重写父类抛出异常的方法时,声明的异常必须是父类所声明异常的同类或子类。
5. 捕获异常使用 try 和 catch 关键字可以捕获异常。try catch 代码块放在异常可能发生的地方。它的语法如下:
try {// 可能会发生异常的代码块} catch (Exception e1) {// 捕获并处理try抛出的异常类型Exception} catch (Exception2 e2) {// 捕获并处理try抛出的异常类型Exception2} finally {// 无论是否发生异常,都将执行的代码块}代码块123456789
我们来看一下上面语法中的 3 种语句块:
try 语句块:用于监听异常,当发生异常时,异常就会被抛出;
catch 语句块:catch 语句包含要捕获的异常类型的声明,当 try 语句块发生异常时,catch 语句块就会被检查。当 catch 块尝试捕获异常时,是按照 catch 块的声明顺序从上往下寻找的,一旦匹配,就不会再向下执行。因此,如果同一个 try 块下的多个 catch 异常类型有父子关系,应该将子类异常放在前面,父类异常放在后面;
finally 语句块:无论是否发生异常,都会执行 finally 语句块。finally 常用于这样的场景:由于 finally 语句块总是会被执行,所以那些在 try 代码块中打开的,并且必须回收的物理资源(如数据库连接、网络连接和文件),一般会放在 finally 语句块中释放资源。
try 语句块后可以接零个或多个 catch 语句块,如果没有 catch 块,则必须跟一个 finally 语句块。简单来说,try 不允许单独使用,必须和 catch 或 finally 组合使用,catch 和 finally 也不能单独使用。
实例如下:
public class ExceptionDemo3 {// 打印 a / b 的结果public static void divide(int a, int b) {System.out.println(a / b);}public static void main(String[] args) {try {// try 语句块// 调用 divide() 方法divide(2, 0);} catch (ArithmeticException e) {// catch 语句块System.out.println("catch: 发生了算数异常:" + e);} finally {// finally 语句块System.out.println("finally: 无论是否发生异常,都会执行");}}}代码块1234567891011121314151617181920
运行结果:
catch: 发生了算数异常:java.lang.ArithmeticException: / by zerofinally: 无论是否发生异常,都会执行代码块12
运行过程:
divide() 方法中除数 b 为 0,会发生除零异常,我们在方法调用处使用了 try 语句块对异常进行捕获;如果捕获到了异常, catch 语句块会对 ArithmeticException 类型的异常进行处理,此处打印了一行自定义的提示语句;最后的 finally 语句块,无论发生异常与否,总会执行。
Java 7 以后,catch 多种异常时,也可以像下面这样简化代码:
6. 自定义异常try {// 可能会发生异常的代码块} catch (Exception | Exception2 e) {// 捕获并处理try抛出的异常类型} finally {// 无论是否发生异常,都将执行的代码块}代码块1234567
自定义异常,就是定义一个类,去继承 Throwable 类或者它的子类。
Java 内置了丰富的异常类,通常使用这些内置异常类,就可以描述我们在编码时出现的大部分异常情况。一旦内置异常无法满足我们的业务要求,就可以通过自定义异常描述特定业务产生的异常类型。
实例:
public class ExceptionDemo4 {static class MyCustomException extends RuntimeException {/*** 无参构造方法*/public MyCustomException() {super("我的自定义异常");}}public static void main(String[] args) {// 直接抛出异常throw new MyCustomException();}}代码块12345678910111213141516
运行结果:
Exception in thread "main" ExceptionDemo4$$MyCustomException: 我的自定义异常at ExceptionDemo4.main(ExceptionDemo4.java:13)代码块12
运行过程:
在代码中写了一个自定义异常 MyCustomException,继承自 RuntimeException,它是一个静态内部类,这样在主方法中就可以直接抛出这个异常类了。当然,也可以使用 catch 来捕获此类型异常。
7. 异常链异常链是以一个异常对象为参数构造新的异常对象,新的异常对象将包含先前异常的信息。简单来说,就是将异常信息从底层传递给上层,逐层抛出,我们来看一个实例:
public class ExceptionDemo5 {/*** 第一个自定义的静态内部异常类*/static class FirstCustomException extends Exception {// 无参构造方法public FirstCustomException() {super("第一个异常");}}/*** 第二个自定义的静态内部异常类*/static class SecondCustomException extends Exception {public SecondCustomException() {super("第二个异常");}}/*** 第三个自定义的静态内部异常类*/static class ThirdCustomException extends Exception {public ThirdCustomException() {super("第三个异常");}}/*** 测试异常链静态方法1,直接抛出第一个自定义的静态内部异常类* @throws FirstCustomException*/public static void f1() throws FirstCustomException {throw new FirstCustomException();}/*** 测试异常链静态方法2,调用f1()方法,并抛出第二个自定义的静态内部异常类* @throws SecondCustomException*/public static void f2() throws SecondCustomException {try {f1();} catch (FirstCustomException e) {throw new SecondCustomException();}}/*** 测试异常链静态方法3,调用f2()方法, 并抛出第三个自定义的静态内部异常类* @throws ThirdCustomException*/public static void f3() throws ThirdCustomException {try {f2();} catch (SecondCustomException e) {throw new ThirdCustomException();}}public static void main(String[] args) throws ThirdCustomException {// 调用静态方法f3()f3();}}代码块12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
运行结果:
Exception in thread "main" ExceptionDemo5$$ThirdCustomException: 第三个异常at ExceptionDemo5.f3(ExceptionDemo5.java:46)at ExceptionDemo5.main(ExceptionDemo5.java:51)代码块123
运行过程:
通过运行结果,我们只获取到了静态方法 f3() 所抛出的异常堆栈信息,前面代码所抛出的异常并没有被显示。
我们改写上面的代码,让异常信息以链条的方式 “连接” 起来。可以通过改写自定义异常的构造方法,来获取到之前异常的信息。实例如下:
/*** @author colorful@TaleLin*/public class ExceptionDemo6 {/*** 第一个自定义的静态内部异常类*/static class FirstCustomException extends Exception {// 无参构造方法public FirstCustomException() {super("第一个异常");}}/*** 第二个自定义的静态内部异常类*/static class SecondCustomException extends Exception {/*** 通过构造方法获取之前异常的信息* @param cause 捕获到的异常对象*/public SecondCustomException(Throwable cause) {super("第二个异常", cause);}}/*** 第三个自定义的静态内部异常类*/static class ThirdCustomException extends Exception {/*** 通过构造方法获取之前异常的信息* @param ca(户籍所在地怎么填写?户籍所在地是指我国居民户口簿登记所在地,一般是指出生时其父母户口登记地方。按照户口登记管理条例,公民填写户籍所在地,应该填写到户籍管理机关所在地,即城市户口的应该填**省**市(县)**区;农村户口的应该填**省**县**乡。一般在填写户籍所在地时,只填写到县就可以了。)use 捕获到的异常对象*/public ThirdCustomException(Throwable cause) {super("第三个异常", cause);}}/*** 测试异常链静态方法1,直接抛出第一个自定义的静态内部异常类* @throws FirstCustomException*/public static void f1() throws FirstCustomException {throw new FirstCustomException();}/*** 测试异常链静态方法2,调用f1()方法,并抛出第二个自定义的静态内部异常类* @throws SecondCustomException*/public static void f2() throws SecondCustomException {try {f1();} catch (FirstCustomException e) {throw new SecondCustomException(e);}}/*** 测试异常链静态方法3,调用f2()方法, 并抛出第三个自定义的静态内部异常类* @throws ThirdCustomException*/public static void f3() throws ThirdCustomException {try {f2();} catch (SecondCustomException e) {throw new ThirdCustomException(e);}}public static void main(String[] args) throws ThirdCustomException {// 调用静态方法f3()f3();}}代码块12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
运行结果:
Exception in thread "main" ExceptionDemo6$$ThirdCustomException: 第三个异常at ExceptionDemo6.f3(ExceptionDemo6.java:74)at ExceptionDemo6.main(ExceptionDemo6.java:80)Caused by: ExceptionDemo6$$SecondCustomException: 第二个异常at ExceptionDemo6.f2(ExceptionDemo6.java:62)at ExceptionDemo6.f3(ExceptionDemo6.java:72)... 1 moreCaused by: ExceptionDemo6$$FirstCustomException: 第一个异常at ExceptionDemo6.f1(ExceptionDemo6.java:51)at ExceptionDemo6.f2(ExceptionDemo6.java:60)... 2 more代码块1234567891011
运行过程:
通过运行结果,我们看到,异常发生的整个过程都打印到了屏幕上,这就是一个异常链。
1、通过本小节的学习,我们知道了异常就是程序上的错误,良好的异常处理可以提高代码的健壮性。Java 语言中所有错误(Error)和异常(Exception)的父类都是 Throwable。Error 和 Exception 是 Throwable 的直接子类,我们通常说的异常处理实际上就是处理 Exception 及其子类,异常又分为检查型异常和非检查型异常。通过抛出异常和捕获异常来实现异常处理。我们亦可以通过继承 Throwable 类或者它的子类来自定义异常类。通过构造方法获取之前异常的信息可以实现异常链。
2、本小节我们学习了 Java 的 Scanner类,它是位于java.util包下的一个工具类,我们知道了它是一个简单的文本扫描器,可以解析基础数据类型和字符串。我们也学会了如何使用Scanner类来获取用户的输入,next()方法和nextLine()方法都可以扫描用户输入的字符串,要注意这两个方法的区别。我们也在最后给出了一个计算体脂率的示例代码,学习了Scanner类,你就可以实现比较有意思的一些小程序了。如果你想了解更多有关Scanner类的接口,也可翻阅官方文档。
3、本小节我们介绍了 Java 的 StringBuilder 类,它具有可变性,对于频繁操作字符串的场景,使用它来代替 String 类可以提高程序的执行效率;也知道了 StringBuffer 是 StringBuilder 的线程安全版本,官方更推荐使用 StringBuilder;最后我们介绍了 StringBuilder 的常用构造方法和成员方法,如果你想了解更多关于 StringBuilder 的接口,可以翻阅官方文档进行学习。
4、本小节我们介绍了 Java String类的常用方法:
使用length()方法可以获取字符串长度;
使用charAt()、indexOf()以及lastIndexOf()方法可以对字符串进行查找;
substring()方法可以对字符串的进行截取,split()、getBytes()方法可以将字符串切割为数组;
toLowerCase()和toUpperCase()方法分别用于大小写转换,使用equals()方法对字符串进行比较,这里要注意,对字符串内容进行比较时,永远都不要使用==运算符。
这些方法大多有重载方法,实际工作中,要根据合适的场景选用对应的重载方法。
当然,本小节还有很多未介绍到的方法,使用到可以翻阅官网文档来进行学习。
本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。