Подтвердить что ты не робот

Почему toString не дает правильное значение для неизменяемого BigDecimal?

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());
  }
}

Вкомпилировать

>javac BigDecimalTest.java

Выполнить

 >java BigDecimalTest

Выход

x: 625054983208066198204593354911415430438704792574969565088267203004781525349051886368978966454635866976757873019902352587338204709349419540445048397640668053751325307746498089964597558898932143981799355575346628545040975710892600034453462303030824526026617372479672702318775234126736309035340551798242305697053918011236108116969184203450147688710548806249178948798950602635292084669950732365353235782823866975230624679863759260425959459791169573662813659882560711299260566798548341409068343765881208298932278254261294646140590112068258200980117045324292667804864432756961810725182370437206902961756578170730203574233660279475700447597108771501423828064891010088908598454793225469099307839235742968560582894084123332587841678908692453688646424002096420169762493752403209194120933311549724412343492102761719612412226021289199823441354383529928770138627744900421912301539068635884552971941408134.8856600179050611289788749333661467630922532694031193377751928459953017059824923573892149119923856234431388706196397956490750352971729842937634895018670939708354823574625828791536366736979476766589326086875409807351989786090090279478781367082883474934694924763036804348502963946884054479650783337788950079302927905246137931881022596647890564269534539014810606033753362254652128419763750928651303475678198850650473651453073743837739070377816899469866500215337149978217017797004675976721899561358322045967266798653940112240121024238988798224822218203993329849451071671755903125554170025962201010130308257571374613023572917101445758904604655642902352167479118496542289087726701938867138026569109982914825090572482443761923819950022043159771189713669219385693445567010592510898703998395859012610071144546558746041294923614800026040585757943037935297161564798258664422461809370948330482806766116607140637816031325356147998234497034752

y: 711510949782866099699296193137700951609335763543887012748548458182417747578081585833524887774072570691956766860384875364912060891737185872746005419263400444156198098581226885923291670353816772414798224148927688218647602446762953730527741703572368727049379249227044080281137229152770971832240631944592537.904743732558993126

z: 6863200148645991450016700150728475158275817266239021182863526677885700921863906334312309256001619020949572592642200844420107346867400206096485382274175041601107978676753014927820457112641389679172479926134263590581506384223135957016211147412682886175625161361918270282067511320630977561140325469899962049739132122854543111824994613211802165652292305592183629295330885779837415870933600699791946039851356918600890315497940083093271504897016557099915008808164166772999720870505507779642391694002178573568389923682384862328430119487673749084566046514914589822168578412569408216619911686172

Значение z.toString() на выходе правильное

4.883242e+888 / 7.115109e+302 = 6.863200e+585

как значение y.toString(), но обратите внимание, что значение, заданное для x.toString(), совершенно неверно.

Почему это?

Странно, если масштаб (т.е. желаемые десятичные разряды) результата деления изменяется

BigDecimal z = x.divide(y, 3, RoundingMode.HALF_UP);

то x.toString() произведет правильное значение для x.

Или, если операнды обмениваются

BigDecimal z = y.divide(x, 0, RoundingMode.HALF_UP);

затем x.toString() также произведет правильное значение.

Или, если показатель степени x изменен с e+888 на, например,. e+878, тогда x.toString() будет правильным.

Или, если к операции divide добавлен другой вызов x.toString(), то оба вызова x.toString() будут выдавать правильное значение!

На машине я тестирую это, 64-разрядный Windows 7, поведение одинаково с использованием java 7 и 8, как 32-битной, так и 64-битной версий, но тестирование онлайн на https://ideone.com/ дает разные результаты для java 7 и java 8.

Используя java 7, значение x задано правильно: http://ideone.com/P1sXQQ, но с использованием java 8 его значение неверно: http://ideone.com/OMAq7a.

Кроме того, это поведение не уникально для этого конкретного значения x, как вызов toString для других BigDecimals с более чем 1500 цифрами после их передачи в качестве первого операнда для операции divide также приведет к некорректному значения.

Каково объяснение этого?

Операция divide, по-видимому, мутирует значение, вызванное последующими вызовами toString на его операнды.

Это происходит на вашей платформе?

Edit:

Проблема, похоже, связана только с исполняемой средой java 8, так как вышеприведенная программа, скомпилированная с помощью java 7, производит корректный вывод при исполнении с использованием среды java 7, но некорректный вывод, когда выполняется со временем выполнения java 8.

Edit:

Я тестировал с ранним доступом jre1.8.0_60, и ошибка не появляется, и, согласно ответу Marco13, она была исправлена ​​в build 51. Двоичные файлы Oracle JDK 8 находятся только в обновлении 40, поэтому может потребоваться некоторое время, прежде чем широко используются фиксированные версии.

4b9b3361

Ответ 1

Не так сложно найти причину нечетного поведения.

Вызов divide переходит на

public BigDecimal divide(BigDecimal divisor, int scale, RoundingMode roundingMode) {
    return divide(divisor, scale, roundingMode.oldMode);
}

Это внутренне делегирует другой метод divide, основанный на режиме округления:

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);
        }
    }
}

В этом случае применяется последний вызов. Обратите внимание, что intVal (который является BigInteger, который хранится в BigDecimal) передается непосредственно этому методу в качестве первого аргумента:

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);
    }
}

Наконец, здесь берется путь к второй divideAndRound, снова проходящий dividend on (который был intVal оригинала BigDecimal), в результате чего код:

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);
    ...

И здесь вводится ошибка: mdivididend является изменчивым BigInteger, который был создан как изменяемое представление в массиве mag BigInteger, которое сохраненный в BigDecimal x из исходного вызова. Деление изменяет поле mag и, следовательно, состояние (теперь не столь неизменяемое) BigDecimal.

Это явно ошибка в реализации одного из методов divide. Я уже начал отслеживать набор изменений OpenJDK, но еще не заметил определенного виновника. (Изменить: см. Обновления ниже)

(Замечание: Вызов x.toString() перед выполнением деления на самом деле не исключает, но только скрывает ошибку: он вызывает внутреннюю копию правильного состояния. Правильное значение печатается, но внутреннее состояние по-прежнему не так - это касается, если не сказать больше...)


Обновить. Чтобы подтвердить, что @MikeM сказал: ошибка была указана в списке ошибок openjdk, и она была разрешена в JDK8 Build 51

Обновить: Престижность Майку и exex zian для выкапывания отчетов об ошибках. Согласно обсуждению, ошибка была введена с этим набором изменений. (По общему признанию, в то время как я просматривал изменения, я также считал это горячим кандидатом, но не мог поверить, что это было введено четыре года назад и до сих пор оставалось незамеченным...)