Hola Habr! Les presento la traducción del artículo "
5 secretos ocultos en Java " de
Justin Albano .
¿Quieres convertirte en un Jedi de Java? Descubre los antiguos secretos de Java. Nos centraremos en ampliar las anotaciones, la inicialización, los comentarios y las interfaces de enumeración.
Con el desarrollo de lenguajes de programación, las funciones ocultas también comienzan a aparecer, y las construcciones en las que los fundadores nunca pensaron se están extendiendo cada vez más para uso general. Algunas de estas funciones se aceptan generalmente en el idioma, mientras que otras se trasladan a los rincones más oscuros de la comunidad lingüística. En este artículo, veremos cinco secretos que a menudo pasan por alto muchos desarrolladores de Java (para ser justos, algunos de ellos tienen buenas razones para esto). Consideraremos tanto las opciones para su uso como las razones que llevaron a la aparición de cada función, así como algunos ejemplos que demuestran cuándo es aconsejable usar estas funciones.
El lector debe comprender que no todas estas funciones están realmente ocultas, simplemente no se usan con frecuencia en la programación diaria. Algunos de ellos pueden ser muy útiles en el momento adecuado, mientras que usar otros es casi siempre una mala idea, y se muestran en este artículo para interesar al lector (y posiblemente hacer que se ría). El lector también debe decidir cuándo usar las funciones descritas en este artículo: "El hecho de que esto se pueda hacer no significa que deba hacerse".
1. Implementar anotaciones
Comenzando con el Java Development Kit (JDK) 5, las anotaciones son una parte integral de muchas aplicaciones y entornos Java. En la gran mayoría de los casos, las anotaciones se aplican a construcciones como clases, campos, métodos, etc. Sin embargo, también se pueden usar como interfaces implementadas. Por ejemplo, supongamos que tenemos la siguiente definición de anotación:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Test { String name(); }
Usualmente aplicamos esta anotación a un método como se muestra a continuación:
public class MyTestFixure { @Test public void givenFooWhenBarThenBaz() {
Entonces podemos procesar esta anotación como se describe en
Creación de anotaciones en Java . Si también quisiéramos crear una interfaz que nos permita crear pruebas como objetos, tendríamos que crear una nueva interfaz, llamándola de otra manera, y no Prueba:
public interface TestInstance { public String getName(); }
A continuación, podemos crear una instancia del objeto TestInstance:
public class FooTestInstance implements TestInstance { @Override public String getName() { return "Foo"; } } TestInstance myTest = new FooTestInstance();
Aunque nuestra anotación e interfaz son casi idénticas, con una duplicación muy notable, parece que no hay forma de combinar estas dos construcciones. Afortunadamente, las apariencias engañan, y hay un método para combinar estas dos construcciones: Implementación de anotaciones:
public class FooTest implements Test { @Override public String name() { return "Foo"; } @Override public Class<? extends Annotation> annotationType() { return Test.class; } }
Tenga en cuenta que debemos implementar el método annotationType y también devolver el tipo de anotación, ya que esta es una parte implícita de la interfaz Annotation. Aunque en la mayoría de los casos la implementación de anotaciones no es la solución adecuada para el diseño (el compilador de Java mostrará una advertencia al implementar la interfaz), esto puede ser útil en algunos casos, por ejemplo, en el marco basado en anotaciones.
2. Bloques de inicialización no estáticos.
En Java, como en la mayoría de los lenguajes de programación orientados a objetos, los objetos se crean exclusivamente utilizando el constructor (con algunas excepciones, como deserializar objetos Java). Incluso cuando creamos métodos de fábrica estáticos para crear objetos, simplemente encerramos una llamada en el constructor del objeto para instanciarlo. Por ejemplo:
public class Foo { private final String name; private Foo(String name) { this.name = name; } public static Foo withName(String name) { return new Foo(name); } } Foo foo = Foo.withName("Bar");
Por lo tanto, cuando queremos inicializar un objeto, combinamos la lógica de inicialización en el constructor del objeto. Por ejemplo, establecemos el campo de nombre de la clase Foo en su constructor parametrizado. Aunque puede parecer razonable suponer que toda la lógica de inicialización está en el constructor o conjunto de constructores para la clase, este no es el caso en Java. En cambio, podemos usar
bloques de inicialización no estáticos para ejecutar código al crear un objeto:
public class Foo { { System.out.println("Foo:instance 1"); } public Foo() { System.out.println("Foo:constructor"); } }
Los bloques de inicialización no estáticos se especifican agregando lógica de inicialización a un conjunto de llaves en la definición de clase. Cuando se crea un objeto, primero se llaman los bloques de inicialización no estáticos y luego los constructores del objeto. Tenga en cuenta que puede especificar más de un bloque de inicialización no estático, en cuyo caso cada uno se llama en el orden en que se especifica en la definición de clase. Además de los bloques de inicialización no estáticos, también podemos crear bloques estáticos que se ejecutan cuando la clase se carga en la memoria. Para crear un bloque de inicialización estática, simplemente agregamos la palabra clave estática:
public class Foo { { System.out.println("Foo:instance 1"); } static { System.out.println("Foo:static 1"); } public Foo() { System.out.println("Foo:constructor"); } }
Cuando los tres métodos de inicialización están presentes en la clase (constructores, bloques de inicialización no estáticos y bloques de inicialización estática), los estáticos siempre se ejecutan primero (cuando la clase se carga en la memoria) en el orden de su declaración, luego los bloques de inicialización no estáticos se ejecutan en el orden en que se declaran, y después de ellos, los diseñadores. Cuando se introduce una superclase, el orden de ejecución cambia un poco:
- Bloques de inicialización de superclase estática, en el orden de su declaración
- Bloques de inicialización de subclase estática, en el orden de su declaración
- Bloques de inicialización de superclase no estáticos, en el orden en que se declaran
- Constructor de superclase
- Bloques de inicialización de subclase no estáticos, en el orden en que se declaran
- Constructor de subclase
Por ejemplo, podemos crear la siguiente aplicación:
public abstract class Bar { private String name; static { System.out.println("Bar:static 1"); } { System.out.println("Bar:instance 1"); } static { System.out.println("Bar:static 2"); } public Bar() { System.out.println("Bar:constructor"); } { System.out.println("Bar:instance 2"); } public Bar(String name) { this.name = name; System.out.println("Bar:name-constructor"); } } public class Foo extends Bar { static { System.out.println("Foo:static 1"); } { System.out.println("Foo:instance 1"); } static { System.out.println("Foo:static 2"); } public Foo() { System.out.println("Foo:constructor"); } public Foo(String name) { super(name); System.out.println("Foo:name-constructor"); } { System.out.println("Foo:instance 2"); } public static void main(String... args) { new Foo(); System.out.println(); new Foo("Baz"); } }
Si ejecutamos este código, obtenemos el siguiente resultado:
Bar:static 1 Bar:static 2 Foo:static 1 Foo:static 2 Bar:instance 1 Bar:instance 2 Bar:constructor Foo:instance 1 Foo:instance 2 Foo:constructor Bar:instance 1 Bar:instance 2 Bar:name-constructor Foo:instance 1 Foo:instance 2 Foo:name-constructor
Tenga en cuenta que los bloques de inicialización estática se ejecutaron solo una vez, incluso si se crearon dos objetos Foo. Aunque los bloques de inicialización no estadísticos y estáticos pueden ser útiles, la lógica de inicialización debe colocarse en los constructores, y los métodos (o métodos estáticos) deben usarse en casos donde la lógica compleja requiere inicializar el estado del objeto.
3. Inicialización de paréntesis doble
Muchos lenguajes de programación incluyen algún tipo de mecanismo de sintaxis para crear rápida y brevemente una lista o mapa (o diccionario) sin usar un código de plantilla detallado. Por ejemplo, C ++ incluye la
inicialización de paréntesis , que permite a los desarrolladores crear rápidamente una lista de valores enumerados o incluso inicializar objetos completos si el constructor del objeto admite esta función. Desafortunadamente, antes de JDK 9, dicha función no se implementó (más sobre eso más adelante). Para crear simplemente una lista de objetos, haríamos lo siguiente:
List<Integer> myInts = new ArrayList<>(); myInts.add(1); myInts.add(2); myInts.add(3);
Aunque esto cumple nuestro objetivo de crear una nueva lista inicializada con tres valores, es demasiado detallada, lo que requiere que el desarrollador repita el nombre de la variable de lista para cada adición. Para acortar este código, podemos usar la
inicialización doble de corchetes :
List < Integer >List<Integer> myInts = new ArrayList<>() {{ add(1); add(2); add(3); }};
Una inicialización de doble paréntesis, que deriva su nombre de un conjunto de dos llaves rizadas abiertas y cerradas, es en realidad una colección de varios elementos de sintaxis. Primero, creamos
una clase interna anónima que extiende la clase ArrayList. Como ArrayList no tiene métodos abstractos, podemos crear un cuerpo vacío para una implementación anónima:
List<Integer> myInts = new ArrayList<>() {};
Usando este código, esencialmente creamos una subclase anónima, ArrayList es exactamente la misma que la ArrayList original. Una de las principales diferencias es que nuestra clase interna tiene una referencia implícita a la clase que lo contiene (en forma de una variable capturada por esto), porque Creamos una clase interna no estática. Esto nos permite escribir una lógica interesante, si no confusa. Por ejemplo, al agregar esta variable a una clase interna anónima inicializada con un paréntesis doble:
public class Foo { public List<Foo> getListWithMeIncluded() { return new ArrayList<Foo>() {{ add(Foo.this); }}; } public static void main(String... args) { Foo foo = new Foo(); List<Foo> fooList = foo.getListWithMeIncluded(); System.out.println(foo.equals(fooList.get(0))); } }
Si esta clase interna se definiera como estática, no tendríamos acceso a Foo.this. Por ejemplo, el siguiente código que crea una clase interna FooArrayList estática no tiene acceso al enlace Foo.this y, por lo tanto, no se compila:
public class Foo { public List<Foo> getListWithMeIncluded() { return new FooArrayList(); } private static class FooArrayList extends ArrayList<Foo> {{ add(Foo.this); }} }
Al reanudar la construcción con nuestra ArrayList inicializada con dos corchetes, una vez que se ha creado una clase interna no estática, utilizamos bloques de inicialización no estática, como se describió anteriormente, para agregar los tres elementos iniciales al instanciar una clase interna anónima. Cuando se crea una clase interna anónima y cuando solo hay un objeto de una clase interna anónima, podemos decir que creamos un objeto interno no estático que agrega tres elementos iniciales cuando se crea. Esto se verá si separamos un par de llaves, donde una llave representa la definición de una clase interna anónima y la otra marca el comienzo de la lógica de inicialización de la instancia:
List<Integer> myInts = new ArrayList<>() { { add(1); add(2); add(3); } };
Aunque este truco puede ser útil, JDK 9 (
JEP 269 ) ha reemplazado la utilidad de este truco con un conjunto de métodos de fábrica estáticos para List (así como muchos otros tipos de colecciones). Por ejemplo, podríamos crear una Lista anteriormente usando estos métodos de fábrica estáticos, como se muestra a continuación:
List<Integer> myInts = List.of(1, 2, 3);
Esta técnica de fábrica estática se usa por dos razones principales: (1) no se crea una clase interna anónima y (2) para reducir el código estándar necesario para crear una Lista. Debe recordarse que en este caso el resultado de la Lista no cambia y no se puede cambiar después de su creación. Para crear un archivo de Lista mutable con cualquier elemento inicial, tenemos que usar un método regular o un método con un paréntesis de inicialización doble.
Tenga en cuenta que la inicialización simple, el paréntesis doble y los métodos de fábrica estáticos JDK 9 no solo están disponibles para List. Están disponibles para establecer y asignar objetos, como se muestra en el siguiente fragmento de código:
Es importante comprender cómo se inicializa el paréntesis doble antes de decidir sobre su uso. Esto mejora la legibilidad del código, pero pueden aparecer algunos efectos secundarios.
4. Comentarios ejecutables
Los comentarios son una parte integral de casi todos los programas, y la principal ventaja de los comentarios es que no se ejecutan. Esto se vuelve aún más obvio cuando comentamos una línea de código en nuestro programa: queremos guardar el código en nuestra aplicación, pero no queremos que se ejecute. Por ejemplo, el siguiente programa muestra "5" como resultado:
public static void main(String args[]) { int value = 5;
Mucha gente piensa que los comentarios nunca se ejecutan, pero esto no es del todo cierto. Por ejemplo, ¿cuál será el resultado del siguiente fragmento de código?
public static void main(String args[]) { int value = 5;
Podría suponer que esto es nuevamente 5, pero si ejecutamos el código anterior, veremos 8 en la salida. La razón de este "error" es el carácter Unicode \ u000d; Este carácter es en realidad un
retorno de carro Unicode , y el compilador utiliza el código fuente de Java como archivos de texto en formato Unicode. Su adición al código establece el valor = 8 en la línea que sigue al comentario, asegurando su ejecución. Esto significa que el fragmento de código anterior es en realidad igual al siguiente:
public static void main(String args[]) { int value = 5;
Aunque esto parece un error de Java, en realidad es una característica especialmente agregada al lenguaje. El objetivo inicial era crear un lenguaje independiente de la plataforma (de ahí la creación de una máquina virtual Java o JVM), y la interoperabilidad del código fuente es un aspecto clave de este objetivo. Al permitir que el código fuente de Java contenga caracteres Unicode, podemos usar caracteres no latinos de manera universal. Esto garantiza que el código escrito en una región del mundo (que puede contener caracteres no latinos, como en los comentarios), se pueda ejecutar en cualquier otra. Consulte la
Sección 3.3 Especificaciones del lenguaje Java o JLS para obtener más información.
Podemos llevar esto al extremo e incluso escribir una aplicación completa en Unicode. Por ejemplo, ¿qué hace el siguiente programa (código fuente, derivado de
Java: ¿Ejecución de código en comentarios? )
\u0070\u0075\u0062\u006c\u0069\u0063\u0020\u0020\u0020\u0020 \u0063\u006c\u0061\u0073\u0073\u0020\u0055\u0067\u006c\u0079 \u007b\u0070\u0075\u0062\u006c\u0069\u0063\u0020\u0020\u0020 \u0020\u0020\u0020\u0020\u0073\u0074\u0061\u0074\u0069\u0063 \u0076\u006f\u0069\u0064\u0020\u006d\u0061\u0069\u006e\u0028 \u0053\u0074\u0072\u0069\u006e\u0067\u005b\u005d\u0020\u0020 \u0020\u0020\u0020\u0020\u0061\u0072\u0067\u0073\u0029\u007b \u0053\u0079\u0073\u0074\u0065\u006d\u002e\u006f\u0075\u0074 \u002e\u0070\u0072\u0069\u006e\u0074\u006c\u006e\u0028\u0020 \u0022\u0048\u0065\u006c\u006c\u006f\u0020\u0077\u0022\u002b \u0022\u006f\u0072\u006c\u0064\u0022\u0029\u003b\u007d\u007d
Si coloca el código anterior en un archivo llamado Ugly.java y lo ejecuta, Hello world se imprimirá en la salida estándar. Si convertimos estos caracteres Unicode en caracteres del
Código Estándar Americano para el Intercambio de Información (ASCII) , obtenemos el siguiente programa:
public class Ugly { public static void main(String[] args){ System.out.println("Hello w"+"orld"); } }
Por lo tanto, los caracteres Unicode se pueden incluir en el código fuente de Java, sin embargo, si no son necesarios, se recomienda encarecidamente no usarlos (por ejemplo, para incluir caracteres no latinos en los comentarios). Sin embargo, si se requieren, asegúrese de que no incluyan caracteres, como retornos de carro, que modifiquen el comportamiento esperado del código fuente.
5. Implementación de la interfaz Enum
Una de las limitaciones de las enumeraciones (una lista de enumeración) en comparación con otras clases en Java es que las enumeraciones no pueden extender otra clase o enumeraciones por sí mismas. Por ejemplo, no puede hacer lo siguiente:
public class Speaker { public void speak() { System.out.println("Hi"); } } public enum Person extends Speaker { JOE("Joseph"), JIM("James"); private final String name; private Person(String name) { this.name = name; } } Person.JOE.speak();
Sin embargo, podemos obligar a nuestras enumeraciones a implementar la interfaz y proporcionar una implementación para sus métodos abstractos de la siguiente manera:
public interface Speaker { public void speak(); } public enum Person implements Speaker { JOE("Joseph"), JIM("James"); private final String name; private Person(String name) { this.name = name; } @Override public void speak() { System.out.println("Hi"); } } Person.JOE.speak();
Ahora podemos usar una instancia de Persona donde se requiera un objeto Speaker. Además, también podemos garantizar la implementación de métodos abstractos de interfaz de forma continua (los llamados métodos específicos de las constantes):
public interface Speaker { public void speak(); } public enum Person implements Speaker { JOE("Joseph") { public void speak() { System.out.println("Hi, my name is Joseph"); } }, JIM("James"){ public void speak() { System.out.println("Hey, what's up?"); } }; private final String name; private Person(String name) { this.name = name; } @Override public void speak() { System.out.println("Hi"); } } Person.JOE.speak();
A diferencia de algunos de los otros secretos de este artículo, esta técnica solo debe usarse cuando sea necesario. Por ejemplo, si se puede usar una constante enum, como JOE o JIM, en lugar de una interfaz como Speaker, entonces la definición de una constante debe implementar este tipo de interfaz. Consulte el Párrafo 38 (p. 176-9)
Java efectivo, 3a edición para obtener más información.
Conclusión
En este artículo, examinamos cinco secretos ocultos en Java, a saber: (1) las anotaciones se pueden extender, (2) los bloques de inicialización no estáticos se pueden usar para configurar un objeto cuando se crea, (3) la inicialización con corchetes dobles se puede usar para ejecutar instrucciones al crear una clase interna anónima, (4) los comentarios a veces se pueden ejecutar, y (5) las enumeraciones pueden implementar interfaces. Aunque estas funciones son utilizadas por cierto tipo de tarea, algunas de ellas deben evitarse (por ejemplo, crear comentarios ejecutables). Cuando decida usar estos secretos, asegúrese de observar la regla: "El hecho de que esto se pueda hacer no significa que deba hacerse".