接口(3):java中的多重继承、通过继承来扩展接口、适配接口

野性酷女 2023-07-04 04:48 96阅读 0赞

一、java中的多重继承

  1. 接口不仅仅只是一种更纯粹形式的抽象类,它的目标比这要高。因为接口是根本没有任何具体实现的--也就是说,没有任何与接口相关的存储;因此,也就无法阻止多个接口的组合。这一点是很有价值的,因为你有时需要去表示“一个x是一个a和一个b以及一个c”。在C++中,组合多个类的接口的行为被称作多重继承。它可能会使你背负很沉重的包袱,因为每个类都有一个具体实现。在java中,你可以执行相同的行为,但是只有一个类可以有具体实现;因此,通过组合多个接口,C++中的问题是不会出现在java中发生的:

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQwMjk4MzUx_size_16_color_FFFFFF_t_70

  1. 在导出类中,不强制要求必须有一个是抽象的或“具体的”(没有任何抽象方法的)基类。如果要从一个非接口的类继承,那么只能从一个类去继承。其余的基元素都必须是接口。需要将所有的接口名都置于implements关键字之后,用逗号将它们一一隔开。可以继承任意多个接口,并可以向上转型为每个接口,因为每一个接口都是一个独立类型。下面的例子展示了一个具体类组合数个接口之后产生了一个新类:
  2. /**
  3. * 可以战斗
  4. */
  5. interface CanFight {
  6. void fight();
  7. }
  8. /**
  9. * 可以游泳
  10. */
  11. interface CanSwim {
  12. void swim();
  13. }
  14. /**
  15. * 可以飞
  16. */
  17. interface CanFly {
  18. void fly();
  19. }
  20. /**
  21. * 角色
  22. */
  23. class ActionCharacter {
  24. public void fight() {
  25. }
  26. }
  27. class Hero extends ActionCharacter implements CanFight, CanSwim, CanFly {
  28. @Override
  29. public void fly() {
  30. }
  31. @Override
  32. public void swim() {
  33. }
  34. }
  35. public class Adventure {
  36. public static void t(CanFight x) {
  37. x.fight();
  38. }
  39. public static void u(CanSwim x) {
  40. x.swim();
  41. }
  42. public static void v(CanFly x) {
  43. x.fly();
  44. }
  45. public static void w(ActionCharacter x) {
  46. x.fight();
  47. }
  48. public static void main(String[] args) {
  49. Hero h = new Hero();
  50. t(h);
  51. u(h);
  52. v(h);
  53. w(h);
  54. }
  55. }
  56. 可以看到,Hero组合了具体类ActionCharacter和接口CanFightCanSwimCanFly。当通过这种方式将一个具体类和多个接口组合到一起时,这个具体类必须放在前面,后面跟着的才是接口(否则编译器会报错)。
  57. 注意,CanFight接口与ActionCharacter类中的fight()方法的特征签名是一样的,而且,在Hero中并没有提供fight()的定义。可以扩展接口,但是得到的只是另一个接口。当想要创建对象时,所有的定义首先必须都存在。即使Hero没有显式的提供fight()的定义,其定义也因ActionCharacter而随之而来,这样就使得创建Hero对象成为了可能。
  58. Adventure类中,可以看到有四个方法把上述各种接口和具体类作为参数。当Hero对象被创建时,它可以被传递给这些方法中的任何一个,这意味着它依次被向上转型为每一个接口。由于java中这种设计接口的方式,使得这项工作并不需要程序员付出任何特别的努力。
  59. 一定要记住,前面的例子所展示的就是使用接口的核心原因:为了能够向上转型为多个基类型(以及由此而带来的灵活性)。然而,使用接口的第二个原因却是与使用抽象基类相同:防止客户端程序员创建该类的对象,并确保这仅仅是建立一个接口。这就带来了一个问题:我们应该使用接口还是抽象类?如果要创建不带任何方法定义和成员变量的基类,那么就应该选择接口而不是抽象类。事实上,如果知道某事物应该成为一个基类,那么第一选择应该是使它成为一个接口。

二、通过继承来扩展接口

  1. 通过继承,可以很容易地在接口中添加新的方法声明,还可以通过继承在新接口中组合数个接口。这两种情况都可以获得新的接口,就像在下例中所看到的:
  2. /**
  3. * 怪物
  4. */
  5. interface Monster {
  6. // 威胁
  7. void menace();
  8. }
  9. /**
  10. * 危险的怪物
  11. */
  12. interface DangerousMonster extends Monster {
  13. void destroy();
  14. }
  15. /**
  16. * 致命的
  17. */
  18. interface Lethal {
  19. void kill();
  20. }
  21. class DragonZilla implements DangerousMonster {
  22. @Override
  23. public void menace() {
  24. }
  25. @Override
  26. public void destroy() {
  27. }
  28. }
  29. /**
  30. * 吸血鬼
  31. */
  32. interface Vampire extends DangerousMonster, Lethal {
  33. // 吸血
  34. void drinkBlood();
  35. }
  36. class VeryBadVampire implements Vampire {
  37. @Override
  38. public void destroy() {
  39. }
  40. @Override
  41. public void menace() {
  42. }
  43. @Override
  44. public void kill() {
  45. }
  46. @Override
  47. public void drinkBlood() {
  48. }
  49. }
  50. public class HorrorShow {
  51. static void u(Monster b) {
  52. b.menace();
  53. }
  54. static void v(DangerousMonster d) {
  55. d.menace();
  56. d.destroy();
  57. }
  58. static void w(Lethal l) {
  59. l.kill();
  60. }
  61. public static void main(String[] args) {
  62. DangerousMonster barney = new DragonZilla();
  63. u(barney);
  64. v(barney);
  65. Vampire vlad = new VeryBadVampire();
  66. u(vlad);
  67. v(vlad);
  68. w(vlad);
  69. }
  70. }
  71. DangerousMonsterMonster的直接扩展,它产生了一个新接口。DragonZilla中实现了这个接口。
  72. Vampire中使用的语法仅适用于接口继承。一般情况下,只可以将extends用于单一类,但是可以引用多个基类接口。就想所看到的,只需用逗号将接口名一一分隔即可。

三、组合接口时的名字冲突

  1. 在实现多重继承时,可能会碰到一个小陷阱。在前面例子中,CanFightActionCharacter都有一个相同的void fight()方法。这不是问题所在,因为该方法在二者中是相同的。相同的方法不会又什么问题,但是如果它们的签名或返回类型不同,又会怎么样呢?这有一个例子:
  2. interface I1 {
  3. void f();
  4. }
  5. interface I2 {
  6. int f(int i);
  7. }
  8. interface I3 {
  9. int f();
  10. }
  11. class C {
  12. public int f() {
  13. return 1;
  14. }
  15. }
  16. class C2 implements I1, I2 {
  17. @Override
  18. public int f(int i) {
  19. return 1;
  20. }
  21. @Override
  22. public void f() {
  23. }
  24. }
  25. class C3 extends C implements I2 {
  26. @Override
  27. public int f(int i) {
  28. return 1;
  29. }
  30. }
  31. class C4 extends C implements I3 {
  32. public int f() {
  33. return 1;
  34. }
  35. }
  36. // Method differ only by return type
  37. // class C5 extends C implements I1{}
  38. // interface I4 extends I1,I3{}
  39. 此时困难来了,因为覆盖、实现和重载令人不快地搅在了一起,而且重载方法仅通过返回类型是区分不开的。当撤销最后两行注释时,下列错误消息就说明了这一切:

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()

  1. 在打算组合不同接口中使用相同的方法名通常会造成代码可读性的混乱,请尽量避免这种情况。

四、适配接口

  1. 接口最吸引人的原因之一就是允许同一个接口具有多个不同的具体实现。在简单的情况下,它的体现形式通常是一个接受接口类型的方法,而该接口的实现和向该方法传递的对象则取决于方法的使用者。
  2. 因此,接口的一种常见用法就是前面提到的策略设计模式,此时你编写一个执行某些操作的方法,而该方法将接受一个同样是你指定的接口。你主要就是要声明:“你可以用任何你想要的对象来调用我的方法,只要你的对象遵循我的接口。”这使得你的方法更加灵活、通用,并更具可复用性。
  3. 例如,Java SE5Scanner类的构造器接受的就是一个Readable接口。你会发现Readable没有用作Java标准类库中其他任何方法的参数,它是单独为Scanner创建的,以使得Scanner不必将其参数限制为某个特定类。通过这种方式,Scanner可以作用于更多的类型。如果你创建了一个新的类,并且想让Scanner可以作用于它,那么你就应该让它成为Readable,就像下面这样:
  4. import java.io.IOException;
  5. import java.nio.CharBuffer;
  6. import java.util.Random;
  7. import java.util.Scanner;
  8. public class RandomWords implements Readable {
  9. private static Random r = new Random();
  10. private static final char[] capitals = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
  11. private static final char[] lowers = "abcdefghijklmnopqrstuvwxyz".toCharArray();
  12. private static final char[] vowels = "aeiou".toCharArray();
  13. private int count;
  14. public RandomWords(int count) {
  15. this.count = count;
  16. }
  17. @Override
  18. public int read(CharBuffer cb) throws IOException {
  19. if (count-- == 0)
  20. return -1;
  21. cb.append(capitals[r.nextInt(capitals.length)]);
  22. for (int i = 0; i < 4; i++) {
  23. cb.append(vowels[r.nextInt(vowels.length)]);
  24. cb.append(lowers[r.nextInt(lowers.length)]);
  25. }
  26. cb.append(" ");
  27. return 10;// 拼接的字符数量
  28. }
  29. public static void main(String[] args) {
  30. Scanner s = new Scanner(new RandomWords(10));
  31. while (s.hasNext())
  32. System.out.println(s.next());
  33. }
  34. }
  35. Readable接口只要求实现read()方法,在read()内部,将输入内容添加到CharBuffer参数中,或者在没有任何输入时返回-1
  36. 假设你有一个还未实现Readable的类,怎样才能让Scanner作用于它呢?下面这个类就是一个例子,它可以产生随机浮点数:
  37. import java.util.Random;
  38. public class RandomDoubles {
  39. private static Random r = new Random();
  40. public double next() {
  41. return r.nextDouble();
  42. }
  43. public static void main(String[] args) {
  44. RandomDoubles rd = new RandomDoubles();
  45. for (int i = 0; i < 7; i++)
  46. System.out.print(rd.next() + " ");
  47. }
  48. }
  49. 我们再次使用了适配器模式,但是在本例中,被适配的类可以通过继承和实现Readable接口来创建。因此,通过使用interface关键字提供的伪多重继承机制,我们可以生成既是RandomDoubles又是Readable的新类:
  50. import java.io.IOException;
  51. import java.nio.CharBuffer;
  52. import java.util.Scanner;
  53. public class AdaptedRandomDoubles extends RandomDoubles implements Readable {
  54. private int count;
  55. public AdaptedRandomDoubles(int count) {
  56. this.count = count;
  57. }
  58. @Override
  59. public int read(CharBuffer cb) throws IOException {
  60. if (count-- == 0)
  61. return -1;
  62. String result = Double.toString(next()) + " ";
  63. cb.append(result);
  64. return result.length();
  65. }
  66. public static void main(String[] args) {
  67. Scanner s = new Scanner(new AdaptedRandomDoubles(7));
  68. while (s.hasNext())
  69. System.out.print(s.nextDouble() + " ");
  70. }
  71. }
  72. 因为在这种方式中,我们可以在任何现有类之上添加新的接口,所以这意味着让方法接受接口类型,是一种让任何类都可以对该方法进行适配的方式。这就是使用接口而不是使用类的强大之处。

如果本文对您有很大的帮助,还请点赞关注一下。

发表评论

表情:
评论列表 (有 0 条评论,96人围观)

还没有评论,来说两句吧...

相关阅读