在JAVA中,从一个抽象方法继承超载方法

5

给定一个抽象类

public abstract class AbstractStomach {
    public abstract void consume(Food food);
}

我希望有一些具体的类,这些类拥有不同的重载方法:

public class FruitStomach extends AbstractStomach {
    public static class Apple extends Food {}
    public static class Orange extends Food {}
    @Override
    public void consume(Apple food) {...}
    @Override
    public void consume(Orange food) {...}
}

public class MeatStomach extends AbstractStomach {
    public static class Beef extends Food {}
    @Override
    public void consume(Beef food) {...}
}

上述方法不能有效实现我的需求,因为它要求在每个具体类中也实现一般的 consume(Food) 抽象方法。但我不希望具体类实现 consume(Food),因为这已经被它们重载的变量所覆盖了。实际上,每个具体类只应该消耗特定种类的食物,而不是任何一种食物。
AbstractStomach stomach = new FruitStomach();
stomach.consume(new Beef());

应该会出现一个错误。

如果我在抽象类中删除了抽象方法,那么它们扩展抽象类就没有意义了,因为它们不知道要实现什么。

如何修复上述设计,使得每个具体类都可以有其重载方法,但是它们知道必须实现一个抽象方法consume(),而不需要实现consume(Food)

编辑


我可以解决上述问题而不使用重载,例如:

public class FruitStomach extends AbstractStomach {
    public static class Apple extends Food {}
    public static class Orange extends Food {}
    @Override
    public void consume(Food food) {
        if (food instanceof Apple) {
           ...
        } else if (food instanceof Orange) {
           ...
        } else {
           // throws an error
        }
    }
}

但这不是我想要的。我希望看到一个使用方法重载的解决方案。

在声明它们时,您忘记了单词“class”。 - Raman Sahasi
4
在Java中,重载方法是在编译时确定的,而不是运行时,因此如果调用者拥有某些“食物”但不知道它是什么类型(比如它被传递给它),就永远无法调用正确的consume重载方法。Java类型系统并不适合您所尝试的操作。 - David Conrad
在大约2分钟的时间内,获得了5个答案,真是太棒了。 - Nick Ziebert
1
我想看到一个使用方法重载的解决方案...抱歉,在Java中你不会看到这样的解决方案。在运行时决定执行哪个方法被称为多态性,正如这个网站所指出的那样,“多态性适用于覆盖,而不是重载”--这与David所说的重载方法在编译时选择而不是运行时相同。 - ajb
@STaefi 泛型不能提供多态性。也许在该帖子的情况下,泛型已经足够好了,但我们没有足够的代码来确定。 - ajb
显示剩余3条评论
5个回答

5

抽象类中的抽象方法不仅仅是“提供实现指导”的问题。抽象类允许您拥有可以是抽象类的任何子类的对象。也就是说,您可以有一个像这样的方法

public void evaluateDigestion(AbstractStomach stomach) {
}

你可以使用任何类型的Stomach对象来调用它,如FruitStomach、MeatStomach或DessertStomach等。在evaluateDigestion方法中,该方法可以调用stomach.consume(someKindOfFood)。由于这是抽象类定义的consume方法(在编译时,evaluateDigestion不知道类是什么),因此它是使用Food参数的consume方法。然后,在运行时,调用consume将“分派”到为具体类定义的consume(Food food)方法。这就是为什么必须为每个具体类定义一个带有Food参数的consume的原因。调度调用不会搜索具有Apple、Orange等参数的重载方法;Java不会这样做。(因此,你关于“已经被重载变量处理了”的说法是不正确的;重载和覆盖之间存在很大的区别,这可能会让新手感到困惑。)
所以你确实需要编写。
public void consume(Food food)

在每个具体类中实现。一种非常笨拙的实现方式是

public void consume(Food food) {
    if (food instanceof Apple) {
        consume((Apple)food);  // calls the overloaded version because you've use a cast to tell Java how to view the object
    } else if (food instanceof Orange) {
        consume((Orange)food);
    } else {
        throw ... // some exception that occurs when you try to eat the wrong kind of food
    }
}

更好的做法是在抽象类 Food 中定义一个抽象方法:
public abstract class Food {
    public void abstract consumedBy(AbstractStomach stomach);
}

AppleOrange等类中定义具体版本,然后像下面这样编写consume

public static void consume(Food food) {
    food.consumedBy(this);
}

然而,这并不能阻止试图让FruitStomach消化牛肉的尝试。您可能需要使用instanceof来确保食物是兼容的。(一种可能性:定义一个继承自FoodFruit类,并将AppleOrange定义为Fruit类的子类;然后您可以使用food instanceof Fruit来验证它是正确类型的食物。不过,可能有比这更好的设计模式,我一时想不到。)


喜欢你在这个主题上的知识。选择你的回答。 - user1589188

3
你可以使用泛型来(部分)解决你的问题:
AbstractStomach 定义了一个类型参数 T,声明它消化什么类型的食物:
public abstract class AbstractStomach<T extends Food> {
    public abstract void consume(T food);
}

接下来,您可以声明一种食物类型 Fruit 和相应的 FruitStomach:

public class Fruit extends Food {}
public class FruitStomach extends AbstractStomach<Fruit> {
    public static class Apple extends Fruit {}
    public static class Orange extends Fruit {}
    @Override
    public void consume(Fruit food) {}
}

还有类似的类MeatMeatStomach

public class Meat extends Food {}
public class MeatStomach extends AbstractStomach<Meat> {
    public static class Beef extends Meat {}
    @Override
    public void consume(Meat food) {}
}

是的,谢谢,但这只是将问题从consume(Food)推到consume(Fruit)。在其中,您仍然需要检查Fruit的实例是苹果还是橙子。 - user1589188
@user1589188 检查Fruit实例是苹果还是橙子仍然是必要的,但在consume(Fruit)中它总是一个Fruit而不是某种Meat。正如我所写的:它部分地解决了你的问题。 - Thomas Kläger

1
要重写一个方法,你需要具有完全相同的返回类型或子类型、名称和参数,因此你无法将“Food food”更改为“Orange orange”。
我认为你可以在具体类中在方法内部对需要操作的对象进行转换(或者像你使用 instanceof 一样)。或者更好的做法是创建一个对象:
Food orange = new Orange();
Food apple = new Apple(); 

因此,在橙色类中使用第一次方法覆盖,然后在苹果类中使用一个方法。

如果这不是您想要的解决方案,请评论,以便我可以尝试其他方法。


你的第一句话不正确。自Java 5以来,协变返回已被允许。重载方法可以返回一个方法重载的返回类型的子类型。 - David Conrad
但我认为使用 @Override 注释时,您需要使用实现接口或扩展类的完全相同的方法。我会找一些相关文档来支持这个论点,感谢您的建议。 - Mike
1
如果你有一个返回 T 的方法,你可以用一个返回 T2 的方法来覆盖它,前提是 T2T 的子类(自 Java 5 开始)。这是关于使用完全相同的方法的唯一例外。参数类型仍然必须相同(不允许使用子类)。 - ajb
谢谢ajb,我几分钟前看了文档并编辑了帖子。 - Mike

1
The above doesn't work as it is forcing me to implement also the generic consume(Food)

这是正确的行为。在抽象类AbstractStomach中的契约中,您同意,
任何扩展AbstractStomach的内容都将具有一个方法consume(Food)(任何食物), 然后在子类中,您强制其仅使用两种类型的食物,从而绕过了合同。

1
我更愿意看到一份合同,上面写着“至少一种食物”,而不是“任何食物” :) - user1589188

1
您面临的问题是,您的抽象方法签名需要传递一个名为Food的超类到consume方法中。每个该抽象类(因此其抽象方法)的实现都必须覆盖相同的签名。
例如:
   public FruitStomach extends AbstractStomach {
    public static class Apple extends Food {}
    public static class Orange extends Food {}
    @Override
    public void consume(Food food) {
      if (food instanceof Orange) {
        Orange orange = (Orange) food;
        // do smth. with orange     
      } else{
        // so smth with food
      }
    }

   public MeatStomach extends AbstractStomach {
      public static class Beef extends Food {}
      @Override
      public void consume(Food food) {
        Beef beef = (Beef) food;
        // do smth. with beef...
      }
    }

这样做是可行的。虽然代码不太干净,而且没有编译时验证你的代码是否正确地连接在一起。 但正如你所说,如果你在错误的输入食物上调用了错误的Stomach.consume()方法,可能会引发ClassCastException,但你可以处理。


是的,除了使用 instanceof 之外,我找不到其他替代方案。这并不是很优雅,但这就是我们所能做的。 - user1589188

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接