PONY λ M2 Modula-2

Perl.CodeCompared.To/Java

An interactive executable cheatsheet comparing Perl and Java

Perl 5.40 Java 26
Hello World & Running
Hello, World
use v5.38; say "Hello, World!";
class Main { public static void main(String[] args) { System.out.println("Hello, World!"); } }
This is the starkest contrast between the two languages, visible from the very first line. Perl runs a script top to bottom with no ceremony at all — say "Hello, World!"; is a complete, valid program. Java requires every single line of code, even the simplest one, to live inside a class, and that class must expose an entry point method with the exact signature public static void main(String[] args). There is no such thing as top-level code in Java; the class-and-main wrapper is mandatory, not optional boilerplate that can be trimmed away.
Running a program — script vs compile step
# Run directly (no compile step): # perl hello.pl # # Check syntax only: # perl -c hello.pl # # One-liner: # perl -e 'print "Hello!\n"'
// Java has a mandatory two-step process — compile, then run: // javac Hello.java // produces Hello.class (JVM bytecode) // java Hello // loads the .class file and runs it // // Since JDK 11, a single source file can skip the manual compile step: // java Hello.java // compiles in-memory and runs immediately // // Either way, the class name must match the filename (Hello.java -> class Hello).
Perl is purely interpreted — there is no separate artifact produced before running a script, and perl -c only checks syntax without executing anything. Java has always required a distinct compile step producing JVM bytecode (a .class file) before java can run it; the single-file source-launcher shortcut added in JDK 11 (java Hello.java) hides that step for quick scripts, but the compilation still happens in memory every time — it is a convenience, not proof that Java became interpreted. A Perl programmer's instinct to just "run the file" mostly transfers with the single-file launcher, but production Java code is always compiled ahead of time into .class or .jar files.
Comments
use v5.38; # Single-line comment — the only inline comment syntax # Perl has no multi-line comment syntax. # Block documentation uses Pod: =begin comment ... =end comment my $name = "Alice"; # inline comment say $name;
class Main { public static void main(String[] args) { // Single-line comment /* Multi-line block comment */ /** Javadoc comment — documents the following declaration and is processed by the javadoc tool to generate HTML API docs. */ String name = "Alice"; // inline comment System.out.println(name); } }
Java uses the familiar C-style comment syntax — // for a single line, /* ... */ for a genuine multi-line block, and the special /** ... */ Javadoc form that the javadoc tool consumes to generate browsable HTML API documentation. This is a real improvement over Perl, which has no true multi-line comment construct at all — Perl programmers either stack consecutive # lines or reach for the Pod =begin/=end markers borrowed from documentation tooling, and Perl has nothing equivalent to Javadoc's tight integration between comments and generated documentation.
print/say vs System.out
use v5.38; print "no newline"; print "\n"; say "with newline"; # say appends a newline automatically say 42; # works with numbers too
class Main { public static void main(String[] args) { System.out.print("no newline"); System.out.print("\n"); System.out.println("with newline"); // println appends a newline automatically System.out.println(42); // works with numbers too, no coercion needed } }
Java's System.out.println() is the direct equivalent of Perl's say — both append a trailing newline automatically, and both accept a bare number with no explicit stringification required. Suppressing the newline needs System.out.print(), the mirror of Perl's print (no newline) versus say (with newline) being two separate functions. The System.out prefix itself is worth noting: it is a static field on the System class holding a PrintStream object, so every call is a genuine method call on an object, not a standalone global function the way Perl's print/say are.
Variables & Static Typing
Sigils vs explicit type declarations
use v5.38; my $name = "Alice"; my @colors = ("red", "green", "blue"); my %person = (name => "Alice", age => 30); say $name; say "@colors"; say $person{name};
import java.util.List; import java.util.Map; class Main { public static void main(String[] args) { String name = "Alice"; List<String> colors = List.of("red", "green", "blue"); Map<String, Object> person = Map.of("name", "Alice", "age", 30); System.out.println(name); System.out.println(colors); System.out.println(person.get("name")); } }
Perl marks the shape of a variable with a leading sigil — $ for a scalar, @ for an array, % for a hash — and the value can hold anything of that shape without ever declaring a type. Java instead requires every variable to carry an explicit compile-time type declaration (String, List<String>, Map<String, Object>): the type is written once at declaration and checked by the compiler forever after, rather than inferred dynamically from whatever value happens to be assigned. This is the single biggest theme running through this entire cheatsheet — nearly every section below returns to some consequence of Java's mandatory static typing.
Static typing — errors caught before running
use v5.38; my $age = "thirty"; # Perl allows this without complaint say $age + 5; # runtime warning, not a compile error: # "Argument \"thirty\" isn't numeric"
class Main { public static void main(String[] args) { int age = 30; // must be an int — the compiler enforces it System.out.println(age + 5); // int brokenAge = "thirty"; // COMPILE ERROR — caught before the // program ever runs, not a runtime surprise } }
This is the recurring theme a Perl programmer will notice throughout every section of this cheatsheet: Perl catches type mismatches, if at all, at runtime — often only as a warning, and only if that exact line of code actually executes during a particular run. Java catches them at compile time, before the program runs at all, because javac statically verifies that every expression's type matches what its context expects. The tradeoff is real: Java demands upfront ceremony (every variable, parameter, and return value needs a declared type) in exchange for a whole category of bugs — passing the wrong shape of data — being caught long before deployment rather than discovered by a user in production.
var — type inference without dynamic typing
use v5.38; my $count = 0; my @numbers = (1, 2, 3); say ref(\@numbers) ? "reference" : "not a reference";
import java.util.List; class Main { public static void main(String[] args) { var count = 0; // inferred as int var numbers = List.of(1, 2, 3); // inferred as List<Integer> System.out.println(count); System.out.println(numbers); // count is still permanently an int — var is NOT dynamic typing; // it is compile-time inference of a type that is then fixed forever. } }
The var keyword (Java 10+) lets the compiler infer a local variable's type from its initializer, saving some of the declaration verbosity a Perl programmer might expect Java to always demand. It is important not to mistake this for dynamic typing, though: the type is still determined once, at compile time, from the right-hand side of the assignment, and it is then fixed for that variable's entire lifetime — attempting to later assign a String to a var that was inferred as int is still a compile error. var is also restricted to local variables only; it cannot be used for fields, method parameters, or return types.
The compile step as a recurring theme
use v5.38; # Perl has no separate compile step a programmer interacts with directly. # Syntax errors and undeclared subroutines are typically only discovered # when that specific code path actually runs. sub risky { return undefined_subroutine_call(); # not caught until this line executes } say "This line runs fine even though risky() would fail if called.";
class Main { // Java's checked exceptions are enforced by the SAME compile step that // catches type errors — a method that can throw a checked exception // must declare it, or the compiler rejects the whole file: static void risky() throws java.io.IOException { throw new java.io.IOException("simulated failure"); } public static void main(String[] args) { System.out.println("This line only compiles because main() also " + "declares 'throws Exception' below, or wraps the call in try/catch."); // risky(); // COMPILE ERROR here without a throws clause or try/catch } }
Java's checked exceptions are really just the compile step extending its type-checking discipline to error handling: a method that can throw a checked exception must say so in its signature with throws, and every caller must either handle it or declare it too, all verified before the program runs. Perl has nothing resembling this — a subroutine can die for any reason at any time with zero declaration anywhere, and the only way to discover an unhandled error path is to actually execute it. This ceremony is the single most surprising recurring "gotcha" a Perl programmer hits throughout Java code: forgetting a throws clause or a try/catch block is a compile error, not a runtime crash.
Constants — convention vs compiler enforcement
use v5.38; use constant MAX_RETRIES => 3; use constant API_URL => "https://example.com"; say MAX_RETRIES; say API_URL;
class Main { static final int MAX_RETRIES = 3; static final String API_URL = "https://example.com"; public static void main(String[] args) { System.out.println(MAX_RETRIES); System.out.println(API_URL); // MAX_RETRIES = 5; // COMPILE ERROR: cannot assign a value to final variable } }
Perl's use constant pragma enforces immutability at compile time — reassigning one is a compile error, matching Java's guarantee here fairly closely. The Java final keyword on a field makes the binding permanently unassignable after its first value, verified by the compiler; pairing it with static makes it a single shared value belonging to the class itself rather than to any particular instance, which is the idiomatic way to declare a constant in Java. As with JavaScript's const, final only locks the reference itself — a final List<String> still permits adding elements to the list it points to, since the list's contents are a separate concern from the variable binding.
Truthiness — Perl's flexible rules vs Java's boolean-only
use v5.38; # false: undef, 0, "", "0" # true: everything else — including "00" and " " for my $value (undef, 0, "", "0", "00", " ") { my $label = defined($value) ? "'$value'" : "undef"; say "$label is " . ($value ? "truthy" : "falsy"); }
class Main { public static void main(String[] args) { // Java has no truthiness concept at all. An if condition MUST be a // genuine boolean expression — there is no automatic coercion of // a String, an int, or null into true/false. String value = "0"; // if (value) { } // COMPILE ERROR: String cannot be converted to boolean if (!value.isEmpty() && !value.equals("0")) { System.out.println("truthy-equivalent"); } else { System.out.println("falsy-equivalent"); } } }
This is a place where Java's static typing eliminates an entire category of Perl subtlety rather than merely relocating it: Java has no concept of truthiness whatsoever. An if condition must be an expression of type boolean (or the boxed Boolean), full stop — there is no automatic coercion from String, int, or a reference into true or false, and passing a non-boolean expression to if is a compile error, not a runtime surprise. Reproducing Perl's "empty string or literal \"0\" is falsy" idiom in Java always requires writing out the exact boolean condition explicitly, as shown here.
Strings
String interpolation vs concatenation/formatted
use v5.38; my $name = "Alice"; my $age = 30; say "Hello, $name! You are $age years old."; say "Next year: @{[ $age + 1 ]}"; # expression interpolation trick
class Main { public static void main(String[] args) { String name = "Alice"; int age = 30; System.out.println("Hello, " + name + "! You are " + age + " years old."); // Formatted strings (Java 15+) are closer to Perl's interpolation: System.out.println(String.format("Hello, %s! You are %d years old.", name, age)); System.out.println("Next year: " + (age + 1)); } }
Perl interpolates a bare $name directly inside a double-quoted string with no ceremony, and even embeds an arbitrary expression via the @{[ expression ]} trick. Java has no true string interpolation at all — the idiomatic approach is either plain + concatenation, which gets unwieldy fast, or String.format() with C-style %s/%d placeholders, which is closer in spirit to Perl's sprintf than to its interpolation. Java 21 introduced a string templates preview feature that would have closed this gap directly, but it was withdrawn before becoming a permanent language feature, so String.format() and concatenation remain the standard tools.
String functions vs String methods
use v5.38; my $text = " Hello, World! "; $text =~ s/^\s+|\s+$//g; # trim: no built-in method, use regex say $text; say length($text); say uc($text); say lc($text); say scalar reverse($text); say index($text, "World") >= 0 ? "found" : "not found";
class Main { public static void main(String[] args) { String text = " Hello, World! ".strip(); // strip() trims in one call System.out.println(text); System.out.println(text.length()); System.out.println(text.toUpperCase()); System.out.println(text.toLowerCase()); System.out.println(new StringBuilder(text).reverse()); System.out.println(text.contains("World") ? "found" : "not found"); } }
Perl's string operations are mostly standalone functions — length($text), uc($text), index($text, ...) — that take the string as an argument, and there is no built-in trim function at all (Perl trims with a manual regex substitution). Java's String is a genuine class with methods called directly on the object: text.length(), text.toUpperCase(), and — since Java 11 — text.strip() for Unicode-aware whitespace trimming, a built-in Perl still lacks. Reversing has no dedicated String method, since String is immutable in Java; the idiomatic approach wraps it in a mutable StringBuilder, which does have a .reverse() method, then converts back via toString() (implicit here in println).
Heredocs vs text blocks
use v5.38; my $name = "Alice"; my $message = <<~LETTER; Dear $name, Welcome to the team! Regards, HR LETTER print $message;
class Main { public static void main(String[] args) { String name = "Alice"; // Text blocks (Java 15+) — like Perl's squiggly heredoc, common // leading indentation IS automatically stripped based on the // closing delimiter's position. String message = """ Dear %s, Welcome to the team! Regards, HR """.formatted(name); System.out.print(message); } }
Java's text blocks (Java 15+), delimited by triple double-quotes """, are a close match for Perl's squiggly heredoc <<~LABEL: both automatically strip the common leading indentation shared by every line, based on the position of the closing delimiter, so the source can be indented naturally to match the surrounding code. Text blocks do not interpolate variables directly the way Perl heredocs do, though — combining one with .formatted(...) (or String.format()) is the idiomatic way to substitute values in, mirroring Perl's heredoc interpolation with one extra method call.
sprintf vs String.format/printf
use v5.38; printf("%-10s: %6.2f\n", "Price", 9.99); say sprintf("Hex: 0x%08X", 255); say sprintf("Count: %05d", 42);
class Main { public static void main(String[] args) { System.out.printf("%-10s: %6.2f%n", "Price", 9.99); System.out.println(String.format("Hex: 0x%08X", 255)); System.out.println(String.format("Count: %05d", 42)); } }
This is one of the more comfortable corners of Java for a Perl programmer: both languages inherited the same C-style format-string mini-language, and the placeholders here — %-10s, %6.2f, %08X, %05d — are spelled identically in both. Java's System.out.printf() mirrors Perl's printf directly (prints immediately, no return value), while String.format() mirrors Perl's sprintf (returns the formatted string instead of printing it). The one gotcha: Java's %n is the portable newline placeholder — it produces the host platform's native line ending, unlike the literal \n Perl always uses.
Splitting and joining strings
use v5.38; my $csv = "apple,banana,cherry"; my @fruits = split(/,/, $csv); say "@fruits"; say join(" | ", @fruits); my @words = split ' ', " the quick fox "; # special-cased whitespace split say scalar(@words);
import java.util.List; import java.util.stream.Collectors; class Main { public static void main(String[] args) { String csv = "apple,banana,cherry"; String[] fruits = csv.split(","); System.out.println(String.join(" ", fruits)); System.out.println(String.join(" | ", fruits)); // A trimmed string split on a whitespace regex collapses runs of // whitespace, matching Perl's special-cased split ' ', $string form String[] words = " the quick fox ".strip().split("\\s+"); System.out.println(words.length); } }
Java's string.split(regex) mirrors Perl's split closely, including accepting a regular expression directly as the separator — though Java calls it as a method on the string, returning a plain String[] array, while Perl calls it as a standalone function returning a list. Joining reverses the calling convention: Java's String.join(separator, array) is a static method taking the separator first, while Perl's join($separator, @list) is also a standalone function with the separator first — genuinely the same shape here, unlike the array-method style JavaScript and Ruby both use for joining.
Mutable Perl scalars vs immutable Java strings
use v5.38; # Perl scalars holding strings are always mutable my $greeting = "hello"; $greeting .= ", world"; # append in place, no error say $greeting;
class Main { public static void main(String[] args) { // Java Strings are always immutable — every "modification" // builds a brand-new String and rebinds the variable to it. String greeting = "hello"; greeting += ", world"; // creates a new String; does not mutate the original System.out.println(greeting); // Building a string incrementally in a loop should use StringBuilder, // which IS mutable, to avoid allocating a new String on every append: StringBuilder builder = new StringBuilder(greeting); builder.append("!"); System.out.println(builder); } }
Perl string scalars are always mutable — .= appends in place with no restriction. Java strings are immutable by design: every operation that looks like mutation, including +=, actually creates an entirely new String object and rebinds the variable to it. This has real performance implications in a loop that builds up a string incrementally — repeated += concatenation allocates a new string on every iteration — so the idiomatic fix is the mutable StringBuilder class, appending to it in a loop and calling .toString() (implicit in println here) once at the end.
Numbers
One scalar type vs int/long/double primitives
use v5.38; my $integer = 42; my $float = 3.14; say ref(\$integer); # "SCALAR" — Perl doesn't distinguish int from float at the type level say $integer + $float;
class Main { public static void main(String[] args) { int integerValue = 42; long bigValue = 9_876_543_210L; // needs long — int overflows past ~2.1 billion double floatValue = 3.14; System.out.println(integerValue + floatValue); System.out.println(bigValue); // int overflowExample = 9876543210; // COMPILE ERROR: doesn't fit in int } }
Perl's scalar draws no distinction between integer and floating-point values at the type level — one scalar can hold either, and arithmetic between them just works. Java instead has a family of distinct numeric primitive types with fixed bit widths: int (32-bit), long (64-bit, needs an L suffix on literals that exceed int's range), float (32-bit decimal), and double (64-bit decimal, the default for a literal like 3.14). Choosing the wrong one is a genuine compile-time concern in Java — assigning a value too large for int to an int variable is a compile error, not a silent overflow or an automatic promotion the way Perl (and JavaScript) handle numbers past their safe integer range.
Boxed wrapper types — a genuine Java-only wrinkle
use v5.38; # Perl has no analog: every scalar is already a first-class value # usable anywhere, with no distinction between "primitive" and "object" numbers. my @numbers = (1, 2, 3); say ref($numbers[0]) || "not a reference — a plain scalar";
import java.util.List; import java.util.ArrayList; class Main { public static void main(String[] args) { // A primitive int cannot be stored in a generic collection directly — // it must be autoboxed into its wrapper class, Integer, first. List<Integer> numbers = new ArrayList<>(); numbers.add(42); // autoboxing: int 42 -> Integer.valueOf(42), automatic int first = numbers.get(0); // auto-unboxing: Integer -> int, automatic System.out.println(first); System.out.println(numbers.get(0) instanceof Integer); } }
This is a wrinkle with no Perl equivalent at all: because Java primitives like int are not objects, they cannot be used as a generic type parameter (List<int> is illegal) or stored in a collection directly. Every primitive type has a corresponding boxed wrapper class — int/Integer, double/Double, boolean/Boolean — and the compiler automatically converts between them (autoboxing and auto-unboxing) so the code above never needs to write Integer.valueOf() explicitly. Perl has nothing resembling this distinction since every scalar is already usable anywhere, with no separate "primitive" representation to convert away from.
Integer division and modulo with negative numbers
use v5.38; say 10 / 3; # 3.33333... — Perl's / is always floating-point division say int(10 / 3); # 3 — truncate toward zero for integer division say 10 % 3; # 1 say -7 % 3; # 2 in Perl — result takes the sign of the right operand
class Main { public static void main(String[] args) { System.out.println(10.0 / 3); // 3.3333333333333335 — floating-point division System.out.println(10 / 3); // 3 — int / int TRUNCATES automatically, no cast needed System.out.println(10 % 3); // 1 System.out.println(-7 % 3); // -1 in Java — result takes the sign of the LEFT operand } }
This is a genuine mechanical difference worth calling out: Perl's / always performs floating-point division regardless of operand types, so getting a truncated integer result needs an explicit int(...) call. Java's / instead branches on the operand types at compile time — dividing two int values truncates toward zero automatically, while dividing with at least one double operand produces a floating-point result — so 10 / 3 and 10.0 / 3 genuinely mean different things in Java. The two languages also disagree on the sign of % with a negative operand: Perl's modulo takes the sign of the right operand (-7 % 3 is 2), while Java's % is a true C-style remainder that takes the sign of the left operand (-7 % 3 is -1) — a real trap when porting wraparound-index logic without adjustment.
Built-in functions vs the Math class
use v5.38; my $number = 42; say $number % 2 == 0 ? "even" : "odd"; say $number > 0 ? "positive" : "not positive"; say abs(-42); for my $count (0..2) { say "tick"; }
class Main { public static void main(String[] args) { int numberValue = 42; System.out.println(numberValue % 2 == 0 ? "even" : "odd"); System.out.println(numberValue > 0 ? "positive" : "not positive"); System.out.println(Math.abs(-42)); for (int count = 0; count < 3; count++) { System.out.println("tick"); } } }
Both languages check parity and sign with inline comparison expressions — there is no dedicated even?-style predicate method in either. Perl's abs is a standalone built-in function, while Java groups nearly all of its numeric functions as static methods on the Math class — Math.abs(), Math.floor(), Math.max() — rather than exposing them as free-floating global functions. Java has no native integer range syntax matching Perl's 0..2; the classic C-style three-part for loop, which Java kept exactly as C and Perl both spell it, is the idiomatic replacement for a fixed count of iterations.
Numeric conversions and parsing
use v5.38; my $parsed = "42" + 0; # Perl coerces strings to numbers automatically say $parsed; say "$parsed"; # back to string via interpolation say "3.14" + 0.0; say hex("FF"); # 255 say sprintf("%x", 255); # "ff"
class Main { public static void main(String[] args) { int parsedInteger = Integer.parseInt("42"); // explicit parsing, no implicit coercion System.out.println(parsedInteger); System.out.println(String.valueOf(parsedInteger)); // back to string System.out.println(Double.parseDouble("3.14")); System.out.println(Integer.parseInt("FF", 16)); // hex parsing: 255 System.out.println(Integer.toHexString(255)); // to hex string: "ff" } }
Perl coerces a numeric-looking string to a number implicitly the moment it appears in numeric context — "42" + 0 just works. Java has no implicit string-to-number coercion at all: "42" + 0 in Java is actually string concatenation (Java overloads + for strings the same way JavaScript does), producing the string "420", not the number 42. Every conversion in Java must be explicit — Integer.parseInt(), Double.parseDouble() — and going the other direction, Integer.parseInt(string, radix) takes an explicit base argument just like Perl's hex() is really shorthand for base 16, while Integer.toHexString() replaces Perl's sprintf("%x", ...).
Arrays & Lists
Perl @array vs Java's fixed-size array
use v5.38; my @numbers = (1, 2, 3, 4, 5); say $numbers[0]; # element access uses $, not @ say scalar(@numbers); # count push @numbers, 6; # Perl arrays grow freely say "@numbers";
class Main { public static void main(String[] args) { int[] numbers = {1, 2, 3, 4, 5}; System.out.println(numbers[0]); System.out.println(numbers.length); // a field, not a method — no parentheses // A Java array's size is FIXED at creation — there is no push(). // numbers[5] = 6; // ArrayIndexOutOfBoundsException at runtime: // // the array was only ever 5 elements long. } }
This is a genuine structural difference, not just a syntax gap: a Perl @array is always dynamically sized — push, pop, shift, and unshift all grow or shrink it freely at runtime. A Java array's length is fixed permanently at the moment it is created; there is no way to grow one, and attempting to write past its bounds throws an ArrayIndexOutOfBoundsException rather than silently extending the array the way Perl would. Also note the length check itself: Java's array.length is a plain field access (no parentheses), while Perl's equivalent is the function call scalar(@numbers). For anything resembling Perl's freely growing array, Java's java.util.List/ArrayList (next concept) is the tool to reach for instead of a raw array.
ArrayList — the dynamically sized equivalent
use v5.38; my @items = (2, 3); push @items, 4; # add to end unshift @items, 1; # add to front say "@items"; # 1 2 3 4 my $last = pop @items; # remove from end my $first = shift @items; # remove from front say $last; say $first; say "@items";
import java.util.ArrayList; import java.util.List; class Main { public static void main(String[] args) { List<Integer> items = new ArrayList<>(List.of(2, 3)); items.add(4); // add to end items.add(0, 1); // add at index 0 — no dedicated "prepend" method System.out.println(items); // [1, 2, 3, 4] int lastItem = items.remove(items.size() - 1); // remove from end int firstItem = items.remove(0); // remove from front System.out.println(lastItem); System.out.println(firstItem); System.out.println(items); } }
A Java ArrayList is the genuinely dynamic counterpart to Perl's @array — it grows and shrinks freely, backed internally by a plain array that gets reallocated and copied as needed, entirely hidden from the caller. The method vocabulary is less friendly than JavaScript's exact-name match with Perl, though: .add(value) replaces both push and (with an index argument) unshift, and .remove(index) replaces both pop (pass size() - 1) and shift (pass 0) — there is no single dedicated method spelled exactly like Perl's four array-mutation primitives, so the index argument does the work of distinguishing "front" from "back."
Negative indexing and slicing
use v5.38; my @numbers = (10, 20, 30, 40, 50); say $numbers[0]; # first: 10 say $numbers[-1]; # last: 50 say $numbers[-2]; # second-to-last: 40 my @middle = @numbers[1..3]; say "@middle";
import java.util.List; class Main { public static void main(String[] args) { List<Integer> numbers = List.of(10, 20, 30, 40, 50); System.out.println(numbers.get(0)); // first: 10 System.out.println(numbers.get(numbers.size() - 1)); // last: 50 — no negative indexing at all System.out.println(numbers.get(numbers.size() - 2)); // second-to-last: 40 System.out.println(numbers.subList(1, 4)); // [20, 30, 40] — stop index EXCLUSIVE } }
Perl supports negative indexing directly ($numbers[-1] for the last element), one of its most convenient array features. Java has absolutely no negative-indexing support anywhere — .get(-1) throws an IndexOutOfBoundsException immediately rather than silently returning something unexpected, so reaching the last element always needs the explicit size() - 1 arithmetic shown here. Slicing has the same inclusive/exclusive mismatch seen elsewhere in this comparison: Perl's @numbers[1..3] range slice is inclusive on both ends, while Java's .subList(1, 4) is half-open — the stop index is exclusive — so the equivalent three-element slice needs 4, not 3, as the stop argument.
sort and reverse
use v5.38; my @numbers = (3, 1, 4, 1, 5, 9); my @sorted = sort { $a <=> $b } @numbers; say "@sorted"; say join(" ", reverse @sorted);
import java.util.ArrayList; import java.util.Collections; import java.util.List; class Main { public static void main(String[] args) { List<Integer> numbers = new ArrayList<>(List.of(3, 1, 4, 1, 5, 9)); Collections.sort(numbers); // sorts numerically by default for Integer System.out.println(numbers); List<Integer> reversed = new ArrayList<>(numbers); Collections.reverse(reversed); // mutates in place, like Perl's reverse does NOT System.out.println(reversed); } }
Perl's sort defaults to string comparison — sorting numbers correctly requires the explicit { $a <=> $b } comparator block, a common beginner trap. Java's Collections.sort() avoids this trap for a List<Integer> specifically, since Integer implements Comparable with genuine numeric ordering built in — no comparator needed for the common case, though a custom Comparator can still be supplied as a second argument for other orderings. The bigger difference is mutability: Perl's sort and reverse both return new lists, leaving the original untouched, while Collections.sort() and Collections.reverse() both mutate the list argument in place and return nothing — copying first (new ArrayList<>(numbers)) is necessary to preserve the original.
map/grep/reduce vs the Streams API
use v5.38; use List::Util qw(reduce sum0); my @numbers = (1, 2, 3, 4, 5, 6); my @doubled = map { $_ * 2 } @numbers; my @evens = grep { $_ % 2 == 0 } @numbers; my $total = sum0(@numbers); say "@doubled"; say "@evens"; say $total;
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> doubled = numbers.stream() .map(number -> number * 2) .collect(Collectors.toList()); List<Integer> evens = numbers.stream() .filter(number -> number % 2 == 0) .collect(Collectors.toList()); int total = numbers.stream() .reduce(0, Integer::sum); System.out.println(doubled); System.out.println(evens); System.out.println(total); } }
Perl's map and grep are standalone functions taking a block and a list directly; summing needs the CPAN-distributed core module List::Util's sum0. Java's Streams API (Java 8+) covers the same three operations as chained method calls on a .stream() view of the collection: .map() transforms, .filter() replaces grep under a different name, and .reduce() covers summing (and any other fold) with no import needed beyond the collector for gathering results back into a List. The .collect(Collectors.toList()) step is real ceremony Perl has no equivalent for — a Java stream is lazy and one-shot, so it must be explicitly materialized back into a concrete collection before it can be reused or printed directly.
Hashes vs Maps
Hashes vs java.util.Map
use v5.38; my %scores = ( alice => 95, bob => 87, carol => 91, ); say $scores{alice}; # element access uses $, not % say scalar(keys %scores); # count
import java.util.Map; class Main { public static void main(String[] args) { Map<String, Integer> scores = Map.of( "alice", 95, "bob", 87, "carol", 91 ); System.out.println(scores.get("alice")); System.out.println(scores.size()); } }
Just as with arrays, Perl declares a hash with the % sigil but reads an individual value with $scores{alice}, while Java requires an explicit generic type declaration — Map<String, Integer> — spelling out both the key type and the value type up front, something dynamically typed Perl has no concept of at all. Map.of(...) (Java 9+) builds a small, genuinely immutable map from alternating key-value arguments — a reasonable match for a Perl hash literal, though .get("alice") is a method call rather than the bracket-and-sigil syntax Perl uses, and counting entries needs the explicit .size() method rather than a standalone keys function.
HashMap — the mutable, general-purpose map
use v5.38; my %config = (timeout => 30, retries => 3); $config{timeout} = 60; # set / update say $config{timeout}; say exists $config{missing} ? "yes" : "no"; # existence check say $config{missing} // "default"; # defined-or with a default
import java.util.HashMap; import java.util.Map; class Main { public static void main(String[] args) { Map<String, Integer> config = new HashMap<>(); config.put("timeout", 30); config.put("retries", 3); config.put("timeout", 60); // set / update System.out.println(config.get("timeout")); System.out.println(config.containsKey("missing") ? "yes" : "no"); // existence check System.out.println(config.getOrDefault("missing", -1)); // default fallback } }
Map.of(...) from the previous example is genuinely immutable — attempting .put() on it throws UnsupportedOperationException — so ordinary mutable key-value storage needs HashMap instead, constructed with new HashMap<>() and built up with repeated .put() calls. Perl's exists checks key presence without triggering autovivification; Java's .containsKey() plays the same role directly. Perl's // defined-or default has a close match in .getOrDefault(key, fallback), a single method call rather than a separate operator, which returns the fallback value immediately if the key is absent rather than returning null and requiring a follow-up check.
Iterating over a hash/map
use v5.38; my %scores = (alice => 95, bob => 87, carol => 91); for my $name (sort keys %scores) { say "$name: $scores{$name}"; } say join(", ", keys %scores); say join(", ", values %scores);
import java.util.Map; import java.util.TreeMap; class Main { public static void main(String[] args) { Map<String, Integer> scores = new TreeMap<>(Map.of("alice", 95, "bob", 87, "carol", 91)); // TreeMap keeps keys in sorted order automatically, unlike HashMap for (Map.Entry<String, Integer> entry : scores.entrySet()) { System.out.println(entry.getKey() + ": " + entry.getValue()); } System.out.println(String.join(", ", scores.keySet())); System.out.println(scores.values()); } }
Perl's keys and values functions have direct equivalents in Java's Map interface, .keySet() and .values(), though called as methods rather than as free functions. Where a Perl programmer loops with for my $key (keys %hash) and then looks up $hash{$key} inside the loop, Java's .entrySet() yields both the key and value together as a Map.Entry object in one step, read out with .getKey()/.getValue() — no separate lookup needed. Ordinary HashMap has no guaranteed iteration order at all (unlike a plain Perl hash, which is merely randomized for security, not fully unordered); reaching for TreeMap instead, as shown here, is the idiomatic way to get keys back in sorted order automatically rather than calling sort separately.
Hash key coercion vs typed map keys
use v5.38; # Perl hash keys are always coerced to strings — there is no way # to use a number, an array reference, or another hash as a key directly # without it stringifying first. my %counts; $counts{"apple"}++; $counts{"apple"}++; say $counts{"apple"};
import java.util.HashMap; import java.util.Map; class Main { public static void main(String[] args) { // A Java Map's key type is declared explicitly and is NOT coerced to // a string — an Integer key stays a genuine Integer, an object key // stays a genuine object reference, as long as it implements // equals()/hashCode() correctly (records do this automatically). Map<String, Integer> wordCount = new HashMap<>(); String word = "apple"; wordCount.merge(word, 1, Integer::sum); wordCount.merge(word, 1, Integer::sum); System.out.println(wordCount.get(word)); Map<Integer, String> numberKeyedMap = new HashMap<>(); numberKeyedMap.put(42, "the answer"); System.out.println(numberKeyedMap.get(42)); // a genuine Integer key, no stringification } }
Perl hashes silently coerce every key to a string, so a Perl programmer already knows to be careful using a reference as a hash key (it stringifies to its memory address, not something useful to compare later). Java's Map keys are not coerced at all — the key type is declared explicitly as a generic parameter, and an Integer key genuinely stays an Integer, compared by value via its .equals()/.hashCode() implementation, never silently turned into a string. The .merge(key, 1, Integer::sum) call is also worth a note on its own: it is Java's built-in equivalent of Perl's autovivifying $counts{key}++ — insert the value if absent, otherwise combine it with the existing value using the supplied function — collapsing what would otherwise be an explicit containsKey check into one call.
Context Sensitivity
Scalar vs list context — a Perl-only concept
use v5.38; my @animals = ("cat", "dog", "bird"); my $count = @animals; # scalar context: array evaluates to its length say $count; # 3 my @copy = @animals; # list context: array evaluates to all its elements say "@copy"; my $last_result = (1, 2, 3); # scalar context on a list: LAST element, not count! say $last_result; # 3 — a classic Perl surprise
import java.util.List; class Main { public static void main(String[] args) { List<String> animals = List.of("cat", "dog", "bird"); int count = animals.size(); // explicit method call — no ambiguity whatsoever System.out.println(count); List<String> copy = animals; // always the list itself, no context switch System.out.println(copy); // There is no equivalent surprise: a value's meaning never changes // depending on how it is assigned or used. int lastElement = List.of(1, 2, 3).get(2); System.out.println(lastElement); } }
This is the single biggest conceptual difference between the two languages, and Java's static type system leaves absolutely no room for it — every expression in Java has one fixed, statically known type, determined entirely by its declaration, never by the context surrounding where it is used. In Perl, the exact same expression (@animals, or a parenthesized list) evaluates differently depending on whether it appears in a context that wants one value (scalar context) or many (list context) — assigning an array to a scalar silently gives you its length, but assigning a literal list (1, 2, 3) to a scalar gives you its last element, not its count. Java has no such duality: getting a count always means calling .size() or reading .length explicitly, and there is no assignment target that could ever reinterpret a List as anything other than the same List.
wantarray — no Java equivalent
use v5.38; sub context_aware { if (wantarray()) { return (1, 2, 3); # caller wants a list } else { return "scalar result"; # caller wants a scalar } } my @list_result = context_aware(); my $scalar_result = context_aware(); say "@list_result"; say $scalar_result;
import java.util.List; class Main { // A Java method's return TYPE is fixed permanently in its signature. // There is no way for a method to inspect how its caller intends to // use the result and change what it returns based on that. static List<Integer> contextAware() { return List.of(1, 2, 3); } public static void main(String[] args) { List<Integer> listResult = contextAware(); int firstOnly = contextAware().get(0); System.out.println(listResult); System.out.println(firstOnly); } }
Perl's wantarray lets a subroutine inspect the context it was called in and return a fundamentally different value — a different shape entirely — depending on whether the caller wants a list, a scalar, or nothing (void context). Java has no equivalent mechanism whatsoever, and its type system actively forbids the idea: a method's return type is declared once in its signature and is permanently fixed, so a method returning List<Integer> can never instead return a single String based on caller context. It is entirely up to the caller to decide what to do with the one, statically typed value a Java method returns.
Forcing a count — always an explicit method call
use v5.38; my @items = (10, 20, 30, 40); say scalar(@items); # explicit count via the scalar() function say 0 + @items; # numeric context also forces scalar say "There are " . @items . " items"; # string concatenation, too — forces scalar
import java.util.List; class Main { public static void main(String[] args) { List<Integer> items = List.of(10, 20, 30, 40); System.out.println(items.size()); // always call .size() explicitly System.out.println("There are " + items.size() + " items"); // no implicit context conversion // items alone concatenated with a string prints its toString(), // e.g. "[10, 20, 30, 40]" — never its count } }
Perl offers several implicit ways to coerce an array into its count — numeric context, string concatenation, or the explicit scalar() function all work interchangeably. Java offers no such shortcuts at all: concatenating a List directly into a string calls its .toString() method, printing something like [10, 20, 30, 40], not its size — .size() must always be called out explicitly, with no context-sensitive alternative anywhere in the language.
Control Flow
if / elsif / else vs if / else if / else
use v5.38; my $temperature = 22; if ($temperature > 30) { say "hot"; } elsif ($temperature > 20) { say "warm"; } elsif ($temperature > 10) { say "cool"; } else { say "cold"; }
class Main { public static void main(String[] args) { int temperature = 22; if (temperature > 30) { System.out.println("hot"); } else if (temperature > 20) { System.out.println("warm"); } else if (temperature > 10) { System.out.println("cool"); } else { System.out.println("cold"); } } }
Java spells the middle branch as two full words, else if, rather than Perl's single contracted keyword elsif — technically Java has no elseif keyword at all; it is simply an if statement nested as the body of an else, which happens to read the same way. The condition itself must be a genuine boolean expression in Java (see the truthiness concept above), where Perl accepts any value at all and applies its own truthiness rules — otherwise the syntax and control flow here are close to identical between the two languages.
unless and postfix modifiers — no Java equivalent
use v5.38; my $authenticated = 0; unless ($authenticated) { say "Please log in"; } # Postfix form say "Please log in" unless $authenticated; say "Welcome!" if $authenticated;
class Main { public static void main(String[] args) { boolean authenticated = false; // Java has no unless keyword and no postfix if/unless statement // modifier at all — every guard needs a full if statement: if (!authenticated) { System.out.println("Please log in"); } if (authenticated) { System.out.println("Welcome!"); } // The closest Java gets is the conditional (ternary) EXPRESSION // (not a statement modifier) — useful only when producing a value: String message = authenticated ? "Welcome!" : "Please log in"; System.out.println(message); } }
This is a real loss for a Perl programmer: Java has neither an unless keyword nor a postfix statement-modifier form for if at all — Ruby borrowed both of these directly from Perl, but Java never adopted either. Every conditional guard must be written as a full if (...) { } block, even for the simplest one-line guard clause that Perl would express as statement unless condition;. Java does have a conditional (ternary) expression (condition ? valueIfTrue : valueIfFalse), but it produces a value for assignment rather than gating a statement, so it is not a substitute for the postfix-modifier idiom.
for/foreach vs for and enhanced for
use v5.38; for my $number (1..5) { say $number * $number; } foreach my $letter ("a".."e") { print "$letter "; } print "\n";
class Main { public static void main(String[] args) { // Enhanced for-each — the closest match to Perl's foreach over a range for (int number = 1; number <= 5; number++) { System.out.println(number * number); } // Java has no native character range — build one from a char loop for (char letter = 'a'; letter <= 'e'; letter++) { System.out.print(letter + " "); } System.out.println(); } }
Perl's numeric range 1..5 has no single built-in Java equivalent — the classic C-style three-part for loop is the standard replacement, since Java has no range literal syntax at all. Perl's character range "a".."e" also has no direct Java equivalent, but Java's primitive char type is itself an integer type under the hood, so a for loop over char values with letter++ works naturally with ordinary arithmetic comparison — arguably a smaller gap than the one Python or JavaScript have here, since Java's char arithmetic requires no explicit code-point conversion functions at all.
while loops — no until in Java
use v5.38; my $counter = 0; while ($counter < 5) { say $counter; $counter++; } $counter = 5; until ($counter == 0) { $counter--; } say $counter;
class Main { public static void main(String[] args) { int counter = 0; while (counter < 5) { System.out.println(counter); counter++; } // Java has no "until" keyword — negate the condition instead counter = 5; while (counter != 0) { counter--; } System.out.println(counter); } }
Both languages have a while loop with the same semantics, and both keep C-style ++/-- increment and decrement operators (Ruby dropped these, but Perl and Java both kept them). Java has no until keyword at all (Ruby has one, borrowed from Perl, but Java never adopted it) — "loop while some condition is false" must always be written as a negated while, exactly the workaround JavaScript needs for the same gap.
given/when workaround vs switch expressions
use v5.38; my $direction = "north"; for ($direction) { # topicalize $_ — the "poor man's given/when" since Perl deprecated given/when if (/^north$/) { say "Going up"; last; } if (/^south$/) { say "Going down"; last; } if (/^east$/ || /^west$/) { say "Going sideways"; last; } say "Unknown"; }
class Main { public static void main(String[] args) { String direction = "north"; // Modern switch EXPRESSION (Java 14+) — no fall-through, no break needed, // and it directly produces a value. String result = switch (direction) { case "north" -> "Going up"; case "south" -> "Going down"; case "east", "west" -> "Going sideways"; default -> "Unknown"; }; System.out.println(result); } }
Perl once had an experimental given/when construct but removed it due to design problems with smart matching, leaving Perl programmers to fake it with a for ($topic) { if (...) { ...; last } } loop. Java's modern switch expression (Java 14+, using -> rather than the old colon-and-break statement form) is a genuine, permanent, and considerably safer equivalent: there is no fall-through by default, no break to remember, and comma-separated case labels (case "east", "west" ->) group several values under one branch cleanly — closer in spirit and safety to Perl's workaround than the traditional C-style switch statement most other languages still default to.
Subroutines & Methods
sub with @_ vs mandatory typed parameters
use v5.38; sub square { my ($number) = @_; # unpack the args array manually return $number * $number; } sub greet { my ($name) = @_; return "Hello, $name!"; } say square(7); say greet("Alice");
class Main { static int square(int number) { return number * number; } static String greet(String name) { return "Hello, " + name + "!"; } public static void main(String[] args) { System.out.println(square(7)); System.out.println(greet("Alice")); } }
Perl subroutines receive all arguments bundled into the single array @_ and must manually unpack them with my ($number) = @_; — there is no way to name parameters in the signature itself in the widely used, portable syntax, and no way to declare what type each argument must be. Java methods declare every parameter with both a name and a mandatory type directly in the signature (static int square(int number)), and the return type is likewise mandatory and fixed (int, String) — so there is no unpacking step at all, but every method signature is considerably more ceremony-laden than Perl's bare sub name { ... }. A Java method must also declare an explicit return statement matching its declared return type; there is no implicit "last expression is the return value" convention the way Ruby has.
Anonymous subs vs lambda expressions
use v5.38; my $square = sub { my ($number) = @_; return $number * $number; }; say $square->(7); my @numbers = (1, 2, 3); my @doubled = map { $_ * 2 } @numbers; say "@doubled";
import java.util.List; import java.util.function.Function; import java.util.stream.Collectors; class Main { public static void main(String[] args) { Function<Integer, Integer> square = number -> number * number; System.out.println(square.apply(7)); List<Integer> numbers = List.of(1, 2, 3); List<Integer> doubled = numbers.stream() .map(number -> number * 2) .collect(Collectors.toList()); System.out.println(doubled); } }
Perl's anonymous subroutine (sub { ... }) needs the explicit -> arrow to be called ($square->(7)), since a code reference is just another kind of reference requiring dereferencing. Java's lambda expression syntax (Java 8+) is considerably terser for the common case of a single-expression callback — number -> number * number needs no return keyword, no braces, and no explicit type declaration for the parameter (it is inferred from the target functional interface, here Function<Integer, Integer>) — but calling it requires the interface's single abstract method, .apply() in this case, rather than a universal calling syntax. Lambda expressions are also the idiomatic way to write the transform passed to .map(), playing the same role as Perl's { $_ * 2 } block passed to map.
Default parameters vs method overloading
use v5.38; sub greet { my ($name, $greeting) = @_; $greeting //= "Hello"; # defined-or assigns the default return "$greeting, $name!"; } say greet("Alice"); say greet("Bob", "Hi");
class Main { static String greet(String name, String greeting) { return greeting + ", " + name + "!"; } // Java has no default-parameter syntax at all — an overload with fewer // parameters delegates to the full version, providing the default: static String greet(String name) { return greet(name, "Hello"); } public static void main(String[] args) { System.out.println(greet("Alice")); System.out.println(greet("Bob", "Hi")); } }
Perl has no default-parameter syntax in the subroutine signature either — the idiomatic approach is unpacking positionally from @_ and then using //= (defined-or assignment) to fill in a default for any argument left undef. Java goes further: it has no default-parameter syntax whatsoever, not even inside the method body, because every method's parameter list is part of its fixed, compile-time signature. The idiomatic Java replacement is method overloading — declaring a second method with the same name but a shorter parameter list, which simply delegates to the fuller version — a solution that scales poorly once more than one or two optional parameters are involved, unlike Perl's single flexible subroutine.
Simulated keyword arguments — hash unpacking vs a parameter record
use v5.38; sub create_user { my (%args) = @_; # unpack into a hash to simulate keyword args my $name = $args{name}; my $age = $args{age}; my $city = $args{city} // "Unknown"; say "$name, age $age, from $city"; } create_user(name => "Alice", age => 30, city => "Paris");
class Main { record UserParameters(String name, int age, String city) { UserParameters(String name, int age) { this(name, age, "Unknown"); // compact constructor overload supplies the default } } static void createUser(UserParameters parameters) { System.out.println(parameters.name() + ", age " + parameters.age() + ", from " + parameters.city()); } public static void main(String[] args) { createUser(new UserParameters("Alice", 30, "Paris")); } }
Neither language has true keyword arguments as a core language feature the way Python does. Perl simulates them by unpacking a flat list into a hash (my (%args) = @_;), reading each named value out afterward. Java has no equivalent unpacking trick at all — passing a single low-ceremony record (see the Records section below) as one parameter is the closest idiomatic substitute, giving named, typed fields at the call site (parameters.name()) instead of Perl's hash-key lookups, at the cost of needing to construct the record positionally (new UserParameters("Alice", 30, "Paris")) rather than with Perl's order-independent name => "Alice" style.
References vs Object References
Explicit \/-> vs implicit object references
use v5.38; my @numbers = (1, 2, 3); my $array_ref = \@numbers; # explicit reference operator say $array_ref->[0]; # explicit arrow dereference push @$array_ref, 4; # explicit sigil-based dereference say "@numbers";
import java.util.ArrayList; import java.util.List; class Main { public static void main(String[] args) { List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3)); List<Integer> arrayReference = numbers; // Lists are ALWAYS references — no operator needed System.out.println(arrayReference.get(0)); // ordinary method call, no special dereference syntax arrayReference.add(4); System.out.println(numbers); } }
This is the most consequential structural difference between the two languages, after context sensitivity — and it happens to be one where Java is closer to a language a Perl programmer might not expect: C, not Ruby or JavaScript, is the odd one out here, since C requires explicit pointer syntax the way Perl requires explicit reference syntax. Perl distinguishes plain values from references explicitly: creating one needs the \ operator, and reading through one needs either the -> arrow or a sigil-based dereference like @$array_ref — the reference-ness of a variable is always visible in the syntax used to touch it. Java has no separate reference type or reference-creation operator at all: every non-primitive value (an object, an array, a List) is a reference by nature, assignment never copies one, and reading or writing through it uses the exact same dot or bracket syntax as any other value — there is nothing that visually marks a variable as "a reference" the way Perl's sigils and arrows do. In this respect Java's object semantics resemble Perl's own reference semantics far more than Perl's plain scalar-copy semantics.
Value copy vs reference copy
use v5.38; my @original = (1, 2, 3); my @copy = @original; # list assignment COPIES the elements push @copy, 4; say "@original"; # unaffected: 1 2 3 say "@copy"; # 1 2 3 4 my $ref_a = \@original; my $ref_b = $ref_a; # reference assignment SHARES the same array push @$ref_b, 99; say "@original"; # affected: 1 2 3 99
import java.util.ArrayList; import java.util.List; class Main { public static void main(String[] args) { List<Integer> original = new ArrayList<>(List.of(1, 2, 3)); List<Integer> assigned = original; // assignment SHARES the same list — no copy happens assigned.add(4); System.out.println(original); // affected: [1, 2, 3, 4] System.out.println(assigned); // affected too — same underlying list // A genuine copy needs an explicit constructor call: List<Integer> realCopy = new ArrayList<>(original); realCopy.add(99); System.out.println(original); // unaffected: [1, 2, 3, 4] } }
This is where Perl's explicitness becomes a real safety feature: assigning one Perl array to another with @copy = @original always copies every element, and only an explicit reference assignment shares the underlying storage. Java has no plain-value assignment mode for a List or any other object type at all — List<Integer> assigned = original always shares the same underlying list, full stop, since there is no non-reference way to hold one in the first place. A genuine copy requires an explicit operation, most commonly the copy constructor shown here (new ArrayList<>(original)), and forgetting this is one of the most common sources of action-at-a-distance bugs for a Perl programmer arriving in Java, exactly the same trap JavaScript sets for arrays and objects.
undef vs null — and NullPointerException
use v5.38; my $maybe_name; # undef by default say defined($maybe_name) ? $maybe_name : "no name set"; say length($maybe_name) if defined($maybe_name); # guarded — never dereferences undef unsafely
class Main { public static void main(String[] args) { String maybeName = null; System.out.println(maybeName != null ? maybeName : "no name set"); // maybeName.length(); // NullPointerException — Java does NOT // // silently no-op on a null reference the // // way Perl treats undef in string context; // // calling ANY method on null crashes at runtime. if (maybeName != null) { System.out.println(maybeName.length()); } } }
Perl's undef behaves gracefully in most contexts — it stringifies to an empty string (with a warning), and numeric operations on it quietly treat it as zero. Java's null is far less forgiving: it is a legitimate value for any object-reference type, but calling any method on a null reference — even something as simple as .length() — throws a NullPointerException immediately and crashes the program if uncaught. This is such a pervasive category of Java bug that it has its own long history of mitigation techniques (the Optional<T> type, Objects.requireNonNullElse(), and simple defensive != null checks like the one here) — a Perl programmer's instinct to check defined() before using a value maps directly onto checking != null in Java, but the consequence of skipping the check is a hard crash rather than a warning.
Regular Expressions
Native regex literals vs Pattern/Matcher
use v5.38; my $text = "The quick brown fox"; if ($text =~ /quick (\w+) fox/) { say "Matched: $1"; } say "matches" if $text =~ /^The/;
import java.util.regex.Matcher; import java.util.regex.Pattern; class Main { public static void main(String[] args) { String text = "The quick brown fox"; Pattern pattern = Pattern.compile("quick (\\w+) fox"); Matcher matcher = pattern.matcher(text); if (matcher.find()) { System.out.println("Matched: " + matcher.group(1)); } System.out.println(text.matches("^The.*") ? "matches" : "no match"); } }
This is a big ergonomics gap worth calling out plainly: Perl treats a regular expression as a genuine first-class literal syntax, delimited by forward slashes, matched directly against a string with the =~ binding operator — regex use is woven into the language itself. Java has no regex literal syntax at all; every pattern is an ordinary string that must be explicitly compiled with Pattern.compile() into a Pattern object, then matched against text through a separate Matcher object obtained via .matcher(), with .find() to search and .group(n) to read a captured group — four extra layers of object machinery, plus the double-backslash escaping every pattern needs since it lives inside an ordinary Java string. This verbosity is one of the most immediately noticeable differences a Perl programmer hits moving to Java.
s/// substitution vs replaceAll
use v5.38; my $text = "The rain in Spain"; (my $replaced = $text) =~ s/ain/AIN/g; say $replaced; my $count = () = $text =~ /ain/g; # count matches via list-context trick say $count;
import java.util.regex.Matcher; import java.util.regex.Pattern; class Main { public static void main(String[] args) { String text = "The rain in Spain"; String replaced = text.replaceAll("ain", "AIN"); System.out.println(replaced); Pattern pattern = Pattern.compile("ain"); Matcher matcher = pattern.matcher(text); int count = 0; while (matcher.find()) { count++; } System.out.println(count); } }
Perl's s/pattern/replacement/g operator performs the substitution in place on a copy (the (my $replaced = $text) =~ s/.../ idiom copies first, then substitutes destructively). Java's text.replaceAll(regex, replacement) is a pure method that always returns a new string, leaving the original untouched — no destructive substitution operator exists in Java, matching the language's immutable-string design overall. Counting matches has no dedicated syntax in Java comparable to Perl's clever my $count = () = $text =~ /pattern/g; list-context assignment trick; the idiomatic Java approach falls back to manually looping over matcher.find() and incrementing a counter, considerably more verbose than either language's one-liner alternatives.
Named captures — a close syntax match
use v5.38; my $date = "2026-07-03"; if ($date =~ /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/) { say "Year: $+{year}, Month: $+{month}, Day: $+{day}"; }
import java.util.regex.Matcher; import java.util.regex.Pattern; class Main { public static void main(String[] args) { String date = "2026-07-03"; Pattern pattern = Pattern.compile("(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})"); Matcher matcher = pattern.matcher(date); if (matcher.matches()) { System.out.println("Year: " + matcher.group("year") + ", Month: " + matcher.group("month") + ", Day: " + matcher.group("day")); } } }
Named capture groups are spelled identically in both languages — (?<name>...) — since Java's regex engine directly copied Perl's named-capture syntax rather than inventing its own, one of the closest syntax matches anywhere in this comparison. The difference is entirely in how the captured value is retrieved: Perl populates the special hash %+, accessed as $+{year}, while Java requires calling matcher.group("year") as a method — still through the same Pattern/Matcher machinery as every other Java regex operation, with none of Perl's direct-binding-operator convenience.
OOP
package + bless vs native class
use v5.38; package Animal; sub new { my ($class, %args) = @_; return bless { name => $args{name}, sound => $args{sound} // "...", }, $class; } sub speak { my $self = shift; say "$self->{name} says $self->{sound}"; } package main; my $dog = Animal->new(name => "Rex", sound => "Woof"); $dog->speak;
class Animal { private final String name; private final String sound; Animal(String name, String sound) { this.name = name; this.sound = sound; } void speak() { System.out.println(name + " says " + sound); } } class Main { public static void main(String[] args) { Animal dog = new Animal("Rex", "Woof"); dog.speak(); } }
This is where a Perl programmer will feel real envy — a huge step up. Perl OOP is built manually from two separate primitives: a package to hold the namespace, and bless to associate a plain hash reference with that package so it is recognizable as an object — every constructor is hand-written, and there is no compiler enforcement of what fields an "object" of a given class is even supposed to have. Java has a genuinely mandatory, fully-featured native class system: class Animal { ... } declares real fields with real declared types, a real constructor with a fixed signature the compiler checks at every call site, and real encapsulation via access modifiers (private here) — none of it hand-rolled, all of it enforced by the compiler before the program ever runs.
Hand-written accessors vs enforced private fields
use v5.38; package Person; sub new { my ($class, %args) = @_; return bless { name => $args{name}, age => $args{age} }, $class; } sub name { $_[0]->{name} } # read-only getter — nothing stops direct hash access though sub age { $_[0]->{age} } package main; my $person = Person->new(name => "Alice", age => 30); say $person->name; say $person->{age}; # nothing actually enforces going through the accessor
class Person { private final String name; // private — the compiler enforces this, not a naming convention private int age; Person(String name, int age) { this.name = name; this.age = age; } String getName() { return name; } int getAge() { return age; } void setAge(int age) { this.age = age; } } class Main { public static void main(String[] args) { Person person = new Person("Alice", 30); System.out.println(person.getName()); // person.age; // COMPILE ERROR outside Person: private is enforced by the compiler person.setAge(31); System.out.println(person.getAge()); } }
Perl has no built-in accessor generation and no true privacy at all — the accessor methods shown here are entirely hand-written, and even calling $person->{age} directly, bypassing the accessor completely, works fine since a leading underscore or any other privacy signal is purely a naming convention that nothing in the language enforces. Java's private keyword on the name and age fields is a genuine, compiler-enforced access restriction — attempting person.age from outside the Person class is a hard compile error, not merely bad manners. This is a real step up in encapsulation strength for a Perl programmer used to every object's internals always being reachable if someone really wants to reach them.
Inheritance — @ISA vs extends
use v5.38; package Animal; sub new { bless { name => $_[1] }, $_[0] } sub name { $_[0]->{name} } sub speak { say $_[0]->{name} . " makes a sound" } package Dog; our @ISA = ('Animal'); # declare inheritance sub speak { my $self = shift; say $self->name . " barks"; } package main; my $dog = Dog->new("Rex"); $dog->speak; say $dog->isa("Animal") ? "is an Animal" : "not an Animal";
class Animal { protected final String name; Animal(String name) { this.name = name; } void speak() { System.out.println(name + " makes a sound"); } } class Dog extends Animal { Dog(String name) { super(name); // must call the parent constructor explicitly } @Override void speak() { System.out.println(name + " barks"); } } class Main { public static void main(String[] args) { Dog dog = new Dog("Rex"); dog.speak(); System.out.println(dog instanceof Animal ? "is an Animal" : "not an Animal"); } }
Perl declares inheritance by populating the special array @ISA with parent package names (or, in modern Perl, the more readable use parent 'Animal'; pragma) — a mechanism with no compiler involvement at all. Java uses the extends keyword directly in the class declaration, and a subclass constructor must explicitly call the parent constructor with super(...) as its first statement (the compiler inserts an implicit no-argument super() call only if the parent has a no-argument constructor available). Java's instanceof operator replaces Perl's isa method call for checking ancestry, and the @Override annotation — optional but strongly idiomatic — has no Perl equivalent: it asks the compiler to verify that speak genuinely overrides a parent method, catching a typo'd method name as a compile error rather than a silent no-op.
No operator overloading in Java either
use v5.38; package Money; use overload '+' => \&add, '""' => \&stringify; sub new { bless { cents => $_[1] }, $_[0] } sub add { Money->new($_[0]->{cents} + $_[1]->{cents}) } sub stringify { sprintf('$%.2f', $_[0]->{cents} / 100) } package main; my $total = Money->new(150) + Money->new(250); say "$total";
class Money { private final int cents; Money(int cents) { this.cents = cents; } Money add(Money other) { return new Money(this.cents + other.cents); } @Override public String toString() { return String.format("$%.2f", cents / 100.0); } } class Main { public static void main(String[] args) { Money total = new Money(150).add(new Money(250)); System.out.println(total); // toString() is called implicitly here } }
Java has no true operator-overloading protocol for a custom class, just like JavaScript — unlike Perl's use overload pragma, there is no way to make Java's built-in + operator invoke a method on a custom object; addition between two Money instances must be an explicit method call like .add() rather than total + other. Where the languages do align is stringification: overriding toString() (always annotated @Override by convention, since it is inherited from the universal base class Object) plays exactly the role Perl's '""' overload entry plays, and Java needs no separate pragma to opt in — System.out.println(total) calls toString() automatically wherever an object is coerced to a string.
Records
Records — a genuinely low-ceremony data class
use v5.38; # The closest Perl analog is an unblessed hash reference, with no # enforced shape, no generated methods, and no immutability at all. my $point = { x => 3, y => 4 }; say $point->{x}; say $point->{y};
record Point(int x, int y) {} class Main { public static void main(String[] args) { Point point = new Point(3, 4); System.out.println(point.x()); System.out.println(point.y()); System.out.println(point); // Point[x=3, y=4] — toString() generated automatically } }
This is a genuinely comparable pairing worth pausing on: Java's record (Java 16+) is the closest thing in this entire comparison to a lightweight Perl hash-based struct, and it is arguably the single least ceremony-heavy corner of the whole language. A one-line record declaration, record Point(int x, int y) {}, automatically generates a constructor, accessor methods named after the components (x() and y(), not the Java-conventional getX()), a working equals(), hashCode(), and a readable toString() — all four are hand-rolled or entirely absent for a plain Perl hash reference, which enforces no shape at all and generates nothing. All record components are implicitly final, making every record immutable and thread-safe by construction, a guarantee a plain Perl hash reference has no way to express.
Custom methods and immutable updates
use v5.38; my $point = { x => 3, y => 4 }; sub distance_from_origin { my ($point) = @_; return sqrt($point->{x}**2 + $point->{y}**2); } say distance_from_origin($point); my $moved = { x => $point->{x} + 1, y => $point->{y} + 1 }; # manual immutable "update" say "$moved->{x}, $moved->{y}";
record Point(int x, int y) { double distanceFromOrigin() { return Math.sqrt(x * x + y * y); } Point translate(int deltaX, int deltaY) { return new Point(x + deltaX, y + deltaY); // "modifying" a record returns a new instance } } class Main { public static void main(String[] args) { Point point = new Point(3, 4); Point moved = point.translate(1, 1); System.out.printf("%.2f%n", point.distanceFromOrigin()); System.out.println(moved); } }
Custom methods can be added inside a record body and have full access to its components as plain local variable names, no accessor call needed even from inside the record itself. Because every component is implicitly final, a method that "modifies" a record — translate here — must return a brand-new instance rather than mutating the existing one, exactly the immutable-update discipline a careful Perl programmer would otherwise have to enforce entirely by convention when building a similar pattern from a plain hash reference, with nothing stopping a less careful caller from mutating the hash directly instead.
Sealed classes + records — exhaustive data modeling
use v5.38; # Perl has no sealed classes and no exhaustiveness checking at all — # any package can inherit from any other, and nothing verifies that a # dispatch table or if/elsif chain covers every possible case. package Shape; sub new { bless {}, shift } package Circle; our @ISA = ('Shape'); package Rectangle; our @ISA = ('Shape'); # No compile-time guarantee that only these two exist.
sealed interface Shape permits Circle, Rectangle {} record Circle(double radius) implements Shape {} record Rectangle(double width, double height) implements Shape {} class Main { static double area(Shape shape) { return switch (shape) { case Circle circle -> Math.PI * circle.radius() * circle.radius(); case Rectangle rectangle -> rectangle.width() * rectangle.height(); // No default needed — the compiler knows these are the only two permitted subtypes }; } public static void main(String[] args) { System.out.printf("Circle area: %.2f%n", area(new Circle(5))); System.out.printf("Rectangle area: %.2f%n", area(new Rectangle(4, 6))); } }
Sealed classes (Java 17+) restrict which classes may extend or implement them — the permits clause names every allowed subtype — and combined with records, this gives Java a genuine algebraic-data-type story with compiler-verified exhaustiveness: if Shape can only ever be Circle or Rectangle, the switch expression above needs no default branch at all, because the compiler itself proves every case is handled. Perl has no equivalent whatsoever — duck typing means any package can inherit from any other at any time, so nothing can ever prove a dispatch table or if/elsif chain is exhaustive, and a missing case is only discovered when that exact code path runs (or never discovered at all).
Error Handling
die/eval vs try/catch/throw
use v5.38; eval { die "Something went wrong\n"; say "This won't print"; }; if ($@) { say "Caught: $@"; }
class Main { public static void main(String[] args) { try { throw new RuntimeException("Something went wrong"); // System.out.println("This won't print"); // unreachable — compile error if left in } catch (RuntimeException error) { System.out.println("Caught: " + error.getMessage()); } } }
die is Perl's throw; a bare eval { } block is the try-block equivalent of Java's try/catch. Java's error-handling ergonomics are a real improvement over Perl's global-variable-based approach: the caught exception object is bound directly to a local variable at the point of the catch clause (catch (RuntimeException error)), with no equivalent of Perl's easy-to-forget or easy-to-accidentally-clear $@ global to check manually. Java additionally distinguishes RuntimeException (unchecked, used here) from checked exceptions like IOException — the next concept covers exactly why that distinction matters and why it will surprise a Perl programmer more than anything else in this section.
Checked exceptions — mandatory declarations with no Perl analog
use v5.38; # Perl subroutines never declare what they might die with — die is # always available, unconditionally, from anywhere, with no signature # annotation of any kind required or even possible. sub read_config { die "config file missing\n" unless -e "config.txt"; return "loaded"; } eval { say read_config() }; say "Caught: $@" if $@;
import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; class Main { // A checked exception MUST be declared in the method signature with // "throws" — this is verified by the compiler, not discovered at runtime. static String readConfig() throws IOException { return Files.readString(Path.of("config.txt")); } public static void main(String[] args) { try { System.out.println(readConfig()); } catch (IOException error) { // The compiler FORCES this catch (or a "throws IOException" on // main itself) to exist — omitting it entirely is a compile error, // something Perl has absolutely no equivalent restriction for. System.out.println("Caught: " + error.getMessage()); } } }
This is the single biggest error-handling surprise for a Perl programmer moving to Java. Perl subroutines can die for any reason at any time, and nothing in the language requires — or even allows — declaring which failures a subroutine might raise; discovering an unhandled die means actually hitting it at runtime. Java's checked exceptions (like IOException here) are verified by the compiler itself: any method that can throw one must declare it with throws in its signature, and every caller must either catch it or declare it onward — omitting both is a compile error, not a runtime crash waiting to happen. Unchecked exceptions (subclasses of RuntimeException, as in the previous concept) opt out of this requirement entirely, which is why most everyday logic errors in Java use the unchecked family while I/O and other genuinely recoverable external failures use checked exceptions.
Blessed error objects vs Exception subclasses
use v5.38; package ValidationError; sub new { my ($class, $message, $field) = @_; return bless { message => $message, field => $field }, $class; } sub message { $_[0]->{message} } sub field { $_[0]->{field} } package main; eval { die ValidationError->new("must not be empty", "username"); }; if (my $error = $@) { if (ref($error) && $error->isa("ValidationError")) { say "Validation failed on '" . $error->field . "': " . $error->message; } }
class ValidationException extends RuntimeException { private final String field; ValidationException(String message, String field) { super(message); this.field = field; } String getField() { return field; } } class Main { public static void main(String[] args) { try { throw new ValidationException("must not be empty", "username"); } catch (ValidationException error) { System.out.println("Validation failed on '" + error.getField() + "': " + error.getMessage()); } } }
Perl can die with a blessed object instead of a plain string, letting the surrounding if branch on ref($error) && $error->isa("ValidationError") to distinguish error types — a manual pattern with no dedicated exception-class hierarchy backing it. Java's exception classes are designed to be subclassed directly: extends RuntimeException plus a super(message) call in the constructor produces a genuine exception type complete with a stack trace and full participation in Java's exception-matching machinery, and the catch (ValidationException error) clause matches by declared type natively — no isa-style runtime check needed, since the compiler and JVM both understand the type hierarchy directly.
Cleanup — local $@ save/restore vs finally
use v5.38; my $file_opened = 0; eval { $file_opened = 1; say "Working..."; die "failure during work\n"; }; my $error = $@; # save before anything can clobber it if ($file_opened) { say "Cleaning up (would close the file handle here)"; } say "Caught: $error" if $error;
class Main { public static void main(String[] args) { boolean fileOpened = false; try { fileOpened = true; System.out.println("Working..."); throw new RuntimeException("failure during work"); } catch (RuntimeException error) { System.out.println("Caught: " + error.getMessage()); } finally { if (fileOpened) { System.out.println("Cleaning up (would close the file handle here)"); } } } }
Perl has no dedicated cleanup block syntax in the language core — guaranteeing cleanup code runs whether or not die was called requires either careful manual bookkeeping (saving $@ immediately, since a later operation can silently clear it) or a module like Try::Tiny or core Feature::Compat::Try's finally block. Java's try/catch/finally has a dedicated finally clause built directly into core syntax: code inside it always runs, whether the try block completed successfully, threw an exception that was caught, or even if the catch block itself returned or re-threw — no manual state-saving required, and no risk of a later statement silently clobbering error state the way Perl's $@ can be overwritten by an unrelated subsequent operation.
Generics
Generics — a genuine Java-only concept
use v5.38; # Perl has no static type system at all, so there is nothing resembling # generics: a single Perl array or hash freely holds values of any type, # mixed together, with no declared "type parameter" of any kind. my @mixed = (1, "two", 3.0, [4, 5]); say ref($_) || "plain scalar" for @mixed;
import java.util.List; class Box<T> { private final T contents; Box(T contents) { this.contents = contents; } T get() { return contents; } } class Main { public static void main(String[] args) { Box<String> stringBox = new Box<>("hello"); Box<Integer> integerBox = new Box<>(42); System.out.println(stringBox.get()); System.out.println(integerBox.get()); // Box<String> wrongBox = new Box<>(42); // COMPILE ERROR: int is not a String List<Integer> numbers = List.of(1, 2, 3); // List<T> is itself a generic class System.out.println(numbers); } }
This is a concept with genuinely no Perl analog: Perl has no static type system whatsoever, so a single array or hash freely mixes values of any type with no declared constraint — there is nothing resembling a "type parameter" to even talk about. Java's generics, written with angle brackets (Box<T>, List<Integer>), let a class or method be written once and parameterized over whatever type its caller chooses, with the compiler verifying at every call site that the chosen type is used consistently — new Box<String>("hello") can never accidentally hold an Integer, because the compiler rejects that assignment before the program ever runs. Every one of Java's collection types (List<T>, Map<K, V>) is itself built on generics, which is why a type declaration like Map<String, Integer> appears throughout the Hashes section above.
Generic methods — one algorithm, any type
use v5.38; # Perl needs no special syntax here at all: a single subroutine already # works on any type of scalar, since nothing about Perl subroutines is # type-constrained in the first place. sub first_element { my (@items) = @_; return $items[0]; } say first_element(1, 2, 3); say first_element("a", "b", "c");
import java.util.List; class Main { // <T> before the return type declares a type parameter scoped to this // method alone — it works for a List<String>, a List<Integer>, or any // other type, all verified by the compiler, not merely by convention. static <T> T firstElement(List<T> items) { return items.get(0); } public static void main(String[] args) { System.out.println(firstElement(List.of(1, 2, 3))); System.out.println(firstElement(List.of("a", "b", "c"))); } }
Perl needs no special syntax to write a single subroutine that works on any type of value, since nothing about a Perl subroutine's parameters is ever type-constrained in the first place — genericity is simply the default, unremarked-upon behavior. Java has to earn the same flexibility explicitly through a generic method: the <T> before the return type introduces a type parameter scoped to that one method, letting firstElement work on a List<Integer> or a List<String> alike while the compiler still verifies, for each individual call, that the return type matches what the caller expects — genuine flexibility, but arrived at through explicit declaration rather than Perl's default absence of any type constraint to begin with.
Interfaces
Roles via duck typing vs a native interface
use v5.38; # Perl has no interface keyword — a "contract" is purely a convention: # any object with a method named info() satisfies it, and nothing # checks this at compile time or even declares the expectation formally. package Document; sub new { bless { title => $_[1] }, $_[0] } sub info { $_[0]->{title} } package main; my $document = Document->new("My Report"); say "Info: " . $document->info;
interface Printable { String info(); default void printInfo() { System.out.println("Info: " + info()); } } class Document implements Printable { private final String title; Document(String title) { this.title = title; } @Override public String info() { return title; } } class Main { public static void main(String[] args) { new Document("My Report").printInfo(); } }
Perl has no interface keyword at all — a "contract" like "this object must have an info method" exists purely as an unenforced convention, satisfied by duck typing: any object with a method of that name works, and nothing checks this formally or catches a missing method before runtime. Java's interface keyword declares a genuine, compiler-checked contract: a class declares implements Printable and the compiler refuses to compile it unless every abstract method (info() here) is actually implemented. Default methods (Java 8+, the printInfo() method here) let an interface provide a working method body that calls the abstract method a conforming class must supply — the closest Java equivalent to a Perl role or mixin, but with the method names and their required signatures verified ahead of time rather than discovered only when called.
Composing behavior — multiple roles vs multiple interfaces
use v5.38; package Duck; sub new { bless {}, shift } sub fly { "Duck is flying" } sub swim { "Duck is swimming" } # Perl has no formal role-composition keyword in core — this "composes" # two behaviors simply by both methods living on the same package. package main; my $duck = Duck->new; say $duck->fly; say $duck->swim;
interface Flyable { default String fly() { return "This creature is flying"; } } interface Swimmable { default String swim() { return "This creature is swimming"; } } class Duck implements Flyable, Swimmable {} class Main { public static void main(String[] args) { Duck duck = new Duck(); System.out.println(duck.fly()); System.out.println(duck.swim()); } }
A Java class may implement multiple interfaces at once, separated by commas — a formal, compiler-verified version of the ad hoc composition a Perl programmer gets simply by defining several methods on one package (or, more formally, using Role::Tiny or Moo/Moose roles from CPAN, which Java has no need of since the capability is built into the core language). If two implemented interfaces declared conflicting default methods with the same signature, Java would force the implementing class to override the method explicitly to resolve the ambiguity — a compile-time safeguard against silent conflicts that a Perl programmer composing behavior from multiple sources has no equivalent protection against. Java allows only single inheritance of classes (extends) but unlimited interface implementation (implements).
File & Console I/O
Reading a file
use v5.38; open(my $file_handle, '<', 'data.txt') or die "Cannot open: $!"; while (my $line = <$file_handle>) { chomp $line; say $line; } close $file_handle;
import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; class Main { public static void main(String[] args) throws IOException { List<String> lines = Files.readAllLines(Path.of("data.txt")); for (String line : lines) { System.out.println(line); } } }
Perl opens a file handle with a three-argument open call and reads it line by line with the diamond operator <$file_handle> inside a while loop, closing it explicitly afterward. Java's modern java.nio.file.Files API reads every line into a List<String> in one call, with the file handle opened and closed entirely internally — no explicit close to remember, at the cost of loading the whole file into memory at once rather than streaming it line by line. Note also that main itself declares throws IOException here, propagating the checked exception up rather than catching it locally — a legitimate, common Java pattern for a small program willing to let an I/O failure simply crash with a stack trace. Both sides assume a data.txt file already exists on disk, so neither can run in the sandboxed backends used by this page.
Writing a file
use v5.38; open(my $file_handle, '>', 'output.txt') or die "Cannot open: $!"; print $file_handle "Hello, World!\n"; close $file_handle;
import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; class Main { public static void main(String[] args) throws IOException { Files.writeString(Path.of("output.txt"), "Hello, World!\n"); } }
Perl opens the file handle in write mode with the > mode string, prints directly to the handle, and closes it explicitly. Java's Files.writeString() collapses the open-write-close sequence into a single call that handles opening and closing the underlying file descriptor internally — there is no separate handle object for the caller to manage or remember to close, at the cost of always writing the entire content in one call rather than streaming it incrementally the way Perl's handle-based approach naturally allows.
@ARGV vs the args array
use v5.38; # Run as: perl script.pl foo bar say "Number of arguments: " . scalar(@ARGV); say "First argument: $ARGV[0]" if @ARGV; say "All arguments: @ARGV";
class Main { // Run as: java Main.java foo bar // args contains ONLY the user-supplied arguments, indexed from 0 — // unlike Node.js's process.argv, there is no extra leading executable // or script path entry to skip. public static void main(String[] args) { System.out.println("Number of arguments: " + args.length); if (args.length > 0) { System.out.println("First argument: " + args[0]); } System.out.println("All arguments: " + String.join(" ", args)); } }
Perl's @ARGV contains exactly and only the user-supplied command-line arguments, indexed from 0. Java's args parameter to main matches this precisely — it too contains only the user-supplied arguments with no extra leading entries for the executable path or script name, unlike Node.js's process.argv, which does carry those two extra entries. This is one of the more comfortable, low-friction corners of Java for a Perl programmer: the mental model transfers with no offset adjustment needed at all.
%ENV vs System.getenv
use v5.38; my $home = $ENV{HOME} // "unset"; say "HOME: $home"; $ENV{MY_VAR} = "custom value"; # setting also works say $ENV{MY_VAR};
import java.util.Map; class Main { public static void main(String[] args) { String homeDirectory = System.getenv("HOME"); System.out.println("HOME: " + (homeDirectory != null ? homeDirectory : "unset")); // Unlike Perl's %ENV, Java's environment is READ-ONLY at the process // level — System.getenv() returns an immutable view. There is no // Java equivalent of assigning into %ENV; a child process's // environment can only be set via ProcessBuilder.environment(). Map<String, String> allVariables = System.getenv(); System.out.println(allVariables.size() + " environment variables visible"); } }
Perl's %ENV hash supports both reading and writing directly — assigning $ENV{MY_VAR} = "custom value" works immediately and is visible to any child process spawned afterward. Java's System.getenv() is read-only: it returns an immutable view of the current process's environment, and there is no way to modify the running JVM process's own environment variables at all from within Java code — setting an environment variable for a child process requires the separate ProcessBuilder.environment() API instead of a simple map assignment. Reading a missing variable returns null in Java rather than Perl's undef, but the defensive-check pattern is otherwise identical.
CPAN vs Maven Central
CPAN/cpanm vs Maven Central
# Installing a dependency: # cpanm JSON::PP # # Declaring dependencies (cpanfile): # requires 'JSON::PP', '4.16'; # requires 'HTTP::Tiny', '0.088'; # # Installing everything a cpanfile lists: # cpanm --installdeps .
// Declaring a dependency (pom.xml, if using Maven as the build tool): // <dependency> // <groupId>com.google.code.gson</groupId> // <artifactId>gson</artifactId> // <version>2.11.0</version> // </dependency> // // Installing everything pom.xml lists: // mvn install // // (Gradle is the other common build tool, with an equivalent // dependencies block in build.gradle instead of pom.xml.)
CPAN (the Comprehensive Perl Archive Network) predates Maven Central by roughly a decade and pioneered the idea of a central package repository with a dependency-declaration file — Perl's cpanfile plays a similar role to Maven's pom.xml or Gradle's build.gradle. The practical experience differs sharply, though: cpanm is a single, universally agreed-upon installer, while the Java ecosystem has two dominant, mutually incompatible build tools (Maven and Gradle), each with its own dependency-declaration syntax and its own way of resolving and downloading artifacts from Maven Central — a genuinely richer, more enterprise-oriented ecosystem than CPAN, but with more upfront tooling choice to make.
cpanfile.snapshot vs Maven's version pinning
# Perl's reproducible-install story is comparatively new and less # universal: Carton (a Bundler-inspired tool) generates a # cpanfile.snapshot pinning every dependency's exact resolved version. # carton install # carton exec perl script.pl
// Maven resolves dependency versions from pom.xml directly — pinning an // exact version (as in the previous example) already gives reproducible // builds with no separate lockfile format required: // mvn dependency:tree // shows exactly what got resolved, transitively // // Gradle instead supports an explicit lockfile: // dependencyLocking { lockAllConfigurations() } // gradle dependencies --write-locks
Reproducible installs are a more recent, optional addition to the Perl ecosystem: Carton, modeled explicitly on Ruby's Bundler, generates a cpanfile.snapshot pinning exact resolved versions, but it is a separate tool a project must opt into. Maven takes a different approach entirely: because pom.xml typically pins an exact version number per dependency already (as shown in the previous example), a Maven build is largely reproducible without any separate lockfile format, though transitive dependencies can still resolve differently across time unless explicitly managed with a dependencyManagement block. Gradle, the other major Java build tool, offers an explicit opt-in lockfile mechanism closer in spirit to Carton's snapshot file, for teams that want that stronger guarantee.
JSON::PP vs a third-party JSON library
use v5.38; use JSON::PP; my $data = { name => "Alice", age => 30, tags => ["admin", "verified"] }; my $json_text = encode_json($data); say $json_text; my $decoded = decode_json($json_text); say $decoded->{name};
// Java has NO built-in JSON support at all in its standard library — unlike // Perl's core-bundled JSON::PP, every Java project reaches for a third-party // library from Maven Central, most commonly Jackson or Gson: // // import com.google.gson.Gson; // // class Main { // record Person(String name, int age, java.util.List<String> tags) {} // // public static void main(String[] args) { // Gson gson = new Gson(); // Person person = new Person("Alice", 30, java.util.List.of("admin", "verified")); // String jsonText = gson.toJson(person); // System.out.println(jsonText); // // Person decoded = gson.fromJson(jsonText, Person.class); // System.out.println(decoded.name()); // } // }
Perl bundles JSON::PP directly in core Perl since 5.14, so use JSON::PP; plus encode_json/decode_json is available with no external dependency at all. Java, somewhat surprisingly given how enterprise-focused the language is, has no JSON support whatsoever in its standard library — every Java project needing JSON reaches for a third-party library from Maven Central, most commonly Google's Gson or the more feature-rich Jackson, and the resulting object-to-JSON mapping (as shown in the commented example here, since it depends on an external library the sandboxed backend cannot install) pairs especially naturally with the low-ceremony records covered earlier in this cheatsheet, since a record is already a plain, immutable data holder that a library like Gson can serialize with almost no configuration.