Blog

Interfaces, herança e composição em Java

Interfaces dizem o que um tipo sabe fazer, a herança dá a uma classe tudo o que a classe pai faz, e a composição embrulha um objeto. Aprenda métodos default, super, classes abstratas e a armadilha da classe base frágil rodando programas pequenos.

O Java dá três jeitos de ligar tipos. Uma interface é um contrato que qualquer classe pode assinar. A herança, com extends, dá a uma classe tudo o que a classe pai tem. A composição guarda outro objeto num campo e chama esse objeto. Escolher o jeito errado é como uma subclasse acaba quebrada por um código que ela nunca viu.

Este post cobre interfaces, métodos de interface default, static e private, extends e super, @Override, protected e final, classes abstratas, o problema da classe base frágil, composição com repasse e decorators, e cast. Todo programa abaixo rodou no Java 25, e a saída foi colada da execução. Para rodar um você mesmo, salve como Main.java e rode java Main.java.

Interfaces são contratos

Uma interface lista os métodos que um tipo promete ter, sem dizer como eles funcionam. Qualquer classe ou record pode implementá-la, e um código que só conhece a interface consegue usar todos eles. Aqui estão três formas que não têm mais nada em comum:

interface Shape {
    double area();

    String name();
}

record Circle(double radius) implements Shape {
    @Override
    public double area() {
        return Math.PI * radius * radius;
    }

    @Override
    public String name() {
        return "circle";
    }
}

record Rectangle(double width, double height) implements Shape {
    @Override
    public double area() {
        return width * height;
    }

    @Override
    public String name() {
        return "rectangle";
    }
}

class Triangle implements Shape {
    private final double base;
    private final double height;

    Triangle(double base, double height) {
        this.base = base;
        this.height = height;
    }

    @Override
    public double area() {
        return base * height / 2;
    }

    @Override
    public String name() {
        return "triangle";
    }
}

double totalArea(List<Shape> shapes) {
    double total = 0;
    for (Shape s : shapes) {
        total += s.area();
    }
    return total;
}

void main() {
    List<Shape> shapes = List.of(new Circle(1), new Rectangle(2, 3), new Triangle(4, 5));
    for (Shape s : shapes) {
        IO.println(String.format("%-9s %6.2f", s.name(), s.area()));
    }
    IO.println(String.format("total     %6.2f", totalArea(shapes)));
}

Ele imprime:

circle      3.14
rectangle   6.00
triangle   10.00
total      19.14

totalArea nunca menciona círculos nem triângulos. Ele pede a área a cada Shape, e cada objeto responde com o próprio código. Isso é polimorfismo: uma chamada só, s.area(), e o objeto decide qual método roda. Adicione um Hexagon amanhã e totalArea funciona sem nenhuma mudança.

Os métodos de uma interface são public, escrevendo ou não. É por isso que toda implementação diz public double area(). Tire o public e o javac recusa com area() in Circle cannot implement area() in Shape.

Métodos default e static deixam uma interface crescer

Um método default é um método de interface com corpo. Toda classe que implementa a interface ganha esse método de graça, e qualquer uma delas pode sobrescrevê-lo. Depois que muitas classes implementam uma interface, adicionar um método abstrato quebra todas elas. Um método default não quebra.

interface Shape {
    double area();

    default String describe() {
        return String.format("a shape with area %.2f", area());
    }

    static Shape square(double side) {
        return new Rectangle(side, side);
    }
}

record Circle(double radius) implements Shape {
    @Override
    public double area() {
        return Math.PI * radius * radius;
    }
}

record Rectangle(double width, double height) implements Shape {
    @Override
    public double area() {
        return width * height;
    }

    @Override
    public String describe() {
        return "a " + width + " by " + height + " rectangle";
    }
}

void main() {
    List<Shape> shapes = List.of(new Circle(1), new Rectangle(2, 3), Shape.square(4));
    for (Shape s : shapes) {
        IO.println(s.describe());
    }
}

Ele imprime:

a shape with area 3.14
a 2.0 by 3.0 rectangle
a 4.0 by 4.0 rectangle

Circle nunca escreveu describe, então ficou com o default. O default chama area(), e essa chamada chega ao area do próprio círculo. Rectangle sobrescreveu describe com algo melhor. O JDK usou isso para adicionar stream() a Collection sem quebrar todas as classes de coleção.

square é um método static de interface. Você o chama na interface, Shape.square(4), e ele não é herdado: Rectangle.square(4) não compila. É um bom lugar para factory methods que fazem parte do contrato, como List.of.

Dois defaults com o mesmo nome não compilam

Uma classe pode implementar duas interfaces, e às vezes as duas têm um método default com a mesma assinatura. O Java não escolhe um por você:

interface Camera {
    default String describe() {
        return "takes photos";
    }
}

interface Phone {
    default String describe() {
        return "makes calls";
    }
}

class SmartPhone implements Camera, Phone {
}

void main() {
    IO.println(new SmartPhone().describe());
}

O build falha com:

Main.java:13: error: types Camera and Phone are incompatible;
class SmartPhone implements Camera, Phone {
^

A linha seguinte da saída do javac explica: class Main.SmartPhone inherits unrelated defaults for describe() from types Camera and Phone. O Main. aparece porque um arquivo-fonte compacto aninha suas classes dentro de uma classe Main escondida, como a parte sobre classes explica.

Isso se chama problema do diamante, e o Java obriga você a resolver na mão. Sobrescreva o método e chame os defaults que quiser com Interface.super.method():

interface Camera {
    default String describe() {
        return "takes photos";
    }
}

interface Phone {
    default String describe() {
        return "makes calls";
    }
}

class SmartPhone implements Camera, Phone {
    @Override
    public String describe() {
        return Phone.super.describe() + " and " + Camera.super.describe();
    }
}

void main() {
    IO.println(new SmartPhone().describe());
}

Ele imprime:

makes calls and takes photos

Phone.super.describe() quer dizer “rode o default que Phone oferece, neste objeto”. Você pode chamar um, os dois ou nenhum, mas precisa escolher.

Métodos privados compartilham código entre defaults

Quando dois métodos default precisam do mesmo helper, o helper pode ser um método private da interface. Ele não faz parte do contrato, então quem implementa a interface não consegue ver nem chamar esse método:

interface Greeter {
    String name();

    default String hello() {
        return wrap("Hello, " + name());
    }

    default String goodbye() {
        return wrap("Goodbye, " + name());
    }

    private String wrap(String text) {
        return "[" + text + "]";
    }
}

record English(String name) implements Greeter {}

void main() {
    var g = new English("Ana");
    IO.println(g.hello());
    IO.println(g.goodbye());
}

Ele imprime:

[Hello, Ana]
[Goodbye, Ana]

Métodos privados em interfaces chegaram no Java 9. Conferimos: a mesma interface, compilada com javac --release 8, falha com private interface methods are not supported in -source 8. English não precisa de corpo: o accessor name() do record cumpre o método da interface.

Interfaces com um só método podem ser lambdas

Uma interface com exatamente um método abstrato se chama interface funcional, e o Java deixa você escrever uma implementação dela como lambda, em vez de uma classe. Runnable, Comparator e Function são todas interfaces desse tipo, e métodos default não contam para esse “um”. A anotação @FunctionalInterface pede ao compilador que confira se a interface continua assim. A parte sobre lambdas e streams mostra como escrever e usar lambdas.

Herança com extends

Uma classe que faz extends de outra herda os campos e os métodos dela, e pode sobrescrever os métodos para mudar o que eles fazem. A classe estendida é a superclasse, ou classe pai. Uma classe só pode estender uma classe. Aqui está uma pequena família de animais, com três níveis:

class Animal {
    protected final String name;

    Animal(String name) {
        this.name = name;
    }

    String sound() {
        return "...";
    }

    String describe() {
        return name + " says " + sound();
    }
}

class Dog extends Animal {
    Dog(String name) {
        super(name);
    }

    @Override
    String sound() {
        return "Woof";
    }
}

class Puppy extends Dog {
    Puppy(String name) {
        super(name);
    }

    @Override
    String sound() {
        return super.sound().toLowerCase() + "?";
    }

    @Override
    String describe() {
        return super.describe() + " (a puppy called " + name + ")";
    }
}

void main() {
    List<Animal> animals = List.of(new Animal("Rock"), new Dog("Rex"), new Puppy("Bit"));
    for (Animal a : animals) {
        IO.println(a.describe());
    }
}

Ele imprime:

Rock says ...
Rex says Woof
Bit says woof? (a puppy called Bit)

Muita coisa aconteceu num programa curto:

  • super(name) chama o construtor da classe pai. Animal não tem construtor sem argumentos, então Dog precisa chamar este. Todo construtor roda o construtor da classe pai antes de os próprios campos ficarem prontos.
  • super.sound() chama a versão da classe pai de um método, mesmo que esta classe o sobrescreva. Puppy pegou o "Woof" do cachorro e mudou.
  • protected em name deixa as subclasses lerem o campo, e foi assim que Puppy.describe usou name. Ele também abre o campo para toda classe do mesmo pacote, e todas as classes de um arquivo compartilham um pacote, então um programa de um arquivo só não consegue mostrar protected barrando alguém.
  • Dynamic dispatch (despacho dinâmico) é o item que mais importa. describe() é escrito uma vez, em Animal, e chama sound(). Quando describe() roda num Dog, essa chamada a sound() vai para Dog.sound(). O Java escolhe o método pela classe real do objeto em tempo de execução, não pelo tipo da variável nem pela classe onde a chamada foi escrita.

Essa regra é o que torna a herança poderosa, e também o que a torna perigosa.

@Override pega o erro de digitação

@Override diz ao compilador que um método deve substituir um método da classe pai. Ele é opcional, e deixá-lo de fora deixa passar um erro de digitação. Aqui Dog escreve sound errado:

class Animal {
    String sound() {
        return "...";
    }
}

class Dog extends Animal {
    String sonud() {
        return "Woof";
    }
}

void main() {
    Animal rex = new Dog();
    IO.println(rex.sound());
}

Ele imprime:

...

Isso compilou, até com javac -Xlint:all -Werror. sonud é um método novinho que ninguém chama, e o cachorro faz o som da classe pai sem avisar. Coloque @Override nele:

class Animal {
    String sound() {
        return "...";
    }
}

class Dog extends Animal {
    @Override
    String sonud() {
        return "Woof";
    }
}

void main() {
    Animal rex = new Dog();
    IO.println(rex.sound());
}

O build falha com:

Main.java:8: error: method does not override or implement a method from a supertype
    @Override
    ^

Ele também pega um tipo de parâmetro errado, que cria uma sobrecarga em vez de uma sobrescrita.

final impede a sobrescrita

Um método final não pode ser sobrescrito, e uma classe final não pode ser estendida. Use final num método quando as regras da classe pai precisam valer para toda subclasse:

class Account {
    private int balance;

    final void deposit(int amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("deposit must be positive");
        }
        balance += amount;
    }
}

class SneakyAccount extends Account {
    @Override
    void deposit(int amount) {
        IO.println("no checks here");
    }
}

void main() {
    new SneakyAccount().deposit(-50);
}

O build falha com:

Main.java:14: error: deposit(int) in Main.SneakyAccount cannot override deposit(int) in Main.Account
    void deposit(int amount) {
         ^

O javac acrescenta overridden method is final na linha seguinte. Numa classe inteira, final fecha a porta de vez. String é final, então class LoudString extends String falha com cannot inherit from final String. Records também são final. A parte sobre sealed types cobre o meio-termo, em que uma classe lista exatamente quais subclasses aceita.

Um construtor que chama um método sobrescrevível

O construtor da classe pai roda antes do corpo do construtor da subclasse, e o dynamic dispatch continua funcionando dentro dele. Junte esses dois fatos e a classe pai pode chamar um método da subclasse antes de a subclasse definir os próprios campos:

class Widget {
    Widget() {
        IO.println("Widget constructor calls render()");
        render();
    }

    void render() {
        IO.println("plain widget");
    }
}

class Label extends Widget {
    private final String text;

    Label(String text) {
        this.text = text;
    }

    @Override
    void render() {
        IO.println("label: " + text.toUpperCase());
    }
}

void main() {
    new Label("hello");
}

Ele imprime e para:

Widget constructor calls render()
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.toUpperCase()" because "this.text" is null

text é final e mesmo assim ainda está null aqui. O primeiro passo do construtor de Label foi um super() invisível, que rodou Widget(), que chamou render(), que chegou a Label.render(). this.text = text ainda não tinha rodado. Um campo final é atribuído uma vez, mas dá para lê-lo antes disso.

O Java 25 traz uma solução. Os flexible constructor bodies (corpos de construtor flexíveis) deixam um construtor atribuir os próprios campos antes de chamar super(), como mostra a parte sobre classes. Escreva this.text = text; e depois super(); no construtor de Label, e o mesmo programa imprime label: HELLO. A regra antiga é mais simples: não chame métodos sobrescrevíveis de dentro de um construtor.

Classes abstratas vs interfaces

Uma classe abstrata é uma classe que você não pode criar diretamente. Ela pode ter campos, construtores e métodos prontos, além de métodos abstract que cada subclasse precisa escrever. new Account() na classe abaixo falha com Main.Account is abstract; cannot be instantiated.

abstract class Account {
    private int balance;
    private final List<String> history = new ArrayList<>();

    abstract int fee(int amount);

    final void withdraw(int amount) {
        int total = amount + fee(amount);
        balance -= total;
        history.add("-" + total);
    }

    final void deposit(int amount) {
        balance += amount;
        history.add("+" + amount);
    }

    String statement() {
        return getClass().getSimpleName() + " " + history + " balance " + balance;
    }
}

class Checking extends Account {
    @Override
    int fee(int amount) {
        return 1;
    }
}

class Savings extends Account {
    @Override
    int fee(int amount) {
        return amount / 10;
    }
}

void main() {
    List<Account> accounts = List.of(new Checking(), new Savings());
    for (Account a : accounts) {
        a.deposit(100);
        a.withdraw(50);
        IO.println(a.statement());
    }
}

Ele imprime:

Checking [+100, -51] balance 49
Savings [+100, -55] balance 45

Account é dona do saldo, do histórico e das regras para mudá-los. Os métodos final fixam os passos, e o único método abstract, fee, é a única lacuna que cada subclasse preenche. Esse formato se chama padrão template method.

Uma interface não conseguiria fazer isso, e a diferença se resume a três coisas:

  • Estado. Uma classe abstrata pode ter campos de instância e construtor. Uma interface não pode. Ela só pode ter constantes, e seus métodos default precisam trabalhar por meio de outros métodos.
  • Quantidade. Uma classe estende uma classe só, mas implementa quantas interfaces quiser. Um record ou um enum não pode estender classe nenhuma, mas pode implementar interfaces.
  • Acoplamento. Estender uma classe prende você aos campos e à implementação dela. Implementar uma interface prende você só às assinaturas dos métodos.

Comece com uma interface, porque qualquer tipo pode assiná-la. Adicione uma classe abstrata quando as implementações compartilham estado e passos de verdade, e mantenha a interface como o tipo que as pessoas usam. O JDK faz isso com List e AbstractList.

O problema da classe base frágil

Uma subclasse depende de como a classe pai foi escrita por dentro, não só do que a classe pai promete. Quando esses detalhes mudam, ou nunca foram o que você imaginava, a subclasse quebra. Este é o exemplo clássico, do Effective Java de Joshua Bloch: um HashSet que conta quantos elementos já foram adicionados.

class InstrumentedHashSet<E> extends HashSet<E> {
    private static final long serialVersionUID = 1L;

    private int addCount = 0;

    @Override
    public boolean add(E e) {
        addCount++;
        return super.add(e);
    }

    @Override
    public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c);
    }

    int addCount() {
        return addCount;
    }
}

void main() throws Exception {
    var set = new InstrumentedHashSet<String>();
    set.addAll(List.of("Snap", "Crackle", "Pop"));
    IO.println("size:      " + set.size());
    IO.println("addCount:  " + set.addCount());

    var addAll = HashSet.class.getMethod("addAll", Collection.class);
    IO.println("addAll is written in " + addAll.getDeclaringClass().getName());
}

Ele imprime:

size:      3
addCount:  6
addAll is written in java.util.AbstractCollection

Entraram três elementos, e a contagem diz seis. Rodamos no Java 25 para ver se o JDK ainda se comporta assim, e ele se comporta. A última linha mostra o motivo: HashSet não escreve o próprio addAll. Ele herda um de AbstractCollection, e esse percorre a coleção e chama add para cada elemento. getMethod pode lançar uma exceção checada, e é por isso que main diz throws Exception.

Esta é a cadeia de chamadas:

set.addAll(List.of(3 itens)) InstrumentedHashSet.addAll: addCount += 3 super.addAll(c) AbstractCollection.addAll: add(e) a cada e add(e) acha o override InstrumentedHashSet.add: addCount++ (x3) super.add(e) HashSet.add: guarda o elemento addCount 3 3 6, conta dobrada 6, mas size é 3

Uma chamada a addAll conta os três elementos e passa a vez ao AbstractCollection.addAll herdado. Esse método chama add para cada elemento, e o dynamic dispatch manda cada chamada de volta ao add da subclasse, que conta tudo de novo.

As duas sobrescritas estão corretas sozinhas. O bug vem de um fato que não está no contrato de HashSet: quais dos próprios métodos ele chama por dentro. Isso se chama self-use (autouso), e não dá para ver de fora.

A correção óbvia é apagar a sobrescrita de addAll, e ela funciona hoje. Mas agora sua contagem só está certa porque AbstractCollection.addAll por acaso chama add. Se um JDK futuro desse ao HashSet um addAll mais rápido que guarda os elementos direto, sua contagem cairia para zero em adições em lote, sem aviso nenhum. De um jeito ou de outro, sua classe depende de um código que não é seu e que você não vê.

Explicado como se você tivesse dez anos

Herança é ganhar a casa inteira dos seus pais. Você ganha os cômodos, os móveis e o jardim, e se muda no primeiro dia sem construir nada. Também ganha a goteira que você não sabia que existia, e o interruptor do corredor que, em segredo, também liga a mangueira do jardim. Você pode pintar qualquer cômodo de novo, mas a fiação atrás das paredes continua sendo deles.

Composição é chamar um encanador quando você precisa. Você mora na sua própria casa. Quando um cano precisa de conserto, você chama o encanador e diz qual cano é. Ele pode mudar o jeito de trabalhar, comprar ferramentas novas ou se aposentar, e aí você só chama outro encanador. Nada do que ele fizer pode alagar um cômodo onde você não o deixou entrar.

A versão precisa

Uma subclasse herda a implementação da classe pai, não só a interface dela. Todo método que ela sobrescreve pode ser chamado pelo próprio código da classe pai, porque o Java faz o dispatch pela classe do objeto em tempo de execução. Então a correção da subclasse depende do self-use da classe pai: quais métodos chamam quais outros métodos, e em que ordem. Esse detalhe costuma não estar documentado, e o autor da classe pai é livre para mudá-lo na próxima versão.

A composição não tem esse problema. Um wrapper guarda o outro objeto num campo privado e chama os métodos públicos dele. As chamadas internas do objeto embrulhado vão para os próprios métodos dele, nunca de volta para o wrapper, porque o wrapper não é uma subclasse. O wrapper depende só do contrato.

Onde a analogia falha: uma casa não pode ser construída pensando no próximo dono, mas uma classe pode. Uma classe pai projetada para herança documenta o próprio self-use, como AbstractList faz, e marca o resto como final. Estender uma classe assim é seguro.

Composição: repasse em vez de estender

Com composição, sua classe tem um set em vez de ser um. Ela guarda um Set num campo e repassa cada chamada para ele. Aqui está o contador de novo, escrito desse jeito:

class InstrumentedSet<E> {
    private final Set<E> inner;
    private int addCount = 0;

    InstrumentedSet(Set<E> inner) {
        this.inner = inner;
    }

    boolean add(E e) {
        addCount++;
        return inner.add(e);
    }

    boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return inner.addAll(c);
    }

    int size() {
        return inner.size();
    }

    int addCount() {
        return addCount;
    }

    @Override
    public String toString() {
        return inner.toString();
    }
}

void main() {
    var fromHash = new InstrumentedSet<String>(new HashSet<>());
    fromHash.addAll(List.of("Snap", "Crackle", "Pop"));
    IO.println("HashSet: size " + fromHash.size() + ", addCount " + fromHash.addCount());

    var fromTree = new InstrumentedSet<String>(new TreeSet<>());
    fromTree.addAll(List.of("Snap", "Crackle", "Pop"));
    fromTree.add("Snap");
    IO.println("TreeSet: size " + fromTree.size() + ", addCount " + fromTree.addCount());
    IO.println(fromTree);
}

Ele imprime:

HashSet: size 3, addCount 3
TreeSet: size 3, addCount 4
[Crackle, Pop, Snap]

A contagem está certa. inner.addAll(c) ainda chama add por dentro, mas é o add do próprio HashSet, e nosso wrapper nunca fica sabendo. Adicionar "Snap" pela segunda vez conta como uma tentativa de adição, que é o que a classe promete, enquanto o tamanho continua 3.

Tem um bônus. O wrapper aceita qualquer Set, então a mesma classe conta adições a um TreeSet, que mantém os elementos ordenados. A versão com subclasse só podia ser um HashSet.

O custo é o código de repasse. Para passar esse wrapper onde se espera um Set, ele teria que implementar Set<E> e repassar todos os métodos. O Effective Java escreve esses métodos uma vez só, num ForwardingSet reutilizável.

“É um” ou “tem um”

A herança diz que um Dog é um Animal: em qualquer lugar onde você precisa de um animal, um cachorro tem que servir. A composição diz que uma classe tem uma coisa que ela usa. Antes de escrever extends, pergunte se todo método da classe pai faz sentido na subclasse. O próprio JDK já errou nisso. Stack estende Vector, então uma pilha é uma lista, e herda métodos de lista que quebram a ideia de pilha:

void main() {
    var stack = new Stack<String>();
    stack.push("first");
    stack.push("second");
    stack.push("third");

    stack.add(0, "sneaked in at the bottom");
    stack.remove(2);

    IO.println(stack);
    IO.println("pop: " + stack.pop());
}

Ele imprime:

[sneaked in at the bottom, first, third]
pop: third

Uma pilha só deveria deixar você empilhar e desempilhar no topo. Esta deixou a gente inserir no fundo e apagar do meio, porque Stack não consegue tirar os métodos de Vector. Uma pilha tem uma lista por dentro. Ela não é uma lista. É por isso que a própria documentação de Stack manda usar um Deque, como ArrayDeque, no lugar.

Um decorator empilha wrappers

Um decorator é um wrapper que implementa a mesma interface do objeto que ele embrulha. Como o wrapper também é desse tipo, você pode embrulhar um wrapper e montar comportamento em camadas. Com records, cada camada vira um tipo curto:

interface Formatter {
    String format(String text);
}

record Plain() implements Formatter {
    @Override
    public String format(String text) {
        return text;
    }
}

record Shout(Formatter inner) implements Formatter {
    @Override
    public String format(String text) {
        return inner.format(text).toUpperCase();
    }
}

record Exclaim(Formatter inner) implements Formatter {
    @Override
    public String format(String text) {
        return inner.format(text) + "!";
    }
}

record Timestamp(Formatter inner, String time) implements Formatter {
    @Override
    public String format(String text) {
        return "[" + time + "] " + inner.format(text);
    }
}

void main() {
    Formatter f = new Timestamp(new Exclaim(new Shout(new Plain())), "09:30");
    IO.println(f.format("server started"));

    Formatter g = new Shout(new Timestamp(new Plain(), "9am"));
    IO.println(g.format("server started"));
}

Ele imprime:

[09:30] SERVER STARTED!
[9AM] SERVER STARTED

Cada camada chama a camada de dentro e depois aplica a própria mudança. Em g, Shout fica do lado de fora, então também deixou o timestamp em maiúsculas, e 9am virou 9AM. A ordem importa, e você escolhe a ordem quando monta o objeto.

Com herança, você precisaria de uma subclasse para cada combinação. Aqui, quatro tipos pequenos cobrem todas. As classes de I/O do JDK funcionam assim: um BufferedReader embrulha um InputStreamReader, que embrulha um InputStream.

instanceof e cast

instanceof confere se um objeto é de um certo tipo, e um cast diz ao compilador para tratá-lo como esse tipo. O compilador confia no cast, então a verificação acontece em tempo de execução. Quando o objeto não é desse tipo, o cast lança uma exceção:

void main() {
    List<Object> values = List.of("8080", 8080);
    for (Object v : values) {
        if (v instanceof String) {
            String s = (String) v;
            IO.println("a string of length " + s.length());
        } else {
            IO.println("not a string: " + v);
        }
    }
    String port = (String) values.get(1);
    IO.println("port is " + port);
}

Ele imprime e para:

a string of length 4
not a string: 8080
Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')

O loop era seguro porque o cast dele ficava atrás de uma verificação com instanceof. O último cast não tinha verificação, e o valor é um Integer. Código de verdade esbarra nisso quando os valores saem de um Map<String, Object>.

Quando você faz cast das suas próprias classes num arquivo-fonte compacto, a mensagem cita o class loader e termina com um @ e um número hexadecimal que pode mudar entre execuções. É por isso que este exemplo usa tipos do JDK.

Uma cadeia longa de verificações com instanceof costuma indicar que o método deveria estar na interface, como area() estava. Quando uma verificação de tipo é a ferramenta certa, a parte sobre sealed types mostra v instanceof String s, que confere e faz o cast num passo só.

O que lembrar

  • Uma interface é um contrato. Qualquer classe ou record pode implementar várias, e um código escrito para a interface funciona com todas elas.
  • Métodos default deixam uma interface ganhar comportamento sem quebrar quem a implementa. Dois defaults em conflito não compilam; sobrescreva o método e escolha com Camera.super.describe().
  • extends herda os campos e os métodos de uma única classe pai. O Java faz o dispatch pela classe real do objeto, então o código da classe pai pode chamar uma sobrescrita da subclasse, até de dentro de um construtor.
  • Coloque @Override em toda sobrescrita, para que um erro de digitação quebre o build. Use final para impedir a sobrescrita e super para chamar a classe pai.
  • Use uma classe abstrata quando as implementações compartilham estado e passos de verdade. Fora isso, prefira uma interface.
  • Estender uma classe que você não controla prende você ao self-use escondido dela. InstrumentedHashSet conta 6 para 3 elementos no Java 25, porque o addAll herdado chama add.
  • A composição embrulha um objeto e repassa as chamadas para ele, então o funcionamento interno do objeto embrulhado não consegue chamar de volta o seu código. Decorators empilham esses wrappers.

Estenda uma classe só quando a subclasse realmente é a classe pai e a classe pai foi feita para ser estendida. Fora isso, embrulhe.

Quanto este post te ajudou?

Clique em um coração para avaliar!

Média das avaliações 0 / 5. Total de votos: 0

Nenhum voto até agora. Seja o primeiro a avaliar este post.