Day 1: Intro to Java

The language basics: types, methods, collections, logic, control flow, files and a first HTTP call.

Subsections of Day 1: Intro to Java

Why Java?

Java compiles to bytecode, which runs on the JVM — the same .class file works on Linux, macOS and Windows. A new release ships every six months, an LTS release every two years.

Version Year Highlights
8 2014 lambdas, streams, Optional, java.time
11 LTS 2018 HTTP client
17 LTS 2021 records, sealed classes, text blocks, switch expressions
21 LTS 2023 virtual threads, pattern matching for switch
25 LTS 2025 compact source files & IO, module imports, scoped values

Where it is used

Backends (Spring Boot, Quarkus), Android, big data (Kafka, Spark, Elasticsearch), and most developer tooling.

Java ≠ JavaScript

Unrelated languages. The name was a 1995 marketing decision.

Three acronyms

  • JDK — compiler, JShell, tools (what you install)
  • JRE — JVM + standard library
  • JVM — runs bytecode, compiles hot code to machine code (JIT), collects garbage

You never free memory manually.

Static typing

Types are checked at compile time:

int age = 30;
age = "thirty";     // compile error

var infers the type — it does not make Java dynamic:

var age = 30;                        // int
var names = new ArrayList<String>(); // ArrayList<String>

Conventions

Element Style Example
classes, records, interfaces UpperCamelCase BankAccount
methods, variables lowerCamelCase computeTotal
constants UPPER_SNAKE_CASE MAX_SIZE
packages all lowercase com.example.shop
file name = public class name BankAccount.java

Four spaces of indentation, opening brace on the same line.

class Account {
    private final String owner;

    Account(String owner) {
        this.owner = owner;
    }
}

Comments

// line

/* block */

/**
 * Javadoc — rendered to HTML docs.
 * @param amount deposit in cents
 * @return the new balance
 */

What Java does not have

  • free functions (every method belongs to a type)
  • multiple class inheritance (interfaces instead)
  • pointer arithmetic or manual memory management
  • operator overloading (+ on strings is the one exception)

★ Exercises

  1. Which Java version is active on your machine? Is it an LTS?
  2. Rewrite in proper Java style: max retry count (constant), user profile (class), load data (method).
  3. Explain JDK vs JRE vs JVM in your own words.
  4. Why does var x; not compile? Try it in JShell and read the error.

Basic Data Types

Variables

int count = 42;
double price = 19.99;
boolean active = true;
String name = "Ann";

var count2 = 42;        // same thing, type inferred
final double VAT = 0.19;  // constant

var works for local variables only — not fields, parameters or return types.

Tip

Make everything final that never changes. final var works too.

The eight primitives

Type Size Use Default
byte 8 bit −128…127 0
short 16 bit −32,768…32,767 0
int 32 bit default integer type 0
long 64 bit big integers, suffix L 0L
float 32 bit suffix f 0.0f
double 64 bit default decimal type 0.0
char 16 bit one character, single quotes '�'
boolean true / false false
long population = 8_500_000_000L;   // underscores for readability
char initial = 'A';

Arithmetic

7 / 2       // 3   ← integer division!
7 % 2       // 1
7 / 2.0     // 3.5

int max = Integer.MAX_VALUE;
max + 1                  // overflows silently
Math.addExact(max, 1)    // throws ArithmeticException

0.1 + 0.2   // 0.30000000000000004

For money use BigDecimal, or count cents in a long:

new BigDecimal("0.1").add(new BigDecimal("0.2"))   // 0.3
var n = 0;
n += 5; n -= 2; n *= 4; n++; n--;

Wrappers and autoboxing

Every primitive has an object twin: Integer, Long, Double, Boolean, Character. Collections need those, and conversion is automatic.

List<Integer> nums = new ArrayList<>();
nums.add(42);          // int → Integer
int first = nums.get(0);
Warning

Wrappers can be null. Integer x = null; int y = x; throws a NullPointerException.

Strings

Strings are immutable — every method returns a new one.

var text = "Hello World";

text.length()             // 11
text.toUpperCase()
text.charAt(0)            // 'H'
text.substring(6)         // "World"
text.contains("World")    // true
text.indexOf("World")     // 6
text.replace("World", "Java")
text.split(" ")           // ["Hello", "World"]
"  edge  ".strip()
"   ".isBlank()           // true
"ab".repeat(3)            // "ababab"

Building strings

var s1 = name + " is " + age;
var s2 = "%s is %d".formatted(name, age);
var s3 = String.join(", ", "red", "green", "blue");
Format Meaning
%s any value as text
%d integer, %05d00042
%.2f two decimals
%n newline

Comparing strings

a == b          // reference identity — almost never what you want
a.equals(b)     // content
a.equalsIgnoreCase(b)
The classic beginner bug

Use equals for objects, == only for primitives and enum constants.

Text blocks

var json = """
    {
      "name": "Ann",
      "role": "developer"
    }
    """;

Indentation is stripped relative to the closing """; no escaping of quotes needed.

StringBuilder

var sb = new StringBuilder();
for (var i = 1; i <= 5; i++) sb.append(i).append(" ");
sb.toString().strip();   // "1 2 3 4 5"

Input and output

void main() {
    var name = IO.readln("Your name? ");
    IO.println("Hello, " + name + "!");
}

The classic way still works: System.out.println(...), System.err.println(...).

★ Exercises

  1. Store your name, birth year and height in suitable types and print one sentence.
  2. Compute your age in days (365 per year is fine).
  3. Write three versions of 5 / 2 that yield 2.5.
  4. Split "ann,miller,berlin" and print each part capitalized on its own line.
  5. Build a small HTML page with a text block and print it.
  6. Why is 0.1 + 0.2 == 0.3 false? How would you compare instead?

Methods

Return type, name, parameters, body:

int add(int a, int b) {
    return a + b;
}

void greet(String name) {          // void = returns nothing
    IO.println("Hello, " + name);
}

Arguments are copies

Java is always pass-by-value. For objects the reference is copied — you can mutate the object, but not reassign the caller’s variable.

void fill(List<String> list) {
    list.add("new");            // visible to the caller
    list = new ArrayList<>();   // local only
}

Overloading

Same name, different parameter lists:

int area(int side)                { return side * side; }
int area(int width, int height)   { return width * height; }
double area(double radius)        { return Math.PI * radius * radius; }

A different return type alone is not enough.

Varargs

int sum(int... numbers) {
    var total = 0;
    for (var n : numbers) total += n;
    return total;
}

sum();            // 0
sum(1, 2, 3);     // 6

Must be the last parameter.

Scope

A variable lives inside its block:

void demo() {
    var outer = 1;
    if (outer > 0) {
        var inner = 2;
    }
    IO.println(inner);   // error: does not exist here
}

static methods belong to the class, not an instance:

public class MathHelper {
    public static int square(int x) { return x * x; }
}

MathHelper.square(5);

Writing good methods

  • one job per method, and the name says it — if you need “and”, split it
  • verbs first: computeDiscount, loadCustomer, isValid
  • more than three parameters? use a record instead
  • return early instead of nesting
String grade(int points) {
    if (points < 0 || points > 100) return "invalid";
    return points >= 50 ? "pass" : "fail";
}

Recursion

long factorial(int n) {
    if (n <= 1) return 1;        // base case
    return n * factorial(n - 1);
}

Java has no tail-call optimisation — deep recursion throws StackOverflowError.

★ Exercises

  1. isPalindrome(String) — ignore case.
  2. Overload max three ways: two int, two double, any number of int.
  3. countVowels(String).
  4. Write Fibonacci recursively and iteratively; time both for n = 40 with System.nanoTime().
  5. formatName(String first, String last)"Miller, Ann", tolerating stray spaces and mixed case.

Arrays & Collections

Arrays

Fixed length, zero-based:

int[] numbers = new int[5];
String[] colors = {"red", "green", "blue"};

colors.length     // 3 — a field, not a method
colors[1]         // "green"
colors[3]         // ArrayIndexOutOfBoundsException

Arrays.sort(numbers);
Arrays.toString(numbers);

Arrays are rigid. In practice you use a List.

List

Ordered, resizable, duplicates allowed:

var names = new ArrayList<String>();
names.add("Ann");
names.add("Ben");

names.get(0)            // "Ann"
names.size()            // 2
names.contains("Ben")   // true
names.indexOf("Ben")    // 1
names.remove("Ben");
names.set(0, "Anne");
names.isEmpty()

List<String> means “list of strings” — the compiler keeps everything else out. On the right side the diamond <> is enough: List<String> names = new ArrayList<>();

var days = List.of("Mon", "Tue", "Wed");   // immutable
days.add("Thu");                            // UnsupportedOperationException

var copy = new ArrayList<>(days);           // mutable copy
Tip

Default to List.of(...). Immutable is the common case and prevents accidental changes.

for (var name : names) IO.println(name);

for (var i = 0; i < names.size(); i++) IO.println(i + ": " + names.get(i));

names.forEach(IO::println);

Set

No duplicates. HashSet (unordered), LinkedHashSet (insertion order), TreeSet (sorted).

var tags = new HashSet<String>();
tags.add("java");
tags.add("java");     // no effect
tags.size()           // 1

var a = Set.of(1, 2, 3, 4);
var b = Set.of(3, 4, 5);

var intersection = new HashSet<>(a); intersection.retainAll(b);   // [3, 4]
var union        = new HashSet<>(a); union.addAll(b);             // [1..5]
var difference   = new HashSet<>(a); difference.removeAll(b);     // [1, 2]

Map

Keys → values, keys are unique.

var ages = new HashMap<String, Integer>();
ages.put("Ann", 30);
ages.put("Ann", 31);            // overwrites

ages.get("Ann")                 // 31
ages.get("Zoe")                 // null
ages.getOrDefault("Zoe", 0)     // 0
ages.containsKey("Ann")
ages.remove("Ann");

Useful patterns:

counts.merge("java", 1, Integer::sum);              // count things
counts.putIfAbsent("course", 1);
groups.computeIfAbsent("a", k -> new ArrayList<>()).add("Ann");

for (var e : ages.entrySet()) IO.println(e.getKey() + " " + e.getValue());
ages.forEach((name, age) -> IO.println(name + ": " + age));

var capitals = Map.of("Germany", "Berlin", "France", "Paris");   // immutable

Which one?

Need Type
ordered, duplicates ok ArrayList
no duplicates, fast lookup HashSet
no duplicates, sorted TreeSet
key → value HashMap
key → value, sorted TreeMap
queue / stack ArrayDeque

Records: bundling data

When values belong together, use a type — not a map:

record Person(String name, int age) {}

var ann = new Person("Ann", 30);
ann.name()      // "Ann"
IO.println(ann) // Person[name=Ann, age=30]

You get the constructor, accessors, equals, hashCode and toString for free — which also makes records good map keys:

record Coordinate(int x, int y) {}

var map = new HashMap<Coordinate, String>();
map.put(new Coordinate(1, 2), "treasure");
map.get(new Coordinate(1, 2));   // "treasure"  different object, equal content

More on records in chapter 11.

★ Exercises

  1. Sum, average, min and max of ten numbers, using a loop.
  2. Remove duplicates from a list — once with a Set, once by hand.
  3. Count letter frequency of a sentence in a Map<Character, Integer>.
  4. Build a phone book Map<String, String> with add, lookup and delete methods.
  5. Define record Item(String name, double price, int qty) and total a cart of five items.
  6. Turn a List<String> into a Map<Integer, List<String>> grouped by word length.

Boolean Logic

There is no “truthy”

A condition must be a boolean. Nothing else.

if (text) {  }              // error
if (!text.isEmpty()) {  }   // ok

if (count) {  }             // error
if (count != 0) {  }        // ok

Comparison

Operator Meaning
== != equal / not equal (identity for objects)
< > <= >= ordering
a.equals(b)              // content comparison
Objects.equals(a, b)     // null-safe on both sides

The wrapper trap — small values are cached:

Integer x = 127, y = 127;
Integer p = 128, q = 128;

x == y        // true  (cache −128…127)
p == q        // false
p.equals(q)   // true

Logical operators

age >= 18 && hasId      // and
age < 18 || hasId       // or
!hasId                  // not

&& and || short-circuit — the right side is skipped when the result is already known. That protects you:

if (name != null && name.length() > 3) {  }   // safe
if (name != null &  name.length() > 3) {  }   // NPE: & always evaluates both
a b a && b a || b !a
true true true true false
true false false true false
false true false true true
false false false false true

Ternary

var status = points >= 50 ? "pass" : "fail";

Fine for simple cases, never nest it — use switch instead.

Dealing with null

String name = null;
name.length();   // NullPointerException

Java tells you exactly what was null:

Cannot invoke "String.length()" because "name" is null

Strategies:

if (name != null && !name.isBlank()) {  }         // check
Objects.requireNonNullElse(name, "unknown");       // fallback
Objects.requireNonNull(name, "name is required");  // fail fast
Optional<String> found = find("Ann");              // see chapter 10
Tip

Never return null for a collection — return an empty one. It saves your callers hundreds of null checks.

Keep conditions readable

var isAdult    = c.age() >= 18;
var isDomestic = c.country().equals("US");
var isActive   = !c.blocked() && c.balance() > 0;

if (isAdult && isDomestic && isActive) {  }

De Morgan: !(a && b)!a || !b, and !(a || b)!a && !b.

★ Exercises

  1. boolean isLeapYear(int year) — divisible by 4, not by 100, unless by 400.
  2. Why is "a" == "a" often true, yet unreliable? Try it in JShell.
  3. Rewrite !(age < 18 || blocked) without the leading !.
  4. boolean isValidPassword(String) — 8+ chars, a digit, an uppercase letter; null is invalid but must not throw.
  5. What does 1 == 1.0 print, and why?

Loops & Control Flow

if / else

if (temp > 30) {
    IO.println("hot");
} else if (temp > 15) {
    IO.println("mild");
} else {
    IO.println("cold");
}

Braces are optional for a single statement — always write them anyway.

switch

Old form, needs break or it falls through:

switch (day) {
    case 1:
    case 7:
        IO.println("weekend");
        break;
    default:
        IO.println("weekday");
}

Arrow form — no fallthrough, and it produces a value:

var label = switch (day) {
    case 1, 7 -> "weekend";
    case 2, 3, 4, 5, 6 -> "weekday";
    default -> throw new IllegalArgumentException("bad day: " + day);
};

var text = switch (grade) {
    case 1 -> "excellent";
    default -> {
        var s = "grade " + grade;
        yield s.toUpperCase();     // yield inside a block
    }
};

Pattern matching

String describe(Object o) {
    return switch (o) {
        case Integer i when i > 100 -> "big number: " + i;
        case Integer i              -> "number: " + i;
        case String s               -> "text of %d chars".formatted(s.length());
        case int[] a                -> "array of " + a.length;
        case null                   -> "nothing";
        default                     -> "unknown";
    };
}

when adds a guard. Without case null, a null value throws.

Record patterns

sealed interface Shape permits Circle, Rect {}
record Circle(double radius) implements Shape {}
record Rect(double w, double h) implements Shape {}

double area(Shape s) {
    return switch (s) {
        case Circle(double r)    -> Math.PI * r * r;
        case Rect(double w, double h) -> w * h;
    };
}
Note

No default needed: because Shape is sealed, the compiler knows every case — and will flag this switch if you add another shape later.

instanceof with a pattern

if (o instanceof String s && s.length() > 3) {
    IO.println(s.toUpperCase());
}

Loops

for (var name : names) {  }                  // for-each: the default

for (var i = 0; i < 5; i++) {  }             // when you need the index
for (var i = 10; i > 0; i -= 2) {  }

while (rest > 1) { rest /= 2; }

do { answer = IO.readln("again? "); }         // runs at least once
while (!answer.equals("n"));

break and continue

for (var n : numbers) {
    if (n % 2 != 0) continue;   // skip
    if (n > 15) break;          // leave the loop
    IO.println(n);
}

Labels exist for nested loops, but extracting a method and return is usually cleaner:

outer:
for () { for () { if () break outer; } }

Putting it together

void main() {
    var secret = new java.util.Random().nextInt(1, 101);
    var tries = 0;

    while (true) {
        var input = IO.readln("Guess 1-100: ");
        int guess;
        try {
            guess = Integer.parseInt(input.strip());
        } catch (NumberFormatException e) {
            IO.println("Not a number.");
            continue;
        }

        tries++;
        switch (Integer.compare(guess, secret)) {
            case -1 -> IO.println("too low");
            case  1 -> IO.println("too high");
            default -> {
                IO.println("Got it in %d tries!".formatted(tries));
                return;
            }
        }
    }
}

★ Exercises

  1. Print a multiplication table from 1 to 10, aligned with "%4d".
  2. FizzBuzz for 1–100 — once with if/else, once with a switch expression.
  3. Print all primes up to n.
  4. Write String classify(Object o) handling Integer, Double, String, List and null.
  5. Add a Triangle to the shapes above. What does the compiler say about your old switch?
  6. Change the guessing game to stop after 7 wrong guesses.

Working With Java Programs

The classic structure

public class Calculator {

    public static void main(String[] args) {
        System.out.println("start");
    }
}
  • public class Calculator — file must be Calculator.java
  • static — the JVM can call it without an object
  • String[] args — command line arguments

The compact form (Java 25) does the same thing:

void main() {
    IO.println("start");
}

Both may contain fields and methods:

final double VAT = 0.19;

double gross(double net) { return net * (1 + VAT); }

void main() { IO.println(gross(100)); }
Note

Compact for scripts and exercises, classic for real projects. The rest of the language is identical.

Compile and run

java Calculator.java          # run a single file directly
javac Calculator.java         # produce Calculator.class
java Calculator

javac -d out $(find src -name "*.java")
java -cp out com.example.Calculator
void main(String[] args) {
    if (args.length < 2) {
        IO.println("usage: java Calculator.java <a> <b>");
        return;
    }
    var a = Integer.parseInt(args[0]);
    var b = Integer.parseInt(args[1]);
    IO.println(a + b);
}

Reading errors

Compile time — the program never runs:

Calculator.java:5: error: incompatible types: String cannot be converted to int
    int x = "hello";
            ^

Runtime — read the stack trace bottom-up:

Exception in thread "main" java.lang.ArithmeticException: / by zero
        at Calculator.divide(Calculator.java:12)
        at Calculator.main(Calculator.java:5)

Catching exceptions

try {
    var n = Integer.parseInt(input);
    IO.println(100 / n);
} catch (NumberFormatException | ArithmeticException e) {
    IO.println("bad input: " + e.getMessage());
} finally {
    IO.println("always runs");
}
Unchecked Checked
base class RuntimeException Exception
must be handled no yes — catch or throws
typical NullPointerException, IllegalArgumentException IOException, SQLException
String read(Path p) throws IOException {   // pass it up
    return Files.readString(p);
}

throw new IllegalArgumentException("age must not be negative: " + age);

Details in chapter 12.

Files

import java.nio.file.Files;
import java.nio.file.Path;

var path = Path.of("notes.txt");

Files.writeString(path, "line 1\nline 2\n");
Files.writeString(path, "line 3\n", StandardOpenOption.APPEND);

var content = Files.readString(path);
for (var line : Files.readAllLines(path)) IO.println(line);

Large files — stream them, and close the stream:

try (var lines = Files.lines(Path.of("big.log"))) {
    lines.filter(l -> l.contains("ERROR")).limit(10).forEach(IO::println);
}

try (...) closes everything automatically, even when an exception is thrown:

try (var writer = Files.newBufferedWriter(Path.of("out.txt"))) {
    writer.write("hello");
    writer.newLine();
}

Paths:

var p = Path.of("data", "2025", "report.csv");

Files.exists(p);  Files.isDirectory(p);  Files.size(p);
Files.createDirectories(p.getParent());
Files.deleteIfExists(p);
p.getFileName();  p.toAbsolutePath();

UTF-8 is the default since Java 18 — no charset arguments needed.

A complete program

import java.nio.file.*;

void main(String[] args) {
    if (args.length == 0) {
        IO.println("usage: java WordCount.java <file>");
        return;
    }
    try {
        var words = Files.readString(Path.of(args[0])).toLowerCase().split("\\W+");

        var counts = new java.util.HashMap<String, Integer>();
        for (var w : words) {
            if (!w.isBlank()) counts.merge(w, 1, Integer::sum);
        }

        counts.entrySet().stream()
            .sorted((a, b) -> b.getValue() - a.getValue())
            .limit(10)
            .forEach(e -> IO.println("%-15s %d".formatted(e.getKey(), e.getValue())));

    } catch (java.io.IOException e) {
        IO.println("cannot read file: " + e.getMessage());
    }
}

★ Exercises

  1. Run the word counter above on any text file.
  2. Add a second argument: how many words to print.
  3. Copy a file, adding line numbers.
  4. What happens on a missing file? Catch it and print something useful.
  5. Read a CSV of name;age;city into a list of records.
  6. Trigger a NullPointerException on purpose and read the stack trace.

APIs & HTTP

A web API answers HTTP requests with structured data, usually JSON.

Method Purpose Status Meaning
GET read 2xx success
POST create 3xx redirect
PUT/PATCH update 4xx your mistake (404, 401, 429)
DELETE delete 5xx server’s mistake

The built-in HTTP client

import java.net.URI;
import java.net.http.*;

void main() throws Exception {
    try (var client = HttpClient.newHttpClient()) {

        var request = HttpRequest.newBuilder()
            .uri(URI.create("https://api.github.com/repos/openjdk/jdk"))
            .header("Accept", "application/json")
            .GET()
            .build();

        var response = client.send(request, HttpResponse.BodyHandlers.ofString());

        IO.println(response.statusCode());
        IO.println(response.body());
    }
}

Configuration:

var client = HttpClient.newBuilder()
    .connectTimeout(Duration.ofSeconds(10))
    .followRedirects(HttpClient.Redirect.NORMAL)
    .build();

var request = HttpRequest.newBuilder(URI.create(url))
    .timeout(Duration.ofSeconds(20))
    .header("User-Agent", "java-course/1.0")
    .build();

POST with a body:

var body = """
    {"title": "New task", "done": false}
    """;

HttpRequest.newBuilder()
    .uri(URI.create("https://example.test/api/tasks"))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(body))
    .build();
Warning

A 404 does not throw. Check response.statusCode() yourself.

if (response.statusCode() >= 400) {
    throw new IllegalStateException("API error %d".formatted(response.statusCode()));
}

JSON

Java has no JSON parser in the standard library. Use Jackson or Gson:

<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.18.2</version>
</dependency>
@JsonIgnoreProperties(ignoreUnknown = true)
record Repository(String name, String description, int stargazers_count) {}

var mapper = new ObjectMapper();
var repo = mapper.readValue(response.body(), Repository.class);
var json = mapper.writeValueAsString(repo);

Full example

void main() throws Exception {
    var zip = IO.readln("ZIP code: ").strip();

    try (var client = HttpClient.newHttpClient()) {
        var request = HttpRequest.newBuilder(
            URI.create("https://api.zippopotam.us/us/" + zip)).build();

        var response = client.send(request, HttpResponse.BodyHandlers.ofString());

        switch (response.statusCode()) {
            case 200 -> IO.println(response.body());
            case 404 -> IO.println("unknown ZIP code");
            default  -> IO.println("unexpected status " + response.statusCode());
        }
    }
}

Manners

  • respect rate limits — on 429, back off instead of retrying immediately
  • always set timeouts
  • keys go in environment variables: System.getenv("API_TOKEN")
  • one HttpClient per application, not per request

★ Exercises

  1. Call any public API; print the status code and the first 200 characters.
  2. Print all response headers (response.headers().map()).
  3. Fetch three URLs one after another and measure the total time — you will parallelise this in chapter 15.
  4. Retry up to three times with growing delays when you get a 429.
  5. Read a token from an environment variable and send it as an Authorization header. What should happen if the variable is missing?