我正在实现有理数的类,但是对于复数以及打算在给定数学对象上执行大量计算的应用程序中使用的其他类,问题和问题基本相同。
在随JRE一起分发的库中以及许多第三方库中,数字类是不可变的。这样做的优点是"等于"和"哈希码"可以按预期可靠地一起实施。这将使实例可以用作各种集合中的键和值。实际上,必须保持实例在其整个生命周期中作为集合中的关键值的不变性,以便对集合进行可靠的操作。如果类阻止一旦创建实例时可能改变哈希码方法所依赖的内部状态的操作,而不是让代码的开发人员和后续维护者遵守删除约定,则可以更好地维护这一操作。来自集合的实例在修改其状态之后,然后将实例添加回它们必须属于的任何集合。
然而,如果类设计强制执行 - 在语言的限制范围内 - 不变性,数学表达式会在执行简单的数学运算时承受过多的对象分配和随后的垃圾收集。请考虑以下内容作为复杂计算中重复出现的内容的明确示例:
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();
}
}
脚注:
可变类和不可变类的核心问题是 - 非常值得怀疑 - &#34;哈希码&#34; Object上的方法:
hashCode的一般合约是:
每当在执行Java应用程序期间多次在同一对象上调用它时,hashCode方法必须始终返回相同的整数,前提是不修改对象上的equals比较中使用的信息。从应用程序的一次执行到同一应用程序的另一次执行,此整数不需要保持一致。
如果两个对象根据equals(Object)方法相等,则对两个对象中的每一个调用hashCode方法必须产生相同的整数结果。
如果两个对象根据equals(java.lang.Object)方法不相等,则不需要在两个对象中的每一个上调用hashCode方法必须生成不同的整数结果。但是,程序员应该知道为不等对象生成不同的整数结果可能会提高哈希表的性能。
但是,一旦将一个对象添加到一个集合中,该集合依赖于其哈希码的值,该哈希码源自用于确定&#34;相等的内部状态,&#34;当状态发生变化时,它不再适当地融入集合中。是的,程序员有责任确保可变对象不会不正确地存储在集合中,但是维护程序员的负担更大,除非首先不防止不正当使用可变类。这就是为什么我相信正确的答案&#34; for&#34; hashcode&#34;在可变对象上,总是抛出UnsupportedOperationException,同时仍然实现&#34; equals&#34;确定对象相等性 - 想想要比较相等的矩阵,但绝不会想到要添加到Set中。但是,可能存在一种争论,即抛出异常违反了上述合同&#34;其自身的可怕后果。在这种情况下,将可变类的所有实例哈希到相同的值可以是&#34;正确的&#34;尽管实施的性质非常差,但仍然保持合同的方式。返回一个常量值 - 可能是通过散列类名生成的 - 建议抛出异常?
答案 0 :(得分:0)
你写道:“在进行简单的数学运算时,数学表达式会因过多的对象分配和随后的垃圾收集而变得沉重。” 并且“表达式包括三个分配 - 其中两个很快被丢弃”。
现代垃圾收集器实际上是针对这种分配模式进行了优化的,因此您的(隐式)假设分配和后续垃圾收集很昂贵,这是错误的。
例如,请参阅此白皮书: http://www.oracle.com/technetwork/java/whitepaper-135217.html#garbage 在“Generational Copying Collection”下,它声明:
“......首先,因为新对象在对象托儿所中以类似堆栈的方式连续分配,所以分配变得非常快,因为它只涉及更新单个指针并对托儿所溢出执行单个检查。托儿所溢出的时间,托儿所中的大多数对象已经死亡,允许垃圾收集器简单地将少数幸存的对象移动到其他地方,并避免对托儿所中的死对象进行任何回收工作。“
因此,我建议你真正问题的答案是你应该使用Immutable对象,因为感知成本根本不是真正的成本,但感知的好处(例如简单性,代码可读性)是真正的好处。
答案 1 :(得分:0)
一个可能有用的模式是为“可读”事物定义抽象类型或接口,然后同时具有可变形式和不可变形式。如果基类型或接口类型包含AsMutable
,AsNewMutable
和AsImmutable
方法,可以在派生对象中以适当的方式覆盖,则此模式可能特别好。这种方法允许人们在需要时实现可变性的好处,并且还具有使用不可变类型的好处。想要保留一个值而不是改变它的代码,如果它使用一个可变类型,则必须使用“防御性复制”,但如果它接收到“可读”的东西,则可以使用AsImmutable
。如果事情恰好是可变的,它会复制,但如果它是不可变的,则不需要复制。
顺便提一下,如果设计一个不可变类型,除了对包含实际数据的大对象的引用之外,字段相对较少,并且如果经常比较类型的东西是否相等,那么每个类型都可能有帮助。 type保存唯一的序列号以及对已知最旧的实例(如果有)的引用(如果不知道不存在旧实例,则为null)。当比较两个实例的相等性时,确定已知匹配每个实例的最旧实例(递归检查最早的已知实例,直到它为空)。如果已知两个实例匹配相同的实例,则它们是相等的。如果没有,但结果是相同的,那么无论哪个“较老的实例”更年轻,都应该将另一个视为一个与之相等的旧实例。该方法将产生加速比较,就像实习一样,但不使用单独的实习字典,也不需要哈希值。
答案 2 :(得分:0)
目前,我正在使用不可变对象实现有理数。这允许在我需要执行的计算中频繁重复使用ZERO和ONE对象。但是,使用有理数元素实现的Matrix类是可变的 - 甚至在内部对“虚拟”零使用null。由于需要无缝地处理“小”有理数和任意精度的“大”有理数,所以现在可以接受不可变的实现,直到我有时间描述我为此目的可用的问题库以便确定是否可变对象或更大的“常见”不可变对象集合将赢得胜利。
当然,如果我最终需要实现“equals”来测试Matrix相等性,那么当对方法的可能需求极不可能时,我将回到Matrix“hashcode”的相同问题。这让我再次回到“hashcode” - 而且可能“等于”的相当无用的抱怨 - 首先应该永远不会成为java.lang.Object契约的一部分......