¿Qué es el azúcar sintáctico? ¿Qué azúcar sintáctico hay en Java?

Desde la perspectiva de los principios de compilación de Java, este artículo profundiza en el código de bytes y los archivos de clase, y comprende los principios y el uso del azúcar sintáctico en Java. Le ayudará a comprender los principios detrás de estos azúcares sintácticos mientras aprende a usar el azúcar sintáctico de Java.

1 azúcar sintáctico

El azúcar sintáctico, también conocido como gramática recubierta de azúcar, es un término inventado por el informático británico Peter.J.Landin. Se refiere a cierta gramática añadida a un lenguaje informático. Esta gramática no tiene ningún efecto sobre la función del lenguaje. Pero es más conveniente para los programadores de usar. En resumen, el azúcar sintáctico hace que los programas sean más concisos y legibles.

Curiosamente, en el campo de la programación, además del azúcar gramatical, también hay términos de sal gramatical y sacarina gramatical, y el espacio es limitado, por lo que no lo extenderé aquí.

Casi todos los lenguajes de programación que conocemos tienen azúcar sintáctico. El autor cree que la cantidad de azúcar gramatical es uno de los criterios para juzgar si un idioma es lo suficientemente poderoso.

Mucha gente dice que Java es un "lenguaje bajo en azúcar". De hecho, desde Java 7, se han agregado varios azúcares al nivel del lenguaje Java, principalmente desarrollados bajo el proyecto "Project Coin". Aunque algunas personas en Java todavía piensan que el Java actual es bajo en azúcar, continuará desarrollándose en la dirección de "alto contenido de azúcar" en el futuro.

2 solución al azúcar sintáctico

Como se mencionó anteriormente, la existencia de azúcar sintáctico es principalmente para conveniencia de los desarrolladores. Pero, de hecho, la máquina virtual de Java no admite estos azúcares sintácticos. Estos azúcares gramaticales se reducirán a estructuras gramaticales básicas simples durante la fase de compilación, y este proceso es la solución de los azúcares gramaticales.

Hablando de compilación, todos deben saber que en el lenguaje Java, el comando javac puede compilar un archivo fuente con el sufijo .java en un código de bytes con el sufijo .class que puede ejecutarse en una máquina virtual Java.

Si observa el código fuente de com.sun.tools.javac.main.JavaCompiler, encontrará que uno de los pasos en compile() es llamar a desugar(), que es responsable de la realización de la solución de azúcar sintáctica.

El azúcar sintáctico más utilizado en Java incluye principalmente genéricos, parámetros de longitud variable, compilación condicional, desempaquetado automático, clases internas, etc. Este artículo analiza principalmente los principios detrás de estos azúcares gramaticales. Paso a paso para quitar la guinda y ver de qué se trata.

3 Introducción a los dulces

El interruptor 3.1 admite cadenas y enumeración

Como se mencionó anteriormente, a partir de Java 7, el azúcar sintáctico en el lenguaje Java se enriquece gradualmente. Uno de los más importantes es que Switch en Java 7 comienza a admitir String.

Antes de comenzar a codificar, popularicemos la ciencia: el conmutador en Java admite tipos básicos. Como int, char, etc.

Para el tipo int, compare los valores directamente. Para el tipo char, compare su código ascii.

Por lo tanto, para el compilador, solo se pueden usar números enteros en el conmutador, y cualquier tipo de comparación debe convertirse en un número entero. Por ejemplo byte. short, char (el código ackii es un número entero) e int.

Luego, veamos el soporte del conmutador para String, con el siguiente código:

public class switchDemoString {
    public static void main(String[] args) {
        String str = "world";
        switch (str) {
        case "hello":
            System.out.println("hello");
            break;
        case "world":
            System.out.println("world");
            break;
        default:
            break;
        }
    }
}

Después de la descompilación, el contenido es el siguiente:

public class switchDemoString
{
    public switchDemoString()
    {
    }
    public static void main(String args[])
    {
        String str = "world";
        String s;
        switch((s = str).hashCode())
        {
        default:
            break;
        case 99162322:
            if(s.equals("hello"))
                System.out.println("hello");
            break;
        case 113318802:
            if(s.equals("world"))
                System.out.println("world");
            break;
        }
    }
}

Al ver este código, sabe que el cambio de la cadena original se implementa a través de los métodos equals() y hashCode(). Afortunadamente, el método hashCode() devuelve un int, no un long.

Si observa detenidamente, puede encontrar que el cambio real es el valor hash, y luego la verificación de seguridad se realiza utilizando el método de igualdad para comparar.Esta verificación es necesaria porque el hash puede colisionar. Por lo tanto, no es tan eficaz como cambiar con una enumeración o usar una constante entera simple, pero tampoco está mal.

3.2 Genéricos

Todos sabemos que muchos lenguajes admiten genéricos, pero lo que mucha gente no sabe es que los diferentes compiladores manejan los genéricos de diferentes maneras.

Por lo general, un compilador maneja los genéricos de dos maneras: especialización de código y uso compartido de código.

C++ y C# utilizan el mecanismo de procesamiento de la especialización de código, mientras que Java utiliza el mecanismo de código compartido.

El método de código compartido crea una representación de código de bytes única para cada tipo genérico y asigna instancias del tipo genérico a esta representación de código de bytes única. La asignación de varias instancias de tipos genéricos a representaciones de código de bytes únicas se logra mediante el borrado de tipos.

En otras palabras, para la máquina virtual Java, no conoce la sintaxis del mapa Map<String, String> en absoluto. Es necesario eliminar la sintaxis del azúcar mediante el borrado de tipos en la etapa de compilación.

El proceso principal de borrado de tipos es el siguiente:

  • 1. Reemplace todos los parámetros genéricos con su tipo de límite más a la izquierda (tipo principal más alto).

  • 2. Eliminar todos los parámetros de tipo.

El siguiente código:

Map<String, String> map = new HashMap<String, String>();  
map.put("name", "hollis");  
map.put("wechat", "Hollis");  
map.put("blog", "www.hollischuang.com");  

Después de desazucarar el azúcar sintáctico, se convertirá en:

Map map = new HashMap();  
map.put("name", "hollis");  
map.put("wechat", "Hollis");  
map.put("blog", "www.hollischuang.com");  

El siguiente código:

public static <A extends Comparable<A>> A max(Collection<A> xs) {
    Iterator<A> xi = xs.iterator();
    A w = xi.next();
    while (xi.hasNext()) {
        A x = xi.next();
        if (w.compareTo(x) < 0)
            w = x;
    }
    return w;
}

Después de borrar el tipo, se convierte en:

 public static Comparable max(Collection xs){
    Iterator xi = xs.iterator();
    Comparable w = (Comparable)xi.next();
    while(xi.hasNext())
    {
        Comparable x = (Comparable)xi.next();
        if(w.compareTo(x) < 0)
            w = x;
    }
    return w;
}

No hay genéricos en la máquina virtual, solo clases ordinarias y métodos ordinarios. Los parámetros de tipo de todas las clases genéricas se borrarán en el momento de la compilación, y las clases genéricas no tienen sus propios objetos Class únicos. Por ejemplo, no hay List<String>.class o List<Integer>.class, sino solo List.class.

3.3 Empaquetado y desempaquetado automático

Autoboxing significa que Java convierte automáticamente valores de tipo primitivo en objetos correspondientes, como convertir una variable int en un objeto Integer. Este proceso se denomina boxing. Por el contrario, convertir un objeto Integer en un valor de tipo int se denomina unboxing.

Debido a que el empaquetado y desempaquetado aquí es una conversión automática no humana, se llama empaquetado y desempaquetado automático.

Las clases de encapsulación correspondientes a los tipos primitivos byte, short, char, int, long, float, double y boolean son Byte, Short, Character, Integer, Long, Float, Double, Boolean.

Primero veamos el código para el boxeo automático:

public static void main(String[] args) {
    int i = 10;
    Integer n = i;
}

El código descompilado es el siguiente:

public static void main(String args[])
{
    int i = 10;
    Integer n = Integer.valueOf(i);
}

Veamos el código para el desempaquetado automático:

public static void main(String[] args) {

    Integer i = 10;
    int n = i;
}

El código descompilado es el siguiente:

public static void main(String args[])
{
    Integer i = Integer.valueOf(10);
    int n = i.intValue();
}

Se puede ver en el contenido descompilado que el método valueOf(int) de Integer se llama automáticamente al boxear. El método intValue de Integer se llama automáticamente al desempaquetar.

Por lo tanto, el proceso de empaquetado se realiza llamando al método valueOf del contenedor, y el proceso de desempaquetado se realiza llamando al método xxxValue del contenedor.

3.4 Parámetros de longitud variable del método

Los argumentos variables (variable arguments) es una característica introducida en Java 1.5. Permite que un método tome cualquier número de valores como parámetros.

Mire el siguiente código de parámetro variable, donde el método de impresión recibe parámetros variables:

public static void main(String[] args)
    {
        print("Holis", "公众号:Hollis", "博客:www.hollischuang.com", "QQ:907607222");
    }

public static void print(String... strs)
{
    for (int i = 0; i < strs.length; i++)
    {
        System.out.println(strs[i]);
    }
}

Código descompilado:

public static void main(String args[])
{
    print(new String[] {
        "Holis", "\u516C\u4F17\u53F7:Hollis", "\u535A\u5BA2\uFF1Awww.hollischuang.com", "QQ\uFF1A907607222"
    });
}

// transient 不能修饰方法,这里应该是反编译错误了?
public static transient void print(String strs[])
{
    for(int i = 0; i < strs.length; i++)
        System.out.println(strs[i]);

}

Se puede ver en el código descompilado que cuando se usa un parámetro variable, primero crea una matriz cuya longitud es la cantidad de parámetros reales pasados ​​llamando al método, y luego coloca todos los valores de los parámetros en esta matriz, y luego pase esta matriz como un parámetro al método llamado.

3.5 Enumeración

Java SE5 proporciona un nuevo tipo: el tipo de enumeración de Java. La palabra clave enum puede crear un conjunto limitado de valores con nombre como un nuevo tipo, y estos valores con nombre se pueden usar como componentes regulares del programa. Esta es una característica muy útil.

Si desea ver el código fuente, primero debe tener una clase, entonces, ¿qué tipo de clase es el tipo de enumeración? ¿Es una enumeración?

La respuesta obviamente es no, enum es como clase, es solo una palabra clave, no es una clase.

Entonces, ¿por qué clase se mantiene la enumeración? Simplemente escribimos una enumeración:

public enum t {
    SPRING,SUMMER;
}

Luego usamos la descompilación para ver cómo se implementa este código.Después de la descompilación, el contenido del código es el siguiente:

public final class T extends Enum
{
    private T(String s, int i)
    {
        super(s, i);
    }
    public static T[] values()
    {
        T at[];
        int i;
        T at1[];
        System.arraycopy(at = ENUM$VALUES, 0, at1 = new T[i = at.length], 0, i);
        return at1;
    }

    public static T valueOf(String s)
    {
        return (T)Enum.valueOf(demo/T, s);
    }

    public static final T SPRING;
    public static final T SUMMER;
    private static final T ENUM$VALUES[];
    static
    {
        SPRING = new T("SPRING", 0);
        SUMMER = new T("SUMMER", 1);
        ENUM$VALUES = (new T[] {
            SPRING, SUMMER
        });
    }
}

Después de descompilar el código, podemos ver que la clase final pública T extiende Enum indica que esta clase hereda la clase Enum, y la palabra clave final nos dice que esta clase no se puede heredar.

Cuando usamos enmu para definir un tipo de enumeración, el compilador creará automáticamente una clase de tipo final para que heredemos la clase Enum, por lo que el tipo de enumeración no se puede heredar.

3.6 Clases internas

La clase interna también se denomina clase anidada, y la clase interna puede entenderse como un miembro ordinario de la clase externa.

La razón por la que una clase interna también es azúcar sintáctica es porque es solo un concepto de tiempo de compilación.

Una clase interna interna se define en externa.java. Una vez compilada con éxito, se generarán dos archivos .clase completamente diferentes, a saber, externa.clase y externa$inner.clase. Entonces, el nombre de la clase interna puede ser el mismo que el nombre de la clase externa.

public class OutterClass {
    private String userName;

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public static void main(String[] args) {

    }

    class InnerClass{
        private String name;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }
}

Después de compilar el código anterior, se generarán dos archivos de clase: OutterClass$InnerClass.class y OutterClass.class.

Cuando intentemos usar jad para descompilar el archivo OutterClass.class, la línea de comando imprimirá lo siguiente:

Parsing OutterClass.class...
Parsing inner class OutterClass$InnerClass.class...
Generating OutterClass.jad

Descompilará los dos archivos y luego generará un archivo OutterClass.jad juntos. El contenido del archivo es el siguiente:

public class OutterClass
{
    class InnerClass
    {
        public String getName()
        {
            return name;
        }
        public void setName(String name)
        {
            this.name = name;
        }
        private String name;
        final OutterClass this$0;

        InnerClass()
        {
            this.this$0 = OutterClass.this;
            super();
        }
    }

    public OutterClass()
    {
    }
    public String getUserName()
    {
        return userName;
    }
    public void setUserName(String userName){
        this.userName = userName;
    }
    public static void main(String args1[])
    {
    }
    private String userName;
}

3.7 Compilación condicional

—En circunstancias normales, cada línea de código del programa debe participar en la compilación. Pero a veces, por el bien de la optimización del código del programa, desea compilar solo una parte del contenido. En este momento, debe agregar condiciones al programa, de modo que el compilador solo pueda compilar el código que cumple con las condiciones y compilar el código que no cumple las condiciones Abandonado, esta es una compilación condicional.

Como en C o CPP, la compilación condicional se puede lograr a través de sentencias preparadas. De hecho, la compilación condicional también se puede implementar en Java. Veamos primero un fragmento de código:

public class ConditionalCompilation {
    public static void main(String[] args) {
        final boolean DEBUG = true;
        if(DEBUG) {
            System.out.println("Hello, DEBUG!");
        }

        final boolean ONLINE = false;

        if(ONLINE){
            System.out.println("Hello, ONLINE!");
        }
    }
}

El código descompilado es el siguiente:

public class ConditionalCompilation
{

    public ConditionalCompilation()
    {
    }

    public static void main(String args[])
    {
        boolean DEBUG = true;
        System.out.println("Hello, DEBUG!");
        boolean ONLINE = false;
    }
}

En primer lugar, encontramos que no hay System.out.println("¡Hola, EN LÍNEA!") en el código descompilado, que en realidad es una compilación condicional.

Cuando if(ONLINE) es falso, el compilador no compila el código en él.

Por tanto, la compilación condicional de la gramática Java se realiza mediante la sentencia if cuya condición de juicio es constante. Según sea verdadero o falso de la condición if, el compilador elimina directamente el bloque de código cuya rama es falsa. La compilación condicional realizada por este método debe realizarse en el cuerpo del método, y la compilación condicional no puede realizarse sobre la estructura de toda la clase Java o los atributos de la clase.

De hecho, esto es más limitado que la compilación condicional de C/C++. Al comienzo del diseño del lenguaje Java, no se introdujo la función de compilación condicional, aunque existen limitaciones, es mejor que nada.

3.8 Afirmaciones

En Java, la palabra clave assert se introdujo desde JAVA SE 1.4 Para evitar errores causados ​​por el uso de la palabra clave assert en la versión anterior del código Java, Java no habilita la verificación de aserciones de forma predeterminada al ejecutar (en este momento, todas las declaraciones de aserciones son ignorados!).

Si desea habilitar la verificación de aserciones, debe usar el modificador -enableassertions o -ea para habilitarlo.

Considere una pieza de código que contiene aserciones:

public class AssertTest {
    public static void main(String args[]) {
        int a = 1;
        int b = 1;
        assert a == b;
        System.out.println("公众号:Hollis");
        assert a != b : "Hollis";
        System.out.println("博客:www.hollischuang.com");
    }
}

El código descompilado es el siguiente:

public class AssertTest {
   public AssertTest()
    {
    }
    public static void main(String args[])
{
    int a = 1;
    int b = 1;
    if(!$assertionsDisabled && a != b)
        throw new AssertionError();
    System.out.println("\u516C\u4F17\u53F7\uFF1AHollis");
    if(!$assertionsDisabled && a == b)
    {
        throw new AssertionError("Hollis");
    } else
    {
        System.out.println("\u535A\u5BA2\uFF1Awww.hollischuang.com");
        return;
    }
}

static final boolean $assertionsDisabled = !com/hollis/suguar/AssertTest.desiredAssertionStatus();


}

Obviamente, el código descompilado es mucho más complicado que nuestro propio código. Por lo tanto, ahorramos mucho código usando el azúcar sintáctico de assert.

De hecho, la implementación subyacente de la aserción es el lenguaje if. Si el resultado de la aserción es verdadero, no se hará nada y el programa continuará ejecutándose. Si el resultado de la aserción es falso, el programa lanzará un AssertError para interrumpir el ejecución del programa.

-enableassertions establecerá el valor del campo $assertionsDisabled.

3.9 Literales numéricos

En Java 7, los literales numéricos, ya sean números enteros o de coma flotante, permiten insertar cualquier número de guiones bajos entre números. Estos guiones bajos no afectarán el valor literal, el propósito es facilitar la lectura.

Por ejemplo:

public class Test {
    public static void main(String... args) {
        int i = 10_000;
        System.out.println(i);
    }
}

Después de la descompilación:

public class Test
{
  public static void main(String[] args)
  {
    int i = 10000;
    System.out.println(i);
  }
}

Después de la descompilación, _ se elimina. Es decir, el compilador no reconoce el _ en el literal numérico y necesita eliminarlo en la etapa de compilación.

3.10 para cada uno

Se cree que el bucle for mejorado (for-each) es familiar para todos. A menudo se usa en el desarrollo diario. Escribirá mucho menos código que el bucle for. Entonces, ¿cómo se implementa este azúcar sintáctico?

public static void main(String... args) {
    String[] strs = {"Hollis", "公众号:Hollis", "博客:www.hollischuang.com"};
    for (String s : strs) {
        System.out.println(s);
    }
    List<String> strList = ImmutableList.of("Hollis", "公众号:Hollis", "博客:www.hollischuang.com");
    for (String s : strList) {
        System.out.println(s);
    }
}

El código descompilado es el siguiente:

public static transient void main(String args[])
{
    String strs[] = {
        "Hollis", "\u516C\u4F17\u53F7\uFF1AHollis", "\u535A\u5BA2\uFF1Awww.hollischuang.com"
    };
    String args1[] = strs;
    int i = args1.length;
    for(int j = 0; j < i; j++)
    {
        String s = args1[j];
        System.out.println(s);
    }

    List strList = ImmutableList.of("Hollis", "\u516C\u4F17\u53F7\uFF1AHollis", "\u535A\u5BA2\uFF1Awww.hollischuang.com");
    String s;
    for(Iterator iterator = strList.iterator(); iterator.hasNext(); System.out.println(s))
        s = (String)iterator.next();

}

El código es muy simple, y el principio de for-each es en realidad usar bucles for ordinarios e iteradores.

3.11 probar con recurso

En Java, los recursos que son muy costosos, como las operaciones de archivos, los flujos de E/S y las conexiones de bases de datos, deben cerrarse con el método de cierre en el tiempo posterior al uso; de lo contrario, los recursos siempre estarán abiertos, lo que puede causar problemas como fugas de memoria. .

La forma común de cerrar un recurso es liberarlo en el bloque finalmente, es decir, llamar al método de cierre. Por ejemplo, a menudo escribimos código como este:

public static void main(String[] args) {
    BufferedReader br = null;
    try {
        String line;
        br = new BufferedReader(new FileReader("d:\\hollischuang.xml"));
        while ((line = br.readLine()) != null) {
            System.out.println(line);
        }
    } catch (IOException e) {
        // handle exception
    } finally {
        try {
            if (br != null) {
                br.close();
            }
        } catch (IOException ex) {
            // handle exception
        }
    }
}

A partir de Java 7, jdk proporciona una mejor manera de cerrar recursos. Use la instrucción try-with-resources para reescribir el código anterior. El efecto es el siguiente:

public static void main(String... args) {
    try (BufferedReader br = new BufferedReader(new FileReader("d:\\ hollischuang.xml"))) {
        String line;
        while ((line = br.readLine()) != null) {
            System.out.println(line);
        }
    } catch (IOException e) {
        // handle exception
    }
}

Mire, esto es una gran ayuda. Aunque normalmente uso IOUtils para cerrar el flujo antes, no uso el método de escribir mucho código finalmente, pero este nuevo azúcar sintáctico parece ser mucho más elegante.

Descompile el código anterior para ver el principio detrás de él:

public static transient void main(String args[])
    {
        BufferedReader br;
        Throwable throwable;
        br = new BufferedReader(new FileReader("d:\\ hollischuang.xml"));
        throwable = null;
        String line;
        try
        {
            while((line = br.readLine()) != null)
                System.out.println(line);
        }
        catch(Throwable throwable2)
        {
            throwable = throwable2;
            throw throwable2;
        }
        if(br != null)
            if(throwable != null)
                try
                {
                    br.close();
                }
                catch(Throwable throwable1)
                {
                    throwable.addSuppressed(throwable1);
                }
            else
                br.close();
            break MISSING_BLOCK_LABEL_113;
            Exception exception;
            exception;
            if(br != null)
                if(throwable != null)
                    try
                    {
                        br.close();
                    }
                    catch(Throwable throwable3)
                      {
                        throwable.addSuppressed(throwable3);
                    }
                else
                    br.close();
        throw exception;
        IOException ioexception;
        ioexception;
    }
}

De hecho, el principio detrás de esto también es muy simple, el compilador ha hecho por nosotros las operaciones de cierre de recursos que nosotros no hicimos.

Por lo tanto, se confirma nuevamente que la función del azúcar sintáctico es facilitar el uso de los programadores, pero al final debe convertirse a un lenguaje que el compilador entienda.

3.12 Expresiones lambda

Con respecto a las expresiones lambda, algunas personas pueden tener dudas, porque algunas personas en Internet dicen que no es azúcar sintáctico. En realidad, quiero corregir esta afirmación.

Las expresiones de Labmda no son azúcar sintáctica para clases internas anónimas, pero también lo son. El método de implementación en realidad se basa en varias API relacionadas con lambda proporcionadas por la capa inferior de la JVM.

Veamos primero una expresión lambda simple. Iterar sobre una lista:

public static void main(String... args) {
    List<String> strList = ImmutableList.of("Hollis", "公众号:Hollis", "博客:www.hollischuang.com");

    strList.forEach( s -> { System.out.println(s); } );
}

¿Por qué dices que no es el azúcar sintáctico de la clase interna? Anteriormente dijimos que la clase interna tendrá dos archivos de clase después de la compilación, pero la clase que contiene la expresión lambda solo tendrá un archivo después de la compilación.

El código descompilado es el siguiente:

public static /* varargs */ void main(String ... args) {
    ImmutableList strList = ImmutableList.of((Object)"Hollis", (Object)"\u516c\u4f17\u53f7\uff1aHollis", (Object)"\u535a\u5ba2\uff1awww.hollischuang.com");
    strList.forEach((Consumer<String>)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)V, lambda$main$0(java.lang.String ), (Ljava/lang/String;)V)());
}

private static /* synthetic */ void lambda$main$0(String s) {
    System.out.println(s);
}

Se puede ver que en el método forEach, en realidad se llama al método java.lang.invoke.LambdaMetafactory#metafactory, y el cuarto parámetro implMethod del método especifica la implementación del método. Se puede ver que aquí se llama a un método lambda$main$0 para la salida.

Veamos uno un poco más complicado, primero filtremos la Lista y luego emitamos:

public static void main(String... args) {
    List<String> strList = ImmutableList.of("Hollis", "公众号:Hollis", "博客:www.hollischuang.com");

    List HollisList = strList.stream().filter(string -> string.contains("Hollis")).collect(Collectors.toList());

    HollisList.forEach( s -> { System.out.println(s); } );
}

El código descompilado es el siguiente:

public static /* varargs */ void main(String ... args) {
    ImmutableList strList = ImmutableList.of((Object)"Hollis", (Object)"\u516c\u4f17\u53f7\uff1aHollis", (Object)"\u535a\u5ba2\uff1awww.hollischuang.com");
    List<Object> HollisList = strList.stream().filter((Predicate<String>)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)Z, lambda$main$0(java.lang.String ), (Ljava/lang/String;)Z)()).collect(Collectors.toList());
    HollisList.forEach((Consumer<Object>)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)V, lambda$main$1(java.lang.Object ), (Ljava/lang/Object;)V)());
}

private static /* synthetic */ void lambda$main$1(Object s) {
    System.out.println(s);
}

private static /* synthetic */ boolean lambda$main$0(String string) {
    return string.contains("Hollis");
}

Las dos expresiones lambda llaman a los métodos lambda$main$1 y lambda$main$0 respectivamente.

Por lo tanto, la implementación de las expresiones lambda en realidad se basa en algunas API subyacentes. Durante la fase de compilación, el compilador elimina las expresiones lambda y las convierte en formas de llamar a las API internas.

4 pozos que se pueden encontrar

4.1 Genéricos: cuando los genéricos se sobrecargan

public class GenericTypes {

    public static void method(List<String> list) {  
        System.out.println("invoke method(List<String> list)");  
    }  

    public static void method(List<Integer> list) {  
        System.out.println("invoke method(List<Integer> list)");  
    }  
}  

El código anterior tiene dos funciones sobrecargadas porque sus tipos de parámetros son diferentes, uno es List y el otro es List. Sin embargo, este código no se puede compilar. Como dijimos anteriormente, los parámetros List y List se borran después de la compilación y se convierten en el mismo tipo nativo List. La acción de borrar hace que las firmas de características de estos dos métodos se vuelvan exactamente iguales.

4.2 Genéricos - cuando los genéricos encuentran trampa

Los parámetros de tipo genérico no se pueden utilizar en sentencias catch para el manejo de excepciones de Java. Porque el manejo de excepciones lo realiza la JVM en tiempo de ejecución. Dado que la información de tipo se borra, la JVM no puede distinguir entre los dos tipos de excepción MyException<String> y MyException<Integer>

4.3 Genéricos: cuando las variables estáticas se incluyen en los genéricos

public class StaticTest{
    public static void main(String[] args){
        GT<Integer> gti = new GT<Integer>();
        gti.var=1;
        GT<String> gts = new GT<String>();
        gts.var=2;
        System.out.println(gti.var);
    }
}
class GT<T>{
    public static int var=0;
    public void nothing(T x){}
}

La salida del código anterior es: 2! Debido al borrado de tipo, todas las instancias de clase genérica se asocian con el mismo código de bytes y se comparten todas las variables estáticas de la clase genérica.

4.4 Autoboxing y unboxing: comparación de igualdad de objetos

public static void main(String[] args) {
    Integer a = 1000;
    Integer b = 1000;
    Integer c = 100;
    Integer d = 100;
    System.out.println("a == b is " + (a == b));
    System.out.println(("c == d is " + (c == d)));
}

Resultado de salida:

a == b is false
c == d is true

En Java 5, se introdujo una nueva función en las operaciones en Integer para ahorrar memoria y mejorar el rendimiento. Los objetos enteros permiten el almacenamiento en caché y la reutilización mediante el uso de la misma referencia de objeto.

Funciona con valores enteros en el rango -128 a +127.

Se aplica solo al autoboxing. No se aplica la creación de un objeto mediante un constructor.

4.5 Bucle for mejorado

for (Student stu : students) {    
    if (stu.getId() == 2)     
        students.remove(stu);    
}

Se lanzará una ConcurrentModificationException.

Iterator trabaja en un subproceso independiente y posee un bloqueo mutex. Después de que se crea el iterador, creará una tabla de índice de enlace único que apunta al objeto original. Cuando cambia el número de objetos originales, el contenido de la tabla de índice no cambiará sincrónicamente, por lo que cuando el puntero del índice se mueva hacia atrás, lo hará. no se encuentra para iterar.objeto, por lo que de acuerdo con el principio de falla rápida, Iterator lanzará java.util.ConcurrentModificationException inmediatamente.

Por lo tanto, el iterador no permite cambiar el objeto iterado mientras está funcionando. Pero puede usar el método remove() de Iterator para eliminar el objeto, y el método Iterator.remove() mantendrá la consistencia del índice mientras elimina el objeto de iteración actual.

5 resumen

La sección anterior presentó 12 azúcares sintácticos de uso común en Java. Debido a problemas de espacio, existen otros azúcares sintácticos comunes, como la concatenación de cadenas basada en StringBuilder.La palabra clave var en Java 10 declara variables locales mediante la inferencia de tipo inteligente y no se mencionará aquí.

El llamado azúcar sintáctico es solo un tipo de sintaxis proporcionada a los desarrolladores para facilitar el desarrollo. Pero esta sintaxis solo la conocen los desarrolladores. Para ser ejecutado, necesita ser desazucarado, es decir, convertido a una sintaxis reconocida por la JVM.

Cuando le quitamos el azúcar a la gramática, encontrará que las gramáticas convenientes que usamos todos los días en realidad están compuestas por otras gramáticas más simples.

Con estos azúcares gramaticales podemos mejorar mucho la eficiencia en el desarrollo diario, pero al mismo tiempo evitar su uso excesivo. Lo mejor es entender el principio antes de usarlo para evitar caer en el pozo.

Supongo que te gusta

Origin blog.csdn.net/qq_37284798/article/details/129681119
Recomendado
Clasificación