web-dev-qa-db-ja.com

Java=の開発者は意識的にRAIIを放棄しましたか?

私は長年のC#プログラマーとして、最近Resource Acquisition Is Initializationの利点について詳しく知るようになりました(RAII )。特に、C#のイディオムが次のことを発見しました。

using (var dbConn = new DbConnection(connStr)) {
    // do stuff with dbConn
}

同等のC++があります。

{
    DbConnection dbConn(connStr);
    // do stuff with dbConn
}

つまり、DbConnectionのようなリソースの使用をusingブロックで囲むことを覚えておくことは、C++では不要です。これはC++の大きな利点のようです。たとえば、DbConnection型のインスタンスメンバーを持つクラスを考えると、これはさらに説得力があります。

class Foo {
    DbConnection dbConn;

    // ...
}

C#では、FooにIDisposableをそのように実装させる必要があります:

class Foo : IDisposable {
    DbConnection dbConn;

    public void Dispose()
    {       
        dbConn.Dispose();
    }
}

さらに悪いことに、Fooのすべてのユーザーは、次のようにFoousingブロックで囲むことを覚えておく必要があります。

   using (var foo = new Foo()) {
       // do stuff with "foo"
   }

ここでC#とそのJavaルーツを考えています...私は不思議に思っています... Javaの開発者がスタックをヒープのおかげでRAIIを放棄しますか?

(同様に、 Stroustrup はRAIIの重要性を完全に認めましたか?)

84
JoelFan

さて、C#とそのJavaのルートを見てみましょう。Javaの開発者は、ヒープを優先してスタックを放棄してRAIIを放棄したときに放棄したことを十分に理解しましたか?

(同様に、StroustrupはRAIIの重要性を十分に理解しましたか?)

ゴスリングがJavaを設計したときにRAIIの重要性を理解していなかったと私は確信しています。彼のインタビューで彼はジェネリックと演算子のオーバーロードを除外する理由についてしばしば話しましたが、決定論的デストラクタとRAIIについては言及していませんでした。

面白いことに、Stroustrupでさえ、設計時に決定論的デストラクタの重要性を認識していませんでした。引用文は見つかりませんが、本当に興味があれば、彼のインタビューの中で見つけることができます: http://www.stroustrup.com/interviews.html

38

はい、C#(そしてJavaだと思います)の設計者たちは、確定的ファイナライズに反対することを明確に決定しました。私はこれについて1999年から2002年にかけて何度もAnders Hejlsbergに質問しました。

まず、スタックベースかヒープベースかに基づいたオブジェクトの異なるセマンティクスの考え方は、両方の言語の統一された設計目標に完全に反します。これは、まさにそのような問題からプログラマーを解放することでした。

第2に、利点があることを認めたとしても、簿記に伴う実装の複雑さと非効率が大きくなります。 本当にマネージ言語でスタックのようなオブジェクトをスタックに置くことはできません。 「スタックのようなセマンティクス」と言って、重要な作業に専念できます(値の型はすでに十分に困難です。参照がマネージメモリに出入りする複雑なクラスのインスタンスであるオブジェクトについて考えてください)。

そのため、「(ほとんど)すべてがオブジェクトである」プログラミングシステムでは、すべてのオブジェクトを確定的にファイナライズする必要はありません。したがって、doは、通常追跡されるオブジェクトを確定的なファイナライズを持つオブジェクトから分離するために、ある種のプログラマ制御の構文を導入する必要があります。

C#には、usingキーワードがあります。これは、C#1.0になったものの設計のかなり後の方にあります。 IDisposable全体はかなり悲惨で、ボイラープレートのusingパターンを自動的に適用できるクラスをマークするC++デストラクタ構文~IDisposableを機能させるほうがエレガントなのではないでしょうか。

60
Larry OBrien

Javaは、C++が非常に異なる言語であった1991-1995で開発されたことに注意してください。例外(RAIIが必要になったため、)とテンプレート(スマートポインタの実装を容易にした)は「新しい」機能であり、ほとんどのC++プログラマはCから来ており、手動のメモリ管理に慣れていました。

したがって、Javaの開発者が意図的にRAIIを放棄することを決定したとは思えません。ただし、Javaの場合、値のセマンティクスではなく参照のセマンティクスを優先することを意図的に決定しました。 決定論的な破棄は、参照セマンティクス言語で実装するのは困難です。

それでは、なぜ値セマンティクスの代わりに参照セマンティクスを使用するのでしょうか。

言語を大幅に簡略化するため

  • Fooと_Foo*_、または_foo.bar_と_foo->bar_の構文を区別する必要はありません。
  • すべての割り当てがポインターのコピーである場合、オーバーロードされた割り当ての必要はありません。
  • コピーコンストラクタは必要ありません。 (たまにclone()のような明示的なコピー関数が必要です。多くのオブジェクトはコピーする必要がないだけです。たとえば、不変要素はそうではありません。)
  • クラスをコピー不可にするために、privateコピーコンストラクターおよび_operator=_を宣言する必要はありません。クラスのオブジェクトをコピーしたくない場合は、それをコピーする関数を記述しないでください。
  • swap関数は必要ありません。 (ソートルーチンを作成している場合を除きます)。
  • C++ 0xスタイルの右辺値参照は必要ありません。
  • (N)RVOは必要ありません。
  • スライスの問題はありません。
  • 参照のサイズは固定されているため、コンパイラーがオブジェクトのレイアウトを決定するのは簡単です。

参照セマンティクスの主な欠点は、すべてのオブジェクトに複数の参照が含まれている可能性がある場合、いつ削除するかを知ることが難しくなることです。あなたは自動メモリ管理を持っています

Javaは、非決定的なガベージコレクタを使用することを選択しました。

GCは確定的ではありませんか?

はい、できます。たとえば、 Python のC実装は参照カウントを使用します。そして、後でトレースGCを追加して、refcountが失敗した循環ガベージを処理します。

しかし、リカウントはひどく非効率的です。カウントの更新に費やされた多くのCPUサイクル。マルチスレッド環境(これらの更新を同期する必要がある種類Javaが設計された)など)ではさらに悪い。 nullガベージコレクタ を使用する方がはるかに別のものに切り替える必要があります。

Javaは、ファイルやソケットなどの非消去可能なリソースを犠牲にして、一般的なケース(メモリ)を最適化することを選択しました。今日、C++でのRAIIの採用に照らして、これはJavaの対象読者の多くは、これらを明示的に閉じるために使用されていたC(または "C with classes")プログラマであったことを覚えておいてください。

しかし、C++/CLIの「スタックオブジェクト」についてはどうでしょうか。

これらは、C#Disposeによく似た usingの構文糖元のリンク )です。ただし、匿名gcnew FileStream("filename.ext")を作成でき、C++/CLIはそれを自動破棄しないため、確定的破壊の一般的な問題は解決しません。

42
dan04

Java7はC#に似たものを導入しました usingtry-with-resourcesステートメント

1つ以上のリソースを宣言するtryステートメント。 resourceは、プログラムの終了後に閉じる必要のあるオブジェクトです。 try- with-resourcesステートメントは、各リソースがステートメントの最後で確実に閉じられるようにします。 Java.lang.AutoCloseableを実装するすべてのオブジェクトを含むJava.io.Closeableを実装するすべてのオブジェクトは、リソースとして使用できます...

したがって、彼らはRAIIを実装しないことを意識的に選択しなかったか、またはその間に彼らの考えを変えたと思います。

20
Patrick

Javaには、意図的にスタックベースのオブジェクト(別名値オブジェクト)がありません。これらは、そのようなメソッドの最後にオブジェクトを自動的に破壊するために必要です。

これとJavaはガベージコレクションされているため、確定的なファイナライズはほぼ不可能です(ex。「ローカル」オブジェクトの場合別の場所で参照された場合、メソッドが終了したときに、それを破壊したくありません)

ただし、ネイティブ(C++)リソースと対話する場合を除いて、確定的ファイナライズのneedはほとんどないため、これはほとんどの人にとって問題ありません!


Javaにスタックベースのオブジェクトがないのはなぜですか?

(プリミティブ以外..)

スタックベースのオブジェクトは、ヒープベースの参照とは意味が異なるためです。 C++で次のコードを想像してください。それは何をするためのものか?

return myObject;
  • myObjectがローカルのスタックベースのオブジェクトである場合、コピーコンストラクタが呼び出されます(結果が何かに割り当てられている場合)。
  • myObjectがローカルのスタックベースのオブジェクトであり、参照を返す場合、結果は未定義です。
  • myObjectがメンバー/グローバルオブジェクトの場合、(結果が何かに割り当てられている場合)コピーコンストラクターが呼び出されます。
  • myObjectがメンバー/グローバルオブジェクトであり、参照を返す場合、参照が返されます。
  • myObjectがローカルのスタックベースのオブジェクトへのポインターである場合、結果は未定義です。
  • myObjectがメンバー/グローバルオブジェクトへのポインターである場合、そのポインターが返されます。
  • myObjectがヒープベースのオブジェクトへのポインターである場合、そのポインターが返されます。

同じコードがJavaで何をするのでしょうか?

return myObject;
  • myObjectへの参照が返されます。変数がローカル、メンバー、グローバルのいずれであるかは関係ありません。また、スタックベースのオブジェクトやポインタケースについて心配する必要はありません。

上記は、スタックベースのオブジェクトがC++でのプログラミングエラーの原因非常に一般的である理由を示しています。そのため、Javaデザイナーはそれらを取り除きました;そして、それらなしでは、JavaでRAIIを使用する意味がありません。

usingの穴についての説明は不完全です。次の問題を検討してください。

_interface Bar {
    ...
}
class Foo : Bar, IDisposable {
    ...
}

Bar b = new Foo();

// Where's the Dispose?
_

私の意見では、RAIIとGCの両方がないことは悪い考えです。 Javaでファイルを閉じる場合は、malloc()free()があります。

17
DeadMG

私はかなり年をとっています。私はそこに行って見て、何度も頭を叩きました。

私はHursley Parkの会議で、IBMの少年たちがこのブランドの新しい素晴らしさを話してくれたJava言語は素晴らしかった、誰かだけが尋ねた...なぜこれらのオブジェクトのデストラクタがないのか。彼はC++でデストラクタとして知っていることを意味していませんでしたが、ファイナライザーなしがありました(またはファイナライザーがありましたが、基本的には機能しませんでした)。 Javaはその時点ではちょっとしたおもちゃの言語でした。

現在、彼らはファイナライザーを言語仕様に追加し、Javaが採用されました。

もちろん、GCの速度が大幅に低下したため、後でファイナライザーをオブジェクトに配置しないようにと誰もが言われました。 (GCがアプリの実行を一時停止したため、これらのメソッドを呼び出すことができなかったため、ヒープをロックするだけでなく、ファイナライズするオブジェクトを一時領域に移動する必要がありました。代わりに、次の直前に呼び出されます。 GCサイクル)(さらに悪いことに、アプリのシャットダウン時にファイナライザがまったく呼び出されない場合があります。ファイルハンドルが閉じられていないことを想像してみてください)

次に、C#がありました。この新しいC#言語の素晴らしさを伝えられたMSDNのディスカッションフォーラムを覚えています。誰かがなぜ確定的なファイナライズが存在しないのかと尋ねたところ、MSの少年たちは、そのようなものが必要ではないことを教えてくれ、次に、アプリの設計方法を変える必要があることを教えてくれた。すべての循環参照のために、ゴミとは決して機能しません。次に、彼らはプレッシャーに陥り、このIDisposeパターンを私たちが使用できる仕様に追加したことを私たちに伝えました。その時点では、C#アプリでの手動のメモリ管理にかなり戻っていると思いました。

もちろん、MSの少年たちは後で彼らが私たちに言ったすべてが...であることに気づきました、まあ、彼らはIDisposeを単なる標準のインターフェースよりも少しだけ作って、後でusingステートメントを追加しました。えっ!彼らは決定論的なファイナライズは結局言語に欠けているものであることに気づきました。もちろん、それをどこにでも置くことを覚えておく必要があるので、それはまだ少し手作業ですが、それはより良いです。

それでは、最初から各スコープブロックにusingスタイルのセマンティクスを自動的に配置できたのに、なぜそうしたのでしょうか。おそらく効率的ですが、私はそれらが実現しなかっただけだと思いたいです。ついに.NET(Google SafeHandle)でスマートポインターがまだ必要であることに気付いたように、GCはすべての問題を実際に解決すると考えていました。彼らは、オブジェクトが単なるメモリではなく、GCが主にメモリ管理を処理するように設計されていることを忘れていました。彼らは、GCがこれを処理するという考えに追いついて、そこに他のものを入れるのを忘れていました。オブジェクトは、しばらくの間削除しなくても関係のない単なるメモリの塊ではありません。

しかし、元のJavaにfinalizeメソッドがなかったため、少し問題があった-作成したオブジェクトはすべてメモリに関するものであり、他のものを削除したい場合( DBハンドルやソケットなどのように)手動で行うが期待されていました。

Javaは、多くの手動割り当てを使用してCコードを作成することに慣れている組み込み環境用に設計されているため、自動解放がないことはそれほど問題ではありませんでした。なぜJavaでそれが必要なのでしょうか?問題はスレッドやスタック/ヒープに関係するものではなく、おそらくメモリの割り当て(つまり、割り当て解除)を少し簡単にするためだけにありました。最後に、ステートメントはおそらく非メモリリソースを処理するためのより良い場所です。

つまり、.NETがJavaの最大の欠陥を単純にコピーした方法は、その最大の弱点です。 .NETは、より優れたJavaではなく、より優れたC++である必要があります。

14
gbjbaanb

「Thinking in Java」と「Thinking in C++」の著者であり、C++標準委員会のメンバーであるBruce Eckelは、多くの分野(RAIIだけでなく)で、ゴスリングとJavaチーム 宿題をしなかった。

...言語がいかに不快で複雑であり、同時にうまく設計されているかを理解するには、C++のすべての要素がかかった主要な設計上の決定、つまりC. Stroustrupとの互換性が決定したことを覚えておく必要があります。 、それは表示されます-Cプログラマの大衆をオブジェクトに移動させる方法は、移動を透過的にすることでした:C++の下でCコードを変更せずにコンパイルできるようにすることです。これは大きな制約であり、常にC++の最大の強みであり...そしてその悩みの種です。それがC++を以前と同じように成功させ、現在のように複雑にした理由です。

また、C++を十分に理解していないJavaデザイナーをだましました。たとえば、演算子のオーバーロードはプログラマーが適切に使用するには難しすぎると考えていました。C++ではスタック割り当てとヒープ割り当ての両方です。すべての状況を処理し、メモリリークを引き起こさないように、オペレーターをオーバーロードする必要があります。ただし、実際には困難です。ただし、Javaには単一のストレージ割り当てメカニズムとガベージコレクターがあり、オペレーターのオーバーロードを簡単にします。 C#で(ただし、Javaに先行するPythonですでに表示されていました。)しかし、長年の間、Javaチームからの部分的な行は、「演算子のオーバーロードが複雑すぎる」でした。これと他の多くの誰かが明らかに宿題をしなかったという決定は、ゴスリングとJavaチームが行った選択の多くを放棄することで評判がある理由です。

他にもたくさんの例があります。プリミティブは「効率化のために含まれている必要があった」。正しい答えは、「すべてがオブジェクトである」に忠実であり、効率が必要なときに低レベルのアクティビティを実行するためのトラップドアを提供することです(これにより、ホットスポットテクノロジーが透過的に効率を上げることができ、最終的には効率が向上します。持ってる)。そして、浮動小数点プロセッサを直接使用して超越関数を計算することはできません(代わりにソフトウェアで行われます)。私はこのような問題について私が我慢できる限り書いてきましたが、私が聞く答えは常に「これはJavaの方法です」という効果に対するトートロジー的な回答でした。

ジェネリックがいかにひどく設計されたかについて書いたとき、「Javaで行われた以前の(悪い)決定との下位互換性がなければならない」とともに、同じ応答を得ました。最近、ますます多くの人々がGenericsで十分な経験を積んでおり、実際に使用するのは非常に難しいことがわかります。実際、C++テンプレートは、はるかに強力で一貫性があります(コンパイラーのエラーメッセージが許容されるようになったため、はるかに使いやすくなりました)。人々は具体化を真剣に受け止めています-役立つものですが、自分で課した制約によって損なわれているデザインにそれほどへこみを付けません。

リストは、それが退屈なだけのところまで続きます...

11
Gnawme

最良の理由は、ここでのほとんどの回答よりもはるかに単純です。

スタックに割り当てられたオブジェクトを他のスレッドに渡すことはできません。

立ち止まって考えてみてください。考え続ける....誰もがRAIIに熱心になったとき、C++にはスレッドがありませんでした。 Erlang(スレッドごとに別個のヒープ)でさえ、あまりに多くのオブジェクトを渡しすぎると厄介になります。 C++はC++ 2011でのみメモリモデルを取得しました。これで、コンパイラの「ドキュメント」を参照しなくても、C++での同時実行性についてほぼ推論できます。

Javaは(ほぼ)初日から複数のスレッド用に設計されました。

「C++プログラミング言語」の古いコピーはまだ手元にあり、Stroustrupを使用するとスレッドが不要になることが保証されます。

2番目の痛みを伴う理由は、スライスを回避することです。

10
Tim Williscroft

C++では、より汎用的な低レベルの言語機能(デストラクタがスタックベースのオブジェクトで自動的に呼び出される)を使用して高レベルの機能(RAII)を実装します。このアプローチはC#/ Java人々はあまり好きではないようです。特定のニーズに合わせて特定の高レベルツールを設計し、言語に組み込まれた既製のプログラマーに提供したいと考えています。このような特定のツールの問題は、多くの場合、それらをカスタマイズすることは不可能です(一部は、それらを学ぶのが非常に簡単になる理由です)。小さなブロックから構築する場合、より良い解決策が時間とともに発生する可能性がありますが、onlyビルトイン構造体である場合、これはあまり起こりません。

ええ、私はそう思います(実際にはそこにいませんでした...)言語の選択を容易にすることを目標とする、それは意識的な決定でしたが、私の意見では、それは悪い決定でした。繰り返しになりますが、私は一般に、C++のプログラマーにチャンスを与えるという独自の哲学を好むので、少し偏っています。

5
imre