[Pesquisar este blog]

domingo, 30 de agosto de 2015

Interfaces::criação, uso e atualização no Java 8

O termo interface na programação orientada a objetos é usualmente associado ao conjunto dos elementos visíveis nos objetos de uma classe, ou seja, denomina os atributos e operações expostas para outras classes e objetos. Assim, os métodos e campos públicos de um objeto podem ser entendidos como sua interface.

Ao mesmo tempo, linguagens de programação como o Java (e outras) possuem uma construção denominada interface que permite definir um grupo de métodos públicos relacionados, mas sem implementação (abstratos portanto).

Criação

Uma interface é como esta que segue:

/* Arquivo: Reversible.java
 */
package jandl.j8i;
public interface Reversible {
    char SEPARATOR =',';
    Object getElement(int p);
    boolean isReversed();
    int length();
    Reversible reverse();
    String toString();
}

A declaração de uma interface em Java utiliza a palavra reservada interface. No corpo do exemplo da interface Reversible observamos a presença de alguns métodos:
  • getElement(int) que retorna o elemento da posição indicada deste objeto;
  • isReversed() que retorna true se o elemento está invertido em relação a sua definição original;
  • length() que retorna o número de elementos presentes neste objeto;
  • reverse() que efetua a inversão da sequência de elementos contida pelo objeto; e
  • toString() que retorna uma String com a representação do conteúdo do objeto.
Todos estes métodos são implicitamente public e abstract. Embora seja redundante indicá-los, outros especificadores e  modificadores não podem ser usados.

Também é possível que uma interface contenha campos, como SEPARATOR,  mas que serão considerados constantes, ou seja, são implicitamente static e final, sendo igualmente redundante seu emprego.

Uso

Quando desejado, uma classe pode adotar uma interface existente, isto é, pode incluir a codificação dos métodos por ela definidos. Neste caso, dizemos que a classe realiza (ou implementa) a interface. Por exemplo:

/* Arquivo: ReversibleString.java
 */
package jandl.j8i;

public class ReversibleString implements Reversible {
    private StringBuilder content;
    private boolean inverted;

    public ReversibleString(String content) {
        if (content == null)
            throw new IllegalArgumentException("content==null");
        this.content = new StringBuilder(content);
        inverted = false;
    }

    @Override
    public Object getElement(int p) {
        return content.charAt(p);
    }

    public String getText() {
        return content.toString();
    }

    @Override
    public boolean isReversed() {
        return inverted;
    }

    @Override
    public int length() {
        return content.length();
    }

    @Override
    public Reversible reverse() {
        content.reverse();
        inverted = !inverted;
        return this;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        for(int p=0; p<content.length(); p++) {
            sb.append(content.charAt(p));
            if (p<content.length()-1) sb.append(SEPARATOR);
        }
        return sb.toString();
    }
}

A classe ReversibleString representa uma cadeia de caracteres (uma string) que pode invertida, ou seja, representada de trás-para-frente e, assim, implementa a interface Reversible, ou seja, oferece todos métodos especificados na declaração da interface que são getElement(int), isReversed() e length(), reverse() e toString().

Um objeto do tipo StringBuilder é usado como representação interna da string reversível por duas razões: uma porque permite que seu conteúdo seja alterado (diferentemente de String cujos objetos são imutáveis); outra porque oferece a operação de inversão de conteúdo por meio de seu método reverse().

Também é importante destacar que enquanto uma classe pode extender apenas uma outra classe (pois o Java só oferece o mecanismo de herança simples entre classes), é possível que uma classe realize tantas interfaces quanto desejado.

Assim, podemos dizer que o Java oferece a herança simples para implementação, mas dispõem da herança múltipla para interfaces. No caso, a classe ReversibleString é, implicitamente, uma subclasse de java.lang.Object.

Várias classes diferentes podem implementar uma mesma interface, como a classe IntArray.

/* Arquivo: IntArray.java
 */
package jandl.j8i;

public class IntArray implements Reversible {
    private int[] value;
    private boolean inverted;

    public IntArray(int tam) {
        value = new int[tam];
        inverted = false;
    }

    public IntArray(int... valor) {
        this.value = new int[valor.length];
        for (int i = 0; i < valor.length; i++) {
            this.value[i] = valor[i];
        }
        inverted = false;
    }

    @Override
    public Object getElement(int p) {
        return value[p];
    }

    @Override
    public boolean isReversed() {
        return inverted;
    }

    @Override
    public int length() {
        return value.length;
    }

    @Override
    public Reversible reverse() {
        for (int i = value.length / 2 - 1; i >= 0; i--) {
            int aux = value[i];
            value[i] = value[value.length - 1 - i];
            value[value.length - 1 - i] = aux;
        }
        inverted = !inverted;
        return this;
    }

    public void setValor(int p, int v) {
        value[p] = v;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        for(int p=0; p<value.length; p++) {
            sb.append(getElement(p));
            if (p<value.length-1) sb.append(SEPARATOR);
        }
        return sb.toString();
    }
}

A classe IntArray representa um arranjo reversível de valores inteiros que também realiza a interface Reversible, pois implementa as operações nela especificadas.

Outro aspecto importante, classes diferentes que realizam uma mesma interface passam a ter um conjunto de operações comuns. Assim, seus objetos podem ser tratados como sendo do tipo indicado pela interface em questão. Esta é uma conveniente manifestação do polimorfismo.

Assim, objetos do tipo ReversibleString ou IntArray, apesar de possuírem implementações razoavelmente distintas, podem ser manipulados como objetos do tipo Reversible, que limita seu uso aos métodos disponíveis nesta interface, como feito no exemplo que segue.

/* Arquivo: Teste.java
 */
import jandl.j8i.IntArray;
import jandl.j8i.Reversible;
import jandl.j8i.ReversibleString;

public class Teste {
    public static void main(String[] args) {
        ReversibleString rs = 
                new ReversibleString("Peter Jandl Junior");
        testaReversao(rs);
        IntArray arranjo = new IntArray(20, 15, 8, 30);
        testaReversao(arranjo);
        arranjo.reverse();
    }

    public static void testaReversao(Reversible r) {
        for(int i=0; i<2; i++) {
            System.out.println(r + ":" + r.isReversed());
            r.reverse();
        }
        System.out.println(r + ":" + r.isReversed());
    }
}

O método estático testaReversao(Reversible) toma um argumento do tipo Reversible, exibindo no console uma representação do objeto e seu estado de inversão, além de efetuar a inversão de seu conteúdo. Estas operações são repetidas duas vezes, o que permite retornar o objeto ao estado inicial, que é reexibido ao final.

No código do método main(String[]) são instanciados objetos do tipo ReversibleString e IntArray, os quais podem ser utilizados indistintamente pelo método testaReversao(Reversible).

O exemplo Teste produz o seguinte resultado:

P,e,t,e,r, ,J,a,n,d,l, ,J,u,n,i,o,r:false
r,o,i,n,u,J, ,l,d,n,a,J, ,r,e,t,e,P:true
P,e,t,e,r, ,J,a,n,d,l, ,J,u,n,i,o,r:false
20,15,8,30:false
30,8,15,20:true
20,15,8,30:false

Atualização

Existem circunstâncias onde uma interface deveria ser modificada para acompanhar a evolução do projeto onde se insere. Uma possibilidade para isso é a definição de subinterfaces. Tal como para classes, a herança permite que interfaces existentes sejam estendidas em subclasses, possibilitando o reuso das definições e também a adição de novas operações.

Assim, a interface Reversible poderia ser tomada como base para criação da nova interface Reversible2.

/* Arquivo: Reversible2.java
 */
package jandl.j8i;

public interface Reversible2 extends Reversible {
    String toStringUnreversed(Reversible r);
}

A nova interface Reversible2 adiciona o método toStringUnreversed() às definições existentes em Reversible. Assim, quaisquer novas classes poderiam optar pela implementação de Reversible ou Reversible2, como exemplificado anteriormente.

Esta alternativa, de criação de uma nova interface, é simples e adequada em muitas situações, mas não permite resolver os casos onde uma nova operação deveria ser adicionada a uma interface existente. Até o Java 7 não existia solução para isso, pois a adição de novas operações em uma interface existente criaria um problema muito inconveniente: todas as classes que realizassem a interface modificada deveriam ser modificadas para incluir a codificação da nova operação, caso contrário se tornariam abstratas propagando o problema.

Com o Java 8 tornou-se possível a atualização de interfaces que, mesmo modificadas, são compatíveis com suas versões anteriores, simplificando muito o trabalho de manutenção do código. Para isto foram introduzidos os métodos default e estáticos às interfaces.

Métodos Default

A partir do Java 8, uma interface pode conter uma ou mais implementações de métodos, ou seja, podem incluir o código necessário para realizar uma operação, eliminando a necessidade da programação destes métodos nas classes que realizam esta interface. Para que isso seja possível, estes métodos devem declarados como default, como feito na nova versão da interface Reversible.

/* Arquivo: Reversible.java
 * Interface modificada com adição de método default. 
 */
package jandl.j8i;
public interface Reversible {
    char SEPARATOR =',';
    Object getElement(int p);
    boolean isReversed();
    int length();
    Reversible reverse();
    String toString();

// Método default (dotado de implementação)
    default String toStringUnreversed() {
        if (isReversed()) {
            StringBuilder sb = new StringBuilder();
            for(int p=length()-1; p>=0; p--) {
                sb.append(getElement(p));
                if (p>0) sb.append(SEPARATOR);
            }
            return sb.toString();
        } else {
            return toString();
        }
    }
}

O método toStringUnreversed() permite obter a representação textual do objeto em sua situação natural, isto é, conforme criado, quando é considerado não invertido. Observe ainda que este método pode fazer uso dos métodos definidos na interface apesar de não terem sido implementados. Isto não é um problema, visto que o acionamento deste método só pode se dar por meio de uma instância, que para poder existir, tem que ser obtida de uma classe concreta, a qual necessariamente dispõe da implementação de todos os métodos definidos pela interface Reversible.

A adição do método default toStringUnreversed() na interface Reversible não implica em qualquer modificação nas classes ReversibleString e IntArray, as quais realizam a interface Reversible. Ao mesmo tempo, este método default pode ser utilizado por quaisquer objetos destas e de outras classes que realizem a interface Reversible.

Métodos estáticos

Enquanto os métodos comuns ou de instância são operações que podem ser executadas apenas por meio de instâncias, os métodos estáticos podem ser executados por meio de suas classes, sem necessidades de instâncias, além de serem compartilhados por todos os objetos dessa classe. A partir do Java 8, as interfaces também podem conter métodos estáticos.

Assim, todos os objetos das classes que realizam interfaces dotadas de métodos estáticos passam a compartilham desses métodos. Mas diferentemente dos métodos default, os métodos estáticos podem ser acessados diretamente por meio das interfaces que os contém, sem necessidade de instâncias de uma classe. Por outro lado, os métodos estáticos só podem utilizar elementos externos estáticos ou de instâncias criadas localmente.

A seguir temos uma nova modificação da interface Reversible, que recebe um método estático para obter a versão não invertida de um objeto Reversible qualquer. Como o novo método estático toStringUnreversed(Reversible) tornou-se parecido com a implementação do método default toStringUnreversed(), este último foi modificado para utilizar-se do primeiro (pois o método estático não poderia utilizar-se da implementação do método default).

/* Arquivo: Reversible.java
 * Interface modificada com adição de métodos default e estáticos. 
 */
package jandl.j8i;
public interface Reversible {
    char SEPARATOR =',';
    Object getElement(int p);
    boolean isReversed();
    int length();
    Reversible reverse();
    String toString();

// Método estático (dotado de implementação)
    static String toStringUnreversed(Reversible r) {
        if (r.isReversed()) {
            StringBuilder sb = new StringBuilder();
            for(int p=r.length()-1; p>=0; p--) {
                sb.append(r.getElement(p));
                if (p>0) sb.append(SEPARATOR);
            }
            return sb.toString();
        } else {
            return r.toString();
        }
    }
// Método default (dotado de implementação)
    default String toStringUnreversed() {
        if (isReversed()) {
            return toStringUnreversed(this);
        } else {
            return toString();
        }
    }
}

O exemplo Teste2 que segue mostra o uso de métodos default e estáticos.

/* Arquivo: Teste2.java
 */
import jandl.j8i.IntArray;
import jandl.j8i.Reversible;
import jandl.j8i.ReversibleString;

public class Teste2 {
    public static void main(String[] args) {
        ReversibleString rs = 
                new ReversibleString("Java 8 Interfaces");
        teste(rs);
        IntArray ia = new IntArray(31, 35, 64, 68, 95);
        teste(ia);
    }

    public static void teste(Reversible r) {
        r.reverse();
        System.out.println(r);
        System.out.println(r.toStringUnreversed());
        System.out.println(Reversible.toStringUnreversed(r));
        System.out.println(r);
    }
}

A execução deste exemplo produz:

s,e,c,a,f,r,e,t,n,I, ,8, ,a,v,a,J
J,a,v,a, ,8, ,I,n,t,e,r,f,a,c,e,s
J,a,v,a, ,8, ,I,n,t,e,r,f,a,c,e,s
s,e,c,a,f,r,e,t,n,I, ,8, ,a,v,a,J
95,68,64,35,31
31,35,64,68,95
31,35,64,68,95
95,68,64,35,31

Considerações finais

Os métodos default permitem que as interfaces possam evoluir sem a necessidade de modificar e recompilar as classes que as realizam. Tal como para os métodos default, o uso de métodos estáticos nas interfaces também permite sua evolução, sem a necessidade de alteração das classes que delas dependem. Nos dois casos, a evolução de interfaces com métodos default e estáticos garante compatibilidade binária com suas versões antigas.

Finalmente, devemos considerar que os uso dos métodos default acabam por constituir uma espécie de herança múltipla, pois uma classe pode realizar várias interfaces e compartilhar os métodos default ali definidos. No entanto, devemos destacar que essa herança múltipla é estritamente funcional, pois é suprida por métodos. Não existe qualquer herança relacionada ao estado, cuja informação é armazenada em campos.

Referências

Este artigo faz parte de uma pequena série: