The JVM Ecosystem: Maven Central, Spring, and What's Standard

You know the Python landscape by heart: pip installs from PyPI, venv isolates environments, the standard library covers files/dates/JSON/HTTP, and when you need a web service you reach for Django or Flask, pytest for tests, and the logging module when things get serious. None of that knowledge transfers name-for-name — but every one of those needs has a Java answer, and this post is the map.

Here's the honest version of the map, in one paragraph. Java's standard library ships with the JDK — collections, file I/O, dates, and even an HTTP client come free with every install. Maven Central is the PyPI of the JVM: a public artifact repository where every library has exact coordinates, and you download them with a build tool instead of pip. The build tool is usually Maven or Gradle (think pip + setuptools/pyproject.toml combined). A JAR is the distribution format — a zip with a manifest, roughly a wheel. And Spring Boot is the closest thing to Django: an opinionated framework that wires everything together — but unlike Django, it is not part of the standard platform, and plenty of real Java runs without it.

Lab honesty, stated up front. Everything below marked ran in this lab was executed here on JDK 21: the stdlib tour, the Gson JSON download-and-parse, the jar tf listing, the classpath demo, the JUnit 5 test run, and the SLF4J/Logback logging demo. Maven, Gradle, and Spring are not installed on this machine, so every pom.xml, build.gradle, and Spring snippet is shown as source only and never executed — there is no Maven/Gradle/Spring output to show. Two JDK features also weren't run: java.net.http and jshell, because this sandbox's firewall blocks Java's direct socket connections (its loopback engine included). They're marked where they appear. Nothing in this post is invented output.

1. The JDK as standard library

In Python, "batteries included" means the stdlib: json, datetime, pathlib, urllib. In Java, the equivalent promise lives in java.base — the module that is always on the classpath of every Java program. Installing the JDK gives you:

NeedPython stdlibjava.base (always available)
Collectionslist, dict, collectionsjava.util — List, Map, Set, streams
Dates & timesdatetimejava.time — LocalDate, ZonedDateTime, Duration
Filespathlib, osjava.nio.file — Path, Files
HTTP clienturllib / http.clientjava.net.http — HttpClient (since Java 11)
Regexrejava.util.regex
JSONjsonnot in the JDK — you use Gson or Jackson (section 5)
Threadsthreadingjava.lang.Thread, and virtual threads since Java 21
The one deliberate gap: JSON. Python ships json; the JDK doesn't ship a JSON parser, because the ecosystem converged on external libraries (Gson, Jackson) long before the JDK considered adding one. This is your first taste of how Java works: a small platform core plus a rich public repository of libraries. The stdlib tour below is real output from this lab:
$ export PATH=$HOME/workspace/tooling/jdk21/bin:$PATH
$ javac StdLibTour.java && java StdLibTour
word counts: {arjun=1, meera=1, priya=2}
java is 30 years old
formatted : 06 Oct 2026
file read : hello from nio

The program behind it — streams for the word count, java.time for dates, NIO one-liners for file I/O:

import java.time.LocalDate;
import java.time.Period;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
import java.nio.file.Files;
import java.nio.file.Path;

public class StdLibTour {
    public static void main(String[] args) throws Exception {
        // collections: immutable list + stream word count into a sorted map
        List<String> names = List.of("priya", "arjun", "meera", "priya");
        Map<String, Long> counts = names.stream()
            .collect(Collectors.groupingBy(n -> n, TreeMap::new, Collectors.counting()));
        System.out.println("word counts: " + counts);

        // java.time: no more java.util.Date arithmetic
        LocalDate today = LocalDate.of(2026, 10, 6);
        LocalDate released = LocalDate.of(1996, 1, 23);
        System.out.println("java is " + Period.between(released, today).getYears() + " years old");
        System.out.println("formatted : " + today.format(DateTimeFormatter.ofPattern("dd MMM yyyy")));

        // NIO: one-liner file write/read/delete
        Path f = Files.createTempFile("stdlib", ".txt");
        Files.writeString(f, "hello from nio");
        System.out.println("file read : " + Files.readString(f));
        Files.delete(f);
    }
}

Three things a Python dev should notice. First, List.of(...) makes an immutable list — the mutable default you're used to is new ArrayList<>(). Second, java.time is the fix for the old java.util.Date mess: dates are immutable value types with sane formatting, like datetime done right. Third, Files.readString is the pathlib-equivalent one-liner — the NIO package is where modern Java file code lives, not the legacy java.io.File.

jshell: the REPL teaser

Python has python3 at the terminal; Java has jshell — a real REPL that ships with the JDK. You type expressions, it evaluates them, no main method required. The canonical first session looks like this:

$ jshell
|  Welcome to JShell -- Version 21.0.12.1
|  For an introduction type: /help intro

jshell> var names = List.of("priya", "arjun");
names ==> [priya, arjun]

jshell> names.stream().map(String::toUpperCase).toList()
$2 ==> [PRIYA, ARJUN]

jshell> /exit
|  Goodbye
Not run in this lab. The transcript above is the standard jshell interaction, but this sandbox's firewall blocks jshell's loopback execution engine, so it couldn't launch here. Run the three lines above on your own machine — jshell is the fastest way to sanity-check a JDK API before committing it to a file.

2. Maven Central: the PyPI of the JVM

PyPI holds Python packages; Maven Central (repo1.maven.org/maven2) holds JVM artifacts — Java, Kotlin, Scala, anything that compiles to bytecode. Same role, different unit of distribution: PyPI serves packages, Maven Central serves artifacts identified by three coordinates:

groupId:artifactId:version
com.google.code.gson:gson:2.10.1
  • groupId — the namespace, almost always a reversed domain: com.google.code.gson, org.springframework.boot, com.fasterxml.jackson.core. This is PyPI's author/organization slot, but enforced by domain ownership.
  • artifactId — the library name: gson, spring-boot, jackson-databind.
  • version — the exact release: 2.10.1. No floating versions in serious use — you pin, and the build is reproducible.

Maven Central is browsable over plain HTTPS, and every artifact has machine-readable metadata. This is a real response, fetched in this lab:

$ curl -s https://repo1.maven.org/maven2/com/google/code/gson/gson/maven-metadata.xml | head -12
<?xml version="1.0" encoding="UTF-8"?>
<metadata>
  <groupId>com.google.code.gson</groupId>
  <artifactId>gson</artifactId>
  <versioning>
    <latest>2.14.0</latest>
    <release>2.14.0</release>
    <versions>
      <version>1.1</version>
      <version>1.4</version>
      <version>1.5</version>
...

The URL pattern is the whole addressing scheme: /maven2/<groupId with dots as slashes>/<artifactId>/<version>/<artifactId>-<version>.jar. So:

$ curl -O https://repo1.maven.org/maven2/com/google/code/gson/gson/2.10.1/gson-2.10.1.jar
$ ls -la gson-2.10.1.jar
-rw-rw---- 1 root nogroup 283367 Oct  6 19:06 gson-2.10.1.jar

That's the entire "install" step — no installer, no pip, just a file. What you do with the file is the next two sections. (Some other versions I verified against Maven Central metadata while writing this: jackson-databind 2.22.3, logback-classic 1.6.5, HikariCP 7.1.0 — checked October 2026, and they will have moved on by the time you read this, which is exactly why you pin versions.)

Why coordinates instead of names? Two libraries can share an artifactId under different groupIds (org.json:json vs com.vaadin.external.google:android-json), and two versions of one library coexist forever on Central — nothing is ever deleted. Coordinates are the only unambiguous handle, and every build tool resolves them deterministically. It's stricter than PyPI, and that strictness is load-bearing: reproducible builds depend on it.

3. Dependency management: Maven vs Gradle

Downloading jars by hand stops scaling at about two dependencies. Java's answer to requirements.txt / pyproject.toml is a build tool that declares dependencies, downloads them from Maven Central, and wires them onto the classpath. Two dominate, and you'll meet both:

MavenGradle
Config filepom.xml (XML)build.gradle / build.gradle.kts (Groovy/Kotlin DSL)
PhilosophyConvention over configuration: fixed lifecycle (compile → test → package)Task graph: you compose tasks, more flexible, more rope
Where you'll see itMost enterprises, Spring Boot's defaultAndroid, large polyglot builds, Spring Framework itself
Python analogypip + setup.py with strong opinionspip + a programmable Makefile

A real pom.xml — this is what the Gson section of this post looks like once you stop downloading jars by hand:

<!-- pom.xml — NOT RUN IN THIS LAB: Maven is not installed on this machine -->
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
             https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example</groupId>      <!-- your namespace (reverse domain) -->
  <artifactId>cityops-demo</artifactId> <!-- your project name -->
  <version>1.0.0</version>            <!-- your release -->
  <properties>
    <maven.compiler.release>21</maven.compiler.release>
  </properties>
  <dependencies>
    <dependency>                        <!-- com.google.code.gson:gson:2.10.1 -->
      <groupId>com.google.code.gson</groupId>
      <artifactId>gson</artifactId>
      <version>2.10.1</version>
    </dependency>
    <dependency>                        <!-- com.fasterxml.jackson.core:jackson-databind:2.22.3 -->
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.22.3</version>
    </dependency>
  </dependencies>
</project>

And the Gradle equivalent — same dependency, one line:

// build.gradle — NOT RUN IN THIS LAB: Gradle is not installed on this machine
plugins { id 'java' }
java { toolchain { languageVersion = JavaLanguageVersion.of(21) } }
repositories { mavenCentral() }                       // like pip's default index
dependencies {
    implementation 'com.google.code.gson:gson:2.10.1'  // group:artifact:version
    testImplementation 'org.junit.jupiter:junit-jupiter:5.13.4'
}
Neither tool ran here. Both snippets are accurate to each tool's syntax, but I could not execute mvn or gradle in this lab — run them on your own machine to see the dependency-download output. Note the Gradle snippet pins JUnit 5.13.4: I did not verify that exact version against Maven Central metadata while writing this, so check before you copy it.

Transitive dependencies and nearest-wins

pip resolves the whole tree and installs one version per package (its resolver backtracks to find a consistent set). Maven does something simpler and more predictable: it walks the dependency tree breadth-first and the nearest declaration wins — the first version found at the shallowest depth is the one you get, no backtracking.

myapp
├── com.fasterxml.jackson.core:jackson-databind:2.22.3   (direct, depth 1)
└── com.example:some-lib:1.0                             (direct, depth 1)
    └── com.fasterxml.jackson.core:jackson-databind:2.17.0 (transitive, depth 2)

Maven picks 2.22.3 — the depth-1 declaration beats the depth-2 transitive one. If two transitive versions sit at the same depth, the one declared first wins. This rule is why Java dependency conflicts feel different from Python's: instead of a resolver error, you silently get one version, and the loser is simply absent. The defense is mvn dependency:tree — the Java equivalent of pip freeze plus the "why" — which shows exactly which version won and which path it came from. When something breaks after adding a dependency, the tree is the first place you look.

Ecosystem principle: pin versions, read the tree. Maven Central never deletes anything and Maven resolves conflicts by proximity, not by correctness. Exact versions in the build file plus a periodic look at the dependency tree is the Java equivalent of a lockfile discipline — and Spring Boot's dependency-management BOM (section 7) is the ecosystem's way of doing the pinning for you.

4. Worked example: your first Maven Central download (the "pip install + import" moment)

In Python the moment goes pip install requests then import requests. Here is the Java version, end to end, executed in this lab. Download a JSON library from Maven Central, put it on the classpath, parse JSON:

$ curl -O https://repo1.maven.org/maven2/com/google/code/gson/gson/2.10.1/gson-2.10.1.jar
$ cat JsonDemo.java
import com.google.gson.Gson;
import java.util.List;
import java.util.Map;

public class JsonDemo {
    static class Person {
        String name;
        int age;
        List<String> skills;
        @Override public String toString() {
            return name + " (" + age + ") knows " + skills;
        }
    }
    public static void main(String[] args) {
        String json = "{\"name\": \"Priya\", \"age\": 29, \"skills\": [\"python\", \"java\", \"sql\"]}";
        Gson gson = new Gson();

        // Untyped: JSON object -> Map<String, Object>
        Map<String, Object> person = gson.fromJson(json, Map.class);
        System.out.println("name  = " + person.get("name"));
        System.out.println("age   = " + person.get("age"));
        System.out.println("skills= " + person.get("skills"));
        System.out.println("class = " + person.getClass().getName());

        // Typed: JSON -> POJO (plain old Java object)
        Person p = gson.fromJson(json, Person.class);
        System.out.println("typed : " + p);
        System.out.println("round-trip: " + gson.toJson(p));
    }
}

The compile-and-run — the only new flag is -cp, "put this jar where the compiler and JVM can find classes":

$ javac -cp gson-2.10.1.jar JsonDemo.java
$ java -cp .:gson-2.10.1.jar JsonDemo
name  = Priya
age   = 29.0
skills= [python, java, sql]
class = com.google.gson.internal.LinkedTreeMap
typed : Priya (29) knows [python, java, sql]
round-trip: {"name":"Priya","age":29,"skills":["python","java","sql"]}

Real output, ran in this lab. Now read it like a Python dev:

  • age = 29.0 — the untyped parse turned JSON's 29 into a Double, the way json.loads would hand you a Python float. Gson's untyped mode maps every JSON number to Double, just as Python's json maps them to int/float by shape. If you want the real types, parse into a typed class — the Person POJO, where age is a genuine int.
  • class = com.google.gson.internal.LinkedTreeMap — the map you get back is Gson's own ordered-map implementation, not java.util.HashMap. It behaves like a map; the concrete class is an implementation detail you're not supposed to depend on.
  • The round-trip is clean: toJson of the POJO reproduces the input document. Serialization and deserialization are symmetric, like json.dumps/json.loads with a dataclass in the middle.

That's the whole pattern you'll repeat for the rest of your Java career: find coordinates, get the jar (by hand today, via Maven/Gradle tomorrow), add it to the classpath, import the package, use it. Everything from here on is refinement of that loop.

5. What's standard: the libraries every Java developer is expected to know

Python has its de-facto standards — requests for HTTP, pytest for tests, the logging module, SQLAlchemy for databases. Java's equivalents aren't in the JDK; they're Maven Central artifacts so universal that job interviews assume them. Each one below either ran in this lab or is marked.

Logging: SLF4J + Logback (ran in this lab)

The standard is a two-layer design Python never quite adopted: your code logs against the SLF4J API (org.slf4j), and at deploy time you plug in a backend — almost always Logback. Libraries log to the API, applications choose the backend, and nobody's logging framework fights anyone else's. The Python equivalent would be if every library logged to the stdlib logging module and you only ever configured handlers — which, to be fair, is exactly what good Python libraries already do.

$ curl -O https://repo1.maven.org/maven2/org/slf4j/slf4j-api/2.0.16/slf4j-api-2.0.16.jar
$ curl -O https://repo1.maven.org/maven2/ch/qos/logback/logback-core/1.5.20/logback-core-1.5.20.jar
$ curl -O https://repo1.maven.org/maven2/ch/qos/logback/logback-classic/1.5.20/logback-classic-1.5.20.jar
$ cat LogDemo.java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LogDemo {
    private static final Logger log = LoggerFactory.getLogger(LogDemo.class);
    public static void main(String[] args) {
        String user = "priya";
        log.debug("this debug line is hidden once the root level is INFO");
        log.info("user {} logged in", user);
        log.warn("disk usage at {}%", 87);
        try {
            Integer.parseInt("not-a-number");
        } catch (NumberFormatException e) {
            log.error("failed to parse port", e);
        }
    }
}
$ javac -cp slf4j-api-2.0.16.jar:logback-core-1.5.20.jar:logback-classic-1.5.20.jar LogDemo.java

Without any configuration, Logback's default logs everything at DEBUG and above. Add a logback.xml on the classpath to set the root level to INFO — the Java equivalent of logging.basicConfig(level=logging.INFO):

$ cat logback.xml
<configuration>
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{HH:mm:ss} %-5level %logger{0} -- %msg%n</pattern>
    </encoder>
  </appender>
  <root level="INFO">
    <appender-ref ref="STDOUT"/>
  </root>
</configuration>
$ java -cp .:slf4j-api-2.0.16.jar:logback-core-1.5.20.jar:logback-classic-1.5.20.jar LogDemo
19:09:01 INFO  LogDemo -- user priya logged in
19:09:01 WARN  LogDemo -- disk usage at 87%
19:09:01 ERROR LogDemo -- failed to parse port
java.lang.NumberFormatException: For input string: "not-a-number"
        at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
        at java.base/java.lang.Integer.parseInt(Integer.java:662)
        at java.base/java.lang.Integer.parseInt(Integer.java:778)
        at LogDemo.main(LogDemo.java:12)

Real output, ran in this lab — note the debug line vanished once the root level became INFO, and the {} placeholders work like logging's %s formatting without the string-concatenation cost. Two conventions to absorb: one Logger per class (named after the class), and {} placeholders instead of string concatenation in hot paths.

Testing: JUnit 5 (ran in this lab)

JUnit is Java's pytest: the test framework everything else integrates with. JUnit 5 ("Jupiter") tests are annotated methods with assertions, and the platform console launcher runs them from the command line without any build tool:

$ curl -O https://repo1.maven.org/maven2/org/junit/platform/junit-platform-console-standalone/1.11.3/junit-platform-console-standalone-1.11.3.jar
$ cat StringUtils.java
public class StringUtils {
    /** Uppercases s; null stays null (no NullPointerException). */
    public static String upper(String s) {
        return s == null ? null : s.toUpperCase();
    }
}
$ cat StringUtilsTest.java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;

public class StringUtilsTest {
    @Test
    void uppercasesWord() {
        assertEquals("HELLO", StringUtils.upper("hello"));
    }
    @Test
    void uppercasesNullSafely() {
        assertNull(StringUtils.upper(null));
    }
}
$ javac -cp junit-platform-console-standalone-1.11.3.jar StringUtils.java StringUtilsTest.java
$ java -jar junit-platform-console-standalone-1.11.3.jar execute --disable-ansi-colors \
      -cp . --select-class StringUtilsTest

Thanks for using JUnit! Support its development at https://junit.org/sponsoring

?
?? JUnit Platform Suite ?
?? JUnit Jupiter ?
?  ?? StringUtilsTest ?
?     ?? uppercasesWord() ?
?     ?? uppercasesNullSafely() ?
?? JUnit Vintage ?

Test run finished after 608 ms
[         2 tests found           ]
[         2 tests started         ]
[         2 tests successful      ]
[         0 tests failed          ]

Real output, ran in this lab (the ? glyphs are the console launcher's tree rendering with colors disabled). The mapping from pytest is direct: @Test is your test function marker, assertEquals/assertNull are the assertion helpers, and the standalone console jar is the pytest command — in real projects Maven or Gradle runs these for you via the Surefire plugin, and mvn test becomes your pytest.

JSON: Jackson (the default) and Gson (the simple one)

You already ran Gson in section 4. The other name you'll see everywhere is Jackson (com.fasterxml.jackson.core:jackson-databind) — it's the default JSON engine inside Spring Boot and most enterprises, faster and more configurable than Gson, with a bigger API surface to match. The rule of thumb: Gson when you want JSON parsing in ten lines (scripts, small services), Jackson when the framework or the team already chose it. Both live on Maven Central; both are one dependency line away once you have a build tool.

HTTP client: java.net.http (pure JDK — shown as source only)

Java 11 added a modern HTTP client to the JDK itself — no dependency needed, the equivalent of urllib but actually pleasant:

// Shown as source only — NOT RUN in this lab: this sandbox's firewall
// blocks Java's direct socket connections, so client.send() cannot
// complete here. It runs normally on any standard machine or CI runner.
import java.net.URI;
import java.net.http.*;
import java.time.Duration;

HttpClient client = HttpClient.newHttpClient();
HttpRequest req = HttpRequest.newBuilder(URI.create("https://example.com"))
    .timeout(Duration.ofSeconds(10))
    .build();
HttpResponse<String> resp = client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(resp.statusCode());   // 200
System.out.println(resp.body().length());

For anything beyond simple calls — connection pooling, retries, JSON binding — the ecosystem reaches for libraries (Spring's RestClient/WebClient, or OkHttp), the way Python reaches past urllib for requests/httpx. But knowing the JDK ships a working client matters: it means HTTP is a platform capability, not a framework feature.

Database connections: HikariCP (mention)

Python has SQLAlchemy's connection pool; Java's de-facto pool is HikariCP (com.zaxxer:HikariCP) — small, fast, and the default pool Spring Boot configures. You rarely touch it directly: you declare a DataSource, the pool sits underneath, and the framework wires it. I'm not running a database in this lab, so this one stays a name to recognize: when a Java job mentions "the pool," they mean HikariCP, and when a Spring Boot app slows down under load, the pool settings are the first config to check.

6. Spring Boot: "the Django of Java," said carefully

Every Python dev asks this question, so let's answer it precisely. Django is batteries-included: one install gives you routing, ORM, templates, admin, auth — a coherent whole with one way of doing things. Spring Boot is the closest Java equivalent, but the analogy has sharp edges worth knowing:

Django conceptSpring Boot equivalentThe sharp edge
One framework installStarters — curated dependency bundles like spring-boot-starter-webBoot is not one thing; it's dozens of opt-in starters over the Spring Framework. You compose your stack, Django hands you its stack.
manage.py runserverEmbedded server — Tomcat/Jetty runs inside your app; java -jar app.jar is the whole deployThis is genuinely better than Django's dev-server/production split: the server you test is the server you ship.
Settings moduleAuto-configuration — Boot inspects your classpath and wires beans accordinglyConvention over configuration taken further than Django. When it guesses wrong, you debug with --debug to see the auto-config report.
ORM (Django models)Spring Data JPA over HibernateSame idea (entities, repositories), more explicit mapping, more knobs.
pip install djangoNot in the JDK. It's a Maven Central dependency like any other.The biggest edge: nothing about Spring is "standard Java." Plain Java + libraries is a legitimate, common architecture.

What a minimal Spring Boot REST endpoint looks like (compare with the plain-JDK HTTP server in this track's earlier posts — same HTTP, radically less plumbing):

// NOT RUN IN THIS LAB — Spring Boot is not installed on this machine.
// Shown so you can read Spring code when you meet it in the wild.
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;

@RestController
class HelloController {
    @GetMapping("/hello")
    Map<String, String> hello() {
        return Map.of("message", "hello from spring boot");
    }
}

And the Maven coordinates that make it real — the starter pulls the framework, the embedded Tomcat, Jackson for JSON, and validation, all version-aligned by Boot's dependency-management BOM (that's the "bill of materials" doing the version-pinning from section 3 for you):

<!-- NOT RUN IN THIS LAB: Maven is not installed on this machine -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
  <!-- no version: inherited from spring-boot-starter-parent's BOM -->
</dependency>
When do you actually need Spring Boot? Reach for it when you're building a multi-endpoint web service inside an organization that already speaks Spring — the ecosystem (Spring Data, Spring Security, Actuator health checks) pays off fast at that scale. Skip it when you're writing a library, a CLI tool, a data pipeline stage, or learning the JVM itself: plain Java plus the libraries from section 5 is simpler, starts faster, and teaches you the mechanisms the framework would otherwise hide. The fastest way to start a real Boot project is start.spring.io — pick Web + your JDK version, download the zip, and you have a running app skeleton.

7. JAR files and the classpath: wheels, PYTHONPATH, and the -cp flag

A JAR is a zip file with a META-INF/MANIFEST.MF — Java's wheel. It holds compiled .class files in package-directory layout plus metadata. You already downloaded one; here's what's inside, from this lab:

$ jar tf gson-2.10.1.jar | head -20
META-INF/
META-INF/MANIFEST.MF
com/
com/google/
com/google/gson/
com/google/gson/stream/
com/google/gson/reflect/
...
$ jar tf gson-2.10.1.jar | wc -l
238
$ unzip -p gson-2.10.1.jar META-INF/MANIFEST.MF | head -8
Manifest-Version: 1.0
Created-By: 11.0.16.1 (Azul Systems, Inc.)
Bundle-Name: Gson
Bundle-SymbolicName: com.google.gson
Bundle-Version: 2.10.1
Export-Package: com.google.gson;uses:="com.google.gson.reflect,com.google.gson.stream";version="2.10.1",...

238 entries, mostly com/google/gson/**/*.class — the package name is the directory path, exactly like a Python package on disk. The manifest even records the OSGi bundle metadata (that's the Bundle-* lines — enterprise Java's module story before JPMS, section 8).

Classpath: PYTHONPATH with no forgiveness

The classpath is Java's PYTHONPATH: the ordered list of directories and jars the JVM searches for classes. You've been using it all post (-cp gson-2.10.1.jar). Two differences from Python hurt newcomers:

  1. It is explicit and empty by default. Python puts the script's directory on sys.path automatically; java searches only what you list. Forget a jar and you get ClassNotFoundException, not a helpful hint.
  2. Entries are searched in order, first hit wins. Two jars containing the same class? The classpath order decides silently — this is the runtime cousin of Maven's nearest-wins.

Real demo from this lab — two classes compiled into separate directories, run together and then with one directory missing:

$ find cp-demo/classes -name '*.class'
cp-demo/classes/main/Main.class
cp-demo/classes/util/util/Util.class
$ java -cp cp-demo/classes/main:cp-demo/classes/util Main
CLASSPATH WORKS!
$ java -cp cp-demo/classes/main Main
Exception in thread "main" java.lang.NoClassDefFoundError: util/Util
        at Main.main(Main.java:4)
Caused by: java.lang.ClassNotFoundException: util.Util
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:526)
        ... 1 more

Real output, ran in this lab. On Linux/macOS the separator is : (Windows uses ;). That NoClassDefFoundError — thrown because the class was present at compile time but missing at run time — is the single most common classpath error in Java, and now you've seen its exact shape. Maven and Gradle exist largely to make this flag unnecessary: they compute the full -cp from your dependency tree and hand it to the JVM for you.

8. Modules (JPMS): the short version

Since Java 9, the JDK itself is modularized, and your code can be too. A module-info.java declares what a module requires and what it exports:

// module-info.java — snippet, not compiled in this lab
module com.example.cityops {
    requires com.google.gson;          // Gson's module name
    requires java.net.http;           // JDK HTTP client module
    exports com.example.cityops.api;  // only this package is visible to others
}

Why it exists: strong encapsulation. Before modules, any public class in any jar was reachable from anywhere — libraries couldn't hide their internals (remember Gson's internal.LinkedTreeMap leaking into our output in section 4? modules are the language-level answer to exactly that). The JDK's own sun.misc.Unsafe era ended here: internal APIs became genuinely internal.

Why it's short in this post: most application developers meet JPMS as "the thing that makes the error message say module X does not export package Y" rather than something they author daily. Spring Boot apps historically ran on the classpath, not the module path. Know that it exists, know what requires/exports mean when you read them, and reach for it when you're publishing a library whose internals must stay internal.

9. LTS versions: which Java are we even talking about

Python has 3.9 → 3.13; Java has LTS (long-term support) releases every two years, with six-month feature releases in between that most teams skip. When Java developers say "modern Java," they mean the LTS line:

LTSReleasedWhy it matters
82014Lambdas, streams. Still running legacy systems; the "Python 2.7" of Java — you'll inherit it, don't start there.
112018var, the java.net.http client, nestmates. A common enterprise minimum.
172021Records, sealed classes, pattern matching for instanceof finalized. The most common baseline you'll meet.
212023Virtual threads, pattern matching for switch, sequenced collections, string templates (preview). The current recommended target — this whole track runs on 21.
252025Current LTS as of this writing. If you're starting a greenfield project today, 21 or 25.

Two practical consequences. First, target the LTS: libraries and employers specify a minimum Java version the way Python packages specify requires-python, and anything before 17 is legacy territory now. Second, virtual threads are why 21 matters — they give Java the "a thread per request, cheaply" model that Python's asyncio chased with syntax, but as plain blocking code the JVM schedules for you. The Concurrency & JVM track on this site goes deep; for now, just know the version number unlocked it.

10. The ecosystem map

Everything in this post, in one picture — solid boxes are things you ran or fetched in this lab, dashed boxes are source-only snippets (Maven/Gradle/Spring aren't installed here):

The JVM ecosystem — solid ran in this lab, dashed is source-only here JDK 21 java.base: collections java.time · NIO java.net.http · jshell javac · java · jar BUILD TOOLS Maven (pom.xml) Gradle (build.gradle) declare deps · compute the -cp for you MAVEN CENTRAL groupId:artifactId: version coordinates fetched by curl in this lab — real jars JAR zip + manifest 238 entries in gson-2.10.1.jar jar tf ran here STANDARD LIBRARIES Gson / Jackson — JSON SLF4J + Logback — logging JUnit 5 — testing HikariCP — connection pool all ran or fetched here SPRING BOOT "the Django of Java" starters · auto-config embedded server not in the JDK — a choice, not a given RUNNING IT -cp .:gson.jar java -jar app.jar NoClassDefFoundError: the classic classpath error, seen live The loop: find coordinates → get the jar → -cp → import → use. Build tools automate the middle two steps.
Ecosystem principle: the JDK is the platform, Maven Central is the library, the framework is a choice. Python bundles the framework decision into the language's culture (Django is Python web development for most teams). Java separates the three layers, and each layer has its own versioning, its own upgrade cadence, and its own failure modes. Once you see the layers, every Java stack trace, every pom.xml, and every "which version" argument reads like a map instead of a maze.

Field check

Reading the map isn't the same as walking it. Five exercises, all doable with just a JDK and curl:

  1. Repeat the centerpiece with Jackson. Download jackson-databind-2.22.3.jar (plus jackson-core and jackson-annotations — check the metadata URLs) from Maven Central and rewrite JsonDemo using ObjectMapper. You'll discover Jackson needs three jars where Gson needed one — that's transitive dependencies, and it's the reason build tools exist.
  2. Break the classpath on purpose. Rebuild the section-7 demo, then delete one -cp entry and predict the exact exception before you run it. Then put two different Gson versions on the classpath in both orders and observe which one wins.
  3. Read a real dependency tree. Open https://repo1.maven.org/maven2/org/springframework/boot/spring-boot-starter-web/ in a browser, pick a version, download its .pom, and count how many artifacts one "starter" transitively pulls in. That's what the BOM was pinning for you.
  4. Make a test fail. Add a third @Test to StringUtilsTest that asserts something false, run the console launcher, and read the failure output — assertion message, expected vs actual, and the summary counts. Knowing what red looks like is half of testing.
  5. Audit your Java version. Run java -version on your machine, find your version in the LTS table, and decide: if you were starting a service today, would you target 21 or 25, and what would stop you from using the other?

What's next

You now know where everything lives — the JDK, Maven Central, the standard libraries, and where Spring Boot fits. The last post in this bridge track turns to how you work with it daily: the IDE, the REPL, and the build pipeline that ties the ecosystem together.

Next: Tooling: IntelliJ, jshell, and the Build Pipeline

Continue: Java Learning Roadmap 2026

Comments

Popular posts from this blog

JSP Servlet Interview Questions For Freshers Series 1

Java Banking Finance Services and Insurance (BFSI) domain interview questions

Java program to check even or odd number