Java では演算子のオーバーロードが提供されないのはなぜですか?
-
09-06-2019 - |
質問
C++ から Java に至るまでの明らかな未解決の疑問は、なぜ Java には演算子のオーバーロードが含まれていないのかということです。
そうじゃない Complex a, b, c; a = b + c;
よりもはるかに簡単です Complex a, b, c; a = b.add(c);
?
これには既知の理由、有効な引数はありますか? ない 演算子のオーバーロードを許可しますか?その理由は恣意的なものですか、それとも時間の経過によるものですか?
解決
によって参照されるオブジェクトの前の値を上書きしたいと仮定します。 a
, の場合、メンバー関数を呼び出す必要があります。
Complex a, b, c;
// ...
a = b.add(c);
C++ では、この式はコンパイラに、スタック上に 3 つのオブジェクトを作成し、加算を実行し、 コピー 一時オブジェクトから既存のオブジェクトへの結果の値 a
.
ただし、Java では、 operator=
は参照型の値のコピーを実行せず、ユーザーは新しい参照型のみを作成でき、値型は作成できません。したがって、次の名前のユーザー定義型の場合、 Complex
, 、代入とは、参照を既存の値にコピーすることを意味します。
代わりに次のように考えてください。
b.set(1, 0); // initialize to real number '1'
a = b;
b.set(2, 0);
assert( !a.equals(b) ); // this assertion will fail
C++ では、これにより値がコピーされるため、比較の結果は等しくなくなります。Javaでは、 operator=
参照コピーを実行するため、 a
そして b
は同じ値を参照するようになりました。結果として、オブジェクトはそれ自体と等しいと比較されるため、比較では「等しい」が生成されます。
コピーと参照の違いは、演算子のオーバーロードの混乱を招くだけです。@Sebastianが述べたように、JavaとC#はどちらも値と参照の等価性を個別に処理する必要があります。 operator+
おそらく値とオブジェクトを扱うことになるでしょうが、 operator=
参照を処理するためにすでに実装されています。
C++ では、一度に 1 種類の比較のみを処理する必要があるため、混乱が少なくなります。たとえば、 Complex
, operator=
そして operator==
どちらも値を処理しており、それぞれ値をコピーし、値を比較しています。
他のヒント
演算子のオーバーロードについて不満を言う投稿がたくさんあります。
「演算子のオーバーロード」の概念を明確にして、この概念に対する別の視点を提供する必要があると感じました。
コードを難読化しますか?
この議論は誤りです。
難読化はすべての言語で可能です...
C または Java で関数/メソッドを使用してコードを難読化するのは、C++ で演算子のオーバーロードを使用するのと同じくらい簡単です。
// C++
T operator + (const T & a, const T & b) // add ?
{
T c ;
c.value = a.value - b.value ; // subtract !!!
return c ;
}
// Java
static T add (T a, T b) // add ?
{
T c = new T() ;
c.value = a.value - b.value ; // subtract !!!
return c ;
}
/* C */
T add (T a, T b) /* add ? */
{
T c ;
c.value = a.value - b.value ; /* subtract !!! */
return c ;
}
...Java の標準インターフェイスでも
別の例を見てみましょう。 Cloneable
インターフェース Javaでは:
このインターフェースを実装するオブジェクトのクローンを作成する必要があります。しかし、嘘をつくこともできます。そして別のオブジェクトを作成します。実際、このインターフェースは非常に弱いので、楽しむために別のタイプのオブジェクトをまとめて返すこともできます。
class MySincereHandShake implements Cloneable
{
public Object clone()
{
return new MyVengefulKickInYourHead() ;
}
}
として Cloneable
インターフェイスは悪用/難読化される可能性がありますが、C++ 演算子のオーバーロードと同じ理由で禁止されるべきでしょうか?
過負荷になる可能性があります toString()
の方法 MyComplexNumber
クラスを使用して、文字列化された時刻を返すようにします。すべきです toString()
過積載も禁止されるの?妨害行為をすることもできる MyComplexNumber.equals
ランダムな値を返すようにするには、オペランドを変更します...等等等..
Java では、C++ やその他の言語と同様に、プログラマはコードを記述するときに最小限のセマンティクスを尊重する必要があります。これは、 add
とを追加する関数 Cloneable
クローンを作成する実装方法と、 ++
インクリメントより演算子。
それにしても何がわかりにくくしているのでしょうか?
初期の Java メソッドを使用してもコードが破壊される可能性があることがわかったので、C++ での演算子のオーバーロードの実際の使用法について自問できます。
明確で自然な表記:メソッドと演算子のオーバーロード?
以下では、Java と C++ の「同じ」コードをさまざまなケースで比較して、どちらの種類のコーディング スタイルがより明確であるかを理解します。
自然な比較:
// C++ comparison for built-ins and user-defined types
bool isEqual = A == B ;
bool isNotEqual = A != B ;
bool isLesser = A < B ;
bool isLesserOrEqual = A <= B ;
// Java comparison for user-defined types
boolean isEqual = A.equals(B) ;
boolean isNotEqual = ! A.equals(B) ;
boolean isLesser = A.comparesTo(B) < 0 ;
boolean isLesserOrEqual = A.comparesTo(B) <= 0 ;
演算子のオーバーロードが提供されている限り、A と B は C++ のどの型でもよいことに注意してください。Java では、A と B がプリミティブではない場合、プリミティブのようなオブジェクト (BigInteger など) であっても、コードが非常に混乱する可能性があります。
自然な配列/コンテナー アクセサーとサブスクリプション:
// C++ container accessors, more natural
value = myArray[25] ; // subscript operator
value = myVector[25] ; // subscript operator
value = myString[25] ; // subscript operator
value = myMap["25"] ; // subscript operator
myArray[25] = value ; // subscript operator
myVector[25] = value ; // subscript operator
myString[25] = value ; // subscript operator
myMap["25"] = value ; // subscript operator
// Java container accessors, each one has its special notation
value = myArray[25] ; // subscript operator
value = myVector.get(25) ; // method get
value = myString.charAt(25) ; // method charAt
value = myMap.get("25") ; // method get
myArray[25] = value ; // subscript operator
myVector.set(25, value) ; // method set
myMap.put("25", value) ; // method put
Java では、各コンテナが同じこと (インデックスまたは識別子を介してコンテナのコンテンツにアクセス) を実行するのに、異なる方法があり、混乱を招きます。
C++ では、演算子のオーバーロードにより、各コンテナーは同じ方法を使用してコンテンツにアクセスします。
自然な高度な型の操作
以下の例では、 Matrix
オブジェクト、Google で見つかった最初のリンクを使用して見つかりました。Java マトリックス オブジェクト" そして "C++行列オブジェクト":
// C++ YMatrix matrix implementation on CodeProject
// http://www.codeproject.com/KB/architecture/ymatrix.aspx
// A, B, C, D, E, F are Matrix objects;
E = A * (B / 2) ;
E += (A - B) * (C + D) ;
F = E ; // deep copy of the matrix
// Java JAMA matrix implementation (seriously...)
// http://math.nist.gov/javanumerics/jama/doc/
// A, B, C, D, E, F are Matrix objects;
E = A.times(B.times(0.5)) ;
E.plusEquals(A.minus(B).times(C.plus(D))) ;
F = E.copy() ; // deep copy of the matrix
そして、これは行列に限定されません。の BigInteger
そして BigDecimal
Java のクラスも同様に混乱を招く冗長性を抱えていますが、C++ のクラスと同等のクラスは組み込み型と同じくらい明確です。
自然反復子:
// C++ Random Access iterators
++it ; // move to the next item
--it ; // move to the previous item
it += 5 ; // move to the next 5th item (random access)
value = *it ; // gets the value of the current item
*it = 3.1415 ; // sets the value 3.1415 to the current item
(*it).foo() ; // call method foo() of the current item
// Java ListIterator<E> "bi-directional" iterators
value = it.next() ; // move to the next item & return the value
value = it.previous() ; // move to the previous item & return the value
it.set(3.1415) ; // sets the value 3.1415 to the current item
自然関数:
// C++ Functors
myFunctorObject("Hello World", 42) ;
// Java Functors ???
myFunctorObject.execute("Hello World", 42) ;
テキストの連結:
// C++ stream handling (with the << operator)
stringStream << "Hello " << 25 << " World" ;
fileStream << "Hello " << 25 << " World" ;
outputStream << "Hello " << 25 << " World" ;
networkStream << "Hello " << 25 << " World" ;
anythingThatOverloadsShiftOperator << "Hello " << 25 << " World" ;
// Java concatenation
myStringBuffer.append("Hello ").append(25).append(" World") ;
わかりました、Java では使用できます MyString = "Hello " + 25 + " World" ;
あまりにも...しかし、ちょっと待ってください。これは演算子のオーバーロードですよね。浮気じゃないの???
:-D
一般的なコード?
オペランドを変更する同じ汎用コードは、組み込み/プリミティブ (Java にインターフェイスがない)、標準オブジェクト (適切なインターフェイスがない)、およびユーザー定義オブジェクトの両方に使用できる必要があります。
たとえば、任意の型の 2 つの値の平均値を計算します。
// C++ primitive/advanced types
template<typename T>
T getAverage(const T & p_lhs, const T & p_rhs)
{
return (p_lhs + p_rhs) / 2 ;
}
int intValue = getAverage(25, 42) ;
double doubleValue = getAverage(25.25, 42.42) ;
complex complexValue = getAverage(cA, cB) ; // cA, cB are complex
Matrix matrixValue = getAverage(mA, mB) ; // mA, mB are Matrix
// Java primitive/advanced types
// It won't really work in Java, even with generics. Sorry.
演算子のオーバーロードについて議論する
演算子のオーバーロードを使用した C++ コードと Java の同じコードを公平に比較したので、概念としての「演算子のオーバーロード」について説明します。
演算子のオーバーロードはコンピュータ以前から存在していました
コンピューター サイエンスの外でも、演算子のオーバーロードがあります。たとえば、数学では次のような演算子が使用されます。 +
, -
, *
, 、など。過負荷になっています。
確かに、その意味は、 +
, -
, *
, 、など。オペランドの種類(数値、ベクトル、量子波動関数、行列など)に応じて変化します。
私たちのほとんどは、科学コースの一環として、オペランドの型に応じて演算子の複数の意味を学びました。彼らは混乱していると思いましたか?
演算子のオーバーロードはオペランドに依存します
これは演算子のオーバーロードの最も重要な部分です。数学や物理学と同様、演算はオペランドの型によって異なります。
したがって、オペランドの型を知ることで、演算の効果がわかります。
C や Java にも (ハードコードされた) 演算子のオーバーロードがあります
C では、演算子の実際の動作はオペランドに応じて変化します。たとえば、2 つの整数を加算することは、2 つの double を加算すること、または 1 つの整数と 1 つの double を加算することとは異なります。ポインターの算術ドメイン全体もあります (キャストを行わずに、ポインターに整数を追加できますが、2 つのポインターを追加することはできません...)。
Java にはポインター演算はありませんが、それでも誰かが文字列連結を発見しました。 +
演算子は、「演算子のオーバーロードは悪である」という信条の例外を正当化するのに十分ばかばかしいでしょう。
それは、あなたが C (歴史的な理由で) または Java ( 個人的な理由, 、下記を参照)コーダー、独自のものを提供することはできません。
C++ では、演算子のオーバーロードはオプションではありません...
C++ では、組み込み型の演算子のオーバーロードは不可能ですが (これは良いことです)、 ユーザー定義の タイプには次のものがあります ユーザー定義の 演算子のオーバーロード。
すでに述べたように、C++ では、Java とは逆に、組み込み型と比較した場合、ユーザー型は言語の二級市民とはみなされません。したがって、組み込み型に演算子がある場合、ユーザー型にも演算子を含めることができるはずです。
真実は、 toString()
, clone()
, equals()
メソッドは Java 用です (つまり準標準的な)、C++ 演算子のオーバーロードは C++ の一部であるため、元の C 演算子や前述の Java メソッドと同じくらい自然になります。
テンプレート プログラミングと組み合わせると、演算子のオーバーロードはよく知られた設計パターンになります。実際、オーバーロードされた演算子を使用したり、独自のクラスの演算子をオーバーロードしたりせずに、STL をさらに進めることはできません。
...しかし、悪用すべきではありません
演算子のオーバーロードでは、演算子のセマンティクスを尊重するように努める必要があります。で減算しないでください +
演算子 (「a では減算しないでください」のように) add
関数」または「クソを返す clone
方法")。
キャストのオーバーロードはあいまいさを引き起こす可能性があるため、非常に危険です。したがって、これらは明確に定義されたケースのために実際に予約されるべきです。はどうかと言うと &&
そして ||
, 何をしているのかよくわかっていない限り、決してオーバーロードしないでください。ネイティブの演算子による短絡評価が失われるからです。 &&
そして ||
楽しむ。
それで...わかりました...では、なぜ Java ではそれができないのでしょうか?
ジェームズ・ゴズリングがこう言ったからです。
演算子のオーバーロードは省略しました。 かなり個人的な選択 それは、あまりにも多くの人が C++ でそれを悪用しているのを見てきたからです。
ジェームズ・ゴズリング。ソース: http://www.gotw.ca/publications/c_family_interview.htm
上記の Gosling のテキストと以下の Stroustrup のテキストを比較してください。
C++ の設計上の決定の多くは、人々に特定の方法で物事を行うことを強制することへの私の嫌悪感に根ざしています。[...] 個人的に嫌いな機能を非合法化したい誘惑に駆られることがよくありましたが、そうすることは控えていました。 自分の意見を他人に押し付ける権利があるとは思わなかった.
ビャルネ・ストルストラップ。ソース:C++ の設計と進化 (1.3 一般的な背景)
演算子のオーバーロードは Java に利益をもたらしますか?
一部のオブジェクトは、演算子のオーバーロードから大きな恩恵を受けます (BigDecimal などの具体的または数値型、複素数、行列、コンテナ、イテレータ、コンパレータ、パーサーなど)。
C++ では、Stroustrup の謙虚さにより、この利点を活用できます。Java では、Gosling のせいでただひどいことになるだけです 個人的な選択.
Javaに追加できるでしょうか?
現在 Java に演算子のオーバーロードを追加しない理由としては、内部政治、機能に対するアレルギー、開発者に対する不信感 (ご存じのとおり、Java チームにつきまとう妨害工作員たち...)、以前の JVM との互換性などが考えられます。正しい仕様書を書く時間など。
この機能を息をひそめて待ってはいけません...
しかし、彼らはそれをC#で行います!!!
うん...
2 つの言語の違いはこれだけではありませんが、この点が私を常に楽しませてくれます。
どうやら、C# の人たちは、 「あらゆるプリミティブは struct
, 、そして struct
オブジェクトから派生します」, 、最初の試行で正解しました。
そして彼らはそれを行うのです 他の言語!!!
使用される定義された演算子のオーバーロードに対するあらゆる FUD にもかかわらず、次の言語はそれをサポートしています。 スカラ座, ダーツ, パイソン, F#, C#, D, アルゴル68, 雑談, グルーヴィー, パール6, 、C++、 ルビー, ハスケル, MATLAB, エッフェル, ルア, クロージュア, フォートラン90, 迅速, エイダ, デルフィ 2005...
非常に多くの言語があり、非常に多くの異なる (そして時には対立する) 哲学を持っていますが、それでもその点ではすべてが同意しています。
思考の糧...
James Gosling は Java の設計を次のように例えました。
「あるアパートから別のアパートに移動するとき、移動には次のような原則があります。興味深い実験は、アパートを荷造りし、すべてを箱に入れてから次のアパートに移り、必要になるまで何も開梱しないというものです。あなたは初めての食事を作り、箱から何かを取り出しています。そして、1か月ほど経つと、それを使って自分の人生に実際に必要なものは何かがほぼわかり、残りのものを手に入れることになります。それがどれだけ好きか、どれだけクールかということは忘れてしまいます。ただ捨てるだけです。これにより生活がいかに簡素化されるかは驚くべきことであり、その原則をあらゆる種類の設計上の問題に使用できます。単にクールだからとか、面白いからという理由だけで物事を行うのではありません。」
読むことができます ここでの引用のコンテキスト
基本的に、演算子のオーバーロードは、ある種の点、通貨、または複素数をモデル化するクラスに最適です。しかし、その後はサンプルがすぐになくなってしまいます。
もう 1 つの要因は、開発者が「&&」、「||」、キャスト演算子、そしてもちろん「new」などの演算子をオーバーロードすることによる C++ の機能の悪用でした。これと値渡しおよび例外を組み合わせることで生じる複雑さについては、次のセクションで詳しく説明しています。 優れた C++ 本。
Boost.Units を確認してください。 リンクテキスト
演算子のオーバーロードを通じて、オーバーヘッドゼロの次元解析を提供します。これでどこまで鮮明になるでしょうか?
quantity<force> F = 2.0*newton;
quantity<length> dx = 2.0*meter;
quantity<energy> E = F * dx;
std::cout << "Energy = " << E << endl;
実際には「Energy = 4 J」が出力されますが、これは正しいです。
Java 設計者は、演算子のオーバーロードは価値があるというよりも問題の方が多いと判断しました。そのような単純な。
すべてのオブジェクト変数が実際には参照である言語では、演算子のオーバーロードは、少なくとも C++ プログラマにとっては非常に非論理的であるというさらなる危険を伴います。C# の == 演算子のオーバーロードと状況を比較してください。 Object.Equals
そして Object.ReferenceEquals
(またはそれが何と呼ばれても)。
グルーヴィー 演算子のオーバーロードがあり、JVM で実行されます。パフォーマンスへの影響 (毎日小さくなります) を気にしないのであれば。これはメソッド名に基づいて自動的に行われます。たとえば、「+」は「plus(argument)」メソッドを呼び出します。
これは、開発者に意図を明確に伝える名前の関数を作成するよう強制するための、意識的な設計上の選択だったのではないかと思います。C++ では、開発者は、指定された演算子の一般に受け入れられている性質とは無関係な機能を演算子にオーバーロードすることが多く、演算子の定義を見ずにコードの一部が何を行うかを判断することはほぼ不可能になります。
オペレータに過負荷がかかると、実際に自分の足を撃つ可能性があります。それは、人々がポインターで愚かな間違いを犯すので、ハサミを取り上げることが決定されたのと同じです。
少なくともそれが理由だと思います。とにかく私はあなたの側にいます。:)
演算子のオーバーロードは、演算子が演算ロジックと一致しないタイプの論理エラーを引き起こすと言っているのは、何も言っていないのと同じです。関数名が操作ロジックに不適切な場合にも、同じ種類のエラーが発生します。解決策は次のとおりです。機能使用能力を落とす!?これはコミカルな答えです - 「操作ロジックには不適切」、すべてのパラメーター名、すべてのクラス、関数など、論理的に不適切な可能性があります。私は、このオプションは立派なプログラミング言語で利用できるべきだと思います。そして、それが安全ではないと考えている人たちも、それを使用しなければならないとは言いません。C# を見てみましょう。ポインタは垂れ下がっていますが、「安全でないコード」という記述がありますので、自己責任で好きなようにプログラムしてください。
Java での演算子のオーバーロードは難読化につながると言う人もいます。これらの人々は、BigDecimal を使用して財務値をパーセントずつ増やすなどの基本的な計算を実行している Java コードを立ち止まって見たことがありますか?....このような演習の冗長性は、それ自体が難読化の実証となります。皮肉なことに、Java に演算子のオーバーロードを追加すると、そのような数学コードをエレガントでシンプルにする (難読化が少なくなる) 独自の Currency クラスを作成できるようになります。
技術的には、さまざまなタイプの数値を処理できるすべてのプログラミング言語に演算子のオーバーロードがあります。整数と実数。説明:オーバーロードという用語は、1 つの関数に対して複数の実装が存在することを意味します。ほとんどのプログラミング言語では、演算子 + に対して、整数用と実数用のさまざまな実装が提供されており、これは演算子のオーバーロードと呼ばれます。
さて、多くの人は、Java に演算子 + 文字列を加算するための演算子のオーバーロードがあることを奇妙に感じています。数学的な観点からすると、これは確かに奇妙ですが、プログラミング言語の開発者の観点から見ると、組み込みの演算子のオーバーロードを追加することに何の問題もありません。演算子 + 他のクラスの場合 例:弦。ただし、String に対して + の組み込みオーバーロードを追加したら、開発者にもこの機能を提供するのが一般的に良い考えであることにほとんどの人が同意します。
演算子のオーバーロードがコードを難読化するという誤った考えには完全に同意しません。これは開発者の判断に委ねられています。これは考えが甘いですし、正直に言うと、それは古くなりつつあります。
Java 8 で演算子のオーバーロードを追加するには +1。
Java を実装言語と仮定すると、a、b、c はすべて、初期値が null の Complex 型への参照になります。また、前述のように Complex が不変であると仮定します。 ビッグ整数 および同様の不変 BigDecimal, b と c を加算して返された Complex に参照を割り当てており、この参照を a と比較していないため、次のことを意味していると思います。
そうではありません:
Complex a, b, c; a = b + c;
多くの 以下よりも簡単です。
Complex a, b, c; a = b.add(c);
演算子のオーバーロード、フレンド クラス、多重継承があれば便利な場合があります。
しかし、それでも良い決断だったと思っています。もし Java に演算子のオーバーロードがあったとしたら、ソース コードを調べなければ演算子の意味を確信することはできません。現時点ではその必要はありません。また、演算子のオーバーロードの代わりにメソッドを使用する例も非常に読みやすいと思います。物事をより明確にしたい場合は、複雑なステートメントの上にコメントを追加することもできます。
// a = b + c
Complex a, b, c; a = b.add(c);
これはこれを禁止する十分な理由ではありませんが、実用的な理由です。
人々は常に責任を持ってそれを使用するとは限りません。Python ライブラリ scapy の例を見てください。
>>> IP()
<IP |>
>>> IP()/TCP()
<IP frag=0 proto=TCP |<TCP |>>
>>> Ether()/IP()/TCP()
<Ether type=0x800 |<IP frag=0 proto=TCP |<TCP |>>>
>>> IP()/TCP()/"GET / HTTP/1.0\r\n\r\n"
<IP frag=0 proto=TCP |<TCP |<Raw load='GET / HTTP/1.0\r\n\r\n' |>>>
>>> Ether()/IP()/IP()/UDP()
<Ether type=0x800 |<IP frag=0 proto=IP |<IP frag=0 proto=UDP |<UDP |>>>>
>>> IP(proto=55)/TCP()
<IP frag=0 proto=55 |<TCP |>>
説明は次のとおりです。
/演算子は、2つの層の間の組成演算子として使用されています。そうする場合、下層は、上層に従って1つ以上のデフォルトフィールドを過負荷にすることができます。(あなたはまだあなたが望む価値を与えることができます)。文字列を生のレイヤーとして使用できます。
Java オペレーターのオーバーロードのネイティブ サポートの代替手段
Java には演算子のオーバーロードがないため、検討できる代替手段をいくつか示します。
- 別の言語を使用してください。両方 グルーヴィー そして スカラ座 演算子のオーバーロードがあり、Java に基づいています。
- 使用 ジャワおお, 、Java で演算子のオーバーロードを可能にするプラグイン。プラットフォームに依存しないことに注意してください。また、多くの問題があり、Java の最新リリースと互換性がありません (つまり、Java10)。(元の StackOverflow ソース)
- 使用 JNI, 、Java ネイティブ インターフェイス、または代替手段。これにより、Java で使用する C または C++ (おそらく他のもの?) メソッドを作成できるようになります。もちろん、これもプラットフォームに依存しません。
誰かが他の人を知っている場合は、コメントしてください。このリストに追加します。