PONY λ M2 Modula-2

JavaScript.CodeCompared.To/Java

An interactive executable cheatsheet comparing JavaScript and Java

JavaScript (ES2025) Java 25
Output & Running
Hello, World
There is no top-level code in Java. Every statement lives inside a method, every method inside a class, and the runtime starts at the one method with this exact signature. The ceremony is not optional.
console.log("Hello, World!");
class Main { public static void main(String[] args) { System.out.println("Hello, World!"); } }
There is also a compile step: javac turns source into bytecode before java runs it. Nothing executes until the whole file type-checks, which is the central trade this page is about.
Interpolation becomes formatting
Java has no template literals. String concatenation with + works, and printf or String.format covers the rest using %s, %d, and %.2f placeholders.
const city = "Reykjavik"; const celsius = -3.5; console.log(`${city}: ${celsius}°C`); console.log(`rounded: ${celsius.toFixed(0)}`);
class Main { public static void main(String[] args) { String name = "Ada"; int score = 96; System.out.println(name + " scored " + score); System.out.printf("%s scored %d%n", name, score); System.out.println(String.format("%.2f", 3.14159)); } }
Use %n rather than \n in a format string — it emits the platform line separator. The format string is checked at runtime, not compile time, so a mismatched specifier throws rather than failing to build.
There is no built-in JSON
JSON is part of the JavaScript language. Java has no JSON support in the standard library at all — real projects add Jackson or Gson, and the mapping between JSON and typed objects becomes a configuration concern.
const employee = { name: "Ada", salary: 90000, tags: ["staff"] }; const text = JSON.stringify(employee); console.log(text); const parsed = JSON.parse(text); console.log(parsed.name, parsed.tags[0]);
record Employee(String name, double salary) {} class Main { public static void main(String[] args) { Employee employee = new Employee("Ada", 90000); // Without a library, assembling JSON is manual string work: String text = "{\"name\":\"" + employee.name() + "\"," + "\"salary\":" + employee.salary() + "}"; System.out.println(text); System.out.println(employee); } }
This is the clearest place JavaScript simply wins: no dependency, no annotations, no object mapper. The Java compensation is that once a library does deserialize the JSON, you get a typed Employee the compiler understands, rather than an object whose shape is only a hope.
Static Types & Declarations
Every variable has a declared type
A Java variable's type is fixed at declaration and enforced at compile time. var infers the type from the initializer — it is not JavaScript's var and does not make the variable dynamic.
let value = 42; value = "now a string"; console.log(typeof value); const maxRetries = 3; console.log(maxRetries);
class Main { public static void main(String[] args) { int value = 42; // value = "now a string"; // compile error: incompatible types System.out.println(value); var inferred = "still a String forever"; System.out.println(inferred.getClass().getSimpleName()); final int maxRetries = 3; System.out.println(maxRetries); } }
final is the counterpart of const — it prevents reassignment, not mutation. The commented line is a genuine compile error, which is the whole point: the class of bug that surfaces at runtime in JavaScript is refused at build time here.
Primitives are not objects
Java has eight primitive types (int, double, boolean, char, long, short, byte, float) that are not objects, have no methods, and cannot be null. Each has a wrapper class — Integer, Double — that boxes it when an object is required.
const count = 42; console.log(count.toString()); console.log((42).toFixed(1)); // Everything in JavaScript responds to methods.
import java.util.List; class Main { public static void main(String[] args) { int count = 42; // count.toString(); // compile error: int has no methods System.out.println(Integer.toString(count)); Integer boxed = count; // autoboxing System.out.println(boxed.toString()); List<Integer> numbers = List.of(1, 2, 3); // generics need objects System.out.println(numbers); } }
Collections cannot hold primitives, which is why you see List<Integer> and never List<int>. Autoboxing hides the conversion but not its cost, and an unboxed null Integer throws NullPointerException at the moment it is used as an int.
Conditions must be boolean
There is no truthiness. An if requires an actual boolean, so every emptiness or nullity test must be written explicitly. This eliminates the falsy-value class of bug entirely.
const name = ""; if (name) { console.log("has a name"); } else { console.log("no name"); } console.log(Boolean([]), Boolean("0"));
class Main { public static void main(String[] args) { String name = ""; // if (name) { } // compile error: String cannot be converted to boolean if (name != null && !name.isEmpty()) { System.out.println("has a name"); } else { System.out.println("no name"); } } }
The verbosity buys certainty: there is no list of falsy values to memorize and no argument about whether an empty array should be true. Note that String.isBlank() also treats whitespace-only strings as empty, which is usually what you meant.
Enums are real types
A Java enum is a fixed set of named instances that can carry fields and methods. JavaScript has no enum, so the usual stand-in is a frozen object of string constants with nothing checking them.
const Status = Object.freeze({ ACTIVE: "ACTIVE", SUSPENDED: "SUSPENDED", }); function describe(status) { return status === Status.ACTIVE ? "running" : "paused"; } console.log(describe(Status.ACTIVE)); console.log(describe("TYPO_SURVIVES"));
enum Status { ACTIVE("running"), SUSPENDED("paused"); private final String description; Status(String description) { this.description = description; } String describe() { return description; } } class Main { public static void main(String[] args) { System.out.println(Status.ACTIVE.describe()); System.out.println(Status.valueOf("SUSPENDED")); for (Status status : Status.values()) { System.out.println(status + " -> " + status.describe()); } } }
The JavaScript column accepts "TYPO_SURVIVES" without complaint; the Java one cannot be passed anything that is not a Status. Enums also work in switch with exhaustiveness checking, and values() gives you the full set for iteration.
instanceof with pattern binding
Where JavaScript uses typeof and instanceof loosely, Java pairs instanceof with a binding that declares the narrowed variable inline — no separate cast needed after the check.
function describe(value) { if (typeof value === "string") return `string of ${value.length}`; if (Array.isArray(value)) return `array of ${value.length}`; if (value instanceof Date) return "a date"; return "something else"; } console.log(describe("hello"), describe([1, 2]), describe(42));
import java.util.List; class Main { static String describe(Object value) { if (value instanceof String text) { return "string of " + text.length(); } if (value instanceof List<?> items) { return "list of " + items.size(); } return "something else"; } public static void main(String[] args) { System.out.println(describe("hello")); System.out.println(describe(List.of(1, 2))); System.out.println(describe(42)); } }
The bound variable — text, items — is in scope only where the check succeeded, so the old cast-after-check pattern is gone. List<?> uses a wildcard because generics are erased, and instanceof List<String> would not compile.
Primitives & Numbers
Integer division truncates
Java distinguishes integer from floating-point arithmetic. 7 / 2 on two ints is 3 — the fractional part is discarded, not rounded. Getting 3.5 requires at least one operand to be floating point.
console.log(7 / 2); console.log(Math.trunc(7 / 2)); console.log(0.1 + 0.2);
class Main { public static void main(String[] args) { System.out.println(7 / 2); System.out.println(7.0 / 2); System.out.println(7 % 2); System.out.println(0.1 + 0.2); } }
The 0.1 + 0.2 line is identical in both languages because Java's double is the same IEEE-754 type JavaScript uses for everything. The difference is that Java lets you avoid it by choosing an integer type.
Fixed-width integers overflow silently
An int is exactly 32 bits and a long is 64. Exceeding the range wraps around rather than losing precision — the opposite failure mode from JavaScript, where large integers degrade instead of wrapping.
console.log(Number.MAX_SAFE_INTEGER); console.log(9007199254740993); console.log(2n ** 70n);
import java.math.BigInteger; class Main { public static void main(String[] args) { System.out.println(Integer.MAX_VALUE); System.out.println(Integer.MAX_VALUE + 1); System.out.println(Math.addExact(1, 1)); System.out.println(BigInteger.TWO.pow(70)); } }
Integer.MAX_VALUE + 1 is negative — a wrap, with no warning. The Math.addExact family throws ArithmeticException instead, and BigInteger is the unbounded option, matching JavaScript's BigInt.
BigDecimal for money
Because JavaScript has only doubles, currency bugs are a known hazard there. Java offers BigDecimal: exact decimal arithmetic with an explicit rounding mode, and it is the expected choice for money.
const total = 0.1 + 0.2; console.log(total); console.log(total === 0.3); // The usual workaround is integer cents. console.log((10 + 20) / 100);
import java.math.BigDecimal; import java.math.RoundingMode; class Main { public static void main(String[] args) { BigDecimal first = new BigDecimal("0.10"); BigDecimal second = new BigDecimal("0.20"); BigDecimal total = first.add(second); System.out.println(total); System.out.println(total.compareTo(new BigDecimal("0.30")) == 0); System.out.println(total.setScale(1, RoundingMode.HALF_UP)); } }
Always construct from a String, never a doublenew BigDecimal(0.1) captures the binary approximation you were trying to escape. Compare with compareTo rather than equals, since equals also compares scale, so 0.30 and 0.3 are unequal.
Strings
Comparing strings needs equals()
This is the single most common first-week Java bug for a JavaScript developer. == on objects compares references, not contents. Strings are objects, so string comparison must use equals().
const first = "hello"; const built = ["hel", "lo"].join(""); console.log(first === built); console.log(first === "hello");
class Main { public static void main(String[] args) { String first = "hello"; String built = new StringBuilder("hel").append("lo").toString(); System.out.println(first == built); System.out.println(first.equals(built)); System.out.println(first.equalsIgnoreCase("HELLO")); System.out.println("apple".compareTo("banana") < 0); } }
Literals are interned, so "hello" == "hello" happens to be true — which is worse than if it always failed, because it lets the bug survive testing. Use equals unconditionally, and compareTo where JavaScript would use <.
StringBuilder for repeated concatenation
Java strings are immutable, as in JavaScript, but the compiler does not optimize concatenation inside a loop — each += allocates a new string. StringBuilder is the mutable buffer written for that case.
let accumulated = ""; for (let index = 1; index <= 5; index++) { accumulated += index + ","; } console.log(accumulated); console.log(["alpha", "beta"].join("-"));
import java.util.List; class Main { public static void main(String[] args) { StringBuilder builder = new StringBuilder(); for (int index = 1; index <= 5; index++) { builder.append(index).append(","); } System.out.println(builder); System.out.println(String.join("-", List.of("alpha", "beta"))); } }
The += loop that is ordinary practice in JavaScript is a recognized performance mistake here, quadratic in the number of appends. For joining a known collection, String.join is clearer than either.
Text blocks
A text block is delimited by triple quotes and spans lines. Unlike a JavaScript template literal, it strips incidental indentation — the common leading whitespace is removed, so the literal can be indented to match surrounding code.
const card = `<article> <h1>Reykjavik</h1> <p>-3.5°C</p> </article>`; console.log(card);
class Main { public static void main(String[] args) { String query = """ SELECT name, salary FROM employees WHERE salary > 80000"""; System.out.println(query); } }
The indentation stripping is what JavaScript lacks, and it is why template literals in JS codebases are so often written flush against the left margin. Text blocks perform no interpolation, though — use formatted() for that.
Splitting and regular expressions
Java has no regex literal — a pattern is a String, which means backslashes must be escaped twice (\\d for a digit). Pattern and Matcher replace the methods JavaScript hangs off strings.
const line = "Ada,36,Portland"; console.log(line.split(",")); console.log(/\d+/.test(line)); console.log(line.match(/\d+/)[0]); console.log(line.replace(/\d+/g, "#"));
import java.util.Arrays; import java.util.regex.Matcher; import java.util.regex.Pattern; class Main { public static void main(String[] args) { String line = "Ada,36,Portland"; System.out.println(Arrays.toString(line.split(","))); Pattern digits = Pattern.compile("\\d+"); Matcher matcher = digits.matcher(line); System.out.println(matcher.find()); System.out.println(matcher.group()); System.out.println(line.replaceAll("\\d+", "#")); } }
The double backslash is the most common transcription error moving a regex from JavaScript to Java. Pattern.compile outside a loop is worth doing — unlike a JavaScript regex literal, the compilation is not cached for you.
Arrays & Collections
Arrays have a fixed length and a type
A Java array is a fixed-size, single-type block chosen at creation. It cannot grow, and indexing out of bounds throws rather than returning undefined. It is much closer to a typed array than to a JavaScript array.
const languages = []; languages.push("JavaScript"); languages.push("Java"); console.log(languages.length, languages[0]); console.log(languages[99]);
import java.util.Arrays; class Main { public static void main(String[] args) { String[] languages = new String[2]; languages[0] = "JavaScript"; languages[1] = "Java"; System.out.println(languages.length + " " + languages[0]); System.out.println(Arrays.toString(languages)); try { System.out.println(languages[99]); } catch (ArrayIndexOutOfBoundsException error) { System.out.println("out of bounds, not undefined"); } } }
Printing an array directly gives you [Ljava.lang.String;@1b6d3586 — arrays do not override toString, so Arrays.toString is required. The out-of-bounds throw is a real improvement: the error surfaces at the access rather than as an undefined traveling through your program.
ArrayList is the growable one
List is the interface and ArrayList the usual implementation — you declare the interface and instantiate the class. This split, which JavaScript has no counterpart for, runs through the whole collections framework.
const languages = ["JavaScript"]; languages.push("Java"); languages.shift(); console.log(languages, languages.length); console.log(languages.includes("Java"));
import java.util.ArrayList; import java.util.List; class Main { public static void main(String[] args) { List<String> languages = new ArrayList<>(List.of("JavaScript")); languages.add("Java"); languages.remove(0); System.out.println(languages + " " + languages.size()); System.out.println(languages.contains("Java")); } }
List.of(...) builds an immutable list — calling add on it throws UnsupportedOperationException, which is why it is wrapped in new ArrayList<>(...) here. Note also size() is a method, unlike the length property on arrays.
Sorting is type-aware by default
Java sorts using each element's natural ordering, so numbers sort numerically with no comparator. This is the mirror of JavaScript's notorious lexicographic default.
const cities = ["Oslo", "Reykjavik", "Bergen"]; console.log([...cities].sort()); console.log([...cities].sort((left, right) => left.length - right.length));
import java.util.ArrayList; import java.util.Comparator; import java.util.List; class Main { public static void main(String[] args) { List<Integer> values = new ArrayList<>(List.of(10, 9, 100, 1)); values.sort(null); System.out.println(values); List<String> names = new ArrayList<>(List.of("Charlie", "ada", "Bob")); names.sort(Comparator.comparing(String::toLowerCase)); System.out.println(names); } }
You never get JavaScript's [1, 10, 100, 9] surprise, because the compiler knows the elements are Integer. Comparator.comparing composes with thenComparing and reversed(), which is more expressive than a hand-written comparison function.
Sets and immutable collections
Set deduplicates by equals/hashCode, so structurally equal records collapse — JavaScript's Set compares by identity and would keep both. The List.of/Set.of factories return genuinely immutable collections.
const unique = new Set([1, 2, 2, 3]); console.log(unique.size, [...unique]); const frozen = Object.freeze([1, 2, 3]); try { frozen.push(4); } catch (error) { console.log("frozen:", error.constructor.name); }
import java.util.HashSet; import java.util.List; import java.util.Set; class Main { public static void main(String[] args) { Set<Integer> unique = new HashSet<>(List.of(1, 2, 2, 3)); System.out.println(unique.size() + " " + unique); List<Integer> immutable = List.of(1, 2, 3); try { immutable.add(4); } catch (UnsupportedOperationException error) { System.out.println("immutable: " + error.getClass().getSimpleName()); } } }
Note Set.of(1, 2, 2, 3) would throw rather than deduplicate — the factories reject duplicate elements, which is why this builds the set from a List. Immutability here throws at runtime rather than being caught by the compiler, unlike final.
Maps & Sets
HashMap and structural keys
HashMap is the Map counterpart, with one significant advantage: keys are compared with equals/hashCode, so two structurally equal keys are the same key. JavaScript's Map compares by identity and cannot do this.
const ages = new Map(); ages.set("Ada", 36); console.log(ages.get("Ada"), ages.has("Bob"), ages.size); const keyOne = { id: 1 }; const keyTwo = { id: 1 }; const byObject = new Map([[keyOne, "first"]]); console.log(byObject.get(keyTwo));
import java.util.HashMap; import java.util.Map; record EmployeeId(int id) {} class Main { public static void main(String[] args) { Map<String, Integer> ages = new HashMap<>(); ages.put("Ada", 36); System.out.println(ages.get("Ada") + " " + ages.containsKey("Bob") + " " + ages.size()); Map<EmployeeId, String> byRecord = new HashMap<>(); byRecord.put(new EmployeeId(1), "first"); System.out.println(byRecord.get(new EmployeeId(1))); } }
The last line prints first where JavaScript prints undefined. A record generates equals and hashCode from its components, making it a correct map key for free — this is one of the clearest wins Java has over JavaScript.
Iterating and updating a map
Iteration goes through entrySet(). Java also offers atomic update helpers — merge, computeIfAbsent, getOrDefault — that replace the read-check-write dance JavaScript requires.
const counts = {}; for (const word of ["a", "b", "a"]) { counts[word] = (counts[word] ?? 0) + 1; } for (const [word, count] of Object.entries(counts)) { console.log(`${word} = ${count}`); }
import java.util.LinkedHashMap; import java.util.List; import java.util.Map; class Main { public static void main(String[] args) { Map<String, Integer> counts = new LinkedHashMap<>(); for (String word : List.of("a", "b", "a")) { counts.merge(word, 1, Integer::sum); } for (Map.Entry<String, Integer> entry : counts.entrySet()) { System.out.println(entry.getKey() + " = " + entry.getValue()); } } }
A plain HashMap has no defined iteration order — use LinkedHashMap when insertion order matters, as it does here, or TreeMap for sorted keys. JavaScript objects and Map both preserve insertion order, so this is a distinction you have not had to make before.
Sorted and ordered maps
JavaScript gives you exactly one iteration order: insertion. Java lets you choose the data structure — HashMap (unordered), LinkedHashMap (insertion), or TreeMap (sorted by key).
const scores = new Map([["Charlie", 70], ["ada", 96], ["Bob", 81]]); console.log([...scores.keys()]); const sorted = [...scores.entries()].sort(([left], [right]) => left.localeCompare(right) ); console.log(sorted.map(([name]) => name));
import java.util.Map; import java.util.TreeMap; class Main { public static void main(String[] args) { Map<String, Integer> scores = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); scores.put("Charlie", 70); scores.put("ada", 96); scores.put("Bob", 81); System.out.println(scores.keySet()); TreeMap<String, Integer> tree = new TreeMap<>(scores); System.out.println(tree.firstKey() + " " + tree.lastKey()); } }
A TreeMap keeps keys sorted at all times rather than sorting on demand, and adds navigation methods — firstKey, headMap, ceilingKey — that have no JavaScript counterpart short of re-sorting an array.
Control Flow
Switch expressions — no fall-through
Modern Java's arrow form is an expression that produces a value, with no fall-through and no break. The compiler also checks exhaustiveness. JavaScript has nothing comparable.
const day = "SATURDAY"; let kind; switch (day) { case "SATURDAY": case "SUNDAY": kind = "weekend"; break; default: kind = "weekday"; } console.log(kind);
class Main { public static void main(String[] args) { int status = 404; String category = switch (status) { case 200, 201, 204 -> "success"; case 301, 302 -> "redirect"; case 400, 401, 403, 404 -> "client error"; default -> "server error"; }; System.out.println(category); } }
Because it is an expression, the result can be assigned to a final variable in one statement — the JavaScript version needs a mutable let declared before the switch. Forgetting break is not a possible bug in this form.
Pattern matching over sealed types
A sealed interface lists exactly which types may implement it, and switch can then match on type and destructure records in one step. The compiler rejects the switch if a permitted type is unhandled — a discriminated union with real checking.
const shapes = [ { kind: "circle", radius: 2 }, { kind: "rectangle", width: 3, height: 4 }, ]; for (const shape of shapes) { switch (shape.kind) { case "circle": console.log((Math.PI * shape.radius ** 2).toFixed(2)); break; case "rectangle": console.log((shape.width * shape.height).toFixed(2)); break; } }
import java.util.List; sealed interface Shape permits Circle, Rectangle {} record Circle(double radius) implements Shape {} record Rectangle(double width, double height) implements Shape {} class Main { public static void main(String[] args) { List<Shape> shapes = List.of(new Circle(2), new Rectangle(3, 4)); for (Shape shape : shapes) { double area = switch (shape) { case Circle circle -> Math.PI * circle.radius() * circle.radius(); case Rectangle(double width, double height) -> width * height; }; System.out.printf("%.2f%n", area); } } }
Note there is no default branch and none is needed — permits tells the compiler the list is complete. The Rectangle(double width, double height) case is a record deconstruction pattern, binding the components directly, which is closer to JavaScript destructuring than anything else in Java.
Loops and iteration
The enhanced for loop is Java's for...of. There is no built-in way to get an index alongside the element, so a classic indexed loop is still common.
const languages = ["JavaScript", "Java"]; for (const language of languages) { console.log(language); } for (const [index, language] of languages.entries()) { console.log(`${index}: ${language}`); }
import java.util.List; class Main { public static void main(String[] args) { List<String> languages = List.of("JavaScript", "Java"); for (String language : languages) { System.out.println(language); } for (int index = 0; index < languages.size(); index++) { System.out.println(index + ": " + languages.get(index)); } } }
Note that languages.get(index) is O(1) on an ArrayList but O(n) on a LinkedList — the interface hides a performance characteristic that the indexed loop then depends on. This is why the enhanced for loop is preferred where the index is not needed.
Ternaries and labeled breaks
Both constructs are identical in the two languages, down to the punctuation — a rare case of complete agreement. The only Java restriction is that both ternary branches must have compatible types.
const score = 96; console.log(score >= 90 ? "A" : "B"); outer: for (let row = 0; row < 3; row++) { for (let column = 0; column < 3; column++) { if (row * column > 2) break outer; console.log(`${row},${column}`); } }
class Main { public static void main(String[] args) { int score = 96; System.out.println(score >= 90 ? "A" : "B"); outer: for (int row = 0; row < 3; row++) { for (int column = 0; column < 3; column++) { if (row * column > 2) break outer; System.out.println(row + "," + column); } } } }
A ternary mixing incompatible branches — flag ? "text" : 42 — is a compile error in Java but perfectly legal in JavaScript, where the result is simply a union of whatever the branches produce.
Methods
Method overloading
Java picks among same-named methods by argument types and count, resolved at compile time. JavaScript has no overloading at all, so this replaces the default-parameter and argument-sniffing patterns you are used to.
function price(amount, currency = "USD") { return `${amount.toFixed(2)} ${currency}`; } console.log(price(19.5)); console.log(price(19.5, "ISK"));
class Main { static String price(double amount) { return price(amount, "USD"); } static String price(double amount, String currency) { return String.format("%.2f %s", amount, currency); } public static void main(String[] args) { System.out.println(price(19.5)); System.out.println(price(19.5, "ISK")); } }
Java has no default parameter values, so the one-argument overload delegating to the fuller one is the standard idiom. Overload resolution happens on the static type of the arguments, which occasionally surprises people expecting runtime dispatch.
Varargs
A parameter written int... values accepts any number of arguments and arrives as an array. It must be the last parameter, and there is no equivalent of collecting into a growable list.
function sum(...values) { return values.reduce((total, value) => total + value, 0); } console.log(sum(1, 2, 3)); console.log(sum()); console.log(sum(...[4, 5]));
class Main { static int sum(int... values) { int total = 0; for (int value : values) { total += value; } return total; } public static void main(String[] args) { System.out.println(sum(1, 2, 3)); System.out.println(sum()); System.out.println(sum(new int[] { 4, 5 })); } }
Spreading an existing array means passing the array itself, since a varargs parameter is an array — there is no ... spread operator at the call site. Mixing varargs with overloading makes resolution genuinely hard to predict, so most codebases avoid the combination.
Classes & Records
Classes declare types and control access
A Java class defines a genuine type — a value either is an Employee or the code does not compile. Access modifiers (private, protected, public) are enforced by the compiler on every member.
class Employee { #salary; constructor(name, salary) { this.name = name; this.#salary = salary; } describe() { return `${this.name} earns ${this.#salary}`; } } console.log(new Employee("Ada", 90000).describe()); const impostor = { describe: () => "not an Employee" }; console.log(impostor.describe());
class Employee { private final String name; private double salary; Employee(String name, double salary) { this.name = name; this.salary = salary; } String describe() { return name + " earns " + salary; } } class Main { public static void main(String[] args) { Employee employee = new Employee("Ada", 90000); System.out.println(employee.describe()); // Employee other = new Object(); // compile error: incompatible types } }
The duck-typed impostor in the JavaScript column has no Java equivalent — an object of an unrelated class cannot stand in, however matching its methods. this also always means the receiver here; it can never be lost by detaching a method.
Records — object literals with a type
A record declares an immutable data carrier in one line, generating the constructor, accessors, equals, hashCode, and toString. It is the closest thing Java has to a JavaScript object literal, with a type attached.
const first = { name: "Ada", salary: 90000 }; const second = { name: "Ada", salary: 90000 }; console.log(first); console.log(first === second); console.log(JSON.stringify(first) === JSON.stringify(second));
record Employee(String name, double salary) {} class Main { public static void main(String[] args) { Employee first = new Employee("Ada", 90000); Employee second = new Employee("Ada", 90000); System.out.println(first); System.out.println(first == second); System.out.println(first.equals(second)); System.out.println(first.name()); } }
Structural equality comes free, which JavaScript can only approximate with the order-sensitive JSON.stringify trick. Accessors are named after the component — name(), not getName() — and records are shallowly immutable, so there is no setter to write.
Inheritance and @Override
extends and super read as they do in JavaScript. The addition is @Override: an annotation that makes the compiler verify you are actually overriding something, catching the misspelling that silently defines a new method in JavaScript.
class Animal { constructor(name) { this.name = name; } speak() { return `${this.name} makes a sound`; } } class Dog extends Animal { speak() { return `${super.speak()} — a bark`; } } console.log(new Dog("Rex").speak());
class Animal { protected final String name; Animal(String name) { this.name = name; } String speak() { return name + " makes a sound"; } } class Dog extends Animal { Dog(String name) { super(name); } @Override String speak() { return super.speak() + " — a bark"; } } class Main { public static void main(String[] args) { System.out.println(new Dog("Rex").speak()); } }
A subclass constructor must call super(...) first — there is no way to touch this before the parent is initialized. Java also has no prototype chain to inspect or modify: the hierarchy is fixed at compile time.
Static members and initialization
A static member belongs to the class rather than an instance, as in JavaScript. Java adds a static initializer block that runs once when the class is first loaded — there is no direct equivalent.
class Counter { static created = 0; static describe() { return `created ${Counter.created}`; } constructor() { Counter.created += 1; } } new Counter(); new Counter(); console.log(Counter.describe());
class Counter { static int created; static final String LABEL; static { LABEL = "counter"; } Counter() { created++; } static String describe() { return LABEL + " created " + created; } } class Main { public static void main(String[] args) { new Counter(); new Counter(); System.out.println(Counter.describe()); } }
A static final field assigned in a static block is a constant computed once at class-load time. Note that a static method cannot reference this or any instance field, which the compiler enforces — JavaScript would let the mistake reach runtime.
Interfaces & Nominal Typing
Interfaces must be declared, not inferred
Java is nominally typed: a class works as a Describable only if it says implements Describable. Matching the method signatures is not enough — the opposite of JavaScript's duck typing.
// Any object with the method works — nothing to declare. const employee = { describe: () => "Ada, engineer" }; const project = { describe: () => "Apollo, active" }; function show(item) { console.log(item.describe()); } show(employee); show(project);
import java.util.List; interface Describable { String describe(); } record Employee(String name) implements Describable { public String describe() { return name + ", engineer"; } } record Project(String title) implements Describable { public String describe() { return title + ", active"; } } class Main { public static void main(String[] args) { List<Describable> items = List.of(new Employee("Ada"), new Project("Apollo")); for (Describable item : items) { System.out.println(item.describe()); } } }
The cost is ceremony; the benefit is that the compiler knows every implementor and refuses a type that does not qualify. A class may implement many interfaces but extend only one class, which is how Java approaches multiple inheritance.
Default methods
An interface method may carry an implementation with default, letting an interface add behavior without breaking existing implementors. It is roughly what a mixin does in JavaScript, but type-checked.
const greetable = { greet() { return `Hello, ${this.name}`; }, }; const person = Object.assign({ name: "Ada" }, greetable); console.log(person.greet());
interface Greetable { String name(); default String greet() { return "Hello, " + name(); } } record Person(String name) implements Greetable {} class Main { public static void main(String[] args) { System.out.println(new Person("Ada").greet()); } }
The Person record satisfies name() automatically with its generated accessor, so the whole type is one line. Unlike a JavaScript mixin, nothing is copied at runtime and the compiler resolves conflicts between two interfaces offering the same default.
Generics
Generics — types with parameters
A generic type takes another type as a parameter, so List<String> is checked to contain only strings. JavaScript has no equivalent; this is territory a JavaScript developer only meets in TypeScript.
const names = ["Ada", "Bob"]; names.push(42); console.log(names); // Nothing prevents a mixed array.
import java.util.ArrayList; import java.util.List; class Main { static <T> T firstOrDefault(List<T> items, T fallback) { return items.isEmpty() ? fallback : items.get(0); } public static void main(String[] args) { List<String> names = new ArrayList<>(List.of("Ada", "Bob")); // names.add(42); // compile error: incompatible types System.out.println(firstOrDefault(names, "nobody")); System.out.println(firstOrDefault(new ArrayList<Integer>(), 0)); } }
The <T> before the return type declares the type parameter, and it is inferred at each call site. Generics are erased at runtime — the bytecode does not know T — which is why you cannot write new T[] or test instanceof List<String>.
Lambdas & Streams
Lambdas need a functional interface
Java lambdas look like arrow functions but are not free-floating values — each has an interface type with exactly one abstract method. Function, Predicate, Supplier, and Consumer cover most cases.
const withTax = (amount) => amount * 1.24; console.log(withTax(100).toFixed(2)); const compose = (outer, inner) => (value) => outer(inner(value)); const roundedWithTax = compose(Math.round, withTax); console.log(roundedWithTax(100));
import java.util.function.Function; class Main { public static void main(String[] args) { Function<Double, Double> withTax = amount -> amount * 1.24; System.out.printf("%.2f%n", withTax.apply(100.0)); Function<Double, Long> roundedWithTax = withTax.andThen(Math::round); System.out.println(roundedWithTax.apply(100.0)); } }
The lambda must be invoked through its interface method — doubler.apply(21), not doubler(21). A Java lambda also captures this from the enclosing instance and never rebinds it, so the arrow-versus-function distinction that matters so much in JavaScript has no counterpart.
Streams — lazy pipelines
A stream is opened with .stream(), transformed, and closed with a terminal operation like collect or sum. Unlike JavaScript array methods, the pipeline is lazy: nothing runs until the terminal operation, and no intermediate arrays are built.
const employees = [ { name: "Ada", salary: 90000 }, { name: "Bob", salary: 62000 }, { name: "Cleo", salary: 105000 }, ]; const wellPaid = employees .filter((employee) => employee.salary > 80000) .map((employee) => employee.name); console.log(wellPaid); console.log(employees.reduce((sum, employee) => sum + employee.salary, 0));
import java.util.List; import java.util.stream.Collectors; record Employee(String name, double salary) {} class Main { public static void main(String[] args) { List<Employee> employees = List.of( new Employee("Ada", 90000), new Employee("Bob", 62000), new Employee("Cleo", 105000)); List<String> wellPaid = employees.stream() .filter(employee -> employee.salary() > 80000) .map(Employee::name) .collect(Collectors.toList()); System.out.println(wellPaid); double payroll = employees.stream().mapToDouble(Employee::salary).sum(); System.out.println(payroll); } }
A stream is single-use — reusing one throws IllegalStateException, where a JavaScript array can be iterated forever. The mapToInt step exists to escape boxing, a concern with no JavaScript analog.
Grouping with Collectors
Collectors.groupingBy is the counterpart of Object.groupBy, and it preserves the key's real type instead of stringifying it. Collectors compose, so counting or summing per group is a second argument rather than a second pass.
const words = ["apple", "avocado", "banana"]; const grouped = Object.groupBy(words, (word) => word[0]); console.log(grouped.a.length); const lengths = {}; for (const word of words) { lengths[word.length] = (lengths[word.length] ?? 0) + 1; } console.log(lengths);
import java.util.List; import java.util.Map; import java.util.stream.Collectors; class Main { public static void main(String[] args) { List<String> words = List.of("apple", "avocado", "banana"); Map<Character, List<String>> grouped = words.stream() .collect(Collectors.groupingBy(word -> word.charAt(0))); System.out.println(grouped.get('a').size()); Map<Integer, Long> lengths = words.stream() .collect(Collectors.groupingBy(String::length, Collectors.counting())); System.out.println(lengths); } }
The key type is Character and Integer here, not a string — JavaScript's Object.groupBy coerces every key to a string, so grouping by a number gives you "5". Use Map.groupBy there when that matters.
Method references
A method reference like String::toUpperCase is shorthand for a lambda that calls that method. Unlike a detached JavaScript method, it carries its receiver correctly — there is no this to lose.
const names = ["ada", "bob"]; console.log(names.map((name) => name.toUpperCase())); // Passing the bare method loses its receiver. const upper = String.prototype.toUpperCase; try { console.log(names.map(upper)); } catch (error) { console.log("lost this:", error.constructor.name); }
import java.util.List; import java.util.stream.Collectors; class Main { public static void main(String[] args) { List<String> names = List.of("ada", "bob"); System.out.println(names.stream() .map(String::toUpperCase) .collect(Collectors.toList())); System.out.println(names.stream() .map(Main::shout) .collect(Collectors.toList())); } static String shout(String text) { return text.toUpperCase() + "!"; } }
There are four forms — static, bound instance, unbound instance, and constructor (Employee::new). The unbound form used here, String::toUpperCase, treats the stream element as the receiver, which is exactly the case JavaScript cannot express without bind or a wrapper arrow.
null & Optional
One null, and it throws
Java has null and no undefined. A missing map key returns null, and calling a method on it throws NullPointerException rather than propagating quietly.
const employee = {}; console.log(employee.address); console.log(employee.address?.city); console.log(employee.address?.city ?? "UNKNOWN");
import java.util.HashMap; import java.util.Map; class Main { public static void main(String[] args) { Map<String, String> employee = new HashMap<>(); String city = employee.get("city"); System.out.println(city); try { System.out.println(city.toUpperCase()); } catch (NullPointerException error) { System.out.println("NPE instead of undefined"); } System.out.println(employee.getOrDefault("city", "UNKNOWN")); } }
There is no ?. operator for general use, so a deep access needs either nested checks or Optional. Modern JVMs produce helpful NPE messages naming the exact expression that was null, which makes the failure far easier to diagnose than a stray undefined.
Optional as a return type
Optional<T> makes absence part of the signature, so a caller cannot ignore it by accident. It is a wrapper you chain with map and unwrap with orElse — the closest structural analog to ?. plus ??.
const employees = [{ name: "Ada", city: "Portland" }]; const found = employees.find((employee) => employee.name === "Bob"); console.log(found?.city ?? "UNKNOWN"); const present = employees.find((employee) => employee.name === "Ada"); console.log(present?.city?.toUpperCase() ?? "UNKNOWN");
import java.util.List; import java.util.Optional; record Employee(String name, String city) {} class Main { static Optional<Employee> findByName(List<Employee> employees, String name) { return employees.stream().filter(employee -> employee.name().equals(name)).findFirst(); } public static void main(String[] args) { List<Employee> employees = List.of(new Employee("Ada", "Portland")); System.out.println(findByName(employees, "Bob").map(Employee::city).orElse("UNKNOWN")); System.out.println(findByName(employees, "Ada") .map(Employee::city).map(String::toUpperCase).orElse("UNKNOWN")); } }
Use Optional for return types, not for fields or parameters — that is the community convention, and it is not enforced. Calling get() without checking defeats the purpose and throws, so prefer orElse, orElseThrow, or ifPresent.
Null-safe helpers
The Objects utility class covers the checks JavaScript writes with ?? and ?.: requireNonNullElse for defaults, requireNonNull to fail fast, and equals for a null-tolerant comparison.
const supplied = null; console.log(supplied ?? "default"); function create(name) { if (name == null) throw new TypeError("name is required"); return name; } try { create(null); } catch (error) { console.log("rejected:", error.message); } console.log(null === null);
import java.util.Objects; class Main { static String create(String name) { return Objects.requireNonNull(name, "name is required"); } public static void main(String[] args) { String supplied = null; System.out.println(Objects.requireNonNullElse(supplied, "default")); try { create(null); } catch (NullPointerException error) { System.out.println("rejected: " + error.getMessage()); } System.out.println(Objects.equals(null, null)); } }
Objects.equals is the null-safe comparison — calling first.equals(second) throws when first is null, a mistake JavaScript's === makes impossible. requireNonNull at a method's entry converts a distant, confusing NPE into an immediate one that names the parameter.
Exceptions
Checked exceptions must be declared
This has no JavaScript counterpart at all. A checked exception must be either caught or declared with throws, and the compiler refuses to build code that does neither. Failure modes become part of the signature.
function readConfig(path) { if (!path) throw new Error("no path given"); return "contents"; } // Nothing forces a caller to handle this. console.log(readConfig("app.json")); try { readConfig(""); } catch (error) { console.log("caught:", error.message); }
class ConfigException extends Exception { ConfigException(String message) { super(message); } } class Main { static String readConfig(String path) throws ConfigException { if (path.isEmpty()) { throw new ConfigException("no path given"); } return "contents"; } public static void main(String[] args) { try { System.out.println(readConfig("app.json")); readConfig(""); } catch (ConfigException error) { System.out.println("caught: " + error.getMessage()); } } }
Extending Exception makes it checked; extending RuntimeException makes it unchecked and JavaScript-like. Whether checked exceptions are worth the friction is Java's longest-running argument — but the compiler genuinely will not let you forget a documented failure.
Typed and multi-catch clauses
Each catch names the type it handles, so dispatching on error type happens in the clause header rather than with instanceof inside the block. One clause can list several types with |.
function parse(text) { const value = Number(text); if (Number.isNaN(value)) throw new TypeError("not a number"); if (value < 0) throw new RangeError("must be positive"); return value; } for (const input of ["abc", "-1", "5"]) { try { console.log(parse(input)); } catch (error) { if (error instanceof TypeError) console.log("type:", error.message); else if (error instanceof RangeError) console.log("range:", error.message); } }
import java.util.List; class Main { static int parse(String text) { int value = Integer.parseInt(text); if (value < 0) { throw new IllegalStateException("must be positive"); } return value; } public static void main(String[] args) { for (String input : List.of("abc", "-1", "5")) { try { System.out.println(parse(input)); } catch (NumberFormatException | IllegalStateException error) { System.out.println(error.getClass().getSimpleName() + ": " + error.getMessage()); } } } }
Multi-catch alternatives may not be related by subclassing — NumberFormatException | IllegalArgumentException does not compile, because the first extends the second and the broader one already covers it. Clauses are otherwise tried top to bottom, so a more specific type must precede a broader one. Only Throwable subclasses can be thrown at all, unlike JavaScript where a bare string is legal.
try-with-resources
A resource declared in the try (...) header is closed automatically when the block exits, in reverse order, even on an exception. Any type implementing AutoCloseable qualifies. JavaScript has no equivalent and relies on a hand-written finally.
const open = (label) => ({ label, close: () => console.log(`closed ${label}`), }); const input = open("input"); try { const output = open("output"); try { console.log("copying"); } finally { output.close(); } } finally { input.close(); }
record Stream(String label) implements AutoCloseable { @Override public void close() { System.out.println("closed " + label); } } class Main { public static void main(String[] args) { try (Stream input = new Stream("input"); Stream output = new Stream("output")) { System.out.println("copying"); } } }
Correct cleanup is the shortest thing to write here, which is the opposite of the JavaScript situation where forgetting finally silently leaks. A stage-3 using declaration is coming to JavaScript but is not yet dependable.
Threads & Concurrency
Real threads, and what they cost
Java code runs on real OS threads that execute in parallel and can be preempted mid-statement. Nothing here corresponds to the single-threaded event loop — and blocking is normal rather than forbidden.
console.log("first"); setTimeout(() => console.log("deferred"), 0); Promise.resolve().then(() => console.log("microtask")); console.log("last");
class Main { public static void main(String[] args) throws InterruptedException { Thread reader = new Thread(() -> System.out.println("reader finished"), "reader"); Thread writer = new Thread(() -> System.out.println("writer finished"), "writer"); reader.start(); writer.start(); reader.join(); writer.join(); System.out.println("both joined on " + Thread.currentThread().getName()); } }
A blocking call like join() or Thread.sleep() stops only that thread; the equivalent in JavaScript would freeze the entire program. In exchange you inherit data races and must reason about shared mutable state, which JavaScript never forces on you.
Shared mutable state needs protection
Because threads run simultaneously, count++ is not atomic — two threads can interleave and lose an update. AtomicInteger or a synchronized block makes it safe. This entire category of bug cannot occur in JavaScript.
// A data race is impossible: nothing preempts running code. let count = 0; const increment = () => { count++; }; for (let index = 0; index < 1000; index++) increment(); console.log(count);
import java.util.concurrent.atomic.AtomicInteger; class Main { public static void main(String[] args) throws InterruptedException { AtomicInteger safeCount = new AtomicInteger(); Thread first = new Thread(() -> { for (int index = 0; index < 1000; index++) safeCount.incrementAndGet(); }); Thread second = new Thread(() -> { for (int index = 0; index < 1000; index++) safeCount.incrementAndGet(); }); first.start(); second.start(); first.join(); second.join(); System.out.println(safeCount.get()); } }
Replacing AtomicInteger with a plain int here would usually print something under 2000, and would do so intermittently — the worst kind of bug. The absence of this whole problem is the strongest argument for JavaScript's single-threaded model.
CompletableFuture — promises with a get()
CompletableFuture is the Promise counterpart: thenApply chains like then, and allOf matches Promise.all. The difference is that get() blocks — legal here because another thread can still make progress.
const delayed = (value, ms) => new Promise((resolve) => setTimeout(() => resolve(value), ms)); async function main() { const doubled = await delayed(21, 10).then((value) => value * 2); console.log(doubled); console.log(await Promise.all([delayed(1, 20), delayed(2, 10)])); } main();
import java.util.List; import java.util.concurrent.CompletableFuture; class Main { public static void main(String[] args) throws Exception { CompletableFuture<Integer> doubled = CompletableFuture.supplyAsync(() -> 21).thenApply(value -> value * 2); System.out.println(doubled.get()); CompletableFuture<Integer> first = CompletableFuture.supplyAsync(() -> 1); CompletableFuture<Integer> second = CompletableFuture.supplyAsync(() -> 2); CompletableFuture.allOf(first, second).join(); System.out.println(List.of(first.get(), second.get())); } }
allOf returns CompletableFuture<Void> and does not collect results, so each future must be read individually — Promise.all hands you the array directly. Note the throws Exception on main: get() is checked.
Virtual threads
Virtual threads are cheap enough to create by the thousand, so blocking code scales like async code without being rewritten. They are Java's answer to the problem async/await solves — reached from the opposite direction, by making blocking cheap instead of avoiding it.
const delayed = (value, ms) => new Promise((resolve) => setTimeout(() => resolve(value), ms)); async function main() { const tasks = Array.from({ length: 5 }, (unused, index) => delayed(index, 5)); const results = await Promise.all(tasks); console.log(results.length, results[4]); } main();
import java.util.ArrayList; import java.util.List; class Main { public static void main(String[] args) throws InterruptedException { List<Thread> workers = new ArrayList<>(); List<Integer> results = java.util.Collections.synchronizedList(new ArrayList<>()); for (int index = 0; index < 5; index++) { int value = index; workers.add(Thread.ofVirtual().start(() -> { try { Thread.sleep(5); } catch (InterruptedException ignored) { } results.add(value); })); } for (Thread worker : workers) { worker.join(); } System.out.println(results.size()); } }
The blocking Thread.sleep inside a virtual thread parks it without holding an OS thread, so a million of these is practical. Note the synchronizedList — the shared collection still needs protection, because virtual threads are still genuinely concurrent.
Thread pools with ExecutorService
An ExecutorService runs tasks on a managed pool rather than a thread per task. There is no JavaScript counterpart because there is no pool to manage — the event loop is the only worker.
// The closest analog is limiting concurrency by hand. async function main() { const tasks = [1, 2, 3, 4]; const results = []; for (const task of tasks) { results.push(await Promise.resolve(task * 10)); } console.log(results); } main();
import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; class Main { public static void main(String[] args) throws Exception { try (ExecutorService pool = Executors.newFixedThreadPool(2)) { List<Future<Integer>> futures = new ArrayList<>(); for (int task : List.of(1, 2, 3, 4)) { futures.add(pool.submit(() -> task * 10)); } List<Integer> results = new ArrayList<>(); for (Future<Integer> future : futures) { results.add(future.get()); } System.out.println(results); } } }
ExecutorService became AutoCloseable in JDK 19, so try-with-resources shuts the pool down and waits — before that, forgetting shutdown() left the JVM running forever. Results are collected in submission order because the futures are read in order.