惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

G
Google Developers Blog
GbyAI
GbyAI
Y
Y Combinator Blog
The GitHub Blog
The GitHub Blog
B
Blog
博客园 - 叶小钗
V
Visual Studio Blog
小众软件
小众软件
阮一峰的网络日志
阮一峰的网络日志
博客园 - 聂微东
S
SegmentFault 最新的问题
Engineering at Meta
Engineering at Meta
博客园 - Franky
V
V2EX
人人都是产品经理
人人都是产品经理
H
Hackread – Cybersecurity News, Data Breaches, AI and More
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
月光博客
月光博客
IT之家
IT之家
T
The Blog of Author Tim Ferriss
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
C
Check Point Blog
N
Netflix TechBlog - Medium
博客园 - 【当耐特】

Neil Madden

Are we any closer to the Quantum Apocalypse? Java’s SSLContext protocol name is a footgun Mythos and its impact on security Maybe version ranges are a good idea after all? Why I don’t use LLMs for programming Looking for vulnerabilities is the last thing I do Were URLs a bad idea? Monotonic Collections: a middle ground between immutable and fully mutable Fluent Visitors: revisiting a classic design pattern Rating 26 years of Java changes No, no, no. You’re still not doing REST right! Streaming public key authenticated encryption with insider auth security Are we overthinking post-quantum cryptography? A look at CloudFlare’s AI-coded OAuth library The square roots of all evil Digital signatures and how to avoid them Machine Learning and the triumph of GOFAI Galois/Counter Mode and random nonces SipHash-based encryption for constrained devices Newsletter A controversial opinion about REST API design Regular JSON I still don’t really get “hash shucking” Entity authentication with a KEM Book review: The Joy of Cryptography A few programming language features I’d like to see On PBKDF2 iterations A few clarifications about CVE-2022-21449 CVE-2022-21449: Psychic Signatures in Java Is Datalog a good language for authorization?
Java sealed classes and exhaustive pattern matching
Neil Madden · 2026-04-24 · via Neil Madden

Java 17 introduced sealed classes, which allow you to explicitly list the allowed sub-types of an interface or base class. For example, here’s a toy example using a sealed interface and records (inner classes are implicitly added to the permitted sub-types if an explicit list is not given):

public sealed interface SealedType {
    record TypeA() implements SealedType {}
    record TypeB() implements SealedType {}

    static SealedType of(String type) {
        return switch (type) {
            case “A” -> new TypeA();
            case “B” -> new TypeB();
            default -> throw new IllegalArgumentException();
        };
    }
}

If you are familiar with functional programming languages with algebraic datatypes, you can view this as similar to a datatype declaration in Haskell or ML:

data SealedType = TypeA | TypeB

We can then use this in a simple Main class:

void main(String[] args) {
    var val = SealedType.of(args[0]);
    System.out.println(switch (val) {
        case SealedType.TypeA() -> “A”;
        case SealedType.TypeB() -> “B”;
    });
}

OK, not so exciting. But one thing to note here is that we didn’t have to add a default clause to the switch expression in our main method. This is because sealed classes (and enums) enable exhaustiveness checking: the compiler knows exactly what the possible cases are, and so can check if you have covered them all. If you have, then you don’t need a default clause. If you forget one (and don’t have a default clause), then you get a compile-time error.

This is great when you want to ensure that all uses of some type do cover all of the cases, but it does introduce a new type of breaking change: adding a new sub-type to a sealed class/interface may break consumers of that code. For example, adding a new TypeC case to our example will cause the main method to fail to compile due to the missing case. So if you export a sealed type in your API then adding a new subtype is a breaking change that would require a major version bump (if you’re following SemVer).

Compile-time or runtime error?

Although Java will produce a compile-time error for a non-exhaustive switch when you compile the consumer (main in this case), it cannot do so if the consumer is not recompiled when the sealed type changes. For example, suppose that we extend our SealedType with another case:

public sealed interface SealedType {
    record TypeA() implements SealedType {}
    record TypeB() implements SealedType {}
    record TypeC() implements SealedType {}

    static SealedType of(String type) {
        return switch (type) {
            case “A” -> new TypeA();
            case “B” -> new TypeB();
            case “C” -> new TypeC();
            default -> throw new IllegalArgumentException();
        };
    }
}

If we just recompiled SealedType.java and don’t recompile Main, then we end up with a runtime exception if we trigger the new case:

Here we have the new MatchException being thrown. The Javadoc notes this potential issue with separate compilation, and also some corner-cases with nulls in patterns. So even if you were hoping that using sealed classes would statically ensure that you update all consumers when a new case is added, this is not the case unless you recompile everything.

Conclusions

I think for me the conclusion is that sealed types are probably most useful within the implementation of a component, and are less useful when exposed in the public API that a component offers to other components (eg a library). For internal use, where you typically are going to recompile everything together, you get the nice properties of exhaustiveness checking and higher compile-time safety guarantees. But when used across module boundaries, you may just be introducing new ways to break code, often only detectable at runtime.

(I discovered these subtleties when reviewing the preview support for PEM-encoded cryptographic objects, which makes exactly this mistake of baking a sealed interface into a public API and recommend clients to pattern match against that type. A predict a very high chance of breakage if they ever want to add a new case).