用Java实现数字系统:可变与不可变 [英] Implementing a Number System in Java: Mutable vs. Immutable

查看:73
本文介绍了用Java实现数字系统:可变与不可变的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I am implementing classes for rational numbers, but the problems and issues are essentially the same for complex numbers as well as other classes intended to be used in applications with a significant number of calculations performed on a given mathematical object.

在与JRE一起分发的库中以及许多第三方库中,数字类是不可变的.这样做的好处是,"equals"和"hashcode"可以按预期可靠地一起实现.这将使实例既可以用作各种集合中的键又可以用作值.实际上,必须在实例的整个生命周期中将其作为集合中的键值进行不变,以确保对该集合的可靠操作.如果该类阻止一旦创建实例就阻止可能改变哈希码方法所依赖的内部状态的操作,则比将其留给开发人员和代码的后续维护者遵守删除约定的做法要强得多.集合中的实例在修改其状态之前,然后将其添加回它们必须属于的任何集合中.

In the libraries distributed with the JRE and in many third-party libraries, number classes are immutable. This has the advantage that "equals" and "hashcode" can be reliably implemented together as intended. This will enable instances to be used as both keys and values in various collections. In fact, immutability of an instance throughout its lifetime as a key value in a collection must be maintained for reliable operations on the collection. This is much more robustly maintained if the class prevents operations which may alter the internal state on which the hashcode method relies once an instance is created than if it is left up to the developer and subsequent maintainers of the code to abide by the convention of removing instances from collections before modifying their state followed by adding the instances back to whichever collections they must belong.

但是,如果类设计在语言的范围内实现了不变性,则即使执行简单的数学运算,数学表达式也将负担过多的对象分配和后续的垃圾回收.考虑以下作为在复杂计算中反复发生的事情的明确示例:

Yet, if the class design enforces -- within the limits of the language -- immutability, mathematical expressions become burdened with excessive object allocation and subsequent garbage collection when performing even simple mathematical operations. Consider the following as an explicit example of what occurs repeatedly in complex computations:

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

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

The expression includes three allocations -- two of which are quickly discarded. To avoid some of the overhead, classes typically preallocate commonly used "constants" and may even maintain a hash table of frequently used "numbers." Of course, such a hash table would likely be less performant than simply allocating all of the necessary immutable objects and relying on the Java compiler and JVM to manage the heap as efficiently as possible.

另一种方法是创建支持可变实例的类.通过以流畅的方式实现类的方法,可以评估功能上与上述类似的简洁表达式,而无需将要从"divide"方法返回的第三个对象分配为"result".同样,这对于这一表达而言不是特别重要.但是,对于数学对象而言,通过对矩阵进行运算来解决复杂的线性代数问题是一种更为现实的情况,因为数学对象可以更好地处理为可变对象,而不必对不变实例进行运算.对于有理数矩阵,可变的有理数类似乎更容易辩解.

The alternative is to create classes which support mutable instances. By implementing the methods of the classes in a fluent style, it is possible to evaluate concise expressions functionally similar to the above without allocating a third object to be returned from the "divide" method as the "result." Again, this is not particularly significant for this one expression. However, solving complex linear algebra problems by operating on matrices is a more realistic case for mathematical objects which are better processed as mutable objects rather than having to operate on immutable instances. And for matrices of rational numbers, a mutable rational number class would seem to be much more easily justified.

所有这些,我有两个相关的问题:

With all that, I have two related questions:

  1. 关于Sun/Oracle Java编译器,JIT或JVM,有什么可以肯定地建议在可变类上使用不可变的有理数或复数类吗?

  1. Is there anything about the Sun/Oracle Java compiler, JIT, or JVM which would conclusively recommend immutable rational or complex number classes over mutable classes?

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

If not, how should "hashcode" be handled when implementing mutable classes? I am inclined to "fail-fast" by throwing an unsupported operation exception rather than providing either an implementation prone to misuse and unnecessary debugging sessions or one which is robust even when the state of immutable objects change, but which essentially turns hash tables into linked lists.

测试代码:

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

For those wondering whether immutable numbers matter when performing calculations roughly similar to those I need to implement:

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

脚注:

可变类和不可变类的核心问题是

The core problem with mutable vs. immutable classes is the -- highly questionable --"hashcode" method on Object:

hashCode的一般约定为:

The general contract of hashCode is:

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

  • Whenever it is invoked on the same object more than once during an execution of a Java application, the hashCode method must consistently return the same integer, provided no information used in equals comparisons on the object is modified. This integer need not remain consistent from one execution of an application to another execution of the same application.

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

If two objects are equal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result.

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

It is not required that if two objects are unequal according to the equals(java.lang.Object) method, then calling the hashCode method on each of the two objects must produce distinct integer results. However, the programmer should be aware that producing distinct integer results for unequal objects may improve the performance of hashtables.

但是,一旦将对象添加到依赖于其内部代码(用于确定相等性"的内部状态)的哈希码值而添加到集合中,则在其状态更改时,它不再正确地哈希到集合中.是的,程序员要确保不将可变对象存储在集合中,这是负担,但是维护程序员的负担更大,除非首先要防止不正确使用可变类.这就是为什么我认为可变对象上哈希码"的正确答案"是总是抛出UnsupportedOperationException,同时仍然实现等于"来确定对象是否相等-想想要比较相等的矩阵,但永远不会想到添加到集合中.但是,可能有一个论点,即抛出异常是对上述合同"的违反,并带来其自身的可怕后果.在那种情况下,尽管实现的性质很差,但将可变类的所有实例哈希化为相同的值可能是维护合同的正确"方法.是否建议返回一个常量值(可能是通过对类名进行哈希处理而生成的),而不是抛出异常?

But once an object is added to a collection dependent on the value of its hash code derived from its internal state used to determine "equality," it is no longer properly hashed into the collection when it's state changes. Yes, the burden is on the programmer to ensure that mutable objects are not improperly stored in collections, but the burden is even greater on the maintenance programmer unless improper use of a mutable class is not prevented in the first place. This is why I believe the right "answer" for "hashcode" on mutable objects is to always throw an UnsupportedOperationException while still implementing "equals" to determine object equality -- think of matrices which you want to compare for equality, but would never think to add to a Set. However, there may be an argument that throwing an exception is a violation of the above "contract" with dire consequences of its own. In that case, hashing all instances of a mutable class to the same value may be the "correct" way to maintain the contract despite the very poor nature of the implementation. Is returning a constant value -- perhaps generated from hashing the class name -- recommended over throwing an exception?

推荐答案

当前,我正在使用不可变对象实现有理数.这允许大量重用零和一个对象,而这在我需要执行的计算中经常发生.但是,用有理数元素实现的Matrix类是可变的-甚至可以在内部将null用作虚拟"零.迫切需要无缝处理小"有理数和任意精度大"有理数,因此直到我有时间描述为此目的可用的问题库之前,不可变的实现现在是可以接受的可变对象还是更大的常见"不可变对象集将赢得胜利.

Currently, I am implementing the rational numbers with immutable objects. This allows heavy reuse of ZERO and ONE objects which occur frequently in the computations I need to perform. However, the Matrix class which is implemented with rational number elements is mutable -- going so far as to use null internally for "virtual" zeros. With a more pressing need to handle "small" rational numbers and arbitrary-precision "big" rational numbers seamlessly, the immutable implementation is acceptable for now until I have time to profile the library of problems I have available for that purpose so as to determine whether mutable objects or a larger set of "common" immutable objects will win the day.

当然,如果最终我需要实现等于"以测试Matrix相等性,那么当该方法的可能需求极不可能出现时,我将回到与Matrix哈希码"相同的问题.这又使我再次回想起毫无用处的抱怨,即哈希码"(可能还有等于")本来就不应该成为java.lang.Object合同的一部分...

Of course, if I end up needing to implement "equals" to test Matrix equality, I will be back to the same issue with Matrix "hashcode" when the possible need for the method is highly unlikely. Which takes me back again to the rather useless complaint that "hashcode" -- and probably "equals" as well -- should never have been part of the java.lang.Object contract in the first place...

这篇关于用Java实现数字系统:可变与不可变的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆