Numbers and Math

This section documents the current Java release line, with Java 25 LTS as the reference point (no specific patch version is pinned), as published at the Java developer portal, the Java Tutorials, and the Java SE API specification — which are the references these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Java has two families of numbers: the primitives (int, long, double, …​) covered on Primitive Types and Variables, and the object types that wrap them. This page covers the wrappers and their autoboxing traps, the Math utility class, arbitrary-precision BigInteger and BigDecimal, and the parsing, formatting and floating-point rules every numeric program runs into. The narrative overview is The Numbers Classes in the Java Tutorials.

Wrapper Classes and Autoboxing

Every primitive has a matching immutable class: Integer, Long, Short, Byte, Double, Float, Character, and Boolean. The six numeric ones extend java.lang.Number. Wrappers exist so numbers can be used as generic type arguments and collection elements, can be null, and can host the static parse/format/constant helpers.

int prim = 42;
Integer boxed = prim;              // autoboxing:  Integer.valueOf(42)
int back = boxed;                  // auto-unboxing:  boxed.intValue()

Integer.MAX_VALUE;                 // 2147483647
Integer.parseInt("1010", 2);      // 10  -- radix 2
Integer.toBinaryString(10);       // "1010"
Integer.compare(3, 7);            // -1  -- safe; unlike (3 - 7) it cannot overflow
Long.parseLong("9999999999");     // 9999999999
Character.isDigit('7');           // true
Boolean.parseBoolean("TRUE");     // true  -- case-insensitive; anything else is false

List<Integer> ids = new ArrayList<>();   // generics need a reference type
ids.add(1);                              // autoboxes int -> Integer

Autoboxing pitfalls

NPE when unboxing null. A wrapper expression used where a primitive is expected calls xxxValue(); if the reference is null that throws NullPointerException.

Map<String, Integer> counts = new HashMap<>();
int n = counts.get("missing");        // get() returns null -> NPE on unboxing
int safe = counts.getOrDefault("missing", 0);   // fix: never unbox a null

== compares identities, not values. On two wrapper references == asks "same object?", not "same number?". Always use equals or unbox explicitly.

Integer a = 1000, b = 1000;
a == b;                    // false -- two distinct objects
a.equals(b);               // true
a.intValue() == b.intValue();   // true

The Integer cache makes == "sometimes work". JLS 5.1.7 requires Integer.valueOf (which autoboxing calls) to return cached instances for -128..127, so == on small boxed values happens to be true — which is worse than a consistent failure.

Integer x = 127, y = 127;
x == y;            // true  -- same cached object
Integer p = 128, q = 128;
p == q;            // false -- outside the cache, fresh objects

Byte, Short and Long cache -128..127 too; Character caches 0..127; Boolean caches both values.

Boxing cost in loops. A wrapper-typed accumulator allocates a new object every iteration.

Long sum = 0L;                       // WRONG: Long, not long
for (long i = 0; i < 10_000_000; i++) {
    sum += i;                        // unbox, add, box -> ~10M throwaway Long objects
}
// fix: declare the accumulator 'long'

See Autoboxing and Unboxing for the full conversion rules.

Math and StrictMath

java.lang.Math holds static numeric helpers plus Math.PI and Math.E. Its transcendental functions may differ across platforms by up to one unit in the last place; StrictMath has the identical API but is bit-for-bit reproducible everywhere. Use Math unless you need cross-platform identical results.

Math.max(3, 7);            // 7
Math.abs(-4);              // 4
Math.pow(2, 10);           // 1024.0
Math.sqrt(2);              // 1.4142135623730951
Math.hypot(3, 4);          // 5.0
Math.clamp(15, 0, 10);     // 10  -- constrain to [min, max]
Math.toRadians(180);       // 3.141592653589793

Math.floorDiv(-7, 2);      // -4   -- rounds toward negative infinity ((-7)/2 is -3)
Math.floorMod(-1, 3);      // 2    -- result has the sign of the divisor ((-1)%3 is -1)

int and long arithmetic wraps silently on overflow: Integer.MAX_VALUE + 1 is Integer.MIN_VALUE, no error. The *Exact methods throw ArithmeticException instead — use them wherever a wrapped result would be a correctness bug.

Math.addExact(Integer.MAX_VALUE, 1);      // ArithmeticException: integer overflow
Math.multiplyExact(100_000, 100_000);     // ArithmeticException
Math.toIntExact(3_000_000_000L);          // ArithmeticException
Math.incrementExact(counter);
Math.negateExact(Long.MIN_VALUE);         // ArithmeticException: long overflow

Arbitrary Precision: BigInteger and BigDecimal

BigInteger is an immutable integer bounded only by memory; operations are methods, not operators.

import java.math.BigInteger;

BigInteger two   = BigInteger.valueOf(2);
BigInteger big   = two.pow(1000);                     // 2^1000, 302 digits
BigInteger fact  = BigInteger.ONE;
for (int i = 2; i <= 100; i++) {
    fact = fact.multiply(BigInteger.valueOf(i));      // 100!
}
big.mod(BigInteger.valueOf(7));                       // 5
new BigInteger("123456789012345678901234567890").isProbablePrime(20);   // false

BigDecimal is an immutable decimal with an explicit scale (digits after the point) — exact decimal arithmetic.

Why BigDecimal, not double, for money. double is binary floating point, and most decimal fractions have no exact binary form, so values drift:

0.1 + 0.2;              // 0.30000000000000004
0.1 + 0.2 == 0.3;       // false

Rounding once at the end does not undo error accumulated across thousands of amounts. Use BigDecimal — or integer minor units (cents as long) — for currency.

The new BigDecimal(double) trap. Constructing from a double copies its binary error in; construct from a String (or use valueOf, which routes through Double.toString).

new BigDecimal(0.1);        // 0.1000000000000000055511151231257827021181583404541015625
new BigDecimal("0.1");      // exactly 0.1
BigDecimal.valueOf(0.1);    // 0.1
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;

var price = new BigDecimal("19.99");
var net   = price.multiply(new BigDecimal("3"));                 // 59.97  (scale 2)
var tax   = net.multiply(new BigDecimal("0.21"))
               .setScale(2, RoundingMode.HALF_UP);               // 12.59
var gross = net.add(tax);                                        // 72.56

net.compareTo(new BigDecimal("59.970"));   // 0     -- compareTo ignores scale
net.equals(new BigDecimal("59.970"));      // false -- equals compares scale too

// division must state scale + rounding, or a non-terminating result throws
new BigDecimal("10").divide(new BigDecimal("3"), 4, RoundingMode.HALF_UP);   // 3.3333
new BigDecimal("10").divide(new BigDecimal("3"));   // ArithmeticException

// MathContext = significant digits + rounding, applied to a whole computation
var mc = new MathContext(10, RoundingMode.HALF_EVEN);
new BigDecimal("2").divide(new BigDecimal("7"), mc);   // 0.2857142857

Rules of thumb: compare with compareTo, never equals; every divide needs an explicit RoundingMode (HALF_UP for invoices, HALF_EVEN — banker’s rounding — for statistics); keep a single scale for each currency and setScale after every multiplication.

Parsing, Formatting, and IEEE-754

The static parseXxx methods turn text into a primitive and throw NumberFormatException on malformed input.

int i    = Integer.parseInt("42");
int hex  = Integer.parseInt("ff", 16);      // 255
long l   = Long.parseLong("9999999999");
double d = Double.parseDouble("3.14e2");     // 314.0
Integer.parseInt("12x");                     // NumberFormatException

java.text.NumberFormat is locale-sensitive (grouping, currency symbol, percent) and not thread-safe — build one per use or guard it.

import java.text.NumberFormat;
import java.util.Locale;

NumberFormat eur = NumberFormat.getCurrencyInstance(Locale.of("es", "ES"));
eur.format(1234.5);                 // "1234,50 EUR" style, locale-formatted

NumberFormat pct = NumberFormat.getPercentInstance(Locale.US);
pct.setMinimumFractionDigits(1);
pct.format(0.075);                  // "7.5%"

String grouped = String.format(Locale.US, "%,.2f", 1234.5);   // "1,234.50"

IEEE-754 caveats. Floating point is approximate and not associative:

  • Never test double/float with ==. Compare against a tolerance: Math.abs(a - b) < 1e-9.

  • 1.0 / 0.0 is Double.POSITIVE_INFINITY and 0.0 / 0.0 is Double.NaN — no exception (unlike integer / 0). NaN is not equal to anything, including itself, so x == Double.NaN is always false; test with Double.isNaN(x).

  • Double.compare(a, b) and Double.equals impose a total order where -0.0 < 0.0 and NaN sorts above every number, which is why TreeSet<Double> and sort stay consistent even though < does not order NaN.

0.0 == -0.0;                  // true
Double.compare(0.0, -0.0);    // 1
Double.isNaN(0.0 / 0.0);      // true
Double.valueOf(Double.NaN).equals(Double.NaN);   // true

Random values. RandomGenerator is the modern interface (with pluggable algorithms), the legacy java.util.Random implements it, and ThreadLocalRandom is the one to use from concurrent code (never share a Random across threads).

import java.util.random.RandomGenerator;
import java.util.concurrent.ThreadLocalRandom;

RandomGenerator rng = RandomGenerator.getDefault();
int dice   = rng.nextInt(1, 7);          // 1..6
double u   = rng.nextDouble();           // [0.0, 1.0)
rng.ints(5, 0, 100).forEach(System.out::println);

var named  = RandomGenerator.of("L64X128MixRandom");   // choose an algorithm by name
int worker = ThreadLocalRandom.current().nextInt(100); // in server / parallel code

For tokens, keys and nonces use java.security.SecureRandom, not Random or the default RandomGenerator.

See Also