我在车库里真的有一辆车吗?

258

我是一个Java编程的新手,正在努力掌握面向对象编程。

因此我建立了这个抽象类:

public abstract class Vehicle{....}

还有2个子类:

public class Car extends Vehicle{....}
public class Boat extends Vehicle{....}

CarBoat还拥有一些独特的字段和方法,它们与普通的内容不同(没有相同名称,因此我无法在Vehicle中定义一个抽象方法来涵盖它们)。

现在在mainClass中,我已经设置好了我的新车库:

Vehicle[] myGarage= new Vehicle[10];
myGarage[0]=new Car(2,true);
myGarage[1]=new Boat(4,600);

在我尝试访问Car独有的字段时,我对多态感到非常满意。

boolean carIsAutomatic = myGarage[0].auto;
编译器不接受那个。我使用转换解决了这个问题:
boolean carIsAutomatic = ((Car)myGarage[0]).auto;

那可以用,但只适用于字段,对方法是无效的。这意味着我不能做到

(Car)myGarage[0].doSomeCarStuff();
所以我的问题是 - 我的车库里真正有什么?我试图获得直觉,并了解“幕后发生了什么”。
为了未来读者的利益,下面是答案的简短摘要:
  1. 是的,在myGarage[]中有一辆Car
  2. 作为静态类型语言,如果通过基于Vehicle超类(例如Vehicle myGarage [])的数据结构访问非“ Vehicle”的方法/字段,则Java编译器将不会提供访问权。
  3. 至于如何解决,以下是两种主要方法:
    1. 使用类型转换,这将减轻编译器的担忧并将任何设计错误留给运行时
    2. 需要转换的事实表明设计存在缺陷。如果我需要访问非Vehicle功能,则不应该将汽车和船存储在基于Vehicle的数据结构中。要么使所有这些功能属于Vehicle,要么使用更具体(派生)类型的结构
  4. 在许多情况下,组合和/或接口可能是继承的更好替代方案。可能是我下一个问题的主题...
  5. 此外,还有许多其他很好的见解,如果有时间浏览答案的话。

142
尝试执行 ((Car)myGarage[0]).doSomeCarStuff(); - Andrew Stubbs
5
如果你告诉编译器你有一个Vehicle[],为什么它会期望你实际上有一辆Car或者一艘Boat(或者任何其他特定的派生类型)。 它相信你了——你有一个Vehicles数组(或者可能是继承自Vehicle的东西,但你明确声明你不想知道那些细节)。 - Eric Towers
1
你使用的Java版本是哪个?boolean carIsAutomatic = (Car)myGarage[0].auto对我来说无法编译。 - Vitalii Fedorenko
17
更重要的是,如果您不想将它们视为“车辆”,为什么要将“汽车”塞入“Vehicle []”中呢? - cHao
3
@cHao提出了一个很好的问题。如果您想检查一个“车辆”是否是自动的,而不是先检查它是否是汽车,那么您可以将“auto”或更好的是“isAuto()”移到“Vehicle”类中,并且对于“Boat”,它始终返回“false”。或者将“isAuto()”推入仅由某些“Vehicle”实现的接口中,称为“CanBeAutomatic”。 - joeytwiddle
显示剩余7条评论
13个回答

147
如果您需要在车库中区分汽车和船,则应将它们存储在不同的结构中。
例如:
public class Garage {
    private List<Car> cars;
    private List<Boat> boats;
}

然后您可以定义特定于船或特定于汽车的方法。

那么为什么要使用多态?

假设 Vehicle 是这样的:

public abstract class Vehicle {
   protected int price;
   public getPrice() { return price; }
   public abstract int getPriceAfterYears(int years);
}

每一辆车辆都有一个价格,因此可以将其放入车辆抽象类中。

然而,确定n年后价格的公式取决于车辆类型,因此需要由实现类来定义。例如:

public Car extends Vehicle {
    // car specific
    private boolean automatic;
    @Override
    public getPriceAfterYears(int years) {
        // losing 1000$ every year
        return Math.max(0, this.price - (years * 1000));  
    }
}

Boat类可能会有另一个定义getPriceAfterYears的方法以及特定的属性和方法。

因此,现在回到Garage类中,您可以定义:

// car specific
public int numberOfAutomaticCars() {
    int s = 0;
    for(Car car : cars) {
        if(car.isAutomatic()) {
            s++;
        }
    }
    return s;
}
public List<Vehicle> getVehicles() {
    List<Vehicle> v = new ArrayList<>(); // init with sum
    v.addAll(cars);
    v.addAll(boats);
    return v;
}
// all vehicles method
public getAveragePriceAfterYears(int years) {
    List<Vehicle> vehicules = getVehicles();
    int s = 0;
    for(Vehicle v : vehicules) {
        // call the implementation of the actual type!
        s += v.getPriceAfterYears(years);  
    }
    return s / vehicules.size();
}

多态技术的优势在于能够在不关心实现细节的情况下,对一个车辆(Vehicle)调用getPriceAfterYears方法。

通常来说,向下转型是设计上的缺陷表现:如果你需要区分不同种类的车辆,请勿将它们统一存储。

注:当然,这里的设计可以很容易地改进。这只是为了演示这些观点而提供的示例。


12
为什么需要多态性?谁说我们需要它?我家的汽车肯定不知道它值多少钱,这是一些汽车市场应用程序的任务。我想说的是,价格可能不是最好的例子,也许我们应该使用组合而不是继承。 - Esben Skov Pedersen
1
@T-Rex 绝对不是这样的!有些情况下,一个基类可以独立存在,但你可能也想要继承它。以“车辆”为例,你可能有一艘“船”停在你的车库里,它拥有你操作船只所需的所有信息。但是你的邻居可能有一艘“滑雪艇”,它可能还有一个关于滑雪绳长度的方法,等等。 - Ogre Psalm33
30
在这种情况下,我不认为使用向下转型有什么问题。如果你的车库有很多停车位,每个停车位可以容纳一辆汽车或一艘船,那么OP的设计是完全合理的。这里的设计,使用分开的汽车和船的列表意味着每当你添加一种新类型的车辆(摩托车、拖车等)时,你都必须更新代码,这显然比强制转换更糟糕。 - Gabe
9
使用继承和多态的主要原因是:大多数情况下,您不关心汽车和船之间的区别。(如果这不是真的,那么请使用单独的列表) - user253751
4
@Gabe: public function canDrive(Vehicle vehicle) { for (Skill skill : skills) { if (vehicle.canBeDrivenWith(skill)) { return true; } } return false; }. 现在只有车辆需要知道它的传动方式。现在你的代客停车员可以把拖车作为一项技能,而你不必完全换另一辆车了--当需要时,他们会直接使用拖车技能来牵引拖车/船,而不是试图启动并驾驶它。 - cHao
显示剩余8条评论

85

回答你的问题,你可以按照以下步骤查看车库的具体内容:

Vehicle v = myGarage[0];

if (v instanceof Car) {
   // This vehicle is a car
   ((Car)v).doSomeCarStuff();
} else if(v instanceof Boat){
   // This vehicle is a boat
   ((Boat)v).doSomeBoatStuff();
}

更新:正如您可以从下面的评论中读到的那样,这种方法对于简单解决方案来说是可以的,但如果您的车库里有大量车辆,则不是一个好的做法。因此,只有在您知道车库规模不会太大时才使用它。如果不是这种情况,请在堆栈溢出上搜索“避免使用instanceof”,有多种方法可以实现。


105
这里需要注意的一点是:如果你发现自己经常使用instanceof,那么你应该退后一步,看看是否可以以更多态的方式重写代码。 - Keppil
1
@Keppil 你说得完全正确,但也许这样发布是为了演示目的,向OP展示他拥有哪些类,这是可以的。 - Alexander Rühl
2
@T-Rex:你已经从@Keppil那里得到了答案 - 不是的。如果可能的话,请避免使用instanceof。上面的代码很好地证明了这一点,但不适用于常见用途。 - Alexander Rühl
4
无论如何,我建议在那里至少加上一个 else { throw new UnsupportedVehicleError(v); } ,除非你确信不做任何处理是处理除汽车或船只以外的所有车辆的正确方式。 - Ilmari Karonen
5
如果汽车可以做一些汽车的事情,船也可以做一些船的事情,那么你能不能说“交通工具”都是在做一些事情呢?你知道它是一个交通工具,每种类型的交通工具都会做一些事情(可能还有其他的东西)。你不一定需要知道这些具体是什么,因为交通工具本身就能处理。车库并不关心是汽车开出去了还是船被拖走了,它只知道现在有一个空位了。 - ssube
显示剩余3条评论

22

如果您操作基本类型,只能访问其公共方法和字段。

如果您想访问扩展类型,但是有一个存储在基本类型中的字段(就像您的情况一样),则首先必须对其进行强制转换,然后才能访问它:

Car car = (Car)myGarage[0];
car.doSomeCarStuff();

或者更短,不需要临时字段:

((Car)myGarage[0]).doSomeCarStuff();

因为您正在使用Vehicle对象,所以您只能在未进行转换的情况下调用它们的基类方法。因此,对于您的车库,最好将不同类型的对象区分到不同的数组或更好的列表中 - 数组通常不是一个好主意,因为它比基于Collection的类处理起来要不灵活得多。


为了简单起见,我会在每个类中重写ToString()方法并返回具体类型作为字符串,然后使用switch()来决定如何处理每个项目。 - Captain Kenpachi
3
我反对-因为toString()不是用于该用例,switch将是一个脆弱的结构,因为这意味着期望每个子类也这样做。 - Alexander Rühl

13

您定义了车库将储存车辆,因此您不关心您有哪种类型的车辆。这些车辆具有共同的特征,例如发动机、轮子和移动等行为。实际上,这些特征的表示可能不同,但在抽象层面上是相同的。您使用了抽象类,这意味着某些属性和行为对两种车辆完全相同。如果您想表达您的车辆具有共同的抽象特征,则使用接口,例如移动可能因汽车和船而异。两者都可以从A点到B点,但方式不同(在轮子上或在水上 - 因此实现将有所不同)。因此,您在车库中拥有行为相同的车辆,而且您不关心它们的具体特征。

回答评论:

接口指的是描述如何与外界通信的契约。在契约中,您定义了您的交通工具可以移动,可以操纵,但您不描述它将如何实际工作,这在实现中描述。通过抽象类,您可以拥有一些函数,其中共享某些实现,但您还有一些函数,您不知道如何实现。

使用抽象类的一个例子:

    abstract class Vehicle {

    protected abstract void identifyWhereIAm();
    protected abstract void startEngine();
    protected abstract void driveUntilIArriveHome();
    protected abstract void stopEngine();

    public void navigateToHome() {
        identifyWhereIAm();
        startEngine();
        driveUntilIArriveHome();
        stopEngine();
    } 
}

你将对每辆车使用相同的步骤,但步骤的实施因车辆类型而异。汽车可能会使用GPS,船只可能会使用声纳来确定其位置。


谢谢。准确来说,我没有详细说明我在这里做的完整练习。Vehicle有一个抽象方法steer(),在CarBoat中实现不同。它工作得很好。Vehicle还实现了moveable并实现了move()方法,对于CarBoat都很有效。另一个问题是:使用抽象与使用接口之间的区别是什么 - 除了我可以拥有一个独立于类的方法来移动可移动对象的事实? - T-Rex
哦,太好了,我不知道抽象化可以这样工作。非常感谢! - T-Rex
@T-Rex 有些编程语言(如Java)限制一个子类只能继承自一个超类(在这种情况下是抽象父类),但是你可以在一个子类中实现多个接口。 - Raven Dreamer

13

我是一个Java编程新手,试图掌握面向对象编程。

我只想简单地陈述一下 —— 因为已经有很多有趣的东西被说过了。但事实上,这里有两个问题。一个是关于“面向对象编程”,另一个是如何在Java中实现它。

首先,是的,你的车库里确实一辆汽车。所以你的假设是正确的。但是,Java是一种静态类型的语言。而编译器中的类型系统只能通过它们对应的声明来“知道”各种对象的类型。而不能通过它们的使用来确定。如果你有一个Vehicle数组,编译器只知道这个信息。因此它会检查你只对任何Vehicle允许的操作进行操作。(换句话说,方法属性Vehicle声明中可见)。

你可以通过使用显式转换(Car)来告诉编译器“实际上你知道这个Vehicle是一辆Car。即使在Java中运行时会有一个检查,如果你撒谎了可能会导致ClassCastException从而防止进一步的损害(其他语言如C++不会在运行时进行检查——你必须知道你在做什么)。

最后,如果你真的需要,你可以依靠运行时类型识别(即:instanceof)来检查对象的“真实”类型,然后再尝试将其转换。但这在Java中被认为是一个不好的实践。

正如我所说,这是实现面向对象编程的Java方式。还有一整个不同语言家族称为"动态语言",它们只会在运行时检查操作是否允许在对象上执行。使用这些语言,您无需将所有公共方法“移动”到某个(可能是抽象的)基类中以满足类型系统。这被称为鸭子类型

实际上,某些静态类型语言也支持鸭子类型。例如,ML家族中的许多语言和Go语言都支持鸭子类型。 - user
@user 显然,这是过于简化了。更进一步地说,一些像Groovy这样的动态语言是基于JVM的,这证明了如果需要的话,Java具有成为动态语言所需的所有必要“东西”。关于ML和Go,我不会就类型推断或结构化类型是否是鸭子类型展开辩论。这里的重点只是说,“OOP”和“Java(实现OOP的方式)”是两回事。如果OP最初的直觉是正确的,那么它将与Java OOP模型发生冲突。 - Sylvain Leroux

9
你向你的管家提出了问题:
Jeeves,请记得我在爪哇岛上的车库吗?去检查一下那里停的第一辆车是否是自动的。
懒惰的 Jeeves 说:
但是先生,如果它是一辆不能自动或非自动的车呢?
就这些。

8
你的问题在更根本的层面上:你以这样一种方式构建了Vehicle,以至于Garage需要知道比Vehicle接口透露的更多关于它的对象的信息。你应该尝试从Garage的角度(以及通常来自使用Vehicle的所有事物的角度)构建Vehicle类:他们需要用他们的车做什么?如何通过我的方法使这些事情成为可能?
例如,根据你的例子:
bool carIsAutomatic = myGarage[0].auto;

您的车库想了解某辆车的发动机情况,出于某些原因?无论如何,这并不需要仅由Car来公开。您仍然可以在Vehicle中公开未实现的isAutomatic()方法,然后在Boat中将其实现为return True,在Car中将其实现为return this.auto

最好有一个三值EngineType枚举(HAS_NO_GEARSHAS_GEARS_AUTO_SHIFTHAS_GEARS_MANUAL_SHIFT),它将使您的代码能够清晰而准确地推断出通用Vehicle的实际特征。(无论如何,您需要这种区别来处理摩托车。)


如果车库需要重新安排车辆,各种车辆可能始终、从不或有时(例如取决于值班服务员是否会开手动挡)需要拖车。只能由拖车移动的车辆应该停放在可以在拖车时到达的区域;手动挡车辆不应该停放在会“堵住”自动挡车辆的地方。我建议Vehicle应包括一个方法,指示它是否只能由拖车、手动挡驾驶员或...移动。 - supercat
请注意,这种能力不一定取决于对象的“类型”;即使汽车的发动机抛锚了,它仍然是一辆“汽车”,但是GetRequirementsForMotion()会报告它需要拖车。 - supercat
@supercat: 车库不应关心停车员的驾驶能力--这只会导致每当停车员出现新的障碍时,就会引发一系列变化。:P 相反,由于停车员显然是一个具有自己属性的实体,所以它应该是一个对象。车库应该告诉停车员车辆已经到达,并让停车员将车辆放在停车位上(必要时移动其他汽车)。 - cHao
@supercat:关于建议...我实际上会建议更通用的东西——一种标记车辆具有特定属性并查询它们的方法。您可以使用表示“需要拖车”,“手动挡”等的值。子类可以通过将属性映射到属性来响应查询——虽然有点不好看,但至少将丑陋隔离在调用者之外。(尽管这样做可能会完全取消对不同的CarBoat等类型的需求,这取决于这些类还需要做什么。) - cHao
@supercat 这可能是我说的一些傻话,如果您觉得不对,请随意忽略。但是请查阅thatwhich的限定性和非限定性使用方法,或者简单来说,请始终使用that,除非明显需要使用which。这是本人发布的公共服务通告! - ErikE

7

这是一个很好的应用访问者设计模式的地方。

这个模式的美妙之处在于,您可以在超类的不同子类上调用不相关的代码,而无需在各处进行奇怪的转换或将大量不相关的方法放入超类中。

这通过创建一个访问者对象并允许我们的车辆accept()访问者来实现。

您还可以创建许多类型的访问者并使用相同的方法调用不相关的代码,只需使用不同的访问者实现即可。这使得在创建干净的类时,这种设计模式非常强大。

例如,以下是演示:

public class VisitorDemo {

    // We'll use this to mark a class visitable.
    public static interface Visitable {

        void accept(Visitor visitor);
    }

    // This is the visitor
    public static interface Visitor {

        void visit(Boat boat);

        void visit(Car car);

    }

    // Abstract
    public static abstract class Vehicle implements Visitable {

            // NO OTHER RANDOM ABSTRACT METHODS!

    }

    // Concrete
    public static class Car extends Vehicle {

        public void doCarStuff() {
            System.out.println("Doing car stuff");
        }

        @Override
        public void accept(Visitor visitor) {
            visitor.visit(this);
        }

    }

    // Concrete
    public static class Boat extends Vehicle {

        public void doBoatStuff() {
            System.out.println("Doing boat stuff");
        }

        @Override
        public void accept(Visitor visitor) {
            visitor.visit(this);
        }

    }

    // Concrete visitor
    public static class StuffVisitor implements Visitor {

        @Override
        public void visit(Boat boat) {
            boat.doBoatStuff();
        }

        @Override
        public void visit(Car car) {
            car.doCarStuff();
        }
    }

    public static void main(String[] args) {
        // Create our garage
        Vehicle[] garage = {
            new Boat(),
            new Car(),
            new Car(),
            new Boat(),
            new Car()
        };

        // Create our visitor
        Visitor visitor = new StuffVisitor();

        // Visit each item in our garage in turn
        for (Vehicle v : garage) {
            v.accept(visitor);
        }
    }

}

正如您所看到的,StuffVisitor 允许您在调用不同的 visit 实现时,在 BoatCar 上调用不同的代码。您还可以创建其他 Visitor 的实现,以相同的 .visit() 模式调用不同的代码。
此外,请注意使用此方法,没有使用 instanceof 或任何 hacky 类检查。类之间唯一重复的代码是方法 void accept(Visitor)
如果您想支持三种具体子类,例如,您只需将该实现添加到 Visitor 接口中即可。

7

你的车库里有车辆,因此编译器的静态控制视图认为你有一辆车。由于.auto是一个Car字段,所以你无法访问它,动态地它是一辆汽车,因此强制转换不会引起问题。如果它是一艘船,并尝试将其转换为汽车,则会在运行时引发异常。


6

我只是在这里汇集了其他人的想法(我不是Java专家,所以这只是伪代码而不是实际代码),但在这个编造的例子中,我会将我的汽车检查方法抽象成一个专门的类,该类只知道汽车并且只在查看车库时关心汽车:

abstract class Vehicle { 
    public abstract string getDescription() ;
}

class Transmission {
    public Transmission(bool isAutomatic) {
        this.isAutomatic = isAutomatic;
    }
    private bool isAutomatic;
    public bool getIsAutomatic() { return isAutomatic; }
}

class Car extends Vehicle {
    @Override
    public string getDescription() { 
        return "a car";
    }

    private Transmission transmission;

    public Transmission getTransmission() {
        return transmission;
    }
}

class Boat extends Vehicle {
    @Override
    public string getDescription() {
        return "a boat";
    }
}

public enum InspectionBoolean {
    FALSE, TRUE, UNSUPPORTED
}

public class CarInspector {
    public bool isCar(Vehicle v) {
        return (v instanceof Car);
    }
    public bool isAutomatic(Car car) {
        Transmission t = car.getTransmission();
        return t.getIsAutomatic();
    }
    public bool isAutomatic(Vehicle vehicle) {
        if (!isCar(vehicle)) throw new UnsupportedVehicleException();
        return isAutomatic((Car)vehicle);
    }
    public InspectionBoolean isAutomatic(Vehicle[] garage, int bay) {
        if (!isCar(garage[bay])) return InspectionBoolean.UNSUPPORTED;
        return isAutomatic(garage[bay]) 
             ? InspectionBoolean.TRUE
             : InspectionBoolean.FALSE;
    }
}

重点是,当你询问汽车的变速器时,已经决定了你只关心汽车。所以,只需询问CarInspector。由于三州枚举,现在您可以知道它是自动还是非汽车。
当然,您需要针对每辆您关心的车辆使用不同的VehicleInspectors。而且,您已经将实例化VehicleInspector的问题推到了链上。
因此,您可能希望查看接口。
将抽象的getTransmission转移到接口中(例如HasTransmission)。这样,您就可以检查车辆是否具有传动系统,或编写一个TransmissionInspector:
abstract class Vehicle { }

class Transmission {
    public Transmission(bool isAutomatic) {
        this.isAutomatic = isAutomatic;
    }
    private bool isAutomatic;
    public bool getIsAutomatic() { return isAutomatic; }
}

interface HasTransmission { 
    Transmission getTransmission(); 
}

class Car extends Vehicle, HasTransmission {
    private Transmission transmission;

    @Override
    public Transmission getTransmission() {
        return transmission;
    }
}

class Bus extends Vehicle, HasTransmission {
    private Transmission transmission;

    @Override
    public Transmission getTransmission() {
        return transmission;
    }
}

class Boat extends Vehicle { }

enum InspectionBoolean {
    FALSE, TRUE, UNSUPPORTED
}

class TransmissionInspector {
    public bool hasTransmission(Vehicle v) {
        return (v instanceof HasTransmission);
    }
    public bool isAutomatic(HasTransmission h) {
        Transmission t = h.getTransmission();
        return t.getIsAutomatic();
    }
    public bool isAutomatic(Vehicle v) {
        if (!hasTranmission(v)) throw new UnsupportedVehicleException();
        return isAutomatic((HasTransmission)v);
    }
    public InspectionBoolean isAutomatic(Vehicle[] garage, int bay) {
        if (!hasTranmission(garage[bay])) return InspectionBoolean.UNSUPPORTED;
        return isAutomatic(garage[bay]) 
             ? InspectionBoolean.TRUE
             : InspectionBoolean.FALSE;
    }
}

现在你说,你只关心传输,不管车辆,所以可以询问TransmissionInspector。无论是公交车还是汽车都可以由TransmissionInspector检查,但只能询问有关传输的问题。
现在,您可能会发现布尔值并不是您关心的所有内容。此时,您可能更喜欢使用通用的支持类型,该类型同时显示支持状态和值:
class Supported<T> {
    private bool supported = false;
    private T value;

    public Supported() { }
    public Supported(T value) { 
        this.isSupported = true;
        this.value = value; 
    }

    public bool isSupported() { return supported; }
    public T getValue() { 
        if (!supported) throw new NotSupportedException();
        return value;
    }
}

现在你的检查器可能被定义为:
class TransmissionInspector {
    public Supported<bool> isAutomatic(Vehicle[] garage, int bay) {
        if (!hasTranmission(garage[bay])) return new Supported<bool>();
        return new Supported<bool>(isAutomatic(garage[bay]));
    }

    public Supported<int> getGearCount(Vehicle[] garage, int bay) {
        if (!hasTranmission(garage[bay])) return new Supported<int>();
        return new Supported<int>(getGearCount(garage[bay]));
    }
}

如我所说,我不是一个Java程序员,因此上述某些语法可能有误,但是概念应该是正确的。然而,在没有进行测试之前,请不要在任何重要的地方运行上述代码。


这个概念是正确的,但它违反了每一个面向对象编程原则。你需要看一下访问者模式的实现方式,这将消除对 InspectionBoolean 枚举的需求 - 它并不是一个布尔值。 - Matthieu Bertin
@Matt - 注意到 InspectionBoolean 的语义了 -- 当时我只是想不出更好的名称... - jimbobmcgee

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