我正在为有理数实现类,但对于复数以及其他旨在用于在给定数学对象上执行大量计算的应用程序的类,问题和问题基本相同。
在随 JRE 分发的库和许多第三方库中,数字类是不可变的。这样做的好处是“equals”和“hashcode”可以按预期可靠地一起实现。这将使实例能够在各种集合中用作键和值。事实上,必须保持实例在其整个生命周期中作为集合中的键值的不变性,以便对集合进行可靠操作。
然而,如果类设计在语言的限制范围内强制执行不变性,那么在执行即使是简单的数学运算时,数学表达式也会因过多的对象分配和随后的垃圾收集而负担。考虑以下作为复杂计算中重复发生的明确示例:
Rational result = new Rational( 13L, 989L ).divide( new Rational( -250L, 768L ) );
该表达式包括三个分配——其中两个被迅速丢弃。为了避免一些开销,类通常预先分配常用的“常量”,甚至可能维护常用“数字”的哈希表。当然,与简单地分配所有必要的不可变对象并依靠 Java 编译器和 JVM 尽可能高效地管理堆相比,这样的哈希表的性能可能会更低。
另一种方法是创建支持可变实例的类。通过以流利的风格实现类的方法,可以在功能上与上述类似的简洁表达式求值,而无需分配从“divide”方法返回的第三个对象作为“结果”。同样,这对于这个表达式并不是特别重要。但是,通过对矩阵进行运算来解决复杂的线性代数问题对于数学对象来说是一种更现实的情况,这些数学对象可以更好地处理为可变对象,而不是必须对不可变实例进行运算。对于有理数矩阵,可变有理数类似乎更容易证明是合理的。
尽管如此,我有两个相关的问题:
Sun/Oracle Java 编译器、JIT 或 JVM 是否有任何东西可以最终推荐不可变有理数或复数类而不是可变类?
如果不是,在实现可变类时应该如何处理“哈希码”?我倾向于通过抛出不受支持的操作异常来“快速失败”,而不是提供易于误用和不必要的调试会话的实现,或者即使在不可变对象的状态发生变化时也很健壮但本质上将哈希表转换为链接的实现列表。
测试代码:
对于那些想知道在执行与我需要实现的计算大致相似的计算时不可变数字是否重要的人:
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”来确定对象相等性——想想你想要比较是否相等的矩阵,但永远不会想到添加到集合。然而,可能有人认为抛出异常违反了上述“合同”,并带来了可怕的后果。在这种情况下,将可变类的所有实例散列到相同的值可能是维护合同的“正确”方式,尽管实现的性质很差。是否建议返回一个常量值(可能是通过散列类名生成)而不是抛出异常?