[Pesquisar este blog]

domingo, 13 de setembro de 2015

Referências para métodos e seu uso no Java 8

Uma importante construção introduzida no Java 8, juntamente com as expressões lambda, foi a definição de referências para métodos ou construtores. Uma referência para método é uma indicação de que um método existente em outra classe deve ser utilizado num ponto específico do código, sendo que este mecanismo também é válido para construtores.

Para que seja possível o uso de uma referência de método, o target-type (tipo-alvo) do método deve ser compatível com aquele requerido no local da referência e também previamente estabelecido por meio da declaração de uma interface funcional. Como visto no artigo anterior, sobre expressões lambda, o target-type é a combinação das informações relativas ao tipo de retorno da expressão lambda, seguida da lista dos tipos de seus parâmetros entre parêntesis.

Por conta da compatibilidade exigida entre o target-type requerido e a referência de método utilizada, é possível utilizar uma referência de método no local onde uma expressão lambda é aceita e também o inverso, ou seja, usar uma expressão lambda para substituir uma referência de método. Assim, o uso de referências para métodos é útil para evitar a redefinição de métodos existentes em outras classes, ao mesmo tempo que não interfere na possibilidade de uso de expressões lambda.

Existem quatro tipos diferentes de referências para métodos, cuja sintaxe é um pouco diferente: referências para métodos estáticos; referências para métodos de instância de objeto específico; referências para métodos de instância de objeto arbitrário de tipo; e referências para construtores.

Referências para métodos estáticos

O tipo mais simples de referência para método é aquela para um método estático com visibilidade pública, cuja sintaxe é:

NomeClasse::metodoEstatico

Em que NomeClasse é o nome da classe e metodoEstatico é o nome do método. Tomando a classe Math como exemplo, observam-se vários métodos estáticos cujo target-type é double (double), tais como cbrt, exp e log. Todos estes métodos são compatíveis com algumas das interfaces predefinidas no pacote java.util.function, no caso DoubleUnaryOperator,  DoubleFunction<T> e Function<T,V>. Isto permite declarar referências para métodos de target-type específicos como:

DoubleUnaryOperator f_cbrt = Math::cbrt;
DoubleFunction<Double> f_exp = Math::exp;
Function<Double,Double> f_log = Math::log;

Já os métodos max, mix e pow, também da classe Math, possuem um target-type double (double, double) e podem ser referenciados como Math::max, Math::min e Math::pow:

DoubleBinaryOperator f_max = Math::max;
BinaryOperator<Double> f_min = Math::min;
Calculavel f_pow = Math::pow;

Deve-se observar que toda referência para método possui uma expressão lambda equivalente (de mesmo target-type):

DoubleUnaryOperator f_cbrt2 = (v) -> Math.cbrt(v);
BinaryOperator<Double> f_min2 = (a, b) -> Math.min(a, b);

Embora exista tal equivalência, é importante notar que o uso da referência para método exige o acionamento do método declarado pela interface funcional e não do método referenciado. Assim, a referência f_max, declarada anteriormente como do tipo DoubleBinaryOperator, deve ser acionada pelo método applyAsDouble(double, double) declarado nesta interface. Da mesma maneira f_cbrt, do tipo DoubleUnaryOperator, deve ser acionado pelo método applyAsDouble(double).

O exemplo que segue mostra o uso direto de referências para métodos estáticos.

/* Arquivo: RefMetEstatico.java
 */
package jandl.j8r;

import java.util.function.*;

public class RefMetEstatico {
    public static void main(String[] args) {
    // Uso direto dos métodos da classe Math
        System.out.println("cbrt(27.0)=" + Math.cbrt(27.0));
        System.out.println("exp(1.0)=" + Math.exp(1.0));
        System.out.println("max(2.0, 1.5)=" + Math.max(2.0, 1.5));
        System.out.println("min(2.0, 1.5)=" + Math.min(2.0, 1.5));
    
    // Referências de métodos com assinatura double (double)
        DoubleUnaryOperator f_cbrt = Math::cbrt;
        DoubleFunction<Double> f_exp = Math::exp;

    // Referências de métodos com assinatura 
    // double (double, double)
        DoubleBinaryOperator f_max = Math::max;
        BinaryOperator<Double> f_min = Math::min;

    // Uso das referências de métodos
        System.out.println("cbrt(27.0)=" +
                f_cbrt.applyAsDouble(27.0));
        System.out.println("exp(1.0)=" + 
                f_exp.apply(1.0));
        System.out.println("max(2.0, 1.5)=" + 
                f_max.applyAsDouble(2.0, 1.5));
        System.out.println("min(2.0, 1.5)=" + 
                f_min.apply(2.0, 1.5));

    // Expressões lambda equivalentes
        DoubleUnaryOperator f_cbrt2 = v -> Math.cbrt(v);
        BinaryOperator<Double> f_min2 = (a, b) -> a < b ? a : b;

    // Uso das referências (de métodos) para expressões lambda
        System.out.println("cbrt2(729)=" + 
                f_cbrt2.applyAsDouble(729));
        System.out.println("min2(2.0, 1.5)=" + 
                f_min2.apply(2.0, 1.5));
    }
}

Este programa produz o resultado que segue, onde é possível verificar que o uso das referências e suas expressões lambda equivalentes produzem os mesmos resultados.

cbrt(27.0)=3.0
exp(1.0)=2.718281828459045
max(2.0, 1.5)=2.0
min(2.0, 1.5)=1.5
cbrt(27.0)=3.0
exp(1.0)=2.718281828459045
max(2.0, 1.5)=2.0
min(2.0, 1.5)=1.5
cbrt2(729)=9.0
min2(2.0, 1.5)=1.5

Referências para métodos de instância de objetos específicos

A sintaxe para uma referência para um método de instância (de objeto) com visibilidade pública é:

objeto::metodoInstancia

Aqui objeto é uma referência válida (não nula) para uma instância disponível no escopo e metodoInstancia é o nome do método público referenciado. Consideremos, por exemplo, a classe Resistor, que representa um resistor segundo a lei de Ohm.

/* Arquivo: Resistor.java
 */
package jandl.j8r;

public class Resistor {
    protected double resistencia;

    public Resistor(double resistencia) {
        setResistencia(resistencia);
    }

    public double getResistencia() {
        return resistencia;
    }

    public void setResistencia(double resistencia) {
        if (resistencia <= 0) {
            throw new RuntimeException("Resistência inválida: " +
                    resistencia);
        }
        this.resistencia = resistencia;
    }

    public double correnteParaTensao(double tensao) {
        return tensao / resistencia;
    }

    public double tensaoParaCorrente(double corrente) {
        return resistencia * corrente;
    }

    @Override
    public String toString() {
        return String.format("R:%7.1fOhms", resistencia);
    }
}

Esta classe dispõe de alguns métodos de instância, como getResistencia()setResistencia(double), correnteParaTensao(double) e tensaoParaCorrente(double). Uma instância desse tipo, denominada r10k, pode ser obtida com:

Resistor r10k = new Resistor(10000);

Dessa maneira, seus métodos de instância podem ser referenciados como:

r10K::getResistencia
r10K::setResistencia
r10K::correnteParaTensao
r10K::tensaoParaCorrente

O exemplo que segue mostra como referências para métodos de instância permitem a manipulação indireta de um objeto. Como antes, as interfaces funcionais predefinidas existentes no pacote java.util.function simplificam a programação, evitando a definição de interfaces específicas. Foram usadas as interfaces DoubleConsumerDoubleFunction<T> DoubleSupplier.

/* Arquivo: RefMetInstancia.java
 */
package jandl.j8r;

import java.util.function.DoubleConsumer;
import java.util.function.DoubleFunction;
import java.util.function.DoubleSupplier;

public class RefMetInstancia {
    public static void main(String[] args) {
    // Instanciação de objeto tipo Resistor
        Resistor resistor = new Resistor(10000);
        System.out.println("Resistência : " + resistor);
    // Referências para métodos da instância r10k
        DoubleSupplier get_resistencia = resistor::getResistencia;
        DoubleConsumer set_resistencia = resistor::setResistencia;
        DoubleFunction<Double> i_V = resistor::correnteParaTensao;
        DoubleFunction<Double> v_I = resistor::tensaoParaCorrente;
    // Manipulação da instância via referências para métodos
        System.out.println("Resistência : " + 
                get_resistencia.getAsDouble());
        System.out.println("i para V=12V: " + 
                i_V.apply(12));
        System.out.println("v para i=0.001A: " + 
                v_I.apply(0.001));
        set_resistencia.accept(1000);
        System.out.println("Nova Resistência : " + resistor);
        System.out.println("Resistência : " + 
                get_resistencia.getAsDouble());
        System.out.println("i para V=12V: " + 
                i_V.apply(12));
        System.out.println("v para i=0.001A: " + 
                v_I.apply(0.001));
    }
}

Este programa produz o resultado seguinte que ilustra como referências para métodos podem manipular um objeto específico.

Resistência : R:10000,0Ohms
Resistência : 10000.0
i para V=12V: 0.0012
v para i=0.001A: 10.0
Nova Resistência : R: 1000,0Ohms
Resistência : 1000.0
i para V=12V: 0.012
v para i=0.001A: 1.0

Deve ser destacado que as referências para métodos de instância somente afetam o objeto especificamente referenciado. Como antes, estas referências também são equivalentes a expressões lambda, como segue:

DoubleSupplier get_resistencia_2 = () -> r10k.getResistencia();
DoubleConsumer set_resistencia_2 = (r) -> r10k.setResistencia(r);
DoubleFunction<Double> i_V_2 = (v) -> r10k.correnteParaTensao(v);
DoubleFunction<Double> v_I_2 = (i) -> r10k.tensaoParaCorrente(i);

Referências para métodos de instância de objetos arbitrários

Outra possibilidade para utilização das referências para métodos é quando se deseja acionar um método com visibilidade pública para quaisquer objetos de um tipo específico. Neste caso a sintaxe das referências para métodos é:

NomeClasse::metodoInstancia

Aqui NomeClasse indica o nome da classe/tipo que contém o metodoInstancia, cujo acionamento é desejado para qualquer instância, ou seja, para objetos arbitrários.

Como é bastante comum a necessidade de ordenar-se um conjunto de objetos de um tipo particular, existe neste problema uma situação de uso conveniente das referências para métodos. Vários objetos da classe Resistor, definida anteriormente, poderiam ser armazenados em um array como segue:

Resistor[] resistorArray = {
        new Resistor(1234), new Resistor(23456),
        new Resistor(345), new Resistor(67),
        new Resistor(879), new Resistor(1098),
        new Resistor(543) };

Caso fosse necessário ordenar os elementos deste array de objetos tipo Resistor, poderia ser utilizado o método sort (T[], Comparator<T>), da classe Arrays, o qual toma como argumentos o array a ser ordenado e um comparador adequado. Assim a solução convencional seria construir uma classe dotada da interface Comparator<T> para suprir o comparador necessário, ou seja, uma classe cuja implementação contenha o método compare (T a, T b) destinado à comparar objetos para possibilitar sua ordenação.

/* Arquivo: ResistorComparator.java
 */
package jandl.j8r;

import java.util.Comparator;
// Comparador específico para objetos Resistor
public class ResistorComparator implements Comparator<Resistor> {
    @Override
    public int compare(Resistor a, Resistor b) {
        return (int) (a.getResistencia() - b.getResistencia());
    }
}

Desta maneira, o array resistorArray pode ser ordenado com:

Arrays.sort(resistorArray, new ResistorComparator());

Outra opção, mais simples, seria utilizar uma expressão lambda para suprir a operação de comparação, pois Comparator<T> é uma interface funcional.
Arrays.sort(resistorArray,
    (Resistor a, Resistor b) —> (int) (a.getResistencia() - 
                                       b.getResistencia()));

Uma outra opção seria incluir um método estático, como este que segue, capaz de efetuar a comparação de dois objetos do tipo Resistor, na própria classe Resistor

public static int comparador(Resistor a, Resistor b) {
    return (int) (a.getResistencia() - b.getResistencia());
}

Como o target-type desse método é a mesmo da expressão lambda usada na ordenação do array, i.e., int (Resistor, Resistor), é possível realizar a ordenação do array de tipos Resistor com:

Arrays.sort(resistorArray, Resistor::comparador);

Nessa alternativa, muito compacta, foi usada uma referência para método estático. Mas a adição de um método de instância na classe Resistor para realizar a comparação também é uma opção a ser considerada e que pode ser obtida com:

public int comparador2(Resistor b) {
    return (int) (this.getResistencia() - b.getResistencia());
}

Embora a assinatura deste métodos seja int (Resistor), sua semântica, de fato é int (Resistor, Resistor), pois uma expressão lambda equivalente teria de capturar duas referências do tipo Resistor para computar o resultado de sua expressão (a autorreferência this e o argumento b), de modo que seu target-type efetivo seja int (Resistor, Resistor), a mesma da interface funcional Comparator<T> necessária para ordenação do array, permitindo escrever:

Arrays.sort(resistorArray, Resistor::comparador2);

Esta última opção mostra uma referência para um método de instância indicada por meio de sua classe (e não por um objeto), o que implicitamente adiciona um argumento do mesmo tipo ao método (que corresponde a autorreferência). Assim, esta situação considera o primeiro argumento da expressão lambda como instância a partir da qual se invoca o método, tornando possível seu uso para quaisquer pares de objetos do tipo, implicando no target-type tipoRetorno (Tipo, Tipo).

O próximo exemplo ilustra essas várias alternativas. Utilizando cópias do array resistorArray, obtidas com uso do método clone(), a primeira operação de ordenação emprega uma instância de ResistorComparator, classe que é uma implementação específica de comparador para objetos do tipo Resistor. A segunda operação de ordenação, destacada na sequência, utiliza uma expressão lambda, opção concisa que elimina a necessidade da implementação de um comparador. A terceira ordenação é realizada com uso de uma referência o método estático  comparador da classe Resistor. A quarta operação de ordenação emprega uma referência para método de instância comparador2 do tipo Resistor. As duas últimas alternativas se mostram muito mais concisas que as primeiras, embora só possam ser utilizadas quando se dispõem de métodos convenientes para serem referenciados.

/* Arquivo: RefMetTipo.java
 */
package jandl.j8r;

import java.util.Arrays;

public class RefMetTipo {
    public static void main(String[] args) throws Exception {
        // Array de resistores
        Resistor[] resistorArray = { 
                new Resistor(1234), new Resistor(23456),
                new Resistor(345), new Resistor(67), 
                new Resistor(879), new Resistor(1098), 
                new Resistor(543) };
        Resistor[] copia;
        System.out.println("0:" + Arrays.toString(resistorArray));
        // Ordenação do array com comparator específico
        copia = resistorArray.clone();
        Arrays.sort(copia, new ResistorComparator());
        System.out.println("1:" + Arrays.toString(copia));
        // Ordenação do array com expressão lambda
        copia = resistorArray.clone();
        Arrays.sort(copia, (Resistor a, Resistor b) ->
               (int) (a.getResistencia() - b.getResistencia()));
        System.out.println("2:" + Arrays.toString(copia));
        // Ordenação do array com referência para método estático
        copia = resistorArray.clone();
        Arrays.sort(copia, Resistor::comparador);
        System.out.println("3:" + Arrays.toString(copia));
        // Ordenação do array com referência para 
        // método de instância de tipo
        copia = resistorArray.clone();
        Arrays.sort(copia, Resistor::comparador2);
        System.out.println("4:" + Arrays.toString(copia));
    }
}

Quando executado este exemplo produz o resultado apresentado abaixo, onde se observa que todas as alternativas de ordenação apresentam o mesmo resultado, mostrando a conveniência de expressões lambda e referências de métodos para simplificar a definição de comparadores:

0:[R: 1234,0Ohms, R:23456,0Ohms, R:  345,0Ohms,
    R:   67,0Ohms,
 R:  879,0Ohms, R: 1098,0Ohms, R:  543,0Ohms]
1:[R:   67,0Ohms, R:  345,0Ohms, R:  543,0Ohms,
    R:  879,0Ohms, R: 1098,0Ohms, R: 1234,0Ohms, R:23456,0Ohms]
2:[R:   67,0Ohms, R:  345,0Ohms, R:  543,0Ohms,
    R:  879,0Ohms, R: 1098,0Ohms, R: 1234,0Ohms, R:23456,0Ohms]
3:[R:   67,0Ohms, R:  345,0Ohms, R:  543,0Ohms,
    R:  879,0Ohms, R: 1098,0Ohms, R: 1234,0Ohms, R:23456,0Ohms]
4:[R:   67,0Ohms, R:  345,0Ohms, R:  543,0Ohms,
    R:  879,0Ohms, R: 1098,0Ohms, R: 1234,0Ohms, R:23456,0Ohms]

Referências para construtores

O último tipo de referências para métodos é voltado para o uso de construtores, cuja sintaxe é semelhante à criação de referências para métodos estáticos:

NomeClasse::new

É claro que o NomeClasse indica a classe cujos construtores se deseja referenciar com a indicação de new. Quando existem múltiplos construtores, o compilador infere aquele de será usado efetivamente considerando o contexto de uso da referência. Para especificar a instanciação de arrays, basta acrescentar um par de colchetes após a indicação do tipo:

NomeClasse[]::new

Desta maneira String::new referencia um construtor da classe String, enquanto String[]::new é adequado para referenciar a construção de um array de objetos do tipo String.

Segue um exemplo que ilustra maneiras diferentes para instanciar um objeto de tipo específico: a primeira por meio de instanciação explícita; a segunda por meio de um método fábrica (um padrão de projeto útil); e a terceira pela combinação do uso de uma interface funcional com uma referência para construtor compatível.

/* Arquivo: RefMetConstrutor.java
 */
package jandl.j8r;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;

public class RefMetConstrutor {
    // Método fábrica de listas de objetos do tipo T
    public static <T> List<T> createList() {
        return new ArrayList<T>();
    }

    // Supplier de listas<Resistor>
    public static Supplier<List<Resistor>> resistorListFactory =
            ArrayList<Resistor>::new;

    public static void main(String[] args) throws Exception {
        // Obtenção explícita de lista
        List<Long> lista1 = new ArrayList<Long>();
        // Obtenção de lista com fábrica com inferência de tipo
        List<Long> lista2 = RefMetConstrutor.createList();
        // Obtenção de lista com Supplier
        List<Resistor> lista3 = 
                RefMetConstrutor.resistorListFactory.get();
        // Uso das listas
        lista1.add(123456789L);
        lista1.add(135791113L);
        lista2.addAll(lista1);
        lista2.add(2468101214L);
        lista3.add(new Resistor(10));
        lista3.add(new Resistor(360));
        lista3.add(new Resistor(4700));
        lista3.add(new Resistor(82000));
        // Exibição das listas
        System.out.println("Lista1: " + lista1);
        System.out.println("Lista2: " + lista2);
        System.out.println("Lista3: " + lista3);
    }
}

Este exemplo produz o resultado que segue, onde as listas exibidas mostram os elementos adicionados corretamente.

Lista1: [123456789, 135791113]
Lista2: [123456789, 135791113, 2468101214]

Lista3: [R:   10,0Ohms, R:  360,0Ohms, R: 4700,0Ohms, R:82000,0Ohms]

O uso da referência para construtor permite alguma simplificação do código necessário, embora o método fábrica genérico seja mais flexível devido à possível parametrização de tipos. Também deve ser observado que a referência para construtor é, como antes, equivalente a uma expressão lambda, como esta:

public static Supplier<List<Long>> listFactory =
        () -> new ArrayList<Long>();

Considerações finais

No Java 8 tornou-se possível o uso de referências para métodos existentes no lugar de expressões lambda, desde que suas assinaturas sejam compatíveis. Isto constitui uma alternativa interessante que, em muitos casos, permite substituir o uso de instâncias de classes auxiliares, instâncias de classes anônimas e até mesmo expressões lambda pela indicação de métodos e construtores disponíveis, reduzindo a quantidade de código necessária, simplificando o desenvolvimento de aplicações.

Como para as expressões lambda, as substituições possíveis dependem da existência de interfaces funcionais compatíveis, onde a API do Java 8 contribui significativamente aos oferecer muitas interfaces funcionais de propósito geral no pacote java.util.function que podem servir de target-type para lambdas e referências de métodos.

Referências

Este artigo faz parte de uma pequena série:

sábado, 12 de setembro de 2015

IV Workshop em Tecnologia de Informação::WTI2015

Evento anual dos cursos de graduação em Análise e Desenvolvimento de Sistemas e Sistemas de Informação do Centro Universitário Padre Anchieta (Jundiaí-SP), o WTI2015 será realizado no período de 14 a 17 de setembro de 2015 com a temática de Cloud Computing.
O evento pretende divulgar novas tecnologias, serviços, metodologias, práticas e oportunidades de carreira na área de Tecnologia de Informação; bem como a permitir a discussão das influências da tecnologia na organização da sociedade, considerando aspectos sociais, econômicos e culturais, incluindo as questões da inclusão e exclusão digital, da igualdade de gênero, raça e credo; além de oferecer motivação para os estudantes no sentido do prosseguimento de seus estudos e desenvolvimento de suas carreiras.

Composto de palestras e minicursos, o evento é dirigido aos estudantes dos cursos de graduação em Análise e Desenvolvimento de Sistemas e Sistemas de Informação, mas está aberto aos demais estudantes da instituição e ao público em geral.

Importante

  • PALESTRAS: não é necessário inscrição! Basta o comparecimento e o registro de presença na recepção do evento (muito importante para obtenção de certificado válido para Atividades Complementares).
  • MINI-CURSOS: a inscrição será realizada apenas no dia 14/09/2015, na recepção do evento, das 19H00 as 22H30. As vagas são limitadas.

Programação

  • 14/Setembro/2015, 2a-feira [Anfiteatro do Câmpus Universitário]
    19H00::Recepção dos participantes. Inscrições para Mini-cursos.
    19H30::Palestra::Segurança e Computação em Nuvem
    Sr. Hildemar Antonio Baldan
  • 15/Setembro/2015, 3a-feira [Anfiteatro do Câmpus Universitário]
    19H00::Recepção dos participantes.
    19H30::Palestra::Cloud Computing::Definição, Tendências e Aplicações
    Sr. Marcelo Passaro Fontana
  • 16/Setembro/2015, 4a-feira [Prédio 2, Sala 25]
    19H30::Palestra::Cloud Computing
    Sr. 
    Gustavo Leite de Mendonça Chaves
  • 17/Setembro/2015, 5a-feira [Prédio 2, Sala 25]
    19H30::Palestra::Domótica
    Prof. Leandro Pottes

Mini-Cursos

  • Você e Cloud
    15, 16 e 17/Setembro/2015, 21H00 as 22H30 [Prédio 2, Laboratório 17]
    Sr. Eliseo Ulisses Fajardo de Lima
  • Novidades do Java 8
    16 e 17/Setembro/2015, 19H30 as 22H30
     [Prédio 2, Laboratório 18]
    Prof. Peter Jandl Junior
  • COBIT
    16 e 17/Setembro/2015, 19H30 as 22H30 [Prédio 2, Sala 24]
    Prof. Antônio Rogério Tabalipa
  • Desenvolvimento Básico de WebServices
    16 e 17/Setembro/2015, 19H30 as 22H30 [Prédio 2, Laboratório 19]
    Prof. Juliano Silva Marçal

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