首页 文章

在Java中实现数字系统:可变与不可变

提问于
浏览
0

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

在随JRE一起分发的库中以及许多第三方库中,数字类是不可变的 . 这具有如下优点:“等于”和“哈希码”可以按预期可靠地一起实现 . 这将使实例可以用作各种集合中的键和值 . 实际上,必须保持实例在其整个生命周期中作为集合中的关键值的不变性,以便对集合进行可靠的操作 . 如果类阻止了一旦创建实例就可能改变hashcode方法所依赖的内部状态的操作,而不是让代码的开发人员和后续维护者遵守删除约定的操作,则可以更好地维护这一点 . 在修改状态之前从集合中添加实例,然后将实例添加回它们必须属于的任何集合 .

然而,如果类设计强制执行 - 在语言的限制范围内 - 不变性,则数学表达式在执行甚至简单的数学运算时会受到过多的对象分配和随后的垃圾收集的负担 . 请考虑以下内容作为复杂计算中重复出现的内容的明确示例:

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

该表达式包括三个分配 - 其中两个被快速丢弃 . 为了避免一些开销,类通常预先分配常用的“常量”,甚至可以维护常用“数字”的哈希表 . 当然,这样的哈希表可能不如简单地分配所有必需的不可变对象并依赖Java编译器和JVM来尽可能有效地管理堆 .

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

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

  • 是否有关于Sun / Oracle Java编译器,JIT或JVM的任何内容,它们最终会推荐可变类的不可变理性或复数类?

  • 如果没有,在实现可变类时应如何处理“hashcode”?我倾向于通过抛出一个不受支持的操作异常来“快速失败”,而不是提供易于滥用的实现和不必要的调试会话,或者即使在不可变对象的状态发生变化时也是健壮的,但这实际上将哈希表转换为链接名单 .

Test Code:

对于那些想知道在执行大致类似于我需要实现的计算时不可变数字是否重要的人:

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();
    }
}

Footnote:

可变类和不可变类的核心问题是Object上的 - 非常值得怀疑的"hashcode"方法:

hashCode的一般 Contract 是:每当在Java应用程序的执行期间多次在同一对象上调用它时,hashCode方法必须始终返回相同的整数,前提是不修改对象的equals比较中使用的信息 . 从应用程序的一次执行到同一应用程序的另一次执行,该整数不需要保持一致 . 如果两个对象根据equals(Object)方法相等,则对两个对象中的每一个调用hashCode方法必须生成相同的整数结果 . 如果两个对象根据equals(java.lang.Object)方法不相等,则不需要在两个对象中的每一个上调用hashCode方法必须生成不同的整数结果 . 但是,程序员应该知道为不等对象生成不同的整数结果可能会提高哈希表的性能 .

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

3 回答

  • 0

    你写道:“在进行简单的数学运算时,数学表达式会因过多的对象分配和随后的垃圾收集而变得沉重 . ”和“表达式包括三个分配 - 其中两个被迅速丢弃” .

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

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

    “......首先,因为新对象在对象托儿所中以类似堆栈的方式连续分配,所以分配变得非常快,因为它只涉及更新单个指针并对托儿所溢出执行单个检查 . 其次,到时候托儿所溢出,托儿所中的大多数对象已经死亡,允许垃圾收集器简单地将少数幸存的对象移动到其他地方,并避免对托儿所中的死对象进行任何回收工作 .

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

  • 0

    目前,我正在使用不可变对象实现有理数 . 这允许在我需要执行的计算中频繁重复使用ZERO和ONE对象 . 但是,使用有理数元素实现的Matrix类是可变的 - 甚至在内部对“虚拟”零使用null . 由于需要无缝地处理“小”有理数和任意精度的“大”有理数,所以现在可以接受不可变实现,直到我有时间描述我可用于此目的的库的问题以便确定是否可变对象或更大的“常见”不可变对象集合将赢得胜利 .

    当然,如果我最终需要实现“等于”来测试Matrix相等性,那么当对方法的可能需求极不可能时,我将回到Matrix“hashcode”的相同问题 . 这让我再次回到“hashcode” - 而且可能“等于”的相当无用的抱怨 - 首先应该永远不会成为java.lang.Object Contract 的一部分......

  • 0

    一种可能有用的模式是为"readable"事物定义抽象类型或接口,然后同时具有它的可变形式和不可变形式 . 如果基类型或接口类型包含 AsMutableAsNewMutableAsImmutable 方法,则可以在派生对象中以适当的方式覆盖此模式 . 这种方法允许人们在需要时实现可变性的好处,并且还具有使用不可变类型的好处 . 想要保留值但不变异的代码必须使用"defensive copying",如果它使用可变类型,但如果它收到"readable"事件,则可以使用 AsImmutable . 如果事情恰好是可变的,它会复制,但如果它是不可变的,则不需要复制 .

    顺便提一下,如果设计一个不可变类型,除了对保存实际数据的大对象的引用之外,其他字段相对较少,并且如果经常比较类型的事物是否相等,那么让每个类型保持一个类型可能会有所帮助 . 唯一序列号以及对已知最旧实例(如果有)的引用(如果已知不存在旧实例,则为null) . 比较两个实例的相等性时,确定已知匹配每个实例的最旧实例(递归检查最早的已知实例,直到它为空) . 如果已知两个实例匹配相同的实例,则它们是相等的 . 如果没有,但结果是相同的,那么无论哪个“较老的实例”更年轻,都应该将另一个视为一个与之相等的旧实例 . 该方法将产生加速比较,就像实习一样,但不使用单独的实习字典,并且不必哈希值 .

相关问题