Java学习笔记——接口

本文为Java编程思想第四版的学习笔记,在此感谢作者Bruce Eckel给我们带来这样一本经典著作,也感谢该书的翻译及出版人员。本文为原创笔记,转载请注明出处,谢谢。


接口和内部类为我们提供了一种将接口与实现分离的更加结构化的方法。这种机制在编程语言中并不通用。例如,C++对这些概念只有间接的支持。在Java中存在语言关键字这个事实表明,人们认为这些思想是很重要的,以至于要提供对他们的直接支持。首先,我们讲学习抽象类,他是普通的类和接口之间的一种中庸之道。尽管在构建具有某些为实现方法的类是,你的第一想法可能是创建接口,但是抽象类仍旧是用于此目的的一种重要而必须的工具。因为你不可能总是使用纯接口。


1.抽象类和抽象方法

在上一章所有“乐器”的例子中,基类Instrument中的方法往往是“哑”(dummy)方法。若要调用这些方法,就会出现一些错误。这是因为Instrument类的目的是为他的所有导出类创建一个通用的接口。在那些示例中,建立这个通用接口的唯一理由是,不同的子类可以用不同的方式表示此接口。通用接口建立起一种基本形式,一次表示所有导出类的共同部分。另一种说法是将Instrument类称作抽象基类,或简称抽象类。

如果我们只有一个像Instrument这样的抽象类,那么该类的独享几乎没有任何意义。我们创建抽象类是希望通过这个通用接口操纵一系列类。因此,Instrument只是表示了一个接口,没有具体的实现内容;因此,创建一个Instrument对象没有什么意义,并且我们可能还想组织使用者这样做。通过让Instrument中的所有方都产生错误,就可以实现则个目的。但是这样做会将错误信息延迟到运行时才获得,并且需要在客户端进行可靠、相近的测试。所以最好是在编译时捕获这些问题。

为此Java提供一个叫抽象方法的机制,这种方法是不完整的;仅有声明而没有方法体。下面是抽象方法声明所采用的语法:

abstact void f();

包含抽象方法的类叫做抽象类。如果一个类包含一个或多个抽象方法,该类必须被限定为抽象的(否则,编译器就会报错)。如果从一个抽象类基层,并想创建该新类的对象,那么就必须为基类中的所有抽象方法提供方法定义。如果不这样做(可以选择不做),那么导出类便也是抽象类,且编译器将会强制我们用abstract关键字来限定这个类。我们也可能会创建一个没有任何抽象方法的抽象类。考虑这种情况:如果有一个类,让其包含任何abstract方法都显得没有实际意义,而且我们也想要组织产生则个类的任何对象,那么这时这样做就很有意义了。创建抽象类和抽象方法非常有用,因为他们可以使类的抽象性明确起来,并告诉用户和编译器打算怎样来使用它们。


2.接口

interface关键字使抽象的概念更向前迈进了一步。interface这个关键字产生一个完全抽象的类,它根本没有提供任何具体实现。它允许创建者确定方法名、参数列表和返回值类型,但是没有任何方法体。接口只提供了形式,而未提供任何具体实现。一个接口表示“所有实现了该特定接口的类看起来都像这样”。因此,任何使用特定接口的代码都知道可以调用该接口的哪些方法,而且仅需要知道这些。因此,接口被用来建立类与类之间的协议(某些面向对象编程语言使用关键字protocol来完成这一功能)。但是,interface不仅仅是一个极度抽象的类,因为它允许人们通过创建一个能够被向上转型为多种基类的类型,来实现某种类似多重继变种的特征。想要创建一个接口,需要用interface关键字来替代class关键字。就像类一样,可以再interface关键字前面添加public关键字(但仅限于接口在于其同名的文件中被定义)。如果不添加public关键字,则它只具有包访问权限,这样它就只能在同一个包内可用。接口也可以包含域,但是这些域隐式地是static和final的。要让一个类遵循某个特定接口(或者是一组接口),需要使用implements冠军艾娘子,它表示:“interface只是它的外貌,但是现在我要声明它是如何工作的”。除此之外,它看起来还很想继承。

可以选择在接口中显式地将方法声明为public的,但即使你不这么做,它们也是public的。因此,当要实现一个接口时,在接口中被定义的方法必须被定义为public的;否则,他们将只能得到默认的包访问权限,这样在方法被继承的过程中,其可访问权限就被降低了,这是Java编译器所不允许的。


3.完全解耦

只要一个方法操作的是类而非接口,那么你就只能使用这个类及其子类。如果你想要将这个方法应用于不在此继承结构中的某个类,那么就会触霉头了。接口可以在很大程度上放宽这种限制,因此,它使得我们可以编写可复用性更好的代码。将接口从具体实现中解耦使得接口可以应用于多种不同的具体实现,因此代码也就更具可复用性。


4.Java中的多继承

接口不仅仅只是一种更纯粹形式的抽象类,它的目标比这个要高。因为接口是根本没有任何具体实现的——也就是说,没有任何与接口相关的存储;因此,也就无法阻止多个接口的组合,这一点是很有价值的,因为你有时需要去表示“一个x是一个a和一个b以及一个c”。在C++中,组合多个类的接口的行为被称作多重继承。它可能会使你背负很沉重的包袱,因为每个类都有一个具体实现。在Java中,你可以执行相同的行为,但是只有一个类可以有具体实现;因此,通过组合多个接口,C++中的问题是不会发生的。在导出类中,不强制要求必须有一个抽象的或“具体的”(没有任何抽象方法的)基类。如果要从一个非接口的类继承,那么只能从一个类去继承(Java不支持多继承)。其余的基元素都必须是接口。需要将所有接口名都置于implements关键字之后,用逗号将它们一一隔开。可以继承任意多个接口,并可以向上转型为每个接口,因为每一个接口都是一个独立类型。一定要记住:使用接口的核心原因:为了能够向上转型为多个基类型(以及由此而带来的灵活性)。然而,使用接口的第二个原因却是与使用抽象基类相同:防止客户端程序员创建该类的对象,并确保这仅仅是简历一个接口。这就带来了一个问题:我们应该使用接口还是抽象类?如果要创建不带任何方法定义和成员变量的基类,那么就应该选择接口而不是抽象类。事实上,如果知道事物应该成为一个基类,那么第一选择应该是使它成为一个接口。


5.通过继承来扩展接口

通过继承,可以很容易地在接口中添加新的方法声明,还可以通过继承在新接口中组合数个接口。这两种情况都可以获得新的接口。

5.1 组合接口时的名字冲突

在实现多继承时,可能会碰到一个小陷阱。如果继承的多个接口中有重名方法该如何处理?当重名的方法的返回值、方法名和参数列表完全相同时,这两个方法实际上是一个方法,在继承接口的接口或者实现接口的类中,将只保留一个方法。但是如果两个方法方法名相同,返回值或参数列表却不同,此时困难来了。因为覆盖、实现和重载令人不快的搅在了一起,而且重载方法仅通过返回类型是区分不开的。在打算组合的不同接口中使用相同的方法名通常会造成代码可读性的混乱,请尽量避免这种情况。


菜鸟总结:当继承或实现多个接口时,如果出现名字冲突,则:

1)如果两个方法的返回值、方法名和参数列表完全相同,这不会产生什么问题,只是在继承这些接口的接口或实现这些接口的类中,只保留一个对这个方法的定义或实现;

2)如果两个方法的返回值、方法名相同,参数列表不同,这也不会产生什么问题,在继承这些接口的接口或实现这些接口的类中,会有两个方法的定义或实现,这时是方法的重载;

3)如果两个方法的方法名相同而参数列表不同,这是会发生问题,编译器将会报错

总之,当继承或实现多个接口时,如果出现名字冲突,如果两个方法的方法签名(返回值、方法名、参数列表)完全相同,或者符合重载的相关规定,则不会发生什么问题。


6.适配接口

接口最吸引人的原因之一就是允许同一个接口具有多个不同的具体实现。在简单的情况中,它的体现形似通常是一个接受接口类型的方法,而该接口的实现和想方法传递的对象则取决于方法的使用者。因此,接口的一种常见用法就是设计模式中的策略模式,此时你编写一个执行某些操作的方法,而该方法将接受一个同样是你指定的接口。你主要就是要晟敏:“你可以用任何你想要的对象来调用我的方法,只要你的对象遵循我的接口”。这使得你的方法更加灵活、通用,并更具可复用性。


7.接口中的域

因为你放入接口中的任何域都是static和final的,所以接口就成为了一种很便捷的用阿里创建常量组的工具。在Java SE5之前,这是产生与C或C++中的enum具有相同效果的唯一途径。有了Java SE5你就可以使用更加强大而灵活的enum关键字,因此,使用接口来群组常量已经显得没有什么意义了。


8.嵌套接口

接口可以嵌套在类或其他接口中。这揭示了愈多非常有趣的特性:

<span style="font-size:14px;">class A {
  interface B {
    void f();
  }
  public class BImp implements B {
    public void f() {}
  }
  private class BImp2 implements B {
    public void f() {}
  }
  public interface C {
    void f();
  }
  class CImp implements C {
    public void f() {}
  }	
  private class CImp2 implements C {
    public void f() {}
  }
  private interface D {
    void f();
  }
  private class DImp implements D {
    public void f() {}
  }
  public class DImp2 implements D {
    public void f() {}
  }
  public D getD() { return new DImp2(); }
  private D dRef;
  public void receiveD(D d) {
    dRef = d;
    dRef.f();
  }
}	

interface E {
  interface G {
    void f();
  }
  // Redundant "public":
  public interface H {
    void f();
  }
  void g();
  // Cannot be private within an interface:
  //! private interface I {}
}	

public class NestingInterfaces {
  public class BImp implements A.B {
    public void f() {}
  }
  class CImp implements A.C {
    public void f() {}
  }
  // Cannot implement a private interface except
  // within that interface's defining class:
  //! class DImp implements A.D {
  //!  public void f() {}
  //! }
  class EImp implements E {
    public void g() {}
  }
  class EGImp implements E.G {
    public void f() {}
  }
  class EImp2 implements E {
    public void g() {}
    class EG implements E.G {
      public void f() {}
    }
  }	
  public static void main(String[] args) {
    A a = new A();
    // Can't access A.D:
    //! A.D ad = a.getD();
    // Doesn't return anything but A.D:
    //! A.DImp2 di2 = a.getD();
    // Cannot access a member of the interface:
    //! a.getD().f();
    // Only another A can do anything with getD():
    A a2 = new A();
    a2.receiveD(a.getD());
  }
} </span>

在类中嵌套接口的语法是相当显而易见的就像非嵌套接口一样,可以拥有public和“包访问权限”两种可视性。作为一种新添加方式,接口也可以被实现为private的,就像在A.D中所看到的(相同的语法即适用于嵌套接口,也适用于嵌套类)。那么private的嵌套接口能带来什么好处呢?读者可能会猜想,它只能够被实现为DImp中的一个private内部类,但是A.DImp2展示了它同样可以被实现为public类。但是,A.DImp2只能被自身使用,你无法说它实现了一个private接口D。因此,实现一个private接口只是一种方式,它可以强制该接口中的方法定义不要添加任何类型信息(也就是说,不允许向上转型)。getD()方法是我们陷入了一个进退两难的境地,这个问题与private接口相关:它是一个返回对private接口的引用和public方法。你对这个方法的返回值能做些什么呢?在main()方法中,可以看到数次常事使用返回值的行为都失败了。只有一种方式可成功,那就是将返回值交给有权使用它的对象。接口E说明接口之间也可以嵌套。然而作用于接口的各种规则,特鄙视所有的接口元素都必须是public的,在此会被严格执行。因此,嵌套在另一个接口中的接口自动就是public的,而不能声明为private。NestingInterface展示了嵌套接口的各种实现方式。特别要注意的是,当实现某个接口时,并不需要实现嵌套在其内部的任何接口。而且,private接口不能再定义它的类之外被实现。添加这些特性的最初原因可能是出于对严格语法一致性的考虑,但是我(Bruce Eckel)总认为,一旦你了解了某种也行,就总能够找到他的用武之地。


9.接口与工厂

接口是实现多重继承的途径,而生成遵循某个接口的对象的典型方式就是工厂方法设计模式。这与直接调用构造器不同,我们在工厂对象上调用的是创建方法,而该工厂对象将生成接口的某个实现的对象。理论上,通过这种方式,我们的代码将完全与接口的实现分离,这就使得我们可以透明地将某个实现踢馆为另一个实现。为什么我们想要添加这种额外级别的间接性呢?一个常见的原因是想要创建框架。


总结:“确定接口是理想选择,因而应该总是选择接口而不是具体的类”。这其实是一种引诱。当然,对于创建类,几乎在任何时刻,都可以替代创建一个接口和一个工厂。与多人都掉进了这种诱惑的陷阱,只要有可能就去创建接口和工厂。这种逻辑看起来好像是因为需要使用不同的具体实现,因此总是应该添加这种抽象性。这实际上已经变成了一种草率的设计优化。任何抽象性都应该是应真正的需求而产生的。当必需时,你应该重构接口而不是到处添加额外级别的间接性,并由此带来的额外的复杂性。这种额外的复杂性非常显著,如果你让某人去处理这种复杂性,只是因为你意识到由于以防万一而添加了新接口,而没有其他更有说服力的原因,那么好吧,如果我碰上了这种事,那么就会质疑此人所做的所有设计了。

恰当的原则应该是优先选择类而不是接口。从类开始,如果接口的必需性变得非常明确,那么就进行重构。接口是一种重要的工具,但是他们容易被滥用。








  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值