代码之家  ›  专栏  ›  技术社区  ›  Ned

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

  •  0
  • Ned  · 技术社区  · 12 年前

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

    在与JRE一起分发的库和许多第三方库中,数字类是不可变的。这样做的优点是“equals”和“hashcode”可以按预期可靠地一起实现。这将使实例能够在各种集合中同时用作键和值。事实上,为了在集合上进行可靠的操作,必须维护实例在其整个生命周期中作为集合中的键值的不变性。如果类在创建实例后阻止可能改变hashcode方法所依赖的内部状态的操作,那么这一点将得到更有力的维护,而不是让代码的开发人员和后续维护人员遵守在修改实例状态之前从集合中删除实例,然后将实例添加回任意一个它们必须属于的集合。

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

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

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

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

    综上所述,我有两个相关的问题:

    1. 有没有关于Sun/Oracle Java编译器、JIT或JVM的任何东西会最终推荐不可变的有理数或复数类而不是可变类?

    2. 如果没有,那么在实现可变类时应该如何处理“hashcode”?我倾向于通过抛出不受支持的操作异常来“快速失败”,而不是提供一个容易被滥用和不必要的调试会话的实现,或者一个即使不可变对象的状态发生变化也很健壮的实现,但它本质上会将散列表变成链表。

    测试代码:

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

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

    脚注:

    可变类与不可变类的核心问题是上的“hashcode”方法 Object :

    hashCode的总合同是:

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

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

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

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

    3 回复  |  直到 12 年前
        1
  •  0
  •   Conor O'Neill    12 年前

    您曾写道:“即使在执行简单的数学运算时,数学表达式也会被过度的对象分配和随后的垃圾收集所拖累。” 以及“表达式包括三个分配,其中两个被快速丢弃”。

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

    例如,请参阅本白皮书: http://www.oracle.com/technetwork/java/whitepaper-135217.html#garbage 在“世代复制集合”下,它指出:

    “…首先,由于新对象是在对象库中以类似堆栈的方式连续分配的,因此分配变得非常快,因为它只需要更新一个指针并执行一次库溢出检查。其次,当库溢出时,库中的大多数对象已经死了,这使得垃圾收集器可以简单地移动少量的survi将物体放置在其他地方,并避免对苗圃中的死物体进行任何回收工作。"

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

        2
  •  0
  •   supercat    12 年前

    一种有用的模式是为“可读”的东西定义一个抽象类型或接口,然后有可变和不可变的形式。如果基本类型或接口类型包括 AsMutable , AsNewMutable AsImmutable 可以在派生对象中以适当的方式重写的方法。这种方法允许人们在需要时实现可变性的好处,同时也可以获得使用不可变类型的好处。想要持久化值而不发生突变的代码,如果使用可变类型,则必须使用“防御性复制”,但可以使用 不可变的 如果它收到一个“可读”的东西。如果这个东西碰巧是可变的,它会生成一个副本,但如果它是不可变的,就不需要复制了。

    顺便说一句,如果设计一个不可变类型,除了引用一个保存实际数据的大对象之外,字段相对较少,并且如果经常比较该类型的内容是否相等,那么让每个类型保存一个唯一的序列号以及引用最旧的实例(如果有的话)可能会有所帮助,已知其等于(或者如果不存在旧实例则为空)。当比较两个实例是否相等时,确定已知与每个实例匹配的最旧实例(递归检查已知最旧实例,直到其为null)。如果已知两个实例与同一实例匹配,则它们是相等的。如果不是,但事实证明它们是平等的,那么无论哪个“老实例”年轻,都应该将另一个视为与其平等的老实例。这种方法将产生与interning一样的加速比较,但不需要使用单独的interning字典,也不需要对值进行散列。

        3
  •  0
  •   Ned    12 年前

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

    当然,如果我最终需要实现“equals”来测试矩阵等式,那么当对该方法的可能需求极不可能时,我将回到矩阵“hashcode”的相同问题。这让我再次回到了一个相当无用的抱怨,即“hashcode”——可能还有“equals”——一开始就不应该成为java.lang.Object合约的一部分。。。