0

私は有理数のクラスを実装していますが、問題と問題は、複素数だけでなく、特定の数学的オブジェクトで実行される多数の計算を伴うアプリケーションで使用されることを意図した他のクラスでも本質的に同じです。

JRE と共に配布されるライブラリと多くのサードパーティ ライブラリでは、number クラスは不変です。これには、「equals」と「hashcode」を意図したとおりに確実に一緒に実装できるという利点があります。これにより、さまざまなコレクションでインスタンスをキーと値の両方として使用できるようになります。実際、コレクションでの信頼できる操作のためには、コレクション内のキー値としてのインスタンスの存続期間全体にわたる不変性を維持する必要があります。

しかし、クラスの設計が (言語の制限内で) 不変性を強制する場合、単純な数学演算を実行する場合でも、数式は過剰なオブジェクト割り当てとその後のガベージ コレクションの負担になります。複雑な計算で繰り返し発生するものの明示的な例として、次のことを検討してください。

Rational result = new Rational( 13L, 989L ).divide( new Rational( -250L, 768L ) );

この式には 3 つの割り当てが含まれており、そのうちの 2 つはすぐに破棄されます。オーバーヘッドの一部を回避するために、クラスは通常、一般的に使用される「定数」を事前に割り当て、頻繁に使用される「数値」のハッシュ テーブルを保持することさえあります。もちろん、このようなハッシュ テーブルは、必要なすべての不変オブジェクトを単純に割り当て、Java コンパイラと JVM に依存してヒープをできるだけ効率的に管理するよりもパフォーマンスが低い可能性があります。

別の方法は、可変インスタンスをサポートするクラスを作成することです。クラスのメソッドを流暢なスタイルで実装することにより、「結果」として「divide」メソッドから返される 3 番目のオブジェクトを割り当てることなく、上記と機能的に同様の簡潔な式を評価することができます。繰り返しますが、これはこの 1 つの式では特に重要ではありません。ただし、行列を操作して複雑な線形代数の問題を解決することは、不変のインスタンスを操作するよりも、可変オブジェクトとしてより適切に処理される数学的オブジェクトのより現実的なケースです。また、有理数の行列の場合、可変有理数クラスは、はるかに簡単に正当化されるように思われます。

以上のことから、関連する 2 つの質問があります。

  1. Sun/Oracle Java コンパイラ、JIT、または JVM について、変更可能なクラスよりも不変の有理数または複素数クラスを決定的に推奨するものはありますか?

  2. そうでない場合、可変クラスを実装するときに「ハッシュコード」をどのように処理する必要がありますか? 私は、誤用されやすい実装や不必要なデバッグ セッションを提供するのではなく、サポートされていない操作の例外をスローすることによって「フェイル ファスト」する傾向があります。リスト。

テストコード:

私が実装する必要があるものとほぼ同様の計算を実行するときに、不変の数値が重要かどうか疑問に思っている人のために:

import java.util.Arrays;

public class MutableOrImmutable
{
    private int[] pseudomatrix = { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
                                   0, 1, 0, 0, 0, 0, 0, 0, 0, 0,
                                   0, 0, 1, 0, 0, 0, 0, 0, 0, 0,
                                   0, 0, 0, 1, 0, 0, 0, 0, 0, 0,
                                   0, 0, 0, 0, 1, 0, 0, 0, 0, 0,
                                   0, 0, 0, 0, 0, 1, 0, 0, 0, 0,
                                   0, 0, 0, 0, 0, 0, 1, 0, 0, 0,
                                   0, 0, 0, 0, 0, 0, 0, 1, 0, 0,
                                   0, 0, 0, 0, 0, 0, 0, 0, 1, 0,
                                   0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
                                   1, 2, 0, 0, 0, 0, 0, 0, 0, 0,
                                   0, 0, 3, 4, 0, 0, 0, 0, 0, 0,
                                   0, 0, 0, 0, 5, 5, 0, 0, 0, 0,
                                   0, 0, 0, 0, 0, 0, 4, 3, 0, 0,
                                   0, 0, 0, 0, 0, 0, 0, 0, 2, 1 };

    private int[] scalars = { 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };

    private static final int ITERATIONS = 500;

    private void testMutablePrimitives()
    {
        int[] matrix = Arrays.copyOf( pseudomatrix, pseudomatrix.length );

        long startTime = System.currentTimeMillis();

        for ( int iteration = 0 ; iteration < ITERATIONS ; ++iteration )
        {
            for ( int scalar : scalars )
            {
                for ( int index = 0 ; index < matrix.length ; ++index )
                {
                    matrix[ index ] *= scalar;
                }
            }

            for ( int scalar : scalars )
            {
                for ( int index = 0 ; index < matrix.length ; ++index )
                {
                    matrix[ index ] /= scalar;
                }
            }
        }

        long stopTime    = System.currentTimeMillis();
        long elapsedTime = stopTime - startTime;

        System.out.println( "Elapsed time for mutable primitives: " + elapsedTime );

        assert Arrays.equals( matrix, pseudomatrix ) : "The matrices are not equal.";
    }

    private void testImmutableIntegers()
    {
        // Integers are autoboxed and autounboxed within this method.

        Integer[] matrix = new Integer[ pseudomatrix.length ];

        for ( int index = 0 ; index < pseudomatrix.length ; ++index )
        {
            matrix[ index ] = pseudomatrix[ index ];
        }

        long startTime = System.currentTimeMillis();

        for ( int iteration = 0 ; iteration < ITERATIONS ; ++iteration )
        {
            for ( int scalar : scalars )
            {
                for ( int index = 0 ; index < matrix.length ; ++index )
                {
                    matrix[ index ] = matrix[ index ] * scalar;
                }
            }

            for ( int scalar : scalars )
            {
                for ( int index = 0 ; index < matrix.length ; ++index )
                {
                    matrix[ index ] = matrix[ index ] / scalar;
                }
            }
        }

        long stopTime    = System.currentTimeMillis();
        long elapsedTime = stopTime - startTime;

        System.out.println( "Elapsed time for immutable integers: " + elapsedTime );

        for ( int index = 0 ; index < matrix.length ; ++index )
        {
            if ( matrix[ index ] != pseudomatrix[ index ] )
            {
                // When properly implemented, this message should never be printed.

                System.out.println( "The matrices are not equal." );

                break;
            }
        }
    }

    private static class PseudoRational
    {
        private int value;

        public PseudoRational( int value )
        {
            this.value = value;
        }

        public PseudoRational multiply( PseudoRational that )
        {
            return new PseudoRational( this.value * that.value );
        }

        public PseudoRational divide( PseudoRational that )
        {
            return new PseudoRational( this.value / that.value );
        }
    }

    private void testImmutablePseudoRationals()
    {
        PseudoRational[] matrix = new PseudoRational[ pseudomatrix.length ];

        for ( int index = 0 ; index < pseudomatrix.length ; ++index )
        {
            matrix[ index ] = new PseudoRational( pseudomatrix[ index ] );
        }

        long startTime = System.currentTimeMillis();

        for ( int iteration = 0 ; iteration < ITERATIONS ; ++iteration )
        {
            for ( int scalar : scalars )
            {
                for ( int index = 0 ; index < matrix.length ; ++index )
                {
                    matrix[ index ] = matrix[ index ].multiply( new PseudoRational( scalar ) );
                }
            }

            for ( int scalar : scalars )
            {
                for ( int index = 0 ; index < matrix.length ; ++index )
                {
                    matrix[ index ] = matrix[ index ].divide( new PseudoRational( scalar ) );
                }
            }
        }

        long stopTime    = System.currentTimeMillis();
        long elapsedTime = stopTime - startTime;

        System.out.println( "Elapsed time for immutable pseudo-rational numbers: " + elapsedTime );

        for ( int index = 0 ; index < matrix.length ; ++index )
        {
            if ( matrix[ index ].value != pseudomatrix[ index ] )
            {
                // When properly implemented, this message should never be printed.

                System.out.println( "The matrices are not equal." );

                break;
            }
        }
    }

    private static class PseudoRationalVariable
    {
        private int value;

        public PseudoRationalVariable( int value )
        {
            this.value = value;
        }

        public void multiply( PseudoRationalVariable that )
        {
            this.value *= that.value;
        }

        public void divide( PseudoRationalVariable that )
        {
            this.value /= that.value;
        }
    }

    private void testMutablePseudoRationalVariables()
    {
        PseudoRationalVariable[] matrix = new PseudoRationalVariable[ pseudomatrix.length ];

        for ( int index = 0 ; index < pseudomatrix.length ; ++index )
        {
            matrix[ index ] = new PseudoRationalVariable( pseudomatrix[ index ] );
        }

        long startTime = System.currentTimeMillis();

        for ( int iteration = 0 ; iteration < ITERATIONS ; ++iteration )
        {
            for ( int scalar : scalars )
            {
                for ( PseudoRationalVariable variable : matrix )
                {
                    variable.multiply( new PseudoRationalVariable( scalar ) );
                }
            }

            for ( int scalar : scalars )
            {
                for ( PseudoRationalVariable variable : matrix )
                {
                    variable.divide( new PseudoRationalVariable( scalar ) );
                }
            }
        }

        long stopTime    = System.currentTimeMillis();
        long elapsedTime = stopTime - startTime;

        System.out.println( "Elapsed time for mutable pseudo-rational variables: " + elapsedTime );

        for ( int index = 0 ; index < matrix.length ; ++index )
        {
            if ( matrix[ index ].value != pseudomatrix[ index ] )
            {
                // When properly implemented, this message should never be printed.

                System.out.println( "The matrices are not equal." );

                break;
            }
        }
    }

    public static void main( String [ ] args )
    {
        MutableOrImmutable object = new MutableOrImmutable();

        object.testMutablePrimitives();
        object.testImmutableIntegers();
        object.testImmutablePseudoRationals();
        object.testMutablePseudoRationalVariables();
    }
}

脚注:

可変クラスと不変クラスの中心的な問題は、非常に疑わしいObjectの「ハッシュコード」メソッドです。

hashCode の一般的な契約は次のとおりです。

  • Java アプリケーションの実行中に同じオブジェクトに対して複数回呼び出された場合は常に、オブジェクトの equals 比較で使用される情報が変更されていない限り、hashCode メソッドは一貫して同じ整数を返す必要があります。この整数は、あるアプリケーションの実行から同じアプリケーションの別の実行まで一貫性を保つ必要はありません。

  • equals(Object) メソッドに従って 2 つのオブジェクトが等しい場合、2 つのオブジェクトのそれぞれで hashCode メソッドを呼び出すと、同じ整数結果が生成される必要があります。

  • equals(java.lang.Object) メソッドに従って 2 つのオブジェクトが等しくない場合、2 つのオブジェクトのそれぞれで hashCode メソッドを呼び出すと、異なる整数結果が生成される必要はありません。ただし、プログラマーは、等しくないオブジェクトに対して個別の整数結果を生成すると、ハッシュテーブルのパフォーマンスが向上する可能性があることに注意する必要があります。

しかし、オブジェクトがコレクションに追加されると、「同等性」を判断するために使用される内部状態から派生したハッシュ コードの値に依存するようになると、状態が変化したときにコレクションに適切にハッシュされなくなります。はい、可変オブジェクトがコレクションに不適切に格納されないようにすることはプログラマーの負担ですが、可変クラスの不適切な使用が最初から防止されない限り、メンテナンス プログラマーの負担はさらに大きくなります。これが、可変オブジェクトの「ハッシュコード」に対する正しい「答え」は、オブジェクトの等価性を判断するために「equals」を実装しながら、常に UnsupportedOperationException をスローすることだと私が信じている理由です。セットに追加します。でも、例外をスローすることは、上記の「契約」に違反し、それ自体が悲惨な結果をもたらすという議論があるかもしれません。その場合、可変クラスのすべてのインスタンスを同じ値にハッシュすることが、実装の性質が非常に悪いにもかかわらず、コントラクトを維持する「正しい」方法である可能性があります。おそらくクラス名のハッシュから生成された定数値を返すことは、例外をスローするよりも推奨されていますか?

4

3 に答える 3

0

有用なパターンの 1 つは、「読み取り可能な」ものに対して抽象型またはインターフェイスを定義し、それを可変形式と不変形式の両方にすることです。このパターンは、基本型またはインターフェイス型にAsMutableAsNewMutable、およびAsImmutable派生オブジェクトで適切な方法でオーバーライドできるメソッドが含まれている場合に特に便利です。このようなアプローチにより、必要に応じて可変性の利点を実現できると同時に、不変型を使用する利点も得られます。値を保持したいが変更したくないコードは、変更可能な型で機能する場合は「防御的コピー」を使用する必要がありますがAsImmutable、「読み取り可能な」ものを受け取った場合は代わりに使用できます。物事がたまたま可変であればコピーを作成しますが、不変であればコピーは必要ありません。

ちなみに、実際のデータを保持する大きなオブジェクトへの参照以外のフィールドが比較的少ない不変型を設計している場合、およびその型のものが頻繁に等しいかどうか比較される場合は、各型に一意のシーケンス番号と、それが等しいことがわかっている最も古いインスタンス (存在する場合) への参照 (または、古いインスタンスが存在しないことがわかっている場合は null)。2 つのインスタンスが等しいかどうかを比較するときは、それぞれに一致することがわかっている最も古いインスタンスを特定します (null になるまで、最も古い既知のインスタンスを再帰的にチェックします)。両方のインスタンスが同じインスタンスに一致することがわかっている場合、それらは等しいです。そうでない場合でも、それらが等しいことが判明した場合は、「古いインスタンス」の方が若い方が、他のインスタンスと等しい古いインスタンスと見なす必要があります。

于 2013-11-22T17:59:33.753 に答える