Java之异常和泛型
1异常的概念
1、异常是程序运行过程中产生的一种事件,该事件会打乱程序的正常流程。
2、异常,是一种意外,是指程序没有按照正常或期望的方式执行。
3、数组越界,空指针等等,都是异常事件。
4、当异常产生时,运行时系统会自动创建一个异常类的对象,该对象含有异常产生的相关信息。如果程序没有对异常进行处理,异常产生之后的语句都将不会得到执行。同时,异常会沿着方法的调用向上进行传播(传播给方法的调用端)。如果到了main方法,还没有处理该异常,则main方法会将异常传播给JVM,JVM就会终止当前线程的执行。然后,会在控制台打印出异常产生堆栈的轨迹。(这个轨迹就是方法调用的轨迹)
如果对程序中产生的异常进行了捕获,虽然异常之后的语句会得到执行。但是需要注意的是,位于异常产生之后,异常处理之前的语句仍然不会得到执行。
如下面的例子。
package cn.ancony.exceptionTest; |
程序的输出为:
Deal exception After exception, after catch. |
2异常产生的后果
一旦引发异常,而程序没有对其进行处理,程序(线程)将终止执行。异常处理的好坏关系到系统的健壮性和稳定度。
异常可以分为三种:
1、受检异常
2、运行时异常
3、错误
说明:运行时异常和错误统称为非受检异常。
3异常的层次结构
其中,受检异常要求必须在编译期间对其进行明确的处理。而非受检异常无此要求。如果方法中会产生受检异常,则所有的受检异常必须同时声明在方法的异常列表中。
4 Java常见的异常类
5异常的处理
当异常发生的时候,我们可以采用以下两种方式进行处理:
1、捕获异常
2、抛出异常
说明:受检异常要求程序员在编译时显式处理,而非受检异常则不需要。
6捕获异常
1、当异常产生时,就会创建一个相关异常类的对象。该对象包含异常对象的相关信息。
2、运行时系统会尝试搜索相应的代码块来处理该异常(异常处理器),搜索顺序与方法调用的顺序相反。
3、异常处理器(程序)使用try-catch-finally的方式对异常进行捕获。
4、使用try去执行可能产生异常的程序,一旦异常发生,系统会自动创建一个相应的异常类对象,同时终止try的执行(try语句块异常产生位置之后的代码将不会执行)。
5、一旦产生异常,将会从上到下去匹配catch中的异常类,一旦匹配成功,则异常对象就会传递给catch参数,执行相应catch语句块。如果没有匹配成功,则该异常会继续向上抛出给方法的调用端。
6、catch语句块最多只会执行一个。如果所有方法中都没有合适的异常处理器,则当前的线程终止。
7、无论try是否抛出异常,也无论catch是否捕获异常,finally语句块都将在try或catch执行后执行。(但是,虚拟机退出的情况除外。只有这一种情况,finally里面的语句得不到执行)
例如:
package cn.ancony.exceptionTest; |
上述程序什么也不会输出。
又如:
package cn.ancony.exceptionTest; |
上述程序输出如下:
Exception in thread "main" java.lang.ArithmeticException: / by zero at cn.ancony.exceptionTest.ExceptionTest.main(ExceptionTest.java:11) finally run. |
8、catch捕获的异常类型必须是Throwable类型或其子类型。
9、finally是可选的。catch也是可选的,但是catch与finally不能同时为空。
7多重catch块
1、一段代码可能会产生多个异常,当引发异常的时候,会按顺序来查看每个catch语句,并执行第一个类型与异常类型匹配的语句。执行其中的一条catch语句之后,其他的catch语句将被忽略。
2、当捕获到一个异常的时候,剩下的catch语句就不再进行匹配,所以在安排catch语句的顺序的时候,首先应该捕获最特殊最容易出现的异常,然后再逐渐一般化。也就是说,一定要先捕获子类异常,再捕获父类异常,即异常子类一定要位于异常父类之前。
3、如果两个异常类没有继承关系,那么catch的顺序不重要。
8 finally块
1、与try块一起使用,无论是否出现异常,finally块都将运行。一个异常处理程序只有一个finally块,但并不强制必须要有finally块。
2、介于finally的特征,其确保了在出现异常时所有清除工作都将得到处理。因此,使用finally来处理一些收尾工作是非常合适的。比如,关闭数据库的连接,流连接等资源。
finally的示例。
1、finally会镇压break。
package cn.ancony.exceptionTest; |
程序输出如下:
finally run |
2、finally先于return执行。
package cn.ancony.exceptionTest; |
程序输出:
finally run 1 |
3、finally中的return会镇压try中的return。
package cn.ancony.exceptionTest; |
程序输出:
2 |
9多重捕获
1、当两个或者多个异常的处理方式相同的时候,我们可以使用多重捕获。使用多重捕获可以减少代码量,并且可以避免捕获过多的异常。
2、当使用多重异常捕获时,catch的参数将隐式使用final修饰。
10try-with-resources
可以使用含资源的try来实现资源的自动关闭。
11抛出异常
除了捕获异常意外,我们还可以使用throw手动抛出异常。
1、手动抛出异常后,将终止当前的方法,由方法的调用端进行处理。
2、throw抛出的异常类型需要是Throwable或其子类型。
12 throws
在方法内使用throw抛出异常时,如果该异常是受检异常,我们还需要在方法声明处使用throws声明方法可能抛出的异常列表。如果是非受检异常,则不需要声明。
13关于方法重写的要求
当子类重写父类的方法时,要求子类不能比父类方法抛出更多的受检异常。是为什么呢?
因为Java规定,受检异常是程序员必须在编程时处理的。如果子类抛出了父类没有抛出的异常,这意味着,该异常必须在父类中得到处理。而父类很可能不受我们的控制,也就导致在父类中不能处理该异常。父类不能处理该异常,则父类必须抛出该异常。这与现实相矛盾。所以Java规定,重写时,子类不能比父类抛出更多的受检异常。而非受检异常(比如运行时异常和错误)则不受这条规则的约束。
14自定义异常
尽管Java提供了非常多的异常类,但是,这并不能满足我们所有的要求。我们有时需要根据情况创建自定义异常类。
自定义的异常类需要继承Throwable类(或其子类)。
自定义受检异常需要继承Exception或者其子类。
自定义运行时异常需要继承RuntimeException或者其子类。
自定义的异常如果是受检异常,同样需要使用try—catch捕获。
自定义异常的命名惯例是使用Exception结尾。
示例:
package cn.ancony.exceptionTest; |
程序运行结果如下:
Exception in thread "main" cn.ancony.exceptionTest.NegtiveAgeException: The age is negtive. at cn.ancony.exceptionTest.Server.setAge(ExceptionTest.java:11) at cn.ancony.exceptionTest.ExceptionTest.main(ExceptionTest.java:5) |
15异常的优势
1可以将正常的代码和异常代码处理的代码相分离。
2可以将异常信息沿着方法调用轨迹向上传播。
3异常具有明确的类型与继承体系,便于分类处理。
16 泛型的产生
1在JDK5.0之前,为了使集合具有通用性,能够处理各种类型,只能将相关的类型设计为Object类型。但是,这种刷机会将所有类型都视为Object,这样会带来一定的繁琐和不安全性。
2Java的泛型设计参考了C++中的模板,通过泛型,可以为编译期间提供严格的类型检查,并且在运行时不会出现类型转换的异常。
17泛型的概念
1、泛型就是在声明类,接口或者方法的时候,同时附带声明一个或者多个类型参数(即形式类型参数),然后在使用该类型或者在调用方法的时候,为类型参数提供(指定)相应的类型(即实际类型参数)。
2、类型参数使用<类型参数>表示,按照惯例,类型参数使用一个大写字母命名。
下面是常用的类型参数:
T:代表Type,类型
E:代表Element,元素
K:代表key,键
V:代表value,值
3、类型参数可以类比方法参数列表中声明的参数。这与方法声明与调用非常相似。不同在于,方法调用时,我们传递的实际参数是某个具体的值,而使用泛型时,我们传递的实际类型参数是某个具体的类型。
4、使用泛型类型,并提供相应的类型参数(实际类型参数),这样的类型我们称为参数化类型。
5、在使用泛型后,编译器可以进行严格的类型检查。并且无需繁琐的转换操作。
说明:类型参数可以是任意的引用类型。但不可以是基本数据类型。
18原生类型
1、与参数化类型相对应,如果在使用泛型类型的时候,并没有提供任何类型参数,则称这种类型为原生类型。
2、原生类型只是作为一种历史遗留的产物,之所以还可以使用,完全是为了向前兼容。
3、我们应该总是为泛型类型提供相应的类型参数,而避免使用原生类型。
4、将参数化类型赋值给原生类型或者使用原生类型调用含有类型参数的方法将会产生未经检查的警告信息。
19类型推断
从JDK7开始,编译器改进了类型推断的能力。
List<String> list=newArrayList<String>();
可以写为:
List<String> list=new ArrayList<>();
编译器根据引用类型参数的类型,能够自动推断出对象的类型参数为String类型。
20参数化类型不具有继承性
1、尽管String是Object的子类型,但是List<String>不是List<Object>的子类型。
2、而数组类型则具有继承性。存在一种类型A,就会存在对应的数组类型A[]。
如果A是B的子类型,那么A[]也是B[]的子类型。
package cn.ancony.generic; |
程序输出true。
21类型通配符
类型通配符?表示一种不确定的类型。它可以是一种任意的类型。
List<String> strList=new ArrayList<>();
List<?> list=strList;
一个?表示无界通配符。还可以使用有界通配符。通配符的种类:
1、无界通配符<?>
2、<?extends X>含有上界的通配符。表示可以是任意的X或者其子类型。
3、<?Super X>含有下界的通配符,表示可以是任意的X或者其父类型。
虽然通配符表示一种类型,但是这个类型却是一种不确定的类型,因此,当集合类的类型参数使用通配符的时候,加入元素会受到一定的限制。
形式类型参数和通配符的区别:
1、通配符既可以含有上界,也可以含有下界。
形式类型参数只能含有上界,不能含有下界。
2、形式类型参数可以作为一种类型而存在,通配符不能作为类型存在。
例如: ?t是错误的,但是T t是正确的。
3、形式类型参数可以多个上界,通配符只能含有一个上界。
形式类型参数含有多个上界时的顺序要求:
1 、如果两个上界都是接口类型,那么没有顺序的要求。
2、如果两个上界一个是类类型,一个是借口类型,则类必须在接口的前面。
例如:
class Animal{} |
如上,Monkey2和Rest类都是含有形式类型参数的泛型类。
22自定义泛型类
1、我们可以自定义泛型类。只需要提供相应的类型参数(形式类型参数)即可。类型参数可以含有多个,以“,”进行分隔。
2、在声明类型引用和使用new创建对象的时候,将实际的类型参数传递给形式类型参数。
3、类型参数也可以像通配符一样界定(extends),但是不可以指定super。
4、类型参数可以指定多个上限。
5、泛型类只有一个,不会因为类型参数的不同而不同。(擦除)
23泛型方法和泛型构造器
1、泛型方法需要提供类型参数,类型参数放在返回类型前。
2、可以声明泛型构造器。泛型构造器的类型参数在构造器名的前面声明。
package cn.ancony.generic; |
当类是泛型类,构造器又是泛型构造器,而又显式提供了泛型构造器的类型参数的时候,new创建对象后的类型参数也需要显式指定。
在new创建对象后指定了类型参数的时候就不再报错了。
24类型擦除
1、在Java中,泛型设计的类型检查仅仅是在编译的期间。在编译过后(运行期间),所有的类型参数都会丢失。也就是说,我们没有办法在运行的时候,获得泛型真实的类型信息。
2、在编译后的class(字节码)文件中,类型参数会替换成其所能表示的上限的类型。参数化类型将使用原生类型来表示。
类型擦除的替换:
1、参数化类型使用原生类型来代替。
2、形式类型参数,使用相应的上界进行替换。
>无界的类型参数,使用Object进行替换。
>含有一个上界的类型参数,使用其上界进行替换。
>含有多个上界的类型参数,使用第一个上界进行替换。
25泛型方法的重载与重写
1、当方法含有类型参数的时候,能否重载成功,我们要以擦除后的类型来看待。
2、当子类重写父类的方法时,要求子类的参数列表与父类的相同,或者与父类擦除后的参数列表相同。