0

我正在为有理数实现类,但对于复数以及其他旨在用于在给定数学对象上执行大量计算的应用程序的类,问题和问题基本相同。

在随 JRE 分发的库和许多第三方库中,数字类是不可变的。这样做的好处是“equals”和“hashcode”可以按预期可靠地一起实现。这将使实例能够在各种集合中用作键和值。事实上,必须保持实例在其整个生命周期中作为集合中的键值的不变性,以便对集合进行可靠操作。

然而,如果类设计在语言的限制范围内强制执行不变性,那么在执行即使是简单的数学运算时,数学表达式也会因过多的对象分配和随后的垃圾收集而负担。考虑以下作为复杂计算中重复发生的明确示例:

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

该表达式包括三个分配——其中两个被迅速丢弃。为了避免一些开销,类通常预先分配常用的“常量”,甚至可能维护常用“数字”的哈希表。当然,与简单地分配所有必要的不可变对象并依靠 Java 编译器和 JVM 尽可能高效地管理堆相比,这样的哈希表的性能可能会更低。

另一种方法是创建支持可变实例的类。通过以流利的风格实现类的方法,可以在功能上与上述类似的简洁表达式求值,而无需分配从“divide”方法返回的第三个对象作为“结果”。同样,这对于这个表达式并不是特别重要。但是,通过对矩阵进行运算来解决复杂的线性代数问题对于数学对象来说是一种更现实的情况,这些数学对象可以更好地处理为可变对象,而不是必须对不可变实例进行运算。对于有理数矩阵,可变有理数类似乎更容易证明是合理的。

尽管如此,我有两个相关的问题:

  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”方法:

hashCode 的一般合约是:

  • 每当在 Java 应用程序执行期间对同一个对象多次调用它时,hashCode 方法必须始终返回相同的整数,前提是没有修改对象上的 equals 比较中使用的信息。该整数不需要从应用程序的一次执行到同一应用程序的另一次执行保持一致。

  • 如果两个对象根据 equals(Object) 方法相等,则对两个对象中的每一个调用 hashCode 方法必须产生相同的整数结果。

  • 如果根据 equals(java.lang.Object) 方法,如果两个对象不相等,则不需要对两个对象中的每一个调用 hashCode 方法都必须产生不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同的整数结果可能会提高哈希表的性能。

但是,一旦一个对象被添加到一个依赖于其内部状态的哈希码的值的集合中,用于确定“相等性”,当它的状态发生变化时,它就不再正确地散列到集合中。是的,程序员有责任确保可变对象不会不正确地存储在集合中,但维护程序员的负担甚至更大,除非首先不防止不正确地使用可变类。这就是为什么我相信可变对象上“hashcode”的正确“答案”是始终抛出 UnsupportedOperationException,同时仍然实现“equals”来确定对象相等性——想想你想要比较是否相等的矩阵,但永远不会想到添加到集合。然而,可能有人认为抛出异常违反了上述“合同”,并带来了可怕的后果。在这种情况下,将可变类的所有实例散列到相同的值可能是维护合同的“正确”方式,尽管实现的性质很差。是否建议返回一个常量值(可能是通过散列类名生成)而不是抛出异常?

4

3 回答 3

0

您曾写道:“即使在执行简单的数学运算时,数学表达式也会因过多的对象分配和后续的垃圾收集而负担。” 和“该表达式包括三个分配——其中两个被迅速丢弃”。

现代垃圾收集器实际上针对这种分配模式进行了优化,因此您(隐式)假设分配和随后的垃圾收集是昂贵的,这是错误的。

例如,请参阅此白皮书: http ://www.oracle.com/technetwork/java/whitepaper-135217.html#garbage 在“Generational Copying Collection”下,它指出:

“...首先,由于新对象在对象苗圃中以类似堆栈的方式连续分配,因此分配变得非常快,因为它仅涉及更新单个指针并执行单个检查苗圃溢出。其次,到托儿所溢出,托儿所中的大部分对象已经死亡,让垃圾收集器可以简单地将少数幸存的对象移到其他地方,而避免为托儿所中的死物做任何回收工作。”

因此,我建议您真正问题的答案是您应该使用不可变对象,因为感知成本根本不是真正的成本,但感知的好处(例如简单性、代码可读性)是真正的好处。

于 2013-10-24T20:35:26.230 回答
0

一种可能有用的模式是为“可读”事物定义抽象类型或接口,然后具有可变和不可变形式。AsMutable如果基类或接口类型包括、AsNewMutableAsImmutable可以在派生对象中以适当方式覆盖的方法,则此模式可能特别好。这种方法允许人们在需要时获得可变性的好处,同时也获得使用不可变类型的好处。想要保留一个值但不改变它的代码如果它与可变类型一起使用,则必须使用“防御性复制”,但AsImmutable如果它接收到“可读”的东西则可以改为使用。如果事物碰巧是可变的,它会创建一个副本,但如果它是不可变的,则不需要复制。

顺便说一句,如果一个人正在设计一个不可变类型,除了对保存实际数据的大对象的引用之外,它的字段相对较少,并且如果经常比较该类型的内容是否相等,那么让每个类型都保存一个唯一的序列号以及对已知与其相等的最旧实例(如果有)的引用(如果不知道旧实例存在,则为 null)。在比较两个实例是否相等时,确定已知与每个实例匹配的最旧实例(递归检查最旧的已知实例,直到它为空)。如果已知两个实例都匹配同一个实例,则它们是相等的。如果不是,但结果是相等的,那么无论哪个“旧实例”更年轻,都应该将另一个视为与其相等的旧实例。

于 2013-11-22T17:59:33.753 回答
0

目前,我正在用不可变对象实现有理数。这允许重用在我需要执行的计算中经常出现的零和一对象。然而,用有理数元素实现的 Matrix 类是可变的——甚至在内部使用 null 作为“虚拟”零。由于更迫切地需要无缝处理“小”有理数和任意精度的“大”有理数,在我有时间分析我为此目的可用的问题库以确定不可变实现之前,现在可以接受不可变实现无论是可变对象还是更大的“通用”不可变对象集都会胜出。

当然,如果我最终需要实现“等于”来测试矩阵相等性,那么当极不可能需要该方法时,我将回到与 Matrix“哈希码”相同的问题。这让我又回到了一个相当无用的抱怨上,即“哈希码”——也可能是“等于”——一开始就不应该成为 java.lang.Object 合同的一部分......

于 2013-12-11T01:54:07.950 回答