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.equalsreturn 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:
| Type | Holds | Example |
|---|---|---|
int | whole numbers | int balance = 5000; |
double | decimals | double rate = 6.5; |
boolean | true/false | boolean isActive = true; |
char | one character | char 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:
| Comparison | What it actually checks | Use it when |
|---|---|---|
a == b | Are 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:
- Fixed size.
new int[3]is 3 slots forever. No.push()like JS arrays — growable lists (ArrayList) come next module. - Zero-indexed. First slot is
[0], last is[length - 1]. Ask beyond that:ArrayIndexOutOfBoundsException(you met it last module). - Default values. A new array isn’t empty — it’s pre-filled:
0for numbers,falsefor booleans,nullfor objects. Anew 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 adouble. Declarevoidand 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.
- 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);
}
}
-
Compile and run:
javac Atm.javathenjava Atm. Do a deposit, a withdrawal, check balance, exit. -
Attack it. Type
abcat the menu. Type-500as a deposit. Try withdrawing more than the balance. Every one should produce a message, not a crash, and the loop should continue. -
Prove the gotcha. In
readInt, comment out thesc.next();line, recompile, run, and typeabcat 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. -
Extend it yourself (no looking anything up): add option
5 Mini Statementthat stores the last 5 transaction amounts in anint[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’sArrayList.
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
| Resource | What to do there | How long |
|---|---|---|
| codingbat.com/java | Warmup-1 and String-1 sections — tiny method-writing reps | 45 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.java | Skim “Variables” and “Control Flow” tutorials as reference, not reading homework | 15 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 intonextInt). Trigger each one once and read the message.
Check Yourself
int a = 5; int b = a; b = 10;— what isa? Now:int[] x = {5}; int[] y = x; y[0] = 10;— what isx[0]? Why do they differ?- What does “String is immutable” actually mean, and what does
s = s + "!"really do? - When does
==on two strings returntrue, and why must you still use.equalsfor content? - What’s in a freshly created
new String[4]array, and what happens if you call a method on one of its slots? - What two advantages does a switch expression (
->) have over the oldbreak-style switch? - Two methods named
pay— one takes(int), one takes(int, String). What is this called, and how does Java pick? - Why does the ATM’s
readIntneedsc.next()inside its loop? - Is
varthe same as JS’slet? What’s the one-line difference?
Answers
ais still5— primitives copy the value.x[0]is10—y = xcopied the arrow, so both names point at the same heap array.- The character data of a String object can never change after creation.
s = s + "!"builds a brand-new String on the heap and re-pointssat it; the original is untouched and abandoned. ==istrueonly 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..equalscompares the characters themselves.- Four
nulls — object arrays default to null. Calling a method on a slot, likearr[0].length(), throwsNullPointerException. - It returns a value directly, and there’s no fall-through — no forgotten-
breakbugs, and the compiler forces exhaustive handling viadefault. - Overloading. The compiler picks at compile time by matching the number and types of the arguments you pass.
hasNextInt()only peeks — it never consumes the bad token. Withoutsc.next()discarding it, the loop re-checks the same garbage forever.- No.
varis compile-time type inference — the type is deduced once from the initializer and fixed forever.letis 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