私はJavaプログラミングの初心者で、OOPのコツをつかもうとしています。
そこで、この抽象クラスを作成しました。
public abstract class Vehicle{....}
および2つのサブクラス:
public class Car extends Vehicle{....}
public class Boat extends Vehicle{....}
Car
とBoat
には、一般的ではない固有のフィールドとメソッドがいくつかあります(同じ名前を持たないため、Vehicleでそれらの抽象メソッドを定義できません)。
これでmainClassに新しいGarageをセットアップしました:
Vehicle[] myGarage= new Vehicle[10];
myGarage[0]=new Car(2,true);
myGarage[1]=new Boat(4,600);
次のようなCarに固有のフィールドの1つにアクセスしようとするまで、ポリモーフィズムに非常に満足していました。
boolean carIsAutomatic = myGarage[0].auto;
コンパイラはそれを受け入れません。私はキャストを使用してこの問題を回避しました:
boolean carIsAutomatic = ((Car)myGarage[0]).auto;
それは機能します...しかし、メソッドでは役に立ちません、フィールドだけです。できない意味
(Car)myGarage[0].doSomeCarStuff();
だから私の質問は-私は本当に私のガレージに何を持っていますか?私は直観を得ようとしているだけでなく、「舞台裏」で何が起こっているのかを理解しようとしています。
将来の読者のために、以下の回答の短い要約:
myGarage[]
にCar
がありますVehicle myGarage[]
など)に基づくデータ構造を介してアクセスする場合、「Vehicle」以外のメソッド/フィールドへのアクセスを許可しません。ガレージでCar
とBoat
を区別する必要がある場合は、異なる構造に格納する必要があります。
例えば:
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();
}
ポリモーフィズムの関心事は、getPriceAfterYears
でVehicle
を呼び出すことができることですwithout実装を気にすることです。
通常、ダウンキャストは欠陥のあるデザインの兆候です。実際のタイプを区別する必要がある場合は、車両をすべて一緒に保管しないでください。
注:もちろん、ここの設計は簡単に改善できます。これは、ポイントを示すための単なる例です。
あなたの質問に答えるために、あなたはあなたのガレージに正確に何があるかを知ることができます、あなたは以下をします:
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();
}
更新:以下のコメントからわかるように、この方法は単純な解決策としては問題ありませんが、特にガレージに膨大な数の車両がある場合、良い方法ではありません。ガレージが小さいままであることがわかっている場合にのみ使用してください。そうでない場合は、スタックオーバーフローで「Avoiding instanceof」を検索してください。複数の方法があります。
ベース型を操作する場合、パブリックメソッドとそのフィールドにのみアクセスできます。
拡張型にアクセスしたいが、それが格納されているベース型のフィールドがある場合(あなたの場合のように)、最初にキャストする必要があり、それからアクセスできます:
Car car = (Car)myGarage[0];
car.doSomeCarStuff();
または一時フィールドなしの短い:
((Car)myGarage[0]).doSomeCarStuff();
Vehicle
オブジェクトを使用しているため、キャストせずにそれらの基本クラスからのみメソッドを呼び出すことができます。したがって、ガレージでは、異なる配列またはより優れたリストのオブジェクトを区別することをお勧めします。配列は、Collection
ベースのクラスよりも処理の柔軟性がはるかに低いため、多くの場合、良いアイデアではありません。
ガレージに車両を保管すると定義したので、所有している車両の種類を気にしません。車両には、エンジン、ホイール、動くような動作などの共通の機能があります。これらの機能の実際の表現は異なる場合がありますが、抽象レイヤーでは同じです。抽象クラスを使用しました。これは、一部の属性、動作が両方のビークルでまったく同じであることを意味します。車両に共通の抽象的な機能があることを表明したい場合は、移動のようなインターフェイスを使用することは、車とボートによって異なることを意味する場合があります。両方ともポイント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を使用し、ボートはソナーを使用して現在地を特定します。
私はJavaプログラミングの初心者で、OOPのコツをつかもうとしています。
ちょうど私の2セント—多くの興味深いことがすでに述べられているので、私はそれを短くしようとします。しかし、実際には、2つの質問があります。 1つは「OOP」に関するもので、もう1つはJavaでの実装方法に関するものです。
まず第一に、はい、あなたはガレージで車をhave_しています。だからあなたの仮定は正しい。ただし、Javaは静的に型指定された言語です。そして、コンパイラの型システムは、対応する宣言によってのみ、さまざまなオブジェクトの型を「知る」ことができます。使用法ではありません。 Vehicle
の配列がある場合、コンパイラはそれだけを知っています。したがって、anyVehicle
で許可されている操作のみを実行することを確認します。 (つまり、Vehicle
宣言で表示されるメソッドおよび属性)。
明示的なキャスト(Car)
を使用することにより、"実際、このVehicle
がCar
であることを知っています"であることをコンパイラーに説明できます。コンパイラはあなたを信じます-in Java実行時にチェックがあったとしても、liedの場合、さらなるダメージを防ぐClassCastException
につながるかもしれません_ (C++のような他の言語は実行時にチェックしません-あなたは何をするかを知る必要があります)
最後に、本当に必要な場合は、実行時の型識別(つまり、instanceof
)を使用して、オブジェクトをキャストする前にオブジェクトの「実際の」型を確認します。しかし、これはほとんどJavaの悪い習慣と見なされます。
私が言ったように、これはOOPを実装するJava方法です。まったく違う クラス "動的言語"として広く知られている言語のファミリー、その実行時のみチェックは、オブジェクトで操作が許可されているかどうかを示します。これらの言語を使用すると、型システムを満たすために、すべての一般的なメソッドを(おそらく抽象的な)基本クラスに「移動」する必要はありません。これは ダックタイピング と呼ばれます。
あなたは執事に尋ねました:
ジャイブス、ジャワ島のガレージを覚えていますか?そこに駐車した最初の車両が自動かどうかを確認します。
怠zyなジーブスは言った:
しかし、それが自動または非自動にできない車両の場合はどうでしょうか?
それで全部です。
現実は静的に型付けされるよりもカモに型付けされるので、それだけではありません。だから私はジーブスが怠zyだと言った。
ここでの問題は、より基本的なレベルにあります:Vehicle
は、Garage
インターフェースが提供するものよりもVehicle
がそのオブジェクトについてもっと知る必要があるような方法でビルドしました。 Vehicle
クラスをGarage
の観点から(および一般的にVehicle
を使用するすべての観点から)試してビルドする必要があります。車両でどのようなことを行う必要がありますか?私の方法でそれらを可能にするにはどうすればよいですか?
たとえば、あなたの例から:
bool carIsAutomatic = myGarage[0].auto;
あなたのガレージは...理由で車両のエンジンについて知りたいですか?とにかく、これをCar
で公開する必要はありません。未実装のisAutomatic()
メソッドをVehicle
で公開し、Boat
でreturn True
として、Car
でreturn this.auto
として実装できます。
3つの値を持つEngineType
enum(HAS_NO_GEARS
、HAS_GEARS_AUTO_SHIFT
、HAS_GEARS_MANUAL_SHIFT
)を使用すると、ジェネリックVehicle
の実際の特性をコードで明確かつ正確に推論できます。 (とにかくバイクを扱うにはこの区別が必要でしょう。)
ガレージにはVehicleが含まれているため、Vehicleがあり、.autoはアクセスできないCarフィールドなので、動的にCarであるため、キャストが問題を引き起こさないように、コンパイラの静的制御ビューにボートでCarにキャストしようとすると、実行時に例外が発生します。
これは、Visitor
デザインパターンを適用するのに適した場所です。
このパターンの利点は、どこでも奇妙なキャストをしたり、無関係なメソッドを大量にスーパークラスに入れたりすることなく、スーパークラスの異なるサブクラスで無関係なコードを呼び出すことができることです。
これは、Visitor
オブジェクトを作成し、Vehicle
クラスに訪問者のaccept()
を許可することで機能します。
また、多くのタイプの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
を使用すると、Boat
の実装が呼び出されるかどうかに応じて、Car
またはvisit
で異なるコードを呼び出すことができます。また、Visitorの他の実装を作成して、同じ.visit()
パターンで異なるコードを呼び出すこともできます。
また、このメソッドを使用すると、instanceof
またはハッキングクラスのチェックが使用されないことに注意してください。クラス間で重複するコードは、メソッドvoid accept(Visitor)
のみです。
たとえば、3種類の具象サブクラスをサポートする場合は、その実装をVisitor
インターフェイスにも追加できます。
私は本当に他の人のアイデアをここにプールしています(そして私は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に問い合わせてください。トライステートEnumのおかげで、自動であるか、車でなくてもわかるようになりました。
もちろん、気にする車両ごとに異なる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でバスと車の両方を検査できますが、トランスミッションについてのみ質問できます。
さて、あなたはブール値があなたが気にするすべてではないと決めるかもしれません。その時点で、サポートされている状態と値の両方を公開する一般的な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;
}
}
設定継承クラスの車両レベルフィールドを適切な値に設定します。
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タイプを適切に解読したい場合は、いくつかのクラスレベルフィールドを設定する必要があります。 isCar
およびisBoat
を使用すると、プログラマーは、使用している車両の種類をよりよく理解できます。
Javaはタイプセーフな言語なので、Boat
sやCar
sのようにキャストされたデータを処理する前に、常にタイプチェックを行うのが最善です。
Javaを使用している場合は、リフレクションを使用して、関数が使用可能かどうかを確認して実行することもできます。
(何らかの問題を解決するために)プログラムで提示するオブジェクトのモデリングは1つのことであり、コーディングは別の話です。あなたのコードでは、配列を使用してガレージをモデル化することは本質的に不適切だと思います。配列はオブジェクトと見なされることはあまりありませんが、通常は言語の整合性の自己完結型のソートのためにappearになりますが、ただし、配列としての配列を拡張することはできません。
ガレージを表すクラスを正しくモデル化しても、「ガレージの車」の質問に答えるのに役立たないことを理解しています。ほんの一言。
コードに戻ります。 OOPにハングアップする以外に、シーンを作成するためのいくつかの質問が役立ちます。したがって、解決したい問題をよりよく理解するには(「ハングアップする」だけでなく、1つがあると仮定します):
carIsAutomatic
を理解したいのは誰ですか?carIsAutomatic
が与えられた場合、誰が、または何がdoSomeCarStuff
を実行しますか?検査官か、自動変速車の運転方法などしか知らない人かもしれませんが、ガレージの観点からは、それが知っているのは車両を保持しているだけなので、(このモデルでは)この検査官の責任ですまたは、ドライバーが車かボートかを判断します。この時点で、シーン内の同様のタイプの*アクター*を表す別のクラスのクラスの作成を開始できます。解決する問題に応じて、本当に必要な場合は、ガレージをスーパーインテリジェントシステムにモデル化して、通常のガレージではなく自動販売機のように動作させることができます。 「ボート」。ユーザーがボタンを押して、好きなように車やボートを手に入れることができます。これにより、この超インテリジェントなガレージは、ユーザー(車やボート)をユーザーに提示する責任を負います。この即興に追従するために、ガレージは車両を受け入れる際に何らかの記帳を必要とする場合があります。誰かが情報を提供する必要がある場合など、これらの責任はすべて単純なMainクラスを超えます。
これだけ言って、OOプログラムをコーディングするためのボイラープレートと一緒にすべてのトラブルを確かに理解しています。特に、解決しようとする問題が非常に単純な場合、OOは実際に実行可能な方法です他の多くの問題を解決します。私の経験から、ユースケースを提供するいくつかの入力で、人々はオブジェクトが相互作用するシーンを設計し始め、それらをクラス(およびJavaのインターフェース)に分類し、あなたのMainクラスをbootstrap worldにします。