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.