Java 8: Lambdas

Posted on Sun 30 March 2014 in coding

O Java 8 foi lançado há uma semana e tal. Para quem me conhece, sabe que eu tenho um pouco de pavor à linguagem. Após este lançamento, fiquei genuinamente com a impressão de Java se estar a tornar numa linguagem menos detestável e um pouquinho mais usável.

Portanto decidi começar uma série de posts a descrever as novas features, e, para um primeiro post, nada melhor do que falar da minha feature preferida: lambdas!

Considerem esta situação muito comum:

class Chatserver {
    private List<User> users;
    (...)

    /* Utilizadores com uma idade superior a "age" */
    List<User> getUsersOlderThan(int age) {
        List<User> list = new ArrayList<>();
        for (User user: users) {
            if (user.getAge() > age)
                list.add(user);
        }
        return list;
    }

    /* Raparigas (true) ou rapazes (false) */
    List<User> getUsersByGender(boolean isFemale) {
        List<User> list = new ArrayList<>();
        for (User user: users) {
            if (isFemale && user.getGender() == 'f' ||
                    !isFemale && user.getGender() != 'f')
                list.add(user);
        }
        return list;
    }

    /* Utilizadores em estado online */
    List<User> getUsersOnline() {
        List<User> list = new ArrayList<>();
        for (User user: users) {
            if (user.isOnline())
                list.add(user);
        }
        return list;
    }
}

Neste exemplo, há um padrão que salta à vista. Em todos os 3 métodos, itera-se sobre a lista de utilizadores e filtra-se os elementos que obedecem a uma certa condição, armazenando-os numa lista auxiliar que é retornada no final.

Existe uma repetição óbvia e potencialmente desnecessária - uma violação do princípio DRY (don't repeat yourself) . É uma oportunidade para generalizar um pouco mais.

Para fazer um refactor precisa-se de identificar a parte comum - iteração e coleção de elementos - e a parte variável - condição de teste. Aqui a variável não surge sobre a forma de dados, mas sim, sobre a forma de comportamento.

Em Java, podemos declarar uma interface (ou uma classe abstrata) que representa "uma determinada condição" e que tem apenas um método implementável que serve para testar a condição propriamente dita. Assim, podemos transformar os três métodos acima num único método:

class Chatserver {
    private List<User> users;
    (...)

    interface Condition {
        boolean test(User user);
    }

    List<User> filterUsers(Condition condition) {
        List<User> list = new ArrayList<>();
        for (User user: users) {
            if (condition.test(user))
                list.add(user);
        }
        return list;
    }
}

Para implementar as três diferentes condições, podemos derivar 3 classes de Condition, ou então, declarar classes anónimas. A seguir segue-se um exemplo de como retirar os "utilizadores com idade acima de 17 anos" utilizando uma classe anónima:

Condition over17 = new Condition() {
    public boolean test(User user) {
        return user.getAge() > 17;
    }
};
List<User> users = cs.filterUsers(over17);

Isto é uma forma extremamente horrível de fornecer comportamento novo para uma função - é preciso declarar uma classe inteira só para especificar um bitaitezinho. É aqui que os lambdas (ou funções anónimas) geralmente brilham.

A sintaxe de um lambda é:

(argumentos) -> {corpo da função}

Então, podíamos resumir o caso de cima (users > 17 anos) da seguinte forma:

users = cs.filterUsers(
    (User user) -> { return user.getAge() > 17; }
)

Pode-se omitir o tipo dos argumentos de entrada assim como os parêntesis, fincando apenas:

users = cs.filterUsers(
    user -> { return user.getAge() > 17; }
)

Dado que só existe um statement return no corpo da função, podemos omitir o return e os sinais de pontuação envolventes:

users = cs.filterUsers(user -> user.getAge() > 17)

How sweet!

Para as pessoas mais atentas, devem estranhar aqui alguma coisa. O argumento para filterUsers não devia ser do tipo Condition? Como raio foi implementado o método boolean test(User u)?

Como devem saber, Java tem de agradar o enterprise e tem de ser tudo backwards compatible e, é por essa razão, que isto foi tudo pensado com cuidado e os lambdas foram construídos com base na ideia já existente de classes e interfaces. Quando declaramos um lambda, precisamos de uma interface funcional associada. E o que é isso de interface funcional?

Interface funcional é um nome pomposo para designar uma interface com apenas um método. Ao declararmos um lambda, estamos implicitamente a implementar o único método descrito nessa interface. That's it.

No código visto anteriormente,

Condition over17 = user -> user.getAge() > 17

equivale a:

Condition over17 = new Condition() {
    public boolean test(User user) {
        return user.getAge() > 17;
    }
};

No lambda, test é implementado porque é o único método de Condition. É por essa razão que também não precisamos de indicar os tipos dos argumentos.

Para obtermos os utilizadores online, poderíamos fazer:

users = cs.filterUsers(user -> user.isOnline())

como isOnline já é uma condição de teste por si mesmo, é possível utilizar o novo operador :: e associar a interface funcional ao método User.isOnline diretamente, ficando simplesmente:

users = cs.filterUsers(User::isOnline)

Existem já diversas interfaces funcionais declaradas em: import java.util.function.*. Neste caso, era possível trocar a interface Condition por Predicate<User>, já declarado nas bibliotecas padrão.

Para quem ainda não ficou convencido, vou resolver o clássico exercício da função derivada. A ideia é a seguinte:

definir um método que dada uma qualquer função matemática, devolva a sua derivada.

Primeiro define-se o conceito de função matemática, e eu como horrível matemático que sou, digo que é algo que recebe um número real e devolve outro número real.

Ou seja,

interface Function {
    double apply(double x);
}

Assim, definir a derivada da função é trivial com lambdas:

static Function derivative(Function f, double dx) {
    return x -> (f.apply(x + dx) - f.apply(x)) / dx;
}

dx é um infinitesimal, um número suficientemente pequeno para que os resultados sejam bons para uma dada precisão.

Um programa exemplo que usa este método:

public static void main(String[] args) {
    Function f = x -> 3*x*x + 2*x + 4;  // f(x) = 3x²+2x+4
    Function g = derivative(f, 0.001);  // f'(x) = 6x+2
    Function h = derivative(g, 0.001);  // f''(x) = 6

    double[] list = {0.0, 1.0, 2.0, 3.0, 4.0};
    for (double x: list) {
        System.out.println(String.format(
        "x=%5.2f  f(x)=%5.2f  f'(x)=%5.2f  f''(x)=%5.2f",
        x, f.apply(x), g.apply(x), h.apply(x)));
    }       
}

Output:

x= 0.00  f(x)= 4.00  f'(x)= 2.00  f''(x)= 6.00
x= 1.00  f(x)= 9.00  f'(x)= 8.00  f''(x)= 6.00
x= 2.00  f(x)=20.00  f'(x)=14.00  f''(x)= 6.00
x= 3.00  f(x)=37.00  f'(x)=20.00  f''(x)= 6.00
x= 4.00  f(x)=60.00  f'(x)=26.00  f''(x)= 6.00

Para fins de comparação, deixo a tarefa de resolver isto sem lambdas para mais corajosos ;)

No próximo post, vou falar sobre streams, que em conjunto com os lambdas, introduzem o paradigma funcional ao mundo do Java.