Variables, Types & Operators — Java's Type System, Precisely

Why Java Is So Particular About Types

In some languages, a variable is a bucket that can hold anything. In Java, every variable carries a label — its type — and the compiler checks every assignment against that label before your program ever runs. This is deliberate. Most bugs in dynamic languages ("expected a number, got a string") show up in production at 3 a.m. In Java, the same bug shows up in your editor as a red squiggle, in seconds.

But the type system only helps if you understand exactly what it promises. That starts with the split at the heart of Java: primitives vs. reference types.

Primitives vs. Reference Types: Two Different Things in Memory

Java has exactly eight primitive types: byte, short, int, long, float, double, char, boolean. A primitive variable holds the value itself. An int variable named age contains the number 30 directly, packed into 32 bits.

Everything else — String, arrays, objects you create — is a reference type. A reference variable does not hold the object. It holds an address — a pointer to where the object lives in memory. The variable is like a sticky note with a shelf location; the object sits on the shelf.

This one distinction explains a surprising amount of Java behavior. Watch:

int a = 30;
int b = a;     // copies the VALUE 30
b = 40;
System.out.println(a);  // 30 — a is untouched

String s1 = new String("hello");
String s2 = s1;         // copies the REFERENCE, not the object
System.out.println(s1 == s2);  // true — both notes point to the same shelf

Output:

30
true

Copying a primitive copies the value — the two variables are independent afterward. Copying a reference copies the address — both variables point at the same object. When you change the object through one reference, the other sees it. We'll work through the consequences of this (and null, the sticky note with no address at all) in later posts.

The 8 Primitives, and What They Default To

TypeSizeWhat it holdsDefault (field)
byte8 bitstiny integers0
short16 bitssmall integers0
int32 bitseveryday integers0
long64 bitshuge integers0L
float32 bitsdecimals (approximate)0.0f
double64 bitsdecimals (approximate)0.0
char16 bitsa single UTF-16 code unit'\u0000'
boolean(JVM-defined)true / falsefalse

A crucial Java rule: local variables have no default. Fields (variables inside a class) get the defaults above, but a variable declared inside a method must be assigned before you use it — the compiler refuses to guess:

void demo() {
    int count;
    System.out.println(count);  // COMPILE ERROR: variable count might not have been initialized
}

This is a feature, not an annoyance. It catches the "I forgot to set it" bug class at compile time instead of letting a silent zero flow through your logic. (Post 01 covered compiling; this is your first taste of the compiler as a safety net.)

Which numeric type should you reach for?

Decision rule, in order of preference:

  1. int — the default for integers. Loop counters, counts, indexes, quantities. All arithmetic on byte/short/char gets promoted to int anyway, so smaller types rarely buy you anything in local variables.
  2. long — when values can exceed int's range (timestamps in millis, file sizes, database IDs).
  3. double — the default for decimals. Scientific measurement, geometry, graphics.
  4. Neither float nor double for money. Both are binary floating-point and cannot represent 0.1 exactly — errors accumulate. For currency, use BigDecimal or integer cents (a dedicated post covers this).

var: Letting the Compiler Fill In the Type

Since Java 10, you can declare a local variable with var and let the compiler infer the type from the initializer:

var name = "Aisha";              // String
var count = 0;                   // int
var price = 19.99;               // double
var users = new ArrayList<String>();  // ArrayList<String>

Output: (types are fixed at compile time — this prints via a quick check)

var name = "Aisha";  // String
var count = 0;       // int
var price = 19.99;   // double

var is not dynamic typing. Once inferred, the type is locked — count = "hello" won't compile. It's purely shorthand for the compiler reading the type from the right-hand side.

When var is idiomatic

Use it when the type is obvious from the initializer and repeating it adds noise:

// Idiomatic: the type is staring at you from the right-hand side
var users = new ArrayList<String>();
var config = loadConfiguration();
for (var entry : entries) { System.out.println(entry); }

When var hurts readability

Never use var when the initializer doesn't make the type clear — especially for method return values whose type you'd have to look up:

// BAD: what type is `result`? You'd have to open fetchReport() to find out.
var result = fetchReport();

// GOOD: the contract is explicit at the call site.
Report result = fetchReport();

Decision rule: var is fine when deleting the explicit type costs the reader nothing. If a future reader (or you, in three months) would need to chase down a method signature to know what the variable is, spell it out. var works only for local variables — never for fields, method parameters, or return types.

Literals: Writing Values Down

A literal is a value written directly in source code. Java reads your intent from the shape of the literal:

int i = 42;            // int literal
long big = 9_000_000_000L;   // L suffix → long (underscores group digits, ignored by compiler)
double d = 19.99;      // double is the default for decimals
float f = 19.99F;      // F suffix → float (without F this is a compile error)
int hex = 0xFF;        // 0x prefix → hexadecimal = 255
int binary = 0b1010;   // 0b prefix → binary = 10
char c = 'A';          // single quotes → char
boolean ok = true;

Output:

9000000000
255
10

(System.out.println(big); println(hex); println(binary); produce the three lines above.)

Two gotchas worth memorizing now:

  • float f = 19.99; does not compile. The literal 19.99 is a double, and narrowing double → float needs an explicit cast or the F suffix.
  • Always use uppercase L for long literals. Lowercase l is legal but reads as the digit 1 — a classic misread bug.

Casting: Widening Is Safe, Narrowing Is Lossy

Converting between numeric types comes in two flavors, and the compiler treats them very differently.

Widening: safe, automatic

Going from a smaller type to a larger one (int → long, int → double) can never lose information, so Java does it silently:

int small = 100;
long big = small;      // automatic widening — no cast needed
double d = small;      // also automatic: 100.0
System.out.println(big + " " + d);

Output:

100 100.0

Narrowing: lossy, needs your explicit permission

Going the other direction can destroy data — fractional parts get chopped, big values wrap around. Java makes you write the cast explicitly, as a way of saying "I know what I'm doing":

double pi = 3.99;
int almost = (int) pi;          // explicit cast — fractional part is TRUNCATED, not rounded
System.out.println(almost);     // 3

long huge = 3_000_000_000L;
int wrapped = (int) huge;       // overflows int's range — result is garbage
System.out.println(wrapped);    // -1294967296

Output:

3
-1294967296

Decision rule: a cast is a claim. (int) pi claims "I accept truncation." (int) huge claims "I accept wrap-around" — and as you can see, that claim is usually wrong. If you're casting because the compiler complains, stop and ask whether the types are right before silencing it with a cast.

Operators Tour

Arithmetic: + - * / %

Mostly what you'd expect — with one trap that catches nearly every beginner:

The integer division trap

System.out.println(5 / 2);      // 2, not 2.5!
System.out.println(5.0 / 2);    // 2.5
System.out.println(5 / 2.0);    // 2.5

Output:

2
2.5
2.5

When both operands are integers, / is integer division: it truncates the fraction. This isn't a bug — it's how the JVM's integer division is defined, and it's what makes % (remainder) useful. But it silently ruins averages, ratios, and percentages:

int passed = 7, total = 10;
double rate = passed / total;   // 0! Integer division happens FIRST, then widening to 0.0
System.out.println(rate);       // 0.0

double fixed = (double) passed / total;  // cast one operand → 0.7
System.out.println(fixed);      // 0.7

Output:

0.0
0.7

The cast has to happen before the division. Casting the result is too late — the truncation already happened. This is the single most common arithmetic bug in beginner Java; now you know it on sight.

Overflow: when the number doesn't fit

Integer arithmetic wraps around silently when it overflows. No exception, no warning — just a wrong number:

int max = Integer.MAX_VALUE;
System.out.println(max);            // 2147483647
System.out.println(max + 1);        // -2147483648 — wrapped around!

Output:

2147483647
-2147483648

For code where overflow would be a real bug (money, inventory, scores), Java gives you the safe alternative — Math.addExact, Math.subtractExact, Math.multiplyExact — which throw ArithmeticException instead of wrapping:

try {
    int result = Math.addExact(Integer.MAX_VALUE, 1);
    System.out.println(result);
} catch (ArithmeticException e) {
    System.out.println("Overflow caught: " + e.getMessage());
}

Output:

Overflow caught: integer overflow

Decision rule: default arithmetic is fine for bounded, obviously-small values (loop counters). Reach for addExact (or long/BigInteger) anywhere the inputs come from outside your control.

Comparison: == != < > <= >=

For primitives, == compares values — exactly what you'd expect. For objects, == compares references (do both notes point to the same shelf?), not the contents. Comparing two strings with == is one of Java's classic bugs; objects get their own dedicated treatment in post 07 ("Don't Trust =="), where we'll cover .equals() properly.

Logical operators and short-circuiting: && || !

&& and || are short-circuit operators: they stop evaluating as soon as the answer is determined. This isn't just an optimization — it's a tool:

String name = null;
// The right side only runs if name is not null — no NullPointerException
if (name != null && name.length() > 0) {
    System.out.println("has a name");
} else {
    System.out.println("no name");
}

Output:

no name

If && evaluated both sides, name.length() would crash on null. Short-circuiting makes the null-check-first pattern safe. The same logic applies to ||: the right side runs only if the left is false.

Java also has single & and | variants that don't short-circuit — they always evaluate both sides. In modern code you almost always want && / ||; the single-character forms are legacy edge cases you'll meet in old codebases.

The ternary operator: a teaser

Java's one three-operand operator is a compact if/else expression:

int score = 85;
String grade = score >= 60 ? "pass" : "fail";
System.out.println(grade);   // pass

Output:

pass

Read it as: "if score ≥ 60, then "pass", else "fail"." Handy for simple either/or assignments; we'll lean on it once control flow (post 03) is in place. Nest ternaries and readability dies — one level is the sane limit.

String Concatenation with + (Teaser)

+ on strings glues them together, and Java will happily convert other types to strings to make it work:

String label = "Score: " + 95 + " / " + 100;
System.out.println(label);   // Score: 95 / 100

Output:

Score: 95 / 100

Two warnings, full story in post 08 (Strings): first, + evaluates left to right, so "total: " + 5 + 5 gives "total: 55", not "total: 10". Second, building strings in a loop with + creates a new object every iteration — fine for a handful, a performance trap at scale. For now, use + for simple messages and move on.

Putting It Together

One snippet exercising most of what we covered — read it, predict the output, then check:

public class TypesDemo {
    public static void main(String[] args) {
        var items = 7;                 // int, inferred
        var total = 10;                // int, inferred
        double rate = (double) items / total;

        long bigId = 9_000_000_000L;
        int narrowed = (int) (bigId % Integer.MAX_VALUE);

        boolean ok = rate > 0.5 && narrowed >= 0;
        String verdict = ok ? "PASS" : "FAIL";

        System.out.println("rate=" + rate);
        System.out.println("narrowed=" + narrowed);
        System.out.println("verdict=" + verdict);
    }
}

Output:

rate=0.7
narrowed=1794967296
verdict=PASS

Key Takeaways

  • Primitives hold values; references hold addresses. Copying a reference copies the address — two variables, one object.
  • Local variables must be initialized — the compiler refuses to guess, and that's protecting you.
  • Default to int, double, and boolean. Reach for long for big counts, and never use floating-point for money.
  • Use var when the type is obvious from the initializer; spell it out when a reader would have to hunt for it. Never for unclear return types.
  • Integer division truncates (5 / 2 == 2) — cast an operand before dividing to get decimals.
  • Integer math overflows silently — use Math.addExact (and friends) where overflow would be a real bug.
  • && and || short-circuit — which is what makes null-check-first patterns safe.
  • Casts are claims. Write (int) only when you can state exactly what data you're willing to lose.

Next up: control flow — if, switch, and the loops that make programs actually do things (post 03).

Continue: Java Learning Roadmap 2026

Comments

Popular posts from this blog

JSP Servlet Interview Questions For Freshers Series 1

Java Banking Finance Services and Insurance (BFSI) domain interview questions

Java program to check even or odd number