java - initialize - ¿Por qué toString no produce el valor correcto en un BigDecimal inmutable?
set bigdecimal java (1)
import java.math.BigDecimal;
import java.math.RoundingMode;
public class BigDecimalTest {
public static void main (String[] args) {
// 4.88...e+888 (1817 digits)
BigDecimal x = new BigDecimal("4.8832420563130171734733855852454330503023811919919497272520875234748556667894678622576481754268427107559208829679871295885797242917923401597269406065677191699322289667695163278484184288979073748578074654323955355081326227413484377691676742424283166095829482224974429868654315166151274143385980609237680132582337344627820946638217515894542788180511625488217105374918015830882194114839900966043221545533114607439892553114356192220778082796185122942407317178325055570254731781136589172583464356709469398354084238614163644229733602505332671951571644165960364672255033809137641462904872690406789293887232669588297154237004709334039097468524122773548736567569610163195984254280720739773383424940292419418795538600322135358425131164741597944425501875163782825762694824406500718290697914964822219714335320528259344719705157913218736206355811213275685167080292570345461898557739737178480700932922510537942188898832900701474604169230215866582286672118698263686941093382945779882215421032414999405126831495224267159359035083987132591639397950272617333366716471522059176764287433877865132652162238979110053714139119937420203828830308427979430335027147927304099711225033972679405835031675622271744826476172494554124259735452592820489036311645033355738586053207859638698142614469753279404304130088308403735928520706825401977138623732336487326694527108332032932321484204820451539099031068139840323111890984119271864483907126875945501867099986131423579718697889448836497435592993168391953327829695391643033262276364164246663414855044991442223872210174626308430613254236633497864858897399515832571171741522071020097519091890029843359547212185712419638040776450730043492270253991396124987467648536016180816769990203447616590740625203442076233929983869509074724986395815800482885710533831896927860285993286232937744729344906236207008084e+888");
BigDecimal y = new BigDecimal("7.11510949782866099699296193137700951609335763543887012748548458182417747578081585833524887774072570691956766860384875364912060891737185872746005419263400444156198098581226885923291670353816772414798224148927688218647602446762953730527741703572368727049379249227044080281137229152770971832240631944592537904743732558993126e+302");
BigDecimal z = x.divide(y, 0, RoundingMode.HALF_UP);
System.out.println("x: " + x.toString());
System.out.println();
System.out.println("y: " + y.toString());
System.out.println();
System.out.println("z: " + z.toString());
}
}
Compilar
>javac BigDecimalTest.java
Ejecutar
>java BigDecimalTest
Salida
x: 625054983208066198204593354911415430438704792574969565088267203004781525349051886368978966454635866976757873019902352587338204709349419540445048397640668053751325307746498089964597558898932143981799355575346628545040975710892600034453462303030824526026617372479672702318775234126736309035340551798242305697053918011236108116969184203450147688710548806249178948798950602635292084669950732365353235782823866975230624679863759260425959459791169573662813659882560711299260566798548341409068343765881208298932278254261294646140590112068258200980117045324292667804864432756961810725182370437206902961756578170730203574233660279475700447597108771501423828064891010088908598454793225469099307839235742968560582894084123332587841678908692453688646424002096420169762493752403209194120933311549724412343492102761719612412226021289199823441354383529928770138627744900421912301539068635884552971941408134.8856600179050611289788749333661467630922532694031193377751928459953017059824923573892149119923856234431388706196397956490750352971729842937634895018670939708354823574625828791536366736979476766589326086875409807351989786090090279478781367082883474934694924763036804348502963946884054479650783337788950079302927905246137931881022596647890564269534539014810606033753362254652128419763750928651303475678198850650473651453073743837739070377816899469866500215337149978217017797004675976721899561358322045967266798653940112240121024238988798224822218203993329849451071671755903125554170025962201010130308257571374613023572917101445758904604655642902352167479118496542289087726701938867138026569109982914825090572482443761923819950022043159771189713669219385693445567010592510898703998395859012610071144546558746041294923614800026040585757943037935297161564798258664422461809370948330482806766116607140637816031325356147998234497034752
y: 711510949782866099699296193137700951609335763543887012748548458182417747578081585833524887774072570691956766860384875364912060891737185872746005419263400444156198098581226885923291670353816772414798224148927688218647602446762953730527741703572368727049379249227044080281137229152770971832240631944592537.904743732558993126
z: 6863200148645991450016700150728475158275817266239021182863526677885700921863906334312309256001619020949572592642200844420107346867400206096485382274175041601107978676753014927820457112641389679172479926134263590581506384223135957016211147412682886175625161361918270282067511320630977561140325469899962049739132122854543111824994613211802165652292305592183629295330885779837415870933600699791946039851356918600890315497940083093271504897016557099915008808164166772999720870505507779642391694002178573568389923682384862328430119487673749084566046514914589822168578412569408216619911686172
El valor de z.toString()
en la salida es correcto
4.883242e+888 / 7.115109e+302 = 6.863200e+585
como es el valor de y.toString()
, pero observe que el valor dado para x.toString()
es completamente incorrecto.
¿Por qué es esto?
Extrañamente, si se cambia la escala (es decir, los decimales deseados) del resultado de la división
BigDecimal z = x.divide(y, 3, RoundingMode.HALF_UP);
entonces x.toString()
producirá el valor correcto para x
.
O bien, si se cambian los operandos.
BigDecimal z = y.divide(x, 0, RoundingMode.HALF_UP);
entonces x.toString()
también producirá el valor correcto.
O, si el exponente de x
cambia de e+888
a, por ejemplo, e+878
entonces x.toString()
será correcto.
O, si se x.toString()
otra llamada x.toString()
sobre la operación de divide
, entonces ambas llamadas x.toString()
producirán el valor correcto.
En la máquina que estoy probando esto, Windows 7 de 64 bits, el comportamiento es el mismo que con las versiones de Java 7 y 8, tanto de 32 bits como de 64 bits, pero las pruebas en línea en https://ideone.com/ producen resultados diferentes para Java 7 y java 8.
Al usar java 7, el valor de x
se da correctamente: http://ideone.com/P1sXQQ , pero al usar java 8 su valor es incorrecto: http://ideone.com/OMAq7a .
Además, este comportamiento no es exclusivo de este valor particular de x
, ya que llamar a ToString en otros BigDecimals con más de aproximadamente 1500 dígitos después de pasarlos como el primer operando a una operación de divide
también producirá valores incorrectos.
¿Cuál es la explicación para esto?
La operación de divide
parece estar mutando el valor producido por las subsiguientes llamadas a toString
en sus operandos.
¿Ocurre esto en tu plataforma?
Editar:
El problema parece ser solo con el tiempo de ejecución de java 8, ya que el programa anterior compilado con java 7 produce una salida correcta cuando se ejecuta con el tiempo de ejecución de java 7, pero una salida incorrecta cuando se ejecuta con el tiempo de ejecución de java 8.
Editar:
He probado con el acceso temprano jre1.8.0_60 y el error no aparece, y de acuerdo con la respuesta de Marco13, se solucionó en la compilación 51. Los binarios del producto Oracle JDK 8 solo están en la actualización 40, por lo que puede ser un tiempo antes Las versiones fijas son ampliamente utilizadas.
No es tan difícil rastrear el motivo del extraño comportamiento.
La llamada divide
va a
public BigDecimal divide(BigDecimal divisor, int scale, RoundingMode roundingMode) {
return divide(divisor, scale, roundingMode.oldMode);
}
Esto, internamente, se delega a otro método de divide
, basado en el modo de redondeo:
public BigDecimal divide(BigDecimal divisor, int scale, int roundingMode) {
if (roundingMode < ROUND_UP || roundingMode > ROUND_UNNECESSARY)
throw new IllegalArgumentException("Invalid rounding mode");
if (this.intCompact != INFLATED) {
if ((divisor.intCompact != INFLATED)) {
return divide(this.intCompact, this.scale, divisor.intCompact, divisor.scale, scale, roundingMode);
} else {
return divide(this.intCompact, this.scale, divisor.intVal, divisor.scale, scale, roundingMode);
}
} else {
if ((divisor.intCompact != INFLATED)) {
return divide(this.intVal, this.scale, divisor.intCompact, divisor.scale, scale, roundingMode);
} else {
return divide(this.intVal, this.scale, divisor.intVal, divisor.scale, scale, roundingMode);
}
}
}
En este caso, se aplica la última llamada. Tenga en cuenta que el intVal
(que es un BigInteger
que se almacena en BigDecimal
) se pasa directamente a este método como primer argumento:
private static BigDecimal divide(BigInteger dividend, int dividendScale, BigInteger divisor, int divisorScale, int scale, int roundingMode) {
if (checkScale(dividend,(long)scale + divisorScale) > dividendScale) {
int newScale = scale + divisorScale;
int raise = newScale - dividendScale;
BigInteger scaledDividend = bigMultiplyPowerTen(dividend, raise);
return divideAndRound(scaledDividend, divisor, scale, roundingMode, scale);
} else {
int newScale = checkScale(divisor,(long)dividendScale - scale);
int raise = newScale - divisorScale;
BigInteger scaledDivisor = bigMultiplyPowerTen(divisor, raise);
return divideAndRound(dividend, scaledDivisor, scale, roundingMode, scale);
}
}
Finalmente, el camino hacia la segunda divideAndRound
se toma aquí, nuevamente pasando el dividend
(que era el valor intVal
del BigDecimal
original), terminando con este código:
private static BigDecimal divideAndRound(BigInteger bdividend, BigInteger bdivisor, int scale, int roundingMode,
int preferredScale) {
boolean isRemainderZero; // record remainder is zero or not
int qsign; // quotient sign
// Descend into mutables for faster remainder checks
MutableBigInteger mdividend = new MutableBigInteger(bdividend.mag);
MutableBigInteger mq = new MutableBigInteger();
MutableBigInteger mdivisor = new MutableBigInteger(bdivisor.mag);
MutableBigInteger mr = mdividend.divide(mdivisor, mq);
...
Y aquí es donde se introduce el error: mdivididend
es un BigInteger
mutable , que se creó como una vista mutable en la matriz mag
del BigInteger
que se almacena en BigDecimal
x
desde la llamada original. La división modifica el campo mag
y, por lo tanto, el estado del BigDecimal
(ahora no tan inmutable).
Esto es claramente un error en la implementación de uno de los métodos de divide
. Ya empecé a rastrear los conjuntos de cambios del OpenJDK, pero aún no he descubierto al culpable definitivo. ( Editar: ver actualizaciones a continuación )
(Una nota al margen: Llamar a x.toString()
antes de hacer la división no evita realmente, pero solo oculta el error: hace que se cree internamente un caché de cadena del estado correcto. Se imprime el valor correcto, pero el estado interno sigue siendo incorrecto - lo que es preocupante, por decir lo menos ...
Actualización : Para confirmar lo que dijo
@MikeM
: El error se ha incluido en la lista de errores de openjdk y se ha resuelto enJDK8 Build 51
Actualización : Felicitaciones a Mike y exex zian por desenterrar los informes de errores. Según la discusión allí, el error se introdujo con este conjunto de cambios . (Es cierto que, mientras repasaba los cambios, también consideré esto como un candidato candente, pero no podía creer que esto se introdujera hace cuatro años y permaneciera inadvertido hasta ahora ...)