异常
1. 异常的基本概念
观察以下代码:
public class ExceptionTest01 {
public static void main(String[] args) {
int a = 100;
int b = 0;
int c = a / b;//java.lang.ArithmeticException: / by zero
System.out.println(c);
}
}
显然,b=0作为除数时,系统抛出了算术异常。即程序执行时控制台打印了
Exception in thread "main" java.lang.ArithmeticException: / by zero
at ExceptionTest01.main(ExceptionTest01.java:6)
这个信息叫做异常信息,是JVM负责打印输出的!
那么,Java作为一门完善的语言,为什么要提供这种异常机制呢?异常机制有什么用?
实际上,上面的代码片段在执行过程中,出现了不正常状况,这种非正常情况被称为:异常。程序出现异常不进行处理的话,JVM会终止程序;所以,在JAVA中提供了异常机制,JVM把异常信息打印在控制台上,供程序员参考。程序员看到异常信息后,可以对程序进行修改,让程序更加健壮!
比如,对于上面的程序,进行如下改进:
public class ExceptionTest01 {
public static void main(String[] args) {
int a = 100;
int b = 0;
/*int c = a / b;//java.lang.ArithmeticException: / by zero
System.out.println(c);*/
//我观察到异常信息之后,对程序进行修改,更加健壮。
if(b == 0){
System.out.println("除数不能为0");
}else{
int c = a / b;
System.out.println(c);
}
}
}
总结:
什么是异常?程序执行过程中的不正常情况。
异常的作用?提高程序的健壮性。
2.异常的本质?异常以类和对象的方式存在!
实际上,在点击上面的异常 java.lang.ArithmeticException后,发现实际上他是一个类:
package java.lang;
public class ArithmeticException extends RuntimeException {
@java.io.Serial
private static final long serialVersionUID = 2256477558314496007L;
public ArithmeticException() {
super();
}
public ArithmeticException(String s) {
super(s);
}
}
实际上,JVM发现程序中存在异常时,会在底层new一个对应的异常对象:
new ArithmeticException("/ by zero");//构造方法 public ArithmeticException(String s){}
接着,JVM将new的异常对象抛出,打印输出到控制台上。
下面的代码直接new一个异常对象并输出:
public class ExceptionTest01 {
public static void main(String[] args) {
//执行结果:java.lang.ArithmeticException: / by 0
System.out.println(new ArithmeticException("/ by 0"));
}
}
综上:JAVA中异常以类的形式存在,每一个异常类都可以创建异常对象!
举几个例子:通过异常类创建异常对象
public class ExceptionTest02 {
public static void main(String[] args) {
NullPointerException n = new NullPointerException("空指针异常");
System.out.println(n);//java.lang.NullPointerException: 空指针异常
NumberFormatException nfe = new NumberFormatException("数字格式化异常");
System.out.println(nfe);//java.lang.NumberFormatException: 数字格式化异常
}
}
3.异常的继承结构
异常以类的形式存在,有关异常的继承结构主要是:
可以看到异常和错误的父类都是java.lang.Throwable,表示Error和Exception都是可以抛出的。但是,在JAVA中,Error一旦发生,JVM只能终止程序,退出 JVM。错误是不能处理的!
异常则分为编译时异常和运行时异常。但是,不论是什么异常,异常都是发生在程序的运行阶段!
那为什么有编译时异常和运行时异常呢?
因为异常如果不进行区分,那么程序员将花费大量精力来处理异常,代码也会很复杂!
所谓编译时异常,是指此类异常在编译阶段需要对可能发生的异常进行预处理,否则编译器会报错。
而运行时异常不需要在编译阶段显式处理,如第一个程序中的ArithmeticException: / by zero异常,JAVA并没有要求一定要对该异常进行预处理。
一般的,编译时异常的发生概率比较高,运行时异常发生的概率比较低。并且运行时异常一般是程序的逻辑出现了问题。
4.编译时异常和运行时异常
- 所有的异常的是发生在程序的运行阶段。
- 运行时异常又叫不检查异常(UnCheckedException),在编写代码阶段不进行处理,编译器不会报错。举例如下:
代码:ArithmeticException异常就是一个运行时异常。
public class ExceptionTest03 {
public static void main(String[] args) {
//程序执行到此处时发生了ArithmeticException异常,实际上底层new了一个ArithmeticException对象
//并抛出了该异常,由于是在main方法中调用了100 / 0,所以ArithmeticException抛给了main方法。
//在main方法中没有对异常进行处理,继续自动上抛给JVM,JVM只能终止程序的运行!
System.out.println(100 / 0);//java.lang.ArithmeticException: / by zero
//没有输出hello
System.out.println("hello");//上面出现了异常,这里并没有执行
}
}
- 编译时异常又叫检查异常(CheckedException),在代码编写阶段,对可能出现的编译时异常不进行预处理的话,编译器会报错!举例如下:
public class ExceptionTest04 {
public static void main(String[] args) {
/*
ClassNotFoundException异常属于编译时异常,必须在编写阶段进行预处理。
如果不处理,编译器就会报错:Unhandled exception: java.lang.ClassNotFoundException
doSome();//Unhandled exception: java.lang.ClassNotFoundException
*/
}
/**
* doSome方法在方法声明时使用了: throws ClassNotFoundException
* 这表示,doSome方法在执行过程中,有可能会出现ClassNotFoundException异常
* ClassNotFoundException的父类是Exception,这表示ClassNotFoundException是编译时异常
* @throws ClassNotFoundException
*/
public static void doSome() throws ClassNotFoundException{
System.out.println("hello");
}
}
那么,出现异常不进行处理的话,异常最终会由JVM接收,JVM会终止程序并退出JVM,程序无法继续执行。所以针对出现的异常,我们需要进行处理,提高程序的健壮性!
5.异常的两种处理方式
一般异常的处理多用于编译时异常,运行时异常可以处理,也可以不处理。编译时异常必须处理!
- 1.在方法声明中使用throws关键字,将方法中出现的异常上抛给调用者。
- 即谁调用我,我就抛给谁。抛给上一级。
public class ExceptionTest05 {
// 第一种处理方式:在方法声明的位置上继续使用:throws,来完成异常的继续上抛。抛给调用者。
// 上抛类似于推卸责任。(继续把异常传递给调用者。)
public static void main(String[] args) throws ClassNotFoundException{
doSome();//doSome
}
public static void doSome() throws ClassNotFoundException{
System.out.println("doSome");
}
}
注意:异常的上抛将异常抛给调用者,对于调用者而言,调用者仍需要对异常进行处理,同样有两种处理方式。否则发现编译时异常编译器会报错。
注意:Java中异常发生之后如果一直上抛,最终抛给了main方法,main方法继续向上抛,抛给了调用者JVM,JVM知道这个异常一旦发生,只有一个结果。终止java程序的执行。
- 2.使用try…catch语句对出现的异常进行捕捉并处理。
对异常捕捉处理后相等于消除了程序的异常,调用者并不知道发生了异常!
使用try…catch对异常进行捕捉:
public class ExceptionTest05 {
/* public static void main(String[] args) throws ClassNotFoundException{
doSome();//doSome
}*/
//
public static void main(String[] args) {
try {
doSome();
}catch (ClassNotFoundException e){
e.printStackTrace();
}
}
public static void doSome() throws ClassNotFoundException{
System.out.println("doSome");
}
}
举个例子:
我是某集团的一个销售员,因为我的失误,导致公司损失了1000元,
“损失1000元”这可以看做是一个异常发生了。我有两种处理方式,
第一种方式:我把这件事告诉我的领导【异常上抛】
第二种方式:我自己掏腰包把这个钱补上。【异常的捕捉】
张三 --> 李四 ---> 王五 --> CEO
实际开发中,选择throws上抛还是选择try…catch捕捉异常?
如果希望调用者来处理,选择throws上报。
其它情况使用捕捉的方式。
5.1 异常上抛和捕捉的联合使用
1. throws关键字学习
- throws是将异常上抛,不在本方法中进行处理。
- throws关键字在方法声明中使用,一次可以抛出多个异常。但是选择异常上抛时抛出的异常必须包括在本方法中可能发生的编译时异常,否则还是会编译报错:Unhanded exception: java.lang.xxxxException
public static void doSome() throws IOException,ClassCastException{
//...
}
- 使用throws抛出异常时,可以抛出当前异常的父类异常,比如下面的代码执行中会出现FileNotFoundException异常,但是我们可以使用throws关键字上抛IOException异常。
public static void m3() throws IOException {
//编译报错的原因是什么?
// 第一:这里调用了一个构造方法:FileInputStream(String name)
// 第二:这个构造方法的声明位置上有:throws FileNotFoundException
// 第三:通过类的继承结构看到:FileNotFoundException父类是IOException,IOException的父类是Exception,
System.out.println("m3 begin!");
new FileInputStream("C:\\Users\\Administrator\\Desktop\\垫付说明.doc");
System.out.println("m3 over!");
}
注意:一般不建议在main方法声明中抛出异常。因为如果main方法中真的发生了该异常,抛出的异常会由JVM接收,JVM只能终止程序的运行。而异常机制的存在时为了提高程序的健壮性,所以,一般main方法中可能发生的异常选择try…catch进行捕捉。这样main方法就不会继续上抛异常了。
综上,代码如下:
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
public class ExceptionTest06 {
public static void main(String[] args) {
System.out.println("main begin!");
try {
//try表示尝试执行try语句块中的代码,一旦发生异常将由catch匹配并捕捉,进而在catch语句块中进行处理
m1();
}catch (FileNotFoundException e){ // catch后面的好像一个方法的形参。
// 这个分支中可以使用e引用,e引用保存的内存地址是那个new出来异常对象的内存地址。
// catch是捕捉异常之后走的分支。
// 在catch分支中干什么?处理异常。
System.out.println("文件路径错误!");
System.out.println(e);
}
System.out.println("main over!");
}
public static void m1() throws FileNotFoundException{
System.out.println("m1 begin!");
m2();
System.out.println("m1 over!");
}
//在抛出异常时,可以选择抛出精确的异常,也可以选择抛出当前异常的父类异常,因为异常以类的形式存在
//public static void m2() throws IOException
//public static void m2() throws Exception
//上面两个声明都正确
public static void m2() throws FileNotFoundException{
//m3()方法将异常上抛后,m2()作为调用者,仍然需要处理该异常,
System.out.println("m2 begin");
// 编译器报错原因是:m3()方法声明位置上有:throws FileNotFoundException
// 我们在这里调用m3()没有对异常进行预处理,所以编译报错。
// m3();
m3();
System.out.println("m2 over!");
}
public static void m3() throws FileNotFoundException {
// 调用SUN jdk中某个类的构造方法。
// 这个类FileInputStream还没有接触过,后期IO流的时候就知道了。
// 我们只是借助这个类学习一下异常处理机制。
// 创建一个输入流对象,该流指向一个文件。
//FileInputStream的构造方法会抛出异常FileNotFoundException extends IOException extends Exception
//显然FileNotFoundException是编译时异常,需要进行异常预处理
//编译报错的原因是什么?
// 第一:这里调用了一个构造方法:FileInputStream(String name)
// 第二:这个构造方法的声明位置上有:throws FileNotFoundException
// 第三:通过类的继承结构看到:FileNotFoundException父类是IOException,IOException的父类是Exception,
// 最终得知,FileNotFoundException是编译时异常。
//
// 错误原因?编译时异常要求程序员编写程序阶段必须对它进行处理,不处理编译器就报错。
// */
//new FileInputStream("C:\\Users\\Administrator\\Desktop\\垫付说明.doc");
//采用异常上抛来处理该异常,在方法的声明中使用throws关键字
System.out.println("m3 begin!");
new FileInputStream("C:\\Users\\Administrator\\Desktop\\垫付说明.doc");
System.out.println("m3 over!");
}
}
//执行结果:
/*
main begin!
m1 begin!
m2 begin
m3 begin!
m3 over!
m2 over!
m1 over!
main over!
*/
上述代码中并没有发生异常,现在更改一个错误的文件路径,这样m3会上抛异常给m2,m2又会上抛给m1…一直到在Main方法中捕获此时执行结果如下:
/*
main begin!
m1 begin!
m2 begin
m3 begin!
文件路径错误!
java.io.FileNotFoundException: C:\Users\Administrator\Desktop\垫说明.doc (系统找不到指定的文件。)
main over!
*/
5.2.程序中出现异常后的代码执行顺序?哪里执行?哪里不执行?
根据前面的知识,我们知道程序中出现异常时,会影响代码的执行效果。现在,观察上面的代码执行结果,我们可以知道:
- 发生异常后,只要选择上抛,该语句后的代码将不再执行。
- try语句块中,一旦某一行发生了异常,该行之后的代码将不再执行,并由catch捕捉异常,转至catch语句块中执行相关处理。
- try…catch捕捉完异常后,try…catch在同方法中的后续代码将依次执行。
5.3 异常上抛和异常捕捉的选择?
主要看一条标准,如果希望调用者来处理异常,选择throws上报。
其他情况使用try…catch捕捉
比如前面用到的FileInputStream,看一下源代码:
public FileInputStream(File file) throws FileNotFoundException {
String name = (file != null ? file.getPath() : null);
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkRead(name);
}
if (name == null) {
throw new NullPointerException();
}
if (file.isInvalid()) {
//这里手动抛出了异常,显然在调用者调用本方法来访问文件时,抛出的异常时告诉调用者文件可能不存在。
//自己抛出自己捕捉的话没有意义
//所以应该上抛
throw new FileNotFoundException("Invalid file path");
}
fd = new FileDescriptor();
fd.attach(this);
path = name;
open(name);
FileCleanable.register(fd); // open set the fd, register the cleanup
}
6. try…catch深入
- try…catch使用要求:
- catch后面的小括号中的类型可以是具体的异常类型,也可以是该异常类型的父类型(多态。抛出的异常的本质是对象,对象可以使用多态)。
- catch可以写多个。建议catch的时候,精确的一个一个处理。这样有利于程序的调试。
- catch写多个的时候,从上到下,必须遵守从小到大。
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class ExceptionTest07 {
//try...catch深入
//1.catch后面的小括号中的类型可以是具体的异常类型,也可以是该异常类型的父类型。
//2.catch可以写多个。建议catch的时候,精确的一个一个处理。这样有利于程序的调试。
//3.catch写多个的时候,从上到下,必须遵守从小到大。
public static void main(String[] args) {
//编译报错,catch必须捕捉发生的异常
/*try {
FileInputStream fis = new FileInputStream("C:\\Users\\Administrator\\Desktop\\垫付说明.doc");
} catch (CalssNotFoundException e) {
}*/
/* try {
FileInputStream fis = new FileInputStream("C:\\Users\\Administrator\\Desktop\\垫付说明.doc");
} catch (FileNotFoundException e) {
System.out.println("文件未找到");
}
System.out.println("这里能执行");*/
/* try {
FileInputStream fis = new FileInputStream("C:\\Users\\Administrator\\Desktop\\垫付说明.doc");
} catch (IOException e) {//catch后面可以使用父类型的异常,多态:IOException e = new FileNotFoundException();
}*/
/*try {
FileInputStream fis = new FileInputStream("C:\\Users\\Administrator\\Desktop\\垫付说明.doc");
fis.read();
} catch (FileNotFoundException e) {
System.out.println("文件不存在");
}catch (IOException e){
System.out.println("读取错误");
}*/
/* try {
FileInputStream fis = new FileInputStream("C:\\Users\\Administrator\\Desktop\\垫付说明.doc");
fis.read();
} catch (IOException e) {
System.out.println("文件不存在");
}catch (FileNotFoundException e){//报错:Exception 'java.io.FileNotFoundException' has already been caught
System.out.println("读取错误");
}*/
}
}
7. JDK 8 新特性
catch语句中可以使用或|运算符
import java.io.FileInputStream;
import java.io.FileNotFoundException;
public class ExceptionTest08 {
public static void main(String[] args) {
//JDK 8 新特性
try {
new FileInputStream("C:\\Users\\Administrator\\Desktop\\垫付说明.doc");
System.out.println(100 / 0);
}catch (FileNotFoundException | ArithmeticException | NullPointerException e){//可以使用或来同时捕捉多个异常
System.out.println("文件不存在?数学异常?空指针异常?都有可能!");
}
}
}
//文件不存在?数学异常?空指针异常?都有可能!
8.异常类的常用方法
1. getMessage()方法
-
public String getMessage(){return detailMessage}
上面的方法用来获取异常简单的描述信息:
观察Throwable类的源码可知:
该类中有一个属性:
private String detailMessage;
此外,Throwable类存在一个有参构造方法:
public Throwable(String message) {
fillInStackTrace();//这个先不管
detailMessage = message;//可以看到在调用参数为字符串的构造方法时,将message传给了detailMessage
}
那么,观察getMessage()方法:
public String getMessage(){
return detailMessage;
}
所以,我们知道:异常在JAVA中以类和对象的方式存在,当我们使用异常类的有参构造方法传进去一个字符串信息时,调用getMessage可以打印输出该信息。
比如:
显然,detailMessage是String类型,调用无参构造器的话会赋默认值null。
public class ExceptionTest09 {
public static void main(String[] args) {
//异常也是类,也可以创建异常对象。此时只要不手动抛出,JVM会认为这是一个简单的JAVA对象
NullPointerException e = new NullPointerException("空指针异常aaaaaa");
String mes = e.getMessage();
System.out.println(mes);//空指针异常aaaaaa
NullPointerException e1 = new NullPointerException();
String mes1 = e1.getMessage();
System.out.println(mes1);//默认值为null
}
}
2. printStackTrace()方法
-
public void printStackTrace() { printStackTrace(System.err);//打印异常堆栈信息 }
这个方法就是将异常的追踪信息打印输出在控制台上。需要注意的是:java后台打印异常堆栈追踪信息的时候,采用了异步线程的方式打印的。因为是异步线程,先输出了你好世界再打印了异常堆栈信息。
public class ExceptionTest09 {
public static void main(String[] args) {
//异常也是类,也可以创建异常对象。此时只要不手动抛出,JVM会认为这是一个简单的JAVA对象
NullPointerException e = new NullPointerException("空指针异常aaaaaa");
e.printStackTrace();
/*java.lang.NullPointerException: 空指针异常aaaaaa
at ExceptionTest09.main(ExceptionTest09.java:4)
*/
System.out.println("你好世界");
}
/*
你好世界
java.lang.NullPointerException: 空指针异常aaaaaa
at ExceptionTest09.main(ExceptionTest09.java:4)
*/
}
3.上面两个方法的运用
一般多使用printStackTrace()方法,因为该方法能打印详细的异常追踪信息。方便在系统出现异常时进行调试!因为在异常处理机制下,对异常捕获后并不会影响程序的执行(健壮性),所以 getMessage()方法简单的打印异常
不便于调试系统哪里出现了异常。
我们以后查看异常的追踪信息,我们应该怎么看,可以快速的调试程序呢?
异常信息追踪信息,从上往下一行一行看。
但是需要注意的是:SUN写的代码就不用看了(看包名就知道是自己的还是SUN的。)。
主要的问题是出现在自己编写的代码上。
import java.io.FileInputStream;
import java.io.FileNotFoundException;
public class ExceptionTest10 {
public static void main(String[] args) {
try {
m1();
} catch (FileNotFoundException e) {
/* //获取异常简单的信息描述
String s = e.getMessage();
System.out.println(s);//C:\Uers\Administrator\Desktop\垫付说明.doc (系统找不到指定的路径。)*/
e.printStackTrace();
/*
java.io.FileNotFoundException: C:\Users\dministrator\Desktop\垫付说明.doc (系统找不到指定的路径。)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:211)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:153)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:108)
at ExceptionTest10.m3(ExceptionTest10.java:24)
at ExceptionTest10.m2(ExceptionTest10.java:21)
at ExceptionTest10.m1(ExceptionTest10.java:18)
at ExceptionTest10.main(ExceptionTest10.java:7)
//上面表示24行异常导致了21行有错
21行导致了18行有错
18行导致了7行有错
所以应该先看24行代码。24行是代码错误的根源
*/
}
System.out.println("hello world");
}
public static void m1() throws FileNotFoundException {
m2();
}
public static void m2() throws FileNotFoundException{
m3();
}
public static void m3() throws FileNotFoundException {
new FileInputStream("C:\\Users\\dministrator\\Desktop\\垫付说明——陈必露.doc");//编译时异常
}
}
9. finally子句的使用
为什么需要finally子句?
finally是一个关键字,后面跟一个 代码块 执行相应的操作!
finally 关键字
和try一起联合使用。
finally语句块中的代码是必须执行的。
需要注意的是:finally和try…catch联用时的语法规则;
-
- try…catch中的finally子句是最后执行的,并且一定会执行。即使try语句中出现了异常,被catch捕捉后,finally代码块依旧会执行。注意:finally不能单独使用,必须配合try使用。
因为finally代码块最后执行且一定会执行,所以:
-
- 通常在finally代码块中完成资源的释放/关闭。因为finally的代码比较有保障(最后执行并且一定会执行)。即使try语句块中代码出现了异常,finally中代码也会正常执行。
举个例子:
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
//实际上,IO流是会占用资源的,使用完毕应该close来完成资源的释放/关闭
//如果不关闭会造成资源的浪费
public class ExceptionTest11 {
public static void main(String[] args) {
//将文件流的声明放在try外部
FileInputStream fis = null;
try {
//创建输入流对象
//一开始声明在try内部
//FileInputStream fis = new FileInputStream("C:\\Users\\Administrator\\Desktop\\垫付说明——陈必露.doc");
fis = new FileInputStream("C:\\Users\\Administrator\\Desktop\\垫付说明——陈必露.doc");
//开始读文件
//fis.read();
String s = null;
s.toString();
//假如文件流的关闭写在try语句块中
//显然前面发生了空指针异常,程序会自动进入catch分支,下面的代码就不会执行
//所以文件流并没有关闭
//那么为了防止此类情况发生,资源的释放与关闭一般在finally语句块中执行
//因为finally语句块最后执行并且一定会执行
//fis.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}catch(NullPointerException e){
e.printStackTrace();
}finally {
//流的关闭放在这里比较保险
//finally代码块中的语句一定会执行
//下面为什么报错?
//因为一开始fis声明在try语句块中,属于局部变量
//所以要将文件流的声明放在try外部
//fis.close();
System.out.println("finally语句块执行了");
if(fis != null){
try {
fis.close();//close会出现IOException属于编译时异常
System.out.println("文件流关闭");
} catch (IOException e) {
e.printStackTrace();
}
}
}
System.out.println("程序执行完毕");
}
}
/*
java.lang.NullPointerException: Cannot invoke "String.toString()" because "s" is null
at ExceptionTest11.main(ExceptionTest11.java:21)
finally语句块执行了
文件流关闭
程序执行完毕
*/
再次强调:finally语句块中的代码是一定会执行的!!!!!
那么:
由于finally必须配合try一起使用,没有catch可以吗?可以。并且try不能单独使用,必须和catch或者finally联用。
try 和 finally 可以联用!
看看下面代码的执行顺序:
public class ExceptionTest12 {
public static void main(String[] args) {
/* 以下代码的执行顺序:
先执行try...
再执行finally...
最后执行 return (return语句只要执行方法必然结束。)*/
try{
System.out.println("try begin");
return;
}finally {
System.out.println("finally begin");
}
//这里不能写代码,因为上面的return会执行,终止了本方法
//System.out.println(); Unreachable statement
}
}
/*
try begin
finally begin
*/
那什么情况finally会不能执行呢?当JVM提前退出时,finally将不再执行
public class ExceptionTest13 {
public static void main(String[] args) {
try{
System.out.println("try begin");
System.exit(0);//退出JVM
}finally {
System.out.println("finally begin");//由于JVM退出了,这里将不再执行
}
}
}
//try begin
根据上面的finally语法规则,写一道面试题如下:
public class ExceptionTest14 {
public static void main(String[] args) {
int ret = m();
System.out.println(ret);//100
//为什么返回的是100?
/* 因为java语法规则(有一些规则是不能破坏的,一旦这么说了,就必须这么做!):
java中有一条这样的规则:
方法体中的代码必须遵循自上而下顺序依次逐行执行(亘古不变的语法!)
java中还有一条语法规则:
return语句一旦执行,整个方法必须结束(亘古不变的语法!)*/
}
public static int m(){
int i = 100;
try{
// 这行代码出现在int i = 100;的下面,所以最终结果必须是返回100
// return语句还必须保证是最后执行的。一旦执行,整个方法结束。
return i;
}finally {//finally一定会执行,那返回的i是101吗?
i++;
}
}
}
//反编译之后的效果:
/*public static int m(){
int i = 100;
int j = i;
i++;
return j;
}*/
10.自定义异常
10.1为什么要自定义异常?
SUN公司提供的JDK的内置异常在实际开发中是不够用的。在实际的开发中,有很多业务,这些业务出现异常后,JDK中没有此类异常。所以我们可以根据业务自定义异常。
10.2 JAVA中怎么自定义异常?
JAVA中异常以类和对象的方式存在,观察一些异常类我们发现
public class FileNotFoundException extends IOException {
@java.io.Serial
private static final long serialVersionUID = -897856973823710492L;
public FileNotFoundException() {
super();
}
public FileNotFoundException(String s) {
super(s);
}
private FileNotFoundException(String path, String reason) {
super(path + ((reason == null)
? ""
: " (" + reason + ")"));
}
}
主要就包含缺省构造器和有参构造器,多数方法都继承了父类Exception,有需求可以重写!
综上:JAVA中怎么自定义异常?
- 编写一个异常类继承Exception或者RuntimeException
- 提供两个构造方法,缺省构造器和有参构造器,其中有参构造器的参数为String s
例如:
public class MyException extends Exception{//编译时异常
public MyException(){}
public MyException(String s){
super(s);
}
}
/*
public class MyException extends RuntimeException{
...
} // 运行时异常*/
对自定义异常进行测试:
public class ExceptionTest15 {
public static void main(String[] args) {
//有了自定义异常就可以创建异常对象
//注意:只是创建了异常对象,并没有抛出。
//此时,JVM只认为创建了一个普通的对象
MyException e = new MyException("自定义异常");
e.printStackTrace();//打印异常堆栈信息
System.out.println(e.getMessage());//打印异常简单信息
}
}
/*
MyException: 自定义异常
at ExceptionTest15.main(ExceptionTest15.java:4)
自定义异常*/
自定义异常是为了满足业务的需求。显然,有了自定义异常,怎么用?在发生异常时,手动抛出即可。
使用throw关键字手动抛出异常。
利用前面写的数组栈来完成手动抛异常的操作:显然栈满继续压栈和栈空继续弹栈就是异常
/*
编写程序,使用一维数组,模拟栈数据结构。
要求:
1、这个栈可以存储java中的任何引用类型的数据。
2、在栈中提供push方法模拟压栈。(栈满了,要有提示信息。)
3、在栈中提供pop方法模拟弹栈。(栈空了,也有有提示信息。)
4、编写测试程序,new栈对象,调用push pop方法来模拟压栈弹栈的动作。
5、假设栈的默认初始化容量是10.(请注意无参数构造方法的编写方式。)
*/
public class MyStack {
// 向栈当中存储元素,我们这里使用一维数组模拟。存到栈中,就表示存储到数组中。
// 因为数组是我们学习java的第一个容器。
// 为什么选择Object类型数组?因为这个栈可以存储java中的任何引用类型的数据
// new Animal()对象可以放进去,new Person()对象也可以放进去。因为Animal和Person的超级父类就是Object。
// 包括String也可以存储进去。因为String父类也是Object。
private Object[] elements;
// 栈帧,永远指向栈顶部元素
// 那么这个默认初始值应该是多少。注意:最初的栈是空的,一个元素都没有。
//private int index = 0; // 如果index采用0,表示栈帧指向了顶部元素的上方。
//private int index = -1; // 如果index采用-1,表示栈帧指向了顶部元素。
private int index;
/**
* 无参数构造方法。默认初始化栈容量10.
*/
public MyStack() {
// 一维数组动态初始化
// 默认初始化容量是10.
this.elements = new Object[10];
// 给index初始化
this.index = -1;
}
/**
* 压栈的方法
* @param obj 被压入的元素
*/
public void push(Object obj){
if(index >= elements.length - 1){
System.out.println("压栈失败,栈已满!");
return;
}
// 程序能够走到这里,说明栈没满
// 向栈中加1个元素,栈帧向上移动一个位置。
index++;
elements[index] = obj;
// 在声明一次:所有的System.out.println()方法执行时,如果输出引用的话,自动调用引用的toString()方法。
System.out.println("压栈" + obj + "元素成功,栈帧指向" + index);
}
/**
* 弹栈的方法,从数组中往外取元素。每取出一个元素,栈帧向下移动一位。
* @return
*/
public void pop(){
if(index < 0){
System.out.println("弹栈失败,栈已空!");
return;
}
// 程序能够执行到此处说明栈没有空。
System.out.print("弹栈" + elements[index] + "元素成功,");
// 栈帧向下移动一位。
index--;
System.out.println("栈帧指向" + index);
}
// set和get也许用不上,但是你必须写上,这是规矩。你使用IDEA生成就行了。
// 封装:第一步:属性私有化,第二步:对外提供set和get方法。
public Object[] getElements() {
return elements;
}
public void setElements(Object[] elements) {
this.elements = elements;
}
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
}
对上面的代码可以看到,当栈满继续压栈和栈空继续弹栈采用了return语句终止程序,实际上可以采取异常抛出的方式来应对上面的两种情况。下面把return换成手动抛出异常!
先自定义栈操作异常:
public class MyStackOperationException extends Exception{//编译时异常
public MyStackOperationException() {
}
public MyStackOperationException(String message) {
super(message);
}
}
改进压栈弹栈方法,选择手动抛出异常代替return.
public class MyStack {
private Object[] elements;
// 栈帧,永远指向栈顶部元素
// 那么这个默认初始值应该是多少。注意:最初的栈是空的,一个元素都没有。
//private int index = 0; // 如果index采用0,表示栈帧指向了顶部元素的上方。
//private int index = -1; // 如果index采用-1,表示栈帧指向了顶部元素。
private int index;
/**
* 无参数构造方法。默认初始化栈容量10.
*/
public MyStack() {
// 一维数组动态初始化
// 默认初始化容量是10.
this.elements = new Object[10];
// 给index初始化
this.index = -1;
}
/**
* 压栈的方法
* @param obj 被压入的元素
*/
public void push(Object obj) throws MyStackOperationException{
if(index >= elements.length - 1){
//改良之前
/* System.out.println("压栈失败,栈已满!");
return;*/
throw new MyStackOperationException("栈已满,压栈失败");
}
// 程序能够走到这里,说明栈没满
// 向栈中加1个元素,栈帧向上移动一个位置。
index++;
elements[index] = obj;
// 在声明一次:所有的System.out.println()方法执行时,如果输出引用的话,自动调用引用的toString()方法。
System.out.println("压栈" + obj + "元素成功,栈帧指向" + index);
}
/**
* 弹栈的方法,从数组中往外取元素。每取出一个元素,栈帧向下移动一位。
* @return
*/
public void pop() throws MyStackOperationException {
if(index < 0){
//改良之前
/* System.out.println("弹栈失败,栈已空!");
return;*/
throw new MyStackOperationException("栈已空,弹栈失败");
}
// 程序能够执行到此处说明栈没有空。
System.out.print("弹栈" + elements[index] + "元素成功,");
// 栈帧向下移动一位。
index--;
System.out.println("栈帧指向" + index);
}
// set和get也许用不上,但是你必须写上,这是规矩。你使用IDEA生成就行了。
// 封装:第一步:属性私有化,第二步:对外提供set和get方法。
public Object[] getElements() {
return elements;
}
public void setElements(Object[] elements) {
this.elements = elements;
}
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
}
测试:
public class MyStackTest {
public static void main(String[] args) {
MyStack stack = new MyStack();
try {
stack.push(new Object());//编译时异常
stack.push(new Object());
stack.push(new Object());
stack.push(new Object());
stack.push(new Object());
stack.push(new Object());
stack.push(new Object());
stack.push(new Object());
stack.push(new Object());
stack.push(new Object());
//栈已满
stack.push(new Object());
} catch (MyStackOperationException e) {
System.out.println(e.getMessage());
}
try{
stack.pop();//编译时异常
stack.pop();
stack.pop();
stack.pop();
stack.pop();
stack.pop();
stack.pop();
stack.pop();
stack.pop();
stack.pop();
//栈已空
stack.pop();
}catch (MyStackOperationException e){
System.out.println(e.getMessage());
}
}
}/*
压栈java.lang.Object@2d98a335元素成功,栈帧指向0
压栈java.lang.Object@16b98e56元素成功,栈帧指向1
压栈java.lang.Object@7ef20235元素成功,栈帧指向2
压栈java.lang.Object@27d6c5e0元素成功,栈帧指向3
压栈java.lang.Object@4f3f5b24元素成功,栈帧指向4
压栈java.lang.Object@15aeb7ab元素成功,栈帧指向5
压栈java.lang.Object@7b23ec81元素成功,栈帧指向6
压栈java.lang.Object@6acbcfc0元素成功,栈帧指向7
压栈java.lang.Object@5f184fc6元素成功,栈帧指向8
压栈java.lang.Object@3feba861元素成功,栈帧指向9
栈已满,压栈失败
弹栈java.lang.Object@3feba861元素成功,栈帧指向8
弹栈java.lang.Object@5f184fc6元素成功,栈帧指向7
弹栈java.lang.Object@6acbcfc0元素成功,栈帧指向6
弹栈java.lang.Object@7b23ec81元素成功,栈帧指向5
弹栈java.lang.Object@15aeb7ab元素成功,栈帧指向4
弹栈java.lang.Object@4f3f5b24元素成功,栈帧指向3
弹栈java.lang.Object@27d6c5e0元素成功,栈帧指向2
弹栈java.lang.Object@7ef20235元素成功,栈帧指向1
弹栈java.lang.Object@16b98e56元素成功,栈帧指向0
弹栈java.lang.Object@2d98a335元素成功,栈帧指向-1
栈已空,弹栈失败*/
异常练习:
编写程序模拟用户注册:
1、程序开始执行时,提示用户输入“用户名”和“密码”信息。
2、输入信息之后,后台java程序模拟用户注册。
3、注册时用户名要求长度在[6-14]之间,小于或者大于都表示异常。
注意:
完成注册的方法放到一个单独的类中。
异常类自定义即可。
class UserService {
public void register(String username,String password){
//这个方法中完成注册!
}
}
编写main方法,在main方法中接收用户输入的信息,在main方法
中调用UserService的register方法完成注册。
第一步:自定义异常
package homework;
//自定义异常
public class UsernameException extends Exception{
public UsernameException(){}
public UsernameException(String s){
super(s);
}
}
第二步:编写注册类
package homework;
import java.util.Scanner;
public class UserService {
private String username;
private String password;
public void register(String username, String password) throws UsernameException{
this.username = username;
if(username == null || username.length() < 6 || username.length() > 14){
throw new UsernameException("用户名要求长度在[6-14]之间");
}
this.password = password;
System.out.println("注册成功,欢迎你");
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
第三步:测试
package homework;
import java.util.Scanner;
public class Homework1 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
try {
System.out.println("请输入用户名");
String username = sc.next();
System.out.println("请输入密码");
String password = sc.next();
UserService userService = new UserService();
userService.register(username,password);//可能会抛出编译时异常
} catch (UsernameException e) {
System.out.println(e.getMessage());
}finally {
System.out.println("关闭流");
sc.close();
}
}
}
11. Day-24接口作业
写一个类Army,代表一支军队,这个类有一个属性Weapon数组w(用来存储该军队所拥有的所有武器),
该类还提供一个构造方法,在构造方法里通过传一个int类型的参数来限定该类所能拥有的最大武器数量,
并用这一大小来初始化数组w。
该类还提供一个方法addWeapon(Weapon wa),表示把参数wa所代表的武器加入到数组w中。
在这个类中还定义两个方法attackAll()让w数组中的所有武器攻击;
以及moveAll()让w数组中的所有可移动的武器移动。
写一个主方法去测试以上程序。
提示:
Weapon是一个父类。应该有很多子武器。
这些子武器应该有一些是可移动的,有一些
是可攻击的。
代码比较多,这里就放一个测试类:
package day24homework;
public class Army {
private Weapon[] weapons;
public Army(int i) {
weapons = new Weapon[i];
}
public void addWeapon(Weapon weapon) throws AddWeaponException {
for (int i = 0; i < weapons.length; i++) {
if (null == weapons[i]){
weapons[i] = weapon;
System.out.println("武器添加成功");
return;
}
}
//程序执行到这表示武器数组已经满了,可以抛出异常
throw new AddWeaponException("武器已满,无法添加");
}
//攻击方法
public void attackAll(){
for (int i = 0; i < weapons.length; i++) {
if(weapons[i] instanceof Attackable){//这里是多态:Weapon类型的数组,但是元素是Weapon类的子类:多态
//为了调用子类独有的方法,需要向下转型,但是具体是哪一个子类不知道
//此时JAVA语法允许类和接口之间的强制转换
//其实,转换成接口后,还是多态:父类接口指向子类的对象 面向接口编程
//类和接口之间不需要继承关系也可以强制转换
((Attackable)weapons[i]).attack();
//编译上访问Attackable接口的attack()方法
//实际上运行时执行的是底层Attackable实现类具体对象的attack()方法
//所以还是多态
}
}
}
//移动方法
public void moveAll(){
for (Weapon w : weapons) {
if (w instanceof Moveable) {
((Moveable)w).move();
}
}
}
public Weapon[] getWeapons() {
return weapons;
}
public void setWeapons(Weapon[] weapons) {
this.weapons = weapons;
}
}
注意:
实际上在 JAVA中:
类在强制类型转换过程中,如果是类转换成接口类型。
* 那么类和接口之间不需要存在继承关系,也可以转换,
* java语法中允许。
但本质上是多态机制:以下面的代码为例
//攻击方法
public void attackAll(){
for (int i = 0; i < weapons.length; i++) {
if(weapons[i] instanceof Attackable){//这里是多态:Weapon类型的数组,但是元素是Weapon类的子类:多态
//Weapon类中并没有attack()方法
//为了调用子类独有的方法,需要向下转型,但是具体是哪一个子类不知道
//此时JAVA语法允许类和接口之间的强制转换
//类和接口之间不需要继承关系也可以强制转换
//其实,转换成接口后,还是多态:父类接口指向子类的对象
((Attackable)weapons[i]).attack();
//编译上访问Attackable接口的attack()方法
//实际上运行时执行的是底层Attackable实现类具体对象的attack()方法
//所以还是多态
}
}
}