Tooling: IntelliJ, jshell, and the Build Pipeline

You've made it to the end of the bridge. Post 1 gave you the static-type mindset, post 2 the object model and collections, post 3 dependency management — and now the part nobody warns Python developers about: in Java, the toolchain is a bigger part of the job than the language.

In Python, the interpreter is the toolchain. You write app.py, run python app.py, and you're done. Packaging, docs, profiling, and the REPL are afterthoughts you bolt on later. In Java, the equivalent surface is a whole pipeline of small, sharp tools that ship with the JDK — plus an IDE culture and a build-pipeline culture that Python simply doesn't have. This post walks the full loop: edit → compile → run → debug, the REPL you didn't know Java had, the CLI toolkit, the IDE everyone actually uses, and the build pipeline that turns source into a shippable artifact.

Lab honesty, stated up front. Everything shown as terminal output below is real output from this lab machine (Temurin JDK 21.0.12.1 on Linux), pasted verbatim. Two things are not on this machine and are clearly marked NOT RUN: Maven and Gradle (no installs), and IntelliJ IDEA (it's a GUI — there is no screenshot to take). I describe those from standard, verifiable knowledge, not from pretending.

The whole post in one diagram — the loop you'll run ten thousand times:

The Java development loop Edit .java source files Compile javac → .class bytecode Run java JVM executes Debug stack trace / IDE Ship jar artifact failure / surprise

1. The edit → compile → run loop (vs Python's edit → run)

In Python, the loop you know is: edit discount.py, run python discount.py, see output. There is no step in between — the interpreter reads your source directly. In Java, the loop has an extra, non-negotiable step in the middle: javac translates .java source into .class bytecode, and java runs the bytecode on the JVM. Here is the real thing, end to end:

$ cat Discount.java
public class Discount {
    public static void main(String[] args) {
        double total = 0.0;
        for (String a : args) {
            double price = Double.parseDouble(a);
            total += price;
        }
        double rate = args.length >= 5 ? 0.15 : 0.05;
        System.out.printf("Subtotal: %.2f, discount rate: %.0f%%, due: %.2f%n",
                          total, rate * 100, total * (1 - rate));
    }
}
$ javac Discount.java && java Discount 19.99 42.50 8.00 5.25 3.75 12.00
Subtotal: 91.49, discount rate: 15%, due: 77.77
$ ls
Discount.class  Discount.java

Three things to notice, each a habit change:

  • javac Discount.java produces Discount.class — bytecode, not machine code. You never run the .java file; you run the class by name (java Discount, no .class suffix). This is the single most common first-day stumble: java Discount.java works in recent JDKs (11+) as a convenience for single files, but it hides the real pipeline — learn the two-step form first.
  • Command-line args arrive as String[] args — Java's sys.argv[1:]. Parsing them is your job (Double.parseDouble, the strict cousin of float()).
  • System.out.printf is print with C-style formatting — %.2f, and %n for a platform-correct newline (prefer it over \n in Java).
Python mapping: javac has no Python equivalent — it's a required ahead-of-time translation step, the closest analogy being a type-checker like mypy that you cannot skip. java is the analog of the python command itself: it launches the runtime (the JVM) and runs your program inside it.

Multi-file projects: -d and the classpath

Real programs aren't one file. Here's a two-file program — an Order class and a Billing driver — compiled together, with compiled classes kept separate from source via -d:

$ javac -d out Order.java Billing.java && java -cp out Billing
Order subtotal: 70.49

-d out tells javac where to put the .class files (keeping source and build output separate — a hygiene habit from day one). -cp out (classpath) tells java where to find classes: it is Java's PYTHONPATH. javac resolves references between your source files automatically — you don't compile Order and Billing separately or in any order.

"Compilation errors are free tests"

Now the payoff of the extra step. Watch what happens when I pass a String where a double belongs — the exact bug from post 1's type-system lesson, this time caught by the machine:

$ javac -d out Order.java Billing.java
Billing.java:4: error: incompatible types: String cannot be converted to double
        order.addItem("19.99");   // deliberate: String where double belongs
                      ^
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
1 error

In Python, order.add_item("19.99") sails through python billing.py and blows up at 2 AM in production — or worse, silently concatenates. In Java, the program cannot be built until the types line up. Every compile is a whole-program consistency check over every type, every method signature, every import: thousands of assertions you never had to write. That's the principle this track keeps circling:

Post-4 principle: the compiler is your first test suite. In Python you earn type safety by writing tests. In Java you get a large, free, fast subset of it on every build — and the discipline is to treat a clean compile as the starting line for real tests, not the finish line.

2. jshell: the REPL you didn't know Java had

"But what about experimenting?" — the Python developer's first objection to a compiled language. Java's answer, since JDK 9, is jshell: a real read-eval-print loop that ships with every JDK. No build files, no main method, no ceremony. Here is a real session, pasted verbatim:

$ printf 'double price = 19.99;\n...' | jshell --no-startup --execution local
|  Welcome to JShell -- Version 21.0.12.1
|  For an introduction type: /help intro

jshell>  price ==> 19.99

jshell>  |  created method total(double,int)

jshell>  $3 ==> 64.7676

jshell>  |  created method label(String)

jshell>  $5 ==> "HELLO JSHELL!"

jshell>  |    double price = 19.99
|    double $3 = 64.7676
|    String $5 = "HELLO JSHELL!"

jshell>
   1 : double price = 19.99;
   2 : double total(double p, int qty) { return p * qty * 1.08; }
   3 : total(price, 3);
   4 : String label(String s) { return s.toUpperCase() + "!"; }
   5 : label("hello jshell");

jshell>  |  Goodbye

Variables persist, methods persist, bare expressions evaluate and get auto-named ($3, $5), /vars shows your state, /list shows your history. It feels like a REPL because it is one.

Environment note (honest, not important). On this sandbox I had to pass --execution local: jshell's default mode launches a separate execution JVM over a localhost socket, and this sandbox blocks that socket handshake. On any normal machine the plain jshell command works. The transcript above is otherwise exactly what you'd see.

jshell vs the Python REPL: an honest comparison

CapabilityPython REPLjshell
Variables & functions persist across linesYesYes
Define methods/classes inlinedef works naturallyYes — methods and even classes, semicolons optional
Forward referencesNo — name must exist when the line runsAllowed. Define a method that calls another method you haven't written yet; jshell warns and fixes it up when the missing piece arrives
Tab completionBasic (readline)Excellent — completes identifiers, suggests imports, and Shift+Tab v/m/i turns an expression into a variable, a method, or auto-imports a type
Magic commands (%timeit, ? docs)IPython's whole superpower setNo magic system. /vars, /list, /drop, /open, /save — useful, but it's a tool, not a laboratory
Startup timeInstantMeasured here: ~4 seconds warm (time on a trivial session: real 0m4.053s). Noticeable, but this is JVM warmup, not something wrong
Best useExploration, data poking, debuggingTrying a JDK API, sketching an algorithm, checking a type's behavior — then the code moves into a real file
The real difference is cultural, not technical. Python developers live in the REPL; Java developers visit jshell. The Java habit is: sketch in jshell, then promote to a file and a unit test within minutes. The REPL is a scratchpad, not a home — because the moment code matters, Java wants it compiled, tested, and packaged.

3. The JDK CLI toolkit tour

The JDK ships with a toolbox that, taken together, covers most of what Python developers reach for third-party tools to do. The table maps each tool to its Python-world equivalent — learn these names and you'll never feel lost on a Java machine:

ToolWhat it doesPython equivalent
javacCompiles .java → .class bytecodeNone required — closest is mypy, but optional there and mandatory here
javaLaunches the JVM and runs classesThe python command itself
jshellREPL for Javapython -i / IPython
javadocGenerates HTML API docs from /** ... */ commentspydoc / Sphinx
jarPackages classes + resources into a .jar (a zip with a manifest)zipapp / shiv / wheels — but jars are executable by the runtime
javapDisassembles .class files — see signatures and bytecodeThe dis module
jdepsAnalyzes class-level and module-level dependenciespipdeptree / modulefinder
jfr / jcmdFlight Recorder profiling and JVM diagnostics on running processescProfile / py-spy
keytoolManages TLS certificates and keystoresopenssl CLI habits

Real run: javadoc

Java's doc comments (/** ... */ with @param/@return) aren't decoration — javadoc turns them into a browsable HTML API reference, the same reference style you see on every JDK class. Real run on a small documented class:

$ javadoc -quiet -d api -author DiscountPolicy.java
DiscountPolicy.java:9: warning: use of default constructor, which does not provide a comment
public class DiscountPolicy {
       ^
1 warning
$ ls api | head -5
DiscountPolicy.html
allclasses-index.html
allpackages-index.html
constant-values.html
copy.svg

Note the honest warning: javadoc even nags you about an undocumented default constructor. The generated DiscountPolicy.html renders your @param itemCount and @return text into a proper method contract. The lesson for a Python developer used to docstrings that only humans read: in Java, doc comments are compiled documentation — IDEs surface them as hover help, and publishing a library without them is considered rude.

Real run: jar — packaging and running an artifact

A .jar is a zip file with a manifest; a manifest naming a Main-Class makes it directly executable. This is Java's answer to "how do I ship it":

$ javac -d out BillingCli.java
$ printf 'Manifest-Version: 1.0\nMain-Class: BillingCli\n' > manifest.txt
$ jar cfm billing-cli.jar manifest.txt -C out .
$ jar tf billing-cli.jar
META-INF/
META-INF/MANIFEST.MF
BillingCli.class
$ java -jar billing-cli.jar 19.99 42.50 8.00
Billed: 70.49

java -jar billing-cli.jar — no classpath, no class name, just run. The Python analogy is python -m zipapp producing a .pyz, except jars are the universal Java distribution unit: libraries, tools, and services all ship as jars (or as wars/ears in enterprise-land). When section 7 talks about build pipelines producing "artifacts," this is the artifact.

4. Project layout: packages are directories

Python: cityops/billing.py with an __init__.py gives you cityops.billing. Java: the directory is the package — com/cityops/billing/Money.java declares package com.cityops.billing;, no __init__ files, and the directory structure must match the package name exactly or compilation fails. Real two-package project, compiled in one shot:

$ find packages -name '*.java'
packages/com/cityops/billing/Money.java
packages/com/cityops/reporting/Summary.java
$ find packages -name '*.java' | xargs javac -d packages/out
$ find packages/out -name '*.class'
packages/out/com/cityops/billing/Money.class
packages/out/com/cityops/reporting/Summary.class
$ java -cp packages/out com.cityops.reporting.Summary 19.99 42.50 8.00
Day total: $70.49 across 3 orders

Notice: you run the class by its fully qualified name (com.cityops.reporting.Summary), and javac -d recreated the package directory tree under out/ automatically. The convention you'll see in every real project builds on this:

my-project/
├── src/main/java/com/cityops/billing/Money.java   # production code
├── src/test/java/com/cityops/billing/MoneyTest.java # tests mirror the tree
└── src/main/resources/application.properties       # non-code resources

That src/main/java / src/test/java split isn't a suggestion — it's the Maven/Gradle standard layout (section 7), and every tool in the ecosystem assumes it. Tests live in a parallel tree with the same package names so they can access package-private members — Java's answer to "how do I test internals."

5. IntelliJ IDEA — why Java developers live in an IDE [NOT RUN]

Not run — and it couldn't be. IntelliJ IDEA is a graphical IDE; this lab machine has no display and no install. Everything in this section is accurate, standard knowledge about the tool — described, not demonstrated. No screenshots are shown because none were taken.

Python developers often treat the IDE as a fancy text editor: VS Code + Pylance is lovely, but you could do the whole job in vim and lose little. In Java, the IDE is load-bearing infrastructure, and the reason is the payoff of post 1's lesson: static types make whole-program refactoring safe, and the IDE is the machine that performs it.

Refactoring: the superpower Python can't fully have

Rename a method in a Python codebase and you're doing a text search, hoping you caught every call site — dynamic dispatch means no tool can be sure. Rename a method in IntelliJ (Shift+F6) and the IDE rewrites every call site across the project provably correctly, because the compiler-grade type information tells it exactly which references are yours. Same for Extract Method, Change Signature (add a parameter — every caller updated), Move Class between packages (imports rewritten), and Inline Variable. This is why Java codebases stay malleable at hundreds of thousands of lines: the IDE + the type system make large-scale restructuring a ten-second, zero-fear operation.

The debugger: breakpoints beat print()

Python developers debug with print() and pdb. The IntelliJ debugger is pdb with the training wheels off and a jet engine attached: click a gutter to set a breakpoint (conditional breakpoints — "stop only when total > 1000"), and when it hits you get the full call stack, every local variable, and Evaluate Expression — run arbitrary Java against the live, paused program state. HotSwap even lets you edit a method body and keep debugging without restarting. Once you've evaluated an expression against a paused production-like state, going back to re-running with new print statements feels like sending letters.

Inspections: a linter with a compiler's brain

IntelliJ continuously runs hundreds of inspections — think ruff/flake8 crossed with mypy, with full type information: it flags the unreachable code, the Optional.get() without a presence check, the string concatenation in a loop, the resource you forgot to close, and offers one-click fixes. Many Java developers experience inspections as "the IDE pair-programming with me" — it catches the bug class from section 1's compile errors and keeps going into logic-level suggestions.

Honest comparison with VS Code + Python. VS Code with Pylance is genuinely excellent — for Python. But Pylance is doing archaeology on dynamic code, inferring what it can. IntelliJ is doing accounting on typed code, knowing everything for certain. The practical upshot: in Java, "let me just restructure this whole module" is a casual afternoon; in Python, it's a careful, test-covered expedition. Neither is wrong — but now you know why Java shops treat the IDE as non-negotiable, and why the Community Edition (free) vs Ultimate (paid, with framework support) distinction matters when you pick one.

6. The build pipeline: Maven, Gradle, and the road to "release"

Not run — not installed here. This lab machine has no Maven and no Gradle, so the snippets below are canonical, standard configurations shown for learning, not output from a run. Every behavior described (mvn test, the lifecycle phases) is standard, documented tool behavior.

Post 3 introduced dependency management — declaring libraries instead of vendoring jars. Maven and Gradle are the machines that turn that declaration (plus your source) into a tested, packaged artifact, repeatably, on any machine. If pip + pyproject.toml is "install my deps," Maven/Gradle is "install my deps, compile everything, run all tests, and hand me a shippable jar" — the whole pipeline as one command.

Maven: convention over configuration

Maven's pom.xml ("project object model") declares coordinates (groupId:artifactId:version — the same vocabulary as post 3), dependencies, and build plugins. A minimal real-world-style pom.xml:

<!-- pom.xml — NOT RUN (Maven not installed in this lab) -->
<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.cityops</groupId>
  <artifactId>billing-cli</artifactId>
  <version>1.0.0</version>
  <packaging>jar</packaging>

  <properties>
    <maven.compiler.release>21</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <version>5.10.3</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

Then the lifecycle — Maven's fixed pipeline of phases, each building on the last:

mvn validate   # is the project structurally sound?
mvn compile    # javac over src/main/java (what we did by hand in section 1)
mvn test       # compile + run everything under src/test/java (JUnit)
mvn package    # run the full pipeline, then jar it up → target/billing-cli-1.0.0.jar
mvn verify     # package + integration checks

Running mvn package executes every earlier phase — you can't package what didn't compile, and you can't package what didn't pass tests. That ordering is the point: the build pipeline encodes "compilation errors are free tests" (section 1) into an enforced sequence. mvn test alone is the command Java developers run dozens of times a day — it's pytest, except the compile step runs first and for free.

Gradle: the same pipeline, scripted

Gradle covers the same ground with a Groovy/Kotlin DSL instead of XML, and an obsessive focus on incremental builds (only recompile what changed — big projects care enormously). The equivalent build.gradle:

// build.gradle — NOT RUN (Gradle not installed in this lab)
plugins {
    id 'java'
}

group = 'com.cityops'
version = '1.0.0'

java {
    toolchain { languageVersion = JavaLanguageVersion.of(21) }
}

repositories {
    mavenCentral()   // where dependencies come from (post 3's vocabulary)
}

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter:5.10.3'
}

tasks.named('test') {
    useJUnitPlatform()
}

gradle build ≈ mvn verify: compile, test, package into build/libs/billing-cli-1.0.0.jar. Which to learn? Maven for reading the enormous existing corpus (most enterprise Java is Maven); Gradle for Android and for greenfield projects where build speed matters. The concepts — coordinates, scopes, lifecycle, artifact — transfer completely.

From jar to "release": CI and artifacts

On your machine, mvn package makes a jar. In a team, a CI pipeline (typically GitHub Actions) runs the same lifecycle on every push — compile, test, package — so "it works on my machine" stops being a defense. A standard sketch:

# .github/workflows/build.yml — NOT RUN (conceptual sketch)
name: build
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with: { distribution: temurin, java-version: '21' }
      - run: mvn -B verify        # the whole pipeline: compile → test → package
      - uses: actions/upload-artifact@v4
        with: { name: billing-cli, path: target/*.jar }

And "release" itself is a versioning discipline, not a button: versions like 1.0.0 are immutable once published to a repository (Maven Central for open source, a private Nexus/Artifactory in-house), while 1.1.0-SNAPSHOT means "the moving target that will become 1.1.0." Downstream projects depend on released versions — never on snapshots — which is how the ecosystem from post 3 stays reproducible. The jar that CI uploads is the release artifact: the exact bytes that get deployed.

Python mapping for the whole section: pyproject.toml + pip covers dependencies; Maven/Gradle cover dependencies plus compilation, testing, and packaging in one reproducible lifecycle. The closest Python analog to the full pipeline is a CI workflow that runs pip install → pytest → python -m build — Java just standardized that sequence into the build tool itself, twenty years ago.

7. Debugging workflow: read the stack trace first

When a Java program dies, it dies loudly and precisely — and the stack trace is the first tool in the debugging workflow, before the debugger, before print statements. Here's a real one, from a real run. This program dereferences a null record:

$ cat NullTrace.java
public class NullTrace {
    record Item(String sku, double price) {}
    public static void main(String[] args) {
        Item item = null;
        System.out.println(item.sku().toUpperCase());
    }
}
$ javac NullTrace.java && java NullTrace
Exception in thread "main" java.lang.NullPointerException:
    Cannot invoke "NullTrace$Item.sku()" because "<local1>" is null
        at NullTrace.main(NullTrace.java:5)

Read it inside-out, the way experienced Java developers do:

  1. The exception type (NullPointerException) tells you the category — the single most common Java runtime failure, and the reason post 2's Optional exists.
  2. The message does the detective work for you: "Cannot invoke NullTrace$Item.sku() because <local1> is null". Since JDK 14, "helpful null messages" pinpoint which reference was null in a chained expression — compare Python's bare AttributeError: 'NoneType' object has no attribute 'sku', which never tells you which link in a.b.c was None.
  3. The stack frames (at NullTrace.main(NullTrace.java:5)) give the exact file and line — read top frame first for where it blew up, then down the stack for how you got there. (In a real app there'd be dozens of frames; the application frames are at the top, framework internals below.)

The workflow, in order of escalation:

  1. Read the trace. Most NPEs and IndexOutOfBoundsExceptions are solved right here — the message plus the line number is the diagnosis.
  2. Reproduce in jshell. Paste the suspect expression into a REPL session and poke at it — five seconds, no rebuild.
  3. Reach for the IDE debugger (section 5) when the trace isn't enough: conditional breakpoint at the failing line, evaluate the expression against live state, watch which reference is null instead of guessing.
  4. Add a test capturing the failure, so the build pipeline (section 6) catches the regression forever. The loop closes: debug → test → the compiler and CI guard it from here on.
A word on jdb. The JDK ships a command-line debugger (jdb), the analog of pdb. It works, and it's what you'd use over SSH with no GUI — but in practice, everyone uses the IDE debugger from section 5. Know jdb exists; reach for IntelliJ first.

Field check

Before you close the track, prove the toolchain is yours — everything here runs on the plain JDK, no installs:

  1. Compile-loop drill: write a two-file program (any Order/Billing pair of your own), compile with javac -d out, run with java -cp out. Then deliberately break a type, read the compiler error, and fix it. Notice how the error names the file, line, and the exact incompatible types.
  2. jshell sketch: open jshell, define a method, call it before defining a helper it uses (forward reference), then define the helper and watch the earlier method start working. Run /vars and /list. Time the startup — compare with python3.
  3. Docs + packaging: write a class with real /** */ doc comments, run javadoc -d api, and open the HTML. Then jar it with a Main-Class manifest and run it with java -jar.
  4. Trace reading: take the NullTrace program above, extend the chain (item.sku().toUpperCase().charAt(0)), and confirm the helpful-null message still names the null link. Then fix it with Optional (post 2) instead of a null check.
  5. Not-run inventory: list the three things from this post you couldn't run here (Maven, Gradle, IntelliJ) and write one sentence for each on what you'd do first with it on a real machine. Knowing what you haven't touched is part of knowing the toolchain.

What's next — closing the bridge

That's the bridge crossed. Four posts, one arc:

  • Post 1 — the static-type mindset: types as machine-checked documentation, and why the compiler catching your bug is a feature, not friction.
  • Post 2 — objects, collections, and Optional: Java's object model and standard library, mapped against the Python you already know.
  • Post 3 — dependency management: coordinates, repositories, and declaring libraries instead of vendoring them.
  • Post 4 — this post: the toolchain that turns all of the above into a daily workflow — compile loop, jshell, JDK utilities, the IDE, the build pipeline, and reading stack traces.

You now know enough Java to be dangerous in the right direction: you can read Java code, write small programs, navigate the tooling, and — most importantly — you know what you don't know yet. The bridge got you across; now it's time to walk the road properly.

Your next step is the full curriculum: Continue: Java Learning Roadmap 2026 — the complete, ordered map of every track. Start at Core Java, post 1, and work the fundamentals the way Java developers actually learn them: one compiled, tested, packaged concept at a time.

See you on the other side — bring your compiler. It tests for free.

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