目录
抽象类和抽象方法
接口定义
默认方法
多重继承
接口中的静态方法
作为接口的Instrument
本笔记参考自: 《On Java 中文版》
接口和抽象类提供了一种更加结构化的方式分离接口和实现。
抽象类和抽象方法
抽象类,其介于普通类和接口之间。在构建具有字段而未实现方法的类时,抽象类是重要且必要的工具。以Instrument类为例:
package music;public class Instrument {public void play(Note n) {}
}
由这个类可以衍生出很多子类:Wind、Brass等。在这里,Instrument存在的目的就是为了为它的子类创建一个公共接口。换言之,Instrument建立了一种基本形式,用于抽象出所有子类的共同之处。在这里,Instrument可以被称为抽象基类,简称抽象类。
创建抽象类的目的,就是通过一个公共接口来操作一组的类。因此,Instrument只需要提供一个接口就可以了。
Java提供了一种抽象方法的机制,这是一个不完整的方法:只有声明,而没有方法体(类似于C++的纯虚函数)。其声明语法如下:
abstract void f();
包含抽象方法的类就是抽象类。若一个类包含有一个或以上的抽象方法,则该类就必须被定义为抽象类,否则就会报错。
abstract class Basic {abstract void unimplemented();
}
因为抽象类是不完整的,为了防止其被误用,当试图创建一个抽象类的对象时,就会收到报错:
若想要继承一个抽象类,那么这个新类就必须为基类中的所有抽象方法提高方法定义。否则,认为子类也是抽象的,此时编译器会强制要求使用abstract关键字来限定这个子类:
abstract class Basic2 extends Basic {int f() {return 111;}abstract void g();// 仍然没有实现unimplemented()方法
}
一个抽象类可以不包含任何的抽象方法。这种用法主要用于阻止对于该方法的任何实例化:
abstract class Basic3 {int f() {return 111;}// 可以不需要抽象方法
}public class AbstractWithoutAbstracts {// 实例化依旧会报错:Basic3是抽象类// Basic3 b3 = new Basic3();
}
使用一个继承了抽象类的子类,若想要实例化这个子类,就需要为抽象类的所有抽象方法提供定义:
abstract class Uninstantiable {abstract void f();abstract void g();
}public class Instantiable extends Uninstantiable {@Overridevoid f() {System.out.println("f()");}@Overridevoid g() {System.out.println("g()");}public static void main(String[] args) {Uninstantiable ui = new Instantiable();}
}
在上述程序中,即使@Override不标注,只要没有使用相同的方法名称或是方法签名,抽象机制仍然可以知道程序员有没有实现抽象方法。在这里,@Override主要用于提示,表示该方法已经被重写了。
抽象类对于访问权限没有过多限制,其的默认访问权限就是包访问权限。但是,抽象方法是不允许private的:
abstract class AbstractAccess {private void m1() {}// private abstract void m1a();protected void m2() {}protected abstract void m2a();void m3() {}abstract void m3a();public void m4() {}public abstract void m4a();
}
不允许private abstract是因为,这种抽象方法无法在子类中得到一个合法的定义。
抽象类不会要求其所有的方法都是抽象的,将需要使用的公共接口声明为abstract即可。依据这些已知的知识,以乐器(Instrument)为例:
package music4;import music.Note;abstract class Instrument {private int i; // 这一变量在每个对象中都会被分配储存public abstract void play(Note n);public String what() {return "Instrument";}public abstract void adjust();
}class Wind extends Instrument {@Overridepublic void play(Note n) {System.out.println("Wind.play(): " + n);}@Overridepublic String what() {return "Wind";}@Overridepublic void adjust() {System.out.println("对Wind进行调整");}
}class Percussion extends Instrument {@Overridepublic void play(Note n) {System.out.println("Percussion.play(): " + n);}@Overridepublic String what() {return "Percussion";}@Overridepublic void adjust() {System.out.println("对Percussion进行调整");}
}class Stringed extends Instrument {@Overridepublic void play(Note n) {System.out.println("Stringed.play(): " + n);}@Overridepublic String what() {return "Stringed";}@Overridepublic void adjust() {System.out.println("对Stringed进行调整");}
}class Brass extends Wind {@Overridepublic void play(Note n) {System.out.println("Brass.play(): " + n);}@Overridepublic void adjust() {System.out.println("对Brass进行调整");}
}class Woodwind extends Wind {@Overridepublic void play(Note n) {System.out.println("Woodwind.play(): " + n);}@Overridepublic String what() {return "Woodwind";}
}public class Music4 {static void tune(Instrument i) { // 新的类型也可以使用tune()方法// ...i.play(Note.MIDDLE_C);}static void tuneAll(Instrument[] e) {for (Instrument i : e)tune(i);}public static void main(String[] args) {Instrument[] orchestra = {new Wind(),new Percussion(),new Stringed(),new Brass(),new Woodwind()};tuneAll(orchestra);}
}
程序执行的结果如下:
抽象类和抽象方法明确了类的抽象性,并且告诉用户和编译器自己的预期用途,这种工具也常被用在重构之中。
接口定义
接口通过interface进行定义。在Java 8之前,它只被运行使用抽象方法:
public interface PureInterface {int m1();void m2();double m3();
}
在接口中定义抽象方法不需要使用abstract关键字,因为在接口中不会存在方法体。
总结一下:在Java 8之前,interface可以创建一个完全抽象的类,不代表任何实现。接口仅负责描述,它确定方法名、方法体和返回类型,但不提供方法体。接口只提供一种形式,而使用了接口的代码会知道可以为接口调用哪些方法。
Java 8开始,接口中允许默认方法和静态方法。此时接口的基本概念依旧成立,即接口是一个类型的概念,而非实现。
接口与抽象类的区别:
- 接口通常暗示“类的类型”或作为形容词使用。
- 抽象类通常是类层次结构的一部分。
接口的方法默认是public的,而不是包访问权限。
接口可以包含字段,这些字段是隐式的static和final。
使用implement关键字,可以创建一个符合特定接口(或一组接口)的类。例如:
interface Concept { // 类前没有使用public修饰,是包访问权限void idea1();void idea2();
}class ImplementingAnInterface implements Concept {@Overridepublic void idea1() {System.out.println("idea1");}@Overridepublic void idea2() {System.out.println("idea2");}
}
注意:当实现一个接口时,来自接口的方法必须被定义为public。因为Java编译器不会允许将接口方法的访问权限设为包访问权限,这会降低继承期间方法的可访问性。
默认方法
Java 8之后,default关键字有了一个额外的用途:在接口中,default会允许方法创建一个方法体。实现了该接口的类,可以不定义default修饰的方法而直接使用方法。
interface InterfaceWithDefault {void firstMethod();void secondMethod();default void defaultMethod() {System.out.println("这是一个由default修饰的方法");}
}
只要实现上述接口的firstMethod方法和secondMethod方法,就可以使用这个接口了:
// 和InterfaceWithDefault.java处于同一文件夹中public class Implementation implements InterfaceWithDefault {@Overridepublic void firstMethod() {System.out.println("方法一");}@Overridepublic void secondMethod() {System.out.println("方法二");}public static void main(String[] args) {InterfaceWithDefault i = new Implementation();i.firstMethod();i.secondMethod();i.defaultMethod();}
}
程序执行的结果是:
之所以添加默认方法,原因之一是:这允许向现有接口中添加方法,而不会破坏已经在使用该接口的所有方法(默认方法也称防御方法或虚拟扩展方法)。
JDK 9中,接口的default和static方法都可以是private的。
多重继承
多重继承,即一个类可以从多个基类型继承特性和功能。但Java在严格意义上是一种单继承语言:Java只允许继承一个类(或抽象类)。在默认方法出现之后,Java才拥有了一些多重继承的特性。
现在,我们可以通过把接口和默认方法结合起来,来结合多个基类型的行为。
但Java只允许结合“行为”,换句话说,接口中不允许存在字段(除非是静态字段)。字段依旧只能来自单个基类或抽象类,所以我们无法获得状态的多重继承。例如:
interface One {default void first() {System.out.println("方法One.first()");}
}interface Two {default void second() {System.out.println("方法Two.second()");}
}interface Three {default void third() {System.out.println("方法Three.third()");}
}class MI implements One, Two, Three { // 结合了多个接口
}public class MultipleInheritance {public static void main(String[] args) {MI mi = new MI();mi.first();mi.second();mi.third();}
}
程序执行的结果如下:
只要所有基类方法都有不同名称和参数列表,就可以组合多个来源。否则,编译器就会报错:
interface Bob1 {default void bob() {System.out.println("Bob1::bob");}
}interface Bob2 {default void bob() {System.out.println("Bob2::bob");}
}// class Bob implements Bob1, Bob2 { // 不可以,会发生报错
// }interface Sam1 {default void sam() {System.out.println("Sam1::sma");}
}interface Sam2 {default void sam(int i) {System.out.println("Sam2::sma = " + i * 2);}
}class Sam implements Sam1, Sam2 { // 可以,因为方法的参数列表不同
}interface Max1 {default void max() {System.out.println("Max1::max");}
}interface Max2 {default int max() {System.out.println("Max2::max");return 1;}
}// class Max implements Max1, Max2 { // 不可以,参数列表不足以区分方法
// }
编译器会通过方法签名来区分不同的方法,因为方法签名具有唯一性:签名包括名称和参数类型。但是,返回类型不是方法签名的一部分。
如果发生如上注释中的冲突,就需要通过重写冲突的方法来解决问题:
interface Coco1 {default void coco() {System.out.println("Coco1::coco");}
}interface Coco2 {default void coco() {System.out.println("Coco2::coco");}
}public class Coco implements Coco1, Coco2 {@Overridepublic void coco() { // 重写存在冲突的方法Coco2.super.coco();}public static void main(String[] args) {new Coco().coco();}
}
上述程序最终会输出:Coco2::coco 。在Coco类中进行重写方法时,通过super关键字选择了基类Coco2进行实现。除此之外,也可以通过其他任何可行的方式进行实现。
接口中的静态方法
Java 8还允许接口包含静态方法。这种设计使得我们可以将逻辑上属于接口的方法赋予接口本身。通常,会将用来操作接口的方法,以及通用工具放入接口中:
public interface Operation {void execute();static void runOps(Operation... ops) { // 用来操作接口for (Operation op : ops)op.execute();}static void show(String msg) { // 通用方法System.out.println(msg);}
}
其中,runOps()是一个模板方法设计模式的例子。
借由runOps()这个方法,下面展示的是创建Operation的不同方法:
import operation.Operation;class Heat implements Operation {@Overridepublic void execute() {Operation.show("Heat");}
}public class MetaWork {public static void main(String[] args) {Operation twist = new Operation() {public void execute() { // 在使用前,必须在静态上下文中对方法进行定义Operation.show("Twist");}};Operation.runOps(new Heat(), // 【1】:按常规方式创建new Operation() { // 【2】:匿名类public void execute() {Operation.show("Hammer");}},twist::execute, // 使用方法引用() -> Operation.show("Anneal")); // Lambda表达式,需要最少的代码}
}
总结上述程序,可以得出各种创建Operation的不同方式:
- 常规类Heat
- 匿名类
- 方法引用
- Lambda表达式,需要最少的代码
作为接口的Instrument
使用接口更新关于乐器的Instrument:
接口一经实现,这个实现就会变成一个可以用常规方式扩展的普通类。接口中,任何方法的默认权限都是public的。Instrument中的play()和adjust()都使用default关键字定义。
package music5;import music.Note;interface Instrument {int VALUR = 5; // 默认是static并且final的,即编译时常量default void play(Note n) { // 默认权限是public的System.out.println(this + ".play()" + n);}default void adjust() {System.out.println("调整:" + this);}
}class Wind implements Instrument {@Overridepublic String toString() {return "Wind";}
}class Percussion implements Instrument {@Overridepublic String toString() {return "Percussion";}
}class Stringed implements Instrument {@Overridepublic String toString() {return "Stringed";}
}class Brass extends Wind {@Overridepublic String toString() {return "Brass";}
}class Woodwind extends Wind {@Overridepublic String toString() {return "Woodwind";}
}public class Music5 {static void tune(Instrument i) {// ...i.play(Note.MIDDLE_C);}static void tuneAll(Instrument[] e) {for (Instrument i : e)tune(i);}public static void main(String[] args) {Instrument[] orchestra = {new Wind(),new Percussion(),new Stringed(),new Brass(),new Woodwind()};tuneAll(orchestra);}
}
程序执行的结果是:
上述程序中,使用根类Object的方法toString()替代了what()方法。
无论是向上转型为常规类、抽象类或是接口,tune()方法的行为都是一样的。实际上tune()也无从得知Instrument到底是什么类。