第九章 接口(下)

             第九章    接口 (下)
9.4  Java中的多重继承
       接口不仅仅只是一种更纯粹形式的抽象类,它的目标比这要高。因为接口是根本没有任何具体实现的——也就是说,没有任何与接口相关的存储;因此,也就无法阻止多个接口的组合。这一点是很有价值的,因为你有时需要去表示"一个X是一个a和一个b以及一个c"。在C++中,组合多个类的接口的行为被称作多重继承。它可能会使你背负很沉重的包袱,因为每个类都有一个具体实现。在Java中,你可以执行相同的行为,但是只有一个类可以有具体实现;因此,通过组合多个接口,C++中的问题是不会在Java中发生的:
                                
                                              
     在导出类中,不强制要求必须有一个是抽象的或“具体的”(没有任何抽象方法的)基类。如果要从一个非接口的类继承,那么只能从一个类去继承。其余的基本元素都必须是接口。需要将所有的接口名都置于implements关键字之后,用逗号将它们一一隔开。可以继承任意多个接口,并可以向上转型为每个接口,因为每一个接口都是一个独立类型。下面的例子展示一个具体类组合数个接口之后产生了一个新类:
     interface CanFight{
     void fight();
}
interface CanSwim{
     void swim();
}
interface CanFly{
     void fly();
}
class ActionCharacter{
     public void fight(){}
}
class Hero extends ActionCharacter implements CanFight,CanSwim,CanFly{
     @Override
     public void fly() {}
     @Override
     public void swim() {}
}
public class Adventure {
     public static void t(CanFight x){
          x.fight();
     }
     public static void u(CanSwim x){
          x.swim();
     }
     public static void v(CanFly x){
          x.fly();
     }
     public static void w(ActionCharacter x){
          x.fight();
     }
     public static void main(String[] args) {
          Hero h = new Hero();
          t(h);
          u(h);
          v(h);
          w(h);
     }
}
       可以看到,Hero组合了具体类ActionCharacter和接口CanFight、CanSwim和CanFly。当通过这种方式将一个具体类和多个接口组合到一起时,这个具体类必须放在前面,后面跟着的才是接口(否则编译器会报错)。
      注意,CanFight接口与ActionCharacter类中的fight()方法的特征签名是一样的,而且,在Hero中并没有提供fight()的定义。可以扩展接口,但是得到的只是另一个接口。当想要创建对象时,所有的定义首先必须都存在。即使Hero没有显示地提供fight()的定义,其定义也因ActionCharacter而随之而来,这样就使得创建Hero对象成为了可能。
       在Adventure类中,可以看到有四个方法把上述各种接口和具体类作为参数。当Hero对象被创建时,它可以被传递给这些方法中的任何一个,这意味着它依次被向上转型为每一个接口。由于Java中这种设计接口的方式,使得这项工作不需要程序员付出任何特别的努力。
        一定要记住,前面的例子所展示的就是使用接口的核心原因:为了能够向上转型为多个基类型(以及由此而带来的灵活性)。然而,使用接口的第二个原因却是与使用抽象基类相同:防止客户端程序员创建该类的对象,并确保这仅仅是建立一个接口。这就带来一个问题:我们应该使用接口还是抽象类?如果要创建不带任何方法定义和成员变量的基类,那么就应该选择接口而不是抽象类。事实上,如果知道某事物应该称为一个基类,那么第一选择应该使它称为一个接口(该主题在本章的总结中再次讨论)

9.5  通过继承来扩展接口
      通过继承,可以很容易地在接口中添加新的方法声明,还可以通过继承在新接口中组合数个接口。这两种情况都可以获得新的接口,就像在下例所看到的:
     interface Monster{
     void menace();
}
interface DangerousMonster extends Monster{
     void destroy();
}
interface Lethal{
     void kill();
}
class DragonZilla implements DangerousMonster{
     public void menace(){}
     public void destroy(){}
}
interface Vampire extends DangerousMonster,Lethal{
     void drinkBlood();
}
class VeryBadVampire implements Vampire{
     public void menace(){}
     public void destroy(){}
     public void kill(){}
     public void drinkBlood(){}
}
public class HorrorShow {
     static void u(Monster b){
          b.menace();
     }
     static void v(DangerousMonster d){
          d.menace();
          d.destroy();
     }
     static void w(Lethal l){
          l.kill();
     }
     public static void main(String[] args) {
          DangerousMonster barney = new DragonZilla();
          u(barney);
          v(barney);
          Vampire vlad = new VeryBadVampire();
          u(vlad);
          v(vlad);
          w(vlad);
     }
}

       DangerousMonster是Monster的直接扩展,它产生了一个新接口。DragonZilla中实现了这个接口。
       在Vampire中使用的语法仅适用于接口继承。一般情况下,只可以将extends用于单一类,但是可以引用多个基类接口。就像所看到的,只需用逗号将接口名一一分隔开即可。

9.5.1   组合接口时的名字冲突
       在实现多重继承时,可能会碰到一个小陷阱。在前面的例子中,CanFight和ActionCharacter都有一个相同的void fight()方法。这不是问题所在,因为该方法在二者中是相同的。相同的方法不会有什么问题,但是如果它们的签名或返回类型不同,又会怎么样呢?这有一个例子:
     interface I1{
     void f();
}
interface I2{
     int f(int i);
}
interface I3{
     int f();
}
class C{
     public int f(){
          return 1;
     }
}
class C2 implements I1,I2{
     public void f(){}
     public int f(int i){
          return 1;
     }
}
class C3 extends C implements I2{
     public int f(int i){
          return 1;
     }
}
class C4 extends C implements I3{
     public int f(){
          return 1;
     }
}
//class C5 extends C implements I1{}
//interface I4 extends I1,I3{}
   
       此时困难来了,因为覆盖、实现和重载令人不快地搅在了一起,而且重载方法仅通过返回类型是区分不开的。当撤销最后两行的注释时,下列错误消息就说明了这一切:
     The return types are incompatible for the inherited methods I1.f(), C.f()
     The return types are incompatible for the inherited methods I1.f(), I3.f()
    
     在打算组合的不同接口中使用相同的方法名通常会造成代码可读性的混乱,请尽量避免这种情况。

9.6  适配接口
       接口最吸引人的原因之一就是允许同一个接口具有多个不同的具体实现。在简单的情况中,它的体现形式通常是一个接受接口类型的方法,而该接口的实现和向该方法传递的对象则取决于方法的使用者。
       因此,接口的一种常见的用法就是前面提到的策略设计模式,此时你编写一个执行某些操作的方法,而该方法将接受一个同样是你指定的接口。你主要就是要声明:“你可以用任何你想要的对象来调用我的方法,只要你的对象遵循我的接口。”这使得你的方法更加灵活、通用,并更具可复用性。
       例如,Java SE5的Scanner类(在第13章中就更多地了解它)的构造器接受的就是一个Readable接口。你会发现Readable没有用作Java标准类库中其他任何方法的参数,它是单独为Scanner创建的,以使得Scanner不必将其参数限制为某个特定类。通过这种方式,Scanner可以作用于更多的类型。如果你创建了一个新的类,并且想让Scanner可以作用于它,那么你就应该让它成为Readable,就像下面这样:
     public class RandomWords implements Readable{
     private static Random rand=new Random(47);
     private static final char[] capitals="ABCDEFJ".toCharArray();
     private static final char[] lowers="abcdefj".toCharArray();
     private static final char[] vowels="aeiou".toCharArray();
     private int count;
     public RandomWords(int count){
          this.count=count;
     }
     @Override
     public int read(CharBuffer cb) throws IOException {
          if(count--==0){
               return -1;
          }
          cb.append(capitals[rand.nextInt(capitals.length)]);
          for(int i=0;i<4;i++){
               cb.append(vowels[rand.nextInt(vowels.length)]);
               cb.append(lowers[rand.nextInt(lowers.length)]);
          }
          cb.append(" ");
          return 10;
     }
@SuppressWarnings("resource")
public static void main(String[] args) {
     Scanner s = new Scanner(new RandomWords(10));
     while(s.hasNext()){
          System.out.println(s.next());
     }
}
}
运行结果:
Jajefuaaf
Aofebudof
Aoaaaiaoa
Fajudaeia
Jucaeeaif
Aaeediauf
Aufibibud
Duaaeeaid
Jududoaod
Dafiaeeod

        Readable接口只要求实现read()方法,在read()内部,将输入内容添加到CharBuffer参数中(有多种方法可以实现此目的,请查看CharBuffer的文档),或者在没有任何输出时返回-1.
        假设你有一个还未实现Readable的类,怎样才能让Scanner作用于它呢?下面这个类就是一个例子,它可以产生随机浮点数:
     public class RandomDoubles {
     private static Random rand=new Random(47);
     public double next(){
          return rand.nextDouble();
     }
     public static void main(String [] args){
          RandomDoubles rd = new RandomDoubles();
          for(int i=0;i<7;i++){
               System.out.println(rd.next());
          }
     }
}
运行结果:
0.7271157860730044 
0.5309454508634242 
0.16020656493302599 
0.18847866977771732 
0.5166020801268457 
0.2678662084200585 
0.2613610344283964

       我们再次使用了适配器模式,但是在本例中,被适配的类可以通过继承和实现Readable接口来创建。因此,通过使用interface关键字提供的伪多重继承机制,我们可以生成既是RandomDoubles又是Readable的新类:
     public class AdaptedRandomDouble extends RandomDoubles implements Readable{
     private int count;
     public AdaptedRandomDouble(int count){
          this.count=count;
     }
     @Override
     public int read(CharBuffer cb) throws IOException {
          if(count--==0){
               return -1;
          }
          String result=Double.toString(next())+"  ";
          cb.append(result);
          return result.length();
     }
     @SuppressWarnings("resource")
     public static void main(String[] args) {
          Scanner s = new Scanner(new AdaptedRandomDouble(7));
          while(s.hasNextDouble()){
               System.out.println(s.nextDouble());
          }
     }
}
运行结果:
0.7271157860730044
0.5309454508634242
0.16020656493302599
0.18847866977771732
0.5166020801268457
0.2678662084200585
0.2613610344283964

       因为在这种方式中,我们可以在任何现有类之上添加新的接口,所以这意味这让方法接受接口类型,是一种让任何类都可以对该方法进行适配的方式。这就是使用接口而不是类的强大之处。

9.7  接口中的域
       因为你放入接口中的任何域都是自动是static和final的,所以接口就成为了一种很便捷的用来创建常量组的工具。在Java SE5之前,这是产生与C或C++中的enum(枚举类型)具有相同效果的类型的唯一途径。因此在Java SE5之前的代码中你会看到下面这样的代码:
     public interface Months {
     int
         JANUARY=1,FEBRUARY=2,MARCH=3;
}
      请注意,Java中标识具有常量初始化值的static final时,会使用大写字母的风格(在一个标识符中用下划线来分隔多个单词)。接口中的域自动是public的,所以没有显式地指明这一点。
       有了Java SE5,你就可以使用更加强大而灵活的enum关键字,因此,使用接口来群组常量已经显得没什么意义了。但是,当你阅读遗留的代码时,在许多情况下你可能还是会碰到这种旧的习惯用法。在第19章中可以看到更多的关于使用enum的细节说明。

9.7.1  初始化接口中的域
        在接口中定义的域不能是“空final”,但是可以被非常量表达式初始化。例如:
     public interface RandVals {
     Random RAND=new Random(47);
     int RANDOM_INT=RAND.nextInt(10);
     long RANDOM_LONG=RAND.nextLong()*10;
     float RANDOM_FLOAT=RAND.nextLong()*10;
     double RANDOM_DOUBLE=RAND.nextDouble()*10;
}

public class TestRandvals {
     public static void main(String[] args) {
          System.out.println(RandVals.RANDOM_INT);
          System.out.println(RandVals.RANDOM_LONG);
          System.out.println(RandVals.RANDOM_FLOAT);
          System.out.println(RandVals.RANDOM_DOUBLE);
     }

}
运行结果:
8
-32032247016559954
-8.5939291E18
5.779976127815049


9.8  嵌套接口
       接口可以嵌套在类或其他接口中。这揭示了许多非常有趣的特性:
       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();
     }
     public interface H{
          void f();
     }
     void g();
//     private interface I{}
}
public class NestingInterfaces {
     public class BImp implements A.B{
          public void f(){}
     }
     class CImp implements A.C{
          public void f(){}
     }
//     class CImp 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();
//     A.D getd = a.getd();
//     A.DImp2 di2=a.getd();
//     a.getd().f();
     A a2 = new A();
     a2.receiveD(a.getd());
}
}

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

9.9  接口与工厂
       接口是实现多重继承的途径,而生成遵循某个接口的对象的典型方式就是工厂方法设计模式。这与直接调用构造器不同,我们在工厂对象上调用的是创建方法,而该工厂对象将生成接口的某个实现的对象。理论上,通过这种方式,我们的代码将完全与接口的实现分离,这就使得我们可以透明地将某个实现替换为另一个实现。下面的实例展示了工厂方法的结构:
     interface Service{
     void method1();
     void method2();
}
interface ServiceFactory{
     Service getService();
}
class Implementation1 implements Service{
     public Implementation1() {}
     public void method1(){
          System.out.println("Implementation1 method1");
     }
     public void method2(){
          System.out.println("Implementation1 method2");
     }
}
class Implementation1Factory implements ServiceFactory{
     public Service getService(){
          return new Implementation1();
     }
}
class Implementation2 implements Service{
     public Implementation2() {}
     public void method1(){
          System.out.println("Implementation2 method1");
     }
     public void method2(){
          System.out.println("Implementation2 method2");
     }
}
class Implementation2Factory implements ServiceFactory{
     public Service getService(){
          return new Implementation2();
     }
}
public class Factories {
     public static void serviceConsumer(ServiceFactory fact){
          Service s = fact.getService();
          s.method1();
          s.method2();
     }
     public static void main(String[] args) {
          serviceConsumer(new Implementation1Factory());
          serviceConsumer(new Implementation2Factory());
     }

}

    如果不是用工厂方法,你的代码就必须在某处指定将要创建的Service的确切类型,以便调用合适的构造器。
     为什么我们想要添加这种额外级别的间接性呢?一个常见的原因是想要创建框架:假设你正在创建一个对弈游戏系统,例如,在相同的棋盘上下国际象棋和西洋跳棋:
     interface Game{
     boolean move();
}
interface GameFactory{
     Game getGame();
}
class Checkers implements Game{
     private int moves=0;
     private static final int MOVES=3;
     public boolean move(){
          System.out.println("Checkers move"+moves);
          return ++moves!=MOVES;
     }
}
class CheckersFactory implements GameFactory{
     public Game getGame(){
          return new Checkers();
     }
}
class Chess implements Game{
     private int moves=0;
     private static final int MOVES=4;
     public boolean move(){
          System.out.println("Chess move"+moves);
          return ++moves!=MOVES;
     }
}
class ChessFactory implements GameFactory{
     public Game getGame(){
          return new Chess();
     }
}
public class Games {
     public static void playGame(GameFactory factory){
          Game game = factory.getGame();
          while(game.move());
     }
public static void main(String[] args) {
     playGame(new CheckersFactory());
     playGame(new ChessFactory());
}
}
运行结果:
Checkers move0
Checkers move1
Checkers move2
Chess move0
Chess move1
Chess move2
Chess move3

       如果Games类表示一段复杂的代码,那么这种方式就允许你在不同类型的游戏中复用这段代码。你可以再想象一些能够从这个模式中受益的更加精巧的游戏。
       在下一章中,你将可以看到另一种更优雅的工厂实现方式,那就是使用匿名内部类。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值