JavaRush /Blog Java /Random-ES /Funciones de Java 8: la guía definitiva (Parte 1)
0xFF
Nivel 9
Донецк

Funciones de Java 8: la guía definitiva (Parte 1)

Publicado en el grupo Random-ES
La primera parte de la traducción del artículo Funciones de Java 8: la guía ULTIMATE . La segunda parte está aquí (el enlace puede cambiar). Funciones de Java 8: la guía definitiva (Parte 1) - 1 Nota del editor : este artículo se publicó mientras Java 8 estaba disponible para el público y todo indica que se trata de una versión principal. Aquí hemos proporcionado abundantes guías de Java Code Geeks, como Jugando con Java 8: Lambdas y concurrencia , Guía de API de fecha y hora de Java 8: LocalDateTime y clase abstracta frente a interfaz en la era de Java 8 . También enlazamos a 15 tutoriales de Java 8 de lectura obligada de otras fuentes . Por supuesto, analizamos algunas de las desventajas, como el lado oscuro de Java 8 . Entonces, es hora de recopilar todas las características principales de Java 8 en un solo lugar para su conveniencia. ¡Disfrutar!

1. Introducción

Sin duda, el lanzamiento de Java 8 es el mayor acontecimiento desde Java 5 (lanzado hace bastante tiempo, en 2004). Aportó muchas características nuevas a Java tanto en el lenguaje, compilador, bibliotecas, herramientas y JVM (Java Virtual Machine). En este tutorial, veremos estos cambios y demostraremos diferentes casos de uso con ejemplos de la vida real. La guía consta de varias partes, cada una de las cuales aborda un aspecto específico de la plataforma:
  • Idioma
  • Compilador
  • Bibliotecas
  • Herramientas
  • Entorno de ejecución (JVM)

2. Nuevas funciones en Java 8

En cualquier caso, Java 8 es una versión importante. Podemos decir que tomó tanto tiempo debido a la implementación de las características que todo desarrollador de Java buscaba. En esta sección vamos a cubrir la mayoría de ellos.

2.1. Lambdas e interfaces funcionales

Lambdas (también conocidos como métodos privados o anónimos) son el cambio de lenguaje más grande y esperado en toda la versión de Java 8. Nos permiten especificar la funcionalidad como un argumento de método (declarando una función a su alrededor) o especificar código como datos. : conceptos con los que todo desarrollador funcional está familiarizado.programación _ Muchos lenguajes en la plataforma JVM (Groovy, Scala ,...) tuvieron lambdas desde el primer día, pero los desarrolladores de Java no tuvieron más remedio que representar lambdas a través de clases anónimas. El debate sobre el diseño lambda requirió mucho tiempo y esfuerzo del público. Pero finalmente se llegaron a compromisos, lo que condujo a la aparición de nuevos diseños concisos. En su forma más simple, una lambda se puede representar como una lista de parámetros separados por comas, un –> símbolo y un cuerpo. Por ejemplo:
Arrays.asList( "a", "b", "d" ).forEach( e -> System.out.println( e ) )
Tenga en cuenta que el tipo de argumento e lo determina el compilador. Además, puede especificar explícitamente el tipo de un parámetro encerrándolo entre paréntesis. Por ejemplo:
Arrays.asList( "a", "b", "d" ).forEach( ( String e ) -> System.out.println( e ) );
En caso de que el cuerpo lambda sea más complejo, se puede encerrar entre llaves, similar a una definición de función normal en Java. Por ejemplo:
Arrays.asList( "a", "b", "d" ).forEach( e -< {
    System.out.print( e );
    System.out.print( e );
} );
Una lambda puede hacer referencia a miembros de clase y variables locales (implícitamente haciendo que la llamada sea efectiva independientemente de si finalse accede al campo o no). Por ejemplo, estos 2 fragmentos son equivalentes:
String separator = ",";
Arrays.asList( "a", "b", "d" ).forEach(
    ( String e ) -> System.out.print( e + separator ) );
Y:
final String separator = ",";
Arrays.asList( "a", "b", "d" ).forEach(
    ( String e ) -> System.out.print( e + separator ) );
Lambdas puede devolver un valor. El tipo de devolución lo determinará el compilador. No se requiere declaración returnsi el cuerpo de la lambda consta de una línea. Los dos fragmentos de código siguientes son equivalentes:
Arrays.asList( "a", "b", "d" ).sort( ( e1, e2 ) -> e1.compareTo( e2 ) );
Y:
Arrays.asList( "a", "b", "d" ).sort( ( e1, e2 ) -> {
    int result = e1.compareTo( e2 );
    return result;
} );
Los desarrolladores del lenguaje pensaron durante mucho tiempo en cómo hacer que las funciones existentes sean compatibles con Lambda. Como resultado, surgió el concepto de interfaz funcional. Una interfaz funcional es una interfaz con un solo método. Como resultado, se puede convertir implícitamente en una expresión lambda. java.lang.Runnabley java.util.concurrent.Callabledos grandes ejemplos de interfaces funcionales. En la práctica, las interfaces funcionales son muy frágiles: si alguien agrega incluso un método más a la definición de la interfaz, ya no será funcional y el proceso de compilación no se completará. Para evitar esta fragilidad y definir explícitamente la intención de una interfaz como funcional, se agregó una anotación especial en Java 8 @FunctionalInterface(todas las interfaces existentes en la biblioteca Java recibieron la anotación @FunctionalInterface). Veamos esta definición simple de una interfaz funcional:
@FunctionalInterface
public interface Functional {
    void method();
}
Hay una cosa a tener en cuenta: los métodos predeterminados y los métodos estáticos no violan el principio de una interfaz funcional y pueden declararse:
@FunctionalInterface
public interface FunctionalDefaultMethods {
    void method();

    default void defaultMethod() {
    }
}
Lambdas es la característica más popular de Java 8. Tienen todo el potencial para atraer más desarrolladores a esta maravillosa plataforma y brindar soporte inteligente para funciones en Java puro. Para obtener información más detallada, consulte la documentación oficial .

2.2. Interfaces predeterminadas y métodos estáticos

Java 8 amplió la definición de interfaces con dos nuevos conceptos: método predeterminado y método estático. Los métodos predeterminados hacen que las interfaces sean algo similares a los rasgos, pero tienen un propósito ligeramente diferente. Le permiten agregar nuevos métodos a interfaces existentes sin romper la compatibilidad con versiones escritas previamente de estas interfaces. La diferencia entre los métodos predeterminados y los métodos abstractos es que los métodos abstractos deben implementarse, mientras que los métodos predeterminados no. En cambio, cada interfaz debe proporcionar la denominada implementación predeterminada, y todos los descendientes la recibirán de forma predeterminada (con la capacidad de anular esta implementación predeterminada si es necesario). Veamos el ejemplo a continuación.
private interface Defaulable {
    // Интерфейсы теперь разрешают методы по умолчанию,
    // клиент может реализовывать  (переопределять)
    // o не реализовывать его
    default String notRequired() {
        return "Default implementation";
    }
}

private static class DefaultableImpl implements Defaulable {
}

private static class OverridableImpl implements Defaulable {
    @Override
    public String notRequired() {
        return "Overridden implementation";
    }
}
La interfaz Defaulabledeclara un método predeterminado notRequired()utilizando una palabra clave defaultcomo parte de la definición del método. Una de las clases, DefaultableImplimplementa esta interfaz dejando el método predeterminado como está. Otra clase, OverridableImpl, anula la implementación predeterminada y proporciona la suya propia. Otra característica interesante introducida en Java 8 es que las interfaces pueden declarar (y ofrecer implementaciones de) métodos estáticos. He aquí un ejemplo:
private interface DefaulableFactory {
    // Interfaces now allow static methods
    static Defaulable create( Supplier<Defaulable> supplier ) {
        return supplier.get();
    }
}
Un pequeño fragmento de código combina el método predeterminado y el método estático del ejemplo anterior:
public static void main( String[] args ) {
    Defaulable defaulable = DefaulableFactory.create( DefaultableImpl::new );
    System.out.println( defaulable.notRequired() );

    defaulable = DefaulableFactory.create( OverridableImpl::new );
    System.out.println( defaulable.notRequired() );
}
La salida de consola de este programa se ve así:
Default implementation
Overridden implementation
La implementación del método predeterminado en la JVM es muy eficiente y la invocación del método está respaldada por instrucciones de código de bytes. Los métodos predeterminados permitieron que las interfaces Java existentes evolucionaran sin interrumpir el proceso de compilación. Buenos ejemplos son los muchos métodos agregados a la interfaz java.util.Collection: stream(), parallelStream(), forEach(), removeIf(), ... Aunque son poderosos, los métodos predeterminados deben usarse con precaución: antes de declarar un método predeterminado, debe pensar dos veces si esto es realmente necesario, ya que esto puede llevar a a ambigüedades y errores de compilación en jerarquías complejas. Para obtener información más detallada, consulte la documentación .

2.3. Métodos de referencia

Los métodos de referencia implementan una sintaxis útil para hacer referencia a métodos o constructores existentes de clases u objetos (instancias) de Java. Junto con las expresiones lambda, los métodos de referencia hacen que las construcciones del lenguaje sean compactas y concisas, haciéndolo basado en plantillas. A continuación se muestra una clase Carcomo ejemplo de diferentes definiciones de métodos. Resaltemos los cuatro tipos admitidos de métodos de referencia:
public static class Car {
    public static Car create( final Supplier<Car> supplier ) {
        return supplier.get();
    }

    public static void collide( final Car car ) {
        System.out.println( "Collided " + car.toString() );
    }

    public void follow( final Car another ) {
        System.out.println( "Following the " + another.toString() );
    }

    public void repair() {
        System.out.println( "Repaired " + this.toString() );
    }
}
El primer método de referencia es una referencia a un constructor con sintaxis Class::newo una alternativa para genéricos Class< T >::new. Tenga en cuenta que el constructor no tiene argumentos.
final Car car = Car.create( Car::new );
final List<Car> cars = Arrays.asList( car );
La segunda opción es una referencia a un método estático con la sintaxis Class::static_method. Tenga en cuenta que el método toma exactamente un parámetro de tipo Car.
cars.forEach( Car::collide );
El tercer tipo es una referencia a un método de una instancia de un objeto arbitrario de cierto tipo con la sintaxis Class::method. Tenga en cuenta que el método no acepta argumentos.
cars.forEach( Car::repair );
Y el último, cuarto tipo es una referencia a un método de una instancia de una clase específica con la sintaxis instance::method. Tenga en cuenta que el método solo acepta un parámetro de tipo Car.
final Car police = Car.create( Car::new );
cars.forEach( police::follow );
La ejecución de todos estos ejemplos como programas Java produce el siguiente resultado de consola (la referencia de clase Carpuede variar):
Collided com.javacodegeeks.java8.method.references.MethodReferences$Car@7a81197d
Repaired com.javacodegeeks.java8.method.references.MethodReferences$Car@7a81197d
Following the com.javacodegeeks.java8.method.references.MethodReferences$Car@7a81197d
Para obtener información más detallada y detalles de los métodos de referencia, consulte la documentación oficial .

2.4. Anotaciones duplicadas

Desde que Java 5 introdujo el soporte para anotaciones , esta característica se ha vuelto muy popular y ampliamente utilizada. Sin embargo, una de las limitaciones del uso de anotaciones fue el hecho de que la misma anotación no puede declararse más de una vez en el mismo lugar. Java 8 rompe esta regla e introduce anotaciones duplicadas. Esto permite que las mismas anotaciones se repitan varias veces en el lugar donde se declaran. Las anotaciones duplicadas deben anotarse a sí mismas mediante anotación @Repeatable. De hecho, esto no es tanto un cambio en el lenguaje sino un truco del compilador, mientras que la técnica sigue siendo la misma. Veamos un ejemplo sencillo:
package com.javacodegeeks.java8.repeatable.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

public class RepeatingAnnotations {
    @Target( ElementType.TYPE )
    @Retention( RetentionPolicy.RUNTIME )
    public @interface Filters {
        Filter[] value();
    }

    @Target( ElementType.TYPE )
    @Retention( RetentionPolicy.RUNTIME )
    @Repeatable( Filters.class )
    public @interface Filter {
        String value();
    };

    @Filter( "filter1" )
    @Filter( "filter2" )
    public interface Filterable {
    }

    public static void main(String[] args) {
        for( Filter filter: Filterable.class.getAnnotationsByType( Filter.class ) ) {
            System.out.println( filter.value() );
        }
    }
}
Como podemos ver, la clase Filterestá anotada con @Repeatable( Filters. class). Filterses simplemente el propietario de las anotaciones Filter, pero el compilador de Java intenta ocultar su presencia a los desarrolladores. Por tanto, la interfaz Filterablecontiene anotaciones Filterque se declaran dos veces (sin mencionar Filters). La API de Reflection también proporciona un nuevo método getAnnotationsByType()para devolver anotaciones duplicadas de algún tipo (recuerde que Filterable. class.getAnnotation( Filters. class) devolverá una instancia inyectada por el compilador Filters). La salida del programa se verá así:
filter1
filter2
Para obtener información más detallada, consulte la documentación oficial .

2.5. Inferencia de tipos mejorada

El compilador de Java 8 ha recibido muchas mejoras en la inferencia de tipos. En muchos casos, el compilador puede definir parámetros de tipo explícitos, lo que hace que el código sea más limpio. Veamos un ejemplo:
package com.javacodegeeks.java8.type.inference;

public class Value<T> {
    public static<T> T defaultValue() {
        return null;
    }

    public T getOrDefault( T value, T defaultValue ) {
        return ( value != null ) ? value : defaultValue;
    }
}
Y aquí está el uso con tipo Value<String>:
package com.javacodegeeks.java8.type.inference;

public class TypeInference {
    public static void main(String[] args) {
        final Value<String> value = new Value<>();
        value.getOrDefault( "22", Value.defaultValue() );
    }
}
El parámetro de tipo Value.defaultValue()se determina automáticamente y no es necesario proporcionarlo explícitamente. En Java 7, el mismo ejemplo no se compilará y debe reescribirse como <NOBR>Value.<String>defaultValue()</NOBR>.

2.6. Soporte de anotación ampliado

Java 8 amplía el contexto donde se pueden utilizar las anotaciones. Hoy en día, casi cualquier cosa puede tener una anotación: variables locales, tipos genéricos, superclases e interfaces implementadas, incluso excepciones de métodos. A continuación se presentan algunos ejemplos:
package com.javacodegeeks.java8.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.Collection;

public class Annotations {
    @Retention( RetentionPolicy.RUNTIME )
    @Target( { ElementType.TYPE_USE, ElementType.TYPE_PARAMETER } )
    public @interface NonEmpty {
    }

    public static class Holder<@NonEmpty T> extends @NonEmpty Object {
        public void method() throws @NonEmpty Exception {
        }
    }

    @SuppressWarnings( "unused" )
    public static void main(String[] args) {
        final Holder<String> holder = new @NonEmpty Holder<String>();
        @NonEmpty Collection<@NonEmpty String> strings = new ArrayList<>();
    }
}
ElementType.TYPE_USEy ElementType.TYPE_PARAMETERdos nuevos tipos de elementos para describir el contexto de anotación relevante. Annotation Processing APITambién ha sufrido cambios menores para reconocer nuevos tipos de anotaciones en Java.

3. Nuevas funciones en el compilador de Java.

3.1. Nombres de parámetros

A lo largo del tiempo, los desarrolladores de Java han inventado una variedad de formas de almacenar nombres de parámetros de métodos en código de bytes de Java para que estén disponibles en tiempo de ejecución (por ejemplo, la biblioteca Paranamer ). Finalmente, Java 8 crea esta función difícil en el lenguaje (usando la API y el método Reflection Parameter.getName()) y el código de bytes (usando el nuevo argumento del compilador javac:) –parameters.
package com.javacodegeeks.java8.parameter.names;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;

public class ParameterNames {
    public static void main(String[] args) throws Exception {
        Method method = ParameterNames.class.getMethod( "main", String[].class );
        for( final Parameter parameter: method.getParameters() ) {
            System.out.println( "Parameter: " + parameter.getName() );
        }
    }
}
Si compila esta clase sin usar un argumento –parametersy luego ejecuta el programa, verá algo como esto:
Parameter: arg0
Con el parámetro –parameterspasado al compilador, la salida del programa será diferente (se mostrará el nombre real del parámetro):
Parameter: args
Para usuarios avanzados de Maven, el argumento –parameters se puede agregar a la compilación usando la sección maven-compiler-plugin:
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.1</version>
    <configuration>
    <compilerArgument>-parameters</compilerArgument>
    <source>1.8</source>
    <target>1.8</target>
    </configuration>
</plugin>
Para comprobar la disponibilidad de los nombres de los parámetros existe un isNamePresent()método conveniente proporcionado por la clase Parameter.

4. Nuevas herramientas Java

Java 8 viene con un nuevo conjunto de herramientas de línea de comandos. En esta sección veremos los más interesantes.

4.1. Motor Nashorn: jjs

jjs es un motor Nashorn independiente basado en la línea de comandos. Toma una lista de archivos de código fuente JavaScript y los ejecuta. Por ejemplo, creemos un archivo func.js con el siguiente contenido:
function f() {
     return 1;
};

print( f() + 1 );
Para ejecutar este archivo, pasémoslo como argumento a jjs :
jjs func.js
La salida de la consola será así:
2
Para más detalles consulte la documentación .

4.2. Analizador de dependencia de clases: jdeps

jdeps es una herramienta de línea de comandos realmente excelente. Muestra dependencias a nivel de paquete o clase para clases Java. Acepta un archivo .class , carpeta o archivo JAR como entrada. De forma predeterminada , jdeps genera dependencias en la salida estándar (consola). Como ejemplo, veamos el informe de dependencia de la popular biblioteca Spring Framework . Para que el ejemplo sea breve, veamos únicamente las dependencias del archivo JAR org.springframework.core-3.0.5.RELEASE.jar.
jdeps org.springframework.core-3.0.5.RELEASE.jar
Este comando genera bastante, por lo que solo analizaremos una parte del resultado. Las dependencias están agrupadas por paquetes. Si no hay dependencias, se mostrará No encontrado .
org.springframework.core-3.0.5.RELEASE.jar -> C:\Program Files\Java\jdk1.8.0\jre\lib\rt.jar
   org.springframework.core (org.springframework.core-3.0.5.RELEASE.jar)
      -> java.io
      -> java.lang
      -> java.lang.annotation
      -> java.lang.ref
      -> java.lang.reflect
      -> java.util
      -> java.util.concurrent
      -> org.apache.commons.logging                         not found
      -> org.springframework.asm                            not found
      -> org.springframework.asm.commons                    not found
   org.springframework.core.annotation (org.springframework.core-3.0.5.RELEASE.jar)
      -> java.lang
      -> java.lang.annotation
      -> java.lang.reflect
      -> java.util
Para obtener información más detallada, consulte la documentación oficial .
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION