JavaRush /Blog Java /Random-ES /Pausa para el café #56. Una guía rápida de mejores prácti...

Pausa para el café #56. Una guía rápida de mejores prácticas en Java

Publicado en el grupo Random-ES
Fuente: DZone Esta guía incluye las mejores prácticas de Java y referencias para mejorar la legibilidad y confiabilidad de su código. Los desarrolladores tienen la gran responsabilidad de tomar las decisiones correctas todos los días y lo mejor que puede ayudarlos a tomar las decisiones correctas es la experiencia. Y aunque no todos tienen una amplia experiencia en el desarrollo de software, todos pueden utilizar la experiencia de los demás. He preparado algunas recomendaciones para ti que he adquirido de mi experiencia con Java. Espero que te ayuden a mejorar la legibilidad y confiabilidad de tu código Java.Pausa para el café #56.  Una guía rápida de mejores prácticas en Java - 1

Principios de programación

No escriba código que simplemente funcione . Esfuércese por escribir código que sea mantenible , no sólo por usted, sino por cualquier otra persona que pueda terminar trabajando en el software en el futuro. Un desarrollador dedica el 80% de su tiempo a leer código y el 20% a escribir y probar código. Entonces, concéntrate en escribir código legible. Su código no debería necesitar comentarios para que nadie entienda lo que hace. Para escribir un buen código, existen muchos principios de programación que podemos utilizar como pautas. A continuación enumeraré los más importantes.
  • • KISS – Significa “Keep It Simple, Stupid”. Puede notar que los desarrolladores al comienzo de su viaje intentan implementar diseños complejos y ambiguos.
  • • SECO: “No te repitas”. Intente evitar duplicados y, en su lugar, colóquelos en una sola parte del sistema o método.
  • YAGNI - “No lo necesitarás”. Si de repente empiezas a preguntarte: "¿Qué tal si agregas más (características, código, etc.)?", entonces probablemente necesites pensar si realmente vale la pena agregarlas.
  • Código limpio en lugar de código inteligente : en pocas palabras, deje su ego en la puerta y olvídese de escribir código inteligente. Quieres código limpio, no código inteligente.
  • Evite la optimización prematura : el problema con la optimización prematura es que nunca se sabe dónde estarán los cuellos de botella en el programa hasta que aparecen.
  • Responsabilidad única : cada clase o módulo de un programa sólo debe preocuparse por proporcionar una parte de una funcionalidad específica.
  • Composición en lugar de herencia de implementación: los objetos con comportamiento complejo deben contener instancias de objetos con comportamiento individual, en lugar de heredar una clase y agregar nuevos comportamientos.
  • La gimnasia con objetos son ejercicios de programación diseñados como un conjunto de 9 reglas .
  • Falla rápido, detente rápido : este principio significa detener la operación actual cuando ocurre algún error inesperado. El cumplimiento de este principio conduce a un funcionamiento más estable.

Paquetes

  1. Priorizar los paquetes de estructuración por área temática y no por nivel técnico.
  2. Favorezca diseños que promuevan la encapsulación y el ocultamiento de información para proteger contra el mal uso en lugar de organizar clases por razones técnicas.
  3. Trate los paquetes como si tuvieran una API inmutable; no exponga mecanismos internos (clases) destinados únicamente al procesamiento interno.
  4. No exponga clases que estén destinadas a usarse únicamente dentro del paquete.

Clases

Estático

  1. No permita la creación de una clase estática. Cree siempre un constructor privado.
  2. Las clases estáticas deben permanecer inmutables, no permitir subclases ni clases multiproceso.
  3. Las clases estáticas deben protegerse de los cambios de orientación y deben proporcionarse como utilidades como el filtrado de listas.

Herencia

  1. Elija la composición sobre la herencia.
  2. No establezca campos protegidos . En su lugar, especifique un método de acceso seguro .
  3. Si una variable de clase se puede marcar como final , hágalo.
  4. Si no se espera herencia, haga que la clase sea definitiva .
  5. Marque un método como final si no se espera que las subclases puedan anularlo.
  6. Si no se requiere un constructor, no cree un constructor predeterminado sin lógica de implementación. Java proporcionará automáticamente un constructor predeterminado si no se especifica ninguno.

Interfaces

  1. No utilice el patrón de interfaz de constantes porque permite que las clases implementen y contaminen la API. Utilice una clase estática en su lugar. Esto tiene el beneficio adicional de permitirle realizar una inicialización de objetos más compleja en un bloque estático (como completar una colección).
  2. Evite el uso excesivo de la interfaz .
  3. Tener una y sólo una clase que implemente una interfaz probablemente conducirá a un uso excesivo de las interfaces y hará más daño que bien.
  4. "Programa para la interfaz, no para la implementación" no significa que debas agrupar cada una de las clases de tu dominio con una interfaz más o menos idéntica; al hacerlo, estás rompiendo YAGNI .
  5. Mantenga siempre las interfaces pequeñas y específicas para que los clientes sólo conozcan los métodos que les interesan. Consulte el ISP de SOLID.

Finalizadores

  1. El objeto #finalize() debe usarse con prudencia y solo como medio de protección contra fallas al limpiar recursos (como cerrar un archivo). Proporcione siempre un método de limpieza explícito (como close() ).
  2. En una jerarquía de herencia, siempre llame al finalize() del padre en un bloque try . La limpieza de clase debería estar en un bloque finalmente .
  3. Si no se llamó a un método de limpieza explícito y el finalizador cerró los recursos, registre este error.
  4. Si no hay un registrador disponible, utilice el controlador de excepciones del hilo (que termina pasando un error estándar que se captura en los registros).

Reglas generales

Declaraciones

Una aserción, generalmente en forma de verificación de condiciones previas, impone un contrato de "falla rápido, detente rápido". Deben utilizarse ampliamente para identificar errores de programación lo más cerca posible de la causa. Condición del objeto:
  • • Nunca se debe crear un objeto ni ponerlo en un estado no válido.
  • • En constructores y métodos, siempre describa y haga cumplir el contrato mediante pruebas.
  • • Se debe evitar la palabra clave Java afirmar , ya que se puede desactivar y suele ser una construcción frágil.
  • • Utilice la clase de utilidad Aserciones para evitar condiciones detalladas si-si no para verificaciones de condiciones previas.

Genéricos

Una explicación completa y extremadamente detallada está disponible en las preguntas frecuentes sobre Java Generics . A continuación se detallan escenarios comunes que los desarrolladores deben conocer.
  1. Siempre que sea posible, es mejor utilizar la inferencia de tipos en lugar de devolver la clase/interfaz base:

    // MySpecialObject o = MyObjectFactory.getMyObject();
    public  T getMyObject(int type) {
    return (T) factory.create(type);
    }

  2. Si el tipo no se puede determinar automáticamente, inclúyalo.

    public class MySpecialObject extends MyObject {
     public MySpecialObject() {
      super(Collections.emptyList());   // This is ugly, as we loose type
      super(Collections.EMPTY_LIST();    // This is just dumb
      // But this is beauty
      super(new ArrayList());
      super(Collections.emptyList());
     }
    }

  3. Comodines:

    Use un comodín extendido cuando solo obtenga valores de una estructura, use un súper comodín cuando solo coloque valores en una estructura y no use un comodín cuando esté haciendo ambas cosas.

    1. ¡ A todo el mundo le encanta PECS ! ( Productor-extiende, Consumidor-super )
    2. Utilice Foo para el productor T.
    3. Utilice Foo para el consumidor T.

solteros

Un singleton nunca debe escribirse en el estilo de patrón de diseño clásico , que está bien en C++ pero no es apropiado en Java. Aunque es adecuadamente seguro para subprocesos, nunca implemente lo siguiente (¡sería un cuello de botella en el rendimiento!):
public final class MySingleton {
  private static MySingleton instance;
  private MySingleton() {
    // singleton
  }
  public static synchronized MySingleton getInstance() {
    if (instance == null) {
      instance = new MySingleton();
    }
    return instance;
  }
}
Si realmente se desea una inicialización diferida, entonces funcionará una combinación de estos dos enfoques.
public final class MySingleton {
  private MySingleton() {
   // singleton
  }
  private static final class MySingletonHolder {
    static final MySingleton instance = new MySingleton();
  }
  public static MySingleton getInstance() {
    return MySingletonHolder.instance;
  }
}
Spring: de forma predeterminada, un bean se registra con alcance singleton, lo que significa que el contenedor solo creará una instancia y se conectará a todos los consumidores. Esto proporciona la misma semántica que un singleton normal, sin limitaciones vinculantes ni de rendimiento.

Excepciones

  1. Utilice excepciones marcadas para condiciones corregibles y excepciones de tiempo de ejecución para errores de programación. Ejemplo: obtener un número entero de una cadena.

    Malo: NumberFormatException extiende RuntimeException, por lo que pretende indicar errores de programación.

  2. No hagas lo siguiente:

    // String str = input string
    Integer value = null;
    try {
       value = Integer.valueOf(str);
    } catch (NumberFormatException e) {
    // non-numeric string
    }
    if (value == null) {
    // handle bad string
    } else {
    // business logic
    }

    Uso correcto:

    // String str = input string
    // Numeric string with at least one digit and optional leading negative sign
    if ( (str != null) && str.matches("-?\\d++") ) {
       Integer value = Integer.valueOf(str);
      // business logic
    } else {
      // handle bad string
    }
  3. Tienes que manejar las excepciones en el lugar correcto, en el lugar correcto a nivel de dominio.

    MANERA INCORRECTA: la capa de objetos de datos no sabe qué hacer cuando ocurre una excepción en la base de datos.

    class UserDAO{
        public List getUsers(){
            try{
                ps = conn.prepareStatement("SELECT * from users");
                rs = ps.executeQuery();
                //return result
            }catch(Exception e){
                log.error("exception")
                return null
            }finally{
                //release resources
            }
        }}
    

    MANERA RECOMENDADA : la capa de datos simplemente debe volver a generar la excepción y pasar la responsabilidad de manejar la excepción o no a la capa correcta.

    === RECOMMENDED WAY ===
    Data layer should just retrow the exception and transfer the responsability to handle the exception or not to the right layer.
    class UserDAO{
       public List getUsers(){
          try{
             ps = conn.prepareStatement("SELECT * from users");
             rs = ps.executeQuery();
             //return result
          }catch(Exception e){
           throw new DataLayerException(e);
          }finally{
             //release resources
          }
      }
    }

  4. Por lo general, las excepciones NO deben registrarse en el momento en que se emiten, sino en el momento en que realmente se procesan. Las excepciones de registro, cuando se lanzan o se vuelven a lanzar, tienden a llenar los archivos de registro con ruido. Tenga en cuenta también que el seguimiento de la pila de excepciones todavía registra dónde se produjo la excepción.

  5. Admite el uso de excepciones estándar.

  6. Utilice excepciones en lugar de códigos de retorno.

Iguales y HashCode

Hay una serie de cuestiones a considerar al escribir métodos adecuados de equivalencia de código hash y objeto. Para que sea más fácil de usar, use los iguales y hash de java.util.Objects .
public final class User {
 private final String firstName;
 private final String lastName;
 private final int age;
 ...
 public boolean equals(Object o) {
   if (this == o) {
     return true;
   } else if (!(o instanceof User)) {
     return false;
   }
   User user = (User) o;
   return Objects.equals(getFirstName(), user.getFirstName()) &&
    Objects.equals(getLastName(),user.getLastName()) &&
    Objects.equals(getAge(), user.getAge());
 }
 public int hashCode() {
   return Objects.hash(getFirstName(),getLastName(),getAge());
 }
}

Administracion de recursos

Formas de liberar recursos de forma segura: la declaración de prueba con recursos garantiza que cada recurso se cierre al final de la declaración. Cualquier objeto que implemente java.lang.AutoCloseable, que incluye todos los objetos que implementen java.io.Closeable , se puede utilizar como recurso.
private doSomething() {
try (BufferedReader br = new BufferedReader(new FileReader(path)))
 try {
   // business logic
 }
}

Utilice ganchos de apagado

Utilice un enlace de apagado que se llama cuando la JVM se cierra correctamente. (Pero no podrá manejar interrupciones repentinas, como las debidas a un corte de energía). Esta es una alternativa recomendada en lugar de declarar un método finalize() que solo se ejecutará si System.runFinalizersOnExit() es verdadero (el valor predeterminado es falso) .
public final class SomeObject {
 var distributedLock = new ExpiringGeneralLock ("SomeObject", "shared");
 public SomeObject() {
   Runtime
     .getRuntime()
     .addShutdownHook(new Thread(new LockShutdown(distributedLock)));
 }
 /** Code may have acquired lock across servers */
 ...
 /** Safely releases the distributed lock. */
 private static final class LockShutdown implements Runnable {
   private final ExpiringGeneralLock distributedLock;
   public LockShutdown(ExpiringGeneralLock distributedLock) {
     if (distributedLock == null) {
       throw new IllegalArgumentException("ExpiringGeneralLock is null");
     }
     this.distributedLock = distributedLock;
   }
   public void run() {
     if (isLockAlive()) {
       distributedLock.release();
     }
   }
   /** @return True if the lock is acquired and has not expired yet. */
   private boolean isLockAlive() {
     return distributedLock.getExpirationTimeMillis() > System.currentTimeMillis();
   }
 }
}
Permita que los recursos se vuelvan completos (y renovables) distribuyéndolos entre servidores. (Esto permitirá la recuperación de una interrupción repentina, como un corte de energía). Vea el código de ejemplo anterior que usa ExpiringGeneralLock (un bloqueo común a todos los sistemas).

Fecha y hora

Java 8 introduce la nueva API de fecha y hora en el paquete java.time. Java 8 introduce una nueva API de fecha y hora para abordar las siguientes deficiencias de la antigua API de fecha y hora: falta de subprocesos, diseño deficiente, manejo complejo de zonas horarias, etc.

Paralelismo

Reglas generales

  1. Tenga cuidado con las siguientes bibliotecas, que no son seguras para subprocesos. Sincronice siempre con objetos si son utilizados por varios subprocesos.
  2. Fecha ( no inmutable ): utilice la nueva API de fecha y hora, que es segura para subprocesos.
  3. SimpleDateFormat: utilice la nueva API de fecha y hora, que es segura para subprocesos.
  4. Prefiere utilizar clases java.util.concurrent.atomic en lugar de hacer que las variables sean volátiles .
  5. El comportamiento de las clases atómicas es más obvio para el desarrollador promedio, mientras que volátil requiere una comprensión del modelo de memoria Java.
  6. Las clases atómicas envuelven variables volátiles en una interfaz más conveniente.
  7. Comprender los casos de uso en los que lo volátil es apropiado . (ver artículo )
  8. Usar invocable cuando se requiere una excepción marcada pero no hay ningún tipo de devolución. Dado que no se puede crear una instancia de Void, comunica la intención y puede devolver null de forma segura .

Corrientes

  1. java.lang.Thread debería estar en desuso. Aunque oficialmente no es así, en casi todos los casos el paquete java.util.concurrent proporciona una solución más clara al problema.
  2. Extender java.lang.Thread se considera una mala práctica: implemente Runnable en su lugar y cree un nuevo hilo con una instancia en el constructor (regla de composición sobre herencia).
  3. Prefiere ejecutores y subprocesos cuando se requiere procesamiento paralelo.
  4. Siempre se recomienda especificar su propia fábrica de subprocesos personalizada para administrar la configuración de los subprocesos creados ( más detalles aquí ).
  5. Utilice DaemonThreadFactory en Ejecutores para subprocesos no críticos para que el grupo de subprocesos se pueda cerrar inmediatamente cuando se apague el servidor ( más detalles aquí ).
this.executor = Executors.newCachedThreadPool((Runnable runnable) -> {
   Thread thread = Executors.defaultThreadFactory().newThread(runnable);
   thread.setDaemon(true);
   return thread;
});
  1. La sincronización de Java ya no es tan lenta (55-110 ns). No lo evites usando trucos como el bloqueo de doble verificación .
  2. Prefiere la sincronización con un objeto interno en lugar de una clase, ya que los usuarios pueden sincronizar con su clase/instancia.
  3. Sincronice siempre varios objetos en el mismo orden para evitar interbloqueos.
  4. La sincronización con una clase no bloquea inherentemente el acceso a sus objetos internos. Utilice siempre los mismos candados al acceder a un recurso.
  5. Recuerde que la palabra clave sincronizada no se considera parte de la firma del método y, por lo tanto, no se heredará.
  6. Evite una sincronización excesiva, ya que esto puede provocar un rendimiento deficiente y un punto muerto. Utilice la palabra clave sincronizada estrictamente para la parte del código que requiere sincronización.

Colecciones

  1. Utilice colecciones paralelas de Java-5 en código multiproceso siempre que sea posible. Son seguros y tienen excelentes características.
  2. Si es necesario, utilice CopyOnWriteArrayList en lugar de sincronizadoList.
  3. Utilice Collections.unmodifiable list(...) o copie la colección cuando la reciba como parámetro en new ArrayList(list) . Evite modificar colecciones locales desde fuera de su clase.
  4. Devuelve siempre una copia de tu colección, evitando modificar tu lista externamente con una nueva ArrayList (lista) .
  5. Cada colección debe estar envuelta en una clase separada, por lo que ahora el comportamiento asociado con la colección tiene un hogar (por ejemplo, métodos de filtrado, aplicación de una regla a cada elemento).

Misceláneas

  1. Elija lambdas en lugar de clases anónimas.
  2. Elija referencias de métodos en lugar de lambdas.
  3. Utilice enumeraciones en lugar de constantes int.
  4. Evite el uso de flotante y doble si se requieren respuestas precisas; en su lugar, utilice BigDecimal como Money.
  5. Elija tipos primitivos en lugar de primitivos encuadrados.
  6. Debes evitar el uso de números mágicos en tu código. Utilice constantes.
  7. No devuelvas Nulo. Comuníquese con su cliente de método usando "Opcional". Lo mismo ocurre con las colecciones: devuelve matrices o colecciones vacías, no nulos.
  8. Evite crear objetos innecesarios, reutilice objetos y evite la limpieza innecesaria de GC.

Inicialización diferida

La inicialización diferida es una optimización del rendimiento. Se utiliza cuando los datos se consideran “caros” por algún motivo. En Java 8 tenemos que utilizar la interfaz del proveedor funcional para esto.
== Thread safe Lazy initialization ===
public final class Lazy {
   private volatile T value;
   public T getOrCompute(Supplier supplier) {
       final T result = value; // Just one volatile read
       return result == null ? maybeCompute(supplier) : result;
   }
   private synchronized T maybeCompute(Supplier supplier) {
       if (value == null) {
           value = supplier.get();
       }
       return value;
   }
}
Lazy lazyToString= new Lazy<>()
return lazyToString.getOrCompute( () -> "(" + x + ", " + y + ")");
Eso es todo por ahora, espero que haya sido útil.
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION