Aquí hay una lista de 10 prácticas de codificación de Java que son más sutiles que las reglas efectivas de Java de Josh Bloch. En comparación con la lista de Josh Bloch que es fácil de aprender y se centra en situaciones diarias, esta lista contendrá situaciones que involucran cosas poco comunes en el diseño API/SPI, que pueden tener un gran impacto.
Los he encontrado mientras escribí y mantengo a Jooq (DSL interno modelado de SQL en Java). Como DSL interno, Jooq desafía a los compiladores de Java y los genéricos en la mayor medida, combinando genéricos, parámetros mutables y sobrecargas, que Josh Bloch puede no recomendar para una API tan amplia.
Déjame compartir con ustedes 10 mejores prácticas sutiles para la codificación de Java:
1. Recuerda el destructor de C ++
¿Recuerdas el destructor de C ++? ¿No recuerdas? Entonces tienes mucha suerte porque no tienes que depurar las filtraciones de memoria debido a la memoria asignada después de que no se libera la eliminación de objetos. ¡Gracias a Sun/Oracle por el mecanismo de recolección de basura implementado!
Sin embargo, el destructor proporciona una característica interesante. Entiende la orden de asignación inversa para liberar la memoria. Recuerde que este es el caso en Java, cuando opera la sintaxis del destructor de clase:
Hay varios otros casos de uso. Aquí hay un ejemplo concreto de cómo implementar SPI de algunos oyentes de eventos:
@OverridePublic Void BeforeVent (eventContext E) {super.beforeEvent (e); // Super Code antes de mi código} @OverridePublic Void AfterEvent (EventContext E) {// Super Code después de mi código Super.AfTerEvent (E);} El infame problema gastronómico del filósofo es otro buen ejemplo de por qué importa. Para obtener preguntas sobre la cena filósofa, consulte el enlace:
http://adit.io/posts/2013-05-05-11-ting-philosophers-problem-with-ron-swanson.html
Regla : Siempre que use antes/After, asigne/gratis, tome/devuelva la semántica para implementar la lógica, considere si realizar operaciones después/gratis/return en orden inverso.
Proporcione a los clientes métodos SPI que les faciliten inyectar comportamientos personalizados en su biblioteca/código. Tenga cuidado de que sus juicios de evolución SPI puedan confundirlo, haciéndole pensar que (no) tiene la intención de necesitar parámetros adicionales. Por supuesto, las funciones no deben agregarse demasiado temprano. Pero una vez que publique su SPI, una vez que decida seguir las versiones semánticas, realmente lamentará haber agregado un parámetro estúpido a su SPI cuando se dé cuenta de que en algunos casos puede necesitar otro parámetro:
interfaz EventListener {// mensaje void malo (mensaje de cadena);}¿Qué pasa si necesita una ID de mensaje y una fuente de mensaje? La evolución de la API le impedirá agregar parámetros a los tipos anteriores. Por supuesto, con Java 8, puede agregar un método de defensor que "defienda" sus malas decisiones de diseño en los primeros días:
Interface EventListener {// Mensaje void predeterminado incorrecto (mensaje de cadena) {Mensaje (Mensaje, NULL, NULL); } // ¿Mejor? Mensaje void (mensaje de cadena, ID de entero, fuente de mensajes);} Tenga en cuenta que desafortunadamente, el método de defensa no puede usar el modificador final.
Pero sería mucho mejor usar objetos de contexto (o objetos de parámetros) que contaminar su SPI utilizando muchos métodos.
interfaz MessageContext {String Message (); Entero id (); Messagesource Source ();} Interface EventListener {// ¡Awesome! Mensaje vacío (contexto de MessageContext);} Puede evolucionar la API de MessageContext más fácilmente que EventListner SPI, porque pocos usuarios lo implementarán.
Regla : siempre que se especifique un SPI, considere usar un objeto de contexto/parámetro en lugar de escribir métodos con parámetros fijos.
Nota : También es una buena idea intercambiar resultados a través de un tipo de Messageresult dedicado, que se puede construir utilizando la API del constructor. Esto aumentará en gran medida la flexibilidad de la evolución SPI.
Los programadores de swing generalmente solo presionan unas pocas teclas de acceso directo para generar cientos o miles de clases anónimas. En la mayoría de los casos, no está de más hacerlo siempre que se siga la interfaz y no se viole el ciclo de vida del subtipo SPI. Pero no use clases anónimas, locales o internas con frecuencia por una razón simple: ahorrarán referencias a clases externas. Porque no importa a dónde van, las categorías externas tienen que seguir. Por ejemplo, si la operación externa de una clase local es incorrecta, todo el gráfico de objeto sufrirá cambios sutiles, lo que puede causar fuga de memoria.
Regla: Antes de escribir clases anónimas, locales o internas, piense dos veces sobre si puede convertirlo en clases estáticas u ordinarias de nivel superior, evitando así formas de devolver sus objetos a un dominio de nivel exterior.
Nota: Use aparatos ortopédicos dobles para inicializar objetos simples:
new HashMap <String, String> () {{Put ("1", "A"); poner ("2", "b");}}Este método utiliza el inicializador de instancia descrito en la especificación JLS §8.6. Se ve bien en la superficie, pero de hecho no se recomienda. Porque si usa un objeto HashMap completamente independiente, la instancia no siempre tendrá referencias a objetos externos. Además, esto permitirá que el cargador de clases administre más clases.
El ritmo de Java8 se acerca. Con Java 8 trae expresiones lambda, le guste o no. Aunque puede que le gusten a su API, es mejor que se asegure de que puedan usarlo con la mayor frecuencia posible. Entonces, a menos que su API reciba tipos simples "escalares", como INT, Long, String y Date, deje que su API reciba SAM con la mayor frecuencia posible.
¿Qué es Sam? SAM es un solo método abstracto [tipo]. También conocida como interfaz de función, pronto se anotará como @FunctionalInterface. Esto coincide con la regla 2, EventListener es en realidad un SAM. El mejor SAM solo tiene un parámetro, ya que esto simplificará aún más la escritura de expresiones lambda. Imagina escribir
oyentes.add (c -> system.out.println (c.message ()));
Reemplazar
oyentes.add (new EventListener () {@Override public void Message (MessageContext C) {System.out.println (C.Message ())); }});Imagina manejar XML en Joox. Joox contiene mucho de Sam:
$ (documento) // Encuentre elementos con una id .find (c -> $ (c) .id ()! = nulo) // Encuentre sus hijos Elementos .children (c -> $ (c) .tag (). Equals ("Order")) // Imprima todos los coincidencias .each (c -> System.out.println ($ (c))))Reglas: Sea amable con los usuarios de su API, comience a escribir interfaces SAM/funciones a partir de ahora .
Nota: Hay muchos blogs interesantes sobre las expresiones Lambda Java8 y la API de colecciones mejoradas:
He escrito 1 o 2 artículos sobre Java Nulls, y también expliqué la introducción de nuevas clases opcionales en Java 8. Desde un punto de vista académico o práctico, estos temas son bastante interesantes.
Aunque NULL y NULLPOINTEREXCECTION siguen siendo un defecto en Java en esta etapa, aún puede diseñar una API que no tenga ningún problema. Al diseñar API, debe evitar devolver el nulo tanto como sea posible, porque sus usuarios pueden llamar al método en una cadena:
inicialize (someargument) .calcule (datos) .dispatch ();
Como se puede ver en el código anterior, ningún método debe devolver nulo. De hecho, el uso de NULL generalmente se considera una heterogeneidad comparable. Biblioteca como JQuery o Joox ha abandonado completamente nulo en objetos iterables.
NULL generalmente se usa en la inicialización tardía. En muchos casos, la inicialización tardía también debe evitarse sin afectar severamente el rendimiento. De hecho, si la estructura de datos involucrada es demasiado grande, entonces la inicialización de retraso debe usarse con precaución.
Regla: los métodos deben evitar devolver nulo cuando lo sean. NULL solo se usa para representar la semántica de "no inicializado" o "no existido".
Aunque está bien devolver un método con un valor nulo en algunos casos, ¡nunca devuelva una matriz vacía o una colección vacía! Consulte el método java.io.file.list (), está diseñado así:
Este método devuelve una matriz de cadenas para todos los archivos o directorios en el directorio especificado. Si el directorio está vacío, la matriz devuelta está vacía. Devuelve nulo si la ruta especificada no existe o se produce un error de E/S.
Por lo tanto, este método generalmente se usa de esta manera:
Directorio de archivos = // ... if (directorio.isDirectory ()) {string [] list = directorio.list (); if (list! = null) {for (archivo de cadena: list) {// ...}}}¿Crees que es necesario un cheque nulo? La mayoría de las operaciones de E/S producirán IOEXcepciones, pero este método solo devuelve NULL. NULL no puede almacenar mensajes de error de E/S. Por lo tanto, dicho diseño tiene las siguientes tres deficiencias:
Si observa el problema desde la perspectiva de las colecciones, entonces una matriz o colección vacía es la mejor implementación de "no existida". Devolver una matriz o colección vacía es de poca importancia práctica a menos que se use para la inicialización tardía.
Regla: La matriz o colección devuelta no debe ser nula.
El beneficio de HTTP es apátrido. Todos los estados relevantes se transfieren en cada solicitud y respuesta. Esta es la esencia del nombre de reposo: transferencia de estado (transferencia de estado de representación). También es genial hacer esto en Java. Piense en esto desde la perspectiva de la Regla 2 cuando el método recibe el objeto de parámetro de estado. Si el estado se transmite a través de dicho objeto, en lugar de operar el estado desde afuera, las cosas serán más fáciles. Tome JDBC como ejemplo. El siguiente ejemplo lee un cursor de un programa almacenado.
CallableStatement S = Connection.PrepareCall ("{? = ...}"); // manipulación verbosa del estado de la declaración: s.RegisterOutParameter (1, cursor); S.SetString (2, "ABC"); S.ExeCute (); ResultSet rs = s.getObject (1); // Manipulación verbosa del set de resultados: Rs.NEXT ();););Esto hace que la API JDBC sea tan extraña. Cada objeto es con el estado y difícil de operar. Específicamente, hay dos problemas principales:
Reglas: más implementaciones en estilo funcional. Estado de transferencia a través de parámetros del método. Muy pocos estados de objetos operativos.
Esta es una forma relativamente fácil de operar. En sistemas de objetos relativamente complejos, puede lograr mejoras de rendimiento significativas, siempre que primero haga juicios iguales en el método igual () de todos los objetos:
@OverridePublic boolean es igual a (objeto otro) {if (this == otro) return true; // Otra lógica del juicio de igualdad ...}Tenga en cuenta que otras verificaciones de cortocircuito pueden involucrar verificaciones de valor nulas, por lo que también deben agregarse:
@OverridePublic boolean es igual a (objeto otro) {if (this == otro) return true; if (otro == null) return false; // Lógica del resto de la igualdad ...}Regla: use cortocircuitos en todos sus métodos iguales () para mejorar el rendimiento.
Algunas personas pueden estar en desacuerdo con esto, porque hacer el método por defecto a la final es contrario al hábito de los desarrolladores de Java. Pero si tiene un control completo sobre el código, ciertamente es correcto hacer que el método sea predeterminado a la final:
Esto es especialmente adecuado para métodos estáticos, en cuyo caso la "superposición" (en realidad oclusión) apenas funciona. Recientemente encontré un ejemplo de un método estático de sombreado muy malo en Apache Tika. Echa un vistazo:
TikainputStream extiende TaggedInputStream para enmascarar su método static get () con una implementación relativamente diferente.
A diferencia de los métodos convencionales, los métodos estáticos no se pueden sobrescribir entre sí porque la llamada está vinculada a la llamada del método estático en el momento de la compilación. Si tiene mala suerte, puede obtener accidentalmente el método incorrecto.
Regla: si tiene el control completo de su API, haga tantos métodos como sea posible a la final.
En ocasiones especiales, puede usar el método de parámetro variable "Aceptar todo" para recibir un objeto ... Parámetro:
void aceptal (objeto ... todo);
Escribir dicho método trae una pequeña sensación de JavaScript al ecosistema Java. Por supuesto, es posible que desee limitar el tipo real en función de la situación real, como la cadena ... Como no desea limitar demasiado, podría pensar que es una buena idea reemplazar el objeto con T genérico:
void aceptal (t ... todos);
Pero no. T siempre se inferirá como un objeto. De hecho, puede pensar que los genéricos no se pueden usar en los métodos anteriores. Más importante aún, podría pensar que puede sobrecargar el método anterior, pero no puede:
void aceptal (t ... todos); void actgeall (mensaje de cadena, t ... todos);
Parece que opcionalmente puede pasar un mensaje de cadena al método. Pero, ¿qué pasa con esta llamada?
Aceptall ("Mensaje", 123, "ABC");El compilador infiere t como <? Extiende serializable y comparable <? >>, ¡lo que hará que la llamada no esté clara!
Entonces, cada vez que tenga una firma de "Aceptar All" (incluso si es un genérico), nunca podrá sobrecargarla. Los consumidores de API solo pueden dejar que el compilador "accidentalmente" elija el método "correcto" cuando tengan suerte. Pero también es posible usar el método Accept-All o no llamar a ningún método.
Regla : evite las firmas de "aceptar todo" si es posible. Si no, no sobrecargue dicho método.
Java es una bestia. A diferencia de otros idiomas más idealistas, lentamente evolucionó a lo que es hoy. Esto puede ser algo bueno, porque a la velocidad del desarrollo de Java, ya hay cientos de advertencias, y estas advertencias solo se pueden aprovechar a través de años de experiencia.
¡Estén atentos para más de las diez listas principales sobre este tema!
Enlace original: Traducción de Jooq: importnew.com - comparación
Enlace de traducción: http://www.importnew.com/10138.html
[Mantenga la fuente original, el traductor y el enlace de traducción al reimprimir. ]