AlgoInsight

Free lesson · Java Basic To Advance

Variables Types

Easy8 min readjava

The First Real Code

Module 1 explained what Java is. Module 2 explains how to actually write it. We start with the most fundamental concept: variables — named containers that hold values your program works with.

This Topic introduces variables and data types. The next 7 Topics drill into the specifics: primitives, references, casting, operators, I/O, comments, and naming conventions. By the end of Module 2, you'll have the vocabulary to write meaningful Java code.

I'm Olena, leading Module 2. The foundational syntax is exactly where my self-taught background helps — I remember which parts of Java were confusing when I learned them, and which felt natural. The order matters here. Primitives before references. Operators before I/O. Each Topic builds on the previous.

What a Variable Is

A variable in Java is a named storage location with a declared type. Three things define it:

  1. A type — what kind of value it holds (int, String, boolean, etc.)
  2. A name — an identifier you use to refer to it
  3. A value — the data currently stored in it
int age = 25;
String name = "Olena";
boolean isStudent = false;

Three variables declared and initialized. Java is statically typed, so you have to declare the type before using the variable. The compiler checks every operation against the declared type — if you try to use a String where an int is expected, the code won't compile.

This is different from Python or JavaScript, where variables can hold any type and the type can change. In Java, once you declare int age, that variable will always hold integers. You can't later say age = "twenty-five" — that's a compile error.

The trade-off is real. Static typing catches errors at compile time but makes code more verbose. Most senior engineers prefer static typing for production code because the verbosity pays off when refactoring and debugging large codebases. We'll see this trade-off throughout the course.

Declaration vs Initialization

There are two operations that often happen together but are conceptually separate:

Declaration — telling the compiler "this name exists and has this type"

int count;       // declared but not initialized
String message;  // declared but not initialized

Initialization — giving the variable its first value

count = 10;
message = "Hello";

You can combine them on one line, which is the common pattern:

int count = 10;
String message = "Hello";

Local variables (declared inside methods) must be initialized before use. The compiler enforces this — if you try to read from an uninitialized local variable, you get a compile error. This is a real bug-catcher that languages like C let through.

Instance variables (declared inside a class but outside methods) get default values automatically: 0 for numeric types, false for booleans, null for references. We cover this distinction more in Module 7 when we get to classes.

public class Example {
    int instanceField;        // automatically initialized to 0

    void method() {
        int localVariable;
        // System.out.println(localVariable);  // compile error — not initialized
        localVariable = 5;
        System.out.println(localVariable);    // ok
    }
}

The Two Categories of Types

Java has two fundamentally different categories of types:

Primitive types — built into the language, hold values directly

Reference types — everything else, hold references to objects

This distinction is one of the most-tested Java interview topics. Topics 2.2 and 2.3 cover each category in detail. The crucial difference: primitives store the value directly in memory; references store an address pointing to an object stored elsewhere (in the heap).

graph TD
    A[Variable in memory] --> B[Primitive type]
    A --> C[Reference type]

    B --> D[Value stored directly<br/>int x = 5<br/>memory holds: 5]
    C --> E[Reference points to object<br/>String s = new String text<br/>memory holds: pointer to heap]

    style B fill:#10b981,color:#fff
    style C fill:#7c3aed,color:#fff
This diagram contrasts how primitive and reference types are stored in memory. Primitive variables store their values directly. When you write int x equals 5, the memory location for x contains the value 5. Reference variables store a reference — essentially a pointer — to an object stored elsewhere in heap memory. When you write String s equals new String, the variable s holds the address of the String object, not the string content itself. This distinction matters for assignment, comparison, and method parameters — all behaviors we cover throughout this module.

Type Inference with var

Since Java 10, you can use var for local variables to let the compiler infer the type:

var age = 25;              // inferred as int
var name = "Olena";        // inferred as String
var users = new ArrayList<String>();  // inferred as ArrayList<String>

The compiler determines the type from the right-hand side. The variable is still statically typed — var age = 25 creates an int, not a "dynamic" variable. You just don't have to write the type explicitly.

var has rules:

When to use var:

Some teams adopt var universally; some avoid it entirely. The Java community is split. My recommendation: use it when it reduces redundancy without reducing clarity. The reader should be able to tell the type at a glance.

Interactive animation: Good var beside harmful var - can a reader still tell the type at a glance?

Constants with final

The final keyword marks a variable as a constant — its value cannot change after initialization:

final int MAX_USERS = 100;
final String API_URL = "https://api.example.com";

MAX_USERS = 200;   // compile error — cannot reassign final variable

final is used heavily in production Java for several reasons:

Constants by convention are named in SCREAMING_SNAKE_CASE — all uppercase with underscores. We cover naming conventions in Topic 2.8.

A subtlety: final on a reference type makes the reference unchangeable, but the object it points to can still be mutated:

final List<String> users = new ArrayList<>();
users.add("Olena");      // ok — modifying the list
users.add("Brandon");    // still ok
users = new ArrayList<>();  // compile error — can't reassign

The final keyword doesn't make the contents immutable. We cover true immutability in Module 11.

graph LR
    A[Variable declaration] --> B{Marked final?}
    B -->|No| C[Can be reassigned<br/>Value can change]
    B -->|Yes| D[Cannot be reassigned<br/>Reference is fixed]
    D --> E{Type is reference?}
    E -->|Yes| F[Object content<br/>can still be modified]
    E -->|No| G[Value is truly constant<br/>primitive cannot change]

    style D fill:#10b981,color:#fff
    style F fill:#f59e0b,color:#fff
    style G fill:#7c3aed,color:#fff
This diagram explains how the final keyword works. When you declare a variable, you can mark it as final or not. Non-final variables can be reassigned and their values can change. Final variables cannot be reassigned — but here's the subtlety. For primitive types, final means the value is truly constant — an int marked final cannot change. For reference types, final means only the reference itself is fixed — the object the reference points to can still have its internal state modified. A final List can have items added to it. A final int cannot be incremented. This distinction is one of the most-tested Java interview questions about final.

Interactive animation: final freezes the reference, not the object - the final list still grows, but reassigning it will not compile.

Variable Scope

A variable's scope is the region of code where it can be used. Java has block scope — a variable is visible from where it's declared until the end of the enclosing block (delimited by { and }).

public class Example {
    int field = 1;   // class-level scope — accessible throughout this class

    void method() {
        int localVar = 2;   // method scope — only this method

        if (localVar > 0) {
            int blockVar = 3;   // block scope — only this if-block
            System.out.println(blockVar);  // ok
        }

        // System.out.println(blockVar);   // compile error — out of scope
        System.out.println(localVar);     // ok
        System.out.println(field);        // ok
    }
}

Variables declared in a narrower scope shadow variables of the same name in wider scopes — but Java is strict about this. You cannot redeclare a local variable in the same method, even in a nested block:

void method() {
    int x = 1;
    {
        int x = 2;   // compile error — x already defined in enclosing scope
    }
}

This is different from JavaScript (where let allows this) and helps prevent a common source of bugs.

Common Pitfalls

Warning: Using == to compare strings. == on reference types compares whether they point to the same object, not whether they have equal content. "hello" == "hello" is sometimes true (due to string pooling) and sometimes false — never reliable. Always use .equals() for string content comparison. We cover this in detail in Topic 5.7.

Forgetting that primitives have default values for fields but not for locals. An int field automatically gets 0. A local int variable must be initialized before use. The compiler enforces the latter but not the former, which catches some learners off guard.

Overusing var. var is meant to reduce noise, not hide types. If a code reviewer can't tell the type at a glance, the var is doing harm. Use it judiciously.

Mistaking final for immutability. A final reference doesn't prevent object mutation. final List<String> list = new ArrayList<>() still allows list.add(...). True immutability requires either immutable classes or wrappers like List.copyOf().

Why This Matters in Interviews

Interview: Variable declaration questions appear in early-round screens. Common ones: "What's the difference between int and Integer?" (primitive vs wrapper — Topic 2.2). "What does final do?" (covered above, with the subtlety about references). "When should you use var?" (modern Java practice).

Topic 2.9 (the Interview Lens closer for this module) covers the full interview treatment for the foundational syntax cluster.

Try It

Exercise: In your IDE, create a class called VariableExperiments with a main method. Try each of these and observe the behavior:

  1. Declare a final int LIMIT = 100. Try to reassign it. Observe the compile error.
  2. Declare a final List<String> items = new ArrayList<>(). Add elements to it with items.add("test"). Observe that this works despite final.
  3. Try var x; on its own line (no initializer). Observe the compile error.
  4. Try var y = null;. Observe the compile error.
  5. Inside a nested block, try to declare int x when x already exists in the enclosing scope. Observe the error.

Seeing each error explicitly cements which rules the compiler enforces. The IDE will often explain the error inline — read the message carefully, as the same error messages appear later when you make these mistakes in real code.

Back to Java Basic To Advance