2023年将会持续于B站、CSDN等各大平台更新,可加入粉丝群与博主交流:838681355,为了老板大G共同努力。
【商务合作请私信或进群联系群主】
四、继承、多态
继承机制的使用可以复用一些定义好的类,减少重复代码的编写。多态机制可以动态调整对象的调用,降低对象之间的依存关系。
4.1 封装
封装将类的某些信息隐藏在类内部,不允许外部程序直接访问,只能通过该类提供的方法来实现对隐藏信息的操作和访问。
封装的特点:
1. 只能通过规定的方法访问数据。
2. 隐藏类的实例细节,方便修改和实现。
实现封装的具体步骤如下:
1. 修改属性的可见性来限制对属性的访问,一般设为 private 。
2. 为每个属性创建一对赋值(setter)方法和取值(getter)方法,一般设为 public ,用于属性的读写。
3. 在赋值和取值方法中,加入属性控制语句(对属性值的合法性进行判断)。
下面以一个员工类的封装为例介绍封装过程。一个员工的主要属性有姓名、年龄、联系电话和家庭住址。假设员工类为 Employee ,示例如下:
public class Employee {
private String name;
private int age;
private String phone;
private String address;
public String getName ( ) {
return name;
}
public void setName ( String name) {
this . name = name;
}
public int getAge ( ) {
return age;
}
public void setAge ( int age) {
if ( age < 18 || age > 40 ) {
System . out. println ( "年龄必须在18到40之间!" ) ;
this . age = 20 ;
} else {
this . age = age;
}
}
public String getPhone ( ) {
return phone;
}
public void setPhone ( String phone) {
this . phone = phone;
}
public String getAddress ( ) {
return address;
}
public void setAddress ( String address) {
this . address = address;
}
}
如上述代码所示,使用 private 关键字修饰属性,这就意味着除了 Employee 类本身外,其他任何类都不可以访问这些属性。但是,可以通过这些属性的 setXxx ( ) 方法来对其进行赋值,通过 getXxx ( ) 方法来访问这些属性。
在 age 属性的 setAge ( ) 方法中,首先对用户传递过来的参数 age 进行判断,如果 age 的值不在 18 到 40 之间,则将 Employee 类的 age 属性值设置为 20 ,否则为传递过来的参数值。
编写测试类 EmployeeTest ,在该类的 main ( ) 方法中调用 Employee 属性的 setXxx ( ) 方法对其相应的属性进行赋值,并调用 getXxx ( ) 方法访问属性,代码如下:
public class EmployeeTest {
public static void main ( String [ ] args) {
Employee people = new Employee ( ) ;
people. setName ( "王丽丽" ) ;
people. setAge ( 35 ) ;
people. setPhone ( "13653835964" ) ;
people. setAddress ( "河北省石家庄市" ) ;
System . out. println ( "姓名:" + people. getName ( ) ) ;
System . out. println ( "年龄:" + people. getAge ( ) ) ;
System . out. println ( "电话:" + people. getPhone ( ) ) ;
System . out. println ( "家庭住址:" + people. getAddress ( ) ) ;
}
}
4.2 继承
Java 中的继承就是在已经存在类的基础上进行扩展,从而产生新的类。已经存在的类称为父类、基类或超类,而新产生的类称为子类或派生类。在子类中,不仅包含父类的属性和方法,还可以增加新的属性和方法。
Java 中子类继承父类的语法格式如下:
修饰符 class class_name extends extend_class {
}
其中,class_name 表示子类(派生类)的名称;extend_class 表示父类(基类)的名称;extends 关键字直接跟在子类名之后,其后面是该类要继承的父类名称。例如:
public class Student extends Person { }
继承的优缺点
在面向对象语言中,继承是必不可少的、非常优秀的语言机制,它有如下优点:
* 实现代码共享,减少创建类的工作量,使子类可以拥有父类的方法和属性。
* 提高代码维护性和可重用性。
* 提高代码的可扩展性,更好的实现父类的方法。
自然界的所有事物都是优点和缺点并存的,继承的缺点如下:
* 继承是侵入性的。只要继承,就必须拥有父类的属性和方法。
* 降低代码灵活性。子类拥有父类的属性和方法后多了些约束。
* 增强代码耦合性(开发项目的原则为高内聚低耦合)。当父类的常量、变量和方法被修改时,需要考虑子类的修改,有可能会导致大段的代码需要重构。
示例:
public class People {
public String name;
public int age;
public String sex;
public String sn;
public People ( String name, int age, String sex, String sn) {
this . name = name;
this . age = age;
this . sex = sex;
this . sn = sn;
}
public String toString ( ) {
return "姓名:" + name + "\n年龄:" + age + "\n性别:" + sex + "\n身份证号:" + sn;
}
}
创建 People 类的子类 Student 类,并定义 stuNo 和 department 属性,代码如下:
public class Student extends People {
private String stuNo;
private String department;
public Student ( String name, int age, String sex, String sn, String stuno, String department) {
super ( name, age, sex, sn) ;
this . stuNo = stuno;
this . department = department;
}
public String toString ( ) {
return "姓名:" + name + "\n年龄:" + age + "\n性别:" + sex + "\n身份证号:" + sn + "\n学号:" + stuNo + "\n所学专业:" + department;
}
}
由于 Student 类继承自 People 类,因此,在 Student 类中同样具有 People 类的属性和方法,这里重写了父类中的 toString ( ) 方法。
4.2.1 super关键字
由于子类不能继承父类的构造方法,因此,如果要调用父类的构造方法,可以使用 super 关键字。super 可以用来访问父类的构造方法、普通方法和属性。
super 关键字的功能:
* 在子类的构造方法中显式的调用父类构造方法
* 访问父类的成员方法和变量。
4.2.2 super调用父类构造方法
super 关键字可以在子类的构造方法中显式地调用父类的构造方法,其中,parameter- list 指定了父类构造方法中的所有参数。super ( ) 必须是在子类构造方法的方法体的第一行。基本格式如下:
super ( parameter- list) ;
如果一个类中没有写任何的构造方法,JVM 会生成一个默认的无参构造方法。在继承关系中,由于在子类的构造方法中,第一条语句默认为调用父类的无参构造方法(即默认为 super ( ) ,一般这行代码省略了)。所以当在父类中定义了有参构造方法,但是没有定义无参构造方法时,编译器会强制要求我们定义一个相同参数类型的构造方法。
示例:
声明父类 Person ,类中定义两个构造方法。示例代码如下:
public class Person {
public Person ( String name, int age) {
}
public Person ( String name, int age, String sex) {
}
}
子类 Student 继承了 Person 类,使用 super 语句来定义 Student 类的构造方法。示例代码如下:
public class Student extends Person {
public Student ( String name, int age, String birth) {
super ( name, age) ;
}
public Student ( String name, int age, String sex, String birth) {
super ( name, age, sex) ;
}
}
从上述 Student 类构造方法代码可以看出,super 可以用来直接调用父类中的构造方法,使编写代码也更加简洁方便。
编译器会自动在子类构造方法的第一句加上super ( ) ; 来调用父类的无参构造方法,必须写在子类构造方法的第一句,也可以省略不写。通过 super 来调用父类其它构造方法时,只需要把相应的参数传过去。
4.2.3 super访问父类成员
当子类的成员变量或方法与父类同名时,可以使用 super 关键字来访问。如果子类重写了父类的某一个方法,即子类和父类有相同的方法定义,但是有不同的方法体,此时,我们可以通过 super 来调用父类里面的这个方法。
使用 super 访问父类中的成员与 this 关键字的使用相似,只不过它引用的是子类的父类,语法格式如下:使用 super 访问父类的属性和方法时不用位于第一行。
super . member
示例: super 调用成员属性
当父类和子类具有相同的数据成员时,JVM 可能会模糊不清。我们可以使用以下代码片段更清楚地理解它。
class Person {
int age = 12 ;
}
class Student extends Person {
int age = 18 ;
void display ( ) {
System . out. println ( "学生年龄:" + super . age) ;
}
}
class Test {
public static void main ( String [ ] args) {
Student stu = new Student ( ) ;
stu. display ( ) ;
}
}
4.2.4 super和this的区别
this 指的是当前对象的引用,super 是当前对象的父对象的引用。下面先简单介绍一下 super 和 this 关键字的用法。
super 关键字的用法:
super . 父类属性名:调用父类中的属性
super . 父类方法名:调用父类中的方法
super ( ) :调用父类的无参构造方法
super ( 参数) :调用父类的有参构造方法
如果构造方法的第一行代码不是 this ( ) 和 super ( ) ,则系统会默认添加 super ( ) 。
this 关键字的用法:
this . 属性名:表示当前对象的属性
this . 方法名( 参数) :表示调用当前对象的方法
关于 Java super 和 this 关键字的异同,可简单总结为以下几条。
* 子类和父类中变量或方法名称相同时,用 super 关键字来访问。可以理解为 super 是指向自己父类对象的一个指针。在子类中调用父类的构造方法。
* this 是自身的一个对象,代表对象本身,可以理解为 this 是指向对象本身的一个指针。在同一个类中调用其它方法。
* this 和 super 不能同时出现在一个构造方法里面,因为 this 必然会调用其它的构造方法,其它的构造方法中肯定会有 super 语句的存在,所以在同一个构造方法里面有相同的语句,就失去了语句的意义,编译器也不会通过。
* this ( ) 和 super ( ) 都指的是对象,所以,均不可以在 static 环境中使用,包括 static 变量、static 方法和 static 语句块。
* 从本质上讲,this 是一个指向对象本身的指针, 然而 super 是一个 Java 关键字。
4.3 对象类型转换
将一个类型强制转换成另一个类型的过程被称为类型转换。本节所说的对象类型转换,是指存在继承关系的对象,不是任意类型的对象。当对不存在继承关系的对象进行强制类型转换时,会抛出 Java 强制类型转换(java. lang. ClassCastException )异常。
Java 语言允许某个类型的引用变量引用子类的实例,而且可以对这个引用变量进行类型转换。Java 中引用类型之间的类型转换(前提是两个类是父子关系)主要有两种,分别是向上转型(upcasting)和向下转型(downcasting)。
4.3.1 向上转型
父类引用指向子类对象为向上转型,语法格式如下:
fatherClass obj = new sonClass ( ) ;
其中,fatherClass 是父类名称或接口名称,obj 是创建的对象,sonClass 是子类名称。
向上转型就是把子类对象直接赋给父类引用,不用强制转换。使用向上转型可以调用父类类型中的所有成员,不能调用子类类型中特有成员,最终运行效果看子类的具体实现。
4.3.2 向下转型
与向上转型相反,子类对象指向父类引用为向下转型,语法格式如下:
sonClass obj = ( sonClass) fatherClass;
其中,fatherClass 是父类名称,obj 是创建的对象,sonClass 是子类名称。
向下转型可以调用子类类型中所有的成员,不过需要注意的是如果父类引用对象指向的是子类对象,那么在向下转型的过程中是安全的,也就是编译是不会出错误。但是如果父类引用对象是父类本身,那么在向下转型的过程中是不安全的,编译不会出错,但是运行时会出现我们开始提到的 Java 强制类型转换异常,一般使用 instanceof 运算符来避免出此类错误。
4.3.3 向上与向下转型示例
Animal 类表示动物类,该类对应的子类有 Dog 类,使用对象类型表示如下:
Animal animal = new Dog ( ) ;
Dog dog = ( Dog ) animal;
4.3.4 强制对象类型转换
Java 编译器允许在具有直接或间接继承关系的类之间进行类型转换。对于向下转型,必须进行强制类型转换;对于向上转型,不必使用强制类型转换。
示例:
对于一个引用类型的变量,Java 编译器按照它声明的类型来处理。如果使用 animal 调用 str 和 eatMethod ( ) 方法将会出错,如下:
animal. str = "" ;
animal. eatMethod ( ) ;
如果要访问 Cat 类的成员,必须通过强制类型转换,如下:
( ( Cat ) animal) . str = "" ;
( ( Cat ) animal) . eatMethod ( ) ;
把 Animal 对象类型强制转换为 Cat 对象类型,这时上面两句编译成功。对于如下语句,由于使用了强制类型转换,所以也会编译成功,例如:
Cat cat = ( Cat ) animal;
类型强制转换时想运行成功就必须保证父类引用指向的对象一定是该子类对象,最好使用 instanceof 运算符判断后,再强转,例如:
Animal animal = new Cat ( ) ;
if ( animal instanceof Cat ) {
Cat cat = ( Cat ) animal;
. . .
}
子类的对象可以转换成父类类型,而父类的对象实际上无法转换为子类类型。因为通俗地讲,父类拥有的成员子类肯定也有,而子类拥有的成员,父类不一定有。因此,对于向上转型,不必使用强制类型转换。例如:
Cat cat = new Cat ( ) ;
Animal animal = cat;
如果两种类型之间没有继承关系,那么将不允许进行类型转换。例如:
Dog dog = new Dog ( ) ;
Cat cat = ( Cat ) dog;
4.4 重载与重写
Java 允许同一个类中定义多个同名方法,只要它们的形参列表不同即可。如果同一个类中包含了两个或两个以上方法名相同的方法,但形参列表不同,这种情况被称为方法重载(overload)。
方法重载的要求是两同一不同:同一个类中方法名相同,参数列表不同。至于方法的其他部分,如方法返回值类型、修饰符等,与方法重载没有任何关系。
使用方法重载其实就是避免出现繁多的方法名,有些方法的功能是相似的,如果重新建立一个方法,重新取个方法名称,会降低程序可读性。
示例:
在比较数值时,数值的个数和类型是不固定的,可能是两个 int 类型的数值,也可能是两个 double 类型的数值,或者是两个 double 、一个 int 类型的数值;在这种情况下就可以使用方法的重载来实现数值之间的比较功能。具体实现代码如下:
public class OverLoading {
public void max ( int a, int b) {
System . out. println ( a > b ? a : b) ;
}
public void max ( double a, double b) {
System . out. println ( a > b ? a : b) ;
}
public void max ( double a, double b, int c) {
double max = ( double ) ( a > b ? a : b) ;
System . out. println ( c > max ? c : max) ;
}
public static void main ( String [ ] args) {
OverLoading ol = new OverLoading ( ) ;
System . out. println ( "1 与 5 比较,较大的是:" ) ;
ol. max ( 1 , 5 ) ;
System . out. println ( "5.205 与 5.8 比较,较大的是:" ) ;
ol. max ( 5.205 , 5.8 ) ;
System . out. println ( "2.15、0.05、58 中,较大的是:" ) ;
ol. max ( 2.15 , 0.05 , 58 ) ;
}
}
在子类中如果创建了一个与父类中相同名称、相同返回值类型、相同参数列表的方法,只是方法体中的实现不同,以实现不同于父类的功能,这种方式被称为方法重写(override),又称为方法覆盖。当父类中的方法无法满足子类需求或子类具有特有功能的时候,需要方法重写。
子类可以根据需要,定义特定于自己的行为。既沿袭了父类的功能名称,又根据子类的需要重新实现父类方法,从而进行扩展增强。
在重写方法时,需要遵循下面的规则:
1. 参数列表必须完全与被重写的方法参数列表相同。
2. 返回的类型必须与被重写的方法的返回类型相同(Java1 .5 版本之前返回值类型必须一样,之后的 Java 版本放宽了限制,返回值类型必须小于或者等于父类方法的返回值类型)。
3. 访问权限不能比父类中被重写方法的访问权限更低(public > protected > default > private )。
4. 重写方法一定不能抛出新的检査异常或者比被重写方法声明更加宽泛的检査型异常。例如,父类的一个方法声明了一个检査异常 IOException ,在重写这个方法时就不能抛出 Exception ,只能拋出 IOException 的子类异常,可以抛出非检査异常。
另外还要注意以下几条:
1. 重写的方法可以使用 @Override 注解来标识。
2. 父类的成员方法只能被它的子类重写。
3. 声明为 final 的方法不能被重写。
4. 声明为 static 的方法不能被重写,但是能够再次声明。
5. 构造方法不能被重写。
6. 子类和父类在同一个包中时,子类可以重写父类的所有方法,除了声明为 private 和 final 的方法。
7. 子类和父类不在同一个包中时,子类只能重写父类的声明为 public 和 protected 的非 final 方法。
8. 如果不能继承一个方法,则不能重写这个方法。
示例:
下面编写 Java 程序,在父类 Animal 中定义 getInfo ( ) 方法,并在子类 Cat 中重写该方法, 实现猫的介绍方式。父类 Animal 的代码如下:
public class Animal {
public String name;
public int age;
public Animal ( String name, int age) {
this . name = name;
this . age = age;
}
public String getInfo ( ) {
return "我叫" + name + ",今年" + age + "岁了。" ;
}
}
子类 Cat 的代码如下:
public class Cat extends Animal {
private String hobby;
public Cat ( String name, int age, String hobby) {
super ( name, age) ;
this . hobby = hobby;
}
public String getInfo ( ) {
return "喵!大家好!我叫" + this . name + ",我今年" + this . age + "岁了,我爱吃" + hobby + "。" ;
}
public static void main ( String [ ] args) {
Animal animal = new Cat ( "小白" , 2 , "鱼" ) ;
System . out. println ( animal. getInfo ( ) ) ;
}
}
4.5 多态
多态性是面向对象编程的又一个重要特征,它是指在父类中定义的属性和方法被子类继承之后,可以具有不同的数据类型或表现出不同的行为.
对面向对象来说,多态分为编译时多态和运行时多态。
Java 实现多态有 3 个必要条件:继承、重写和向上转型。
1. 继承:在多态中必须存在有继承关系的子类和父类。
2. 重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
3. 向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才既能可以调用父类的方法,又能调用子类的方法。
示例:
1 )创建 Figure 类,在该类中首先定义存储二维对象的尺寸,然后定义有两个参数的构造方法,最后添加 area ( ) 方法,该方法计算对象的面积。代码如下:
public class Figure {
double dim1;
double dim2;
Figure ( double d1, double d2) {
this . dim1 = d1;
this . dim2 = d2;
}
double area ( ) {
System . out. println ( "父类中计算对象面积的方法,没有实际意义,需要在子类中重写。" ) ;
return 0 ;
}
}
2 )创建继承自 Figure 类的 Rectangle 子类,该类调用父类的构造方法,并且重写父类中的 area ( ) 方法。代码如下:
public class Rectangle extends Figure {
Rectangle ( double d1, double d2) {
super ( d1, d2) ;
}
double area ( ) {
System . out. println ( "长方形的面积:" ) ;
return super . dim1 * super . dim2;
}
}
3 )创建继承自 Figure 类的 Triangle 子类,该类与 Rectangle 相似。代码如下:
public class Triangle extends Figure {
Triangle ( double d1, double d2) {
super ( d1, d2) ;
}
double area ( ) {
System . out. println ( "三角形的面积:" ) ;
return super . dim1 * super . dim2 / 2 ;
}
}
4 )创建 Test 测试类,在该类的 main ( ) 方法中首先声明 Figure 类的变量 figure,然后分别为 figure 变量指定不同的对象,并调用这些对象的 area ( ) 方法。代码如下:
public class Test {
public static void main ( String [ ] args) {
Figure figure;
figure = new Rectangle ( 9 , 9 ) ;
System . out. println ( figure. area ( ) ) ;
System . out. println ( "===============================" ) ;
figure = new Triangle ( 6 , 8 ) ;
System . out. println ( figure. area ( ) ) ;
System . out. println ( "===============================" ) ;
figure = new Figure ( 10 , 10 ) ;
System . out. println ( figure. area ( ) ) ;
}
}
从上述代码可以发现,无论 figure 变量的对象是 Rectangle 还是 Triangle ,它们都是 Figure 类的子类,因此可以向上转型为该类,从而实现多态。
4.6 instanceof关键字
严格来说 instanceof 是 Java 中的一个双目运算符,由于它是由字母组成的,所以也是 Java 的保留关键字。在 Java 中可以使用 instanceof 关键字判断一个对象是否为一个类(或接口、抽象类、父类)的实例,语法格式如下所示。
( obj是一个对象, Class 表示一个类或接口obj是class 类( 或接口) 的实例或者子类实例时, 结果result返回true , 否则返回 false )
boolean result = obj instanceof Class
4.7 抽象类
在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,那么这样的类称为抽象类。
抽象方法的 3 个特征如下:
* 抽象方法没有方法体
* 抽象方法必须存在于抽象类中
* 子类重写父类时,必须重写父类所有的抽象方法
( abstract 表示该类或该方法是抽象的: class_name表示抽象类的名称: method_name表示抽象方法名称: parameter- list表示方法参数列表)
在 Java 中抽象类的语法格式如下:
< abstract > class < class_name> {
< abstract > < type> < method_name> ( parameter- iist) ;
}
抽象类的定义和使用规则如下:
* 抽象类和抽象方法都要使用 abstract 关键字声明。
* 如果一个方法被声明为抽象的,那么这个类也必须声明为抽象的。而一个抽象类中,可以有 0 ~ n 个抽象方法,以及 0 ~ n 个具体方法。
* 抽象类不能实例化,也就是不能使用 new 关键字创建对象。
4.8 Interface接口定义和实现
抽象类是从多个类中抽象出来的模板,如果将这种抽象进行的更彻底,则可以提炼出一种更加特殊的“抽象类”——接口(Interface )。接口是 Java 中最重要的概念之一,它可以被理解为一种特殊的类,不同的是接口的成员没有执行体,是由全局常量和公共的抽象方法所组成。
4.8.1 定义
Java 接口的定义方式与类基本相同,不过接口定义使用的关键字是 interface ,接口定义的语法格式如下:
[ public ] interface interface_name [ extends interface1_name[ , interface2_name, …] ] {
[ public ] [ static ] [ final ] type constant_name = value;
[ public ] [ abstract ] returnType method_name ( parameter_list) ;
}
* public 表示接口的修饰符,当没有修饰符时,则使用默认的修饰符,此时该接口的访问权限仅局限于所属的包;
* interface_name 表示接口的名称。接口名应与类名采用相同的命名规则,即如果仅从语法角度来看,接口名只要是合法的标识符即可。如果要遵守 Java 可读性规范,则接口名应由多个有意义的单词连缀而成,每个单词首字母大写,单词与单词之间无需任何分隔符。
* extends 表示接口的继承关系;
* interface1_name 表示要继承的接口名称;
* constant_name 表示变量名称,一般是 static 和 final 型的;
* returnType 表示方法的返回值类型;
* parameter_list 表示参数列表,在接口中的方法是没有方法体的。
接口对于其声明、变量和方法都做了许多限制,这些限制作为接口的特征归纳如下:
* 具有 public 访问控制符的接口,允许任何类使用;没有指定 public 的接口,其访问将局限于所属的包。
* 方法的声明不需要其他修饰符,在接口中声明的方法,将隐式地声明为公有的(public )和抽象的(abstract )。
* 在 Java 接口中声明的变量其实都是常量,接口中的变量声明,将隐式地声明为 public 、static 和 final ,即常量,所以接口中定义的变量必须初始化。
* 接口没有构造方法,不能被实例化。
示例:
public interface A {
publicA ( ) { …}
}
一个接口不能够实现另一个接口,但它可以继承多个其他接口。子接口可以对父接口的方法和常量进行重写。例如:
public interface StudentInterface extends PeopleInterface {
int age = 25 ;
void getInfo ( ) ;
}
例如,定义一个接口 MyInterface ,并在该接口中声明常量和方法,如下:
public interface MyInterface {
String name;
int age = 20 ;
void getInfo ( ) ;
}
4.8.2 实现接口
接口的主要用途就是被实现类实现,一个类可以实现一个或多个接口,继承使用 extends 关键字,实现则使用 implements 关键字。因为一个类可以实现多个接口,这也是 Java 为单继承灵活性不足所作的补充。类实现接口的语法格式如下:
< public > class < class_name> [ extends superclass_name] [ implements interface1_name[ , interface2_name…] ] {
}
对以上语法的说明如下:
* public :类的修饰符;
* superclass_name:需要继承的父类名称;
* interface1_name:要实现的接口名称。
实现接口需要注意以下几点:
1. 实现接口与继承父类相似,一样可以获得所实现接口里定义的常量和方法。如果一个类需要实现多个接口,则多个接口之间以逗号分隔。
2. 一个类可以继承一个父类,并同时实现多个接口,implements 部分必须放在 extends 部分之后。
3. 一个类实现了一个或多个接口之后,这个类必须完全实现这些接口里所定义的全部抽象方法(也就是重写这些抽象方法);否则,该类将保留从父接口那里继承到的抽象方法,该类也必须定义成抽象类。
示例:
1 )创建一个名称为 IMath 的接口,代码如下:
public interface IMath {
public int sum ( ) ;
public int maxNum ( int a, int b) ;
}
2 )定义一个 MathClass 类并实现 IMath 接口,MathClass 类实现代码如下:
public class MathClass implements IMath {
private int num1;
private int num2;
public MathClass ( int num1, int num2) {
this . num1 = num1;
this . num2 = num2;
}
public int sum ( ) {
return num1 + num2;
}
public int maxNum ( int a, int b) {
if ( a >= b) {
return a;
} else {
return b;
}
}
}
在实现类中,所有的方法都使用了 public 访问修饰符声明。无论何时实现一个由接口定义的方法,它都必须实现为 public ,因为接口中的所有成员都显式声明为 public 。
3 )最后创建测试类 NumTest ,实例化接口的实现类 MathClass ,调用该类中的方法并输出结果。该类内容如下:
public class NumTest {
public static void main ( String [ ] args) {
MathClass calc = new MathClass ( 100 , 300 ) ;
System . out. println ( "100 和 300 相加结果是:" + calc. sum ( ) ) ;
System . out. println ( "100 比较 300,哪个大:" + calc. maxNum ( 100 , 300 ) ) ;
}
}
4.9 内部类
在类内部可定义成员变量和方法,且在类内部也可以定义另一个类。如果在类 Outer 的内部再定义一个类 Inner ,此时类 Inner 就称为内部类(或称为嵌套类),而类 Outer 则称为外部类(或称为宿主类)。
内部类可以很好地实现隐藏,一般的非内部类是不允许有 private 与 protected 权限的,但内部类可以。内部类拥有外部类的所有元素的访问权限。
内部类可以分为:实例内部类、静态内部类和成员内部.
内部类的特点如下:
* 内部类仍然是一个独立的类,在编译之后内部类会被编译成独立的. class 文件,但是前面冠以外部类的类名和$符号。
* 内部类不能用普通的方式访问。内部类是外部类的一个成员,因此内部类可以自由地访问外部类的成员变量,无论是否为 private 的。
* 内部类声明成静态的,就不能随便访问外部类的成员变量,仍然是只能访问外部类的静态成员变量。
示例:
public class Test {
public class InnerClass {
public int getSum ( int x, int y) {
return x + y;
}
}
public static void main ( String [ ] args) {
Test. InnerClass ti = new Test ( ) . new InnerClass ( ) ;
int i = ti. getSum ( 2 , 3 ) ;
System . out. println ( i) ;
}
}
有关内部类的说明有如下几点。
* 外部类只有两种访问级别:public 和默认;内部类则有 4 种访问级别:public 、protected 、 private 和默认。
* 在外部类中可以直接通过内部类的类名访问内部类。
InnerClass ic = new InnerClass ( ) ;
* 在外部类以外的其他类中则需要通过内部类的完整类名访问内部类。
Test. InnerClass ti = newTest ( ) . new InnerClass ( ) ;
内部类与外部类不能重名。
4.9.1 实例内部类
实例内部类是指没有用 static 修饰的内部类,有的地方也称为非静态内部类。
示例:
public class Outer {
class Inner {
}
}
特点:
1 )在外部类的静态方法和外部类以外的其他类中,必须通过外部类的实例创建内部类的实例。
public class Outer {
class Inner1 {
}
Inner1 i = new Inner1 ( ) ;
public void method1 ( ) {
Inner1 i = new Inner1 ( ) ;
}
public static void method2 ( ) {
Inner1 i = new Outer ( ) . new inner1 ( ) ;
}
class Inner2 {
Inner1 i = new Inner1 ( ) ;
}
}
class OtherClass {
Outer. Inner i = new Outer ( ) . new Inner ( ) ;
}
2 )在实例内部类中,可以访问外部类的所有成员。
public class Outer {
public int a = 100 ;
static int b = 100 ;
final int c = 100 ;
private int d = 100 ;
public String method1 ( ) {
return "实例方法1" ;
}
public static String method2 ( ) {
return "静态方法2" ;
}
class Inner {
int a2 = a + 1 ;
int b2 = b + 1 ;
int c2 = c + 1 ;
int d2 = d + 1 ;
String str1 = method1 ( ) ;
String str2 = method2 ( ) ;
}
public static void main ( String [ ] args) {
Inner i = new Outer ( ) . new Inner ( ) ;
System . out. println ( i. a2) ;
System . out. println ( i. b2) ;
System . out. println ( i. c2) ;
System . out. println ( i. d2) ;
System . out. println ( i. str1) ;
System . out. println ( i. str2) ;
}
}
提示:如果有多层嵌套,则内部类可以访问所有外部类的成员。
3 )在外部类中不能直接访问内部类的成员,而必须通过内部类的实例去访问。如果类 A 包含内部类 B ,类 B 中包含内部类 C ,则在类 A 中不能直接访问类 C ,而应该通过类 B 的实例去访问类 C 。
4 )外部类实例与内部类实例是一对多的关系,也就是说一个内部类实例只对应一个外部类实例,而一个外部类实例则可以对应多个内部类实例。
如果实例内部类 B 与外部类 A 包含有同名的成员 t,则在类 B 中 t 和 this . t 都表示 B 中的成员 t,而 A . this . t 表示 A 中的成员 t。
public class Outer {
int a = 10 ;
class Inner {
int a = 20 ;
int b1 = a;
int b2 = this . a;
int b3 = Outer . this . a;
}
public static void main ( String [ ] args) {
Inner i = new Outer ( ) . new Inner ( ) ;
System . out. println ( i. b1) ;
System . out. println ( i. b2) ;
System . out. println ( i. b3) ;
}
}
5 )在实例内部类中不能定义 static 成员,除非同时使用 final 和 static 修饰。
4.9.2 静态内部类
静态内部类是指使用 static 修饰的内部类。
示例:
public class Outer {
static class Inner {
}
}
特点:
1 )在创建静态内部类的实例时,不需要创建外部类的实例。
public class Outer {
static class Inner {
}
}
class OtherClass {
Outer. Inner oi = new Outer. Inner ( ) ;
}
2 )静态内部类中可以定义静态成员和实例成员。外部类以外的其他类需要通过完整的类名访问静态内部类中的静态成员,如果要访问静态内部类中的实例成员,则需要通过静态内部类的实例。
public class Outer {
static class Inner {
int a = 0 ;
static int b = 0 ;
}
}
class OtherClass {
Outer. Inner oi = new Outer. Inner ( ) ;
int a2 = oi. a;
int b2 = Outer. Inner . b;
}
3 )静态内部类可以直接访问外部类的静态成员,如果要访问外部类的实例成员,则需要通过外部类的实例去访问。
public class Outer {
int a = 0 ;
static int b = 0 ;
static class Inner {
Outer o = new Outer ;
int a2 = o. a;
int b2 = b;
}
}
4.9.3 局部内部类
局部内部类是指在一个方法中定义的内部类。
示例:
public class Test {
public void method ( ) {
class Inner {
}
}
}
特点:
1 )局部内部类与局部变量一样,不能使用访问控制修饰符(public 、private 和 protected )和 static 修饰符修饰。
2 )局部内部类只在当前方法中有效。
public class Test {
Inner i = new Inner ( ) ;
Test. Inner ti = new Test. Inner ( ) ;
Test. Inner ti2 = new Test ( ) . new Inner ( ) ;
public void method ( ) {
class Inner {
}
Inner i = new Inner ( ) ;
}
}
3 )局部内部类中不能定义 static 成员。
4 )局部内部类中还可以包含内部类,但是这些内部类也不能使用访问控制修饰符(public 、private 和 protected )和 static 修饰符修饰。
5 )在局部内部类中可以访问外部类的所有成员。
6 )在局部内部类中只可以访问当前方法中 final 类型的参数与变量。如果方法中的成员与外部类中的成员同名,则可以使用 < OuterClassName > . this . < MemberName > 的形式访问外部类中的成员。
public class Test {
int a = 0 ;
int d = 0 ;
public void method ( ) {
int b = 0 ;
final int c = 0 ;
final int d = 10 ;
class Inner {
int a2 = a;
int c2 = c;
int d2 = d;
int d3 = Test . this . d;
}
Inner i = new Inner ( ) ;
System . out. println ( i. d2) ;
System . out. println ( i. d3) ;
}
public static void main ( String [ ] args) {
Test t = new Test ( ) ;
t. method ( ) ;
}
}
4.9.4 匿名内部类
匿名类是指没有类名的内部类,必须在创建时使用 new 语句来声明类。
这种形式的 new 语句声明一个新的匿名类,它对一个给定的类进行扩展,或者实现一个给定的接口。使用匿名类可使代码更加简洁、紧凑,模块化程度更高,语法如下:
new < 类或接口> ( ) {
} ;
匿名类有两种实现方式:
* 继承一个类,重写其方法。
* 实现一个接口(可以是多个),实现其方法。
示例:
public class Out {
void show ( ) {
System . out. println ( "调用 Out 类的 show() 方法" ) ;
}
}
public class TestAnonymousInterClass {
private void show ( ) {
Out anonyInter = new Out ( ) {
void show ( ) {
System . out. println ( "调用匿名类中的 show() 方法" ) ;
}
} ;
anonyInter. show ( ) ;
}
public static void main ( String [ ] args) {
TestAnonymousInterClass test = new TestAnonymousInterClass ( ) ;
test. show ( ) ;
}
}
程序的输出结果如下:调用匿名类中的 show ( ) 方法
从输出结果可以看出,匿名内部类有自己的实现。
特点:
1 )匿名类和局部内部类一样,可以访问外部类的所有成员。如果匿名类位于一个方法中,则匿名类只能访问方法中 final 类型的局部变量和参数。
public static void main ( String [ ] args) {
int a = 10 ;
final int b = 10 ;
Out anonyInter = new Out ( ) {
void show ( ) {
System . out. println ( "调用了匿名类的 show() 方法" + b) ;
}
} ;
anonyInter. show ( ) ;
}
从 Java 8 开始添加了 Effectively final 功能,在 Java 8 及以后的版本中代码第 6 行不会出现编译错误,详情可点击《Java8 新特性之Effectively final 》进行学习。
2 )匿名类中允许使用非静态代码块进行成员初始化操作。
Out anonyInter = new Out ( ) {
int i; {
i = 10 ;
}
public void show ( ) {
System . out. println ( "调用了匿名类的 show() 方法" + i) ;
}
} ;
3 )匿名类的非静态代码块会在父类的构造方法之后被执行。
4.9.5 JAVA8特性:Effectively final(了解)
Java 中局部内部类和匿名内部类访问的局部变量必须由 final 修饰,以保证内部类和外部类的数据一致性。但从 Java 8 开始,我们可以不加 final 修饰符,由系统默认添加,当然这在 Java 8 以前的版本是不允许的。Java 将这个功能称为 Effectively final 功能。
编写同样的代码,分别在 Java 7 和 Java 8 下运行,代码如下:
public class Test {
public static void main ( String [ ] args) {
String name = "语言网" ;
new Runnable ( ) {
@Override
public void run ( ) {
System . out. println ( name) ;
}
}
}
}
因为系统会默认添加 final 修饰符,所以在图 2 和图 3 中可以在匿名内部类中直接使用非 final 变量,而 final 修饰的局部变量不能在被重新赋值,所以图 3 中出现编译错误。也就是说从 Java 8 开始,它不要求程序员必须将访问的局部变量显式的声明为 final 的。只要该变量不被重新赋值就可以。
一个非 final 的局部变量或方法参数,其值在初始化后就从未更改,那么该变量就是 effectively final 。在 Lambda 表达式中,使用局部变量的时候,也要求该变量必须是 final 的,所以 effectively final 在 Lambda 表达式上下文中非常有用。
Lambda 表达式在编程中是经常使用的,而匿名内部类是很少使用的。那么,我们在 Lambda 编程中每一个被使用到的局部变量都去显示定义成 final 吗?显然这不是一个好方法。所以,Java 8 引入了 effectively final 新概念。
总结一下,规则没有改变,Lambda 表达式和匿名内部类访问的局部变量必须是 final 的,只是不需要程序员显式的声明变量为 final 的,从而节省时间。
4.10 Lambda表达式
Lambda 表达式(Lambda expression)是一个匿名函数,基于数学中的λ演算得名,也可称为闭包(Closure )。
Lambda 表达式是推动 Java 8 发布的重要新特性,它允许把函数作为一个方法的参数(函数作为参数传递进方法中)
示例:
先定义一个计算数值的接口,代码如下。
public interface Calculable {
int calculateInt ( int a, int b) ;
}
Calculable 接口只有一个方法 calculateInt,参数是两个 int 类型,返回值也是 int 类型。实现方法代码如下:
public class Test {
public static Calculable calculate ( char opr) {
Calculable result;
if ( opr == '+' ) {
result = new Calculable ( ) {
@Override
public int calculateInt ( int a, int b) {
return a + b;
}
} ;
} else {
result = new Calculable ( ) {
@Override
public int calculateInt ( int a, int b) {
return a - b;
}
} ;
}
return result;
}
}
方法 calculate 中 opr 参数是运算符,返回值是实现 Calculable 接口对象。代码第 13 行和第 23 行都采用匿名内部类实现 Calculable 接口。代码第 16 行实现加法运算。代码第 26 行实现减法运算。
public static void main ( String [ ] args) {
int n1 = 10 ;
int n2 = 5 ;
Calculable f1 = calculate ( '+' ) ;
Calculable f2 = calculate ( '-' ) ;
System . out. println ( n1 + "+" + n2 + "=" + f1. calculateInt ( n1, n2) ) ;
System . out. println ( n1 + "-" + n2 + "=" + f1. calculateInt ( n1, n2) ) ;
}
代码第 5 行中 f1 是实现加法计算 Calculable 对象,代码第 7 行中 f2 是实现减法计算 Calculable 对象。代码第 9 行和第 12 行才进行方法调用。
上述代码中列出了两种输出方式,下面简单介绍一下 Java 中常见的输出函数:
* printf 主要继承了C 语言中 printf 的一些特性,可以进行格式化输出。
* print 就是一般的标准输出,但是不换行。
* println 和 print 基本没什么差别,就是最后会换行。
lambda表达式示例: 修改之后的通用方法 calculate 代码如下:
public static Calculable calculate ( char opr) {
Calculable result;
if ( opr == '+' ) {
result = ( int a, int b) -> {
return a + b;
} ;
} else {
result = ( int a, int b) -> {
return a - b;
} ;
}
return result;
}
代码第 10 行和第 15 行用 Lambda 表达式替代匿名内部类,可见代码变得简洁。通过以上示例我们发现,Lambda 表达式是一个匿名函数(方法)代码块,可以作为表达式、方法参数和方法返回值。
Lambda 表达式标准语法形式如下:
( 参数列表) -> {
}
-> 被称为箭头操作符或 Lambda 操作符,箭头操作符将 Lambda 表达式拆分成两部分:
1. 左侧:Lambda 表达式的参数列表。
2. 右侧:Lambda 表达式中所需执行的功能,用{ } 包起来,即 Lambda 体。
Java Lambda 表达式的优缺点
优点:
* 代码简洁,开发迅速
* 方便函数式编程
* 非常容易进行并行计算
* Java 引入 Lambda ,改善了集合操作(引入 Stream API )
缺点:
* 代码可读性变差
* 在非并行计算中,很多计算未必有传统的 for 性能要高
* 不容易进行调试
4.10.1 函数式接口
Lambda 表达式实现的接口不是普通的接口,而是函数式接口。如果一个接口中,有且只有一个抽象的方法(Object 类中的方法不包括在内),那这个接口就可以被看做是函数式接口。这种接口只能有一个方法。如果接口中声明多个抽象方法,那么 Lambda 表达式会发生编译错误:
The target type of this expression must be a functional interface
这说明该接口不是函数式接口,为了防止在函数式接口中声明多个抽象方法,Java 8 提供了一个声明函数式接口注解 @FunctionalInterface ,示例代码如下。
@FunctionalInterface
public interface Calculable {
int calculateInt ( int a, int b) ;
}
在接口之前使用 @FunctionalInterface 注解修饰,那么试图增加一个抽象方法时会发生编译错误。但可以添加默认方法和静态方法。
@FunctionalInterface 注解与 @Override 注解的作用类似。Java 8 中专门为函数式接口引入了一个新的注解 @FunctionalInterface 。该注解可用于一个接口的定义上,一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。需要注意的是,即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。
提示:Lambda 表达式是一个匿名方法代码,Java 中的方法必须声明在类或接口中,那么 Lambda 表达式所实现的匿名方法是在函数式接口中声明的。
4.10.2 作为参数使用Lambda表达式
Lambda 表达式一种常见的用途就是作为参数传递给方法,这需要声明参数的类型声明为函数式接口类型。示例代码如下:
public static void main ( String [ ] args) {
int n1 = 10 ;
int n2 = 5 ;
display ( ( a, b) -> {
return a + b;
} , n1, n2) ;
display ( ( a, b) -> a - b, n1, n2) ;
}
public static void display ( Calculable calc, int n1, int n2) {
System . out. println ( calc. calculateInt ( n1, n2) ) ;
}
上述代码第 19 行定义 display 打印计算结果方法,其中参数 calc 类型是 Calculable ,这个参数即可以接收实现 Calculable 接口的对象,也可以接收 Lambda 表达式,因为 Calculable 是函数式接口。 代码第 7 行和第 9 行两次调用 display 方法,它们第一个参数都是 Lambda 表达式。
4.10.3 访问变量
Lambda 表达式可以访问所在外层作用域定义的变量,包括成员变量和局部变量。
4.10.3.1 访问成员变量
成员变量包括实例成员变量和静态成员变量。在 Lambda 表达式中可以访问这些成员变量,此时的 Lambda 表达式与普通方法一样,可以读取成员变量,也可以修改成员变量。
public class LambdaDemo {
private int value = 10 ;
private static int staticValue = 5 ;
public static Calculable add ( ) {
Calculable result = ( int a, int b) -> {
staticValue++ ;
int c = a + b + staticValue;
return c;
} ;
return result;
}
public Calculable sub ( ) {
Calculable result = ( int a, int b) -> {
staticValue++ ;
this . value++ ;
int c = a - b - staticValue - this . value;
return c;
} ;
return result;
}
}
LambdaDemo 类中声明一个实例成员变量 value 和一个静态成员变量 staticValue。此外,还声明了静态方法 add(见代码第 8 行)和实例方法 sub(见代码第 20 行)。add 方法是静态方法,静态方法中不能访问实例成员变量,所以代码第 13 行的 Lambda 表达式中也不能访问实例成员变量,也不能访问实例成员方法。
sub 方法是实例方法,实例方法中能够访问静态成员变量和实例成员变量,所以代码第 23 行的 Lambda 表达式中可以访问这些变量,当然实例方法和静态方法也可以访问。当访问实例成员变量或实例方法时可以使用 this ,如果不与局部变量发生冲突情况下可以省略 this 。
4.10.3.2 访问局部变量
对于成员变量的访问 Lambda 表达式与普通方法没有区别,但是访问局部变量时,变量必须是 final 类型的(不可改变)。
示例代码如下:
public class LambdaDemo {
private int value = 10 ;
private static int staticValue = 5 ;
public static Calculable add ( ) {
int localValue = 20 ;
Calculable result = ( int a, int b) -> {
int c = a + b + localValue;
return c;
} ;
return result;
}
public Calculable sub ( ) {
final int localValue = 20 ;
Calculable result = ( int a, int b) -> {
int c = a - b - staticValue - this . value;
return c;
} ;
return result;
}
}
上述代码第 10 行和第 23 行都声明一个局部变量 localValue,Lambda 表达式中访问这个变量,如代码第 14 行和第 25 行。不管这个变量是否显式地使用 final 修饰,它都不能在 Lambda 表达式中修改变量,所以代码第 12 行和第 26 行如果去掉注释会发生编译错误。
Lambda 表达式只能访问局部变量而不能修改,否则会发生编译错误,但对静态变量和成员变量可读可写。
4.10.4 方法引用
方法引用可以理解为 Lambda 表达式的快捷写法,它比 Lambda 表达式更加的简洁,可读性更高,有很好的重用性。如果实现比较简单,复用的地方又不多,推荐使用 Lambda 表达式,否则应该使用方法引用。
Java 8 之后增加了双冒号:: 运算符,该运算符用于“方法引用”,注意不是调用方法。“方法引用”虽然没有直接使用 Lambda 表达式,但也与 Lambda 表达式有关,与函数式接口有关。 方法引用的语法格式如下:
ObjectRef :: methodName
其中,ObjectRef 是类名或者实例名,methodName 是相应的方法名。
注意:被引用方法的参数列表和返回值类型,必须与函数式接口方法参数列表和方法返回值类型一致,示例代码如下。
public class LambdaDemo {
public static int add ( int a, int b) {
return a + b;
}
public int sub ( int a, int b) {
return a - b;
}
}
LambdaDemo 类中提供了一个静态方法 add,一个实例方法 sub。这两个方法必须与函数式接口方法参数列表一致,方法返回值类型也要保持一致。
public class HelloWorld {
public static void main ( String [ ] args) {
int n1 = 10 ;
int n2 = 5 ;
display ( LambdaDemo :: add , n1, n2) ;
LambdaDemo d = new LambdaDemo ( ) ;
display ( d:: sub , n1, n2) ;
}
public static void display ( Calculable calc, int n1, int n2) {
System . out. println ( calc. calculateInt ( n1, n2) ) ;
}
}
代码第 18 行声明 display 方法,第一个参数 calc 是 Calculable 类型,它可以接受三种对象:Calculable 实现对象、Lambda 表达式和方法引用。代码第 6 行中第一个实际参数LambdaDemo :: add 是静态方法的方法引用。代码第 9 行中第一个实际参数d:: sub ,是实例方法的方法引用,d 是 LambdaDemo 实例。
提示:代码第 6 行的LambdaDemo :: add 和第 9 行的d:: sub 是方法引用,此时并没有调用方法,只是将引用传递给 display 方法,在 display 方法中才真正调用方法。
五、异常处理
5.1 异常处理
异常(exception)是在运行程序时产生的一种异常情况。
Java 中的异常又称为例外,是一个在程序执行期间发生的事件,它中断正在执行程序的正常指令流。
在 Java 中一个异常的产生,主要有如下三种原因:
1. Java 内部错误发生异常,Java 虚拟机产生的异常。
2. 编写的程序代码中的错误所产生的异常,例如空指针异常、数组越界异常等。
3. 通过 throw 语句手动生成的异常,一般用来告知该方法的调用者一些必要信息。
示例:
为了更好地理解什么是异常,下面来看一段非常简单的 Java 程序。下面的示例代码实现了允许用户输入 1 ~ 3 以内的整数,其他情况提示输入错误。
import java. util. Scanner ;
public class Test01 {
public static void main ( String [ ] args) {
System . out. println ( "请输入您的选择:(1~3 之间的整数)" ) ;
Scanner input = new Scanner ( System . in) ;
int num = input. nextInt ( ) ;
switch ( num) {
case 1 :
System . out. println ( "one" ) ;
break ;
case 2 :
System . out. println ( "two" ) ;
break ;
case 3 :
System . out. println ( "three" ) ;
break ;
default :
System . out. println ( "error" ) ;
break ;
}
}
}
正常情况下,用户会按照系统的提示输入 1 ~ 3 之间的数字。但是,如果用户没有按要求进行输入,例如输入了一个字母“a”,则程序在运行时将会发生异常,运行结果如下所示。
请输入您的选择:(1 ~ 3 之间的整数)
a
Exception in thread "main" java. util. InputMismatchException
at java. util. Scanner. throwFor ( Unknown Source )
at java. util. Scanner. next ( Unknown Source )
at java. util. Scanner. nextInt ( Unknown Source )
at java. util. Scanner. nextInt ( Unknown Source )
at text. text. main ( text. java: 11 )
5.1.1 异常类型
在 Java 中所有异常类型都是内置类 java. lang. Throwable 类的子类,即 Throwable 位于异常类层次结构的顶层。Throwable 类是所有异常和错误的超类,下面有 Error 和 Exception 两个子类分别表示错误和异常:
* Exception 类用于用户程序可能出现的异常情况,它也是用来创建自定义异常类型类的类。
* Error 定义了在通常环境下不希望被程序捕获的异常。一般指的是 JVM 错误,如堆栈溢出。
异常类型 说明 ArithmeticException 算术错误异常,如以零做除数 ArraylndexOutOfBoundException 数组索引越界 ArrayStoreException 向类型不兼容的数组元素赋值 ClassCastException 类型转换异常 IllegalArgumentException 使用非法实参调用方法 lIIegalStateException 环境或应用程序处于不正确的状态 lIIegalThreadStateException 被请求的操作与当前线程状态不兼容 IndexOutOfBoundsException 某种类型的索引越界 NullPointerException 尝试访问 null 对象成员,空指针异常 NegativeArraySizeException 再负数范围内创建的数组 NumberFormatException 数字转化格式异常,比如字符串到 float 型数字的转换无效 TypeNotPresentException 类型未找到
异常类型 说明 ClassNotFoundException 没有找到类 IllegalAccessException 访问类被拒绝 InstantiationException 试图创建抽象类或接口的对象 InterruptedException 线程被另一个线程中断 NoSuchFieldException 请求的域不存在 NoSuchMethodException 请求的方法不存在 ReflectiveOperationException 与反射有关的异常的超类
5.1.2 Error和Exception异同
Error (错误)和 Exception (异常)都是 java. lang. Throwable 类的子类,在 Java 代码中只有继承了 Throwable 类的实例才能被 throw 或者 catch 。
如下是常见的 Error 和 Exception :
1 )运行时异常(RuntimeException ):
NullPropagation :空指针异常;
ClassCastException :类型强制转换异常
IllegalArgumentException :传递非法参数异常
IndexOutOfBoundsException :下标越界异常
NumberFormatException :数字格式异常
2 )非运行时异常:
ClassNotFoundException :找不到指定 class 的异常
IOException :IO 操作异常
3 )错误(Error ):
NoClassDefFoundError :找不到 class 定义异常
StackOverflowError :深递归导致栈被耗尽而抛出的异常
OutOfMemoryError :内存溢出异常
堆栈溢出错误:
class StackOverflow {
public static void test ( int i) {
if ( i == 0 ) {
return ;
} else {
test ( i++ ) ;
}
}
}
public class ErrorEg {
public static void main ( String [ ] args) {
StackOverflow . test ( 5 ) ;
}
}
5.2 异常处理机制
Java 的异常处理通过 5 个关键字来实现:try 、catch 、throw 、throws 和 finally 。
try catch 语句用于捕获并处理异常,finally 语句用于在任何情况下(除特殊情况外)都必须执行的代码,throw 语句用于拋出异常,throws 语句用于声明可能会出现的异常。
Java 的异常处理机制提供了一种结构性和控制性的方式来处理程序执行期间发生的事件。异常处理的机制如下:
* 在方法中用 try catch 语句捕获并处理异常,catch 语句可以有多个,用来匹配多个异常。
* 对于处理不了的异常或者要转型的异常,在方法的声明处通过 throws 语句拋出异常,即由上层的调用方法来处理。
示例:
以下代码是异常处理程序的基本结构:
try {
逻辑程序块
} catch ( ExceptionType1 e) {
处理代码块1
} catch ( ExceptionType2 e) {
处理代码块2
throw ( e) ;
} finally {
释放资源代码块
}
5.2.1 try catch语句
在 Java 中通常采用 try catch 语句来捕获异常并处理: ( 在语法中,把可能引发异常的语句封装在 try 语句块中,用以捕获可能发生的异常。catch 后的( ) 里放匹配的异常类,指明 catch 语句可以处理的异常类型,发生异常时产生异常类的实例化对象。)
try {
} catch ( ExceptionType e) {
}
在上面语法的处理代码块中,可以使用以下3 个方法输出相应的异常信息:
* printStackTrace ( ) 方法:指出异常的类型、性质、栈层次及出现在程序中的位置。
* getMessage ( ) 方法:输出错误的性质。
* toString ( ) 方法:给出异常的类型与性质。
示例:
编写一个录入学生姓名、年龄和性别的程序,要求能捕捉年龄不为数字时的异常。在这里使用 try catch 语句来实现,具体代码如下:
import java. util. Scanner ;
public class Test02 {
public static void main ( String [ ] args) {
Scanner scanner = new Scanner ( System . in) ;
System . out. println ( "---------学生信息录入---------------" ) ;
String name = "" ;
int age = 0 ;
String sex = "" ;
try {
System . out. println ( "请输入学生姓名:" ) ;
name = scanner. next ( ) ;
System . out. println ( "请输入学生年龄:" ) ;
age = scanner. nextInt ( ) ;
System . out. println ( "请输入学生性别:" ) ;
sex = scanner. next ( ) ;
} catch ( Exception e) {
e. printStackTrace ( ) ;
System . out. println ( "输入有误!" ) ;
}
System . out. println ( "姓名:" + name) ;
System . out. println ( "年龄:" + age) ;
}
}
5.2.2 多重catch语句
如果 try 代码块中有很多语句会发生异常,而且发生的异常种类又很多。那么可以在 try 后面跟有多个 catch 代码块。多 catch 代码块语法如下:
try {
} catch ( ExceptionType e) {
} catch ( ExceptionType e) {
} catch ( ExceptionType e) {
. . .
}
示例:
public class Test03 {
public static void main ( String [ ] args) {
Date date = readDate ( ) ;
System . out. println ( "读取的日期 = " + date) ;
}
public static Date readDate ( ) {
FileInputStream readfile = null ;
InputStreamReader ir = null ;
BufferedReader in = null ;
try {
readfile = new FileInputStream ( "readme.txt" ) ;
ir = new InputStreamReader ( readfile) ;
in = new BufferedReader ( ir) ;
String str = in. readLine ( ) ;
if ( str == null ) {
return null ;
}
DateFormat df = new SimpleDateFormat ( "yyyy-MM-dd" ) ;
Date date = df. parse ( str) ;
return date;
} catch ( FileNotFoundException e) {
System . out. println ( "处理FileNotFoundException..." ) ;
e. printStackTrace ( ) ;
} catch ( IOException e) {
System . out. println ( "处理IOException..." ) ;
e. printStackTrace ( ) ;
} catch ( ParseException e) {
System . out. println ( "处理ParseException..." ) ;
e. printStackTrace ( ) ;
}
return null ;
}
}
5.2.3 try catch finally语句
使用 try - catch - finally 语句时需注意以下几点:
1. 异常处理语法结构中只有 try 块是必需的,也就是说,如果没有 try 块,则不能有后面的 catch 块和 finally 块;
2. catch 块和 finally 块都是可选的,但 catch 块和 finally 块至少出现其中之一,也可以同时出现;
3. 可以有多个 catch 块,捕获父类异常的 catch 块必须位于捕获子类异常的后面;
4. 不能只有 try 块,既没有 catch 块,也没有 finally 块;
5. 多个 catch 块必须位于 try 块之后,finally 块必须位于所有的 catch 块之后。
6. finally 与 try 语句块匹配的语法格式,此种情况会导致异常丢失,所以不常见。
try catch finally 语句块的执行情况可以细分为以下 3 种情况:
* 如果 try 代码块中没有拋出异常,则执行完 try 代码块之后直接执行 finally 代码块,然后执行 try catch finally 语句块之后的语句。
* 如果 try 代码块中拋出异常,并被 catch 子句捕捉,那么在拋出异常的地方终止 try 代码块的执行,转而执行相匹配的 catch 代码块,之后执行 finally 代码块。如果 finally 代码块中没有拋出异常,则继续执行 try catch finally 语句块之后的语句;如果 finally 代码块中拋出异常,则把该异常传递给该方法的调用者。
* 如果 try 代码块中拋出的异常没有被任何 catch 子句捕捉到,那么将直接执行 finally 代码块中的语句,并把该异常传递给该方法的调用者。
除非在 try 块、catch 块中调用了退出虚拟机的方法System . exit ( int status) ,否则不管在 try 块或者 catch 块中执行怎样的代码,出现怎样的情况,异常处理的 finally 块总会执行。
finally 语句可以与前面介绍的 try catch 语句块匹配使用,语法格式如下:
try {
} catch ( ExceptionType e) {
} finally {
}
对于以上格式,无论是否发生异常(除特殊情况外),finally 语句块中的代码都会被执行。此外,finally 语句也可以和 try 语句匹配使用,其语法格式如下:
try {
} finally {
}
5.2.4 JAVA9增强的自动资源管理
1. try 语句中声明的资源被隐式声明为 final ,资源的作用局限于带资源的 try 语句。
2. 可以在一条 try 语句中声明或初始化多个资源,每个资源以; 隔开即可。
3. 需要关闭的资源必须实现了 AutoCloseable 或 Closeable 接口。
Closeable 是 AutoCloseable 的子接口,Closeable 接口里的 close ( ) 方法声明抛出了 IOException ,因此它的实现类在实现 close ( ) 方法时只能声明抛出 IOException 或其子类;AutoCloseable 接口里的 close ( ) 方法声明抛出了 Exception ,因此它的实现类在实现 close ( ) 方法时可以声明抛出任何异常。
下面示范如何使用自动关闭资源的 try 语句。
public class AutoCloseTest {
public static void main ( String [ ] args) throws IOException {
try (
BufferedReader br = new BufferedReader ( new FileReader ( "AutoCloseTest.java" ) ) ;
PrintStream ps = new PrintStream ( new FileOutputStream ( "a.txt" ) ) ) {
System . out. println ( br. readLine ( ) ) ;
ps. println ( "C语言中文网" ) ;
}
}
}
上面程序中粗体字代码分别声明、初始化了两个 IO 流,BufferedReader 和 PrintStream 都实现了 Closeable 接口,并在 try 语句中进行了声明和初始化,所以 try 语句会自动关闭它们。
自动关闭资源的 try 语句相当于包含了隐式的 finally 块(这个 finally 块用于关闭资源),因此这个 try 语句可以既没有 catch 块,也没有 finally 块。
Java 7 几乎把所有的“资源类”(包括文件 IO 的各种类、JDBC 编程的 Connection 和 Statement 等接口)进行了改写,改写后的资源类都实现了 AutoCloseable 或 Closeable 接口。
如果程序需要,自动关闭资源的 try 语句后也可以带多个 catch 块和一个 finally 块。
Java 9 再次增强了这种 try 语句。Java 9 不要求在 try 后的圆括号内声明并创建资源,只需要自动关闭的资源有 final 修饰或者是有效的 final ( effectively final ) ,Java 9 允许将资源变量放在 try 后的圆括号内。上面程序在 Java 9 中可改写为如下形式。
public class AutoCloseTest {
public static void main ( String [ ] args) throws IOException {
final BufferedReader br = new BufferedReader ( new FileReader ( "AutoCloseTest.java" ) ) ;
final PrintStream ps = new PrintStream ( new FileOutputStream ( "a. txt" ) ) ;
try ( br; ps) {
System . out. println ( br. readLine ( ) ) ;
ps. println ( "C语言中文网" ) ;
}
}
}
5.3 声明和抛出异常
Java 中的异常处理除了捕获异常和处理异常之外,还包括声明异常和拋出异常。实现声明和抛出异常的关键字非常相似,它们是 throws 和 throw 。可以通过 throws 关键字在方法上声明该方法要拋出的异常,然后在方法内部通过 throw 拋出异常对象。
5.3.1 throws声明异常
当一个方法产生一个它不处理的异常时,那么就需要在该方法的头部声明这个异常,以便将该异常传递到方法的外部进行处理。使用 throws 声明的方法表示此方法不处理异常。throws 具体格式如下:
returnType method_name ( paramList) throws Exception 1 , Exception2 , …{ …}
returnType 表示返回值类型;method_name 表示方法名;paramList 表示参数列表;Exception 1 ,Exception2 ,… 表示异常类。
使用 throws 声明抛出异常的思路是,当前方法不知道如何处理这种类型的异常,该异常应该由向上一级的调用者处理;如果 main 方法也不知道如何处理这种类型的异常,也可以使用 throws 声明抛出异常,该异常将交给 JVM 处理。JVM 对异常的处理方法是,打印异常的跟踪栈信息,并中止程序运行,这就是前面程序在遇到异常后自动结束的原因。
创建一个 readFile ( ) 方法,该方法用于读取文件内容,在读取的过程中可能会产生 IOException 异常,但是在该方法中不做任何的处理,而将可能发生的异常交给调用者处理。在 main ( ) 方法中使用 try catch 捕获异常,并输出异常信息。首先在定义 readFile ( ) 方法时用 throws 关键字声明在该方法中可能产生的异常,然后在 main ( ) 方法中调用 readFile ( ) 方法,并使用 catch 语句捕获产生的异常:
import java. io. FileInputStream ;
import java. io. IOException ;
public class Test04 {
public void readFile ( ) throws IOException {
FileInputStream file = new FileInputStream ( "read.txt" ) ;
int f;
while ( ( f = file. read ( ) ) != - 1 ) {
System . out. println ( ( char ) f) ;
f = file. read ( ) ;
}
file. close ( ) ;
}
public static void main ( String [ ] args) {
Throws t = new Test04 ( ) ;
try {
t. readFile ( ) ;
} catch ( IOException e) {
System . out. println ( e) ;
}
}
}
5.3.2 throw抛出异常
与 throws 不同的是,throw 语句用来直接拋出一个异常,后接一个可拋出的异常类对象,其语法格式如下:
throw ExceptionObject ;
其中,ExceptionObject 必须是 Throwable 类或其子类的对象。如果是自定义异常类,也必须是 Throwable 的直接或间接子类。例如,以下语句在编译时将会产生语法错误:
throw new String ( "拋出异常" ) ;
示例:
在某仓库管理系统中,要求管理员的用户名需要由 8 位以上的字母或者数字组成,不能含有其他的字符。当长度在 8 位以下时拋出异常,并显示异常信息;当字符含有非字母或者数字时,同样拋出异常,显示异常信息。代码如下:
在 validateUserName ( ) 方法中两处拋出了 IllegalArgumentException 异常,即当用户名字符含有非字母或者数字以及长度不够 8 位时。在 main ( ) 方法中,调用了 validateUserName ( ) 方法,并使用 catch 语句捕获该方法可能拋出的异常。
import java. util. Scanner ;
public class Test05 {
public boolean validateUserName ( String username) {
boolean con = false ;
if ( username. length ( ) > 8 ) {
for ( int i = 0 ; i < username. length ( ) ; i++ ) {
char ch = username. charAt ( i) ;
if ( ( ch >= '0' && ch <= '9' ) || ( ch >= 'a' && ch <= 'z' ) || ( ch >= 'A' && ch <= 'Z' ) ) {
con = true ;
} else {
con = false ;
throw new IllegalArgumentException ( "用户名只能由字母和数字组成!" ) ;
}
}
} else {
throw new IllegalArgumentException ( "用户名长度必须大于 8 位!" ) ;
}
return con;
}
public static void main ( String [ ] args) {
Test05 te = new Test05 ( ) ;
Scanner input = new Scanner ( System . in) ;
System . out. println ( "请输入用户名:" ) ;
String username = input. next ( ) ;
try {
boolean con = te. validateUserName ( username) ;
if ( con) {
System . out. println ( "用户名输入正确!" ) ;
}
} catch ( IllegalArgumentException e) {
System . out. println ( e) ;
}
}
}
5.3.3 throws和throw区别
* throws 用来声明一个方法可能抛出的所有异常信息,表示出现异常的一种可能性,但并不一定会发生这些异常;throw 则是指拋出的一个具体的异常类型,执行 throw 则一定抛出了某种异常对象。
* 通常在一个方法(类)的声明处通过 throws 声明方法(类)可能拋出的异常信息,而在方法(类)内部通过 throw 声明一个具体的异常信息。
* throws 通常不用显示地捕获异常,可由系统自动将所有捕获的异常信息抛给上级方法; throw 则需要用户自己捕获相关的异常,而后再对其进行相关包装,最后将包装后的异常信息抛出。
5.3.4 JAVA7新特性(了解)
多 catch 代码块虽然客观上提高了程序的健壮性,但是也导致了程序代码量大大增加。如果有些异常种类不同,但捕获之后的处理是相同的,例如以下代码。
try {
} catch ( FileNotFoundException e) {
} catch ( IOException e) {
} catch ( ParseException e) {
}
3 个不同类型的异常,要求捕获之后的处理都是调用 methodA 方法。为了解决这种问题,Java 7 推出了多异常捕获技术,可以把这些异常合并处理。上述代码修改如下:
try {
} catch ( IOException | ParseException e) {
}
注意:由于 FileNotFoundException 属于 IOException 异常,IOException 异常可以捕获它的所有子类异常。所以不能写成 FileNotFoundException | IOException | ParseException 。
使用一个 catch 块捕获多种类型的异常时需要注意如下两个地方。
* 捕获多种类型的异常时,多种异常类型之间用竖线| 隔开。
* 捕获多种类型的异常时,异常变量有隐式的 final 修饰,因此程序不能对异常变量重新赋值。
下面程序示范了 Java 7 提供的多异常捕获。
public class ExceptionTest {
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 e) {
System . out. println ( "程序发生了数组越界、数字格式异常、算术异常之一" ) ;
e = new ArithmeticException ( "test" ) ;
} catch ( Exception e) {
System . out. println ( "未知异常" ) ;
e = new RuntimeException ( "test" ) ;
}
}
}
上面程序中第一行粗体字代码使用了IndexOutOfBoundsException | NumberFormatException | ArithmeticException 来定义异常类型,这就表明该 catch 块可以同时捕获这 3 种类型的异常。捕获多种类型的异常时,异常变量使用隐式的 final 修饰,因此上面程序的第 12 行代码将产生编译错误;捕获一种类型的异常时,异常变量没有 final 修饰,因此上面程序的第 17 行代码完全正确。
5.3.5 自定义异常
自定义异常的语法形式为:
< class > < 自定义异常名> < extends > < Exception >
自定义异常类一般包含两个构造方法:一个是无参的默认构造方法,另一个构造方法以字符串的形式接收一个定制的异常消息,并将该消息传递给超类的构造方法。
创建的自定义异常类 IntegerRangeException 类继承自 Exception 类,在该类中包含两个构造方法。
创建一个名称为 IntegerRangeException 的自定义异常类:
class IntegerRangeException extends Exception {
public IntegerRangeException ( ) {
super ( ) ;
}
public IntegerRangeException ( String s) {
super ( s) ;
}
}
示例:
编写一个程序,对会员注册时的年龄进行验证,即检测是否在 0 ~ 100 岁。
1 )这里创建了一个异常类 MyException ,并提供两个构造方法。MyException 类的实现代码如下:
public class MyException extends Exception {
public MyException ( ) {
super ( ) ;
}
public MyException ( String str) {
super ( str) ;
}
}
2 )接着创建测试类,调用自定义异常类。代码实现如下:
import java. util. InputMismatchException ;
import java. util. Scanner ;
public class Test07 {
public static void main ( String [ ] args) {
int age;
Scanner input = new Scanner ( System . in) ;
System . out. println ( "请输入您的年龄:" ) ;
try {
age = input. nextInt ( ) ;
if ( age < 0 ) {
throw new MyException ( "您输入的年龄为负数!输入有误!" ) ;
} else if ( age > 100 ) {
throw new MyException ( "您输入的年龄大于100!输入有误!" ) ;
} else {
System . out. println ( "您的年龄为:" + age) ;
}
} catch ( InputMismatchException e1) {
System . out. println ( "输入的年龄不是数字!" ) ;
} catch ( MyException e2) {
System . out. println ( e2. getMessage ( ) ) ;
}
}
}
3 )运行该程序,当用户输入的年龄为负数时,则拋出 MyException 自定义异常,执行第二个 catch 语句块中的代码,打印出异常信息。
在该程序的主方法中,使用了 if …else if …else 语句结构判断用户输入的年龄是否为负数和大于 100 的数,如果是,则拋出自定义异常 MyException ,调用自定义异常类 MyException 中的含有一个 String 类型的构造方法。在 catch 语句块中捕获该异常,并调用 getMessage ( ) 方法输出异常信息。
提示:因为自定义异常继承自 Exception 类,因此自定义异常类中包含父类所有的属性和方法。
5.4 JAVA实例(验证用户信息)
假设在某仓库管理系统的登录界面中需要输入用户名和密码,其中用户名只能由 6 ~ 10 位数字组成,密码只能有 6 位,任何不符合用户名或者密码要求的情况都视为异常,并且需要捕获并处理该异常。
示例:
1 )编写自定义异常类 LoginException ,该类继承自 Exception 。在 LoginException 类中包含两个构造方法,分别为无参的构造方法和含有一个参数的构造方法,代码如下:
public class LoginException extends Exception {
public LoginException ( ) {
super ( ) ;
}
public LoginException ( String msg) {
super ( msg) ;
}
}
2 )创建测试类 Test08 ,在该类中定义 validateLogin ( ) 方法,用于对用户名和密码进行验证。当用户名或者密码不符合要求时,使用自定义异常类 LoginException 输出相应的异常信息。validateLogin ( ) 方法的定义如下:
public boolean validateLogin ( String username, String pwd) {
boolean con = false ;
boolean conUname = false ;
try {
if ( username. length ( ) >= 6 && username. length ( ) <= 10 ) {
for ( int i = 0 ; i < username. length ( ) ; i++ ) {
char ch = username. charAt ( i) ;
if ( ch >= '0' && ch <= '9' ) {
conUname = true ;
} else {
conUname = false ;
throw new LoginException ( "用户名中包含有非数字的字符!" ) ;
}
}
} else {
throw new LoginException ( "用户名长度必须在6?10位之间!" ) ;
}
if ( conUname) {
if ( pwd. length ( ) == 6 ) {
con= true ;
} else {
con = false ;
throw new LoginException ( "密码长度必须为 6 位!" ) ;
}
}
} catch ( LoginException e) {
System . out. println ( e. getMessage ( ) ) ;
}
return con;
}
3 )在 Test08 类中添加 main ( ) 方法,调用 validateLogin ( ) 方法,如果该方法返回 true ,则输出登录成功的信息。main ( ) 方法的定义如下:
public static void main ( String [ ] args) {
Scanner input = new Scanner ( System . in) ;
System . out. println ( "用户名:" ) ;
String username = input. next ( ) ;
System . out. println ( "密码:" ) ;
String password = input. next ( ) ;
Test08 lt = new Test08 ( ) ;
boolean con = lt. validateLogin ( username, password) ;
if ( con) {
System . out. println ( "登录成功!" ) ;
}
}
5.5 异常跟踪栈(printStackTrace方法)
异常对象的 printStackTrace() 方法用于打印异常的跟踪栈信息,根据 printStackTrace() 方法的输出结果,开发者可以找到异常的源头,并跟踪到异常一路触发的过程。
测试 printStackTrace 的例子程序。
class SelfException extends RuntimeException {
SelfException() {
}
SelfException(String msg) {
super(msg);
}
}
public class PrintStackTraceTest {
public static void main(String[] args) {
firstMethod();
}
public static void firstMethod() {
secondMethod();
}
public static void secondMethod() {
thirdMethod();
}
public static void thirdMethod() {
throw new SelfException("自定义异常信息");
}
}
上面程序中 main 方法调用 firstMethod,firstMethod 调用 secondMethod,secondMethod 调用 thirdMethod,thirdMethod 直接抛出一个 SelfException 异常。运行上面程序,会看到如下所示的结果。
Exception in thread "main" Test.SelfException: 自定义异常信息
at Test.PrintStackTraceTest.thirdMethod(PrintStackTraceTest.java:26)
at Test.PrintStackTraceTest.secondMethod(PrintStackTraceTest.java:22)
at Test.PrintStackTraceTest.firstMethod(PrintStackTraceTest.java:18)
at Test.PrintStackTraceTest.main(PrintStackTraceTest.java:14)
上面运行结果的第 2 行到第 5 行之间的内容是异常跟踪栈信息,从打印的异常信息我们可以看出,异常从 thirdMethod 方法开始触发,传到 secondMethod 方法,再传到 firstMethod 方法,最后传到 main 方法,在 main 方法终止,这个过程就是 Java 的异常跟踪栈。
在面向对象的编程中,大多数复杂操作都会被分解成一系列方法调用。这是因为实现更好的可重用性,将每个可重用的代码单元定义成方法,将复杂任务逐渐分解为更易管理的小型子任务。由于一个大的业务功能需要由多个对象来共同实现,在最终编程模型中,很多对象将通过一系列方法调用来实现通信,执行任务。
所以,面向对象的应用程序运行时,经常会发生一系列方法调用,从而形成“方法调用栈”,异常的传播则相反:只要异常没有被完全捕获(包括异常没有被捕获,或异常被处理后重新抛出了新异常),异常从发生异常的方法逐渐向外传播,首先传给该方法的调用者,该方法调用者再次传给其调用者……,直至最后传到 main 方法,如果 main 方法依然没有处理该异常,则 JVM 会中止该程序,并打印异常的跟踪栈信息。
很多初学者一看到上面运行结果的异常提示信息,就会惊慌失措,其实结果中的异常跟踪栈信息非常清晰,它记录了应用程序中执行停止的各个点。
异常跟踪栈信息的第一行一般详细显示异常的类型和异常的详细消息,接下来是所有异常的发生点,各行显示被调用方法中执行的停止位置,并标明类、类中的方法名、与故障点对应的文件的行。一行行地往下看,跟踪栈总是最内部的被调用方法逐渐上传,直到最外部业务操作的起点,通常就是程序的入口 main 方法或 Thread 类的 run 方法(多线程的情形)。
关于多线程可以参考教程的《Java多线程编程》一章,也可忽略本节关于多线程的内容,学习完多线程在了解本节内容。
下面例子程序示范了多线程程序中发生异常的情形。
public class ThreadExceptionTest implements Runnable {
public void run() {
firstMethod();
}
public void firstMethod() {
secondMethod();
}
public void secondMethod() {
int a = 5;
int b = 0;
int c = a / b;
}
public static void main(String[] args) {
new Thread(new ThreadExceptionTest()).start();
}
}
运行上面程序,会看到如下运行结果。
Exception in thread "Thread-0" java.lang.ArithmeticException: / by zero
at Test.ThreadExceptionTest.secondMethod(ThreadExceptionTest.java:14)
at Test.ThreadExceptionTest.firstMethod(ThreadExceptionTest.java:8)
at Test.ThreadExceptionTest.run(ThreadExceptionTest.java:4)
at java.lang.Thread.run(Unknown Source)
多线程异常的跟踪栈,从发生异常的方法开始,到线程的 run 方法结束。从上面的运行结果可以看出,程序在 Thread 的 run 方法中出现了 ArithmeticException 异常,这个异常的源头是 ThreadExcetpionTest 的 secondMethod 方法,位于 ThreadExcetpionTest.java 文件的 14 行。这个异常传播到 Thread 类的 run 方法就会结束(如果该异常没有得到处理,将会导致该线程中止运行)。
前面已经讲过,调用 Exception 的 printStackTrace() 方法就是打印该异常的跟踪栈信息,也就会看到上面两个示例运行结果中的信息。当然,如果方法调用的层次很深,将会看到更加复杂的异常跟踪栈。
提示:虽然 printStackTrace() 方法可以很方便地用于追踪异常的发生情况,可以用它来调试程序,但在最后发布的程序中,应该避免使用它。应该对捕获的异常进行适当的处理,而不是简单地将异常的跟踪栈信息打印出来。
5.6 JDK记录日志类
如果要生成简单的日志记录,可以使用全局日志记录器并调用其 info 方法,代码如下:
Logger . getGlobal ( ) . info ( "打印信息" ) ;
JDK Logging 把日志分为如下表 7 个级别,等级依次降低。
级别 SEVERE WARNING INFO CONFIG FINE FINER FINEST 调用方法 severe() warning() info() config() fine() finer() finest() 含义 严重 警告 信息 配置 良好 较好 最好
Logger 的默认级别是 INFO ,比 INFO 级别低的日志将不显示。Logger 的默认级别定义在 jre 安装目录的 lib 下面。
# Limit the message that are printed on the console to INFO and above.
java. util. logging. ConsoleHandler. level = INFO
所以在默认情况下,日志只显示前三个级别,对于所有的级别有下面几种记录方法:
logger. warning ( message) ;
logger. fine ( message) ;
同时,还可以使用 log 方法指定级别,例如:
logger. log ( Level . FINE , message) ;
可以使用 setLevel 方法设置级别,例如logger. setLevel ( Level . FINE ) ; 可以将 FINE 和更高级别的都记录下来。另外,还可以使用 Level . ALL 开启所有级别的记录,或者使用 Level . OFF 关闭所有级别的记录。
注意:如果将记录级别设计为 INFO 或者更低,则需要修改日志处理器的配置。默认的日志处理器不会处理低于 INFO 级别的信息。
public class Test {
private static Logger log = Logger . getLogger ( Test . class . toString ( ) ) ;
public static void main ( String [ ] args) {
log. finest ( "finest" ) ;
log. finer ( "finer" ) ;
log. fine ( "fine" ) ;
log. config ( "config" ) ;
log. info ( "info" ) ;
log. warning ( "warning" ) ;
log. severe ( "server" ) ;
}
}
5.6.1 修改日志管理器配置
可以通过编辑配置文件来修改日志系统的各种属性。在默认情况下,配置文件存在于 jre 安装目录下“jre/ lib/ logging. properties”。要想使用另一个配置文件,就要将 java. util. logging. config. file 特性设置为配置文件的存储位置,并用下列命令启动应用程序。
java - Djava . util. logging. config. file = configFile MainClass
日志管理器在 JVM 启动过程中初始化,这在 main 执行之前完成。如果在 main 中调用System . setProperty ( "java.util.logging.config.file" , file) ,也会调用LogManager . readConfiguration ( ) 来重新初始化日志管理器。
要想修改默认的日志记录级别,就需要编辑配置文件,并修改以下命令行。
. level= INFO
可以通过添加以下内容来指定自己的日志记录级别
Test. Test . level= FINE
也就是说,在日志记录器名后面添加后缀 . level。
在稍后可以看到,日志记录并不将消息发送到控制台上,这是处理器的任务。另外,处理器也有级别。要想在控制台上看到 FINE 级别的消息,就需要进行下列设置。
java. util. logging. ConsoleHandler. level= FINE
注意:在日志管理器配置的属性设置不是系统属性,因此,用 - Dcom . mycompany. myapp. level= FINE 启动应用程序不会对日志记录器产生任何影响。