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

94

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. Interfaces sempre foram um contrato que você deve cumprir, e este é um conceito muito simples e puro. Agora é uma mistura de várias coisas. Na minha opinião:

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

Resumindo:

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 boa coisa.

Após o Java 8:

  • Não há praticamente nenhuma diferença entre uma interface e uma classe abstrata (além de herança múltipla). Na verdade, você pode emular uma aula regular com uma interface.
  • Ao programar as implementações, os programadores podem esquecer de substituir os métodos padrão.
  • Existe 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, cada 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 Cx implementation Ix e o método foo privado com a mesma assinatura não serão compilados.

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

    
por Mister Smith 20.03.2014 / 16:01
fonte

5 respostas

56

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

list.sort(ordering);

em vez de

Collections.sort(list, ordering);

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

    
por 20.03.2014 / 16:47
fonte
44

Porque você só pode herdar uma turma. Se você tem duas interfaces cujas implementações são complexas o suficiente para que você precise 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 que qualquer implementador da interface chamasse os métodos estáticos e obtivesse a funcionalidade, mas é uma grande quantidade de clichês 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 do Stack:

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

    T pop() throws EmptyException;
 }

Não há como garantir que quando alguém implementa a interface, pop lançará uma exceção se a pilha estiver vazia. Poderíamos aplicar essa regra separando pop em dois métodos: um método public final que impõe o contrato e um método protected abstract que realiza 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, mas também as liberamos 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 uma linguagem com herança única, é uma grande perda de flexibilidade. Isso torna suas interfaces em potencial mutuamente exclusivas. Ser capaz de fornecer implementações que dependem apenas dos próprios 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 a linguagem D permite e fornece suporte nativo para Design por contrato . Não há perigo nesta 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, suponho que quaisquer implementações padrão incluídas nas 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,

There's virtually no difference between an interface and an abstract class (other than multiple inheritance). In fact you can emulate a regular class with an interface.

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

Como um exemplo em favor de métodos estáticos em 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. O Collections.unmodifiableList também poderia ter sido declarado na interface List , e seria mais fácil encontrá-lo.

    
por 20.03.2014 / 17:19
fonte
43

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

[d]efault methods enable you to add new functionality to the interfaces of your libraries and ensure binary compatibility with code written for older versions of those interfaces.

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

A razão para permitir métodos estáticos em interfaces é novamente revelada pela documentação: [isso] torna mais fácil para você organizar 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 utilitários estáticos como java.util.Collections pode agora (finalmente) ser considerado um anti-padrão, em geral (claro que não sempre ). Meu palpite é que adicionar suporte a esse comportamento era trivial, uma vez que 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 . Ele não fornece suporte para os tipos de UUID 1, 2 ou 5, e não pode ser facilmente modificado para isso . Ele também está preso a um gerador aleatório pré-definido que não pode ser substituído. A implementação de código para os tipos de UUID não suportados exige uma dependência direta de uma API de terceiros em vez de uma interface ou a manutenção do código de conversão e o custo da coleta de lixo adicional para acompanhá-lo. Com métodos estáticos, UUID poderia ter sido definido como uma interface, permitindo implementações reais de terceiros das partes ausentes. (Se UUID fosse originalmente definido como uma interface, provavelmente teríamos algum tipo de classe UuidUtil desajeitada com métodos estáticos, o que também seria horrível.) Muitas das principais APIs do Java são degradadas por não se basearem em interfaces mas, a partir do Java 8, o número de desculpas para esse mau comportamento diminuiu, felizmente.

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

Também não é uma boa ideia seguir o exemplo do Doval , na minha opinião. Supõe-se que uma interface defina um contrato, mas não se deve impor o contrato. (Não em Java, de qualquer maneira.) A verificação adequada de uma implementação é responsabilidade de um conjunto de testes ou 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 de javac por meio do processador de anotação API e geração de bytecode em tempo de execução.) Idealmente, os contratos seriam aplicados em tempo de compilação e, no pior dos casos, usando um conjunto de testes, mas meu entendimento é que a imposição de execução é desaprovada. Outra ferramenta interessante que pode ajudar na programação de contratos em Java é a Estrutura do verificador .

    
por 03.09.2014 / 20:06
fonte
2

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

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

    
por 20.03.2014 / 16:53
fonte
1

Os dois propósitos principais que vejo nos métodos default (alguns casos de uso servem a ambos os propósitos):

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

Se fosse apenas o segundo propósito, você não veria isso em uma nova interface como Predicate . Todas as interfaces anotadas @FunctionalInterface precisam ter exatamente um método abstrato para que um lambda possa implementá-lo. Métodos default adicionados como and , or , negate são apenas utilitários e você não deve substituí-los. No entanto, os métodos às vezes estáticos fariam melhor .

Quanto à extensão de interfaces existentes - mesmo assim, alguns novos métodos são apenas açúcar sintático. Métodos de Collection como stream , forEach , removeIf - basicamente, é apenas uma utilidade que você não precisa ignorar. E há métodos como spliterator . A implementação padrão é sub-ótima, mas, pelo menos, o código é compilado. Recorra somente a isso se sua interface já estiver publicada e for amplamente usada.

Quanto aos métodos static , acho que os outros cobrem muito bem: ele permite que a interface seja sua própria classe de utilitário. Talvez possamos nos livrar de Collections no futuro do Java? Set.empty() seria rock.

    
por 24.12.2016 / 22:57
fonte