Keal will feel close: static types, classes, generics, and a compiler that refuses what it cannot prove. The differences that matter are that nullability lives in the type, that there is no inheritance at all, and that a value's memory is counted rather than collected.
| Concept | Java | Keal |
|---|---|---|
| Class with fields | class P { final int x; P(int x){this.x=x;} } | record P(val x: Int) |
| Mutable holder | class C { int n; } | class C(var n: Int) |
| Nullable | @Nullable String s; | val s: String? |
| Null check | if (s != null) s.length() | if (s != null) { s.length } |
| Switch | switch (n) { case 0: … } | when (n) { 0 -> … } |
| Instance test | if (o instanceof Circle c) | when (o) { is Circle(r) -> … } |
| Generic method | <T> T first(List<T> xs) | func first<T>(xs: List<T>): T |
| Interface | interface Ord { int compareTo(…); } | trait Ord { func compareTo(other: Self): Int } |
| Lambda | xs.stream().map(x -> x * 2) | xs.map({ it * 2 }) |
| String format | "hello " + name | "hello ${name}" |
| Destructor | AutoCloseable / try-with-resources | proc deinit() { … } |
There is no extends. Traits with default methods and composition cover the same ground without the diamond, and records cover the data classes. If your design leans on a base class, it becomes a trait plus a field.
String can never be null; String? can. The checker narrows an immutable binding after a test, so the cast Java makes you write is inferred.
No collector, no pauses. keal layout prints every object's size and padding, deinit runs deterministically when the last reference dies, and a cycle is the one thing counting cannot free — write weak on the back edge.
import java.time.LocalDate generates typed wrappers through JNI. The gateway is written in Keal; see the interop document.