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

我是Java编程的新手,试图掌握OOP的诀窍。

所以我创建了这个抽象类:

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,要么使用更具体的(派生的)基于类型的结构
    3. 李< / ol > < / >
    4. 在许多情况下,组合和/或接口将是继承的更好选择。可能是我下一个问题的主题…
    5. 如果你有时间浏览答案,还有很多其他好的见解。
27744 次浏览

你的车库包含车辆,所以编译器静态控制视图中你有一个车辆,因为。auto是一个Car字段,你不能访问它,动态地它是一个Car,所以转换不会产生一些问题,如果它是一个船,你试图将转换为Car会在运行时引发异常。

如果对基类型进行操作,则只能访问它的公共方法和字段。

如果你想访问扩展类型,但有一个基类型的字段存储它(在你的情况下),你首先必须强制转换它,然后你可以访问它:

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

或更短,没有温度字段:

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

由于使用的是Vehicle对象,所以只能从它们的基类调用方法,而不能强制转换。因此,对于你的车库来说,区分不同数组(或更好的列表)中的对象可能是明智的——数组通常不是一个好主意,因为它在处理方面远不如基于__abc1的类灵活。

您定义了您的车库将存储车辆,因此您不关心您拥有什么类型的车辆。这些车辆有共同的特征,比如引擎、车轮、移动等行为。 这些特征的实际表示可能有所不同,但在抽象层上是相同的。 你使用了抽象类,这意味着某些属性,行为在两个载体中是完全相同的。如果你想表达你的车辆有共同的抽象特征,那么使用接口,比如移动可能意味着不同的汽车和船。两者都可以从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 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();
}

更新:正如你可以从下面的评论中读到的,这种方法对于简单的解决方案是可以的,但它不是一个好的实践,特别是如果你的车库里有大量的车辆。所以,只有在你知道车库不会太大的情况下才使用它。如果不是这样,在堆栈溢出时搜索“avoid instanceof”,有多种方法可以做到这一点。

如果你需要在车库中区分CarBoat,那么你应该将它们存储在不同的结构中。

例如:

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);
}

每个Vehicle都有一个价格,所以它可以放在Vehicle抽象类中。

然而,决定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

通常,下倾是设计缺陷的标志:如果需要区分车辆的实际类型,请不要将所有车辆存储在一起。

注:当然这里的设计可以很容易地改进。这只是一个例子来证明这一点。

你的问题在一个更基本的层面上:你以这样的方式构建VehicleGarage需要知道更多关于它的对象的信息,而不是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的实际特征。(无论如何,你需要区分摩托车。)

我是Java编程的新手,试图掌握OOP的诀窍。

这只是我的观点——我将尽量简短,因为很多有趣的事情已经说过了。但实际上,这里有两个问题。一个是关于“面向对象”的,一个是关于它如何在Java中实现的。

首先,是的,你在你的车库里有一辆车。所以你的假设是对的。但是,Java是一种静态类型语言。而编译器中的类型系统只能通过对应的宣言“知道”你的各种对象的类型。不是他们的习惯。如果你有一个Vehicle数组,编译器只知道它。因此,它将检查你只执行任何 Vehicle上允许的操作。(换句话说,方法属性Vehicle声明中可见)。

你可以通过显式强制转换(Car)向编译器解释"你实际上知道这个__ABC0是一个Car"。编译器会相信你——即使在Java中在运行时有一个检查,如果你撒了谎 (其他语言如c++不会在运行时检查-你必须知道你在做什么),这可能会导致一个ClassCastException,防止进一步的损害

最后,如果你确实需要,你可以依靠运行时类型标识(即:instanceof)在尝试强制转换之前检查对象的“真实”类型。但在Java中,这通常被认为是一种糟糕的实践。

正如我所说,这是实现OOP的Java方式。有完全不同的 家庭语言,广泛称为“动态语言”,即只在运行时检查是否允许对对象进行操作。使用这些语言,您不需要将所有公共方法“上移”到某些(可能是抽象的)基类来满足类型系统。这被称为duck typing

你问管家:

吉夫斯,还记得我在爪哇岛的车库吗?去看看第一辆停在那里的车是不是自动挡的。

懒惰的吉夫斯说:

但是,先生,如果这是一辆既不能自动也不能非自动的车呢?

这是所有。

好吧,这还不是全部,因为现实中更多的是鸭子类型而不是静态类型。这就是为什么我说吉夫斯很懒。

我真的只是汇集了其他人的想法(我不是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就行了。多亏了三状态枚举,你现在可以知道它是自动的,甚至它不是一辆车。

当然,你需要为你关心的每辆车使用不同的vehicleinspector。你已经把哪个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可以检查公共汽车和汽车,但它只能询问有关变速器的信息。

现在,您可能认为布尔值并不是您所关心的全部。在这一点上,你可能更喜欢使用泛型的Supported类型,它同时暴露了受支持的状态和值:

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人,所以上面的一些语法可能是错误的,但这些概念应该是成立的。然而,在没有测试之前,不要在任何重要的地方运行上面的代码。

创建车辆级别字段,将帮助使每个单独的车辆更明显。

public abstract class Vehicle {
public final boolean isCar;
public final boolean isBoat;


public Vehicle (boolean isCar, boolean isBoat) {
this.isCar  = isCar;
this.isBoat = isBoat;
}
}

将继承类中的Vehicle level字段设置为适当的值。

public class Car extends Vehicle {
public Car (...) {
super(true, false);
...
}
}


public class Boat extends Vehicle {
public Boat (...) {
super(false, true);
...
}
}

实现使用车辆级别字段正确地破译车辆类型。

boolean carIsAutomatic = false;


if (myGarage[0].isCar) {
Car car = (Car) myGarage[0];
car.carMethod();
carIsAutomatic = car.auto;
}


else if (myGarage[0].isBoat) {
Boat boat = (Boat) myGarage[0];
boat.boatMethod();
}

因为你告诉编译器车库中的所有东西都是Vehicle,所以你只能使用Vehicle类级别的方法和字段。如果你想正确地破译车辆类型,那么你应该设置一些类级字段,例如isCarisBoat,这将使程序员更好地理解你正在使用的车辆类型。

Java是一种类型安全的语言,所以在处理像__abc0和__abc1这样的类型转换数据之前,最好总是进行类型检查。

如果你使用Java,可以使用反射来检查一个函数是否可用并执行它

这是应用Visitor设计模式的好地方。

这种模式的美妙之处在于,您可以在一个超类的不同子类上调用不相关的代码,而不必到处执行奇怪的强制转换,也不必在超类中放入大量不相关的方法。

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

你也可以创建许多类型的Visitor,并使用相同的方法调用不相关的代码,只是不同的Visitor实现,这使得这种设计模式在创建干净的类时非常强大。

举个例子:

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允许你在BoatCar上调用不同的代码,这取决于调用的是visit的哪个实现。你也可以创建访问者的其他实现,用相同的.visit()模式调用不同的代码。

还要注意,使用此方法时,不会使用instanceof或任何hack类检查。类之间唯一重复的代码是方法void accept(Visitor)

例如,如果你想支持3种类型的具体子类,你也可以将该实现添加到Visitor接口中。

建模你想在程序中呈现的对象(为了解决一些问题)是一回事,编码是另一回事。在你的代码,我认为本质上是不合适的模型车库使用数组。数组不应该经常被认为是对象,尽管它们出现被认为是对象,通常是为了一种语言的自包含性完整性,并提供一些熟悉性,但数组作为一种类型实际上只是特定于计算机的东西,恕我直言,尤其是在Java中,在那里你不能扩展数组。

我知道正确地建模一个类来表示车库并不能帮助回答“车库中的汽车”的问题;只是一个建议。

回到代码。除了一些面向对象的问题之外,一些问题将有助于创建一个场景,从而更好地理解你想要解决的问题(假设有一个问题,而不仅仅是“得到一些问题”):

  1. 谁或什么想要理解carIsAutomatic?
  2. 给定carIsAutomatic,谁或什么将执行doSomeCarStuff?

它可能是某个检查员,或者某个只知道如何驾驶自动变速器汽车的人,等等,但从车库的角度来看,它所知道的只是它拥有一些车辆,因此(在这个模型中)这个检查员或司机有责任分辨它是一辆汽车还是一艘船;此时,你可能想要开始创建另一堆类来表示场景中相似类型的*actor。取决于要解决的问题,如果你真的需要,你可以模型车库是一个超级智能系统像一个自动售货机,而不是普通的车库,说有一个按钮“汽车”,另一个说“船”,这样人们可以按下按钮来获得他们想要一辆车或一条船,这反过来使得这个超级智能车库负责告诉(一辆车或船)应该呈现给用户;为了遵循这种即兴,车库在接受车辆时可能需要一些簿记,有人可能必须提供信息,等等,所有这些责任都超出了简单的主要类。

说了这么多,当然我理解编写面向对象程序的所有麻烦,以及样板,特别是当它试图解决的问题非常简单时,但面向对象确实是解决许多其他问题的可行方法。根据我的经验,有了一些提供用例的输入,人们开始设计对象如何相互交互的场景,将它们分类为类(以及Java中的接口),然后使用类似主要类的东西来引导世界