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.
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.
| Type | Bits | Range / values | Literal examples |
|---|---|---|---|
| byte | 8 | −128 … 127 | (byte) 200, 0x0F |
| short | 16 | −32,768 … 32,767 | Rare; legacy buffers |
| int | 32 | ≈ ±2.1×10⁹ | 42, 0xFF, 1_000_000 |
| long | 64 | ≈ ±9.2×10¹⁸ | 99L, 1_700_000_000_000L |
| float | 32 | IEEE 754 | 3.14f, 1e6f |
| double | 64 | IEEE 754 (default float literal) | 3.14, 1.0e-6 |
| char | 16 | UTF-16 code unit 0 … 65535 | 'A', '\u03A9', '\n' |
| boolean | 1 bit* | true, false only | true, false |
* JVM may use a byte internally; not exposed as numeric type.
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:
| Primitive | Wrapper |
|---|---|
| byte | Byte |
| short | Short |
| int | Integer |
| long | Long |
| float | Float |
| double | Double |
| char | Character |
| boolean | Boolean |
Autoboxing & unboxing
The compiler inserts conversions automatically when a primitive meets a reference context (or vice versa):
// 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.
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.
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.
| Kind | Declared | Default | Lifetime |
|---|---|---|---|
| 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) |
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 field — final int x; assigned in each constructor path.
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
| Operator | Meaning | Notes |
|---|---|---|
| + − * / % | Add, subtract, multiply, divide, remainder | Integer / uses truncating division |
| ++ -- | Increment/decrement | Prefix vs postfix differ in expression value |
| += -= … | Compound assignment | Includes implicit cast: byte b; b += 300; wraps |
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.
| Expression | Binary (a=12) | Decimal result | Typical use |
|---|---|---|---|
a << 1 | 00011000 | 24 | Multiply by 2 |
a >> 1 | 00000110 | 6 | Divide by 2 (signed) |
a >>> 1 | fill with 0 | 6 | Unsigned shift (negative ints) |
a & 1 | test bit 0 | 0 | Even: 0, odd: 1 |
a | 8 | set bit 3 | 12 | 8 = 12 | OR flags: READ=1, WRITE=2 |
a ^ 4 | toggle bit 2 | 8 | XOR checksums |
final int READ = 1 << 0; // 0001
final int WRITE = 1 << 1; // 0010
int perms = READ | WRITE;
boolean canRead = (perms & READ) != 0;
Ternary & instanceof
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
}
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.
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:
// 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:
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
| Loop | Use when |
|---|---|
for (init; cond; step) | Index-based, known bounds |
| enhanced for | Iterate every Iterable / array without index |
while | Condition-first (0+ iterations) |
do-while | At least one iteration (menu, retry) |
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);
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
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
| API | Behavior |
|---|---|
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) |
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);
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:
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 caching —
hashCode()can be computed once and cached (fieldhashin String).
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.
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).
== 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
| Type | Mutable | Thread-safe | When to use |
|---|---|---|---|
| String | No | Yes (immutable) | Constants, map keys, short joins of few parts |
| StringBuilder | Yes | No | Loops building SQL, JSON, HTML, log lines |
| StringBuffer | Yes | Yes (synchronized methods) | Legacy shared mutable buffer—avoid in new code |
// 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+
| Method | Example | Result |
|---|---|---|
length() | "hello".length() | 5 (UTF-16 units, not full grapheme count) |
isEmpty() | "".isEmpty() | true |
isBlank() | " \t".isBlank() | true (whitespace only) |
Comparison
| Method | Notes |
|---|---|
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 / endsWith | Prefix/suffix tests; overload with offset |
contains(CharSequence) | Substring search |
Search & slice
| Method | Notes |
|---|---|
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
" 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
"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+
"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):
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+
String line = "balance: %,.2f".formatted(balance);
Text blocks Java 15+
Multi-line literals without \n concatenation; preserves indentation unless you use \ line-ending strip:
String sql = """
SELECT id, name
FROM users
WHERE active = true
""";
String json = """
{
"id": %d,
"name": "%s"
}
""".formatted(userId, name);
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 → …
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)
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:
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());
}
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
nullalone (var x = null;) - Array literals without
new(var arr = {1,2}illegal) - Lambdas when target type is ambiguous (compiler needs context)
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();
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).