6 异常
6.1 异常概述
-
出现背景:
在使用计算机语言进行项目开发的过程中,即使程序员把代码写得尽善尽美
,在系统的运行过程中仍然会遇到一些问题,因为很多问题不是靠代码能够避免的,比如:客户输入数据的格式,读取文件是否存在,网络是否始终保持通畅
等等。 -
异常:
在Java语言中,将程序执行中发生的不正常情况称为"异常"。(开发过程中的语法错误和逻辑错误不是异常)
6.2 异常体系
说明:
Throwable
:在 Java 中,所有的异常都有一个共同的祖先 Throwable(可抛出)Error
:严重问题,不需要处理Exception
:称为异常类,它表示程序本身可以处理的问题-
RuntimeException
:在编译期是不检查的,开发中,通常就不进行显示的处理了。一旦在程序执行中,出现了运行时异常,那么就根据异常的提示信息修改代码即可。 -
非RuntimeException
:编译期就必须处理的,否则程序不能通过编译,就更不能正常运行了
-
6.2.1 Error举例
package com.atguigu.java;
/** Error:* Java虚拟机无法解决的严重问题。如:JVM系统内部错误、资源耗尽等严重情况。比如:StackOverflowError和OOM。* * 一般不编写针对性的代码进行处理。出现问题只能改代码。* * */
public class ErrorTest {public static void main(String[] args) {//1.栈溢出:java.lang.StackOverflowError
// main(args);//2.堆溢出:java.lang.OutOfMemoryError Integer[] arr = new Integer[1024*1024*1024];}
}
6.2.2 Exception举例
package com.atguigu.java1;import java.io.File;
import java.io.FileInputStream;
import java.util.Date;
import java.util.Scanner;import org.junit.Test;/** 一、异常体系结构* * java.lang.Throwable* |-----java.lang.Error:一般不编写针对性的代码进行处理。* |-----java.lang.Exception:可以进行异常的处理* |------编译时异常(checked)* |-----IOException* |-----FileNotFoundException* |-----ClassNotFoundException* |-----SQLException sql异常* |------运行时异常(unchecked,RuntimeException)* |-----NullPointerException* |-----ArrayIndexOutOfBoundsException* |-----ClassCastException* |-----NumberFormatException 数字格式异常* |-----InputMismatchException 输入不匹配异常* |-----ArithmeticException 算术异常* * * * 面试题:常见的异常都有哪些?举例说明*/
public class ExceptionTest {//******************以下是编译时异常***************************@Testpublic void test7(){
// File file = new File("hello.txt");
// FileInputStream fis = new FileInputStream(file);
//
// int data = fis.read();
// while(data != -1){
// System.out.print((char)data);
// data = fis.read();
// }
//
// fis.close();}//******************以下是运行时异常***************************//ArithmeticException@Testpublic void test6(){int a = 10;int b = 0;System.out.println(a / b);}//InputMismatchException@Testpublic void test5(){Scanner scanner = new Scanner(System.in);int score = scanner.nextInt();System.out.println(score);scanner.close();}//NumberFormatException@Testpublic void test4(){String str = "123";str = "abc";int num = Integer.parseInt(str);}//ClassCastException@Testpublic void test3(){Object obj = new Date();String str = (String)obj;}//IndexOutOfBoundsException@Testpublic void test2(){//ArrayIndexOutOfBoundsException
// int[] arr = new int[10];
// System.out.println(arr[10]);//StringIndexOutOfBoundsExceptionString str = "abc";System.out.println(str.charAt(3));}//NullPointerException@Testpublic void test1(){// int[] arr = null;
// System.out.println(arr[3]);String str = "abc";str = null;System.out.println(str.charAt(0));}}
6.3 JVM的默认处理方案
如果程序出现了问题,我们没有做任何处理,最终JVM会做默认的处理
- 把异常的名称,异常原因及异常出现的位置等信息输出在了控制台(由下图可知)
- 程序停止执行(由下图可知,
结束
并没有输出到控制台)
6.4 Throwable的成员方法
6.5 编译时异常和运行时异常的区别
Java程序的执行分为编译时过程和运行时过程。有的错误只有在运行时
才会发生。比如:除数为0,数组下标越界等。
因此,根据异常可能出现的阶段,可以将异常分为:
-
编译时期异常(即checked异常、受检异常):在代码编译阶段,编译器就能明确
警示
当前代码可能发生(不是一定发生)
xx异常,并明确督促
程序员提前编写处理它的代码。如果程序员没有编写
对应的异常处理代码,则编译器就会直接判定编译失败,从而不能生成字节码文件。通常,这类异常的发生不是由程序员的代码引起的,或者不是靠加简单判断就可以避免的,例如:FileNotFoundException(文件找不到异常)。必须显示处理,否则程序就会发生错误,无法通过编译
- 如果抛出的异常是IOException等类型的
非运行时异常
,则必须捕获,否则编译错误
。也就是说,我们必须处理编译时异常,将异常进行捕捉,转化为运行时异常。
-
运行时期异常(即runtime异常、unchecked异常、非受检异常):在代码编译阶段,编译器完全不做任何检查,无论该异常是否会发生,编译器都不给出任何提示。只有等代码运行起来并确实发生了xx异常,它才能被发现。通常,这类异常是由程序员的代码编写不当引起的,只要稍加判断,或者细心检查就可以避免。
无需显示处理,也可以和编译时异常一样处理
- 前面使用的异常都是
RuntimeException类
或是它的子类
,这些类的异常的特点是:即使没有使用try和catch捕获,Java自己也能捕获,并且编译通过 ( 但运行时会发生异常使得程序运行终止 )。所以,对于这类异常,可以不作处理
,因为这类异常很普遍,若全处理可能会对程序的可读性和运行效率
产生影响。一旦在程序执行中,出现了运行时异常,那么就根据异常的提示信息修改代码
即可。 - java.lang.RuntimeException类及它的子类都是运行时异常。比如:ArrayIndexOutOfBoundsException数组下标越界异常,ClassCastException类型转换异常。
6.6 异常处理概述
-
为什么要进行异常处理:
因为java虚拟机的默认处理方案,会让程序在出现异常的地方直接结束掉。而在实际开发中我们程序某一个部分出现问题了,它不应该影响后续的执行,所以我们要自己处理异常。 -
如果程序出现了问题,我们需要自己来处理,有两种方案:
- try … catch …
- throws
-
异常处理:抓抛模型
- 过程一:“抛”:程序在正常执行的过程中,一旦出现异常,就会在异常代码处生成一个异常对象,并将对象抛出。一旦抛出异常对象后,
其后的代码就不会再执行。
- 关于异常对象的产生:
① 系统自动生成的异常对象
② 手动的生成一个异常对象,并抛出(throw)
- 关于异常对象的产生:
- 过程二:“抓”:可以理解为异常的处理方式:
- try-catch-finally :真正的处理异常,后续代码
会执行
- throws:没有真正处理异常仅仅是抛给调用者,后续代码
不会执行
- try-catch-finally :真正的处理异常,后续代码
- 过程一:“抛”:程序在正常执行的过程中,一旦出现异常,就会在异常代码处生成一个异常对象,并将对象抛出。一旦抛出异常对象后,
-
如果程序出现了异常没有处理:
- 最终会跑到main方法中,main方法由jvm虚拟机管理,jvm虚拟机处理异常是杀死进程,结束掉程序。
-
异常处理的意义:
- 程序出现异常后仍能正确执行,不影响程序运行。
6.7 异常处理方式一:(try-catch-finally)
说明:
- try-catch-finally,又叫做捕获方式。
语法:
//try只能而且出现一次 catch可以出现0到N次 finally出现0到1次
try{//可能出现异常的代码}catch(异常类型1 变量名1){//处理异常的方式1}catch(异常类型2 变量名2){//处理异常的方式2}catch(异常类型3 变量名3){//处理异常的方式3}....finally{//一定会执行的代码}
执行流程:
- 程序从try里面的代码开始执行
出现异常,会自动生成一个异常类对象
,该异常对象将被提交给Java运行时系统- 当Java运行时系统接收到异常对象时,会到catch中去找匹配的异常类,找到后进行异常的处理
执行完毕之后,程序还可以继续往下执行
。
6.7.1 测试1:try-catch结构
package com.shuai;
/*** 1.finally是可选的。* * 2.使用try将可能出现异常代码包装起来,在执行过程中,一旦出现异常就会生成一个对应类的异常对象,根据* 此对象的类型到catch中进行匹配。* * 3.一旦try中的异常对象匹配到一个catch时,就进入catch中进行异常的处理,一旦处理完成,就跳出当前的* try-catch结构(在没有写finally的情况),继续执行其后的代码。* * 4.catch中的异常类型如果没有父子类关系,谁声明在上,,谁声明在下无所谓。* catch中的异常类型如果有父子类关系,则要求子类在上父类在下,从小到大的排序,否则报错。* * 5.常用的异常处理方式:1)String e.getMessage() 2)void e.printStackTrace() 这个比较常用* * 6.在try结构中定义的变量,出了try结构以后就不能使用了。想要使用把变量声明在外面,赋值在结构里面。* * 7.为什么不写成int num;而要多赋值个0,首先因为变量想要输出首先要声明好后并且有初始化的值,而局部变量* 没有默认值,一旦try中的程序报错,直接输出num,num没有值程序会报错,为了避免这种情况,加上0不会影响程* 序也不会因为异常情况导致num没有值而保错。* *8.try-catch-finally也可以嵌套* 体会1:使用try-catch-finally结构处理编译异常时,使得编译时不会报错,但运行时有可能报错,相当于try-catch-finally* 将编译时可能出现的异常延迟到运行时出现。* 体会2:开发中,由于运行时异常比较常见,所以我们通常就不针对运行时异常编写try-catch-finally了。* 针对于编译时异常,我们说一定要考虑异常的处理。(如果与前端页面交互提示时,仍要处理。)*/
public class Demo05 {public static void main(String[] args) {String str="123";str="abc";/*为什么不写成int num;而要多赋值个0,首先因为变量想要输出首先要声明好后并且有初始化的值,而局部变量没有默认值,一旦try中的程序报错,直接输出num,num没有值程序会报错,为了避免这种情况,加上0不会影响程序也不会因为异常情况导致num没有值而报错。*/int num = 0;try {num=Integer.parseInt(str);//int num=Integer.parseInt(str);//出现异常System.out.println("hell0-------01");//不会输出} catch (NullPointerException e) { //习惯上异常名叫 eSystem.out.println("出现了空指针转换异常");}catch (NumberFormatException e) { //一个类型e只在一个catch中有效,所以不同的catch中名字可以相同。//System.out.println("出现了数值转换异常"); 一般不这样写,用异常类方法代替,如下://String e.getMessage():此方法返回值是String类型,想要看就要输出它。因为有返回值就要定义变量进行接收,查看就要输出,这里简写一块了。// System.out.println(e.getMessage()); //打印异常信息//void e.printStackTrace():此方法返回值是void类型,不需要在进行输出就能查看。e.printStackTrace(); //打印异常详细信息}catch (Exception e){System.out.println("出现了异常");//不会输出}//System.out.println(num);报错不能调用,因为变量num在catch中定义的,作用域在catch中。System.out.println(num);System.out.println("hell0-------02");//会输出}}
6.7.2 测试2:finally使用场景分析
-
因为异常会引发程序跳转,从而会导致有些语句执行不到。而程序中有一些特定的代码无论异常是否发生,都
需要执行
。例如,数据库连接、输入流输出流、Socket连接、Lock锁的关闭等,这样的代码通常就会放到finally块中。所以,我们通常将一定要被执行的代码声明在finally中。- 唯一的例外,使用 System.exit(0) 来终止当前正在运行的 Java 虚拟机。
-
不论在try代码块中是否发生了异常事件,catch语句是否执行,catch语句是否有异常,catch语句中是否有return,finally块中的语句都会被执行。
-
finally语句和catch语句是可选的,但finally不能单独使用。
try{}finally{}
package com.atguigu.java1;import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;import org.junit.Test;/** try-catch-finally中finally的使用:* * * 1.finally是可选的* * 2.finally中声明的是一定会被执行的代码。即使catch中又出现异常了,try中有return语句,catch中有* return语句等情况。* * 3.像数据库连接、输入输出流、网络编程Socket等资源,JVM是不能自动的回收的,我们需要自己手动的进行资源的* 释放。此时的资源释放,就需要声明在finally中。* * * */
public class FinallyTest {@Testpublic void test2(){FileInputStream fis = null;try {File file = new File("hello1.txt");//可能报FileNotFoundException (编译时异常)fis = new FileInputStream(file);int data = fis.read();//可能报IOException(编译时异常)while(data != -1){System.out.print((char)data);data = fis.read();//可能报IOException}} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}finally{try {//出现FileNotFoundException,文件创建不成功fis为null,// 之后走第一个catch处理异常,// 之后进入到finally当中,fis为null会出现空指针异常,加上if判断避免这一情况。if(fis != null)fis.close();//可能报IOException} catch (IOException e) {e.printStackTrace();}}}@Testpublic void testMethod(){int num = method();System.out.println(num);}public int method(){try{int[] arr = new int[10];System.out.println(arr[10]);return 1;}catch(ArrayIndexOutOfBoundsException e){e.printStackTrace();return 2;}finally{System.out.println("我一定会被执行");return 3;}}@Testpublic void test1(){try{int a = 10;int b = 0;System.out.println(a / b);}catch(ArithmeticException e){e.printStackTrace();// int[] arr = new int[10];
// System.out.println(arr[10]);}catch(Exception e){e.printStackTrace();}
// System.out.println("我好帅啊!!!~~");finally{System.out.println("我好帅啊~~");}}}
快捷键:选中报错包裹的代码—>Suround With—>Try/catch Block
注意:修改代码关闭流放在finally中。
6.8 异常处理方式二:(throws)
- 如果在编写方法体的代码时,某句代码可能发生某个
编译时异常
,不处理编译不通过,但是在当前方法体中可能不适合处理
或无法给出合理的处理方式
,则此方法应显示地
声明抛出异常,表明该方法将不对这些异常进行处理,而由该方法的调用者负责处理。
- 具体方式:在方法声明中用
throws语句
可以声明抛出异常的列表,throws后面的异常类型可以是方法中产生的异常类型,也可以是它的父类。
- 执行流程:
- "throws + 异常类型"写在方法的声明处。指明此方法执行时,可能会抛出的异常类型。
- 一旦当方法体执行时,出现异常,仍会在异常代码处生成一个异常类的对象,此对象满足throws后异常类型时,就会被抛出。
异常代码后续的代码,就不再执行!
6.8.1 throws基本格式
声明异常格式:
修饰符 返回值类型 方法名(参数) throws 异常类名1,异常类名2…{ }
在throws后面可以写多个异常类型,用逗号隔开。
- 注意:这个格式是跟在
方法的括号
后面的
举例:
public void readFile(String file) throws FileNotFoundException,IOException {...// 读文件的操作可能产生FileNotFoundException或IOException类型的异常FileInputStream fis = new FileInputStream(file);//...
}
6.8.2 测试throws关键字
package com.atguigu.java1;import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;/** 异常处理的方式二:throws + 异常类型** 1. "throws + 异常类型"写在方法的声明处。指明此方法执行时,可能会抛出的异常类型。* 一旦当方法体执行时,出现异常,仍会在异常代码处生成一个异常类的对象,此对象满足throws后异常* 类型时,就会被抛出。异常代码后续的代码,就不再执行!** 2. 体会:try-catch-finally:真正的将异常给处理掉了。* throws的方式只是将异常抛给了方法的调用者。 并没有真正将异常处理掉。**/
public class ExceptionTest2 {public static void main(String[] args){try{method2();}catch(IOException e){e.printStackTrace();}// method3();}public static void method3(){try {method2();} catch (IOException e) {//IOException 是FileNotFoundException异常的父类,这里处理方式又相同所以只需要写一个catch即可。e.printStackTrace();}}//method1抛出了2个异常,这里method2调用为什么只抛出一个异常呢??//如果2个异常是父子类关系,并且处理异常的方式相同,比如都是e.printStackTrace();,那么只需要抛出一个异常即可。public static void method2() throws IOException{method1();}public static void method1() throws FileNotFoundException,IOException{File file = new File("hello1.txt");FileInputStream fis = new FileInputStream(file);int data = fis.read();while(data != -1){System.out.print((char)data);data = fis.read();}fis.close();System.out.println("hahaha!");}}
6.8.3 方法重写中throws的要求
方法重写时,对于方法签名是有严格要求的。复习:
(1)方法名必须相同
(2)形参列表必须相同
(3)返回值类型- 基本数据类型和void:必须相同- 引用数据类型:<=
(4)权限修饰符:>=,而且要求父类被重写方法在子类中是可见的
(5)不能是static,final修饰的方法
此外,对于throws异常列表要求:
- 如果父类被重写方法的方法签名后面没有 “throws 编译时异常类型”,那么重写方法时,方法签名后面也不能出现“throws 编译时异常类型”。
- 如果父类被重写方法的方法签名后面有 “
throws 编译时异常类型
”,那么重写方法时,throws的编译时异常类型必须 <= 被重写方法throws的编译时异常类型,或者不throws编译时异常。 - 方法重写,对于“
throws 运行时异常类型
”没有要求。
package com.atguigu03._throws;import java.io.FileNotFoundException;
import java.io.IOException;/*** ClassName: OverrideTest* Description:** @Author 尚硅谷-宋红康* @Create 9:34* @Version 1.0*/
public class OverrideTest {public static void main(String[] args) {Father f = new Son();try{f.method1();}catch(IOException e){e.printStackTrace();}Number n = f.method4();}
}class Father{public void method1() throws IOException {}public void method2(){}public void method3(){}public Number method4(){return null;}}class Son extends Father{/*** 规则1:子类方法抛出的异常要小于等于父类的。* main方法中的多态创建对象,编译看左边调用的是父类方法的声明,* 所以他try-catch处理的是父类的异常,运行期实际上执行的是子类重写的方法体,* 此时子类抛出的异常大于所可以处理的异常(也就是说此时抛出的异常不在try-catch可以处理* 异常的范围内),那么try-catch处理异常就没有什么意义了。*/@Overridepublic void method1() throws FileNotFoundException {}/*** 规则2:父类的方法没有throws抛出异常,那么子类重写的方法也不能throws异常。(针对编译期异常)* 此时子类重写的方法有编译期异常,只能使用try-catch处理。*/
// @Override
// public void method2() throws FileNotFoundException{
//
// }/*** 规则3:父类的方法没有throws抛出异常,但是子类重写的方法仍然可以throws异常。(针对运行时异常)* throws后面也可以写运行时异常类型,只是运行时异常类型,写或不写对于编译器和程序执行来说都没有任何区别。* 如果写了,唯一的区别就是调用者调用该方法后,使用try...catch结构时,IDEA可以获得更多的信息,需要添加哪种catch分支。* 总结:运行时异常在编写器不会报错,你抛不抛出都一个样。*/@Overridepublic void method3() throws RuntimeException{}/*** 同理:子类重写方法的返回值如果是引用类型,要小于等于父类的。* Number n = f.method4();* f.method4() 多态调用的是父类的方法返回Number类型,所以用Number来接收* 之后运行时输出的是子类重写后的方法,此时返回的类型只能是小于等于Number才可以接收,* 如果返回比Number类型还要大的比如Object类型,Number无法接收Object类型会报错。** @return*/@Overridepublic Integer method4(){return null;}}
6.9 两种异常处理方式的选择
前提:对于异常,使用相应的处理方式。此时的异常,主要指的是编译时异常。
- 如果程序代码中,涉及到资源的调用(流、数据库连接、网络连接等),则必须考虑使用try-catch-finally来处理,保证不出现内存泄漏。
- 如果父类中被重写的方法没有throws方式处理异常,则子类重写的方法也不能使用throws,意味着如果子类重写的方法中有异常,必须使用try-catch-finally方式处理。(
针对编译器异常
) - 开发中,方法a中依次调用了方法b,c,d等方法,方法b,c,d之间是递进关系。此时,如果方法b,c,d中有异常,我们通常选择使用throws,而方法a中通常选择使用try-catch-finally。
6.10 手动抛出异常对象(throw)
-
使用背景:在实际开发中,如果出现不满足具体场景的代码问题,我们就有必要手动抛出一个指定类型的异常对象。
-
作用:用于在方法体内部抛出异常对象
-
关于异常对象的产生:
- 系统自动生成的异常对象
- 手动的生成一个异常对象,并抛出(throw)
-
注意:
- 手动抛出异常如果是运行时异常对象可以不用处理,如果抛出的是编译期异常对象一定要处理异常。
- throw语句会导致程序执行流程被改变,throw语句是明确抛出一个异常对象,因此它
下面的代码将不会执行
。 - 如果当前方法没有try…catch处理这个异常对象,throw语句就会
代替return语句
提前终止当前方法的执行,并返回一个异常对象给调用者。
6.10.1 没有手动抛出异常对象之前
package com.atguigu.java2;public class StudentTest {public static void main(String[] args) {Student s = new Student();s.regist(-1001);System.out.println(s);//直接输出对象的引用实际上时输出.toString()方法。}}class Student{private int id;public void regist(int id) {if(id > 0){this.id = id;}else{/** 即便你此时输入的是负数不合理,只能给你个提示输入的数据错误,但是这样写还是把结果输出了。* 正常来讲:应该是输入的是负数不合理,程序直接报错不会再向下执行输出结果,所以这里一般是手动抛出异常对象,* 一旦数据不合理直接报异常,程序停止运行。*/System.out.println("您输入的数据非法!");}}@Overridepublic String toString() {return "Student [id=" + id + "]";}}
6.10.2 手动抛出异常对象之后
package com.atguigu.java2;public class StudentTest {public static void main(String[] args) {try {Student s = new Student();s.regist(-1001);System.out.println(s);} catch (Exception e) {
// e.printStackTrace();/*throw new RuntimeException("您输入的数据非法!")里面的值是由RuntimeException的有参构造方法中super(xxx)调用--->* 父类的Exception的有参构造方法中super(xxx)调用----> Throable中的有参构造方法,在Throable类中通过有参构造方法* 赋值(this.detailMessage = message;) 给成员变量: private String detailMessage;为:您输入的数据非法!* 即:值最终赋值给Throable类的成员变量detailMessage。* * void e.getMessage() 底层源码:* public String getMessage() {* return detailMessage;* }* 这个方法getMessage()的返回值为Throable类中的detailMessage成员变量,所以输出结果为:您输入的数据非法! */System.out.println(e.getMessage());}}}class Student{private int id;public void regist(int id) throws Exception {if(id > 0){this.id = id;}else{
// System.out.println("您输入的数据非法!");//手动抛出异常对象
// throw new RuntimeException("您输入的数据非法!");抛出运行时异常,调用方不用处理。
// throw new Exception("您输入的数据非法!");Exception包含编译器异常,所以一旦throws抛出,在调用方一定要进行处理编译器异常。throw new MyException("不能输入负数");//抛出的是自定义异常//错误的 String不是异常类
// throw new String("不能输入负数");
//return 0;如果方法有返回值且不影响结果,则可以用throw代替return,因为如果报异常也会结束方法运行。}}@Overridepublic String toString() {return "Student [id=" + id + "]";}}
6.10.3 throw和throws的区别
6.11 自定义异常
使用背景:
- 目的是系统提供的异常类型是
有限的
,如果程序产生的异常不在提供中,可以抛出自己定义的异常使得异常类型更加精确
(一般和throw连用) - 一般这个自定义异常类,
定义这个类时要做到见名知意
。
格式:
范例:
6.11.1 测试
和案例6.10.2连用。
package com.atguigu.java2;
/** 如何自定义异常类?* 1.继承于现有的异常结构:RuntimeException 、Exception(包含运行和编译器异常,* 所以编译期就要进行处理异常)* RuntimeException异常:只需要继承RuntimeException异常或者它的子类。* Checked异常:只需要继承Exception异常或者Exception的子类,但需要除RuntimeException之外。* 2.提供全局常量:serialVersionUID* 3.提供重载的构造器,建议大家提供至少两个构造器,一个是无参构造,* 一个是(String message)构造器。* */
public class MyException extends Exception{static final long serialVersionUID = -7034897193246939L;//无参构造方法public MyException(){}//有参构造方法public MyException(String msg){/** 调用父类的有参构造,因为上面的案例中处理异常使用的是e.getMessage()方法,* 这个方法返回值为Throable的成员变量dtailMessage属性。而现在自定义异常对象* 的通过有参构造方法创建对象并传参,这个自定义异常对象的参数不是自己定义的而是定义* 在顶级父类Throable中的成员变量,所以想要赋值给父类Throable的属性需要在子类* 的构造方法中通过super一步一步调用父类构造方法,最终在Throwable的构造方法中把* 值赋值给成员变量dtailMessage。*/super(msg);}
}
7. 内部类(InnerClass)
8.1 概述
8.1.1 什么是内部类
将一个类A定义在另一个类B里面,里面的那个类A就称为内部类(InnerClass)
,类B则称为外部类(OuterClass)
。
8.1.2 为什么要声明内部类呢
具体来说,当一个事物A的内部,还有一个部分需要一个完整的结构B进行描述,而这个内部的完整的结构B又只为外部事物A提供服务,不在其他地方单独使用,那么整个内部的完整结构B最好使用内部类。
总的来说,遵循高内聚、低耦合
的面向对象开发原则。
- 内部类使用举例:
- Thread类内部声明了State类,表示线程的生命周期
- HashMap类中声明了Node类,表示封装的key和value
8.1.3 内部类的分类
根据内部类声明的位置(如同变量的分类),我们可以分为:
8.2 成员内部类
8.2.1 概述
如果成员内部类中不使用外部类的非静态成员,那么通常将内部类声明为静态内部类,否则声明为非静态内部类。
语法格式:
[修饰符] class 外部类{[其他修饰符] [static] class 内部类{}
}
成员内部类的使用特征,概括来讲有如下两种角色:
- 成员内部类作为
类的成员的角色
:- 和外部类不同,Inner class还可以声明为private或protected;
- 可以调用外部类的结构。(
注意:在静态内部类中不能使用外部类的非静态成员
) - Inner class 可以声明为static的,但此时就不能再使用外层类的非static的成员变量;
- 成员内部类作为
类的角色
:- 可以在内部定义属性、方法、构造器等结构
- 可以继承自己的想要继承的父类,实现自己想要实现的父接口们,和外部类的父类和父接口无关
- 可以声明为abstract类 ,因此可以被其它的内部类继承
- 可以声明为final的,表示不能被继承
- 编译以后生成OuterClass$InnerClass.class字节码文件(也适用于局部内部类)
注意点:
-
外部类访问成员内部类的成员,需要“内部类.成员”或“内部类对象.成员”的方式
-
成员内部类可以直接使用外部类的所有成员,包括私有的数据
-
当想要在外部类的静态成员部分使用内部类时,可以考虑内部类声明为静态的
8.2.2 创建成员内部类对象
- 实例化静态内部类
外部类名.静态内部类名 变量 = 外部类名.静态内部类名();
变量.非静态方法();
- 实例化非静态内部类
外部类名 变量1 = new 外部类();
外部类名.非静态内部类名 变量2 = 变量1.new 非静态内部类名();
变量2.非静态方法();
8.2.3 举例
package com.atguigu09.inner;/*** ClassName: OuterClassTest* Description:** @Author 尚硅谷-宋红康* @Create 10:31* @Version 1.0*/
public class OuterClassTest {public static void main(String[] args) {//1. 创建Person的静态的成员内部类的实例Person.Dog dog = new Person.Dog();dog.eat(); //狗吃骨头//2. 创建Person的非静态的成员内部类的实例
// Person.Bird bird = new Person.Bird(); //报错Person p1 = new Person();Person.Bird bird = p1.new Bird();//正确的bird.eat(); //鸟吃虫子bird.show("黄鹂");bird.show1();}}
class Person{ //外部类String name = "Tom";int age = 1;//静态的成员内部类static class Dog{public void eat(){System.out.println("狗吃骨头");}}//非静态的成员内部类class Bird{String name = "啄木鸟";public void eat(){System.out.println("鸟吃虫子");}//3.内部类的方法中调用属性的情况:public void show(String name){ //内部类中的方法//成员内部类可以直接使用外部类的所有成员,包括私有的数据 (调用外部类中的成员变量)System.out.println("age = " + age);//age = 1 省略了Person.this//name相同时遵循就近原则 (调用内部类方法中的形参)System.out.println("name = " + name); //name = 黄鹂//通过this来区分成员和局部 (调用内部类的成员变量)System.out.println("name = " + this.name); //name = 啄木鸟//不是父子类关系不能用super (调用外部类的成员变量)System.out.println("name = " + Person.this.name);//name = Tom}//4.内部类的方法中调用方法的情况:public void show1(){ //内部类中的方法//在内部类的方法中调用内部类的另一个方法eat(); //鸟吃虫子this.eat();//this可省略 鸟吃虫子//在内部类的方法中调用外部类的方法Person.this.eat(); //人吃饭}}public void eat(){ //外部类中的方法System.out.println("人吃饭");}public void method(){//外部类中的方法(普通方法中定义的)//局部内部类class InnerClass1{}}public Person(){//外部类中的方法(构造方法中定义的)//局部内部类class InnerClass1{}}{//局部内部类 (外部类中的代码块中定义的)class InnerClass1{}}}
8.3 局部内部类
8.3.1 非匿名局部内部类
语法格式:
[修饰符] class 外部类{[修饰符] 返回值类型 方法名(形参列表){[final/abstract] class 内部类{}}
}
- 编译后有自己的独立的字节码文件,只不过在内部类名前面冠以外部类名、$符号、编号。
- 这里有编号是因为同一个外部类中,不同的方法中存在相同名称的局部内部类
- 和成员内部类不同的是,它前面不能有权限修饰符等
- 局部内部类如同局部变量一样,有作用域
- 局部内部类中是否能访问外部类的非静态的成员,取决于所在的方法
举例:
package com.atguigu09.inner;/*** ClassName: OuterClassTest1* Description:** @Author 尚硅谷-宋红康* @Create 10:55* @Version 1.0*/
public class OuterClassTest1 {//说明:局部内部类的使用public void method1(){//局部内部类class A{//可以声明属性、方法等}}//开发中的场景:调用方法的时候返回一个实例,这个实例的类型是接口public Comparable getInstance(){//提供了实现了Comparable接口的类//方式1:提供了接口的实现类的对象
// class MyComparable implements Comparable{
// @Override
// public int compareTo(Object o) {
// return 0;
// }
// }
//
// MyComparable m = new MyComparable();
// return m;//方式2:提供了接口的实现类的匿名对象
// class MyComparable implements Comparable{
//
// @Override
// public int compareTo(Object o) {
// return 0;
// }
// }
//
// return new MyComparable();//方式3:提供了接口的匿名实现类的对象
// Comparable c = new Comparable(){
// @Override
// public int compareTo(Object o) {
// return 0;
// }
// };
// return c;//方式4:提供了接口的匿名实现类的匿名对象return new Comparable(){@Overridepublic int compareTo(Object o) {return 0;}};}}
8.3.2 匿名内部类
因为考虑到这个子类或实现类是一次性的,那么我们“费尽心机”的给它取名字,就显得多余。那么我们完全可以使用匿名内部类的方式来实现,避免给类命名的问题。
new 父类([实参列表]){重写方法...
}
new 父接口(){重写方法...
}
举例1:使用匿名内部类的对象直接调用方法:
interface A{void a();
}
public class Test{public static void main(String[] args){new A(){@Overridepublic void a() {System.out.println("aaaa");}}.a();}
}
举例2:通过父类或父接口的变量多态引用匿名内部类的对象
interface A{void a();
}
public class Test{public static void main(String[] args){A obj = new A(){@Overridepublic void a() {System.out.println("aaaa");}};obj.a();}
}
举例3:匿名内部类的对象作为实参
interface A{void method();
}
public class Test{public static void test(A a){a.method();}public static void main(String[] args){test(new A(){@Overridepublic void method() {System.out.println("aaaa");}});}
}
8.4 练习
练习:判断输出结果为何?
public class Test {public Test() {Inner s1 = new Inner();s1.a = 10;Inner s2 = new Inner();s2.a = 20;Test.Inner s3 = new Test.Inner();System.out.println(s3.a);}class Inner {public int a = 5;}public static void main(String[] args) {Test t = new Test();Inner r = t.new Inner();System.out.println(r.a);}
}
练习2:
编写一个匿名内部类,它继承Object,并在匿名内部类中,声明一个方法public void test()打印尚硅谷。
请编写代码调用这个方法。
package com.atguigu.test01;public class Test01 {public static void main(String[] args) {new Object(){public void test(){System.out.println("尚硅谷");}}.test();}
}