Introduction
Java and Scala are both modern, powerful languages that offer a range of features for developers, and while they differ in their paradigms (Java being primarily object-oriented and Scala being a hybrid of object-oriented and functional programming), they do share some similarities, especially as both languages have evolved. As an experienced Java developer (from Java 7 to the latest) and also experienced Scala developer I will try to compare the two languages.
1. Immutability
Immutability in Java refers to the design of objects whose state cannot be modified after they are created. Once an immutable object is instantiated, its fields cannot be changed, which provides several advantages, including thread safety, simplicity, and easier debugging. The most well-known immutable class in Java is String, but developers can create their own immutable classes.
Java 17
public record Person(String name, int age) {}
Records automatically generate:
- constructor,
- getters,
- equals(),
- hashCode(),
- toString().
However, records provide shallow immutability.
record Team(List<String> members) {}
The reference cannot change, but the list can.
Scala 2.13
case class Person(name: String, age: Int)
Case classes are immutable by convention.
val p = Person("John", 30)
Fields are val unless explicitly declared var.
Scala 3
Same concept, with additional compiler improvements.
2. Sealed Types
Sealed types restrict who may extend a hierarchy.
Java 17
sealed interface Shape
permits Circle, Rectangle {}
Useful for exhaustive pattern matching.
Scala 2.13
sealed trait Shape case class Circle(r: Double) extends Shape case class Rectangle(w: Double,h: Double) extends Shape
All subclasses must be in the same source file.
Scala 3
Exactly the same idea.
Scala 3 enums automatically create sealed hierarchies.
3. Pattern Matching
Pattern matching allows branching based on structure, not just type.
Java 17
if (obj instanceof String s) {
System.out.println(s.length());
}
Switch pattern matching was still a preview feature.
Scala 2.13
shape match {
case Circle(r) => ...
case Rectangle(w,h) => ...
}
Can match:
- values
- types
- tuples
- lists
- nested objects
- regex
- custom extractors
Scala’s pattern matching is far more expressive.
4. Type Inference
Type inference lets the compiler determine types automatically.
Java
var list = List.of(1,2,3);
Only local variables.
Scala
val list = List(1,2,3)
Inference works for
- methods
- lambdas
- generic types
- pattern matching
- collections
Scala 3 further improves inference.
5. Lambdas
Lambdas represent anonymous functions.
Java
list.stream()
.map(x -> x * 2)
Requires functional interfaces.
Scala
list.map(_ * 2)
Functions are true objects.
6. Collections
Collections are central to functional programming.
Java
Uses
- List
- Set
- Map
- Stream
Streams are separate from collections.
Scala
Everything supports
map flatMap filter collect groupBy partition fold reduce scan
without converting to streams.
7. Functional Programming
Functional programming emphasizes pure functions, immutable data, and function composition.
Java supports FP, but Scala is designed around it.
Scala libraries:
- Cats
- Cats Effect
- ZIO
- FS2
make purely functional programming practical.
8. Optional vs Option
Both represent optional values.
Java
Optional<String>
Designed mainly for return types.
Scala
Option[String]
Behaves like a collection.
option.map(...) option.flatMap(...) option.filter(...)
9. Null Safety
Null references are a common source of runtime errors.
Java
String s = null;
Perfectly legal.
Scala 2
Prefer
Option[String]
instead of null.
Scala 3
With Explicit Nulls
String
and
String | Null
are different types.
Much safer.
10. Exceptions
Exception handling reports errors during execution.
Java
- checked exceptions
- unchecked exceptions
Compiler forces checked exceptions.
Scala
Only unchecked exceptions.
Functional libraries usually avoid exceptions entirely by using:
TryEitherIOZIO
11. Algebraic Data Types (ADT)
ADTs model complex domains by combining product types (AND) and sum types (OR).
Java
sealed interface Expr {}
record Num(int value) implements Expr {}
record Add(Expr l, Expr r) implements Expr {}
Scala
sealed trait Expr case class Num(v:Int) extends Expr case class Add(l:Expr,r:Expr) extends Expr
Scala 3
enum Expr: case Num(v:Int) case Add(l:Expr,r:Expr)
Scala offers much more concise ADT support.
12. Higher-Order Functions
Higher-order functions either accept functions as parameters or return functions.
Java:
list.forEach(System.out::println);
Scala:
list.foreach(println)
Higher-order functions are fundamental in Scala.
13. Implicits
Implicits allow values or conversions to be supplied automatically by the compiler.
Scala 2:
implicit val ec = ExecutionContext.global
Scala 3 replaces this with clearer syntax:
given ExecutionContext = ExecutionContext.global def run(using ExecutionContext)
Java has no comparable language feature.
14. Extension Methods
Extension methods add behavior to existing types without inheritance.
Java uses utility classes:
Collections.sort(list);
Scala 2
implicit class RichString(s: String)
Scala 3
extension (s: String) def hello = ...
This is now a first-class language feature.
15. Value Classes
Value classes wrap existing types without (in many cases) introducing runtime allocation.
Scala 2:
class UserId(val value: Long) extends AnyVal
Scala 3 favors opaque types:
opaque type UserId = Long
These provide type safety with effectively zero runtime overhead.
Java has no direct equivalent in Java 17 (Project Valhalla aims to introduce value objects in a future release).
16. Type System
A language’s type system determines how expressive and safe programs can be.
Java offers:
- generics
- wildcards
- bounded types
- sealed types
Scala additionally supports:
- higher-kinded types
- variance annotations
- path-dependent types
- abstract type members
- type classes
- dependent function types (Scala 3)
- union types (Scala 3)
- intersection types (Scala 3)
- match types (Scala 3)
- opaque types (Scala 3)
Scala’s type system is among the most expressive of mainstream programming languages.
17. Compilation Speed
Compilation speed affects developer productivity, especially in large codebases.
- Java 17: Fastest compilation due to a simpler type system and mature incremental compilation.
- Scala 2.13: Noticeably slower because the compiler performs advanced type inference and implicit resolution.
- Scala 3: Improves compiler architecture and is often faster than Scala 2.13, though still generally slower than Java for comparable projects.
18. Learning Curve
The learning curve reflects how quickly developers become productive.
- Java 17: Gentle learning curve. A developer can become productive quickly thanks to a relatively small language surface and consistent syntax.
- Scala 2.13: Steep learning curve. Besides object-oriented programming, developers must understand functional programming concepts, advanced collections, implicits, and a sophisticated type system.
- Scala 3: Still challenging, but easier than Scala 2.13. The redesigned syntax,
given/usingmechanism, extension methods, enums, and improved error messages reduce much of the accidental complexity while preserving Scala’s expressive power.
Overall comparison
- Choose Java 17 if you value simplicity, fast compilation, broad industry adoption, and ease of maintenance.
- Choose Scala 2.13 if you work on existing Scala ecosystems (e.g., Spark, Akka Classic, Play Framework) and need maximum compatibility.
- Choose Scala 3 if you want the full expressive power of Scala with a cleaner language design, stronger type safety, modern features such as enums, opaque types, union types, and native extension methods, and are starting a new Scala project.
