Usando Java 8 no Android

Java 8 foi oficialmente lançado em 2014, trazendo uma série de funcionalidades aguardadas por muito tempo pela comunidade, como Lambdas e uma nova API para lidar com tipos de data e hora.

Porém, nós meros devs do robozinho verde ficamos de fora disso, já que Android possui uma versão modificada da JVM. Na realidade, não temos suporte completo oficial nem para Java 7. Mas como a comunidade não tem muita paciência, diversas bibliotecas para solucionar o problema começaram a pipocar, cada uma trazendo o backport de alguma funcionalidade específica.

Bem mais recentemente, junto com o anúncio do Android N ano passado, também foi anunciada uma nova toolchain para compilar código java para .dex: Jack & Jill. Esse novo compilador dá acesso a algumas das funcionalidades do Java 8, sendo o primeiro tipo de suporte oficial que temos.

Porém, sair caçando biblioteca nunca é divertido. Nesse artigo vamos dar uma olhada em algumas das funcionalidades novas trazidas no Java 8, e quais os projetos nos permitem usá-las.

TL; DR

A tabela a seguir mostra as funcionalidades novas do Java 8 e as bibliotecas que dão suporte:


Java 8

O primeiro passo para trazer o Java 8 pro Android é saber o que do Java 8 que queremos.

Você pode ver a lista completa de mudanças, mas eu só vou analisar algumas alterações que julgo mais importantes: Streams, Funções Lambda, Method References, Métodos Default para interfaces, Time API e try-with-resources (que na verdade é do Java 7 mas não está disponível no Android também). Se deixei alguma de lado que você acha fundamental, avise nos comentários.

É importante dizer que uma nova funcionalidade pode ser inserida numa linguagem de duas formas: ela pode ser apenas um conjunto de funções novas, ou seja, apenas uma API nova, ou ela pode ser uma modificação na sintaxe da linguagem.

A primeira é muito mais fácil de se disponibilizar para versões antigas, basta separar em um jar ou uma biblioteca e adicionar como dependência. Das funcionalidades que iremos ver, apenas as APIs de Stream e Time fazem parte desse primeiro tipo.

Já o segundo tipo, uma mudança na sintaxe, é bem mais complicado: é necessário um tradutor que lê o código (ou bytecode) Java 8 e transforma ele em código Java 7.

Time API

Que atire a primeira pedra o programador Java que nunca passou dor de cabeça lidando com Calendars e Dates. A API é extremamente confusa, mesmo para realizar coisas simples, especialmente por que sempre precisamos lidar com fuso horário.

Em um app que trabalhei precisava de um loop que iterava dia por dia, adicionando 24 horas para achar o próximo. Estranhamente, o loop sempre repetia uma data. Passei horas no problema, e só fui conseguir resolver quando ouvi aleatoriamente meu pai lembrando minha mãe de mudar o relógio pro horário de verão: a data que repetia era justamente o dia que o horário mudava. Como eu considerava a hora sendo meia noite, ele avançava as 24h mas o o horário avançava 1h também, fazendo com que o dia se repetisse.

Por causa de problemas como esse, muitos devs adotaram a biblioteca Joda-Time, que traz uma API mais simples. Porém, o próprio criador da biblioteca percebeu que havia algumas falhas no design, e criou uma nova especificação para uma API de tempo, conhecida como JSR-310. Essa especificação se tornou a java.time, uma API muito mais fácil de trabalhar que, entre outras coisas, permite que o dev desconsidere totalmente o fuso horário.

Para dar um gostinho, se em Java 7 tivéssemos que adicionar 3 dias a uma data, faríamos da seguinte forma:

// Java7
Date date, datePlusThreeDays;
date = new GregorianCalendar(2014, Calendar.FEBRUARY, 11).getTime()
Calendar c = Calendar.getInstance();
c.setTime(date);
c.add(Calendar.DATE, 3)
datePlusThreeDays = c.getTime()

Já em Java 8:

// Java 8
LocalDate otherDate, otherDatePlusThreeDays;
otherDate = LocalDate.of(2014, Month.FEBRUARY, 11);
otherDatePlusThreeDays = otherDate.plus(3, ChronoUnit.DAYS);

A versão Java 8 não só é mais curta, como é muito mais intuitiva.

Funções Lambda

Java sempre foi uma linguagem Orientada a Objetos, por isso funções foram deixadas de lado. Para passar um parâmetro como comportamento, fazemos o uso de Functors.

Functors são abstrações de funções em forma de objetos. Normalmente, seria uma interface com um único método, e criaríamos uma implementação anônima desse objeto, onde um exemplo clássico seria o Runnable. Outro exemplo muito utilizado de Functor (embora não percebamos), seria os listeners:

v.setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View view) {
Log.d(TAG, "onClick: ");
}
});

Percebe como na implementação do objeto existe um monte de código que atrapalha a leitura? Comparado com linguagens modernas, essa é um dos maiores contribuintes para o problema da verbosidade do Java, especialmente para quem usa RxJava e precisa implementar vários Functors em sequência:

Observable.from(people)
.filter(new Func1<Person, Boolean>() {
@Override
public Boolean call(Person person) {
return person.age > 16;
}
})
.map(new Func1<Person, String>() {
@Override
public String call(Person person) {
return person.name;
}
})
.subscribe(new Action1<String>() {
@Override
public void call(String s) {
System.out.println(s);
}
});

Péssimo de ler não? O que diabos é Func1 , Action1, Action0? Eu não deveria precisar saber disso.

Funções Lambda resolvem esse problema. É simplesmente uma forma mais sucinta de escrever implementações de interfaces com apenas um método. O exemplo do listener viraria

v.setOnClickListener(view -> Log.d(TAG, "onClick: "));

e o de RxJava viraria

Observable.from(people)
.filter(person -> person.age > 16)
.map(person -> person.name)
.subscribe(s -> System.out.println(s));

MUITO mais fácil de ler e acompanhar o código sem se perder. Agora, é importante lembrar que, ao contrário de linguagens funcionais, Funções Lambda não são objetos de primeira classe, é simplesmente uma sintaxe melhorada. Internamente, a lambda ainda é convertida para o objeto anônimo.

Method Reference

Assim como Funções Lambda, Method References são uma melhoria de sintaxe para diminuir verbosidade. É uma abreviação de lambdas onde o argumento é repassado para outra função.

No subscribe do exemplo de RxJava acima, a String s é repassada para o método println() do objeto System.out. Portanto, Method References nos permite escrever da seguinte forma:

Observable.from(people)
.filter(person -> person.age > 16)
.map(person -> person.name)
.subscribe(System.out::println);
// .subscribe(s -> System.out.println(s));

Try-With-Resources

Outra melhoria de sintaxe, mas que foi trazida no Java 7 (e mesmo assim Android não dá suporte). Sabe aquele momento que você tem que usar um BufferedReader, tendo de um try catch e depois fechar dentro de um finally? Algo do gênero:

// Java 7
BufferedReader br = new BufferedReader(new FileReader(path));
try {
System.out.println(br.readLine());
} finally {
if (br != null) br.close();
}

Um monte de baboseira pra ler uma linha certo? Com a sintaxe introduzida no Java 7 teríamos:

try (BufferedReader br = new BufferedReader(new FileReader(path))) {
System.out.println(br.readLine());
}

Novamente, não é algo que vai mudar sua vida como desenvolvedor, mas melhora bastante a leitura. Você pode usar seus próprios recursos com essa sintaxe, a única obrigação é que ele precisa implementar a interface Closeable.

Streams

Outra coisa que foi sempre chato em Java é operar sobre coleções. Por exemplo, digamos que eu tenha uma lista de pessoas e eu quero os nomes das duas primeiras com idade maior que 16 anos. Se não tiver duas com idade maior que a 16, quero que retorne 0 ou 1. Em Java 7 teríamos algo como:

names = new ArrayList<>();
for (Person p : people) { // itera sobre as pessoas
if(p.age > 16) { // verifica
names.add(p.name); // adiciona na lista de nomes
if (names.size() >= 2) // retorna quando achar 2
break;
}
}
return names;

Percebe a quantidade de blocos existentes nesse pequeno pedaço de código? Com a nova API de Stream, o mesmo resultado seria obtido da seguinte forma:

return people.stream()
.filter(p -> p.age > 16)
.map(p -> p.name)
.limit(2)
.collect(Collectors.toList());

O número de linhas não alterou muito, mas a nova versão é muito mais legível, ainda mais com o uso de Lambdas. O método .stream() faz parte da API de collections, então qualquer coleção pode ser usada dessa forma.

Mas usar Streams não serve só para deixar o código mais bonitinho. Stream é uma ferramenta extremamente poderosa para melhorar o desempenho do sistema, de duas formas:

  1. Lazy Evaluation

Uma stream, ao contrário de uma lista, não é avaliada até que algum valor seja requisitado. Por exemplo, se eu tivesse escrito apenas

people.stream()
.filter(p -> p.age > 16)
.map(p -> p.name)
.limit(2)

nenhuma das três Streams teria sido realmente executada. Ela só é avaliada ao usar algum collector, ou quando uso métodos como max, min, count e outras, que são chamadas de terminal operations.

Ao não avaliar uma Stream, a API pode então alterar a estratégia de execução para evitar execuções desnecessárias. Por exemplo: numa lista de 100 elementos, os 2 primeiros tem idade maior que 16. Como a stream ainda não foi executada, e ela sabe que só precisa de 2 elementos, assim que encontrar o segundo não vai aplicar nem o filter nem o map para o resto da lista.

2. Paralelismo

Observe o método map do exemplo. Ele depende só do item que está sendo iterado, então em vez de eu operar sobre um item por vez eu poderia mandar um item para cada thread, e o operador map cuidaria de todo a questão de sincronismo e barreira.

Muitos operadores fazem uso dessa vantagem (como o filter também), o que PODE trazer vantagens. Eu digo pode por que não é sempre que paralelismo traz benefícios, isso depende de diversos fatores como número de threads disponíveis, do tamanho da coleção, da velocidade de comunicação entre threads e outros.

Para habilitar execução paralela, basta chamar o método .parallelStream() em vez de .stream() na coleção.

Métodos Default em Interfaces

Em Java 8, pode-se declarar implementações de métodos em interfaces, da seguinte forma:

interface Veiculo {
default void print(){
System.out.println("Sou um veículo");
}
}
class Carro implements Veiculo {
public void print(){
Vehicle.super.print();
System.out.println("Sou um carro");
}
}
class Barco implements Veiculo {
}

Em Java 7, a classe Barco lançaria um erro, pois não está implementando o método print(). Porém, como estamos usando Java 8, a interface Veiculo provê uma implementação default para o método. Se chamássemos o método print() de um barco veríamos na tela "Sou um veículo".

Essa funcionalidade foi inserida para tornar o método .stream() e .parallelStream() possível em coleções. Imagine que o time do Java adicione um método em uma interface amplamente implementada por desenvolvedores. Todo mundo que fizesse o update e tivesse uma implementação da classe Collection teria um código quebrado até que implementasse os métodos .stream() e parallelStream(). Para evitar isso, esses métodos são default da classe Collection.


Jack & Jill

Como já mencionei, logo após o anúncio do Android N ano passado, também foi anunciada uma nova toolchain para transformar código Java em .dex: Jack & Jill.

Atualmente, para gerarmos um .dex primeiro compilamos o código Java para .class através do javac e depois transformamos para .dex usando a ferramenta dx.

O Jack opera de forma diferente: ele compila diretamente o código Java para .dex, pulando o passo intermediário. Isso pode trazer vantagens, mas no cenário atual traz um grande problema: muitas bibliotecas que usamos (dagger2, autovalue, retrofit, etc), geram código em tempo de compilação fazendo uso de anotações. Como mudou a forma que o código é compilado, muitas delas ainda não estão preparadas para o Jack, fazendo com que elas funcionem de forma inesperada.

Apesar disso, essa toolchain já traz uma tentativa de suporte oficial do Java 8 para o Android, pelo menos de algumas funcionalidades.

Primeiramente, Jack dá suporte completo e retrocompatível para todas as versões do Android para Funções Lambda e Method References. Isso significa que eu poderia escrever lambdas livremente e meu código rodaria em qualquer API do Android.

Em contrapartida, ele só da suporte para Métodos Default para API's do Android a partir da versão 24 (N) e, consequentemente, Streams só ficam disponíveis também para essa versão, não sendo retrocompatíveis.

Infelizmente, o Jack ainda não tornaria disponível a nova Time API, nem a sintaxe de Try-With-Resources.

Não entrarei em detalhes sobre como ativar o Jack, mas se você quiser testar ele no seu projeto basta seguir as instruções encontradas aqui.


Bibliotecas da Comunidade

Antes do Jack, não existia nenhuma forma de suporte oficial ao Java 8 no Android e, mesmo agora, é arriscado usar algo que ainda está em fase experimental.

Por isso, a comunidade trouxe alternativas para poder usar as novas features e parar de escrever tanto código. Normalmente, cada uma das bibliotecas traz alguma funcionalidade diferente, e mais de uma biblioteca pode trazer a mesma funcionalidade. Daremos uma visão geral delas, mas sem comparar performance, apenas sintaxe e pontos fortes e fracos da empregabilidade delas.

Streams

Comecemos por Streams. Como falado, Jack só dá suporte a essa funcionalidade a partir da API 24+, o que faz necessário ter uma biblioteca pra isso mesmo que você use o novo compilador (a não ser que você queira desenvolver focando apenas nos 0.1% de usuários que já tem essa versão).

Existem duas opções bem usadas pela comunidade: LightweightStreams e RxJava. Para comparar a sintaxe, usaremos o exemplo anterior:

// Stream API
people.stream()
.filter(p -> p.age > 16)
.map(p -> p.name)
.limit(2)
.collect(Collectors.toList());

Essa biblioteca é uma tentativa de implementar a API de streams da forma mais fiel possível, porém usando Collections normais. Isso é bastante interessante por que caso a API oficial ficasse disponível, seria tranquilo refatorar o código para remover essa dependência. O exemplo seria escrito da seguinte forma:

// LightweightStreams
Stream.of(people)
.filter(p -> p.age > 16)
.map(p -> p.name)
.limit(2)
.collect(Collectors.toList());

As duas sintaxes são bem parecidas, apenas a forma como a stream é criada muda. Essa biblioteca ainda trás algumas melhorias e métodos diferentes, mas isso quebraria compatibilidade com a API oficial. Além disso, é uma biblioteca relativamente pequena, com 719 métodos.

  • RxJava

Ok ok, eu sei, RxJava e Streams são fundamentalmente diferentes, possuem um modo de funcionamento beeeem diferente uma da outra, mas RxJava pode ser tranquilamente um substituto usando Observable.from(myCollection). Eu comecei a usar RxJava justamente para facilitar lidar com coleções, só fui começar a brincar com arquitetura reativa muito recentemente. De forma simples, RxJava faz tudo o que Streams faz, mas Streams fazem muito menos do que RxJava faz.

Em RxJava, já teríamos um código um pouco diferente para o exemplo acima, mas mesmo assim ainda é bem parecido.

Observable.from(people)
.filter(p -> p.age > 16)
.map(p -> p.name)
.take(2)
.toList().toBlocking().first();

RxJava já é uma biblioteca grande, com 4605 métodos na versão 1.1.10 e 8282 na versão 2.0.0-RC2. Porém, muitos de nós já temos RxJava na lista de dependências, então por que não usar?

Embora a versão 1.1.x não traga qualquer tipo de compatibilidade com Streams, um dos objetivos da versão 2.x é ser compatível com ReactiveStreams, uma API que está prevista para o Java 9. Ou seja, não só RxJava traz Streams do Java 8, já traz um pouco da funcionalidade do Java 9 também.


Time API

O próprio criador da JSR-310 criou uma implementação da API para Java 5, 6 e 7, chamada ThreeTenBP, que é totalmente compatível com a API de tempo do Java 8. Só tem um probleminha: assim como Joda-Time (também dele), informações sobre localização são carregados a partir de um jar, que é algo extremamente lento e ineficiente em Android.

Nisso entrou nosso querido Jake Wharton e otimizou a biblioteca para Android, criando a biblio ThreeTenABP, que simplesmente melhora essa parte do carregamento de localização usando Android Resources em vez de um jar.

Isso significa que, assim como LightweightStreams, se por algum motivo a Time API oficial vier dar as caras no Android, vai ser extremamente simples refatorar o código para remover a dependência.

Em contrapartida, a biblioteca não é das menores (3265 métodos). Além disso, os componentes do Android usam o jeito clássico para lidar com tempo, então é necessário ficar convertendo de um para outro.

Lambda, Method References e Try-With-Resources

Essas três funcionalidades fazem parte do tipo mais difícil de dar suporte, pois são mudanças na própria linguagem. Felizmente, elas já estão disponíveis há um bom tempo pela biblioteca Retrolambda, que pode ser usado no Android com um plugin para gradle. Segundo o repo da biblioteca (tradução livre):

Retrolambda permite rodar código Java 8 com expressões lambda, method references e a sintaxe try-with-resources em Java's 7, 6 ou 5. Ela faz isso transformando o bytecode compilado de Java 8 de forma que possa rodar em um runtime mais antigo do Java. Depois da transformação, temos apenas arquivos .class normais, sem nenhuma adição de dependências.
Existe também suporte limitado para métodos default e estáticos em interfaces, porém é desabilitado por default.

Ou seja, você consegue ter as funcionalidades tem adicionar nenhum método a mais na contagem, pois tudo acontece em tempo de compilação. O único porém é que, assim como Jack, para dar retrocompatibilidade as expressões lambda e method references são substituídos por objetos anônimos, que são compostos por 3 ou 4 métodos. A tabela a seguir, copiada dessa apresentação, mostra a comparação entre o número de métodos criados:

Comparação entre número de métodos gerados por lambdas e method references, por Jake Wharton.

Retrolambda já é um projeto bem maduro para se usar em produção, e caso ocorra algum problema ele será acusado em tempo de compilação.

Resumo

A tabela a seguir mostra um resumo de tudo o que eu falei aqui. Mostra a relação entre as funcionalidades aqui apresentadas e as bibliotecas que implementam elas. Na tabela, RL é Retrolambda, LS é LightWeight Streams e TT é ThreeTenABP.

Era isso que eu tinha para trazer para os senhores no dia de hoje. Qualquer dúvida por favor deixe um comentário ou me procure no slack da comunidade :D.