Por que os métodos padrão e estáticos foram adicionados às interfaces no Java 8 quando já tínhamos classes abstratas?


99

No Java 8, as interfaces podem conter métodos implementados, métodos estáticos e os chamados métodos "padrão" (que as classes de implementação não precisam substituir).

Na minha opinião (provavelmente ingênua), não havia necessidade de violar interfaces como essa. As interfaces sempre foram um contrato que você deve cumprir, e esse é um conceito muito simples e puro. Agora é uma mistura de várias coisas. Na minha opinião:

  1. métodos estáticos não pertencem a interfaces. Eles pertencem a classes de utilidade.
  2. Os métodos "padrão" não deveriam ter sido permitidos nas interfaces. Você sempre pode usar uma classe abstrata para esse fim.

Em resumo:

Antes do Java 8:

  • Você pode usar classes abstratas e regulares para fornecer métodos estáticos e padrão. O papel das interfaces é claro.
  • Todos os métodos em uma interface devem ser substituídos pela implementação de classes.
  • Você não pode adicionar um novo método em uma interface sem modificar todas as implementações, mas isso é realmente uma coisa boa.

Após o Java 8:

  • Praticamente não há diferença entre uma interface e uma classe abstrata (exceto herança múltipla). De fato, você pode emular uma classe regular com uma interface.
  • Ao programar as implementações, os programadores podem esquecer de substituir os métodos padrão.
  • Há um erro de compilação se uma classe tentar implementar duas ou mais interfaces com um método padrão com a mesma assinatura.
  • Ao adicionar um método padrão a uma interface, toda classe de implementação herda automaticamente esse comportamento. Algumas dessas classes podem não ter sido projetadas com essa nova funcionalidade em mente, e isso pode causar problemas. Por exemplo, se alguém adicionar um novo método padrão default void foo()a uma interface Ix, a classe Cximplementando Ixe tendo um foométodo privado com a mesma assinatura não será compilada.

Quais são as principais razões para essas mudanças importantes e que novos benefícios (se houver) eles acrescentam?


30
Pergunta bônus: Por que eles não introduziram herança múltipla para as aulas?

2
métodos estáticos não pertencem a interfaces. Eles pertencem a classes de utilidade. Não, eles pertencem à @Deprecatedcategoria! métodos estáticos são uma das construções mais abusadas em Java, devido à ignorância e preguiça. Muitos métodos estáticos geralmente significam programadores incompetentes, aumentam o acoplamento em várias ordens de magnitude e são um pesadelo para testar e refatorar a unidade quando você percebe por que é uma má ideia!

11
@JarrodRoberson Você pode fornecer mais algumas dicas (os links seriam ótimos) sobre "são um pesadelo para teste de unidade e refatoração quando você percebe por que é uma má idéia!"? Eu nunca pensei nisso e gostaria de saber mais sobre isso.
aguado

11
@ Chris A herança múltipla de estado causa muitos problemas, principalmente na alocação de memória ( o problema clássico dos diamantes ). A herança múltipla de comportamento, no entanto, depende apenas da implementação que atende ao contrato já definido pela interface (a interface pode chamar outros métodos que declara e requer). Uma distinção sutil, mas muito interessante.
ssube

1
você deseja adicionar método à interface existente, era complicado ou quase impossível antes do java8 agora você pode adicioná-lo como método padrão.
VdeX 27/08/15

Respostas:


59

Um bom exemplo motivador para métodos padrão está na biblioteca padrão Java, onde agora você tem

list.sort(ordering);

ao invés de

Collections.sort(list, ordering);

Eu não acho que eles poderiam ter feito isso de outra maneira sem mais de uma implementação idêntica de List.sort.


19
C # supera esse problema com métodos de extensão.
Robert Harvey

5
e permite que uma lista vinculada use uma combinação de espaço extra O (1) e tempo O (n log n), porque as listas vinculadas podem ser mescladas no lugar, no java 7 ele despeja em uma matriz externa e depois classifica isso
catraca anormal

5
Encontrei este artigo em que Goetz explica o problema. Então, eu vou marcar essa resposta como a solução por enquanto.
Mister Smith

1
@RobertHarvey: Crie uma lista de 200 milhões de itens <Byte>, use-os IEnumerable<Byte>.Appendpara juntá-los e Count, em seguida, ligue e diga-me como os métodos de extensão resolvem o problema. Se CountIsKnowne Counteram membros de IEnumerable<T>, o retorno de Appendpoderia anunciar CountIsKnownse as coleções constituintes o fizessem, mas sem esses métodos, isso não é possível.
supercat 12/06

6
@ supercat: Eu não tenho a menor idéia do que você está falando.
Robert Harvey

48

A resposta correta é encontrada na documentação da Java , que afirma:

Os métodos [d] efault permitem adicionar novas funcionalidades às interfaces de suas bibliotecas e garantir compatibilidade binária com o código escrito para versões mais antigas dessas interfaces.

Essa tem sido uma fonte de dor de longa data em Java, porque as interfaces tendem a ser impossíveis de evoluir depois que são tornadas públicas. (O conteúdo da documentação está relacionado ao documento ao qual você vinculou em um comentário: Evolução da interface por meio de métodos de extensão virtual .) Além disso, a rápida adoção de novos recursos (por exemplo, lambdas e as novas APIs de fluxo) só pode ser feita estendendo o interfaces de coleções existentes e fornecendo implementações padrão. Quebrar a compatibilidade binária ou introduzir novas APIs significaria que vários anos se passariam até que os recursos mais importantes do Java 8 estivessem em uso comum.

A razão para permitir métodos estáticos nas interfaces é novamente revelada pela documentação: [isso] facilita a organização de métodos auxiliares em suas bibliotecas; você pode manter métodos estáticos específicos para uma interface na mesma interface, em vez de em uma classe separada. Em outras palavras, classes de utilidade estática como java.util.Collectionsagora podem (finalmente) ser consideradas um antipadrão, em geral (é claro que nem sempre ). Meu palpite é que adicionar suporte a esse comportamento foi trivial quando os métodos de extensão virtual foram implementados, caso contrário, provavelmente não teria sido feito.

Em uma nota semelhante, um exemplo de como esses novos recursos podem ser benéficos é considerar uma classe que recentemente me incomodou java.util.UUID,. Realmente não oferece suporte para os tipos 1, 2 ou 5 de UUID e não pode ser facilmente modificado para isso. Também está preso a um gerador aleatório predefinido que não pode ser substituído. A implementação do código para os tipos de UUID não suportados requer uma dependência direta de uma API de terceiros e não de uma interface, ou a manutenção do código de conversão e o custo da coleta de lixo adicional. Com métodos estáticos, UUIDpoderia ter sido definido como uma interface, permitindo implementações reais de terceiros das peças ausentes. (Se UUIDoriginalmente fosse definido como uma interface, provavelmente teríamos algum tipo de desajeitadoUuidUtil classe com métodos estáticos, o que também seria horrível.) Muitas APIs principais do Java são degradadas por não se basearem em interfaces, mas a partir do Java 8 o número de desculpas por esse mau comportamento diminuiu felizmente.

Não é correto dizer que [t] praticamente não há diferença entre uma interface e uma classe abstrata , porque as classes abstratas podem ter estado (isto é, declarar campos) enquanto as interfaces não. Portanto, não é equivalente a herança múltipla ou mesmo herança ao estilo mixin. Mixins apropriados (como as características do Groovy 2.3 ) têm acesso ao estado. (O Groovy também suporta métodos de extensão estáticos.)

Também não é uma boa ideia seguir o exemplo de Doval , na minha opinião. Uma interface deve definir um contrato, mas não deve impor o contrato. (De qualquer maneira, não em Java.) A verificação adequada de uma implementação é de responsabilidade de um conjunto de testes ou de outra ferramenta. A definição de contratos pode ser feita com anotações e o OVal é um bom exemplo, mas não sei se ele suporta restrições definidas nas interfaces. Esse sistema é viável, mesmo que não exista atualmente. (As estratégias incluem a personalização em tempo de compilação javacdo processador de anotaçõesAPI e geração de bytecode em tempo de execução.) Idealmente, os contratos seriam aplicados em tempo de compilação e, na pior das hipóteses, usando um conjunto de testes, mas meu entendimento é de que a imposição de tempo de execução é desaprovada. Outra ferramenta interessante que pode auxiliar a programação de contratos em Java é o Checker Framework .


1
Para acompanhamento ainda mais no meu último parágrafo (ou seja, não cumprir os contratos nas interfaces ), é importante ressaltar que defaultos métodos não pode substituir equals, hashCodee toString. Uma análise de custo / benefício muito informativa sobre por que isso não é permitido pode ser encontrada aqui: mail.openjdk.java.net/pipermail/lambda-dev/2013-March/…
ngreen

É uma pena que o Java tenha apenas equalsum único hashCodemétodo virtual e um único , pois existem dois tipos diferentes de igualdade que as coleções podem precisar testar e os itens que implementariam várias interfaces podem ficar presos a requisitos contratuais conflitantes. Ser capaz de usar listas que não serão alteradas como hashMapchaves é útil, mas também há momentos em que seria útil armazenar coleções nas hashMapquais as coisas correspondiam com base na equivalência e não no estado atual [equivalência implica estado e imutabilidade correspondentes ] .
Supercat 11/14

Bem, o Java meio que tem uma solução alternativa com as interfaces Comparator e Comparable. Mas essas são meio feias, eu acho.
ngreen 14/10

Essas interfaces são suportados apenas para determinados tipos de coleção, e eles representam o seu próprio problema: um comparador pode -se potencialmente encapsular estado (por exemplo, um comparador corda especializado pode ignorar um número configurável de caracteres no início de cada corda, caso em que o número de caracteres a serem ignorados seriam parte do estado do comparador), que por sua vez se tornaria parte do estado de qualquer coleção classificada por ele, mas não há mecanismo definido para perguntar a dois comparadores se eles são equivalentes.
Supercat 14/14 /

Ah, sim, sinto a dor dos comparadores. Estou trabalhando em uma estrutura de árvore que deve ser simples, mas não é porque é tão difícil acertar o comparador. Provavelmente vou escrever uma classe de árvore personalizada apenas para resolver o problema.
ngreen

44

Porque você só pode herdar uma classe. Se você possui duas interfaces cujas implementações são complexas o suficiente para precisar de uma classe base abstrata, essas duas interfaces são mutuamente exclusivas na prática.

A alternativa é converter essas classes base abstratas em uma coleção de métodos estáticos e transformar todos os campos em argumentos. Isso permitiria a qualquer implementador da interface chamar os métodos estáticos e obter a funcionalidade, mas é uma enorme quantidade de clichê em uma linguagem que já é muito detalhada.


Como um exemplo motivador de por que ser capaz de fornecer implementações em interfaces pode ser útil, considere esta interface Stack:

public interface Stack<T> {
    boolean isEmpty();

    T pop() throws EmptyException;
 }

Não há como garantir que, quando alguém implementa a interface, poplançará uma exceção se a pilha estiver vazia. Poderíamos aplicar essa regra separando-a popem dois métodos: um public finalmétodo que impõe o contrato e um protected abstractmétodo que executa o popping real.

public abstract class Stack<T> {
    public abstract boolean isEmpty();

    protected abstract T pop_implementation();

    public final T pop() throws EmptyException {
        if (isEmpty()) {
            throw new EmptyException();
        else {
            return pop_implementation();
        }
    }
 }

Não apenas garantimos que todas as implementações respeitem o contrato, como também os libertamos de verificar se a pilha está vazia e lançando a exceção. É uma grande vitória! ... exceto pelo fato de termos que mudar a interface para uma classe abstrata. Em um idioma com herança única, é uma grande perda de flexibilidade. Isso torna suas possíveis interfaces mutuamente exclusivas. Ser capaz de fornecer implementações que dependem apenas dos métodos de interface resolveria o problema.

Não tenho certeza se a abordagem do Java 8 para adicionar métodos a interfaces permite adicionar métodos finais ou métodos abstratos protegidos, mas eu sei que a linguagem D permite e fornece suporte nativo para Design by Contract . Não existe perigo nessa técnica, pois popé final, portanto, nenhuma classe de implementação pode substituí-la.

Quanto às implementações padrão de métodos substituíveis, presumo que todas as implementações padrão adicionadas às APIs Java dependam apenas do contrato da interface à qual foram adicionadas, portanto, qualquer classe que implemente corretamente a interface também se comportará corretamente com as implementações padrão.

Além disso,

Praticamente não há diferença entre uma interface e uma classe abstrata (exceto herança múltipla). De fato, você pode emular uma classe regular com uma interface.

Isso não é verdade, pois você não pode declarar campos em uma interface. Qualquer método que você escreve em uma interface não pode confiar em nenhum detalhe de implementação.


Como um exemplo a favor de métodos estáticos nas interfaces, considere classes de utilitários como Coleções na API Java. Essa classe existe apenas porque esses métodos estáticos não podem ser declarados em suas respectivas interfaces. Collections.unmodifiableListtambém poderia ter sido declarado na Listinterface e seria mais fácil de encontrar.


4
Contra-argumento: como os métodos estáticos, se escritos corretamente, são independentes, eles fazem mais sentido em uma classe estática separada, onde podem ser coletados e categorizados pelo nome da classe, e menos sentido em uma interface, onde são essencialmente uma conveniência. que convida a abusos, como manter o estado estático no objeto ou causar efeitos colaterais, tornando os métodos estáticos não testáveis.
Robert Harvey

3
@RobertHarvey O que o impede de fazer coisas igualmente tolas se o seu método estático estiver em uma classe? Além disso, o método na interface pode não exigir nenhum estado. Você pode simplesmente estar tentando fazer cumprir um contrato. Suponha que você tenha uma Stackinterface e deseje garantir que quando popfor chamado com uma pilha vazia, uma exceção seja lançada. Dados métodos abstratos boolean isEmpty()e protected T pop_impl(), você pode implementar final T pop() { isEmpty()) throw PopException(); else return pop_impl(); }Isso impõe o contrato a TODOS os implementadores.
Doval

Espere o que? Os métodos Push e Pop em uma pilha não serão static.
Robert Harvey

@RobertHarvey Eu teria ficado mais claro se não fosse o limite de caracteres nos comentários, mas estava defendendo implementações padrão em uma interface, não métodos estáticos.
Doval

8
Eu acho que os métodos de interface padrão são um hack que foi introduzido para poder estender a biblioteca padrão sem a necessidade de adaptar o código existente com base nela.
Giorgio

2

Talvez a intenção fosse fornecer a capacidade de criar classes mixin , substituindo a necessidade de injetar informações ou funcionalidades estáticas por meio de uma dependência.

Essa ideia parece relacionada a como você pode usar métodos de extensão em C # para adicionar funcionalidade implementada às interfaces.


1
Os métodos de extensão não adicionam funcionalidade às interfaces. Os métodos de extensão são apenas um açúcar sintático para chamar métodos estáticos em uma classe usando o list.sort(ordering);formulário conveniente .
Robert Harvey

Se você observar a IEnumerableinterface em C #, poderá ver como a implementação de métodos de extensão para essa interface (como LINQ to Objectsfaz) adiciona funcionalidade a todas as classes que implementam IEnumerable. Foi isso que eu quis dizer com adicionar funcionalidade.
rae1

2
Essa é a grande coisa sobre métodos de extensão; eles dão a ilusão de que você está apostando na funcionalidade de uma classe ou interface. Apenas não confunda isso com a adição de métodos reais a uma classe; Os métodos de classe têm acesso aos membros privados de um objeto, os métodos de extensão não (uma vez que são apenas outra maneira de chamar métodos estáticos).
Robert Harvey

2
Exatamente, e é por isso que vejo alguma relação em ter métodos estáticos ou padrão em uma interface em Java; a implementação é baseada no que está disponível para a interface e não na própria classe.
rae1

1

Os dois propósitos principais que vejo nos defaultmétodos (alguns casos de uso atendem aos dois propósitos):

  1. Sintaxe de açúcar. Uma classe de utilitário poderia servir a esse propósito, mas os métodos de instância são melhores.
  2. Extensão de uma interface existente. A implementação é genérica, mas às vezes ineficiente.

Se fosse apenas o segundo objetivo, você não veria isso em uma nova interface como essa Predicate. Todas as @FunctionalInterfaceinterfaces anotadas precisam ter exatamente um método abstrato para que um lambda possa implementá-lo. Adicionado defaultmétodos como and, or, negatesão apenas utilidade, e você não é suposto a substituí-los. No entanto, às vezes os métodos estáticos se saem melhor .

Quanto à extensão de interfaces existentes - mesmo lá, alguns novos métodos são apenas açúcar de sintaxe. Métodos de Collectioncomo stream, forEach, removeIf- basicamente, é só utilitário que você não precisa substituir. E depois existem métodos como spliterator. A implementação padrão é abaixo do ideal, mas ei, pelo menos o código é compilado. Apenas recorra a isso se sua interface já estiver publicada e amplamente utilizada.


Quanto aos staticmétodos, acho que os outros o abordam muito bem: permite que a interface seja sua própria classe de utilidade. Talvez possamos nos livrar Collectionsno futuro do Java? Set.empty()iria balançar.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.