Variables, Primitive Types, and Reference Types
Why Java makes you declare a type for every variable, and the one distinction — primitive vs. reference — that explains most of Java's variable behavior.
What you'll learn
- Declare variables using Java's eight primitive types correctly
- Explain the difference between a primitive value and a reference to an object
- Predict when assigning one variable to another copies a value versus copies a reference
Prerequisites
Explanation
Every Java variable has a declared type, fixed for its entire lifetime — you cannot later store a String in a variable declared int. Java's types split into two fundamentally different categories, and confusing them is the source of a huge share of beginner bugs. Primitive types — byte, short, int, long, float, double, char, boolean — hold their actual value directly in the variable's storage. When you write int a = 5; int b = a;, b gets its own independent copy of 5; changing b afterward never affects a.
Reference types — every class, including String, arrays, and any class you write — work differently. A reference-type variable doesn't hold the object itself; it holds a reference (conceptually, an address) pointing to an object that lives elsewhere. When you write Point p1 = new Point(1, 2); Point p2 = p1;, p2 doesn't get a new Point — it gets a copy of the reference, so p1 and p2 now both point at the exact same object. Mutating that object through p2 (e.g. p2.x = 99;) is visible through p1 too, because there was only ever one Point. This is the single most common source of "I changed one variable and a completely different one changed too" confusion for people arriving from languages that don't make this distinction as sharply.
String deserves a specific note: it's a reference type, but Java strings are immutable — no method on a String ever changes the characters it holds; every "modifying" method (toUpperCase(), concat(), replace()) returns a brand-new String and leaves the original untouched. String s = "cat"; s.toUpperCase(); does nothing observable, because the result was thrown away — you'd need s = s.toUpperCase(); to actually update what s refers to. Type conversion between primitives can be widening (int to long, done automatically, no data loss possible) or narrowing (double to int, requires an explicit cast like (int) 3.9, because precision can be lost — Java forces you to write the cast so data loss is always a visible, deliberate choice, never an accident.
Example
The same value-vs-reference distinction Java enforces, shown with a plain JS object standing in for a Java object.
// primitives: assignment copies the value
let a = 5;
let b = a;
b = 99;
console.log(a, b); // 5 99 -- unaffected
// objects: assignment copies the REFERENCE, not the object
const p1 = { x: 1, y: 2 };
const p2 = p1;
p2.x = 99;
console.log(p1.x, p2.x); // 99 99 -- same object, both variables see the changeTry it yourself
Predict the output before running, then change p2.x again and re-check your prediction.
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Guided exercise
Guided exercise
Write sharesReference(a, b) modeling Java's rule: two object (non-primitive) variables 'share a reference' if they are the exact same object (use === ). Two primitive-style plain values (numbers, strings, booleans) never share a reference even if equal -- return false for those.
Checks: the same object shares a reference with itself · two distinct objects with equal contents do not share a reference · primitive-style values never share a reference
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Stuck? Get a hint.
Independent exercise
Independent exercise
Write widensSafely(fromType, toType) modeling Java's widening rule for a small subset of types. Widening (safe, no cast needed) is allowed: int->long, int->double, long->double, float->double. Every other pair (including any narrowing, like long->int) returns false.
Checks: int->long widens · long->int narrows (not safe) · float->double widens · double->int narrows (not safe)
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Stuck? Get a hint.
Common mistakes
- Assuming `String s = "cat"; s.toUpperCase();` changes s -- it doesn't; the return value must be reassigned.
- Comparing objects (including Strings) with == expecting content equality -- == on reference types compares identity, not contents (covered in depth in the equality lesson later in this course).
- Narrowing without realizing data can be silently lost in a cast, e.g. `(int) 3.99` truncates to 3, and `(byte) 200` wraps around to a negative number because 200 doesn't fit in a byte's range.
Knowledge check
Takeaway
Primitive variables hold their value directly, so assignment copies the value; reference-type variables hold a pointer to an object, so assignment copies the pointer and both variables can end up looking at the same mutable object.
Summary
Java has eight primitive types that store values directly, and reference types (every class, including String) that store a reference to an object. Strings are immutable reference types. Widening conversions are automatic; narrowing conversions require an explicit cast because they can lose data.
References
Your notes
Notes save automatically.
Finished this lesson?
Mark it complete to track your progress and schedule a future review.
AI tutor
The optional AI tutor isn't enabled in this deployment. All lessons, exercises, quizzes, and search work fully without it.