10.2.5 使用finally回收资源

垃圾回收机制不会回收物理资源

Java的垃圾回收机制不会回收任何物理资源,垃圾回收机制只能回收堆内存对象所占用的内存

try块中打开的物理资源由程序员显示回收

程序在try块里打开了一些物理资源(例如数据库连接、网络连接和磁盘文件等),这些物理资源都必须显式回收。

不可以在try块或者catch块中回收物理资源

  • 假设程序在try块里进行资源回收,如果try块的某条语句引起了异常,该语句后的其他语句通常不会获得执行的机会,这将导致位于该语句之后的资源回收语句得不到执行。
  • 如果在catch块里进行资源回收,但catch块完全有可能得不到执行,这将导致不能及时回收这些物理资源。

finally块专门用来回收物理资源

为了保证一定能回收try块中打开的物理资源,异常处理机制提供了finally块。不管try块中的代码是否出现异常,也不管哪一个catch块被执行,甚至在try块或catch块中执行了return语句, finally块总会被执行

finally块总会被执行,即使在try块或catch块中执行了return语句,finally块依然被执行

完整的java异常处理语法

完整的Java异常处理语法结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
try
{
//业务实现代码。..
}
catch(SubException1 e)
{
//异常处理块1
}
catch(SubException2 e)
{
//异常处理块2
}
...
catch(Exception e)
{
//异常处理块2
}
finally
{
//资源回收块
}

try块必须出现 catch块和finally块可选

  • 异常处理语法结构中只有try块是必需的,也就是说,如果没有try块,则不能有后面的catch块和finally块;
  • catch块和finally块都是可选的,但catch块和finally块至少出现其中之一,也可以同时出现;
  • 可以有多个catch块,捕获父类异常的catch块必须位于捕获子类异常的后面;
  • 但不能只有try块,既没有catch块,也没有finally块;
  • 多个catch块必须位于try块之后,
  • finally块必须位于所有的catch块之后。

看如下程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.io.*;

public class FinallyTest
{
public static void main(String[] args)
{
FileInputStream fis = null;
try
{
fis = new FileInputStream("a.txt");
}
catch (IOException ioe)
{
System.out.println(ioe.getMessage());
// return语句强制方法返回
return ; // 1
// 使用exit来退出虚拟机
// System.exit(1); // 2
}
finally
{
// 关闭磁盘文件,回收资源
if (fis != null)
{
try
{
fis.close();
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
}
System.out.println("finally块被执行!");
}
}
}

即使try块 catch块中有return语句 finally块一样得到执行

上面程序的try块后增加了finally块,用于回收在try块中打开的物理资源。注意程序的catch块中1号代码处有一条return语句,该语句强制方法返回。
在通常情况下,一旦在方法里执行到return语句的地方,程序将立即结束该方法

trycatch块中,虽然return语句会强制结束方法,但一定会先执行finally块里的代码。运行上面程序,看到如下结果:

1
2
a.txt (系统找不到指定的文件。)
finally块被执行!

上面运行结果表明方法返回之前还是执行了finally块的代码

如果异常处理代码中退出虚拟机 则finally将失去执行的机会

将1处的return语句注释掉,取消2处代码的注释,即在异常处理的catch块中使用System.exit(1)语句来退出虚拟机。执行上面代码,看到如下结果:

1
a.txt (系统找不到指定的文件。)

上面执行结果表明finally块没有被执行。如果在异常处理代码中使用System.exit()语句来退出虚拟机,则finally块将失去执行的机会

除非在try块,catch块中调用了退出虛拟机的方法,finally才会因为失去执行的机会,而得不到执行。否则不管在try块、 catch块中执行怎样的代码,出现怎样的情况,异常处理的finally块总会被执行

finally块的return,throw语句会覆盖try catch块的return throw

在通常情况下,不要在finally块中使用如returnthrow等导致方法终止的语句,一旦在finally块中使用了returnthrow语句,将会导致try块、 catch块中的returnthrow语句失效。看如下程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class FinallyFlowTest
{
public static void main(String[] args)
throws Exception
{
boolean a = test();
System.out.println(a);
}
public static boolean test()
{
try
{
// 因为finally块中包含了return语句
// 所以下面的return语句失去作用
return true;
}
finally
{
return false;
}
}
}

上面程序在finally块中定义了一个return false语句,这将导致try块中的return true失去作用。运行上面程序,将打印出false的结果。

try块或catch块中有return或throw语句的情况

Java程序执行try块、 catch块时遇到了returnthrow语句,这两个语句都会导致该方法立即结束,但是系统执行这两个语句是会先去寻找该异常处理流程中是否包含finally块。

  • 如果没有finally块,程序立即执行returnthrow语句,方法终止;
  • 如果有finally块,系统立即开始执行finally块,只有当finally块执行完成后,系统才会再次跳回来执行try块、 catch块里的returnthrow语句;
    • 如果finally块里也使用了returnthrow等导致方法终止的语句, finally块已经终止了方法,系统将不会跳回去执行try块、 catch块里的任何代码。

所以,尽量避免在fnly块里使用 returthrow等导致方法终止的语句,否则可能出现

10.2.4 访问异常信息

如果程序需要在catch块中访问异常对象的相关信息,则可以通过访问catch块的后异常形参来获得。当Java运行时决定调用某个catch块来处理该异常对象时,会将异常对象赋给catch块后的异常参数,程序即可通过该参数来获得异常的相关信息。

异常对象的常用方法

所有的异常对象都包含了如下几个常用方法。

方法 描述
getMessage() 返回该异常的详细描述字符串。
printStackTrace() 将该异常的跟踪栈信息输出到标准错误输出。
printStackTrace(PrintStream s) 将该异常的跟踪栈信息输出到指定输出流。
getStackTrace() 返回该异常的跟踪栈信息。

下面例子程序演示了程序如何访问异常信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.io.*;

public class AccessExceptionMsg
{
public static void main(String[] args)
{
try
{
FileInputStream fis = new FileInputStream("a.txt");
}
catch (IOException ioe)
{
System.out.println(ioe.getMessage());
ioe.printStackTrace();
}
}
}

上面程序调用了Exception对象的getMessage()方法来得到异常对象的详细信息,也使用了printStackTrace()方法来打印该异常的跟踪信息。运行上面程序,控制台输出如下:

1
2
3
4
5
6
7
a.txt (系统找不到指定的文件。)
java.io.FileNotFoundException: a.txt (系统找不到指定的文件。)
at java.io.FileInputStream.open0(Native Method)
at java.io.FileInputStream.open(FileInputStream.java:195)
at java.io.FileInputStream.<init>(FileInputStream.java:138)
at java.io.FileInputStream.<init>(FileInputStream.java:93)
at AccessExceptionMsg.main(AccessExceptionMsg.java:9)

控制台的输出可以看到异常的详细描述信息:a.txt (系统找不到指定的文件。),这就是调用异常的getMessage()方法返回的字符串。下面更详细的信息是该异常的跟踪栈信息,关于异常的跟踪栈信息后面还有更详细的介绍,此处不再赘述。

10.2.3 Java 7提供的多异常捕获机制

Java 7以前,每个catch块只能捕获一种类型的异常;但Java 7开始,一个catch块可以捕获多种类型的异常
使用一个catch块捕获多种类型的异常时需要注意如下两个地方。

  • 捕获多种类型的异常时,多种异常类型之间用竖线(|)隔开
  • 捕获多种类型的异常时,异常变量有隐式的final修饰,因此程序不能对异常变量重新赋值

示例程序

下面程序示范了Java 7提供的多异常捕获。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class MultiExceptionTest
{
public static void main(String[] args)
{
try
{
int a = Integer.parseInt(args[0]);
int b = Integer.parseInt(args[1]);
int c = a / b;
System.out.println("您输入的两个数相除的结果是:" + c );
}
catch (IndexOutOfBoundsException|NumberFormatException
|ArithmeticException ie)
{
System.out.println("程序发生了数组越界、数字格式异常、算术异常之一");
// 捕捉多异常时,异常变量默认有final修饰,
// 所以下面代码有错:
ie = new ArithmeticException("test"); // 1
}
catch (Exception e)
{
System.out.println("未知异常");
// 只捕捉一个类型的异常时,异常变量没有final修饰
// 所以下面代码完全正确。
e = new RuntimeException("test"); // 2
}
}
}

上面程序中使用了IndexOutOfBoundsException|NumberFormatException|ArithmeticException来定义异常类型,这就表明该catch块可以同时捕获这三种类型的异常。

单异常捕获可以对异常变量重新赋值

  • 捕获多种类型的异常时,异常变量使用隐式的final修饰final修饰的变量初始化后就无法更改,因此上面程序中1号代码将产生编译错误;
  • 捕获一种类型的异常时,异常变量没有final修饰,因此上面程序中2号代码完全正确。

总结

Java 7catch块可以捕获多个异常,捕获多异常时,每个异常以竖杠分隔,多个异常变量隐式使用final修饰,不可以在捕获多异常的catch块中给异常变量重新赋值

10.2.2 异常类的继承体系

Java运行时环境接收到异常对象后,会依次判断该异常对象是否是catch块后异常类或其子类的实例,如果是,Java运行时环境将调用该catch块来处理该异常;如果不是,则再次拿该异常对象和下一个catch块里的异常类进行比较。Java异常捕获流程示意图如图10.1所示。

这里有一张图片

当程序进入负责异常处理的catch块时,系统生成的异常对象ex将会传给catch块后的异常形参,从而允许catch块通过该对象来获得异常的详细信息。

从图10.1中可以看出,try块后可以有多个catch块,这是为了针对不同的异常类提供不同的异常处理方式。当系统发生不同的意外情况时,系统会生成不同的异常对象,Java运行时就会根据该异常对象所属的异常类来决定使用哪个catch块来处理该异常。

通过在try块后提供多个catch块可以无须在异常处理块中使用ifswitch判断异常类型,但依然可以针对不同的异常类型提供相应的处理逻辑,从而提供更细致、更有条理的异常处理逻辑。

出现异常时只会执行多个catch块中的一个

从图10.1中可以看出,在通常情况下,如果try块被执行一次,则try块后只有一个catch块会被执行,绝不可能有多个catch块被执行。除非在循环中使用了continue开始下一次循环,下一次循环又重新运行了try块,这才可能导致多个catch块被执行,但对应其中一次循环还是只会有一个catch块被执行。

不可省略try块和catch块后面的花括号

try块与if语句不一样,try块后的花括号{...}不可以省略,即使try块里只有一行代码,也不可省略这个花括号。与之类似的是, catch块后的花括号({...})也不可以省略。

try块中声明的变量只在该try块中有效

try块里声明的变量是代码块内局部变量,它只在try块内有效,在catch块中不能访问该变量。

Java提供了丰富的异常类,这些异常类之间有严格的继承关系,图10.2显示了Java常见的异常类:

这里有一张图片

从图10.2中可以看出,Java把所有的非正常情况分成两种:异常( Exception)和错误(Error),它们都继承Throwable父类。

错误

错误Error,一般是指与虚拟机相关的问题,如系统崩溃、虚拟机错误、动态链接失败等,这种错误无法恢复或不可能捕获,将导致应用程序中断。通常应用程序无法处理这些错误,因此应用程序不应该试图使用catch块来捕获Error对象。在定义该方法时,也无须在其throws子句中声明该方法可能抛出Error及其任何子类。下面看几个简单的异常捕获例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class DivTest
{
public static void main(String[] args)
{
try
{
int a = Integer.parseInt(args[0]);
int b = Integer.parseInt(args[1]);
int c = a / b;
System.out.println("您输入的两个数相除的结果是:" + c );
}
catch (IndexOutOfBoundsException ie)
{
System.out.println("数组越界:运行程序时输入的参数个数不够");
}
catch (NumberFormatException ne)
{
System.out.println("数字格式异常:程序只能接受整数参数");
}
catch (ArithmeticException ae)
{
System.out.println("算术异常");
}
catch (Exception e)
{
System.out.println("未知异常");
}
}
}

上面程序针对IndexOutOfBoundsExceptionNumberFormatExceptionArithmeticException类型的异常,提供了专门的异常处理逻辑。Java运行时的异常处理逻辑可能有如下几种情形。

  • 如果运行该程序时输入的参数不够,将会发生数组越界异常,Java运行时将调用IndexOutOfBoundsException对应的catch块处理该异常。
  • 如果运行该程序时输入的参数不是数字,而是字母,将发生数字格式异常,Java运行时将调用NumberFormatException对应的catch块处理该异常。
  • 如果运行该程序时输入的第二个参数是0,将发生除0异常,Java运行时将调用ArithmeticException对应的catch块处理该异常。
  • 如果程序运行时出现其他异常,该异常对象总是Exception类或其子类的实例,Java运行时将调用Exception对应的catch块处理该异常。

上面程序中的三种异常,都是非常常见的运行时异常,读者应该记住这些异常,并掌握在哪些情况下可能出现这些异常。

常见运行时异常

1
2
3
IndexOutOfBoundsException
NumberFormatException
ArithmeticException

NullPointerException

什么时候会引发NullPointerException异常

当试图调用一个null对象的实例方法或实例变量时,就会引发NullPointerException异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.*;

public class NullTest
{
public static void main(String[] args)
{
Date d = null;
try
{
System.out.println(d.after(new Date()));
}
catch (NullPointerException ne)
{
System.out.println("空指针异常");
}
catch(Exception e)
{
System.out.println("未知异常");
}
}
}

上面程序针对NullPointerException异常提供了专门的异常处理块。上面程序调用一个null对象的after()方法,这将引发NullPointerException异常,Java运行时将会调用NullPointerException对应的catch块来处理该异常;如果程序遇到其他异常,Java运行时将会调用最后的catch块来处理异常。

先捕获小异常 再捕获大异常

正如在前面程序所看到的,程序总是把对应Exception类的catch块放在最后,这是为什么呢?想下图10.1所示的Java异常捕获流程,读者可能明白原因:

如果把Exception类对应的catch块排在其他catch块的前面,Java运行时将直接进入该catch块(因为所有的异常对象都是Exception或其子类的实例),而排在它后面的catch块将永远也不会获得执行的机会。

实际上,进行异常捕获时不仅应该把Exception类对应的catch块放在最后,而且所有父类异常的catch块都应该排在子类异常catch块的后面(简称:先处理小异常,再处理大异常),否则将出现编译错误。看如下代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try
{
...
}
catch(RuntimeException e)
{
//代码1
System.out.println("运行时异常");
}
catch(NullPointerException e)
{
// 代码2
System.out.println("空指针异常");
}

上面代码中有两个catch块,前一个catch块捕获RuntimeException异常,后一个catch块捕获NullPointerException异常,编译上面代码时将会在 代码2 处出现已捕获到异常java.lang.NullPointerException的错误,因为 代码1 处的RuntimeException已经包括了NullPointerException异常,所以 代码2 处的catch块永远也不会获得执行的机会。

注意
异常捕获时,一定要记住先捕获小异常,再捕获大异常。

10.2 异常处理机制

Java的异常处理机制可以让程序具有极好的容错性,让程序更加健壮。当程序运行出现意外情形时,系统会自动生成一个Exception对象来通知程序,从而实现将”业务功能实现代码”和”错误处理代码”分离,提供更好的可读性。

10.2.1 使用try…catch捕获异常

下面是Java异常处理机制的语法结构:

1
2
3
4
5
6
7
8
9
10
try
{
//业务实现代码
...
}
catch (Exception e)
{
//错误处理代码
...
}

抛出异常的过程

如果执行try块里的业务逻辑代码时出现异常,系统自动生成一个异常对象,该异常对象被提交给Java运行时环境,这个过程被称为抛出(throw)异常。

捕获异常的过程

Java运行时环境收到异常对象时,会寻找能处理该异常对象的catch块,
如果找到合适的catch块,则把该异常对象交给该catch块处理,这个过程被称为捕获(catch)异常;
如果Java运行时环境找不到捕获异常的catch块,则运行时环境终止,Java程序也将退出。

出现异常时没有处理该异常的catch块时程序直接退出

不管程序代码块是否处于try块中,甚至包括catch块中的代码,只要执行该代码块时出现了异常,系统总会自动生成一个异常对象。如果程序没有为这段代码定义任何的catch块,则Java运行时环境无法找到处理该异常的catch块,程序就在此退出。

本章要点

  • 异常的定义和概念
  • Java异常机制的优势
  • 使用try...catch捕获异常
  • 多异常捕获
  • Java异常类的继承体系
  • 异常对象的常用方法
  • finally块的作用
  • 自动关闭资源的try语句
  • 异常处理的合理嵌套
  • Checked异常和Runtime异常
  • 使用throws声明异常
  • 使用throw抛出异常
  • 自定义异常
  • 异常链和异常转译
  • 异常的跟踪栈信息
  • 异常的处理规则

10.0 疯狂java讲义4 第10章 前言

异常机制已经成为判断一门编程语言是否成熟的标准,除传统的像C语言没有提供异常机制之外目前主流的编程语言如JavaC#RubyPython等都提供了成熟的异常机制。异常机制可以使程序中的异常处理代码正常业务代码分离,保证程序代码更加优雅,并可以提高程序的健壮性。

java异常处理用到的关键字

Java的异常机制主要依赖于trycatchfinallythrowthrows五个关键字,其中

  • try关键字后紧跟一个花括号扩起来的代码块(花括号不可省略),简称try块,它里面放置可能引发异常的代码。
  • catch后对应异常类型和一个代码块,用于表明该catch块用于处理这种类型的代码块。多个catch块后还可以跟一个finally块,finally块用于回收在try块里打开的物理资源,异常机制会保证finally块总被执行。
  • throws关键字主要在方法签名中使用,用于声明该方法可能抛出的异常
  • throw用于抛出一个实际的异常throw可以单独作为语句使用,抛出一个具体的异常对象。

Java7异常处理增强

Java7进一步增强了异常处理机制的功能,包括带资源的try语句捕获多异常的catch两个新功能,这两个功能可以极好地简化异常处理。

Java异常分类

开发者都希望所有的错误都能在编译阶段被发现,就是在试图运行程序之前排除所有错误,但这是不现实的,余下的问题必须在运行期间得到解决。**Java将异常分为两种,Checked异常和Runtime异常**,Java认为Checked异常都是可以在编译阶段被处理的异常,所以:

  • Checked异常需要程序显示处理
  • Runtime异常则无须处理

Checked异常可以提醒程序员需要处理所有可能发生的异常,但Checked异常也给编程带来一些烦琐之处,所以Checked异常也是Java领域一个备受争论的话题

9.7 本章小结

  • 本章主要介绍了Java提供的泛型支持,还介绍了为何需要在编译时检查集合元素的类型,以及如何编程来实现这种检査,从而引出Java泛型给程序带来的简洁性和健壮性。
  • 本章详细讲解了如何定义泛型接口、泛型类,以及如何从泛型类、泛型接口派生子类或实现类,并深入讲解了泛型类的实质。
  • 本章介绍了类型通配符的用法,包括设定类型通配符的上限、下限等;
  • 本章重点介绍了泛型方法的知识,包括如何在方法签名时定义泛型形参,以及泛型方法和类型通配符之间的区别与联系。
  • 本章最后介绍了Java不支持创建泛型数组,并深入分析了原因

9.6 泛型与数组

Java泛型有一个很重要的设计原则—如果——段代码在编译时没有提出“unchecked未经检查的转换”警告,则程序在运行时不会引发ClassCastException异常

正是基于这个原因,所以数组元素的类型不能包含泛型变量或泛型形参,除非是无上限的类型通配符。但可以声明元素类型包含泛型变量或泛型形参的数组。也就是说,只能声明List<String>形式的数组,但不能创建ArrayList<String>[10]这样的数组对象

不能创建ArrayList[10]这样的数组对象

反证法

假设Java支持创建ArrayList<String>[10]这样的数组对象,则有如下程序:

1
2
3
4
5
6
7
8
9
10
11
// 下面代码实际上是不允许的
List<String>[] lsa= new ArrayList<String>[10];// 1号代码
// 将lsa向上转型为Object[]类型的变量
Object[] oa = lsa;
List<Integer> li = new ArrayList<>();
li.add(3);
// 将List<Integer>对象作为oa的第二个元素
// 下面代码也不会有任何警告,但将引发ClassCastException异常
oa[1] = li;
// 下面代码引起ClassCastException异常
String s = lsa[1].get(0); // 2号代码

在上面代码中,如果1号代码是合法的,经过中间系列的程序运行,势必在2号代码处引发运行时异常,这就违背了Java泛型的设计原则。

如果将程序中的1代码:

1
List<String>[] lsa= new ArrayList<String>[10];// 1号代码

改为:

1
List<String>[] lsa=new ArrayList[10];

其他不变:

1
2
3
4
5
6
7
8
9
//下面代码编译时有“ [unchecked]未经检査的转换”警告
List<String>[] lsa = new ArrayList[10];//1号代码
// 将lsa向上转型为Object[]类型的变量
Object[] oa=lsa;
List<Integer> li = new ArrayList<>();
li.add(3);
oa[1] = li;
// 下面代码引起ClassCastException异常
String s = lsa[1].get(0);//2号代码

上面程序1号代码声明了List<String>类型的数组变量,这是允许的;但不允许创建List<String>囗类型的对象,所以创建了一个类型为ArrayList[10]的数组对象,这也是允许的。

只是把ArrayList[10]对象赋给List<String>口变量时会有编译警告“unchecked]未经检査的转换”’即编译器并不保证这段代码是类型安全的。
上面代码同样会在2号代码引发运行时异常,但因为编译器已经提出了警告,所以完全可能出现这种异常。

可以创建无上限的通配符泛型数组

Java允许创建无上限的通配符泛型数组,例如new ArrayList<?>[10],因此也可以将第一段代码改为使用无上限的通配符泛型数组,在这种情况下,程序不得不进行强制类型转换。正如前面所介绍的,在进行强制类型转换之前应通过instanceof运算符来保证它的数据类型。将上面代码改为如下形式:

1
2
3
4
5
6
7
8
9
10
List<?>[] lsa = new ArrayList<?>[10];
Object[] oa = lsa;
List<Integer> li = new ArrayList<>();
li.add(3);
oa[1] = li;
Object target = lsa[1].get(0);
if (target instanceof String) {
// 下面代码安全了
String s = (String) target;
}

不能创建元素类型是泛型的数组

与此类似的是,创建元素类型是泛型类型的数组对象也将导致编译错误。如下代码所示:

1
2
3
4
<T> T[] makeArray (Collection<T> coll){
//下面代码导致编译错误
return new T [coll.size()];
}

9.5 擦除和转换

在严格的泛型代码里,带泛型声明的类总应该带着类型参数。但为了与老的Java代码保持一致,也允许在使用带泛型声明的类时不指定实际的类型。如果没有为这个泛型类指定实际的类型,此时被称作raw type(原始类型),默认是声明该泛型形参时指定的第一个上限类型。

当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,所有在尖括号之间的类型信息都将被扔掉。比如一个List<String>类型被转换为List,则该List对集合元素的类型检查变成了泛型参数的上限(即Object)。下面程序示范了这种擦除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Apple<T extends Number>
{
T size;
public Apple()
{
}
public Apple(T size)
{
this.size = size;
}
public void setSize(T size)
{
this.size = size;
}
public T getSize()
{
return this.size;
}
}
public class ErasureTest
{
public static void main(String[] args)
{
Apple<Integer> a = new Apple<>(6); // 代码1
// a的getSize方法返回Integer对象
Integer as = a.getSize();
// 把a对象赋给Apple变量,丢失尖括号里的类型信息
Apple b = a; // 代码2
// b只知道size的类型是Number
Number size1 = b.getSize();
// 下面代码引起编译错误
// Integer size2 = b.getSize(); // 代码3
}
}

上面程序中定义了一个带泛型声明的Apple类,其泛型形参的上限是Number,这个泛型形参用来定义Apple类的size变量。程序在 代码1 处创建了一个Apple对象,该Apple对象的泛型代表了Integer类型,所以调用a的getSize方法时返回Integer类型的值。当把a赋给一个不带泛型信息的b变量时,编译器就会丢失a对象的泛型信息,即所有尖括号里的信息都会丢失—因为Apple的泛型形参的上限是Number类,所以编译器依然知道b的getSize方法返回Number类型,但具体是Number的哪个子类就不清楚了。

从逻辑上来看,List<String>List的子类,如果直接把一个List对象赋给一个List<String>对象应该引起编译错误,但实际上不会。对泛型而言,可以直接把一个List对象赋给一个List<String>对象,编译器仅仅提示“未经检查的转换”,看下面程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.util.*;

public class ErasureTest2 {
public static void main(String[] args) {
List<Integer> li = new ArrayList<>();
li.add(6);
li.add(9);
List list = li;
// 下面代码引起“未经检查的转换”的警告,编译、运行时完全正常
List<String> ls = list; // 代码1
// 但只要访问ls里的元素,如下面代码将引起运行时异常。
System.out.println(ls.get(0));
}
}

上面程序中定义了一个List<Integer>对象,这个List<Integer>对象保留了集合元素的类型信息。当把这个List<Integer>对象赋给一个List类型的list后,编译器就会丢失前者的泛型信息,即丢失list集合里元素的类型信息,这是典型的擦除Java又允许直接把List对象赋给一个List<Type>(Type可以是任何类型)类型的变量,所以程序在 代码1 处可以编译通过,只是发出“未经检査的转换”警告。但对list变量实际上引用的是List<Integer>集合,所以当试图把该集合里的元素当成String类型的对象取出时,将引发ClassCastException异常。

下面代码与上面代码的行为完全相似。

1
2
3
4
5
6
7
8
9
10
import java.util.*;

public class ErasureTest3 {
public static void main(String[] args) {
List<Integer> li = new ArrayList<>();
li.add(6);
li.add(9);
System.out.println((String) li.get(0));
}
}

程序从li中获取一个元素,并且试图通过强制类型转换把它转换成一个String,将引发运行时异常。前面使用泛型代码时,系统与之存在完全相似的行为,所以引发相同的ClassCastException异常。