Arquivos da categoria:Design
Código tímido
Além de ignorantes, apáticos e egoístas, bons objetos são tímidos.
É o que dizem os Programadores Pragmáticos (Andy Hunt e Dave Thomas), no artigo de 2003 The Art of Enbugging:
Métodos default, colisão de interfaces e precedência de superclasses no Java 8
Métodos públicos e abstratos em Interfaces
Imagine que temos a interface Aquatico, que define um método move:
interface Aquatico {
public abstract void move();
}
Se tivermos uma classe Navio que implementa essa interface mas não fornece uma implementação para o método move, teremos um erro de compilação:
class Navio implements Aquatico {
^
error: Navio is not abstract and does not override abstract method move() in Aquatico
}
Uma interface nos obriga a respeitar um contrato, ou seja, a assinatura (nome, parâmetros e retorno) de um ou mais métodos.
No Java, desde os primórdios, os métodos das interfaces são implicitamente public e abstract. Essa última palavra-chave indica que são métodos abstratos, ou seja, sem corpo definido e sem código, meros moldes a serem preenchidos.
Para o código da classe concreta Navio ser compilado com sucesso, temos que fornecer uma implementação para o método move. Por exemplo:
class Navio implements Aquatico {
public void move() {
System.out.println("Navega na água!");
}
}
O código acima compila normalmente e podemos invocá-lo de maneira polimórfica, como a seguir:
public class Programa {
public static void main(String[] args){
Aquatico titanic = new Navio();
titanic.move(); //imprime "Navega na água!"
}
}
Métodos com mesma assinatura em Interfaces diferentes
Considere que temos uma interface Terrestre, conforme abaixo:
interface Terrestre {
public abstract void move();
}
Poderíamos criar um classe Caminhao que implementa a interface Terrestre, de maneira semelhante ao que fizemos acima:
class Caminhao implements Terrestre {
public void move() {
System.out.println("Move na longa estrada da vida!");
}
}
Para utilizar a classe Caminhao, faríamos de maneira parecida com a anterior:
public class Programa {
public static void main(String[] args){
Caminhao scania = new Caminhao();
scania.move(); //imprime "Move na longa estrada da vida!"
}
}
Mas o que acontece se tivermos uma classe que representa um Hovercraft, um veículo anfíbio, que pode se deslocar tanto na água como na terra?
Será que podemos implementar tanto a interface Aquatico como a interface Terrestre simultaneamente?
Claro que sim, já que a interface define apenas a assinatura (ou seja, nome, parâmetros e retorno) do método move e não uma implementação.
class Hovercraft implements Aquatico, Terrestre {
public void move(){
System.out.println("Move na água e na terra!");
}
}
Não há problema algum, porque a implementação do método move na classe Hovercraft é a mesma tanto para Aquatico como para Terrestre.
Podemos usar a classe Hovercraft conforme a seguir:
public class Programa {
public static void main(String[] args){
Hovercraft srn4 = new Hovercraft();
srn4.move(); //imprime "Move na terra e na água!"
}
}
Métodos default em Interfaces no Java 8
Conforme vimos em um post anterior, no Java 8 agora é possível definir, além de contratos, implementações de métodos em interfaces.
Esse novo mecanismo é chamado de métodos default e, com ele, é possível evoluir interfaces antigas como a Iterable, que agora possui um método forEach, por exemplo.
No exemplo acima, poderíamos definir o comportamento de navegar na água diretamente na interface Aquatico, utilizando a palavra reservada default:
interface Aquatico {
public default void move() {
System.out.println("Navega na água!");
}
}
Métodos default só podem ser definidos em interfaces e são implicitamente públicos e, é claro, não-abstratos porque possuem um corpo definido, com código.
A classe Navio já não precisará fornercer uma implementação para o método move:
class Navio implements Aquatico {
}
A interface Terrestre também pode ter uma implementação do método move com um método default:
interface Terrestre {
public default void move() {
System.out.println("Move na longa estrada da vida!");
}
}
A classe Caminhao também seria simplificada:
class Caminhao implements Terrestre {
}
O programa que utiliza as classes Navio e Caminhao, bem como suas respectivas interfaces, não precisa ser alterado:
public class Programa {
public static void main(String[] args){
Aquatico titanic = new Navio();
titanic.move(); //imprime "Navega na água!"
Caminhao scania = new Caminhao();
scania.move(); //imprime "Move na longa estrada da vida!"
}
}
Colisão de métodos default
Mas o que será que acontece com a nossa classe Hovercraft, que implementa tanto Aquatico como Terrestre?
class Hovercraft implements Aquatico, Terrestre {
^
error: class Hovercraft inherits unrelated defaults for move() from types Aquatico and Terrestre
Observe que, para a classe Hovercraft, o código não pode ser compilado. Como com um método default não apenas define uma assinatura mas também uma implementação, há ambiguidade na definição do método move. O compilador não pode definir qual das duas implementações deve ser escolhida e, por isso, nos avisa dessa colisão com um erro de compilação.
Para fazermos a classe Hovercraft compilar com sucesso, temos que definir uma implementação para o método move, evitando qualquer ambiguidade:
class Hovercraft implements Aquatico, Terrestre {
public void move() {
System.out.println("Move na terra e na água!");
}
}
Uma alternativa é invocar um método default de uma interface a partir de uma classe que a implementa, utilizando o nome da interface seguido de super e do nome do método default. Por exemplo:
class Hovercraft implements Aquatico, Terrestre {
public void move() {
Aquatico.super.move();
Terrestre.super.move();
System.out.println("Move na terra e na água!");
}
}
Se utilizarmos a classe Hovercraft acima em um programa, teremos algo como:
public class Programa {
public static void main(String[] args){
Hovercraft srn4 = new Hovercraft();
srn4.move();
// imprime
// "Navega na água!"
// "Move na longa estrada da vida!"
// "Move na terra e na água!"
}
}
Precedência de métodos da superclasse
Suponha que tenhamos uma interface Aereo, com um método default chamado move:
interface Aereo {
public default void move() {
System.out.println("Move no ar!");
}
}
Também temos uma classe Aviao que implementa a interface Aereo, porém sobreescrevendo o método move:
class Aviao implements Aereo {
public void move(){
System.out.println("Voa...");
}
}
Se criarmos um objeto da classe Aviao, qual método será chamado: o método sobreescrito na classe ou o método default da interface?
Será escolhido o método sobreescrito na classe Aviao, funcionando da mesma maneira que sobreescrita de métodos para classes:
public class Programa {
public static void main(String[] args){
Aviao boeing747 = new Aviao();
boeing747.move(); //imprime "Voa..."
}
}
Agora, se quisermos representar um Hidroavião, um tipo de avião que pode decolar e pousar na água, teremos uma classe semelhante a seguinte:
class HidroAviao extends Aviao implements Aquatico {
}
A classe HidroAviao é um Aviao que implementa a interface Aquatico.
Mas o que será que acontece ao invocarmos o método move de uma instância de HidroAviao? Será que é invocado a implementação da superclasse Aviao ou da interface Aquatico?
A implementação escolhida será a da superclasse:
public class Programa {
public static void main(String[] args){
HidroAviao curtissNC = new HidroAviao();
curtissNC.move(); //imprime "Voa..."
}
}
Uma implementação definida em uma superclasse sempre tem precedência sobre uma implementação definida em um método default de uma interface.
É importante lembrar que, em Java, uma classe só pode ter uma superclasse.
Resumindo
As regras do uso de métodos default são bem claras:
- Colisão de interfaces – se uma classe implementa duas interfaces que possuem métodos default com a mesma assinatura, o método deve ser sobreescrito
- Precedência de superclasses – se uma superclasse provê uma implementação de um método que tem a mesma assinatura de um método default de uma interface, a implementação da superclasse tem precedência
Iteração interna com Java 8 e métodos default
Iterando externamente
Vamos dizer que temos uma lista com nomes de personagens da Disney:
List<String> personagens = Arrays.asList("Pato Donald",
"Mickey", "Pateta", "Pluto");
Temos algumas opções para percorrer essa lista e imprimir o conteúdo. A mais intuitiva para iniciantes, é um for:
for(int i = 0; i < personagens.size(); i++){
String personagem = personagens.get(i);
System.out.println(personagem);
}
Uma opção mais interessante, mas que requer um pouco mais de conhecimento das bibliotecas do Java, é utilizar um Iterator:
for(Iterator<String> it = personagens.iterator(); it.hasNext();){
String personagem = it.next();
System.out.println(personagem);
}
Ambas as opções, se desconsiderarmos o uso de tipos genéricos, já funcionavam até o Java 4. Porém, do Java 5 em diante, percorrer uma lista ficou mais conciso através do for-each:
for(String personagem : personagens){
System.out.println(personagem);
}
Mesmo com um for-each do Java 5, percorrer uma lista é algo tedioso e repetitivo. Quantas vezes um desenvolvedor Java experiente já escreveu um for desse tipo durante a carreira?
Com esse tipo de código, o desenvolvedor precisa se preocupar em “como” percorrer uma lista ao invés de focar apenas no mais importante: “o que” fazer com os elementos.
Essa maneira clássica, que deixa o “como” a cargo do desenvolvedor, pode ser chamada de iteração externa.
Iterando internamente
Com o Java 8, é possível focar apenas no “o que” fazer com os elementos da lista percorrida, utilizando o método forEach:
personagens.forEach(personagem -> System.out.println(personagem));
Observe que o método forEach pertence à própria lista e recebe um lambda.
Na verdade, pode ser passado qualquer lambda ou classe anônima que atenda à interface funcional Consumer, que recebe um argumento e não retorna resultado.
Poderíamos ter abreviado mais ainda o código utilizando uma referência ao método System.out.println:
personagens.forEach(System.out::println);
Essa maneira nova, que permite ao desenvolvedor focar no “o que” fazer com os elementos, pode ser chamada de iteração interna.
Inserindo implementações em interfaces antigas com métodos default
O método forEach está definido em Iterable e, por consequência, em todos os List, Set e Queue da API de Coleções do Java. Porém, esse método não é abstrato.
O que iria acontecer se colocassem mais um método abstrato na interface Iterable? Código de várias bibliotecas e frameworks por aí, que fornececem suas próprias implementações para essas classes, iriam deixar de funcionar. E o Java tem como um de seus princípios mais fortes a retrocompatibilidade e evolução suave (para muitos, suave até demais).
Para evitar qualquer quebra inaceitável, foi criada uma implementação concreta na interface Iterable para o método forEach. Como foi possível inserir uma implementação em uma interface? Antes do Java 8, as interfaces só podiam ter métodos públicos e abstratos, afinal de contas…
Foi criado um novo mecanismo: os métodos default. Para definir um método default, criamos um método com a palavra-chave default e, em seguida, fornecemos uma implementação para o método.
Veja o que aconteceu com a interface Iterable:
package java.lang;
//imports ...
public interface Iterable<T> {
Iterator<T> iterator(); //método implicitamente
//abstrato e público
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
//...
}
Métodos default permitem evoluir uma API antiga de maneira não disruptiva, sem romper com o passado. A implementação definida em uma método default é “copiada” para todas as classes que implementam essa interface. Em um próximo post, veremos com mais detalhes como funciona esse mecanismo.
Métodos default acabaram com a prática comum de ter uma interface acompanhada de uma superclasse abstrata que fornece implementações para a maioria dos métodos e deixando apenas detalhes para as subclasses concretas.
Um exemplo disso é o par
ListeAbstractList. Agora, se fossemos reprojetar a API de Coleções do Java, poderíamos colocar boa parte do código deAbstractListcomo métodos default da interfaceList.
O código desse post pode ser encontrado em: https://gist.github.com/alexandreaquiles/9753849