Java · Fundamentals

Java Data Types & Variables — Primitives, Wrappers & the String Pool

5 min read Updated 2026-06-18

Practice Data Types & Variables interview questions

Java data types and variables

Java is statically typed, and its type system draws a hard line between primitives (raw values) and objects (references on the heap). That distinction drives how == behaves, why Integer caching produces baffling comparison results, why Strings are immutable, and what "Java is pass-by-value" really means. This guide walks through it all.

The eight primitives

TypeSizeNotes
byte8-bit−128…127
short16-bit−32,768…32,767
int32-bitdefault for integer literals
long64-bitsuffix L
float32-bitIEEE-754, suffix f
double64-bitdefault for decimal literals
char16-bita single UTF-16 code unit
booleanJVM-definedtrue / false

Primitives hold their value directly and can never be null. Everything else — String, arrays, your classes — is a reference type accessed through a pointer. Fields default to a zero value (0, false, null); local variables get no default and must be assigned before use.

Primitives vs wrappers, and autoboxing

Every primitive has an object wrapper (int->Integer, double->Double, …). The primitive is a raw value; the wrapper is a heap object that can be null and is required wherever Java needs an object — generics (List<Integer>, never List<int>), collections, nullable fields.

int a = 5;          // raw value
Integer b = 5;      // object reference, can be null
Integer c = null;
int x = c;          // NullPointerException — unboxing null

Autoboxing converts a primitive to its wrapper automatically; unboxing does the reverse. It's convenient but has costs: unboxing null throws, and boxing in a loop creates garbage (Integer sum = 0; sum += x; boxes every iteration — use a primitive accumulator).

The == vs .equals() story (and Integer caching)

== compares references for objects and raw values for primitives; .equals() compares logical equality defined by the class. This produces one of Java's most famous gotchas:

Integer a = 127, b = 127;
System.out.println(a == b);   // true  — Java caches Integers from −128 to 127
Integer c = 128, d = 128;
System.out.println(c == d);   // false — outside the cache, distinct objects
System.out.println(c.equals(d)); // true — value comparison

The IntegerCache reuses small, common integers. The lesson: always compare wrapper values with .equals() (or unbox), never ==.

Strings: immutability and the pool

A String's character data is final and never changes; any "modification" returns a new String. Immutability enables the string pool, thread safety, cached hash codes, and security.

String s = "hello";
s.concat(" world");      // new String, discarded
s = s.concat(" world");  // reassign to the new String

The string pool stores one shared copy of each literal, so identical literals are the same object — but new String(...) creates a fresh heap object that isn't pooled.

String a = "hi", b = "hi";
String c = new String("hi");
a == b          // true  (same pooled object)
a == c          // false (new heap object)
a == c.intern() // true  (intern() returns the pooled copy)

This is why comparing strings with == seems to work for literals but breaks for computed strings — always use .equals(). For heavy string building, String concatenation in a loop is O(n²) garbage; use a StringBuilder (mutable, unsynchronized) — or StringBuffer when you need thread safety.

Pass-by-value — always

Java is always pass-by-value, no exceptions. The subtlety: for objects, the value copied is the reference, not the object. So a method can mutate the object the reference points to, but reassigning the parameter doesn't affect the caller.

void mutate(int[] arr) { arr[0] = 99; }    // changes the caller's array
void reassign(int[] arr) { arr = new int[]{0}; } // no effect on caller

int[] a = {1};
mutate(a);    // a is now {99}
reassign(a);  // a is still {99}

Say it crisply: "Java passes references by value." The reference is copied; both copies point at the same object until one is reassigned.

Conversions, overflow, and floating point

Widening (smaller -> larger type) is implicit and safe; narrowing requires an explicit cast and truncates (toward zero for floating->integer):

long l = 100;        // widening — automatic
int n = (int) 3.99;  // narrowing — explicit; truncates to 3
byte b = (byte) 300; // overflows to 44

Integer arithmetic wraps silently on overflow (Integer.MAX_VALUE + 1 is Integer.MIN_VALUE) — use long, Math.addExact, or BigInteger when that matters. And float/double are binary IEEE-754, so 0.1 + 0.2 != 0.3; use BigDecimal (constructed from a String) for exact decimals, and never compare floats with ==.

In expressions, operands smaller than int promote to int, and integer division truncates — 5 / 2 is 2, while 5 / 2.0 is 2.5.

var, final, and static

var (Java 10+) is local type inference — still statically typed, just less verbose, and only for local variables with an initializer. final means assign-once: for a reference it fixes which object the variable points to, not the object's contents. static binds a member to the class (one shared copy) rather than each instance.

var list = new ArrayList<String>(); // inferred ArrayList<String>
final List<String> xs = new ArrayList<>();
xs.add("ok");   // mutating allowed
xs = new ArrayList<>(); // can't reassign a final binding

Constants are the idiomatic static final in UPPER_SNAKE_CASE.

Recap

Java splits the world into primitives (raw values, never null) and objects (references). That split explains == vs .equals(), the surprising Integer cache, and why Strings (immutable, pooled) must be compared with .equals(). Java is always pass-by-value — it copies references, so methods can mutate but not reassign. Mind widening vs narrowing, silent integer overflow, and floating-point imprecision, and use var/final/static deliberately. These fundamentals underpin everything else in Java.

Practice tests are coming soon

Get notified when interactive mock interviews and quizzes launch.