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

Implementing a Number System in Java: Mutable vs. Immutable

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

在使用JRE分发的库和许多第三方库中,数字类是不可变的。这样做的好处是"等于"和"hashcode"可以按预期可靠地一起实现。这将使实例在各种集合中同时用作键和值。事实上,实例作为集合中的键值在其整个生命周期中都必须保持不变,才能对集合进行可靠的操作。如果类阻止操作,这些操作可能会在创建实例后改变hashcode方法所依赖的内部状态,那么这种维护要比让代码的开发人员和后续维护人员在修改实例的状态之前遵守从集合中删除实例的约定要有力得多。通过将实例添加回它们必须属于的集合。

然而,如果类设计强制(在语言的限制范围内)不可变,那么在执行甚至简单的数学操作时,数学表达式都会承受过多的对象分配和随后的垃圾收集。将以下内容作为复杂计算中重复发生的情况的显式示例:

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

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

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

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

  • 关于Sun/Oracle Java编译器、JIT或JVM,有什么可以在可变类上最终推荐不可变的有理数或复数类吗?

  • 如果不是,那么在实现可变类时应该如何处理"hashcode"?我倾向于通过抛出一个不受支持的操作异常来"快速失败",而不是提供一个容易被误用和不必要的调试会话的实现,或者一个即使在不可变对象的状态发生变化时仍然健壮的实现,但实际上它将哈希表转换为链表。

  • 测试代码:

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    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"方法:

    The general contract of hashCode is:

    • 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.

    • 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.

    • 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.

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


    目前,我正在用不可变的对象实现有理数。这允许大量重用在我需要执行的计算中经常出现的零和一个对象。然而,用有理数元素实现的矩阵类是可变的——甚至可以在内部对"虚"零使用空值。随着对"小"有理数和任意精度"大"有理数无缝处理的迫切需要,目前不可变实现是可以接受的,直到我有时间分析为此目的而可用的问题库,以便确定可变对象或更大的"公共"不可变对象集我会赢的。

    当然,如果我最终需要实现"equals"来测试矩阵的相等性,我将回到矩阵"hashcode"的相同问题上,这时对方法的可能需求是非常不可能的。这又让我想起了一个相当无用的抱怨,"hashcode"(可能也是"equals")一开始就不应该成为java.lang.object契约的一部分……


    一种可能有用的模式是为"可读"的东西定义一个抽象类型或接口,然后同时具有可变和不可变的形式。如果基类型或接口类型包括AsMutableAsNewMutableAsImmutable方法(这些方法可以在派生对象中以适当的方式被重写),则此模式可能特别好。这种方法允许人们在需要时实现可变性的好处,而且还可以获得使用不可变类型的好处。想要保持一个值但不改变的代码,如果它使用一个可变类型,就必须使用"防御性复制",但是如果它接收到一个"可读"的东西,就可以使用AsImmutable。如果这个东西是可变的,它会复制一份,但是如果它是不变的,就不需要复制了。

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


    您已经写到:"在执行甚至简单的数学操作时,数学表达式都会承受过多的对象分配和随后的垃圾收集。""表达式包括三个分配——其中两个很快被丢弃"。

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

    例如,请参阅此白皮书:http://www. Oracle .COM/TeaTeWorks/Java/WalePaPer-135217.html垃圾在"世代复制集"下,它指出:

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

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