Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

Java Crash Course

Java is the workhorse of the software industry. From massive enterprise backends and Android apps to big data processing and high-frequency trading, Java runs the world.
This crash course is designed to take you from “Hello World” to understanding the JVM, concurrency, and modern Java (17/21) features.

Why Java?

Java has consistently been one of the top languages for 25+ years. Netflix, LinkedIn, Amazon, and most major banks run their core systems on Java. When you withdraw money from an ATM, place a stock trade, or stream a show, there is a very good chance Java is doing the heavy lifting behind the scenes. Think of Java like a Toyota Camry: it is not the flashiest language, it is not the fastest in synthetic benchmarks, and nobody writes love letters about its syntax. But it starts every morning, it runs for 300,000 miles, parts are available everywhere, and every mechanic in the world knows how to service it. That reliability at scale is why the world’s most critical systems — banking, healthcare, air traffic control — choose Java over trendier alternatives.

Write Once, Run Anywhere

The JVM (Java Virtual Machine) acts as a universal translator between your code and the operating system — like a diplomat who speaks every OS’s native language. Your code runs on the JVM, and the JVM handles the differences between platforms. Write once, deploy to Linux servers, Windows desktops, or macOS laptops without changing a line.

Enterprise Grade

Strict type safety, massive ecosystem, and backward compatibility make it the number one choice for large systems. Java code written in 2005 still compiles and runs today — try saying that about most other ecosystems. This backward compatibility is not an accident; it is a core design principle that lets billion-dollar systems upgrade incrementally.

Multithreading

Built-in support for concurrency means your application can handle thousands of simultaneous users. Java was designed for multithreading from day one, not as an afterthought. With Java 21’s Virtual Threads, you can now create millions of lightweight threads — making the reactive programming complexity of the last decade largely unnecessary.

Modern Evolution

With a 6-month release cycle (since Java 10), Java is evolving faster than ever: Records, Pattern Matching, Virtual Threads, Structured Concurrency, and more. The language that people called “slow to evolve” now ships features faster than most competitors.

Course Roadmap

We will peel back the layers of abstraction to understand how Java really works.
1

Fundamentals

Understand the JVM, bytecode, types, and control flow. Start Learning
2

Object-Oriented Programming

Classes, interfaces, inheritance, and polymorphism. Explore OOP
3

Collections Framework

Lists, Maps, Sets, and the Stream API. Master Collections
4

Concurrency

Threads, Executors, and CompletableFutures. Go Parallel
5

Modern Java

Lambdas, Streams, Records, and Pattern Matching (Java 8 to 21). Go Modern

Prerequisites

  • Basic programming knowledge (variables, loops, functions in any language).
  • JDK 17 or higher installed (java -version in your terminal should print a version number). If you are installing fresh, go straight to JDK 21 — it is the latest Long-Term Support (LTS) release and includes Virtual Threads.
  • An IDE (IntelliJ IDEA Community Edition is free and highly recommended — its auto-complete, refactoring tools, and debugger are best-in-class for Java. VS Code with the “Extension Pack for Java” is also a solid choice).
Common Setup Pitfall: Many beginners install a JDK but their system still points to an older version. After installing, always verify with java -version AND javac -version — both should show the same version. If they differ, your PATH environment variable is pointing to a stale JDK installation. On macOS, /usr/libexec/java_home -V lists all installed JDKs.

The Java Philosophy

“Java is C++ without the guns, knives, and clubs.” — James Gosling
Java manages memory for you (Garbage Collection — think of it as hiring a cleaning service for your house: you create objects freely and the GC periodically walks through memory, identifies objects nobody is using anymore, and reclaims that space). It prevents buffer overflows (Array Bounds Checking — every array access is checked at runtime, so you cannot accidentally read memory you do not own), and enforces type safety at compile time so entire categories of bugs never make it to production. It trades some raw control for safety and productivity — a trade-off that has proven its worth across millions of production systems. The philosophy boils down to: make the common case safe by default, and provide escape hatches (like sun.misc.Unsafe, now being replaced by the Foreign Function and Memory API) for the rare cases where you truly need manual control.
Coming from another language? If you know Python, Java will feel more structured and verbose — but that structure pays off in large codebases where explicit types and compile-time checks catch bugs early. If you know C/C++, Java will feel familiar syntactically but liberating — no manual memory management, no segfaults, no header files. If you know JavaScript or TypeScript, Java’s type system is similar to TypeScript but enforced at both compile time and runtime, and the build/dependency tooling (Maven, Gradle) is more mature but also more complex.

Common Pitfalls Before You Start

These three terms are nested like Russian dolls. The JVM (Java Virtual Machine) is the engine that runs bytecode. The JRE (Java Runtime Environment) is the JVM plus the standard libraries your code needs at runtime. The JDK (Java Development Kit) is the JRE plus development tools like javac (compiler), jdb (debugger), and jar (packaging). As a developer, you always install the JDK. Since Java 11, Oracle stopped distributing a standalone JRE — the JDK is the only download you need.
Oracle JDK, OpenJDK, Amazon Corretto, Eclipse Temurin, Azul Zulu — the sheer number of JDK distributions confuses beginners. They are all built from the same OpenJDK source code. The differences are in licensing, support contracts, and minor patches. For learning and most production use, Eclipse Temurin (from Adoptium) or Amazon Corretto are free, well-supported choices. Do not pay for a JDK unless your company specifically needs Oracle’s commercial support.
Java projects almost never compile with a bare javac command. Real projects use Maven or Gradle to manage dependencies, compile code, run tests, and package artifacts. Learn at least one early — Maven is more common in enterprises, Gradle is more flexible and used by Android. Ignoring the build tool and trying to manage classpaths manually leads to dependency conflicts that will waste hours of your time.

Interview Deep-Dive

Strong Answer:
  • Java source compiles to bytecode (.class files) via javac. Bytecode is a platform-agnostic instruction set for an abstract stack-based virtual machine. Any platform with a conforming JVM implementation can execute those instructions, which is the foundation of “write once, run anywhere.” The key insight is that Java pushes the platform-specific complexity into the JVM itself — each OS gets a JVM build that knows how to translate bytecode into native instructions for that OS and CPU architecture.
  • In practice, this works extremely well for business logic and pure computation. A Spring Boot service compiled on a developer’s macOS laptop will run identically on a Linux production server without recompilation. This is not theoretical — companies like Netflix and LinkedIn deploy the same JARs across thousands of heterogeneous nodes.
  • Where the promise breaks down: anything involving native code, file system paths, or OS-specific behavior. If your application uses JNI (Java Native Interface) to call a C library, that C library must be compiled for each target platform separately. File path separators differ (/ vs \), case sensitivity varies across file systems, and line endings differ. Socket options, process management, and signal handling have subtle platform differences. The JVM abstracts most of this, but edge cases leak through.
  • The less obvious limit is performance characteristics. The same bytecode runs on all platforms, but JIT compilation behavior differs by JVM implementation and hardware. A method that gets aggressively optimized by the C2 compiler on x86-64 Linux might behave differently on ARM-based Graviton instances on AWS. You get correctness portability, but not performance portability. This is why serious production deployments benchmark on the exact hardware and JVM version they will run in production.
Follow-up: If bytecode is platform-agnostic, why do JDK distributions like Corretto or Temurin ship separate downloads for each OS and architecture?
  • The JDK distribution itself contains native code — the JVM runtime (libjvm.so or jvm.dll), the JIT compilers (C1 and C2), the garbage collector implementations, and the native methods backing the standard library (file I/O, networking, cryptography). These must be compiled for each target platform. The bytecode your application produces is portable, but the engine that runs it is not.
  • Additionally, each platform’s JDK bundles platform-specific optimizations. The x86-64 build uses SSE/AVX instructions for certain intrinsics (like Arrays.sort or String.equals), while the AArch64 build uses NEON instructions. The GC’s memory management uses OS-specific system calls (mmap on Linux, VirtualAlloc on Windows).
  • This is why containerized deployments are so popular in the Java ecosystem — you bundle a specific JDK distribution in your Docker image and guarantee the exact same JVM binary runs everywhere, eliminating even the JVM-level platform variance.
Strong Answer:
  • The first and most impactful risk is removed or encapsulated internal APIs. Java 9 introduced the module system (JPMS), which strongly encapsulates sun.misc.* and com.sun.* packages. Code that used sun.misc.Unsafe for off-heap memory, sun.misc.BASE64Encoder, or reflective access to internal JDK classes will break. The --illegal-access flag that provided a grace period was removed entirely in Java 17. I have seen migrations stall for months because a single transitive dependency deep in the classpath relied on internal APIs.
  • The second risk is library and framework compatibility. Older versions of Spring, Hibernate, Lombok, Mockito, and bytecode manipulation libraries like ASM, ByteBuddy, and cglib had to be updated to work with the module system and changed class file formats. The practical approach is: update all dependencies first while still on Java 8, get tests passing, then switch the JDK. Never change two things at once.
  • The third risk is garbage collector behavior changes. Java 8 defaults to Parallel GC; Java 17+ defaults to G1GC. Applications tuned with specific GC flags for Parallel GC (-XX:+UseParallelGC, -XX:ParallelGCThreads, etc.) may see different pause characteristics on G1. The good news is that G1 is usually better out of the box, but applications with carefully tuned GC configurations need re-benchmarking.
  • My migration plan: (1) update all dependencies on Java 8 first, (2) compile on Java 11 with --illegal-access=warn to identify all illegal reflective access, (3) fix violations and test, (4) jump to Java 17 and then 21, running full regression suites at each step, (5) re-benchmark GC behavior and tune if needed. I would not jump directly from 8 to 21 — the intermediate LTS versions (11, 17) serve as stable checkpoints.
Follow-up: You mentioned the module system (JPMS). In practice, how many production applications actually use modules versus just running on the classpath?
  • The honest answer is that the vast majority of production applications still run on the classpath, not as modular applications. The module system was designed for the JDK itself (to modularize rt.jar) and for library authors who want to enforce strong encapsulation. Application developers at most companies found the migration cost too high and the benefits too marginal for typical business applications.
  • Where modules shine: libraries that want to hide internal packages from consumers (Guava, Jackson, Netty have all adopted modules), and applications that want to create minimal custom JVM runtimes using jlink (useful for CLI tools and microservices where a 30MB custom runtime replaces a 300MB full JDK).
  • The pragmatic stance: do not force-modularize your application during a migration. Add module-info.java to libraries you publish, but for services and applications, running on the classpath with automatic modules is perfectly fine and avoids a huge class of migration headaches.
Strong Answer:
  • Java’s backward compatibility guarantee means that code compiled with Java 1.0 in 1996 still compiles and runs on Java 21 in 2024. This is not an accident — it is a deliberate design contract that Sun (and later Oracle) committed to because Java’s primary market is enterprise systems with 10-20 year lifespans. Banks, insurance companies, and government agencies cannot rewrite millions of lines of code every time a language version ships. This guarantee is why enterprises trust Java in a way they do not trust languages with frequent breaking changes.
  • The cost is accumulated design debt. The most visible example is generics. Java added generics in Java 5 via type erasure — at runtime, a List<String> and a List<Integer> are the same raw List type. This was done to maintain backward compatibility with pre-generics bytecode. The consequence is that you cannot do new T(), cannot create generic arrays (new T[]), and cannot distinguish generic types via reflection at runtime. C# added reified generics because they were willing to break binary compatibility; Java was not.
  • Other costs include: the continued existence of java.util.Date and Calendar (terrible APIs that cannot be removed because too much code depends on them), checked exceptions (which most modern languages reject but Java cannot remove), and the primitive/wrapper dichotomy (int vs Integer) that adds complexity and boxing overhead. Project Valhalla aims to fix the last one with value types, but it has been in development for years precisely because it must not break existing code.
  • The way I think about it: backward compatibility is an investment that pays compound interest over decades. Each individual instance of design debt seems small, but the cumulative benefit of “your code never breaks on upgrade” is why Java powers more production systems than any other language. The trade-off is worth it for the ecosystem, even if individual language designers wish they could break free.
Follow-up: If removing features is off the table, how does Java actually evolve and deprecate problematic APIs?
  • Java uses a multi-stage deprecation process. First, an API is marked @Deprecated with a forRemoval=true flag (added in Java 9). This generates compiler warnings. Then, after several major releases with the warning in place, the API is actually removed. The Applet API, SecurityManager, and Finalization all followed this path. But the timeline is measured in years, not months — SecurityManager was deprecated for removal in Java 17 and is being removed in Java 24.
  • For internal APIs (like sun.misc.Unsafe), the module system provides a mechanism to encapsulate them behind module boundaries. Replacement APIs are provided first (like java.lang.invoke.VarHandle and the Foreign Function and Memory API), giving the ecosystem time to migrate before access is fully cut off.
  • Preview features (introduced in Java 12) let Java ship experimental features that can change between releases without breaking the backward compatibility contract. Records, pattern matching, and virtual threads all went through multiple preview rounds before being finalized. This lets the language evolve quickly while maintaining stability.
Strong Answer:
  • HotSpot is the standard OpenJDK JVM that ships with every mainstream JDK distribution. It uses the C1 (client) and C2 (server) JIT compilers with tiered compilation. It is battle-tested across billions of production deployments, has predictable behavior, and every Java monitoring tool, profiler, and APM agent is designed for it. For long-running server applications — which is the vast majority of Java in production — HotSpot is the default and usually the right choice.
  • GraalVM is an alternative runtime from Oracle Labs that includes the Graal JIT compiler (written in Java, replacing C2) and, more importantly, Native Image — an ahead-of-time (AOT) compiler that produces standalone executables. The Graal JIT can produce better peak throughput than C2 for certain workloads (particularly those heavy on polymorphic calls and partial escape analysis), but its compilation is slower, which means longer warmup times.
  • Native Image is the killer feature. It compiles your Java application to a native binary with millisecond startup time and fixed memory footprint. This is transformative for serverless functions (AWS Lambda), CLI tools, and microservices in Kubernetes where fast scale-up matters. Quarkus and Micronaut frameworks are built specifically to work well with Native Image.
  • The trade-offs of Native Image are significant: no dynamic class loading, limited reflection (must be configured at build time), no runtime bytecode generation (breaks some libraries like cglib-based proxies), and longer build times (minutes, not seconds). Peak throughput is often lower than HotSpot because AOT compilation cannot optimize as aggressively as JIT compilation that observes actual runtime behavior. Spring Boot has invested heavily in Native Image support, but not all Spring features work natively.
  • My decision framework: use HotSpot for long-running servers where warmup time is amortized over hours of uptime and peak throughput matters. Use GraalVM Native Image for serverless functions, CLI tools, and microservices that need sub-second startup and minimal memory footprint. For most enterprise backends, HotSpot remains the better choice.
Follow-up: You said JIT can optimize more aggressively than AOT because it observes runtime behavior. Give a specific example of an optimization JIT can do that AOT cannot.
  • Speculative devirtualization is the classic example. When the JIT compiler observes that a virtual method call site always dispatches to the same concrete implementation (a “monomorphic” call site), it replaces the vtable lookup with a direct call and inlines the method body. It inserts a guard (“if this is not the expected type, deoptimize and fall back to the slow path”), but for the common case, the overhead drops to near zero. AOT compilation cannot know which concrete types will appear at a call site, so it must always go through the vtable.
  • Profile-guided optimizations like branch prediction hints are another example. The JIT observes which branch of an if statement is taken 99% of the time and reorders the machine code to put the hot path first, improving instruction cache utilization. AOT can do this with PGO (profile-guided optimization) data collected from test runs, but that is an extra build step and the profile may not match production behavior.
  • On-stack replacement (OSR) is unique to JIT. If a long-running loop is discovered to be hot mid-execution, the JIT can compile it and replace the currently executing interpreted code with the optimized version without waiting for the method to exit and be re-entered. AOT has no concept of this — all code is compiled before execution begins.