Types and Variables
JVM Architecture and Memory Model
Section titled “JVM Architecture and Memory Model”Before understanding types and variables, you must understand where they live at runtime. The JVM Divides its runtime data area into several regions, each with distinct purposes and lifecycle rules.
graph TB
subgraph JVM Runtime Data Area
subgraph Thread-Shared
Heap["Heap<br/>(Objects, Arrays)"]
Metaspace["Metaspace<br/>(Class Metadata, Static Fields)"]
CodeCache["Code Cache<br/>(JIT Compiled Code)"]
end
subgraph Thread-Private ["Thread-Private (per thread)"]
Stack["JVM Stack<br/>(Frames, Local Vars, Operands)"]
PC["Program Counter<br/>(Bytecode Index)"]
NAT["Native Method Stack"]
end
endStack vs Heap vs Metaspace
Section titled “Stack vs Heap vs Metaspace”JVM Stack — Each thread has its own stack, divided into frames. Each method invocation pushes a New frame containing local variables (including primitive values and object references) and an Operand stack. Local variables of primitive type are stored directly on the stack; reference-type Locals store a pointer to the heap. Stack memory is automatically reclaimed when a method returns.
Heap — All objects and arrays are allocated on the heap. This includes String objects, wrapper Instances (Integer``Double), and user-defined class instances. The heap is shared across all Threads and managed by the garbage collector. Heap allocations are relatively expensive compared to Stack allocations.
Metaspace (replaced PermGen in Java 8) — Stores class metadata (method bytecodes, field and Method definitions, constant pool entries), static fields, and runtime constant pool information. Unlike PermGen, Metaspace uses native memory and can grow dynamically (bounded by -XX:MaxMetaspaceSize).
public class MemoryExample { private static long classCounter = 0; // Metaspace (static field)
private int instanceField; // Heap (part of object)
public void compute(int param) { // param is on the caller's frame long local = param * 2L; // local is on this frame's stack String s = "hello"; // reference on stack; "hello" may be in String pool (heap) Integer boxed = local; // reference on stack; Integer object on heap }}Primitive Types
Section titled “Primitive Types”Java defines eight primitive types. They are not objects — they hold raw bit patterns and live Directly on the stack or within object layouts on the heap. Primitives have no methods, no identity (only value equality), and cannot be null.
| Type | Size (bits) | Size (bytes) | Default | Range |
|---|---|---|---|---|
byte | 8 | 1 | 0 | -128 to 127 |
short | 16 | 2 | 0 | -32,768 to 32,767 |
int | 32 | 4 | 0 | -2^31 to 2^31 - 1 |
long | 64 | 8 | 0L | -2^63 to 2^63 - 1 |
float | 32 | 4 | 0.0f | IEEE 754 binary32 |
double | 64 | 8 | 0.0 | IEEE 754 binary64 |
char | 16 | 2 | '\u0000' | 0 to 65,535 (UTF-16 code unit) |
boolean | 1 | ~ | false | true or false |
Java’s integral types are two’s complement signed integers. The byte``short``intAnd long types use two’s complement representation, meaning the most significant bit is the sign bit. The char type is the only unsigned integral type — it represents a UTF-16 code unit.
int maxInt = Integer.MAX_VALUE; // 2147483647 (0x7FFFFFFF)int minInt = Integer.MIN_VALUE; // -2147483648 (0x80000000)
// Overflow wraps around silently (no exception)int overflow = maxInt + 1; // -2147483648int underflow = minInt - 1; // 2147483647
// Use Math.addExact etc. to detect overflowtry { int safe = Math.addExact(maxInt, 1); // throws ArithmeticException} catch (ArithmeticException e) { // overflow detected}IEEE 754 Floating-Point Types
Section titled “IEEE 754 Floating-Point Types”float and double conform to IEEE 754-2019 binary Interchange format. Understanding the bit layout is essential for debugging numerical issues.
float (binary32): 1 sign bit, 8 exponent bits (bias 127), 23 fraction bits.
double (binary64): 1 sign bit, 11 exponent bits (bias 1023), 52 fraction bits.
// Special values defined by IEEE 754double posInf = Double.POSITIVE_INFINITY; // 0x7FF0000000000000double negInf = Double.NEGATIVE_INFINITY; // 0xFFF0000000000000double nan = Double.NaN; // 0x7FF8000000000000
// NaN is contagious and never equals itselfSystem.out.println(Double.NaN == Double.NaN); // falseSystem.out.println(Double.isNaN(Double.NaN)); // true
// Floating-point addition is not associativedouble a = 1.0;double b = 1.0e20;double c = -1.0e20;System.out.println((a + b) + c); // 0.0System.out.println(a + (b + c)); // 1.0
// Decimal fractions cannot be represented exactly in binarySystem.out.println(0.1 + 0.2); // 0.30000000000000004System.out.println(0.1 + 0.2 == 0.3); // falseDesign Decision: Why Java Has Both Primitives and Wrappers
Section titled “Design Decision: Why Java Has Both Primitives and Wrappers”The JVM provides primitives for performance. Primitive values are stored inline on the stack or In object fields with no heap allocation, no indirection, and no object header overhead. A double Takes exactly 8 bytes; a Double object requires 16+ bytes (12-16 byte object header + 8 byte Field + padding) plus a heap pointer.
The wrappers exist for two reasons:
Generics require reference types.
List<int>is illegal; you must writeList<Integer>. This is a consequence of Java’s type system — generics are implemented via type erasure (JLS §4.6), where type parameters are replaced by their bounds (orObject) at compile time. Primitives cannot be substituted forObject.Collections and APIs designed for objects. The Java Collections Framework (pre-generics) was built around
ObjectRequiring boxing to store primitive values.
Reference Types
Section titled “Reference Types”A reference variable does not contain the object itself — it contains a pointer (or handle) to An object on the heap. The JVM specification does not mandate a specific pointer representation; it May be a direct pointer, an indirect handle, or a compressed oop (ordinary object pointer).
String s = new String("hello");// s is a reference (4 or 8 bytes on the stack)// the actual String object is on the heap// the char[] backing the String is also on the heapReference types include: classes, interfaces, arrays, enums, annotations, and (since Java 14 Preview) records. All reference variables can be nullMeaning they point to no object.
Autoboxing and Unboxing
Section titled “Autoboxing and Unboxing”Autoboxing is the automatic conversion from a primitive type to its corresponding wrapper class. Unboxing is the reverse. The compiler inserts calls to Integer.valueOf()``Integer.intValue() Etc.
// Explicit boxing (pre-Java 5)Integer boxed = Integer.valueOf(42);
// Autoboxing (Java 5+)Integer autoBoxed = 42; // compiler inserts Integer.valueOf(42)
// Explicit unboxingint primitive = autoBoxed.intValue();
// Autounboxingint autoPrimitive = autoBoxed; // compiler inserts autoBoxed.intValue()The Integer Cache
Section titled “The Integer Cache”Integer.valueOf(int) does not always create a new object. For values in the range -128 to 127 (inclusive), it returns a pre-allocated cached instance. This is mandated by JLS §5.1.7, which Requires boxing of values in the range -128 to 127 to always produce identical objects.
Integer a = 127;Integer b = 127;System.out.println(a == b); // true (cached, same object)
Integer c = 128;Integer d = 128;System.out.println(c == d); // false (different objects)System.out.println(c.equals(d)); // true (same value)
// The cache boundary can be adjusted with -XX:AutoBoxCacheMax=<size>// DANGEROUS: hidden allocations in a loopLong sum = 0L; // Long reference on stackfor (long i = 0; i < 1_000_000; i++) { sum += i; // unbox Long, add, autobox result -> allocates ~1M objects}// The above allocates approximately 1 million Long objects on the heap// Use primitive long instead:long primitiveSum = 0L;for (long i = 0; i < 1_000_000; i++) { primitiveSum += i; // no allocation, purely stack/register}Design Decision: Why Autoboxing Is a Source of Bugs
Section titled “Design Decision: Why Autoboxing Is a Source of Bugs”Autoboxing was introduced to reduce verbosity, but it creates a false impression that primitives and Wrappers are interchangeable. They are not. The key problems are:
NullPointerExceptionfrom unboxing null: Unboxing anullreference throwsNPEWith no explicit null check in your source code.
Integer maybeNull = null;int value = maybeNull; // NullPointerException — no visible dereferenceIdentity confusion:
==compares identity for reference types but value for primitives. Autoboxing silently changes the semantics of==.Hidden performance costs: Every autobox operation allocates a heap object (outside the cache range). In tight loops or performance-critical code, this creates significant GC pressure.
Ambiguous overloading: Method resolution behaves differently than expected when both primitive and wrapper overloads exist.
void overloaded(int i) { System.out.println("int"); }void overloaded(Integer i) { System.out.println("Integer"); }
overloaded(42); // prints "int" — primitive is preferredInteger boxed = 42;overloaded(boxed); // prints "Integer" — reference matches referenceImmutability
Section titled “Immutability”String objects are immutable — once created, their internal state cannot change. Every “modification” operation creates a new String object.
String original = "hello";String modified = original.concat(" world");System.out.println(original == modified); // false — different objectsSystem.out.println(original); // "hello" — unchangedThe String class is declared final and its internal value array is stored as a private final byte[] (since Java 9, using compact strings encoding; prior to Java 9 it was char[]).
Design Decision: Why String Is Immutable
Section titled “Design Decision: Why String Is Immutable”Security: String arguments are passed to
ClassLoader``FileInputStream``Runtime.exec()JDBC drivers, and many other sensitive APIs. Immutability prevents a caller from modifying the string after passing it, closing a class of security vulnerabilities.Thread safety: Immutable objects are inherently thread-safe. No synchronization is needed when sharing strings across threads. This is critical because strings are the most shared data type in Java.
Hash code caching: The hash code of a
Stringis computed lazily on first call tohashCode()and cached in a private field (hash). Since the string content never changes, the cached hash is always valid. This makesHashMapandHashSetlookups for string keys very efficient.String literal interning: The JVM can safely share string literals across the entire application, knowing they will never be modified. This saves significant memory for applications that reuse common strings.
Substring safety: Prior to Java 7u6,
substring()shared the underlyingchar[]with the original string, which could cause memory leaks. Immutable strings ensure that even this optimization was safe (the new string could not modify the shared array).
The String Pool
Section titled “The String Pool”The string pool (intern pool) is a special region of the heap that stores unique String instances. When the JVM encounters a string literal, it checks the pool. If the string already exists, the Reference is returned; otherwise, the string is added to the pool.
graph LR
subgraph String Pool ["String Pool (Heap)"]
S1["\"hello\"<br/>0x100"]
S2["\"world\"<br/>0x200"]
S3["\"hello world\"<br/>0x300"]
end
L1["String a = \"hello\""] --> S1
L2["String b = \"hello\""] --> S1
L3["String c = \"world\""] --> S2
L4["String d = a + \" \" + b"] --> S4["\"hello world\"<br/>0x400 (heap, NOT pooled)"]
L5["String e = d.intern()"] --> S3// String literals are interned at class loading timeString a = "hello";String b = "hello";System.out.println(a == b); // true — same object from the pool
// new String() always creates a new object on the heapString c = new String("hello");System.out.println(a == c); // false — c is a different object
// intern() returns the pooled instanceString d = c.intern();System.out.println(a == d); // true — d points to the pooled "hello"
// Compile-time constant concatenation is internedString e = "hel" + "lo"; // compiler folds this to "hello"System.out.println(a == e); // true
// Runtime concatenation is NOT internedString part1 = "hel";String f = part1 + "lo"; // StringBuilder is used at runtimeSystem.out.println(a == f); // falseSystem.out.println(a.equals(f)); // trueThe string pool exists to reduce memory consumption and enable fast equality comparison via Reference identity. In a typical Java application, string literals (class names, method names, Field names, string constants) make up a significant portion of the heap. Without the pool, every Occurrence of "http" across thousands of HTTP requests would allocate a separate object.
The pool also enables the == comparison for interned strings, which is an O(1) pointer comparison Versus O(n) character-by-character comparison for String.equals(). The JVM uses this internally — Class names, method descriptors, and string constants in the constant pool are all deduplicated via Interning.
StringBuilder vs StringBuffer
Section titled “StringBuilder vs StringBuffer”| Aspect | StringBuilder (Java 5+) | StringBuffer (Java 1.0+) |
|---|---|---|
| Thread-safe | No | Yes (all methods synchronized) |
| Performance | Faster (no synchronization) | Slower (lock acquisition overhead) |
| API | Same as StringBuffer | Same as StringBuilder |
// Prefer StringBuilder in single-threaded contextsStringBuilder sb = new StringBuilder(64); // specify initial capacity when knownsb.append("Hello").append(", ").append("World");String result = sb.toString();
// StringBuffer is only needed when the same buffer is shared across threads// (which is rare — in standard practice you'd use a local variable)Array Basics
Section titled “Array Basics”Arrays are objects in Java. They have a length field, can be nullAnd are allocated on the Heap. However, unlike regular objects, their structure is defined by the JVM specification, not by a Class file.
int[] primitives = new int[10]; // zero-initialized: [0, 0, 0, ...]String[] references = new String[5]; // null-initialized: [null, null, ...]
// Array initializationint[] fib = {0, 1, 1, 2, 3, 5, 8, 13};
// Multidimensional arrays (arrays of arrays)int[][] matrix = new int[3][4]; // 3 rows, 4 columnsint[][] jagged = {{1}, {1, 2}, {1, 2, 3}}; // jagged arrayCovariance
Section titled “Covariance”Java arrays are covariant: an array of a supertype reference can hold an array of a subtype Reference. This is a deliberate design decision (made for genericity before generics existed) but it Creates a type-safety hole.
// Covariance allows this assignmentNumber[] numbers = new Integer[10]; // compiles fine
// But the runtime checks will catch misusenumbers[0] = 3.14; // ArrayStoreException at runtime!// The compile-time type says "Number", but the runtime type says "Integer[]"| Aspect | int[] | ArrayList<Integer> |
|---|---|---|
| Element type | Primitives or references | References only (boxing required) |
| Size | Fixed at creation | Dynamic (resizes internally) |
| Type safety | Covariant (runtime checks) | Invariant (compile-time safe) |
| Memory overhead | Minimal (object header + N * element_size) | Object header + internal array + extra fields |
| Null elements | For reference arrays only | Allowed |
| Length query | array.length (field) | list.size() (method call) |
| Generics support | Cannot use as generic type | Full generics support |
// Primitive array — no boxing, cache-friendly, minimal overheadint[] primitiveArray = {1, 2, 3, 4, 5};int sum = 0;for (int value : primitiveArray) { sum += value; // no unboxing}
// ArrayList requires boxing — each element is a heap objectArrayList<Integer> boxed = new ArrayList<>();boxed.add(1); // autoboxing: Integer.valueOf(1)boxed.add(2); // autoboxing: Integer.valueOf(2)The var Keyword (Local Variable Type Inference)
Section titled “The var Keyword (Local Variable Type Inference)”Introduced in Java 10 (JEP 286), var enables the compiler to infer The type of local variables from the initializer. It does not make Java dynamically typed — the Inferred type is a concrete, compile-time type, and the variable cannot be reassigned to an Incompatible type.
// Clear and beneficial uses of varvar list = new ArrayList<String>(); // inferred: ArrayList<String>var stream = list.stream(); // inferred: Stream<String>var entry = map.entrySet().iterator().next(); // inferred: Map.Entry<K,V>
// These are equivalentvar name = "Alice"; // inferred: StringString name2 = "Alice"; // explicit: String
// The inferred type is determined by the right-hand sidevar number = 42; // inferred: int (NOT long, NOT Integer)var boxed = (Integer) 42; // inferred: Integer
// var cannot be used without an initializer// var x; // compile error: cannot use 'var' on variable without initializer
// var cannot be used for fields, method parameters, or return types// var is strictly for local variables with initializersImplicit Type Promotion (Widening)
Section titled “Implicit Type Promotion (Widening)”When an operation involves two operands of different numeric types, the smaller type is widened To the larger type. The widening conversion rules are defined in JLS §5.1.2.
The widening promotion order is:
byte -> short -> int -> long -> float -> double ^ | charint i = 42;double d = i; // int -> double (widening, no data loss)
long big = 1_000_000L;float f = big; // long -> float (widening, but may lose precision!)
// Binary numeric promotionbyte b = 10;short s = 20;// byte + short -> int + int -> intint result = b + s; // both promoted to int before addition
// char + int -> int + int -> intchar c = 'A';int code = c + 1; // code = 66Narrowing conversions (e.g., long to int``double to float) may lose information and require An explicit cast. The compiler enforces this to prevent accidental data loss.
double pi = 3.14159;int truncated = (int) pi; // 3 — fractional part is discarded (not rounded)int rounded = (int) Math.round(pi); // 3 — use Math.round for actual rounding
long bigValue = 300_000_000_000L;int narrowed = (int) bigValue; // -647710720 — overflow, bits are truncated
// char is an unsigned 16-bit integer — you can cast between char and numeric typeschar letter = 'A';int letterCode = (int) letter; // 65char fromCode = (char) 66; // 'B'Special Cases
Section titled “Special Cases”// Compound assignment operators include implicit narrowingbyte b = 1;b += 1; // equivalent to b = (byte)(b + 1); — the cast is implicit// b = b + 1; // compile error: b + 1 is int, cannot assign int to byte without cast
// Literal assignment to smaller types is allowed if the value fitsbyte b2 = 127; // OK — 127 fits in a byte// byte b3 = 128; // compile error — 128 does not fit in a byteThe final Keyword
Section titled “The final Keyword”The final keyword restricts reassignment (for variables), overriding (for methods), and Subclassing (for classes). It does not make objects immutable — only the reference is immutable.
// final local variable — cannot be reassigned after initializationfinal int x = 10;// x = 20; // compile error: cannot assign a value to final variable x
// final reference — the reference cannot change, but the object can be mutatedfinal List<String> list = new ArrayList<>();list.add("hello"); // OK — mutating the object// list = new ArrayList<>(); // compile error — reassigning the reference
// blank final — initialized exactly once (in constructor or initializer block)class Point { final int x; final int y;
Point(int x, int y) { this.x = x; // single assignment this.y = y; // single assignment }}
// final method — cannot be overridden by subclassesclass Base { final void validate() { /* ... */ }}
// final class — cannot be extendedfinal class Configuration { // prevents subclassing, ensures the class's contract cannot be weakened}Effectively Final
Section titled “Effectively Final”A variable is effectively final if it is never reassigned after initialization. The compiler Treats effectively final variables the same as explicitly final variables for the purpose of Lambda capture and anonymous class access.
String captured = "hello"; // effectively final — never reassignedRunnable r = () -> System.out.println(captured); // OK — lambda captures effectively final var
// String notFinal = "hello";// notFinal = "world";// Runnable r2 = () -> System.out.println(notFinal); // compile error — not effectively finalinstanceof Pattern Matching (Java 16+)
Section titled “instanceof Pattern Matching (Java 16+)”Pattern matching for instanceof (JEP 394, standardized in Java 16) Combines the type test, cast, and variable declaration into a single expression.
// Before Java 16if (obj instanceof String) { String s = (String) obj; System.out.println(s.length());}
// Java 16+ — pattern matchingif (obj instanceof String s) { System.out.println(s.length()); // s is in scope here — already cast and available}
// s is NOT in scope here — the pattern variable is scoped to the true branch
// Pattern matching with conditionsif (obj instanceof String s && s.length() > 5) { System.out.println(s.toUpperCase());}
// The pattern variable is in scope for the entire condition when// the pattern match is guaranteed by short-circuit evaluationif (obj instanceof String s && !s.isEmpty()) { // s is available here}Records (JEP 395, standardized in Java 16) provide a compact syntax For declaring classes that are transparent carriers of immutable data.
public record Point(int x, int y) {}
// The compiler generates:// - A final class with the given name// - private final fields for each component// - A public constructor with parameters matching the components// - public accessor methods: x() and y() (NOT getX()/getY())// - equals(), hashCode(), and toString() implementations// - Implements java.lang.Recordrecord Point(int x, int y) { // Compact constructor — validation without repeating field declarations public Point { if (x < 0 || y < 0) { throw new IllegalArgumentException("Coordinates must be non-negative"); } }}
// Usagevar p = new Point(3, 4);System.out.println(p.x()); // 3 — accessor method, not field accessSystem.out.println(p); // Point[x=3, y=4]
// Records are inherently immutable// p.x(5); // no setter exists
// Records provide value-based equalityvar p2 = new Point(3, 4);System.out.println(p.equals(p2)); // true — structural equalitySystem.out.println(p == p2); // false — different objects
// Records can implement interfacesrecord NamedPoint(int x, int y, String name) implements Comparable<NamedPoint> { @Override public int compareTo(NamedPoint other) { return this.name.compareTo(other.name); }}Sealed classes (JEP 409, standardized in Java 17) restrict which Classes can extend or implement a given type. This enables the compiler to perform exhaustive Pattern matching over a closed type hierarchy.
// sealed class — specifies exactly which subclasses are permittedpublic sealed interface Shape permits Circle, Rectangle, Triangle { double area();}
// Each permitted subclass must use one of: final, sealed, non-sealedpublic final record Circle(double radius) implements Shape { public double area() { return Math.PI * radius * radius; }}
public sealed record Rectangle(double width, double height) implements Shape permits FilledRectangle { public double area() { return width * height; }}
public final record FilledRectangle(double width, double height, int color) extends Rectangle implements Shape { public double area() { return width * height; }}
public non-sealed record Triangle(double base, double height) implements Shape { public double area() { return 0.5 * base * height; }}Exhaustive Pattern Matching
Section titled “Exhaustive Pattern Matching”Sealed classes shine when combined with pattern matching in switch:
// Java 21+ — exhaustive switch with sealed class hierarchydouble computeArea(Shape shape) { return switch (shape) { case Circle c -> c.area(); case Rectangle r -> r.area(); case Triangle t -> t.area(); // No default needed — the compiler knows all permitted subtypes };}
// With pattern matching and guards (Java 21+)String describe(Shape shape) { return switch (shape) { case Circle c when c.radius() > 10 -> "large circle"; case Circle c -> "small circle"; case Rectangle r -> "rectangle " + r.width() + "x" + r.height(); case Triangle t -> "triangle"; };}The three subclass modifiers enforce a clear contract:
final: No further subclassing. The hierarchy terminates here.sealed: Further subclassing is allowed but restricted to a newpermitslist.non-sealed: The class reopens to unrestricted inheritance. The sealed hierarchy ends.
Value Types (Preview)
Section titled “Value Types (Preview)”Value types are a preview feature (JEP 401) designed to combine the Performance characteristics of primitives with the abstraction capabilities of classes. They are Part of the Valhalla project.
The Problem Value Types Solve
Section titled “The Problem Value Types Solve”Consider the performance gap between primitives and objects:
// Primitive — 8 bytes, no allocation, inline storagedouble x = 3.14;
// Object — 16+ bytes header, 8 bytes field, heap allocation, indirectionComplexNumber c = new ComplexNumber(3.14, 2.72);// To get the real part: load reference -> load field -> (cache miss possible)Arrays and generics with reference types incur pointer overhead, cache misses, and GC pressure. Value types aim to provide user-defined types with the flat, inline layout of primitives.
Value Type Semantics (Preview)
Section titled “Value Type Semantics (Preview)”// Value type declaration (syntax subject to change)value class ComplexNumber { final double real; final double imag;
ComplexNumber(double real, double imag) { this.real = real; this.imag = imag; }}
// Unlike regular classes, value types:// 1. Have no identity — two instances with the same fields are indistinguishable// 2. Are stored inline (flat) in arrays and objects — no pointer indirection// 3. Cannot be synchronized on (no monitor)// 4. Cannot have mutable fields (or identity would be observable)// 5. Cannot be used as lock objects
// Array of value types — flat layout, no per-element object headersComplexNumber[] points = new ComplexNumber[1000];// Memory layout: [real0, imag0, real1, imag1, ...] — 16 bytes per element// vs. reference array: [ptr0, ptr1, ...] + 1000 separate heap objects