Free lesson · Java Basic To Advance
Variables Types
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:
- A type — what kind of value it holds (
int,String,boolean, etc.) - A name — an identifier you use to refer to it
- 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
byte,short,int,long(integers of varying sizes)float,double(floating-point numbers)char(a single character)boolean(true or false)
Reference types — everything else, hold references to objects
String, arrays, classes you define, classes from libraries
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:
- Only for local variables (inside methods). Not for fields, method parameters, or return types.
- The variable must be initialized on the same line — the compiler needs the right-hand side to infer the type.
- Can't be
nullalone —var x = nulldoesn't compile becausenulldoesn't have a meaningful inferred type.
When to use var:
- Use it when the type is obvious from the right side —
var users = new ArrayList<String>()reads cleaner thanArrayList<String> users = new ArrayList<String>() - Avoid it when the right side doesn't make the type obvious —
var result = service.process(input)hides whatresultactually is - Avoid it for primitives that might be ambiguous —
var x = 5is fine but consider whether explicitint x = 5is clearer in context
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:
- Documents intent. A
finalvariable signals "this won't change," which is information for readers. - Helps the compiler optimize. The JIT compiler can be more aggressive when it knows values won't change.
- Enables safe concurrent access. Final variables are inherently thread-safe for reading (they can't change after initialization).
- Required for variables captured by lambdas or anonymous inner classes.
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 sometimestrue(due to string pooling) and sometimesfalse— 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
intfield automatically gets0. A localintvariable must be initialized before use. The compiler enforces the latter but not the former, which catches some learners off guard.Overusing
var.varis meant to reduce noise, not hide types. If a code reviewer can't tell the type at a glance, thevaris doing harm. Use it judiciously.Mistaking
finalfor immutability. Afinalreference doesn't prevent object mutation.final List<String> list = new ArrayList<>()still allowslist.add(...). True immutability requires either immutable classes or wrappers likeList.copyOf().
Why This Matters in Interviews
Interview: Variable declaration questions appear in early-round screens. Common ones: "What's the difference between
intandInteger?" (primitive vs wrapper — Topic 2.2). "What doesfinaldo?" (covered above, with the subtlety about references). "When should you usevar?" (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
VariableExperimentswith amainmethod. Try each of these and observe the behavior:
- Declare a
final int LIMIT = 100. Try to reassign it. Observe the compile error.- Declare a
final List<String> items = new ArrayList<>(). Add elements to it withitems.add("test"). Observe that this works despitefinal.- Try
var x;on its own line (no initializer). Observe the compile error.- Try
var y = null;. Observe the compile error.- Inside a nested block, try to declare
int xwhenxalready 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.