Last updated: 2026-09-18

F
Fundamental / general audience

Java Language Fundamentals

Java's defining decision, made before any of the OOP machinery covered elsewhere on this site comes into play, is static typing: every variable has a type fixed at compile time, checked before the program ever runs, rather than discovered while it runs. That single decision shapes almost everything else about how Java code is written and where its characteristic bugs come from — and it's specified, precisely and normatively, in the Java Language Specification itself, the primary source everything below ultimately traces back to1.

Primitives versus Reference Types

Java splits its type system in two, and the split is not cosmetic. Primitives (int, double, boolean, char, and five others) hold their value directly — a variable of type int is its number, stored inline, never null, never shared. Reference types (every class, including String and the wrapper classes like Integer) hold a reference to an object stored elsewhere — a variable of type String is a pointer to a String object, and can be null, meaning "points at nothing."

int a = 5;
int b = a;      // b gets its own copy of the value 5
b = 10;         // a is still 5 — they were never connected

Integer x = 1000;
Integer y = x;  // y gets a copy of the REFERENCE, not the object
// x and y now both point at the same Integer object

Autoboxing — Java automatically converting between int and Integer where needed — hides this distinction most of the time, which is exactly what makes it a trap when it resurfaces. Integer caches and reuses small boxed values (-128 to 127 by default), so Integer.valueOf(100) == Integer.valueOf(100) is true by accident of implementation, while the identical-looking Integer.valueOf(200) == Integer.valueOf(200) is false — the same code pattern, silently correct for small numbers and silently wrong for large ones, which is precisely why comparing reference types with == at all is the mistake, not the caching behaviour2.

== versus .equals()

This is the single most common bug a Java beginner writes and doesn't notice: == on a reference type compares identity (are these two variables pointing at the exact same object in memory), not equality (do these two objects represent the same value). Two separately-constructed String objects holding the identical text are, correctly, ==-unequal — they're different objects that happen to hold the same characters.

String a = new String("cat");
String b = new String("cat");
System.out.println(a == b);       // false — different objects
System.out.println(a.equals(b));  // true  — same content

int x = 5, y = 5;
System.out.println(x == y);       // true — primitives compare by value, always

The rule that actually resolves this: primitives always compare correctly with ==, because there's no identity/value distinction for a value that is its own value. Every reference type needs .equals() for a value comparison — == on a reference type answers "are these the same object," which is a different, and much rarer, question than the one most code actually means to ask.

Arrays versus the Collections Framework

Java's array is the lowest-level sequential structure: fixed size, set once at creation, direct index access. The Collections framework (covered in full, with its Big-O trade-offs, on Data Structures as Behavioural Contracts) sits on top of arrays and adds what they lack — resizing, insertion/removal in the middle, a shared interface contract — at the cost of being a genuine object with method-call overhead rather than a raw block of memory.

int[] fixedSize = new int[5];       // exactly 5 ints, forever
fixedSize[0] = 10;

List<Integer> growable = new ArrayList<>();
growable.add(10);                    // grows as needed
growable.add(20);

The practical rule of thumb: reach for an array when the size is genuinely fixed and known up front and raw performance matters (numerical work, a fixed-size buffer); reach for ArrayList or another Collections type otherwise, which is most of the time — the flexibility almost always outweighs the small overhead, and it's easy to convert between the two (Arrays.asList(), list.toArray()) when a specific API demands one or the other.

Recursion and the Call Stack

A recursive method calls itself with a smaller version of the same problem, and Java implements this exactly the way every call works: each call gets its own stack frame — its own copy of local variables and parameters — pushed onto the call stack, and popped off when that call returns.

int factorial(int n) {
    if (n <= 1) return 1;        // base case — stops the recursion
    return n * factorial(n - 1); // recursive case — smaller problem
}

Calling factorial(4) pushes a frame for n=4, which calls factorial(3) (pushing another frame), down to factorial(1) (the base case, which returns without recursing further) — then each pending multiplication resolves as the stack unwinds: 1, then 2×1=2, then 3×2=6, then 4×6=24. A missing or unreachable base case means the stack grows without bound until it overflows — a StackOverflowError, Java's direct, literal report that the call stack ran out of room, which is worth reading as exactly what it says rather than a mysterious failure.

The Edit-Compile-Run Cycle

Java's static typing has a direct consequence for the development cycle: a whole category of errors (a type mismatch, a missing semicolon, an undeclared variable) is caught by the compiler, before the program runs at all, rather than surfacing as a runtime crash partway through execution the way an equivalent mistake would in Python. This is a genuine trade: writing Java means satisfying the compiler before ever seeing the program run, which catches certain bugs early at the cost of a slower edit-test loop than a language that starts running immediately and fails only when it hits the actual broken line. Checked exceptions extend the same philosophy one step further — the compiler requires code that might throw a checked exception (like reading a file that might not exist) to either handle it or declare that it might happen, forcing the possibility of failure into the visible method signature rather than leaving it as an undocumented possibility discovered only by reading the implementation (see Advanced Java and the JVM for exception-hierarchy design beyond this point).

References


  1. Gosling, J., Joy, B., Steele, G., Bracha, G., Buckley, A., Smith, D., & Bierman, G. (2021). The Java Language Specification (Java SE 17 ed.). Oracle. https://docs.oracle.com/javase/specs/jls/se17/html/index.html

  2. Bloch, J. (2017). Effective Java (3rd ed.). Addison-Wesley Professional.