RizTech Academy logo
RizTech Academy
Language BasicsLesson 1 of 625 min

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:

  1. Integer paise in a long. Store 8235, not 82.35. All arithmetic is exact. Divide by 100 only when printing.
  2. 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

  1. Make an int overflow and find the boundary. Print Integer.MAX_VALUE, then add 1, 2 and 3 to it. Then do the same with Long.MAX_VALUE. Say out loud what the pattern is before you read on.

  2. Build a bill twice. A tiffin costs Rs 82.35 and a customer took 26 this month. Calculate the total once with double and once with int paise. Print both to two decimal places, then print whether each equals the expected 2141.10. Only one will.

  3. Write a paise formatter. Given a long of paise, print it as Rs 2,141.10 — with the comma. Hint: %,d handles the thousands separator, and / and % give you the two halves. Test it with 0, 5, 100 and 123456789. The 5 case is the interesting one: Rs 0.05, not Rs 0.5.

  4. Cause four compiler errors deliberately and read each message: assign a String to an int; declare var x; with no initialiser; read a local variable you never assigned; assign 3000000000 to an int without the L.

  5. Reproduce the Integer cache. Compare two Integer variables holding 127 with ==, then two holding 128. Then compare two int variables holding 128 with ==. Explain to yourself why the third is true.

  6. 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 int and prints the fare in rupees and paise, with no double anywhere 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