JavaRush /Blog Java /Random-ES /Guía de Java 8. 1 parte.
ramhead
Nivel 13

Guía de Java 8. 1 parte.

Publicado en el grupo Random-ES

"Java sigue vivo y la gente está empezando a entenderlo".

Bienvenido a mi introducción a Java 8. Esta guía lo llevará paso a paso a través de todas las nuevas características del lenguaje. A través de ejemplos de código breves y sencillos, aprenderá a utilizar los métodos predeterminados de la interfaz , las expresiones lambda , los métodos de referencia y las anotaciones repetibles . Al final del artículo, estará familiarizado con los últimos cambios en las API, como transmisiones, interfaces de funciones, extensiones de asociación y la nueva API de fecha. Sin paredes de texto aburrido, solo un montón de fragmentos de código comentados. ¡Disfrutar!

Métodos predeterminados para interfaces

Java 8 nos permite agregar métodos no abstractos implementados en la interfaz mediante el uso de la palabra clave predeterminada . Esta característica también se conoce como métodos de extensión . Aquí está nuestro primer ejemplo: interface Formula { double calculate(int a); default double sqrt(int a) { return Math.sqrt(a); } } además del método abstracto calcular , la interfaz Fórmula también define un método predeterminado sqrt . Las clases que implementan la interfaz Fórmula implementan sólo el método de cálculo abstracto . El método sqrt predeterminado se puede utilizar nada más sacarlo de la caja. Formula formula = new Formula() { @Override public double calculate(int a) { return sqrt(a * 100); } }; formula.calculate(100); // 100.0 formula.sqrt(16); // 4.0 El objeto de fórmula se implementa como un objeto anónimo. El código es bastante impresionante: 6 líneas de código para calcular simplemente sqrt(a * 100) . Como veremos en la siguiente sección, existe una forma más atractiva de implementar objetos de método único en Java 8.

expresiones lambda

Comencemos con un ejemplo simple de cómo ordenar una matriz de cadenas en las primeras versiones de Java: el método auxiliar estadístico Collections.sort toma una lista y un Comparador para ordenar los elementos de la lista dada. Lo que sucede a menudo es que crea comparadores anónimos y los pasa a métodos de clasificación. En lugar de crear objetos anónimos todo el tiempo, Java 8 le ofrece la posibilidad de utilizar mucha menos sintaxis y expresiones lambda : como puede ver, el código es mucho más corto y más fácil de leer. Pero aquí se vuelve aún más breve: para un método de una línea, puede deshacerse de las llaves {} y la palabra clave return . Pero aquí es donde el código se vuelve aún más corto: el compilador de Java conoce los tipos de parámetros, por lo que también puedes omitirlos. Ahora profundicemos en cómo se pueden utilizar las expresiones lambda en la 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 funcionales

¿Cómo encajan las expresiones lambda en el sistema de tipos de Java? Cada lambda corresponde a un tipo determinado definido por una interfaz. Y la llamada interfaz funcional debe contener exactamente un método abstracto declarado. Cada expresión lambda de un tipo determinado corresponderá a este método abstracto. Dado que los métodos predeterminados no son métodos abstractos, usted es libre de agregar métodos predeterminados a su interfaz funcional. Podemos usar una interfaz arbitraria como expresión lambda, siempre que la interfaz contenga solo un método abstracto. Para asegurarse de que su interfaz cumpla con estas condiciones, debe agregar la anotación @FunctionalInterface . Esta anotación informará al compilador que la interfaz debe contener solo un método y, si encuentra un segundo método abstracto en esta interfaz, generará un error. Ejemplo: tenga en cuenta que este código también sería válido incluso si no se hubiera declarado la anotación @FunctionalInterface . @FunctionalInterface interface Converter { T convert(F from); } Converter converter = (from) -> Integer.valueOf(from); Integer converted = converter.convert("123"); System.out.println(converted); // 123

Referencias a métodos y constructores.

El ejemplo anterior se puede simplificar aún más utilizando una referencia de método estadístico: Java 8 le permite pasar referencias a métodos y constructores utilizando la palabra clave :: símbolos . El ejemplo anterior muestra cómo se pueden utilizar los métodos estadísticos. Pero también podemos hacer referencia a métodos en objetos: Echemos un vistazo a cómo funciona el uso de :: para constructores. Primero, definamos un ejemplo con diferentes constructores: A continuación, definimos la interfaz de fábrica PersonFactory para crear nuevos objetos persona : 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); } En lugar de implementar la fábrica manualmente, unimos todo usando una referencia de constructor: creamos una referencia al constructor de la clase Person a través de Person::new . El compilador de Java llamará automáticamente al constructor apropiado comparando la firma de los constructores con la firma del método PersonFactory.create . PersonFactory personFactory = Person::new; Person person = personFactory.create("Peter", "Parker");

región lambda

Organizar el acceso a variables de alcance externo desde expresiones lambda es similar a acceder desde un objeto anónimo. Puede acceder a variables finales desde el ámbito local, así como a campos de instancia y variables agregadas.
Accediendo a variables locales
Podemos leer una variable local con el modificador final desde el alcance de una expresión lambda: pero a diferencia de los objetos anónimos, no es necesario declarar las variables finales para que sean accesibles desde una expresión lambda . Este código también es correcto: sin embargo, la variable num debe permanecer inmutable, es decir ser final implícito para la compilación del código. El siguiente código no se compilará: Tampoco se permiten cambios en num dentro de una expresión lambda. 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;
Acceso a campos de instancia y variables estadísticas
A diferencia de las variables locales, podemos leer y modificar campos de instancia y variables estadísticas dentro de expresiones lambda. Conocemos este comportamiento por 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); }; } }
Acceso a métodos predeterminados de interfaces.
¿Recuerda el ejemplo con la instancia de fórmula de la primera sección? La interfaz Fórmula define un método sqrt predeterminado al que se puede acceder desde cada instancia de fórmula , incluidos los objetos anónimos. Esto no funciona con expresiones lambda. No se puede acceder a los métodos predeterminados dentro de expresiones lambda. El siguiente código no se compila: Formula formula = (a) -> sqrt( a * 100);

Interfaces funcionales integradas

La API JDK 1.8 contiene muchas interfaces funcionales integradas. Algunos de ellos son bien conocidos por versiones anteriores de Java. Por ejemplo Comparator o Runnable . Estas interfaces se amplían para incluir compatibilidad con lambda mediante la anotación @FunctionalInterface . Pero la API de Java 8 también está llena de nuevas interfaces funcionales que te harán la vida más fácil. Algunas de estas interfaces son bien conocidas por la biblioteca Guava de Google . Incluso si está familiarizado con esta biblioteca, debería observar más de cerca cómo se extienden estas interfaces, con algunos métodos de extensión útiles.
Predicados
Los predicados son funciones booleanas con un argumento. La interfaz contiene varios métodos predeterminados para crear expresiones lógicas complejas (y, o 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();
Funciones
Las funciones toman un argumento y producen un resultado. Se pueden utilizar métodos predeterminados para combinar varias funciones en una cadena (componer y luego). Function toInteger = Integer::valueOf; Function backToString = toInteger.andThen(String::valueOf); backToString.apply("123"); // "123"
Proveedores
Los proveedores devuelven un resultado (instancia) de un tipo u otro. A diferencia de las funciones, los proveedores no aceptan argumentos. Supplier personSupplier = Person::new; personSupplier.get(); // new Person
Consumidores
Los consumidores representan métodos de interfaz con un único argumento. Consumer greeter = (p) -> System.out.println("Hello, " + p.firstName); greeter.accept(new Person("Luke", "Skywalker"));
Comparadores
Conocemos comparadores de versiones anteriores de Java. Java 8 le permite agregar varios métodos predeterminados a las 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
Opcionales
La interfaz de Opciones no es funcional, pero es una gran utilidad para prevenir NullPointerException . Este es un punto importante para la siguiente sección, así que echemos un vistazo rápido a cómo funciona esta interfaz. La interfaz opcional es un contenedor simple para valores que pueden ser nulos o no nulos. Imagine que un método puede devolver un valor o nada. En Java 8, en lugar de devolver null , devuelve una instancia 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

Arroyo

java.util.Stream es una secuencia de elementos sobre los que se realizan una o varias operaciones. Cada operación de Stream es intermedia o terminal. Las operaciones de terminal devuelven un resultado de un tipo específico, mientras que las operaciones intermedias devuelven el objeto de flujo en sí, lo que permite crear una cadena de llamadas a métodos. Stream es una interfaz, como java.util.Collection para listas y conjuntos (no se admiten mapas). Cada operación de Stream se puede ejecutar de forma secuencial o en paralelo. Echemos un vistazo a cómo funciona la transmisión. Primero, crearemos un código de muestra en forma de lista de cadenas: Las colecciones en Java 8 están mejoradas para que pueda crear secuencias simplemente llamando a Collection.stream() o Collection.parallelStream() . La siguiente sección explicará las operaciones de transmisión simples más 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");
Filtrar
El filtro acepta predicados para filtrar todos los elementos de la secuencia. Esta operación es intermedia, lo que nos permite llamar a otras operaciones de flujo (por ejemplo, forEach) en el resultado resultante (filtrado). ForEach acepta una operación que se realizará en cada elemento del flujo ya filtrado. ForEach es una operación de terminal. Además, es imposible llamar a otras operaciones. stringCollection .stream() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa2", "aaa1"
Ordenado
Ordenada es una operación intermedia que devuelve una representación ordenada de la secuencia. Los elementos se ordenan en el orden correcto a menos que especifique su Comparador . stringCollection .stream() .sorted() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa1", "aaa2" Tenga en cuenta que ordenado crea una representación ordenada de la secuencia sin afectar la colección en sí. El orden de los elementos stringCollection permanece intacto: System.out.println(stringCollection); // ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1
Mapa
La operación de mapa intermedia convierte cada elemento en otro objeto utilizando la función resultante. El siguiente ejemplo convierte cada cadena en una cadena en mayúsculas. Pero también puedes usar map para convertir cada objeto a un tipo diferente. El tipo de objetos de flujo resultantes depende del tipo de función que pase al mapa. stringCollection .stream() .map(String::toUpperCase) .sorted((a, b) -> b.compareTo(a)) .forEach(System.out::println); // "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"
Fósforo
Se pueden utilizar varias operaciones de coincidencia para probar la verdad de un predicado particular en la relación de flujo. Todas las operaciones de coincidencia son terminales y devuelven un 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 es una operación de terminal que devuelve el número de elementos de la secuencia como un largo . long startsWithB = stringCollection .stream() .filter((s) -> s.startsWith("b")) .count(); System.out.println(startsWithB); // 3
Reducir
Esta es una operación de terminal que acorta los elementos de la secuencia utilizando la función pasada. El resultado será un Opcional que contendrá el valor acortado. Optional reduced = stringCollection .stream() .sorted() .reduce((s1, s2) -> s1 + "#" + s2); reduced.ifPresent(System.out::println); // "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"

Corrientes paralelas

Como se mencionó anteriormente, las transmisiones pueden ser secuenciales o paralelas. Las operaciones de flujo secuencial se realizan en un subproceso en serie, mientras que las operaciones de flujo paralelo se realizan en múltiples subprocesos paralelos. El siguiente ejemplo demuestra cómo aumentar fácilmente el rendimiento utilizando una transmisión paralela. Primero, creemos una lista grande de elementos únicos: ahora determinaremos el tiempo dedicado a ordenar el flujo de esta colección. int max = 1000000; List values = new ArrayList<>(max); for (int i = 0; i < max; i++) { UUID uuid = UUID.randomUUID(); values.add(uuid.toString()); }
flujo en serie
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
Corriente paralela
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 puede ver, ambos fragmentos son casi idénticos, pero la clasificación en paralelo es un 50% más rápida. Todo lo que necesitas es cambiar stream() a paraleloStream() .

Mapa

Como ya se mencionó, los mapas no admiten transmisiones. En cambio, map comenzó a admitir métodos nuevos y útiles para resolver problemas comunes. El código anterior debe ser intuitivo: putIfAbsent nos advierte contra la escritura de comprobaciones nulas adicionales. forEach acepta una función para ejecutar para cada uno de los valores del mapa. Este ejemplo muestra cómo se realizan operaciones en valores del mapa usando funciones: A continuación, aprenderemos cómo eliminar una entrada para una clave determinada solo si se asigna a un valor determinado: Otro buen método: fusionar entradas del mapa es bastante fácil: Fusionar insertará la clave/valor en el mapa, si no hay ninguna entrada para la clave dada, o se llamará a la función de combinación, que cambiará el valor de la 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
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION