Desafios em Java 8
1. Visão geral
O Java 8 introduziu alguns novos recursos, que giravam principalmente em torno do uso de expressões lambda. Neste artigo rápido, vamos dar uma olhada nas desvantagens de alguns deles.
E, embora esta não seja uma lista completa, é uma coleção subjetiva das reclamações mais comuns e populares sobre os novos recursos do Java 8.
2. Java 8 Stream and Thread Pool
Antes de tudo, os fluxos paralelos destinam-se a facilitar o processamento paralelo de seqüências, e isso funciona bem em cenários simples.
O Stream usa o padrãoForkJoinPool comum - divide as sequências em pedaços menores e executa operações usando vários threads.
No entanto, há um porém. Não há uma boa maneira despecify which ForkJoinPool to use e, portanto, se um dos threads travar, todos os outros, usando o pool compartilhado, terão que esperar a conclusão das tarefas de longa duração.
Felizmente, há uma solução alternativa para isso:
ForkJoinPool forkJoinPool = new ForkJoinPool(2);
forkJoinPool.submit(() -> /*some parallel stream pipeline */)
.get();
Isso criará um novoForkJoinPool separado e todas as tarefas geradas pelo fluxo paralelo usarão o pool especificado e não o padrão compartilhado.
É importante notar que há outro problema potencial:“this technique of submitting a task to a fork-join pool, to run the parallel stream in that pool is an implementation ‘trick' and is not guaranteed to work”, de acordo com Stuart Marks - desenvolvedor Java e OpenJDK da Oracle. Uma nuance importante a ser lembrada ao usar esta técnica.
3. Diminuição da capacidade de depuração
The new coding style simplifies our source code, yetcan cause headaches while debugging it.
Em primeiro lugar, vejamos este exemplo simples:
public static int getLength(String input) {
if (StringUtils.isEmpty(input) {
throw new IllegalArgumentException();
}
return input.length();
}
List lengths = new ArrayList();
for (String name : Arrays.asList(args)) {
lengths.add(getLength(name));
}
Este é um código Java obrigatório padrão que é autoexplicativo.
Se passarmosString vazio como uma entrada - como resultado - o código lançará uma exceção e, no console de depuração, podemos ver:
at LmbdaMain.getLength(LmbdaMain.java:19)
at LmbdaMain.main(LmbdaMain.java:34)
Agora, vamos reescrever o mesmo código usando a API Stream e ver o que acontece quando umString vazio é passado:
Stream lengths = names.stream()
.map(name -> getLength(name));
A pilha de chamadas terá a seguinte aparência:
at LmbdaMain.getLength(LmbdaMain.java:19)
at LmbdaMain.lambda$0(LmbdaMain.java:37)
at LmbdaMain$$Lambda$1/821270929.apply(Unknown Source)
at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:512)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:502)
at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.util.stream.LongPipeline.reduce(LongPipeline.java:438)
at java.util.stream.LongPipeline.sum(LongPipeline.java:396)
at java.util.stream.ReferencePipeline.count(ReferencePipeline.java:526)
at LmbdaMain.main(LmbdaMain.java:39)
Esse é o preço que pagamos por alavancar várias camadas de abstração em nosso código. No entanto, os IDEs já desenvolveram ferramentas sólidas para depurar o Java Streams.
4. Métodos que retornamNull ouOptional
Optional foi introduzido no Java 8 para fornecer uma forma segura de expressar opcionalidade.
Optional, indica explicitamente que o valor de retorno pode não estar presente. Portanto, chamar um método pode retornar um valor, eOptional é usado para envolver esse valor - o que acabou sendo útil.
Infelizmente, devido à compatibilidade com versões anteriores do Java, algumas vezes terminamos com APIs Java combinando duas convenções diferentes. Na mesma classe, podemos encontrar métodos que retornam nulos, bem como métodos que retornamOptionals.
5. Muitas interfaces funcionais
No pacotejava.util.function, temos uma coleção de tipos de destino para expressões lambda. Podemos distinguir e agrupá-los como:
-
Consumer - representa uma operação que leva alguns argumentos e não retorna nenhum resultado
-
Function - representa uma função que recebe alguns argumentos e produz um resultado
-
Operator - representa uma operação em alguns argumentos de tipo e retorna um resultado do mesmo tipo que os operandos
-
Predicate - representa um predicado (funçãobooleancom valor s) de alguns argumentos
-
Supplier - representa um fornecedor que não aceita argumentos e retorna resultados
Além disso, temos tipos adicionais para trabalhar com primitivos:
-
IntConsumer
-
IntFunction
-
IntPredicate
-
IntSupplier
-
IntToDoubleFunction
-
IntToLongFunction
-
… E mesmas alternativas paraLongs eDoubles
Além disso, tipos especiais para funções com aridade de 2:
-
BiConsumidor
-
BiPredicate
-
BinaryOperator
-
BiFunction
Como resultado, o pacote inteiro contém 44 tipos funcionais, que certamente podem começar a ser confusos.
6. Exceções verificadas e expressões lambda
As exceções verificadas já foram uma questão problemática e controversa antes do Java 8. Desde a chegada do Java 8, surgiu o novo problema.
As exceções verificadas devem ser capturadas imediatamente ou declaradas. Como as interfaces funcionaisjava.util.function não declaram exceções, o código que lança a exceção verificada falhará durante a compilação:
static void writeToFile(Integer integer) throws IOException {
// logic to write to file which throws IOException
}
List integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(i -> writeToFile(i));
Uma maneira de superar esse problema é envolver a exceção verificada em um blocotry-catche relançarRuntimeException:
List integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(i -> {
try {
writeToFile(i);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
Isso vai funcionar. No entanto, lançarRuntimeException contradiz o propósito da exceção verificada e faz com que todo o código seja empacotado com código clichê, que estamos tentando reduzir aproveitando as expressões lambda. Uma das soluções hacky éto rely on the sneaky-throws hack.
Outra solução é escrever uma Interface Funcional do Consumidor - que pode gerar uma exceção:
@FunctionalInterface
public interface ThrowingConsumer {
void accept(T t) throws E;
}
static Consumer throwingConsumerWrapper(
ThrowingConsumer throwingConsumer) {
return i -> {
try {
throwingConsumer.accept(i);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
};
}
Infelizmente, ainda estamos envolvendo a exceção verificada em uma exceção de tempo de execução.
Finalmente, para uma solução e explicação mais detalhadas do problema, podemos explorar o seguinte mergulho profundo:Exceptions in Java 8 Lambda Expressions.
8. Conclusão
Nesta rápida descrição, discutimos algumas das desvantagens do Java 8.
Enquanto alguns deles foram escolhas deliberadas de design feitas por arquitetos da linguagem Java e, em muitos casos, existe uma solução alternativa ou alternativa; precisamos estar cientes de seus possíveis problemas e limitações.