Career OS

The Language Core

You can run Java now. This module gives you the working vocabulary — variables, strings, arrays, control flow, methods — and one idea that will save you from Java’s most common beginner bug: knowing whether your variable holds a value or an arrow pointing at one. Get that picture right and == vs .equals stops being a memorised rule and becomes obvious.

The Goal

By the end of this module you can:

  • Explain the difference between primitives and reference types using a memory drawing
  • Predict what == and .equals return for two strings, and why
  • Write control flow — if, switch expressions, all three loop styles — without looking it up
  • Build methods with parameters, return values, and overloads, and know where a variable’s scope ends
  • Read user input safely with Scanner and reject garbage instead of crashing

The Lesson

Primitives vs reference types — the one picture

In JS you wrote let x = 5 and never thought about what x is. In Java, the type is part of the variable, and there are exactly two kinds:

Primitives — the value itself sits in the stack frame. Eight of them exist; you’ll use four constantly:

TypeHoldsExample
intwhole numbersint balance = 5000;
doubledecimalsdouble rate = 6.5;
booleantrue/falseboolean isActive = true;
charone characterchar grade = 'A';

(The other four — long, short, byte, float — are size variants you’ll meet when needed. Money in real systems uses long paise or BigDecimal, never double — file that away for later.)

Reference types — everything else: String, arrays, every object. The variable in your frame holds an arrow (a reference) to an object on the heap.

flowchart LR
    subgraph Frame[Stack frame of main]
        A[int balance holds 5000 directly]
        B[String name holds an arrow]
        C[int array txns holds an arrow]
    end
    subgraph Heap[Heap]
        S[String object Darshan]
        T[array object 500 1200 250]
    end
    B --> S
    C --> T

Why this matters: when you copy a primitive, you copy the value. When you copy a reference, you copy the arrow — both variables now point at the same object.

int a = 5;
int b = a;          // b gets its own 5
b = 99;             // a is still 5

int[] x = {1, 2, 3};
int[] y = x;        // y is the SAME arrow, not a new array
y[0] = 99;          // x[0] is now also 99 - one object, two arrows

This is the root of half the bugs you’ll write this year. Draw the arrows when confused.

String — immutable, and the == trap

A String is an object on the heap, and it’s immutable — once created, its characters never change. Every “modification” secretly creates a new string:

String s = "Hi";
s = s + " Darshan";   // does NOT edit "Hi" - builds a brand new "Hi Darshan"
                      // and points s at it. The old "Hi" is abandoned.

That’s fine for a few concatenations. Inside a loop running 10,000 times, you’re creating 10,000 throwaway objects — that’s why StringBuilder exists (coming in a later module; for now just know why it’ll show up).

Now the trap. Two ways to compare strings:

ComparisonWhat it actually checksUse it when
a == bAre these the same arrow? Same object on the heap?Almost never for strings
a.equals(b)Do they contain the same characters?Always, for content
String typed = new String("exit");   // forces a fresh heap object
String command = "exit";

System.out.println(typed == command);        // false - two different objects
System.out.println(typed.equals(command));   // true  - same characters

The evil part: string literals are pooled by the JVM, so "exit" == "exit" happens to be true — your code works in testing, then breaks the moment a string arrives from user input or a file (a fresh object). In JS, === compares string content; in Java, == on objects compares identity. Rule: content → .equals, always.

flowchart LR
    subgraph Stack[Stack]
        P[typed]
        Q[command]
    end
    subgraph Heap[Heap]
        O1[String object exit number 1]
        O2[String object exit number 2]
    end
    P --> O1
    Q --> O2
    O1 -.same characters so equals is true.- O2

== asks “do P and Q point at the same box?” — no. .equals asks “do the boxes contain the same letters?” — yes.

Arrays — fixed size, zero-indexed, pre-filled

A Java array is a heap object with three hard rules:

  1. Fixed size. new int[3] is 3 slots forever. No .push() like JS arrays — growable lists (ArrayList) come next module.
  2. Zero-indexed. First slot is [0], last is [length - 1]. Ask beyond that: ArrayIndexOutOfBoundsException (you met it last module).
  3. Default values. A new array isn’t empty — it’s pre-filled: 0 for numbers, false for booleans, null for objects. A new String[5] is five nulls waiting to NPE you.
int[] txns = new int[3];        // [0, 0, 0]
txns[0] = 500;
int[] fixed = {500, -1200, 250}; // create and fill in one go
System.out.println(fixed.length); // 3 - note: length, no parentheses, unlike JS

Control flow — mostly familiar, two upgrades

if/else, while, and the classic for work like JS (conditions must be real booleans though — no truthy/falsy; if (count) won’t compile, write if (count > 0)).

Upgrade 1 — switch expressions (Java 14+). The old switch with break everywhere is legacy. The modern form returns a value and can’t fall through:

String menu = switch (choice) {
    case 1 -> "Deposit";
    case 2 -> "Withdraw";
    case 3 -> "Balance";
    case 4 -> "Exit";
    default -> "Invalid";    // compiler forces you to handle the rest
};

Upgrade 2 — for-each, for when you want every element and don’t care about the index:

int[] txns = {500, -1200, 250};
for (int t : txns) {           // read it as for each t in txns
    System.out.println("Rs " + t);
}

Like JS’s for...of. Use the classic for (int i = 0; ...) only when you actually need i.

Methods — parameters, returns, overloading

A method is a named block with typed inputs and a typed output. The types are the contract:

static double applyInterest(double balance, double ratePercent) {
    return balance + (balance * ratePercent / 100);
}
  • Parameters are local variables born from the caller’s arguments. Primitives arrive as copies — reassigning a parameter never affects the caller. References arrive as copied arrows — you can’t change which object the caller points at, but you can change the object’s contents through the arrow. (That’s the primitives-vs-references picture again. It explains everything.)
  • Return type is enforced: declare double, and every path must return a double. Declare void and you return nothing.

Overloading: several methods may share a name if their parameter lists differ. The compiler picks by what you pass:

static void log(String msg)            { System.out.println("LOG " + msg); }
static void log(String msg, int code)  { System.out.println("LOG " + code + " " + msg); }

You’ve used this all along — System.out.println is overloaded for strings, ints, doubles, everything.

Scope — where a variable exists

A variable lives from its declaration to the end of the block (the { }) it was declared in. After that, it’s gone — the compiler won’t even let you mention it:

static void demo() {
    int total = 0;                  // alive until demo ends
    for (int i = 0; i < 3; i++) {   // i alive only inside this loop
        int doubled = i * 2;        // doubled alive only inside this iteration
        total += doubled;
    }
    // i and doubled do not exist here - using them is a compile error
}

Same spirit as JS’s let block scoping, but stricter: no hoisting, no global leakage, and the compiler also refuses to let you read a local variable before assigning it a value.

Reading input with Scanner

Scanner wraps System.in (the keyboard) and parses what’s typed:

import java.util.Scanner;            // first import you've needed - Scanner lives in java.util

public class Greet {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.print("Your name: ");
        String name = sc.nextLine();          // reads a whole line of text
        System.out.print("Deposit amount: ");
        int amount = sc.nextInt();            // reads one int - crashes on garbage
        System.out.println(name + " deposited Rs " + amount);
    }
}

The crash matters: type abc when nextInt() is waiting and you get InputMismatchException. Real programs check before reading:

if (sc.hasNextInt()) {        // peek - is the next token an int?
    int amount = sc.nextInt();
} else {
    System.out.println("Numbers only.");
    sc.next();                // throw away the bad token or it blocks forever
}

That sc.next() line is the classic gotcha — hasNextInt() doesn’t consume the garbage, so without discarding it you loop on the same bad input forever. You’ll prove this in the build.

var — inference, not dynamic typing

var lets the compiler figure out the type from the right-hand side:

var balance = 5000.0;            // compiler locks it in as double
var sc = new Scanner(System.in); // obviously a Scanner - var removes the noise

This is not JS’s let. The type is fixed forever at compile time — balance = "hello" is still an error. Skip var when the right side doesn’t make the type obvious (var result = process(data); — what is that?), and remember it only works for local variables with an initial value.

Build This

A terminal ATM. Menu loop, one method per action, and it must survive a user mashing the keyboard.

  1. Create Atm.java:
import java.util.Scanner;

public class Atm {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        double balance = 1000.0;          // opening balance Rs 1000
        boolean running = true;

        while (running) {
            printMenu();
            int choice = readInt(sc);     // never trust raw input
            switch (choice) {
                case 1 -> balance = deposit(sc, balance);
                case 2 -> balance = withdraw(sc, balance);
                case 3 -> showBalance(balance);
                case 4 -> running = false;
                default -> System.out.println("Pick 1 to 4.");
            }
        }
        System.out.println("Thanks for banking with us.");
    }

    static void printMenu() {
        System.out.println("\n=== ATM ===");
        System.out.println("1 Deposit  2 Withdraw  3 Balance  4 Exit");
        System.out.print("Choice: ");
    }

    // one gatekeeper for all numeric input - garbage never reaches the logic
    static int readInt(Scanner sc) {
        while (!sc.hasNextInt()) {
            System.out.print("Numbers only, try again: ");
            sc.next();                    // discard the bad token
        }
        return sc.nextInt();
    }

    static double deposit(Scanner sc, double balance) {
        System.out.print("Amount to deposit: ");
        int amount = readInt(sc);
        if (amount <= 0) {
            System.out.println("Deposit must be positive.");
            return balance;               // unchanged - invalid input changes nothing
        }
        System.out.println("Deposited Rs " + amount);
        return balance + amount;
    }

    static double withdraw(Scanner sc, double balance) {
        System.out.print("Amount to withdraw: ");
        int amount = readInt(sc);
        if (amount <= 0) {
            System.out.println("Withdrawal must be positive.");
            return balance;
        }
        if (amount > balance) {           // the check that makes it a bank, not a money printer
            System.out.println("Insufficient funds. Balance Rs " + balance);
            return balance;
        }
        System.out.println("Withdrew Rs " + amount);
        return balance - amount;
    }

    static void showBalance(double balance) {
        System.out.println("Balance: Rs " + balance);
    }
}
  1. Compile and run: javac Atm.java then java Atm. Do a deposit, a withdrawal, check balance, exit.

  2. Attack it. Type abc at the menu. Type -500 as a deposit. Try withdrawing more than the balance. Every one should produce a message, not a crash, and the loop should continue.

  3. Prove the gotcha. In readInt, comment out the sc.next(); line, recompile, run, and type abc at the menu. Watch it spin forever — hasNextInt() keeps seeing the same unconsumed garbage. Kill it with Ctrl+C, restore the line, recompile. Now you know why that line exists instead of believing it.

  4. Extend it yourself (no looking anything up): add option 5 Mini Statement that stores the last 5 transaction amounts in an int[5] array and prints them with a for-each loop. You’ll hit the fixed-size limitation immediately — decide how to handle the 6th transaction. That pain is the setup for next module’s ArrayList.

flowchart TD
    A[Show menu] --> B[Read choice through readInt]
    B --> C{Which option}
    C -->|1| D[deposit validates then returns new balance]
    C -->|2| E[withdraw validates funds then returns new balance]
    C -->|3| F[showBalance prints]
    C -->|4| G[Exit loop]
    C -->|other| H[Pick 1 to 4 message]
    D --> A
    E --> A
    F --> A
    H --> A

Where to Practice

ResourceWhat to do thereHow long
codingbat.com/javaWarmup-1 and String-1 sections — tiny method-writing reps45 min
exercism.org Java track”Annalyns Infiltration” (booleans) and “Cars Assemble” (numbers, ifs)40 min
HackerRank Java”Java If-Else”, “Java Loops I”, “Java Datatypes”30 min
dev.javaSkim “Variables” and “Control Flow” tutorials as reference, not reading homework15 min

How to Practice

  • Type everything yourself — especially the ATM. Muscle memory on switch -> and method signatures is the goal.
  • Never copy-paste, even your own earlier code. Retyping is rehearsal.
  • Time-box: 20 minutes stuck means write down the error and ask a why question — don’t doom-scroll solutions.
  • Break it on purpose: every concept here has a failure mode (== on input strings, index past the end, garbage into nextInt). Trigger each one once and read the message.

Check Yourself

  1. int a = 5; int b = a; b = 10; — what is a? Now: int[] x = {5}; int[] y = x; y[0] = 10; — what is x[0]? Why do they differ?
  2. What does “String is immutable” actually mean, and what does s = s + "!" really do?
  3. When does == on two strings return true, and why must you still use .equals for content?
  4. What’s in a freshly created new String[4] array, and what happens if you call a method on one of its slots?
  5. What two advantages does a switch expression (->) have over the old break-style switch?
  6. Two methods named pay — one takes (int), one takes (int, String). What is this called, and how does Java pick?
  7. Why does the ATM’s readInt need sc.next() inside its loop?
  8. Is var the same as JS’s let? What’s the one-line difference?
Answers
  1. a is still 5 — primitives copy the value. x[0] is 10y = x copied the arrow, so both names point at the same heap array.
  2. The character data of a String object can never change after creation. s = s + "!" builds a brand-new String on the heap and re-points s at it; the original is untouched and abandoned.
  3. == is true only when both references point at the same object — which can happen by accident with pooled literals. Strings from input or files are fresh objects, so identity fails even when characters match. .equals compares the characters themselves.
  4. Four nulls — object arrays default to null. Calling a method on a slot, like arr[0].length(), throws NullPointerException.
  5. It returns a value directly, and there’s no fall-through — no forgotten-break bugs, and the compiler forces exhaustive handling via default.
  6. Overloading. The compiler picks at compile time by matching the number and types of the arguments you pass.
  7. hasNextInt() only peeks — it never consumes the bad token. Without sc.next() discarding it, the loop re-checks the same garbage forever.
  8. No. var is compile-time type inference — the type is deduced once from the initializer and fixed forever. let is dynamically typed; the variable can hold anything later.

Explain it out loud: Empty chair or voice note — draw (in the air if you must) the stack frame and heap for String a = "hi"; String b = new String("hi"); and explain why a == b is false but a.equals(b) is true. If your explanation needs the word “memorise,” you don’t have it yet — re-read the String section.

Still Unclear?

Copy-paste these into Claude — understanding-builders, not code vending machines:

I know JavaScript. Explain Java primitives vs reference types by contrasting
with how JS variables work, then give me 5 short code snippets and make me
predict the output of each before you reveal the answers.
Why did Java's designers make String immutable? What concrete bugs and
security problems does immutability prevent in a real banking backend,
and what's the performance cost I pay for it?
Here's my Atm.java: [paste it]. Don't rewrite it. Instead, review my
input validation like a senior engineer - what user input could still
break it, and why?

Why AI Can’t Do This For You

AI will happily generate the ATM in three seconds. But when production money math is off by one rupee, the engineer who can trace which variable held a copy and which held a shared reference finds it — that’s a memory model in your head, not in a prompt.

== vs .equals bugs don’t crash; they silently take the wrong branch. AI can’t spot the bug it isn’t shown — you, reading code in review, can. That’s the job.

Module done? Add it to today’s tracker

Saves your progress on this device.