Java 17 LTS through the eyes of Scala developer

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:

  • Try
  • Either
  • IO
  • ZIO

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/using mechanism, 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.