Learnixo

Java & Spring Boot · Lesson 1 of 7

Java Fundamentals: Types, Streams & Records

The Java Type System

Java is statically typed — every variable has a declared type known at compile time. This catches whole classes of bugs before runtime.

Primitives vs Objects

JAVA
// Primitives — stored directly, zero overhead
int     count   = 42;
double  price   = 9.99;
boolean active  = true;
char    grade   = 'A';

// Objects — stored as references on the heap
String  name    = "Jane";
Integer wrapped = 42;        // autoboxing: int → Integer

Autoboxing happens automatically when you assign a primitive to its wrapper type. Unboxing is the reverse. Be careful of NullPointerException when unboxing a null reference.

Strings

Strings in Java are immutable — every "modification" creates a new object.

JAVA
String s = "hello";
s.toUpperCase();      // does nothing — result discarded!
s = s.toUpperCase();  // correct

// String formatting (Java 15+)
String msg = "Patient %s has appointment on %s".formatted("Jane", "2026-04-16");

// Text blocks (Java 15+) — multiline without escaping
String json = """
        {
            "name": "Jane",
            "status": "active"
        }
        """;

For repeated concatenation inside loops, use StringBuilder:

JAVA
StringBuilder sb = new StringBuilder();
for (String tag : tags) {
    sb.append(tag).append(",");
}
String result = sb.toString();

Records (Java 16+)

Records are immutable data carriers — they auto-generate constructor, getters, equals(), hashCode(), and toString().

JAVA
// Old way — 30+ lines of boilerplate
public class PatientDTO {
    private final String id;
    private final String name;
    private final String email;
    // constructor, getters, equals, hashCode, toString...
}

// New way — one line
public record PatientDTO(String id, String name, String email) {}

// Usage
PatientDTO patient = new PatientDTO("PAT-001", "Jane Doe", "jane@example.com");
System.out.println(patient.name());   // getter via record component name
System.out.println(patient);          // PatientDTO[id=PAT-001, name=Jane Doe, email=jane@example.com]

Records support compact constructors for validation:

JAVA
public record AppointmentRequest(String clinicId, String patientId, LocalDateTime dateTime) {
    public AppointmentRequest {
        Objects.requireNonNull(clinicId,   "clinicId is required");
        Objects.requireNonNull(patientId,  "patientId is required");
        Objects.requireNonNull(dateTime,   "dateTime is required");
        if (dateTime.isBefore(LocalDateTime.now())) {
            throw new IllegalArgumentException("Appointment must be in the future");
        }
    }
}

Generics

Generics let you write type-safe code that works with any type:

JAVA
// Without generics — you lose type safety
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0); // cast required, can fail at runtime

// With generics — type checked at compile time
List<String> names = new ArrayList<>();
names.add("hello");
String name = names.get(0);  // no cast needed

// Generic method
public <T> List<T> paginate(List<T> items, int page, int pageSize) {
    int from = page * pageSize;
    int to   = Math.min(from + pageSize, items.size());
    return items.subList(from, to);
}

// Generic class
public class ApiResponse<T> {
    private final T data;
    private final int status;
    private final String message;

    public ApiResponse(T data, int status, String message) {
        this.data    = data;
        this.status  = status;
        this.message = message;
    }

    public static <T> ApiResponse<T> ok(T data) {
        return new ApiResponse<>(data, 200, "ok");
    }

    public static <T> ApiResponse<T> error(String message) {
        return new ApiResponse<>(null, 500, message);
    }
}

Bounded Type Parameters

JAVA
// T must extend Number
public <T extends Number> double sum(List<T> numbers) {
    return numbers.stream().mapToDouble(Number::doubleValue).sum();
}

// Wildcards — when you don't need to use T
public void printAll(List<?> items) {
    items.forEach(System.out::println);
}

// Upper bounded — read-only list of Number or subclass
public double total(List<? extends Number> numbers) { ... }

// Lower bounded — writable list of Number or superclass
public void addNumbers(List<? super Integer> list) { ... }

Lambdas & Functional Interfaces

A lambda is an anonymous function — assign it to a functional interface (an interface with exactly one abstract method):

JAVA
// Verbose anonymous class
Runnable r = new Runnable() {
    @Override
    public void run() { System.out.println("running"); }
};

// Lambda equivalent
Runnable r = () -> System.out.println("running");

// Common functional interfaces
Predicate<String>         isLong    = s -> s.length() > 10;
Function<String, Integer> getLength = String::length;        // method reference
Consumer<String>          printer   = System.out::println;
Supplier<List<String>>    newList   = ArrayList::new;
Comparator<Integer>       desc      = Comparator.reverseOrder();

The Stream API

Streams provide a declarative, functional-style pipeline for processing collections. They don't mutate the source — they produce a new result.

JAVA
List<Appointment> appointments = getAppointments();

// Filter → map → collect
List<String> upcomingPatientNames = appointments.stream()
    .filter(a -> a.status() == Status.SCHEDULED)
    .filter(a -> a.dateTime().isAfter(LocalDateTime.now()))
    .sorted(Comparator.comparing(Appointment::dateTime))
    .map(Appointment::patientName)
    .distinct()
    .collect(Collectors.toList());

// Group by clinic
Map<String, List<Appointment>> byClinic = appointments.stream()
    .collect(Collectors.groupingBy(Appointment::clinicId));

// Count by status
Map<Status, Long> countByStatus = appointments.stream()
    .collect(Collectors.groupingBy(Appointment::status, Collectors.counting()));

// Flat map — flatten nested lists
List<String> allTags = clinics.stream()
    .flatMap(clinic -> clinic.tags().stream())
    .distinct()
    .toList();  // Java 16+ immutable list

// Reduce — aggregate to a single value
OptionalDouble avgDuration = appointments.stream()
    .mapToLong(Appointment::durationMinutes)
    .average();

avgDuration.ifPresent(avg -> System.out.printf("Avg duration: %.1f min%n", avg));

Parallel Streams

For CPU-intensive work on large collections:

JAVA
// Use parallelStream() — automatically uses ForkJoinPool
List<String> processed = records.parallelStream()
    .map(this::expensiveTransform)
    .collect(Collectors.toList());

Warning: don't use parallel streams for I/O operations — use async/reactive for that.


Optional

Optional<T> is a container that may or may not hold a value — it makes nullability explicit and eliminates NPEs:

JAVA
// Avoid — return null forces callers to remember null-checks
public Patient findPatient(String id) {
    return db.find(id);  // could be null!
}

// Better — Optional communicates "may be absent"
public Optional<Patient> findPatient(String id) {
    return Optional.ofNullable(db.find(id));
}

// Idiomatic Optional usage
findPatient("PAT-001")
    .filter(p -> p.isActive())
    .map(Patient::email)
    .ifPresentOrElse(
        email -> sendReminder(email),
        ()    -> log.warn("Patient not found")
    );

// With default
String email = findPatient(id)
    .map(Patient::email)
    .orElse("noreply@clinic.com");

// Throw if absent
Patient patient = findPatient(id)
    .orElseThrow(() -> new PatientNotFoundException(id));

Pattern Matching (Java 16–21)

instanceof Pattern Matching (Java 16)

JAVA
// Old
if (obj instanceof String) {
    String s = (String) obj;
    System.out.println(s.length());
}

// New
if (obj instanceof String s) {
    System.out.println(s.length());
}

Switch Expressions (Java 14+)

JAVA
String label = switch (status) {
    case SCHEDULED  -> "Upcoming";
    case COMPLETED  -> "Done";
    case CANCELLED  -> "Cancelled";
    case NO_SHOW    -> "No Show";
};

// With guards (Java 21 pattern matching for switch)
String tier = switch (score) {
    case int s when s >= 90 -> "Gold";
    case int s when s >= 75 -> "Silver";
    default                 -> "Bronze";
};

Sealed Classes (Java 17)

Restrict which classes can extend a type — great for domain modelling:

JAVA
public sealed interface CallEvent
    permits CallStarted, CallEnded, CallTransferred {
    String callId();
    Instant occurredAt();
}

public record CallStarted(String callId, String queueId, Instant occurredAt) implements CallEvent {}
public record CallEnded(String callId, int durationSeconds, Instant occurredAt)  implements CallEvent {}
public record CallTransferred(String callId, String toAgent, Instant occurredAt) implements CallEvent {}

// Exhaustive switch — compiler errors if you miss a case
String describe(CallEvent event) {
    return switch (event) {
        case CallStarted   e -> "Call %s started in queue %s".formatted(e.callId(), e.queueId());
        case CallEnded     e -> "Call %s ended after %ds".formatted(e.callId(), e.durationSeconds());
        case CallTransferred e -> "Call %s transferred to %s".formatted(e.callId(), e.toAgent());
    };
}

Collections Cheat Sheet

| Need | Use | |------|-----| | Ordered, fast random access | ArrayList<T> | | Fast insert/delete at head/tail | LinkedList<T> (also a Deque) | | Unique items, insertion order | LinkedHashSet<T> | | Unique items, sorted | TreeSet<T> | | Key-value, insertion order | LinkedHashMap<K,V> | | Key-value, sorted by key | TreeMap<K,V> | | Thread-safe map | ConcurrentHashMap<K,V> | | Immutable list | List.of(a, b, c) | | Immutable map | Map.of("k1", v1, "k2", v2) |