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
Enterprise Grade
Multithreading
Modern Evolution
Course Roadmap
We will peel back the layers of abstraction to understand how Java really works.Fundamentals
Object-Oriented Programming
Collections Framework
Concurrency
Modern Java
Prerequisites
- Basic programming knowledge (variables, loops, functions in any language).
- JDK 17 or higher installed (
java -versionin 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).
The Java Philosophy
“Java is C++ without the guns, knives, and clubs.” — James GoslingJava 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.
Common Pitfalls Before You Start
Confusing JDK, JRE, and JVM
Confusing JDK, JRE, and JVM
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.Choosing the wrong JDK distribution
Choosing the wrong JDK distribution
Ignoring the build tool
Ignoring the build tool
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
Explain how the JVM achieves 'Write Once, Run Anywhere.' What are the real-world limits of this promise?
Explain how the JVM achieves 'Write Once, Run Anywhere.' What are the real-world limits of this promise?
- Java source compiles to bytecode (
.classfiles) viajavac. 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.
- The JDK distribution itself contains native code — the JVM runtime (
libjvm.soorjvm.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.sortorString.equals), while the AArch64 build uses NEON instructions. The GC’s memory management uses OS-specific system calls (mmapon Linux,VirtualAllocon 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.
A team is migrating from Java 8 to Java 21. What are the biggest technical risks and how would you plan the migration?
A team is migrating from Java 8 to Java 21. What are the biggest technical risks and how would you plan the migration?
- The first and most impactful risk is removed or encapsulated internal APIs. Java 9 introduced the module system (JPMS), which strongly encapsulates
sun.misc.*andcom.sun.*packages. Code that usedsun.misc.Unsafefor off-heap memory,sun.misc.BASE64Encoder, or reflective access to internal JDK classes will break. The--illegal-accessflag 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=warnto 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.
- 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.javato 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.
Why does Java maintain backward compatibility so aggressively, and what are the costs of that decision?
Why does Java maintain backward compatibility so aggressively, and what are the costs of that decision?
- 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 aList<Integer>are the same rawListtype. This was done to maintain backward compatibility with pre-generics bytecode. The consequence is that you cannot donew 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.DateandCalendar(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 (intvsInteger) 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.
- Java uses a multi-stage deprecation process. First, an API is marked
@Deprecatedwith aforRemoval=trueflag (added in Java 9). This generates compiler warnings. Then, after several major releases with the warning in place, the API is actually removed. TheAppletAPI,SecurityManager, andFinalizationall followed this path. But the timeline is measured in years, not months —SecurityManagerwas 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 (likejava.lang.invoke.VarHandleand 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.
Compare the HotSpot JVM with GraalVM. When would you choose one over the other in production?
Compare the HotSpot JVM with GraalVM. When would you choose one over the other in production?
- 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.
- 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
ifstatement 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.