Variables #

A variable is a name given to a memory location where data is stored. In Java, every variable must have an explicitly declared type before it can be used — this is what’s called statically typed. This rule feels strict at first, but it delivers real benefits: the compiler catches type errors before the program runs, IDEs can provide accurate autocomplete, and code is easier to understand because the data type is immediately visible from the declaration. This article covers how to declare and initialize variables, the three categories of variables based on where they’re declared, scope and lifetime rules, type inference with var, and proper usage patterns.

Declaration and Initialization #

A declaration tells the compiler that a variable with a certain name and type will be used. Initialization is giving that variable its first value. Both can be done separately or at once.

// Declaration separate from initialization
int count;         // declaration — no value yet
count = 42;        // initialization — given its first value

// Declaration + initialization at once (the more common way)
int price     = 15000;
double discount = 0.1;
String name   = "Budi";
boolean active = true;

// Declaring multiple variables of the same type in one line
// (allowed, but it hurts readability — avoid for variables with different meanings)
int x = 1, y = 2, z = 3;

Local variables in Java have no default value. Using a local variable before it’s initialized is a compile error — the compiler rejects it outright. This is different from instance and static variables, which get automatic default values (0, false, null).

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

Three Categories of Variables #

Java divides variables into three categories based on where they’re declared, which also determines who owns the data and how long it lives.

flowchart TD
    A[Java Variables] --> B[Local Variables]
    A --> C[Instance Variables]
    A --> D["Static / Class Variables"]

    B --> B1["Declared inside a method/block\nOnly exists while the method runs\nNo default value"]
    C --> C1["Declared inside the class\noutside methods\nOne copy per object\nHas a default value"]
    D --> D1["Declared with static\nShared across all objects\nExists for the program's lifetime\nHas a default value"]

    style B color:#fff,stroke:#3b82f6,stroke-width:2px
    style C color:#fff,stroke:#7c3aed,stroke-width:2px
    style D color:#fff,stroke:#059669,stroke-width:2px

Local Variables #

Local variables are declared inside a method, constructor, or {} block. They only exist while that block executes and are immediately removed from the stack when the block ends.

public class LocalExample {

    public double calculateArea(double length, double width) {
        // area is a local variable — only exists inside this method
        double area = length * width;
        return area;
    } // area is removed here

    public void processData() {
        int total = 0;

        for (int i = 0; i < 10; i++) {
            // i is a local variable of the for block
            // temp is a local variable of the for block
            int temp = i * 2;
            total += temp;
        }
        // System.out.println(i);    // COMPILE ERROR: i is out of scope
        // System.out.println(temp); // COMPILE ERROR: temp is out of scope

        System.out.println("Total: " + total); // total is still in scope
    }
}

Instance Variables #

Instance variables are declared inside the class but outside any method. Every object created from that class has its own copy of the variable — changing the value in one object doesn’t affect another.

public class Product {
    // Instance variables — one per object
    String name;        // default: null
    double price;       // default: 0.0
    int stock;          // default: 0
    boolean available;  // default: false

    public Product(String name, double price, int stock) {
        this.name    = name;   // this. distinguishes the field from the parameter
        this.price   = price;
        this.stock   = stock;
        this.available = stock > 0;
    }
}

// Each object has its own copy of the instance variables
Product p1 = new Product("Laptop", 12_000_000, 5);
Product p2 = new Product("Mouse",  150_000,    20);

p1.price = 11_500_000; // only changes p1's price
System.out.println(p1.price); // 11500000.0
System.out.println(p2.price); // 150000.0 — unchanged

Static Variables (Class Variables) #

Static variables are declared with the static keyword. Unlike instance variables, there is only one copy for the entire class — all objects share the same value. Static variables fit data that is genuinely global to the class, like counters, constants, or shared configuration.

public class Employee {
    // Static variables — one for the whole class
    static int totalEmployees = 0;
    static String companyName = "PT Example";

    // Instance variables — one per object
    String name;
    int id;

    public Employee(String name) {
        totalEmployees++; // each new object increments the shared counter
        this.id   = totalEmployees;
        this.name = name;
    }
}

Employee e1 = new Employee("Andi");
Employee e2 = new Employee("Budi");
Employee e3 = new Employee("Cici");

// Access static variables via the class name (not via an object)
System.out.println(Employee.totalEmployees);  // 3
System.out.println(Employee.companyName); // PT Example

Comparing the Three Categories #

AspectLocalInstanceStatic
Declaration locationInside a method/blockInside the class, outside methodsInside the class with static
OwnershipNone (stack frame)Per objectPer class
Default valueNone (must initialize)Yes (0, false, null)Yes (0, false, null)
LifetimeWhile the method/block runsWhile the object livesFor the program’s lifetime
AccessDirect namethis.name or nameClassName.name

Scope and Shadowing #

Scope is the region of code where a variable can be accessed. Java uses block scope — a variable only lives inside the {} block where it’s declared and all blocks nested within it.

public class ScopeExample {
    int x = 10; // scope: the whole class (instance variable)

    public void method() {
        int y = 20; // scope: this method only

        if (true) {
            int z = 30; // scope: this if block only
            System.out.println(x); // ✓ can access x
            System.out.println(y); // ✓ can access y
            System.out.println(z); // ✓ can access z
        }

        System.out.println(x); // ✓
        System.out.println(y); // ✓
        // System.out.println(z); // ✗ COMPILE ERROR: z is out of scope
    }
}

Variable Shadowing #

Shadowing happens when a local variable has the same name as an instance variable. Java allows this, but it can be confusing:

public class Account {
    String name = "default"; // instance variable

    public void setName(String name) { // the parameter 'name' hides the field 'name'
        // ANTI-PATTERN: forgetting this. — the field isn't assigned, only the parameter to itself
        name = name; // ✗ does nothing useful

        // CORRECT: use this. to distinguish the field from the parameter
        this.name = name; // ✓ the field 'name' is assigned from the parameter 'name'
    }
}

Type Inference with var #

Since Java 10, you can use var instead of an explicit type for local variables. The compiler infers the type from the value given — this is called local variable type inference.

// Without var — explicit type
ArrayList<Map<String, Integer>> data = new ArrayList<Map<String, Integer>>();
BufferedReader reader = new BufferedReader(new FileReader("file.txt"));

// With var — the compiler infers the type, code is more concise
var data   = new ArrayList<Map<String, Integer>>(); // type: ArrayList<Map<String, Integer>>
var reader = new BufferedReader(new FileReader("file.txt")); // type: BufferedReader
var number  = 42;       // type: int
var name   = "Budi";   // type: String
var price  = 99.9;     // type: double

var can only be used for local variables that are initialized immediately — not for instance variables, method parameters, or return types:

// ANTI-PATTERN: var without immediate initialization — the compiler can't infer the type
var x;           // ✗ COMPILE ERROR
var y = null;    // ✗ COMPILE ERROR — the type of null can't be inferred

// ANTI-PATTERN: var for a variable name that doesn't explain the type
var d = getDashboardData(); // ✗ readers don't know d's type without looking at the method
var r = process(x);         // ✗ what type is r?

// CORRECT: var for types already clear from the context
var list = new ArrayList<String>();  // ✓ clear: ArrayList<String>
var map  = new HashMap<String, Integer>(); // ✓ clear
var i    = 0; // ✓ clear: int

// CORRECT: var is very useful in try-with-resources
try (var conn = DriverManager.getConnection(url);
     var stmt = conn.prepareStatement(sql)) {
    // ...
}
flowchart TD
    A{"Does using var\nkeep the type clear\nfrom the right-hand context?"} -- Yes --> B["✓ Use var\nmore concise code"]
    A -- No --> C["✗ Write the explicit type\nreadability matters more"]
    B --> D["var list = new ArrayList‹String›()\nvar conn = getConnection()"]
    C --> E["UserRepository repo = getRepo()\nString result = process(data)"]

    style B color:#fff,stroke:#16a34a,stroke-width:2px
    style C color:#fff,stroke:#3b82f6,stroke-width:2px

Default Values of Instance and Static Variables #

Instance and static variables that aren’t explicitly initialized get a default value from the compiler. Understanding this matters so you’re not surprised when reading a value that was never set.

TypeDefault Value
byte, short, int, long0
float, double0.0
char'\u0000' (null character)
booleanfalse
All reference types (String, arrays, objects)null
public class DefaultExample {
    int number;          // 0
    double decimal;      // 0.0
    boolean flag;        // false
    String text;         // null
    int[] arr;           // null (not an empty array!)

    public void check() {
        System.out.println(number);   // 0
        System.out.println(decimal);  // 0.0
        System.out.println(flag);     // false
        System.out.println(text);     // null

        // DON'T: access a null array directly
        // System.out.println(arr.length); // ✗ NullPointerException!

        // CORRECT: check for null first
        if (arr != null) {
            System.out.println(arr.length);
        }
    }
}

final Variables #

The final keyword on a variable means the variable’s value cannot be changed after its first initialization. This differs from constants (covered in the Constants article) — final can be used for local variables and parameters, not just static fields.

public void process(final String input) {
    // input = "other"; // ✗ COMPILE ERROR: a final parameter can't be changed

    final int limit = 100;
    // limit = 200; // ✗ COMPILE ERROR

    final List<String> list = new ArrayList<>();
    list.add("item"); // ✓ the list's contents can change
    // list = new ArrayList<>(); // ✗ the reference itself can't be reassigned
}

final on local variables and parameters is a signal to the reader that this value won’t change throughout that scope — it helps readability and prevents bugs from accidental reassignment.


Variable Anti-Patterns #

Some common mistakes often made when declaring and using variables in Java:

// ✗ ANTI-PATTERN 1: non-descriptive names
int a = getUserCount();
String s = getCustomerName();
double d = calculateTotalPrice();

// ✓ CORRECT: names explain the content and purpose
int userCount = getUserCount();
String customerName = getCustomerName();
double totalPrice = calculateTotalPrice();

// ✗ ANTI-PATTERN 2: declaring all variables at the top of the method (old C style)
public void processOrder() {
    int i, j, total, discount, price;
    String name, address, city;
    // ... 30 lines of code ...
    total = calculateTotal(); // readers must scroll up to see the types
}

// ✓ CORRECT: declare variables as close as possible to their usage
public void processOrder() {
    int total = calculateTotal();
    int discount = calculateDiscount(total);
    String shippingAddress = getAddress();
    // ...
}

// ✗ ANTI-PATTERN 3: reusing a variable for different things
int result = calculateArea(5, 3);
System.out.println("Area: " + result);
result = calculatePerimeter(5, 3); // 'result' now means something different
System.out.println("Perimeter: " + result);

// ✓ CORRECT: different variables for different values
int area      = calculateArea(5, 3);
int perimeter = calculatePerimeter(5, 3);
System.out.println("Area: " + area + ", Perimeter: " + perimeter);

// ✗ ANTI-PATTERN 4: accessing a static variable via an object — misleading
Employee e = new Employee("Andi");
System.out.println(e.totalEmployees); // ✗ looks like it belongs to object e

// ✓ CORRECT: access static variables via the class name
System.out.println(Employee.totalEmployees); // ✓ clear that this belongs to the class

Summary #

  • Three categories of variables — local (inside methods/blocks, no default), instance (per object, has default), static (per class, has default); the declaration location determines ownership and lifetime.
  • Local variables must be initialized — the compiler rejects using a local variable that hasn’t been given a value; there’s no default value like with instance variables.
  • this. to avoid shadowing — use this.fieldName = parameter in constructors and setters so class fields aren’t shadowed by same-named parameters.
  • var for type inference — use var only when the type is already clear from the right-hand side of the expression; avoid it when the type isn’t obvious from context.
  • final for values that don’t change — mark local variables and parameters with final if their values shouldn’t be reassigned; it’s a signal to readers and protection from accidental reassignment.
  • Default values only for instance and static — numeric types default to 0, boolean to false, all references to null; null is different from an empty array/collection.
  • Declare close to usage — declare variables as close as possible to where they’re first used, not all at the top of the method; this shrinks the mental scope readers must hold.
  • Access static via the class nameClassName.staticVariable is clearer than object.staticVariable because it shows the data belongs to the class, not a specific object.

← Previous: Exceptions   Next: Constants →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact