网站开发容易学吗,深圳怎么制作公司网页,免费制作软件官网,网站建设哪家好 需要多少钱本章概要
抽象类和方法接口创建 默认方法多继承接口中的静态方法Instrument 作为接口
接口和抽象类提供了一种将接口与实现分离的更加结构化的方法。
这种机制在编程语言中不常见#xff0c;例如 C 只对这种概念有间接的支持。而在 Java 中存在这些关键字#xff0c;说明…本章概要
抽象类和方法接口创建 默认方法多继承接口中的静态方法Instrument 作为接口
接口和抽象类提供了一种将接口与实现分离的更加结构化的方法。
这种机制在编程语言中不常见例如 C 只对这种概念有间接的支持。而在 Java 中存在这些关键字说明这些思想很重要Java 为它们提供了直接支持。
首先我们将学习抽象类一种介于普通类和接口之间的折中手段。尽管你的第一想法是创建接口但是对于构建具有属性和未实现方法的类来说抽象类也是重要且必要的工具。你不可能总是使用纯粹的接口。
抽象类和方法
在上一章的乐器例子中基类 Instrument 中的方法往往是“哑”方法。如果调用了这些方法就会出现一些错误。这是因为接口的目的是为它的派生类创建一个通用接口。
在那些例子中创建这个通用接口的唯一理由是不同的子类可以用不同的方式表示此接口。通用接口建立了一个基本形式以此表达所有派生类的共同部分。另一种说法把 Instrument 称为抽象基类或简称抽象类。
对于像 Instrument 那样的抽象类来说它的对象几乎总是没有意义的。创建一个抽象类是为了通过通用接口操纵一系列类。因此Instrument 只是表示接口不是具体实现所以创建一个 Instrument 的对象毫无意义我们可能希望阻止用户这么做。通过让 Instrument 所有的方法产生错误就可以达到这个目的但是这么做会延迟到运行时才能得知错误信息并且需要用户进行可靠、详尽的测试。最好能在编译时捕捉问题。
Java 提供了一个叫做_抽象方法_的机制这个方法是不完整的它只有声明没有方法体。下面是抽象方法的声明语法
abstract void f();包含抽象方法的类叫做_抽象类_。如果一个类包含一个或多个抽象方法那么类本身也必须限定为抽象的否则编译器会报错。
// interface/Basic.java
abstract class Basic {abstract void unimplemented();
}如果一个抽象类是不完整的当试图创建这个类的对象时Java 会怎么做呢它不会创建抽象类的对象所以我们只会得到编译器的错误信息。这样保证了抽象类的纯粹性我们不用担心误用它。
// interfaces/AttemptToUseBasic.java
// {WillNotCompile}
public class AttemptToUseBasic {Basic b new Basic();// error: Basic is abstract; cannot be instantiated
}如果创建一个继承抽象类的新类并为之创建对象那么就必须为基类的所有抽象方法提供方法定义。如果不这么做可以选择不做新类仍然是一个抽象类编译器会强制我们为新类加上 abstract 关键字。
// interfaces/Basic2.java
abstract class Basic2 extends Basic {int f() {return 111;}abstract void g() {// unimplemented() still not implemented}
}可以将一个不包含任何抽象方法的类指明为 abstract在类中的抽象方法没啥意义但想阻止创建类的对象时这么做就很有用。
// interfaces/AbstractWithoutAbstracts.java
abstract class Basic3 {int f() {return 111;}// No abstract methods
}public class AbstractWithoutAbstracts {// Basic3 b3 new Basic3();// error: Basic3 is abstract; cannot be instantiated
}为了创建可初始化的类就要继承抽象类并提供所有抽象方法的定义
// interfaces/Instantiable.java
abstract class Uninstantiable {abstract void f();abstract int g();
}public class Instantiable extends Uninstantiable {Overridevoid f() {System.out.println(f());}Overrideint g() {return 22;}public static void main(String[] args) {Uninstantiable ui new Instantiable();}
}留意 Override 的使用。没有这个注解的话如果你没有定义相同的方法名或签名抽象机制会认为你没有实现抽象方法从而产生编译时错误。因此你可能认为这里的 Override 是多余的。但是Override 还提示了这个方法被覆写——我认为这是有用的所以我会使用 Override不仅仅是因为当没有这个注解时编译器会告诉我出错。
记住事实上的访问权限是“friendly”。你很快会看到接口自动将其方法指明为 public。事实上接口只允许 public 方法如果不加访问修饰符的话接口的方法不是 friendly 而是 public。然而抽象类允许每件事
// interfaces/AbstractAccess.java
abstract class AbstractAccess {private void m1() {}// private abstract void m1a(); // illegalprotected void m2() {}protected abstract void m2a();void m3() {}abstract void m3a();public void m4() {}public abstract void m4a();
}private abstract 被禁止了是有意义的因为你不可能在 AbstractAccess 的任何子类中合法地定义它。
上一章的 Instrument 类可以很轻易地转换为一个抽象类。只需要部分方法是 abstract 即可。将一个类指明为 abstract 并不强制类中的所有方法必须都是抽象方法。如下图所示 下面是修改成使用抽象类和抽象方法的管弦乐器的例子
// interfaces/music4/Music4.java
// Abstract classes and methods
// {java interfaces.music4.Music4}
package interfaces.music4;
import polymorphism.music.Note;abstract class Instrument {private int i; // Storage allocated for eachpublic 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(Adjusting 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(Adjusting 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(Adjusting Stringed);}
}class Brass extends Wind {Overridepublic void play(Note n) {System.out.println(Brass.play() n);}Overridepublic void adjust() {System.out.println(Adjusting Brass);}
}class Woodwind extends Wind {Overridepublic void play(Note n) {System.out.println(Woodwind.play() n);}Overridepublic String what() {return Woodwind;}
}public class Music4 {// Doesnt care about type, so new types// added to system still work right: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) {// Upcasting during addition to the array:Instrument[] orchestra {new Wind(),new Percussion(),new Stringed(),new Brass(),new Woodwind()};tuneAll(orchestra);}
}输出
Wind.play() MIDDLE_C
Percussion.play() MIDDLE_C
Stringed.play() MIDDLE_C
Brass.play() MIDDLE_C
Woodwind.play() MIDDLE_C除了 Instrument基本没区别。
创建抽象类和抽象方法是有帮助的因为它们使得类的抽象性很明确并能告知用户和编译器使用意图。抽象类同时也是一种有用的重构工具使用它们使得我们很容易地将沿着继承层级结构上移公共方法。
接口创建
使用 interface 关键字创建接口。在本书中interface 和 class 一样随处常见除非特指关键字 interface其他情况下都采用正常字体书写 interface。
描述 Java 8 之前的接口更加容易因为它们只允许抽象方法。像下面这样
// interfaces/PureInterface.java
// Interface only looked like this before Java 8
public interface PureInterface {int m1(); void m2();double m3();
}我们甚至不用为方法加上 abstract 关键字因为方法在接口中。Java 知道这些方法不能有方法体仍然可以为方法加上 abstract 关键字但是看起来像是不明白接口徒增难堪罢了。
因此在 Java 8之前我们可以这么说interface 关键字产生一个完全抽象的类没有提供任何实现。我们只能描述类应该像什么做什么但不能描述怎么做即只能决定方法名、参数列表和返回类型但是无法确定方法体。接口只提供形式通常来说没有实现尽管在某些受限制的情况下可以有实现。
一个接口表示所有实现了该接口的类看起来都像这样。因此任何使用某特定接口的代码都知道可以调用该接口的哪些方法而且仅需知道这些。所以接口被用来建立类之间的协议。一些面向对象编程语言中使用 protocol 关键字完成相同的功能。
Java 8 中接口稍微有些变化因为 Java 8 允许接口包含默认方法和静态方法——基于某些重要原因看到后面你会理解。接口的基本概念仍然没变介于类型之上、实现之下。接口与抽象类最明显的区别可能就是使用上的惯用方式。接口的典型使用是代表一个类的类型或一个形容词如 Runnable 或 Serializable而抽象类通常是类层次结构的一部分或一件事物的类型如 String 或 ActionHero。
使用关键字 interface 而不是 class 来创建接口。和类一样需要在关键字 interface 前加上 public 关键字但只是在接口名与文件名相同的情况下否则接口只有包访问权限只能在接口相同的包下才能使用它。
接口同样可以包含属性这些属性被隐式指明为 static 和 final。
使用 implements 关键字使一个类遵循某个特定接口或一组接口它表示接口只是外形现在我要说明它是如何工作的。除此之外它看起来像继承。
// interfaces/ImplementingAnInterface.java
interface Concept { // Package accessvoid idea1();void idea2();
}class Implementation implements Concept {Overridepublic void idea1() {System.out.println(idea1);}Overridepublic void idea2() {System.out.println(idea2);}
}你可以选择显式地声明接口中的方法为 public但是即使你不这么做它们也是 public 的。所以当实现一个接口时来自接口中的方法必须被定义为 public。否则它们只有包访问权限这样在继承时它们的可访问权限就被降低了这是 Java 编译器所不允许的。
默认方法
Java 8 为关键字 default 增加了一个新的用途之前只用于 switch 语句和注解中。当在接口中使用它时任何实现接口却没有定义方法的时候可以使用 default 创建的方法体。默认方法比抽象类中的方法受到更多的限制但是非常有用我们将在“流式编程”一章中看到。现在让我们看下如何使用
// interfaces/AnInterface.java
interface AnInterface {void firstMethod();void secondMethod();
}我们可以像这样实现接口
// interfaces/AnImplementation.java
public class AnImplementation implements AnInterface {public void firstMethod() {System.out.println(firstMethod);}public void secondMethod() {System.out.println(secondMethod);}public static void main(String[] args) {AnInterface i new AnImplementation();i.firstMethod();i.secondMethod();}
}输出
firstMethod
secondMethod如果我们在 AnInterface 中增加一个新方法 newMethod()而在 AnImplementation 中没有实现它编译器就会报错
AnImplementation.java:3:error: AnImplementation is not abstract and does not override abstract method newMethod() in AnInterface
public class AnImplementation implements AnInterface {
^
1 error如果我们使用关键字 default 为 newMethod() 方法提供默认的实现那么所有与接口有关的代码能正常工作不受影响而且这些代码还可以调用新的方法 newMethod()
// interfaces/InterfaceWithDefault.java
interface InterfaceWithDefault {void firstMethod();void secondMethod();default void newMethod() {System.out.println(newMethod);}
}关键字 default 允许在接口中提供方法实现——在 Java 8 之前被禁止。
// interfaces/Implementation2.java
public class Implementation2 implements InterfaceWithDefault {Overridepublic void firstMethod() {System.out.println(firstMethod);}Overridepublic void secondMethod() {System.out.println(secondMethod);}public static void main(String[] args) {InterfaceWithDefault i new Implementation2();i.firstMethod();i.secondMethod();i.newMethod();}
}输出
firstMethod
secondMethod
newMethod尽管 Implementation2 中未定义 newMethod()但是可以使用 newMethod() 了。
增加默认方法的极具说服力的理由是它允许在不破坏已使用接口的代码的情况下在接口中增加新的方法。默认方法有时也被称为_守卫方法_或_虚拟扩展方法_。
多继承
多继承意味着一个类可能从多个父类型中继承特征和特性。
Java 在设计之初C 的多继承机制饱受诟病。Java 过去是一种严格要求单继承的语言只能继承自一个类或抽象类但可以实现任意多个接口。在 Java 8 之前接口没有包袱——它只是方法外貌的描述。
多年后的现在Java 通过默认方法具有了某种多继承的特性。结合带有默认方法的接口意味着结合了多个基类中的行为。因为接口中仍然不允许存在属性只有静态属性不适用所以属性仍然只会来自单个基类或抽象类也就是说不会存在状态的多继承。正如下面这样
// interfaces/MultipleInheritance.java
import java.util.*;interface One {default void first() {System.out.println(first);}
}interface Two {default void second() {System.out.println(second);}
}interface Three {default void third() {System.out.println(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();}
}输出
first
second
third现在我们做些在 Java 8 之前不可能完成的事结合多个源的实现。只要基类方法中的方法名和参数列表不同就能工作得很好否则会得到编译器错误
// interface/MICollision.java
import java.util.*;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 {}
/* Produces:
error: class Bob inherits unrelated defaults
for bob() from types Bob1 and Bob2
class Bob implements Bob1, Bob2 {}
^
1 error
*/interface Sam1 {default void sam() {System.out.println(Sam1::sam);}
}interface Sam2 {default void sam(int i) {System.out.println(i * 2);}
}// This works because the argument lists are distinct:
class Sam implements Sam1, Sam2 {}interface Max1 {default void max() {System.out.println(Max1::max);}
}interface Max2 {default int max() {return 47;}
}// class Max implements Max1, Max2 {}
/* Produces:
error: types Max2 and Max1 are imcompatible;
both define max(), but with unrelated return types
class Max implements Max1, Max2 {}
^
1 error
*/Sam 类中的两个 sam() 方法有相同的方法名但是签名不同——方法签名包括方法名和参数类型编译器也是用它来区分方法。但是从 Max 类可看出返回类型不是方法签名的一部分因此不能用来区分方法。为了解决这个问题需要覆写冲突的方法
// interfaces/Jim.java
import java.util.*;interface Jim1 {default void jim() {System.out.println(Jim1::jim);}
}interface Jim2 {default void jim() {System.out.println(Jim2::jim);}
}public class Jim implements Jim1, Jim2 {Overridepublic void jim() {Jim2.super.jim();}public static void main(String[] args) {new Jim().jim();}
}输出
Jim2::jim当然你可以重定义 jim() 方法但是也能像上例中那样使用 super 关键字选择基类实现中的一种。
接口中的静态方法
Java 8 允许在接口中添加静态方法。这么做能恰当地把工具功能置于接口中从而操作接口或者成为通用的工具
// onjava/Operations.java
package onjava;
import java.util.*;public interface Operations {void execute();static void runOps(Operations... ops) {for (Operations op: ops) {op.execute();}}static void show(String msg) {System.out.println(msg);}
}这是_模板方法_设计模式的一个版本在“设计模式”一章中详细描述runOps() 是一个模板方法。runOps() 使用可变参数列表因而我们可以传入任意多的 Operation 参数并按顺序运行它们
// interface/Machine.java
import java.util.*;
import onjava.Operations;class Bing implements Operations {Overridepublic void execute() {Operations.show(Bing);}
}class Crack implements Operations {Overridepublic void execute() {Operations.show(Crack);}
}class Twist implements Operations {Overridepublic void execute() {Operations.show(Twist);}
}public class Machine {public static void main(String[] args) {Operations.runOps(new Bing(), new Crack(), new Twist());}
}输出
Bing
Crack
Twist这里展示了创建 Operations 的不同方式一个外部类(Bing)一个匿名类一个方法引用和 lambda 表达式——毫无疑问用在这里是最好的解决方法。
这个特性是一项改善因为它允许把静态方法放在更合适的地方。
Instrument 作为接口
回顾下乐器的例子使用接口的话 类 Woodwind 和 Brass 说明一旦实现了某个接口那么其实现就变成一个普通类可以按常规方式扩展它。
接口的工作方式使得我们不需要显式声明其中的方法为 public它们自动就是 public 的。play() 和 adjust() 使用 default 关键字定义实现。在 Java 8 之前这些定义要在每个实现中重复实现显得多余且令人烦恼
enum Note {MIDDLE_C, C_SHARP, B_FLAT; // Etc.
}interface Instrument {// Compile-time constant:int VALUE 5; // static finaldefault void play(Note n) { // Automatically publicSystem.out.println(this .play() n);}default void adjust() {System.out.println(Adjusting 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 {// Doesnt care about type, so new types// added to the system still work right: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) {// Upcasting during addition to the array:Instrument[] orchestra {new Wind(),new Percussion(),new Stringed(),new Brass(),new Woodwind()};tuneAll(orchestra);}
}输出
Wind.play() MIDDLE_C
Percussion.play() MIDDLE_C
Stringed.play() MIDDLE_C
Brass.play() MIDDLE_C
Woodwind.play() MIDDLE_C这个版本的例子的另一个变化是what() 被修改为 toString() 方法因为 toString() 实现的正是 what() 方法要实现的逻辑。因为 toString() 是根基类 Object 的方法所以它不需要出现在接口中。
注意到无论是将其向上转型为称作 Instrument 的普通类或称作 Instrument 的抽象类还是叫作 Instrument 的接口其行为都是相同的。事实上从 tune() 方法上看不出来 Instrument 到底是一个普通类、抽象类还是一个接口。