A simple article to understand the "Effective Java" recommendations

Consider using static factory methods instead of constructors

Traditionally, obtaining an object instance is usually through the construction method, new an object; different numbers of input parameters will have different construction methods;

For example, to return a unified result class, the traditional method (pseudocode) is as follows:

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

We use the static factory method alternative to rewrite, as follows:

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

Using static factory methods has the following main advantages:

  1. Unlike constructors with the same name, they can customize method names for easy use;
  2. Unlike the constructor method that calls new a new object every time, they do not need to create a new object every time; (flyweight mode or singleton mode)
  3. Unlike constructors, they can return,the defined return type and its subtypes;
  4. The class of the returned object can be different according to the input parameters; (Abstract Programming Oriented)
  5. When writing a class that contains this method, the class that returns the object does not need to exist; (for abstract programming, derived classes can be returned)

The main disadvantages of static factory methods are:

  1. Because it is a static class and is oriented to abstract programming, it is not easy to instantiate subclasses.

  2. Because of the custom method name, it is difficult for programmers to find it if there is no documentation or source code.

Use the Builder pattern when there are too many constructor parameters

For example, there is currently a user class with multiple attributes:

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;
    }
    
    ...

​ The way to create objects according to the traditional method is as follows:

 		//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);

The disadvantage of the construction method is that it is inconvenient to scale when faced with multiple optional parameters; it is necessary to define a corresponding construction method for each situation;

JavaBeans method (setter method), the assignment may be lengthy; a more serious shortcoming is that since the construction assignment method can be divided in multiple calls, the JavaBean may be in an inconsistent state during the construction process ; (for example, the JavaBean object as Parameters, when passed by reference, different methods of set assignment may lead to JavaBean inconsistency)

The advantage of the Builder pattern over the construction method is that it can have multiple variable parameters and the construction is more flexible;

Use private constructors or enumerations to implement 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

Enum implementation singletons are similar to public property methods, but are more concise, provide a free serialization mechanism, and provide solid guarantees against multiple instantiations, even in the case of complex serialization or reflection attacks .

This approach may feel a little unnatural, but a single-element enum class is often the best way to implement a singleton. Note that this method cannot be used if the singleton must inherit a parent class other than Enum (although it is possible to declare an Enum to implement the interface) .

​ Reference link: https://juejin.cn/post/7229660119658512441

Use private constructors to avoid instantiation

Occasionally you will want to write a class, which is just a set of static methods and static properties; (such as a utility class or a constant class)

​ Such utility classes are not designed to be instantiated, so they can be de-instantiated by including a private constructor.

Use dependency injection instead of hard-wiring resources

​ Many classes depend on one or more underlying resources; the so-called hard link resources are to set the dependent resources as static or singletons; such hard links will be inconvenient to expand and not flexible enough; they can be replaced by dependency injection ; Static utility classes and singletons are inappropriate for classes whose behavior is parameterized by underlying resources

Hard link resource method:

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

Dependency injection method:

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) {
    
     ... }
}

Avoid creating unnecessary objects

This entry should not be misinterpreted as implying that object creation is expensive and object creation should be avoided. In contrast, it is very cheap to create and recycle small objects using constructors, which do very little explicit work, especially on modern JVM implementations. It's usually a good thing to create additional objects to enhance your program's clarity, simplicity, or functionality.

Conversely, it is a bad idea to avoid object creation by maintaining your own object pool unless the objects in the pool are very heavyweight . A typical example of an object pool is a database connection. The cost of establishing a connection is very high, so it only makes sense to reuse these objects

Eliminate expired object references

If a stack grows and then shrinks, objects popped off the stack will not be garbage collected , even if those objects are no longer referenced by the program using the stack. This is because the stack maintains obsolete references to these objects. An expired reference is simply a reference that will never be released. In this case, any reference outside the "active portion" of the element array is stale. The active part is composed of elements whose index subscript is less than size

Memory leaks in garbage-collected languages ​​(more appropriately called unintentional object retentions) are insidious. If an object reference is inadvertently retained, not only is the object excluded from garbage collection, but so are any objects referenced by the object. Even if only a few object references are inadvertently retained, it can prevent the garbage collection mechanism from recycling many objects, which has a large impact on performance.

The solution to this type of problem is simple: once object references expire, set them to null ;

When programmers are first troubled by this problem, they may clear all object references immediately after the program ends. This is neither necessary nor desirable; it clutters the program unnecessarily. Clearing object references should be the exception rather than the norm . A good way to eliminate stale references is to make the variable containing the reference go out of scope. This naturally occurs if each variable is defined in a close scope (Item 57)

Avoid using Finilizer and Cleaner mechanisms

The Finalizer mechanism is unpredictable, often dangerous, and often unnecessary . Their use can lead to erratic behavior, poor performance and portability issues. The Finalizer mechanism has some special uses, which we'll cover later in this entry, but they should generally be avoided. Starting from Java 9, the Finalizer mechanism has been deprecated but is still used by Java class libraries. The Cleaner mechanism in Java 9 replaces the Finalizer mechanism. The Cleaner mechanism is not as dangerous as the Finalizer mechanism, but is still unpredictable, slow and often unnecessary

One drawback of the Finalizer and Cleaner mechanisms is that they are not guaranteed to execute in a timely manner [JLS, 12.6]. The time between when an object becomes inaccessible and when the Finalizer and Cleaner mechanisms start running is arbitrarily long. This means that you should never do anything time-critical with the Finalizer and Cleaner mechanisms .

The try-with-resources statement replaces the try-finally statement

​ When Java 7 introduced the try-with-resources statement, all these problems were solved at once [JLS, 14.20.3]. To use this construct, the resource must implement the AutoCloseable interface , which consists of a close that returns void. Many classes and interfaces in Java and third-party libraries now implement or inherit the AutoCloseable interface . If you write a class that represents a resource that must be closed, then this class should also implement the AutoCloseable interface

When dealing with resources that must be closed, use the try-with-resources statement instead of the try-finally statement . The generated code is cleaner and clearer, and the generated exceptions are more useful. The try-with-resources statement makes it easier and error-free to write code that must close resources, which is virtually impossible with the try-finally statement.

Follow common conventions when overriding the equals method

Although Object is a concrete class, it is primarily designed for inheritance . All its non-final methods (equals, hashCode, toString, clone, and finalize) have clear general contracts because they are designed to be overridden by subclasses. Any class is obliged to override these methods to comply with their general contract; failure to do so will prevent other classes that rely on the convention (such as HashMap and HashSet) from working properly with this class .

When do you need to override the equals method? If a class contains a concept of logical equality that is distinct from object identity, and the parent class has not overridden the equals method. This is typically used in the case of value classes. A value class is simply a class that represents a value, such as an Integer or String class. Programmers use the equals method to compare references to value objects, expecting to find whether they are logically equal, rather than referencing the same object. Overriding the equals method can not only meet programmer's expectations, it also supports overriding instances of equals as keys of Map or elements in Set to meet expectations and desired behavior.

When you override the equals method, you must adhere to its general conventions. The specification of Object is as follows: The equals method implements an equivalence relation. It has the following properties:

  • Reflexivity: x.equals(x) must return true for any non-null reference x
  • Symmetry: For any non-null reference x and y, x.equals(y) must return true if and only if y.equals(x) returns true
  • Transitivity: For any non-null reference x, y, z, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) must return true
  • Consistency: For any non-null references x and y, multiple calls to x.equals(y) must always return true or always return false if the information used in the equals comparison is not modified.
  • For any non-null reference x, x.equals(null) must return false

Putting it all together, here's a recipe for writing a high-quality equals method:

  1. Use the == operator to check whether the argument is a reference to the object . If yes, return true. This is just a performance optimization, but if this comparison is likely to be expensive, it's worth doing.
  2. Use the instanceof operator to check whether the parameters have the correct type . If not, return false. Usually, the correct type is the class where the equals method is located. Sometimes, the class is modified to implement some interfaces. If a class implements an interface that improves the equals convention to allow comparison of classes that implement the interface, then use the interface. Collection interfaces such as Set, List, Map and Map.Entry have this feature. 3. Convert the parameters to the correct type. Because the conversion operation is already handled in instanceof, it will definitely succeed.
  3. For each "important" property in the class, check whether the parameter property matches the corresponding property of the object . Returns true if all these tests succeed, false otherwise. If the type in step 2 is an interface, then the parameter's properties must be accessed through interface methods; if the type is a class, the properties can be accessed directly, depending on the property's access permissions.

In short, don't override the equals method unless you must: in many cases, the implementation inherited from Object is exactly what you want. If you do override the equals method, be sure to compare all important properties of the class, and do so in a way that protects the five provisions of the equals contract above.

When overriding the equals method, you must also override the hashcode method.

​In each class, when overriding the equals method, be sure to override the hashcode method . If you don't do this, your class violates the general convention of hashCode, which prevents it from working properly with collections like HashMap and HashSet

According to the Object specification, the following are specific conventions:

  • When the hashCode method is called repeatedly on an object during the execution of an application, it must always return the same value if no information is modified in the equals method comparison . The value returned from each execution from one application to another can be inconsistent
  • If two objects are equal according to the equals(Object) method, then calling hashCode on both objects must produce the same integer.
  • If two objects do not compare equal according to the equals(Object) method, there is no requirement that calling hashCode on each object must produce a different result. However, programmers should be aware that generating different results for unequal objects may improve the performance of hash tables.

When hashCode cannot be overridden, the second key clause violated is: equal objects must have equal hash codes

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

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

​ In short, the hashCode method must be rewritten every time the equals method is overridden , otherwise the program will not run properly. Your hashCode method must follow the general conventions specified by the Object class, and must do a reasonable job of assigning unequal hash codes to unequal instances.

Always override toString method

Although the Object class provides an implementation of the toString method, the string it returns is usually not what users of your class want to see. It consists of the class name followed by an "at" sign (@) and the unsigned hexadecimal representation of the hash code, for example User@163b91.

The general convention for toString requires that the returned string should be "a concise but informative representation that is easily readable by humans."

Although it's not as important as following the equals and hashCode conventions, providing a good toString implementation makes your class easier to use, and systems that use it easier to debug. The toString method is automatically called when the object is passed to println, printf, string concatenation operator or assertion, or printed by the debugger

Override the clone method carefully

Suppose you want to implement the Cloneable interface in a class whose parent class provides a well-behaved clone method. First call super.clone. The resulting object will be a fully functional replica of the original. Any properties declared in your class will have the same value as the original property. If each property contains a primitive value or a reference to an immutable object, the returned object may be exactly what you need, in which case no further processing is required

Considering all the problems associated with the Cloneable interface, new interfaces should not inherit it and new extensible classes should not implement it. Although there is no harm in implementing the Cloneable interface for final classes, it should be considered from a performance optimization perspective and is only justified in rare cases (Item 67). Usually, copying functionality is best provided by a constructor or factory. The obvious exception to this rule is arrays, which can be copied using the clone method.

Consider implementing the Comparable interface

​ Sometimes, you may see that compareTo or compare methods rely on the difference between two values, which is negative if the first value is less than the second value; zero if the two values ​​​​are equal, and zero if the first value is equal. If the value is greater than , it is a positive value. Here is an example:

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

Don’t use this technique! It may lead to dangers of integer large-length overflow and IEEE 754 floating-point arithmetic distortion [JLS 15.20.1, 15.21.1]. Furthermore, the resulting method is unlikely to be significantly faster than one written using the above techniques. Use the static compare method:

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

Or use the Comparator build method:

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

In summary, whenever you implement a value class with reasonable ordering, you should have that class implement the Comparable interface so that its instances can be easily sorted, searched, and used in comparison-based collections. When comparing field values ​​in implementations of the compareTo method, avoid using the "<" and ">" operators. Instead, use the static compare method in the wrapper class or the build method in the Comparator interface

Make classes and members less accessible

A well-designed component hides all its implementation details, cleanly separating its API from its implementation . The components then only communicate through their APIs and know nothing about each other's inner workings. This concept, known as information hiding or encapsulation, is a fundamental principle of software design (Demit's law, the principle of least known).

Information hiding is important for many reasons, most of which stem from the fact that it separates the components that make up a system, allowing them to be independently developed, tested, optimized, used, understood, and modified. This speeds up system development because components can be developed in parallel . It eases the burden of maintenance because components can be understood, debugged, or replaced more quickly without fear of damaging other components.

Java provides many mechanisms to aid information hiding. The access control mechanism [JLS, 6.6] specifies the accessibility of classes, interfaces, and members. The accessibility of an entity depends on where it is declared, and which access modifiers (private, protected and public) are present in the declaration. Proper use of these modifiers is critical to information hiding.

The rule of thumb is simple: make every class or member as inaccessible as possible. In other words, use the lowest possible access level that is consistent with the functionality of the software you are writing

Use access methods instead of public properties in public classes

For public classes, it is correct to stick to object orientation: if a class is accessible outside its package, provide access methods to preserve the flexibility to change the internal representation of the class . If a public class exposes its data properties, it is essentially impossible to change its representation later because client code can be spread out in many places.

However, if a class is package-level private, or is a private inner class, then there is nothing inherently wrong with exposing its data properties - assuming they provide sufficient description of the abstraction provided by the class

In short, public classes should not expose mutable properties. The harm of public exposure to immutable properties is still problematic, but less harmful. However, sometimes a package-level private or private inner class is needed to expose properties, whether or not such class is mutable.

Minimize variability

An immutable . All information contained in each instance is fixed for the lifetime of the object, so no changes will be observed. The Java platform class library contains many immutable classes, including the String class, basic type wrapper classes, and the BigInteger and BigDecimal classes. There are many good reasons: immutable classes are easier to design, implement, and use than mutable classes. They are less prone to errors and more secure.

To make a class immutable, follow these five rules:

  1. Do not provide methods to modify the object's state
  2. Make sure this class cannot be inherited . This prevents careless or malicious subclasses from assuming that the object's state has changed, thereby breaking the class's immutable behavior. Preventing subclassing is usually done by making a class final, but we'll discuss another method later.
  3. Set all properties to final . Enforce through the system and clearly communicate your intentions. Additionally, if a reference to a newly created instance is passed from one thread to another without synchronization, correct behavior must be guaranteed, as described in the memory model [JLS, 17.5; Goetz06,16].
  4. Set all properties to private . This prevents clients from gaining access to mutable objects referenced by properties and modifying those objects directly. While it is technically allowed for an immutable class to have a public final property containing a base type numeric value or a reference to an immutable object, this is not recommended as it does not allow the internal representation to change in a later version
  5. Ensure mutually exclusive access to any mutable components . If your class has any properties that reference mutable objects, make sure that clients of the class cannot get references to those objects. Never initialize such a property to a client-supplied object reference or return the property from an access method. Defensive copying in constructors, accessors, and readObject methods (Item 88)

Immutable objects are simple. An immutable object can be completely in one state, that is, the state when it was created.

Immutable objects are inherently thread-safe; they do not require synchronization . They are not corrupted when accessed by multiple threads simultaneously. This is a simple way to achieve thread safety. Because no thread can observe another thread's effect on an immutable object, immutable objects can be freely shared .

Not only immutable objects can be shared, but also internal information can be shared;

Immutable objects provide great building blocks for other objects , whether mutable or immutable. Maintaining invariants for a complex component is much easier if you know that its internal objects won't change

Immutable objects provide a free atomic failure mechanism . Their state never changes, so temporary inconsistencies are impossible

The main disadvantage of immutable classes is that a separate object is required for each different value .

All in all, never write a get method for each property and then write a corresponding set method. Classes should be immutable unless there is a good reason to make them mutable . Immutable classes offer many advantages, the only disadvantage is that performance issues may arise in some cases. You should always use smaller value objects and make them immutable.

For some classes, immutability is impractical. If a class cannot be designed to be immutable, then its mutability should be limited as much as possible . Reducing the number of states an object can exist in makes it easier to analyze the object and reduces the likelihood of errors. Therefore, every property should be set to final unless there is a good reason to set the property to non-final. Combining the advice in this item with the advice in Item 15, your natural inclination is to declare every property as private final unless there is a good reason not to do so .

Composition is better than inheritance

Unlike method invocation, inheritance breaks encapsulation . In other words, a subclass relies on the implementation details of its parent class to ensure its correct functionality. The parent class's implementation may keep changing from release version, and if so, the child class may be broken even though nothing in its code has changed. Therefore, a subclass must be updated and changed along with its superclass, unless the author of the superclass specifically designed it for the purpose of inheritance and documented the instructions.

Both of these problems stem from overriding methods. You might think it's safe to inherit from a class if you just add new methods and don't override existing methods. While this extension is more secure, it is not without risks. If the parent class adds a new method in a subsequent version, and you unfortunately give the subclass a method with the same signature and a different return type, then your subclass will fail to compile . If you have already provided a child class with a method that has the same signature and return type as the new parent class method, you are now overriding it and will therefore encounter the problems described earlier. Furthermore, it is questionable whether your method will fulfill the contract of the new parent class method, since this contract has not yet been written when you write the subclass method

Fortunately, there is a way to avoid all of the above problems. Instead of inheriting an existing class, add a private property to your new class that is an instance reference of the existing class. This design is called composition because the existing class becomes the new class 's component. Each instance method in the new class calls the corresponding method on the containing instance of the existing class and returns the result. This is called forwarding, and the methods in the new class are called forwarding methods.

In summary, inheritance is powerful, but it is problematic because it violates encapsulation. This only applies if there is a true subtype relationship between the child class and the parent class . Even so, inheritance can lead to fragility if the child class is not in the same package as the parent class, and the parent class is not designed for inheritance. To avoid this fragility, use composition and forwarding instead of inheritance, especially if a suitable interface exists to implement the wrapper class. Wrapper classes are not only more robust than subclasses, they are also more powerful.

If inheritance is used, design it and document it.

​ So what does it mean to design and document a class for inheritance?

First, the class must accurately describe the impact of overriding this method. In other words, the class must document the self-use of the overridable method. For each public or protected method, the documentation must indicate which overridden methods the method calls, in which order, and how the results of each call affect subsequent processing. (Overriding methods, here refers to non-final modified methods, whether public or protected.) More generally, a class must document any situation in which an overridable method may be called.

​ So when you design an inherited class, how do you decide which protected members to expose? Unfortunately, there is no magic bullet. The best you can do is think hard, make good tests, and then test them by writing subclasses. Protected members should be exposed as little as possible because each member represents a commitment to implementation details.

The only way to test . If you omit a critical protected member, trying to write a subclass will make the omission painfully obvious. Conversely, if you write several subclasses and none of them uses a protected member, you should make it private.

Constructors must not call overridable methods directly or indirectly . Violation of this rule will cause the program to fail. The parent class constructor runs before the child class constructor, so the overridden method in the child class is called before the child class constructor runs. If the overridden method relies on any initialization performed by the subclass constructor, this method will not function as expected.

A good way to solve this problem is to disable subclassing in classes that don't have a design and documentation that says you want to be able to subclass safely. There are two ways to disable subclassing. The easier of the two is to declare the class final. Another approach is to make all constructors private or package-level private, and add public static factories in place of the constructors .

Interfaces are better than abstract classes

Java has two mechanisms for defining types that allow multiple implementations: interfaces and abstract classes . Since default methods of interfaces were introduced in Java 8, both mechanisms allow providing implementations for certain instance methods. One major difference is that to implement a type defined by an abstract class, the class must be a subclass of the abstract class.

Interfaces are ideal for defining mixins. Generally speaking, a mixin is a class that, in addition to its "main type", can also be declared to provide some optional behavior. For example, Comparable is a type interface that allows a class to declare that its instances are ordered relative to other mutually comparable objects. Such an interface is called a type because it allows optional functionality to be "mixed" into the main functionality of the type. Abstract classes cannot be used to define mixin classes because they cannot be loaded into existing classes: a class cannot have more than one parent class, and there is no reasonable place in the class hierarchy to insert a type.

Interfaces allow the construction of non-hierarchical frameworks. Type hierarchies are great for organizing some things, but other things don't fall neatly into a strict hierarchy.

​ However, you can combine the advantages of interfaces and abstract classes by providing an abstract skeletal implementation class to be used with the interface . The interface defines the type and may provide some default methods, while the skeleton implementation class implements the remaining non-primitive interface methods on top of the original interface methods. Inheriting skeleton implementation requires most of the work to implement an interface. This is the template method design pattern. For example, the Collections Framework provides a framework implementation to accompany each of the major collection interfaces: AbstractCollection, AbstractSet, AbstractList, and AbstractMap.

In summary, an interface is usually the best way to define a type that allows multiple implementations . If you export an important interface, you should strongly consider providing a skeleton implementation class. Where possible, a skeleton implementation should be provided via a default method on the interface so that it is available to all implementers of the interface. That is, restrictions on interfaces often require skeleton implementation classes to take the form of abstract classes .

Design interfaces for future generations

Before Java 8, it was not possible to add methods to an interface without breaking the existing implementation. If a new method is added to an interface, the existing implementation will often be missing the method, causing a compile-time error. In Java 8, the default method construct was added to allow methods to be added to existing interfaces . But adding new methods to existing interfaces is fraught with risks.

The declaration of a default . Although adding default methods in Java adds methods to existing interfaces, there is no guarantee that these methods will be available in all existing implementations. Default methods are "injected" into the existing implementation without the knowledge or consent of the implementing class. Before Java 8, these implementations were written using the default interface, which never got any new methods.

Therefore, it is very important to test every new interface before releasing it. At least, you should prepare three different implementations. It is equally important to write multiple client programs that use instances of each new interface to perform various tasks. This will go a long way to ensuring that each interface meets all of its intended uses. These steps will allow you to discover flaws in your interface before release, but still fix them easily. Although some existing flaws may be fixed after the interface is released, don't count on this .

Interfaces are only used to define types

One type of interface that fails is the so-called constant interface. Such an interface does not contain any methods; it only contains static final properties, each outputting a constant. The class that uses these constants implements the interface to avoid the need to qualify the constant name with the class name.

The constant interface pattern is a poor use of interfaces . Classes use some constants internally, which are entirely implementation details. Implementing a constant interface causes this implementation detail to leak into the class's exported API. To the user of the class, it makes no sense for the class to implement a constant interface. In fact, it might even confuse them. Worse, it represents a promise: if the class is modified in a future version so that it no longer needs to use constants, it must still implement the interface to ensure binary compatibility. If a non-final class implements a constant interface, the namespaces of all its subclasses will be polluted by the constants in the interface .

If you want to export constants, there are several reasonable options. If a constant is closely related to an existing class or interface, it should be added to that class or interface. For example, all wrapper classes for numeric primitive types, such as Integer and Double, export the MIN_VALUE and MAX_VALUE constants. If constants can be treated as members of an enumeration type, they should be exported using the enumeration type. Otherwise, you should use a non-instantiable utility class to export the constants .

​ In short, interfaces can only be used to define types. They should not be used only to export constants

Prefer class hierarchy over tag classes

​ Sometimes you may encounter a class whose instances have two or more styles and contain a tag field that represents the style of the instance. For example, consider this class, which can represent a circle or rectangle:

// 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);        
        }    
    } 
}

​ Such tag classes have many disadvantages. Their messy boilerplate code includes enumeration declarations, label properties, and switch statements. It's less readable because multiple implementations are jumbled together in one class .

​ If you add a style, you must remember to add a case to each switch statement, otherwise the class will fail at runtime. Finally, an instance's data type provides no clue about style. In short, tag classes are verbose, error-prone, and inefficient .

Fortunately, object-oriented languages ​​like Java provide a better option for defining a single data type that can represent multiple styles of objects: subtyping . Tag classes are just a simple imitation of a class hierarchy.

To convert a tag class into a class hierarchy, first define an abstract class containing abstract methods whose behavior depends on the tag value. Next, define a concrete subclass of the root class for each type of the original label class.

// 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; 
    } 
}

Another advantage of class , thereby increasing flexibility and making compile-time type checking more efficient.

​ In short, label classes are rarely applicable. If you want to write a class with an explicit label attribute, consider whether the label attribute can be removed and the class replaced by the class hierarchy. When you encounter an existing class with a label attribute, consider refactoring it into a class hierarchy .

Give priority to static member classes

​A nested class is a class defined within another class. Nested classes should only exist within their enclosing class. If a nested class is useful in some other situation, then it should be a top-level class.

There are four types of nested classes: static member classes, non-static member classes, anonymous classes and partial classes . Except for the first one, the remaining three are called inner classes. This entry tells you when to use which type of nested class and why.

A common use of static , useful only when used with their outer classes. For example, consider an enumeration type that describes the operations supported by a calculator (Item 34). The Operation enumeration should be a public static member class of the Calculator class. Calculator clients can refer to operations using names such as Calculator.Operation.PLUS and Calculator.Operation.MINUS.

Syntactically, the only difference between static member classes and non-static member classes is that static member classes have the static modifier in their declaration. Each instance of a nonstatic member class is implicitly associated with the host instance of its containing class. In an instance method of a nonstatic member class, you can call a method on the host instance or obtain a reference to the host instance using a qualified constructor [JLS, 15.8.4]. If instances of a nested class can exist in isolation from instances of its host class, then the nested class must be a static member class: it is not possible to create an instance of a non-static member class without a host instance .

The association between a non- . Normally, the association is automatically established by calling the non-static member class constructor in the instance method of the host class.

If you declare a member class that does not require access to the host instance, put the static modifier on its declaration to make it a static member class rather than a non-static member class . If you omit this modifier, each instance will have a hidden external reference to its host instance. As mentioned before, storing this reference takes time and space. What's more serious is that it will still reside in memory even if the host class meets the conditions for garbage collection (Item 7). The resulting memory leak can be catastrophic. Because references are invisible, they are often difficult to detect.

Define a single top-level class in a single source file

Although the Java compiler allows multiple top-level classes to be defined in a single source file, there is no benefit to doing so and there are significant risks. The risk arises from defining multiple top-level classes in a source file making it possible to provide multiple definitions for a class. Which definition is used is affected by the order in which the source files are passed to the compiler.

// 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"; 
} 

If you are trying to put multiple top-level classes into a single source file, consider using static member classes (Item 24) as an alternative to splitting the class into separate source files. If these classes are dependent on another class, it is usually a better option to turn them into static member classes, as it improves readability and can reduce the accessibility of the class by declaring them private .

Don't use raw types directly

​ A class or interface whose declaration has one or more type parameters (type parameters) is called a generic class or generic interface. For example, the List interface has a single type parameter E, which represents its element type. The full name of the interface is List (pronounced "E" for list), but people often call it List. Generic classes and interfaces are collectively called generic types.

Each generic defines a raw type, which is the name of the generic type without any type parameters [JLS, 4.8]. For example, the primitive type corresponding to List is List. Primitive types behave as if all generic type information had been cleared from the type declaration. They exist primarily for compatibility with pre-generics code .

It is legal to use primitive types (generics without type parameters), but you should not do it. If you use primitive types, you lose all the safety and expressive advantages of generics . Why did language designers allow primitive types in the first place, given that you shouldn't use them? The answer is for compatibility.

Eliminate unchecked warnings

When programming with generics, you see a number of compiler warnings: unchecked cast warnings, unchecked method call warnings, unchecked parameterized variable-length type warnings, and unchecked conversion warnings. The more experience you gain with generics, the fewer warnings you will get, but don't expect newly written code to compile cleanly.

When you get a warning that you need to think further, persevere! Eliminate every unchecked warning possible . If you eliminate all warnings, you can rest assured that your code is type-safe, which is a very good thing . This means you won't get a ClassCastException at runtime, and increases your confidence that your program will behave as you intended.

If you can't suppress a warning, but you can prove that the code that caused the warning is type-safe, then (and only then) suppress the warning with the @SuppressWarnings("unchecked") annotation . If you suppress warnings without first proving that your code is type-safe, you give yourself a false sense of security. The code may compile without any warnings, but it may still throw a ClassCastException at run time.

The SuppressWarnings annotation can be used on any declaration, from a single local variable declaration to an entire class. Always use the SuppressWarnings annotation in the smallest possible scope . Usually this is a variable declaration or a very short method or constructor. Never use the SuppressWarnings annotation on an entire class. Doing so may obscure important warnings.

Whenever using the @SuppressWarnings(“unchecked”) annotation, add a comment explaining why it is safe. This will help others understand the code, and more importantly, it will reduce the possibility of someone modifying the code to make the computation unsafe.

Lists are better than arrays

Arrays differ from generics in two important ways.

  1. Arrays are covariant . This means that if Sub is a subtype of Super, then the array type Sub[] is a subtype of the array type Super[].
  2. Generics are invariant : for any two different types, Type1 and Type2, List is neither a subtype nor a supertype of List.

You might think this means generics are inadequate, but it could be argued that arrays are deficient. This code is legal:

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

​ But this one is not:

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

Either way, you can't put a String type into a Long type container, but with an array, you will find an error at runtime; with a list, the error can be found at compile time . Of course, you'd rather find the error at compile time.

The second major difference between arrays and generics is that arrays are reified.

  1. Arrays know and enforce their element types at runtime . As mentioned earlier, if you try to put a String into a Long array, you get an ArrayStoreException.
  2. Generics are implemented through erasure. This means that they only enforce type constraints at compile time, and discard (or erase) their element type information at runtime . Erasure ensures a smooth transition to generics in Java 5 by allowing generic types to freely interoperate with legacy code that does not use generics (Item 26).

In summary, arrays and generics have very different type rules. Arrays are covariant and reified; generics are immutable and type-erased. Therefore, arrays provide run-time type safety but not compile-time type safety, and vice versa . Generally speaking, arrays and generics don't mix well . If you find yourself mixing them together and getting compile-time errors or warnings, your first impulse should be to replace the array with a list.

Prioritize generics

Generic types are safer and easier to use than types that require casting in client code . When you design new types, make sure they can be used without such casts. This usually means making the type generic. If you have any existing types that should be generic but aren't, make them generic. This makes usage easier for new users of these types without breaking existing clients.

Prefer using generic methods

In summary, like generic types, generic methods are safer and easier to use than methods that require the client to perform explicit casts on input parameters and return values. Like types, you should ensure that your methods can be used without casting, which usually means they are generic. Existing methods should be genericized and their use requires casting . This makes it easier for new users to use without breaking existing clients.

Use qualified wildcards to increase flexibility

For maximum flexibility, use wildcard types for input parameters that represent producers or consumers. If an input parameter is both a producer and a consumer, then wildcard types do you no good: you need an exact type match, which is the case without any wildcards.

​ Here's a mnemonic to help you remember which wildcard type to use: PECS stands for: producer-extends, consumer-super. In other words, if a parameterized type represents a T producer, use <? extends T>; if it represents a T consumer, use <? super T>.

In summary, using wildcard types in your API, although tricky, makes the API more flexible . If writing a class library that will be widely used, the correct use of wildcard types should be considered mandatory. Remember the basic rule: producer-extends, consumer-super (PECS). Also remember that all Comparables and Comparators are consumers.

Proper combination of generics and variadic parameters

Why is it legal to declare a method with generic variadic parameters, when explicitly creating a generic array is illegal? The answer is that methods with variadic arguments of generic or parameterized types can be very useful in practice , so language designers choose to live with this inconsistency. In fact, the Java class library exports several such methods, including Arrays.asList(T… a), Collections.addAll(Collection<? super T> c, T… elements), EnumSet.of(E first, E… rest) . Unlike the dangerous methods shown earlier, these library methods are type-safe.

In Java 7, the @SafeVarargs annotation was added to the platform to allow authors of methods with generic variadic arguments to automatically suppress client warnings . In essence, the @SafeVarargs annotation constitutes the author's commitment to a type-safe method. In exchange for this promise, the compiler agrees not to warn users about calling potentially unsafe methods.

Be careful not to annotate a method with the @SafeVarargs annotation unless it is actually safe. So what needs to be done to ensure this? Recall that when a method is called a generic array is created to hold the variadic arguments. A method is safe if it does not store anything in the array (it overwrites the parameters) and does not allow references to the array to be escaped (which allows untrusted code to access the array). In other words, if the variadic array is used only to pass a variable number of arguments to the method from the caller—which is, after all, the purpose of variadic arguments—then the method is safe.

In summary, varargs and generics don't interact well because the varargs mechanism is a fragile abstraction built on top of arrays, and arrays have different type rules than generics. Although generic variadic parameters are not type-safe, they are legal. If you choose to write a method using generic (or parameterized) varargs, first ensure that the method is type-safe, and then annotate it with the @SafeVarargs annotation to avoid unpleasant use.

Prioritize type-safe heterogeneous containers

Common uses of generics include collections, such as Set and Map<K,V>, and single-element containers, such as ThreadLocal and AtomicReference. In all these uses it is a parameterized container. This limits each container to a fixed number of type parameters. Usually this is what you want. A Set has a single type parameter, representing its element type; a Map has two, representing its key and value types; and so on.

Sometimes, however, you need more flexibility. For example, a database row can have any number of columns, and it's nice to be able to access them in a type-safe way. Fortunately, there is an easy way to achieve this effect. The idea is to parameterize keys instead of containers. The parameterized key is then submitted to the container to insert or retrieve a value. The generic type system is used to ensure that the type of a value is consistent with its key .

As a simple example of this approach, consider a Favorites class that allows its clients to save and retrieve favorite instances of any number of types. Class objects of this type will act as part of the parameterized key . The reason is that this Class class is generic. The type of class is not simply Class literally, but Class. For example, String.class is of type Class and Integer.class is of type Class. When a literal class is passed in a method to pass compile-time and run-time type information, it is called a type token.

Sample code:

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

In short, the common usage of generic APIs (taking the collection API as an example) limits each container to a fixed number of type parameters. You can work around this limitation by putting the type parameter on the key rather than the container . You can use Class objects as keys for this type-safe heterogeneous container. Class objects used in this way are called type tokens. Custom key types can also be used. For example, you can have a DatabaseRow type that represents a database row (container) and a generic type Column as its key.

Use enumeration types instead of integer constants

Before enumeration types were added to the language, a common pattern for representing enumeration types was to declare a set of constants named ints, one for each member of the type:

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

This technique, known as int enumeration mode, has many disadvantages. It provides no type safety, nor does it provide any expressiveness. There is no easy way to convert an int enum constant to a printable string. If you print such a constant or display it from the debugger, all you see is a number, which is not very useful. There is no reliable way to iterate over all int enumeration constants in a group, or even get the size of an int enumeration group .

Fortunately, Java provides an alternative that avoids all the disadvantages of the int and String enum patterns and provides many additional benefits.

The basic idea behind Java enumeration types is simple: they are classes that export an instance for each enumeration constant through a public static final property. Since there are no accessible constructors, enumeration types are actually final. Since a client can neither create an instance of an enumeration type nor inherit from it, there cannot be any instances other than the declared enumeration constants . In other words, enumerated types are instance-controlled (page 6). They are generics of singletons (Item 3), which are basically single-element enumerations.

Enumerations provide compile-time type safety . Attempting to pass a value of the wrong type will result in a compile-time error because an attempt will be made to assign an expression of one enumeration type to a variable of another type, or to use the == operator to compare values ​​of different enumeration types.

There is a better way to associate different behavior with each enum constant : declare an abstract apply method in the enum type and override it with a concrete method for each constant in the constant-specific class body . This approach is called a constant-specific method implementation:

// 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); 
} 

If you add new constants to the operation in the above example, it is unlikely that you will forget to provide the apply method because it follows each constant declaration. In case you forget, the compiler will remind you because abstract methods in enumeration types must be overridden by concrete methods in all constants .

In short, the advantages of enumeration types over int constants are convincing. Enumerations are more readable, safer, and more powerful. Many enumerations do not require explicit constructors or members, but others benefit from associating data with each constant and providing methods whose behavior is affected by this data. Using a single method to associate multiple behaviors reduces enumeration. In this relatively rare case, prefer to use constant-specific methods to enumerate your own values. If some (but not all) enumeration constants share common behavior, consider the strategic enumeration pattern.

Use instance properties instead of ordinal numbers

Never derive the value associated with an enumeration from its ordinal number; save it in an instance property:

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; 
    } 
} 

Use EnumSet instead of bit properties

​ Below is the previous example of using enumerations and enumeration collections instead of bit properties. It's shorter, clearer and safer:

// 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) {
    
     ... } 
}

In summary, just because the enumeration type will be used in a collection, there is no reason to represent it with a bit attribute. The EnumSet class combines the simplicity and performance of bit properties with all the advantages of the enumeration type described in Item 34. One real drawback of EnumSet is that it doesn't create an immutable EnumSet like in Java 9, but that may be remedied in an upcoming release. At the same time, you can use Collections.unmodifiableSet to encapsulate an EnumSet, but simplicity and performance will be affected

Use EnumMap instead of ordinal index

In short, using ordinal numbers to index arrays is inappropriate: use EnumMap instead. If the relationship you represent is multi-dimensional, use EnumMap<…, EnumMap<…>>. Application programmers should rarely use Enum.ordinal (Item 35), and if used, it is a special case of the general principle.

Usage example:

// 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     
    } 
} 

Implement extensible enumerations using interfaces

Most of the time, enum extensibility is a bad idea. Confusingly, elements of an extended type are instances of the base type and vice versa. There is no good way to enumerate all elements of a base type and its extensions. Finally, scalability complicates many aspects of design and implementation.

That said, there is at least one compelling use case for extensible enumeration types, and that is operation codes , also known as opcodes. Opcodes are enumerated types whose elements represent operations on some machine, such as the Operation type in Item 34, which represents functions on a simple calculator. Sometimes it is necessary to let users of the API provide their own operations, effectively extending the set of operations provided by the 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;     
    } 
} 

In short, although you cannot write an extensible enumeration type, you can write an interface to match the basic enumeration type that implements the interface to simulate it. This allows clients to write their own enumerations (or other types) that implement the interface. If the API is written in terms of interfaces, these enum type instances can be used wherever base enum type instances are used.

Annotations are better than naming

In the past , naming patterns were often used to indicate that certain program elements required special handling by a tool or framework . For example, prior to version 4, the JUnit testing framework required its users to specify test methods by starting the name with test[Beck04]. This technique is effective, but it has several big drawbacks. First, a typo causes a failure but does not prompt. For example, suppose you accidentally named the test method tsetSafetyOverride instead of testSafetyOverride. JUnit 3 won't throw errors, but it also won't execute the tests, leading to a false sense of security.

A second disadvantage of naming patterns is the inability to ensure that they are used only for appropriate program elements.

A third disadvantage of naming patterns is that they do not provide a good way to associate parameter values ​​with program elements. For example, suppose you want to support a class of tests that only succeed when a specific exception is thrown. The exception type is basically a parameter of the test. You could use some elaborate naming pattern to encode the exception type name into the test method name, but this gets ugly and brittle.

The test framework in this project is just a demo, but it clearly demonstrates the superiority of annotations over named patterns, and it only paints the appearance of what you can do with them. If you are writing a tool that requires programmers to add information to the source code, define appropriate annotation types. There is no reason to use naming patterns when you can use annotations instead .

This means that most programmers, except for specific developers (toolsmiths), do not need to define annotation types. But all programmers should use the predefined annotation types provided by Java (Items 40, 27). Also, consider using annotations provided by your IDE or static analysis tool. These annotations can improve the quality of diagnostic information provided by these tools. Note, however, that these annotations are not yet standardized, so some additional work may be required if switching tools or standards emerge.

Always use the Override annotation

Therefore, you should use the Override annotation on every method declaration that you think will override the parent class declaration . There is a small exception to this rule. If you are writing a class that is not marked abstract and you are sure that it overrides an abstract method in its parent class, you do not need to put an Override annotation on the method. In a class that is not declared abstract, the compiler will issue an error message if the abstract parent class method cannot be overridden. However, you may want to focus on all methods in your class that override methods of the parent class, in which case you should always annotate these methods as well. Most IDEs can be set up to automatically insert an Override annotation when an overridden method is selected.

The Override annotation can be used to override method declarations from interfaces and classes. With the advent of default methods, it is a good practice to use Override on the concrete implementation of the interface method to ensure that the signature is correct. If you know that an interface does not have a default method, you can choose to ignore the Override annotation on the specific implementation of the interface method to reduce confusion.

Define types using marker interfaces

A marker interface does not contain method declarations, but simply specifies (or "marks") a class that implements an interface with certain properties . For example, consider the Serializable interface (Chapter 12). By implementing this interface, a class indicates that its instances can write to (or "serialize") an ObjectOutputStream.

You may have heard of the marking annotation (Item 39) marking an interface as deprecated. This assertion is incorrect. Marker interfaces have two advantages over marker annotations:

  1. First, the marker interface defines a type that is implemented by instances of the marker class; marker annotations do not . The presence of a marker interface type allows errors to be caught at compile time, whereas if marker annotations are used, errors cannot be caught until runtime;
  2. Another advantage of the marker interface for marker annotations is that they can be targeted more precisely . If an annotation type is declared using the target ElementType.TYPE, it can be applied to any class or interface. Suppose there is a tag that only applies to implementations of a specific interface. If it is defined as a marker interface, you can extend the unique interface to which it applies, ensuring that all marker types are also subtypes of the unique interface to which it applies;

In summary, both marker interfaces and marker annotations have their uses. If you want to define a type without any associated new methods, a tagged interface is the way to go. Markup annotations are the right choice if you want to mark program elements other than classes and interfaces, or if you want to conform markup into a framework that already makes heavy use of annotation types. If you find yourself writing a tagged annotation type targeting ElementType.TYPE, take a moment to determine whether it should be an annotation type and whether a tagged interface would be more appropriate .

Lambda expressions are better than anonymous classes

Unlike methods and classes, lambdas have no names and no documentation; if the calculation is not self-explanatory, or exceeds a few lines, do not put it in a lambda expression . One line of code is ideal for a lambda, and three lines of code is a reasonably large value. Violating this rule may seriously impair the readability of your program. If a lambda is long or difficult to read, either find a way to simplify it or refactor your program to eliminate it.

Likewise, you might think that anonymous classes are obsolete in the era of lambdas. This is closer to the truth, but there are some things you can do with anonymous classes that you can't do with lambdas. Lambda is limited to functional interfaces. If you want to create an instance of an abstract class, you can do so using an anonymous class, but not a lambda. Likewise, you can use anonymous classes to create interface instances with multiple abstract methods . Finally, the lambda cannot obtain a reference to itself. In lambdas, the this keyword refers to the enclosing instance, which is usually what you want. In an anonymous class, the this keyword refers to the anonymous class instance. If you need to access a function object from within it, you must use an anonymous class.

In summary, starting from Java 8, lambda is by far the best way to represent small function objects. Do not use anonymous classes as function objects unless you must create instances of non-functional interface types .

Method references are better than lambda expressions

The main advantage of lambda over anonymous classes is that it is more concise. Java provides a way to generate function objects, which is more concise than lambda: method references.

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

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

They also give you a consequence if the lambda becomes too long or complex: you can extract the code from the lambda into a new method and replace the lambda with a reference to that method . You can give this method a good name and document it.

Method references often provide a cleaner alternative to lambdas. If method references seem shorter and clearer, use them; otherwise, stick with lambdas .

Prefer standard functional interfaces

The java.util.function package provides a large number of standard functional interfaces for you to use. If one of the standard functional interfaces does the job, you should generally use it in preference to a purpose-built functional interface . This will make your API easier to learn by reducing its unnecessary concepts, and will provide important interoperability benefits because many standard functional interfaces provide useful default methods. For example, the Predicate interface provides methods for combining judgments. In our LinkedHashMap example, the standard BiPredicate<Map<K,V>, Map.Entry<K,V>> interface should take precedence over the use of the custom EldestEntryRemovalFunction interface.

There are 43 interfaces in java.util.Function. You can't expect to remember them all, but if you remember the six basic interfaces, you can derive the rest when you need them. The basic interface operates on object reference types.

  1. The Operator interface represents a method whose result and parameter types are of the same type.
  2. The Predicate interface means that its method accepts one parameter and returns a Boolean value.
  3. Function interface represents methods whose parameters and return types are different.
  4. The Supplier interface represents a method that takes no parameters and returns a value (or "supply").
  5. Consumer means that the method accepts a parameter and does not return anything, essentially using its parameter.

The six basic functional interfaces are summarized as follows:

[External link image transfer failed. The source site may have an anti-leeching mechanism. It is recommended to save the image and upload it directly (img-lEnJWnyc-1685157110629) (C:\Users\lixuewen\AppData\Roaming\Typora\typora-user-images\ image-20230522140251781.png)]

​ In short, Java now has lambda expressions, so you must consider lambda expressions to design your API. Accepts functional interface types on input and returns them on output. Generally speaking, it is best to use the standard interfaces provided in java.util.function.Function, but please note that in relatively rare cases it is best to write your own functional interface.

Use Streams wisely and judiciously

The Stream API was added in Java 8 to simplify the task of performing batch operations sequentially or in parallel. The API provides two key abstractions: streams, which represent finite or infinite sequences of data elements, and stream pipelines, which represent multi-level computations on these elements. Elements in a Stream can come from anywhere. Common sources include collections, arrays, files, regular expression pattern matchers, pseudo-random number generators, and other streams.

A stream pipeline consists of zero or more intermediate operations on a source stream and one final operation . Each intermediate operation transforms the stream in some way, such as mapping each element to a function on that element or filtering out all elements that don't meet some condition. Intermediate operations all transform one stream into another stream, whose element types may or may not be the same as the input stream. Finalization A final computation resulting from an intermediate operation performed on a stream, such as storing its elements in a collection, returning an element, or printing all its elements .

​Enhance readability by . Using helper methods is more important for readability in streaming pipelines than in iterative code because pipelines lack explicit type information and named temporary variables. (Extracted functions to simplify code)

When you start using streams, you may feel the urge to convert all your loops to streams, but resist this urge. While this is possible, it may harm the readability and maintainability of your codebase. Typically, moderately complex tasks work well using some combination of streams and iteration. So, refactor existing code to use streams, and only use them in new code when it makes sense .

​ In short, some tasks are best accomplished using flows, and some tasks are best accomplished using iterations. Combining these two methods can accomplish many tasks well. There are no hard and fast rules for choosing which method to use for a task, but there are some useful heuristics. In many cases it will be clear which method to use; in some cases it won't be. If you're not sure whether a task is better accomplished through flow or iteration, try both methods and see which one works better.

Prioritize side-effect-free functions in streams

Java programmers know how to use for-each loops, and the forEach finalization operation is similar. But the forEach operation is one of the least powerful operations in terminal operations, and it is also an unfriendly stream operation. It is explicitly iterative and therefore not suitable for parallelization. The forEach operation should only be used to report the results of a stream calculation, not to perform the calculation . Sometimes it makes sense to use forEach for other purposes, such as adding the results of a stream calculation to a pre-existing collection.

The improved code uses collectors, a new concept that must be learned when working with streams. Collectors' API is daunting: it has 39 methods, some of which have as many as five type parameters. The good news is that you can get most of the benefits from this API without having to delve into its full complexity. For starters, you can ignore the collector interface and think of the collector as an opaque object that encapsulates a reduction strategy. A collector that collects the elements of a stream into a real collection is very simple. There are three such collectors: toList(), toSet() toCollection(collectionFactory). They return sets, lists, and programmer-specified collection types respectively .

In short, the essence of programming flow pipeline is a function object without side effects. This applies to all many function objects passed to streams and related objects. The final operation forEach should only be used to report the results of calculations performed by the stream, not to perform the calculations. In order to use streams correctly, you must understand collectors. The important collector factories are toList, toSet, toMap, groupingBy and join.

Prefer Collection over Stream as return type

The Collection interface is a subtype of Iterable and has a stream method, so it provides both iteration and stream access. Therefore, Collection or an appropriate subtype is usually the best return type for public sequence return methods . Arrays also provide simple iteration and stream access using the Arrays.asList and Stream.of methods. If the returned sequence is small enough to easily fit in memory, it is better to return a standard collection implementation such as an ArrayList or HashSet. But don't store a large sequence in memory just to return it as a collection.

In summary, when writing methods that return sequences of elements, keep in mind that some users may want to process them as a stream, while others may want to process them iteratively. Try to accommodate both groups. If returning a collection is feasible, do so. If you already have the elements in the collection, or the number of elements in the sequence is small enough to create a new element, then return a standard collection, such as an ArrayList. Otherwise, consider implementing a custom set, as we did for the power set program. If returning a collection is not feasible, return a stream or iterable, whichever seems more natural. If in a future version of Java the Stream interface declaration is modified to inherit from Iterable, then you should feel free to return streams as they will allow streaming and iterative processing.

Use stream parallelism with caution

Don’t indiscriminately parallelize stream pipelines . The performance consequences can be catastrophic.

In general, performance gains from parallelism are best on streams of ArrayList, HashMap, HashSet, and ConcurrentHashMap instances, arrays, ranges of type int, and ranges of type long. What these data structures have in common is that they can be split accurately and cheaply into subroutines of any size, making it easy to divide work among parallel threads. The abstraction used by the teardrop library to perform this task is a spliterator, which is returned by the spliterator methods on Streams and Iterables.

In summary, don't even try to parallelize a streaming pipeline unless you have good reason to believe it will keep the computation correct and increase its speed . The cost of improperly parallelizing a stream can be program failure or performance disaster. If you think parallelism is reasonable, then make sure your code behaves correctly when run in parallel, and do careful performance measurements under real-world conditions. If your code is correct and these experiments confirm your suspicions about performance improvements, then and only then can you parallelize the flow in production code.

Check parameter validity

If an invalid parameter value is passed to a method, and the method checks its parameters before executing, it throws an appropriate exception and then fails quickly and cleanly . If the method fails to check its parameters, something might happen. During processing, the method may throw confusing exceptions. Even worse, the method returns normally but silently computes the wrong result. What's worse is that the method returns normally but leaves an object in a corrupted state, causing an error at some unrelated point in the code at some undetermined time in the future. In other words, failure to validate parameters may result in a violation of failure atomicity.

Constructors are a special case of this principle, and you should check the validity of parameters you want to store for later use . Checking the validity of constructor parameters is important to prevent constructed objects from violating class invariants.

You should explicitly check method parameters before performing calculations, but there are exceptions to this rule. An important exception is where the validity check is expensive or impractical, and the check is performed implicitly as part of the computation.

In summary, every time you write a method or constructor, you should consider what restrictions exist on its parameters . These restrictions should be noted and enforced using explicit checks at the beginning of the method body. It’s important to get into the habit of doing this. The small amount of work it requires will be rewarded when the first validity check fails.

Make defensive copies when necessary

Even in a safe language, isolation from other classes is not possible without some effort. Programs must be written defensively, assuming that clients of a class strive to destroy the class's invariants . This becomes more and more true as people try harder to break the security of the system, but more often than not, your classes will have to deal with unexpected behavior due to honest mistakes by well-intentioned programmers. Regardless, it's worth spending time writing classes that remain robust despite poor client behavior.

Where possible, you should use immutable objects as components of objects so that you don’t have to worry about defensive copies (Item 17). In our Period example, use Instant (or LocalDateTime or ZonedDateTime) unless you are using a pre-Java 8 version. If you are using an earlier version, one option is to store the base type long returned by Date.getTime() instead of the Date reference.

There may be a performance penalty associated with defensive copying, and it is not always justified. If a class trusts its callers not to modify internal components, perhaps because the class and its clients are part of the same package, then it may not need defensive copying. In these cases, the class documentation should clearly state that the caller cannot modify the affected parameters or returns.

In summary, if a class has mutable components that are obtained or returned from its clients, then the class must defensively copy these components. If the cost of copying is too high, and the class trusts its clients not to modify components inappropriately, defensive copying can be replaced with a document that outlines the client's responsibility not to modify affected components.

Design method signatures carefully

​Choose method names carefully . Names should always adhere to standard naming conventions (Item 68). Your main goal should be to choose a name that is consistent with other names in the same package and easy to understand. The second step should be to choose a name that is consistent with the broader consensus. Avoid using long method names;

Don’t overdo it by providing convenience methods . Every method should be done "to the best of its ability". Too many methods make a class difficult to learn, use, document, test, and maintain. This is especially true for interfaces, where too many methods complicate the work of implementers and users;

​Avoid overly long parameter lists . Aim for four parameters or less. Most programmers cannot remember longer parameter lists.

For parameter types, prefer interfaces over classes (Item 64). If there is a suitable interface that defines a parameter, then use it to support a class that implements that interface. For example, there is no reason to use a HashMap as an input parameter when writing a method. Instead, use a Map as a parameter, which allows passing in a HashMap, a TreeMap, a ConcurrentHashMap, a submap of a TreeMap, or any Map implementation that has not yet been written.

​Use two-element enumeration types in preference to , unless the meaning of the Boolean parameter is clear in the method name. Enum types make code easier to read and write. Plus, they make it easy to add more options later .

Use overloading wisely and judiciously

​Sample code:

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));     
    } 
} 

You might expect this program to print Set, then List and Unknown Collection strings, but it doesn't. Instead, the Unknown Collection string is printed three times. Why is this happening? Because the classify method is overloaded, which overloaded method to call is selected at compile time . For all three iterations of the loop, the compile-time type of the argument is the same: Collection<?> .

​ Because the selection between overloaded methods is static, while the selection between overridden methods is dynamic . When an overridden method is called, the compile-time type of the object has no effect on which method is executed ; the "most specific" overridden method is always executed. Compare this to overloading, where the runtime type of the object has no effect on which overload is performed; the selection is done at compile time , based entirely on the compile-time type of the parameter.

A safe and conservative strategy is to never export two overloads with the same number of arguments . If a method uses variadic parameters, the conservative strategy is not to overload it at all. If these restrictions are followed, the programmer has no doubt which overloads apply to any set of actual parameters. These restrictions are not very onerous as it is always possible to give methods different names instead of overloading them.

In summary, just because you can overload a method doesn't mean you should. In general, it's best to avoid overloading methods with multiple signatures that have the same number of parameters . In some cases, especially where constructors are involved, it may not be possible to follow this advice. In these cases, you should at least avoid passing the same set of parameters to different overloads by adding casts. If this cannot be avoided, for example, because an existing class is being retrofitted to implement a new interface, then you should ensure that all overloads behave the same when passing the same parameters. Without this, the programmer will have a difficult time using an overloaded method or constructor effectively or understanding why it doesn't work.

Use variadic parameters wisely and judiciously

Variable parameter methods, formally known as variable arity methods [JLS, 8.4.1], accept zero or more parameters of a specified type. The variadic mechanism first creates an array whose size is the number of arguments passed at the call site, then puts the argument values ​​into the array, and finally passes the array to the method.

Be careful when using variadic arguments in performance-critical situations. Each call to a variadic method results in array allocation and initialization . If you empirically determine that you can't afford this cost, but still need the flexibility of variadic parameters, then there is a model that allows you to have the best of both worlds. Assuming that you have determined that 95% of calls are to methods with three or fewer arguments, declare five overloads of that method. Each overloaded method contains 0 to 3 normal parameters. When the number of parameters exceeds 3, a variadic method is used.

In summary, variadic arguments are useful when you need to define a method with a variable number of arguments. Prepend any required arguments before using variadic arguments, and be aware of the performance consequences of using variadic arguments.

Return an empty array or collection, do not return null

It is sometimes argued that a null return value is preferable to an empty collection or array because it avoids the overhead of allocating an empty container. This argument fails on two counts. First, you shouldn't worry about performance at this level unless measurements show that the allocation in question is the real cause of the performance issue. Second, empty collections and arrays can be returned without allocating them. If there is evidence that allocating an empty collection harms performance, the allocation can be avoided by repeatedly returning the same immutable empty collection, since immutable objects can be freely shared .

In summary, never return null in place of an empty array or collection. It makes your API harder to use, more error-prone, and offers no performance benefits.

Return Optional wisely and prudently

Prior to Java 8, there were two approaches, neither of which were perfect, when writing a method that could not return any value in a specific case:

  1. Either throw an exception, but exceptions should be reserved for exception conditions, and throwing exceptions is expensive because the entire stack trace is captured when the exception is created. Returning null does not have these disadvantages, but it has its own drawbacks.
  2. Either return null (assuming the return type is an object or a reference type); if the method returns null, the client must include special case code to handle the possibility of a null return, unless the programmer can prove that a null return is impossible. If the client neglects to check for null returns and stores the null return value in a data structure, there is a possibility that a NullPointerException will be thrown at some point in the future at a code location that is not relevant to this problem.

In Java 8, there is a third way to write methods that may not return any value. The Optional class represents an immutable container that can contain a non-null reference to T or nothing at all. An Optional that contains no content is called empty. A non-empty Optional containing a value is said to be present. Optional is essentially an immutable collection that can hold at most one element. Optional does not implement the Collection interface, but in principle it is possible.

The Optional.of(value) method accepts a value that may be null. If null is passed in, an empty Optional is returned. Never return a null value from a method that returns an Optional: it defeats the purpose of Optional's design .

In summary, if you find that the method you have written cannot always return a value, and you think it is important for the user of the method to consider this possibility every time it is called, then perhaps you should return an Optional method. However, you should be aware that returning Optional has real performance consequences; for performance-critical methods, it is best to return null or throw an exception.

Write documentation comments for all exposed API elements

If an API is to be usable, it must be documented. Traditionally, API documentation was generated manually, and keeping documentation in sync with code was a chore. The Java programming environment simplifies this task using the Javadoc utility. Javadoc uses specially formatted documentation comments (often called doc comments) to automatically generate API documentation from source code.

To properly document an API, each exported class, interface, constructor, method, and property declaration must be preceded by a documentation comment.

There is a general principle that documentation comments should be readable in both source code and generated documentation.

To avoid confusion, two members or constructors in a class or interface should not have the same summary description . Pay special attention to overloaded methods, for which it is often natural to use the same first sentence (but not acceptable in documentation comments)

​ In short, documentation comments are the best and most effective way to document APIs. Their use should be considered required for all exported API elements. Use a consistent style that adheres to standard conventions. Remember that arbitrary HTML is allowed in documentation comments, but the HTML's metacharacters must be escaped.

Minimize the scope of local variables

This item is similar in nature to "Minimize the accessibility of classes and members." By minimizing the scope of local variables, you can improve the readability and maintainability of your code and reduce the possibility of errors .

A powerful technique for . If variables are declared before they are used, it becomes even more confusing - and it adds another distraction for the reader trying to understand the program. By the time the variable is used, the reader may not remember the variable's type or initial value.

Declaring a local variable too early may result in its scope not only starting too early but also ending too late. The scope of a local variable extends from the location where it is declared to the end of the enclosing block. If a variable is declared outside the enclosing block in which it is used, it remains visible after the program exits the enclosing block. If a variable is accidentally used before or after its intended use area, the consequences can be catastrophic.

Nearly every local variable declaration should contain an initializer . If there is not yet enough information to reasonably initialize a variable, then declaration should be postponed until it is deemed possible to do so. One exception to this rule is the try-catch statement.

The ultimate technique for minimizing . If you combine two activities in the same method, local variables related to one activity may be in the scope of the code that executes the other activity. To prevent this from happening, just split the method into two: one method for each behavior.

The for-each loop is better than the traditional for loop

The for-each loop (officially called the "enhanced for statement") solves all these problems. It eliminates clutter and chances for errors by hiding iterators or index variables. However, there are three common situations where you cannot use a for-each loop separately:

  1. Destructive filtering - If you need to traverse the collection and remove a specified selection, you need to use an explicit iterator so that its remove method can be called. Explicit traversal can usually be avoided by using the removeIf method in the Collection class added in Java 8.
  2. Conversion - If you need to iterate through a list or array and replace some or all of the values ​​of its elements, then you need a list iterator or array index to replace the value of the element
  3. Parallel iteration - If you need to iterate over multiple collections in parallel, you need to explicitly control the iterators or index variables so that all of them can be done simultaneously (as inadvertently demonstrated in the erroneous card and dice example above that way)

If you find yourself in any of these situations, use a traditional for loop and be wary of the pitfalls mentioned in this entry.

In summary, the for-each loop offers compelling advantages over traditional for loops in terms of clarity, flexibility, and error prevention, without the performance penalty. Whenever possible, use for-each loops in preference to for loops.

Understand and use libraries

By using the standard library, you leverage the knowledge of the experts who wrote it and the experience of those who have used it before .

Starting from Java 7, Random should no longer be used. In most cases, the random number generator of choice is now ThreadLocalRandom. It produces higher quality random numbers and is very fast. On my machine, it's 3.6 times faster than Random. For fork connection pools and parallel streams, use SplittableRandom.

The second benefit of using these libraries is that you don't have to waste time writing specialized solutions to problems that are not relevant to your work . If you're like most programmers, you'd rather spend your time working on your application than on the underlying pipeline.

The third advantage of using standard libraries is that their performance improves over time without any effort on your part . Since many people use them and they are used in industry-standard benchmarks, the organizations that provide these libraries have a strong incentive to make them run faster.

Considering all these advantages, it seems logical to use library tools instead of choosing specialized implementations, but many programmers do not do this. why not? Maybe they don't know the library exists. With each major release, many features are added to the library, and it pays to learn about these additions .

​ Sometimes, library tools may not meet your needs. The more specialized your needs are, the more likely this is to happen. Although your first thought should be to use these libraries, if you already understand the functionality they provide in some areas, and these functionality does not meet your needs, then you can use another implementation. There are always holes in the functionality provided by any limited set of libraries. If you can't find what you need in the Java platform libraries, your next option should be to find a high-quality third-party library, such as Google's excellent open source Guava library [Guava]. If you can't find the functionality you need in any appropriate library, you may have no choice but to implement it yourself .

All in all, don’t reinvent the wheel. If you need to do something that seems fairly common, there's probably already a tool in the library that does what you want. If it's there, use it; if you don't know it, check it out. In general, library code is likely to be better than code you would write yourself, and may be improved over time. This does not reflect your abilities as a programmer. Economies of scale dictate that library code gets far more attention than most developers can afford for the same functionality.

To be precise, avoid using float and double

The float and double types are mainly used in scientific calculations and engineering calculations. They perform binary floating point operations, an algorithm carefully designed to quickly provide accurate approximations over a wide range. However, they do not provide accurate results and should not be used where precise results are required. The float and double types are particularly unsuitable for monetary calculations because it is impossible to represent 0.1 (or any negative power of 10) exactly as a float or double.

In summary, do not use float or double types for any calculation that requires an exact answer. If you want the system to handle decimal points and don't mind the inconvenience and cost of not using a primitive type, use BigDecimal . Another benefit of using BigDecimal is that it gives you complete control over rounding and you can choose from eight rounding modes when performing operations that require rounding. This is very convenient if you perform business calculations using legal rounding behavior. If performance is important, you don't mind handling the decimal point yourself, and the number is not too large, use an int or long. If the value does not exceed 9 decimal places, you can use int; if it does not exceed 18 decimal places, you can use long. If the quantity is likely to exceed 18 digits, use BigDecimal.

Basic data types are better than wrapper classes

There are three main differences between basic types and wrapped types. First, primitive types only have their values, while wrapper types have identities that are distinct from their values. In other words, two wrapper type instances can have the same value and different identities. Second, the basic type only has full-function values, while each wrapper type has a non-functional value, which is null, in addition to all the functional values ​​of the corresponding basic type. Finally, basic types are more time and space efficient than packaged types . All three differences can cause you real trouble if you're not careful.

​ In short, whenever you have a choice, you should prefer using basic types over wrapper types. Basic types are simpler and faster. If you must use wrapper types, be careful! Autoboxing reduces the verbosity of using wrapper types, but not the dangers. When your program uses the == operator to compare two wrapped types, it performs an identity comparison, which is almost certainly not what you want. When your program performs mixed type calculations involving wrapped types and primitive types, it will perform unboxing. When your program performs unboxing, a NullPointerException will be thrown. Finally, when your program boxes primitive types, it can result in costly and unnecessary object creation.

Avoid using strings when other types are more appropriate

Strings are designed to represent text, and they do that very well. Because strings are so common and well supported by Java, it's natural to use them for other purposes than the scenarios for which they are intended.

Strings are a poor substitute for other value types. In general, if there is a suitable value type, whether a primitive type or an object reference, you should use it; if there is not, you should write one.

Strings are a poor substitute for enumeration types. As discussed in Item 34, enumeration type constants are more suitable for enumeration type constants than strings.

Strings are a poor substitute for aggregate types. If an entity has multiple components, it's generally a bad idea to represent it as a single string.

​Sample code:

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

​ This method has many disadvantages. This can cause confusion if the characters used to separate fields appear in one of the fields. To access individual fields, you have to parse the string, which is a slow, lengthy, and error-prone process.

In summary, using strings to represent objects should be avoided when better data types exist or can be written. If used incorrectly, strings are more cumbersome, less flexible, slower, and more error-prone than other types. Commonly misused string types include primitive types, enumerations, and aggregate types.

Be aware of performance issues caused by string concatenation

The string concatenation operator (+) is a convenient way to combine several strings into one string. It's OK for producing a single line of output or constructing a string representation of a small, fixed-size object, but it doesn't scale. Repeatedly concatenating n strings using the string concatenation operator takes n squared time. This is a consequence of the fact that strings are immutable (Item-17). When two strings are concatenated, the contents of both strings are copied.

The idea is simple: don't use the string concatenation operator to merge multiple strings unless performance doesn't matter . Otherwise use the append method of StringBuilder. Alternatively, use a character array, or work with one string at a time instead of combining them.

Reference objects through interfaces

Generally speaking, you should use interfaces instead of classes to reference objects. If a suitable interface type exists, parameters, return values, variables, and fields should be declared using the interface type . The only time you really need to reference an object's class is when you create it using a constructor.

If you develop the habit of using interfaces as types, your programs will be more flexible . If you decide you want to switch implementations, just change the class name in the constructor (or use a different static factory).

If no suitable interface exists, it is perfectly appropriate to use a class to reference the object . For example, consider value classes such as String and BigInteger. Value classes are rarely written with multiple implementations in mind. They are usually final and rarely have corresponding interfaces. It is perfectly suitable to use such value classes as parameters, variables, fields or return types.

In a real application, it should be obvious whether a given object has an appropriate interface. If so, your program will be more flexible and popular if you use interfaces to reference objects. If there is no suitable interface, use the lower level class in the class hierarchy that provides the required functionality.

Interfaces over reflection

The core reflection mechanism java.lang.reflect provides programmatic access to any class . Given a Class object, you can obtain Constructor, Method and Field instances, which respectively represent the constructor, method and field of the class represented by the Class instance. These objects provide programmatic access to the class's member names, field types, method signatures, and so on.

In addition, Constructor, Method, and Field instances allow you to manipulate their underlying counterparts reflectively: by calling methods on Constructor, Method, and Field instances, you can construct instances of the underlying class, call methods of the underlying class, and access Fields in the underlying class. For example, Method.invoke allows you to invoke any method (subject to default security constraints) on any object of any class. Reflection allows one class to use another class even if the latter does not exist when the former is compiled . However, this ability comes at a price:

  • You lose all the benefits of compile-time type checking, including exception checking . If a program attempts to reflexively call a non-existent or inaccessible method, it will fail at runtime unless you take special precautions.
  • The code required to perform reflective access is unwieldy and lengthy . It's tedious to write and difficult to read. Performance is reduced.
  • Reflected method calls are much slower than normal method calls . Just how much slower is hard to say because there are many factors at play. On my machine, reflection is 11 times slower when calling a method that takes no input parameters and returns an int.

By using reflection in a very limited form, you get many of the benefits of reflection at a fraction of the cost . For many programs, they must use a class that is not available at compile time, and there is an appropriate interface or superclass to reference the class at compile time (see Item 64 for details). If this is the case, you can create instances using reflection and access them normally through their interface or superclass .

In summary, reflection is a powerful tool that is necessary for some complex system programming tasks, but it has many shortcomings. If you write a program that must deal with unknown classes at compile time, you should use reflection only to instantiate objects whenever possible, and access the objects using interfaces or superclasses that are known at compile time.

Use local methods wisely and judiciously

The Java Native Interface (JNI) allows Java programs to call native methods, which are written in native programming languages ​​such as C or C++. Historically, native methods have had three main uses. They provide access to platform-specific facilities such as registries. They provide access to existing local code bases, including providing access to legacy data. Finally, a native approach can improve performance by writing performance-focused portions of the application in the native language.

To improve performance, it is rarely recommended to use native methods .

​ In short, think twice before using native methods. There is generally little need to use them to improve performance. If you must use native methods to access underlying resources or native libraries, use as little native code as possible and test it thoroughly . A single error in native code can break the entire application.

Optimize wisely and prudently

Don’t sacrifice sound architecture for performance. Strive to write good programs, not fast programs . If a good program isn't fast enough, its architecture will allow it to be optimized. Good programs embody the principle of information hiding: where possible, they localize design decisions within individual components, so individual decisions can be changed without affecting the rest of the system.

​Try to avoid design decisions that limit performance . The components of a design that are difficult to change are those that specify the interactions between components and with the outside world. Chief among these design components are APIs, line-layer protocols, and persistent data formats. Not only are these design components difficult or impossible to change after the fact, but all of them can impose significant limitations on the performance a system can achieve.

Consider the performance consequences of API design decisions . Converting a public type to mutable may require a lot of unnecessary defensive copying (see Item 50 for details). Similarly, using inheritance in a public class (where composition would be appropriate) binds that class forever to its superclass, artificially limiting the performance of subclasses (see Item 18 for details). The latter example is that using implementation classes rather than interfaces in an API binds you to a specific implementation, even if a faster implementation might be written in the future.

​Measure performance before . A tool that deserves special mention is jmh, which is not a profiler but a microbenchmarking framework that provides unparalleled predictability of Java code performance.

​ In short, don’t work hard to write fast programs, but work hard to write good programs; the speed will naturally increase. But be sure to consider performance when designing your system, especially when designing APIs, line-layer protocols, and persistent data formats . When you've finished building your system, measure its performance. If it's fast enough, it's done. If not, use the analyzer to find the source of the problem and optimize the relevant parts of the system. The first step is to examine the algorithm choice: no amount of low-level optimization can make up for poor algorithm choice. Repeat this process as needed, measuring performance after each change, until you are satisfied.

Follow widely recognized command conventions

The Java platform has a well-established set of naming conventions, many of which are contained in "The Java Language Specification" [JLS, 6.1]. Loosely speaking, naming conventions fall into two categories: typography and syntax.

Package and module names should be hierarchical, with periods separating components. Components should consist of lowercase letters, with numbers rarely used. The name of any package used outside your organization should begin with your organization's Internet domain name, with the components reversed, for example, edu.cmu, com.google, org.e.

In summary, internalize standard naming conventions and use them as secondary sexual characteristics. Typographic conventions are straightforward and largely unambiguous; grammatical conventions are more complex and loose. To quote "The Java Language Specification" [JLS, 6.1], "You should not blindly follow these conventions if long-standing traditional usage requires that they are not followed." Common sense judgment should be used.

Use exceptions only for exceptional situations

Exceptions should only be used in unusual situations; they should never be used in the normal flow of program control.

A well-designed API should not force its clients to use exceptions for the sake of normal control flow. If a class has "state-dependent" methods, that is, methods that can only be called under specific unpredictable conditions, the class should also have a separate "state-testing" ) method, which indicates whether the method related to this state can be called . For example, the Iterator interface contains the state-related next method and the corresponding state test method hasNext. This makes it possible to use the standard pattern of iterating over a collection using a traditional for loop (and a for-each loop, which uses the hasNext method internally)

In summary, exceptions are designed and used in unusual situations. Don't subject them to ordinary control flow, and don't write APIs that force them to do so.

Use checkable exceptions for recoverable situations and runtime exceptions for programming errors.

The Java programming language provides three throwables: checked exceptions, runtime exceptions, and errors. There is confusion among programmers about which throwable is appropriate for which situation. Although this decision is not always clear cut, there are some general principles that provide strong guidance.

The main rule of thumb when deciding whether to use checked or unchecked exceptions is that if the caller is expected to be able to reasonably resume program operation, then you should use checked exceptions . By throwing a checked exception, the caller is forced to handle the exception in a catch clause or propagate it . Therefore, every checked exception declared in a method to be thrown is a potential hint to the API user that the condition associated with the exception is a possible outcome of calling this method.

There are two types of unchecked throwables: runtime exceptions and errors. Behaviorally the two are equivalent: they are both throwables that do not need to and should not be caught. If a program throws an unchecked exception or error, it is often an unrecoverable situation, and it is harmful and useless to continue executing the program . If the program does not catch such a throwable, it will cause the current thread to halt and an appropriate error message to appear.

​Use runtime exceptions to indicate programming errors. Most runtime exceptions represent precondition violations . The so-called premise violation means that the client of the API fails to comply with the agreement established by the API specification. For example, the reservation for array access specifies that the array index value must be between 0 and the array length - 1. ArrayIndexOutOfBoundsException indicates a violation of this premise.

Although not required by the JLS (Java Language Specification), by convention, errors (Error) are often reserved for use by the JVM to indicate insufficient resources, constraint failures, or other conditions that prevent the program from continuing to execute. Since this is already an almost universally accepted management, there is no need to implement any new Error subclasses. Therefore, all unchecked throwables you implement should be subclasses of RuntimeExceptiond (either directly or indirectly). Not only should you not define a subclass of Error, you should also not throw an AssertionError exception.

​ In short, for recoverable situations, checked exceptions should be thrown; for program errors, runtime exceptions should be thrown. If it is not sure whether it can be recovered, it will throw out as a checked exception. Do not define any throw type that is neither a checked exception nor a runtime exception. Provide methods on checked exceptions to assist program recovery.

Avoid unnecessary checked exceptions

Java programmers don't like checked exceptions, but if used correctly, they can improve APIs and programs. The reason for no return codes and unchecked exceptions is that they force the programmer to handle exceptional conditions, greatly enhancing reliability. In other words, excessive use of checked exceptions will make the API very inconvenient to use. If a method throws checked exceptions, the code calling the method must handle these exceptions in one or more catch blocks, or it must declare that these exceptions are thrown and let them propagate. No matter which method is used, it adds a burden that cannot be ignored on the programmer. This burden is even heavier in Java 8, because methods that throw checked exceptions cannot be used directly in Streams .

​ All in all, checked exceptions can improve program readability when used with caution; if used excessively, it will make the API very painful to use. If the caller cannot recover from the failure, an unchecked exception should be thrown. If recovery is possible and you want to force the caller to handle the exceptional condition, it is preferred to return an optional value. Checked exceptions should be thrown if, and only if, these fail to provide sufficient information in case of failure.

Prefer standard exceptions

A major difference between expert programmers and less experienced programmers is that experts strive for, and often achieve, a high degree of code reuse . Code reuse is worth promoting. This is a general rule, and exceptions are no exception. The Java platform class library provides a basic set of unchecked exceptions, which meet the exception throwing requirements of most APIs.

Reusing standard exceptions has several benefits. The main benefit is that it makes the API easier to learn and use because it is consistent with idioms that programmers are already familiar with. The second benefit is that programs that use these APIs will be more readable because they won't have many exceptions that programmers are not familiar with. Last (and not least), fewer exception classes mean a smaller memory footprint and less time spent loading these classes.

​Don’t reuse Exception, RuntimeException, Throwable or Error directly . Treat these classes like abstract classes. You cannot reliably test these exceptions because they are superclasses of other exceptions that a method may throw.

Choosing which exception to reuse is not always precise because the "use cases" in the above table are not mutually exclusive. For example, consider an object representing a deck of cards. Suppose there is a method that handles the card dealing operation, and its parameter is the number of cards to be dealt in a hand. Assume that the value passed by the caller in this parameter is greater than the remaining number of cards in the entire deck. This situation can be interpreted as either an IllegalArgumentException (the value of the handSize parameter is too large) or an IllegalStateException (the card object contains too few cards). In this case, if no parameter value is available, an llegalStateException is thrown, otherwise an llegalArgumentException is thrown.

Throws the exception corresponding to the abstraction

If the exception thrown by a method is not clearly related to the task it performs, this situation can be confusing. This tends to happen when a method passes an exception thrown by a lower-level abstraction. In addition to confusing people, this also "pollutes" the higher-level API with implementation details. If the high-level implementation changes in subsequent releases, the exceptions it throws may also change, potentially breaking existing client programs.

To avoid this problem, higher-level implementations should catch low-level exceptions and throw exceptions that can be interpreted in terms of high-level abstractions . This approach is called exception translation, as shown in the following code:

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

​ A special form of exception translation is called exception chaining. If low-level exceptions are very helpful in debugging problems that cause high-level exceptions, exception chaining is appropriate . The low-level exception (reason) is passed to the high-level exception, and the high-level exception provides an access method (the getCause method of Throwable) to obtain the low-level exception:

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

The high-level exception constructor passes the reason to the chaining-aware super constructor, so it will eventually be passed to one of Throw able's constructors that run the exception chain, such as Throwable(Throwable):

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

Although exception translation is an improvement over indiscriminately passing exceptions from lower layers, it cannot be abused . If possible, a good practice for handling exceptions from lower-level methods is to ensure that they will execute successfully before calling low-level methods, thereby preventing them from throwing exceptions. Sometimes, you can check the validity of the parameters of a higher-level method before passing them to the lower-level method to avoid the lower-level method from throwing exceptions.

​ All in all, if you cannot prevent or handle exceptions from lower layers, the general approach is to use exception translation, only if the specification of the lower layer method happens to ensure that "all exceptions it throws are also appropriate for higher layers". Exceptions can be propagated from lower levels to higher levels. Exception chaining provides the best functionality for both high-level and low-level exceptions: it allows appropriate high-level exceptions to be thrown, while capturing low-level causes for failure analysis.

The exceptions thrown by each method are documented

Describing the exceptions thrown by a method is an important part of the documentation required for the correct use of this method. Therefore, it is especially important to take the time to carefully document the exceptions thrown by each method.

​Always declare checked exceptions individually, and use Javadoc's @throws tag to document exactly the conditions under which each exception is thrown . If a public method may throw multiple exception classes, do not use a "shortcut" to declare that it throws a superclass of those exception classes. Never declare a public method to directly "throws Exception", or worse yet declare it to directly "throws Throwable" . This is a very extreme example. Such a declaration not only does not provide the programmer with any guidance about "which exceptions this method can throw", but also greatly hinders the use of this method, because it actually masks the possibility that this method may throw under the same execution environment . any other exceptions . An exception to this advice is that the main method can be safely declared to throw Exception because it is only called by the virtual machine.

​ Use Javadoc's @throws tag to document every unchecked exception that a method may throw, but do not use the throws keyword to include unchecked exceptions in the method's declaration.

If many methods in a class throw the same exception for the same reason, it is acceptable to document the exception in a documentation comment for the class, rather than documenting each method individually. A common example is NullPointerException.

In summary, document every exception that can be thrown by every method you write. This is true for unchecked exceptions and checked exceptions, and for abstract and concrete methods. This documentation should use the @throws tag in documentation comments. Provide a separate declaration for each checked exception in the method's throws clause, but do not declare unchecked exceptions. Without documentation of the exceptions that can be thrown, it will be difficult or impossible for others to use your classes and interfaces effectively.

Include failure-capture information in details

​ When the program fails due to an uncaught exception, the system will automatically print out the stack trace of the exception. Include in the stack trace the string representation of the exception, that is, the result of the call to its toString method. It usually contains the exception's class name, followed by a detail message. Typically, this is just information that a programmer or website reliability engineer must examine when investigating the cause of a software failure. If the failure cannot be easily reproduced, obtaining additional information will be difficult or even impossible. Therefore, it is particularly important that the toString method of an exception type should return as much information as possible about the cause of the failure. In other words, the string representation of the exception should capture the failure for easy subsequent analysis .

To catch a failure, the exception details should include the values ​​of all parameters and fields that contributed to the exception . For example, the details of an IndexOutOfBoundsException exception should include the lower bound, the upper bound, and the index value that does not fall within the bounds. This detail message provides a lot of information about the failure.

​A word of advice for security-sensitive information. Because stack traces can be seen by many people during the process of diagnosing and fixing software problems, never include passwords, keys, and similar information in detail messages .

Exception details should not be confused with user-level error messages, which must be understandable to the end user. Unlike user-level error messages, exception string representations are primarily used by programmers or website reliability engineers to analyze the cause of the failure . Therefore, the content of the information is much more important than readability. User-level error messages are often localized, while exception detail messages are rarely localized . (Translation Internationalization)

Guaranteed atomicity of failure

When an object throws an exception, we usually expect the object to remain in a well-defined usable state, even if the failure occurs in the middle of performing an operation. This is especially important for checked exceptions, since the caller expects to be able to recover from such exceptions. In general, a failed method call should leave the object in the state it was in before it was called . Methods with this property are said to have failure atomicity. (Basics of Transactions).

There are several ways to achieve this effect. The simplest way is to design an immutable object (see Item 17 for details). If the object is immutable, failure atomicity is obvious. If an operation fails, it may prevent new objects from being created, but it will never leave existing objects in an inconsistent state because each object is in a consistent state as it is created. There will be no changes in the future.

A common way to achieve failure atomicity for methods that perform operations on mutable objects is to check the validity of parameters before performing the operation. This allows the appropriate exception to be thrown before the object's state is modified .

A third way to achieve failure atomicity is to perform an operation on a temporary copy of the object and then replace the contents of the object with the result from the temporary copy when the operation is complete . If the data is stored in a temporary data structure, the calculation process will be faster, so it is natural to use this method. For example, some sorting functions back up their input list into an array before performing the sort, in order to reduce the overhead of accessing elements in the inner loop of the sort. This is done for performance reasons, but it has the added advantage of ensuring that the input list remains intact even if the sort fails.

The last way to achieve failure atomicity, which is far less common, is to write a recovery code that intercepts failures that occur during the operation and rolls the object back to the state before the operation started. . This approach is mainly used for persistent (disk-based) data structures.

In summary, as part of a method's specification, any exception it raises should leave the object in the state it was in before the method was called. If this rule is violated, the API documentation should clearly indicate what state the object will be in. Unfortunately, much of the existing API documentation fails to do this.

Don't ignore exceptions

An empty , which is to force you to handle exception situations. Ignoring anomalies is like ignoring a fire alarm.

Exceptions can be ignored in some cases. For example, when closing FileinputStream. Because you have not changed the state of the file, you do not need to perform any recovery actions, and you have read the required information from the file, so you do not need to terminate the ongoing operation. Even in this case, it's wise to log exceptions because you can investigate their causes if they occur frequently. If you choose to ignore the exception, the catch block should contain a comment explaining why this is possible, and the variable should be named ignored:

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 
}

Handling exceptions correctly can completely avoid failure. As long as the exception is propagated to the outside world, it will at least cause the program to fail quickly, thus preserving information that can help debug the failure condition.

Synchronous access to shared mutable data

The keyword synchronized can ensure that only one thread can execute a certain method or a certain code block at the same time.

Without synchronization, changes in one thread cannot be seen by other threads. Synchronization not only prevents one thread from seeing an object in an inconsistent state, it also ensures that every thread that enters a synchronized method or synchronized code block can see the effects of all previous modifications protected by the same lock.

You may have said that in order to improve performance, you should avoid using synchronization when reading or writing atomic data. This advice is very dangerous and wrong. Although the language specification guarantees that threads will not see arbitrary values ​​when reading atomic data, it does not guarantee that values ​​written by one thread will be visible to another thread. Synchronization is necessary for reliable communication between threads and for mutually exclusive access .

The best way to avoid the problems discussed in this article is to not share mutable data. Either share immutable data (see Item 17 for details) or not share it at all. In other words, limit mutable data to a single thread .

In summary, when multiple threads share mutable data, each thread reading or writing data must perform synchronization . Without synchronization, there is no guarantee that changes made by one thread will be known by another thread. Failure to synchronize shared mutable data can cause liveness failure and safety failure of the program. Such failures are difficult to debug. They can be intermittent and time-dependent, and the behavior of the program can be fundamentally different on different virtual machines. If only interactive communication between threads is required, and no mutual exclusion is required, the vo latile modifier is an acceptable form of synchronization, but using it correctly may require some tricks.

Avoid over-synchronization

To avoid liveness and safety failures, never give up control of the client within a synchronized method or block of code. In other words, within a synchronized area, do not call methods that are designed to be overridden, or that are provided by the client in the form of a function object (see Item 24 for details). From the perspective of the class containing the synchronized area, such a method is alien. The class has no idea what the method will do and has no control over it. Depending on the effect of the foreign method, calling it from a synchronized area can cause an exception, deadlock, or data corruption.

In fact, there is a better way to move the foreign method calls out of the synchronized code block. The Java class library provides a concurrent collection (concurrent collection), see Item 81 for details, called CopyOnWriteArrayList, which is specially customized for this purpose. This CopyOnWriteArrayList is a variant of ArrayList, which implements all write operations here by re-copying the entire underlying array. Since the internal array never changes, iteration requires no locking and is very fast . The performance of CopyOnWriteArrayList will be greatly affected if used heavily, but it is good for observer lists because they rarely change and are iterated over frequently.

Generally speaking, you should do as little work as possible within the synchronization area . Obtain the lock, examine the shared data, transform the data if necessary, and then release the lock. If you must perform a time-consuming action, you should try to move the action outside the synchronization area without compromising the security of the shared data.

. In this multi-core era, the real cost of over-synchronization is not the CPU time spent acquiring locks; it is the lost opportunity for parallelism and the latency caused by the need to ensure that each core has a consistent view of memory. . Another potential cost of excessive synchronization is that it limits the virtual machine's ability to optimize code execution.

In summary, in order to avoid deadlock and data corruption, never call foreign methods from inside the synchronization area field. More generally speaking, try to limit the amount of work inside the sync area fields to as little as possible . When you design a mutable class, consider whether they should synchronize themselves. In today's multi-core era, this is more important than never over-synchronizing. You should only do this if you have a good reason to synchronize a class internally, and you should clearly document this decision.

Executor, task and stream take precedence over threads

Choosing an executor service for a particular application is tricky.

​ If you are writing a small program or a lightly loaded server, using Executors.newCachedThreadPool is usually a good choice because it requires no configuration and generally does the job correctly.

​ But for servers with heavy loads, cached thread pools are not a good choice! In the cached thread pool, submitted tasks are not queued, but are directly handed over to the thread for execution. If no thread is available, create a new thread. If a server is so heavily loaded that all of its CPUs are fully occupied, when more tasks come in, more threads will be created, which will only make the situation worse.

Therefore, in a heavily loaded production server, it is best to use Executors.newFixedThreadPool, which provides you with a thread pool containing a fixed number of threads, or to maximize control over it, use the ThreadPoolExecutor class directly.

Not only should you try not to write your own work queue, but you should also try not to use threads directly. When using threads directly, Thread acts as both a unit of work and an execution mechanism. In Executor Framework, the unit of work and the execution mechanism are separated . The key abstraction now is the unit of work, called a task. There are two types of tasks: Runnable and its close cousin Callable (it is similar to Runnable, but it returns a value and can throw arbitrary exceptions). The common mechanism for executing tasks is the executor service. If you look at the problem from a task perspective and let an executor service perform the task for you, you gain great flexibility in choosing the appropriate execution strategy. Essentially, what the Executor Framework does is execution, and what the Collections Framework does is aggregation.

In Java 7, the Executor Framework has been extended to support fork-join tasks, which are run through a special executor service called a fork-join pool. A fork-join task is represented by a ForkJoinTask instance, which can be divided into smaller subtasks. The thread containing the ForkJoinPool must not only process these tasks, but also "steal" tasks from another thread to ensure that all threads stay busy, thus improving CPU usage, improved throughput, and lower latency . Writing and tuning fork-join tasks is tricky. Concurrent streams (see Item 48 for details) are written on fork join pools, and we can enjoy their performance advantages with little effort, assuming that they are suitable for the task at hand.

Prioritize the use of concurrency tools over wait and notify

The more advanced tools in java.util.concurrent are divided into three categories: Executor Framework, Concurrent Collection and Synchronizer.

​ Concurrent collections provide high-performance concurrent implementations for standard collection interfaces (such as List, Queue, and Map). To provide high concurrency, these implementations manage synchronization internally themselves. Therefore, it is impossible to exclude concurrent activity from a concurrent collection; locking it does nothing but make the program slower .

In addition to providing excellent concurrency, ConcurrentHashMap is also very fast. On my machine, the optimized intern method above is more than 6 times faster than String.intern (but remember, String.intern must use some kind of weak reference to avoid memory leaks over time). Concurrent collections resulted in synchronized collections being mostly deprecated. For example, ConcurrentHashMap should be used in preference to Collections.synchronizedMap . Simply replacing synchronous Maps with concurrent Maps can greatly improve the performance of concurrent applications.

A synchronizer is an object that enables a thread to wait for another thread, allowing them to coordinate actions. Commonly used synchronizers are CountDownLatch and Semaphore. Less commonly used are CyclicBarrier and Exchanger. A powerful synchronizer is Phaser .

A countdown latch is a one-time barrier that allows one or more threads to wait for one or more other threads to do something. The only constructor of Count DownLatch takes a parameter of type int. This int parameter refers to the number of times that the countDown method must be called on the latch before all waiting threads are processed.

In short, using the wait method and notify method directly is like programming in "concurrent assembly language", while java.util.concurrent provides a higher-level language. There is little, if any, reason to use wait and notify methods in new code . If you are maintaining code that uses the wait method and the notify method, be sure to always call the wait method from inside a while loop using the standard pattern. In general, the notifyAll method should be used in preference to the notify method. If you use the notify method, be careful to ensure program liveness.

Documentation should contain thread-safety attributes

The behavior of a class when its methods are used concurrently is an important part of its agreement with the client. If you don't document a class's behavior in this regard, its users will be forced to make assumptions. If these assumptions are wrong, the resulting program may lack enough synchronization or may have excessive synchronization. In either case, serious errors can result.

You may have heard that you can determine whether a method is thread-safe by looking for the synchronized modifier in the method's documentation. This view is wrong in several respects. In normal operation, there is a reason why the synchronization modifier is not included in the Javadoc output. The presence of the synchronized modifier in a method declaration is an implementation detail and not part of its API. It does not reliably indicate that a method is thread-safe .

To enable safe concurrent use, a class must clearly document the thread safety levels it supports .

In short, each class should have a carefully worded description or clearly document its thread-safety attributes using thread-safe annotations . The synchronized modifier has no effect in the document. A conditionally thread-safe class must record which sequences of method calls require external synchronization, and which locks need to be acquired while executing those sequences. If you write a class that is unconditionally thread-safe, consider using a private lock object instead of a synchronized method. This will protect you from synchronization interference from clients and subclasses, and give you greater flexibility to adopt sophisticated concurrency control methods in subsequent releases.

Use lazy initialization wisely and judiciously

​ Lazy initialization is delaying the initialization of a field until its value is needed. If the value is not required, the field is not initialized. This technique works for both static fields and instance fields. Although lazy initialization is primarily an optimization, it can also be used to break harmful loops and instance initializations in classes.

Lazy initialization also has its uses. If a field is only accessed on a small subset of instances of the class, and initializing the field is expensive, lazy initialization may be worthwhile. The only way to know for sure is to measure the performance of a class with and without lazy initialization.

In most cases, regular initialization is better than lazy initialization .

If you use lazy initialization instead of a circularity of initialization, use synchronized accessors , as they are a simple, clear alternative:

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

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

Both idioms (normal initialization and lazy initialization using synchronized accessors) are unchanged when applied to static fields, except for the addition of the static modifier to the field and accessor declarations.

If you need to use lazy initialization to improve the performance of instance fields, use double-check mode. This pattern avoids the locking cost when accessing fields after initialization .

In summary, you should initialize most fields normally rather than lazily initializing them. If you must lazily initialize fields to achieve performance goals or to break harmful initialization loops, use appropriate lazy initialization techniques. For fields, use double-check mode; for static fields, you should use the lazy initialization holder class idiom. For example, to tolerate repeated initialization of instance fields, you might also consider the single-check pattern.

Don't rely on the thread scheduler

When many threads can run, the thread scheduler decides which threads can run and for how long. Any reasonable operating system will try to make this decision fairly, but strategies may vary. Therefore, well-written programs should not rely on the details of this strategy. Any program that relies on a thread scheduler for correctness or performance is likely to be non-portable .

The best way to write robust, responsive, and portable programs is to ensure that the average number of runnable threads is not significantly greater than the number of processors. This leaves the thread scheduler with few options: it only runs runnable threads until they are no longer runnable. Even under completely different thread scheduling strategies, the behavior of the program does not change much.

The main technique for keeping the number of runnable threads low is to have each thread do some useful work and then wait for more work. If threads are not doing useful work, they shouldn't be running . For the Executor framework (see Item 80 for details), this means sizing the thread pool appropriately [Goetz06, 8.2] and keeping tasks short (but not too short), otherwise the dispatch overhead will still hurt performance.

In short, do not rely on the thread scheduler to judge the correctness of the program. The resulting program is neither robust nor portable. Therefore, do not rely on Thread.yield or thread priority. These tools are just hints to the scheduler. Thread priority can be used sparingly to improve the quality of service of an already working program, but should never be used to "fix" a program that is barely working.

Prefer alternatives to Java serialization

​ A fundamental problem with serialization is that it is too open to attack and difficult to protect, and the problem continues to grow: deserializing the object graph by calling the readObject method on ObjectInputStream. This method is essentially a magic constructor that can be used to instantiate almost any type of object on the classpath, as long as the type implements the Serializable interface. During the process of deserializing the byte stream, this method can execute code from any of these types, so all of these types of code are within the attack scope.

​ Attacks can involve Java platform libraries, third-party libraries (such as the Apache Commons collection), and classes in the application itself. Even if you adhere to all relevant best advice and successfully write serializable classes that are not vulnerable to attacks, your application may still be vulnerable.

When you deserialize a byte stream that you don't trust, you are vulnerable. A good way to avoid serialization exploitation is to never deserialize anything . There is no reason to use Java serialization in any new system you write .

If you can't avoid Java serialization entirely, perhaps because you need to work in a legacy system environment, then your next best option is to never deserialize untrusted data .

In summary, serialization is dangerous and should be avoided. If you are designing a system from scratch, you can use cross-platform structured data such as JSON or protobuf. Don't deserialize untrusted data. If you must do this, use object deserialization filtering, but be aware that it is not guaranteed to block all attacks. Avoid writing serializable classes. If you must do this, be very careful.

Implement Serializable with caution

​ Making instances of a class serializable is very simple, just implement the Serializable interface. Because it is so easy to do, there is a common misconception that serialization requires very little effort on the part of the programmer. The reality is much more complex. While the immediate cost of making a class serializable is negligible, the long-term cost is often huge.

A major cost of implementing the Serializable interface is that it reduces the flexibility to change the class's implementation once it is published.

The second cost of implementing the Serializable interface is that it increases the possibility of bugs and security vulnerabilities .

The third cost of implementing the Serializable interface is that it increases the testing burden associated with releasing new versions of the class.

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

Classes designed for inheritance are rarely suitable for implementing the Serializable interface, and interfaces are rarely suitable for extending it. Violating this rule places a significant burden on anyone extending the class or implementing the interface.

Inner classes should not implement Serializable . They use compiler-generated synthetic fields to store references to the enclosing instance and to store values ​​of local variables from the enclosing instance. The correspondence between these fields and the class definition is the same as if the names of anonymous classes and partial classes were not specified. Therefore, the default serialization form of inner classes is undefined. However, static member classes can implement the Serializable interface.

Consider using a custom serialization form

When you're writing classes under a time crunch, you should usually focus on designing a well-designed API. Sometimes this means releasing a "one-off" implementation that you know will be replaced in a future release. Normally this is not a problem, but if your class implements the Serializable interface and uses the default form of serialization, you will never completely get rid of this "one-off" implementation . It will always affect the serialized form. This is not just a theoretical question.

​Do not accept the default serialization form without . Accepting the default serialization form should be a decision that is justified from a combination of flexibility, performance, and correctness perspectives. In general, when designing a custom serialization form, you should only accept the default serialization form if it is substantially the same as the encoding selected by the default serialization form.

Regardless of the serialization form you choose, declare an explicit serial version UID in every serializable class you write . This eliminates serial version UIDs as a potential source of incompatibilities (see Item 86 for details). There is also a small performance advantage to be gained by doing this. If a serial version UID is not provided, expensive calculations need to be performed to generate one at runtime.

In summary, if you have decided that a class should be serializable, think carefully about what the serialization form should be. Only use the default serialization form when the logical state of the object is reasonably described; otherwise, design a custom serialization form suitable for describing the object. Designing the serialized form of a class should take as much time as designing the exported method, and both should be treated with caution (see Item 51 for details). Just as exported methods cannot be removed from future versions, fields cannot be removed from the serialized form; they must be saved forever to ensure serialization compatibility. Choosing the wrong form of serialization can have a permanent negative impact on your class's complexity and performance.

Write the readObject method protectively

In summary, when writing the readObject method, think like this: you are writing a public constructor, and no matter what byte stream is passed to it, it must produce a valid instance. Don't assume that this byte stream necessarily represents an actual serialized instance. Although in the examples of this entry, the class uses the default serialization form, all of the possible problems discussed also apply to classes with custom serialization forms. Below are some guidelines in summary form that will help you write more robust readObject methods.

  • Object reference fields in a class must be kept as private properties, and each object in these fields must be protectively copied. Mutable components in immutable classes fall into this category
  • For any constraint, an InvalidObjectException is thrown if the check fails. These checks should follow all protective copies.
  • If the entire object graph must be validated after being deserialized, you should use the ObjectInputValidation interface (not discussed in this book).
  • Whether it is a direct method or an indirect method, do not call any overridable method in the class.

For instance control, enumeration is better than readResolve

In fact, if you rely on readResolve for instance control, all instance fields with object reference types must be declared transient.

The accessibility of readResolve . If you put the readResolve method on a final class, it should be private. If you place the readResolve method on a non-final class, you must carefully consider its accessibility. If it is private, it will not apply to any subclass. If it is package-level private, it applies to subclasses within the same package. If it is protected or public and the subclass does not override it, deserializing the serialized subclass will produce a superclass instance, which may result in a ClassCastException.

In summary, enumerated types should be used whenever possible to implement instance-controlled constraints . If this is not possible, and you need a class that is both serializable and instance-controlled, you must provide a readResolve method and ensure that all instantiated fields of the class are of basic types or are transient.

Consider using serialization proxies instead of serialization instances

Implementing the Serializable interface increases the possibility of errors and security issues because it allows instances to be created using mechanisms outside the language rather than using ordinary constructors. However, there is a way to greatly reduce these risks. It is the serialization proxy pattern.

The serialization proxy pattern is fairly simple. First, design a private static nested class for the serializable class that accurately represents the logical state of the enclosing class. This nested class is called the serialization proxy, and it should have a separate constructor whose parameter type is the enclosing class. This constructor simply copies the data from its arguments: it does not need to perform any consistency checks or protective copies. From a design perspective, the default serialization form of the serialization agent is the best serialization form of the enclosing class. Both the peripheral class and its serial proxy must declare to implement the Serializable interface.

The serialization It is not compatible with classes that can be extended by clients. It's also not compatible with some classes that contain loops in the object graph : if you try to call a method on an object from within the readResovle method of that object's serialization proxy, you'll get a ClassCastException because you don't have the object yet, just Its serialization proxy.

​ Finally, the enhanced functionality and security provided by the serialized proxy pattern do not come without a price. On my machine, the overhead of serializing and deserializing Period instances through the serialization proxy is 14% higher than using protective copy.

​ In summary, when you find that you must write the readObject or writeObject method on a class that cannot be extended by the client, you should consider using the serialization proxy pattern. This pattern is the easy way to robustly serialize objects with important constraints.

Guess you like

Origin blog.csdn.net/weixin_40709965/article/details/130898972
Recommended