cswork / libraryOpen practice workspace

Articles

Java BigDecimal Interview Questions: Scale, Equality, and Rounding Boundaries

Prepare for Java BigDecimal interview questions with scale-aware equality, exact construction, collection behavior, and explicit rounding boundaries.

Two Java amounts, 2.0 and 2.00, compare numerically equal but behave differently as HashSet keys. Three invoice lines of 0.005 total 0.03 when each line is rounded first, yet 0.02 when their sum is rounded once. BigDecimal prevents binary floating-point arithmetic from deciding these outcomes accidentally; it does not choose your equality or rounding policy for you.

For Java BigDecimal interview questions, separate decimal representation, numerical ordering, and the business boundary where rounding is authorized. That separation explains both collection surprises and seemingly contradictory financial totals.

Evidence boundary: The original probes below were run with Homebrew OpenJDK 21.0.12.1. Current Oracle Java 26 API documentation was checked for the described contracts; documentation review is not a claim that the probes ran on Java 26. All amounts are synthetic examples, not accounting or currency-policy advice.

For adjacent implementation and debugging practice, use the verified Software Engineer questions.

How do scale and precision differ?

A BigDecimal represents an unscaled integer multiplied by ten to the negative scale. Scale is not always the number of visible decimal places, because it can be negative. Precision concerns the number of digits in the unscaled representation. BigDecimal is immutable: methods return values rather than modifying the original amount. Oracle BigDecimal API

For new BigDecimal("12.340"), the unscaled value is 12340, the scale is 3, and the precision is 5. The final zero participates in the representation even though it does not change the numerical value. For 1000 with trailing zeros stripped, the tested scale is −3: one unscaled digit represents a value multiplied by 1000.

An interview answer should not define precision as “digits after the decimal point.” That confuses it with a commonly used positive scale. A four-digit precision can produce different fractional lengths for 123.456 and 0.00123456.

Also distinguish storage from presentation. Scientific notation may be a valid representation but unsuitable for a particular API or user-facing label. Choose the serialization contract deliberately; do not assume stripping zeros is harmless cosmetic cleanup.

Why are 2.0 and 2.00 equal in one comparison but not another?

The executed probe used:

BigDecimal a = new BigDecimal("2.0");
BigDecimal b = new BigDecimal("2.00");

System.out.println(a.equals(b));        // false
System.out.println(a.compareTo(b));     // 0
System.out.println(a.scale());         // 1
System.out.println(b.scale());         // 2

equals checks the value and scale representation, whereas compareTo orders numerical values. The observed outputs are therefore consistent, not contradictory. Decide which notion of equality the requirement needs before selecting an assertion or key type.

For a threshold, numerical comparison is usually the relevant operation: amount.compareTo(limit) > 0. For an object whose representation is part of its contract, such as a value carrying explicitly meaningful precision, scale-aware equality may be appropriate. “Always use compareTo” is as incomplete as “always use equals.”

Do not use == to compare independently created values. It compares object identity. A passing identity check on reused constants or shared references would not establish the intended decimal equality rule.

In a unit test, name the expected contract. A test called amountsAreNumericallyEqual should not unexpectedly fail because one fixture has an extra trailing zero unless representation is itself the requirement.

What happens in HashSet versus TreeSet?

The original collection probe produced two elements in a HashSet and one in a naturally ordered TreeSet:

var hashed = new HashSet<BigDecimal>();
hashed.add(new BigDecimal("2.0"));
hashed.add(new BigDecimal("2.00"));

var ordered = new TreeSet<BigDecimal>();
ordered.add(new BigDecimal("2.0"));
ordered.add(new BigDecimal("2.00"));

// Tested sizes: hashed = 2, ordered = 1

Oracle explains that TreeSet uses comparison to decide whether elements are the same for set membership. BigDecimal's natural ordering is a notable case where comparison equality and equals differ. Oracle TreeSet API

This matters beyond a trivia question. Moving a deduplication step from a hash-based structure to a sorted structure can change the accepted key population without any change to input values. A Map keyed by an amount also needs an explicit domain rationale: many independent transactions can share one amount.

If numerical identity is intended for a domain key, define normalization or a suitable key representation at the boundary. Test zero, negative values, and powers of ten. If scale matters, do not normalize it away merely to make a collection test pass.

For monetary keys, a currency code and a documented unit are often additional required dimensions. 2.00 by itself cannot distinguish two currencies or two unrelated business entities. BigDecimal solves neither identity nor data-model design.

Which constructor preserves the input you actually received?

The probe compared three construction paths:

BigDecimal fromText = new BigDecimal("0.1");
BigDecimal fromDouble = new BigDecimal(0.1);
BigDecimal viaValueOf = BigDecimal.valueOf(0.1);

The text and valueOf cases were equal in this test. The double constructor produced:

0.1000000000000000055511151231257827021181583404541015625

The long decimal is the exact representation of the already approximate binary double. It is not a random BigDecimal rounding error. If the source is a decimal string, construct directly from that string rather than routing through a double first.

valueOf is useful when accepting an existing double under its documented conversion contract, but it cannot recover the original decimal intent after an earlier floating-point computation. If the upstream result is already wrong for the business contract, changing the final wrapper type does not rewind that calculation.

A good interview counterexample starts at input parsing: a JSON decimal amount or CSV price is converted to double, multiplied, then wrapped in BigDecimal. Ask where precision was lost. The answer should change the ingestion and calculation path, not just the final display format.

For trusted integer minor units, construction from an integer plus an explicit scale can be appropriate. The scale belongs to a known unit contract; do not infer that every currency or measurement uses two fractional digits.

Why does exact division fail for one third?

The executed test confirmed that BigDecimal.ONE.divide(new BigDecimal("3")) throws ArithmeticException because the exact result has no terminating decimal representation. BigDecimal cannot store infinitely many decimal digits.

Specify an authorized boundary when an approximate decimal is acceptable:

BigDecimal share = BigDecimal.ONE.divide(
    new BigDecimal("3"), 2, RoundingMode.HALF_UP);
// Tested result: 0.33

The result answers a particular policy: two fractional digits with HALF_UP. It is not the only defensible answer. For a ratio used in further calculations, premature rounding to cents may introduce avoidable error. For a posted amount, the contract may require a specific scale and rule.

Watch the overload as well. The tested new BigDecimal("4.0").divide(new BigDecimal("3"), HALF_UP) produced 1.3, while the 4.00 dividend produced 1.33. That overload uses the dividend's scale. Adding a trailing zero can therefore change a rounded division result even when compareTo says the starting values are numerically equal.

In code review, make precision or scale explicit where the domain needs stability. A shorter method call is not automatically a clearer contract. Ask what happens if an upstream serialization changes 4.00 to 4.0.

Where should rounding happen in an invoice?

Use three identical synthetic lines, each exactly 0.005. This is a boundary exercise, not a recommendation for handling real invoices.

BigDecimal line = new BigDecimal("0.005");
BigDecimal roundedLines = line.setScale(2, RoundingMode.HALF_UP)
    .multiply(new BigDecimal("3"));
BigDecimal roundedTotal = line.multiply(new BigDecimal("3"))
    .setScale(2, RoundingMode.HALF_UP);
// Tested: roundedLines = 0.03; roundedTotal = 0.02

Both computations follow the requested operations. Rounding is not distributive over addition or multiplication, so they need not agree. The interview question is which boundary the specification authorizes: each line, each tax component, an allocation, or the final invoice total.

If displayed line amounts must sum to the displayed total, rounding only the final sum may require an explicit allocation or reconciliation policy. Do not hide a discrepancy with a display-only adjustment that changes no underlying amount and leaves consumers with inconsistent totals.

State the chosen policy before coding, then test the opposite path as a counterexample. The three-line fixture makes the trade-off visible with very little data and is more informative than a large set of ordinary two-decimal amounts.

How do negative values expose a mistaken rounding rule?

Oracle defines UP as away from zero and DOWN as toward zero; CEILING and FLOOR refer to positive and negative infinity. HALF_UP and HALF_EVEN differ in their treatment of ties. Oracle RoundingMode API

The native probe rounded −2.5 to scale zero and observed:

ModeTested result
HALF_UP−3
HALF_EVEN−2
CEILING−2
FLOOR−3

Calling HALF_UP “always round upward” would predict the wrong sign behavior. Test positive and negative ties, nearby nonties, and zero rather than memorizing only the familiar 2.5 example.

HALF_EVEN is also not “always round down on a tie.” The retained digit's parity determines the direction. Your test suite should include adjacent ties whose rounded results differ in direction. The selected rule should be justified by the domain contract, not a claim that one enum is universally best for money.

How do you reject an invalid minor-unit conversion?

For an explicitly two-decimal unit contract, 12.34 maps to 1234 minor units. The tested conversion was movePointRight(2).longValueExact(). The exact conversion rejects a fractional integer result or an out-of-range long instead of silently truncating.

For 12.345, setScale(2, UNNECESSARY) threw ArithmeticException in the fixture. That is a useful validation boundary when extra fractional digits are forbidden. If the policy allows rounding, choose and document that rule instead; do not silently change validation into rounding to avoid an exception.

Finally, distinguish MathContext precision from setScale. The probe applying four significant digits to 123.456 produced 123.5, not a four-decimal-place value. Oracle's MathContext reference defines its precision and rounding settings.

When explaining a repair, identify the exact crossing: decimal input to internal representation, internal calculation to posted amount, or posted amount to integer units. Validate at that crossing and preserve the original input for diagnosis where appropriate.

Include a size boundary in the regression tests. A decimal amount can be valid internally yet exceed the destination's integer capacity after unit conversion. Test the largest accepted value and one beyond it, and specify whether rejection is returned as a validation error or propagated as an exception. Silent narrowing is not an acceptable replacement for a documented range limit.

Sources and Further Reading