PONY λ M2 Modula-2

Java.CodeCompared.To/Scala

An interactive executable cheatsheet comparing Java and Scala

Java 26 Scala 2.13
Output & Comments
Hello World
class Main { public static void main(String[] args) { System.out.println("Hello, Scala!"); } }
object Main { def main(args: Array[String]): Unit = { println("Hello, Scala!") } }
Every runnable Scala program requires a main method on an object. println in Scala is available without a prefix — it comes from the automatically imported Predef object. Unit is the return type for methods that produce no meaningful value, equivalent to Java's void.
print vs println
class Main { public static void main(String[] args) { System.out.print("Hello, "); System.out.print("World!"); System.out.println(); System.out.println("Done"); } }
object Main { def main(args: Array[String]): Unit = { print("Hello, ") print("World!") println() // newline only println("Done") } }
print writes without a trailing newline; println adds one. Both behave identically to Java's System.out.print and System.out.println. Semicolons are optional in Scala — newlines serve as statement separators.
Formatted output
class Main { public static void main(String[] args) { String name = "Alice"; int age = 30; double score = 98.5; System.out.printf("Name: %s, Age: %d, Score: %.1f%n", name, age, score); String line = "Name: %s, Age: %d".formatted(name, age); System.out.println(line); } }
object Main { def main(args: Array[String]): Unit = { val name = "Alice" val age = 30 val score = 98.5 printf("Name: %s, Age: %d, Score: %.1f%n", name, age, score) val line = "Name: %s, Age: %d".format(name, age) println(line) } }
Scala's printf and .format mirror Java's System.out.printf and String.formatted. For most cases, prefer Scala's s-interpolation (s"Name: $name") or f-interpolation (f"Score: $score%.1f"), which are more concise and type-safe.
Comments
class Main { // Single-line comment /* Multi-line comment */ /** Javadoc comment for documentation */ public static void main(String[] args) { System.out.println("Comments"); // inline } }
// Single-line comment /* Multi-line comment */ /** Scaladoc comment for documentation */ object Main { def main(args: Array[String]): Unit = { println("Comments") // inline } }
Scala uses the same comment syntax as Java. Scaladoc uses /** ... */ with @param, @return, and @tparam (for type parameters) tags. Unlike Java, comments in Scala are often lighter — expressive types and names reduce the need for explanatory comments.
Variables & Type Inference
val (immutable) vs var (mutable)
class Main { public static void main(String[] args) { final int count = 42; // immutable int mutableCount = 0; // mutable var message = "Hello"; // type-inferred (Java 10+), still mutable mutableCount = 10; System.out.println(count); System.out.println(mutableCount); System.out.println(message); } }
object Main { def main(args: Array[String]): Unit = { val count = 42 // immutable — prefer this var mutableCount = 0 // mutable — use sparingly val message = "Hello" mutableCount = 10 println(count) println(mutableCount) println(message) } }
val in Scala corresponds to Java's final — the binding cannot be reassigned. The key difference is philosophy: Scala treats immutability as the default and discourages var. Java's var (Java 10+) infers the type but does not enforce immutability. In idiomatic Scala, mutable variables are rare.
Type inference
class Main { public static void main(String[] args) { var number = 42; // Java infers int — but var is mutable var greeting = "Hello"; // String var ratio = 3.14; // double System.out.println(number); System.out.println(greeting); System.out.println(ratio); } }
object Main { def main(args: Array[String]): Unit = { val number = 42 // Scala infers Int val greeting = "Hello" // String val ratio = 3.14 // Double println(number) println(greeting) println(ratio) } }
Scala infers types throughout the language — local variables, generic type parameters, and method return types (when not public API). Unlike Java's limited var, Scala's inference works in more contexts and combines with immutability. Explicit types are still recommended in public method signatures for documentation and stability.
Explicit type annotations
class Main { public static void main(String[] args) { int count = 10; String label = "items"; double ratio = 0.75; boolean ready = true; System.out.println(count + " " + label); System.out.println(ratio + ", " + ready); } }
object Main { def main(args: Array[String]): Unit = { val count: Int = 10 val label: String = "items" val ratio: Double = 0.75 val ready: Boolean = true println(s"$count $label") println(s"$ratio, $ready") } }
Scala's primitive types are capitalized: Int, Double, Boolean, Long, Float, Char, Byte, Short. Unlike Java, Scala has no distinction between primitive types and their boxed equivalents — the compiler handles boxing automatically where needed.
lazy val — deferred initialization
class Main { static int computeExpensive() { System.out.println("Computed!"); return 42; } public static void main(String[] args) { // Java: no lazy val — compute eagerly or use Supplier System.out.println("Before"); int result = computeExpensive(); // always computed immediately System.out.println("Result: " + result); } }
object Main { def computeExpensive(): Int = { println("Computed!") 42 } def main(args: Array[String]): Unit = { lazy val result = computeExpensive() println("Before") println(s"Result: $result") println(s"Again: $result") // cached — not recomputed } }
lazy val delays evaluation until first access, then caches the result permanently. Java has no built-in equivalent — developers use double-checked locking or Supplier<T> wrappers. lazy val is thread-safe in Scala: the first access initializes it exactly once. This is useful for expensive computations that may not always be needed.
Tuples — multiple values
class Main { record Pair(int x, int y) {} public static void main(String[] args) { // Java: use records or arrays for multiple return values var coordinates = new Pair(10, 20); System.out.println("x=" + coordinates.x() + ", y=" + coordinates.y()); } }
object Main { def main(args: Array[String]): Unit = { val coordinates = (10, 20) val (x, y) = coordinates // destructuring println(s"x=$x, y=$y") println(coordinates._1) // access by index (1-based) println(coordinates._2) val triple = ("Alice", 30, true) val (name, age, active) = triple println(s"$name age $age active=$active") } }
Scala tuples are typed, immutable, and support up to 22 elements (Tuple1 through Tuple22). A tuple (10, "Alice") has type (Int, String). Tuple destructuring via pattern matching is the idiomatic way to unpack values. Java has no built-in tuple — use record for named fields or an array for homogeneous elements.
Strings & Interpolation
s"" string interpolation
class Main { public static void main(String[] args) { String name = "Alice"; int age = 30; // Java: concatenation or String.formatted String greeting = "Hello, " + name + "! You are " + age + " years old."; System.out.println(greeting); System.out.println("Next year: " + (age + 1)); } }
object Main { def main(args: Array[String]): Unit = { val name = "Alice" val age = 30 val greeting = s"Hello, $name! You are $age years old." println(greeting) println(s"Next year: ${age + 1}") // braces allow any expression } }
The s"" prefix activates string interpolation, allowing $variable and ${expression} directly in the string. This is far cleaner than Java's string concatenation or String.formatted. Any Scala expression can appear inside ${...}.
String operations
class Main { public static void main(String[] args) { String text = "Hello, World!"; System.out.println(text.length()); System.out.println(text.toUpperCase()); System.out.println(text.contains("World")); System.out.println(text.replace("World", "Scala")); System.out.println(text.substring(7)); } }
object Main { def main(args: Array[String]): Unit = { val text = "Hello, World!" println(text.length) // no () needed for zero-arg methods println(text.toUpperCase) println(text.contains("World")) println(text.replace("World", "Scala")) println(text.substring(7)) } }
Scala strings are Java strings (java.lang.String), so all Java String methods work. By convention, zero-argument methods may be called without parentheses in Scala: text.length is equivalent to text.length(). Scala also adds extra methods to String via implicit conversions in StringOps.
Multi-line strings
class Main { public static void main(String[] args) { String poem = """ Roses are red, Violets are blue, Java has text blocks, Scala has them too. """; System.out.println(poem.strip()); } }
object Main { def main(args: Array[String]): Unit = { val poem = """Roses are red, |Violets are blue, |Scala has stripMargin, |Which is quite nice too.""".stripMargin println(poem) } }
Scala's triple-quoted strings preserve all whitespace literally — use stripMargin with the | prefix character to align multi-line content cleanly. Java 15+ text blocks normalize indentation automatically. Both approaches work; Scala's stripMargin is more explicit. Triple-quoted strings can also contain interpolation with the s prefix: s"""Hello, $name""".
f"" typed format interpolation
class Main { public static void main(String[] args) { double pi = Math.PI; int count = 42; System.out.printf("Pi: %.4f%n", pi); System.out.printf("Count: %05d%n", count); String result = "Pi: %.4f".formatted(pi); System.out.println(result); } }
object Main { def main(args: Array[String]): Unit = { val pi = math.Pi val count = 42 println(f"Pi: $pi%.4f") println(f"Count: $count%05d") val result = f"Pi: $pi%.4f" println(result) } }
The f"" interpolator is Scala's type-safe printf — format specifiers follow the variable reference with %. Unlike printf, the types are checked at compile time: f"$pi%d" is a compile error because pi is a Double, not an Int. Use s"" for simple embedding and f"" when numeric formatting is needed.
split, strip, and join
class Main { public static void main(String[] args) { String text = " hello, world, scala "; System.out.println(text.strip()); String[] parts = text.strip().split(", "); System.out.println(parts[0]); System.out.println(parts.length); System.out.println(String.join(" | ", parts)); } }
object Main { def main(args: Array[String]): Unit = { val text = " hello, world, scala " println(text.strip()) val parts = text.strip().split(", ") println(parts(0)) // Array indexing uses () not [] println(parts.length) println(parts.mkString(" | ")) } }
Array indexing in Scala uses () instead of [] — arrays are objects with an apply method, so parts(0) is syntactic sugar for parts.apply(0). For joining sequences, mkString on a collection is more idiomatic than String.join. All Java String methods (strip, split, replace, etc.) work identically.
Collections
List — immutable linked list
import java.util.List; import java.util.ArrayList; class Main { public static void main(String[] args) { var numbers = List.of(1, 2, 3, 4, 5); // immutable System.out.println(numbers); System.out.println(numbers.get(0)); System.out.println(numbers.size()); var extended = new ArrayList<>(numbers); extended.add(6); System.out.println(extended); } }
object Main { def main(args: Array[String]): Unit = { val numbers = List(1, 2, 3, 4, 5) println(numbers) println(numbers.head) // first element println(numbers(0)) // by index println(numbers.length) val prepended = 0 :: numbers // prepend — O(1), returns new List println(prepended) val appended = numbers :+ 6 // append — O(n), returns new List println(appended) } }
Scala's List is an immutable singly-linked list — "modifications" return a new list. :: prepends efficiently (O(1)); :+ appends less efficiently (O(n)). For frequent appending, prefer Vector. Java's List.of() is also immutable but uses index-based get(i); Scala uses list(i) syntax (the apply method).
Vector — efficient indexed collection
import java.util.ArrayList; import java.util.List; class Main { public static void main(String[] args) { var items = new ArrayList<String>(List.of("alpha", "beta", "gamma")); System.out.println(items.get(1)); // O(1) random access items.add("delta"); System.out.println(items.size()); System.out.println(items); } }
object Main { def main(args: Array[String]): Unit = { val items = Vector("alpha", "beta", "gamma") println(items(1)) // near-O(1) random access val updated = items :+ "delta" // returns new Vector println(updated.size) println(updated) } }
Scala's Vector is the default choice for indexed sequences — it offers near-O(1) random access, prepend (+:), and append (:+) via persistent data structure techniques. Unlike List, Vector supports efficient indexed access. Java's ArrayList is mutable; Scala's Vector returns a new collection for every "modification".
Map — immutable key-value store
import java.util.Map; class Main { public static void main(String[] args) { var person = Map.of("name", "Alice", "city", "Paris"); System.out.println(person.get("name")); System.out.println(person.getOrDefault("age", "unknown")); System.out.println(person.containsKey("city")); System.out.println(person.size()); } }
object Main { def main(args: Array[String]): Unit = { val person = Map("name" -> "Alice", "city" -> "Paris") println(person("name")) // throws if missing println(person.getOrElse("age", "unknown")) println(person.contains("city")) println(person.size) val updated = person + ("country" -> "France") // new Map println(updated) } }
Scala's immutable Map uses -> to create key-value pairs (shorthand for Tuple2). map("key") throws NoSuchElementException if absent — use getOrElse for safe access, or get which returns Option[V]. The + operator returns a new map with the entry added; - returns one with a key removed.
Set — immutable unique elements
import java.util.Set; import java.util.HashSet; class Main { public static void main(String[] args) { var fruits = Set.of("apple", "banana", "cherry"); System.out.println(fruits.contains("apple")); System.out.println(fruits.size()); var more = new HashSet<>(fruits); more.add("mango"); System.out.println(more.size()); more.retainAll(Set.of("apple", "grape")); // intersection in-place System.out.println(more); } }
object Main { def main(args: Array[String]): Unit = { val fruits = Set("apple", "banana", "cherry") println(fruits.contains("apple")) println(fruits.size) val extended = fruits + "mango" // new Set println(extended.size) val overlap = fruits & Set("apple", "grape") // intersection println(overlap) val union = fruits | Set("mango", "grape") // union println(union) } }
Scala's immutable Set supports set operations: + (add element), - (remove), & (intersection), | (union), &~ or -- (difference). Each operation returns a new Set. Java's Set operations like retainAll are destructive; Scala's are pure.
Mutable collections
import java.util.ArrayList; class Main { public static void main(String[] args) { var numbers = new ArrayList<Integer>(); numbers.add(10); numbers.add(20); numbers.add(30); numbers.remove(Integer.valueOf(20)); System.out.println(numbers); System.out.println(numbers.size()); } }
import scala.collection.mutable.ArrayBuffer object Main { def main(args: Array[String]): Unit = { val numbers = ArrayBuffer[Int]() numbers += 10 numbers += 20 numbers += 30 numbers -= 20 println(numbers) println(numbers.size) } }
Mutable collections in Scala require an explicit import from scala.collection.mutable. The immutable equivalents are always in scope by default — Scala's design makes immutability the path of least resistance. ArrayBuffer is the equivalent of Java's ArrayList: indexed, resizable, O(1) amortized append. The += and -= operators modify the buffer in place.
Control Flow
if/else as an expression
class Main { public static void main(String[] args) { int score = 75; String grade; if (score >= 90) grade = "A"; else if (score >= 70) grade = "B"; else grade = "C"; System.out.println(grade); // Ternary for inline value String result = score >= 70 ? "Pass" : "Fail"; System.out.println(result); } }
object Main { def main(args: Array[String]): Unit = { val score = 75 // Scala if/else is an expression — it returns a value val grade = if (score >= 90) "A" else if (score >= 70) "B" else "C" println(grade) val result = if (score >= 70) "Pass" else "Fail" println(result) } }
In Scala, if/else is an expression that returns a value — there is no ternary operator because if already serves that role. Java's if is a statement; to use it for an assignment without a mutable variable, you need Java's ternary (?:). The Scala version eliminates the mutable intermediate variable entirely.
while loop
class Main { public static void main(String[] args) { int counter = 1; while (counter <= 5) { System.out.println(counter); counter++; } } }
object Main { def main(args: Array[String]): Unit = { var counter = 1 while (counter <= 5) { println(counter) counter += 1 // Scala has no ++ operator } } }
Scala has no ++ or -- operators — use += 1 and -= 1 instead. The while loop requires a mutable variable, which is idiomatic but rare in functional Scala code. For iteration over known bounds, prefer ranges with for or recursive functions.
for loop with ranges
class Main { public static void main(String[] args) { for (int i = 1; i <= 5; i++) { System.out.println(i); } int[] numbers = {10, 20, 30}; for (int number : numbers) { System.out.println(number); } } }
object Main { def main(args: Array[String]): Unit = { for (i <- 1 to 5) { println(i) } val numbers = List(10, 20, 30) for (number <- numbers) { println(number) } // 1 until 5 excludes upper bound: 1, 2, 3, 4 for (i <- 1 until 5) print(s"$i ") println() } }
Scala's for uses <- (a generator) to iterate over any iterable — ranges, lists, sets, etc. 1 to 5 is inclusive; 1 until 5 excludes the upper bound. Scala has no C-style three-part for loop. The for is syntactic sugar for foreach, flatMap, map, and filter calls.
match expression — switch equivalent
class Main { public static void main(String[] args) { int day = 3; String name = switch (day) { case 1 -> "Monday"; case 2 -> "Tuesday"; case 3 -> "Wednesday"; case 5 -> "Friday"; default -> "Other"; }; System.out.println(name); } }
object Main { def main(args: Array[String]): Unit = { val day = 3 val name = day match { case 1 => "Monday" case 2 => "Tuesday" case 3 => "Wednesday" case 5 => "Friday" case _ => "Other" } println(name) } }
Scala's match expression replaces Java's switch. Like Java 14's switch expressions, match returns a value. The _ wildcard matches anything, like Java's default. Scala's match is far more powerful — it supports type matching, destructuring, guards, and collection patterns, all covered in the Pattern Matching section.
match on strings, alternation, binding
class Main { public static void main(String[] args) { String status = "active"; String description = switch (status) { case "active", "enabled" -> "Running"; case "inactive", "disabled" -> "Stopped"; default -> "Unknown: " + status; }; System.out.println(description); } }
object Main { def main(args: Array[String]): Unit = { val status = "active" val description = status match { case "active" | "enabled" => "Running" case "inactive" | "disabled" => "Stopped" case other => s"Unknown: $other" } println(description) } }
In Scala's match, the | operator combines multiple patterns in a single case. Binding a name (like other) instead of _ captures the matched value for use in the result expression. Java's switch uses commas to combine cases; Scala uses |. The captured name can be used in guard conditions too.
Pattern Matching
Type pattern matching
class Main { public static void main(String[] args) { Object value = 42; // Java 16+: pattern matching for instanceof if (value instanceof Integer number) { System.out.println("Integer: " + (number * 2)); } else if (value instanceof String text) { System.out.println("String: " + text.toUpperCase()); } else { System.out.println("Other: " + value); } } }
object Main { def main(args: Array[String]): Unit = { val value: Any = 42 val description = value match { case number: Int => s"Integer: ${number * 2}" case text: String => s"String: ${text.toUpperCase}" case other => s"Other: $other" } println(description) } }
Scala's type pattern case x: Type both checks the type and binds the value — making the cast implicit. Java 16+ added similar syntax (instanceof Integer number), but only inside if chains, not in a unified match expression. Scala's approach is more composable: all type tests, bindings, and guards coexist in one construct.
Destructuring case classes in match
class Main { record Point(int x, int y) {} record Circle(Point center, double radius) {} public static void main(String[] args) { Object shape = new Circle(new Point(0, 0), 5.0); // Java 21+: nested record patterns if (shape instanceof Circle(Point(int x, int y), double r)) { System.out.printf("Circle at (%d,%d) r=%.1f%n", x, y, r); } } }
case class Point(x: Int, y: Int) case class Circle(center: Point, radius: Double) object Main { def main(args: Array[String]): Unit = { val shape: Any = Circle(Point(0, 0), 5.0) shape match { case Circle(Point(x, y), radius) => println(f"Circle at ($x,$y) r=$radius%.1f") case _ => println("Unknown shape") } } }
Scala case classes support deep destructuring in match — nested structures are unpacked in one expression. Java 21's record patterns offer similar nesting, but only with records and in instanceof/switch. Scala's pattern matching predates Java's by over a decade and integrates with guards, sealed-type exhaustiveness checking, and collection patterns.
Pattern guards
class Main { public static void main(String[] args) { int score = 85; // Java: switch on ranges requires workarounds String grade = switch (score / 10) { case 10, 9 -> "A"; case 8 -> "B"; case 7 -> "C"; default -> score >= 0 ? "F" : "Invalid"; }; System.out.println(grade); } }
object Main { def main(args: Array[String]): Unit = { val score = 85 val grade = score match { case s if s >= 90 => "A" case s if s >= 80 => "B" case s if s >= 70 => "C" case s if s >= 0 => "F" case _ => "Invalid" } println(grade) } }
Pattern guards add an if condition after a case pattern. The variable bound by the pattern (s here) is available in the guard condition. Java's switch does not directly support range guards — you need integer division tricks or nested if statements. Scala's guards work with any pattern and any boolean expression.
Tuple pattern matching
class Main { public static void main(String[] args) { int x = 1, y = 2; // Java: no tuple matching — separate if/else chains String quadrant; if (x > 0 && y > 0) quadrant = "I"; else if (x < 0 && y > 0) quadrant = "II"; else if (x < 0 && y < 0) quadrant = "III"; else if (x > 0 && y < 0) quadrant = "IV"; else quadrant = "on axis"; System.out.println(quadrant); } }
object Main { def main(args: Array[String]): Unit = { val coordinates = (1, 2) val quadrant = coordinates match { case (x, y) if x > 0 && y > 0 => "I" case (x, y) if x < 0 && y > 0 => "II" case (x, y) if x < 0 && y < 0 => "III" case (x, y) if x > 0 && y < 0 => "IV" case _ => "on axis" } println(quadrant) } }
Tuple matching in Scala destructures all elements simultaneously. Java has no native tuple type or matching support — this logic requires chained if/else on separate variables. The Scala version matches on the structure of the data directly, making the intent explicit and the code more compact.
List pattern matching (head :: tail)
import java.util.List; class Main { public static void main(String[] args) { var numbers = List.of(1, 2, 3); // Java: no list pattern matching if (numbers.isEmpty()) { System.out.println("empty"); } else { int head = numbers.get(0); System.out.println("head=" + head + " size=" + numbers.size()); } } }
object Main { def main(args: Array[String]): Unit = { val numbers = List(1, 2, 3) numbers match { case Nil => println("empty") case head :: Nil => println(s"single: $head") case first :: second :: _ => println(s"first=$first second=$second") } List[Int]() match { case Nil => println("empty list") case head :: _ => println(s"starts with $head") } } }
Scala's :: is both the list cons constructor and a pattern: head :: tail matches any non-empty list, binding head to the first element and tail to the remainder. Nil matches an empty list. This eliminates index-based access that characterizes Java list processing. Java has no equivalent list pattern matching.
Methods & Functions
def — method definitions
class Main { static int add(int a, int b) { return a + b; } static boolean isEven(int number) { return number % 2 == 0; } public static void main(String[] args) { System.out.println(add(3, 4)); System.out.println(isEven(6)); } }
object Main { def add(a: Int, b: Int): Int = a + b def isEven(number: Int): Boolean = number % 2 == 0 def main(args: Array[String]): Unit = { println(add(3, 4)) println(isEven(6)) } }
Scala method syntax uses def with the return type after a colon. Single-expression methods omit braces and return — the last expression is automatically returned. Scala discourages explicit return statements. Methods on an object are the equivalent of Java's static methods.
Default parameter values
class Main { static String greet(String name, String greeting) { return greeting + ", " + name + "!"; } // Java: overloading to simulate defaults 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")); } }
object Main { def greet(name: String, greeting: String = "Hello"): String = s"$greeting, $name!" def main(args: Array[String]): Unit = { println(greet("Alice")) println(greet("Bob", "Hi")) println(greet("Carol", greeting = "Hey")) } }
Scala supports default parameter values directly — no overloading needed. Java developers often write multiple overloaded methods just to provide defaults, which scatters the logic. Scala's approach reduces boilerplate and makes the default values explicit in one place. Default values can appear for any parameter, not only the last one.
Named arguments
class Main { static String formatDate(int year, int month, int day) { return String.format("%04d-%02d-%02d", year, month, day); } public static void main(String[] args) { // Java: positional only — easy to confuse year/month/day System.out.println(formatDate(2026, 5, 25)); } }
object Main { def formatDate(year: Int, month: Int, day: Int): String = f"$year%04d-$month%02d-$day%02d" def main(args: Array[String]): Unit = { println(formatDate(year = 2026, month = 5, day = 25)) println(formatDate(day = 25, year = 2026, month = 5)) // reordered println(formatDate(2026, 5, 25)) // positional still works } }
Scala named arguments let callers specify parameters by name, allowing any order. This eliminates the common Java bug of passing arguments in the wrong order — especially for methods with multiple parameters of the same type (like three int values for year, month, day). Java has no named argument support; all calls are positional.
Varargs (repeated parameters)
class Main { static int sum(int... numbers) { int total = 0; for (int n : numbers) total += n; return total; } public static void main(String[] args) { System.out.println(sum(1, 2, 3)); System.out.println(sum(10, 20)); } }
object Main { def sum(numbers: Int*): Int = numbers.sum def main(args: Array[String]): Unit = { println(sum(1, 2, 3)) println(sum(10, 20)) val values = List(1, 2, 3, 4) println(sum(values: _*)) // spread collection into varargs } }
Scala varargs use Type* (vs Java's Type...). The numbers.sum call uses the built-in sum available on numeric sequences. To expand a collection into varargs, use the : _* splice syntax — equivalent to Java's array spreading, but works on any Seq, not just arrays.
Multiple parameter lists (currying)
import java.util.function.Function; class Main { static Function<Integer, Integer> adder(int addend) { return (number) -> number + addend; } public static void main(String[] args) { var addFive = adder(5); System.out.println(addFive.apply(3)); System.out.println(addFive.apply(10)); } }
object Main { def add(addend: Int)(number: Int): Int = number + addend def main(args: Array[String]): Unit = { val addFive = add(5) _ // partially apply first list println(addFive(3)) println(addFive(10)) println(add(5)(3)) // full application at once } }
Scala methods can have multiple parameter lists. This enables currying (partial application), improves type inference for generic parameters, and allows the last parameter list to serve as a block argument (for DSL-style APIs). Java achieves currying only through explicit Function<A, Function<B, C>> chaining or wrapping, which is verbose.
Higher-Order Functions
Lambda expressions
import java.util.function.*; class Main { public static void main(String[] args) { Function<Integer, Integer> square = x -> x * x; BiFunction<Integer, Integer, Integer> multiply = (a, b) -> a * b; Predicate<Integer> isPositive = n -> n > 0; System.out.println(square.apply(5)); System.out.println(multiply.apply(3, 4)); System.out.println(isPositive.test(-1)); } }
object Main { def main(args: Array[String]): Unit = { val square: Int => Int = x => x * x val multiply: (Int, Int) => Int = (a, b) => a * b val isPositive: Int => Boolean = n => n > 0 println(square(5)) println(multiply(3, 4)) println(isPositive(-1)) } }
Scala function types use A => B (single argument) or (A, B) => C (two arguments). Unlike Java's many functional interfaces (Function, BiFunction, Predicate, Supplier, etc.), Scala uses a unified FunctionN hierarchy. Calling a function value uses () directly — no .apply() needed, though it works too.
map and filter on collections
import java.util.List; import java.util.stream.Collectors; class Main { public static void main(String[] args) { var numbers = List.of(1, 2, 3, 4, 5, 6); var evens = numbers.stream() .filter(n -> n % 2 == 0) .collect(Collectors.toList()); var doubled = numbers.stream() .map(n -> n * 2) .collect(Collectors.toList()); System.out.println(evens); System.out.println(doubled); } }
object Main { def main(args: Array[String]): Unit = { val numbers = List(1, 2, 3, 4, 5, 6) val evens = numbers.filter(n => n % 2 == 0) val doubled = numbers.map(n => n * 2) println(evens) println(doubled) // Placeholder _ shorthand for a single argument val tripled = numbers.map(_ * 3) println(tripled) } }
Scala collections have map, filter, and other higher-order methods directly — no .stream() or .collect() wrapping needed. The _ placeholder is shorthand for a single lambda argument: _ * 3 means x => x * 3. Operations return the same collection type: List.map returns List, Vector.filter returns Vector.
foldLeft and reduce
import java.util.List; class Main { public static void main(String[] args) { var numbers = List.of(1, 2, 3, 4, 5); int total = numbers.stream().reduce(0, Integer::sum); int product = numbers.stream().reduce(1, (a, b) -> a * b); System.out.println(total); System.out.println(product); System.out.println(numbers.stream().mapToInt(Integer::intValue).sum()); } }
object Main { def main(args: Array[String]): Unit = { val numbers = List(1, 2, 3, 4, 5) println(numbers.sum) // built-in for numeric types println(numbers.product) // built-in println(numbers.foldLeft(0)(_ + _)) // explicit: 0 + 1 + 2 + 3 + 4 + 5 println(numbers.foldLeft(1)(_ * _)) // product println(numbers.mkString(", ")) // join as string } }
foldLeft takes an initial value and a binary function, combining them left to right. The _ + _ shorthand means (acc, element) => acc + element. Scala collections have built-in sum, product, min, and max for numeric types. mkString joins elements with a separator, far simpler than Java's Stream reduce workaround.
Methods as function values
import java.util.List; import java.util.function.Function; import java.util.stream.Collectors; class Main { static int doubleIt(int n) { return n * 2; } static boolean isOdd(int n) { return n % 2 != 0; } public static void main(String[] args) { Function<Integer, Integer> fn = Main::doubleIt; var result = List.of(1, 2, 3, 4, 5).stream() .filter(Main::isOdd) .map(fn) .collect(Collectors.toList()); System.out.println(result); } }
object Main { def doubleIt(n: Int): Int = n * 2 def isOdd(n: Int): Boolean = n % 2 != 0 def main(args: Array[String]): Unit = { val fn = doubleIt _ // eta expansion: method → function val result = List(1, 2, 3, 4, 5) .filter(isOdd) // method reference directly .map(fn) println(result) } }
Scala converts methods to function values with the _ suffix (doubleIt _). Methods can also be passed directly where a function is expected — Scala performs automatic eta expansion. Java uses :: method references (Main::doubleIt). In Scala, def defines a method; val f: Int => Int = ... defines a function value — they differ in subtle ways for advanced use.
Case Classes
Defining case classes
class Main { record Person(String name, int age) {} public static void main(String[] args) { var person = new Person("Alice", 30); System.out.println(person.name()); System.out.println(person.age()); System.out.println(person); } }
case class Person(name: String, age: Int) object Main { def main(args: Array[String]): Unit = { val person = Person("Alice", 30) // no new keyword needed println(person.name) println(person.age) println(person) } }
Scala case classes are similar to Java records but predate them by many years. Key differences: no new keyword needed (the companion object's apply handles construction), field access without parentheses (person.name not person.name()), and built-in toString, equals, hashCode, and copy. Case classes integrate seamlessly with pattern matching.
copy — cloning with field overrides
class Main { record Point(int x, int y) { Point withX(int newX) { return new Point(newX, y); } Point withY(int newY) { return new Point(x, newY); } } public static void main(String[] args) { var origin = new Point(0, 0); var moved = origin.withX(5).withY(3); System.out.println(origin); System.out.println(moved); } }
case class Point(x: Int, y: Int) object Main { def main(args: Array[String]): Unit = { val origin = Point(0, 0) val moved = origin.copy(x = 5, y = 3) val sameY = moved.copy(x = 10) // only change x println(origin) println(moved) println(sameY) } }
The copy method is auto-generated for every case class, creating a new instance with specified fields changed and others unchanged. Java records require manually written withX / withY methods for this pattern. copy is essential for "update" operations on immutable data — it is the functional equivalent of setters.
Structural equality with ==
class Main { record Color(int red, int green, int blue) {} public static void main(String[] args) { var red1 = new Color(255, 0, 0); var red2 = new Color(255, 0, 0); System.out.println(red1.equals(red2)); // true — structural System.out.println(red1 == red2); // false — reference System.out.println(red1); } }
case class Color(red: Int, green: Int, blue: Int) object Main { def main(args: Array[String]): Unit = { val red1 = Color(255, 0, 0) val red2 = Color(255, 0, 0) println(red1 == red2) // true — structural equality via equals println(red1.eq(red2)) // false — reference equality println(red1) } }
Scala's == calls equals — for case classes this is structural equality comparing all fields. Java's == is always reference equality; you must use .equals() for value comparison. In Scala, use eq for reference equality (rarely needed outside of performance-critical code). Java records also implement structural equals, so the behavior is similar.
Pattern matching with case classes
class Main { sealed interface Shape permits Circle, Rectangle {} record Circle(double radius) implements Shape {} record Rectangle(double width, double height) implements Shape {} static double area(Shape shape) { return switch (shape) { case Circle(double r) -> Math.PI * r * r; case Rectangle(double w, double h) -> w * h; }; } public static void main(String[] args) { System.out.printf("%.2f%n", area(new Circle(5))); System.out.printf("%.2f%n", area(new Rectangle(4, 6))); } }
sealed trait Shape case class Circle(radius: Double) extends Shape case class Rectangle(width: Double, height: Double) extends Shape object Main { def area(shape: Shape): Double = shape match { case Circle(radius) => math.Pi * radius * radius case Rectangle(width, height) => width * height } def main(args: Array[String]): Unit = { println(f"${area(Circle(5))}%.2f") println(f"${area(Rectangle(4, 6))}%.2f") } }
Sealed traits + case classes form Scala's algebraic data types (ADTs). The compiler verifies that all cases in a match are covered — if you add a new case class extending the sealed trait, every incomplete match is flagged. Java 17+ sealed interfaces with records provide a similar feature, inspired by Scala's design. Exhaustiveness checking prevents entire categories of runtime bugs.
Sealed trait as algebraic data type
class Main { sealed interface Result<T> permits Ok, Err {} record Ok<T>(T value) implements Result<T> {} record Err<T>(String msg) implements Result<T> {} static Result<Integer> divide(int a, int b) { if (b == 0) return new Err<>("Division by zero"); return new Ok<>(a / b); } public static void main(String[] args) { var result = divide(10, 2); switch (result) { case Ok<Integer>(var v) -> System.out.println("Ok: " + v); case Err<Integer>(var e) -> System.out.println("Err: " + e); } } }
sealed trait Result[+T] case class Ok[T](value: T) extends Result[T] case class Err(msg: String) extends Result[Nothing] object Main { def divide(a: Int, b: Int): Result[Int] = if (b == 0) Err("Division by zero") else Ok(a / b) def main(args: Array[String]): Unit = { divide(10, 2) match { case Ok(value) => println(s"Ok: $value") case Err(error) => println(s"Err: $error") } divide(10, 0) match { case Ok(value) => println(s"Ok: $value") case Err(error) => println(s"Err: $error") } } }
This is Scala's idiomatic way to model computations that can succeed or fail. The +T covariance annotation means Result[Dog] is a Result[Animal]. Result[Nothing] for Err lets it be used where any Result[T] is expected — since Nothing is a subtype of everything. Java's generics are invariant, requiring Err<T> instead.
Traits & OOP
Defining and implementing traits
class Main { interface Greeter { String greet(String name); // Java 8+: default method default String greetLoudly(String name) { return greet(name).toUpperCase(); } } static class FormalGreeter implements Greeter { public String greet(String name) { return "Good day, " + name + "."; } } public static void main(String[] args) { Greeter greeter = new FormalGreeter(); System.out.println(greeter.greet("Alice")); System.out.println(greeter.greetLoudly("Alice")); } }
trait Greeter { def greet(name: String): String def greetLoudly(name: String): String = greet(name).toUpperCase } class FormalGreeter extends Greeter { def greet(name: String): String = s"Good day, $name." } object Main { def main(args: Array[String]): Unit = { val greeter: Greeter = new FormalGreeter() println(greeter.greet("Alice")) println(greeter.greetLoudly("Alice")) } }
Scala traits are more powerful than Java interfaces — they can have abstract members, concrete methods, fields, and state. Java's default methods (Java 8+) are a subset of what traits have always supported. Traits are the primary mechanism for code reuse and polymorphism in Scala. The keyword override is required (not optional) when overriding a member.
Multiple trait mixins
class Main { interface Logger { default void log(String msg) { System.out.println("[LOG] " + msg); } } interface Validator { default boolean validate(String s) { return s != null && !s.isBlank(); } } static class UserService implements Logger, Validator { void create(String username) { if (!validate(username)) { log("Invalid username"); return; } log("Creating user: " + username); } } public static void main(String[] args) { var service = new UserService(); service.create("alice"); service.create(""); } }
trait Logger { def log(msg: String): Unit = println(s"[LOG] $msg") } trait Validator { def validate(s: String): Boolean = s != null && s.nonEmpty } class UserService extends Logger with Validator { def create(username: String): Unit = { if (!validate(username)) { log("Invalid username"); return } log(s"Creating user: $username") } } object Main { def main(args: Array[String]): Unit = { val service = new UserService() service.create("alice") service.create("") } }
Scala uses extends FirstTrait with SecondTrait with ThirdTrait for mixin composition. Unlike Java's implements, Scala traits can hold mutable state. Scala resolves diamond-inheritance issues through linearization — a deterministic method resolution order. Traits can also be mixed in at instantiation time: new UserService() with ExtraLogger.
Abstract class
class Main { abstract static class Animal { protected String name; Animal(String name) { this.name = name; } abstract String sound(); void speak() { System.out.println(name + " says " + sound()); } } static class Dog extends Animal { Dog(String name) { super(name); } String sound() { return "Woof!"; } } public static void main(String[] args) { Animal dog = new Dog("Rex"); dog.speak(); } }
abstract class Animal(val name: String) { def sound(): String def speak(): Unit = println(s"$name says ${sound()}") } class Dog(name: String) extends Animal(name) { def sound(): String = "Woof!" } object Main { def main(args: Array[String]): Unit = { val dog: Animal = new Dog("Rex") dog.speak() } }
Scala abstract classes work like Java's — they can have abstract methods and constructor parameters, but cannot be instantiated directly. Constructor parameters become val fields automatically when declared with val. For code sharing in Scala, prefer trait over abstract class; use abstract class only when you need constructor parameters or Java interoperability.
Overriding methods (mandatory override keyword)
class Main { static class Vehicle { String describe() { return "A vehicle"; } int maxSpeed() { return 100; } } static class SportsCar extends Vehicle { @Override String describe() { return "A sports car"; } @Override int maxSpeed() { return 250; } } public static void main(String[] args) { Vehicle car = new SportsCar(); System.out.println(car.describe()); System.out.println(car.maxSpeed()); } }
class Vehicle { def describe(): String = "A vehicle" def maxSpeed(): Int = 100 } class SportsCar extends Vehicle { override def describe(): String = "A sports car" override def maxSpeed(): Int = 250 } object Main { def main(args: Array[String]): Unit = { val car: Vehicle = new SportsCar() println(car.describe()) println(car.maxSpeed()) } }
Scala requires the override keyword — omitting it is a compile error. This prevents accidental overriding when a base class adds a new method that happens to match a subclass method name. Java's @Override annotation is optional and produces only a warning if absent. Scala's approach catches an entire class of subtle bugs at compile time.
apply — factory method pattern
class Main { static class Celsius { private final double degrees; private Celsius(double degrees) { this.degrees = degrees; } double getDegrees() { return degrees; } public String toString() { return String.format("%.1f°C", degrees); } static Celsius of(double degrees) { return new Celsius(degrees); } static Celsius fromFahrenheit(double f) { return new Celsius((f - 32) * 5.0 / 9); } } public static void main(String[] args) { System.out.println(Celsius.of(100)); System.out.println(Celsius.fromFahrenheit(212)); } }
class Celsius private (val degrees: Double) { override def toString: String = f"$degrees%.1f°C" } object Celsius { def apply(degrees: Double): Celsius = new Celsius(degrees) def fromFahrenheit(fahrenheit: Double): Celsius = new Celsius((fahrenheit - 32) * 5.0 / 9.0) } object Main { def main(args: Array[String]): Unit = { println(Celsius(100)) // calls Celsius.apply(100) println(Celsius.fromFahrenheit(212)) } }
When a companion object defines apply(...), the class can be called like a function: Celsius(100) calls Celsius.apply(100). This is how all Scala case classes and collection constructors work. Java's static factory methods like Celsius.of(...) are the closest equivalent. The apply pattern creates natural construction syntax without new.
Option Type
Option[T], Some, and None
import java.util.Optional; class Main { static Optional<String> findUser(int id) { if (id == 1) return Optional.of("Alice"); return Optional.empty(); } public static void main(String[] args) { var user = findUser(1); System.out.println(user.isPresent()); System.out.println(user.get()); var missing = findUser(99); System.out.println(missing.isPresent()); System.out.println(missing.orElse("unknown")); } }
object Main { def findUser(id: Int): Option[String] = if (id == 1) Some("Alice") else None def main(args: Array[String]): Unit = { val user = findUser(1) println(user.isDefined) // true println(user.get) // "Alice" — throws if None val missing = findUser(99) println(missing.isDefined) // false println(missing.getOrElse("unknown")) } }
Option[T] is Scala's built-in equivalent of Java's Optional<T>. Some(value) wraps a value; None represents absence. Unlike Java's null, None is a proper value with a type — you cannot accidentally call methods on it. The Scala convention is to never return null from a public API — use Option instead.
getOrElse and orElse
import java.util.Optional; class Main { public static void main(String[] args) { Optional<String> name = Optional.of("Alice"); Optional<String> empty = Optional.empty(); System.out.println(name.orElse("default")); System.out.println(empty.orElse("default")); System.out.println(empty.orElseGet(() -> "computed-default")); System.out.println(empty.or(() -> Optional.of("fallback")).get()); } }
object Main { def main(args: Array[String]): Unit = { val name: Option[String] = Some("Alice") val empty: Option[String] = None println(name.getOrElse("default")) println(empty.getOrElse("default")) println(empty.getOrElse { "computed-" + "default" }) // lazy val fallback = empty.orElse(Some("fallback")) println(fallback.get) } }
getOrElse extracts the value or returns the default. orElse returns the Option itself or an alternative Option. Both accept lazy arguments via Scala's call-by-name semantics: empty.getOrElse { expensiveComputation() } runs only when the Option is None. Java's orElseGet uses a Supplier for the same purpose.
map and flatMap on Option
import java.util.Optional; class Main { static Optional<String> findCity(int userId) { return userId == 1 ? Optional.of("Paris") : Optional.empty(); } public static void main(String[] args) { Optional<String> upper = findCity(1).map(String::toUpperCase); System.out.println(upper.orElse("unknown")); Optional<String> result = Optional.of(1) .flatMap(id -> findCity(id)); System.out.println(result.orElse("not found")); } }
object Main { def findCity(userId: Int): Option[String] = if (userId == 1) Some("Paris") else None def main(args: Array[String]): Unit = { val upper = findCity(1).map(_.toUpperCase) println(upper.getOrElse("unknown")) val result = Some(1).flatMap(id => findCity(id)) println(result.getOrElse("not found")) val missing = Some(99).flatMap(id => findCity(id)) println(missing.getOrElse("not found")) } }
Option is a monad: map transforms the value inside a Some, leaving None unchanged. flatMap chains operations that themselves return Option, preventing nested Some(Some(...)). This is the foundation of for comprehensions — for { a <- optA; b <- optB } yield f(a, b) desugars into flatMap/map chains.
Pattern matching on Option
import java.util.Map; import java.util.Optional; class Main { public static void main(String[] args) { var config = Map.of("timeout", "30"); var timeout = Optional.ofNullable(config.get("timeout")); if (timeout.isPresent()) { System.out.println("Timeout: " + timeout.get() + "s"); } else { System.out.println("Using default timeout"); } } }
object Main { def main(args: Array[String]): Unit = { val config = Map("timeout" -> "30") val timeout: Option[String] = config.get("timeout") // Map.get returns Option timeout match { case Some(value) => println(s"Timeout: ${value}s") case None => println("Using default timeout") } config.get("host") match { case Some(host) => println(s"Host: $host") case None => println("No host configured") } } }
Scala's Map.get returns Option[V] — not the value directly. This forces you to handle the missing-key case. Pattern matching on Some/None is explicit; getOrElse is more concise for simple defaults. Java's Map.get returns null for missing keys — a common source of NullPointerException bugs.
Error Handling
try / catch / finally
class Main { public static void main(String[] args) { try { int result = Integer.parseInt("not-a-number"); System.out.println(result); } catch (NumberFormatException error) { System.out.println("Parse error: " + error.getMessage()); } finally { System.out.println("Always runs"); } } }
object Main { def main(args: Array[String]): Unit = { try { val result = "not-a-number".toInt println(result) } catch { case error: NumberFormatException => println(s"Parse error: ${error.getMessage}") } finally { println("Always runs") } } }
Scala's try/catch/finally uses match-style case syntax inside the catch block. This allows matching on specific exception types and adding guards. Unlike Java, Scala has no checked exceptions — all exceptions are unchecked. The compiler never forces you to declare or handle them, reducing boilerplate but requiring discipline.
Try[T] — Success and Failure as values
class Main { static int parseAndDouble(String input) { return Integer.parseInt(input) * 2; } public static void main(String[] args) { String[] inputs = {"42", "abc", "7"}; for (var input : inputs) { try { System.out.println(parseAndDouble(input)); } catch (NumberFormatException error) { System.out.println("Error: " + error.getMessage()); } } } }
import scala.util.{Try, Success, Failure} object Main { def parseAndDouble(input: String): Try[Int] = Try(input.toInt * 2) def main(args: Array[String]): Unit = { val inputs = List("42", "abc", "7") for (input <- inputs) { parseAndDouble(input) match { case Success(value) => println(value) case Failure(error) => println(s"Error: ${error.getMessage}") } } } }
Try[T] wraps a computation that may throw, returning Success[T] or Failure(exception). Unlike Java's try/catch (a statement), Try is a value you can pass around, map over, and chain. Try(expr) captures any non-fatal exception thrown by expr. This makes error handling composable — map over a Success and errors propagate through Failure.
Either[L, R] — typed error or success
class Main { sealed interface ValidationResult permits Valid, Invalid {} record Valid(String value) implements ValidationResult {} record Invalid(String error) implements ValidationResult {} static ValidationResult validateEmail(String email) { if (email.contains("@")) return new Valid(email); return new Invalid("Missing @ in: " + email); } public static void main(String[] args) { var result = validateEmail("alice@example.com"); switch (result) { case Valid(String email) -> System.out.println("Valid: " + email); case Invalid(String error) -> System.out.println("Error: " + error); } } }
object Main { def validateEmail(email: String): Either[String, String] = if (email.contains("@")) Right(email) else Left(s"Missing @ in: $email") def main(args: Array[String]): Unit = { validateEmail("alice@example.com") match { case Right(email) => println(s"Valid: $email") case Left(error) => println(s"Error: $error") } validateEmail("not-valid") match { case Right(email) => println(s"Valid: $email") case Left(error) => println(s"Error: $error") } } }
Either[L, R] represents a value that is either a Left (by convention: an error) or a Right (a success). Unlike Try, Either allows a typed error value — Left can be any type, not just Exception. Java has no built-in Either; developers use custom sealed types or third-party libraries.
Custom exceptions
class Main { static class ConfigException extends RuntimeException { private final String key; ConfigException(String key) { super("Missing required config: " + key); this.key = key; } String getKey() { return key; } } static String require(java.util.Map<String, String> config, String key) { var value = config.get(key); if (value == null) throw new ConfigException(key); return value; } public static void main(String[] args) { var config = java.util.Map.of("host", "localhost"); try { System.out.println(require(config, "host")); System.out.println(require(config, "port")); } catch (ConfigException error) { System.out.println("Config error: " + error.getMessage()); } } }
class ConfigException(val key: String) extends RuntimeException(s"Missing required config: $key") object Main { def require(config: Map[String, String], key: String): String = config.getOrElse(key, throw new ConfigException(key)) def main(args: Array[String]): Unit = { val config = Map("host" -> "localhost") try { println(require(config, "host")) println(require(config, "port")) } catch { case error: ConfigException => println(s"Config error: ${error.getMessage}") } } }
Scala throw is an expression with type Nothing — it can appear anywhere an expression is expected, including as the else branch of getOrElse. Custom exceptions extend Java exception classes directly. Scala has no checked exceptions — the compiler never forces you to declare or catch them, which reduces boilerplate but requires careful API design.
For Comprehensions
for with generators and guards
import java.util.List; import java.util.stream.Collectors; class Main { public static void main(String[] args) { var numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); var result = numbers.stream() .filter(n -> n % 2 == 0) .filter(n -> n > 4) .map(n -> n * n) .collect(Collectors.toList()); System.out.println(result); } }
object Main { def main(args: Array[String]): Unit = { val numbers = 1 to 10 val result = for { n <- numbers if n % 2 == 0 if n > 4 } yield n * n println(result.toList) } }
Scala's for { ... } yield ... is a for comprehension — syntactic sugar for chained flatMap, filter, and map calls. Each if guard becomes a .filter. This is far more readable than Java's Stream chain for multi-filter transformations. Without yield, the comprehension uses foreach and returns Unit.
for/yield — transforming collections
import java.util.stream.IntStream; import java.util.stream.Collectors; class Main { public static void main(String[] args) { var table = IntStream.rangeClosed(2, 4) .boxed() .flatMap(row -> IntStream.rangeClosed(1, 5) .mapToObj(col -> row + " x " + col + " = " + (row * col))) .collect(Collectors.toList()); table.forEach(System.out::println); } }
object Main { def main(args: Array[String]): Unit = { val table = for { row <- 2 to 4 col <- 1 to 5 } yield s"$row x $col = ${row * col}" table.foreach(println) } }
Multiple generators in a for comprehension produce a Cartesian product — this desugars to nested flatMap calls. Java's equivalent requires explicit flatMap with a nested stream, which is verbose and harder to read. For comprehensions scale cleanly to any number of generators and filters, remaining readable at any depth.
for comprehension over Option
import java.util.Map; import java.util.Optional; class Main { static Map<String, String> userDb = Map.of("alice", "alice@example.com"); static Map<String, String> domainDb = Map.of("example.com", "Example Corp"); public static void main(String[] args) { var result = Optional.ofNullable(userDb.get("alice")) .flatMap(email -> Optional.ofNullable( domainDb.get(email.split("@")[1]))) .map(company -> "alice works at " + company) .orElse("alice not found"); System.out.println(result); } }
object Main { val userDb = Map("alice" -> "alice@example.com") val domainDb = Map("example.com" -> "Example Corp") def main(args: Array[String]): Unit = { val result = for { email <- userDb.get("alice") // Option[String] domain = email.split("@")(1) // = assigns without unwrapping company <- domainDb.get(domain) // Option[String] } yield s"alice works at $company" println(result.getOrElse("alice not found")) } }
For comprehensions work over Option, List, Try, Either, and any monadic type. When a generator <- produces None, the entire comprehension short-circuits to None — no explicit flatMap chaining needed. The = domain line assigns a value without unwrapping (no <-). Java requires explicit .flatMap chaining with Optional.
for with value bindings and filters
import java.util.List; import java.util.stream.Collectors; class Main { public static void main(String[] args) { var words = List.of("hello", "world", "scala", "java", "functional"); var result = words.stream() .map(w -> w.toUpperCase()) .filter(w -> w.length() > 4) .sorted() .collect(Collectors.toList()); result.forEach(System.out::println); } }
object Main { def main(args: Array[String]): Unit = { val words = List("hello", "world", "scala", "java", "functional") val result = for { word <- words upper = word.toUpperCase // intermediate binding if upper.length > 4 } yield upper result.sorted.foreach(println) } }
Inside a for comprehension, = expression creates an intermediate binding without a generator — the value is computed once per outer iteration. This corresponds to a .map step in the desugared form. The comprehension then applies the guard (if) to filter based on the computed value. Unlike Java streams, the computation order is explicit in the for block.
Companion Objects
Companion object — static member equivalent
class Main { static class Counter { private int value; Counter(int initialValue) { this.value = initialValue; } void increment() { value++; } int getValue() { return value; } static Counter zero() { return new Counter(0); } static final int MAX = 1000; } public static void main(String[] args) { var counter = Counter.zero(); System.out.println(Counter.MAX); counter.increment(); System.out.println(counter.getValue()); } }
class Counter(private var value: Int) { def increment(): Unit = { value += 1 } def getValue: Int = value } object Counter { // same name as class — companion object def zero(): Counter = new Counter(0) val Max: Int = 1000 } object Main { def main(args: Array[String]): Unit = { val counter = Counter.zero() println(Counter.Max) counter.increment() println(counter.getValue) } }
In Scala there are no static members — everything that is "static" in Java becomes a member of the companion object (a singleton object with the same name as its class, in the same file). The companion object and the class can access each other's private members. Java static methods and fields translate directly to companion object methods and vals.
apply — smart constructor pattern
import java.util.Optional; class Main { static class Email { private final String address; private Email(String address) { this.address = address; } String getAddress() { return address; } static Optional<Email> of(String raw) { var trimmed = raw.trim().toLowerCase(); if (trimmed.contains("@")) return Optional.of(new Email(trimmed)); return Optional.empty(); } } public static void main(String[] args) { var email = Email.of("Alice@Example.com"); System.out.println(email.map(Email::getAddress).orElse("invalid")); } }
class Email private (val address: String) object Email { def apply(raw: String): Option[Email] = { val trimmed = raw.trim.toLowerCase if (trimmed.contains("@")) Some(new Email(trimmed)) else None } } object Main { def main(args: Array[String]): Unit = { val email = Email("Alice@Example.com") // calls Email.apply(...) println(email.map(_.address).getOrElse("invalid")) val bad = Email("not-valid") bad match { case Some(e) => println(e.address) case None => println("invalid email address") } } }
A companion object's apply is called when you write Email("...") without new. The constructor is private in the class, but the companion can call it — companions share access. This pattern lets the factory return an Option, encapsulating validation. Java static factories like Email.of(...) are the closest equivalent.
unapply — custom extractor for pattern matching
class Main { record Point(int x, int y) {} public static void main(String[] args) { var point = new Point(3, 4); // Java 21+: record patterns in instanceof/switch if (point instanceof Point(int x, int y)) { System.out.println("x=" + x + " y=" + y); } } }
class Point(val x: Int, val y: Int) { override def toString: String = s"Point($x, $y)" } object Point { def apply(x: Int, y: Int): Point = new Point(x, y) def unapply(p: Point): Option[(Int, Int)] = Some((p.x, p.y)) } object Main { def main(args: Array[String]): Unit = { val point = Point(3, 4) point match { case Point(x, y) => println(s"x=$x y=$y") } val Point(px, py) = Point(10, 20) // destructuring in val println(s"px=$px py=$py") } }
The unapply method on a companion object is the "extractor" — it enables a class to participate in pattern matching, just like case classes. unapply returns Option[(A, B)] for successful matching. Java 21+ enables similar destructuring via record patterns, but only for records. Scala's extractors work on any class and can implement complex matching logic.
object as singleton
class Main { // Java: singleton via enum or static holder enum AppConfig { INSTANCE; final String environment = "production"; final int maxConnections = 100; String describe() { return environment + " (max=" + maxConnections + ")"; } } public static void main(String[] args) { System.out.println(AppConfig.INSTANCE.environment); System.out.println(AppConfig.INSTANCE.describe()); System.out.println(AppConfig.INSTANCE == AppConfig.INSTANCE); // true } }
object AppConfig { val environment: String = "production" val maxConnections: Int = 100 def describe(): String = s"$environment (max=$maxConnections)" } object Main { def main(args: Array[String]): Unit = { println(AppConfig.environment) println(AppConfig.describe()) println(AppConfig eq AppConfig) // always the same instance } }
Every Scala object is a singleton — exactly one instance exists per JVM. No getInstance(), INSTANCE, or double-checked locking is needed. Java's singleton patterns (enum singleton, static holder idiom) exist to compensate for a language feature Scala provides natively. Scala object singletons are initialized lazily on first access and are thread-safe.