PONYλM2Modula-2

Java.CodeCompared.To/JavaScript

An interactive executable cheatsheet comparing Java and JavaScript

Java 25 JavaScript (ES2025)
Output & Running
Hello, World
JavaScript has no class wrapper and no main method. A script is a list of statements, and the file itself is the entry point — the first line of the file is the first line that runs.
class Main { public static void main(String[] args) { System.out.println("Hello, World!"); } }
console.log("Hello, World!");
Everything Java requires as ceremony — the class, the public static void main signature, the String[] args parameter — has no JavaScript equivalent. There is no compilation step and no entry-point convention to satisfy.
Formatted output
Where Java reaches for System.out.printf with format specifiers, JavaScript uses a template literal — a string in backticks where ${...} interpolates any expression directly.
class Main { public static void main(String[] args) { String name = "Ada"; int score = 96; System.out.printf("%s scored %d%n", name, score); System.out.println(String.format("%.2f", 3.14159)); } }
const name = "Ada"; const score = 96; console.log(`${name} scored ${score}`); console.log((3.14159).toFixed(2));
Template literals interpolate expressions, not just variables, so ${score * 2} works. There is no printf-style format string in the language; number formatting is done with methods like toFixed and toLocaleString.
Printing to stderr and inspecting values
console.error writes to stderr, the counterpart of System.err. Unlike println, console.log accepts many arguments and prints them space-separated, and it renders objects structurally rather than calling a toString.
import java.util.List; import java.util.Map; class Main { public static void main(String[] args) { System.err.println("something went wrong"); List<Integer> numbers = List.of(1, 2, 3); Map<String, Integer> ages = Map.of("Ada", 36); System.out.println(numbers); System.out.println(ages); } }
console.error("something went wrong"); const numbers = [1, 2, 3]; const ages = { Ada: 36 }; console.log(numbers, ages);
Java relies on each class implementing toString for readable output, and a class that does not gives you ClassName@1b6d3586. JavaScript inspects the object structurally, so every value prints readably without any cooperation from its author.
Variables & Types
let and const — and never var
const is the closest thing to final and should be your default; let is the reassignable one. Both are block-scoped like Java locals. The older var is function-scoped and should not be used in new code.
class Main { public static void main(String[] args) { final int maxRetries = 3; int attemptCount = 0; attemptCount = 1; System.out.println(maxRetries + " " + attemptCount); } }
const maxRetries = 3; let attemptCount = 0; attemptCount = 1; console.log(maxRetries, attemptCount);
const prevents rebinding, not mutation — exactly like Java's final. A const array can still have elements pushed onto it, just as a final List can still be added to.
Dynamic typing and typeof
A JavaScript variable has no declared type and no type at all — values carry types, and a binding can hold any of them over its lifetime. typeof is the runtime query, roughly Java's getClass().getSimpleName().
class Main { public static void main(String[] args) { Object value = 42; System.out.println(value.getClass().getSimpleName()); value = "now a string"; System.out.println(value.getClass().getSimpleName()); } }
let value = 42; console.log(typeof value); value = "now a string"; console.log(typeof value); console.log(typeof true, typeof undefined, typeof {}, typeof []);
Note that typeof [] reports "object" — arrays are objects, and Array.isArray() is the real test. The compiler catches none of this for you; a type error surfaces at the moment the value is used, not at build time.
null and undefined — two kinds of empty
JavaScript has two absent values where Java has one. undefined means "never given a value" — an unassigned variable, a missing property, a parameter not passed. null means "deliberately set to nothing" and only appears because someone wrote it.
class Main { public static void main(String[] args) { String assigned = null; System.out.println(assigned); // Java has exactly one empty value; an unassigned local // is a compile error rather than a distinct value. System.out.println(assigned == null); } }
let neverAssigned; const deliberatelyEmpty = null; console.log(neverAssigned, deliberatelyEmpty); console.log(neverAssigned === undefined, deliberatelyEmpty === null); // The loose check that catches both, and the only good use of == console.log(neverAssigned == null, deliberatelyEmpty == null);
The idiom value == null is the one place loose equality earns its keep: it is true for exactly null and undefined and nothing else. Everywhere else, use ===.
Truthiness — where an if condition is not a boolean
Java requires a boolean in an if. JavaScript accepts any value and coerces it. Exactly seven values are falsy: false, 0, -0, 0n, "", null, undefined, and NaN. Everything else is truthy — including empty arrays and empty objects.
class Main { public static void main(String[] args) { String name = ""; // Java forces the test to be explicit. if (name != null && !name.isEmpty()) { System.out.println("has a name"); } else { System.out.println("no name"); } } }
const name = ""; if (name) { console.log("has a name"); } else { console.log("no name"); } // The trap: these are all truthy console.log(Boolean([]), Boolean({}), Boolean("0"), Boolean("false"));
The trap for a Java developer is the last line. An empty collection is falsy in many dynamic languages, but in JavaScript [] and {} are truthy — use array.length === 0 to test emptiness.
Numbers & Math
There is no integer type
Every JavaScript number is an IEEE-754 double. There is no int, long, float, or short, and no integer division — 7 / 2 is 3.5, not 3.
class Main { public static void main(String[] args) { int quotient = 7 / 2; double exact = 7.0 / 2.0; System.out.println(quotient); System.out.println(exact); System.out.println(0.1 + 0.2); } }
console.log(7 / 2); console.log(Math.trunc(7 / 2)); console.log(0.1 + 0.2); console.log((0.1 + 0.2).toFixed(2));
Java developers meet floating-point surprise only when they choose a double; in JavaScript it is unavoidable because every number is one. Use Math.trunc or Math.floor where you meant integer division, and never use raw doubles for money.
Integer range and BigInt
Because numbers are doubles, integers are only exact up to 2^53 − 1, exposed as Number.MAX_SAFE_INTEGER. Beyond it, arithmetic silently loses precision. BigInt — written with an n suffix — is the arbitrary-precision escape hatch, the counterpart of BigInteger.
import java.math.BigInteger; class Main { public static void main(String[] args) { long big = 9007199254740993L; System.out.println(big); BigInteger huge = new BigInteger("9007199254740993"); System.out.println(huge.multiply(BigInteger.TWO)); } }
console.log(Number.MAX_SAFE_INTEGER); // Silently wrong — 9007199254740993 cannot be represented console.log(9007199254740993); const huge = 9007199254740993n; console.log(huge, huge * 2n);
The dangerous part is that exceeding the safe range throws nothing — the value is simply wrong. Any Java long that can exceed 2^53 must become a BigInt or a string when it crosses into JavaScript, which is why JSON APIs often send large ids as strings.
NaN and numeric parsing
A failed numeric conversion produces NaN rather than throwing NumberFormatException. NaN is not equal to itself, so Number.isNaN is the only reliable test.
class Main { public static void main(String[] args) { try { int parsed = Integer.parseInt("abc"); System.out.println(parsed); } catch (NumberFormatException error) { System.out.println("not a number: " + error.getMessage()); } System.out.println(Integer.parseInt("42")); } }
const parsed = Number("abc"); console.log(parsed); console.log(parsed === NaN); console.log(Number.isNaN(parsed)); console.log(Number("42"), parseInt("42px", 10));
Nothing announces the failure — NaN propagates silently through arithmetic and often surfaces far from its origin. Note also that parseInt stops at the first non-numeric character, so "42px" quietly yields 42.
Strings
Comparing strings — === actually works
This is one place JavaScript is simpler. Strings are primitives, not objects, so === compares contents. The equals() discipline Java drills into you is unnecessary here.
class Main { public static void main(String[] args) { String first = "hello"; String second = "hel" + "lo"; String built = new String("hello"); System.out.println(first == second); System.out.println(first == built); System.out.println(first.equals(built)); } }
const first = "hello"; const second = "hel" + "lo"; console.log(first === second); console.log(first === "hello"); console.log("apple" < "banana");
There is no interning subtlety and no new String() trap to sidestep. Relational operators compare lexicographically, so < works directly where Java needs compareTo.
Building strings without StringBuilder
JavaScript strings are immutable, exactly as in Java, but engines optimize repeated concatenation so there is no StringBuilder in the language and none is needed. Joining an array is the idiomatic bulk approach.
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.toString()); String[] words = { "alpha", "beta", "gamma" }; System.out.println(String.join("-", words)); } }
let accumulated = ""; for (let index = 1; index <= 5; index++) { accumulated += index + ","; } console.log(accumulated); const words = ["alpha", "beta", "gamma"]; console.log(words.join("-"));
The += loop that would be a performance mistake in older Java is ordinary practice here. For assembling many pieces, building an array and calling join reads better and is what most codebases do.
Common string methods
Most methods have near-identical names, with two spelling traps: Java's length() is a property length here, and indexOf returning -1 is usually replaced by includes.
class Main { public static void main(String[] args) { String phrase = " Hello, World "; System.out.println(phrase.trim().toUpperCase()); System.out.println(phrase.length()); System.out.println(phrase.contains("World")); System.out.println("a,b,c".split(",").length); System.out.println("ha".repeat(3)); } }
const phrase = " Hello, World "; console.log(phrase.trim().toUpperCase()); console.log(phrase.length); console.log(phrase.includes("World")); console.log("a,b,c".split(",").length); console.log("ha".repeat(3));
Writing phrase.length() out of Java habit gives you TypeError: phrase.length is not a function — a very common first-day error. split returns a real array, so .length works on the result too.
Multi-line strings
Template literals span lines directly, filling the role of Java's text blocks. Unlike text blocks, they perform no incidental-indentation stripping — whatever whitespace you write is in the string.
class Main { public static void main(String[] args) { String query = """ SELECT name, salary FROM employees WHERE salary > 80000 """; System.out.println(query); } }
const query = `SELECT name, salary FROM employees WHERE salary > 80000`; console.log(query);
Because there is no de-indentation rule, a template literal indented to match surrounding code carries that indentation into the value. This is why multi-line literals are often written flush against the left margin.
Arrays
Arrays are growable and untyped
A JavaScript array is closer to ArrayList than to int[]: no fixed length, no element type, and index assignment past the end simply extends it. There is no separate array-versus-list distinction to manage.
import java.util.ArrayList; import java.util.List; class Main { public static void main(String[] args) { List<String> languages = new ArrayList<>(); languages.add("Java"); languages.add("JavaScript"); System.out.println(languages.size()); System.out.println(languages.get(0)); languages.remove(0); System.out.println(languages); } }
const languages = []; languages.push("Java"); languages.push("JavaScript"); console.log(languages.length); console.log(languages[0]); languages.shift(); console.log(languages);
Indexing out of range returns undefined rather than throwing ArrayIndexOutOfBoundsException, so an off-by-one error tends to surface later, as an undefined flowing through your code, instead of at the access.
The sort trap — numbers sort as strings
Array.prototype.sort converts elements to strings and sorts lexicographically by default. Sorting numbers without a comparator gives visibly wrong results — this is the single most notorious surprise in the language.
import java.util.Arrays; class Main { public static void main(String[] args) { int[] values = { 10, 9, 100, 1 }; Arrays.sort(values); System.out.println(Arrays.toString(values)); } }
const values = [10, 9, 100, 1]; console.log([...values].sort()); console.log([...values].sort((left, right) => left - right));
The default produces [1, 10, 100, 9] because "10" < "9" as text. Always pass a comparator for numbers. Note too that sort mutates the array in place and returns it, which is why both lines copy with [...values] first.
Destructuring and swapping
Destructuring unpacks an array into bindings positionally. Java has no equivalent for arrays, so this replaces a run of indexed assignments — and it makes a swap a single line with no temporary.
class Main { public static void main(String[] args) { int[] point = { 3, 7 }; int x = point[0]; int y = point[1]; System.out.println(x + " " + y); int temporary = x; x = y; y = temporary; System.out.println(x + " " + y); } }
const point = [3, 7]; const [x, y] = point; console.log(x, y); let first = x; let second = y; [first, second] = [second, first]; console.log(first, second); const [head, ...rest] = [1, 2, 3, 4]; console.log(head, rest);
The rest pattern ...rest collects everything remaining into a new array, which is how head/tail decomposition is written. Destructuring also works in parameter lists, which is common for options objects.
Spread — copying and concatenating
The spread operator ... expands an iterable in place. It covers copying, concatenating, and passing an array as individual arguments — three separate APIs in Java.
import java.util.ArrayList; import java.util.List; class Main { public static void main(String[] args) { List<Integer> first = List.of(1, 2); List<Integer> second = List.of(3, 4); List<Integer> combined = new ArrayList<>(first); combined.addAll(second); System.out.println(combined); System.out.println(Math.max(3, 9)); } }
const first = [1, 2]; const second = [3, 4]; console.log([...first, ...second]); const copy = [...first]; copy.push(99); console.log(first, copy); console.log(Math.max(...[3, 9, 4]));
The copy is shallow, exactly like new ArrayList<>(other) — nested objects are still shared. Spreading into a function call replaces Java's need to convert a collection to an array for a varargs method.
Objects, Maps & Sets
Object literals — records without a class
An object literal creates a structured value with no declared type. It fills the role of both a small POJO or record and an ad-hoc map, which is why JavaScript codebases declare so few classes.
record Employee(String name, String department, int salary) {} class Main { public static void main(String[] args) { Employee employee = new Employee("Ada", "Engineering", 90000); System.out.println(employee.name()); System.out.println(employee); } }
const employee = { name: "Ada", department: "Engineering", salary: 90000 }; console.log(employee.name); console.log(employee["department"]); console.log(employee); employee.startDate = "2026-01-15"; console.log(Object.keys(employee));
Properties are reachable by dot or by bracket with a computed string, and can be added after creation — an object is a live string-keyed map, not a fixed shape. Nothing checks that a property exists; a typo yields undefined rather than a compile error.
Map and Set — when the literal is not enough
Plain objects only take string and symbol keys. Map is the real HashMap counterpart: any value as a key, a genuine size, and preserved insertion order. Set matches HashSet.
import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; class Main { public static void main(String[] args) { Map<String, Integer> ages = new HashMap<>(); ages.put("Ada", 36); System.out.println(ages.get("Ada")); System.out.println(ages.containsKey("Bob")); System.out.println(ages.size()); Set<Integer> unique = new HashSet<>(List.of(1, 2, 2, 3)); System.out.println(unique.size()); } }
const ages = new Map(); ages.set("Ada", 36); console.log(ages.get("Ada")); console.log(ages.has("Bob")); console.log(ages.size); const unique = new Set([1, 2, 2, 3]); console.log(unique.size); console.log([...unique]);
Map keys are compared by identity, not by an equals/hashCode pair — two structurally identical objects are two distinct keys. There is no way to override that, which is the main behavioral gap from HashMap.
Iterating an object's entries
Object.entries turns an object into an array of [key, value] pairs, which destructuring then unpacks in the loop header — the closest thing to iterating a Java entrySet().
import java.util.LinkedHashMap; import java.util.Map; class Main { public static void main(String[] args) { Map<String, Integer> scores = new LinkedHashMap<>(); scores.put("Ada", 96); scores.put("Bob", 81); for (Map.Entry<String, Integer> entry : scores.entrySet()) { System.out.println(entry.getKey() + " = " + entry.getValue()); } } }
const scores = { Ada: 96, Bob: 81 }; for (const [name, score] of Object.entries(scores)) { console.log(`${name} = ${score}`); } console.log(Object.keys(scores), Object.values(scores));
Use Object.entries rather than the older for...in, which also walks inherited properties and is a frequent source of bugs. Note that for...of iterates values while for...in iterates keys — the opposite of what the names suggest.
Optional chaining instead of Optional
Java wraps possibly-absent values in Optional and chains with map. JavaScript leaves the value bare and makes the access safe: ?. short-circuits to undefined instead of throwing, and ?? supplies a default.
import java.util.Optional; class Main { public static void main(String[] args) { Optional<String> city = Optional.of("Portland"); System.out.println(city.map(String::toUpperCase).orElse("UNKNOWN")); Optional<String> missing = Optional.empty(); System.out.println(missing.map(String::toUpperCase).orElse("UNKNOWN")); } }
const employee = { address: { city: "Portland" } }; console.log(employee.address?.city?.toUpperCase() ?? "UNKNOWN"); const remote = {}; console.log(remote.address?.city?.toUpperCase() ?? "UNKNOWN");
?? differs from || in exactly the way that matters: it only falls through for null and undefined, so a legitimate 0 or "" survives. Reaching for || to supply a default is a common bug for values that can be zero.
Control Flow
Looping over a collection
for...of is the enhanced-for equivalent. It works on anything iterable — arrays, strings, Map, Set — and pairs with entries() when you also want the index.
import java.util.List; class Main { public static void main(String[] args) { List<String> languages = List.of("Java", "JavaScript", "Kotlin"); for (String language : languages) { System.out.println(language); } for (int index = 0; index < languages.size(); index++) { System.out.println(index + ": " + languages.get(index)); } } }
const languages = ["Java", "JavaScript", "Kotlin"]; for (const language of languages) { console.log(language); } for (const [index, language] of languages.entries()) { console.log(`${index}: ${language}`); }
The classic three-clause for loop exists and behaves identically, but is rare in modern code outside numeric ranges. Declaring the loop variable with const is normal because it is rebound fresh each iteration.
switch — still fall-through, no pattern matching
JavaScript's switch is the old C-style statement: cases fall through without break, and matching uses ===. There is no arrow form and no switch expression — the modern Java features have no counterpart.
class Main { public static void main(String[] args) { String day = "SATURDAY"; String kind = switch (day) { case "SATURDAY", "SUNDAY" -> "weekend"; default -> "weekday"; }; System.out.println(kind); } }
const day = "SATURDAY"; let kind; switch (day) { case "SATURDAY": case "SUNDAY": kind = "weekend"; break; default: kind = "weekday"; } console.log(kind); const lookup = { SATURDAY: "weekend", SUNDAY: "weekend" }; console.log(lookup[day] ?? "weekday");
Because there is no switch expression, the assignment must happen inside the statement, which is why kind is declared with let. For simple value mapping, an object lookup as shown on the last lines is the idiomatic replacement.
Labeled break
Labels work exactly as they do in Java — a rare case where the two languages agree completely, syntax included.
class Main { public static void main(String[] args) { 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); } } } }
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}`); } }
The construct is identical down to the punctuation, and continue label works too. It is used about as rarely in JavaScript as it is in Java.
Functions
Functions are values
A function is an ordinary value: assignable, passable, returnable, with no interface to implement. Where Java needs a Function or a custom functional interface to pass behavior, JavaScript passes the function itself.
import java.util.function.Function; class Main { public static void main(String[] args) { Function<Integer, Integer> doubler = value -> value * 2; System.out.println(doubler.apply(21)); System.out.println(applyTwice(doubler, 5)); } static int applyTwice(Function<Integer, Integer> operation, int start) { return operation.apply(operation.apply(start)); } }
const doubler = (value) => value * 2; console.log(doubler(21)); function applyTwice(operation, start) { return operation(operation(start)); } console.log(applyTwice(doubler, 5));
There is no Function, BiFunction, Supplier, or Consumer hierarchy, and no apply call — a function is invoked directly. Arity is not checked either: extra arguments are ignored and missing ones are undefined.
No method overloading
JavaScript has no overloading whatsoever. Declaring a second function with the same name replaces the first. Optional and default parameters do the job that overloads do in Java.
class Main { static String greet(String name) { return "Hello, " + name; } static String greet(String name, String greeting) { return greeting + ", " + name; } public static void main(String[] args) { System.out.println(greet("Ada")); System.out.println(greet("Ada", "Welcome")); } }
function greet(name, greeting = "Hello") { return `${greeting}, ${name}`; } console.log(greet("Ada")); console.log(greet("Ada", "Welcome"));
Default values are evaluated at call time and may reference earlier parameters, which makes them more capable than the overload chains they replace. Silent replacement of a same-named function is a real hazard: nothing warns you.
Rest parameters instead of varargs
A parameter written ...names collects the remaining arguments into a real array. Unlike Java's varargs it is a genuine array from the start, so every array method is available without conversion.
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()); } }
function sum(...values) { return values.reduce((total, value) => total + value, 0); } console.log(sum(1, 2, 3)); console.log(sum());
Java's varargs parameter is an array too, but it must be the last parameter and interacts awkwardly with overload resolution. Here there is no overloading to confuse, and the collected value needs no Arrays.stream to become useful.
Hoisting — calling a function before it is declared
A function declaration is hoisted: the whole function is available before the line that declares it. A function stored in a const is not — the binding exists but is unusable until initialized.
class Main { public static void main(String[] args) { // Order of declaration never matters for methods in Java. System.out.println(describe()); } static String describe() { return "declared after the call site"; } }
console.log(hoisted()); function hoisted() { return "declared after the call site"; } try { notHoisted(); } catch (error) { console.log(error.constructor.name); } const notHoisted = () => "unreachable before this line";
Java method order is irrelevant, so hoisting mostly looks like a convenience — until the const case throws a ReferenceError at runtime for what would have been a compile error in Java. Declaring before use avoids the whole topic.
`this` & Closures
this is decided by the call, not the class
This is the deepest divergence on the page. In Java this is the instance the method belongs to, fixed at compile time. In JavaScript this is determined by how the function is called — detach a method from its object and this is lost.
class Counter { private int count = 0; void increment() { this.count++; System.out.println(this.count); } } class Main { public static void main(String[] args) { Counter counter = new Counter(); counter.increment(); Runnable detached = counter::increment; detached.run(); } }
class Counter { count = 0; increment() { this.count++; console.log(this.count); } } const counter = new Counter(); counter.increment(); const detached = counter.increment; try { detached(); } catch (error) { console.log("lost this:", error.constructor.name); } const rebound = counter.increment.bind(counter); rebound();
A Java method reference carries its receiver; a JavaScript method reference does not. This is why callbacks are so often written as () => object.method() or fixed with .bind(object) — and why the arrow function in the next row exists.
Arrow functions capture this lexically
An arrow function has no this of its own — it uses the this of the scope where it was written. That makes it the safe choice for callbacks, and the wrong choice for a method that needs a receiver.
import java.util.List; class Greeter { private String greeting = "Hello"; void greetAll(List<String> names) { // A Java lambda never rebinds this; it is always the enclosing instance. names.forEach(name -> System.out.println(this.greeting + ", " + name)); } } class Main { public static void main(String[] args) { new Greeter().greetAll(List.of("Ada", "Bob")); } }
class Greeter { greeting = "Hello"; greetAll(names) { names.forEach((name) => { console.log(`${this.greeting}, ${name}`); }); } } new Greeter().greetAll(["Ada", "Bob"]);
Replacing that arrow with function (name) { ... } breaks the code — the inner this becomes undefined. Java lambdas already behave the way arrows do, so an arrow is the construct that matches your existing intuition.
Closures capture variables, not values
Java requires captured locals to be final or effectively final, so a lambda sees a frozen copy. JavaScript closures capture the binding — the function sees later changes, and can mutate the variable itself.
import java.util.function.Supplier; class Main { public static void main(String[] args) { int base = 10; // base must be effectively final to be captured Supplier<Integer> reader = () -> base + 1; System.out.println(reader.get()); } }
function makeCounter() { let count = 0; return { increment: () => ++count, current: () => count, }; } const counter = makeCounter(); counter.increment(); counter.increment(); console.log(counter.current());
This is how JavaScript expresses private state without a class — count is unreachable except through the returned functions. Achieving the same in Java takes a class with a private field, because a lambda cannot mutate what it captures.
Classes & Prototypes
Classes look familiar and are not types
The class keyword gives you a constructor, methods, fields, and #private members. What it does not give you is a type: nothing checks that a value is a Employee, and any object with the right properties works anywhere.
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) { System.out.println(new Employee("Ada", 90000).describe()); } }
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 = { name: "Bob", describe: () => "not an Employee at all" }; console.log(impostor.describe());
The #salary field is genuinely private — accessing it from outside is a syntax error, stricter than Java's reflection-defeatable private. But since there is no static type, an object of an entirely unrelated shape can stand in wherever a method is called by name.
What class actually desugars to
JavaScript has no classes underneath. Every object has a hidden link to a prototype object, and a missing property is looked up along that chain. class is syntax over this, and methods live on the shared prototype.
class Point { int x; int y; Point(int x, int y) { this.x = x; this.y = y; } String describe() { return "(" + x + ", " + y + ")"; } } class Main { public static void main(String[] args) { System.out.println(new Point(1, 2).describe()); } }
function Point(x, y) { this.x = x; this.y = y; } Point.prototype.describe = function () { return `(${this.x}, ${this.y})`; }; const point = new Point(1, 2); console.log(point.describe()); console.log(Object.getPrototypeOf(point) === Point.prototype); console.log(Object.hasOwn(point, "x"), Object.hasOwn(point, "describe"));
The last line is the point: x belongs to the instance, but describe belongs to the prototype and is shared by every instance. This is why methods can be added to a type after the fact — including to built-ins, which is powerful and widely regretted.
Inheritance and super
extends and super read exactly as in Java. The differences are that there are no interfaces to implement, no abstract keyword, and no access modifiers beyond #private.
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() + " — specifically, a bark"; } } class Main { public static void main(String[] args) { System.out.println(new Dog("Rex").speak()); } }
class Animal { constructor(name) { this.name = name; } speak() { return `${this.name} makes a sound`; } } class Dog extends Animal { constructor(name) { super(name); } speak() { return `${super.speak()} — specifically, a bark`; } } console.log(new Dog("Rex").speak()); console.log(new Dog("Rex") instanceof Animal);
There is no @Override and nothing verifies that you meant to override — a misspelled method name silently defines a new one. Since duck typing removes most of the reason to inherit, JavaScript codebases use inheritance far more sparingly than Java ones.
Static members and accessors
static works as expected. Accessors differ: get and set define properties that look like fields at the call site, so employee.annualSalary can run code without looking like a method call.
class Temperature { private final double celsius; private Temperature(double celsius) { this.celsius = celsius; } static Temperature ofCelsius(double celsius) { return new Temperature(celsius); } double getFahrenheit() { return celsius * 9 / 5 + 32; } } class Main { public static void main(String[] args) { System.out.println(Temperature.ofCelsius(100).getFahrenheit()); } }
class Temperature { constructor(celsius) { this.celsius = celsius; } static ofCelsius(celsius) { return new Temperature(celsius); } get fahrenheit() { return (this.celsius * 9) / 5 + 32; } set fahrenheit(value) { this.celsius = ((value - 32) * 5) / 9; } } const boiling = Temperature.ofCelsius(100); console.log(boiling.fahrenheit); boiling.fahrenheit = 32; console.log(boiling.celsius);
Getters make refactoring a plain field into computed behavior invisible to callers, which Java cannot do without changing every call site. The cost is that an innocuous-looking property read may execute arbitrary code.
Functional Operations
map, filter, reduce without streams
These are methods directly on Array — no .stream() to open and no .collect() to close. Each returns a new array immediately, so chains are shorter than the Java equivalent.
import java.util.List; import java.util.stream.Collectors; class Main { public static void main(String[] args) { List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6); List<Integer> result = numbers.stream() .filter(number -> number % 2 == 0) .map(number -> number * 10) .collect(Collectors.toList()); System.out.println(result); int total = numbers.stream().mapToInt(Integer::intValue).sum(); System.out.println(total); } }
const numbers = [1, 2, 3, 4, 5, 6]; const result = numbers .filter((number) => number % 2 === 0) .map((number) => number * 10); console.log(result); const total = numbers.reduce((sum, number) => sum + number, 0); console.log(total);
Because there is no lazy pipeline, each step materializes an intermediate array — fine at ordinary sizes, and simpler to reason about than stream laziness. There is also no parallel mode: parallelStream has no counterpart in a single-threaded runtime.
find, some, and every
These match findFirst, anyMatch, and allMatch, but return plain values rather than Optional or a stream — find gives you the element or undefined.
import java.util.List; import java.util.Optional; class Main { public static void main(String[] args) { List<String> languages = List.of("Java", "JavaScript", "Kotlin"); Optional<String> found = languages.stream() .filter(language -> language.startsWith("J")) .findFirst(); System.out.println(found.orElse("none")); System.out.println(languages.stream().anyMatch(language -> language.length() > 6)); System.out.println(languages.stream().allMatch(language -> language.length() > 3)); } }
const languages = ["Java", "JavaScript", "Kotlin"]; console.log(languages.find((language) => language.startsWith("J")) ?? "none"); console.log(languages.some((language) => language.length > 6)); console.log(languages.every((language) => language.length > 3)); console.log(languages.findIndex((language) => language === "Kotlin"));
Since find returns undefined rather than an empty Optional, the ?? default shown here is the usual follow-up. findIndex returns -1 when nothing matches, matching indexOf.
Grouping and flattening
Object.groupBy is the counterpart of Collectors.groupingBy, returning a plain object keyed by the grouping value. flatMap and flat handle nesting.
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", "blueberry"); Map<Character, List<String>> grouped = words.stream() .collect(Collectors.groupingBy(word -> word.charAt(0))); System.out.println(grouped.get('a')); List<List<Integer>> nested = List.of(List.of(1, 2), List.of(3, 4)); System.out.println(nested.stream().flatMap(List::stream).collect(Collectors.toList())); } }
const words = ["apple", "avocado", "banana", "blueberry"]; const grouped = Object.groupBy(words, (word) => word[0]); console.log(grouped.a); const nested = [[1, 2], [3, 4]]; console.log(nested.flatMap((pair) => pair)); console.log([1, [2, [3, [4]]]].flat(Infinity));
Object.groupBy keys are always strings because plain-object keys are strings — grouping by a number gives you "1", not 1. Use Map.groupBy when the key type must be preserved.
Equality & Comparison
=== versus == — always use ===
=== compares without conversion and is what you want. == coerces its operands first, producing results with no analog in Java. Treat == as a defect except in the == null idiom shown earlier.
class Main { public static void main(String[] args) { // Java refuses to compare unrelated types at all — these // would be compile errors, so the comparison cannot arise. System.out.println(1 == 1); System.out.println("1".equals("1")); System.out.println(Integer.valueOf(1).equals(1)); } }
console.log(1 === 1); console.log("1" === 1); console.log("1" == 1); console.log(0 == ""); console.log(null == undefined); console.log([] == false);
The last three lines are all true and none of them is defensible. Java simply rejects comparisons between unrelated types at compile time; JavaScript coerces until something matches, so linters universally ban ==.
Objects compare by identity, always
There is no equals/hashCode pair to override. Two objects are === only when they are literally the same object, and nothing in the language lets you change that.
record Point(int x, int y) {} class Main { public static void main(String[] args) { Point first = new Point(1, 2); Point second = new Point(1, 2); System.out.println(first == second); System.out.println(first.equals(second)); } }
const first = { x: 1, y: 2 }; const second = { x: 1, y: 2 }; console.log(first === second); console.log(JSON.stringify(first) === JSON.stringify(second)); const sameReference = first; console.log(first === sameReference);
Java's record gives you structural equality for free; here you must compare field by field or serialize, and the JSON.stringify trick shown is order-sensitive and unsound for anything non-trivial. Real codebases use a library for deep equality.
Error Handling
No checked exceptions
Every JavaScript error is unchecked. There is no throws clause, nothing forces a caller to handle anything, and the compiler that would have told you is absent — so failure modes must be documented rather than declared.
class Main { static int parsePositive(String text) throws IllegalArgumentException { int value = Integer.parseInt(text); if (value <= 0) { throw new IllegalArgumentException("must be positive"); } return value; } public static void main(String[] args) { try { System.out.println(parsePositive("5")); System.out.println(parsePositive("-1")); } catch (IllegalArgumentException error) { System.out.println("rejected: " + error.getMessage()); } } }
function parsePositive(text) { const value = Number(text); if (Number.isNaN(value) || value <= 0) { throw new RangeError("must be positive"); } return value; } try { console.log(parsePositive("5")); console.log(parsePositive("-1")); } catch (error) { console.log("rejected:", error.message); }
A single catch binding receives every error type; there is no multi-catch and no per-type clause, so distinguishing errors means testing instanceof inside the block. Whether this is liberating or alarming is the long-running argument between the two communities.
Custom error types
Extend the built-in Error. Setting this.name is conventional because it is what appears in stack traces and default messages — the class name alone does not surface.
class ValidationException extends RuntimeException { private final String field; ValidationException(String field, String message) { super(message); this.field = field; } String getField() { return field; } } class Main { public static void main(String[] args) { try { throw new ValidationException("email", "is not valid"); } catch (ValidationException error) { System.out.println(error.getField() + " " + error.getMessage()); } } }
class ValidationError extends Error { constructor(field, message) { super(message); this.name = "ValidationError"; this.field = field; } } try { throw new ValidationError("email", "is not valid"); } catch (error) { if (error instanceof ValidationError) { console.log(error.field, error.message); } console.log(error.name); }
Because there is no typed catch clause, the instanceof test inside the block is doing the work Java's catch (ValidationException error) does in its header. Anything at all can be thrown — including a string — so defensive code checks that it caught an Error.
finally and cleanup
finally behaves identically. What JavaScript lacks is try-with-resources: there is no AutoCloseable protocol, so cleanup is written by hand in the finally block.
class Resource implements AutoCloseable { void use() { System.out.println("using the resource"); } @Override public void close() { System.out.println("closed automatically"); } } class Main { public static void main(String[] args) { try (Resource resource = new Resource()) { resource.use(); } } }
function openResource() { return { use: () => console.log("using the resource"), close: () => console.log("closed by hand"), }; } const resource = openResource(); try { resource.use(); } finally { resource.close(); }
Forgetting the finally leaks the resource with nothing to warn you, where Java's try-with-resources makes correct cleanup the shortest thing to write. A stage-3 using declaration is coming, but is not yet something to rely on.
Async & the Event Loop
One thread, one event loop
JavaScript runs your code on a single thread. Nothing preempts a running function, so there are no locks, no synchronized, and no data races. The cost is that a slow computation blocks everything, and any waiting must be expressed as a callback rather than a blocked thread.
class Main { public static void main(String[] args) throws InterruptedException { System.out.println("first"); Thread worker = new Thread(() -> System.out.println("on another thread")); worker.start(); worker.join(); System.out.println("last"); } }
console.log("first"); setTimeout(() => console.log("deferred to the next turn"), 0); Promise.resolve().then(() => console.log("microtask, before timers")); console.log("last");
The output order — first, last, microtask, deferred — is the whole model in four lines. Synchronous code runs to completion, then queued microtasks, then timers. Nothing you schedule can interrupt code that is already running.
Promises instead of CompletableFuture
A Promise is a value that will settle later, comparable to CompletableFuture. then chains a transformation and catch handles rejection. Crucially, there is no blocking get() — you cannot wait for a promise synchronously.
import java.util.concurrent.CompletableFuture; class Main { public static void main(String[] args) throws Exception { CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 21) .thenApply(value -> value * 2); System.out.println(future.get()); } }
const promise = Promise.resolve(21).then((value) => value * 2); promise.then((value) => console.log(value)); Promise.reject(new Error("failed")) .catch((error) => console.log("caught:", error.message)) .finally(() => console.log("settled"));
The absence of get() is the structural difference: because there is one thread, blocking on a promise would deadlock the loop that is supposed to resolve it. Everything downstream of a promise must therefore also be asynchronous.
async and await
await suspends the enclosing async function until a promise settles, letting asynchronous code read top to bottom. An async function always returns a promise, and await is only legal inside one.
import java.util.concurrent.CompletableFuture; class Main { static CompletableFuture<String> fetchName() { return CompletableFuture.supplyAsync(() -> "Ada"); } public static void main(String[] args) throws Exception { String name = fetchName().get(); System.out.println("Hello, " + name); } }
function fetchName() { return new Promise((resolve) => setTimeout(() => resolve("Ada"), 10)); } async function main() { const name = await fetchName(); console.log(`Hello, ${name}`); try { await Promise.reject(new Error("nope")); } catch (error) { console.log("caught:", error.message); } } main();
A rejected promise that is awaited throws, so ordinary try/catch works — the reason await displaced then chains. Note the wrapper: this example calls main() explicitly because a CommonJS script cannot use await at the top level.
Running work concurrently
Promise.all waits for every promise and rejects as soon as one does — the counterpart of CompletableFuture.allOf, but it collects the results for you rather than returning void.
import java.util.concurrent.CompletableFuture; import java.util.List; import java.util.stream.Collectors; class Main { public static void main(String[] args) throws Exception { CompletableFuture<Integer> first = CompletableFuture.supplyAsync(() -> 1); CompletableFuture<Integer> second = CompletableFuture.supplyAsync(() -> 2); CompletableFuture.allOf(first, second).join(); List<Integer> results = List.of(first.get(), second.get()); System.out.println(results); } }
const delayed = (value, ms) => new Promise((resolve) => setTimeout(() => resolve(value), ms)); async function main() { const results = await Promise.all([delayed(1, 20), delayed(2, 10)]); console.log(results); const settled = await Promise.allSettled([ Promise.resolve("ok"), Promise.reject(new Error("bad")), ]); console.log(settled.map((entry) => entry.status)); } main();
Results come back in argument order, not completion order, even though the second promise here settles first. Use Promise.allSettled when one failure should not discard the successes — there is no allOf equivalent with that behavior.
JSON & Data
JSON is built in
There is no Jackson, no Gson, and no annotations — JSON.parse and JSON.stringify are part of the language, and they map directly onto object literals and arrays.
import java.util.Map; class Main { public static void main(String[] args) { // Java needs a third-party library (Jackson, Gson) for this; // the closest built-in is manual string assembly. Map<String, Object> employee = Map.of("name", "Ada", "salary", 90000); String assembled = "{\"name\":\"" + employee.get("name") + "\"}"; System.out.println(assembled); } }
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]); console.log(JSON.stringify(employee, null, 2));
No mapping layer stands between the JSON and your data because the object model is already JSON-shaped. The flip side is that nothing validates the parsed result — parsed.name is whatever the input contained, or undefined.
Shallow and deep copies
Spread and Object.assign copy one level deep, like a copy constructor that shares its nested references. structuredClone is the built-in deep copy, which Java has no standard equivalent for.
import java.util.ArrayList; import java.util.List; class Main { public static void main(String[] args) { List<List<Integer>> original = new ArrayList<>(); original.add(new ArrayList<>(List.of(1, 2))); List<List<Integer>> shallow = new ArrayList<>(original); shallow.get(0).add(3); System.out.println(original.get(0)); } }
const original = { name: "Ada", address: { city: "Portland" } }; const shallow = { ...original }; shallow.address.city = "Seattle"; console.log(original.address.city); const deep = structuredClone(original); deep.address.city = "Boston"; console.log(original.address.city, deep.address.city);
The shallow copy shows the shared-reference bug clearly: mutating shallow.address changed original. structuredClone handles cycles, Map, Set, and dates, but silently refuses functions — it throws on them.
Modules — import and export
ES modules use import/export with a path to a file, not a package name — there is no classpath and no package declaration. Exports are explicit: a name not exported is private to its file.
// payroll/Employee.java package payroll; public class Employee { public static final int DEFAULT_SALARY = 90000; public static String describe(String name) { return name + " earns " + DEFAULT_SALARY; } } // Main.java import payroll.Employee; import static payroll.Employee.DEFAULT_SALARY; class Main { public static void main(String[] args) { System.out.println(Employee.describe("Ada")); System.out.println(DEFAULT_SALARY); } }
// employee.js export const defaultSalary = 90000; export function describe(name) { return `${name} earns ${defaultSalary}`; } export default class Employee {} // main.js import Employee, { describe, defaultSalary } from "./employee.js"; console.log(describe("Ada"), defaultSalary, Employee.name);
A module has exactly one optional default export plus any number of named ones, which is why import syntax mixes braced and unbraced names on the same line. Java resolves payroll.Employee through the classpath and requires the directory to match the package; JavaScript resolves a relative file path and has no such rule. Both columns are display-only here because each spans two files.