[Pesquisar este blog]

domingo, 6 de setembro de 2015

Expressões Lambda no Java 8

Uma das grandes novidades do Java 8 foi a inclusão das expressões lambda, cujo propósito é permitir a definição métodos anônimos diretamente no local de sua utilização. Um método ou função-membro anônimo sintetiza uma funcionalidade que será utilizada uma única vez, de modo a evitarmos o uso de classes anônimas. De fato, o uso de expressões lambda torna possível a passagem de código para métodos ao invés de objetos.

A origem das expressões lambda é o cálculo lambda ou cálculo-λ que foi proposto por Alonzo Church na década de 1930. O cálculo-λ é uma notação matemática formal composta das expressões lambda e de um conjunto sofisticado de regras de aplicação que permitem descrever e avaliar funções anônimas, recursivas e computáveis as quais podem ser usadas tanto como argumentos ou como valor de retorno de outras funções.

Sintaxe das expressões lambda

Uma expressão lambda é, em essência, uma função anônima, isto é, um método sem nome, sem especificadores de acesso e sem modificadores, onde também se omite o tipo do valor de retorno e, quando possível, o tipo de seus parâmetros. Assim, privilegia-se sua funcionalidade (seu código) e simplifica-se a lista necessária de parâmetros.

Uma expressão lambda pura em Java consiste numa lista de parâmetros enviada diretamente para uma expressão, cujo resultado é retornado implicitamente, ou seja:

(listaParâmetros) -> expressão

Exemplos de expressões lambda puras são:
  • (x) -> 2*x + 1
  • (x,y) -> 3*x >= 2*y
  • (p,m,h) -> 1.5*p + 1.9*m + 2.0*h
A primeira expressão lambda pode ser lida como: a função que, dado o parâmetro de entrada x, retorna o resultado da expressão 2*x + 1. Então o novo operador lambda -> pode ser lido como “que retorna” ou “que produz”. A segunda expressão lambda é, portanto, a função que dado os parâmetros de entrada x e y produz o resultado da expressão 3*x >= 2*y. Outra forma de leitura, da última expressão lambda por exemplo, seria os parâmetros p, m e h enviados a expressão 1.5*p + 1.9*m + 2.0*h retornando seu resultado.

Nestas expressões lambda podem ser observadas as seguintes características voltadas para sua simplicidade:
  • Os parêntesis iniciais são requeridos para delimitar a lista de parâmetros da expressão lambda. Podem ser omitidos apenas quando existe somente um parâmetro.
  • Os tipos dos parâmetros podem ser omitidos, pois em geral são inferidos pelos compilador, a partir do contexto de uso da expressão lambda. Quando a inferência de tipos não é possível, devem ser indicados como na declaração de métodos.
  • Quando a expressão lambda é pura, não é necessário o uso explícito da diretiva return.
As expressões lambda também podem ser mais sofisticadas que uma expressão, envolvendo assim blocos de código que podem conter declarações de variáveis, diretivas Java e a combinação de diversas expressões, incluindo instanciação de objeto e sua utilização. Neste caso a sintaxe requer o uso de chaves para delimitar o bloco de código da expressão lambda como segue:

(listaParâmetros) -> { blocoDeCódigo }

Um exemplo de expressão lambda dotada de bloco de código é:

(double[] array, int sp, int ep) -> {
    double total = 0;
    for(int i=sp; i<ep; i++) total =+ array[i];
    return total;

Aqui foram indicados os tipos dos parâmetros, o que pode ser necessário quando o compilador não é capaz de determinar seus tipos dentro do contexto de uso da expressão lambda. Além disso, como esta expressão lambda emprega um bloco de código, o uso explícito da diretiva return, para indicar seu resultado, é obrigatório.

Tipo-Alvo (target-type) das expressões lambda

É comum descrever métodos por meio de sua assinatura, isto é, a combinação das informações relativas ao seu tipo de retorno, seu nome e a lista dos tipos de seus parâmetros entre parêntesis. Por exemplo, o método Math.pow tem assinatura double pow(double, double), ou seja, toma dois parâmetros do tipo double e retorna um valor double; enquanto o método length da classe String tem assinatura int length(void), pois não toma parâmetros e retorna um valor int.

Já as expressões lambda são descritas por seu target-type ou tipo-alvo, de maneira semelhante aos métodos: o tipo de retorno seguido da lista dos tipos de seus parâmetros entre parêntesis. Então a expressão lambda (x) -> 2*x + 1 pode possuir um target-type int (int), ou seja, recebe um parâmetro int, retornando resultado do mesmo tipo, embora possa ser double (double), dependendo do contexto de sua utilização no programa, mais diretamente de como o parâmetro x foi declarado antes da expressão lambda.

Outros exemplos de tipos-alvo de expressões lambda são:
  • boolean (double, double) para (x,y) -> 3*x >= 2*y;
  • double (double, double, double) para(p,m,h) -> 1.5*p + 1.9*m + 2.0*h;
  • int (int, int) para (a, b) -> a > b ? a : b;
  • float (String) para (s) -> Float.parseFloat(s.substring(1)); e
  • double (void) para ( ) -> Math.PI/2.

É importante conhecer os tipo-alvo das expressões lambda por duas razões: para determinar onde podem ser empregada e para orientar a construção de expressões compatíveis com necessidades específicas.

Para que seja possível aplicar as expressões lambda é preciso conhecer as interfaces funcionais e como são tradicionalmente usadas, isto é, sua forma de aplicação típica antes do Java 8.

Interfaces funcionais

Interfaces funcionais são aquelas que possuem um único método abstrato em suas declarações. Antes do Java 8 as interfaces funcionais já existiam, mas eram conhecidas como single abstract methods (SAM) interfaces. Esta nova denominação acompanha a introdução das expressões lambda que são construções típicas do paradigma de programação funcional.

Uma interface funcional pode ser como Filter, que segue.

/* Arquivo: Filter.java
 */
package jandl.j8l;

@FunctionalInterface
public interface Filter {
    boolean accept(Student s);
}

A anotação @FuntionalInterface possibilita que o compilador verifique se existe apenas um método abstrato na interface, como exigido pelas interfaces funcionais. Outros métodos default e estáticos podem existir, bem como campos. No caso, o único método existente, de assinatura boolean accept(Student), define uma operação de validação sobre o objeto Student recebido como parâmetro, retornando true quando aceito, isto é, quando atende uma condição específica, e false quando rejeitado. A realização da interface Filter requer, portanto, a implementação do método accept para a aplicação do critério desejado.

A classe Student utilizada na interface Filter representa um estudante por meio de suas informações de registro acadêmico (sId), nome e curso. Uma implementação bastante simples poderia ser como segue. Nela observam-se os campos necessários para armazenar as informações do estudante, um construtor parametrizado, os métodos de observação (accessors) get, os métodos correspondentes de alteração (mutatorsset; e o método toString para obtenção de uma representação textual do objeto. 

/* Arquivo: Student.java
 */
package jandl.j8l;

public class Student {
// campos para atributos do tipo
    private long sId;
    private String name;
    private int course;
// construtor
    public Student(long sId, String name, int course) {
        this.sId = sId;
        this.name = name;
        this.course = course;
    }
// métodos getter públicos
    public long getSID() { return sId; }
    public String getName() { return name; }
    public int getCourse() { return course; }
// métodos setter protegidos
    protected void setSID(long sId) {
       this.sId = sId; }
    protected void setName(String name) {
       this.name = name; }
    protected void setCourse(int course) { 
       this.course = course; }
// representação textual
    @Override
    public String toString() {
        return String.format("[%07d|%03d] %s", sId, course, name);
    }
}

Para simplificar este exemplo, a classe StudentDB representa um banco de dados de estudantes, na verdade um ArrayList de objetos do tipo Student, estaticamente instanciado e acrescido de um pequeno conjunto de objetos adequados, acessível por meio da variável estática db. O método estático ArrayList<Student> subList(Filter) retorna um subconjunto dos alunos do banco de dados que atendem ao critério determinado pelo objeto Filter recebido como parâmetro.

/* Arquivo: StudentDB.java
*/
package jandl.j8l;

import java.util.ArrayList;

public class StudentDB {
// campo estático db arraylist que simula banco de dados
    public static ArrayList<Student> db = new ArrayList<>();
// bloco estático para inicialização do campo estático db
    static {
        db.add(new Student(1200001, "Bernardo Silva", 18));
        db.add(new Student(1200015, "Alice Pedrosa Souza", 18));
        db.add(new Student(1200348, "Miguel Costa", 18));
        db.add(new Student(1300001, "Arthur Santos", 18));
        db.add(new Student(1300001, "Julia Oliveira", 116));
        db.add(new Student(1310009, "Henrique Pereira", 18));
        db.add(new Student(1310624, "Murilo Almeida", 18));
        db.add(new Student(1400045, "Gabriel Rodrigues", 116));
        db.add(new Student(1400101, "Sofia Nascimento", 18));
        db.add(new Student(1400632, "Enzo Lima", 116));
        db.add(new Student(1411234, "Davi Araújo", 18));
    }

    public static ArrayList<Studant> subList(Filter filter) {
        // cria sublista
        ArrayList<Student> result = new ArrayList<>();
        // percorre todo banco de dados
        for(Student s: db) {
            // aplica filtro: se critério aceito,
           // adiciona na sublista
            if (filter.accept(s)) result.add(s);
        }
        // retorna sublista
        return result;
    }
}

Aplicação das interfaces funcionais

Uma implementação da interface Filter, para criação de um filtro específico para objetos do tipo Student, pode ser como a classe CourseFilterImpl, cujo método accept verifica se o estudante recebido como argumento está matriculado num curso específico.

/* Arquivo: CourseFilterImpl.java
 */
package jandl.j8l;

public class CourseFilterImpl implements Filter {
    @Override
    public boolean accept(Student s) {
        // aceita estudantes do curso 18
       return s.getCourse()==18;
    }
}

Neste ponto contamos com:
  • uma interface funcional Filter que especifica uma operação de validação de objetos Student;
  • a classe Student que representa um estudante;
  • a classe StudentDB que constitui um banco de dados simulado de estudantes, dispondo de uma operação de filtragem denominada subList(Filter); e
  • uma implementação da interface funcional Filter, denominada CourseFilterImpl, que filtra alunos de um curso específico.
Com estes elementos é possível construir um programa para testar a implementação CourseFilterImpl da interface funcional Filter para obtenção de um subconjunto de alunos específicos do banco de dados. Na classe Teste01 que segue existem três trechos de código. No primeiro são exibidos os estudantes existentes no banco de dados e também declarada uma lista do tipo ArrayList<Student> que será usada nos demais trechos.

No segundo trecho é acionado o método subList(Filter) da classe StudentDB com uso de uma instância da classe CourseFilterImpl. Com isso é retornado um ArrayList<Student> que corresponde ao subconjunto dos estudantes matriculados no curso 18, conforme indicado na classe CourseFilterImpl. A lista retornada é exibida, o que permite verificar se a filtragem ocorreu corretamente.

No último trecho o método subList(Filter) é novamente acionado, mas agora com uso de uma instância de uma classe anônima que realiza a interface Filter. Desta maneira é retornado um outro ArrayList<Student> contendo o subconjunto dos estudantes ingressantes a partir de 2014 (isto é de RA maior que 1400000). A lista retornada também é exibida para possibilitar a conferência da filtragem realizada.

/* Arquivo: Teste01.java
 */
import jandl.j8l.CourseFilterImpl;
import jandl.j8l.Filter;
import jandl.j8l.Student;
import jandl.j8l.StudentDB;

import java.util.ArrayList;

public class Teste01 {
    public static void main(String[] args) {
        // exibe DB de estudantes
        System.out.println("Todos:");
        System.out.println(StudentDB.db.toString());
        // sublista de estudantes
        ArrayList<Student> lista;

        // obtém sublista de estudantes do curso 18,
        // filtro implementado como classe independente
        lista = StudentDB.subList(new CourseFilterImpl());
        // exibe sublista
        System.out.println("Curso 18:");
        System.out.println(lista.toString());

        // obtém sublista de estudantes que
       // ingressaram de 2014 em diante,
        // filtro implementado como classe independente
        lista = StudentDB.subList(new Filter () {
            @Override
            public boolean accept(Student s) {
                return s.getSID()>1400000;
            }
        });
        // exibe sublista
        System.out.println("RA>1400000:");
        System.out.println(lista.toString());
    }
}

Os resultados da classe Teste01 são como abaixo.

Todos:
[[1200001|018] Bernardo Silva, [1200015|018] Alice Pedrosa Souza, [1200348|018] Miguel Costa, [1300001|018] Arthur Santos, [1300001|116] Julia Oliveira, [1310009|018] Henrique Pereira, [1310624|018] Murilo Almeida, [1400045|116] Gabriel Rodrigues, [1400101|018] Sofia Nascimento, [1400632|116] Enzo Lima, [1411234|018] Davi Araújo]
Curso 18:
[[1200001|018] Bernardo Silva, [1200015|018] Alice Pedrosa Souza, [1200348|018] Miguel Costa, [1300001|018] Arthur Santos, [1310009|018] Henrique Pereira, [1310624|018] Murilo Almeida, [1400101|018] Sofia Nascimento, [1411234|018] Davi Araújo]
RA>1400000:
[[1400045|116] Gabriel Rodrigues, [1400101|018] Sofia Nascimento, [1400632|116] Enzo Lima, [1411234|018] Davi Araújo]

Apesar do programa ter funcionado como esperado, devemos considerar algumas questões:
  • O método subList(Filter) é bastante conveniente, pois por meio de filtros diferentes, é possível a obtenção de subconjuntos distintos do conjunto integral de dados.
  • A implementação de uma classe específica para realização, apesar de tornar simples o uso do método subList(Filter), pode exigir a construção de várias classes simples, com código repetitivo. Além disso, a fragmentação do código, neste caso, não contribui para o entendimento da solução, pois o critério da filtragem fica contido na classe que realiza a interface do filtro.
  • O emprego de uma classe anônima reduz a fragmentação do código, permite que o critério de filtragem seja mais explícito. No entanto, a implementação em si da classe anônima acrescenta complexidade desnecessária ao código.
É exatamente por causa deste tipo de problema que as expressões lambda foram introduzidas no Java 8!

Aplicações das expressões lambda

O objetivo das expressões lambda, ou simplesmente lambdas, é permitir a definição simplificada de funções anônimas no local onde serão utilizadas. Com isso, os lambdas constituem meio para que o programador passe funcionalidades ao invés de objetos, o que pode ser tanto utilizado na passagem de parâmetros, como no retorno de valores. Assim, os lambdas são um mecanismo poderoso e ao mesmo tempo simples.

Observe a classe Teste02 que segue, que tem a mesma estrutura da classe Teste01 e realiza, exatamente, as mesmas operações. As alterações se encontram no segundo e terceiro trechos onde as expressões lambda destacadas substituíram tanto a instância de CouserFilterIML quanto a classe anônima que realiza a interface funcional Filter.

/* Arquivo: Teste02.java
 */
import jandl.j8l.Student;
import jandl.j8l.StudentDB;

import java.util.ArrayList;

public class Teste02 {
    public static void main(String[] args) {
        // exibe DB de estudantes
        System.out.println("Todos:");
        System.out.println(StudentDB.db.toString());
        // sublista de estudantes
        ArrayList<Student> lista;

        // obtém sublista de estudantes do curso 18,
        // filtro implementado como classe independente
        lista = StudentDB.subList(s -> s.getCourse() == 18);
        // exibe sublista
        System.out.println("Curso 18:");
        System.out.println(lista.toString());

        // obtém sublista de estudantes que 
        // ingressaram de 2014 em diante,
        // filtro implementado como classe independente
        lista = StudentDB.subList(s -> s.getSID()>1400000);
        // exibe sublista
        System.out.println("RA>1400000:");
        System.out.println(lista.toString());
    }
}

No segundo trecho, a instância de CourseFilterImpl que segue:
public class CourseFilterImpl implements Filter {
  @Override
  public boolean accept(Student s) {
    return s.getCourse()==18;
  }
}

É substituída por esta expressão lambda:
s -> s.getCourse() == 18

No terceiro trecho, a instância de uma classe anônima que realiza Filter destacada:
new Filter () {
  @Override
  public boolean accept(Student s) {
    return s.getSID()>1400000;
  }
}

É substituída por outra expressão lambda:
s -> s.getSID()>1400000

Nos dois casos as expressões lambda substituíram convenientemente as alternativas anteriores, reduzindo a quantidade de código necessária e, mais importante, mantendo a clareza do código, pois a operação desejada torna-se explícita.

Existem inúmeras possibilidades de uso das expressões lambda, dentre elas a criação simplificada de threads, event listeners do tipo ActionListener, objetos Comparator<T> e Comparable. Por exemplo, a criação e registro de um ActionListener, usualmente necessária na construção de interfaces GUI é como:
JButton button = new JButton("Incrementar");
button.addActionListener(new ActionListener () {
    @Override
    public void actionListener(ActionEvent e) {
      // incr. variável valor, declarada no escopo
        valor++;
      // imprime valor no console
        System.out.println("Valor:" + valor);
    }
});

Pode ser feito como segue:
JButton button = new JButton("Incrementar");
button.addActionListener( (e) -> {
    // incr. variável valor, declarada no escopo
      valor++;
    // imprime valor no console
      System.out.println("Valor:" + valor);
});


Considerações finais

Do ponto de vista de programação uma expressão lambda, cujo target-type é compatível com o único método abstrato de uma interface funcional, pode substitui um objeto que implementa tal interface, com a vantagem da maior simplicidade e clareza. Já do ponto de vista de geração de código, um lambda é sempre substituído por uma instância de uma classe anônima equivalente gerada pelo compilador.

Além disso, as expressões lambda têm acesso às variáveis presentes no contexto de sua utilização, podendo utiliza-las em seu código, um mecanismo denominado captura de variáveis. Mesmo que tais variáveis sejam alteradas dentro do código do lambda não ocorrem efeitos colaterais, isto é, as alterações não são propagadas para o escopo externo ao lambda, tornando ainda mais conveniente a utilização desta alternativa de programação.

Para facilitar o uso das expressões lambda no Java 8 foi introduzido o pacote java.util.function, o qual disponibiliza um bom conjunto de interfaces funcionais de propósito geral pré-definidas, muitas das quais são definidas por meio dos genéricos, tornando ainda mais flexível sua utilização.

Referências

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:

segunda-feira, 24 de agosto de 2015

Computação em Grade & Voluntarismo

Não é bacana quando a tecnologia contribui de maneira inovadora para sociedade? Não é ainda melhor quando isto não apenas promove o desenvolvimento científico e tecnológico, mas também permite auxiliar, mesmo que indiretamente, comunidades carentes? Esta é a proposta do World Community Grid, uma iniciativa da IBM cuja proposta é explorar a capacidade computacional ociosa existente no mundo para benefício da humanidade.



Os microcomputadores fazem parte de nosso cotidiano, sendo comum encontrá-los em toda parte: estabelecimentos comerciais, bancos, empresas, repartições públicas e, também, com as pessoas, na forma de notebooks, tablets e desktops. São mais de um bilhão de microcomputadores.

O potencial deste enorme contigente de equipamentos, quando somado, representa a maior parte do poder computacional disponível no mundo. Isto sem contar com os muitos milhões de smartphones, smart TVs, consoles de jogos e outros dispositivos capazes de realizar o processamento de dados. Nos próximos anos espera-se que este segmento possua capacidades bastante superiores de todos os supercomputadores e datacenters existentes.

Esta é uma mudança crítica, pois é natural que todo sistema computacional disponha de alguma capacidade ociosa. Isto é ainda mais significativo nos computadores pessoais, que raramente utilizam mais do que 50% de seu processamento de maneira contínua. Assim, atividades de pesquisa que requerem capacidades de processamento vultuosas poderiam ser beneficiadas pelo uso destes recursos ociosos.

Problemas que requerem quantidades fabulosas de processamento são, por exemplo, simulações climáticas para entender o aquecimento global; testes de novos algoritmos de criptografia para segurança das pessoas e aplicações; pesquisas para novos materiais para filtragem de água ou captação de energia solar; sem considerar as inúmeras necessidades na área da saúde e agricultura, que envolvem simulação de novos produtos químicos e protocolos diferenciados.

É aqui que o BOINC entra em cena.

BOINC

O Berkeley Open Infrastruture for Network Computing é uma sofisticada plataforma pública de computação em grade desenvolvida pela Universidade da Califórnia, mais conhecida como Berkeley University.




O termo computação em grade ou grid computing se refere a uma organização particular de computadores conectados por uma rede onde cada equipamento pode ser acessado individualmente. Cada computador da grade pode, simultanea e paralelamente, executar um programa, assim, quanto maior o número de computadores conectados, maior a capacidade conjunta de processamento. Isto caracteriza um modelo de arquitetura de um computador virtual na qual a infraestrutura comum de rede permite reunir os recursos de muitos computadores separados, proporcionando um desempenho extraordinário, comparável ou superior ao de supercomputadores.

O BOINC é uma solução tecnológica que oferece uma base sólida para construção de um enorme ambiente de processamento distribuído que exibe características únicas. Por meio do BOINC é relativamente simples converter uma aplicação de alta demanda computacional existente em um projeto de computação pública.

Cada projeto pode ser distribuído em servidores e banco de dados próprios, tornando-o autônomo e independentes de outros projetos. Os servidores de um projeto dividem o trabalho computacional necessários em unidades de trabalho relativamente pequenas, isto é, que podem ser completamente processadas em equipamentos comuns (microcomputadores domésticos e notebooks por exemplos) em poucas horas.

Com o uso de um programa cliente do BOINC, os proprietários destes equipamentos comuns podem se registrar em um ou mais projetos para compartilhar seus recursos ociosos com a infraestrutura do BOINC. Assim, o programa cliente do BOINC efetua o download de unidades de trabalho, executando-as localmente e enviando, assim que possível, os resultados obtidos para os servidores do projeto.

Deve-se destacar que o cliente BOINC utiliza os recursos ociosos do computador, ou seja, somente efetua o processamento das unidades de trabalho quando não existem programas e serviços do usuário em uso, tornando quase imperceptível sua presença no sistema. Nem mesmo o consumo de energia sofre alteração substancial, visto que a atuação do BOINC usa os períodos em que o computador permanece ligado, mas com baixa demanda de processamento (por exemplo, a requerida durante o preparo de texto, a leitura de documentos e outras atividades de baixa complexidade computacional).

Além disso, o cliente BOINC pode ser livremente configurado para utilização dos recursos do computador onde, a critério exclusivo de seu proprietário, determinam-se o número de processadores ou núcleos usados; o nível de ociosado empregado; o espaço ocupado em memória e disco; e a frequência de uso da rede. É possível, inclusive, determinar percentuais de dedicação para projetos distintos.

Outro aspecto interessante do BOINC é que os tempo de processamento doado, calculado com base em cada processador utilizado, e os resultados obtidos são pontuados, possibilitando criar um ranking de usuários registrados nos projetos. Os usuários também podem formar times, possibilitando diferentes formas de competição saudável.

Hoje o BOINC é utilizado por diferentes projetos de computação pública ao redor do mundo. Dois dos mais significativos são SETI@home e World Community Grid.

O projeto SETI@home (Search for Extra-Terrestrial Intelligence at home), lançado em 1999, tem como objetivo identificar sinais de rádio emitidos por possíveis civilizações inteligentes fora da terra. O projeto SETI@home conta mais de um milhão de participantes ao redor do mundo, cujo processamento associado é superior a 60 TeraFLOPS (trilhões de operações em ponto flutuante por segundo).

Se o SETI@home pode parecer algo diferente ou até excêntrico, o World Community Grid é bastante diferente disso.

World Community Grid

O World Community Grid (WCG), lançado em 2004 pela IBM, tem como propósito explorar as capacidades não utilizadas de microcomputadores de indivíduos e organizações direcionando seu uso para projetos de natureza científica, educacional ou filantrópica.


Numa infraestrutura mantida pela IBM residem os servidores BOINC que administram diversos subprojetos independentes. Um equipe de profissionais da IBM auxilia a transformação de projetos computacionais selecionados em aplicações distribuídas para o BOINC, permitindo que possam ser executadas por qualquer cliente BOINC conectado na internet. A seleção destes projetos respeita uma série de critérios relacionados à sua criticidade, impacto social e real necessidade de processamento extremo.

Para processar o projetos selecionados, quanto mais clientes registrados melhor, pois mais rapidamente os resultados serão obtidos e disponibilizados para as equipes de cientistas e pesquisadores de cada projeto. Até o presente, o WCG suportou mais de 24 grandes projetos de pesquisa, incluindo a pesquisa por alternativas mais efetivas para o tratamento do câncer, do HIV/AIDS e de uma série de doenças tropicais.

World Community Grid


São mais de 650.000 indivíduos e 460 organizações que contribuem com a maior iniciativa computacional voluntária devotada a ciência humanitária.

Assim, ao se registrar no WCG, os proprietários dos computadores tornam-se voluntários que doam processamento para projetos que objetivam o combate a doenças como malária, câncer infantil, leishmaniose, distrofia muscular, ebola; além de pesquisas para o desenvolvimento de fontes de energia sustentáveis; cultivo otimizado de cereais; entre outros.

Considerando que o impacto individual em termos consumo de energia, consumo de banda de rede e desgaste do computador são mínimos, percebe-se que contribuir com os projetos do WCG é muito simples. Mas os resultados da contribuição dos milhares de voluntários do WCG são muitíssimo significativos, pois quase sempre, demandariam um período inviável de tempo para serem obtido com recursos próprios de cada instituição!

É, de fato, sensacional!

Eu contribuo, e você, não gostaria de participar?


Como participar

Para participar é muito fácil também.

(1) Registre-se no WCG utilizando o link abaixo!


(2) Faça o download de uma versão do BOINC. Sugestão, use a versão portable, cujo link segue.


(3) Após instalar, basta selecionar o projeto World Community Grid e indicar seu usuário e senha de registro!

quarta-feira, 19 de agosto de 2015

Virtualização

No dicionário, o adjetivo virtual é explicado como "o que não existe como realidade, mas sim como potência ou faculdade" e também como "o que equivale a outro, podendo fazer às vezes deste, em virtude ou atividade".

Na área de Tecnologia de Informação o termo é frequentemente utilizado no sentido de indicar dispositivos de hardware, programas de computador ou uma combinação destes que substituem outros dispositivos ou programas, de maneira a obter-se seu funcionamento equivalente. Assim, virtualizar é o ato de criação de uma versão virtual de algo, como uma plataforma de hardware, um servidor, um sistema operacional, um dispositivo de armazenamento ou uma rede de computador.

Os softwares ou frameworks de virtualização podem, entre outras capacidades, atuar de duas maneiras distintas:
  • Dividir um recurso para prover seu uso em múltiplos ambientes de execução;
  • Consolidar múltiplos recursos de maneira que sejam tratados como algo único.
Cada um destes modos tem aplicações bastante convenientes e específicas.

Por exemplo, o simples particionamento de uma unidade de disco rígido pode ser considerado como uma operação de virtualização porque a unidade particionada dá origem a duas ou mais unidades lógicas (isto é, que não são físicas). Mas, em algum sistemas, também é possível fazer o oposto, ou seja, configurar duas ou mais unidades físicas para que operem como uma unidade lógica dotada de maior capacidade. Então divisão e consolidação são estratégias que se aplicam a um grande número de situações.

Muitos ambientes de desenvolvimento voltados para a construção de aplicativos móveis incluem emuladores de modelos genéricos ou específicos para possibilitar o teste destes softwares em um ambiente semelhante onde serão utilizados, mas sem a necessidade do programador dispor de um dispositivo físico.

A configuração de uma Virtual Private Network (VPN) é também uma operação de virtualização, pois com o uso de uma infraestrutura pública de comunicação (a internet, por exemplo) é possível a construção de uma rede privativa, que oferece grande segurança para o tráfego de seus dados, que opera transparentemente na rede pública. O efeito de uma rede privada "dentro" da infraestrutura pública de comunicação é a virtualização de uma rede.

Um framework de virtualização pode também ofertar um ambiente composto de diversos dispositivos, simulando um sistema completo por meio de outro. Assim, dispositivos, aplicações e usuários são capazes de interagir com recursos virtuais, isto é, providos pela plataforma de virtualização, como se fossem recursos reais.

Hoje são relativamente comuns os frameworks de virtualização que empregam uma ou mais metodologias de divisão dos recursos de um computador em múltiplos ambientes de execução por meio da aplicação de conceitos e tecnologias de particionamento de hardware e software, compartilhamento de tempo (time-sharing), simulação de máquina parcial ou completa, emulação, qualidade de serviços (QoS) e muitas outras.

Origem

As técnicas de virtualização surgiram na década de 1960 para prover a operação mais eficiente de mainframes, pois devido a seu altíssimo custo, todos os recursos sistêmicos deveriam ser intensamente utilizados para tornar seu uso mais competitivo.

Inicialmente possibilitava a divisão lógica dos recursos físicos do mainframe em múltiplos sistemas lógicos, os quais podiam ser usados para aplicações distintas e com operação independente. Além de flexibilizar a utilização do sistema, um mesmo mainframe poderia até ser compartilhado por organizações distintas quando tecnologias de acesso remoto eram empregadas para conectar os terminais de usuário ao computador principal.

Um caso de sucesso foi o IBM VM (Virtual Machine), um dos primeiros softwares comerciais de virtualização de sucesso, voltado para operação de mainframes.

Na última década a virtualização tornou-se uma estratégia fundamental para organizar os datacenters (as novas versões dos antigos centros de processamento de dados). Um servidor tradicional trabalha com grande ociosidade na maior parte do tempo, pois é dimensionado para atender os horários de pico. Assim o consumo de energia é maior do que efetivamente necessário. Com a virtualização, um servidor de capacidades maiores pode hospedar vários servidores (máquinas) virtuais, o que é denominado consolidação de servidores, explorando melhor os recursos disponíveis, reduzindo a ociosidade e o consumo total de energia, além de requisitar menos espaço físico e esforço de manutenção. Tudo sem redução do desempenho fornecidos aos aplicativos dos servidores, de maneira imperceptível para os usuários destas aplicações.

Visão conceitual

Sua organização tipicamente envolve três camadas:

  • Camada de hardwareComposta dos dispositivos do hardware que, de fato, existem no sistema. Pode agregar um sistema operacional (SO).
  • Camada de virtualizaçãoSistema/programa de virtualização, que particiona e administra a camada de hardware para prover serviços de virtualização ou suporte para máquinas virtuais. É o Hypervisor ou Monitor das Máquinas Virtuais (Virtual Machine Monitor).
  • Camada de aplicaçãoPartições que constituem as máquinas virtuais específicas para operação de diversos SO e aplicações isoladas.

Aplicações da Virtualização

A virtualização tem inúmeras aplicações. Algumas das mais comuns são:
  • Virtualização de armazenamento, onde múltiplas unidades de armazenamento, locais e remotas, são operadas de maneira combinada e distribuídas dinamicamente como unidade virtuais reconfiguráveis.
  • Virtualização de servidores, na qual um servidor físico pode ser particionado em vários servidores virtuais de menor capacidade.
  • Virtualização de sistemas operacionais, que é um tipo de virtualização, ocorrida no kernel do SO, para prover múltiplas instâncias de um mesmo ou diferentes sistemas operacionais numa mesma máquina.
  • Virtualização de redes, onde torna-se possível o uso seletivos dos recursos de uma rede física por meio de sua segmentação lógica, por exemplo, numa VPN.
  • Virtualização de aplicativos, na qual a operação de sistemas de software ocorre em sistemas virtualizados e cujo acesso se dá por meio de redes, como na computação em nuvem (cloud computing).

Vantagens e Desvantagens

A virtualização é uma tecnologia que tem potencial para proporcionar um grande conjunto de vantagens.

Exemplos clássicos são a possibilidade de uso de múltiplos SOs simultaneamente num mesmo host; e a consolidação de servidores subutilizados num menor número de máquinas, proporcionando economia no hardware, no espaço físico, na operação, na gerência, nos custos e também permitindo a redução dos impactos ambientais.

O uso da virtualização permite a continuidade de aplicações legadas que requerem hardware antigo; pode prover ambientes seguros e isolados (sandboxes) para operar aplicações não confiáveis ou constituir ambientes de teste e depuração; permite configurar ambientes com limitações específicas de recursos, sendo conveniente para sistemas habilitados para QoS (Quality of Service). A recuperação de desastres é grandemente facilitada pela re-instalação de sistemas virtualizados, ao invés de re-instalação e reconfiguração tradicional de sistemas.

Outros aspectos positivos do emprego da virtualização são: a possibilidade de empacotar e distribuir instalações ou aplicações complexas; facilitam a migração de software; facilitar o balanceamento de carga de trabalho; sem contar que pode constituir um infraestrutura mais adequada para atendimento flexível das demandas de sistemas de computação em nuvem.

Mas como toda e qualquer tecnologia, também existem desvantagens. Uma delas é que o hardware requisitado é, de fato, mais sofisticado, pois deve ser bastante mais robusto para suportar, sem comprometimento de desempenho e disponibilidade, a consolidação de servidores. Também existe mão de obra especializada, por requer novos conhecimentos e experiência na condução das etapas de adoção de tecnologia, migração de sistemas e operação contínua. Os custos adicionais de licenças de produtos específicos é outra consideração que não pode ser esquecida.

Conclusões

A virtualização é, do ponto de vista técnico, uma alternativa muito interessante e flexível para operação de sistemas sofisticados, mas também pode ser empregada com vantagens no âmbito pessoal. Uma análise cuidadosa do cenário de sua adoção pode determinar sua conveniência e viabilidade econômica em qualquer segmento de negócio. Mas a considerar-se sua larga adoção, por companhias de todos os tamanhos e em todos os ramos de atividade, constitui a maior prova que seu uso cuidadoso permite alcançar grandes ganhos.

Para Saber Mais



sábado, 15 de agosto de 2015

Java 8 Revisitado

A versão 8 do Java não é novidade. Nem deveria ser, pois foi lançada em 2014. Mas ainda existe um razoável número de programadores Java que não explorou, de fato, algumas das melhores características desta versão. Como é bastante provável que o Java 9 seja lançado em 2016, é uma grande ideia conhecer os diferenciais do Java 8.
Java 8
Além das tradicionais melhorias gerais em muitas das APIs existentes, a versão 8 traz o maior número de novidades na plataforma desde a versão 5. Isso é muito expressivo quando consideramos que na versão 5 (de 2004) foram introduzidos os genéricos, o autoboxing, as enumerações, o for avançado, os varargs, a importação estática e também as anotações. A versão 5 do Java contribuiu definitivamente para a consolidação do Java que, entra ano, sai ano, é uma das linguagens mais utilizadas no mundo, se não a mais utilizada.

Novidades da Versão

Dentre as muitas novidades da versão 8 , existe algumas particularmente importantes:
  • Métodos default e estáticos para interfaces;
  • Expressões lambda;
  • Referências para métodos;
  • Streams e operações em massa para coleções; e
  • API DateTime.
Além destas, temos diversos melhoramentos nas anotações, na inferência de tipos, na API de concorrência,  a paralelização de operações, o engine Nashorn para JavaScript, a codificação/decodificação em Base64, entre outros aperfeiçoamentos.

Para explorarmos melhor estas características, cada novidade principal ganha um pequeno artigo próprio, onde são descritos os seus propósitos, quais são as modificações na linguagem e alguns exemplos para clarificar sua aplicação. Os artigos programados são:
Espero que estes materiais sejam úteis!

Referências