Language Fundamentals

Every production bug trace eventually lands on fundamentals: a widened cast that silently truncates, == on strings, a switch that falls through, or += in a loop allocating gigabytes. This chapter teaches the syntax you write daily—with the depth to avoid those mistakes.

beginner

Data types: primitives & wrappers

Java is statically typed: every variable and expression has a type checked at compile time. Types split into primitives (eight built-in value types) and reference types (classes, interfaces, arrays, enums—objects on the heap, variables hold references).

The eight primitives

Primitives store values directly in fields, array slots, or stack locals. They are not objects—no methods, no null.

TypeBitsRange / valuesLiteral examples
byte8−128 … 127(byte) 200, 0x0F
short16−32,768 … 32,767Rare; legacy buffers
int32≈ ±2.1×10⁹42, 0xFF, 1_000_000
long64≈ ±9.2×10¹⁸99L, 1_700_000_000_000L
float32IEEE 7543.14f, 1e6f
double64IEEE 754 (default float literal)3.14, 1.0e-6
char16UTF-16 code unit 0 … 65535'A', '\u03A9', '\n'
boolean1 bit*true, false onlytrue, false

* JVM may use a byte internally; not exposed as numeric type.

Java
byte flags = 0b0000_0110;     // binary literal (Java 7+)
int port = 0x1F90;            // hex
long nanos = 1_500_000_000L;  // underscores for readability
float ratio = .5f;            // f suffix required
double pi = 3.141592653589793;
char newline = '\n';
boolean enabled = true;

Wrapper classes

Each primitive has a corresponding immutable wrapper class in java.lang:

PrimitiveWrapper
byteByte
shortShort
intInteger
longLong
floatFloat
doubleDouble
charCharacter
booleanBoolean

Autoboxing & unboxing

The compiler inserts conversions automatically when a primitive meets a reference context (or vice versa):

Java
// Source you write:
List<Integer> scores = List.of(90, 85, 88);
int top = scores.get(0);

// Compiler conceptually inserts:
// List.of(Integer.valueOf(90), ...)
// top = scores.get(0).intValue();

Integer cache: Integer.valueOf caches values −128 through 127 by default. Outside that range, each boxing may allocate a new object—relevant for memory micro-optimization and surprising == behavior.

⚠️ Pitfall

Null unboxing: Integer boxed = map.get(key); int x = boxed; → NPE if key missing. Use map.getOrDefault(key, 0) or null checks.

== on wrappers: Integer a = 200, b = 200; a == b is false (distinct objects). Always Objects.equals(a, b).

Performance: Autoboxing in tight numeric loops creates garbage. Use IntStream, long[], or primitive-specialized libraries.

🔬 Under the Hood

Primitive method parameters are passed by value (copy of bits). Reference parameters copy the pointer—reassigning the parameter does not change the caller’s variable. Wrapper immutability means “changing” an Integer means a new object; the variable is reassigned to point at it.

Variables: scope & lifetime

A variable is a name bound to storage with a type. Where it is declared determines lifetime, default initialization, and (for objects) visibility across threads.

KindDeclaredDefaultLifetime
Local Method or block { } None—must assign before read (definite assignment) Stack frame until block ends
Instance Class body, no static 0, false, null Per object; garbage-collected with object
Static (class) static field 0, false, null One per class; lives until class unloaded
final Any of above + final Same as non-final until assigned Must assign exactly once (blank final in constructor)
Java
public class OrderService {
    private static final Logger LOG = LoggerFactory.getLogger(OrderService.class);
    private final OrderRepository repo;  // must set in constructor

    private int processedCount;          // instance, defaults to 0

    public OrderService(OrderRepository repo) {
        this.repo = repo;
    }

    public void handle(Order order) {
        final String id = order.id();    // local final — cannot reassign
        int retries = 0;                 // local
        if (id == null) {
            return;                      // block scope ends — locals discarded
        }
        while (retries < 3) {
            retries++;
        }
    }
}

Shadowing

A local or parameter can reuse a field name—inner variable shadows outer. Legal but confusing; avoid in production code.

final semantics

  • Primitive final — value fixed after assignment.
  • Reference final — cannot point to another object; object contents may still mutate (final List allows list.add(...)).
  • Blank final fieldfinal int x; assigned in each constructor path.
📦 Real World

Spring singleton beans use instance fields for injected dependencies. static mutable caches break tests and leak state across requests unless carefully isolated. Configuration constants are static final.

Operators

Java operators match C-family languages for arithmetic and boolean logic. Bitwise and shift operators manipulate individual bits—common in permissions, codecs, and low-level protocols.

Arithmetic & assignment

OperatorMeaningNotes
+ − * / %Add, subtract, multiply, divide, remainderInteger / uses truncating division
++ --Increment/decrementPrefix vs postfix differ in expression value
+= -= …Compound assignmentIncludes implicit cast: byte b; b += 300; wraps
Java
int a = 10, b = 3;
int q = a / b;              // 3, not 3.333...
double exact = (double) a / b; // 3.333... — cast before divide

int x = 5;
int post = x++;  // post: returns 5, then x is 6
int pre = ++x;   // pre: x becomes 7, returns 7

Relational & logical

&& and || short-circuit: right side may not run. & and | on booleans evaluate both sides (rarely used on booleans).

Bitwise & shift (with binary)

Operands are promoted to int unless long is involved. Example: int a = 12 → binary 00001100.

ExpressionBinary (a=12)Decimal resultTypical use
a << 10001100024Multiply by 2
a >> 1000001106Divide by 2 (signed)
a >>> 1fill with 06Unsigned shift (negative ints)
a & 1test bit 00Even: 0, odd: 1
a | 8set bit 312 | 8 = 12OR flags: READ=1, WRITE=2
a ^ 4toggle bit 28XOR checksums
Java
final int READ = 1 << 0;   // 0001
final int WRITE = 1 << 1;  // 0010
int perms = READ | WRITE;
boolean canRead = (perms & READ) != 0;

Ternary & instanceof

Java
String level = score >= 60 ? "pass" : "fail";

// Pattern matching instanceof (Java 16+)
if (obj instanceof String s) {
    System.out.println(s.length());  // s scoped to true branch
}

// Old style — still valid
if (obj instanceof String) {
    String s = (String) obj;  // extra cast
}
🎯 Interview Tip

Know & vs && and | vs ||. Avoid writing i++ + ++i—undefined behavior in C++; confusing in Java. Understand boolean promotion: null instanceof Object is false, not NPE.

Control flow

Branching and loops compile to conditional bytecode jumps. Modern switch supports arrows (no fall-through), expressions with yield, and String / enum cases.

if / else if / else

Conditions must be boolean (unlike C’s integer truthiness). Common pattern: guard clauses early-return to avoid deep nesting.

Java
if (user == null) {
    throw new IllegalArgumentException("user required");
} else if (!user.isActive()) {
    log.warn("inactive {}", user.id());
} else {
    processActiveUser(user);
}

switch — classic fall-through vs modern Java 14+

Classic colon labels fall through without break:

Java
// DON'T — missing break causes fall-through
switch (day) {
    case MONDAY:
        log.info("start week");
        // falls into TUESDAY without break!
    case TUESDAY:
        log.info("tuesday");
        break;
    default:
        log.info("other");
}

Arrow labels — no fall-through; preferred style:

Java
switch (day) {
    case MONDAY, FRIDAY -> log.info("busy");
    case SATURDAY, SUNDAY -> log.info("weekend");
    default -> log.info("midweek");
}

// Switch expression — produces a value
String badge = switch (status) {
    case OK -> "green";
    case WARN -> "amber";
    case ERROR -> "red";
    default -> "gray";
};

// Multi-line branch needs yield (not return)
String msg = switch (code) {
    case 404 -> {
        log.warn("missing");
        yield "Not found";
    }
    default -> "Error " + code;
};

Loops

LoopUse when
for (init; cond; step)Index-based, known bounds
enhanced forIterate every Iterable / array without index
whileCondition-first (0+ iterations)
do-whileAt least one iteration (menu, retry)
Java
for (int i = 0; i < items.size(); i++) {
    process(items.get(i));
}

for (var line : Files.readAllLines(path)) {
    if (line.isBlank()) continue;  // skip to next iteration
    parse(line);
}

outer:
for (int r = 0; r < matrix.length; r++) {
    for (int c = 0; c < matrix[r].length; c++) {
        if (matrix[r][c] == target) break outer;
    }
}

int n = 0;
do {
    n = readSensor();
} while (n == 0);
⚠️ Pitfall

Enhanced for over a raw array does not box primitives inefficiently, but iterating List<Integer> boxes. Switch on null throws NPE. Expression switches must be exhaustive (cover all enum constants) or include default.

Arrays

Arrays are fixed-length, indexed, contiguous sequences. Reference type (not primitive)— length field, inherited equals uses reference identity unless you use Arrays.equals.

Declaration & initialization

Java
int[] primes = { 2, 3, 5, 7 };           // array literal
int[] buf = new int[1024];                  // default: all 0
String[] names = new String[] { "a", "b" }; // redundant [] on right side optional

// Multidimensional — array of arrays (jagged allowed)
int[][] matrix = {
    { 1, 2, 3 },
    { 4, 5 },
    { 6, 7, 8, 9 }
};
int rows = matrix.length;      // 3
int colsRow0 = matrix[0].length; // 3

System.arraycopy vs Arrays.copyOf

APIBehavior
System.arraycopy(src, srcPos, dst, dstPos, len) Fast native copy; destination must exist and fit; handles overlapping regions
Arrays.copyOf(src, newLength) Allocates new array; truncates or zero-pads; safest default
Arrays.copyOfRange(src, from, to) Slice copy
src.clone() Shallow copy of array elements (references copied for object arrays)
Java
int[] src = { 1, 2, 3, 4, 5 };
int[] dst = Arrays.copyOf(src, src.length);
int[] grown = Arrays.copyOf(src, 10);  // last 5 elements are 0

int[] manual = new int[src.length];
System.arraycopy(src, 0, manual, 0, src.length);

// Compare content, not references
boolean same = Arrays.equals(src, dst);
💡 Pro Tip

Varargs void log(String... parts) receives a real array—mutating it affects the caller unless you copy defensively. For resizable collections, use Collections.

Strings — overview

String is a final class representing immutable UTF-16 sequences. It is the most heavily used reference type in business applications—URLs, JSON, SQL, UI text, log messages.

Because immutability, pooling, and performance characteristics are subtle, the String topics below are split into dedicated sections.

Immutability — why Strings never change

Once constructed, a String’s character sequence cannot change. “Modification” methods return new strings:

Java
String s = "hello";
s.toUpperCase();       // returns "HELLO" — s still "hello"
s = s.toUpperCase();   // s now references "HELLO", "hello" may be GC'd if unreferenced

Why the language enforces it

  • Security — File paths, class names, and network URLs are strings. Mutable strings could be altered after security checks (historical attack vector on other platforms).
  • Thread safety — Share string references across threads without synchronization; readers never see partial updates.
  • String pool — Literals can be interned and reused safely because content never changes.
  • Hash cachinghashCode() can be computed once and cached (field hash in String).
🔬 Under the Hood

From Java 9, compact strings (-XX:+CompactStrings) use byte[] + coder flag (Latin-1 vs UTF-16) to halve memory for ASCII-heavy workloads. Immutability is still guaranteed at the API level.

String pool & intern()

The string pool (in Metaspace / heap depending on JDK version) stores literal strings and interned sequences so "hello" reused across the JVM shares one copy.

Java
String a = "transaction";     // likely pool entry at compile time
String b = "transaction";     // same reference as a
String c = new String("transaction"); // heap object (until interned)
String d = c.intern();        // add to pool if absent; return pooled reference

System.out.println(a == b);     // true — same reference
System.out.println(a == c);     // false — different objects
System.out.println(a == d);     // true after intern

// NEVER for value comparison
System.out.println(a.equals(c)); // true — always use equals for content

When to intern: Massive duplicate strings (parsed tokens, enum-like labels) to save memory—measure first; interning has a cost. Avoid interning unbounded user input (pool growth / effective memory leak).

⚠️ Pitfall

== compares references. Use equals, equalsIgnoreCase, or Objects.equals(a, b) (null-safe). "\n" vs "\\n" in source are different literals.

String vs StringBuilder vs StringBuffer

TypeMutableThread-safeWhen to use
StringNoYes (immutable)Constants, map keys, short joins of few parts
StringBuilderYesNoLoops building SQL, JSON, HTML, log lines
StringBufferYesYes (synchronized methods)Legacy shared mutable buffer—avoid in new code
Java
// O(n²) time and memory — each += copies entire string
String bad = "";
for (int i = 0; i < 50_000; i++) {
    bad += i;  // new char[] each iteration
}

// O(n) amortized — single expandable buffer
StringBuilder sb = new StringBuilder(64_000); // presize if length known
for (int i = 0; i < 50_000; i++) {
    sb.append(i);
}
String good = sb.toString();

// Java 8+ — chain without explicit builder for simple cases
String joined = String.join(",", List.of("a", "b", "c"));

Major String methods

All “transform” methods return new strings; originals unchanged.

Length & emptiness Java 11+

MethodExampleResult
length()"hello".length()5 (UTF-16 units, not full grapheme count)
isEmpty()"".isEmpty()true
isBlank()" \t".isBlank()true (whitespace only)

Comparison

MethodNotes
equals(Object)Content equality; override in custom classes too
equalsIgnoreCase(String)Locale-insensitive ASCII case fold—not full Unicode semantics
compareTo(String)Lexicographic ordering for sort; returns negative/zero/positive
startsWith / endsWithPrefix/suffix tests; overload with offset
contains(CharSequence)Substring search

Search & slice

MethodNotes
indexOf(ch) / lastIndexOf−1 if not found
substring(begin, end)begin inclusive, end exclusive; bounds checks
split(regex)Returns String[]; limit param controls max splits

Transform

Java
"  Hello  ".strip();              // "Hello"
"  Hello  ".stripLeading().stripTrailing();
"abc".toUpperCase();                 // "ABC"
"abc".replace('b', 'B');             // "aBc" — char replace
"abc".replace("bc", "xyz");          // "axyz"
" a b ".replaceAll("\\s+", "-");    // "-a-b-" regex
" a b ".replaceFirst("\\s+", "-");   // "-a b-"

Characters & bytes

Java
"Hi".charAt(0);           // 'H' (returns int code unit)
"Hi".codePointAt(0);        // 72 — use for supplementary chars
"Hi".getBytes(StandardCharsets.UTF_8);
"Hi".toCharArray();

More APIs Java 11+

Java
"ab".concat("cd");                    // "abcd"
String.valueOf(42);                     // "42" — also overloads for primitives
"order-123".matches("order-\\d+");      // true (regex)
"line1\nline2".lines().count();         // 2
"-".repeat(5);                          // "-----"
"  x  ".indent(4);                      // add 4 spaces per line
"\\n".translateEscapes();               // actual newline char

"file.txt".compareTo("file.pdf");       // negative — lexicographic
"HTTP".compareToIgnoreCase("http");     // 0

Formatting & text blocks

String.format

Printf-style placeholders (%s string, %d int, %f float, %n platform newline):

Java
String msg = String.format("User %s: %d orders, total $%,.2f", name, count, total);
// Locale-aware numbers/dates:
String loc = String.format(Locale.US, "%,.2f", 1234.5);

formatted() Java 15+

Java
String line = "balance: %,.2f".formatted(balance);

Text blocks Java 15+

Multi-line literals without \n concatenation; preserves indentation unless you use \ line-ending strip:

Java
String sql = """
                SELECT id, name
                FROM users
                WHERE active = true
                """;

String json = """
    {
      "id": %d,
      "name": "%s"
    }
    """.formatted(userId, name);
📦 Real World

SQL in repository classes, JSON built with libraries (not manual format for production), log templates with structured logging (SLF4J {} placeholders). Text blocks excel for GraphQL snippets, OpenAPI fixtures, and migration scripts in tests.

Type casting

Widening primitive conversions happen automatically (no data loss in order). Narrowing requires explicit cast and may truncate or lose precision.

Widening chain (automatic)

byte → short → int → long → float → double. char → int → …

Java
int i = 100;
long L = i;           // OK
float f = i;          // OK
double d = f;

char c = 'A';
int code = c;         // 65 — char promotes to int in arithmetic

Narrowing (explicit cast)

Java
double pi = 3.14159;
int truncated = (int) pi;    // 3 — toward zero
byte b = (byte) 300;         // 44 — overflow wraps (byte range)

long big = 9_000_000_000L;
int overflow = (int) big;    // low 32 bits — silent data loss

Reference casting & ClassCastException

Casting objects narrows the compile-time type; JVM checks at runtime:

Java
Object obj = "hello";
String s = (String) obj;   // OK at runtime

Object num = Integer.valueOf(42);
// String bad = (String) num;  // ClassCastException

// Safe pattern (Java 16+)
if (obj instanceof String str) {
    System.out.println(str.length());
}
🎯 Interview Tip

Distinguish compile-time type vs runtime type of references. Generics erase—(List<String>) rawList fails at runtime if list holds integers. Use pattern matching instead of reckless casts.

var — local type inference Java 10+

var is not a type—it is a compiler instruction to infer the type from the initializer. The variable still has a fixed static type; you cannot reassign a different type later.

Where var is allowed

  • Local variables with initializer
  • Enhanced for loop index and element (for (var item : list))
  • try-with-resources (try (var in = Files.newInputStream(path)))

Where var is forbidden

  • Fields, method parameters, method return types
  • Locals without initializer (var x;)
  • Initializer null alone (var x = null;)
  • Array literals without new (var arr = {1,2} illegal)
  • Lambdas when target type is ambiguous (compiler needs context)
Java
var path = Path.of("/tmp", "data.csv");
var lines = Files.readAllLines(path);
var map = Map.of("key", 1, "other", 2);

// var data = {1, 2, 3};              // compile error
// var ambiguous = condition ? 1 : 1.0; // compile error — no common type

for (var i = 0; i < 10; i++) { }     // int inferred

// Good when type is obvious:
var response = httpClient.send(request, BodyHandlers.ofString());

// Spell out when it helps readers:
long rowCount = stream.count();
💡 Pro Tip

Use var when the right-hand side clearly states the type (constructors, well-named factory methods). Avoid for numeric literals where you need long vs int (var count = 1_000_000_000_000L infers long—OK; var x = 1 is always int).