Math #
Mathematical operations in Java look simple on the surface — there are the +, -, *, / operators and the Math class with various functions. But hidden underneath are invisible traps: integer overflow that silently produces wrong numbers, floating-point division that’s inaccurate for financial calculations, and double values that can’t precisely represent many decimal values. Understanding the limitations of Java’s numeric types and knowing when to replace them with BigDecimal or BigInteger is the skill that separates developers who write code that “looks right” from developers who write code that’s actually right. This article covers Java’s entire math toolkit — from the simplest Math.abs() to BigDecimal for financial transactions and SecureRandom for cryptography.
The Math Class — Standard Math Functions #
java.lang.Math contains static methods for all common mathematical operations. All methods use the double type unless explicitly stated otherwise.
Absolute Values, Min, and Max #
// abs() — absolute value
Math.abs(-5); // 5
Math.abs(-3.14); // 3.14
Math.abs(Integer.MIN_VALUE); // TRAP! the result is Integer.MIN_VALUE because of overflow!
// Integer.MIN_VALUE = -2147483648, and Math.abs(-2147483648) overflows back to -2147483648
// ✓ CORRECT: use Math.absExact() (Java 15+) for overflow detection
try {
Math.absExact(Integer.MIN_VALUE); // throws ArithmeticException
} catch (ArithmeticException e) {
System.out.println("Overflow detected: " + e.getMessage());
}
// min() and max()
Math.min(3, 7); // 3
Math.max(3, 7); // 7
Math.min(3.5, 2.1); // 2.1
Math.min(Double.NaN, 3.0); // NaN — NaN "infects" the comparison
// clamp() — Java 21+
// ensures a value stays within the range [min, max]
double value = 150.0;
double clamped = Math.clamp(value, 0.0, 100.0); // 100.0
int clampedInt = Math.clamp(250, 0, 100); // 100
Rounding #
// Four different types of rounding — choose the right one for the context
double d = 2.7;
Math.ceil(d); // 3.0 — always up (ceiling)
Math.floor(d); // 2.0 — always down (floor)
Math.round(d); // 3L — round half up (0.5 rounds up), returns a long
Math.rint(d); // 3.0 — round half even (banker's rounding), returns a double
// Rounding 0.5 examples
Math.round(2.5); // 3 — round half up
Math.rint(2.5); // 2.0 — round half even (to the nearest even number)
Math.rint(3.5); // 4.0 — round half even
// TRAP: round(double) vs round(float) behave differently for negative values
Math.round(-2.5); // -2 (not -3!) — "round half up" means toward +infinity
Math.round(-3.5); // -3
// truncate — discard the decimal part (toward zero)
(int) 2.9; // 2 — cast truncates
(int) -2.9; // -2 — always toward zero, not downward
Math.floor(-2.9); // -3.0 — downward (more negative)
Powers, Roots, and Logarithms #
// Powers
Math.pow(2, 10); // 1024.0 — 2^10
Math.pow(9, 0.5); // 3.0 — square root of 9
Math.pow(27, 1.0/3); // 3.0 — cube root of 27 (CAUTION: floating-point precision)
// Square and cube roots
Math.sqrt(16); // 4.0
Math.cbrt(27); // 3.0 — more accurate than Math.pow(x, 1.0/3)
// Logarithms
Math.log(Math.E); // 1.0 — natural logarithm (ln)
Math.log(1); // 0.0
Math.log(0); // -Infinity
Math.log(-1); // NaN
Math.log10(1000); // 3.0 — base-10 logarithm
Math.log10(1); // 0.0
// Any-base log — no direct method, use the change of base formula
double logBase2 = (n) -> Math.log(n) / Math.log(2);
// logBase2(8) = 3.0
// exp() — e^x
Math.exp(1); // 2.718... (the value of e)
Math.exp(0); // 1.0
// hypot() — √(x² + y²) — safer than Math.sqrt(x*x + y*y) (avoids overflow)
Math.hypot(3, 4); // 5.0
Trigonometry #
// All trigonometric functions accept and return radians
// Constants
double PI = Math.PI; // 3.141592653589793
double E = Math.E; // 2.718281828459045
// Degree ↔ radian conversion
double radians = Math.toRadians(90); // π/2 ≈ 1.5707963...
double degrees = Math.toDegrees(Math.PI); // 180.0
// Basic trigonometric functions
Math.sin(Math.toRadians(30)); // 0.5
Math.cos(Math.toRadians(60)); // 0.5
Math.tan(Math.toRadians(45)); // 1.0 (almost, because of floating point)
// Inverses
Math.asin(0.5); // 0.5236... radians (30°)
Math.acos(0.5); // 1.0472... radians (60°)
Math.atan(1.0); // 0.7854... radians (45°)
// atan2 — the angle from the origin to (x, y), considering the quadrant
Math.atan2(1, 1); // 0.7854... radians ( 45°)
Math.atan2(1, -1); // 2.3562... radians (135°)
Math.atan2(-1, -1); // -2.3562... radians (-135°, or 225°)
// Hyperbolic
Math.sinh(1); // 1.1752...
Math.cosh(1); // 1.5431...
Math.tanh(1); // 0.7616...
Safe Integer Arithmetic #
Integer overflow is a bug that often goes unnoticed because Java doesn’t throw an exception — the result just silently “wraps around”.
// ✗ TRAP: silent integer overflow
int a = Integer.MAX_VALUE; // 2147483647
int b = a + 1; // -2147483648 — OVERFLOW! not an exception!
System.out.println(b); // -2147483648
int result = 100000 * 100000; // OVERFLOW: 10_000_000_000 doesn't fit in an int
System.out.println(result); // 1410065408 — wrong!
// ✓ CORRECT: use the Math.*Exact() methods for overflow detection (Java 8+)
try {
int safe = Math.addExact(Integer.MAX_VALUE, 1); // throws ArithmeticException
} catch (ArithmeticException e) {
System.out.println("Overflow detected: " + e.getMessage());
}
// All *Exact operations are available:
Math.addExact(a, b); // a + b, throws on overflow
Math.subtractExact(a, b); // a - b, throws on overflow
Math.multiplyExact(a, b); // a * b, throws on overflow
Math.incrementExact(a); // a + 1, throws on overflow
Math.decrementExact(a); // a - 1, throws on overflow
Math.negateExact(a); // -a, throws on overflow (specifically MIN_VALUE)
Math.toIntExact(longVal); // casts long to int, throws if it doesn't fit
// ✓ ALTERNATIVE: use long if the value can be large
long product = (long) 100000 * 100000; // 10000000000L — safe
System.out.println(product); // 10000000000
// Manual overflow detection for critical code
public static boolean willOverflow(int a, int b) {
return ((long) a + b) != (int) ((long) a + b);
}
Floating-Point Arithmetic — Traps to Watch Out For #
// ✗ CLASSIC TRAP: floating point can't represent all decimals precisely
double a = 0.1 + 0.2;
System.out.println(a); // 0.30000000000000004 — NOT 0.3!
System.out.println(a == 0.3); // false!
// ✗ ANTI-PATTERN: comparing doubles with ==
if (0.1 + 0.2 == 0.3) { // DON'T do this!
System.out.println("equal");
}
// ✓ CORRECT: compare with an epsilon (tolerance)
double epsilon = 1e-10;
if (Math.abs((0.1 + 0.2) - 0.3) < epsilon) {
System.out.println("close enough");
}
// ✓ BETTER: for money and precision calculations — use BigDecimal
// Special floating-point values
double posInf = Double.POSITIVE_INFINITY; // 1.0 / 0.0
double negInf = Double.NEGATIVE_INFINITY; // -1.0 / 0.0
double nan = Double.NaN; // 0.0 / 0.0 or Math.sqrt(-1)
// Checking special values
Double.isInfinite(posInf); // true
Double.isNaN(nan); // true
Double.isFinite(3.14); // true (Java 8+)
// NaN is not equal to itself!
System.out.println(nan == nan); // false — the only value that isn't == itself
System.out.println(Double.isNaN(nan)); // true — the correct way
// Underflow and subnormals
double tiny = Double.MIN_VALUE; // 4.9E-324 — the smallest representable positive value
double tooSmall = tiny / 2; // 0.0 — underflows to zero
BigDecimal — Precision Calculations for Finance #
For calculations involving money, prices, interest, or any value that needs decimal precision, always use BigDecimal, not double or float.
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.math.MathContext;
public class BigDecimalDemo {
public void demo() {
// ✗ ANTI-PATTERN: money as a double
double priceDouble = 1.10;
double taxDouble = priceDouble * 0.1;
System.out.println("Tax (double): " + taxDouble); // 0.11000000000000001!
// ✓ CORRECT: money as BigDecimal
// IMPORTANT: always use the String constructor, NOT the double constructor!
BigDecimal price = new BigDecimal("1.10"); // ✓ precise: "1.10"
BigDecimal wrongPrice = new BigDecimal(1.10); // ✗ imprecise: 1.0999999...
BigDecimal valueOfPrice = BigDecimal.valueOf(1.10); // ✓ precise (converts via String)
// Arithmetic operations
BigDecimal discount = new BigDecimal("0.10");
BigDecimal total = price.add(new BigDecimal("2.50"));
BigDecimal afterDiscount = total.subtract(total.multiply(discount));
BigDecimal perUnit = total.divide(new BigDecimal("3"), 2, RoundingMode.HALF_UP);
System.out.println("Total: " + total); // 3.60
System.out.println("After discount: " + afterDiscount); // 3.240
System.out.println("Per unit: " + perUnit); // 1.20
// Division producing a non-terminating decimal REQUIRES an explicit scale!
// ✗ ANTI-PATTERN: division without a scale — throws ArithmeticException
// new BigDecimal("1").divide(new BigDecimal("3")); // Exception!
// ✓ CORRECT: specify the scale (number of decimals) and rounding mode
BigDecimal third = new BigDecimal("1").divide(
new BigDecimal("3"),
10, // 10 decimal digits
RoundingMode.HALF_UP
);
System.out.println(third); // 0.3333333333
// RoundingMode — choose based on the business context
BigDecimal value = new BigDecimal("2.555");
System.out.println(value.setScale(2, RoundingMode.HALF_UP)); // 2.56 — common
System.out.println(value.setScale(2, RoundingMode.HALF_DOWN)); // 2.55
System.out.println(value.setScale(2, RoundingMode.HALF_EVEN)); // 2.56 (banker's rounding)
System.out.println(value.setScale(2, RoundingMode.CEILING)); // 2.56 — always up
System.out.println(value.setScale(2, RoundingMode.FLOOR)); // 2.55 — always down
System.out.println(value.setScale(2, RoundingMode.UP)); // 2.56 — away from zero
System.out.println(value.setScale(2, RoundingMode.DOWN)); // 2.55 — toward zero
// BigDecimal comparison — DON'T use equals() to compare values!
BigDecimal a = new BigDecimal("2.0");
BigDecimal b = new BigDecimal("2.00");
System.out.println(a.equals(b)); // false! different scales (1 vs 2)
System.out.println(a.compareTo(b) == 0); // true — compares mathematical values
// ✓ The correct pattern for comparisons
boolean equal = a.compareTo(b) == 0; // values equal
boolean greater = a.compareTo(b) > 0; // a > b
boolean less = a.compareTo(b) < 0; // a < b
// Other operations
BigDecimal absolute = new BigDecimal("-5.5").abs(); // 5.5
BigDecimal power = new BigDecimal("2").pow(10); // 1024
BigDecimal max = a.max(b);
BigDecimal min = a.min(b);
// Extracting values
int intVal = value.intValue(); // truncates to int
long longVal = value.longValue(); // truncates to long
double doubleVal = value.doubleValue(); // converts to double (precision may be lost)
String strVal = value.toPlainString(); // "2.555" (not scientific notation)
String strValSci = value.toString(); // could be "2.555" or "2.555E+0"
// Useful constants
BigDecimal zero = BigDecimal.ZERO;
BigDecimal one = BigDecimal.ONE;
BigDecimal ten = BigDecimal.TEN;
}
}
A Financial Calculation Example #
public class FinancialCalculations {
private static final BigDecimal HUNDRED = new BigDecimal("100");
// Calculate the total price with tax
public BigDecimal calculateTotal(BigDecimal price, BigDecimal taxPercent) {
BigDecimal tax = price.multiply(taxPercent)
.divide(HUNDRED, 2, RoundingMode.HALF_UP);
return price.add(tax).setScale(2, RoundingMode.HALF_UP);
}
// Calculate an installment (simplified)
public BigDecimal calculateInstallment(BigDecimal principal, BigDecimal annualRatePercent, int months) {
// r = monthly interest rate
BigDecimal r = annualRatePercent.divide(
HUNDRED.multiply(new BigDecimal("12")),
10, RoundingMode.HALF_UP
);
// Installment = P * r * (1+r)^n / ((1+r)^n - 1)
BigDecimal one = BigDecimal.ONE;
BigDecimal one_plus_r = one.add(r);
BigDecimal power = one_plus_r.pow(months);
return principal.multiply(r).multiply(power)
.divide(power.subtract(one), 2, RoundingMode.HALF_UP);
}
// Compare prices correctly
public boolean isMoreExpensive(BigDecimal price1, BigDecimal price2) {
return price1.compareTo(price2) > 0;
}
}
BigInteger — Arbitrarily Large Integers #
BigInteger is used when the integer value exceeds the capacity of long (more than ±9.2 × 10¹⁸) or when you need cryptographic operations.
import java.math.BigInteger;
public class BigIntegerDemo {
public void demo() {
// Creating BigIntegers
BigInteger a = new BigInteger("123456789012345678901234567890");
BigInteger b = BigInteger.valueOf(42);
BigInteger twoPow100 = BigInteger.TWO.pow(100); // 2^100
// Arithmetic operations — all return a new BigInteger (immutable)
BigInteger sum = a.add(b);
BigInteger difference = a.subtract(b);
BigInteger product = a.multiply(b);
BigInteger[] quotientRemainder = a.divideAndRemainder(b); // [quotient, remainder]
BigInteger quotient = a.divide(b);
BigInteger remainder = a.mod(b);
BigInteger power = b.pow(20); // 42^20
// Bitwise operations
BigInteger and = a.and(b);
BigInteger or = a.or(b);
BigInteger xor = a.xor(b);
BigInteger shiftLeft = a.shiftLeft(10); // a * 2^10
BigInteger shiftRight = a.shiftRight(10); // a / 2^10
// Math functions
BigInteger absolute = new BigInteger("-42").abs(); // 42
BigInteger max = a.max(b);
BigInteger min = a.min(b);
BigInteger gcd = a.gcd(b); // greatest common divisor
// Prime numbers
boolean probablyPrime = a.isProbablePrime(100); // Miller-Rabin with 100 iterations
BigInteger nextPrime = a.nextProbablePrime();
// Comparison
int cmp = a.compareTo(b); // < 0, == 0, or > 0
boolean equal = a.equals(b); // safe for BigInteger (unlike BigDecimal!)
// Conversion
long longVal = b.longValue(); // throws ArithmeticException if it doesn't fit: longValueExact()
int intVal = b.intValueExact(); // Java 8+ — throws if it doesn't fit
byte[] bytes = a.toByteArray(); // byte representation (two's complement)
String hex = a.toString(16); // hex representation
String binary = a.toString(2); // binary representation
// Constants
BigInteger zero = BigInteger.ZERO;
BigInteger one = BigInteger.ONE;
BigInteger two = BigInteger.TWO;
BigInteger ten = BigInteger.TEN;
}
// Factorial — a real-world example of needing BigInteger
public BigInteger factorial(int n) {
BigInteger result = BigInteger.ONE;
for (int i = 2; i <= n; i++) {
result = result.multiply(BigInteger.valueOf(i));
}
return result;
}
// factorial(100) = 93326215443944152681699238856266700490715968264381...
// (158 digits — doesn't fit in a long!)
// Fibonacci with BigInteger
public BigInteger fibonacci(int n) {
BigInteger a = BigInteger.ZERO;
BigInteger b = BigInteger.ONE;
for (int i = 0; i < n; i++) {
BigInteger temp = a.add(b);
a = b;
b = temp;
}
return a;
}
}
Random — Random Numbers #
Java provides several classes for generating random numbers with different characteristics.
java.util.Random #
import java.util.Random;
public class RandomDemo {
public void demo() {
Random rng = new Random();
// Random integers
int random = rng.nextInt(); // the whole int range
int randomBounded = rng.nextInt(100); // 0 to 99 (exclusive)
int randomRange = rng.nextInt(50, 100); // 50 to 99 (Java 17+)
// Other types
long randomLong = rng.nextLong();
long randomLongRange = rng.nextLong(1000L); // 0 to 999 (Java 17+)
double randomDouble = rng.nextDouble(); // 0.0 to < 1.0
double randomDoubleRange = rng.nextDouble(1.0, 10.0); // Java 17+
boolean randomBool = rng.nextBoolean();
// Gaussian distribution (normal distribution)
double gaussian = rng.nextGaussian(); // mean=0, std=1
// Streams of random numbers (Java 8+)
int[] randomArray = rng.ints(10, 1, 101) // 10 numbers, 1-100
.toArray();
double[] randomDoubles = rng.doubles(5, 0.0, 1.0) // 5 doubles, 0-1
.toArray();
// Deterministic seed — for testing and reproducibility
Random seeded = new Random(12345L); // fixed seed
int value1 = seeded.nextInt(100); // ALWAYS produces the same value
int value2 = seeded.nextInt(100); // a deterministic sequence
// Random element from an array
int[] data = {10, 20, 30, 40, 50};
int randomElement = data[rng.nextInt(data.length)];
// Shuffle an array
Integer[] arr = {1, 2, 3, 4, 5};
java.util.List<Integer> list = java.util.Arrays.asList(arr);
java.util.Collections.shuffle(list, rng);
}
}
ThreadLocalRandom — Faster for Multi-threading #
import java.util.concurrent.ThreadLocalRandom;
public class ThreadLocalRandomDemo {
// ✗ ANTI-PATTERN: sharing one Random instance across many threads — contention!
private static final Random SHARED_RANDOM = new Random();
// ✓ CORRECT: ThreadLocalRandom — one instance per thread, no contention
public void demo() {
// No need to store an instance — access via the static method
int random = ThreadLocalRandom.current().nextInt(1, 101); // 1 to 100
// Inside parallel streams — ThreadLocalRandom is used automatically
int[] results = ThreadLocalRandom.current().ints(1000, 0, 100).toArray();
}
}
SecureRandom — Cryptographically Secure Random Numbers #
For security needs (tokens, passwords, cryptographic keys), plain Random isn’t enough because its output can be predicted if the seed is known.
import java.security.SecureRandom;
public class SecureRandomDemo {
// ✗ ANTI-PATTERN: Random for security tokens — predictable!
public String createUnsafeToken() {
Random rng = new Random();
StringBuilder token = new StringBuilder();
for (int i = 0; i < 32; i++) {
token.append(Integer.toHexString(rng.nextInt(16)));
}
return token.toString(); // NOT SAFE
}
// ✓ CORRECT: SecureRandom for security tokens
private static final SecureRandom SECURE_RNG = new SecureRandom();
private static final char[] HEX_CHARS = "0123456789abcdef".toCharArray();
public String createSafeToken(int byteLength) {
byte[] bytes = new byte[byteLength];
SECURE_RNG.nextBytes(bytes);
// Convert to a hex string
StringBuilder sb = new StringBuilder(byteLength * 2);
for (byte b : bytes) {
sb.append(HEX_CHARS[(b >> 4) & 0xF]);
sb.append(HEX_CHARS[b & 0xF]);
}
return sb.toString();
}
// Or use Base64 for shorter tokens
public String createBase64Token(int byteLength) {
byte[] bytes = new byte[byteLength];
SECURE_RNG.nextBytes(bytes);
return java.util.Base64.getUrlEncoder()
.withoutPadding()
.encodeToString(bytes);
}
// Random numbers in a range (safe)
public int safeNumberInRange(int min, int max) {
// nextInt(bound) from SecureRandom avoids modulo bias
return min + SECURE_RNG.nextInt(max - min);
}
}
SecureRandomis far slower than plainRandombecause it uses OS entropy sources (like/dev/urandomon Linux). Use it only for cryptographic needs — session tokens, password resets, API keys. For simulations, games, or test data,RandomorThreadLocalRandomis sufficient.
Numeric Conversion #
Between Primitive Types #
// Widening — automatic, loses no data
byte b = 100;
short s = b; // automatic
int i = s; // automatic
long l = i; // automatic
float f = l; // automatic — but CAUTION: float only has a 23-bit mantissa
double d = f; // automatic
// Narrowing — must be explicit with a cast, can lose data
double doubleValue = 9.99;
int intValue = (int) doubleValue; // 9 — the decimal part is discarded (truncated, not rounded!)
long longValue = 300L;
byte byteValue = (byte) longValue; // 44 — overflow, wraps around!
// The safe way to narrow
try {
int exact = Math.toIntExact(longValue); // throws if it doesn't fit
} catch (ArithmeticException e) {
System.out.println("The value doesn't fit in an int");
}
// String to numeric (already covered in Strings, repeated for completeness)
int fromString = Integer.parseInt("42");
double fromStringD = Double.parseDouble("3.14");
BigDecimal fromStringBD = new BigDecimal("1234.56");
Bit and Byte Representations #
// Bit representations of float/double
int floatBits = Float.floatToIntBits(3.14f); // bit pattern as an int
int floatRawBits = Float.floatToRawIntBits(3.14f); // without NaN normalization
float back = Float.intBitsToFloat(floatBits); // 3.14
long doubleBits = Double.doubleToLongBits(3.14);
double backD = Double.longBitsToDouble(doubleBits);
// Integer representations in various bases
String hex = Integer.toHexString(255); // "ff"
String binary = Integer.toBinaryString(10); // "1010"
String octal = Integer.toOctalString(8); // "10"
// Parsing from various bases
int fromHex = Integer.parseInt("FF", 16); // 255
int fromBinary = Integer.parseInt("1010", 2); // 10
// Low-level bit operations
Integer.bitCount(255); // 8 — the number of 1 bits
Integer.highestOneBit(100); // 64 — the highest active bit
Integer.lowestOneBit(100); // 4 — the lowest active bit
Integer.numberOfLeadingZeros(1); // 31
Integer.numberOfTrailingZeros(8); // 3
Integer.reverse(1); // 2147483648 — reverses all bits
Integer.reverseBytes(0x12345678); // 0x78563412
When to Use Which Numeric Type #
double / float
✓ Scientific and engineering calculations
✓ Graphical coordinates and geometry
✓ Statistics and machine learning
✗ DON'T use for money, prices, or financial values
✗ DON'T use for values compared with ==
int / long
✓ Counters, indices, IDs, timestamps
✓ Bit manipulation
✗ DON'T use if the value can overflow — use *Exact() or BigInteger
BigDecimal
✓ Money, prices, financial values
✓ Calculations needing exact decimal precision
✓ Tax rates, interest, discounts
✗ DON'T create from a double — always from a String or valueOf()
✗ DON'T use equals() for value comparison — use compareTo()
BigInteger
✓ Numbers exceeding Long.MAX_VALUE
✓ Cryptography (RSA, DH key generation)
✓ Factorials, large Fibonacci, combinatorics
✗ Slower and more memory-hungry than int/long
Random vs ThreadLocalRandom vs SecureRandom
Random → single-threaded, deterministic seeds for testing
ThreadLocalRandom → multi-threaded, faster than Random
SecureRandom → cryptography — tokens, passwords, keys
Summary #
- Integer overflow is silent — Java doesn’t throw an exception when
intorlongoverflows. UseMath.addExact(),Math.multiplyExact(), and similar for overflow detection in critical calculations.doubleis inaccurate for money —0.1 + 0.2 != 0.3in floating point. UseBigDecimalfor all financial calculations without exception.- Create
BigDecimalfrom aString, not from adouble—new BigDecimal("0.1")is precise,new BigDecimal(0.1)inherits floating-point inaccuracy.- Compare
BigDecimalwithcompareTo(), notequals()—new BigDecimal("2.0").equals(new BigDecimal("2.00"))returnsfalsebecause the scales differ.Math.round()with negative .5 values behaves unintuitively —-2.5rounds to-2, not-3, because “round half up” means toward positive infinity. UseRoundingMode.HALF_UPwithBigDecimalfor more predictable results.ThreadLocalRandomfor multi-threading — more efficient than sharing oneRandominstance, which causes contention in concurrent environments.SecureRandomonly for cryptography — far slower thanRandom, use it only for tokens, passwords, and keys that need cryptographic unpredictability.NaN != NaNis the only value in Java that isn’t equal to itself. Always useDouble.isNaN()to check for NaN, not==.
← Previous: IO