01 · Java basics — note 3 of 12
Floating point
Store decimals with huge range, and handle the rounding errors they bring.
Floating-point numbers trade exact precision for immense range by storing numbers in scientific notation, meaning decimals suffer tiny rounding errors that break exact equality comparisons.
The idea in real life
Think of splitting a 10-dollar restaurant bill equally among three friends. In decimal currency, each person pays $3.33, but multiplying $3.33 by 3 yields only $9.99, losing one cent to rounding. A computer calculates with base-2 binary fractions instead of base-10 decimals, so even familiar numbers like $0.10 cannot be stored cleanly and leak tiny fractional errors.
Why it exists
- Real-world and scientific problems work with numbers across vast scales, from tiny probabilities like 0.000000001 to galactic distances like 10^30, where storing every digit in an integer would require too much memory.
- Fixed-point numbers dedicate a fixed number of bits to the whole part and the fraction, which either limits how large numbers can grow or wastes memory when values are tiny.
- Floating-point numbers solve this by letting the binary point float: they store a fixed number of precision digits alongside an exponent that scales the value up or down.
- This trade-off gives Java programs immense range for geometry, physics, and probability, while requiring you to handle small rounding errors deliberately.
How it works
float and double
float uses 32 bits for speed; double uses 64 bits for standard precision.
As introduced in primitive types, Java provides two primitive floating-point types: float and double.
A float occupies 32 bits (4 bytes) of memory. It stores 24 bits of binary precision, which translates to roughly 7 significant decimal digits of precision. Its values range from approximately 1.4e-45 up to 3.4e38 in magnitude.
A double occupies 64 bits (8 bytes) of memory. It stores 53 bits of binary precision, which translates to roughly 15 to 16 significant decimal digits of precision. Its values range from approximately 4.9e-324 up to 1.8e308 in magnitude.
In technical interviews and competitive programming, always use double unless a specific problem or external API requires float. The extra memory of a double is negligible for typical problem constraints, but the narrow 7-digit precision of float quickly accumulates errors that cause edge cases to fail.
Decimal literals
Decimals default to double; add f for float and use e for scientific scale.
Any number written with a decimal point, such as 1.5 or 0.0, is a double literal by default in Java (Java Language Specification §3.10.2).
If you assign a decimal literal directly to a float variable without a suffix, the code fails to compile:
float weight = 1.5; // Compile error: incompatible types: possible lossy conversion from double to floatBecause Java refuses to silently narrow a 64-bit double into a 32-bit float, you must append the suffix f or F, as in float weight = 1.5f;. You can also append d or D to declare an explicit double, though it is redundant because decimals already default to double.
Java also supports scientific notation using e or E. The literal 1e9 represents 1.0 multiplied by 10^9, which evaluates to 1000000000.0. Notice that scientific literals produce a double, not an int or long. If an algorithm requires an integer constant of one billion, write 1_000_000_000 rather than 1e9 to avoid accidental conversions.
Why 0.1 + 0.2 is not 0.3
Binary cannot store tenths cleanly, just as decimal cannot store thirds cleanly.
In base 10, a fraction has a finite, terminating decimal representation only if the prime factors of its denominator divide the base 10 (whose prime factors are 2 and 5). That is why 1/2 (0.5), 1/4 (0.25), and 1/5 (0.2) terminate cleanly, while 1/3 repeats infinitely as 0.3333... because 3 does not divide 10.
In base 2 (binary), a fraction has a finite representation only if the prime factors of its denominator divide 2. This means only fractions whose denominators are powers of two (such as 1/2, 1/4, 1/8, 1/16, and 1/32) can be represented cleanly in binary.
The decimal number 0.1 is 1/10. Because 10 has a prime factor of 5, 1/10 cannot be expressed as a finite sum of binary fractions. In binary, 0.1 is an infinite repeating pattern:
0.1 (base 10) = 0.00011001100110011... (base 2)Because a 64-bit double has only 53 bits of precision, it must round this infinite binary expansion to the closest representable value. When you add 0.1 and 0.2, both operands are already slightly inexact. Their sum rounds to a number slightly larger than 0.3, which Java displays as 0.30000000000000004.
The following program adds 0.1 and 0.2 and checks whether the result equals 0.3. In this program:
firstholds the double literal0.1.secondholds the double literal0.2.sumstores the result offirst + second.matchesholds the boolean result of testing exact equalitysum == 0.3.
public class Main {
public static void main(String[] args) {
double first = 0.1;
double second = 0.2;
double sum = first + second;
boolean matches = (sum == 0.3);
System.out.println("first + second = " + sum);
System.out.println("sum == 0.3: " + matches);
}
}first + second = 0.30000000000000004
sum == 0.3: falseGoing deeper
Comparing doubles
Never compare doubles with equality; check whether the difference is within a small tolerance.
Because tiny rounding errors occur in almost every floating-point operation, testing first == second or first != second is almost always a bug.
Instead of testing for exact equality, verify that the absolute difference between the two numbers is smaller than a tiny threshold, known as epsilon ($\varepsilon$):
Math.abs(first - second) < epsilonHow you choose epsilon depends on the scale of your values:
- Absolute tolerance: When numbers stay within a moderate range (such as coordinates between -1000 and 1000), a fixed tolerance like
1e-9(0.000000001) is standard. When a problem prints a decimal answer, its statement says how much error it accepts (for example 1e-6); keep your internal tolerance smaller than that. - Relative tolerance: When numbers can grow very large (such as 10^15), the gap between adjacent representable double values is itself larger than
1e-9. In those situations, scale epsilon by the magnitude of the operands:
Math.abs(first - second) <= epsilon * Math.max(Math.abs(first), Math.abs(second))Integer division vs floating division
Dividing integers discards the remainder; convert at least one number to double.
When both operands of the division operator / are integers (int or long), Java performs integer division (Java Language Specification §15.17.2). Integer division discards any fractional remainder and truncates the result toward zero.
Therefore, 7 / 2 evaluates to 3, not 3.5.
If at least one operand is a double or float, Java promotes the other operand to a floating-point type and performs floating-point division. Expressions like 7 / 2.0, 7.0 / 2, and (double) 7 / 2 all evaluate to 3.5.
A classic mistake in geometric or statistical formulas is writing 1 / 2 * base * height or count / total * 100. Because operators of equal precedence evaluate from left to right, 1 / 2 evaluates first as two integers, yielding 0. The entire expression multiplies by zero and evaluates to 0.0.
The following program calculates triangle area using both expressions. In this program:
baseholds the triangle base integer7.heightholds the triangle height integer5.wrongAreaevaluates1 / 2 * base * height, which fails due to integer division.correctAreaevaluates0.5 * base * height, preserving the fractional result.
public class Main {
public static void main(String[] args) {
int base = 7;
int height = 5;
// Mistake: 1 / 2 evaluates to 0
double wrongArea = 1 / 2 * base * height;
// Correct: 0.5 ensures floating-point arithmetic
double correctArea = 0.5 * base * height;
System.out.println("Wrong area: " + wrongArea);
System.out.println("Correct area: " + correctArea);
}
}Wrong area: 0.0
Correct area: 17.5Rounding
Casting truncates toward zero; Math methods round down, up, or to the nearest integer.
Java gives you four distinct ways to convert a floating-point value to a whole number, each with its own rounding direction:
- Casting
(int)or(long): Discards all fractional digits, truncating directly toward zero. Positive(int) 3.8becomes3, while negative(int) -3.8becomes-3. Math.floor(value): Returns the largest mathematical integer less than or equal tovalue, returned as adouble. For positive values,Math.floor(3.8)gives3.0. For negative values,Math.floor(-3.8)rounds down to-4.0.Math.ceil(value): Returns the smallest mathematical integer greater than or equal tovalue, returned as adouble. For positive values,Math.ceil(3.2)gives4.0. For negative values,Math.ceil(-3.2)rounds up to-3.0.Math.round(value): Rounds to the nearest integer. It returns alongwhen passed adouble, and anintwhen passed afloat. The API documentation says ties round toward positive infinity:Math.round(2.5)produces3, andMath.round(-2.5)produces-2.
The following program compares each rounding strategy on positive and negative numbers. In this program:
positivestores the double3.8.negativestores the double-3.8.
public class Main {
public static void main(String[] args) {
double positive = 3.8;
double negative = -3.8;
System.out.println("(int) positive: " + (int) positive);
System.out.println("(int) negative: " + (int) negative);
System.out.println("Math.floor(negative): " + Math.floor(negative));
System.out.println("Math.ceil(negative): " + Math.ceil(negative));
System.out.println("Math.round(2.5): " + Math.round(2.5));
System.out.println("Math.round(-2.5): " + Math.round(-2.5));
}
}(int) positive: 3
(int) negative: -3
Math.floor(negative): -4.0
Math.ceil(negative): -3.0
Math.round(2.5): 3
Math.round(-2.5): -2Avoiding floating point in DSA
Avoid decimals by cross-multiplying fractions and counting money in cents.
In competitive programming and technical interviews, the most reliable way to avoid floating-point errors is to avoid floating-point types altogether.
Suppose an algorithm asks you to compare two positive fractions, num1 / den1 and num2 / den2. Performing division in double introduces rounding errors that can cause two distinct fractions to compare as equal. Instead of dividing, cross-multiply:
num1 / den1 < num2 / den2 <=> num1 * den2 < num2 * den1Provided both denominators are positive, cross-multiplication preserves the exact ordering with zero rounding error. Always perform the multiplication using long to prevent 32-bit integer overflow.
A similar principle applies to monetary calculations. Never store currency values like $19.99 in a double. Successive additions and subtractions will introduce fractional cents that cause account balances to drift. Instead, store money in the smallest currency unit (such as cents or paise) as an integer or long: store $19.99 as 1999L.
When arbitrary-precision decimal math is strictly required in enterprise applications, Java provides BigDecimal (covered in chapter 05). However, BigDecimal allocates heap objects for every operation and runs substantially slower, making integer arithmetic the preferred tool for speed and precision in algorithms.
Under the hood
How a double is stored
A double stores 1 sign bit, 11 exponent bits, and 52 fraction bits.
Java's double implements the IEEE 754 standard for 64-bit binary floating-point arithmetic (binary64), detailed in Java Language Specification §4.2.3.
The 64 bits of a double are laid out across three distinct fields:
- Sign bit (1 bit, bit 63): Stores
0for positive numbers and1for negative numbers. - Exponent (11 bits, bits 62 to 52): Stores the power of two, biased by adding 1023. An exponent bit pattern representing 1023 corresponds to 2^0 (1.0). The 11 bits allow storing biased exponents from 1 to 2046 for normalized numbers, representing scale factors from 2^-1022 up to 2^1023.
- Fraction or significand (52 bits, bits 51 to 0): Stores the fractional bits of the number. Because every normalized non-zero binary number in scientific notation starts with a leading 1 (such as
1.011..._2 * 2^e), IEEE 754 omits this leading 1 from storage. The leading 1 is implicit, giving 53 bits of effective precision from 52 stored bits.
The mathematical value of a normalized double is computed as:
value = (-1)^sign * 2^(exponent - 1023) * (1.fraction)NaN and infinity
Floating division by zero produces infinity or NaN instead of throwing exceptions.
When an integer operation divides by zero, such as 1 / 0, Java halts execution immediately with an ArithmeticException.
Floating-point division behaves completely differently: it never throws an exception (Java Language Specification §15.17.2). Instead, it returns special sentinel values defined by IEEE 754:
- Dividing a positive non-zero double by zero (
1.0 / 0.0) producesDouble.POSITIVE_INFINITY. - Dividing a negative double by zero (
-1.0 / 0.0) producesDouble.NEGATIVE_INFINITY. - Indeterminate operations, such as dividing zero by zero (
0.0 / 0.0) or taking the square root of a negative number (Math.sqrt(-1.0)), produceDouble.NaN("Not a Number").
The presence of NaN introduces an essential language rule (Java Language Specification §15.21.1): NaN is not equal to any value, including itself.
Therefore, Double.NaN == Double.NaN evaluates to false, and Double.NaN != Double.NaN evaluates to true. Because NaN fails all equality comparisons, testing value == Double.NaN is always false. To check whether a number is NaN, you must call Double.isNaN(value).
The following program demonstrates division by zero and tests NaN behavior. In this program:
positiveZeroholds the double value0.0.infinityholds the result of dividing1.0 / positiveZero.notANumberholds the result of dividing0.0 / positiveZero.selfCheckevaluatesnotANumber == notANumber.validCheckcallsDouble.isNaN(notANumber).
public class Main {
public static void main(String[] args) {
double positiveZero = 0.0;
double infinity = 1.0 / positiveZero;
double notANumber = 0.0 / positiveZero;
boolean selfCheck = (notANumber == notANumber);
boolean validCheck = Double.isNaN(notANumber);
System.out.println("1.0 / 0.0: " + infinity);
System.out.println("0.0 / 0.0: " + notANumber);
System.out.println("NaN == NaN: " + selfCheck);
System.out.println("Double.isNaN(NaN): " + validCheck);
}
}1.0 / 0.0: Infinity
0.0 / 0.0: NaN
NaN == NaN: false
Double.isNaN(NaN): trueLarge doubles lose integers
Doubles have 53 bits of precision; integers above 2^53 lose exactness.
Because a double has 53 bits of precision in its significand, it can represent every consecutive integer from 0 up to 2^53 (which equals 9,007,199,254,740,992, or roughly 9 quadrillion).
Once integers exceed 2^53, the spacing between consecutive representable double values doubles to 2, then 4, then 8. As a result, odd integers above 2^53 cannot be stored in a double. When converting a large long to double, Java silently rounds the number to the nearest representable floating-point value.
For example, 2^53 + 1 is 9007199254740993L. If you cast this integer to double, it rounds down to 9007199254740992.0. Casting the double back to long produces the rounded value, permanently discarding the lowest bit.
The following program demonstrates precision loss when converting large integers to double. In this program:
originalholds the long integer9007199254740993L(2^53 + 1).asDoublecastsoriginaltodouble.restoredcastsasDoubleback tolong.isEqualcomparesoriginal == restored.
public class Main {
public static void main(String[] args) {
long original = 9007199254740993L;
double asDouble = (double) original;
long restored = (long) asDouble;
boolean isEqual = (original == restored);
System.out.println("Original long: " + original);
System.out.println("After double: " + restored);
System.out.println("Values match: " + isEqual);
}
}Original long: 9007199254740993
After double: 9007199254740992
Values match: falseNegative zero
IEEE 754 has signed zero; 0.0 and -0.0 compare equal but behave differently.
In IEEE 754 arithmetic, zero has a sign bit. Positive zero (0.0) has sign bit 0, while negative zero (-0.0) has sign bit 1, with exponent and fraction bits all set to zero. Under standard numeric comparison, 0.0 == -0.0 evaluates to true. However, division exposes the difference: 1.0 / 0.0 yields Infinity, whereas 1.0 / -0.0 yields -Infinity. Chapter 03 shows one more place where the two zeros differ: comparing them as objects.
In Java
The following program demonstrates safe floating-point comparison, exact fraction ordering without division, and safe verification of perfect squares.
In this program:
nearlyEqual(first, second, tolerance)returns true if two doubles differ by less thantolerance.compareFractions(num1, den1, num2, den2)compares two positive fractions using cross-multiplication inlong, returning a negative number if the first is smaller, zero if equal, and a positive number if larger.isPerfectSquare(number)checks whether a non-negative integer is a perfect square. It takesMath.sqrt(number)as a first guess, moves the guess up or down by one if rounding made it wrong, and then checksroot * root == number.sumholds0.1 + 0.2.expectedholds0.3.toleranceholds1e-9.fractionOrderholds the ordering of 1/3 and 2/6.perfectSquareholds 100_000_000_000_000L (10^14).nonSquareholds 100_000_000_000_001L (10^14 + 1).
public class Main {
// Returns true if two doubles differ by less than the given tolerance.
static boolean nearlyEqual(double first, double second, double tolerance) {
return Math.abs(first - second) < tolerance;
}
// Compares positive fractions num1/den1 and num2/den2 using cross-multiplication in long.
// Returns negative if first < second, zero if equal, positive if first > second.
static int compareFractions(long num1, long den1, long num2, long den2) {
return Long.compare(num1 * den2, num2 * den1);
}
// Returns true if number is a perfect square, verifying the integer root to avoid double rounding errors.
static boolean isPerfectSquare(long number) {
if (number < 0) {
return false;
}
long root = (long) Math.sqrt(number);
while (root * root > number) {
root--;
}
while ((root + 1) * (root + 1) <= number) {
root++;
}
return root * root == number;
}
public static void main(String[] args) {
double sum = 0.1 + 0.2;
double expected = 0.3;
double tolerance = 1e-9;
System.out.println("0.1 + 0.2 == 0.3: " + (sum == expected));
System.out.println("nearlyEqual: " + nearlyEqual(sum, expected, tolerance));
int fractionOrder = compareFractions(1, 3, 2, 6);
System.out.println("compareFractions(1/3, 2/6): " + fractionOrder);
long perfectSquare = 100_000_000_000_000L;
long nonSquare = 100_000_000_000_001L;
System.out.println("isPerfectSquare(10^14): " + isPerfectSquare(perfectSquare));
System.out.println("isPerfectSquare(10^14 + 1): " + isPerfectSquare(nonSquare));
}
}0.1 + 0.2 == 0.3: false
nearlyEqual: true
compareFractions(1/3, 2/6): 0
isPerfectSquare(10^14): true
isPerfectSquare(10^14 + 1): falseTraps
Using == to compare doubles
Floating-point arithmetic accumulates tiny rounding errors in binary fractions. Comparing doubles with
==almost always fails because exact bit representations differ. Always compare whether the absolute difference is within a small epsilon tolerance.✕ WRONGif (sum == 0.3) { ... }✓ RIGHTif (Math.abs(sum - 0.3) < 1e-9) { ... }Integer division inside floating formulas
When both operands are integers, the
/operator truncates all fractional digits toward zero before any assignment occurs. In1 / 2 * base * height,1 / 2evaluates to0, causing the entire calculation to evaluate to0.0.✕ WRONGdouble halfArea = 1 / 2 * base * height;✓ RIGHTdouble halfArea = 0.5 * base * height;Casting negative doubles to int
Casting a floating-point number to
intdiscards digits toward zero, whereas mathematical flooring rounds downward toward negative infinity. For negative numbers, casting moves in the opposite direction ofMath.floor.✕ WRONGint truncated = (int) -3.7; // produces -3✓ RIGHTint floored = (int) Math.floor(-3.7); // produces -4Using float instead of double by default
A
floatmaintains only 7 significant decimal digits of precision, which quickly accumulates noticeable errors during iterative calculations. Always default to 64-bitdoublein algorithmic problem solving.✕ WRONGfloat ratio = 1.0f / 3.0f;✓ RIGHTdouble ratio = 1.0 / 3.0;Storing monetary values in double
Decimal currency values cannot be stored cleanly in binary floating point, causing fractional cents to appear or disappear during transactions. Store currency as an integer count of the smallest monetary unit.
✕ WRONGdouble price = 19.99;✓ RIGHTlong priceInCents = 1999L;Trusting Math.sqrt without verifying the integer root
When checking whether a large 64-bit integer is a perfect square, converting it to
doubleinMath.sqrtloses precision for numbers exceeding 2^53. Cast the root tolong, correct it by one up or down if needed, then confirm thatroot * root == number.✕ WRONGboolean isSquare = (Math.sqrt(number) % 1 == 0);✓ RIGHTlong root = (long) Math.sqrt(number); boolean isSquare = (root * root == number);Testing NaN with the == operator
Under the IEEE 754 standard,
NaNis not equal to any value, including itself. The conditionvalue == Double.NaNis unconditionally false, even whenvalueisNaN. Always check usingDouble.isNaN(value).✕ WRONGif (value == Double.NaN) { ... }✓ RIGHTif (Double.isNaN(value)) { ... }