Java Fundamentals
Core Java language notes: types, strings, OOP in Java, exceptions, enums, records, and modern language features for interviews.
📘 Java Fundamentals
Table of Contents
- The Java Platform
- Data Types and Variables
- Wrapper Classes and Autoboxing
- Pass by Value
- Strings
- Control Flow and Modern Switch
- Arrays
- Classes, Constructors, and Initialization Order
- Access Modifiers, static, and final
- The Object Class: equals, hashCode, toString
- Inheritance and Polymorphism in Java
- Abstract Classes vs Interfaces
- Nested and Inner Classes
- Enums
- Records and Sealed Types
- Immutability
- Exception Handling
- Modern Java Feature Cheat Sheet
- Common Pitfalls
- Common Interview Questions
- One-Line Senior Summary
1. The Java Platform
Java is a statically typed, object-oriented, compiled-to-bytecode language.
The source is compiled by javac into platform-independent bytecode (.class files), which the JVM executes.
This is the "write once, run anywhere" idea: the bytecode is portable, and each OS has its own JVM.
1.1 JDK vs JRE vs JVM
| Term | What it is |
|---|---|
| JVM | Virtual machine that loads, verifies, and executes bytecode. Includes the JIT compiler and garbage collector. |
| JRE | JVM plus the standard class libraries needed to run programs. |
| JDK | JRE plus developer tools (javac, jar, jshell, javadoc, profilers). |
Since Java 11 there is no separate JRE download for most distributions, you install a JDK.
1.2 Compile and Run Flow
javac Hello.java # produces Hello.class (bytecode)
java Hello # JVM loads Hello.class and runs main
java Hello.java # Java 11+: compile in memory and run a single-file programpublic class Hello {
public static void main(String[] args) {
System.out.println("Hello, Java");
}
}- The entry point is
public static void main(String[] args). - A
publictop-level class must live in a file with the same name (Hello.java). - One file can hold at most one
publictop-level class.
1.3 How the JVM Runs Code
The JVM starts by interpreting bytecode. The JIT (Just-In-Time) compiler watches for hot methods and compiles them to native machine code at runtime. That is why Java programs are slower during warm-up and then get very fast, and why microbenchmarks need a warm-up phase.
Flowchart: From Source to Running Code
The JVM interprets first, then compiles only the hot methods to native code, which is why Java has a warm-up period.
1.4 LTS Versions
Java ships every 6 months, but only Long-Term-Support releases are commonly used in production: 8, 11, 17, 21. These notes target Java 17 and 21 and call out the version where a feature arrived.
2. Data Types and Variables
Java has 8 primitive types and everything else is a reference type.
| Type | Size | Range / notes | Default |
|---|---|---|---|
byte | 8 bit | -128 to 127 | 0 |
short | 16 bit | -32,768 to 32,767 | 0 |
int | 32 bit | about -2.1 billion to 2.1 billion | 0 |
long | 64 bit | about -9.2e18 to 9.2e18 | 0L |
float | 32 bit | about 7 decimal digits of precision | 0.0f |
double | 64 bit | about 15 decimal digits of precision | 0.0d |
char | 16 bit | one UTF-16 code unit, unsigned (0 to 65,535) | '\u0000' |
boolean | JVM-dependent | true or false only | false |
- Local variables have no default value, they must be assigned before use or the code will not compile.
- Fields (instance and static) get the defaults above, and reference fields default to
null.
2.1 Literals
long big = 3_000_000_000L; // L suffix required beyond int range; underscores aid readability
float f = 3.14f; // f suffix required, otherwise it is a double
int hex = 0xFF; // 255
int bin = 0b1010; // 10
char c = 'A';
var list = new ArrayList<String>(); // Java 10+: local variable type inferencevar only works for local variables with an initializer.
The type is still fixed at compile time, it is not dynamic typing.
2.2 Type Conversion
Widening (smaller to larger) is implicit and safe. Narrowing (larger to smaller) needs an explicit cast and may lose data.
int i = 100;
long l = i; // widening, implicit
double d = i; // widening, implicit
double pi = 3.99;
int truncated = (int) pi; // 3, decimals dropped (not rounded)
int big = 130;
byte b = (byte) big; // -126, overflow wraps aroundArithmetic on byte, short, and char is promoted to int first:
byte a = 10, b = 20;
// byte c = a + b; // compile error: a + b is an int
byte c = (byte) (a + b); // OK
char ch = 'a';
System.out.println(ch + 1); // 98 (int)
System.out.println((char) (ch + 1)); // b
ch += 1; // OK: compound assignment casts implicitly2.3 Integer Math Gotchas
System.out.println(5 / 2); // 2 (integer division truncates toward zero)
System.out.println(-5 / 2); // -2
System.out.println(-5 % 2); // -1 (result takes the sign of the dividend)
System.out.println(5 / 2.0); // 2.5
System.out.println(Integer.MAX_VALUE + 1); // -2147483648, silent overflow
// Detect overflow instead of wrapping
Math.addExact(Integer.MAX_VALUE, 1); // throws ArithmeticException2.4 Floating Point Precision
float and double use binary floating point, so many decimal fractions cannot be represented exactly.
System.out.println(0.1 + 0.2); // 0.30000000000000004
System.out.println(0.1 + 0.2 == 0.3); // falseFor money or anything needing exact decimals, use BigDecimal and construct it from a String, not a double:
BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.2");
System.out.println(a.add(b)); // 0.3
System.out.println(new BigDecimal(0.1)); // 0.1000000000000000055511151231257827...Also, double division by zero gives Infinity or NaN, while int division by zero throws ArithmeticException.
3. Wrapper Classes and Autoboxing
Each primitive has an object wrapper: Integer, Long, Double, Boolean, Character, Byte, Short, Float.
Wrappers are required for generics (List<Integer>, never List<int>) and can hold null.
Autoboxing is the automatic primitive to wrapper conversion, and unboxing is the reverse.
Integer boxed = 42; // autoboxing: Integer.valueOf(42)
int prim = boxed; // unboxing: boxed.intValue()
List<Integer> nums = new ArrayList<>();
nums.add(5); // autoboxed
int first = nums.get(0); // unboxed3.1 The Integer Cache Trap
Integer.valueOf caches instances for values -128 to 127.
That makes == behave inconsistently:
Integer a = 127, b = 127;
System.out.println(a == b); // true (same cached object)
Integer c = 128, d = 128;
System.out.println(c == d); // false (two different objects)
System.out.println(c.equals(d)); // trueAlways compare wrapper objects with .equals(), never ==.
3.2 Unboxing NullPointerException
Map<String, Integer> stock = new HashMap<>();
int count = stock.get("apple"); // NullPointerException: get returns null, unboxing null fails
int safe = stock.getOrDefault("apple", 0); // fixBoxing inside hot loops also allocates objects, so prefer primitives (int, IntStream) in performance-sensitive code.
4. Pass by Value
Java is always pass by value. For objects, the value being copied is the reference, not the object itself.
static void reassign(StringBuilder sb) {
sb = new StringBuilder("new"); // changes only the local copy of the reference
}
static void mutate(StringBuilder sb) {
sb.append(" world"); // follows the copied reference to the SAME object
}
StringBuilder sb = new StringBuilder("hello");
reassign(sb);
System.out.println(sb); // hello
mutate(sb);
System.out.println(sb); // hello world- Mutating the object through the parameter is visible to the caller.
- Reassigning the parameter is not.
- Primitives are copied entirely, so changes to them never propagate.
Flowchart: Pass by Value With Objects
The reference is copied, so the method can change the object but cannot change where the caller points.
5. Strings
String is an immutable class.
Every "modification" returns a new String and leaves the original untouched.
String s = "hello";
s.toUpperCase(); // result discarded, s is still "hello"
s = s.toUpperCase(); // now s refers to a new "HELLO"5.1 The String Pool
String literals are stored in the string pool (a special area inside the heap since Java 7) and reused.
String a = "java";
String b = "java";
String c = new String("java");
String d = c.intern();
System.out.println(a == b); // true (same pooled object)
System.out.println(a == c); // false (new String always creates a new heap object)
System.out.println(a.equals(c)); // true (same characters)
System.out.println(a == d); // true (intern returns the pooled instance)Always use .equals() for string content comparison.
== compares references.
Why is String immutable?
- Security: strings hold file paths, URLs, and credentials that must not change after validation.
- Thread safety: an immutable object can be shared freely without locking.
- Caching: the pool is only safe because strings never change, and
hashCodecan be cached, which makes strings idealHashMapkeys.
Flowchart: The String Pool
Literals share one pooled object, while new String always makes a separate one, which is why == and equals differ.
5.2 StringBuilder vs StringBuffer
Repeated + in a loop creates many temporary strings (O(n^2) copying).
Use a mutable builder instead.
// Bad
String result = "";
for (int i = 0; i < 1000; i++) {
result += i;
}
// Good
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(i);
}
String result2 = sb.toString();| Class | Mutable | Thread-safe | Speed |
|---|---|---|---|
String | No | Yes (immutable) | Slow for repeated concatenation |
StringBuilder | Yes | No | Fast, the default choice |
StringBuffer | Yes | Yes (synchronized methods) | Slower, rarely needed now |
5.3 Commonly Used Methods
String s = " Hello, World ";
s.trim(); // "Hello, World" (strip() is Unicode-aware, Java 11+)
s.length(); // 16
s.charAt(2); // 'H'
s.substring(2, 7); // "Hello" (end index is exclusive)
s.indexOf("World"); // 9
s.contains("Hello"); // true
s.replace("l", "L"); // " HeLLo, WorLd "
s.toLowerCase();
s.isBlank(); // Java 11+: true for empty or whitespace-only
"a,b,c".split(","); // ["a", "b", "c"]
String.join("-", "a", "b"); // "a-b"
"ab".repeat(3); // "ababab" (Java 11+)
String.valueOf(42); // "42"
"Java".equalsIgnoreCase("JAVA"); // true5.4 Formatting and Text Blocks
String msg = String.format("%s scored %d (%.1f%%)", "Sam", 42, 87.456);
// Sam scored 42 (87.5%)
// Java 15+: text block preserves line breaks, no escaping needed for quotes
String json = """
{
"name": "Sam",
"age": 30
}
""";6. Control Flow and Modern Switch
6.1 Loops
for (int i = 0; i < 5; i++) { /* ... */ }
for (String name : names) { /* enhanced for over arrays and Iterables */ }
while (condition) { /* ... */ }
do { /* runs at least once */ } while (condition);
outer:
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (j == 2) continue outer; // labeled continue
if (i == 2) break outer; // labeled break
}
}6.2 Classic Switch and Fall-Through
The classic switch falls through to the next case unless you break.
switch (day) {
case 1:
System.out.println("Mon");
// no break: falls through into case 2
case 2:
System.out.println("Tue");
break;
default:
System.out.println("Other");
}6.3 Switch Expressions (Java 14+)
Arrow form: no fall-through, no break, and the switch returns a value.
String type = switch (day) {
case 6, 7 -> "Weekend";
case 1, 2, 3, 4, 5 -> "Weekday";
default -> throw new IllegalArgumentException("Invalid day: " + day);
};
int len = switch (word) {
case "one" -> 1;
case "two" -> 2;
default -> {
System.out.println("unknown");
yield 0; // yield returns a value from a block
}
};6.4 Pattern Matching
// Java 16+: pattern matching for instanceof
if (obj instanceof String s && s.length() > 3) {
System.out.println(s.toUpperCase()); // s is already cast and in scope
}
// Java 21: pattern matching for switch, with guards and null handling
static String describe(Object o) {
return switch (o) {
case null -> "null";
case Integer i when i > 100 -> "big int " + i;
case Integer i -> "int " + i;
case String s -> "string of length " + s.length();
default -> "something else";
};
}7. Arrays
Arrays are fixed-size, zero-indexed objects that live on the heap.
int[] a = new int[5]; // [0, 0, 0, 0, 0]
int[] b = {1, 2, 3};
int[][] grid = new int[3][4]; // 3 rows, 4 columns
int[][] jagged = new int[3][]; // rows can have different lengths
jagged[0] = new int[2];
System.out.println(b.length); // length is a field, not a method
b[3] = 10; // ArrayIndexOutOfBoundsException7.1 The Arrays Utility Class
int[] nums = {5, 3, 9, 1};
Arrays.sort(nums); // [1, 3, 5, 9] (dual-pivot quicksort for primitives)
Arrays.binarySearch(nums, 5); // 2 (array must be sorted)
Arrays.toString(nums); // "[1, 3, 5, 9]"
Arrays.deepToString(new int[][]{{1}, {2}}); // "[[1], [2]]"
Arrays.fill(nums, 0);
int[] copy = Arrays.copyOf(nums, 6); // pads with defaults if longer
int[] part = Arrays.copyOfRange(nums, 1, 3);
Arrays.equals(new int[]{1}, new int[]{1}); // true (== would be false)
List<String> fixed = Arrays.asList("a", "b"); // fixed-size list backed by the array
fixed.set(0, "x"); // OK
// fixed.add("c"); // UnsupportedOperationExceptionPrinting an array directly (System.out.println(nums)) shows something like [I@1b6d3586, so always use Arrays.toString.
7.2 Array Covariance
Arrays are covariant and checked at runtime, unlike generics:
Object[] objs = new String[2];
objs[0] = 42; // compiles, but throws ArrayStoreException at runtime8. Classes, Constructors, and Initialization Order
public class Person {
private final String name;
private int age;
public Person(String name) {
this(name, 0); // constructor chaining with this(...)
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}- If you write no constructor, the compiler adds a default no-arg constructor.
- If you write any constructor, the default one is not generated.
this(...)orsuper(...)must be the first statement in a constructor.- Every constructor implicitly calls
super()first if you do not write it, so the parent needs an accessible no-arg constructor, or you must callsuper(args)explicitly. - Constructors are not inherited and cannot be
abstract,static, orfinal.
8.1 Initialization Order
class Parent {
static { System.out.println("1. Parent static block"); }
{ System.out.println("3. Parent instance block"); }
Parent() { System.out.println("4. Parent constructor"); }
}
class Child extends Parent {
static { System.out.println("2. Child static block"); }
{ System.out.println("5. Child instance block"); }
Child() { System.out.println("6. Child constructor"); }
}
new Child();Output order:
- Parent static block (once, when the class is first loaded)
- Child static block
- Parent instance block
- Parent constructor
- Child instance block
- Child constructor
Rule: static init runs top-down parent to child once per class, then for each new, parent instance init and constructor run before the child's.
A classic bug follows from this: calling an overridable method from a constructor. The child override may run before the child's fields are initialized.
class Base {
Base() { init(); } // calls overridden method
void init() {}
}
class Derived extends Base {
private String value = "set";
@Override void init() {
System.out.println(value); // prints null: Derived's field initializers have not run yet
}
}Flowchart: Initialization Order
Static blocks run once per class, parent first, then every new runs parent init and constructor before the child.
9. Access Modifiers, static, and final
9.1 Access Modifiers
| Modifier | Same class | Same package | Subclass (other package) | Everywhere |
|---|---|---|---|---|
private | Yes | No | No | No |
| default (none) | Yes | Yes | No | No |
protected | Yes | Yes | Yes | No |
public | Yes | Yes | Yes | Yes |
Default to the most restrictive modifier that works.
9.2 static
static members belong to the class, not to any instance.
class Counter {
private static int total = 0; // one copy shared by all instances
private int id;
Counter() { id = ++total; }
static int getTotal() { return total; } // cannot touch instance fields or use this
}- A static method cannot access instance members directly, there is no
this. staticmethods are hidden, not overridden, and resolved at compile time by the declared type.- A static block runs once when the class is loaded.
9.3 final
| Applied to | Meaning |
|---|---|
| variable | Can be assigned only once. For references, the reference is fixed but the object can still change. |
| method | Cannot be overridden. |
| class | Cannot be extended (String, Integer, and other wrappers are final). |
final List<String> names = new ArrayList<>();
names.add("Sam"); // OK: the list is mutable
// names = new ArrayList<>(); // compile error: cannot reassignfinal, finally, and finalize are three unrelated things.
finalize() is deprecated for removal and should never be used, prefer try-with-resources or Cleaner.
10. The Object Class: equals, hashCode, toString
Every class extends java.lang.Object, which gives you equals, hashCode, toString, getClass, clone, and the wait/notify methods.
10.1 The equals and hashCode Contract
equalsmust be reflexive, symmetric, transitive, consistent, andx.equals(null)must befalse.- If
a.equals(b)thena.hashCode() == b.hashCode(). - The reverse is not required: equal hash codes do not imply equality (that is a collision).
- If you override
equals, you must overridehashCode.
public class Point {
private final int x, y;
public Point(int x, int y) { this.x = x; this.y = y; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point p)) return false; // pattern matching, Java 16+
return x == p.x && y == p.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
@Override
public String toString() {
return "Point(" + x + ", " + y + ")";
}
}What breaks if you override only equals:
Set<Point> set = new HashSet<>();
set.add(new Point(1, 2));
System.out.println(set.contains(new Point(1, 2)));
// false if hashCode is not overridden: the two objects land in different bucketsAlso, never use mutable fields in hashCode for objects stored in hash-based collections.
If the key changes after insertion, the entry can no longer be found.
10.2 Reference vs Content Equality
==on objects compares references (identity).equalscompares logical equality, and the defaultObject.equalsis just==.
11. Inheritance and Polymorphism in Java
Java has single class inheritance (extends one class) but multiple interface implementation.
class Animal {
String sound() { return "..."; }
}
class Dog extends Animal {
@Override
String sound() { return "Woof"; }
}
Animal a = new Dog(); // upcasting, always safe and implicit
System.out.println(a.sound()); // Woof: method chosen at runtime from the actual object
Dog d = (Dog) a; // downcasting needs an explicit cast, can throw ClassCastException11.1 Overriding Rules
- Same name and parameter list.
- Return type must be the same or a covariant (more specific) type.
- Access level cannot be narrower than the parent's.
- It cannot throw broader checked exceptions than the parent method.
private,static, andfinalmethods cannot be overridden.- Always add
@Overrideso the compiler catches typos.
11.2 Overloading vs Overriding
| Overloading | Overriding | |
|---|---|---|
| Where | Same class (or subclass) | Subclass vs superclass |
| Signature | Different parameter list | Same |
| Resolved | Compile time (by declared static types) | Runtime (by actual object type) |
| Return type | Can differ | Same or covariant |
class Printer {
void print(Object o) { System.out.println("Object"); }
void print(String s) { System.out.println("String"); }
}
Object x = "hello";
new Printer().print(x); // Object: overload chosen by the declared type of x
new Printer().print("hello"); // String11.3 Fields and Static Methods Are Not Polymorphic
Only instance methods are dispatched dynamically. Field access and static method calls use the declared type.
class A { String name = "A"; String get() { return "A.get"; } }
class B extends A { String name = "B"; String get() { return "B.get"; } }
A obj = new B();
System.out.println(obj.name); // A (field hiding, resolved at compile time)
System.out.println(obj.get()); // B.get (overriding, resolved at runtime)11.4 super
class Manager extends Employee {
Manager(String name) {
super(name); // call parent constructor
}
@Override
double pay() {
return super.pay() + 1000; // reuse parent behavior
}
}12. Abstract Classes vs Interfaces
abstract class Shape {
private final String color;
Shape(String color) { this.color = color; } // abstract classes can have constructors and state
abstract double area(); // no body, subclasses must implement
String describe() { return color + " shape of area " + area(); }
}
interface Drawable {
void draw(); // implicitly public abstract
default void drawTwice() { draw(); draw(); } // Java 8+: default method with a body
static Drawable noop() { return () -> {}; } // Java 8+: static method
private void helper() {} // Java 9+: private helper for defaults
}
class Circle extends Shape implements Drawable, Comparable<Circle> { /* ... */ }| Abstract class | Interface | |
|---|---|---|
| Inheritance | A class extends one | A class implements many |
| State | Instance fields, constructors | Only public static final constants |
| Methods | Abstract and concrete, any access modifier | Abstract, default, static, private |
| Use it for | Shared code and state among closely related classes ("is-a") | A capability or contract ("can-do") |
An interface with exactly one abstract method is a functional interface and can be implemented with a lambda.
Flowchart: Abstract Class or Interface?
Ask whether you are modelling what something is or what it can do.
12.1 The Diamond Problem
If two interfaces provide the same default method, the implementing class must override it to resolve the conflict.
interface A { default String hello() { return "A"; } }
interface B { default String hello() { return "B"; } }
class C implements A, B {
@Override
public String hello() {
return A.super.hello(); // explicitly pick one
}
}Flowchart: The Diamond Problem
The compiler never guesses which default method you meant.
13. Nested and Inner Classes
| Kind | Declared | Holds outer instance? | Use case |
|---|---|---|---|
| Static nested | static class X inside a class | No | Helper type tied to the outer class (Builder, Map.Entry) |
| Inner (non-static) | class X inside a class | Yes | Object that needs the enclosing instance's state |
| Local | Inside a method | Yes (plus captured locals) | Rare |
| Anonymous | new Type() { ... } inline | Yes | One-off implementation |
class Outer {
private int value = 10;
static class StaticNested {
// cannot access Outer.this.value
}
class Inner {
int read() { return value; } // has an implicit reference to Outer.this
}
}
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
Outer.StaticNested nested = new Outer.StaticNested();
// Anonymous class
Runnable r = new Runnable() {
@Override public void run() { System.out.println("run"); }
};
// Lambda: same idea, far shorter
Runnable r2 = () -> System.out.println("run");Prefer static nested unless you truly need the outer instance. Inner classes keep a hidden reference to the outer object, which can cause memory leaks.
Local and anonymous classes and lambdas can only capture effectively final local variables (never reassigned after initialization).
A lambda's this refers to the enclosing class, while an anonymous class's this refers to the anonymous instance itself.
14. Enums
An enum is a fixed set of named constants and is a full class (it extends java.lang.Enum).
enum Status { PENDING, ACTIVE, CLOSED }
Status s = Status.ACTIVE;
s.name(); // "ACTIVE"
s.ordinal(); // 1
Status.valueOf("CLOSED"); // CLOSED (throws IllegalArgumentException if missing)
Status.values(); // [PENDING, ACTIVE, CLOSED]Enums can carry fields, constructors, and behavior, even a different implementation per constant:
enum Operation {
ADD("+") {
@Override int apply(int a, int b) { return a + b; }
},
MULTIPLY("*") {
@Override int apply(int a, int b) { return a * b; }
};
private final String symbol;
Operation(String symbol) { this.symbol = symbol; } // constructors are implicitly private
abstract int apply(int a, int b);
String symbol() { return symbol; }
}
Operation.ADD.apply(2, 3); // 5Useful facts:
- Compare enums with
==(there is exactly one instance per constant). - Use
EnumMapandEnumSetfor maps and sets keyed by enums, they are fast and compact. - Enums work directly in
switch. - A single-element enum is the safest Singleton (serialization-proof and reflection-proof).
- Avoid
ordinal()for persisted data, reordering constants would silently change meaning.
15. Records and Sealed Types
15.1 Records (Java 16+)
A record is a concise, immutable data carrier.
public record User(String name, int age) {
// compact constructor for validation
public User {
if (age < 0) throw new IllegalArgumentException("age must be >= 0");
}
// extra methods are allowed
public boolean isAdult() { return age >= 18; }
}
User u = new User("Sam", 30);
u.name(); // accessor (no "get" prefix)
System.out.println(u); // User[name=Sam, age=30]
u.equals(new User("Sam", 30)); // trueThe compiler generates a canonical constructor, accessors, equals, hashCode, and toString.
Records are implicitly final, their fields are private final, and they cannot extend another class (they can implement interfaces).
Note that they are shallowly immutable: a List field can still be mutated unless you copy it defensively.
15.2 Sealed Classes and Interfaces (Java 17+)
sealed restricts which classes may extend or implement a type, which enables exhaustive pattern matching.
public sealed interface Shape permits Circle, Square {}
public record Circle(double r) implements Shape {}
public record Square(double side) implements Shape {}
static double area(Shape s) {
return switch (s) { // no default needed: compiler knows all cases
case Circle c -> Math.PI * c.r() * c.r();
case Square q -> q.side() * q.side();
};
}Permitted subclasses must be final, sealed, or non-sealed.
16. Immutability
An immutable object cannot change after construction, which makes it inherently thread-safe and safe as a map key.
Recipe for an immutable class:
- Declare the class
final(or make constructors private). - Make all fields
private final. - Provide no setters.
- Defensively copy mutable inputs and outputs.
public final class Order {
private final String id;
private final List<String> items;
public Order(String id, List<String> items) {
this.id = id;
this.items = List.copyOf(items); // defensive copy, immutable
}
public String getId() { return id; }
public List<String> getItems() { return items; } // already immutable, safe to return
}final on a field alone is not enough: a final List can still be mutated.
17. Exception Handling
17.1 The Hierarchy
| Checked | Unchecked | |
|---|---|---|
| Extends | Exception (not RuntimeException) | RuntimeException or Error |
| Compiler enforces handling | Yes (catch or throws) | No |
| Typical cause | Recoverable external failure (I/O, network) | Programming bug or violated precondition |
17.2 try, catch, finally
try {
int result = Integer.parseInt(input);
process(result);
} catch (NumberFormatException e) {
log.warn("Bad number: " + input, e);
} catch (IOException | SQLException e) { // multi-catch (types must be unrelated)
throw new ServiceException("Failed to process", e); // wrap and keep the cause
} finally {
cleanup(); // runs whether or not an exception occurred
}- Catch blocks are checked top to bottom, so put more specific exceptions first.
finallyruns even onreturn, but not if the JVM exits (System.exit) or crashes.- A
returninsidefinallyoverrides any earlier return and swallows exceptions:
static int test() {
try {
return 1;
} finally {
return 2; // method returns 2. Never do this.
}
}Flowchart: try, catch, finally
finally runs on every path, including a return inside the try.
17.3 try-with-resources
Any AutoCloseable resource declared in the try (...) header is closed automatically, in reverse order, even if an exception is thrown.
try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"));
PrintWriter writer = new PrintWriter("out.txt")) {
String line;
while ((line = reader.readLine()) != null) {
writer.println(line.toUpperCase());
}
} // writer then reader are closed hereIf both the body and close() throw, the body's exception is primary and the close exception is attached as suppressed (e.getSuppressed()), unlike the old finally pattern, where the close exception would mask the original.
Flowchart: try-with-resources
Resources are closed in reverse order, and a close failure never hides the original exception.
17.4 Custom Exceptions
public class InsufficientFundsException extends Exception { // checked
private final double shortfall;
public InsufficientFundsException(String message, double shortfall) {
super(message);
this.shortfall = shortfall;
}
public double getShortfall() { return shortfall; }
}
void withdraw(double amount) throws InsufficientFundsException {
if (amount > balance) {
throw new InsufficientFundsException("Not enough funds", amount - balance);
}
balance -= amount;
}17.5 Best Practices
- Never swallow an exception with an empty
catch. - Do not catch
ExceptionorThrowablebroadly unless at a top-level boundary. - Preserve the cause when wrapping:
new MyException("msg", e). - Throw early with specific messages (
Objects.requireNonNull(x, "x must not be null")). - Do not use exceptions for normal control flow, they are slow and hide intent.
- Prefer unchecked exceptions for programming errors, checked ones for recoverable conditions the caller can realistically handle.
18. Modern Java Feature Cheat Sheet
| Version | Feature |
|---|---|
| 8 | Lambdas, streams, Optional, default and static interface methods, java.time |
| 9 | Modules, List.of / Map.of, private interface methods |
| 10 | var for locals |
| 11 | String helpers (isBlank, strip, repeat), HttpClient, single-file run |
| 14 | Switch expressions |
| 15 | Text blocks |
| 16 | Records, pattern matching for instanceof, Stream.toList() |
| 17 (LTS) | Sealed classes |
| 21 (LTS) | Virtual threads, pattern matching for switch, record patterns, sequenced collections |
19. Common Pitfalls
- Comparing strings or wrapper objects with
==instead ofequals. - Overriding
equalswithouthashCode. - Using
doublefor money instead ofBigDecimal. NullPointerExceptionfrom unboxing anullwrapper.- Modifying a collection while iterating over it (
ConcurrentModificationException). - Calling overridable methods from constructors.
- String concatenation with
+inside loops. - Catching
Exceptionand doing nothing. - Returning a mutable internal collection from a getter, breaking encapsulation.
- Assuming
finalmakes an object immutable. - Forgetting that integer division truncates and integer arithmetic silently overflows.
20. Common Interview Questions
- "Is Java pass by value or pass by reference?" Always pass by value, and for objects the reference is what gets copied (see Section 4).
- "Difference between
==andequals?" Identity vs logical equality (see Section 10). - "Why is
Stringimmutable?" Security, thread safety, and pooling and hash caching. - "String vs StringBuilder vs StringBuffer?" See Section 5.2.
- "What is the
equalsandhashCodecontract?" Equal objects must have equal hash codes, and overriding one requires overriding the other. - "Abstract class vs interface?" State and constructors vs pure contract with multiple inheritance (see Section 12).
- "Overloading vs overriding?" Compile-time vs runtime resolution (see Section 11.2).
- "Checked vs unchecked exceptions?" Compiler-enforced handling for recoverable conditions vs programming errors.
- "What does
finallydo if there is areturnintry?" It still runs, and areturninfinallyoverrides the earlier one. - "What is the difference between
final,finally, andfinalize?" A modifier, a cleanup block, and a deprecated GC hook. - "What are records and when would you use them?" Immutable data carriers such as DTOs and value objects.
- "How does the static block order differ from instance block order?" See Section 8.1.
- "Why is the Integer cache a problem for
==?" Values from -128 to 127 are cached, others are new objects (see Section 3.1). - "Can you override a
staticmethod?" No, it is hidden, and resolved at compile time. - "How do you make a class immutable?" See Section 16.
21. One-Line Senior Summary
Java gives you a statically typed, garbage-collected language where correctness comes from a few habits: prefer immutability, compare with
equals, keepequalsandhashCodein sync, use the most restrictive access, model data with records and enums, and handle exceptions deliberately instead of swallowing them.