Recommended resources
The freely available resources this lesson will direct you to.
Welcome back. The previous lesson made Java’s portability claim precise: bytecode is portable, while the JVM and its generated native code adapt execution to the host operating system and CPU.
Now we return to the language-level model that makes ordinary Java code predictable. Java variables hold either primitive values or reference values. This distinction affects what a variable can contain, how it is initialized, what equality means, and whether null is possible. It also explains many interview questions that appear later around method calls, collections, wrappers, and NullPointerException.
1. Two families of Java types
Java is statically typed: every variable, parameter, field, and return value has a type known at compile time. At the highest level, Java separates types into:
- Primitive types — built-in value types.
- Reference types — types whose values refer to objects.
The Java Language Specification is the authoritative source here. Read the opening classification, then the sections on references and default initialization.
Chapter 4. Types, Values, and Variables - Oracle Help Center
Read the relevant parts of Oracle’s Java Language Specification. It establishes the precise model behind the usual shorthand that primitives “hold values” while references “point to objects.”
In Section 4.1, read the classification of types and values, including the explanation of the special null type. Then move to Section 4.3, “Reference Types and Values,” and read from reference values and objects through the discussion of multiple references to one object. Finally, in Section 4.12.5, “Initial Values of Variables,” focus on the initialization rules beginning with the default-value rule. Notice which variable kinds receive defaults and which do not.
Primitive types
Java has exactly eight primitive types:
| Category | Types |
|---|---|
| Integral numeric | byte, short, int, long, char |
| Floating-point numeric | float, double |
| Logical | boolean |
A primitive variable contains a primitive value of its declared type.
int retryCount = 3;
double taxRate = 0.2;
char grade = 'A';
boolean active = true;
These values are not objects. In particular, int is not an instance of a class, and you cannot invoke methods directly on an int.
int count = 3;
// count.toString(); // Does not compile
Java may box primitives into objects in certain contexts, such as converting an int to an Integer; that is a separate feature called autoboxing, which you will examine later. It does not make int itself an object.
Reference types
Reference types include:
- Class types, such as
String,Order, andInteger - Interface types, such as
List<String>andRunnable - Array types, such as
int[]andString[] - Type variables, such as
Tin generic code
A reference variable stores either:
- a reference to an object, or
null, meaning it currently refers to no object.
String region = "eu-west";
String[] tags = new String[3];
Runnable task = () -> System.out.println("running");
String is especially worth remembering: it feels built into the language, but it is a reference type, not a primitive. So are arrays, including arrays whose elements are primitive values:
int[] scores = {90, 82, 97};
Here, scores is a reference to an array object. Each element inside that array holds an int primitive value.
Declared type versus actual object
A reference has a declared type that restricts how you can use it at compile time. The referenced object may have a more specific runtime type.
Object message = "ready";
The variable message has declared type Object; the object is a String. You may call methods declared by Object, such as toString(), but not String-specific methods without a safe type check and cast.
System.out.println(message.toString());
// message.length(); // Does not compile: Object has no length() method
This distinction—declared reference type versus runtime object type—underpins polymorphism and method dispatch, topics you will cover in the object-oriented module.
2. Copying a value versus copying a reference
The cleanest way to understand the difference is to watch assignment.
Primitive Types and Reference Types in Java
Watch “Primitive Types and Reference Types in Java” by Neso Academy for a visual contrast between copying a primitive value and copying a reference.
Watch the basic model for the distinction between simple values and object references. Then watch assignment behavior closely: compare what happens when one int is assigned to another with what happens when one String reference is assigned to another. Treat the displayed memory addresses as a visual model, not guaranteed physical JVM addresses.
For primitives, assignment copies the value:
int first = 10;
int second = first;
second = 99;
System.out.println(first); // 10
System.out.println(second); // 99
first and second are distinct variables, each holding its own int value. Changing second does not alter first.
For references, assignment also copies a value—but that value is a reference. Both variables can therefore refer to the same object.
class Counter {
int value;
}
Counter first = new Counter();
Counter second = first;
second.value = 99;
System.out.println(first.value); // 99
No Counter object was copied when second = first ran. The reference in first was copied into second, leaving two paths to one mutable object.

A crucial refinement for interviews:
A reference is a value. Assigning it copies that reference value; it does not copy the object.
Avoid over-literal statements such as “references are memory addresses.” They are best understood as JVM-managed values that let code refer to objects. The JVM is free to move objects during garbage collection, so application code cannot observe or manipulate raw object addresses.
3. Default values: fields and array elements versus local variables
Java has default initialization, but only for particular kinds of variables.
Variables that receive defaults
When a class is initialized, its static fields receive defaults. When an object is created, its instance fields receive defaults. When an array is created, every element receives a default value.
| Type | Default value for fields and array elements |
|---|---|
byte, short, int |
0 |
long |
0L |
float |
0.0f |
double |
0.0d |
char |
'\u0000' |
boolean |
false |
| Any reference type | null |
For example:
class RequestSettings {
int timeoutSeconds; // 0
boolean auditEnabled; // false
String region; // null
Integer maxRetries; // null
}
The contrast between int and Integer matters:
int retries; // primitive
Integer limit; // reference
An uninitialized field retries becomes 0; an uninitialized field limit becomes null.
Arrays follow the same rule:
int[] ports = new int[3];
String[] names = new String[3];
System.out.println(ports[0]); // 0
System.out.println(names[0]); // null
Creating new String[3] creates one array object, not three String objects. Its three reference elements initially contain null.
Local variables receive no automatic default
Local variables must be definitely assigned before use:
void connect(boolean enabled) {
int port;
if (enabled) {
port = 8080;
}
// System.out.println(port); // Compile-time error
}
The compiler rejects this because enabled might be false, leaving port unassigned.
This is stricter than JavaScript’s common behavior. A JavaScript declaration such as let value; yields undefined; Java has no equivalent automatic local-variable value. Java forces you to decide what a local variable should mean before you use it.
Defaults are language behavior, not domain decisions
A default of 0, false, or null does not necessarily represent a sensible business value.
For example, 0 might mean:
- a genuine quantity of zero,
- a missing value,
- an invalid value,
- or “not yet configured.”
Those meanings must not be silently merged. For a backend API, initialize important fields deliberately, validate required constructor inputs, and model absence explicitly when it matters.
4. Identity: what == actually asks
The == operator has different meanings depending on its operands.
With primitives: compare values
For primitive operands, == compares primitive values according to the rules for that primitive type.
int requested = 5;
int available = 5;
System.out.println(requested == available); // true
There is no “object identity” question for two int values. The question is simply whether the values compare equal.
With references: compare object identity
For reference operands, == asks:
Do these two references refer to the exact same object, or are both references
null?
String first = new String("Java");
String second = new String("Java");
System.out.println(first == second); // false
System.out.println(first.equals(second)); // true
The two String objects have equal character content but are distinct objects. Therefore:
first == secondisfalsefirst.equals(second)istrue
A useful interview vocabulary distinction is:
| Concept | Question being asked | Typical operation |
|---|---|---|
| Primitive value comparison | Do the primitive values compare equal? | == |
| Reference identity | Are these references to the same object? | == |
| Logical equality | Should these two objects count as equal in this domain? | .equals() |
For references that alias one object, identity is true:
Counter a = new Counter();
Counter b = a;
System.out.println(a == b); // true
.equals() is behavior defined by the class
.equals() is an instance method inherited from Object. A class can override it to define logical equality. String does, so strings with the same characters compare logically equal.
However, a custom class that does not override equals() inherits Object.equals(), whose default behavior is effectively identity-based.
class User {
String email;
}
User first = new User();
first.email = "dev@example.com";
User second = new User();
second.email = "dev@example.com";
System.out.println(first == second); // false
System.out.println(first.equals(second)); // false
The objects may look equivalent to a person, but Java has no reason to assume that two User instances with equal-looking fields should be considered equal. Designing correct equals() and hashCode() methods is important enough to merit a dedicated later lesson.
One practical rule is already safe to use:
- Use
==for primitive-value comparisons. - Use
==for reference identity checks andnullchecks. - Use
.equals()orObjects.equals(...)when you need logical equality of objects.
Do not use == to compare string contents, even if it sometimes appears to work with string literals due to string pooling. That behavior is not a content-comparison strategy.
5. Nullability: a property of references, not primitives
The special literal null can be assigned to any reference type:
String comment = null;
int[] ids = null;
Runnable task = null;
But it cannot be assigned to primitive types:
// int count = null; // Does not compile
// boolean enabled = null; // Does not compile
This is a fundamental distinction:
A primitive variable always has a primitive value once it has been initialized. A reference variable may hold a reference to an object or
null.
The null reference does not identify an object. Therefore, dereferencing it fails at runtime:
String status = null;
// System.out.println(status.length()); // NullPointerException
A null check itself is safe:
if (status == null) {
System.out.println("Status is absent");
}
So is a null-safe logical equality check:
import java.util.Objects;
boolean isOpen = Objects.equals(status, "OPEN");
Objects.equals(a, b) returns true when both values are null, false when exactly one is null, and otherwise delegates to logical equality.
Prefer a meaningful invariant where possible
Rather than treating null as a routine placeholder everywhere, distinguish these cases:
- A value is required: validate it and reject
nullearly. - A value is optional: represent and document absence intentionally.
- A value has a real default: initialize it explicitly to that default.
For example, a backend request object may require an account ID but permit an optional comment:
import java.util.Objects;
class CreateOrderRequest {
private final String accountId;
private final String comment;
CreateOrderRequest(String accountId, String comment) {
this.accountId = Objects.requireNonNull(accountId, "accountId is required");
this.comment = comment;
}
}
The constructor establishes an invariant: a valid CreateOrderRequest always has an accountId. This reduces null checks throughout the rest of the code.
Wrapper types such as Integer, Boolean, and Double are references and can be null, even though their corresponding primitives cannot. Be especially cautious at API and database boundaries, where a nullable Integer may represent a missing database value. Later, when covering autoboxing, you will see why using such a null wrapper where an int is expected can cause a NullPointerException.
6. Interview Q&A: primitives, references, identity, and null
Q: What is the main difference between primitive and reference types in Java?
Answer:
Primitive types directly represent simple language-defined values such as int, double, and boolean. Reference types have values that refer to objects, including class instances and arrays, or they can hold null. Java has eight primitive types; types such as String, arrays, collections, and wrapper classes are reference types.
Q: Is String a primitive type in Java?
Answer:
No. String is a class and therefore a reference type. Java gives string literals special syntax, but a String variable still holds a reference to a String object and can be null.
Q: What happens when one primitive variable is assigned to another?
Answer:
The primitive value is copied. After int b = a, changing b does not change a, because each variable holds its own primitive value.
Q: What happens when one object reference is assigned to another?
Answer:
The reference value is copied, not the object. Both variables can refer to the same object, so mutation through one reference is observable through the other. Reassigning one variable later changes only that variable’s reference.
Q: What is the difference between == and .equals()?
Answer:
For primitives, == compares values. For references, == compares identity: whether both references refer to the same object or are both null. .equals() expresses logical equality as defined by the class. Classes such as String override it; the default implementation inherited from Object is identity-based.
Q: Which variables receive default values in Java?
Answer:
Static fields, instance fields, and array elements receive default values. Numeric primitives receive zero, boolean receives false, char receives '\u0000', and references receive null. Local variables do not receive automatic defaults and must be definitely assigned before use.
Q: Can a primitive be null?
Answer:
No. null is assignable only to reference types. A primitive field may have a default such as 0 or false, but that is a valid primitive value, not null. Wrapper types such as Integer and Boolean are references, so they can be null.
Q: Does new String[10] create ten String objects?
Answer:
No. It creates one array object containing ten String reference elements. Each element initially contains null; individual String objects must be created or assigned separately.
The core model is compact:
- Primitives represent values; they are never
null. - References refer to objects and may be
null. - Assignment copies a primitive value or copies a reference value.
==means value comparison for primitives and identity comparison for references.- Fields and array elements receive defaults; local variables must be initialized before use.
Next, you will apply this model to Java’s pass-by-value parameter passing. The key question will be why a method can mutate an object received through a parameter yet cannot reassign the caller’s variable.