我正在为有理数实现类,但复数的问题和问题本质上是相同的,以及用于对给定数学对象执行大量计算的应用程序的其他类。
在与JRE一起分发的库和许多第三方库中,数字类是不可变的。这样做的优点是“equals”和“hashcode”可以按预期可靠地一起实现。这将使实例能够在各种集合中同时用作键和值。事实上,为了在集合上进行可靠的操作,必须维护实例在其整个生命周期中作为集合中的键值的不变性。如果类在创建实例后阻止可能改变hashcode方法所依赖的内部状态的操作,那么这一点将得到更有力的维护,而不是让代码的开发人员和后续维护人员遵守在修改实例状态之前从集合中删除实例,然后将实例添加回任意一个它们必须属于的集合。
然而,如果类设计在语言的限制范围内强制执行不变性,那么即使在执行简单的数学运算时,数学表达式也会承担过多的对象分配和随后的垃圾收集。将以下内容作为复杂计算中重复发生的情况的明确示例:
Rational result = new Rational( 13L, 989L ).divide( new Rational( -250L, 768L ) );
该表达式包括三个分配,其中两个被快速丢弃。为了避免一些开销,类通常会预先分配常用的“常量”,甚至可能维护一个由常用的“数字”组成的哈希表。当然,这样的哈希表可能比简单地分配所有必要的不可变对象并依靠Java编译器和JVM尽可能有效地管理堆的性能差。
另一种选择是创建支持可变实例的类。通过以流畅的风格实现类的方法,可以在功能上评估与上述类似的简洁表达式,而无需分配第三个对象作为“结果”从“divide”方法返回。同样,这对这一表达式来说并不特别重要。然而,对于数学对象来说,通过对矩阵进行运算来解决复杂的线性代数问题是一种更现实的情况,因为数学对象被更好地处理为可变对象,而不是必须对不可变的实例进行运算。对于有理数矩阵,一个可变的有理数类似乎更容易被证明是合理的。
综上所述,我有两个相关的问题:
-
有没有关于Sun/Oracle Java编译器、JIT或JVM的任何东西会最终推荐不可变的有理数或复数类而不是可变类?
-
如果没有,那么在实现可变类时应该如何处理“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中的矩阵。然而,可能有一种观点认为,抛出例外是违反上述“合同”的行为,其后果是可怕的。在这种情况下,尽管实现的性质很差,但将可变类的所有实例散列为相同的值可能是维护契约的“正确”方式。是否建议返回一个常量值(可能是通过散列类名生成的)而不是抛出异常?