Variables, primitives and var
Java makes you say what type every variable is. That is the deal the language offers: more typing from you, fewer surprises at runtime.
Mostly it keeps its side of the bargain. This lesson is about the four places it does not — and one of them will silently charge a customer the wrong amount if you let it.
The eight primitives
Primitives are not objects. They hold a value directly, and there are exactly eight of them.
| Type | Size | Range | Default | Literal |
|---|---|---|---|---|
byte |
8 bits | -128 to 127 | 0 |
(byte) 100 |
short |
16 bits | -32,768 to 32,767 | 0 |
(short) 1000 |
int |
32 bits | about ±2.1 billion | 0 |
42 |
long |
64 bits | about ±9.2 quintillion | 0L |
42L |
float |
32 bits | ~7 decimal digits | 0.0f |
3.14f |
double |
64 bits | ~15 decimal digits | 0.0 |
3.14 |
char |
16 bits | one UTF-16 unit | '\u0000' |
'A' |
boolean |
— | true or false |
false |
true |
In practice you will use four: int, long, double and boolean. byte turns
up when handling raw data, char when picking a string apart, and short and
float almost never — float's seven digits of precision are not enough for
anything you would trust.
Two syntax notes. A long literal needs the L: 3000000000 is an int and
will not fit. And underscores are allowed in numbers, which is worth using:
long population = 1_428_600_000L;
int pricePaise = 8_235;
Trap one: int overflows silently
int stops at 2,147,483,647. What happens next is the problem:
int max = Integer.MAX_VALUE;
System.out.println(max);
System.out.println(max + 1);
2147483647
-2147483648
No exception. No warning. It wraps around to the most negative value and carries on. Here is how that reaches real code — a tiffin service billing 25,000 rupees a month, in paise, for a thousand customers:
int monthlyPaise = 2_500_000;
System.out.println("1000 customers as int: " + (monthlyPaise * 1000));
System.out.println("as long: " + (monthlyPaise * 1000L));
1000 customers as int: -1794967296
as long: 2500000000
The first line is not an error message. It is a negative revenue figure on a report, and somebody has to notice.
Use long for anything that counts money, milliseconds, bytes or database
identifiers. Those are the four that overflow int in real systems. Note the
fix in the second line: one L on the literal is enough, because it promotes
the whole expression to long arithmetic. Writing (long) (monthlyPaise * 1000)
would not help — the overflow has already happened by then.
Trap two: dividing two integers throws away the remainder
System.out.println(7 / 2);
System.out.println(7 % 2);
System.out.println(-7 / 2);
System.out.println(-7 % 2);
System.out.println((double) 7 / 2);
System.out.println(7 / 2.0);
3
1
-3
-1
3.5
3.5
int / int is integer division: the result is an int, and the fractional part
is discarded, not rounded. 7 / 2 is 3, and so is 9 / 5.
This is usually what you want for "how many whole boxes" and never what you want
for an average. The fix is to make one side a double, either by casting or by
writing the literal with a decimal point.
Note -7 % 2 is -1, not 1. Java's % takes the sign of the left operand,
which differs from Python and catches people moving between the two. For a
guaranteed non-negative result use Math.floorMod(-7, 2), which gives 1.
Trap three: double cannot hold money
This is the one that matters most, and it is not a Java flaw — every language using IEEE 754 doubles behaves this way.
System.out.println(0.1 + 0.2);
System.out.println(1.00 - 0.90);
0.30000000000000004
0.09999999999999998
0.1 has no exact representation in binary, the same way one-third has no exact
decimal representation. Small errors accumulate:
double price = 82.35;
double total = 0;
for (int i = 0; i < 26; i++) {
total += price;
}
System.out.println("total : " + total);
System.out.printf("printed as : Rs %.2f%n", total);
System.out.println("is it 2141.10? " + (total == 2141.10));
total : 2141.099999999999
printed as : Rs 2141.10
is it 2141.10? false
Look carefully at what happened. The printed total is right, because %.2f
rounds. The stored value is wrong, and the comparison fails. A bill that looks
correct on screen and fails a reconciliation check is exactly the kind of bug
that takes a week to find.
Never use double or float for money. Two correct answers:
- Integer paise in a
long. Store 8235, not 82.35. All arithmetic is exact. Divide by 100 only when printing. BigDecimal. Exact decimal arithmetic, at the cost of verbosity and speed. Necessary when you need division with defined rounding — interest, tax, currency conversion.
The paise version of the same calculation:
int pricePaise = 8235;
int totalPaise = pricePaise * 26;
System.out.printf("Rs %d.%02d%n", totalPaise / 100, totalPaise % 100);
Rs 2141.10
Exact, and the integer division and modulo from the previous section are doing
the formatting. double remains the right type for measurements — weights,
distances, percentages — where a fifteenth-decimal-place error is meaningless.
Converting between types
Widening happens automatically, because nothing can be lost:
byte → short → int → long → float → double
char → int → long → float → double
Narrowing requires an explicit cast, because something can be lost — and it will be, quietly:
double d = 9.99;
System.out.println((int) d);
long big = 3_000_000_000L;
System.out.println((int) big);
9
-1294967296
(int) truncates towards zero; it does not round. (int) 9.99 is 9 and
(int) -9.99 is -9. For rounding use Math.round.
And char is a number underneath:
char c = 'A';
int code = c;
System.out.println(c + " is " + code);
System.out.println((char) (c + 1));
A is 65
B
The compound assignment trap
+= performs a hidden cast that + does not:
byte b = 10;
b += 300; // compiles
System.out.println(b);
int i = 5;
i += 2.9; // compiles
System.out.println(i);
54
7
b = b + 300 would be rejected by the compiler. b += 300 is accepted, because
the specification defines it as including an implicit narrowing cast. This is one
of the few places where Java's type system lets something through silently, and
it is worth knowing so that a surprising number in a byte or short does not
mystify you.
Reference types, wrappers, and an == trap
Everything not in the table of eight is a reference type — String,
ArrayList, anything you write. A variable of a reference type holds a reference
to an object, not the object.
Each primitive has a wrapper class — int/Integer, long/Long,
double/Double, boolean/Boolean — because collections cannot hold
primitives. List<int> is illegal; List<Integer> is not. Java converts between
them automatically, which is called autoboxing.
That convenience has a sharp edge:
Integer a = 127, b = 127;
Integer x = 128, y = 128;
System.out.println(a == b);
System.out.println(x == y);
System.out.println(x.equals(y));
true
false
true
Same code, different answer, and the only difference is the number. The JVM
caches Integer objects from -128 to 127 and reuses them, so a and b are the
same object. 128 is outside the cache, so x and y are two objects — and ==
on reference types asks "the same object?", not "the same value?".
Use == for primitives. Use .equals() for everything else. The strings
lesson has the same trap with a different disguise, and it is the commonest bug
in beginner Java.
One more reason to prefer int over Integer for ordinary arithmetic: an
Integer can be null, and unboxing a null throws NullPointerException on a
line that contains no obvious method call.
var
Since Java 10, a local variable can have its type inferred:
var pricePaise = 8_235; // int
var customer = "Priya Deshmukh"; // String
var orders = new ArrayList<String>(); // ArrayList<String>
The variable is still statically typed. var is not Object and not JavaScript's
var — pricePaise is an int forever, and assigning a string to it will not
compile.
It only works where the compiler can see the type on the same line. These all fail:
var a; // no initialiser
var b = null; // no type in null
var c = () -> System.out.println("hi"); // a lambda has no type of its own
error: cannot infer type for local variable a
(cannot use 'var' on variable without initializer)
error: cannot infer type for local variable b
(variable initializer is 'null')
error: cannot infer type for local variable c
(lambda expression needs an explicit target-type)
var is also not allowed on fields, method parameters, or return types. Local
variables only.
When to use it. When the right-hand side already says the type:
var scanner = new Scanner(System.in) repeats nothing. When it does not —
var result = process(input) — write the type out. The reader of your code has
no IDE hover in a pull request.
final, and default values
final means the variable cannot be reassigned:
final int maxTiffinsPerDay = 200;
It does not make the object immutable — a final List can still have things
added to it. It only stops the variable pointing somewhere else.
Last thing, and it is asked in interviews. Fields get default values; local variables do not.
static class Holder {
int count; // 0
String name; // null
boolean active; // false
}
But inside a method:
int count;
System.out.println(count);
error: variable count might not have been initialized
The compiler will not let you read a local variable you have not written to. This is a good rule that prevents a whole class of bug, and the reason fields are different is that an object's memory is zeroed when it is allocated.
Check your work
Why does monthlyPaise * 1000 print a negative number? Both operands are
int, so the multiplication is done in int arithmetic and overflows past
2,147,483,647, wrapping to negative. Writing 1000L makes the whole expression
long. Casting the result afterwards is too late.
What is 9 / 5 and what is 9 % 5? 1 and 4. Integer division discards
the remainder; it does not round. 9 / 5.0 is 1.8.
What is -7 % 2 in Java? -1. The result takes the sign of the left
operand. Math.floorMod(-7, 2) gives 1 if you need it non-negative.
Why did total == 2141.10 print false when the total printed as
Rs 2141.10? The stored value was 2141.099999999999 — accumulated binary
representation error. %.2f rounded it for display and hid the problem. Store
money as integer paise in a long, or use BigDecimal.
What does (int) 9.99 give, and (int) -9.99? 9 and -9. A cast
truncates towards zero. Math.round rounds.
Why does a == b print true for 127 and false for 128? The JVM caches
Integer objects from -128 to 127, so both variables reference the same cached
object. Outside that range two separate objects are created, and == on
references compares identity. Always use .equals() on wrappers.
Why does var b = null; not compile? null carries no type, so there is
nothing to infer. Same reason a bare var a; fails.
Which of these compile: byte b = 10; b = b + 300; and byte b = 10; b += 300;? Only the second. += includes an implicit narrowing cast, so it
compiles and prints 54.
Practice 2, the two bills. The double total comes out as
2141.099999999999 and fails the equality check; the paise total is exactly
214110. Both print as Rs 2141.10, which is the trap.
Practice 3, the paise formatter.
static String rupees(long paise) {
return "Rs %,d.%02d".formatted(paise / 100, paise % 100);
}
0 -> Rs 0.00
5 -> Rs 0.05
100 -> Rs 1.00
123456789 -> Rs 1,234,567.89
%02d is what keeps 5 from printing as Rs 0.5. The , flag belongs only on
the rupee half — putting it on the paise would be meaningless.
Practice 6, the auto fare. Rs 25 for the first 1.5 km, Rs 16.50 per km after, everything in paise and metres:
static int fareInPaise(int metres) {
int base = 2500;
if (metres <= 1500) {
return base;
}
int extraMetres = metres - 1500;
// 1650 paise per 1000 m, rounded up to the next whole paisa
return base + (extraMetres * 1650 + 999) / 1000;
}
0 m -> Rs 25.00
1500 m -> Rs 25.00
1501 m -> Rs 25.02
2500 m -> Rs 41.50
10000 m -> Rs 165.25
Two things worth noticing. (x + 999) / 1000 is the integer way to round up —
adding one less than the divisor before dividing. And the meter charges you for
a part-paisa, which is a business decision the code has to state explicitly; a
double would have made it silently and differently.
Practice
-
Make an
intoverflow and find the boundary. PrintInteger.MAX_VALUE, then add 1, 2 and 3 to it. Then do the same withLong.MAX_VALUE. Say out loud what the pattern is before you read on. -
Build a bill twice. A tiffin costs Rs 82.35 and a customer took 26 this month. Calculate the total once with
doubleand once withintpaise. Print both to two decimal places, then print whether each equals the expected 2141.10. Only one will. -
Write a paise formatter. Given a
longof paise, print it asRs 2,141.10— with the comma. Hint:%,dhandles the thousands separator, and/and%give you the two halves. Test it with0,5,100and123456789. The5case is the interesting one:Rs 0.05, notRs 0.5. -
Cause four compiler errors deliberately and read each message: assign a
Stringto anint; declarevar x;with no initialiser; read a local variable you never assigned; assign3000000000to anintwithout theL. -
Reproduce the
Integercache. Compare twoIntegervariables holding 127 with==, then two holding 128. Then compare twointvariables holding 128 with==. Explain to yourself why the third istrue. -
Harder. An auto fare is Rs 25 for the first 1.5 km and Rs 16.50 per km after. Write a program that takes a distance in metres as an
intand prints the fare in rupees and paise, with nodoubleanywhere in it. Work entirely in paise and metres. This is how billing code is actually written.
Next: strings — immutable, pooled, and the source of the second == trap.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship