C++ から Java に移行すると、明らかに未回答の質問は、なぜ Java に演算子のオーバーロードが含まれていないのかということです。
Complex a, b, c; a = b + c;
よりもはるかに単純ではありませんComplex a, b, c; a = b.add(c);
か?
演算子のオーバーロードを許可しないための有効な引数である、これには既知の理由がありますか? その理由は恣意的なものですか、それとも時間の経過とともに失われたものですか?
C++ から Java に移行すると、明らかに未回答の質問は、なぜ Java に演算子のオーバーロードが含まれていないのかということです。
Complex a, b, c; a = b + c;
よりもはるかに単純ではありませんComplex a, b, c; a = b.add(c);
か?
演算子のオーバーロードを許可しないための有効な引数である、これには既知の理由がありますか? その理由は恣意的なものですか、それとも時間の経過とともに失われたものですか?
演算子のオーバーロードについて不平を言う投稿がたくさんあります。
「演算子のオーバーロード」の概念を明確にし、この概念の代替的な視点を提供する必要があると感じました。
この議論は誤りです。
C ++の場合と同様に、関数/メソッドを使用してCまたはJavaのコードを難読化するのは、演算子のオーバーロードを使用する場合と同じくらい簡単です。
// 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
インターフェースを見てみましょう。
このインターフェースを実装するオブジェクトのクローンを作成することになっています。しかし、あなたは嘘をつくことができます。そして、別のオブジェクトを作成します。実際、このインターフェイスは非常に弱いため、楽しみのために、別のタイプのオブジェクトを完全に返すことができます。
class MySincereHandShake implements Cloneable
{
public Object clone()
{
return new MyVengefulKickInYourHead() ;
}
}
インターフェイスは悪用/難読化される可能性があるためCloneable
、C ++演算子のオーバーロードが想定されているのと同じ理由で禁止する必要がありますか?
toString()
クラスのメソッドをオーバーロードしてMyComplexNumber
、1日の文字列化された時間を返すようにすることができます。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 ++では、演算子のオーバーロードのおかげで、各コンテナーは同じ方法でコンテンツにアクセスします。
以下の例では、「JavaMatrixオブジェクト」および「C++Matrixオブジェクト」についてGoogleで最初に見つかったリンクを使用して見つかったオブジェクトを使用しMatrix
ています。
// 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
そして、これは行列に限定されません。JavaのクラスBigInteger
とBigDecimal
クラスは同じ紛らわしい冗長性に悩まされていますが、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では、演算子の実際の動作はそのオペランドに応じて変化します。たとえば、2つの整数を加算することは、2つの倍精度浮動小数点数を加算すること、または1つの整数と1つの倍精度浮動小数点数を加算することとは異なります。ポインタ演算ドメイン全体もあります(キャストせずに、ポインタに整数を追加することはできますが、2つのポインタを追加することはできません...)。
Javaにはポインタ演算はありませんが、演算子なしで文字列の連結を見つけた人+
は、「演算子のオーバーロードは悪」という信条の例外を正当化するのに十分ばかげているでしょう。
C(歴史的な理由で)またはJava(個人的な理由で、以下を参照)のコーダーとして、独自のコーダーを提供することはできません。
C ++では、組み込み型の演算子のオーバーロードは不可能ですが(これは良いことです)、ユーザー定義型にはユーザー定義の演算子オーバーロードがあります。
すでに前に述べたように、C ++では、Javaとは対照的に、ユーザー型は、組み込み型と比較した場合、言語の二級市民とは見なされません。したがって、組み込み型に演算子がある場合、ユーザー型にも演算子を含めることができるはずです。
真実は、、、メソッドがJava用であるように(つまり、toString()
準標準のような)、C++演算子のオーバーロードはC++の非常に多くの部分であるため、元のC演算子または前述のJavaメソッドと同じくらい自然になります。clone()
equals()
テンプレートプログラミングと組み合わせると、演算子のオーバーロードはよく知られているデザインパターンになります。実際、オーバーロードされた演算子と、独自のクラスのオーバーロード演算子を使用せずに、STLを遠くまで進めることはできません。
演算子のオーバーロードは、演算子のセマンティクスを尊重するように努める必要があります。+
演算子で減算しないでください( add
「関数で減算しない」または「メソッドでがらくたを返すclone
」のように)。
キャストのオーバーロードは、あいまいさを引き起こす可能性があるため、非常に危険です。したがって、それらは明確に定義されたケースのために実際に予約する必要があります。&&
とについては、ネイティブオペレーターが楽しん||
でいる短絡評価が失われるため、自分が何をしているのかを本当に理解していない限り、過負荷にしないでください。&&
||
ジェームズ・ゴスリングがそう言ったので:
C ++で演算子のオーバーロードを悪用する人が多すぎるのを見てきたので、かなり個人的な選択として演算子のオーバーロードを省略しました。
ジェームズ・ゴスリング。出典:http ://www.gotw.ca/publications/c_family_interview.htm
上記のGoslingのテキストと以下のStroustrupのテキストを比較してください。
多くのC++設計の決定は、人々に特定の方法で物事を強制することを嫌うことに根ざしています[...]しばしば、私は個人的に嫌いな機能を非合法化するように誘惑されました。他人に私の見解を強制する権利。
ビャーネ・ストロヴルプ。出典:C ++の設計と進化(1.3一般的な背景)
一部のオブジェクトは、演算子のオーバーロードから大きなメリットがあります(BigDecimal、複素数、行列、コンテナー、イテレーター、コンパレーター、パーサーなどの具象型または数値型)。
C ++では、Stroustrupの謙虚さにより、このメリットを享受できます。Javaでは、Goslingの個人的な選択のために、あなたは単に困惑しています。
現在Javaで演算子のオーバーロードを追加しない理由は、内部の政治、機能へのアレルギー、開発者の不信(Javaチームを悩ませているように見える妨害者...)、以前のJVMとの互換性の組み合わせである可能性があります。正しい仕様を書く時間など。
だから、この機能を待って息を止めないでください...
うん...
これが2つの言語の唯一の違いというわけではありませんが、これは私を面白がらせるのに失敗することはありません。
どうやら、C#の人々は、「すべてのプリミティブはでstruct
あり、struct
オブジェクトから派生している」ので、最初の試みでそれを正しく理解しました。
使用される定義済み演算子のオーバーロードに対するすべてのFUDにもかかわらず、次の言語がサポートしています:Kotlin、Scala、Dart、Python、F#、C#、D、Algol 68、Smalltalk、Groovy、Perl 6、C ++、Ruby、Haskell、MATLAB、Eiffel、Lua、Clojure、Fortran 90、Swift、Ada、Delphi2005 ..。
非常に多くの言語、非常に多くの異なる(そして時には反対の)哲学がありますが、それでも彼らはすべてその点に同意しています。
思考の糧...
James Gosling は、Java の設計を次のように例えました。
「あるアパートから別のアパートに移動するとき、引っ越しにはこの原則があります。興味深い実験は、アパートをまとめてすべてを箱に入れ、次のアパートに移動し、必要になるまで何も開梱しないというものです。だからあなたは」最初の食事を作り、箱から何かを取り出します. それから 1 か月ほど経つと、それを使って自分の生活に実際に必要なものは何かをほぼ把握し、残りの食事を取ります.どれだけ好きか、どれだけクールかを忘れて、ただ捨てるだけです. それがあなたの人生をいかにシンプルにするかは驚くべきことです. この原則は、あらゆる種類のデザインの問題で使用できます.かっこよかった、または単に面白いからです。」
ここで引用の文脈を読むことができます
基本的に、演算子のオーバーロードは、ある種のポイント、通貨、または複素数をモデル化するクラスに最適です。しかし、その後、サンプルがすぐに不足し始めます。
もう 1 つの要因は、'&&'、'||'、キャスト演算子、そしてもちろん 'new' などの演算子をオーバーロードする開発者による C++ の機能の悪用でした。これを値渡しと例外と組み合わせることで生じる複雑さは、Exceptional 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;
実際には正しい「エネルギー=4J」を出力します。
Javaの設計者は、演算子のオーバーロードは価値があるよりも厄介であると判断しました。そのような単純な。
すべてのオブジェクト変数が実際に参照である言語では、演算子のオーバーロードは、少なくともC++プログラマーにとっては非常に非論理的であるという追加の危険を伴います。状況をC#の==演算子のオーバーロードとObject.Equals
and Object.ReferenceEquals
(またはそれが呼ばれるもの)と比較してください。
によって参照されるオブジェクトの以前の値を上書きしたいと仮定すると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++ では、これは値をコピーするため、比較の結果は等しくありません。Javaoperator=
では参照コピーを行うためa
、 とb
は同じ値を参照するようになりました。その結果、オブジェクトがそれ自体と等しいと比較されるため、比較は「等しい」となります。
コピーと参照の違いは、演算子のオーバーロードの混乱を助長するだけです。@Sebastian が述べたように、Java と C# はどちらも値と参照の等価性を個別にoperator+
処理する必要があります。値とオブジェクトを処理する可能性がありますが、operator=
参照を処理するために既に実装されています。
C++ では、一度に 1 種類の比較のみを処理する必要があるため、混乱が少なくなります。たとえば、 onとはどちらも値に取り組んでおりComplex
、それぞれ値のコピーと値の比較を行っています。operator=
operator==
Groovyには演算子のオーバーロードがあり、JVMで実行されます。パフォーマンスへの影響(毎日小さくなります)を気にしない場合。メソッド名に基づいて自動的に行われます。たとえば、「+」は「plus(argument)」メソッドを呼び出します。
これは、名前が明確に意図を伝える関数を開発者に作成させるための意識的な設計上の選択だったのではないかと思います。C ++では、開発者は、特定の演算子の一般的に受け入れられている性質とは関係のない機能で演算子をオーバーロードし、演算子の定義を見ずにコードの一部が何をするかを判断することはほぼ不可能になります。
技術的には、整数や実数など、さまざまなタイプの数値を処理できるすべてのプログラミング言語に演算子のオーバーロードがあります。説明: オーバーロードという用語は、1 つの関数に対して単純に複数の実装があることを意味します。ほとんどのプログラミング言語では、演算子 + に対して異なる実装が提供されています。1 つは整数用、もう 1 つは実数用です。これは、演算子のオーバーロードと呼ばれます。
さて、多くの人は、Java が文字列を加算するための演算子 + に対して演算子のオーバーロードを持っていることを奇妙に感じています。数学的な観点からは、これは確かに奇妙ですが、プログラミング言語の開発者の観点から見ると、組み込みの演算子のオーバーロードを追加しても問題はありません。演算子 + の場合、他のクラス (String など) の場合。ただし、ほとんどの人は、+ に String の組み込みのオーバーロードを追加したら、開発者にもこの機能を提供することをお勧めします。
オペレーターのオーバーロードがコードを難読化するという誤謬には完全に同意しません。これは開発者の判断に委ねられているためです。これは考えが甘いですし、正直に言うと、時代遅れになっています。
Java 8 で演算子のオーバーロードを追加するための +1。
Java での演算子のオーバーロードは難読化につながると言う人もいます。これらの人々は、 BigDecimal を使用して金銭的価値をパーセンテージで増加させるなど、いくつかの基本的な計算を行う Java コードを見て立ち止まったことがありますか? .... そのような演習の冗長性は、難読化の独自のデモンストレーションになります。皮肉なことに、演算子のオーバーロードを Java に追加すると、独自の Currency クラスを作成できるようになり、そのような数学コードをエレガントでシンプルにする (難読化を軽減する) ことができます。
オペレーターの過負荷で本当に自分の足を撃つことができます。ポインターと同じように、人々は愚かな間違いを犯すので、はさみを取り除くことにしました。
少なくともそれが理由だと思います。とにかく私はあなたの側にいます。:)
演算子のオーバーロードが、演算子が演算ロジックと一致しないタイプの論理エラーにつながると言うのは、何も言わないのと同じです。関数名が演算ロジックに不適切だと同様のエラーが発生するので、解決策は? 関数の使用能力を落とす!? これはコミカルな答えです - 「操作ロジックには不適切」、すべてのパラメーター名、すべてのクラス、関数、または論理的に不適切な可能性があるものは何でも。私は、このオプションはまともなプログラミング言語で利用できるべきだと思います。そして、それが安全ではないと考えている人は、それを使用しなければならないと言っているわけではありません。C# を見てみましょう。彼らはポインターを垂らしましたが、ちょっと-「安全でないコード」ステートメントがあります-自分の責任で好きなようにプログラムしてください。
実装言語としてJavaを想定すると、a、b、およびcはすべて、初期値がnullのComplex型への参照になります。また、Complexが前述のBigIntegerおよび同様の不変の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);
決定を下す人々は、複雑な値、行列代数、集合論、および過負荷によりすべてを言語に組み込むことなく標準表記を使用できる場合を単に忘れていたと思います。とにかく、数学指向のソフトウェアだけが、このような機能の恩恵を受けます。一般的な顧客アプリケーションでは、それらが必要になることはほとんどありません。
不必要な難読化に関する彼らの議論は、プログラマーがプログラム固有の演算子を定義し、代わりに関数になる可能性がある場合に明らかに有効です。関数の名前が明確に見える場合、それが実行するヒントを提供します。演算子は、読み取り可能な名前のない関数です。
Java は一般に、コードが読みやすくなるため、余分な冗長性は悪くないという哲学に基づいて設計されています。同じことを行うコンストラクトは、入力するコードが少ないだけで、以前は「シンタックス シュガー」と呼ばれていました。これは、Python の哲学とは大きく異なります。たとえば、2 番目の読者に提供するコンテキストが少なくても、短いほど良いと見なされるという Python の哲学とは大きく異なります。