A ruler with no thirds#
Take a ruler marked only in halves, then quarters, then eighths, then sixteenths, and keep going as fine as you like.
You can measure a half exactly. A quarter, exactly. Three eighths, exactly. Now measure one third. You cannot. Not because your ruler is cheap, and not because you are measuring carelessly — one third simply is not one of the marks, and it never will be, however fine you cut them. The best you can ever do is land on the nearest mark and be a hair out.
A computer stores decimals on exactly that ruler. Its marks are halves and quarters and eighths, because it counts in twos. Anything that is not built from those pieces gets the nearest mark instead, and 0.1 is one of the numbers that misses.
Every type on this page has an edge like that: a point where it stops behaving the way the number line in your head behaves. Those edges are what exams ask about, so we will walk all eight and mark where each one breaks.
Does this print true?#
System.out.println(0.1 + 0.2 == 0.3);
True or false? Decide before you scroll.
Almost everybody says true. Nothing here looks difficult — it is the arithmetic we learned at eight years old.
false
Not a bug, and not Java being awkward. double stores numbers in binary, and 0.1 has no exact binary form, exactly as 1/3 has no exact decimal form. So 0.1 + 0.2 lands a hair above 0.3, and == compares them honestly and says no.
Every primitive type has a shape like this: a size, a range, and an edge where it stops behaving the way the number line does. Those edges are what get examined, so we will go through all eight and mark where each one breaks.
Never compare doubles with ==. Check that they are close enough: Math.abs(a - b) < 1e-9. For money, do not use double at all — use BigDecimal or count whole cents in an int.
The eight primitives#
Java has exactly eight primitive types. They are not objects, they hold a value directly, and they are the only types that work this way — everything else in Java, String included, is a reference type.
| Type | Size | Range | Literal |
|---|---|---|---|
byte | 8 bits | −128 to 127 | (byte) 100 |
short | 16 bits | −32768 to 32767 | (short) 1000 |
int | 32 bits | −2147483648 to 2147483647 | 42 |
long | 64 bits | −9223372036854775808 to 9223372036854775807 | 42L |
float | 32 bits | about 7 decimal digits of precision | 3.14f |
double | 64 bits | about 15 decimal digits of precision | 3.14 |
char | 16 bits | 0 to 65535 (a character code) | 'A' |
boolean | — | true or false only | true |
Two of those rows deserve a second look. int is the default for whole numbers and double is the default for decimals — which is exactly why the next two errors happen.
Literals have types before they are assigned#
This looks like it should obviously work, and it does not compile:
long big = 10000000000;
Big.java:3: error: integer number too large
long big = 10000000000;
^
1 error
The variable is a long, so what is too large? The literal is. Java reads 10000000000 as an int before it ever looks at the left-hand side, and ten billion does not fit in an int. The L suffix tells the compiler to read it as a long from the start:
long big = 10000000000L; // compiles
Say it out loud as “ten billion, long”. The L is not decoration and it is not part of the number; it is an instruction to the compiler about which type to read the digits as, given before it reads them.
The same thing happens with float, in the other direction:
float f = 3.14;
Flt.java:3: error: incompatible types: possible lossy conversion from double to float
float f = 3.14;
^
1 error
3.14 is a double, and squeezing a 64-bit value into a 32-bit variable could lose precision, so Java refuses. Write 3.14f.
Java will silently widen a value into a larger type, but never silently narrow one into a smaller type. Narrowing needs your explicit permission, in the form of a suffix or a cast. Even this fails, because a variable's value is not known at compile time:
int x = 5;
byte b = x; // error: incompatible types: possible lossy conversion from int to byteTrap 1 — integer division throws away the remainder#
If both operands are whole numbers, Java does whole-number division. There is no rounding involved:
System.out.println(7 / 2);
System.out.println(7 % 2);
System.out.println(7.0 / 2);
3
1
3.5
7 / 2 is 3, not 3.5 and not 4. The % operator recovers the remainder that division discarded.
The fix is to make at least one operand floating-point — but where you put the cast decides whether it works:
System.out.println((double) 7 / 2); // cast first, then divide
System.out.println((double) (7 / 2)); // divide first, then cast
3.5
3.0
In the second line the damage is already done: 7 / 2 evaluated to the int 3, and casting 3 to a double gives 3.0. Casting cannot recover information that was already discarded.
Trap 2 — overflow is silent#
System.out.println(Integer.MAX_VALUE);
System.out.println(Integer.MAX_VALUE + 1);
2147483647
-2147483648
Adding one to the largest possible int produces the smallest possible int. No exception, no warning, no crash — the value wraps around like a car odometer rolling over, and the program keeps going with a wrong number.
This is why counters, running totals, and anything measured in milliseconds usually want long, not int.
Trap 3 — decimals are not exact#
System.out.println(0.1 + 0.2);
System.out.println(0.1 + 0.2 == 0.3);
0.30000000000000004
false
double and float store values in binary. One tenth cannot be written exactly in binary for the same reason one third cannot be written exactly in decimal — the digits never stop. What gets stored is the nearest available value, and the tiny errors accumulate.
never compare floating-point values with ==. Compare the size of the difference against a small tolerance instead: Math.abs(a - b) < 1e-9. And never use double for money — count whole cents in an int or long.
Trap 4 — char is a number wearing a costume#
System.out.println('A' + 1);
System.out.println((char) ('A' + 1));
66
B
A char holds a number identifying a character; 'A' is 65. Arithmetic on it therefore produces a number, and Java promotes the result to int — which is why the first line prints 66 and not 'B'. Casting the result back to char gives the letter.
This is genuinely useful once you expect it. ch - 'a' converts a lowercase letter to its position in the alphabet, and (char) ('a' + n) converts a position back to a letter.
Casting truncates, it does not round#
System.out.println((int) 9.99);
System.out.println((int) -9.99);
9
-9
A cast to int deletes the fractional part and moves toward zero. It is not Math.floor either — floor would give −10 for the second one. If you want rounding, say so with Math.round.
Where these go wrong#
- Computing an average with integer division.
(a + b) / 2on twoints silently truncates. This is the single most common version of Trap 1. - Casting after the fact.
(double) (sum / count)is too late. Cast an operand, not the result. - Using
==on doubles. It compiles, it runs, and it returnsfalsefor values that look identical when printed. - Forgetting
Landf. Both produce compile errors that name a type you did not write, which makes them confusing until you know that the literal has a type of its own. - Expecting overflow to announce itself. It does not. A total that suddenly goes negative is the symptom.
- Reading
'A' + 1as string concatenation. Concatenation means joining text end to end, and it needs aStringon one side of the+. Both operands here are numeric, so+is plain addition.
Every primitive has an edge where it stops behaving like the number line. Integer division throws the remainder away, overflow happens in silence, decimals are approximate, and a cast truncates rather than rounds. Four edges, and they are what gets examined.
Test yourself in the free Kestrel Exams app
Topic-selectable practice — offline, no ads, no account.
Practice this topic →Frequently asked questions#
Why does 7 / 2 give 3 in Java?
Because both operands are int, so Java performs integer division and discards the remainder. It is not rounding — 7 / 2 is 3, not 4. To get 3.5, at least one side must be a floating-point value: use 7.0 / 2, or cast one operand with (double) 7 / 2.
What are the eight primitive types in Java?
byte, short, int, long, float, double, char and boolean. Everything else in Java — including String — is a reference type, not a primitive.
Why can't I write long big = 10000000000;?
Because the literal itself is treated as an int before it is ever assigned, and it does not fit in an int. The compiler reports “integer number too large”. Add an L suffix: long big = 10000000000L.
Why is 0.1 + 0.2 not equal to 0.3?
double and float store values in binary, and 0.1 has no exact binary representation, just as one third has no exact decimal representation. The sum comes out as 0.30000000000000004. Never compare floating-point values with ==; compare the absolute difference against a small tolerance instead.
Is char a number or a letter?
Both. A char stores a number from 0 to 65535 that identifies a character. Because it is a number, arithmetic works on it: 'A' + 1 evaluates to the int 66. Casting back with (char) turns 66 into 'B'.
What is the difference between casting and rounding?
Casting a double to an int truncates toward zero — it deletes the fractional part, it does not round. (int) 9.99 is 9, and (int) -9.99 is -9, not -10. Use Math.round when you want rounding.
Something here not clear? A topic you wish we covered? Tell us. We read every message, and a request is the fastest way to get a guide written — several of these exist because somebody asked.
