Java课堂笔记(三):抽象类和接口
在面向对象一文中,我们说了多态的一个功能是将“做什么”和“怎么做”分离开来,所采用的方法是将不同的具体实现放在不同的子类中,然后向接口中传入一个父类对象的引用。而本篇博客要说的内容则为接口(此处"接口"的理解是“可以供外部调用的方法”,与本章所述的“接口”区别)和实现的分离提供了一种更加结构化的方法。抽象类和接口赋予了Java对象抽象能力,为面向对象设计提供了很大的支持。
在《Java编程思想》中,作者在介绍两者的使用方法和特点以外,还结合大量的Demo代码来阐述了它们在程序结构设计方面的巧妙用法。事实上,在很多的设计模式中也经常可以看到这两种机制的身影(尤其是接口)。本篇博客着重于梳理总结两者的基本特点,并对比它们之间的异同。
抽象类
我们已经说过,在Java中一切都是对象,同时对象又是通过类来进行描述的。比如说Cat类中包含color,size等字段,以及eat()、cry()等方法,那么对任意一个Cat对象,我们就可以通过这些内容来描述它。但是并非所有的类都是用来描述对象的,比如说Animal类,我们一定要针对它的一个导出类才能具体只要某一个动物具体时什么样子的,能够如何来描述它。那么在Animal类中,就只需要定义导出类都具有的方法即可,至于这些方法的具体实现,需要他们的子类根据自己的需要来实现。
在Java中将仅有声明而没有具体方法体的方法叫做抽象方法,包含抽象方法的类就叫做抽象类。抽象类中可以包含一个或多个抽象方法,也可以包含普通类所具备的成员变量和普通方法,但只要包含一个抽象方法,那么这个类就是抽象类,且必须使用abstract修饰。语法如下:
1 abstract class Animal { 2 private int color; 3 4 public abstract void cry(); 5 6 public void die(){ 7 System.out.println("animal has dead"); 8 } 9 }
抽象类中定义了接口(做什么),而接口的具体实现(怎么做)就需要通过继承由它的子类来完成。因此抽象类必须被继承,否则就没有意义。在使用抽象类的时候需要注意以下几点:
- 抽象类不能被实例化,实例化的工作应该交给子类来完成
- 子类可以实现抽象类的抽象方法,也可以不实现。如果子类不实现父类的抽象方法,则子类也必须是抽象类
- abstract不能与final共同修饰一个类。抽象类的意义在于被继承,而被final修饰的类不能被继承,因此两者不能共同修饰同一个类。
- 若一个对象为抽象方法,则不可以被private、final或static修饰。private方法不能被子类继承,因此无法让子类来实现这个方法;final和static修饰的方法可以被子类继承,但是不能被子类重写,因此也无法被实现。
创建抽象类和抽象方法非常有用,因为它们可以使类的抽象性明确起来,并告诉用户和编译器打算怎样来使用它们。抽象类还是很有用的重构工具,因为它们使得我们可以很容易地将公共方法沿着继承层次向上移动。
下面给出一个来自《Java编程思想》的抽象类示例:
1 //: polymorphism/music/Note.java 2 // Notes to play on musical instruments. 3 package polymorphism.music; 4 5 public enum Note { 6 MIDDLE_C, C_SHARP, B_FLAT; // Etc. 7 }
1 //: interfaces/music4/Music4.java 2 // Abstract classes and methods. 3 package interfaces.music4; 4 import polymorphism.music.Note; 5 import static net.mindview.util.Print.*; 6 7 abstract class Instrument { 8 private int i; // Storage allocated for each 9 public abstract void play(Note n); 10 public String what() { return "Instrument"; } 11 public abstract void adjust(); 12 } 13 14 class Wind extends Instrument { 15 public void play(Note n) { 16 print("Wind.play() " + n); 17 } 18 public String what() { return "Wind"; } 19 public void adjust() {} 20 } 21 22 class Percussion extends Instrument { 23 public void play(Note n) { 24 print("Percussion.play() " + n); 25 } 26 public String what() { return "Percussion"; } 27 public void adjust() {} 28 } 29 30 class Stringed extends Instrument { 31 public void play(Note n) { 32 print("Stringed.play() " + n); 33 } 34 public String what() { return "Stringed"; } 35 public void adjust() {} 36 } 37 38 class Brass extends Wind { 39 public void play(Note n) { 40 print("Brass.play() " + n); 41 } 42 public void adjust() { print("Brass.adjust()"); } 43 } 44 45 class Woodwind extends Wind { 46 public void play(Note n) { 47 print("Woodwind.play() " + n); 48 } 49 public String what() { return "Woodwind"; } 50 } 51 52 public class Music4 { 53 // Doesn't care about type, so new types 54 // added to the system still work right: 55 static void tune(Instrument i) { 56 // ... 57 i.play(Note.MIDDLE_C); 58 } 59 static void tuneAll(Instrument[] e) { 60 for(Instrument i : e) 61 tune(i); 62 } 63 public static void main(String[] args) { 64 // Upcasting during addition to the array: 65 Instrument[] orchestra = { 66 new Wind(), 67 new Percussion(), 68 new Stringed(), 69 new Brass(), 70 new Woodwind() 71 }; 72 tuneAll(orchestra); 73 } 74 } /* Output: 75 Wind.play() MIDDLE_C 76 Percussion.play() MIDDLE_C 77 Stringed.play() MIDDLE_C 78 Brass.play() MIDDLE_C 79 Woodwind.play() MIDDLE_C 80 *///:~
接口
接口相比于抽象类在抽象程度上更进了一步,它要求产生一个完全抽象的类。在这个类中的所有的方法都是抽象方法,都未提供其具体的实现。
Java中interface来产生一个接口,使用方法如下所示:
1 interface InterfaceDemo{ 2 3 int VALUE = 5;//static & final 4 5 void methodA();//automatically public 6 }
在使用接口的时候,有以下几点需要注意:
- 接口内的方法默认为public,且必须为public。如果人为地将其声明为非public方法,则编译器会报错;
- 接口中可以包含域(成员变量),但这些域都被隐式地被声明为static final的;
- Java中用implements关键字来表示实现一个接口。与抽象类不同,接口的实现类必须实现接口中所有的抽象方法;
- 我们不能用new关键字来为接口创建一个实例对象,但是却可以声明一个接口的引用,并将其指向任意一个实现了该接口的类对象。它提供了一种将多个实现同一接口的类向上转型为同一基类(其实是接口)的方法,是Java中多态的一种体现形式。最常见的就是我们在定义某一种集合类的时候:List其实是一个接口,ArrayList和LinkedList都是实现了该接口的类。list引用可以任意指向这两种类的实例对象,listFunc()方法要求一个List类型的对象,则ArrayList和LinkedList的对象也可以传入其中。
List<String> list = new ArrayList<>(); list = new LinkedList<>(); public void listFunc(List<String> list) {...}
用接口实现多重继承
我们说过在Java中一个类只能有唯一的一个父类,即extends只支持单继承,而接口则为我们提供了一种实现多重继承的方式,具体体现为以下两种方式:1、implements关键字后可以跟多个接口名,用逗号分隔,则该类同时实现多个接口;2、一个类在继承一个父类以后,依然可以继续实现一个或多个接口。在这种情况下,必须将extends关键字写在implements关键字以前。示例如下:
1 interface CanFight { 2 void fight(); 3 } 4 5 interface CanSwim { 6 void swim(); 7 } 8 9 interface CanFly { 10 void fly(); 11 } 12 13 class ActionCharacter { 14 public void fight() {} 15 } 16 17 class Hero extends ActionCharacter 18 implements CanFight, CanSwim, CanFly { 19 public void swim() {} 20 public void fly() {} 21 }
在以上这段代码中,CanFight和ActionCharacter都包含void fight()方法。在这段代码中并不会出现问题,因为这两个方法时完全相同的。但如果他们的签名或返回类型不同,则会产生编译错误,示例如下:
为了避免这种错误出现,在打算组合多个接口时,应尽量避免接口中包含相同的方法名。
用继承来扩展接口
通过继承,可以在接口中添加新的方法,或将数个已有接口组合为一个接口。示例如下:
1 interface Monster { 2 void menace(); 3 } 4 5 interface DangerousMonster extends Monster { 6 void destroy(); 7 } 8 9 interface Lethal { 10 void kill(); 11 } 12 13 class DragonZilla implements DangerousMonster { 14 public void menace() {} 15 public void destroy() {} 16 } 17 18 interface Vampire extends DangerousMonster, Lethal { 19 void drinkBlood(); 20 } 21 22 class VeryBadVampire implements Vampire { 23 public void menace() {} 24 public void destroy() {} 25 public void kill() {} 26 public void drinkBlood() {} 27 }
DangerousMonster是Monster的直接扩展,它产生了一个新接口。DrangonZilla中实现了这个接口。而Vampire接口通过继承将多个接口组合起来,extends的这种使用方法仅适用于接口的继承。
抽象类与接口的区别
我们来总结一下抽象类和接口差别:
- 抽象类中可以包含非抽象的方法,接口中的所有方法都必须为抽象方法;
- 抽象类中可以包含任意范围的域,但接口中只能有用static final域(静态常量数据);
- 继承于抽象类的子类可以不实现其中的抽象方法,而实现接口的类必须实现其中所有的抽象方法。
除此以外,抽象类与接口在设计上也存在着差别。抽象类表现的是纵向的继承,包含“is-a”的关系;接口表现的是横向的一种扩展,包含“is-like-a”的关系。关于设计层次的不同,这篇博客中提出了三点不同,可以借鉴:
- 抽象层次不同。抽象类是对类抽象,而接口是对行为的抽象。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。
- 跨域不同。抽象类所跨域的是具有相似特点的类,而接口却可以跨域不同的类。我们知道抽象类是从子类中发现公共部分,然后泛化成抽象类,子类继承该父类即可,但是接口不同。实现它的子类可以不存在任何关系,共同之处。例如猫、狗可以抽象成一个动物类抽象类,具备叫的方法。鸟、飞机可以实现飞Fly接口,具备飞的行为,这里我们总不能将鸟、飞机共用一个父类吧!所以说抽象类所体现的是一种继承关系,要想使得继承关系合理,父类和派生类之间必须存在"is-a" 关系,即父类和派生类在概念本质上应该是相同的。对于接口则不然,并不要求接口的实现者和接口定义在概念本质上是一致的, 仅仅是实现了接口定义的契约而已。
- 设计层次不同。对于抽象类而言,它是自下而上来设计的,我们要先知道子类才能抽象出父类,而接口则不同,它根本就不需要知道子类的存在,只需要定义一个规则即可,至于什么子类、什么时候怎么实现它一概不知。比如我们只有一个猫类在这里,如果你这是就抽象成一个动物类,是不是设计有点儿过度?我们起码要有两个动物类,猫、狗在这里,我们在抽象他们的共同点形成动物抽象类吧!所以说抽象类往往都是通过重构而来的!但是接口就不同,比如说飞,我们根本就不知道会有什么东西来实现这个飞接口,怎么实现也不得而知,我们要做的就是事前定义好飞的行为接口。所以说抽象类是自底向上抽象而来的,接口是自顶向下设计出来的。
参考文献
[1]. Java编程思想 第四版
[2]. https://blog.csdn.net/u012340794/article/details/60882727