Java8: Por que é proibido definir um método padrão para um método a partir de java.lang.Object

130

Os métodos padrão são uma nova ferramenta interessante em nossa caixa de ferramentas Java. No entanto, tentei escrever uma interface que define uma defaultversão do toStringmétodo. Java me diz que isso é proibido, uma vez que os métodos declarados em java.lang.Objectpodem não ser defaulteditados. Por que esse é o caso?

Eu sei que existe a regra "classe base sempre vence", portanto, por padrão (pun;), qualquer defaultimplementação de um Objectmétodo seria substituída pelo método de Objectqualquer maneira. No entanto, não vejo razão para que não exista uma exceção para métodos Objectna especificação. Especialmente porque toStringpode ser muito útil ter uma implementação padrão.

Então, qual é a razão pela qual os designers de Java decidiram não permitir que os defaultmétodos substituíssem os métodos Object?

gexicida
fonte
1
Eu me sinto tão bem comigo mesmo agora, votando nisso por 100 vezes e, portanto, um distintivo de ouro. boa pergunta!
Eugene

Respostas:

186

Esse é mais um daqueles problemas de design de linguagem que parece "obviamente uma boa idéia" até você começar a cavar e perceber que é realmente uma péssima idéia.

Esse e-mail tem muito a ver com o assunto (e com outros assuntos também.) Houve várias forças de design que convergiram para nos levar ao design atual:

  • O desejo de manter o modelo de herança simples;
  • O fato de que, depois de examinar os exemplos óbvios (por exemplo, transformar AbstractList- se em uma interface), você percebe que a herança de iguais / hashCode / toString está fortemente ligada à herança e ao estado únicos, e as interfaces são herdadas e sem estado multiplicadas;
  • Que potencialmente abriu a porta para alguns comportamentos surpreendentes.

Você já tocou no objetivo de "manter as coisas simples"; as regras de herança e resolução de conflitos são projetadas para serem muito simples (classes ganham interfaces, interfaces derivadas ganham superinterfaces e quaisquer outros conflitos são resolvidos pela classe de implementação.) É claro que essas regras podem ser aprimoradas para fazer uma exceção, mas Acho que você descobrirá quando começar a puxar essa corda que a complexidade incremental não é tão pequena quanto você imagina.

Obviamente, há algum grau de benefício que justificaria mais complexidade, mas, neste caso, não existe. Os métodos que estamos falando aqui são iguais, hashCode e toString. Todos esses métodos são intrinsecamente sobre o estado do objeto, e é a classe que possui o estado, não a interface, que está na melhor posição para determinar o que significa igualdade para essa classe (especialmente porque o contrato de igualdade é bastante forte; consulte Eficaz Java por algumas conseqüências surpreendentes); os gravadores de interface são removidos demais.

É fácil retirar o AbstractListexemplo; seria ótimo se pudéssemos nos livrar AbstractListe colocar o comportamento na Listinterface. Mas uma vez que você se move além desse exemplo óbvio, não há muitos outros bons exemplos a serem encontrados. Na raiz, AbstractListé projetado para herança única. Mas as interfaces devem ser projetadas para herança múltipla.

Além disso, imagine que você está escrevendo esta classe:

class Foo implements com.libraryA.Bar, com.libraryB.Moo { 
    // Implementation of Foo, that does NOT override equals
}

O Fooescritor examina os supertipos, não vê implementação de iguais e conclui que, para obter igualdade de referência, tudo o que ele precisa fazer é herdar iguais Object. Então, na próxima semana, o mantenedor da biblioteca do Bar "útil" adiciona uma equalsimplementação padrão . Opa! Agora, a semântica de Foofoi quebrada por uma interface em outro domínio de manutenção "útil", adicionando um padrão para um método comum.

Padrões devem ser padrões. Adicionar um padrão a uma interface onde não havia nenhum (em nenhum lugar da hierarquia) deve afetar a semântica das classes de implementação concretas. Mas se os padrões pudessem "substituir" os métodos Object, isso não seria verdade.

Portanto, embora pareça um recurso inofensivo, é de fato bastante prejudicial: adiciona muita complexidade para pouca expressividade incremental e facilita demais para que alterações intencionais e inofensivas nas interfaces compiladas separadamente possam prejudicar a semântica pretendida para implementar classes.

Brian Goetz
fonte
13
Fico feliz que você tenha explicado isso e aprecio todos os fatores que foram considerados. Concordo que isso seria perigoso para hashCodee equals, mas acho que seria muito útil toString. Por exemplo, alguma Displayableinterface pode definir um String display()método e economizaria uma tonelada de clichê para poder definir , default String toString() { return display(); }em Displayablevez de exigir que cada um Displayableimplementasse toString()ou estendesse uma DisplayableToStringclasse base.
Brandon
8
@Brandon Você está correto ao permitir que herdar toString () não seria perigoso da mesma maneira que seria para equals () e hashCode (). Por outro lado, agora o recurso seria ainda mais irregular - e você ainda terá toda a mesma complexidade adicional sobre regras de herança, pelo bem desse método ... parece melhor traçar a linha de maneira limpa onde fizemos .
Brian Goetz
5
@gexicide Se toString()for baseado apenas nos métodos da interface, você pode simplesmente adicionar algo parecido default String toStringImpl()à interface e substituir toString()em cada subclasse para chamar a implementação da interface - um pouco feia, mas funciona e melhor que nada. :) Outra maneira de fazer isso é criar algo como Objects.hash(), Arrays.deepEquals()e Arrays.deepToString(). Marcou com +1 a resposta de @ BrianGoetz!
Siu Ching Pong -Asuka Kenji-
3
O comportamento padrão do toString () de uma lambda é realmente desagradável. Sei que a fábrica lambda foi projetada para ser muito simples e rápida, mas cuspir um nome de classe derrotado realmente não é útil. Ter uma default toString()substituição em uma interface funcional nos permitiria - pelo menos - fazer algo como cuspir a assinatura da função e a classe pai do implementador. Melhor ainda, se pudéssemos trazer algumas estratégias recursivas de toString, poderíamos percorrer o fechamento para obter uma descrição realmente boa do lambda e, assim, melhorar drasticamente a curva de aprendizado do lambda.
Groostav
Qualquer alteração no toString em qualquer classe, subclasse, membro da instância pode ter esse efeito na implementação de classes ou usuários de uma classe. Além disso, qualquer alteração em qualquer um dos métodos padrão provavelmente também afetaria todas as classes de implementação. Então, o que há de tão especial no toString, hashCode, quando se trata de alguém alterar o comportamento de uma interface? Se uma classe estender outra classe, ela também poderá alterar. Ou se eles estiverem usando o padrão de delegação. Alguém usando interfaces Java 8 terá que fazer isso atualizando. Um aviso / erro que pode ser suprimido na subclasse poderia ter sido fornecido.
Mmm
30

É proibido definir métodos padrão em interfaces para métodos em java.lang.Object, uma vez que os métodos padrão nunca seriam "alcançáveis".

Os métodos de interface padrão podem ser substituídos nas classes que implementam a interface e a implementação de classe do método tem uma precedência mais alta que a implementação da interface, mesmo se o método for implementado em uma superclasse. Como todas as classes são herdadas java.lang.Object, os métodos em java.lang.Objectteriam precedência sobre o método padrão na interface e seriam invocados.

Brian Goetz, da Oracle, fornece mais alguns detalhes sobre a decisão de design nesta postagem da lista de discussão .

jarnbjo
fonte
3

Eu não vejo na cabeça dos autores da linguagem Java, então podemos apenas adivinhar. Mas vejo muitas razões e concordo absolutamente com elas nesta edição.

O principal motivo para a introdução de métodos padrão é poder adicionar novos métodos às interfaces sem interromper a compatibilidade com versões anteriores de implementações mais antigas. Os métodos padrão também podem ser usados ​​para fornecer métodos de "conveniência" sem a necessidade de defini-los em cada uma das classes de implementação.

Nada disso se aplica a toString e outros métodos de Object. Simplificando, os métodos padrão foram projetados para fornecer o comportamento padrão onde não há outra definição. Não fornecer implementações que "competirão" com outras implementações existentes.

A regra "classe base sempre vence" também tem razões sólidas. Supõe-se que as classes definam implementações reais , enquanto as interfaces definem implementações padrão , que são um pouco mais fracas.

Além disso, a introdução de QUAISQUER exceções às regras gerais causa complexidade desnecessária e levanta outras questões. O objeto é (mais ou menos) uma classe como qualquer outra, então por que deveria ter um comportamento diferente?

No geral, a solução que você propõe provavelmente traria mais desvantagens do que profissionais.

Marwin
fonte
Não notei o segundo parágrafo da resposta do gexicida ao postar a minha. Ele contém um link que explica o problema com mais detalhes.
Marwin
1

O raciocínio é muito simples, porque Object é a classe base para todas as classes Java. Portanto, mesmo se tivermos o método de Object definido como método padrão em alguma interface, será inútil porque o método de Object será sempre usado. É por isso que, para evitar confusão, não podemos ter métodos padrão que estão substituindo os métodos da classe Object.

Kumar Abhishek
fonte
1

Para dar uma resposta muito pedante, só é proibido definir um defaultmétodo para um método público a partir de java.lang.Object. Existem 11 métodos a serem considerados, que podem ser categorizados de três maneiras para responder a essa pergunta.

  1. Seis dos Objectmétodos não pode ter defaultmétodos porque eles são finale não podem ser substituídos em tudo: getClass(), notify(), notifyAll(), wait(), wait(long), e wait(long, int).
  2. Três dos Objectmétodos não pode ter defaultmétodos para Pelas razões expostas por Brian Goetz: equals(Object), hashCode(), e toString().
  3. Dois dos Objectmétodos podem ter defaultmétodos, embora o valor de tais padrões seja questionável na melhor das hipóteses: clone()e finalize().

    public class Main {
        public static void main(String... args) {
            new FOO().clone();
            new FOO().finalize();
        }
    
        interface ClonerFinalizer {
            default Object clone() {System.out.println("default clone"); return this;}
            default void finalize() {System.out.println("default finalize");}
        }
    
        static class FOO implements ClonerFinalizer {
            @Override
            public Object clone() {
                return ClonerFinalizer.super.clone();
            }
            @Override
            public void finalize() {
                ClonerFinalizer.super.finalize();
            }
        }
    }
jaco0646
fonte
.Qual o objetivo? Você ainda não respondeu à parte PORQUE - "Então, qual é a razão pela qual os designers de Java decidiram não permitir que os métodos padrão substituíssem os métodos do Object?"
pro_cheats 02/09