类的继承
继承的概念
在现实生活中,说到继承,多会想到子女继承父辈的财产、事业等。在程序中,继承描述的是事物之间的所属关系,通过继承可以使多种事物之间形成一种关联体系。例如猫和狗都属于动物,程序中便可以描述为猫和狗继承自动物;同理,波斯猫和巴厘猫继承自猫,而沙皮狗和斑点狗继承自狗。这些动物之间会形成一个继承体系,如图1所示。
在Java中,类的继承是指在一个现有类的基础上去构建一个新的类,构建出来的新类被称作子类,现有类被称作父类或基类,子类会自动拥有父类所有可继承的属性和方法。
在程序中,如果想声明一个类继承另一个类,需要使用extends关键字,其基本语法格式如下:
[修饰符] class 子类名 extends 父类名 {
// 程序核心代码
}
在上述语法格式中,类的修饰符是可选的,用来指定类的访问权限,可以使用public或者省略不写;子类名和父类名都是必选的,并且子类与父类之间要使用extends关键字实现继承关系。
接下来通过一个案例来学习子类是如何继承父类的,如文件1所示。
文件1 Example01.java
1 // 定义Animal类
2 class Animal {
3 String name; // 声明name属性
4 // 定义动物叫的方法
5 void shout() {
6 System.out.println("动物发出叫声");
7 }
8 }
9 // 定义Dog类继承Animal类
10 class Dog extends Animal {
11 // 定义一个打印name的方法
12 public void printName() {
13 System.out.println("name=" + name);
14 }
15 }
16 // 定义测试类
17 public class Example01 {
18 public static void main(String[] args) {
19 Dog dog = new Dog(); // 创建一个Dog类的实例对象
20 dog.name = "沙皮狗"; // 为dog对象的name属性进行赋值
21 dog.printName(); // 调用dog对象的printName()方法
22 dog.shout(); // 调用Dog类继承来的shout()方法
23 }
24 }
运行结果如图2所示。
图2 运行结果
在文件1中,Dog类通过extends关键字继承了Animal类,这样Dog类便是Animal类的子类。从图2可以看出,子类虽然没有声明name属性和shout()方法,但是却能访问这两个成员。这就说明,子类在继承父类的时候,会自动拥有父类所有公共的成员。
在实现类的继承中,需要注意一些问题,具体如下:
(1)在Java中,类只支持单继承,不允许多重继承,也就是说一个类只能有一个直接父类,例如下面这种情况是不合法的。
class A{}
class B{}
class C extends A,B{} // C类不可以同时继承A类和B类
(2)多个类可以继承同一个父类,例如下面这种情况是允许的。
class A{}
class B extends A{}
class C extends A{} // 类B和类C都可以继承类A
(3)在Java中,多层继承是可以的,即一个类的父类可以再去继承另外的父类,例如下面这种情况是允许的。
class A{}
class B extends A{} // 类B继承类A,类B是类A的子类
class C extends B{} // 类C继承类B,类C是类B的子类,同时也是类A的子类
(4)在Java中,子类和父类是一种相对概念,也就是说,一个类是某个类父类的同时,也可以是另一个类的子类。例如上面的第3个示例中,B类是A类的子类,同时又是C类的父类。
重写父类方法
在继承关系中,子类会自动继承父类中公共的方法,但有时在子类中需要对继承的方法进行一些修改,即对父类的方法进行重写。需要注意的是,子类中重写的方法需要和父类被重写的方法具有相同的方法名、参数列表以及返回值类型。
在上一节案例中,Dog类从Animal类继承了shout()方法,该方法在被调用时会打印“动物发出叫声”,这显然不能描述一种动物的具体叫声,Dog类对象属于犬类,发出的叫声应该是“汪汪”。为了解决这个问题,可以在Dog类中重写父类Animal中的shout()方法,如文件1所示。
文件1 Example02.java
1 // 定义Animal类
2 class Animal {
3 //定义动物叫的方法
4 void shout() {
5 System.out.println("动物发出叫声");
6 }
7 }
8 // 定义Dog类继承动物类
9 class Dog extends Animal {
10 // 定义狗叫的方法
11 void shout() {
12 System.out.println("汪汪……");
13 }
14 }
15 // 定义测试类
16 public class Example02 {
17 public static void main(String[] args) {
18 Dog dog = new Dog(); // 创建Dog类的实例对象
19 dog.shout(); // 调用Dog类重写的shout()方法
20 }
21 }
运行结果如图1所示。
图1 运行结果
文件1中,定义了Dog类并且继承自Animal类。在子类Dog中定义了一个shout()方法对父类的方法进行重写。从运行结果可以看出,在调用Dog类对象的shout()方法时,只会调用子类重写的方法,并不会调用父类的shout()方法。
注意:
子类重写父类方法时,不能使用比父类中被重写的方法更严格的访问权限。如:父类中的方法访问权限是public,子类重写父类该方法的访问权限就不能是private。
super关键字
从上一节案例的运行结果可以看出,当子类重写父类的方法后,子类对象将无法直接访问父类被重写的方法。为了解决这个问题,在Java中专门提供了一个super关键字来访问父类的成员,例如访问父类的成员变量、成员方法和构造方法。下面分两种情况来学习一下super关键字的具体用法。
(1)使用super关键字调用父类的成员变量和成员方法,具体格式如下:
super.成员变量
super.成员方法([参数1,参数2...])
接下来通过一个案例来学习如何使用super关键字调用父类的成员变量和成员方法,如文件1所示。
文件1 Example03.java
1 // 定义Animal类
2 class Animal {
3 String name = "动物";
4 // 定义动物叫的方法
5 void shout() {
6 System.out.println("动物发出叫声");
7 }
8 }
9 // 定义Dog类继承动物类
10 class Dog extends Animal {
11 String name = "犬类";
12 // 重写父类的shout()方法
13 void shout() {
14 super.shout(); // 访问父类的成员方法
15 }
16 // 定义打印name的方法
17 void printName() {
18 System.out.println("name=" + super.name);// 访问父类的成员变量
19 }
20 }
21 // 定义测试类
22 public class Example03{
23 public static void main(String[] args) {
24 Dog dog = new Dog(); // 创建一个dog对象
25 dog.shout(); // 调用dog对象重写的shout()方法
26 dog.printName(); // 调用dog对象的的printName()方法
27 }
28 }
运行结果如图1所示。
图1 运行结果
文件1中,定义了一个Dog类继承Animal类,重写了Animal类的shout()方法并重新定义了子类的name属性。在子类Dog的shout()方法中使用“super.shout()”调用了父类被重写的方法,在printName()方法中使用“super.name”访问父类的成员变量。从运行结果可以看出,子类通过super关键字成功地访问了父类成员变量和成员方法。
(2)使用super关键字调用父类的构造方法,具体格式如下:
super([参数1,参数2...])
接下来就通过一个案例来学习,如何使用super关键字来调用父类的构造方法,如文件2所示。
文件2 Example04.java
1 // 定义Animal类
2 class Animal {
3 // 定义Animal类有参的构造方法
4 public Animal(String name) {
5 System.out.println("我是一只" + name);
6 }
7 }
8 // 定义Dog类继承Animal类
9 class Dog extends Animal {
10 public Dog() {
11 super("沙皮狗"); // 调用父类有参的构造方法
12 }
13 }
14 // 定义测试类
15 public class Example04 {
16 public static void main(String[] args) {
17 Dog dog = new Dog(); // 创建Dog类的实例对象
18 }
19 }
运行结果如图2所示。
图2 运行结果
根据前面所学的知识,文件1中在创建Dog类对象时一定会调用Dog类的构造方法,从运行结果可以看出,Dog类的构造方法被调用时,执行了内部的super("沙皮狗")方法,从而调用了父类的有参构造方法。需要注意的是,通过super调用父类构造方法的代码必须位于子类构造方法的第一行,并且只能出现一次,否则程序在编译期间就会报错。
将文件1第11行代码进行注释,程序就会出现编译错误,如图3所示。
图3 运行结果
从图3可以看出,程序编译出现错误,显示“Implicit super constructor Animal() is undefined. Must explicitly invoke another constructor(未定义隐式无参构造方法,必须显示的调用另一个构造方法)”的错误。出错的原因是,在子类的构造方法中一定会调用父类的某个构造方法。这时可以在子类的构造方法中通过super关键字指定调用父类的哪个构造方法,如果没有指定,在实例化子类对象时,会默认调用父类无参的构造方法,而在文件2中,父类Animal中只定义了有参构造方法,未定义无参构造方法,所以在子类默认调用父类无参构造方法时就会出错。
为了解决上述程序的编译错误,可以在子类中显示地调用父类中已有的构造方法,或者在父类中定义无参的构造方法。现将文件2中的Animal类进行修改,在父类中添加无参构造方法来解决上述编译错误,如文件3所示。
文件3 Example05.java
1 // 定义Animal类
2 class Animal {
3 // 定义Animal无参的构造方法
4 public Animal() {
5 System.out.println("我是一只动物");
6 }
7 // 定义Animal有参的构造方法
8 public Animal(String name) {
9 System.out.println("我是一只" + name);
10 }
11 }
12 // 定义Dog类,继承自Animal类
13 class Dog extends Animal {
14 // 定义Dog类无参的构造方法
15 public Dog() {
16 }
17 }
18 // 定义测试类
19 public class Example05 {
20 public static void main(String[] args) {
21 Dog dog = new Dog(); // 创建Dog类的实例对象
22 }
23 }
运行结果如图4所示。
图4 运行结果
从图4可以看出,子类在实例化时默认调用了父类无参的构造方法。通过这个案例还可以得出一个结论:在定义一个类时,如果没有特殊需求,当定义了有参构造方法后,尽量在类中再显示地定义一个无参构造方法,这样可以避免该类被继承时出现错误。
Object类
在Java中提供了一个Object类,它是所有类的父类,即每个类都直接或间接继承自该类,因此,Object类通常被称之为超类、基类或根类。当定义一个类时,如果没有使用extends关键字为这个类显示地指定父类,那么该类会默认继承Object类。
Object类中自定义了一些方法,其中常用的方法如表1所示。
表1 Object类的常用方法
方法声明 | 功能描述 |
---|---|
boolean equals(Object) | 判断某个对象与此对象是否相等 |
final Class<?> getClass() | 返回此Object的运行时类 |
int hashCode() | 返回该对象的哈希码值 |
String toString() | 返回该对象的字符串表示 |
void finalize() | 垃圾回收器调用此方法来清理没有被任何引用变量所引用对象的资源 |
了解了Object类中的常用方法后,下面以toString()方法为例来演示Object类中方法的使用,如文件1所示。
文件1 Example06.java
1 // 定义Animal类
2 class Animal {
3 // 定义动物叫的方法
4 void shout() {
5 System.out.println("动物叫!");
6 }
7 }
8 // 定义测试类
9 public class Example06 {
10 public static void main(String[] args) {
11 Animal animal = new Animal(); // 创建Animal类对象
12 System.out.println(animal.toString()); // 调用toString()方法并打印
13 }
14 }
运行结果如图1所示。
图1 运行结果
文件1中,第12行代码调用了Animal对象的toString()方法,虽然Animal类中并没有定义这个方法,但程序并没有报错。这是因为Animal默认继承了Object类,在Object类中定义了toString()方法,在该方法中输出了对象的基本信息。
Object类的toString()方法中输出信息具体格式如下:
getClass().getName() + "@" + Integer.toHexString(hashCode());
为了方便初学者理解上面的代码,接下来分别对其中用到的方法进行解释,具体如下:
● getClass().getName():代表返回对象所属类的类名,即包名+类名的全限定名称。
● hashCode():代表返回该对象的哈希值。
● Integer.toHexString(hashCode()):代表将对象的哈希值用16进制表示。其中,
hashCode()是Object类中定义的一个方法,这个方法将对象的内存地址进行哈希
运算,返回一个int类型的哈希值。
在实际开发中,通常希望toString()方法返回的不仅仅是对象的基本信息,而是一些特有的信息,为此可以通过重写Object的toString()方法来实现,如文件2所示。
文件2 Example07.java
1 // 定义Animal类
2 class Animal {
3 // 重写Object类的toString()方法
4 public String toString() {
5 return "这是一只动物";
6 }
7 }
8 // 定义测试类
9 public class Example07 {
10 public static void main(String[] args) {
11 Animal animal = new Animal(); // 创建Animal对象
12 System.out.println(animal.toString()); // 打印animal的toString()
13 }
14 }
运行结果如图2所示。
图2 运行结果
文件2中,Animal类重写了Object类的toString()方法,当在main()方法中调用toString()方法时,就输出了Animal类的描述信息“这是一只动物”。
final关键字
final关键字修饰类
Java中的类被final关键字修饰后,该类将不可以被继承,也就是不能够派生子类。接下来通过一个案例来进行验证,如文件1所示。
文件1 Example08.java
1 // 使用final关键字修饰Animal类
2 final class Animal {
3 }
4 // Dog类继承Animal类
5 class Dog extends Animal {
6 }
7 // 定义测试类
8 public class Example08 {
9 public static void main(String[] args) {
10 Dog dog = new Dog(); // 创建Dog类的实例对象
11 }
12 }
程序编译报错,如图1所示。
图1 运行结果
文件1中,由于Animal类被final关键字所修饰,因此,当Dog类继承Animal类时,Eclipse的编辑器内出现了“The type Dog cannot subclass the final class Animal(无法从final类Animal进行继承)”的错误。由此可见,被final关键字修饰的类为最终类,不能被其他类继承。
final关键字修饰方法
当一个类的方法被final关键字修饰后,这个类的子类将不能重写该方法。接下来通过一个案例来进行验证,如文件1所示。
文件1 Example09.java
1 // 定义Animal类
2 class Animal {
3 // 使用final关键字修饰shout()方法
4 public final void shout() {
5 }
6 }
7 // 定义Dog类继承Animal类
8 class Dog extends Animal {
9 // 重写Animal类的shout()方法
10 public void shout() {
11 }
12 }
13 // 定义测试类
14 public class Example09 {
15 public static void main(String[] args) {
16 Dog dog=new Dog(); // 创建Dog类的实例对象
17 }
18 }
程序编译报错,如图1所示。
图1 运行结果
文件1中,Dog类重写父类Animal中的shout()方法后,程序出现了“Cannot override the final method from Animal(无法重写父类final方法)”的错误。由此可见,被final关键字修饰的方法为最终方法,子类不能对该方法进行重写。正是由于final的这种特性,当在父类中定义某个方法时,如果不希望被子类重写,就可以使用final关键字修饰该方法。
final关键字修饰变量
Java中被final修饰的变量被称之为常量,它只能被赋值一次,也就是说final修饰的变量一旦被赋值,其值不能改变。如果再次对该变量进行赋值,则程序会在编译时报错。接下来通过一个案例来进行验证,如文件1所示。
文件1 Example10.java
1 public class Example10 {
2 public static void main(String[] args) {
3 final int num = 2; // 第一次可以赋值
4 num = 4; // 再次赋值会报错
5 }
6 }
程序编译报错,如图1所示。
图1 运行结果
文件1中,当在第4行再次对num赋值时,程序出现了“The final local variable num cannot be assigned. It must be blank and not using a compound assignment(final变量num无法被重新赋值)”的错误。由此可见,被final修饰的变量为常量,它只能被赋值一次,其值不可改变。
文件1中,演示的是局部变量被final修饰的情况,当局部变量使用final关键字进行修饰时,可以在声明变量的同时对变量进行赋值,也可以先声明变量然后再进行有且只有一次的赋值。而当成员变量被final修饰时,在声明变量的同时必须进行初始化赋值,否则程序编译报错,接下来通过一个案例来演示final修饰成员变量和局部变量的情况,如文件2所示。
文件2 Example11.java
1 public class Example11 {
2 // final修饰的成员变量,必须在声明的同时进行赋值,否则编译错误
3 final int m;
4 public static void main(String[] args) {
5 // final修饰的局部变量,可以先声明,再进行一次赋值
6 final int n;
7 n = 4;
8 }
9 }
程序编译报错,如图2所示。
图2 运行结果
从图2可以看出,程序出现了编译错误,并提示“The blank final field m may not have been initialized(final修饰的变量m没有初始化)”。这是因为使用final关键字修饰成员变量时,Java虚拟机不会对其进行初始化。因此使用final修饰成员变量时,需要在定义变量的同时赋予一个初始值,下面将第3行代码修改为:
final int m = 0; // 为final修饰的成员变量m初始化赋值
保存后,Eclipse的编辑器将不会提示错误。
抽象类与接口
抽象类
当定义一个类时,常常需要定义一些方法来描述该类的行为特征,但有时这些方法的实现方式是无法确定的。例如前面在定义Animal类时,shout()方法用于表示动物的叫声,但是不同的动物,叫声也是不同的,因此在shout()方法中无法准确描述动物的叫声。如何能使Animal类中既包含shout()方法,又无需提供其方法的实现呢?
针对上述这种情况,Java提供了抽象方法来满足这种需求。抽象方法必须使用abstract关键字来修饰,并且在定义方法时不需要实现方法体。当一个类中包含了抽象方法,那么该类也必须使用abstract关键字来修饰,这种使用abstract关键字修饰的类就是抽象类。
抽象类及抽象方法定义的基本语法格式如下:
// 定义抽象类
[修饰符] abstract class 类名 {
// 定义抽象方法
[修饰符] abstract 方法返回值类型 方法名([参数列表]);
// 其他方法或属性
}
需要注意的是,包含抽象方法的类必须定义为抽象类,但抽象类中可以不包含任何抽象方法。另外,抽象类是不可以被实例化的,因为抽象类中有可能包含抽象方法,抽象方法是没有方法体的,不可以被调用。如果想调用抽象类中定义的抽象方法,需要创建一个子类,在子类中实现抽象类中的抽象方法。
小提示:
定义抽象方法只需要在普通方法上增加abstract关键字,并把普通方法的方法体(花括号以及花括号中的部分)全部去掉,然后在方法名称后增加英文分号即可,例如public abstract void shout();。
接下来通过一个案例来学习如何实现抽象类中的方法,如文件1所示。
文件1 Example12.java
1 // 定义抽象类Animal
2 abstract class Animal {
3 // 定义抽象方法shout()
4 public abstract void shout();
5 }
6 // 定义Dog类继承抽象类Animal
7 class Dog extends Animal {
8 // 实现抽象方法shout(),编写方法体
9 public void shout() {
10 System.out.println("汪汪……");
11 }
12 }
13 // 定义测试类
14 public class Example12 {
15 public static void main(String[] args) {
16 Dog dog = new Dog(); // 创建Dog类的实例对象
17 dog.shout(); // 调用dog对象的shout()方法
18 }
19 }
运行结果如图1所示。
图1 运行结果
从图1可以看出,子类实现了父类的抽象方法后,已经可以正常进行实例化操作,通过实例化的对象即可调用实现的方法。
接口
如果一个抽象类中的所有方法都是抽象的,则可以将这个类定义为Java中的另一种形式——接口。接口是一种特殊的抽象类,它不能包含普通方法,其内部的所有方法都是抽象方法,它将抽象进行的更为彻底。
在JDK 8中,对接口进行了重新定义,接口中除了抽象方法外,还可以有默认方法和静态方法(也叫类方法),默认方法使用default修饰,静态方法使用static修改,并且这两种方法都允许有方法体。
与定义类不同的是,在定义接口时,不再使用class关键字,而是使用interface关键字来声明。接口定义的基本语法格式如下:
[修饰符] interface 接口名 [extends 父接口1,父接口2,...] {
[public] [static] [final] 常量类型 常量名 = 常量值;
[public] [abstract] 方法返回值类型 方法名([参数列表]);
[public] default 方法返回值类型 方法名([参数列表]){
// 默认方法的方法体
}
[public] static 方法返回值类型 方法名([参数列表]){
// 类方法的方法体
}
}
在上述语法格式中,“[]”中的内容都是可选的,修饰符可以使用public或直接省略(省略时默认采用包权限访问控制符);“extends 父接口1,父接口2,...”表示定义一个接口时,可以同时继承多个父接口,这也是为了解决类的单继承的限制;在接口内部可以定义多个常量和抽象方法,定义常量时必须进行初始化赋值,定义默认方法和静态方法时,可以有方法体。
小提示:
在接口中定义常量时,可以省略“public static final”修饰符,此时,接口会默认为常量添加“public static final”修饰符。与此类似,在接口中定义抽象方法时,也可以省略“public abstract”修饰符,定义default默认方法和static静态方法时,可以省略“public”修饰符,这些修饰符系统都会默认进行添加。
从接口定义的语法格式可以看出,接口中可以包含三类方法,抽象方法、默认方法、静态方法,其中静态方法可以通过“接口名.方法名”的形式来调用,而抽象方法和默认方法只能通过接口实现类的实例对象来调用,因此,需要定义一个接口的实现类,该类通过implements关键字实现当前接口,并实现接口中的所有抽象方法。需要注意的是,一个类可以在继承另一个类的同时实现多个接口,并且多个接口之间需要使用英文逗号(,)分隔。
定义接口的实现类语法格式如下:
[修饰符] class 类名 [extends 父类名] [implements 接口1,接口2,...] {
...
}
了解了接口及其方法的定义方式后,接下来通过一个案例来学习接口的实现与方法调用,如文件1所示。
文件1 Example13.java
1 // 定义了Animal接口
2 interface Animal {
3 int ID = 1; // 定义全局常量
4 void breathe(); // 定义抽象方法breathe()
5 // 定义一个默认方法
6 default void getType(String type){
7 System.out.println("该动物属于:"+type);
8 }
9 // 定义一个静态方法
10 static int getID(){
11 return Animal.ID;
12 }
13 }
14 // Dog类实现了Animal接口
15 class Dog implements Animal {
16 // 实现breathe()方法
17 public void breathe() {
18 System.out.println("狗在呼吸");
19 }
20 }
21 // 定义测试类
22 public class Example13 {
23 public static void main(String args[]) {
24 System.out.println(Animal.getID()); // 通过接口名调用类方法
25 Dog dog = new Dog(); // 创建Dog类的实例对象
26 System.out.println(dog.ID); // 在实现类中获取接口全局常量
27 dog.breathe(); // 调用dog对象的breathe()方法
28 dog.getType("犬科"); // 通过接口实现类Dog的实例化对象,调用接口默认方法
29 }
30 }
运行结果如图1所示。
图1 运行结果
文件1中,Dog类通过implements关键字实现了Animal接口,并实现了接口中的抽象方法breathe()。从图4-15可以看出,通过接口实现类Dog的实例化对象可以访问接口中的常量、接口实现方法以及默认方法,而接口中的静态方法则可以直接使用接口名调用。需要注意的是,接口的实现类,必须实现接口中的所有抽象方法,否则程序编译报错。
文件1中,演示的是类与接口之间的实现关系,其实,接口与接口之间还可以是继承关系,接口中的继承同样使用extends关键字来实现,接下来对文件1稍加修改,演示接口之间的继承关系,修改后的代码如文件2所示。
文件2 Example14.java
1 // 定义了Animal接口
2 interface Animal {
3 int ID = 1; // 定义全局常量
4 void breathe(); // 定义抽象方法breathe()
5 // 定义一个默认方法
6 default void getType(String type){
7 System.out.println("该动物属于:"+type);
8 }
9 // 定义一个静态方法
10 static int getID(){
11 return Animal.ID;
12 }
13 }
14 // 定义了LandAnimal接口,并继承了Animal接口
15 interface LandAnimal extends Animal {
16 void run(); // 定义抽象方法run()
17 }
18 // Dog类实现了LandAnimal接口
19 class Dog implements LandAnimal {
20 // 实现breathe()方法
21 public void breathe() {
22 System.out.println("狗在呼吸");
23 }
24 // 实现run()方法
25 public void run() {
26 System.out.println("狗在陆地上跑");
27 }
28 }
29 // 定义测试类
30 public class Example14 {
31 public static void main(String args[]) {
32 System.out.println(Animal.getID()); // 通过接口名调用类方法
33 Dog dog = new Dog(); // 创建Dog类的实例对象
34 System.out.println(dog.ID); // 在实现类中获取接口全局常量
35 dog.breathe(); // 调用dog对象的breathe()方法
36 dog.getType("犬科"); // 通过dog对象,调用接口默认方法
37 dog.run(); // 调用dog对象的run()方法
38 }
39 }
运行结果如图2所示。
图2 运行结果
文件2中,定义了两个接口,其中LandAnimal接口继承了Animal接口,因此LandAnimal接口包含了2个抽象方法。当Dog类实现LandAnimal接口时,就需要实现这2个抽象方法。从图2可以看出,接口实现类Dog的实例化对象可以调用接口中的成员。
为了加深初学者对接口的认识,接下来对接口的特点进行归纳,具体如下:
(1)在JDK 8之前,接口中的方法都必须是抽象的,并且方法不能包含方法体。在调用抽象方法时,必须通过接口的实现类的对象才能调用实现方法;从JDK 8开始,接口中的方法除了包含抽象方法外,还包含默认方法和静态方法,默认方法和静态方法都可以有方法体,并且静态方法可以直接通过“接口.方法名”来调用。
(2)当一个类实现接口时,如果这个类是抽象类,只需实现接口中的部分抽象方法即可,否则需要实现接口中的所有抽象方法。
(3)一个类可以通过implements关键字同时实现多个接口,被实现的多个接口之间要用英文逗号(,)隔开。
(4)接口之间可以通过extends关键字实现继承,并且一个接口可以同时继承多个接口,接口之间用英文逗号(,)隔开。
(5)一个类在继承一个类的同时还可以实现接口,此时,extends关键字必须位于implements关键字之前。具体示例如下:
class A extends B implements C { // 先继承,再实现
...
}
多态
多态概述
在Java中,多态是指不同类的对象在调用同一个方法时所呈现出的多种不同行为。通常来说,在一个类中定义的属性和方法被其他类继承或重写后,当把子类对象直接赋值给父类引用变量时,相同引用类型的变量调用同一个方法所呈现出的多种不同形态。通过多态,消除了类之间的耦合关系,大大提高了程序的可扩展性和可维护性。
Java的多态性是由类的继承、方法重写以及父类引用指向子类对象体现的。由于一个父类可以有多个子类,多个子类都可以重写父类方法,并且多个不同的子类对象也可以指向同一个父类。这样,程序只有在运行时程序才能知道具体代表的是哪个子类对象,这就体现了多态性。
在解了Java多态性的概念后,接下来通过一个案例来进行演示说明,如文件1所示。
文件1 Example15.java
1 // 定义抽象类Animal
2 abstract class Animal {
3 abstract void shout(); // 定义抽象shout()方法
4 }
5 // 定义Cat类继承Animal抽象类
6 class Cat extends Animal {
7 // 实现shout()方法
8 public void shout() {
9 System.out.println("喵喵……");
10 }
11 }
12 // 定义Dog类继承Animal抽象类
13 class Dog extends Animal {
14 // 实现shout()方法
15 public void shout() {
16 System.out.println("汪汪……");
17 }
18 }
19 // 定义测试类
20 public class Example15 {
21 public static void main(String[] args) {
22 Animal an1 = new Cat();
23 Animal an2 = new Dog();
24 an1.shout();
25 an2.shout();
26 }
27 }
运行结果如图1所示。
图1 运行结果
文件1中,首先定义了一个抽象类Animal和抽象方法,接着定义了两个类Cat和Dog继承了Animal,在第22~25行代码中,分别创建了Cat和Dog两个类对象同时指向一个父类对象,并调用shout()方法,程序在编译时自动识别具体的子类对象,从而选择性的调用对应的方法,这就是Java中多态性的体现。由此可见,多态不仅解决了方法同名的问题,而且还使程序变的更加灵活,从而有效的提高程序的可扩展性和可维护性。
对象的类型转换
在多态的学习中,涉及到将子类对象当做父类类型使用的情况,此种情况在Java的语言环境中称为“向上转型”,例如下面两行代码:
Animal an1 = new Cat(); // 将Cat类对象当做Animal类型来使用
Animal an2 = new Dog(); // 将Dog类对象当做Animal类型来使用
将子类对象当做父类使用时不需要任何显式地声明,需要注意的是,此时不能通过父类变量去调用子类特有的方法。
接下来通过一个案例来演示对象的类型转换情况,如文件1所示。
文件1 Example16.java
1 // 定义接口Animal
2 interface Animal {
3 void shout(); // 定义抽象shout()方法
4 }
5 // 定义Cat类实现Animal接口
6 class Cat implements Animal {
7 // 实现接口shout()方法
8 public void shout() {
9 System.out.println("喵喵……");
10 }
11 // 定义Cat类特有的抓老鼠catchMouse()方法
12 public void catchMouse() {
13 System.out.println("小猫抓老鼠……");
14 }
15 }
16 // 定义测试类
17 public class Example16 {
18 public static void main(String[] args) {
19 Animal an1 = new Cat();
20 an1.shout();
21 an1.catchMouse();
22 }
23 }
程序编译报错,如图1所示。
图1 运行结果
从图1可以看出,程序编译出现了“The method catchMouse() is undefined for the type Anima(在父类Animal中未定义catchMouse()方法)”的错误。原因在于,创建Cat对象时指向了Animal父类类型,这样新创建的Cat对象会自动向上转型为Animal类,然后通过父类对象an1分别调用了shout()方法和子类Cat特有的catchMouse()方法,而catchMouse()方法是Cat类特有的,所以通过父类对象调用时,在编译期间就会报错。
文件1中,由于通过“new Cat();”创建的对象本质就是Cat类型,所以通过Cat类型的对象调用catchMouse()方法是可行的,因此要解决上面的问题,可以将父类类型的对象an1强转为Cat类型。接下来对文件1中的main()方法进行修改,具体代码如下:
// 定义测试类
public class Example16 {
public static void main(String[] args) {
Animal an1 = new Cat();
Cat cat = (Cat) an1;
cat.shout();
cat.catchMouse();
}
}
修改后再次编译,程序没有报错,运行结果如图2所示。
图2 运行结果
从图2可以看出,将本质为Cat类型的an1对象由Animal类型向下转型为Cat类型后,程序可以成功运行。需要注意的是,在进行对象向下类型转换时,必须转换为本质类型,否则转换时会出现错误,假如文件4-16中Animal类型引用指向的是一个Dog类型对象,这时进行强制类型转换为Cat类时就会出现出错,如文件2所示。
文件2 Example17.java
1 // 定义接口Animal
2 interface Animal {
3 void shout(); // 定义抽象shout()方法
4 }
5 // 定义Cat类实现Animal接口
6 class Cat implements Animal {
7 // 实现接口shout()方法
8 public void shout() {
9 System.out.println("喵喵……");
10 }
11 // 定义Cat类特有的抓老鼠catchMouse()方法
12 public void catchMouse() {
13 System.out.println("小猫抓老鼠……");
14 }
15 }
16 // 定义Dog类实现Animal接口
17 class Dog implements Animal {
18 // 实现接口shout()方法
19 public void shout() {
20 System.out.println("汪汪……");
21 }
22 }
23 // 定义测试类
24 public class Example17 {
25 public static void main(String[] args) {
26 Animal an1 = new Dog();
27 Cat cat = (Cat) an1;
28 cat.shout();
29 cat.catchMouse();
30 }
31 }
运行结果如图3所示。
图3 运行结果
文件2编译正常,但在运行时就会报错,提示Dog类型不能转换成Cat类型。出错的原因是,创建的Animal对象本质是一个Dog对象,在强制类型转换时,Dog类型的对象显然无法强转为Cat类型。
为了避免上述这种异常情况的发生,Java提供了一个关键字instanceof,它可以判断一个对象是否为某个类(或接口)的实例或者子类实例,语法格式如下:
对象(或者对象引用变量) instanceof 类(或接口)
接下来对文件2的测试类Example17进行修改,具体代码如下:
// 定义测试类
public class Example17 {
public static void main(String[] args) {
Animal an1 = new Dog();
if(an1 instanceof Cat){ // 判断an1本质类型
Cat cat = (Cat) an1;
cat.shout();
cat.catchMouse();
}else{
System.out.println("该类型的对象不是Cat类型!");
}
}
}
再次运行程序,结果如图4所示。
图4 运行结果
在对文件2修改的代码中,使用instanceof关键字判断对象an1本质是否为Cat类型,如果是Cat类型就强制转换为Cat类型,否则就打印“该类型的对象不是Cat类型!”。由于判断的对象an1本质为Dog类型并非Cat类型,因此出现图4的运行结果。
内部类
成员内部类
在一个类中除了可以定义成员变量、成员方法,还可以定义类,这样的类被称作成员内部类。在成员内部类中,可以访问外部类的所有成员,包括成员变量和成员方法;在外部类中,同样可以访问成员内部类的变量和方法。
接下来通过一个案例来学习成员内部类的定义和使用,如文件1所示。
文件1 Example18.java
1 // 定义外部类Outer
2 class Outer {
3 int m = 0; // 定义外部类的成员变量
4 // 定义外部类成员方法
5 void test1() {
6 System.out.println("外部类成员方法");
7 }
8 // 定义成员内部类Inner
9 class Inner {
10 int n = 1;
11 // 1、定义内部类方法,访问外部类成员变量和方法
12 void show1() {
13 System.out.println("外部类成员变量m="+m);
14 test1();
15 }
16 void show2(){
17 System.out.println("内部类成员方法");
18 }
19 }
20 // 2、定义外部类方法,访问内部类变量和方法
21 void test2() {
22 Inner inner = new Inner();
23 System.out.println("内部类成员变量n="+inner.n);
24 inner.show2();
25 }
26 }
27 // 定义测试类
28 public class Example18 {
29 public static void main(String[] args) {
30 Outer outer = new Outer(); // 创建外部类对象
31 Outer.Inner inner = outer.new Inner(); // 创建内部类对象
32 inner.show1(); // 测试在成员内部类中访问外部类成员变量和方法
33 outer.test2(); // 测试在外部类中访问内部类成员变量和方法
34 }
35 }
运行结果如图1所示。
图1 运行结果
文件1中,定义了一个外部类Outer,并在该类中定义了成员变量、成员方法和成员内部类Inner。在成员内部类Inner中,编写了show1()方法来测试内部类对外部类成员变量和方法的调用;同时在外部类Outer中,编写了test2()方法来测试外部类对内部类变量和方法的调用。从图1可以看出,成员内部类可以访问外部类所有成员,同时外部类也可以访问成员内部类的所有成员。
需要注意的是,在文件1中第31行代码是通过外部类对象创建的内部类对象,这样就可以操作内部类中的成员。创建内部类对象的具体语法格式如下:
外部类名.内部类名 变量名 = new 外部类名().new 内部类名();
局部内部类
局部内部类,也叫做方法内部类,就是定义在某个局部范围中的类,它和局部变量一样,都是在方法中定义的,其有效范围只限于方法内部。
在局部内部类中,局部内部类可以访问外部类的所有成员变量和方法,而局部内部类中的变量和方法却只能在创建该局部内部类的方法中进行访问。接下来通过一个案例来学习局部内部类的定义和使用,如文件1所示。
文件1 Example19.java
1 // 定义外部类Outer
2 class Outer {
3 int m = 0;
4 void test1(){
5 System.out.println("外部类成员方法");
6 }
7 void test2() {
8 // 1、定义局部内部类Inner,在局部内部类中访问外部类变量和方法
9 class Inner {
10 int n = 1;
11 void show() {
12 System.out.println("外部类变量m="+m);
13 test1();
14 }
15 }
16 // 2、在创建局部内部类的方法中,调用局部内部类变量和方法
17 Inner inner = new Inner();
18 System.out.println("局部内部类变量n="+inner.n);
19 inner.show();
20 }
21 }
22 // 定义测试类
23 public class Example19 {
24 public static void main(String[] args) {
25 Outer outer= new Outer();
26 outer.test2(); // 通过外部类对象调用创建了局部内部类的方法
27 }
28 }
运行结果如图1所示。
图1 运行结果
文件1中,定义了一个外部类Outer,并在该内中定义了成员变量、成员方法,接着在外部类的成员方法test2()中定义了一个局部内部类Inner;然后在局部内部类Inner中,编写了show()方法来测试对外部类变量和方法的调用;同时在创建该局部内部类的test2()方法中创建了局部内部类对象来测试对局部内部类变量和方法的调用。
从图1可以看出,局部内部类可以访问外部类所有成员,而只有在包含局部内部类的方法中才可以访问内部类的所有成员。
静态内部类
所谓静态内部类,就是使用static关键字修饰的成员内部类。与成员内部类相比,在形式上,静态内部类只是在内部类前增加了static关键字,但在功能上,静态内部类中只能访问外部类的静态成员,同时通过外部类访问静态内部类成员时,可以跳过外部类从而直接通过内部类访问静态内部类成员。
创建静态内部类对象的基本语法格式如下:
外部类名.静态内部类名 变量名 = new 外部类名.静态内部类名();
接下来通过一个案例来学习静态内部类的定义和使用,如文件1所示。
文件1 Example20.java
1 // 定义外部类Outer
2 class Outer {
3 static int m = 0; // 定义外部类静态变量m
4 static class Inner {
5 void show() {
6 // 静态内部类访问外部类静态成员
7 System.out.println("外部类静态变量m="+m);
8 }
9 }
10 }
11 // 定义测试类
12 public class Example20 {
13 public static void main(String[] args) {
14 // 静态内部类可以直接通过外部类创建
15 Outer.Inner inner = new Outer.Inner();
16 inner.show();
17 }
18 }
运行结果如图1所示。
图1 运行结果
文件1中,定义了一个外部类Outer,并在该内中定义了静态成员变量和静态内部类Inner。然后在静态内部类Inner中,编写了show()方法来测试对外部类静态变量的调用。
匿名内部类
在Java中调用某个方法时,如果该方法的参数是一个接口类型,除了可以传入一个参数接口实现类,还可以使用匿名内部类实现接口来作为该方法的参数。匿名内部类其实就是没有名称的内部类,在调用包含有接口类型参数的方法时,通常为了简化代码,不会创建一个接口的实现类作为方法参数传入,而是直接通过匿名内部类的形式传入一个接口类型参数,在匿名内部类中直接完成方法的实现。
创建匿名内部类的基本语法格式如下:
new 父接口(){
// 匿名内部类实现部分
}
接下来通过一个案例来学习匿名内部类的定义和使用,如文件1所示。
文件1 Example21.java
1 // 定义动物类接口
2 interface Animal {
3 void shout();
4 }
5 public class Example21{
6 public static void main(String[] args) {
7 String name = "小花";
8 // 定义匿名内部类作为参数传递给animalShout()方法
9 animalShout(new Animal() {
10 // 实现shout()方法
11 public void shout() {
12 // JDK 8开始,局部内部类、匿名内部类可以访问非final的局部变量
13 System.out.println(name+"喵喵...");
14 }
15 });
16 }
17 //定义静态方法animalShout(),接收接口类型参数
18 public static void animalShout(Animal an) {
19 an.shout(); // 调用传入对象an的shout()方法
20 }
21 }
运行结果如图1所示。
图1 运行结果
文件1中,调用animalShout(Animal an)方法时需要一个Animal接口类型的参数,在第9~15行代码就使用了匿名内部类方式实现Animal接口并作为参数传入。
需要注意的是,在文件1中的匿名内部中类访问了局部变量name,而局部变量name并没有使用final修饰符修饰,程序也没有报错,这是JDK 8开始的新特性,允许在局部内部类、匿名内部类中访问非final修饰的局部变量,而在JDK 8之前,局部变量前必须加final修饰符,否则程序编译报错。
对于初学者而言,可能会觉得匿名内部类的写法比较难理解,接下来分两步来介绍匿名内部类的编写,具体如下:
1.在调用animalShout()方法时,在方法的参数位置写上new Animal(){},这相当于创建了一个实例对象,并将对象作为参数传给animalShout()方法。在new Animal()后面有一对大括号,表示创建的对象为Animal的子类实例,该子类是匿名的。具体代码如下所示:
animalShout(new Animal(){});
2.在大括号中编写匿名子类的实现代码,具体如下所示:
animalShout(new Animal()
{
public void shout() {
System.out.println(name+"喵喵...");
}
});
至此便完成了匿名内部类的编写。对于初学者而言不要求完全掌握这种写法,只需尽量理解语法就可以了。
JDK8-Lambda表达式
Lambda表达式入门
匿名内部类存在的一个问题是,如果匿名内部类的实现非常简单,例如只包含一个抽象方法的接口,那么匿名内部类的语法仍然显得比较冗余。为此,JDK 8中新增了一个特性Lambda表达式,这种表达式只针对有一个抽象方法的接口实现,以简洁的表达式形式实现接口功能来作为方法参数。
一个Lambda表达式由三个部分组成,分别为参数列表、“->”和表达式主体,其语法格式如下:
([数据类型 参数名,数据类型 参数名,...]) -> {表达式主体}
从上述语法格式上看,Lambda表达式的书写非常简单,下面针对Lambda表达式的组成部分进行简单介绍,具体如下:
(1)([数据类型 参数名,数据类型 参数名,...]):用来向表达式主体传递接口方法需要的参数,多个参数名中间必须用英文逗号“,”进行分隔;在编写Lambda表达式时,可以省略参数的数据类型,后面的表达式主体会自动进行校对和匹配;同时,如果只有一个参数,则可以省略括号“()”。
(2)->:表示Lambda表达式箭牌,用来指定参数数据指向,不能省略,且必须用英文横线和大于号书写。
(3){表达式主体}:由单个表达式或语句块组成的主体,本质就是接口中抽象方法的具体实现,如果表达式主体只有一条语句,那么可以省略包含主体的大括号;另外,在Lambda表达式主体中允许有返回值,当只有一条return语句时,也可以省略return关键字。
学习了Lambda表达式的语法后,接下来对文件4-21进行进一步修改,来讲解Lambda表达式的基本使用,如文件1所示。
文件1 Example22.java
1 // 定义动物类接口
2 interface Animal {
3 void shout(); // 定义方法shout()
4 }
5 public class Example22 {
6 public static void main(String[] args) {
7 String name = "小花";
8 // 1、匿名内部类作为参数传递给animalShout()方法
9 animalShout(new Animal() {
10 public void shout() {
11 System.out.println("匿名内部类输出:"+name+"喵喵...");
12 }
13 });
14 // 2、使用Lambda表达式作为参数传递给animalShout()方法
15 animalShout(()-> System.out.println("Lambda表达式输出:"
16 +name+"喵喵..."));
17 }
18 // 创建一个animalShout()静态方法,接收接口类型的参数
19 public static void animalShout(Animal an) {
20 an.shout();
21 }
22 }
运行结果如图1所示。
图1 运行结果
文件1中,先定义了只有一个抽象方法的接口Animal,然后分别使用匿名内部类和Lambda表达式的方式实现了接口方法。从图1可以看出,使用匿名内部类和Lambda表达式都能实现接口中方法,但很显然使用Lambda表达式更加简洁和清晰。
函数式接口
虽然Lambda表达式可以实现匿名内部类的功能,但在使用时却有一个局限,即接口中有且只有一个抽象方法时才能使用Lamdba表达式代替匿名内部类。这是因为Lamdba表达式是基于函数式接口实现的,所谓函数式接口是指有且仅有一个抽象方法的接口,Lambda表达式就是Java中函数式编程的体现,只有确保接口中有且仅有一个抽象方法,Lambda表达式才能顺利地推导出所实现的这个接口中的方法。
在JDK 8中,专门为函数式接口引入了一个@FunctionalInterface注解,该注解只是显示的标注了接口是一个函数式接口,并强制编辑器进行更严格的检查,确保该接口是函数式接口,如果不是函数式接口,那么编译器就会报错,而对程序运行并没有实质上的影响,
接下来通过一个案例来演示函数式接口的定义与使用,如文件1所示。
文件1 Example23.java
1 // 定义无参、无返回值的函数式接口
2 @FunctionalInterface
3 interface Animal {
4 void shout();
5 }
6 // 定义有参、有返回值的函数式接口
7 interface Calculate {
8 int sum(int a, int b);
9 }
10 public class Example23 {
11 public static void main(String[] args) {
12 // 分别两个函数式接口进行测试
13 animalShout(() -> System.out.println("无参、无返回值的函数式接口调用"));
14 showSum(10, 20, (x, y) -> x + y);
15 }
16 // 创建一个动物叫的方法,并传入接口对象Animal作为参数
17 private static void animalShout(Animal animal) {
18 animal.shout();
19 }
20 // 创建一个求和的方法,并传入两个int类型以及接口Calculate类型的参数
21 private static void showSum(int x, int y, Calculate calculate) {
22 System.out.println(x + "+" + y + "的和为:" + calculate.sum(x, y));
23 }
24 }
运行结果如图1所示。
图1 运行结果
文件1中,先定义了两个函数式接口Animal和Calculate,然后在测试类中分别编写了两个静态方法,并将这两个函数式接口以参数的形式传入,最后在main()方法中分别调用这两个静态方法,并将所需要的函数式接口参数以Lambda表达式的形式传入。从图1可以看出,程序中函数式接口的定义和使用完全正确。
方法引用与构造器引用
Lambda表达式的主体只有一条语句时,程序不仅可以省略包含主体的花括号,还可以通过英文双冒号“::”的语法格式来引用方法和构造器(即构造方法),这两种形式可以进一步简化Lambda表达式的书写,其本质都是对Lambda表达式的主体部分已存在的方法进行直接引用,主要区别就是对普通方法与构造方法的引用而已。
在JDK 8中,Lambda表达式支持的引用类型主要有以下几种,如表1所示。
表1 Lambda表达式对普通方法和构造方法的引用形式
种类 | Lambda表达式示例 | 对应的引用示例 |
---|---|---|
类名引用普通方法 | (x,y,...)-> 对象名x.类普通方法名(y,...) | 类名::类普通方法名 |
类名引用静态方法 | (x,y,...) -> 类名.类静态方法名(x,y,...) | 类名::类静态方法名 |
对象名引用方法 | (x,y,...) -> 对象名.实例方法名(x,y,...) | 对象名::实例方法名 |
构造器引用 | (x,y,...) -> new 类名 (x,y,...) | 类名::new |
在了解了Lambda表达式支持的引用类型后,接下来分别通过案例对这几种引用的使用进行演示,由于这里类名引用普通方法的形式较为复杂,因此将会最后讲解。
1.类名引用静态方法
类名引用静态方法也就是通过类名对静态方法的引用,该类可以是Java自带的特殊类,也可以是自定义的普通类。接下来通过一个求绝对值的案例来演示类名(Math特殊类)引用静态方法的使用,如文件1所示。
文件1 Example24.java
1 //定义一个函数式接口
2 @FunctionalInterface
3 interface Calcable {
4 int calc(int num);
5 }
6 // 定义一个类,并在类中定义一个静态方法
7 class Math {
8 // 定义一个求绝对值方法
9 public static int abs(int num) {
10 if (num < 0) {
11 return -num;
12 } else {
13 return num;
14 }
15 }
16 }
17 // 定义测试类
18 public class Example24 {
19 private static void printAbs(int num, Calcable calcable) {
20 System.out.println(calcable.calc(num));
21 }
22 public static void main(String[] args) {
23 // 使用Lambda表达式方式
24 printAbs(-10, n -> Math.abs(n));
25 // 使用方法引用的方式
26 printAbs(-10, Math::abs);
27 }
28 }
运行结果如图1所示。
图1 运行结果
文件1中,先定义了一个函数式接口Calcable,以及一个包含静态方法的Math类,然后在测试类中编写了一个静态方法printAbs()和一个main()方法,最后在main()方法中分别使用了Lambda表达式和方法引用的方式作为静态方法printAbs()的参数进行调用。
从图1可以看出,通过Lambda表达式和类名引用静态方法的方式都可以实现程序功能,并且类名引用静态方法的实现方式显得更为简洁。
2.对象名引用方法
对象名引用方法指的是通过实例化对象的名称来对其方法进行的引用。接下来通过一个返回字符串所有字母大写的案例来演示对象名引用方法的使用,如文件2所示。
文件2 Example25.java
1 // 定义一个函数式接口
2 @FunctionalInterface
3 interface Printable{
4 void print(String str);
5 }
6 class StringUtils {
7 public void printUpperCase(String str) {
8 System.out.println(str.toUpperCase());
9 }
10 }
11 // 定义测试类
12 public class Example25 {
13 private static void printUpper(String text, Printable pt) {
14 pt.print(text);
15 }
16 public static void main(String[] args) {
17 StringUtils stu = new StringUtils();
18 // 使用Lambda表达式方式
19 printUpper("Hello", t -> stu.printUpperCase(t));
20 // 使用方法引用的方式
21 printUpper("Hello", stu::printUpperCase);
22 }
23 }
运行结果如图2所示。
图2 运行结果
文件2中,先定义了一个函数式接口Printable,以及一个包含非静态方法的StringUtils类,该类用于实现字母大写转换。然后在测试类中编写了一个静态方法printUpper()和一个main()方法,最后在main()方法中分别使用了Lambda表达式和方法引用的方式作为静态方法printUpper()的参数进行调用。
从图2可以看出,通过Lambda表达式和对象名名引用方法的方式都可以实现程序功能,并且对象名引用方法的实现方式显得更为简洁。
3.构造器引用方法
构造器引用指的是对类自带的构造器的引用。接下来通过一个构造方法获取属性的案例来演示构造器引用方法的使用,如文件3所示。
文件3 Example26.java
1 // 定义一个函数式接口
2 @FunctionalInterface
3 interface PersonBuilder {
4 Person buildPerson(String name);
5 }
6 // 定义一个Person类,并添加有参构造方法
7 class Person {
8 private String name;
9 public Person(String name) {
10 this.name = name;
11 }
12 public String getName() {
13 return name;
14 }
15 }
16 // 定义测试类
17 public class Example26 {
18 public static void printName(String name, PersonBuilder builder) {
19 System.out.println(builder.buildPerson(name).getName());
20 }
21 public static void main(String[] args) {
22 // 使用Lambda表达式方式
23 printName("赵丽颖", name -> new Person(name));
24 // 使用构造器引用的方式
25 printName("赵丽颖", Person::new);
26 }
27 }
运行结果如图3所示。
图3 运行结果
文件3中,先定义了一个函数式接口PersonBuilder以及一个包含有构造方法的类Person,然后在测试类中编写了一个静态方法printName()和一个main()方法,最后在main()方法中分别使用了Lambda表达式和构造器引用的方式作为静态方法printName()的参数进行调用。
从图3可以看出,通过Lambda表达式和构造器引用方法的方式都可以实现程序功能,并且构造器引用方法的实现方式显得更为简洁。
4.类名引用普通方法
类名引用普通方法指的是通过一个普通类的类名来对其普通方法进行的引用。接下来仍然通过一个返回字符串所有字母大写的案例来演示类名引用普通方法的使用,如文件4所示。
文件4 Example27.java
1 // 定义一个函数式接口
2 @FunctionalInterface
3 interface Printable{
4 void print(StringUtils su, String str);
5 }
6 class StringUtils {
7 public void printUpperCase(String str) {
8 System.out.println(str.toUpperCase());
9 }
10 }
11 // 定义测试类
12 public class Example27 {
13 private static void printUpper(StringUtils su, String text,
14 Printable pt) {
15 pt.print(su, text);
16 }
17 public static void main(String[] args) {
18 // 使用Lambda表达式方式
19 printUpper(new StringUtils(), "Hello",
20 (object, t) -> object.printUpperCase(t));
21 // 使用方法引用的方式
22 printUpper(new StringUtils(), "Hello",
23 StringUtils::printUpperCase);
24 }
25 }
运行结果如图4所示。
图4 运行结果
文件4中,先定义了一个函数式接口Printable以及一个包含普通方法的类StringUtils,然后在测试类中编写了一个静态方法printUpper()和一个main()方法,最后在main()方法中分别使用了Lambda表达式和方法引用的方式作为静态方法printUpper()的参数进行调用。
从图4可以看出,通过Lambda表达式和类名引用普通方法的方式都可以实现程序功能,并且类名引用普通方法的实现方式显得更为简洁。
异常
什么是异常
尽管人人都希望身体健康、事事顺利,但在实际生活中总会遇到各种状况,例如感冒发烧,工作时电脑蓝屏、死机等。同样在程序运行的过程中,也会发生这种非正常状况,例如程序运行时磁盘空间不足、网络连接中断、被加载的类不存在等。针对这些非正常情况,Java语言中引入了异常,以异常类的形式对这些非正常情况进行封装,并通过异常处理机制对程序运行时发生的各种问题进行处理。
接下来通过一个案例来认识一下什么是异常,如文件1所示。
文件1 Example28.java
1 public class Example28 {
2 // 下面的方法实现了两个整数相除
3 public static int divide(int x, int y) {
4 int result = x / y; // 定义一个变量result记录两个数相除的结果
5 return result; // 将结果返回
6 }
7 public static void main(String[] args) {
8 int result = divide(4, 0); // 调用divide()方法
9 System.out.println(result);
10 }
11 }
运行结果如图1所示。
图1 运行结果
从图1可以看出,程序发生了异常,显示“java.lang.ArithmeticException: / by zero(被0除的算术运算异常)”的错误信息。异常提示信息已经非常明确,指出程序中出现了除以0的算术异常。由于程序中的第8行代码调用divide()方法时传入了参数0,所以在文件中的第4行代码的运算中出现了被0除的问题。在这个异常发生后,程序会立即结束,无法继续向下执行。
在文件1中,产生了一个ArithmeticException异常,ArithmeticException异常只是Java异常类中的一种,在Java中还提供了大量的异常类,这些类都继承自java.lang.Throwable类。
接下来通过一张图来展示Throwable类的继承体系,如图2所示。
图2 Throwable异常体系结构图
从图2可以看出,Throwable有两个直接子类Error和Exception,其中Error代表程序中产生的错误,Exception代表程序中产生的异常。接下来就对这两个直接子类进行解释说明。
● Error类称为错误类,它表示Java运行时产生的系统内部错误或资源耗尽的错误,是比较严重的,仅靠修改程序本身是不能恢复执行的,例如系统崩溃,虚拟机错误等。
● Exception类称为异常类,它表示程序本身可以处理的错误。在Java程序开发中进行的异常处理,都是针对Excption类及其子类的。在Exception类的众多子类中有一个特殊的RuntimeException类,该类及其子类用于表示运行时异常。除了此类,Exception类下所有其他的子类都用于表示编译时异常。
通过前面的学习,读者已经了解了Throwable类及其子类,为了方便后面的学习,接下来将Throwable类中的常用方法罗列出来,如表1所示。
表1 Throwable常用方法
方法声明 | 功能描述 |
---|---|
String getMessage() | 返回此throwable的详细消息字符串 |
void printStackTrace() | 将此throwable及其追踪输出至标准错误流 |
void printStackTrace(PrintStream s) | 将此throwable及其追踪输出到指定的输出流 |
表1中,这些方法都用于获取异常信息,由于Error和Exception继承自Throwable类,所以它们都拥有这些方法,读者在后面的异常学习中会逐渐接触到这些方法的使用。
异常的类型
在实际开发中,经常会在程序编译时期产生一些异常,而这些异常必须要进行处理,这种异常被称为编译时期异常,也称为checked异常。另外还有一种异常是在程序运行时期产生的,这种异常即使不编写异常处理代码,依然可以通过编译,因此我们称之为运行时异常,也称为unchecked异常。接下来分别对这两种异常进行详细地讲解。
1.编译时异常
在Exception的子类中,除了RuntimeException类及其子类外,其他子类都是编译时异常。编译时异常的特点是在程序编写过程中,Java编译器就会对编写的代码进行检查,如果出现比较明显的异常就必须对异常进行处理,否则程序无法通过编译。
处理编译时异常有两种方式,具体如下:
● 使用try…catch语句对异常进行捕获处理;
● 使用throws关键字声明抛出异常,让调用者对其处理。
2.运行时异常
RuntimeException类及其子类都是运行时异常。运行时异常是在程序运行时由Java虚拟机自动进行捕获处理的,即使没有使用try..catch语句捕获或使用throws关键字声明抛出,程序也能编译通过,只是在运行过程中可能报错。
在Java中,常见的运行时异常有多种,如表1所示。
表1 常见运行时异常
异常类名称 | 异常类说明 |
---|---|
ArithmeticException, | 算术异常 |
IndexOutOfBoundsException | 角标越界异常 |
ClassCastException | 类型转换异常 |
NullPointerException | 空指针异常 |
NumberFormatException | 数字格式化异常 |
运行时异常一般是由于程序中的逻辑错误引起的,在程序运行时无法恢复。例如通过数组的角标访问数组的元素时,如果超过了数组的最大角标,就会发生运行时异常,代码示例如下:
int [] arr=new int[5];
System.out.println(arr[5]);
上面代码中,由于数组arr的长度为5,最大角标应为4,当使用arr[5]访问数组中的元素就会发生数组角标越界的异常。
try...catch和finally
当程序发生异常时,会立即终止,无法继续向下执行。为了保证程序能够有效的执行,Java中提供了一种对异常进行处理的方式——异常捕获。
异常捕获通常使用try…catch语句,其具体语法格式如下:
try {
// 可能发生异常的语句
} catch(Exception类或其子类 e){
// 对捕获的异常进行相应处理
}
上述代码中,try{}代码块中包含的是可能发生异常的语句,catch(){}代码块中编写针对捕获的异常进行处理的代码。当try{}代码块中的程序发生了异常,系统会将这个异常的信息封装成一个异常对象,并将这个对象传递给catch(){}代码块。catch(){}代码块需要一个形参指明它所能够接收的异常类型,这个参数的类型必须是Exception类或其子类。
接下来使用try...catch语句对文件4-28中出现的异常进行捕获和处理,如文件1所示。
文件1 Example29.java
1 public class Example29 {
2 // 下面的方法实现了两个整数相除
3 public static int divide(int x, int y) {
4 try {
5 int result = x / y; // 定义一个变量result记录两个数相除的结果
6 return result; // 将结果返回
7 } catch (Exception e) { // 对异常进行捕获处理
8 System.out.println("捕获的异常信息为:" + e.getMessage());
9 }
10 // 定义当程序发生异常直接返回-1
11 return -1;
12 }
13 public static void main(String[] args) {
14 int result = divide(4, 0); // 调用divide()方法
15 if(result == -1){ // 对调用方法返回结果进行判断
16 System.out.println("程序发生异常!");
17 }else{
18 System.out.println(result);
19 }
20 }
21 }
运行结果如图1所示。
图1 运行结果
文件1中,在定义的整数除法运算方法divide()中对可能发生异常的代码用try…catch语句进行了捕获处理。在try{}代码块中发生被0除异常,程序会转而执行catch(){}中的代码,通过调用Exception对象的getMessage()方法,即可返回异常信息“/ by zero”。catch(){}代码块对异常处理完毕后,程序仍会向下执行,而不会因为异常而终止运行。
需要注意的是,在try{}代码块中,发生异常语句后面的代码是不会被执行的,如文件4-29中第6行代码的return语句就没有执行。
在程序中,有时候会希望有些语句无论程序是否发生异常都要执行,这时就可以在try…catch语句后,加一个finally{}代码块。接下来对文件4-29进行修改,演示一下finally{}代码块的用法,如文件2所示。
文件2 Example30.java
1 public class Example30 {
2 // 下面的方法实现了两个整数相除
3 public static int divide(int x, int y) {
4 try {
5 int result = x / y; // 定义一个变量result记录两个数相除的结果
6 return result; // 将结果返回
7 } catch (Exception e) { // 对异常进行捕获处理
8 System.out.println("捕获的异常信息为:" + e.getMessage());
9 } finally {
10 System.out.println("执行finally代码块,无论程序是否异常,都会执行");
11 }
12 // 定义当程序发生异常直接返回-1
13 return -1;
14 }
15 public static void main(String[] args) {
16 int result = divide(4, 0); // 调用divide()方法
17 if(result == -1){ // 对调用方法返回结果进行判断
18 System.out.println("程序发生异常!");
19 }else{
20 System.out.println(result);
21 }
22 }
23 }
运行结果如图2所示。
图2 运行结果
文件2中,divide()方法中增加了一个finally{}代码块,用于处理无论程序是否发生异常都要执行的语句,该代码块并不受return语句和程序异常的影响。正是由于这种特殊性,在程序设计时,经常会在try...catch后使用finally{}代码块来完成必须做的事情,例如释放系统资源、关闭线程池等。
需要注意的是,finally中的代码在一种情况下是不会执行的,那就是在try...catch中执行了System.exit(0)语句。System.exit(0)表示退出当前的Java虚拟机,Java虚拟机停止了,任何代码都不能再执行了。
throws关键字
在前面章节案例中,定义除法运算时,开发者通常会意识到可能出现的异常,可以直接通过try...catch对异常进行捕获处理,但有些时候,方法中代码是否会出现异常,开发者并不明确或者并不急于处理,为此,Java允许将这种异常从当前方法中抛出,然后让后续的调用者在使用时再进行异常处理。
在Java中,将异常抛出需要使用throws关键字来实现,该关键字用在会抛出异常的方法名称后,同时支持一次性抛出多种类型的异常,基本语法格式如下:
[修饰符] 返回值类型 方法名([参数类型 参数名1...]) throws 异常类1,异常类2,... {
// 方法体...
}
从上述语法格式中可以看出,throws关键字需要写在方法声明的后面,throws后面需要声明方法中发生异常的类型,通常将这种做法称为方法声明抛出一个异常。接下来对上一节案例进行修改,在devide()方法上声明抛出异常,如文件1所示。
文件1 Example31.java
1 public class Example31 {
2 // 下面的方法实现了两个整数相除,并使用throws关键字声明抛出异常
3 public static int divide(int x, int y) throws Exception {
4 int result = x / y; // 定义变量result记录两个数相除的结果
5 return result; // 将结果返回
6 }
7 public static void main(String[] args) {
8 int result = divide(4, 0); // 调用divide()方法
9 System.out.println(result);
10 }
11 }
编译程序报错,结果如图1所示。
图1 运行结果
文件1中,在调用divide()方法时,由于该方法时声明抛出了异常,所以调用者在调用divide()方法时就必须进行处理,否则就会发生编译错误。从图1可以看出,Eclipse在程序编译时发生了“Unhandled exception type Exception(未处理的异常类型)”的问题,并且给出了两种快速解决的方案。其中“Add throws declaration”表示在方法上继续使用throws关键字抛出异常,而“Surround with try/catch”表示在出现异常的代码处使用try…catch代码块进行捕获处理。
下面对文件1进行修改,在调用divide()方法时对其进行try…catch捕获处理,如文件2所示。
文件2 Example32.java
1 public class Example32 {
2 // 下面的方法实现了两个整数相除,并使用throws关键字声明抛出异常
3 public static int divide(int x, int y) throws Exception {
4 int result = x / y; //定义一个变量result记录两个数相除的结果
5 return result; //将结果返回
6 }
7 public static void main(String[] args) {
8 try {
9 int result = divide(4, 0); //调用divide()方法
10 System.out.println(result);
11 } catch (Exception e) { //对捕获到的异常进行处理
12 System.out.println("捕获的异常信息为:" + e.getMessage());
13 }
14 }
15 }
运行结果如图2所示。
图2 运行结果
文件2中,在调用divide()方法时,并没有对异常进行处理而是继续使用throws关键字将异常抛出。从运行结果可以看出,程序虽然可以通过编译,但在运行时由于没有对“/by zero”的异常进行处理,最终导致程序终止运行。
throw关键字
除了可以通过throws关键字抛出异常外,还可以使用throw关键字抛出异常。与throws有所不同的是,throw用于方法体内,并且抛出的是一个异常类对象,而throws关键字用在方法声明中,用来指明方法可能抛出的多个异常。
通过throw关键字抛出异常后,还需要使用throws关键字或try…catch对异常进行处理。需要注意的是,如果throw抛出的是Error、RuntimeException或它们的子类异常对象,则无需使用throws关键字或try…catch对异常进行处理。
使用throw关键字抛出异常的语法格式如下:
[修饰符] 返回值类型 方法名([参数类型 参数名,...]) throw 抛出的异常类 {
// 方法体...
throw new Exception类或其子类构造方法;
}
接下来通过一个案例来演示throw关键字的使用,如文件1所示。
文件1 Example34.java
1 public class Example34 {
2 // 定义printAge()输出年龄
3 public static void printAge(int age) throws Exception {
4 if(age <= 0){
5 // 对业务逻辑进行判断,当输入年龄为负数时抛出异常
6 throw new Exception("输入的年龄有误,必须是正整数!");
7 }else {
8 System.out.println("此人年龄为:"+age);
9 }
10 }
11 public static void main(String[] args) {
12 // 下面的代码定义了一个try…catch语句用于捕获异常
13 int age = -1;
14 try {
15 printAge(age);
16 } catch (Exception e) { // 对捕获到的异常进行处理
17 System.out.println("捕获的异常信息为:" + e.getMessage());
18 }
19 }
20 }
运行结果如图1所示。
图1 运行结果
文件1中,printAge()方法对输入的年龄进行了逻辑判断,虽然输入负数在语法上能够通过编译,并且程序能够正常运行,但这显然与现实情况不符,因此需要在方法中对输入的内容进行判断,当数值小于0时,使用throw关键字抛出异常,并指定异常提示信息,同时在方法后继续用throws关键字处理抛出的异常。
从图1可以看出,对于代码中的业务逻辑异常,使用throw关键字抛出异常后,同样可以正确捕获异常,从而保证程序的正常运行。当然,throw关键字除了可以抛出代码的逻辑性异常外,也可以抛出Java能够自动识别的异常。
自定义异常
Java中定义了大量的异常类,虽然这些异常类可以描述编程时出现的大部分异常情况,但是在程序开发中有时可能需要描述程序中特有的异常情况,例如在设计divide()方法时不允许被除数为负数。为了解决这样的问题,Java允许用户自定义异常,但自定义的异常类必须继承自Exception或其子类。
接下来通过一个案例来学习自定义异常的创建,如文件1所示。
文件1 DivideByMinusException.java
1 // 下面的代码是自定义一个异常类继承自Exception
2 public class DivideByMinusException extends Exception{
3 public DivideByMinusException (){
4 super(); // 调用Exception无参的构造方法
5 }
6 public DivideByMinusException (String message){
7 super(message); // 调用Exception有参的构造方法
8 }
9 }
在实际开发中,如果没有特殊的要求,自定义的异常类只需继承Exception类,在构造方法中使用super()语句调用Exception的构造方法即可。
既然自定义了异常,那么该如何使用呢?这时就需要用到前面小节讲解的throw关键字,在程序指定位置通过throw关键字抛出自定义的异常对象,然后对抛出的异常进行异常处理。
接下来重新对divide()方法进行改写,在divide()方法中判断被除数是否为负数,如果为负数,就是用throw关键字向调用者抛出自定义的DivideByMinusException异常对象,如文件2所示。
文件2 Example36.java
1 public class Example36 {
2 // 下面的方法实现了两个整数相除,
3 public static int divide(int x,int y) throws DivideByMinusException {
4 if (y == 0) {
5 // 使用throw关键字声明异常对象
6 throw new DivideByMinusException("除数是0");
7 }
8 int result = x / y; // 定义一个变量result记录两个数相除的结果
9 return result; // 将结果返回
10 }
11 public static void main(String[] args) {
12 try {
13 int result = divide(4, 0);
14 System.out.println(result);
15 } catch (DivideByMinusException e) {
16 System.out.println("捕获的异常信息为:" + e.getMessage());
17 }
18 }
19 }
运行结果如图1所示。
图1 运行结果
在文件2中,divide()方法通过逻辑判断对除法运算的除数是否为0进行了判断,如果除数为0就使用throw关键字抛出自定义的DivideByMinusException异常对象,然后通过throws关键字抛出异常,并在最后通过try..catch语句捕获异常。从图2可以看出,程序执行后判断出除数为0,抛出了指定的异常信息。
垃圾回收
在Java中,当一个对象成为垃圾后仍会占用内存空间,时间一长,就会导致内存空间的不足。针对这种情况,Java中引入了垃圾回收机制(Java GC)。有了这种机制,程序员不需要过多关心垃圾对象回收的问题,Java虚拟机会自动回收垃圾对象所占用的内存空间。
当一个对象在堆内存中运行时,根据它被引用变量所引用的状态,可以把它所处的状态分成如下三种:
● 可用状态:当一个对象被创建后,如果有一个以上的引用变量引用它,那么这个对象在程序中将处于可用状态,程序可以通过引用变量来调用该对象的实例变量和方法。
● 可恢复状态:如果程序中某个对象不再有任何引用变量引用它,它就进入了可恢复状态。在这种状态下,系统的垃圾回收机制准备回收该对象所占用的内存,在回收该对象之前,系统会调用所有可恢复状态对象的finalize()方法进行资源清理。如果系统在调用finalize()方法前重新使一个引用变量引用该对象,则这个对象会再次变为可用状态;否则该对象将进入不可用状态。
● 不可用状态:当对象失去了所有引用变量的关联,且系统已经调用所有对象的finalize()方法后依然没有使该对象变成可用状态,那么这个对象将永久的失去引用,变成不可用状态。只有当一个对象处于不可用状态时,系统才会真正的回收该对象所占用的内存空间。
上述三种状态的转换示意图如1所示。
图1 对象的状态转换
一个对象在彻底失去引用成为垃圾后会暂时地保留在内存中,当这样的垃圾堆积到一定程度时,Java虚拟机就会启动垃圾回收器将这些垃圾对象从内存中释放,从而使程序获得更多可用的内存空间。虽然通过程序可以控制一个对象何时不再被任何引用变量所引用,但是却无法精确的控制Java垃圾回收的时机。除了等待Java虚拟机进行自动垃圾回收外,还可以通过如下两种方式强制系统进行垃圾回收。
● 调用System类的gc()静态方法:System.gc()。
● 调用Runtime对象的gc()实例方法:Runtime.getRuntime().gc()。
实际上,调用System.gc()方法时,所执行的也是Runtime.getRuntime().gc()方法。需要说明的是,调用这两种方式可以强制启动垃圾回收器进行垃圾回收,但系统是否立即进行垃圾回收依然具有不确定性。大多数情况下,强制系统垃圾回收后总是有一定的效果。
当一个对象在内存中被释放时,它的finalize()方法会被自动调用,finalize()方法是定义在Object类中的实例方法,其方法原型如下:
protected void finalize() throws Throwable { }
任何Java类都可以重写Object类的finalize()方法,在该方法中清理该对象占用的资源。如果程序终止之前仍然没有进行垃圾回收,则不会调用失去引用对象的finalize()方法来清理资源。
需要注意的是,只有当程序认为需要更多的额外内存时,垃圾回收器才会自动进行垃圾回收,而在一些情况下,对象的finalize()方法并不一定会被调用,例如某个失去了引用的对象只占用了少量的内存,而系统并没有严重的内存需求,此时垃圾回收机制就可能不会回收该对象所占用的资源,所以该对象的finalize()方法也就不会被调用。
接下来通过一个案例来演示Java虚拟机进行垃圾回收的过程,如文件1所示。
文件1 Example37.java
1 class Person {
2 // 下面定义的finalize()方法会在垃圾回收前被调用
3 public void finalize() {
4 System.out.println("对象将被作为垃圾回收...");
5 }
6 }
7 public class Example37 {
8 // 1、演示一个不通知强制垃圾回收的方法
9 public static void recyclegWaste1(){
10 Person p1 = new Person();
11 p1 = null;
12 int i = 1;
13 while (i < 10) {
14 System.out.println("方法1循环中...........");
15 i++;
16 }
17 }
18 // 2、演示一个通知强制垃圾回收的方法
19 public static void recyclegWaste2(){
20 Person p2 = new Person();
21 p2 = null;
22 // 通知垃圾回收器进行强制垃圾回收
23 System.gc();
24 // Runtime.getRuntime().gc();
25 int i = 1;
26 while (i < 10) {
27 System.out.println("方法2循环中...........");
28 i++;
29 }
30 }
31 public static void main(String[] args) {
32 // 分别调用两个模拟演示垃圾回收的方法
33 recyclegWaste1();
34 System.out.println("================");
35 recyclegWaste2();
36 }
37 }
运行结果如图2所示。
图2 运行结果
文件1中,Person类重写了finalize()方法,并在方法中编写了一条输出语句来查看垃圾回收时的执行效果。在测试类中先分别创建了两个演示垃圾回收的方法recyclegWaste1()和recyclegWaste2(),在recyclegWaste1()方法中将一个新创建的对象引用去除后并没有强制调用垃圾回收器,而在recyclegWaste2()方法中将一个新创建的对象引用去除后就立即强制调用垃圾回收器进行垃圾回收,并在两个方法中都添加了一个while循环来模拟程序中其他执行过程。最后,在main()方法中按照顺序先后调用了recyclegWaste1()和recyclegWaste2()方法。
从图2可以看出,在recyclegWaste1()方法中,一个对象失去了引用不会立即作为垃圾被回收,即使整个方法结束也一样;而在recyclegWaste2()方法中,一个对象失去了引用后,就会立即启动垃圾回收器进行垃圾回收,但这个过程也不是瞬间进行回收垃圾的,而是在程序继续执行过程中陆续将所有垃圾进行回收,包括当前recyclegWaste2()方法以及recyclegWaste1()方法产生的垃圾。
如果在文件1中的main()方法中,将调用的两个演示垃圾回收的方法recyclegWaste1()和recyclegWaste2()方法执行顺序互换,再次运行程序,结果如图3所示。
图3 运行结果
从图3可以看出,整个执行过程中只有一条“对象将被作为垃圾回收”的输出信息,说明该过程中只立即执行了一次强制垃圾回收,也就是强制回收了recyclegWaste2()方法中失去引用的垃圾对象p2。其原因是,在main()方法中先调用了recyclegWaste2()方法,在该方法中通知垃圾回收器进行强制垃圾回收时,只发现了垃圾对象p2,所以会在某个时间进行垃圾回收,而在执行了recyclegWaste1()方法时,又产生了一个垃圾对象p1,此时却没有再次通知垃圾回收器,所以不会立即进行垃圾回收。
未完,持续更新中。。。
自学java教程视频+资料+笔记,企鹅49.89.138.68