JavaRush /Blogue Java /Random-PT /Guia Java 8. 1 parte.
ramhead
Nível 13

Guia Java 8. 1 parte.

Publicado no grupo Random-PT

"Java ainda está vivo - e as pessoas estão começando a entendê-lo."

Bem-vindo à minha introdução ao Java 8. Este guia o guiará passo a passo por todos os novos recursos da linguagem. Por meio de exemplos de código curtos e simples, você aprenderá como usar métodos padrão de interface , expressões lambda , métodos de referência e anotações repetíveis . Ao final do artigo, você estará familiarizado com as alterações mais recentes em APIs, como streams, interfaces de função, extensões de associação e a nova API de data. Sem paredes de texto chato - apenas um monte de trechos de código comentados. Aproveitar!

Métodos padrão para interfaces

Java 8 nos permite adicionar métodos não abstratos implementados na interface através do uso da palavra-chave padrão . Esse recurso também é conhecido como métodos de extensão . Aqui está nosso primeiro exemplo: interface Formula { double calculate(int a); default double sqrt(int a) { return Math.sqrt(a); } } Além do método abstrato calcular , a interface Formula também define um método padrão sqrt . As classes que implementam a interface Formula implementam apenas o método abstrato de cálculo . O método sqrt padrão pode ser usado imediatamente. Formula formula = new Formula() { @Override public double calculate(int a) { return sqrt(a * 100); } }; formula.calculate(100); // 100.0 formula.sqrt(16); // 4.0 O objeto fórmula é implementado como um objeto anônimo. O código é bastante impressionante: 6 linhas de código para calcular simplesmente sqrt(a * 100) . Como veremos mais adiante na próxima seção, existe uma maneira mais atraente de implementar objetos de método único em Java 8.

Expressões lambda

Vamos começar com um exemplo simples de como classificar um array de strings nas primeiras versões do Java: O método auxiliar estatístico Collections.sort usa uma lista e um Comparator para classificar os elementos da lista fornecida. O que muitas vezes acontece é que você cria comparadores anônimos e os passa para métodos de classificação. Em vez de criar objetos anônimos o tempo todo, o Java 8 oferece a capacidade de usar muito menos sintaxe e expressões lambda : como você pode ver, o código é muito mais curto e mais fácil de ler. Mas aqui fica ainda mais curto: para um método de uma linha, você pode se livrar das chaves {} e da palavra-chave return . Mas é aqui que o código fica ainda mais curto: o compilador Java conhece os tipos de parâmetros, então você também pode deixá-los de fora. Agora vamos nos aprofundar em como as expressões lambda podem ser usadas na vida real. List names = Arrays.asList("peter", "anna", "mike", "xenia"); Collections.sort(names, new Comparator () { @Override public int compare(String a, String b) { return b.compareTo(a); } }); Collections.sort(names, (String a, String b) -> { return b.compareTo(a); }); Collections.sort(names, (String a, String b) -> b.compareTo(a)); Collections.sort(names, (a, b) -> b.compareTo(a));

Interfaces Funcionais

Como as expressões lambda se encaixam no sistema de tipos Java? Cada lambda corresponde a um determinado tipo definido por uma interface. E a chamada interface funcional deve conter exatamente um método abstrato declarado. Cada expressão lambda de um determinado tipo corresponderá a este método abstrato. Uma vez que os métodos padrão não são métodos abstratos, você é livre para adicionar métodos padrão à sua interface funcional. Podemos usar uma interface arbitrária como expressão lambda, desde que a interface contenha apenas um método abstrato. Para garantir que sua interface atenda a essas condições, você deve adicionar a anotação @FunctionalInterface . O compilador será informado por esta anotação que a interface deve conter apenas um método e se encontrar um segundo método abstrato nesta interface, gerará um erro. Exemplo: Tenha em mente que este código também seria válido mesmo se a anotação @FunctionalInterface não tivesse sido declarada. @FunctionalInterface interface Converter { T convert(F from); } Converter converter = (from) -> Integer.valueOf(from); Integer converted = converter.convert("123"); System.out.println(converted); // 123

Referências a métodos e construtores

O exemplo acima pode ser ainda mais simplificado usando uma referência de método estatístico: Java 8 permite passar referências para métodos e construtores usando a palavra-chave :: símbolos . O exemplo acima mostra como os métodos estatísticos podem ser usados. Mas também podemos fazer referência a métodos em objetos: vamos dar uma olhada em como using :: funciona para construtores. Primeiro, vamos definir um exemplo com diferentes construtores: A seguir, definimos a interface de fábrica PersonFactory para criar novos objetos pessoais : Converter converter = Integer::valueOf; Integer converted = converter.convert("123"); System.out.println(converted); // 123 class Something { String startsWith(String s) { return String.valueOf(s.charAt(0)); } } Something something = new Something(); Converter converter = something::startsWith; String converted = converter.convert("Java"); System.out.println(converted); // "J" class Person { String firstName; String lastName; Person() {} Person(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } } interface PersonFactory

{ P create(String firstName, String lastName); } Em vez de implementar a fábrica manualmente, amarramos tudo usando uma referência de construtor: Criamos uma referência para o construtor da classe Person via Person::new . O compilador Java chamará automaticamente o construtor apropriado comparando a assinatura dos construtores com a assinatura do método PersonFactory.create . PersonFactory personFactory = Person::new; Person person = personFactory.create("Peter", "Parker");

Região lambda

Organizar o acesso a variáveis ​​de escopo externo a partir de expressões lambda é semelhante ao acesso a partir de um objeto anônimo. Você pode acessar variáveis ​​finais do escopo local, bem como campos de instância e variáveis ​​agregadas.
Acessando Variáveis ​​Locais
Podemos ler uma variável local com o modificador final do escopo de uma expressão lambda: Mas, diferentemente dos objetos anônimos, as variáveis ​​não precisam ser declaradas como finais para serem acessíveis a partir de uma expressão lambda . Este código também está correto: no entanto, a variável num deve permanecer imutável, ou seja, ser final implícito para compilação de código. O código a seguir não será compilado: Alterações em num em uma expressão lambda também não são permitidas. final int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); stringConverter.convert(2); // 3 int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); stringConverter.convert(2); // 3 int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); num = 3;
Acessando Campos de Instância e Variáveis ​​Estatísticas
Ao contrário das variáveis ​​locais, podemos ler e modificar campos de instância e variáveis ​​estatísticas dentro de expressões lambda. Conhecemos esse comportamento por meio de objetos anônimos. class Lambda4 { static int outerStaticNum; int outerNum; void testScopes() { Converter stringConverter1 = (from) -> { outerNum = 23; return String.valueOf(from); }; Converter stringConverter2 = (from) -> { outerStaticNum = 72; return String.valueOf(from); }; } }
Acesso a métodos padrão de interfaces
Lembra do exemplo com a instância da fórmula da primeira seção? A interface Formula define um método sqrt padrão que pode ser acessado de qualquer instância de formula , incluindo objetos anônimos. Isso não funciona com expressões lambda. Os métodos padrão não podem ser acessados ​​dentro de expressões lambda. O código a seguir não compila: Formula formula = (a) -> sqrt( a * 100);

Interfaces funcionais integradas

A API JDK 1.8 contém muitas interfaces funcionais integradas. Alguns deles são bem conhecidos de versões anteriores do Java. Por exemplo Comparator ou Runnable . Essas interfaces são estendidas para incluir suporte lambda usando a anotação @FunctionalInterface . Mas a API Java 8 também está repleta de novas interfaces funcionais que facilitarão sua vida. Algumas dessas interfaces são bem conhecidas da biblioteca Guava do Google . Mesmo se você estiver familiarizado com esta biblioteca, você deve observar mais de perto como essas interfaces são estendidas, com alguns métodos de extensão úteis.
Predicados
Predicados são funções booleanas com um argumento. A interface contém vários métodos padrão para criar expressões lógicas complexas (e, ou, negar) usando predicados Predicate predicate = (s) -> s.length() > 0; predicate.test("foo"); // true predicate.negate().test("foo"); // false Predicate nonNull = Objects::nonNull; Predicate isNull = Objects::isNull; Predicate isEmpty = String::isEmpty; Predicate isNotEmpty = isEmpty.negate();
Funções
As funções recebem um argumento e produzem um resultado. Métodos padrão podem ser usados ​​para combinar várias funções em uma cadeia (compor e andThen). Function toInteger = Integer::valueOf; Function backToString = toInteger.andThen(String::valueOf); backToString.apply("123"); // "123"
Fornecedores
Os fornecedores retornam um resultado (instância) de um tipo ou de outro. Ao contrário das funções, os provedores não aceitam argumentos. Supplier personSupplier = Person::new; personSupplier.get(); // new Person
Consumidores
Os consumidores representam métodos de interface com um único argumento. Consumer greeter = (p) -> System.out.println("Hello, " + p.firstName); greeter.accept(new Person("Luke", "Skywalker"));
Comparadores
Os comparadores são conhecidos por nós de versões anteriores do Java. Java 8 permite adicionar vários métodos padrão às interfaces. Comparator comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName); Person p1 = new Person("John", "Doe"); Person p2 = new Person("Alice", "Wonderland"); comparator.compare(p1, p2); // > 0 comparator.reversed().compare(p1, p2); // < 0
Opcionais
A interface Opcionais não é funcional, mas é um ótimo utilitário para prevenir NullPointerException . Este é um ponto importante para a próxima seção, então vamos dar uma olhada rápida em como essa interface funciona. A interface Opcional é um contêiner simples para valores que podem ser nulos ou não nulos. Imagine que um método pode retornar um valor ou nada. No Java 8, em vez de retornar null , você retorna uma instância Opcional . Comparator comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName); Person p1 = new Person("John", "Doe"); Person p2 = new Person("Alice", "Wonderland"); comparator.compare(p1, p2); // > 0 comparator.reversed().compare(p1, p2); // < 0

Fluxo

java.util.Stream é uma sequência de elementos nos quais uma ou mais operações são executadas. Cada operação Stream é intermediária ou terminal. As operações de terminal retornam um resultado de um tipo específico, enquanto as operações intermediárias retornam o próprio objeto de fluxo, permitindo a criação de uma cadeia de chamadas de método. Stream é uma interface, como java.util.Collection para listas e conjuntos (mapas não são suportados).Cada operação Stream pode ser executada sequencialmente ou em paralelo. Vamos dar uma olhada em como o stream funciona. Primeiro, criaremos um código de amostra na forma de uma lista de strings: As coleções em Java 8 são aprimoradas para que você possa criar fluxos simplesmente chamando Collection.stream() ou Collection.parallelStream() . A próxima seção explicará as operações de fluxo simples e mais importantes. List stringCollection = new ArrayList<>(); stringCollection.add("ddd2"); stringCollection.add("aaa2"); stringCollection.add("bbb1"); stringCollection.add("aaa1"); stringCollection.add("bbb3"); stringCollection.add("ccc"); stringCollection.add("bbb2"); stringCollection.add("ddd1");
Filtro
Filter aceita predicados para filtrar todos os elementos do stream. Esta operação é intermediária, o que nos permite chamar outras operações de fluxo (por exemplo, forEach) no resultado resultante (filtrado). ForEach aceita uma operação que será executada em cada elemento do stream já filtrado. ForEach é uma operação de terminal. Além disso, é impossível chamar outras operações. stringCollection .stream() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa2", "aaa1"
Classificado
Classificado é uma operação intermediária que retorna uma representação classificada do fluxo. Os elementos são classificados na ordem correta, a menos que você especifique seu Comparator . stringCollection .stream() .sorted() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa1", "aaa2" Tenha em mente que sorted cria uma representação ordenada do fluxo sem afetar a coleção em si. A ordem dos elementos stringCollection permanece inalterada: System.out.println(stringCollection); // ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1
Mapa
A operação intermediária do mapa converte cada elemento em outro objeto usando a função resultante. O exemplo a seguir converte cada string em uma string maiúscula. Mas você também pode usar map para converter cada objeto em um tipo diferente. O tipo dos objetos de fluxo resultantes depende do tipo de função que você passa para mapear. stringCollection .stream() .map(String::toUpperCase) .sorted((a, b) -> b.compareTo(a)) .forEach(System.out::println); // "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"
Corresponder
Várias operações de correspondência podem ser usadas para testar a veracidade de um predicado específico na relação de fluxo. Todas as operações de correspondência são terminais e retornam um resultado booleano. boolean anyStartsWithA = stringCollection .stream() .anyMatch((s) -> s.startsWith("a")); System.out.println(anyStartsWithA); // true boolean allStartsWithA = stringCollection .stream() .allMatch((s) -> s.startsWith("a")); System.out.println(allStartsWithA); // false boolean noneStartsWithZ = stringCollection .stream() .noneMatch((s) -> s.startsWith("z")); System.out.println(noneStartsWithZ); // true
Contar
Count é uma operação de terminal que retorna o número de elementos do stream como um long . long startsWithB = stringCollection .stream() .filter((s) -> s.startsWith("b")) .count(); System.out.println(startsWithB); // 3
Reduzir
Esta é uma operação de terminal que encurta os elementos do stream usando a função passada. O resultado será um Opcional contendo o valor abreviado. Optional reduced = stringCollection .stream() .sorted() .reduce((s1, s2) -> s1 + "#" + s2); reduced.ifPresent(System.out::println); // "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"

Fluxos paralelos

Conforme mencionado acima, os fluxos podem ser sequenciais ou paralelos. As operações de fluxo sequencial são executadas em um thread serial, enquanto as operações de fluxo paralelo são executadas em vários threads paralelos. O exemplo a seguir demonstra como aumentar facilmente o desempenho usando um fluxo paralelo. Primeiro, vamos criar uma grande lista de elementos únicos: Agora determinaremos o tempo gasto na classificação do fluxo desta coleção. int max = 1000000; List values = new ArrayList<>(max); for (int i = 0; i < max; i++) { UUID uuid = UUID.randomUUID(); values.add(uuid.toString()); }
Fluxo serial
long t0 = System.nanoTime(); long count = values.stream().sorted().count(); System.out.println(count); long t1 = System.nanoTime(); long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0); System.out.println(String.format("sequential sort took: %d ms", millis)); // sequential sort took: 899 ms
Fluxo paralelo
long t0 = System.nanoTime(); long count = values.parallelStream().sorted().count(); System.out.println(count); long t1 = System.nanoTime(); long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0); System.out.println(String.format("parallel sort took: %d ms", millis)); // parallel sort took: 472 ms Como você pode ver, os dois fragmentos são quase idênticos, mas a classificação paralela é 50% mais rápida. Tudo que você precisa é mudar stream() para paraleloStream() .

Mapa

Como já mencionado, os mapas não suportam streams. Em vez disso, o mapa começou a apoiar métodos novos e úteis para resolver problemas comuns. O código acima deve ser intuitivo: putIfAbsent nos avisa contra escrever verificações nulas adicionais. forEach aceita uma função para executar para cada um dos valores do mapa. Este exemplo mostra como as operações são executadas nos valores do mapa usando funções: A seguir, aprenderemos como remover uma entrada para uma determinada chave somente se ela estiver mapeada para um determinado valor: Outro bom método: Mesclar entradas do mapa é bastante fácil: Mesclar irá inserir a chave/valor no map , se não houver entrada para a chave fornecida, ou a função de mesclagem será chamada, o que alterará o valor da entrada existente. Map map = new HashMap<>(); for (int i = 0; i < 10; i++) { map.putIfAbsent(i, "val" + i); } map.forEach((id, val) -> System.out.println(val)); map.computeIfPresent(3, (num, val) -> val + num); map.get(3); // val33 map.computeIfPresent(9, (num, val) -> null); map.containsKey(9); // false map.computeIfAbsent(23, num -> "val" + num); map.containsKey(23); // true map.computeIfAbsent(3, num -> "bam"); map.get(3); // val33 map.remove(3, "val3"); map.get(3); // val33 map.remove(3, "val33"); map.get(3); // null map.getOrDefault(42, "not found"); // not found map.merge(9, "val9", (value, newValue) -> value.concat(newValue)); map.get(9); // val9 map.merge(9, "concat", (value, newValue) -> value.concat(newValue)); map.get(9); // val9concat
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION