Recursos do Java 8 com Exemplos

O Java 8 foi lançado em 18 de março de 2014. Isso foi há muito tempo, mas ainda muitos projetos estão sendo executados no Java 8. Isso ocorre porque foi um lançamento importante com muitos recursos novos. Vamos dar uma olhada em todos os recursos emocionantes e principais do Java 8 com exemplos de código.

Visão geral rápida dos recursos do Java 8

Alguns dos recursos importantes do Java 8 são;

  1. método forEach() na interface Iterable
  2. métodos default e estáticos em Interfaces
  3. Interfaces Funcionais e Expressões Lambda
  4. API de Stream do Java para Operações de Dados em Massa em Coleções
  5. API de Tempo do Java
  6. melhorias na API de Coleções
  7. melhorias na API de Concorrência
  8. melhorias na API de E/S do Java

Vamos dar uma breve olhada nesses recursos do Java 8. Vou fornecer alguns trechos de código para entender melhor os recursos de maneira simples.

1. Método forEach() na interface Iterable

Sempre que precisamos percorrer uma coleção, precisamos criar um Iterador cujo único propósito é iterar sobre ela, e então temos lógica de negócio em um loop para cada um dos elementos na coleção. Podemos receber uma ConcurrentModificationException se o iterador não for usado corretamente.

O Java 8 introduziu o método forEach na interface java.lang.Iterable para que, ao escrevermos código, foquemos na lógica de negócio. O método forEach recebe um objeto java.util.function.Consumer como argumento, então ajuda a ter nossa lógica de negócio em um local separado que podemos reutilizar. Vamos ver o uso do forEach com um exemplo simples.

package com.journaldev.java8.foreach;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.function.Consumer;
import java.lang.Integer;

public class Java8ForEachExample {

	public static void main(String[] args) {
		
		// criando uma coleção de exemplo
		List<Integer> myList = new ArrayList<Integer>();
		for(int i=0; i<10; i++) myList.add(i);
		
		// percorrendo usando um Iterador
		Iterator<Integer> it = myList.iterator();
		while(it.hasNext()){
			Integer i = it.next();
			System.out.println("Iterator Value::"+i);
		}
		
		// percorrendo através do método forEach de Iterable com uma classe anônima
		myList.forEach(new Consumer<Integer>() {

			public void accept(Integer t) {
				System.out.println("forEach anonymous class Value::"+t);
			}

		});
		
		// percorrendo com a implementação da interface Consumer
		MyConsumer action = new MyConsumer();
		myList.forEach(action);
		
	}

}

// implementação de Consumer que pode ser reutilizada
class MyConsumer implements Consumer<Integer>{

	public void accept(Integer t) {
		System.out.println("Consumer impl Value::"+t);
	}
}

O número de linhas pode aumentar, mas o método forEach ajuda a ter a lógica para iteração e a lógica de negócio em lugares separados, resultando em uma maior separação de preocupações e código mais limpo.

2. Métodos padrão e estáticos em Interfaces

Se você ler detalhadamente os detalhes do método forEach, notará que ele está definido na interface Iterable, mas sabemos que interfaces não podem ter um corpo de método. A partir do Java 8, as interfaces foram aprimoradas para ter um método com implementação. Podemos usar a palavra-chave default e static para criar interfaces com implementação de método. A implementação do método forEach na interface Iterable é:

default void forEach(Consumer<? super T> action) {
    Objects.requireNonNull(action);
    for (T t : this) {
        action.accept(t);
    }
}

Sabemos que o Java não fornece herança múltipla em classes porque isso leva ao Problema do Diamante. Então, como isso será tratado com interfaces agora, já que as interfaces agora são semelhantes às classes abstratas?

A solução é que o compilador lançará uma exceção nesse cenário e teremos que fornecer a lógica de implementação na classe que implementa as interfaces.

package com.journaldev.java8.defaultmethod;

@FunctionalInterface
public interface Interface1 {

	void method1(String str);
	
	default void log(String str){
		System.out.println("I1 logging::"+str);
	}
	
	static void print(String str){
		System.out.println("Printing "+str);
	}
	
	// tentar substituir o método Object gera erro de compilação como
	// "Um método padrão não pode substituir um método de java.lang.Object"
	
// default String toString(){
// return "i1";
// }
	
}
package com.journaldev.java8.defaultmethod;

@FunctionalInterface
public interface Interface2 {

	void method2();
	
	default void log(String str){
		System.out.println("I2 logging::"+str);
	}

}

Observe que ambas as interfaces têm um método comum log() com lógica de implementação.

package com.journaldev.java8.defaultmethod;

public class MyClass implements Interface1, Interface2 {

	@Override
	public void method2() {
	}

	@Override
	public void method1(String str) {
	}

	//MyClass não vai compilar sem ter sua própria implementação log()
	@Override
	public void log(String str){
		System.out.println("MyClass logging::"+str);
		Interface1.print("abc");
	}
	
}

Como você pode ver, Interface1 tem uma implementação de método estático que é usada no método MyClass.log(). O Java 8 usa métodos default e estáticos extensivamente na API de Coleções e métodos padrão são adicionados para que nosso código permaneça compatível com versões anteriores.

Se alguma classe na hierarquia tiver um método com a mesma assinatura, então os métodos padrão se tornam irrelevantes. Object é a classe base, então se tivermos métodos padrão equals(), hashCode() na interface, eles se tornarão irrelevantes. É por isso que, para maior clareza, não é permitido que as interfaces tenham métodos padrão de Object.

Para detalhes completos das mudanças de interface no Java 8, por favor, leia Mudanças de interface do Java 8.

3. Interfaces Funcionais e Expressões Lambda

Se você observar o código de interface acima, você notará a anotação @FunctionalInterface. As interfaces funcionais são um novo conceito introduzido no Java 8. Uma interface com exatamente um método abstrato torna-se uma Interface Funcional. Não precisamos usar a anotação @FunctionalInterface para marcar uma interface como uma Interface Funcional. A anotação

@FunctionalInterface é uma facilidade para evitar a adição acidental de métodos abstratos nas interfaces funcionais. Você pode pensar nela como a anotação @Override e é uma prática recomendada utilizá-la. java.lang.Runnable com um único método abstrato run() é um ótimo exemplo de uma interface funcional.

Um dos principais benefícios da interface funcional é a possibilidade de usar expressões lambda para instanciá-las. Podemos instanciar uma interface com uma classe anônima, mas o código fica volumoso.

Runnable r = new Runnable(){
			@Override
			public void run() {
				System.out.println("My Runnable");
			}};

Uma vez que as interfaces funcionais têm apenas um método, as expressões lambda podem fornecer facilmente a implementação do método. Só precisamos fornecer argumentos do método e lógica de negócios. Por exemplo, podemos escrever a implementação acima usando uma expressão lambda como:

Runnable r1 = () -> {
			System.out.println("My Runnable");
		};

Se tivermos apenas uma declaração no método de implementação, também não precisamos de chaves. Por exemplo, a classe anônima Interface1 acima pode ser instanciada usando lambda da seguinte forma:

Interface1 i1 = (s) -> System.out.println(s);
		
i1.method1("abc");

Portanto, expressões lambda são um meio de criar classes anônimas de interfaces funcionais facilmente. Não há benefícios em tempo de execução ao usar expressões lambda, então eu as usarei com cautela porque não me importo de escrever algumas linhas extras de código.

A new package java.util.function has been added with bunch of functional interfaces to provide target types for lambda expressions and method references. Lambda expressions are a huge topic, I will write a separate article on that in the future.

Você pode ler o tutorial completo em Tutorial de Expressões Lambda do Java 8.

4. API de Fluxo Java para Operações em Massa em Coleções

A new java.util.stream has been added in Java 8 to perform filter/map/reduce like operations with the collection. Stream API will allow sequential as well as parallel execution. This is one of the best features for me because I work a lot with Collections and usually with Big Data, we need to filter out them based on some conditions.

A interface Collection foi estendida com os métodos padrão stream() e parallelStream() para obter o Fluxo para execução sequencial e paralela. Vamos ver o uso deles com um exemplo simples.

package com.journaldev.java8.stream;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

public class StreamExample {

	public static void main(String[] args) {
		
		List<Integer> myList = new ArrayList<>();
		for(int i=0; i<100; i++) myList.add(i);
		
		//fluxo sequencial
		Stream<Integer> sequentialStream = myList.stream();
		
		//fluxo paralelo
		Stream<Integer> parallelStream = myList.parallelStream();
		
		//usando lambda com a API de Fluxo, exemplo de filtro
		Stream<Integer> highNums = parallelStream.filter(p -> p > 90);
		//usando lambda em forEach
		highNums.forEach(p -> System.out.println("High Nums parallel="+p));
		
		Stream<Integer> highNumsSeq = sequentialStream.filter(p -> p > 90);
		highNumsSeq.forEach(p -> System.out.println("High Nums sequential="+p));

	}

}

Se você executar o código de exemplo acima, obterá uma saída como esta:

High Nums parallel=91
High Nums parallel=96
High Nums parallel=93
High Nums parallel=98
High Nums parallel=94
High Nums parallel=95
High Nums parallel=97
High Nums parallel=92
High Nums parallel=99
High Nums sequential=91
High Nums sequential=92
High Nums sequential=93
High Nums sequential=94
High Nums sequential=95
High Nums sequential=96
High Nums sequential=97
High Nums sequential=98
High Nums sequential=99

Observe que os valores de processamento paralelo não estão em ordem, então o processamento paralelo será muito útil ao trabalhar com coleções enormes.

Não é possível cobrir tudo sobre a API de Stream neste post, você pode ler tudo sobre a API de Stream em Tutorial de Exemplo da API de Stream do Java 8.

5. API de Tempo do Java

Sempre foi difícil trabalhar com Data, Hora e Fusos Horários em java. Não havia uma abordagem ou API padrão em java para data e hora em Java. Uma das boas adições no Java 8 é o pacote java.time que vai simplificar o processo de trabalhar com tempo em java.

Apenas olhando para os pacotes da API de Tempo do Java, posso perceber que eles serão muito fáceis de usar. Ele tem alguns subpacotes java.time.format que fornecem classes para imprimir e analisar datas e horas e java.time.zone fornece suporte para fusos horários e suas regras.

A nova API de Tempo prefere enums em vez de constantes inteiras para meses e dias da semana. Uma das classes úteis é DateTimeFormatter para converter objetos DateTime em strings. Para um tutorial completo, vá para Tutorial de Exemplo da API de Data e Hora do Java.

6. Melhorias na API de Coleções

Já vimos o método forEach() e a API Stream para coleções. Alguns novos métodos adicionados na API de Coleções são:

  • Iterator método padrão forEachRemaining(Consumer action) para realizar a ação fornecida para cada elemento restante até que todos os elementos tenham sido processados ou a ação lance uma exceção.
  • Collection método padrão removeIf(Predicate filter) para remover todos os elementos desta coleção que satisfaçam o predicado dado.
  • Collection método spliterator() retornando uma instância Spliterator que pode ser usada para percorrer elementos sequencial ou paralelamente.
  • Map métodos replaceAll(), compute(), merge().
  • Melhoria de Desempenho para a classe HashMap com Colisões de Chaves

7. Melhorias na API de Concorrência

Algumas melhorias importantes na API concorrente são:

  • ConcurrentHashMap métodos compute(), forEach(), forEachEntry(), forEachKey(), forEachValue(), merge(), reduce() e search().
  • CompletableFuture que pode ser explicitamente completado (definindo seu valor e status).
  • Executors método newWorkStealingPool() para criar um pool de threads “work-stealing” usando todos os processadores disponíveis como seu nível de paralelismo alvo.

8. Melhorias no Java IO

Algumas melhorias de IO que conheço são:

  • Files.list(Path dir) que retorna um Stream preenchido de forma preguiçosa, cujos elementos são as entradas no diretório.
  • Files.lines(Path path) que lê todas as linhas de um arquivo como um Stream.
  • Files.find() que retorna um Stream preenchido de forma preguiçosa com Path ao procurar arquivos em uma árvore de arquivos enraizada em um determinado arquivo inicial.
  • BufferedReader.lines() que retorna um Stream, cujos elementos são linhas lidas deste BufferedReader.

Melhorias Diversas na API Principal do Java 8

Algumas melhorias diversas na API que podem ser úteis são:

  1. ThreadLocal método estático withInitial(Supplier supplier) para criar instâncias facilmente.
  2. A interface Comparator foi estendida com muitos métodos padrão e estáticos para ordenação natural, ordem reversa, etc.
  3. Métodos min(), max() e sum() nas classes de envoltório Integer, Long e Double.
  4. Métodos logicalAnd(), logicalOr() e logicalXor() na classe Boolean.
  5. Método stream() de ZipFile para obter um Stream ordenado sobre as entradas do arquivo ZIP. As entradas aparecem no Stream na ordem em que aparecem no diretório central do arquivo ZIP.
  6. Vários métodos utilitários na classe Math.
  7. O comando jjs foi adicionado para invocar o Motor Nashorn.
  8. O comando jdeps foi adicionado para analisar arquivos de classe.
  9. A Ponte JDBC-ODBC foi removida.
  10. O espaço de memória PermGen foi removido

Isso é tudo para as características do Java 8 com programas de exemplo. Se eu perdi algumas características importantes do Java 8, por favor me avise através de comentários.

Source:
https://www.digitalocean.com/community/tutorials/java-8-features-with-examples