Un artículo sencillo para entender las recomendaciones de "Java efectivo"

Considere usar métodos de fábrica estáticos en lugar de constructores

Tradicionalmente, la obtención de una instancia de objeto suele realizarse mediante el método de construcción, nuevo objeto; diferentes números de parámetros de entrada tendrán diferentes métodos de construcción;

Por ejemplo, para devolver una clase de resultado unificada, el método tradicional (pseudocódigo) es el siguiente:

//成功
return new Result(200);
//成功,返回信息、对象
return new Result(200,"成功",data);
//失败,返回信息
return new Result(500,"xxx不能为空");

Usamos la alternativa del método de fábrica estático para reescribir, de la siguiente manera:

//成功,无参
return Result.success();
//成功,返回对象
return Result.ofSuccess(data);
//失败,返回信息
return Result.ofFail("xxx不能为空");

El uso de métodos de fábrica estáticos tiene las siguientes ventajas principales:

  1. A diferencia de los constructores con el mismo nombre, pueden personalizar los nombres de los métodos para facilitar su uso;
  2. A diferencia del método constructor que llama a un nuevo objeto cada vez, no necesitan crear un nuevo objeto cada vez; (modo flyweight o modo singleton)
  3. A diferencia de los constructores, pueden devolver el tipo de retorno definido y sus subtipos;
  4. La clase del objeto devuelto puede ser diferente según los parámetros de entrada; (Orientado a la programación abstracta)
  5. Al escribir una clase que contiene este método, no es necesario que exista la clase que devuelve el objeto; (para programación abstracta, se pueden devolver clases derivadas)

Las principales desventajas de los métodos de fábrica estáticos son:

  1. Debido a que es una clase estática y está orientada a la programación abstracta, no es fácil crear instancias de subclases.

  2. Debido al nombre del método personalizado, a los programadores les resulta difícil encontrarlo si no hay documentación o código fuente.

Utilice el patrón Builder cuando haya demasiados parámetros de constructor

Por ejemplo, actualmente existe una clase de usuario con múltiples atributos:

public class User {
    
    
    private String name;
    private String nickname;
    private int sex;
    private int age;
    private String phone;
    private String mail;
    private String address;
    private int height;
    private int weight;

	//构造方法
    public User(String name, String nickname, int sex, int age) {
    
    
        this.name = name;
        this.nickname = nickname;
        this.sex = sex;
        this.age = age;
    }

	//getter和setter方法
    public String getName() {
    
    
        return name;
    }

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

​ La forma de crear objetos según el método tradicional es la siguiente:

 		//1.可选参数,构造方法实例化
        User user = new User("张天空", "张三", 1, 20);

        //2.set方法复制实例化
        User user2 = new User();
        user2.setName("张天空");
        user2.setNickname("张三");
        user2.setSex(1);
        user2.setAge(20);
        user2.setPhone("14785236915");
        user2.setHeight(175);
        user2.setWeight(157);

La desventaja del método de construcción es que es inconveniente escalar cuando se enfrenta a múltiples parámetros opcionales, es necesario definir un método de construcción correspondiente para cada situación;

Método JavaBeans (método setter), la asignación puede ser larga; una deficiencia más grave es que dado que el método de asignación de construcción se puede dividir en varias llamadas, el JavaBean puede estar en un estado inconsistente durante el proceso de construcción ; (por ejemplo, el JavaBean objeto como parámetros, cuando se pasa por referencia, diferentes métodos de asignación de conjuntos pueden provocar inconsistencia en JavaBean)

La ventaja del patrón Builder sobre el método de construcción es que puede tener múltiples parámetros variables y la construcción es más flexible;

Utilice constructores privados o enumeraciones para implementar singletons

// Singleton.java
public enum Singleton {
    
    
    INSTANCE;
 
    public void testMethod() {
    
    
        System.out.println("执行了单例类的方法");
    }
}
 
// Test.java
public class Test {
    
    
 public static void main(String[] args) {
    
    
        //演示如何使用枚举写法的单例类
        Singleton.INSTANCE.testMethod();
        System.out.println(Singleton.INSTANCE);
    }
}

//输出:
执行了单例类的方法
INSTANCE

Los singletons de implementación de enumeración son similares a los métodos de propiedad pública, pero son más concisos, proporcionan un mecanismo de serialización gratuito y brindan garantías sólidas contra múltiples instancias, incluso en el caso de ataques complejos de serialización o reflexión .

Este enfoque puede parecer un poco antinatural, pero una clase de enumeración de un solo elemento suele ser la mejor manera de implementar un singleton. Tenga en cuenta que este método no se puede utilizar si el singleton debe heredar una clase principal que no sea Enum (aunque es posible declarar un Enum para implementar la interfaz) .

Enlace de referencia: https://juejin.cn/post/7229660119658512441

Utilice constructores privados para evitar la creación de instancias.

Ocasionalmente querrás escribir una clase, que es solo un conjunto de métodos estáticos y propiedades estáticas (como una clase de utilidad o una clase constante).

​ Estas clases de utilidad no están diseñadas para crear instancias, por lo que se pueden eliminar instancias incluyendo un constructor privado.

Utilice la inyección de dependencia en lugar de recursos cableados

Muchas clases dependen de uno o más recursos subyacentes; los llamados recursos de enlace duro sirven para configurar los recursos dependientes como estáticos o únicos; dichos enlaces duros serán incómodos de expandir y no serán lo suficientemente flexibles; pueden ser reemplazados por inyección de dependencia; Las clases de utilidad estáticas y los singleton no son apropiados para clases cuyo comportamiento está parametrizado por recursos subyacentes.

Método de recurso de enlace físico:

public class SpellChecker {
    
    
    private static final Lexicon dictionary = ...;
    private SpellChecker() {
    
    } // Noninstantiable
    public static boolean isValid(String word) {
    
     ... }
    public static List<String> suggestions(String typo) {
    
     ... }
}

Método de inyección de dependencia:

public class SpellChecker {
    
    
    private final Lexicon dictionary;
    public SpellChecker(Lexicon dictionary) {
    
    
        this.dictionary = Objects.requireNonNull(dictionary);
    }
    public boolean isValid(String word) {
    
     ... }
    public List<String> suggestions(String typo) {
    
     ... }
}

Evite crear objetos innecesarios

Esta entrada no debe malinterpretarse en el sentido de que la creación de objetos es costosa y debe evitarse. Por el contrario, es muy económico crear y reciclar objetos pequeños utilizando constructores, que realizan muy poco trabajo explícito, especialmente en implementaciones JVM modernas. Generalmente es bueno crear objetos adicionales para mejorar la claridad, simplicidad o funcionalidad de su programa.

Por el contrario, es una mala idea evitar la creación de objetos manteniendo su propio grupo de objetos a menos que los objetos del grupo sean muy pesados . Un ejemplo típico de un grupo de objetos es una conexión de base de datos. El coste de establecer una conexión es muy alto, por lo que sólo tiene sentido reutilizar estos objetos.

Eliminar referencias a objetos caducados

Si una pila crece y luego se reduce, los objetos extraídos de la pila no se recolectarán como basura , incluso si el programa que utiliza la pila ya no hace referencia a esos objetos. Esto se debe a que la pila mantiene referencias obsoletas a estos objetos. Una referencia caducada es simplemente una referencia que nunca se publicará. En este caso, cualquier referencia fuera de la "parte activa" de la matriz de elementos está obsoleta. La parte activa está compuesta por elementos cuyo subíndice de índice es menor que el tamaño.

Las pérdidas de memoria en lenguajes de recolección de basura (más apropiadamente llamadas retenciones no intencionales de objetos) son insidiosas. Si se retiene inadvertidamente una referencia a un objeto, no solo se excluye el objeto de la recolección de basura, sino también todos los objetos a los que hace referencia el objeto. Incluso si solo se retienen inadvertidamente unas pocas referencias a objetos, esto puede evitar que el mecanismo de recolección de basura recicle muchos objetos, lo que tiene un gran impacto en el rendimiento.

La solución a este tipo de problema es simple: una vez que las referencias a objetos caduquen, configúrelas en nulas ;

Cuando los programadores se ven afectados por este problema por primera vez, pueden borrar todas las referencias a objetos inmediatamente después de que finaliza el programa. Esto no es necesario ni deseable; satura el programa innecesariamente. La eliminación de referencias a objetos debería ser la excepción y no la norma . Una buena forma de eliminar las referencias obsoletas es hacer que la variable que contiene la referencia quede fuera de alcance. Esto ocurre naturalmente si cada variable se define en un ámbito cercano (Ítem 57)

Evite el uso de mecanismos Finilizer y Cleaner

El mecanismo Finalizador es impredecible, a menudo peligroso y, a menudo, innecesario . Su uso puede provocar un comportamiento errático, un rendimiento deficiente y problemas de portabilidad. El mecanismo Finalizador tiene algunos usos especiales, que cubriremos más adelante en esta entrada, pero generalmente deben evitarse. A partir de Java 9, el mecanismo Finalizer ha quedado obsoleto, pero las bibliotecas de clases Java aún lo utilizan. El mecanismo Cleaner en Java 9 reemplaza al mecanismo Finalizer. El mecanismo Limpiador no es tan peligroso como el mecanismo Finalizador, pero sigue siendo impredecible, lento y, a menudo, innecesario.

Un inconveniente de los mecanismos Finalizador y Limpiador es que no se garantiza que se ejecuten de manera oportuna [JLS, 12.6]. El tiempo entre el momento en que un objeto se vuelve inaccesible y el momento en que los mecanismos Finalizador y Limpiador comienzan a ejecutarse es arbitrariamente largo. Esto significa que nunca debes hacer nada en el que el tiempo sea crítico con los mecanismos Finalizador y Limpiador .

La declaración try-with-resources reemplaza la declaración try-finally

Cuando Java 7 introdujo la declaración try-with-resources, todos estos problemas se resolvieron a la vez [JLS, 14.20.3]. Para utilizar esta construcción, el recurso debe implementar la interfaz AutoCloseable , que consiste en un cierre que devuelve void. Muchas clases e interfaces en Java y bibliotecas de terceros ahora implementan o heredan la interfaz AutoCloseable . Si escribe una clase que representa un recurso que debe cerrarse, entonces esta clase también debería implementar la interfaz AutoCloseable

Cuando trabaje con recursos que deban cerrarse, utilice la declaración try-with-resources en lugar de la declaración try-finally . El código generado es más limpio y claro y las excepciones generadas son más útiles. La declaración try-with-resources hace que sea más fácil y sin errores escribir código que deba cerrar recursos, lo cual es prácticamente imposible con la declaración try-finally.

Siga las convenciones comunes al anular el método igual

Aunque Object es una clase concreta, está diseñada principalmente para herencia . Todos sus métodos no finales (equals, hashCode, toString, clone y finalize) tienen contratos generales claros porque están diseñados para ser anulados por subclases. Cualquier clase está obligada a anular estos métodos para cumplir con su contrato general; no hacerlo impedirá que otras clases que dependen de la convención (como HashMap y HashSet) funcionen correctamente con esta clase .

¿ Cuándo Si una clase contiene un concepto de igualdad lógica que es distinto de la identidad del objeto y la clase principal no ha anulado el método igual. Esto se utiliza normalmente en el caso de clases de valor. Una clase de valor es simplemente una clase que representa un valor, como una clase Integer o String. Los programadores utilizan el método de igualdad para comparar referencias a objetos de valor, esperando encontrar si son lógicamente iguales, en lugar de hacer referencia al mismo objeto. Anular el método de iguales no solo puede cumplir con las expectativas del programador, sino que también admite anular instancias de iguales como claves de Map o elementos en Set para cumplir con las expectativas y el comportamiento deseado.

Cuando anula el método igual, debe cumplir con sus convenciones generales. La especificación de Object es la siguiente: El método igual implementa una relación de equivalencia. Tiene las siguientes propiedades:

  • Reflexividad: x.equals(x) debe devolver verdadero para cualquier referencia no nula x
  • Simetría: para cualquier referencia x e y no nula, x.equals(y) debe devolver verdadero si y solo si y.equals(x) devuelve verdadero
  • Transitividad: para cualquier referencia no nula x, y, z, si x.equals(y) devuelve verdadero e y.equals(z) devuelve verdadero, entonces x.equals(z) debe devolver verdadero
  • Consistencia: para cualquier referencia no nula xey, múltiples llamadas a x.equals(y) siempre deben devolver verdadero o siempre falso si la información utilizada en la comparación igual no se modifica.
  • Para cualquier referencia x no nula, x.equals(null) debe devolver falso

Poniéndolo todo junto, aquí hay una receta para escribir un método igual de alta calidad:

  1. Utilice el operador == para comprobar si el parámetro es una referencia al objeto . En caso afirmativo, devuelva verdadero. Esto es sólo una optimización del rendimiento, pero si es probable que esta comparación resulte costosa, vale la pena hacerla.
  2. Utilice el operador instancia de para comprobar si los parámetros tienen el tipo correcto . Si no, devuelve falso. Normalmente, el tipo correcto es la clase donde se encuentra el método igual. A veces, la clase se modifica para implementar algunas interfaces. Si una clase implementa una interfaz que mejora la convención igual para permitir la comparación de clases que implementan la interfaz, entonces use la interfaz. Las interfaces de colección como Set, List, Map y Map.Entry tienen esta característica. 3. Convierta los parámetros al tipo correcto. Debido a que la operación de conversión ya se maneja en instancia de, definitivamente tendrá éxito.
  3. Para cada propiedad "importante" de la clase, verifique si la propiedad del parámetro coincide con la propiedad correspondiente del objeto . Devuelve verdadero si todas estas pruebas tienen éxito; falso en caso contrario. Si el tipo en el paso 2 es una interfaz, entonces se debe acceder a las propiedades del parámetro a través de métodos de interfaz; si el tipo es una clase, se puede acceder a las propiedades directamente, dependiendo de los permisos de acceso de la propiedad.

En resumen, no anule el método igual a menos que sea necesario: en muchos casos, la implementación heredada de Object es exactamente lo que desea. Si anula el método de igualdad, asegúrese de comparar todas las propiedades importantes de la clase y hágalo de manera que proteja las cinco disposiciones del contrato de igualdad anterior.

Al anular el método igual, también debe anular el método de código hash.

​En cada clase, al anular el método igual, asegúrese de anular el método hashcode . Si no hace esto, su clase viola la convención general de hashCode, lo que impide que funcione correctamente con colecciones como HashMap y HashSet.

Según la especificación del objeto, las siguientes son convenciones específicas:

  • Cuando el método hashCode se llama repetidamente sobre un objeto durante la ejecución de una aplicación, siempre debe devolver el mismo valor si no se modifica ninguna información en la comparación del método igual . El valor devuelto por cada ejecución de una aplicación a otra puede ser inconsistente
  • Si dos objetos son iguales según el método igual (Objeto), entonces llamar a hashCode en ambos objetos debe producir el mismo número entero.
  • Si dos objetos no se comparan iguales según el método igual (Objeto), no es necesario que llamar a hashCode en cada objeto deba producir un resultado diferente. Sin embargo, los programadores deben tener en cuenta que generar resultados diferentes para objetos desiguales puede mejorar el rendimiento de las tablas hash.

Cuando no se puede anular hashCode, la segunda cláusula clave violada es: los objetos iguales deben tener códigos hash iguales

@Override 
public int hashCode() {
    
     return 42; } 
这是合法的,因为它确保了相等的对象具有相同的哈希码。这很糟糕,因为它确保了每个对象都有相同的哈希 码。因此,每个对象哈希到同一个桶中,哈希表退化为链表。应该在线性时间内运行的程序,运行时间变成了平方级 别。对于数据很大的哈希表而言,会影响到能够正常工作。

 **一个好的 hash 方法趋向于为不相等的实例生成不相等的哈希码**。这也正是 hashCode 约定中第三条的表达。理 想情况下,hash 方法为集合中不相等的实例均地分配 int 范围内的哈希码

En resumen, el método hashCode debe reescribirse cada vez que se anula el método igual ; de lo contrario, el programa no se ejecutará correctamente. Su método hashCode debe seguir las convenciones generales especificadas por la clase Object y debe realizar un trabajo razonable al asignar códigos hash desiguales a instancias desiguales.

Anular siempre el método toString

Aunque la clase Object proporciona una implementación del método toString, la cadena que devuelve normalmente no es la que los usuarios de su clase quieren ver. Consiste en el nombre de la clase seguido de un signo "arroba" (@) y la representación hexadecimal sin signo del código hash, por ejemplo Usuario@163b91.

La convención general para toString requiere que la cadena devuelta sea "una representación concisa pero informativa que los humanos puedan leer fácilmente".

Aunque no es tan importante como seguir las convenciones de iguales y hashCode, proporcionar una buena implementación de toString hace que su clase sea más fácil de usar y que los sistemas que la usan sean más fáciles de depurar. El método toString se llama automáticamente cuando el objeto se pasa a println, printf, operador de concatenación de cadenas o aserción, o el depurador lo imprime.

Anule el método de clonación con cuidado

Suponga que desea implementar la interfaz Cloneable en una clase cuya clase principal proporciona un método de clonación de buen comportamiento. Primero llame a super.clone. El objeto resultante será una réplica completamente funcional del original. Cualquier propiedad declarada en su clase tendrá el mismo valor que la propiedad original. Si cada propiedad contiene un valor primitivo o una referencia a un objeto inmutable, el objeto devuelto puede ser exactamente lo que necesita, en cuyo caso no se requiere procesamiento adicional.

Teniendo en cuenta todos los problemas asociados con la interfaz Cloneable, las nuevas interfaces no deberían heredarla y las nuevas clases extensibles no deberían implementarla. Aunque no hay ningún daño en implementar la interfaz Cloneable para las clases finales, se debe considerar desde una perspectiva de optimización del rendimiento y solo se justifica en casos excepcionales (Ítem 67). Por lo general, la funcionalidad de copia la proporciona mejor un constructor o una fábrica. La excepción obvia a esta regla son las matrices, que se pueden copiar mediante el método de clonación.

Considere implementar la interfaz Comparable

​ A veces, puedes ver que los métodos compareTo o compare se basan en la diferencia entre dos valores, que es negativa si el primer valor es menor que el segundo valor; cero si los dos valores son iguales y cero si el primero El valor es igual. Si el valor es mayor que , es un valor positivo. Aquí hay un ejemplo:

static Comparator<Object> hashCodeOrder = new Comparator<>() {
    
         
    public int compare(Object o1, Object o2) {
    
             
        return o1.hashCode() - o2.hashCode();     
    } 
}; 

¡ No Puede generar peligros de desbordamiento de enteros de gran longitud y distorsión aritmética de punto flotante IEEE 754 [JLS 15.20.1, 15.21.1]. Además, es poco probable que el método resultante sea significativamente más rápido que uno escrito utilizando las técnicas anteriores. Utilice el método de comparación estática:

static Comparator<Object> hashCodeOrder = new Comparator<>() {
    
         
    public int compare(Object o1, Object o2) {
    
             
        return Integer.compare(o1.hashCode(), o2.hashCode());     
    } 
}; 

O utilice el método de construcción Comparator:

static Comparator<Object> hashCodeOrder =         
Comparator.comparingInt(o -> o.hashCode()); 

En resumen, siempre que implemente una clase de valor con un orden razonable, debe hacer que esa clase implemente la interfaz Comparable para que sus instancias puedan ordenarse, buscarse y usarse fácilmente en colecciones basadas en comparación. Al comparar valores de campo en implementaciones del método compareTo, evite utilizar los operadores "<" y ">". En su lugar, utilice el método de comparación estático en la clase contenedora o el método de compilación en la interfaz Comparator.

Hacer que las clases y los miembros sean menos accesibles

Un componente bien diseñado oculta todos sus detalles de implementación, separando claramente su API de su implementación . Luego, los componentes solo se comunican a través de sus API y no saben nada sobre el funcionamiento interno de cada uno. Este concepto, conocido como ocultación o encapsulación de información, es un principio fundamental del diseño de software (ley de Demit, el principio de lo menos conocido).

El ocultamiento de información es importante por muchas razones, la mayoría de las cuales se derivan del hecho de que separa los componentes que componen un sistema, lo que permite desarrollarlos, probarlos, optimizarlos, usarlos, comprenderlos y modificarlos de forma independiente. Esto acelera el desarrollo del sistema porque los componentes se pueden desarrollar en paralelo . Alivia la carga de mantenimiento porque los componentes se pueden entender, depurar o reemplazar más rápidamente sin temor a dañar otros componentes.

Java proporciona muchos mecanismos para ayudar a ocultar información. El mecanismo de control de acceso [JLS, 6.6] especifica la accesibilidad de clases, interfaces y miembros. La accesibilidad de una entidad depende de dónde se declara y qué modificadores de acceso (privado, protegido y público) están presentes en la declaración. El uso adecuado de estos modificadores es fundamental para ocultar información.

La regla general es simple: hacer que cada clase o miembro sea lo más inaccesible posible. En otras palabras, utilice el nivel de acceso más bajo posible que sea coherente con la funcionalidad del software que está escribiendo.

Utilice métodos de acceso en lugar de propiedades públicas en clases públicas

Para las clases públicas, es correcto ceñirse a la orientación a objetos: si se puede acceder a una clase fuera de su paquete, proporcione métodos de acceso para preservar la flexibilidad de cambiar la representación interna de la clase . Si una clase pública expone sus propiedades de datos, es esencialmente imposible cambiar su representación más adelante porque el código del cliente puede distribuirse en muchos lugares.

Sin embargo, si una clase es privada a nivel de paquete, o es una clase interna privada, entonces no hay nada inherentemente malo en exponer sus propiedades de datos, suponiendo que proporcionen una descripción suficiente de la abstracción proporcionada por la clase.

En resumen, las clases públicas no deberían exponer propiedades mutables. El daño de la exposición pública a propiedades inmutables sigue siendo problemático, pero menos dañino. Sin embargo, a veces se necesita una clase interna privada o privada a nivel de paquete para exponer propiedades, ya sea que dicha clase sea mutable o no.

Minimizar la variabilidad

Una clase inmutable . Toda la información contenida en cada instancia es fija durante la vida útil del objeto, por lo que no se observarán cambios. La biblioteca de clases de la plataforma Java contiene muchas clases inmutables, incluida la clase String, las clases contenedoras de tipos básicos y las clases BigInteger y BigDecimal. Hay muchas buenas razones: las clases inmutables son más fáciles de diseñar, implementar y usar que las clases mutables. Son menos propensos a errores y más seguros.

Para hacer que una clase sea inmutable, siga estas cinco reglas:

  1. No proporcione métodos para modificar el estado del objeto.
  2. Asegúrese de que esta clase no se pueda heredar . Esto evita que subclases descuidadas o maliciosas supongan que el estado del objeto ha cambiado, rompiendo así el comportamiento inmutable de la clase. La prevención de subclases generalmente se logra haciendo que una clase sea final, pero discutiremos otro método más adelante.
  3. Establezca todas las propiedades en final . Haga cumplir a través del sistema y comunique claramente sus intenciones. Además, si una referencia a una instancia recién creada se pasa de un hilo a otro sin sincronización, se debe garantizar un comportamiento correcto, como se describe en el modelo de memoria [JLS, 17.5; Goetz06,16].
  4. Establezca todas las propiedades en privadas . Esto evita que los clientes obtengan acceso a objetos mutables a los que hacen referencia las propiedades y modifiquen esos objetos directamente. Si bien técnicamente está permitido que una clase inmutable tenga una propiedad final pública que contenga un valor numérico de tipo base o una referencia a un objeto inmutable, esto no se recomienda ya que no permite que la representación interna cambie en una versión posterior.
  5. Garantice el acceso mutuamente exclusivo a cualquier componente mutable . Si su clase tiene propiedades que hacen referencia a objetos mutables, asegúrese de que los clientes de la clase no puedan obtener referencias a esos objetos. Nunca inicialice dicha propiedad en una referencia de objeto proporcionada por el cliente ni devuelva la propiedad desde un método de acceso. Copia defensiva en constructores, descriptores de acceso y métodos readObject (Artículo 88)

Los objetos inmutables son simples. Un objeto inmutable puede estar completamente en un estado, es decir, el estado en el que fue creado.

Los objetos inmutables son inherentemente seguros para subprocesos; no requieren sincronización . No se dañan cuando varios subprocesos acceden a ellos simultáneamente. Esta es una forma sencilla de lograr la seguridad de los subprocesos. Debido a que ningún hilo puede observar el efecto de otro hilo en un objeto inmutable, los objetos inmutables se pueden compartir libremente .

No solo se pueden compartir objetos inmutables, sino también información interna;

Los objetos inmutables proporcionan excelentes componentes básicos para otros objetos , ya sean mutables o inmutables. Mantener invariantes para un componente complejo es mucho más fácil si sabes que sus objetos internos no cambiarán.

Los objetos inmutables proporcionan un mecanismo de falla atómica gratuito . Su estado nunca cambia, por lo que las inconsistencias temporales son imposibles.

La principal desventaja de las clases inmutables es que se requiere un objeto separado para cada valor diferente .

Considerándolo todo, nunca escriba un método get para cada propiedad y luego escriba un método set correspondiente. Las clases deben ser inmutables a menos que haya una buena razón para hacerlas mutables . Las clases inmutables ofrecen muchas ventajas, la única desventaja es que en algunos casos pueden surgir problemas de rendimiento. Siempre debes usar objetos de valor más pequeño y hacerlos inmutables.

Para algunas clases, la inmutabilidad no es práctica. Si una clase no puede diseñarse para que sea inmutable, entonces su mutabilidad debe limitarse tanto como sea posible . Reducir el número de estados en los que puede existir un objeto facilita su análisis y reduce la probabilidad de errores. Por lo tanto, cada propiedad debe establecerse como final a menos que exista una buena razón para establecerla como no final. Combinando el consejo de este punto con el consejo del Punto 15, su inclinación natural es declarar cada propiedad como privada final a menos que exista una buena razón para no hacerlo .

La composición es mejor que la herencia.

A diferencia de la invocación de métodos, la herencia rompe la encapsulación . En otras palabras, una subclase se basa en los detalles de implementación de su clase principal para garantizar su funcionalidad correcta. La implementación de la clase principal puede seguir cambiando desde la versión de lanzamiento y, de ser así, la clase secundaria puede estar rota aunque no haya cambiado nada en su código. Por lo tanto, una subclase debe actualizarse y modificarse junto con su superclase, a menos que el autor de la superclase la haya diseñado específicamente con fines de herencia y haya documentado las instrucciones.

Ambos problemas surgen de métodos primordiales. Podría pensar que es seguro heredar de una clase si simplemente agrega nuevos métodos y no anula los métodos existentes. Si bien esta extensión es más segura, no está exenta de riesgos. Si la clase principal agrega un nuevo método en una versión posterior y, lamentablemente, usted le da a la subclase un método con la misma firma y un tipo de retorno diferente, entonces su subclase no podrá compilar . Si ya ha proporcionado a una clase secundaria un método que tiene la misma firma y tipo de retorno que el nuevo método de la clase principal, ahora lo está anulando y, por lo tanto, encontrará los problemas descritos anteriormente. Además, es cuestionable si su método cumplirá el contrato del nuevo método de la clase principal, ya que este contrato aún no se había escrito cuando escribió el método de la subclase.

Afortunadamente, existe una manera de evitar todos los problemas anteriores. En lugar de heredar una clase existente, agregue una propiedad privada a su nueva clase que sea una referencia de instancia de la clase existente. Este diseño se llama composición porque la clase existente se convierte en el componente de la nueva clase . Cada método de instancia en la nueva clase llama al método correspondiente en la instancia contenedora de la clase existente y devuelve el resultado. Esto se llama reenvío y los métodos de la nueva clase se denominan métodos de reenvío.

En resumen, la herencia es poderosa, pero problemática porque viola la encapsulación. Esto sólo se aplica si existe una verdadera relación de subtipo entre la clase secundaria y la clase principal . Aun así, la herencia puede generar fragilidad si la clase secundaria no está en el mismo paquete que la clase principal y la clase principal no está diseñada para la herencia. Para evitar esta fragilidad, utilice composición y reenvío en lugar de herencia, especialmente si existe una interfaz adecuada para implementar la clase contenedora. Las clases contenedoras no sólo son más robustas que las subclases, sino que también son más poderosas.

Si se utiliza la herencia, diseñarla y documentarla.

Entonces, ¿qué significa diseñar y documentar una clase para herencia?

Primero, la clase debe describir con precisión el impacto de anular este método. En otras palabras, la clase debe documentar el uso propio del método anulable. Para cada método público o protegido, la documentación debe indicar qué métodos anulados llama el método, en qué orden y cómo los resultados de cada llamada afectan el procesamiento posterior. (Métodos anulables, aquí se refiere a métodos modificados no finales, ya sean públicos o protegidos). De manera más general, una clase debe documentar cualquier situación en la que se pueda llamar a un método anulable.

Entonces, cuando diseñas una clase heredada, ¿cómo decides qué miembros protegidos exponer? Desafortunadamente, no existe una fórmula mágica. Lo mejor que puedes hacer es pensar mucho, hacer buenas pruebas y luego probarlas escribiendo subclases. Los miembros protegidos deben estar expuestos lo menos posible porque cada miembro representa un compromiso con los detalles de implementación.

La única forma de probar . Si omite un miembro protegido crítico, intentar escribir una subclase hará que la omisión sea dolorosamente obvia. Por el contrario, si escribe varias subclases y ninguna de ellas utiliza un miembro protegido, debe hacerla privada.

Los constructores no deben llamar directa o indirectamente a métodos reemplazables . La violación de esta regla hará que el programa falle. El constructor de la clase principal se ejecuta antes que el constructor de la clase secundaria, por lo que el método anulado en la clase secundaria se llama antes de que se ejecute el constructor de la clase secundaria. Si el método anulado se basa en cualquier inicialización realizada por el constructor de la subclase, este método no funcionará como se esperaba.

Una buena manera de resolver este problema es deshabilitar las subclases en clases que no tienen un diseño y documentación que indique que desea poder crear subclases de forma segura. Hay dos formas de desactivar las subclases. El más fácil de los dos es declarar final la clase. Otro enfoque es hacer que todos los constructores sean privados o privados a nivel de paquete y agregar fábricas estáticas públicas en lugar de los constructores .

Las interfaces son mejores que las clases abstractas.

Java tiene dos mecanismos para definir tipos que permiten múltiples implementaciones: interfaces y clases abstractas . Dado que los métodos de interfaz predeterminados se introdujeron en Java 8, ambos mecanismos permiten proporcionar implementaciones para ciertos métodos de instancia. Una diferencia importante es que para implementar un tipo definido por una clase abstracta, la clase debe ser una subclase de la clase abstracta.

Las interfaces son ideales para definir mixins. En términos generales, un mixin es una clase que, además de su "tipo principal", también puede declararse para proporcionar algún comportamiento opcional. Por ejemplo, Comparable es una interfaz de tipo que permite a una clase declarar que sus instancias están ordenadas en relación con otros objetos mutuamente comparables. Esta interfaz se denomina tipo porque permite "mezclar" funcionalidades opcionales con la funcionalidad principal del tipo. Las clases abstractas no se pueden usar para definir clases mixtas porque no se pueden cargar en clases existentes: una clase no puede tener más de una clase principal y no hay un lugar razonable en la jerarquía de clases para insertar un tipo.

Las interfaces permiten la construcción de marcos no jerárquicos. Las jerarquías de tipos son excelentes para organizar algunas cosas, pero otras no caen claramente en una jerarquía estricta.

Sin embargo, puede combinar las ventajas de las interfaces y las clases abstractas proporcionando una clase de implementación esquelética abstracta para usar con la interfaz . La interfaz define el tipo y puede proporcionar algunos métodos predeterminados, mientras que la clase de implementación esqueleto implementa los métodos de interfaz no primitivos restantes además de los métodos de interfaz originales. Heredar la implementación del esqueleto requiere la mayor parte del trabajo para implementar una interfaz. Este es el patrón de diseño del método de plantilla. Por ejemplo, Collections Framework proporciona una implementación de marco para acompañar a cada una de las principales interfaces de colección: AbstractCollection, AbstractSet, AbstractList y AbstractMap.

En resumen, una interfaz suele ser la mejor manera de definir un tipo que permita múltiples implementaciones . Si exporta una interfaz importante, debería considerar seriamente proporcionar una clase de implementación esqueleto. Siempre que sea posible, se debe proporcionar una implementación básica a través de un método predeterminado en la interfaz para que esté disponible para todos los implementadores de la interfaz. Es decir, las restricciones en las interfaces a menudo requieren que las clases de implementación esqueléticas tomen la forma de clases abstractas .

Diseñar interfaces para las generaciones futuras

Antes de Java 8, no era posible agregar métodos a una interfaz sin romper la implementación existente. Si se agrega un nuevo método a una interfaz, a la implementación existente a menudo le faltará el método, lo que provocará un error en tiempo de compilación. En Java 8, se agregó la construcción del método predeterminado para permitir que se agreguen métodos a las interfaces existentes . Pero agregar nuevos métodos a las interfaces existentes está plagado de riesgos.

La declaración de un método predeterminado . Aunque agregar métodos predeterminados en Java agrega métodos a las interfaces existentes, no hay garantía de que estos métodos estén disponibles en todas las implementaciones existentes. Los métodos predeterminados se "inyectan" en la implementación existente sin el conocimiento o consentimiento de la clase implementadora. Antes de Java 8, estas implementaciones se escribían utilizando la interfaz predeterminada, que nunca incluía ningún método nuevo.

Por lo tanto, es muy importante probar cada nueva interfaz antes de lanzarla. Al menos, debes preparar tres implementaciones diferentes. Es igualmente importante escribir múltiples programas cliente que utilicen instancias de cada nueva interfaz para realizar diversas tareas. Esto contribuirá en gran medida a garantizar que cada interfaz cumpla con todos los usos previstos. Estos pasos le permitirán descubrir fallas en su interfaz antes del lanzamiento, pero aún así solucionarlas fácilmente. Aunque algunas fallas existentes pueden corregirse después del lanzamiento de la interfaz, no cuente con esto .

Las interfaces solo se utilizan para definir tipos.

Un tipo de interfaz que falla es la llamada interfaz constante. Una interfaz de este tipo no contiene ningún método; solo contiene propiedades finales estáticas, cada una de las cuales genera una constante. La clase que utiliza estas constantes implementa la interfaz para evitar la necesidad de calificar el nombre de la constante con el nombre de la clase.

El patrón de interfaz constante es un mal uso de las interfaces . Las clases utilizan algunas constantes internamente, que son en su totalidad detalles de implementación. La implementación de una interfaz constante hace que este detalle de implementación se filtre en la API exportada de la clase. Para el usuario de la clase, no tiene sentido que la clase implemente una interfaz constante. De hecho, incluso podría confundirlos. Peor aún, representa una promesa: si la clase se modifica en una versión futura para que ya no necesite usar constantes, aún debe implementar la interfaz para garantizar la compatibilidad binaria. Si una clase no final implementa una interfaz constante, los espacios de nombres de todas sus subclases se verán contaminados por las constantes de la interfaz .

Si desea exportar constantes, existen varias opciones razonables. Si una constante está estrechamente relacionada con una clase o interfaz existente, debe agregarse a esa clase o interfaz. Por ejemplo, todas las clases contenedoras para tipos primitivos numéricos, como Integer y Double, exportan las constantes MIN_VALUE y MAX_VALUE. Si las constantes se pueden tratar como miembros de un tipo de enumeración, se deben exportar utilizando el tipo de enumeración. De lo contrario, debería utilizar una clase de utilidad no instanciable para exportar las constantes .

En resumen, las interfaces solo se pueden usar para definir tipos. No deben usarse solo para exportar constantes.

Prefiere la jerarquía de clases a las clases de etiquetas

A veces puede encontrar una clase cuyas instancias tienen dos o más estilos y contienen un campo de etiqueta que representa el estilo de la instancia. Por ejemplo, considere esta clase, que puede representar un círculo o un rectángulo:

// Tagged class - vastly inferior to a class hierarchy! 
class Figure {
    
        
    enum Shape {
    
     
        RECTANGLE, 
        CIRCLE 
    };
    // Tag field - the shape of this figure    
    final Shape shape;
    
    // These fields are used only if shape is RECTANGLE    
    double length;    double width;
    
    // This field is used only if shape is CIRCLE    
    double radius;
    
    // Constructor for circle
     Figure(double radius) {
    
            
         shape = Shape.CIRCLE;        
         this.radius = radius;    
     }
    
    // Constructor for rectangle    
    Figure(double length, double width) {
    
            
        shape = Shape.RECTANGLE;        
        this.length = length;        
        this.width = width;    
    }
    
    double area() {
    
            
        switch(shape) {
    
              
            case RECTANGLE:            
                return length * width;          
            case CIRCLE:            
                return Math.PI * (radius * radius);          
            default:            
                throw new AssertionError(shape);        
        }    
    } 
}

​ Estas clases de etiquetas tienen muchas desventajas. Su desordenado código repetitivo incluye declaraciones de enumeración, propiedades de etiquetas y declaraciones de cambio. Es menos legible porque se combinan varias implementaciones en una clase .

Si agrega un estilo, debe recordar agregar un caso a cada declaración de cambio; de lo contrario, la clase fallará en tiempo de ejecución. Finalmente, el tipo de datos de una instancia no proporciona ninguna pista sobre el estilo. En resumen, las clases de etiquetas son detalladas, propensas a errores e ineficientes .

Afortunadamente, los lenguajes orientados a objetos como Java brindan una mejor opción para definir un único tipo de datos que puede representar múltiples estilos de objetos: subtipos . Las clases de etiquetas son solo una simple imitación de una jerarquía de clases.

Para convertir una clase de etiqueta en una jerarquía de clases, primero defina una clase abstracta que contenga métodos abstractos cuyo comportamiento dependa del valor de la etiqueta. A continuación, defina una subclase concreta de la clase raíz para cada tipo de clase de etiqueta original.

// Class hierarchy replacement for a tagged class 
abstract class Figure {
    
         
    abstract double area(); 
} 
 
class Circle extends Figure {
    
         
    final double radius; 
 
    Circle(double radius) {
    
     
        this.radius = radius; 
    } 
    
     @Override double area() {
    
     
         return Math.PI * (radius * radius); 
     } 
} 

class Rectangle extends Figure {
    
         
    final double length;     
    final double width; 
 
    Rectangle(double length, double width) {
    
             
        this.length = length;         
        this.width  = width;     
    }     
    @Override double area() {
    
     
        return length * width; 
    } 
}

Otra ventaja de las jerarquías de clases , aumentando así la flexibilidad y haciendo que la verificación de tipos en tiempo de compilación sea más eficiente.

En resumen, las clases de etiquetas rara vez se aplican. Si desea escribir una clase con un atributo de etiqueta explícito, considere si el atributo de etiqueta se puede eliminar y la clase reemplazada por la jerarquía de clases. Cuando encuentre una clase existente con un atributo de etiqueta, considere refactorizarla en una jerarquía de clases .

Dar prioridad a las clases de miembros estáticos

​Una clase anidada es una clase definida dentro de otra clase. Las clases anidadas sólo deberían existir dentro de su clase adjunta. Si una clase anidada es útil en alguna otra situación, entonces debería ser una clase de nivel superior.

Hay cuatro tipos de clases anidadas: clases de miembros estáticos, clases de miembros no estáticos, clases anónimas y clases parciales . Excepto la primera, las tres restantes se denominan clases internas. Esta entrada le indica cuándo utilizar qué tipo de clase anidada y por qué.

Un uso común de las clases de miembros estáticos , útiles solo cuando se usan con sus clases externas. Por ejemplo, considere un tipo de enumeración que describe las operaciones admitidas por una calculadora (elemento 34). La enumeración Operación debe ser una clase miembro estática pública de la clase Calculadora. Los clientes de Calculadora pueden hacer referencia a operaciones utilizando nombres como Calculadora.Operación.PLUS y Calculadora.Operación.MINUS.

Sintácticamente, la única diferencia entre las clases de miembros estáticos y las clases de miembros no estáticos es que las clases de miembros estáticos tienen el modificador estático en su declaración. Cada instancia de una clase miembro no estática está implícitamente asociada con la instancia de host de su clase contenedora. En un método de instancia de una clase miembro no estática, puede llamar a un método en la instancia del host u obtener una referencia a la instancia del host utilizando un constructor calificado [JLS, 15.8.4]. Si las instancias de una clase anidada pueden existir aisladas de las instancias de su clase anfitriona, entonces la clase anidada debe ser una clase miembro estática: no es posible crear una instancia de una clase miembro no estática sin una instancia anfitriona .

La asociación entre una instancia de clase miembro no . Normalmente, la asociación se establece automáticamente llamando al constructor de la clase miembro no estática en el método de instancia de la clase anfitriona.

Si declara una clase miembro que no requiere acceso a la instancia del host, coloque el modificador estático en su declaración para convertirla en una clase miembro estática en lugar de una clase miembro no estática . Si omite este modificador, cada instancia tendrá una referencia externa oculta a su instancia de host. Como se mencionó anteriormente, almacenar esta referencia requiere tiempo y espacio. Lo que es más grave es que seguirá residiendo en la memoria incluso si la clase de host cumple las condiciones para la recolección de basura (elemento 7). La pérdida de memoria resultante puede ser catastrófica. Como las referencias son invisibles, a menudo son difíciles de detectar.

Definir una única clase de nivel superior en un único archivo fuente

Aunque el compilador de Java permite definir varias clases de nivel superior en un único archivo fuente, hacerlo no supone ningún beneficio y conlleva riesgos importantes. El riesgo surge al definir múltiples clases de nivel superior en un archivo fuente, lo que hace posible proporcionar múltiples definiciones para una clase. La definición que se utilice se ve afectada por el orden en que los archivos fuente se pasan al compilador.

// Two classes defined in one file. Don't ever do this! 
//反例如下
class Utensil {
    
         
    static final String NAME = "pan"; 
} 
 
class Dessert {
    
         
    static final String NAME = "cake"; 
} 

Si está intentando colocar varias clases de nivel superior en un único archivo fuente, considere usar clases de miembros estáticos (elemento 24) como alternativa a dividir la clase en archivos fuente separados. Si estas clases dependen de otra clase, normalmente es una mejor opción convertirlas en clases miembro estáticas, ya que mejora la legibilidad y puede reducir la accesibilidad de la clase al declararlas privadas .

No utilice tipos sin formato directamente

​ Una clase o interfaz cuya declaración tiene uno o más parámetros de tipo (parámetros de tipo) se denomina clase genérica o interfaz genérica. Por ejemplo, la interfaz Lista tiene un parámetro de tipo único E, que representa su tipo de elemento. El nombre completo de la interfaz es Lista (se pronuncia "E" para lista), pero la gente suele llamarla Lista. Las clases e interfaces genéricas se denominan colectivamente tipos genéricos.

Cada genérico define un tipo sin formato, que es el nombre del tipo genérico sin ningún parámetro de tipo [JLS, 4.8]. Por ejemplo, el tipo primitivo correspondiente a Lista es Lista. Los tipos primitivos se comportan como si toda la información de tipo genérico se hubiera borrado de la declaración de tipo. Existen principalmente por compatibilidad con código pregenérico .

Es legal utilizar tipos primitivos (genéricos sin parámetros de tipo), pero no deberías hacerlo. Si utiliza tipos primitivos, pierde toda la seguridad y las ventajas expresivas de los genéricos . ¿Por qué los diseñadores de lenguajes permitieron tipos primitivos en primer lugar, dado que no deberías usarlos? La respuesta es por compatibilidad.

Eliminar advertencias no marcadas

Al programar con genéricos, verá una serie de advertencias del compilador: advertencias de conversión no marcadas, advertencias de llamada a métodos no marcadas, advertencias de tipo de longitud variable parametrizadas no marcadas y advertencias de conversión no marcadas. Cuanta más experiencia adquiera con los genéricos, menos advertencias recibirá, pero no espere que el código recién escrito se compile limpiamente.

Cuando reciba una advertencia de que necesita pensar más, ¡persevere! Elimine todas las advertencias no verificadas posibles . Si elimina todas las advertencias, puede estar seguro de que su código tiene seguridad de escritura, lo cual es algo muy bueno . Esto significa que no obtendrá una ClassCastException en tiempo de ejecución y aumenta su confianza en que su programa se comportará como esperaba.

Si no puede suprimir una advertencia, pero puede demostrar que el código que causó la advertencia es de tipo seguro, entonces (y solo entonces) suprima la advertencia con la anotación @SuppressWarnings("unchecked") . Si suprime las advertencias sin demostrar primero que su código es seguro, se dará una falsa sensación de seguridad. El código puede compilarse sin advertencias, pero aún así puede generar una excepción ClassCastException en tiempo de ejecución.

La anotación SuppressWarnings se puede utilizar en cualquier declaración, desde una única declaración de variable local hasta una clase completa. Utilice siempre la anotación SuppressWarnings en el ámbito más pequeño posible . Generalmente se trata de una declaración de variable o de un método o constructor muy breve. Nunca utilice la anotación SuppressWarnings en una clase completa. Hacerlo puede ocultar advertencias importantes.

Siempre que utilice la anotación @SuppressWarnings(“sin marcar”), agregue un comentario que explique por qué es seguro. Esto ayudará a que otros comprendan el código y, lo que es más importante, reducirá la posibilidad de que alguien modifique el código para que el cálculo sea inseguro.

Las listas son mejores que las matrices

Las matrices se diferencian de los genéricos en dos aspectos importantes.

  1. Las matrices son covariantes . Esto significa que si Sub es un subtipo de Super, entonces el tipo de matriz Sub[] es un subtipo del tipo de matriz Super[].
  2. Los genéricos son invariantes : para dos tipos diferentes, Tipo1 y Tipo2, Lista no es ni un subtipo ni un supertipo de Lista.

Se podría pensar que esto significa que los genéricos son inadecuados, pero se podría argumentar que los arreglos son deficientes. Este código es legal:

// Fails at runtime! 
Object[] objectArray = new Long[1]; 
objectArray[0] = "I don't fit in"; 
// Throws ArrayStoreException 

​ Pero este no lo es:

// Won't compile! 
List<Object> ol = new ArrayList<Long>(); 
// Incompatible types ol.add("I don't fit in");

De cualquier manera, no puede colocar un tipo String en un contenedor de tipo Long, pero con una matriz encontrará un error en tiempo de ejecución; con una lista, el error se puede encontrar en tiempo de compilación . Por supuesto, preferirá encontrar el error en el momento de la compilación.

La segunda diferencia importante entre los arreglos y los genéricos es que los arreglos están cosificados.

  1. Las matrices conocen y aplican sus tipos de elementos en tiempo de ejecución . Como se mencionó anteriormente, si intenta colocar una cadena en una matriz larga, obtiene una excepción ArrayStoreException.
  2. Los genéricos se implementan mediante borrado. Esto significa que solo imponen restricciones de tipo en tiempo de compilación y descartan (o borran) la información de tipo de elemento en tiempo de ejecución . Erasure garantiza una transición fluida a los genéricos en Java 5 al permitir que los tipos genéricos interoperen libremente con el código heredado que no utiliza genéricos (Artículo 26).

En resumen, las matrices y los genéricos tienen reglas de tipos muy diferentes. Los arreglos son covariantes y cosificados; los genéricos son inmutables y de tipo borrado. Por lo tanto, las matrices proporcionan seguridad de tipos en tiempo de ejecución, pero no seguridad de tipos en tiempo de compilación, y viceversa . En términos generales, las matrices y los genéricos no se combinan bien . Si se encuentra mezclándolos y recibiendo errores o advertencias en tiempo de compilación, su primer impulso debería ser reemplazar la matriz con una lista.

Priorizar los genéricos

Los tipos genéricos . Cuando diseñe nuevos tipos, asegúrese de que puedan usarse sin dichos modelos. Por lo general, esto significa hacer que el tipo sea genérico. Si tiene algún tipo existente que debería ser genérico pero no lo es, hágalo genérico. Esto facilita el uso para nuevos usuarios de este tipo sin interrumpir a los clientes existentes.

Prefiero usar métodos genéricos.

En resumen, al igual que los tipos genéricos, los métodos genéricos son más seguros y fáciles de usar que los métodos que requieren que el cliente realice conversiones explícitas en parámetros de entrada y valores de retorno. Al igual que los tipos, debes asegurarte de que tus métodos puedan usarse sin conversión, lo que normalmente significa que son genéricos. Los métodos existentes deben generalizarse y su uso requiere casting . Esto facilita el uso para nuevos usuarios sin interrumpir a los clientes existentes.

Utilice comodines calificados para aumentar la flexibilidad

Para obtener la máxima flexibilidad, utilice tipos comodín para los parámetros de entrada que representen a productores o consumidores. Si un parámetro de entrada es tanto un productor como un consumidor, entonces los tipos comodín no sirven de nada: necesita una coincidencia de tipo exacta, que es el caso sin comodines.

Aquí hay un mnemónico para ayudarle a recordar qué tipo de comodín usar: PECS significa: productor-extiende, consumidor-super. En otras palabras, si un tipo parametrizado representa un productor T, use <? extends T>; si representa un consumidor T, use <? super T>.

En resumen, utilizar tipos comodín en su API, aunque complicado, hace que la API sea más flexible . Si se escribe una biblioteca de clases que se utilizará ampliamente, el uso correcto de tipos comodín debe considerarse obligatorio. Recuerde la regla básica: productor-extiende, consumidor-super (PECS). Recuerde también que todos los Comparables y Comparadores son consumidores.

Combinación adecuada de parámetros genéricos y variados.

¿Por qué es legal declarar un método con parámetros variados genéricos, cuando crear explícitamente una matriz genérica es ilegal? La respuesta es que los métodos con argumentos variados de tipos genéricos o parametrizados pueden ser muy útiles en la práctica , por lo que los diseñadores de lenguajes optan por vivir con esta inconsistencia. De hecho, la biblioteca de clases Java exporta varios de estos métodos, incluidos Arrays.asList(T… a), Collections.addAll(Collection<? super T> c, T… elements), EnumSet.of(E first, E… rest) . A diferencia de los métodos peligrosos mostrados anteriormente, estos métodos de biblioteca tienen seguridad de tipos.

En Java 7, se agregó la anotación @SafeVarargs a la plataforma para permitir a los autores de métodos con argumentos variados genéricos suprimir automáticamente las advertencias del cliente . En esencia, la anotación @SafeVarargs constituye el compromiso del autor con un método de tipo seguro. A cambio de esta promesa, el compilador se compromete a no advertir a los usuarios sobre la llamada a métodos potencialmente inseguros.

Tenga cuidado de no anotar un método con la anotación @SafeVarargs a menos que sea realmente seguro. Entonces, ¿qué hay que hacer para garantizar esto? Recuerde que cuando se llama a un método, se crea una matriz genérica para contener los argumentos variados. Un método es seguro si no almacena nada en la matriz (sobrescribe los parámetros) y no permite escapar de las referencias a la matriz (lo que permite que código que no es de confianza acceda a la matriz). En otras palabras, si la matriz variadic se usa solo para pasar un número variable de argumentos al método desde la persona que llama (que es, después de todo, el propósito de los argumentos variadic), entonces el método es seguro.

En resumen, los varargs y los genéricos no interactúan bien porque el mecanismo de los varargs es una abstracción frágil construida sobre matrices, y las matrices tienen reglas de tipo diferentes a las de los genéricos. Aunque los parámetros variados genéricos no son seguros, son legales. Si elige escribir un método utilizando varargs genéricos (o parametrizados), primero asegúrese de que el método tenga seguridad de tipos y luego anótelo con la anotación @SafeVarargs para evitar un uso desagradable.

Priorizar contenedores heterogéneos con seguridad de tipo

Los usos comunes de los genéricos incluyen colecciones, como Set y Map<K,V>, y contenedores de un solo elemento, como ThreadLocal y AtomicReference. En todos estos usos es un contenedor parametrizado. Esto limita cada contenedor a un número fijo de parámetros de tipo. Normalmente esto es lo que quieres. Un Conjunto tiene un único parámetro de tipo, que representa su tipo de elemento; un Mapa tiene dos, que representan sus tipos de clave y valor; y así sucesivamente.

A veces, sin embargo, se necesita más flexibilidad. Por ejemplo, una fila de una base de datos puede tener cualquier cantidad de columnas y es bueno poder acceder a ellas de forma segura. Afortunadamente, existe una manera sencilla de lograr este efecto. La idea es parametrizar claves en lugar de contenedores. Luego, la clave parametrizada se envía al contenedor para insertar o recuperar un valor. El sistema de tipos genéricos se utiliza para garantizar que el tipo de un valor sea coherente con su clave .

Como ejemplo simple de este enfoque, considere una clase Favoritos que permite a sus clientes guardar y recuperar instancias favoritas de cualquier número de tipos. Los objetos de clase de este tipo actuarán como parte de la clave parametrizada . La razón es que esta clase Class es genérica. El tipo de clase no es simplemente Clase literalmente, sino Clase. Por ejemplo, String.class es de tipo Class y Integer.class es de tipo Class. Cuando se pasa una clase literal en un método para pasar información de tipo en tiempo de compilación y en tiempo de ejecución, se denomina token de tipo.

Código de muestra:

// Typesafe heterogeneous container pattern - API 
public class Favorites {
    
         
    
    public <T> void putFavorite(Class<T> type, T instance);  
    
    public <T> T getFavorite(Class<T> type); 
}

En resumen, el uso común de API genéricas (tomando la API de colección como ejemplo) limita cada contenedor a un número fijo de parámetros de tipo. Puede solucionar esta limitación colocando el parámetro de tipo en la clave en lugar del contenedor . Puede utilizar objetos Class como claves para este contenedor heterogéneo con seguridad de tipos. Los objetos de clase utilizados de esta manera se denominan tokens de tipo. También se pueden utilizar tipos de claves personalizadas. Por ejemplo, puede tener un tipo DatabaseRow que represente una fila de base de datos (contenedor) y una columna de tipo genérico como clave.

Utilice tipos de enumeración en lugar de constantes enteras

Antes de que se agregaran los tipos de enumeración al lenguaje, un patrón común para representar tipos de enumeración era declarar un conjunto de constantes denominadas ints, una para cada miembro del tipo:

// The int enum pattern - severely deficient! 
public static final int APPLE_FUJI         = 0; 
public static final int APPLE_PIPPIN       = 1; 

Esta técnica, conocida como modo de enumeración int, tiene muchas desventajas. No proporciona ningún tipo de seguridad, ni proporciona ninguna expresividad. No existe una manera fácil de convertir una constante de enumeración int en una cadena imprimible. Si imprime dicha constante o la muestra desde el depurador, todo lo que verá es un número, lo cual no es muy útil. No existe una forma confiable de iterar sobre todas las constantes de enumeración int en un grupo, o incluso obtener el tamaño de un grupo de enumeración int .

Afortunadamente, Java proporciona una alternativa que evita todas las desventajas de los patrones de enumeración int y String y proporciona muchos beneficios adicionales.

​ Java 枚举类型背后的基本思想很简单:它们是通过公共静态 final 属性为每个枚举常量导出一个实例的类。 由于没有可访问的构造方法,枚举类型实际上是 final 的。 由于客户既不能创建枚举类型的实例也不能继承它, 除了声明的枚举常量外,不能有任何实例。 换句话说,枚举类型是实例控制的(第 6 页)。 它们是单例(条目 3) 的泛型化,基本上是单元素的枚举。

枚举提供了编译时类型的安全性。 尝试传递错误类型的值将导致编译时错误,因为会尝试将一个枚举类型的 表达式分配给另一个类型的变量,或者使用 == 运算符来比较不同枚举类型的值。

有一种更好的方法可以将不同的行为与每个枚举常量关联起来:在枚举类型中声明一个抽象的 apply 方法,并用常量特定的类主体中的每个常量的具体方法重写它。 这种方法被称为特定于常量(constant-specific)的 方法实现:

// Enum type with constant-specific method implementations 
public enum Operation {
    
       
    PLUS  {
    
    public double apply(double x, double y){
    
    return x + y;}},   
    MINUS {
    
    public double apply(double x, double y){
    
    return x - y;}},  
    TIMES {
    
    public double apply(double x, double y){
    
    return x * y;}},   
    DIVIDE{
    
    public double apply(double x, double y){
    
    return x / y;}}; 
 
  public abstract double apply(double x, double y); 
} 

​ 如果向以上示例中操作添加新的常量,则不太可能会忘记提供 apply 方法,因为该方法紧跟在每个常量声 明之后。 万一忘记了,编译器会提醒你,因为枚举类型中的抽象方法必须被所有常量中的具体方法重写

​ 总之,枚举类型优于 int 常量的优点是令人信服的。 枚举更具可读性,更安全,更强大。 许多枚举不需要显 式构造方法或成员,但其他人则可以通过将数据与每个常量关联并提供行为受此数据影响的方法而受益。 使用单一 方法关联多个行为可以减少枚举。 在这种相对罕见的情况下,更喜欢使用常量特定的方法来枚举自己的值。 如果一 些(但不是全部)枚举常量共享共同行为,请考虑策略枚举模式。

使用实例属性代替序数

​ 永远不要从枚举的序号中得出与它相关的值; 请将其保存在实例属 性中:

public enum Ensemble {
    
         
    SOLO(1), 
    DUET(2), 
    TRIO(3), 
    QUARTET(4), 
    QUINTET(5),     
    SEXTET(6), 
    SEPTET(7), 
    OCTET(8), 
    DOUBLE_QUARTET(8),     
    NONET(9), 
    DECTET(10), 
    TRIPLE_QUARTET(12); 
 
    private final int numberOfMusicians;
    
    Ensemble(int size) {
    
     
        this.numberOfMusicians = size; 
    }     
    
    public int numberOfMusicians() {
    
     
        return numberOfMusicians; 
    } 
} 

Utilice EnumSet en lugar de propiedades de bits

A continuación se muestra el ejemplo anterior del uso de enumeraciones y colecciones de enumeraciones en lugar de propiedades de bits. Es más corto, más claro y más seguro:

// EnumSet - a modern replacement for bit fields 
public class Text {
    
         
    
public enum Style {
    
     BOLD, ITALIC, UNDERLINE, STRIKETHROUGH } 
 
// Any Set could be passed in, but EnumSet is clearly best     
    public void applyStyles(Set<Style> styles) {
    
     ... } 
}

En resumen, solo porque el tipo de enumeración se usará en una colección, no hay razón para representarlo con un atributo de bit. La clase EnumSet combina la simplicidad y el rendimiento de las propiedades de bits con todas las ventajas del tipo de enumeración descrito en el Elemento 34. Un verdadero inconveniente de EnumSet es que no crea un EnumSet inmutable como en Java 9, pero eso puede solucionarse en una próxima versión. Al mismo tiempo, puede usar Collections.unmodifiableSet para encapsular un EnumSet, pero la simplicidad y el rendimiento se verán afectados.

Utilice EnumMap en lugar del índice ordinal

En resumen, usar números ordinales para indexar matrices no es apropiado: use EnumMap en su lugar. Si la relación que representa es multidimensional, use EnumMap<…, EnumMap<…>>. Los programadores de aplicaciones rara vez deberían usar Enum.ordinal (Ítem 35) y, si lo usan, es un caso especial del principio general.

Ejemplo de uso:

// Adding a new phase using the nested EnumMap implementation 
public enum Phase {
    
     
 
    SOLID, LIQUID, GAS, PLASMA; 
 
    public enum Transition {
    
             
        MELT(SOLID, LIQUID), 
        FREEZE(LIQUID, SOLID),         
        BOIL(LIQUID, GAS),   
        CONDENSE(GAS, LIQUID),         
        SUBLIME(SOLID, GAS), 
        DEPOSIT(GAS, SOLID),         
        IONIZE(GAS, PLASMA), 
        DEIONIZE(PLASMA, GAS);         ... 
            // Remainder unchanged     
    } 
} 

Implementar enumeraciones extensibles usando interfaces.

La mayoría de las veces, la extensibilidad de enumeraciones es una mala idea. De manera confusa, los elementos de un tipo extendido son instancias del tipo base y viceversa. No existe una buena manera de enumerar todos los elementos de un tipo base y sus extensiones. Finalmente, la escalabilidad complica muchos aspectos del diseño y la implementación.

Dicho esto, existe al menos un caso de uso convincente para los tipos de enumeración extensibles, y son los códigos de operación , también conocidos como códigos de operación. Los códigos de operación son tipos enumerados cuyos elementos representan operaciones en alguna máquina, como el tipo de operación en el elemento 34, que representa funciones en una calculadora simple. A veces es necesario permitir que los usuarios de la API proporcionen sus propias operaciones, ampliando efectivamente el conjunto de operaciones proporcionadas por la API.

// Emulated extensible enum using an interface 
public interface Operation {
    
         
    double apply(double x, double y); 
} 
 
 
public enum BasicOperation implements Operation {
    
         
    PLUS("+") {
    
             
        public double apply(double x, double y) {
    
     return x + y; }     
    },     
    MINUS("-") {
    
             
        public double apply(double x, double y) {
    
     return x - y; }     
    },     
    TIMES("*") {
    
             
        public double apply(double x, double y) {
    
     return x * y; }     
    },     
    DIVIDE("/") {
    
             
        public double apply(double x, double y) {
    
     return x / y; }     
    };     
    
    private final String symbol; 
 
    BasicOperation(String symbol) {
    
             
        this.symbol = symbol;     
    } 
 
    @Override public String toString() {
    
             
        return symbol;     
    } 
} 

​ 总之,虽然不能编写可扩展的枚举类型,但是你可以编写一个接口来配合实现接口的基本的枚举类型,来对它进 行模拟。 这允许客户端编写自己的枚举(或其它类型)来实现接口。如果 API 是根据接口编写的,那么在任何使 用基本枚举类型实例的地方,都可以使用这些枚举类型实例.

注解优于命名方式

过去,通常使用命名模式(naming patterns)来指示某些程序元素需要通过工具或框架进行特殊处理。 例如, 在第 4 版之前,JUnit 测试框架要求其用户通过以 test[Beck04] 开始名称来指定测试方法。 这种技术是有效的,但它 有几个很大的缺点。 首先,拼写错误导致失败,但不会提示。 例如,假设意外地命名了测试方法 tsetSafetyOverride 而不是 testSafetyOverride 。 JUnit 3 不会报错,但它也不会执行测试,导致错误 的安全感。

​ 命名模式的第二个缺点是无法确保它们仅用于适当的程序元素。

​ 命名模式的第三个缺点是它们没有提供将参数值与程序元素相关联的好的方法。 例如,假设想支持只有在抛出 特定异常时才能成功的测试类别。 异常类型基本上是测试的一个参数。 你可以使用一些精心设计的命名模式将异常 类型名称编码到测试方法名称中,但这会变得丑陋和脆弱。

​ 这个项目中的测试框架只是一个演示,但它清楚地表明了注解相对于命名模式的优越性,而且它仅仅描绘了你可 以用它们做什么的外观。 如果编写的工具要求程序员将信息添加到源代码中,请定义适当的注解类型。 当可以使用 注解代替时,没有理由使用命名模式

Esto significa que la mayoría de los programadores, excepto los desarrolladores específicos (herramientas), no necesitan definir tipos de anotaciones. Pero todos los programadores deberían utilizar los tipos de anotaciones predefinidos proporcionados por Java (elementos 40, 27). Además, considere utilizar anotaciones proporcionadas por su IDE o herramienta de análisis estático. Estas anotaciones pueden mejorar la calidad de la información de diagnóstico proporcionada por estas herramientas. Sin embargo, tenga en cuenta que estas anotaciones aún no están estandarizadas, por lo que es posible que se requiera trabajo adicional si surgen cambios de herramientas o estándares.

Utilice siempre la anotación Anular

Por lo tanto, debe utilizar la anotación Override en cada declaración de método que crea que anulará la declaración de clase principal . Hay una pequeña excepción a esta regla. Si está escribiendo una clase que no está marcada como abstracta y está seguro de que anula un método abstracto en su clase principal, no necesita poner una anotación Override en el método. En una clase que no está declarada abstracta, el compilador emitirá un mensaje de error si el método de la clase principal abstracta no se puede anular. Sin embargo, es posible que quieras centrarte en todos los métodos de tu clase que anulan los métodos de la clase principal, en cuyo caso siempre debes anotar también estos métodos. La mayoría de los IDE se pueden configurar para insertar automáticamente una anotación de Anulación cuando se selecciona un método anulado.

La anotación Override se puede utilizar para anular declaraciones de métodos de interfaces y clases. Con la llegada de los métodos predeterminados, es una buena práctica utilizar Override en la implementación concreta del método de interfaz para garantizar que la firma sea correcta. Si sabe que una interfaz no tiene un método predeterminado, puede optar por ignorar la anotación Anular en la implementación específica del método de la interfaz para reducir la confusión.

Definir tipos utilizando interfaces de marcador.

Una interfaz de marcador no contiene declaraciones de métodos, sino que simplemente especifica (o "marca") una clase que implementa una interfaz con ciertas propiedades . Por ejemplo, considere la interfaz serializable (Capítulo 12). Al implementar esta interfaz, una clase indica que sus instancias pueden escribir (o "serializar") un ObjectOutputStream.

Es posible que haya oído hablar de la anotación de marcado (elemento 39) que marca una interfaz como obsoleta. Esta afirmación es incorrecta. Las interfaces de marcadores tienen dos ventajas sobre las anotaciones de marcadores:

  1. En primer lugar, la interfaz de marcador define un tipo que se implementa mediante instancias de la clase de marcador; las anotaciones de marcador no . La presencia de un tipo de interfaz de marcador permite detectar errores en tiempo de compilación, mientras que si se utilizan anotaciones de marcador, los errores no se pueden detectar hasta el tiempo de ejecución;
  2. Otra ventaja de la interfaz de marcador para anotaciones de marcador es que se pueden orientar con mayor precisión . Si se declara un tipo de anotación utilizando el ElementType.TYPE de destino, se puede aplicar a cualquier clase o interfaz. Supongamos que hay una etiqueta que solo se aplica a implementaciones de una interfaz específica. Si se define como una interfaz de marcador, puede extender la interfaz única a la que se aplica, asegurando que todos los tipos de marcador sean también subtipos de la interfaz única a la que se aplica;

En resumen, tanto las interfaces de marcadores como las anotaciones de marcadores tienen sus usos. Si desea definir un tipo sin ningún método nuevo asociado, una interfaz etiquetada es el camino a seguir. Las anotaciones de marcado son la opción correcta si desea marcar elementos del programa distintos de clases e interfaces, o si desea adaptar el marcado a un marco que ya hace un uso intensivo de los tipos de anotaciones. Si se encuentra escribiendo un tipo de anotación etiquetada dirigida a ElementType.TYPE, tómese un momento para determinar si debería ser un tipo de anotación y si una interfaz etiquetada sería más apropiada .

Las expresiones lambda son mejores que las clases anónimas

A diferencia de los métodos y clases, las lambdas no tienen nombres ni documentación; si el cálculo no se explica por sí mismo o excede unas pocas líneas, no lo incluya en una expresión lambda . Una línea de código es ideal para una lambda y tres líneas de código es un valor razonablemente grande. La violación de esta regla puede afectar seriamente la legibilidad de su programa. Si una lambda es larga o difícil de leer, busque una manera de simplificarla o refactorice su programa para eliminarla.

Asimismo, se podría pensar que las clases anónimas están obsoletas en la era de las lambdas. Esto se acerca más a la verdad, pero hay algunas cosas que puedes hacer con clases anónimas que no puedes hacer con lambdas. Lambda se limita a interfaces funcionales. Si desea crear una instancia de una clase abstracta, puede hacerlo utilizando una clase anónima, pero no una lambda. Asimismo, puede utilizar clases anónimas para crear instancias de interfaz con múltiples métodos abstractos . Finalmente, la lambda no puede obtener una referencia a sí misma. En lambdas, la palabra clave this se refiere a la instancia adjunta, que suele ser lo que desea. En una clase anónima, la palabra clave this se refiere a la instancia de clase anónima. Si necesita acceder a un objeto de función desde dentro de él, debe utilizar una clase anónima.

En resumen, a partir de Java 8, lambda es, con diferencia, la mejor manera de representar objetos funcionales pequeños. No utilice clases anónimas como objetos de función a menos que deba crear instancias de tipos de interfaz no funcionales .

Las referencias a métodos son mejores que las expresiones lambda.

La principal ventaja de lambda sobre las clases anónimas es que es más conciso. Java proporciona una forma de generar objetos de función, que es más concisa que lambda: referencias de métodos.

//lambda
map.merge(key, 1, (count, incr) -> count + incr); 

//method references
map.merge(key, 1, Integer::sum); 

También le dan una consecuencia si la lambda se vuelve demasiado larga o compleja: puede extraer el código de la lambda en un nuevo método y reemplazar la lambda con una referencia a ese método . Puedes darle un buen nombre a este método y documentarlo.

Las referencias de métodos Si las referencias a métodos parecen más cortas y claras, úselas; de lo contrario, quédese con lambdas .

Prefiere interfaces funcionales estándar

El paquete java.util.function proporciona una gran cantidad de interfaces funcionales estándar para su uso. Si una de las interfaces funcionales estándar funciona, generalmente debería usarla en lugar de una interfaz funcional diseñada específicamente . Esto hará que su API sea más fácil de aprender al reducir sus conceptos innecesarios y proporcionará importantes beneficios de interoperabilidad porque muchas interfaces funcionales estándar proporcionan métodos predeterminados útiles. Por ejemplo, la interfaz Predicate proporciona métodos para combinar juicios. En nuestro ejemplo de LinkedHashMap, la interfaz estándar BiPredicate<Map<K,V>, Map.Entry<K,V>> debe tener prioridad sobre el uso de la interfaz personalizada EldestEntryRemovalFunction.

Hay 43 interfaces en java.util.Function. No puede esperar recordarlas todas, pero si recuerda las seis interfaces básicas, podrá derivar el resto cuando las necesite. La interfaz básica opera con tipos de referencia de objetos.

  1. La interfaz del operador representa un método cuyos tipos de resultados y parámetros son del mismo tipo.
  2. La interfaz Predicate significa que su método acepta un parámetro y devuelve un valor booleano.
  3. La interfaz de función representa métodos cuyos parámetros y tipos de retorno son diferentes.
  4. La interfaz Proveedor representa un método que no toma parámetros y devuelve un valor (o "suministro").
  5. Consumidor significa que el método acepta un parámetro y no devuelve nada, esencialmente usando su parámetro.

Las seis interfaces funcionales básicas se resumen a continuación:

[Error en la transferencia de la imagen del enlace externo. El sitio de origen puede tener un mecanismo anti-leeching. Se recomienda guardar la imagen y cargarla directamente (img-lEnJWnyc-1685157110629) (C:\Users\lixuewen\AppData\Roaming\Typora\ typora-imagenes-de-usuario\ image-20230522140251781.png)]

​ En resumen, Java ahora tiene expresiones lambda, por lo que debes considerar las expresiones lambda para diseñar tu API. Acepta tipos de interfaz funcionales en la entrada y los devuelve en la salida. En términos generales, es mejor utilizar las interfaces estándar proporcionadas en java.util.function.Function, pero tenga en cuenta que en casos relativamente raros es mejor escribir su propia interfaz funcional.

Utilice Streams de forma inteligente y sensata

La API Stream se agregó en Java 8 para simplificar la tarea de realizar operaciones por lotes de forma secuencial o en paralelo. La API proporciona dos abstracciones clave: secuencias, que representan secuencias finitas o infinitas de elementos de datos, y canalizaciones de secuencias, que representan cálculos de varios niveles sobre estos elementos. Los elementos de un Stream pueden provenir de cualquier lugar. Las fuentes comunes incluyen colecciones, matrices, archivos, comparadores de patrones de expresiones regulares, generadores de números pseudoaleatorios y otras secuencias.

Una canalización de flujo consta de cero o más operaciones intermedias en un flujo de origen y una operación final . Cada operación intermedia transforma la secuencia de alguna manera, como asignar cada elemento a una función en ese elemento o filtrar todos los elementos que no cumplen alguna condición. Todas las operaciones intermedias transforman una secuencia en otra secuencia, cuyos tipos de elementos pueden ser o no los mismos que los de la secuencia de entrada. Finalización Un cálculo final resultante de una operación intermedia realizada en una secuencia, como almacenar sus elementos en una colección, devolver un elemento o imprimir todos sus elementos .

Mejore la legibilidad proporcionando . El uso de métodos auxiliares es más importante para la legibilidad en canalizaciones de streaming que en código iterativo porque las canalizaciones carecen de información de tipo explícita y de variables temporales con nombre. (Funciones extraídas para simplificar el código)

Cuando empiece a utilizar transmisiones, es posible que sienta la necesidad de convertir todos sus bucles en transmisiones, pero resista esta tentación. Si bien esto es posible, puede dañar la legibilidad y el mantenimiento de su código base. Normalmente, las tareas moderadamente complejas funcionan bien utilizando alguna combinación de flujos e iteraciones. Por lo tanto, refactorice el código existente para usar secuencias y utilícelas solo en el código nuevo cuando tenga sentido .

En resumen, algunas tareas se realizan mejor mediante flujos y otras se realizan mejor mediante iteraciones. La combinación de estos dos métodos puede realizar muchas tareas. No existen reglas estrictas y rápidas para elegir qué método utilizar para una tarea, pero existen algunas heurísticas útiles. En muchos casos quedará claro qué método utilizar; en otros, no. Si no está seguro de si una tarea se realiza mejor mediante el flujo o la iteración, pruebe ambos métodos y vea cuál funciona mejor.

Priorice las funciones libres de efectos secundarios en las transmisiones

Los programadores de Java saben cómo utilizar los bucles for-each y la operación de finalización de forEach es similar. Pero la operación forEach es una de las operaciones menos poderosas en las operaciones de terminal, y también es una operación de flujo hostil. Es explícitamente iterativo y, por tanto, no apto para paralelización. La operación forEach solo debe usarse para informar los resultados de un cálculo de flujo, no para realizar el cálculo . A veces tiene sentido utilizar forEach para otros fines, como agregar los resultados de un cálculo de flujo a una colección preexistente.

El código mejorado utiliza recopiladores, un nuevo concepto que se debe aprender al trabajar con transmisiones. La API de coleccionistas es abrumadora: tiene 39 métodos, algunos de los cuales tienen hasta cinco parámetros de tipo. La buena noticia es que puedes obtener la mayoría de los beneficios de esta API sin tener que profundizar en toda su complejidad. Para empezar, puede ignorar la interfaz del recopilador y pensar en el recopilador como un objeto opaco que encapsula una estrategia de reducción. Un coleccionista que reúne los elementos de una secuencia en una colección real es muy simple. Hay tres recopiladores de este tipo: toList(), toSet() toCollection(collectionFactory). Devuelven conjuntos, listas y tipos de colecciones especificados por el programador, respectivamente .

En resumen, la esencia de la programación de flujo de canalización es un objeto de función sin efectos secundarios. Esto se aplica a todos los objetos de función pasados ​​a secuencias y objetos relacionados. La operación final forEach solo debe usarse para informar los resultados de los cálculos realizados por la secuencia, no para realizar los cálculos. Para utilizar las transmisiones correctamente, debes comprender a los recopiladores. Las fábricas de recopiladores importantes son toList, toSet, toMap, groupingBy y join.

Prefiere Colección sobre Transmisión como tipo de devolución

La interfaz Collection es un subtipo de Iterable y tiene un método de transmisión, por lo que proporciona acceso tanto de iteración como de transmisión. Por lo tanto, Colección o un subtipo apropiado suele ser el mejor tipo de retorno para los métodos de retorno de secuencia pública . Las matrices también proporcionan iteración simple y acceso a transmisiones utilizando los métodos Arrays.asList y Stream.of. Si la secuencia devuelta es lo suficientemente pequeña como para caber fácilmente en la memoria, es mejor devolver una implementación de colección estándar como ArrayList o HashSet. Pero no almacene una secuencia grande en la memoria solo para devolverla como una colección.

En resumen, al escribir métodos que devuelven secuencias de elementos, tenga en cuenta que algunos usuarios pueden querer procesarlos como una secuencia, mientras que otros pueden querer procesarlos de forma iterativa. Intente acomodar a ambos grupos. Si es posible devolver una colección, hágalo. Si ya tiene los elementos en la colección, o la cantidad de elementos en la secuencia es lo suficientemente pequeña como para crear un nuevo elemento, devuelva una colección estándar, como ArrayList. De lo contrario, considere implementar un conjunto personalizado, como hicimos para el programa Power Set. Si no es posible devolver una colección, devuelva una secuencia o un iterable, lo que parezca más natural. Si en una versión futura de Java la declaración de la interfaz Stream se modifica para heredar de Iterable, entonces debería sentirse libre de devolver las secuencias, ya que permitirán la transmisión y el procesamiento iterativo.

Utilice el paralelismo de flujo con precaución

No paralelice indiscriminadamente las tuberías de transmisión . Las consecuencias en el rendimiento pueden ser catastróficas.

En general, las ganancias de rendimiento derivadas del paralelismo son mejores en flujos de instancias, matrices, rangos de tipo int y rangos de tipo long de ArrayList, HashMap, HashSet y ConcurrentHashMap. Lo que estas estructuras de datos tienen en común es que se pueden dividir de forma precisa y económica en subrutinas de cualquier tamaño, lo que facilita dividir el trabajo entre subprocesos paralelos. La abstracción utilizada por la biblioteca Teardrop para realizar esta tarea es un spliterator, que es devuelto por los métodos spliterator en Streams e Iterables.

En resumen, ni siquiera intente paralelizar un canal de transmisión a menos que tenga buenas razones para creer que mantendrá el cálculo correcto y aumentará su velocidad . El costo de paralelizar incorrectamente una secuencia puede ser una falla del programa o un desastre en el rendimiento. Si cree que el paralelismo es razonable, asegúrese de que su código se comporte correctamente cuando se ejecute en paralelo y realice mediciones cuidadosas del rendimiento en condiciones del mundo real. Si su código es correcto y estos experimentos confirman sus sospechas sobre las mejoras de rendimiento, entonces y sólo entonces podrá paralelizar el flujo en el código de producción.

Comprobar la validez de los parámetros

​ 如果将无效参数值传递给方法,并且该方法在执行之前检查其参数,则它抛出适当的异常然后快速且清楚地以失 败结束。 如果该方法无法检查其参数,可能会发生一些事情。 在处理过程中,该方法可能会出现令人困惑的异常。 更糟糕的是,该方法可以正常返回,但默默地计算错误的结果。 糟糕的是,该方法可以正常返回但是将某个对象 置于受损状态,在将来某个未确定的时间在代码中的某些不相关点处导致错误。 换句话说,验证参数失败可能导致 违反故障原子性(failure atomicity )。

​ 构造方法是这个原则的一个特例,你应该检查要存储起来供以后使用的参数的有效性。检查构造方法参数的有效 性对于防止构造对象违反类不变性(class invariants)非常重要。

​ 你应该在执行计算之前显式检查方法的参数,但这一规则也有例外。 一个重要的例外是有效性检查昂贵或不切 实际的情况,并且在进行计算的过程中隐式执行检查。

​ 总而言之,每次编写方法或构造方法时,都应该考虑对其参数存在哪些限制。 应该记在这些限制,并在方法体 的开头使用显式检查来强制执行这些限制。 养成这样做的习惯很重要。 在第一次有效性检查失败时,它所需要的少 量工作将会得到对应的回报。

必要时进行防御性拷贝

Incluso en un idioma seguro, el aislamiento de otras clases no es posible sin algún esfuerzo. Los programas deben escribirse de manera defensiva, asumiendo que los clientes de una clase se esfuerzan por destruir las invariantes de la clase . Esto se vuelve cada vez más cierto a medida que las personas se esfuerzan más por romper la seguridad del sistema, pero la mayoría de las veces, sus clases tendrán que lidiar con comportamientos inesperados debido a errores honestos cometidos por programadores bien intencionados. De todos modos, vale la pena dedicar tiempo a escribir clases que sigan siendo sólidas a pesar del mal comportamiento del cliente.

Siempre que sea posible, debes utilizar objetos inmutables como componentes de objetos para no tener que preocuparte por las copias defensivas (elemento 17). En nuestro ejemplo de Period, use Instant (o LocalDateTime o ZonedDateTime) a menos que esté usando una versión anterior a Java 8. Si está utilizando una versión anterior, una opción es almacenar el tipo base long devuelto por Date.getTime() en lugar de la referencia de fecha.

Puede haber una penalización en el rendimiento asociada con la copia defensiva, y no siempre está justificada. Si una clase confía en que sus llamadores no modifiquen los componentes internos, tal vez porque la clase y sus clientes son parte del mismo paquete, entonces puede que no necesite una copia defensiva. En estos casos, la documentación de la clase debe indicar claramente que la persona que llama no puede modificar los parámetros ni los retornos afectados.

En resumen, si una clase tiene componentes mutables que se obtienen o devuelven de sus clientes, entonces la clase debe copiar estos componentes a la defensiva. Si el costo de la copia es demasiado alto y la clase confía en que sus clientes no modificarán los componentes de manera inapropiada, la copia defensiva puede reemplazarse con un documento que describa la responsabilidad del cliente de no modificar los componentes afectados.

Firmas de métodos de diseño cuidadosamente

Elija los nombres de los métodos con cuidado . Los nombres siempre deben cumplir con las convenciones de nomenclatura estándar (Artículo 68). Su principal objetivo debe ser elegir un nombre que sea coherente con otros nombres del mismo paquete y que sea fácil de entender. El segundo paso debería ser elegir un nombre que sea coherente con el consenso más amplio. Evite el uso de nombres de métodos largos;

No se exceda proporcionando métodos convenientes . Cada método debe realizarse "lo mejor que pueda". Demasiados métodos hacen que una clase sea difícil de aprender, usar, documentar, probar y mantener. Esto es especialmente cierto para las interfaces, donde demasiados métodos complican el trabajo de los implementadores y usuarios;

Evite listas de parámetros demasiado largas . Apunte a cuatro parámetros o menos. La mayoría de los programadores no pueden recordar listas de parámetros más largas.

Para los tipos de parámetros, prefiera las interfaces a las clases (Artículo 64). Si hay una interfaz adecuada que define un parámetro, úsela para admitir una clase que implemente esa interfaz. Por ejemplo, no hay razón para usar un HashMap como parámetro de entrada al escribir un método, en su lugar, use un Map como parámetro, lo que permite pasar un HashMap, un TreeMap, un ConcurrentHashMap, un submapa de un TreeMap o cualquier Implementación de mapas que aún no se ha escrito.

Utilice tipos de enumeración de dos elementos con preferencia a , a menos que el significado del parámetro booleano esté claro en el nombre del método. Los tipos de enumeración hacen que el código sea más fácil de leer y escribir. Además, facilitan agregar más opciones más adelante .

Utilice la sobrecarga de forma inteligente y sensata

​Código de muestra:

public class CollectionClassifier {
    
     
 
    public static String classify(Set<?> s) {
    
             
        return "Set";     
    } 
 
    public static String classify(List<?> lst) {
    
             
        return "List";     
    } 
 
    public static String classify(Collection<?> c) {
    
             
        return "Unknown Collection";     
    } 
 
    public static void main(String[] args) {
    
             
        Collection<?>[] collections = {
    
                 
            new HashSet<String>(),             
            new ArrayList<BigInteger>(),             
            new HashMap<String, String>().values()         
        }; 
 
        for (Collection<?> c : collections)             
            System.out.println(classify(c));     
    } 
} 

Es posible que espere que este programa imprima las cadenas Conjunto, luego Lista y Colección desconocida, pero no es así. En cambio, la cadena Colección desconocida se imprime tres veces. ¿Por qué está pasando esto? Debido a que el método de clasificación está sobrecargado, en el momento de la compilación se selecciona qué método sobrecargado llamar . Para las tres iteraciones del bucle, el tipo de argumento en tiempo de compilación es el mismo: Collection<?> .

Porque la selección entre métodos sobrecargados es estática, mientras que la selección entre métodos anulados es dinámica . Cuando se llama a un método anulado, el tipo de tiempo de compilación del objeto no tiene ningún efecto sobre qué método se ejecuta ; siempre se ejecuta el método anulado "más específico". Compare esto con la sobrecarga, donde el tipo de tiempo de ejecución del objeto no tiene ningún efecto sobre qué sobrecarga se realiza; la selección se realiza en tiempo de compilación , basándose completamente en el tipo de tiempo de compilación del parámetro.

Una estrategia segura y conservadora es nunca exportar dos sobrecargas con el mismo número de argumentos . Si un método utiliza parámetros variados, la estrategia conservadora es no sobrecargarlo en absoluto. Si se siguen estas restricciones, el programador no tiene dudas sobre qué sobrecargas se aplican a cualquier conjunto de parámetros reales. Estas restricciones no son muy onerosas ya que siempre es posible dar nombres diferentes a los métodos en lugar de sobrecargarlos.

En resumen, sólo porque puedas sobrecargar un método no significa que debas hacerlo. En general, es mejor evitar sobrecargar métodos con múltiples firmas que tengan la misma cantidad de parámetros . En algunos casos, especialmente cuando se trata de constructores, puede que no sea posible seguir este consejo. En estos casos, al menos deberías evitar pasar el mismo conjunto de parámetros a diferentes sobrecargas agregando conversiones. Si esto no se puede evitar, por ejemplo, porque una clase existente se está adaptando para implementar una nueva interfaz, entonces debe asegurarse de que todas las sobrecargas se comporten igual al pasar los mismos parámetros. Sin esto, el programador tendrá dificultades para utilizar eficazmente un método o constructor sobrecargado o para comprender por qué no funciona.

Utilice parámetros variados de forma inteligente y sensata

Los métodos de parámetros variables, formalmente conocidos como métodos de aridad variable [JLS, 8.4.1], aceptan cero o más parámetros de un tipo específico. El mecanismo variado primero crea una matriz cuyo tamaño es el número de argumentos pasados ​​en el sitio de la llamada, luego coloca los valores de los argumentos en la matriz y finalmente pasa la matriz al método.

Tenga cuidado al utilizar argumentos variados en situaciones críticas para el rendimiento. Cada llamada a un método variado da como resultado la asignación e inicialización de la matriz . Si determina empíricamente que no puede afrontar este costo, pero aún necesita la flexibilidad de los parámetros variables, entonces existe un modelo que le permite tener lo mejor de ambos mundos. Suponiendo que ha determinado que el 95% de las llamadas son a métodos con tres o menos argumentos, declare cinco sobrecargas de ese método. Cada método sobrecargado contiene de 0 a 3 parámetros normales. Cuando el número de parámetros excede 3, se utiliza un método variado.

En resumen, los argumentos variados son útiles cuando es necesario definir un método con un número variable de argumentos. Anteponga los argumentos necesarios antes de utilizar argumentos variables y tenga en cuenta las consecuencias para el rendimiento del uso de argumentos variables.

Devuelve una matriz o colección vacía, no devuelve nulo

A veces se argumenta que un valor de retorno nulo es preferible a una colección o matriz vacía porque evita la sobrecarga de asignar un contenedor vacío. Este argumento falla por dos razones. En primer lugar, no debería preocuparse por el rendimiento a este nivel a menos que las mediciones muestren que la asignación en cuestión es la causa real del problema de rendimiento. En segundo lugar, las colecciones y matrices vacías se pueden devolver sin asignarlas. Si hay evidencia de que la asignación de una colección vacía perjudica el rendimiento, la asignación se puede evitar devolviendo repetidamente la misma colección vacía inmutable, ya que los objetos inmutables se pueden compartir libremente .

En resumen, nunca devuelva un valor nulo en lugar de una matriz o colección vacía. Hace que su API sea más difícil de usar, más propensa a errores y no ofrece beneficios de rendimiento.

Devolver Opcional con sabiduría y prudencia

Antes de Java 8, había dos enfoques, ninguno de los cuales era perfecto, al escribir un método que no podía devolver ningún valor en un caso específico:

  1. O lanza una excepción, pero las excepciones deben reservarse para condiciones de excepción, y lanzar excepciones es costoso porque todo el seguimiento de la pila se captura cuando se crea la excepción. Devolver nulo no tiene estas desventajas, pero tiene sus propias desventajas.
  2. O devuelve nulo (asumiendo que el tipo de retorno es un objeto o un tipo de referencia); si el método devuelve nulo, el cliente debe incluir un código de caso especial para manejar la posibilidad de un retorno nulo, a menos que el programador pueda demostrar que un retorno nulo es imposible . Si el cliente no verifica los retornos nulos y almacena el valor de retorno nulo en una estructura de datos, existe la posibilidad de que se genere una excepción NullPointerException en algún momento en el futuro en una ubicación de código que no sea relevante para este problema.

En Java 8, existe una tercera forma de escribir métodos que pueden no devolver ningún valor. La clase Opcional representa un contenedor inmutable que puede contener una referencia no nula a T o nada en absoluto. Un opcional que no contiene contenido se llama vacío. Se dice que un Opcional no vacío que contiene un valor está presente. Opcional es esencialmente una colección inmutable que puede contener como máximo un elemento. Opcional no implementa la interfaz Colección, pero en principio es posible.

El método Opcional.of(valor) acepta un valor que puede ser nulo. Si se pasa nulo, se devuelve un Opcional vacío. Nunca devuelva un valor nulo de un método que devuelve un Opcional: anula el propósito del diseño de Opcional .

En resumen, si descubre que el método que ha escrito no siempre puede devolver un valor y cree que es importante que el usuario del método considere esta posibilidad cada vez que lo llama, entonces quizás debería devolver un método opcional. Sin embargo, debe tener en cuenta que devolver Opcional tiene consecuencias reales en el rendimiento; para métodos críticos para el rendimiento, es mejor devolver un valor nulo o generar una excepción.

Escriba comentarios de documentación para todos los elementos API expuestos.

Para que una API sea utilizable, debe estar documentada. Tradicionalmente, la documentación de la API se generaba manualmente y mantener la documentación sincronizada con el código era una tarea ardua. El entorno de programación Java simplifica esta tarea utilizando la utilidad Javadoc. Javadoc utiliza comentarios de documentación con formato especial (a menudo llamados comentarios de documento) para generar automáticamente documentación API a partir del código fuente.

Para documentar correctamente una API, cada declaración de clase, interfaz, constructor, método y propiedad exportada debe ir precedida de un comentario de documentación.

Existe un principio general de que los comentarios de la documentación deben ser legibles tanto en el código fuente como en la documentación generada.

Para evitar confusiones, dos miembros o constructores de una clase o interfaz no deben tener la misma descripción resumida . Preste especial atención a los métodos sobrecargados, para los cuales suele ser natural utilizar la misma primera frase (pero no es aceptable en los comentarios de la documentación).

En resumen, los comentarios de documentación son la mejor y más eficaz forma de documentar las API. Su uso debe considerarse obligatorio para todos los elementos API exportados. Utilice un estilo coherente que se adhiera a las convenciones estándar. Recuerde que se permite HTML arbitrario en los comentarios de la documentación, pero los metacaracteres del HTML deben tener caracteres de escape.

Minimizar el alcance de las variables locales.

Este elemento es de naturaleza similar a "Minimizar la accesibilidad de clases y miembros". Al minimizar el alcance de las variables locales, puede mejorar la legibilidad y el mantenimiento de su código y reducir la posibilidad de errores .

Una técnica poderosa para . Si las variables se declaran antes de usarse, se vuelve aún más confuso y agrega otra distracción para el lector que intenta comprender el programa. Cuando se utiliza la variable, es posible que el lector no recuerde el tipo o el valor inicial de la variable.

Declarar una variable local demasiado pronto puede hacer que su alcance no sólo comience demasiado pronto sino que también termine demasiado tarde. El alcance de una variable local se extiende desde la ubicación donde se declara hasta el final del bloque que la contiene. Si una variable se declara fuera del bloque adjunto en el que se utiliza, permanece visible después de que el programa sale del bloque adjunto. Si una variable se utiliza accidentalmente antes o después del área de uso prevista, las consecuencias pueden ser catastróficas.

Casi todas las declaraciones de variables locales deben contener un inicializador . Si todavía no hay suficiente información para inicializar razonablemente una variable, entonces la declaración debe posponerse hasta que se considere posible hacerlo. Una excepción a esta regla es la declaración try-catch.

La técnica definitiva para minimizar . Si combina dos actividades en el mismo método, las variables locales relacionadas con una actividad pueden estar en el alcance del código que ejecuta la otra actividad. Para evitar que esto suceda, simplemente divida el método en dos: un método para cada comportamiento.

El bucle for-each es mejor que el bucle for tradicional

El bucle for-each (oficialmente llamado "declaración for mejorada") resuelve todos estos problemas. Elimina el desorden y las posibilidades de errores al ocultar iteradores o variables de índice. Sin embargo, hay tres situaciones comunes en las que no se puede utilizar un bucle para cada uno por separado:

  1. Filtrado destructivo : si necesita recorrer la colección y eliminar una selección específica, debe utilizar un iterador explícito para poder llamar a su método de eliminación. Por lo general, el recorrido explícito se puede evitar utilizando el método removeIf en la clase Collection agregada en Java 8.
  2. Conversión : si necesita recorrer una lista o matriz y reemplazar algunos o todos los valores de sus elementos, entonces necesita un iterador de lista o un índice de matriz para reemplazar el valor del elemento.
  3. Iteración paralela : si necesita iterar sobre varias colecciones en paralelo, debe controlar explícitamente los iteradores o las variables de índice para que todos puedan realizarse simultáneamente (como se demostró inadvertidamente en el ejemplo erróneo de tarjeta y dados anterior).

Si se encuentra en alguna de estas situaciones, utilice un bucle for tradicional y tenga cuidado con los peligros mencionados en esta entrada.

En resumen, el bucle for-each ofrece ventajas convincentes sobre los bucles for tradicionales en términos de claridad, flexibilidad y prevención de errores, sin perjudicar el rendimiento. Siempre que sea posible, utilice bucles for each en lugar de bucles for.

Comprender y utilizar bibliotecas

Al utilizar la biblioteca estándar, aprovecha el conocimiento de los expertos que la escribieron y la experiencia de quienes la han utilizado antes .

A partir de Java 7, ya no se debería utilizar Random. En la mayoría de los casos, el generador de números aleatorios elegido ahora es ThreadLocalRandom. Produce números aleatorios de mayor calidad y es muy rápido. En mi máquina, es 3,6 veces más rápido que Random. Para grupos de conexiones de bifurcación y transmisiones paralelas, utilice SplittableRandom.

El segundo beneficio de utilizar estas bibliotecas es que no tiene que perder tiempo escribiendo soluciones especializadas a problemas que no son relevantes para su trabajo . Si es como la mayoría de los programadores, preferirá dedicar su tiempo a trabajar en su aplicación que en el proceso subyacente.

La tercera ventaja de utilizar bibliotecas estándar es que su rendimiento mejora con el tiempo sin ningún esfuerzo por su parte . Dado que muchas personas las utilizan y se utilizan en pruebas comparativas estándar de la industria, las organizaciones que proporcionan estas bibliotecas tienen un fuerte incentivo para hacerlas funcionar más rápido.

Teniendo en cuenta todas estas ventajas, parece lógico utilizar herramientas de biblioteca en lugar de elegir implementaciones especializadas, pero muchos programadores no lo hacen. ¿por qué no? Quizás no sepan que la biblioteca existe. Con cada versión importante, se agregan muchas funciones a la biblioteca y vale la pena conocer estas adiciones .

​ A veces, es posible que las herramientas de la biblioteca no satisfagan sus necesidades. Cuanto más especializadas sean sus necesidades, más probabilidades habrá de que esto suceda. Aunque su primer pensamiento debería ser utilizar estas bibliotecas, si ya comprende las funciones que proporcionan en algunas áreas y estas funciones no satisfacen sus necesidades, entonces puede utilizar otra implementación. Siempre hay lagunas en la funcionalidad proporcionada por cualquier conjunto limitado de bibliotecas. Si no puede encontrar lo que necesita en las bibliotecas de la plataforma Java, su siguiente opción debería ser encontrar una biblioteca de terceros de alta calidad, como la excelente biblioteca Guava de código abierto de Google [Guava]. Si no puede encontrar la funcionalidad que necesita en ninguna biblioteca adecuada, es posible que no tenga más remedio que implementarla usted mismo .

Considerándolo todo, no reinventes la rueda. Si necesita hacer algo que parece bastante común, probablemente ya exista una herramienta en la biblioteca que haga lo que desea. Si está ahí, úsalo; si no lo sabes, compruébalo. En general, es probable que el código de la biblioteca sea mejor que el código que usted mismo escribiría y puede mejorarse con el tiempo. Esto no refleja tus habilidades como programador. Las economías de escala dictan que el código de la biblioteca reciba mucha más atención de la que la mayoría de los desarrolladores pueden permitirse para la misma funcionalidad.

Para ser precisos, evite usar flotador y doble

Los tipos flotante y doble se utilizan principalmente en cálculos científicos y cálculos de ingeniería. Realizan operaciones binarias de punto flotante, un algoritmo cuidadosamente diseñado para proporcionar rápidamente aproximaciones precisas en un amplio rango. Sin embargo, no proporcionan resultados precisos y no deben usarse cuando se requieren resultados precisos. Los tipos flotante y doble son particularmente inadecuados para cálculos monetarios porque es imposible representar 0,1 (o cualquier potencia negativa de 10) exactamente como flotante o doble.

En resumen, no utilice tipos flotantes o dobles para ningún cálculo que requiera una respuesta exacta. Si desea que el sistema maneje puntos decimales y no le importa el inconveniente y el costo de no usar un tipo primitivo, use BigDecimal . Otro beneficio de usar BigDecimal es que le brinda control total sobre el redondeo y puede elegir entre ocho modos de redondeo al realizar operaciones que requieren redondeo. Esto es muy conveniente si realiza cálculos comerciales utilizando el comportamiento de redondeo legal. Si el rendimiento es importante, no le importa manejar el punto decimal usted mismo y el número no es demasiado grande, use un int o long. Si el valor no supera los 9 decimales, puede utilizar int; si no supera los 18 decimales, puede utilizar long. Si es probable que la cantidad supere los 18 dígitos, utilice BigDecimal.

Los tipos de datos básicos son mejores que las clases contenedoras

Hay tres diferencias principales entre los tipos básicos y los tipos envueltos. En primer lugar, los tipos primitivos sólo tienen sus valores, mientras que los tipos contenedor tienen identidades distintas de sus valores. En otras palabras, dos instancias de tipo contenedor pueden tener el mismo valor e identidades diferentes. En segundo lugar, el tipo básico solo tiene valores de función completa, mientras que cada tipo contenedor tiene un valor no funcional, que es nulo, además de todos los valores funcionales del tipo básico correspondiente. Finalmente, los tipos básicos son más eficientes en tiempo y espacio que los tipos empaquetados . Las tres diferencias pueden causarte verdaderos problemas si no tienes cuidado.

​ En resumen, siempre que tenga la opción, debería preferir usar tipos básicos en lugar de tipos envolventes. Los tipos básicos son más simples y rápidos. Si debes utilizar tipos de envoltorios, ¡ten cuidado! El autoboxing reduce la verbosidad del uso de tipos de envoltorios, pero no los peligros. Cuando su programa utiliza el operador == para comparar dos tipos ajustados, realiza una comparación de identidad, que casi con seguridad no es lo que desea. Cuando su programa realiza cálculos de tipo mixto que involucran tipos envueltos y tipos primitivos, realizará unboxing. Cuando su programa realice unboxing, se generará una NullPointerException. Finalmente, cuando su programa encuadra tipos primitivos, puede resultar en una creación de objetos costosa e innecesaria.

Evite el uso de cadenas cuando otros tipos sean más apropiados

Las cadenas están diseñadas para representar texto y lo hacen muy bien. Debido a que las cadenas son tan comunes y están bien soportadas por Java, es natural usarlas para otros propósitos distintos a los escenarios para los que están destinadas.

Las cadenas son un mal sustituto de otros tipos de valores. En general, si hay un tipo de valor adecuado, ya sea un tipo primitivo o una referencia de objeto, debe usarlo; si no lo hay, debe escribir uno.

Las cadenas son un mal sustituto de los tipos de enumeración. Como se analizó en el elemento 34, las constantes de tipo de enumeración son más adecuadas para constantes de tipo de enumeración que para cadenas.

Las cadenas son un mal sustituto de los tipos agregados. Si una entidad tiene varios componentes, generalmente es una mala idea representarla como una sola cadena.

​Código de muestra:

String compoundKey = className + "#" + i.next(); 

Este método tiene muchas desventajas. Esto puede causar confusión si los caracteres utilizados para separar campos aparecen en uno de los campos. Para acceder a campos individuales, debe analizar la cadena, lo cual es un proceso lento, largo y propenso a errores.

En resumen, se debe evitar el uso de cadenas para representar objetos cuando existen o se pueden escribir mejores tipos de datos. Si se usan incorrectamente, las cadenas son más engorrosas, menos flexibles, más lentas y más propensas a errores que otros tipos. Los tipos de cadenas que comúnmente se usan incorrectamente incluyen tipos primitivos, enumeraciones y tipos agregados.

Tenga en cuenta los problemas de rendimiento causados ​​por la concatenación de cadenas

El operador de concatenación de cadenas (+) es una forma cómoda de combinar varias cadenas en una sola. Está bien producir una sola línea de salida o construir una representación de cadena de un objeto pequeño de tamaño fijo, pero no escala. La concatenación repetida de n cadenas utilizando el operador de concatenación de cadenas requiere n tiempo al cuadrado. Esto es una consecuencia del hecho de que las cadenas son inmutables (Ítem-17). Cuando se concatenan dos cadenas, se copia el contenido de ambas cadenas.

La idea es simple: no utilice el operador de concatenación de cadenas para fusionar varias cadenas a menos que el rendimiento no importe . De lo contrario, utilice el método append de StringBuilder. Alternativamente, use una matriz de caracteres o trabaje con una cadena a la vez en lugar de combinarlas.

Objetos de referencia a través de interfaces.

En términos generales, debes usar interfaces en lugar de clases para hacer referencia a objetos. Si existe un tipo de interfaz adecuado, los parámetros, valores de retorno, variables y campos deben declararse utilizando el tipo de interfaz . El único momento en el que realmente necesitas hacer referencia a la clase de un objeto es cuando lo creas usando un constructor.

Si desarrolla el hábito de utilizar interfaces como tipos, sus programas serán más flexibles . Si decide que desea cambiar de implementación, simplemente cambie el nombre de la clase en el constructor (o use una fábrica estática diferente).

Si no existe una interfaz adecuada, es perfectamente apropiado utilizar una clase para hacer referencia al objeto . Por ejemplo, considere clases de valores como String y BigInteger. Las clases de valor rara vez se escriben teniendo en cuenta múltiples implementaciones. Suelen ser finales y rara vez tienen interfaces correspondientes. Es perfectamente adecuado utilizar clases de valores como parámetros, variables, campos o tipos de retorno.

En una aplicación real, debería ser obvio si un objeto determinado tiene una interfaz adecuada. Si es así, su programa será más flexible y popular si usa interfaces para hacer referencia a objetos. Si no hay una interfaz adecuada, utilice la clase de nivel inferior en la jerarquía de clases que proporcione la funcionalidad requerida.

Interfaces sobre reflexión

El mecanismo central de reflexión java.lang.reflect proporciona acceso programático a cualquier clase . Dado un objeto de Clase, puede obtener instancias de Constructor, Método y Campo, que representan respectivamente el constructor, el método y el campo de la clase representada por la instancia de Clase. Estos objetos proporcionan acceso programático a los nombres de los miembros de la clase, tipos de campos, firmas de métodos, etc.

Además, las instancias de Constructor, Método y Campo le permiten manipular sus contrapartes subyacentes de manera reflexiva: al llamar a métodos en instancias de Constructor, Método y Campo, puede construir instancias de la clase subyacente, llamar a métodos de la clase subyacente y acceder a Campos. en la clase subyacente. Por ejemplo, Method.invoke le permite invocar cualquier método (sujeto a restricciones de seguridad predeterminadas) en cualquier objeto de cualquier clase. Reflection permite que una clase use otra clase incluso si esta última no existe cuando se compila la primera . Sin embargo, esta capacidad tiene un precio:

  • Se pierden todos los beneficios de la verificación de tipos en tiempo de compilación, incluida la verificación de excepciones . Si un programa intenta llamar reflexivamente a un método inexistente o inaccesible, fallará en tiempo de ejecución a menos que se tomen precauciones especiales.
  • El código necesario para realizar el acceso reflectante es largo y difícil de manejar . Es tedioso escribir y difícil leer. Se reduce el rendimiento.
  • Las llamadas a métodos reflejados son mucho más lentas que las llamadas a métodos normales . Es difícil decir cuánto más lento porque hay muchos factores en juego. En mi máquina, la reflexión es 11 veces más lenta cuando se llama a un método que no toma parámetros de entrada y devuelve un int.

Al utilizar la reflexión en una forma muy limitada, se obtienen muchos de los beneficios de la reflexión a una fracción del costo . Para muchos programas, deben usar una clase que no está disponible en el momento de la compilación, y hay una interfaz o superclase adecuada para hacer referencia a la clase en el momento de la compilación (consulte el Elemento 64 para obtener más detalles). Si este es el caso, puedes crear instancias usando la reflexión y acceder a ellas normalmente a través de su interfaz o superclase .

En resumen, la reflexión es una herramienta poderosa que es necesaria para algunas tareas complejas de programación de sistemas, pero tiene muchas deficiencias. Si escribe un programa que debe tratar con clases desconocidas en tiempo de compilación, debe usar la reflexión solo para crear instancias de objetos siempre que sea posible y acceder a los objetos usando interfaces o superclases que se conocen en tiempo de compilación.

Utilice los métodos locales de forma inteligente y juiciosa.

La interfaz nativa de Java (JNI) permite a los programas Java llamar a métodos nativos, que están escritos en lenguajes de programación nativos como C o C++. Históricamente, los métodos nativos han tenido tres usos principales. Proporcionan acceso a instalaciones específicas de la plataforma, como registros. Proporcionan acceso a bases de códigos locales existentes, incluido el acceso a datos heredados. Finalmente, un enfoque nativo puede mejorar el rendimiento al escribir partes de la aplicación centradas en el rendimiento en el idioma nativo.

Para mejorar el rendimiento, rara vez se recomienda utilizar métodos nativos .

​ En resumen, piénselo dos veces antes de utilizar métodos nativos. Generalmente hay poca necesidad de utilizarlos para mejorar el rendimiento. Si debe utilizar métodos nativos para acceder a recursos subyacentes o bibliotecas nativas, utilice la menor cantidad de código nativo posible y pruébelo exhaustivamente . Un solo error en el código nativo puede dañar toda la aplicación.

Optimice sabia y prudentemente

No sacrifique la arquitectura del sonido por el rendimiento. Esfuércese por escribir buenos programas, no programas rápidos . Si un buen programa no es lo suficientemente rápido, su arquitectura permitirá optimizarlo. Los buenos programas incorporan el principio de ocultación de información: cuando es posible, localizan las decisiones de diseño dentro de componentes individuales, de modo que las decisiones individuales puedan cambiarse sin afectar al resto del sistema.

​Trate de evitar decisiones de diseño que limiten el rendimiento . Los componentes de un diseño que son difíciles de cambiar son aquellos que especifican las interacciones entre los componentes y con el mundo exterior. Los principales componentes de diseño son las API, los protocolos de capa de línea y los formatos de datos persistentes. Estos componentes de diseño no sólo son difíciles o imposibles de cambiar después del hecho, sino que todos ellos pueden imponer limitaciones significativas al rendimiento que un sistema puede lograr.

Considere las consecuencias de rendimiento de las decisiones de diseño de API . Convertir un tipo público en mutable puede requerir muchas copias defensivas innecesarias (consulte el Elemento 50 para obtener más detalles). De manera similar, usar la herencia en una clase pública (donde la composición sería apropiada) vincula esa clase para siempre a su superclase, limitando artificialmente el desempeño de las subclases (consulte el Elemento 18 para más detalles). El último ejemplo es que el uso de clases de implementación en lugar de interfaces en una API lo vincula a una implementación específica, incluso si se pudiera escribir una implementación más rápida en el futuro.

Mida el rendimiento antes . Una herramienta que merece una mención especial es jmh, que no es un generador de perfiles sino un marco de microbenchmarking que proporciona una previsibilidad incomparable del rendimiento del código Java.

En resumen, no trabaje duro para escribir programas rápidos, pero trabaje duro para escribir buenos programas, la velocidad aumentará naturalmente. Pero asegúrese de considerar el rendimiento al diseñar su sistema, especialmente al diseñar API, protocolos de capa de línea y formatos de datos persistentes . Cuando haya terminado de construir su sistema, mida su rendimiento. Si es lo suficientemente rápido, ya está. De lo contrario, utilice el analizador para encontrar el origen del problema y optimizar las partes relevantes del sistema. El primer paso es examinar la elección del algoritmo: ninguna optimización de bajo nivel puede compensar una mala elección del algoritmo. Repita este proceso según sea necesario, midiendo el rendimiento después de cada cambio, hasta que esté satisfecho.

Siga convenciones de comando ampliamente reconocidas

La plataforma Java tiene un conjunto bien establecido de convenciones de nomenclatura, muchas de las cuales están contenidas en "La especificación del lenguaje Java" [JLS, 6.1]. En términos generales, las convenciones de nomenclatura se dividen en dos categorías: tipografía y sintaxis.

Los nombres de paquetes y módulos deben ser jerárquicos, con puntos que separen los componentes. Los componentes deben consistir en letras minúsculas y los números rara vez se utilizan. El nombre de cualquier paquete utilizado fuera de su organización debe comenzar con el nombre de dominio de Internet de su organización, con los componentes invertidos, por ejemplo, edu.cmu, com.google, org.e.

En resumen, internalice las convenciones de nomenclatura estándar y utilícelas como características sexuales secundarias. Las convenciones tipográficas son sencillas y en gran medida inequívocas; las convenciones gramaticales son más complejas y vagas. Para citar "La especificación del lenguaje Java" [JLS, 6.1], "No se deben seguir ciegamente estas convenciones si el uso tradicional desde hace mucho tiempo requiere que no se sigan". Se debe utilizar el sentido común.

Utilice excepciones sólo para situaciones excepcionales

Las excepciones sólo deben usarse en situaciones inusuales; nunca deben usarse en el flujo normal de control del programa.

Una API bien diseñada no debería obligar a sus clientes a utilizar excepciones en aras del flujo de control normal. Si una clase tiene métodos "dependientes del estado", es decir, métodos que sólo pueden llamarse bajo condiciones impredecibles específicas, la clase también debe tener un método de "prueba de estado" separado, que indique si el método relacionado con este estado puede ser llamado . Por ejemplo, la interfaz Iterator contiene el siguiente método relacionado con el estado y el método de prueba de estado correspondiente hasNext. Esto hace posible utilizar el patrón estándar de iteración sobre una colección utilizando un bucle for tradicional (y un bucle for-each, que utiliza el método hasNext internamente).

En resumen, las excepciones se diseñan y utilizan en situaciones inusuales. No los someta a un flujo de control ordinario y no escriba API que los obliguen a hacerlo.

Utilice excepciones comprobables para situaciones recuperables y excepciones de tiempo de ejecución para errores de programación.

El lenguaje de programación Java proporciona tres elementos arrojables: excepciones comprobadas, excepciones de tiempo de ejecución y errores. Existe confusión entre los programadores sobre qué arrojable es apropiado para cada situación. Aunque esta decisión no siempre es clara, existen algunos principios generales que proporcionan una orientación sólida.

La regla general principal al decidir si usar excepciones marcadas o no marcadas es que si se espera que la persona que llama pueda reanudar razonablemente la operación del programa, entonces debe usar excepciones marcadas . Al generar una excepción marcada, la persona que llama se ve obligada a manejar la excepción en una cláusula catch o propagarla . Por lo tanto, cada excepción marcada declarada en un método que se lanzará es una posible pista para el usuario de la API de que la condición asociada con la excepción es un posible resultado de llamar a este método.

Hay dos tipos de elementos arrojables no verificados: excepciones y errores de tiempo de ejecución. Desde el punto de vista del comportamiento, los dos son equivalentes: ambos son objetos arrojables que no necesitan ni deben ser atrapados. Si un programa arroja una excepción o un error no verificado, a menudo es una situación irrecuperable y es dañino e inútil continuar ejecutando el programa . Si el programa no detecta dicho elemento arrojable, provocará que el hilo actual se detenga y aparezca un mensaje de error apropiado.

Utilice excepciones de tiempo de ejecución para indicar errores de programación. La mayoría de las excepciones en tiempo de ejecución representan violaciones de condiciones previas . La llamada violación de premisa significa que el cliente de la API no cumple con el acuerdo establecido por la especificación de la API. Por ejemplo, la reserva para el acceso a la matriz especifica que el valor del índice de la matriz debe estar entre 0 y la longitud de la matriz - 1. ArrayIndexOutOfBoundsException indica una violación de esta premisa.

Aunque no lo exige la JLS (Especificación del lenguaje Java), por convención, los errores (Error) a menudo se reservan para que los utilice la JVM para indicar recursos insuficientes, fallas de restricciones u otras condiciones que impiden que el programa continúe ejecutándose. Dado que esta ya es una gestión casi universalmente aceptada, no es necesario implementar ninguna subclase de Error nueva. Por lo tanto, todos los elementos arrojables no verificados que implemente deben ser subclases de RuntimeExceptiond (ya sea directa o indirectamente). No sólo no deberías definir una subclase de Error, sino que tampoco deberías lanzar una excepción AssertionError.

En resumen, para situaciones recuperables, se deben generar excepciones marcadas; para errores de programa, se deben generar excepciones de tiempo de ejecución. Si no está seguro de poder recuperarlo, lo descartará como una excepción marcada. No defina ningún tipo de lanzamiento que no sea una excepción marcada ni una excepción de tiempo de ejecución. Proporcionar métodos sobre excepciones comprobadas para ayudar en la recuperación del programa.

Evite excepciones marcadas innecesarias

A los programadores de Java no les gustan las excepciones marcadas, pero si se usan correctamente, pueden mejorar las API y los programas. La razón por la que no hay códigos de retorno y excepciones no verificadas es que obligan al programador a manejar condiciones excepcionales, lo que mejora enormemente la confiabilidad. En otras palabras, el uso excesivo de excepciones marcadas hará que el uso de la API sea muy incómodo. Si un método arroja excepciones marcadas, el código que llama al método debe manejar estas excepciones en uno o más bloques catch, o debe declarar que estas excepciones se lanzan y dejar que se propaguen. No importa qué método se utilice, agrega una carga que el programador no puede ignorar. Esta carga es aún mayor en Java 8, porque los métodos que generan excepciones comprobadas no se pueden usar directamente en Streams .

​ En general, las excepciones marcadas pueden mejorar la legibilidad del programa cuando se usan con precaución; si se usan en exceso, hará que el uso de la API sea muy complicado. Si la persona que llama no puede recuperarse del error, se debe generar una excepción no verificada. Si la recuperación es posible y desea obligar a la persona que llama a manejar la condición excepcional, es preferible devolver un valor opcional. Se deben lanzar excepciones marcadas si, y solo si, no brindan suficiente información en caso de falla.

Prefieren excepciones estándar

Una diferencia importante entre los programadores expertos y los programadores menos experimentados es que los expertos se esfuerzan por lograr, y a menudo logran, un alto grado de reutilización del código . Vale la pena promover la reutilización de código: esta es una regla general y las excepciones no son una excepción. La biblioteca de clases de la plataforma Java proporciona un conjunto básico de excepciones no comprobadas, que cumplen con los requisitos de generación de excepciones de la mayoría de las API.

Reutilizar excepciones estándar tiene varios beneficios. El principal beneficio es que hace que la API sea más fácil de aprender y usar porque es consistente con modismos con los que los programadores ya están familiarizados. El segundo beneficio es que los programas que utilizan estas API serán más legibles porque no tendrán muchas excepciones con las que los programadores no estén familiarizados. Por último (y no menos importante), menos clases de excepción significan una menor huella de memoria y menos tiempo dedicado a cargar estas clases.

No reutilice Exception, RuntimeException, Throwable o Error directamente . Trate estas clases como clases abstractas. No se pueden probar de manera confiable estas excepciones porque son superclases de otras excepciones que un método puede generar.

Elegir qué excepción reutilizar no siempre es preciso porque los "casos de uso" de la tabla anterior no son mutuamente excluyentes. Por ejemplo, considere un objeto que representa una baraja de cartas. Supongamos que hay un método que maneja la operación de reparto de cartas y su parámetro es el número de cartas que se repartirán en una mano. Supongamos que el valor pasado por la persona que llama en este parámetro es mayor que el número restante de cartas en todo el mazo. Esta situación se puede interpretar como una IllegalArgumentException (el valor del parámetro handSize es demasiado grande) o una IllegalStateException (el objeto de naipe contiene muy pocas cartas). En este caso, si no hay ningún valor de parámetro disponible, se genera una excepción ilegalStateException; de lo contrario, se genera una excepción ilegalArgumentException.

Lanza la excepción correspondiente a la abstracción.

Si la excepción lanzada por un método no está claramente relacionada con la tarea que realiza, esta situación puede resultar confusa. Esto tiende a suceder cuando un método pasa una excepción lanzada por una abstracción de nivel inferior. Además de confundir a la gente, esto también "contamina" la API de nivel superior con detalles de implementación. Si la implementación de alto nivel cambia en versiones posteriores, las excepciones que genera también pueden cambiar, lo que podría dañar los programas cliente existentes.

Para evitar este problema, las implementaciones de nivel superior deben detectar excepciones de bajo nivel y generar excepciones que puedan interpretarse en términos de abstracciones de alto nivel . Este enfoque se denomina traducción de excepciones, como se muestra en el siguiente código:

/* Exception Translation */ 
try {
    
         
    ... /* Use lower-level abstraction to do our bidding */ 
} catch ( LowerLevelException e ) {
    
         
    throw new HigherLevelException(...); 
}

Una forma especial de traducción de excepciones se llama encadenamiento de excepciones. Si las excepciones de bajo nivel son muy útiles para depurar problemas que causan excepciones de alto nivel, el encadenamiento de excepciones es apropiado . La excepción de bajo nivel (motivo) se pasa a la excepción de alto nivel, y la excepción de alto nivel proporciona un método de acceso (el método getCause de Throwable) para obtener la excepción de bajo nivel:

// Exception Chaining 
try {
    
     
    ... // Use lower-level abstraction to do our bidding 
} catch (LowerLevelException cause) {
    
         
    throw new HigherLevelException(cause); 
} 

El constructor de excepciones de alto nivel pasa el motivo al superconstructor que reconoce el encadenamiento, por lo que eventualmente se pasará a uno de los constructores de Throwable que ejecutan la cadena de excepciones, como Throwable(Throwable):

/* Exception with chaining-aware constructor */ 
class HigherLevelException extends Exception {
    
    
    
    HigherLevelException( Throwable cause ) {
    
             
        super(cause);     
    } 
} 

Aunque la traducción de excepciones es una mejora con respecto a la transferencia indiscriminada de excepciones desde capas inferiores, no se puede abusar de ella . Si es posible, una buena práctica para manejar excepciones de métodos de nivel inferior es garantizar que se ejecutarán correctamente antes de llamar a métodos de bajo nivel, evitando así que generen excepciones. A veces, puede verificar la validez de los parámetros de un método de nivel superior antes de pasarlos al método de nivel inferior para evitar que el método de nivel inferior genere excepciones.

En general, si no puede prevenir o manejar excepciones de capas inferiores, el enfoque general es utilizar la traducción de excepciones, solo si la especificación del método de capa inferior garantiza que "todas las excepciones que arroja también sean apropiadas para capas superiores". Las excepciones se pueden propagar desde niveles inferiores a niveles superiores. El encadenamiento de excepciones proporciona la mejor funcionalidad para excepciones de alto y bajo nivel: permite lanzar excepciones apropiadas de alto nivel y, al mismo tiempo, captura las causas de bajo nivel para el análisis de fallas.

Las excepciones lanzadas por cada método están documentadas.

Describir las excepciones lanzadas por un método es una parte importante de la documentación requerida para el uso correcto de este método. Por lo tanto, es especialmente importante tomarse el tiempo para documentar cuidadosamente las excepciones generadas por cada método.

​Declare siempre . Si un método público puede generar varias clases de excepción, no utilice un "atajo" para declarar que genera una superclase de esas clases de excepción. Nunca declares un método público para "lanzar una excepción" directamente, o peor aún, declararlo directamente para "lanzar un Throwable" Este es un ejemplo muy extremo. Tal declaración no solo no proporciona al programador ninguna guía sobre "qué excepciones puede generar este método", sino que también dificulta en gran medida el uso de este método, porque en realidad enmascara la posibilidad de que este método pueda generarse en el mismo entorno de ejecución . cualquier otra excepción . Una excepción a este consejo es que el método principal se puede declarar de forma segura para generar una excepción porque solo lo llama la máquina virtual.

​ Utilice la etiqueta @throws de Javadoc para documentar cada excepción no comprobada que un método pueda generar, pero no utilice la palabra clave throws para incluir excepciones no comprobadas en la declaración del método.

Si muchos métodos en una clase arrojan la misma excepción por el mismo motivo, es aceptable documentar la excepción en un comentario de documentación para la clase, en lugar de documentar cada método individualmente. Un ejemplo común es NullPointerException.

En resumen, documente cada excepción que pueda generar cada método que escriba. Esto es cierto para las excepciones no comprobadas y las excepciones comprobadas, y para los métodos abstractos y concretos. Esta documentación debe utilizar la etiqueta @throws en los comentarios de la documentación. Proporcione una declaración separada para cada excepción marcada en la cláusula throws del método, pero no declare excepciones no marcadas. Sin documentación de las excepciones que se pueden generar, será difícil o imposible que otros utilicen sus clases e interfaces de manera efectiva.

Incluir información de captura de fallas en detalles

Cuando el programa falla debido a una excepción no detectada, el sistema imprimirá automáticamente el seguimiento de la pila de la excepción. Incluya en el seguimiento de la pila la representación en cadena de la excepción, es decir, el resultado de la llamada a su método toString. Generalmente contiene el nombre de la clase de la excepción, seguido de un mensaje detallado. Normalmente, esto es sólo información que un programador o ingeniero de confiabilidad de un sitio web debe examinar al investigar la causa de una falla del software. Si la falla no se puede reproducir fácilmente, será difícil o incluso imposible obtener información adicional. Por lo tanto, es particularmente importante que el método toString de un tipo de excepción devuelva tanta información como sea posible sobre la causa del error. En otras palabras, la representación de cadena de la excepción debe capturar el error para facilitar el análisis posterior .

Para detectar una falla, los detalles de la excepción deben incluir los valores de todos los parámetros y campos que contribuyeron a la excepción . Por ejemplo, los detalles de una excepción IndexOutOfBoundsException deben incluir el límite inferior, el límite superior y el valor del índice que no se encuentra dentro de los límites. Este mensaje detallado proporciona mucha información sobre el error.

​Un consejo para la información sensible a la seguridad. Debido a que muchas personas pueden ver los seguimientos de la pila durante el proceso de diagnóstico y solución de problemas de software, nunca incluya contraseñas, claves e información similar en mensajes detallados .

Los detalles de la excepción no deben confundirse con los mensajes de error a nivel de usuario, que deben ser comprensibles para el usuario final. A diferencia de los mensajes de error a nivel de usuario, las representaciones de cadenas de excepción son utilizadas principalmente por programadores o ingenieros de confiabilidad de sitios web para analizar la causa de la falla . Por tanto, el contenido de la información es mucho más importante que la legibilidad. Los mensajes de error a nivel de usuario suelen estar localizados, mientras que los mensajes detallados de excepción rara vez están localizados . (Traducción Internacionalización)

Atomicidad garantizada del fallo

Cuando un objeto genera una excepción, generalmente esperamos que el objeto permanezca en un estado utilizable bien definido, incluso si la falla ocurre en medio de la realización de una operación. Esto es especialmente importante para las excepciones marcadas, ya que la persona que llama espera poder recuperarse de dichas excepciones. En general, una llamada fallida a un método debería dejar el objeto en el estado en el que se encontraba antes de ser llamado . Se dice que los métodos con esta propiedad tienen atomicidad de falla. (Conceptos básicos de las transacciones).

Hay varias formas de lograr este efecto. La forma más sencilla es diseñar un objeto inmutable (consulte el elemento 17 para obtener más detalles). Si el objeto es inmutable, la atomicidad del fallo es obvia. Si una operación falla, puede impedir que se creen nuevos objetos, pero nunca dejará los objetos existentes en un estado inconsistente porque cada objeto está en un estado consistente cuando se crea. No habrá cambios en el futuro.

Una forma común de lograr la atomicidad de fallas para los métodos que realizan operaciones en objetos mutables es verificar la validez de los parámetros antes de realizar la operación. Esto permite lanzar la excepción adecuada antes de modificar el estado del objeto .

Una tercera forma de lograr la atomicidad del fallo es realizar una operación en una copia temporal del objeto y luego reemplazar el contenido del objeto con el resultado de la copia temporal cuando se complete la operación . Si los datos se almacenan en una estructura de datos temporal, el proceso de cálculo será más rápido, por lo que es natural utilizar este método. Por ejemplo, algunas funciones de clasificación hacen una copia de seguridad de su lista de entrada en una matriz antes de realizar la clasificación, para reducir la sobrecarga de acceder a los elementos en el bucle interno de la clasificación. Esto se hace por motivos de rendimiento, pero tiene la ventaja adicional de garantizar que la lista de entrada permanezca intacta incluso si falla la clasificación.

La última forma de lograr la atomicidad de las fallas, que es mucho menos común, es escribir un código de recuperación que intercepte las fallas que ocurren durante la operación y devuelva el objeto al estado antes de que comenzara la operación . Este enfoque se utiliza principalmente para estructuras de datos persistentes (basadas en disco).

En resumen, como parte de la especificación de un método, cualquier excepción que genere debe dejar el objeto en el estado en el que se encontraba antes de llamar al método. Si se viola esta regla, la documentación de la API debe indicar claramente en qué estado estará el objeto. Desafortunadamente, gran parte de la documentación API existente no logra hacer esto.

No ignores las excepciones

Un bloque catch vacío , que es obligarlo a manejar situaciones de excepción. Ignorar anomalías es como ignorar una alarma de incendio.

En algunos casos se pueden ignorar las excepciones. Por ejemplo, al cerrar FileinputStream. Como no ha cambiado el estado del archivo, no necesita realizar ninguna acción de recuperación y ha leído la información requerida del archivo, por lo que no necesita finalizar la operación en curso. Incluso en este caso, es aconsejable registrar excepciones porque puede investigar sus causas si ocurren con frecuencia. Si elige ignorar la excepción, el bloque catch debe contener un comentario que explique por qué esto es posible y la variable debe denominarse ignorada:

Future<Integer> f = exec.submit(planarMap::chromaticNumber); 
int numColors = 4; // Default: guaranteed sufficient for any map 
try {
    
        
    numColors = f.get( 1L, TimeUnit.SECONDS ); 
} catch ( TimeoutException | ExecutionException ignored ) {
    
        
    // Use default: minimal coloring is desirable, not required 
}

Manejar las excepciones correctamente puede evitar completamente el fracaso. Mientras la excepción se propague al mundo exterior, al menos hará que el programa falle rápidamente, preservando así información que puede ayudar a depurar la condición de falla.

Acceso sincrónico a datos mutables compartidos

La palabra clave sincronizada puede garantizar que solo un hilo pueda ejecutar un determinado método o un determinado bloque de código al mismo tiempo.

Sin sincronización, los cambios en un hilo no pueden ser vistos por otros hilos. La sincronización no solo evita que un hilo vea un objeto en un estado inconsistente, sino que también garantiza que cada hilo que ingresa a un método sincronizado o bloque de código sincronizado pueda ver los efectos de todas las modificaciones anteriores protegidas por el mismo bloqueo.

Es posible que haya dicho que para mejorar el rendimiento, debe evitar el uso de la sincronización al leer o escribir datos atómicos. Este consejo es muy peligroso y equivocado. Aunque la especificación del lenguaje garantiza que los subprocesos no verán valores arbitrarios al leer datos atómicos, no garantiza que los valores escritos por un subproceso sean visibles para otro subproceso. La sincronización es necesaria para una comunicación confiable entre subprocesos y para un acceso mutuamente excluyente .

La mejor manera de evitar los problemas discutidos en este artículo es no compartir datos mutables. Comparta datos inmutables (consulte el elemento 17 para obtener más detalles) o no los comparta en absoluto. En otras palabras, limite los datos mutables a un solo hilo .

En resumen, cuando varios subprocesos comparten datos mutables, cada subproceso que lee o escribe datos debe realizar una sincronización . Sin sincronización, no hay garantía de que otro hilo conozca los cambios realizados por un hilo. No sincronizar los datos mutables compartidos puede provocar fallos de vida y de seguridad del programa. Estos fallos son difíciles de depurar. Pueden ser intermitentes y dependientes del tiempo, y el comportamiento del programa puede ser fundamentalmente diferente en diferentes máquinas virtuales. Si solo se requiere comunicación interactiva entre subprocesos y no se requiere exclusión mutua, el modificador volátil es una forma aceptable de sincronización, pero usarlo correctamente puede requerir algunos trucos.

Evite la sincronización excesiva

Para evitar fallas de vida y seguridad, nunca ceda el control del cliente dentro de un método sincronizado o bloque de código. En otras palabras, dentro de un área sincronizada, no llame a métodos que estén diseñados para ser anulados o que sean proporcionados por el cliente en forma de objeto de función (consulte el Elemento 24 para obtener más detalles). Desde la perspectiva de la clase que contiene el área sincronizada, dicho método es extraño. La clase no tiene idea de qué hará el método y no tiene control sobre él. Dependiendo del efecto del método externo, llamarlo desde un área sincronizada puede provocar una excepción, un punto muerto o corrupción de datos.

De hecho, existe una mejor manera de sacar las llamadas a métodos externos del bloque de código sincronizado. La biblioteca de clases Java proporciona una colección concurrente (colección concurrente); consulte el Artículo 81 para obtener más detalles, llamada CopyOnWriteArrayList, que está especialmente personalizada para este propósito. Este CopyOnWriteArrayList es una variante de ArrayList, que implementa todas las operaciones de escritura aquí volviendo a copiar toda la matriz subyacente. Dado que la matriz interna nunca cambia, la iteración no requiere bloqueo y es muy rápida . El rendimiento de CopyOnWriteArrayList se verá muy afectado si se usa mucho, pero es bueno para las listas de observadores porque rara vez cambian y se repiten con frecuencia.

En términos generales, debes trabajar lo menos posible dentro del área de sincronización . Obtenga el bloqueo, examine los datos compartidos, transforme los datos si es necesario y luego libere el bloqueo. Si debe realizar una acción que requiere mucho tiempo, debe intentar mover la acción fuera del área de sincronización sin comprometer la seguridad de los datos compartidos.

. En esta era de múltiples núcleos, el costo real de la sobresincronización no es el tiempo de CPU dedicado a adquirir bloqueos; es la oportunidad perdida de paralelismo y la latencia causada por la necesidad de garantizar que cada núcleo tenga una visión consistente de la memoria . Otro costo potencial de una sincronización excesiva es que limita la capacidad de la máquina virtual para optimizar la ejecución del código.

En resumen, para evitar interbloqueos y corrupción de datos, nunca llame a métodos externos desde dentro del campo del área de sincronización. En términos más generales, intente limitar al mínimo posible la cantidad de trabajo dentro de los campos del área de sincronización . Cuando diseñe una clase mutable, considere si deben sincronizarse entre sí. En la era actual de múltiples núcleos, esto es más importante que no sincronizar nunca demasiado. Sólo debe hacer esto si tiene una buena razón para sincronizar una clase internamente y debe documentar claramente esta decisión.

El ejecutor, la tarea y el flujo tienen prioridad sobre los subprocesos.

Elegir un servicio ejecutor para una aplicación en particular es complicado.

Si está escribiendo un programa pequeño o un servidor con poca carga, usar Executors.newCachedThreadPool suele ser una buena opción porque no requiere configuración y generalmente hace el trabajo correctamente.

​ Pero para servidores con cargas pesadas, ¡los grupos de subprocesos en caché no son una buena opción! En el grupo de subprocesos en caché, las tareas enviadas no se ponen en cola, sino que se entregan directamente al subproceso para su ejecución. Si no hay ningún hilo disponible, cree un hilo nuevo. Si un servidor está tan cargado que todas sus CPU están completamente ocupadas, cuando entren más tareas, se crearán más subprocesos, lo que sólo empeorará la situación.

Por lo tanto, en un servidor de producción muy cargado, es mejor usar Executors.newFixedThreadPool, que le proporciona un grupo de subprocesos que contiene un número fijo de subprocesos, o para maximizar el control sobre él, usar la clase ThreadPoolExecutor directamente.

No solo debes intentar no escribir tu propia cola de trabajo, sino que también debes intentar no utilizar subprocesos directamente. Cuando se utilizan subprocesos directamente, Thread actúa como una unidad de trabajo y un mecanismo de ejecución. En Executor Framework, la unidad de trabajo y el mecanismo de ejecución están separados . La abstracción clave ahora es la unidad de trabajo, llamada tarea. Hay dos tipos de tareas: Runnable y su prima cercana Callable (es similar a Runnable, pero devuelve un valor y puede generar excepciones arbitrarias). El mecanismo común para ejecutar tareas es el servicio ejecutor. Si observa el problema desde la perspectiva de la tarea y deja que un servicio ejecutor realice la tarea por usted, obtendrá una gran flexibilidad para elegir la estrategia de ejecución adecuada. Esencialmente, lo que hace Executor Framework es ejecución y lo que hace Collections Framework es agregación.

En Java 7, Executor Framework se ha ampliado para admitir tareas de unión de bifurcación, que se ejecutan a través de un servicio ejecutor especial llamado grupo de unión de bifurcación. Una tarea fork-join está representada por una instancia de ForkJoinTask, que se puede dividir en subtareas más pequeñas. El hilo que contiene ForkJoinPool no solo debe procesar estas tareas, sino también "robar" tareas de otro hilo para garantizar que todos los hilos permanezcan ocupados, de esta manera mejorando el uso de la CPU, el rendimiento mejorado y la latencia más baja . Escribir y realizar tareas de unión de diapasones es complicado. Los flujos simultáneos (consulte el elemento 48 para obtener más detalles) se escriben en grupos de unión de bifurcaciones y podemos disfrutar de sus ventajas de rendimiento con poco esfuerzo, suponiendo que sean adecuados para la tarea en cuestión.

Priorizar el uso de herramientas de concurrencia sobre esperar y notificar

Las herramientas más avanzadas en java.util.concurrent se dividen en tres categorías: Executor Framework, Concurrent Collection y Synchronizer.

Las colecciones concurrentes proporcionan implementaciones concurrentes de alto rendimiento para interfaces de colección estándar (como Lista, Cola y Mapa). Para proporcionar una alta concurrencia, estas implementaciones gestionan la sincronización internamente. Por lo tanto, es imposible excluir la actividad concurrente de una colección concurrente; bloquearla no hace más que hacer que el programa sea más lento .

Además de proporcionar una excelente concurrencia, ConcurrentHashMap también es muy rápido. En mi máquina, el método interno optimizado anterior es más de 6 veces más rápido que String.intern (pero recuerde, String.intern debe usar algún tipo de referencia débil para evitar pérdidas de memoria con el tiempo). Las colecciones simultáneas dieron como resultado que las colecciones sincronizadas quedaran en su mayoría obsoletas. Por ejemplo, se debe utilizar ConcurrentHashMap con preferencia a Collections.synchronizedMap . Simplemente reemplazar mapas sincrónicos con mapas concurrentes puede mejorar en gran medida el rendimiento de las aplicaciones concurrentes.

Un sincronizador es un objeto que permite a un hilo esperar a otro hilo, permitiéndole coordinar acciones. Los sincronizadores más utilizados son CountDownLatch y Semaphore. Los menos utilizados son CyclicBarrier e Exchanger. Un potente sincronizador es Phaser .

Un pestillo de cuenta regresiva es una barrera de una sola vez que permite que uno o más subprocesos esperen a que uno o más subprocesos hagan algo. El único constructor de Count DownLatch toma un parámetro de tipo int. Este parámetro int se refiere al número de veces que se debe llamar al método countDown en el pestillo antes de que se procesen todos los subprocesos en espera.

En resumen, usar el método de espera y el método de notificación directamente es como programar en un "lenguaje ensamblador concurrente", mientras que java.util.concurrent proporciona un lenguaje de nivel superior. Hay pocas razones, si es que hay alguna, para utilizar métodos de espera y notificación en código nuevo . Si mantiene código que utiliza el método de espera y el método de notificación, asegúrese de llamar siempre al método de espera desde dentro de un bucle while usando el patrón estándar. En general, se debe utilizar el método notifyAll con preferencia al método notify. Si utiliza el método de notificación, tenga cuidado de garantizar la vida del programa.

La documentación debe contener atributos de seguridad para subprocesos.

El comportamiento de una clase cuando sus métodos se utilizan simultáneamente es una parte importante de su acuerdo con el cliente. Si no documenta el comportamiento de una clase a este respecto, sus usuarios se verán obligados a hacer suposiciones. Si estas suposiciones son erróneas, el programa resultante puede carecer de suficiente sincronización o puede tener una sincronización excesiva. En cualquier caso, pueden producirse errores graves.

Es posible que haya oído que puede determinar si un método es seguro para subprocesos buscando el modificador sincronizado en la documentación del método. Esta visión es errónea en varios aspectos. En funcionamiento normal, hay una razón por la cual el modificador de sincronización no se incluye en la salida del Javadoc. La presencia del modificador sincronizado en una declaración de método es un detalle de implementación y no parte de su API. No indica de manera confiable que un método sea seguro para subprocesos .

Para permitir el uso concurrente seguro, una clase debe documentar claramente los niveles de seguridad de subprocesos que admite .

En resumen, cada clase debe tener una descripción cuidadosamente redactada o documentar claramente sus atributos de seguridad para subprocesos mediante anotaciones seguras para subprocesos . El modificador sincronizado no tiene ningún efecto en el documento. Una clase condicionalmente segura para subprocesos debe registrar qué secuencias de llamadas a métodos requieren sincronización externa y qué bloqueos deben adquirirse mientras se ejecutan esas secuencias. Si escribe una clase que es incondicionalmente segura para subprocesos, considere usar un objeto de bloqueo privado en lugar de un método sincronizado. Esto lo protegerá de la interferencia de sincronización de clientes y subclases y le brindará mayor flexibilidad para adoptar métodos sofisticados de control de concurrencia en versiones posteriores.

Utilice la inicialización diferida de forma inteligente y sensata

La inicialización diferida retrasa la inicialización de un campo hasta que se necesita su valor. Si el valor no es obligatorio, el campo no se inicializa. Esta técnica funciona tanto para campos estáticos como para campos de instancia. Aunque la inicialización diferida es principalmente una optimización, también se puede utilizar para romper bucles dañinos e inicializaciones de instancias en clases.

La inicialización diferida también tiene sus usos. Si solo se accede a un campo en un pequeño subconjunto de instancias de la clase y la inicialización del campo es costosa, puede valer la pena la inicialización diferida. La única forma de saberlo con seguridad es medir el rendimiento de una clase con y sin inicialización diferida.

En la mayoría de los casos, la inicialización regular es mejor que la inicialización diferida .

Si usa una inicialización diferida en lugar de una inicialización circular, use accesores sincronizados , ya que son una alternativa simple y clara:

// Lazy initialization of instance field - synchronized accessor 
private FieldType field; 

private synchronized FieldType getField() {
    
        
    if (field == null)        
        field = computeFieldValue();    
    return field; 
}

Ambos modismos (inicialización normal e inicialización diferida usando accesores sincronizados) no cambian cuando se aplican a campos estáticos, excepto por la adición del modificador estático al campo y las declaraciones de acceso.

Si necesita utilizar la inicialización diferida para mejorar el rendimiento de los campos de instancia, utilice el modo de doble verificación. Este patrón evita el costo de bloqueo al acceder a los campos después de la inicialización .

En resumen, debe inicializar la mayoría de los campos normalmente en lugar de inicializarlos de forma perezosa. Si debe inicializar campos de forma diferida para lograr objetivos de rendimiento o romper ciclos de inicialización dañinos, utilice técnicas de inicialización diferida apropiadas. Para campos, use el modo de doble verificación; para campos estáticos, debe usar el modismo de clase de titular de inicialización diferida. Por ejemplo, para tolerar la inicialización repetida de campos de instancia, también podría considerar el patrón de verificación única.

No confíes en el programador de hilos

Cuando se pueden ejecutar muchos subprocesos, el programador de subprocesos decide qué subprocesos se pueden ejecutar y durante cuánto tiempo. Cualquier sistema operativo razonable intentará tomar esta decisión de manera justa, pero las estrategias pueden variar. Por lo tanto, los programas bien redactados no deberían depender de los detalles de esta estrategia. Es probable que cualquier programa que dependa de un programador de subprocesos para su corrección o rendimiento no sea portátil .

La mejor manera de escribir programas robustos, responsivos y portátiles es asegurarse de que la cantidad promedio de subprocesos ejecutables no sea significativamente mayor que la cantidad de procesadores. Esto deja al programador de subprocesos con pocas opciones: solo ejecuta subprocesos ejecutables hasta que ya no se pueden ejecutar. Incluso bajo estrategias de programación de subprocesos completamente diferentes, el comportamiento del programa no cambia mucho.

La técnica principal para mantener baja la cantidad de subprocesos ejecutables es hacer que cada subproceso realice un trabajo útil y luego esperar a que haya más trabajo. Si los subprocesos no realizan un trabajo útil, no deberían ejecutarse . Para el marco Executor (consulte el Elemento 80 para obtener más detalles), esto significa dimensionar el grupo de subprocesos de manera adecuada [Goetz06, 8.2] y mantener las tareas cortas (pero no demasiado cortas); de lo contrario, la sobrecarga de despacho seguirá perjudicando el rendimiento.

En resumen, no confíe en el programador de subprocesos para juzgar la corrección del programa. El programa resultante no es ni robusto ni portátil. Por lo tanto, no confíe en Thread.yield o la prioridad del subproceso. Estas herramientas son sólo sugerencias para el planificador. La prioridad de subprocesos se puede utilizar con moderación para mejorar la calidad del servicio de un programa que ya funciona, pero nunca se debe utilizar para "arreglar" un programa que apenas funciona.

Prefiere alternativas a la serialización de Java

​ Un problema fundamental con la serialización es que está demasiado abierta a los ataques y difícil de proteger, y el problema continúa creciendo: deserializar el gráfico de objetos llamando al método readObject en ObjectInputStream. Este método es esencialmente un constructor mágico que se puede utilizar para crear instancias de casi cualquier tipo de objeto en el classpath, siempre que el tipo implemente la interfaz Serializable. Durante el proceso de deserializar el flujo de bytes, este método puede ejecutar código de cualquiera de estos tipos, por lo que todos estos tipos de código están dentro del alcance del ataque.

Los ataques pueden involucrar bibliotecas de la plataforma Java, bibliotecas de terceros (como la colección Apache Commons) y clases en la propia aplicación. Incluso si sigue todos los mejores consejos relevantes y escribe con éxito clases serializables que no sean vulnerables a ataques, su aplicación aún puede ser vulnerable.

Cuando deserializas un flujo de bytes en el que no confías, eres vulnerable. Una buena forma de evitar la explotación de la serialización es nunca deserializar nada . No hay ninguna razón para utilizar la serialización de Java en ningún sistema nuevo que escriba .

Si no puede evitar la serialización de Java por completo, tal vez porque necesita trabajar en un entorno de sistema heredado, entonces su siguiente mejor opción es nunca deserializar datos que no sean de confianza .

En resumen, la serialización es peligrosa y debe evitarse. Si está diseñando un sistema desde cero, puede utilizar datos estructurados multiplataforma como JSON o protobuf. No deserialice datos que no sean de confianza. Si debe hacer esto, utilice el filtrado de deserialización de objetos, pero tenga en cuenta que no se garantiza que bloquee todos los ataques. Evite escribir clases serializables. Si debes hacer esto, ten mucho cuidado.

Implementar serializable con precaución

​ Hacer serializables instancias de una clase es muy sencillo, basta con implementar la interfaz Serializable. Debido a que es tan fácil de hacer, existe la idea errónea de que la serialización requiere muy poco esfuerzo por parte del programador. La realidad es mucho más compleja. Si bien el costo inmediato de hacer que una clase sea serializable es insignificante, el costo a largo plazo suele ser enorme.

Un costo importante de implementar la interfaz Serializable es que reduce la flexibilidad para cambiar la implementación de la clase una vez publicada.

El segundo costo de implementar la interfaz Serializable es que aumenta la posibilidad de errores y vulnerabilidades de seguridad .

El tercer costo de implementar la interfaz Serializable es que aumenta la carga de prueba asociada con el lanzamiento de nuevas versiones de la clase.

	实现 Serializable 接口并不是一个轻松的决定。 如果一个类要参与一个框架,该框架依赖于 Java 序列化来进行 对象传输或持久化,这对于类来说实现 Serializable 接口就是非常重要的。

Las clases diseñadas para herencia rara vez son adecuadas para implementar la interfaz serializable y las interfaces rara vez son adecuadas para extenderla. Violar esta regla supone una carga significativa para cualquiera que amplíe la clase o implemente la interfaz.

Las clases internas . Utilizan campos sintéticos generados por el compilador para almacenar referencias a la instancia adjunta y para almacenar valores de variables locales de la instancia adjunta. La correspondencia entre estos campos y la definición de clase es la misma que si no se especificaran los nombres de las clases anónimas y de las clases parciales. Por lo tanto, la forma de serialización predeterminada de las clases internas no está definida. Sin embargo, las clases de miembros estáticos pueden implementar la interfaz Serializable.

Considere utilizar un formulario de serialización personalizado

Cuando escribes clases en un momento de escasez de tiempo, normalmente debes concentrarte en diseñar una API bien diseñada. A veces, esto significa lanzar una implementación "única" que usted sabe que será reemplazada en una versión futura. Normalmente esto no es un problema, pero si su clase implementa la interfaz Serializable y usa la forma predeterminada de serialización, nunca podrá deshacerse por completo de esta implementación "única" . Siempre afectará al formulario serializado. Ésta no es sólo una cuestión teórica.

​No acepte el formulario de serialización predeterminado sin . Aceptar el formulario de serialización predeterminado debería ser una decisión que se justifique desde una combinación de perspectivas de flexibilidad, rendimiento y corrección. En general, al diseñar un formulario de serialización personalizado, solo debe aceptar el formulario de serialización predeterminado si es sustancialmente el mismo que la codificación seleccionada por el formulario de serialización predeterminado.

Independientemente de la forma de serialización que elija, declare un UID de versión serial explícito en cada clase serializable que escriba . Esto elimina los UID de la versión de serie como fuente potencial de incompatibilidades (consulte el Elemento 86 para obtener más detalles). También se puede obtener una pequeña ventaja de rendimiento al hacer esto. Si no se proporciona un UID de versión en serie, es necesario realizar costosos cálculos para generar uno en tiempo de ejecución.

En resumen, si ha decidido que una clase debe ser serializable, piense detenidamente cuál debería ser la forma de serialización. Utilice únicamente el formulario de serialización predeterminado cuando el estado lógico del objeto se describa razonablemente; de ​​lo contrario, diseñe un formulario de serialización personalizado adecuado para describir el objeto. Diseñar la forma serializada de una clase debería llevar tanto tiempo como diseñar el método exportado, y ambos deben tratarse con precaución (consulte el Elemento 51 para obtener más detalles). Así como los métodos exportados no se pueden eliminar de versiones futuras, los campos no se pueden eliminar del formulario serializado; deben guardarse para siempre para garantizar la compatibilidad de serialización. Elegir la forma incorrecta de serialización puede tener un impacto negativo permanente en la complejidad y el rendimiento de su clase.

Escribe el método readObject de forma protectora.

En resumen, cuando escriba el método readObject, piense así: está escribiendo un constructor público y, sin importar qué flujo de bytes se le pase, debe producir una instancia válida. No asuma que este flujo de bytes representa necesariamente una instancia serializada real. Aunque en los ejemplos de esta entrada la clase usa el formulario de serialización predeterminado, todos los posibles problemas discutidos también se aplican a clases con formularios de serialización personalizados. A continuación se presentan algunas pautas en forma resumida que lo ayudarán a escribir métodos readObject más sólidos.

  • Los campos de referencia de objetos en una clase deben mantenerse como propiedades privadas y cada objeto en estos campos debe copiarse de manera protectora. Los componentes mutables en clases inmutables entran en esta categoría
  • Para cualquier restricción, se lanza una InvalidObjectException si la verificación falla. Estos controles deben seguir todas las copias protectoras.
  • Si se debe validar todo el gráfico de objetos después de deserializarlo, debe usar la interfaz ObjectInputValidation (no se analiza en este libro).
  • Ya sea un método directo o indirecto, no llame a ningún método anulable en la clase.

Por ejemplo, control, la enumeración es mejor que readResolve

De hecho, si confía en readResolve para el control de instancias, todos los campos de instancia con tipos de referencia de objetos deben declararse transitorios.

La accesibilidad de readResolve . Si coloca el método readResolve en una clase final, debería ser privado. Si coloca el método readResolve en una clase no final, debe considerar cuidadosamente su accesibilidad. Si es privado, no se aplicará a ninguna subclase. Si es privado a nivel de paquete, se aplica a las subclases dentro del mismo paquete. Si está protegida o es pública y la subclase no la anula, deserializar la subclase serializada producirá una instancia de superclase, lo que puede resultar en una excepción ClassCastException.

En resumen, los tipos enumerados deben usarse siempre que sea posible para implementar restricciones controladas por instancias . Si esto no es posible y necesita una clase que sea serializable y controlada por instancia, debe proporcionar un método readResolve y asegurarse de que todos los campos instanciados de la clase sean de tipos básicos o transitorios.

Considere la posibilidad de utilizar servidores proxy de serialización en lugar de instancias de serialización.

La implementación de la interfaz Serializable aumenta la posibilidad de errores y problemas de seguridad porque permite crear instancias utilizando mecanismos fuera del lenguaje en lugar de utilizar constructores ordinarios. Sin embargo, existe una manera de reducir en gran medida estos riesgos. Es el patrón de proxy de serialización.

El patrón de proxy de serialización es bastante simple. Primero, diseñe una clase anidada estática privada para la clase serializable que represente con precisión el estado lógico de la clase adjunta. Esta clase anidada se llama proxy de serialización y debe tener un constructor independiente cuyo tipo de parámetro sea la clase adjunta. Este constructor simplemente copia los datos de sus argumentos: no necesita realizar comprobaciones de coherencia ni copias protectoras. Desde una perspectiva de diseño, la forma de serialización predeterminada del agente de serialización es la mejor forma de serialización de la clase adjunta. Tanto la clase periférica como su proxy serie deben declarar implementar la interfaz serializable.

El patrón de proxy de serialización No es compatible con clases que puedan ser ampliadas por los clientes. Tampoco es compatible con algunas clases que contienen bucles en el gráfico de objetos : si intenta llamar a un método en un objeto desde el método readResovle del proxy de serialización de ese objeto, obtendrá una ClassCastException porque no tiene el objeto. todavía, solo su proxy de serialización.

Finalmente, la funcionalidad y seguridad mejoradas proporcionadas por el patrón de proxy serializado tienen un precio. En mi máquina, la sobrecarga de serializar y deserializar instancias de Period a través del proxy de serialización es un 14% mayor que usar una copia protectora.

En resumen, cuando descubra que debe escribir el método readObject o writeObject en una clase que el cliente no puede extender, debería considerar usar el patrón de proxy de serialización. Este patrón es la manera fácil de serializar de manera robusta objetos con restricciones importantes.

Supongo que te gusta

Origin blog.csdn.net/weixin_40709965/article/details/130898972
Recomendado
Clasificación