Location>code7788 >text

The most complete Effective Java summary in history (II)

Popularity:666 ℃/2025-03-28 08:27:46

method

49. Check the validity of parameters

Every time you write a method or constructor, you should consider what limitations there exist in the parameters, record them in the document, and then explicitly check them at the beginning of the method.

Failure atomicity may be violated if the parameters are not verified at the beginning of the method. Because the method may fail when there are confusing exceptions during execution, or calculate the wrong result and return it, and may even hide hidden dangers, resulting in errors in the code somewhere in the future.

For public and protected methods, use@throwsTags record exceptions that can be thrown by violating parameter limits, such as IllegalArgumentException, IndexOutOfBoundsException, or NullPointerException. See the following example:

/**
* Returns a BigInteger whose value is (this mod m). This method
* differs from the remainder method in that it always returns a
* non-negative BigInteger.
**
@param m the modulus, which must be positive
* @return this mod m
* @throws ArithmeticException if m is less than or equal to 0
*/
public BigInteger mod(BigInteger m) {
    if (() <= 0)
        throw new ArithmeticException("Modulus <= 0: " + m);
    ... // Do the computation
}

There is no NullPointerException caused by m being null, because it is recorded in class-level document comments, so that it does not need to be recorded separately on each method.

Added in Java 7The method is very flexible and convenient. It can check whether the input object is null. If so, a NullPointerException with the specified message is thrown; otherwise, the input object is returned:

// Inline use of Java's null-checking facility
 = (strategy, "strategy");

For private methods, the author should make sure that only valid parameter values ​​are passed. Therefore, a private method can check parameters using assertions, as follows:

// Private helper function for a recursive sort
private static void sort(long a[], int offset, int length) {
    assert a != null;
    assert offset >= 0 && offset <= ;
    assert length >= 0 && length <=  - offset;
    ... // Do the computation
}

If the assertion is not enabled, there is no cost. You can-eaThe flags are passed to the java command to enable them.

The parameters should be checked when the object is constructed, rather than when the object is used. The advantage of doing this is to expose the problem as soon as possible, otherwise it will be more troublesome to debug. Typically, a static factory method, which takes an int array and returns a List view of the array. If the client passes null, the method immediately throws a NullPointerException. Similar examples include constructors.

But there are exceptions to this rule. When the validity check is costly or the calculation process itself contains parameter checks, implicit checks are chosen during the calculation process. For example, consider a method that sorts objects List, e.g.(List). All objects in List must be compared with each other. If they cannot be compared, a ClassCastException will be thrown. This checking process is performed implicitly during the comparison process, rather than pre-checking.

The more restrictions on parameters, the better. Instead, you should design as general as possible, with fewer restrictions on parameters, the better.

50. Make a defensive copy when needed

Java is a safe language that is not affected by buffer overflow, array overflow, illegal pointers, and other memory corruption errors without native methods.

Even if a safe language is used, it may inadvertently provide a way to modify the internal state of an object. For example, the following class represents an immutable period of time:

// Broken "immutable" time period class
public final class Period {
    private final Date start;
    private final Date end;

    /**
    * @param start the beginning of the period
    * @param end the end of the period; must not precede start
    * @throws IllegalArgumentException if start is after end
    * @throws NullPointerException if start or end is null
    */
    public Period(Date start, Date end) {
        if ((end) > 0)
            throw new IllegalArgumentException(start + " after " + end);
         = start;
         = end;
    }

    public Date start() {
        return start;
    }

    public Date end() {
        return end;
    }
    ... // Remainder omitted
}

This class requires that the start time of the time period cannot be after the end time. However, this constraint can be bypassed by modifying the internally saved Date:

// Attack the internals of a Period instance
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
(78); // Modifies internals of p!

Starting with Java 8, the typical way to solve this problem is to use an immutable Instant (or LocalDateTime or ZonedDateTime) insteadObsolete Date. But for old code containing Date, a general solution must be found.

The solution is to copy the defensive copy of each mutable parameter to the constructor:

// Repaired constructor - makes defensive copies of parameters
public Period(Date start, Date end) {
     = new Date(());
     = new Date(());
    if (() > 0)
        throw new IllegalArgumentException( + " after " + );
}

The new constructor ensures that previous attacks will not affect the Period instance. Note that the defensive copy is made before checking the validity of the parameters, so that the class is not affected by changes from other threads during the time period between the checking parameters and the copying parameters. In the field of computer security, this is called a time-of-check/time-of-use vulnerability or TOCTOU attack.

The reason why Date's clone method is not used to create a defensive copy is that Date is not final, so it cannot be guaranteed that the clone method returns an instance object of Date. It can return an instance of an untrusted subclass, and there is a risk of malicious damage from here.

You can also modify the internal state of the Period in another way:

// Second attack on the internals of a Period instance
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
().setYear(78); // Modifies internals of p!

The solution is to return a defensive copy of the variable internal fields on the accessor:

// Repaired accessors - make defensive copies of internal fields
public Date start() {
    return new Date(());
}

public Date end() {
    return new Date(());
}

With new constructors and new accessors, Period is actually immutable unless modified using reflection or local methods.

Use immutable objects as components of objects as possible so that you don't have to worry about defensive copying.

Defensive copying can lead to performance losses. If a class trusts its caller and does not modify internal components, it can be explained in the class documentation without being used as a defensive copy: the caller cannot modify the relevant parameter object or return value.

51. Careful design of the signature

This entry is a summary of multiple API design experiences.

Select the method name carefully.A widely recognized name that is understandable and consistent with other names in the same package should be selected.

Providing convenient methods should not be overdoing the end.Too many methods will make the class difficult to maintain. For each operation supported by a class or interface, it is generally enough to provide only a fully functional method. Shortcuts are only considered when used frequently.

Avoid long parameter lists.Set four or fewer parameters, most programmers can't remember a longer parameter list. Parameters of the same type of long sequences are particularly harmful and are very likely to cause errors.

There are three ways to shorten the list of excessively long parameters:

  1. Decompose the original method into multiple sub-methods. The functions of the original method are implemented by combining multiple word methods, so that each sub-method only requires a subset of parameters.
  2. Create a helper class to save parameter groups.
  3. Builder pattern is used from object build to method call.

For the parameter type of the method, the interface is preferred over the class. For example, Map is preferred over HashMap as the parameter type of the method.

For the parameter type of the method, the double element enumeration type is preferred instead of boolean. Enumeration makes the code easier to maintain. Additionally, they make it easier to add more options later. For example, you might have a Thermometer type with a static factory that uses enumeration:

public enum TemperatureScale { FAHRENHEIT, CELSIUS } // Fahrenheit, Celsius

()Not only comparison(true)More meaningful, and you can add KELVIN (Kelvin, Thermodynamic Temperature Scale) to TemperatureScale in future versions.

52. Use reload wisely

Note that the overload mentioned here refers to overload, not override.

The following program is a kind attempt to classify a Collection based on whether a Set, List or other collection type:

// Broken! - What does this program print?
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)
            (classify(c));
    }
}

What you expect is: this program prints Set, List, and Unknown Collection, but the result is: it prints Unknown Collection three times. Because the classify method is overloaded, and it decides which overload to call at compile time.

The best way to fix the CollectionClassifier program is to use instanceof to make type judgments in one method:

public static String classify(Collection<?> c) {
    return c instanceof Set ? "Set" :c instanceof List ? "List" : "Unknown Collection";
}

Confusing the usage of overload should be avoided. The most conservative strategy is to never generate two overloads with the same number of parameters. Or provide a different name for the method so that you don't need to overload it.

If two overloads with the same number of parameters must be generated, then at least one parameter must be guaranteed to have in these two overloadsTotally differenttype. A counterexample is related to Java's automatic boxing mechanism:

public class SetList {
public static void main(String[] args) {
    Set<Integer> set = new TreeSet<>();
    List<Integer> list = new ArrayList<>();
    for (int i = -3; i < 3; i++) {
        (i);
        (i);
    }
    for (int i = 0; i < 3; i++) {
        (i);
        (i);
    }
    (set +""+list);
    }
}

We expect to delete the three numbers 0, 1, and 2 from set and list, respectively, but only do it in set. In fact, the three elements corresponding to the subscripts 0, 1, and 2 are deleted in the list. Because it's right(i)The call selects an overloaded methodremove(int i), notremove(Object o). The correct way to write it should be:

for (int i = 0; i < 3; i++) {
    (i);
    ((Integer) i); // or remove((i))
}

lambda expressions and method references are also prone to confusion in overloading. For example:

new Thread(::println).start();
ExecutorService exec = ();
(::println);

Thread constructor calls and submit method calls look similar, but the former can be compiled while the latter cannot. Because the submit method has an overload, it accepts aCallable<T>, while the thread constructor does not. So the compiler will organize the latter to avoid ambiguity.

53. Use variable parameters wisely

Variable parameter methods accept zero or more parameters of the specified type. The underlying layer of mutable parameters is an array.

A simple example of variable parameters:

// Simple use of varargs
static int sum(int... args) {
    int sum = 0;
    for (int arg : args)
        sum += arg;
    return sum;
}

Suppose you want to write a function to calculate the minimum value of its parameters. If the client does not pass parameters, check the parameter length at runtime and throw an exception:

// The WRONG way to use varargs to pass one or more arguments!
static int min(int... args) {
    if ( == 0)
        throw new IllegalArgumentException("Too few arguments");
    int min = args[0];
    for (int i = 1; i < ; i++)
        if (args[i] < min)
    		min = args[i];
    return min;
}

There are several problems with this solution:

  1. If this method is called without parameters, it will fail at runtime rather than compile time.
  2. Not beautiful. You cannot use a for-each loop unless min is initialized to Integer.MAX_VALUE.

The following writing method can avoid these problems:

// The right way to use varargs to pass one or more arguments
static int min(int firstArg, int... remainingArgs) {
    int min = firstArg;
    for (int arg : remainingArgs)
        if (arg < min)
    		min = arg;
    return min;
}

Be careful when using variable parameters when performance is critical. Each call to a mutable parameter method will result in array creation and initialization. There is a compromise. Assuming that 95% of calls only require three or fewer parameters, use mutable parameters for more than three parameters:

public void foo() { }
public void foo(int a1) { }
public void foo(int a1, int a2) { }
public void foo(int a1, int a2, int a3) { }
public void foo(int a1, int a2, int a3, int... rest) { }

54. Return an empty collection or array, not null

The following methods are common:

// Returns null to indicate an empty collection. Don't do this!
private final List<Cheese> cheesesInStock = ...;
/**
* @return a list containing all of the cheeses in the shop,
* or null if no cheeses are available for purchase.
*/
public List<Cheese> getCheeses() {
    return () ? null: new ArrayList<>(cheesesInStock);
}

The disadvantage of writing this way is that non-null judgments are required when calling the method:

List<Cheese> cheeses = ();
if (cheeses != null && ())
    ("Jolly good, just the thing.");

An error occurs when you forget to make a non-null judgment, and it should be changed to return to an empty list:

//The right way to return a possibly empty collection
public List<Cheese> getCheeses() {
    return new ArrayList<>(cheesesInStock);
}

If newly created empty collections will hurt performance, you can avoid new creations by repeatedly returning empty immutable collections:

// Optimization - avoids allocating empty collections
public List<Cheese> getCheeses() {
    return () ? (): new ArrayList<>(cheesesInStock);
}

Arrays are similar to collections. Never return null, but should return an array of zero length. The following code passes an array of zero length into the toArray method:

//The right way to return a possibly empty array
public Cheese[] getCheeses() {
    return (new Cheese[0]);
}

If you create zero-length arrays that hurt performance, you can repeatedly return the same zero-length array:

// Optimization - avoids allocating empty arrays
private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];
public Cheese[] getCheeses() {
    return (EMPTY_CHEESE_ARRAY);
}

Do not preallocate arrays passed to toArray for performance improvement:

// Don’t do this - preallocating the array harms performance!
return (new Cheese[()]);

55. Return wisely

Before Java 8, when writing methods that could not return values ​​in some cases, there were two ways: throwing an exception or returning null. Neither method is perfect. Throwing an exception is expensive, and returning null requires a special judgment on this situation.

In Java 8, you can returnOptional<T>Solve the above problems. It means that T should be returned in theory, but in some cases it may not be returned.

In Article 30, there is an example of calculating the maximum value of the set based on the natural order of the set elements:

// Returns maximum value in collection - throws exception if empty
public static <E extends Comparable<E>> E max(Collection<E> c) {
    if (())
        throw new IllegalArgumentException("Empty collection");
    E result = null;
    for (E e : c)
        if (result == null || (result) > 0)
    		result = (e);
    return result;
}

A better alternative is to returnOptional<E>

// Returns maximum value in collection as an Optional<E>
public static <E extends Comparable<E>> Optional<E> max(Collection<E> c) {
    if (())
        return ();
    E result = null;
    for (E e : c)
        if (result == null || (result) > 0)
    		result = (e);
    return (result);
}

Notice,Never return null values ​​from methods that have Optional return values, because it goes against the original design intention of this function.

Terminal operations on many streams return Optional. If we use a stream to override the max method, the stream version of the max operation will generate an Optional for us:

// Returns max val in collection as Optional<E> - uses stream
public static <E extends Comparable<E>> Optional<E> max(Collection<E> c) {
    return ().max(());
}

For methods that return Optional, the caller can choose what action to take if the method cannot return a value. You can specify a default value:

// Using an optional to provide a chosen default value
String lastWordInLexicon = max(words).orElse("No words...");

Or any appropriate exception can be thrown:

// Using an optional to throw a chosen exception
Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);

If you can tell that an Optional is not empty, you can directly get the value from Optional without specifying the operation corresponding to the null value. But if the judgment is wrong, a NoSuchElementException will be thrown:

// Using optional when you know there’s a return value
Element lastNobleGas = max(Elements.NOBLE_GASES).get();

Not all return types are suitable for Optional encapsulation. Unsuitable types are: container types, including collections, maps, streams, arrays, and Optional. For example, an empty List should be returned instead ofOptional<List>

A suitable scenario for using Optional is that the method may not return a value, and if there is no return value, the caller has to do special processing.

Use Optional with caution when performance-critical, as it has the additional performance cost of encapsulating the original object.

Optional should not be used anywhere except the method return value.

56. Write documentation comments for all public API elements

To write API documentation correctly, you must include documentation comments before each exposed class, interface, constructor, method, and field declaration.

The documentation comments for methods should concisely describe the conventions between the method and its caller, including:

  1. Explain what the method does, not how it is done.
  2. All preconditions and postconditions of the method should be listed.
  3. Explain the side effects of the method. For example, start a new background thread.
  4. The necessary @param, @return and @throw annotations should be included.

The following is a method comment that meets the above requirements:

/**
* Returns the element at the specified position in this list.
**
<p>This method is <i>not</i> guaranteed to run in constant
* time. In some implementations it may run in time proportional
* to the element position.
**
@param index index of element to return; must be
* non-negative and less than the size of this list
* @return the element at the specified position in this list
* @throws IndexOutOfBoundsException if the index is out of range
* ({@code index < 0 || index >= ()})
*/
E get(int index);

When writing documentation for generic types or methods, make sure to specify all type parameters:

/**
* An object that maps keys to values. A map cannot contain
* duplicate keys; each key can map to at most one value.
**
(Remainder omitted)
**
@param <K> the type of keys maintained by this map
* @param <V> the type of mapped values
*/
public interface Map<K, V> { ... }

For enum types, the document needs to override the enum type itself, constants, and exposed methods:

/**
* An instrument section of a symphony orchestra.
*/
public enum OrchestraSection {
/** Woodwinds, such as flute, clarinet, and oboe. */
WOODWIND,
/** Brass instruments, such as french horn and trumpet. */
BRASS,
/** Percussion instruments, such as timpani and cymbals. */
PERCUSSION,
/** Stringed instruments, such as violin and cello. */
STRING;
}

For annotation types, the document covers the annotation type itself and all members:

/**
* Indicates that the annotated method is a test method that
* must throw the designated exception to pass.
*/
@Retention()
@Target()
public @interface ExceptionTest {
/**
* The exception that the annotated test method must throw
* in order to pass. (The test is permitted to throw any
* subtype of the type described by this class object.)
*/
Class<? extends Throwable> value();
}

Whether the class or static method is thread-safe or not, it should indicate its thread-safe level. If a class is serializable, then its serialized form should be stated.

Javadoc, which comes with Java, will automatically generate documents based on the code and check whether the documents comply with many of the suggestions mentioned in this entry. In Java 8 and Java 9, automatic checking is enabled by default. IDE plugins such as checkstyle are checking if they are in compliance

General Programming

57. Minimize the scope of local variables

The most effective way to minimize the scope of a local variable is to declare it where it is used for the first time.

Each local variable declaration should contain an initialization expression. If there is not enough information to initialize, the declaration should be postponed. An exception to this rule is the try-catch statement. If a variable is to be used outside the try block, it must be declared before the try block. At this time, there may not be enough information to initialize the variable.

The second way to minimize the scope of local variables is to use a for loop. If a variable is no longer used after the loop, then using a for loop is better than while loop. Here is an example of a for-each loop:

// Preferred idiom for iterating over a collection or array
for (Element e : c) {
    ... // Do Something with e
}

When iterator or remove is needed, use traditional for loop instead of for-each loop:

// Idiom for iterating when you need the iterator
for (Iterator<Element> i = (); (); ) {
    Element e = ();
    ... // Do something with e and i
}

The following example illustrates why the for loop is better than the while loop, because this example implies a bug (wrongly write i2 as i):

Iterator<Element> i = ();
while (()) {
    doSomething(());
}
...
Iterator<Element> i2 = ();
while (()) { // BUG!
    doSomethingElse(());
}

If you change to using a for loop, you can find this problem during compilation:

for (Iterator<Element> i = (); (); ) {
Element e = ();
... // Do something with e and i
}
...
// Compile-time error - cannot find symbol i
for (Iterator<Element> i2 = (); (); ) {
Element e2 = ();
... // Do something with e2 and i2
}

Another advantage of for loops over while loops is that it is shorter and has better readability. As shown in the following example:

for (int i = 0, n = expensiveComputation(); i < n; i++) {
    ... // Do something with i;
}

The third way to minimize the scope of local variables is to keep the method small and concentrated. Example A method contains two different logics, and a variable is only used in one of the logic. Then this method can be logically split into two different methods.

58. For-each loops are better than traditional for loops

Here is an example of using a traditional for loop to traverse containers:

// Not the best way to iterate over a collection!
for (Iterator<Element> i = (); (); ) {
    Element e = ();
    ... // Do something with e
}

It is much simpler to use for-each instead:

// The preferred idiom for iterating over collections and arrays
for (Element e : elements) {
    ... // Do something with e
}

Using traditional for loops in nested loops is more likely to cause bugs than for-each loops. For example, the code using traditional for loops implies bugs because the number of times () is executed exceeds expectations:

// Can you spot the bug?
enum Suit { CLUB, DIAMOND, HEART, SPADE }
enum Rank { ACE, DEUCE, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT,NINE, TEN, JACK, QUEEN, KING }
...
static Collection<Suit> suits = (());
static Collection<Rank> ranks = (());
List<Card> deck = new ArrayList<>();
for (Iterator<Suit> i = (); (); )
	for (Iterator<Rank> j = (); (); )
		(new Card((), ()));

Change to using for-each loops can avoid this problem:

// Preferred idiom for nested iteration on collections and arrays
for (Suit suit : suits)
	for (Rank rank : ranks)
		(new Card(suit, rank));

But there are three situations that should not be used for-each:

  1. Destructive filtering: if neededDelete elements during traversal, then iterator and remove methods should be used. In Java 8, you can use the removeIf method provided in the Collection class to achieve the same effect.
  2. Conversion: If you need to replace the values ​​of some elements when iterating through List or array, you need to use an iterator or array index.
  3. Parallel iteration: If you need to traverse multiple containers in parallel, you need to use an iterator to control the iteration progress yourself.

The for-each loop can also be used to iterate through any object that implements the Iterable interface.

59. Understand and use the library

Sometimes programmers like to build some wheels themselves. For example, implement a method to obtain random numbers:

// Common but deeply flawed!
static Random rnd = new Random();
static int random(int n) {
    return (()) % n;
}

This method not only fails to achieve uniform output distribution, but may report an error when running in boundary conditions.

The correct way to do this is to use the Random class in the java library. After Java 7, ThreadLocalRandom with better performance should be selected instead of Random.

The benefits of using libraries are:

  1. Use expert knowledge to implement specific complex algorithms or logic.

  2. Let the caller focus on business development without spending more time on the underlying implementation.

  3. The performance of the library will continue to improve, and the caller will not need to make any extra effort.

  4. Make the code simpler and easier to maintain.

In each major version of Java, many features are added to the library, and it is worth it to understand these new features.

If there are too many contents in the library and it is difficult to master, then every programmer should at least be familiar with , andand its sub-pack. Among them, the collections framework, streams library and library should be mastered.

60. If you need accurate answers, you should avoid using float and double types.

The float and double types are mainly used for engineering and scientific computing. They are essentially binary floating point types and therefore cannot be used to represent precise results. Especially not suitable for currency calculations, because any negative power of 10 cannot be expressed accurately as float or double.

Suppose that the original $1.03 was spent and $0.42 was spent, how much money was left? The output will be 0.6100000000000000001.

(1.03 - 0.42);

Suppose there is a row of candies on the shelf, and the prices are 10 cents, 20 cents, and 30 cents... You have 1 dollar, buy the candies by number of times until you run out of money:

// Broken - uses floating point for monetary calculation!
public static void main(String[] args) {
    double funds = 1.00;
    int itemsBought = 0;
    for (double price = 0.10; funds >= price; price += 0.10) {
        funds -= price;
        itemsBought++;
    }
    (itemsBought +"items bought.");
    ("Change: $" + funds);
}

The results show that you only bought three candies, and you still have $0.399999999999999999999999999999999999999999999999999. This is the wrong answer. The solution is to use BigDecimal, int, or long for currency calculations.

Use BigDecimal calculation instead to get the correct result:

public static void main(String[] args) {
    final BigDecimal TEN_CENTS = new BigDecimal(".10");
    int itemsBought = 0;
    BigDecimal funds = new BigDecimal("1.00");
    for (BigDecimal price = TEN_CENTS;(price) >= 0;price = (TEN_CENTS)) {
        funds = (price);
    itemsBought++;
    }
    (itemsBought +"items bought.");
    ("Money left over: $" + funds);
}

useBigDecimalAnother benefit is that it has 8 rounding modes to choose from, which will be very convenient if the business needs to do numerical rounding. However, it also has two disadvantages: it is inconvenient to use compared to the original arithmetic type and is much slower.

Another solution is to use cents expressed int to calculate:

public static void main(String[] args) {
    int itemsBought = 0;
    int funds = 100;
    for (int price = 10; funds >= price; price += 10) {
        funds -= price;
        itemsBought++;
    }
    (itemsBought +"items bought.");
    ("Cash left over: " + funds + " cents");
}

61. Basic data types are better than packaging

There are three main differences between basic types and packaging types:

  1. A primitive type has only values, while a wrapper type has a different identity than its value.
  2. The basic type has only full-function values, and each packaging type has a non-function value null in addition to all the functional values ​​of the corresponding basic type.
  3. Basic types save time and space more than packaging types.

The difference between basic types and packaging types can cause some trouble. For example, the following comparator makes numerical comparisons for Integer:

// Broken comparator - can you spot the flaw?
Comparator<Integer> naturalOrder =(i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);

There is a hidden bug here. When executed(new Integer(42), new Integer(42))When the execution result is 1, not the expected 0. The reason is to use==Comparing two different Integer objects, the result is definitely false. Will==Operators are almost always wrong to apply to wrapper types.

The correct way to write it is to automatically unbox and then compare:

Comparator<Integer> naturalOrder = (iBoxed, jBoxed) -> {
    int i = iBoxed, j = jBoxed; // Auto-unboxing
    return i < j ? -1 : (i == j ? 0 : 1);
};

The following program will not print out the Unbelievable, but will throw a NullPointerException. The reason is that when a basic type and a packaging type are mixed in operation, the packaging type will be automatically unboxed:

public class Unbelievable {
static Integer i;
public static void main(String[] args) {
    if (i == 42)
        ("Unbelievable");
    }
}

The following code performs poorly because sum is a Long type variable, which undergoes repeated unboxing and packing when calculating with the long type variable i:

// Hideously slow program! Can you spot the object creation?
public static void main(String[] args) {
    Long sum = 0L;
    for (long i = 0; i < Integer.MAX_VALUE; i++) {
        sum += i;
    }
    (sum);
}

The following occasions should use the packaging type, but not the basic type:

  1. As elements, keys, and values ​​in the container.
  2. Parameterized type and method type parameters.
  3. When making reflection method calls.

62. When other types are more suitable, avoid using strings

Strings are designed to represent text. When in other scenarios, if there are other types that are more suitable, then strings should be avoided.

Strings are a bad alternative to other value types. When reading data from an IO input stream, it is usually in the form of a string. But if the data is essentially another type, such as a numeric type, it should be converted into a suitable numeric type, such as float, int, etc.

Strings are a bad alternative to enum types. See the discussion in Article 34.

Strings are a bad alternative to aggregate types. If an entity has multiple components, it is not suitable to represent it as a single string. As shown below:

// Inappropriate use of string as aggregate type
String compoundKey = className + "#" + ();

There are the following disadvantages:

  1. If the delimiter # appears in any field, it will cause confusion.
  2. To access individual fields, parsing strings requires additional performance costs.
  3. The equals, toString and compareTo methods cannot be provided by themselves, and the corresponding methods provided by String can only be used.

A better approach is to represent this aggregate entity as a class, usually a private static member class.

Strings are not a good replacement for capabilities. For example, before Java 1.2 introduced ThreadLocal, someone proposed the following thread local variable design, using string type keys to identify each thread local variable:

// Broken - inappropriate use of string as capability!
public class ThreadLocal {
    private ThreadLocal() { } // Noninstantiable

    // Sets the current thread's value for the named variable.
    public static void set(String key, Object value);

    // Returns the current thread's value for the named variable.
    public static Object get(String key);
}

The problem with this design is that the string key provided by the caller must be unique. If two callers happen to use the same string key for their respective thread local variables, then they will accidentally share a variable, causing potential bugs.

An improvement is to replace the string with a non-forgery key (sometimes called capability):

public class ThreadLocal {
    private ThreadLocal() { } // Noninstantiable

    public static class Key { // (Capability)
        Key() { }
}

// Generates a unique, unforgeable key
public static Key getKey() {
    return new Key();
}

public static void set(Key key, Object value);

public static Object get(Key key);
}

Make further optimization. The key is no longer the key value of a thread local variable, but becomes the thread local variable itself:

public final class ThreadLocal {
    public ThreadLocal();
    public void set(Object value);
    public Object get();
}

The final optimization is modified to a class that supports generics. This ensures type safety.

public final class ThreadLocal<T> {
    public ThreadLocal();
    public void set(T value);
    public T get();
}

63. Beware of performance problems caused by string concatenation

Using the string concatenation operator (+) to concatenate n strings requires n square time. Because when two strings are concatenated, the contents of these two strings will be copied.

For details, please see this articleStitching strings suggest stringbuilder

Do not use string concatenators to manipulate multiple strings unless performance doesn't matter.

64. Reference objects through interfaces

If there is a suitable interface type, the parameters, return values, variables, and fields should be declared using the interface type. The following example follows this guideline:

// Good - uses interface as type
Set<Son> sonSet = new LinkedHashSet<>();

Instead of doing this like the following example:

// Bad - uses class as type!
LinkedHashSet<Son> sonSet = new LinkedHashSet<>();

Using interfaces as types makes the program more flexible. For example, you can flexibly adjust the implementation of Set:

Set<Son> sonSet = new HashSet<>();

If no suitable interface exists, it is perfectly appropriate to refer to objects with classes. At this time, the class that provides the lowest level of the required functions should be used.

65. Interface is better than reflection

The reflection mechanism provides programming access to any class. The functions provided by reflection include:

  1. Gets the class member name, field type, and method signature.
  2. Constructs an instance of the class, calls a method of the class, and accesses a field of the class.
  3. Allows one class to use another class that does not exist at compile time.

But reflection also has some disadvantages:

  1. Lost all the benefits of compile-time type checking, including exception checking.
  2. The code required to perform reflection access is longer than normal code.
  3. Reflection method calls are much slower than normal method calls.

For many programs, they must use classes that cannot be obtained at compile time. Instances can be created with reflection and accessed through their interfaces or superclasses.

The following example is a creationSet<String>The program of the instance, the class is specified by the first command line parameter, and the remaining command line parameters are inserted into the collection and printed:

// Reflective instantiation with interface access
public static void main(String[] args) {

    // Translate the class name into a Class object
    Class<? extends Set<String>> cl = null;
    try {
        cl = (Class<? extends Set<String>>) // Unchecked cast!
        (args[0]);
    } catch (ClassNotFoundException e) {
        fatalError("Class not found.");
    }

    // Get the constructor
    Constructor<? extends Set<String>> cons = null;
    try {
        cons = ();
    } catch (NoSuchMethodException e) {
        fatalError("No parameterless constructor");
    }

    // Instantiate the set
    Set<String> s = null;
    try {
        s = ();
    } catch (IllegalAccessException e) {
        fatalError("Constructor not accessible");
    } catch (InstantiationException e) {
        fatalError("Class not instantiable.");
    } catch (InvocationTargetException e) {
        fatalError("Constructor threw " + ());
    } catch (ClassCastException e) {
        fatalError("Class doesn't implement Set");
    }

    // Exercise the set
    ((args).subList(1, ));
    (s);
}

private static void fatalError(String msg) {
    (msg);
    (1);
}

66. Use local methods with caution

The Java Local Interface (JNI) allows Java programs to call local methods. The local method has three uses:

  1. Provides access to platform-specific features such as registry.
  2. Provides access to existing local code bases.
  3. Writing performance-oriented parts of your application in a native language to improve performance.

It is legal to use local methods to access platform-specific mechanisms, but rarely necessary. Because with the development of Java, it continues to provide features that were previously available on a specific platform.

For performance improvement, it is rarely recommended to use local methods. It is also because Java is constantly optimizing the underlying class library, and many times local methods do not have obvious performance advantages.

There are serious disadvantages to using local methods:

  1. No longer immune to memory destruction errors.
  2. Poor portability and harder to debug.
  3. The garbage collector cannot automatically track local memory usage.
  4. Need to glue code, it is even harder to maintain.

67. Optimize wisely

There are three mottos about optimization that everyone should know:

  1. William A. Wulf: There are more mistakes made in the name of efficiency (not necessarily possible) than any other single reason (including blind stupidity).
  2. Donald E. Knuth: Don't worry about small gains and losses in efficiency. In 97% of the cases, immature optimization is the root of all problems.
  3. M. A. Jackson: Don't optimize until you have absolutely clear unoptimized solutions.

The above motto shows: Optimization can easily lead to more harm than good, and premature optimization is the source of all evil.

Don't sacrifice reasonable architecture for performance. Try to write good programs, not fast programs. If a good program is not fast enough, then start optimizing.

Try to avoid performance-limiting design decisions. The hardest changes in design are those components that interact with the outside world. Such as APIs, line protocols, and persistent data formats. These components can have significant limitations on system performance.

Consider the performance consequences of API design decisions. For example, if a common type is variable, a large number of unnecessary defensive copies may be required.

Typically, a good API design is consistent with a good performance. Changing the API for good performance is a very bad idea. Because the performance of the API may be improved in future versions, the problems caused by changing the API will always exist.

Performance is measured before and after each attempt to optimize. Your own estimates may be different from the actual measurement results. Sometimes the optimizations spent a lot of time without significant performance improvements.

68. Abide by widely recognized naming agreements

Naming conventions are divided into two categories: typesetting and grammar.

Typesetting agreements include:

  1. Package names and module names should be layered and separated by periods between components.

  2. The package name begins with the domain name of the current organization and reverses the component, such as. The rest of the package name should be composed of one or more components describing the package, such as util, awt.

  3. Class and interface names, including enumeration and comment type names, should consist of one or more words, with the initial letter of each word capitalized, such as List and FutureTask.

  4. Methods and field names follow the same typesetting conventions as class and interface names, except that the first letter of the method or field name should be lowercase, such as remove and ensureCapacity.

  5. The name of a constant field should be composed of one or more capital words, separated by an underscore, such as NEGATIVE_INFINITY.

  6. Local variable names have similar typesetting naming conventions to member names, but abbreviations are allowed, such as i, denom, and houseNum.

  7. Type parameter names are usually composed of a single letter. The most common ones are one of the following five types: T represents any type, E represents the element type of the set, K and V represent the key and value types of Map, X represents the exception, and R represents the return type of the function.

For examples of typesetting agreements, please refer to the following table:

Identifier type example
Package or module ,
Class or interface Stream, FutureTask, LinkedHashMap,HttpClient
Method or field remove, groupingBy, getCrc
Constant fields MIN_VALUE, NEGATIVE_INFINITY
Local variables i, denom, houseNum
Type parameters T, E, K, V, X, R, U, V, T1, T2

Syntax conventions include:

  1. Instantized classes, including enum types, are usually named using one or more noun phrases, such as Thread, PriorityQueue, or ChessPiece.
  2. Utility classes that are not instantiated are usually named using plural nouns, such as Collectors or Collections.
  3. The name of an interface is similar to a class, such as Collection or Comparator, or an adjective ending with a able or ible, such as Runnable, Iterable, or Accessible.
  4. Methods that perform certain operations are usually named after verbs or verb phrases, for example, append or drawImage.
  5. The name of a method that returns a boolean value usually starts with the word is or has followed by a noun or adjective, such as isDigit, isProbablePrime, isEmpty, isEnabled, or hasSiblings.
  6. Methods to get the properties of an object are usually named using nouns, noun phrases, or verb phrases starting with get, such as size, hashCode, or getTime.
  7. The names of fields of type boolean are usually similar to the boolean accessor method, omitting the initial value is, such as initialized, composite.

You can see the detailsAlibaba Java Development Manual (Huangshan Edition)

abnormal

9. Use try-with-resources instead of try-finally

Cause attentionDetailed explanation of exceptions

69. Use exceptions only when there are exception conditions

The following code is used to iterate over an array. However, the writing method is very bad, and you need to read it carefully to understand the purpose:

// Horrible abuse of exceptions. Don't ever do this!
try {
    int i = 0;
    while(true)
        range[i++].climb();
    }
    catch (ArrayIndexOutOfBoundsException e) {
}

The normal way to write it should be:

for (Mountain m : range)
    ();

The reason for this writing may be that the code writer mistakenly believes that the standard for-each loop performs poorly in end judgment, and should use an exception mechanism to improve performance. There are three misunderstandings about this idea:

  1. Exceptions are designed for special occasions, so JVM implementations almost never make them as fast as explicit judgments.
  2. Putting the code in a try-catch block suppresses some optimizations that the JVM may perform.
  3. Standard idioms for traversing arrays do not necessarily lead to redundant checks. Many JVM implementations optimize them.

In fact, creating an exception object is about 20 times more time-consuming than establishing a normal Object. For details, please see this article.Exceptional time-consuming

And using exceptions for loop termination, the performance is much lower than the standard usage. And doing so may also lead to a cover-up of the true array's out-of-bounds error. Therefore, exceptions are only applicable to situations where there are indeed exceptions, and they should not be used in general control processes.

The design of APIs should follow the same idea. For example, the hasNext() and next() methods should be provided in the iterator. We call next() "state dependency" because it needs to pass a "state test" method hasNext() to determine whether the call is legal. We use hasNext() to determine whether the loop should be terminated:

for (Iterator<Foo> i = (); (); ) {
    Foo foo = ();
    ...
}

Instead of using exceptions to terminate the loop:

// Do not use this hideous code for iteration over a collection!
try {
    Iterator<Foo> i = ();
    while(true) {
        Foo foo = ();
        ...
    }
}
catch (NoSuchElementException e) {
}

In addition to providing "state testing", another design idea is to let the "state dependency" method return an Optional object, or return null when the calculation cannot be performed.

70. Use detected exceptions for recoverable situations, and use runtime exceptions for programming errors.

Java provides three types of throwable items: checked exception, runtime exception and error.

The case where the detected exception is used is to expect the caller to recover from it. The other two throwable items are non-checked.

Use runtime exceptions to indicate programming errors. For example, array out of bounds ArrayIndexOutOfBoundsException. If you have questions about choosing whether to select the detected exception or the runtime exception, it is recommended to use the runtime exception.

Errors are reserved for use by the JVM to indicate: insufficient resources, unrecoverable failures, or other conditions that cause execution to fail to continue. Don't define new error types yourself.

Please see for detailsDetailed explanation of exceptions

71. Avoid unnecessarily using detected exceptions

Using detected exceptions should satisfy: proper use of the API cannot prevent exceptions, and using the API can take some useful actions when encountering exceptions; otherwise, non-checked exceptions should be used.

} catch (TheCheckedException e) {
    throw new AssertionError(); // Can't happen!
}
} catch (TheCheckedException e) {
    (); // Oh well, we lose.
    (1);
}

Both of the above methods of handling detected exceptions are bad.

The detected exception puts additional burden on the programmer. The easiest way to eliminate the detected exception is to return an Optional object of the desired result type, and an empty Optional object when there is a detected exception. The disadvantage of this method is that it cannot provide additional information to explain why the calculation cannot be continued.

Another way to eliminate the detected exception is to split the method logic. For example, the following method originally required to catch the detected exception:

// Invocation with checked exception
try {
    (args);
}
catch (TheCheckedException e) {
    ... // Handle exceptional condition
}

Its logic can be split: if the parameters are legal, then the logic of the first part is not detected; otherwise, the logic of the second part is handled by the exception condition.

// Invocation with state-testing method and unchecked exception
if ((args)) {
    (args);
}
else {
    ... // Handle exceptional condition
}

If we are sure that the call must be successful, or if we don't mind the call failure causing the thread to abort, we can even simplify the logic to the following statement:

(args);

72. Encourage reuse standard exceptions

The Java library provides a standard set of exceptions that cover most daily exception throwing requirements.

The benefit of reusing exceptions is that the code is easy to read and maintain.

This table summarizes the most common reusable exceptions:

Exception Occasion for Use
IllegalArgumentException Non-null parameter values ​​are inappropriate
IllegalStateException Object state does not apply to method calls
NullPointerException Prohibit null still be passed in when the parameter is null
IndexOutOfBoundsException Index parameter values ​​are out of range
ConcurrentModificationException Detected in a place where concurrent modification objects are prohibited
UnsupportedOperationException The object does not support this method call

Do not reuse Exception, RuntimeException, Throwable, or Error directly, these classes should be treated as abstract classes. The actual exception class used should be the inheritance class of these classes.

Exception multiplexing must be based on documented semantics, not just on name. Also, if you want to add more details, you can subclass standard exceptions.

73. Throw an exception that matches the abstract level

To ensure the hierarchy of abstraction, the high-level should capture the low-level exception and ensure that the thrown exception can be explained by the high-level abstraction. This idiom is called exception conversion:

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

Here is an example of exception conversion from the AbstractSequentialList class:

/**
* Returns the element at the specified position in this list.
* @throws IndexOutOfBoundsException if the index is out of range
* ({@code index < 0 || index >= size()}).
*/
public E get(int index) {
    ListIterator<E> i = listIterator(index);
    try {
        return ();
    }
    catch (NoSuchElementException e) {
        throw new IndexOutOfBoundsException("Index: " + index);
    }
}

If low-level exceptions help debug problems with high-level exceptions, a special form of exception conversion called chain exceptions is needed. Low-level exceptions are passed to high-level exceptions as a cause. High-level exceptions provide an accessor method (Throwable's getCause method) to access low-level exceptions:

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

The implementation code of this chain exception is as follows:

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

While exception conversions can help shield low-level exceptions, they should not be abused. A better approach is to ensure that low-level methods avoid exceptions, such as checking their validity before passing parameters of high-level methods to the lower-level.

Another way to block low-level exceptions is to let the high-level handle these exceptions silently. For example, some appropriate logging tools (such as ) can be used to log exceptions.

74. Record all exceptions that will be thrown for each method.

It is very important to carefully record all exceptions thrown by each method.

Always declare the detected exception separately and use the Javadoc's @throw tag to accurately record the conditions for each exception being thrown.

Use the @throw tag to record every exception that will be thrown by the Javadoc method, but do not use the throws keyword for non-checked exceptions.

If many methods in a class throw exceptions for the same reason, you can log exceptions in the class's documentation comments instead of logging exceptions individually for each method. For example, a NullPointerException can be described in the class's documentation comments like this: "If a null object reference is passed in any parameter, all methods in the class will throw a NullPointerException".

75. Detailed messages should contain failed capture information

To catch a failed exception, the detailed message of the exception should contain the values ​​of all parameters and fields that caused the exception. For example, the detailed message for IndexOutOfBoundsException should contain index values ​​that are lower bound, upper bound, and failed to lie between lower bound.

Detailed messages should not contain sensitive information such as passwords and encryption keys.

One way to ensure that the exception contains enough fault capture information in its details is to configure it in its constructor instead of introducing it in a string:

/**
* Constructs an IndexOutOfBoundsException.
**
@param lowerBound the lowest legal index value
* @param upperBound the highest legal index value plus one
* @param index the actual index value
*/
public IndexOutOfBoundsException(int lowerBound, int upperBound, int index) {
    // Generate a detail message that captures the failure
    super(("Lower bound: %d, Upper bound: %d, Index: %d",lowerBound, upperBound, index));
    // Save failure information for programmatic access
     = lowerBound;
     = upperBound;
     = index;
}

76. Try to ensure the atomicity of the fault

A failed method call should restore the object to its state before the call. Methods with this property are called fault atomicity.

There are several ways to ensure the atomicity of the fault:

  1. Design immutable objects.

  2. For methods that operate on variable objects, check the validity of parameters before performing the operation:

    public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        Object result = elements[--size];
        elements[size] = null; // Eliminate obsolete reference
        return result;
    }
    
  3. Sorting the calculations so that the parts that may fail occur before the parts that modify the object.

  4. Execute the operation as a temporary copy of the object and replace the object's contents with the temporary copy after the operation is completed.

  5. Write recovery code to intercept failures that occur during operation and roll back the object to the state before the operation begins. This approach is mainly used for persistent (disk-based) data structures.

Failure atomicity is not always possible. For example, multithreading modifies the same container class object, causing a ConcurrentModificationException to be thrown, which is unrecoverable at this time.

77. Don't ignore exceptions

The above empty catch block violates the purpose of the exception. The exception exists to force you to handle the exception situation.

// Empty catch block ignores exception - Highly suspect!
try {
    ...
}
catch (SomeException e) {
}

It is appropriate to ignore exceptions in some cases, such as when closing FileInputStream. You have not changed the status of the file, so there is no need to perform any recovery operations, and there is no reason to abort the ongoing operation. You can also choose to record exceptions. If you choose to ignore the exception, the catch block should contain a comment explaining why it is appropriate to do so, and the variable should be named ignore:

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

concurrent

78. Synchronous access to shared variable data

Synchronization not only prevents the object modified by threads from being in an inconsistent state, but also ensures that the result of each thread's modification is visible to other threads.

Synchronization is necessary for reliable communication between threads and mutual exclusion.

Even if it is atomic reading and writing, not setting up synchronization can have bad consequences. The following code shows that you stop another thread from one thread, instead of using , but by setting flag variables:

// Broken! - How long would you expect this program to run?
public class StopThread {
    private static boolean stopRequested;

    public static void main(String[] args) throws InterruptedException {
        Thread backgroundThread = new Thread(() -> {
        int i = 0;
        while (!stopRequested)
            i++;
        });

    ();
    (1);
    stopRequested = true;
    }
}

On some machines, threads never stop. This is because without synchronization, the virtual machine may place the following code

while (!stopRequested)
    i++;

Optimized to the following code:

if (!stopRequested)
    while (true)
        i++;

The solution to the above problem is to synchronously read and write the stopRequested variable, and the program will end immediately:

// Properly synchronized cooperative thread termination
public class StopThread {
    private static boolean stopRequested;

    private static synchronized void requestStop() {
        stopRequested = true;
    }

    private static synchronized boolean stopRequested() {
        return stopRequested;
    }

    public static void main(String[] args) throws InterruptedException {
        Thread backgroundThread = new Thread(() -> {
            int i = 0;
            while (!stopRequested())
            i++;
        });

        ();
        (1);
        requestStop();
    }
}

Simultaneous writing methods are not enough. Unless both read and write operations are synchronized, synchronization cannot be guaranteed to take effect.

A simpler and more efficient approach is to usevolatile. Although volatile does not guarantee mutual exclusion, it guarantees that any thread reading the field will see the recently modified value:

// Cooperative thread termination with a volatile field
public class StopThread {
    private static volatile boolean stopRequested;

    public static void main(String[] args) throws InterruptedException {
        Thread backgroundThread = new Thread(() -> {
        int i = 0;
        while (!stopRequested)
            i++;
    });

    ();
    (1);
    stopRequested = true;
    }
}

Note that volatile does not guarantee the atomicity of variable reading and writing, so the following code cannot guarantee that the sequence number generated each time is strictly incremented:

// Broken - requires synchronization!
private static volatile int nextSerialNumber = 0;

public static int generateSerialNumber() {
    return nextSerialNumber++;
}

The solution is to use atomic variables, such asatomic class atomic

// Lock-free synchronization with 
private static final AtomicLong nextSerialNum = new AtomicLong();

public static long generateSerialNumber() {
    return ();
}

To avoid the problems that occur in this entry, the best way is to not share variable data. Variable data should be constrained to one thread.

79. Avoid excessive synchronization

To avoid activity failures and safety failures, never hand over control to the user in a synchronization method or block.

To demonstrate this problem, the following code implements the observer pattern, allowing users to subscribe to notifications when elements are added to the collection:

// Broken - invokes alien method from synchronized block!
public class ObservableSet<E> extends ForwardingSet<E> {
    public ObservableSet(Set<E> set) { super(set); }

    private final List<SetObserver<E>> observers= new ArrayList<>();

    public void addObserver(SetObserver<E> observer) {
        synchronized(observers) {
            (observer);
        }
    }

    public boolean removeObserver(SetObserver<E> observer) {
        synchronized(observers) {
            return (observer);
        }
    }

    private void notifyElementAdded(E element) {
        synchronized(observers) {
            for (SetObserver<E> observer : observers)
                (this, element);
        }
    }

    @Override
    public boolean add(E element) {
        boolean added = (element);
        if (added)
            notifyElementAdded(element);
        return added;
    }

    @Override
    public boolean addAll(Collection<? extends E> c) {
        boolean result = false;
        for (E element : c)
            result |= add(element); // Calls notifyElementAdded
        return result;
    }
}

The observer subscribes to the notification by calling the addObserver method and unsubscribe from the removeObserver method:

@FunctionalInterface
public interface SetObserver<E> {
    // Invoked when an element is added to the observable set
    void added(ObservableSet<E> set, E element);
}

A rough check, ObservableSet seems to work just fine. For example, print numbers from 0 to 99:

public static void main(String[] args) {
    ObservableSet<Integer> set =new ObservableSet<>(new HashSet<>());
    ((s, e) -> (e));
    for (int i = 0; i < 100; i++)
        (i);
}

Now let's try something more imaginative. Suppose we replace the addObserver call with a call passing the observer, which prints the integer value added to the collection, and if the value is 23, the call will delete itself:

(new SetObserver<>() {
    public void added(ObservableSet<Integer> s, Integer e) {
        (e);
        if (e == 23)
            (this);
    }
});

You may expect this program to print numbers 0 to 23 and then terminate. But in fact it will throw a ConcurrentModificationException. Although we add concurrency to observers, we cannot prevent concurrent modifications to it.

Now let's try something weird: write an observer to unsubscribe, but instead of calling removeObserver directly, it starts another thread using executor to execute:

// Observer that uses a background thread needlessly
(new SetObserver<>() {
    public void added(ObservableSet<Integer> s, Integer e) {
        (e);
        if (e == 23) {
            ExecutorService exec = ();
            try {
                (() -> (this)).get();
            } catch (ExecutionException | InterruptedException ex) {
                throw new AssertionError(ex);
            } finally {
                ();
            }
        }
    }
});

The result is that it will not throw an exception, but will form a deadlock. The reason is that the main thread keeps holding the observers lock after calling addObserver and waits for the child thread to execute. However, the child thread calls removeObserver and also needs to obtain the observers lock to form a circular dependency.

One solution is to move the code that traverses the collection outside the synchronization block:

// Alien method moved outside of synchronized block - open calls
private void notifyElementAdded(E element) {
    List<SetObserver<E>> snapshot = null;
    synchronized(observers) {
        snapshot = new ArrayList<>(observers);
    }
    for (SetObserver<E> observer :snapshot)
        (this, element);
}

Another better approach is to use CopyOnWriteArrayList, suitable for occasions where there are few modifications and frequent traversals:

// Thread-safe observable set with CopyOnWriteArrayList
private final List<SetObserver<E>> observers =new CopyOnWriteArrayList<>();

public void addObserver(SetObserver<E> observer) {
    (observer);
}

public boolean removeObserver(SetObserver<E> observer) {
    return (observer);
}

private void notifyElementAdded(E element) {
    for (SetObserver<E> observer : observers)
        (this, element);
}

As little work as possible should be done in the synchronization area, removing time-consuming code from the synchronization block.

Selection of some Java class libraries:

  1. Replace Vector and Hashtable with the class in it.
  2. In a single threaded environment, StringBuilder replaces StringBuffer.
  3. In a single-threaded environment, replace Random with ThreadLocalRandom.

80. Executor, task, and stream are better than using threads directly

Executor framework is very convenient to use:

ExecutorService exec = ();
(runnable);
();

Executor should be used instead of threads directly. The latter is both a work unit and an execution mechanism; the former has made a good separation of the work unit and an execution mechanism, and can flexibly select the execution mechanism according to actual conditions.

81. Concurrency utility is better than wait-notify

Using wait-notify directly is as raw as programming with "concurrent assembly language", you should use higher-level concurrency utility.

The concurrent set interface is equipped with state-dependent modification operations that combine multiple basic operations into a single atomic operation. For example, the following example demonstrates the use of the putIfAbsent(key, value) method of Map, which is used to simulate the behavior of the implementation:

// Concurrent canonicalizing map atop ConcurrentMap - not optimal
private static final ConcurrentMap<String, String> map =new ConcurrentHashMap<>();
public static String intern(String s) {
    String previousValue = (s, s);
    return previousValue == null ? s : previousValue;
}

In fact, it can be further optimized. ConcurrentHashMap is optimized for retrieval operations such as get. Therefore, it is only worth calling putIfAbsent if get indicates that it is necessary:

// Concurrent canonicalizing map atop ConcurrentMap - faster!
public static String intern(String s) {
    String result = (s);
    if (result == null) {
        result = (s, s);
        if (result == null)
        result = s;
    }
    return result;
}

Use concurrent collections instead of synchronous collections. For example, use ConcurrentHashMap instead.

The following example shows how to build a simple framework to time concurrent execution of an operation. It would be a bit troublesome to implement this logic directly on wait and notify, but it would be very simple to implement on CountDownLatch:

// Simple framework for timing concurrent execution
public static long time(Executor executor, int concurrency,Runnable action) throws InterruptedException {
    CountDownLatch ready = new CountDownLatch(concurrency);
    CountDownLatch start = new CountDownLatch(1);
    CountDownLatch done = new CountDownLatch(concurrency);

    for (int i = 0; i < concurrency; i++) {
        (() -> {
            (); // Tell timer we're ready
            try {
                (); // Wait till peers are ready
                ();
            } catch (InterruptedException e) {
                ().interrupt();
            } finally {
                (); // Tell timer we're done
            }
        });
    }

    (); // Wait for all workers to be ready
    long startNanos = ();
    (); // And they're off!
    (); // Wait for all workers to finish
    return () - startNanos;
}

For interval timing, always use instead of . Not only is it more accurate and accurate, but it is not affected by real-time clock adjustment of the system.

Sometimes, waiting-notify is required to maintain old code. Here are the basic usage of wait-notify:

// The standard idiom for using the wait method
synchronized (obj) {
    while (<condition does not hold>)
        (); // (Releases lock, and reacquires on wakeup)
    ... // Perform action appropriate to condition
}

Always use a loop to call the wait method. The notifyAll method should usually take precedence over notify. If notify is used, you must be very careful to ensure its activity.

82. Make thread-safe documenting

Each class should be described in detail or recorded with thread-safe annotations.

Don't rely on the synchronized modifier to record thread-safe, it is just implementation details, not part of the API, and it cannot reliably indicate that the method is thread-safe.

The class must carefully record the thread safety level it supports, which can be divided into:

  1. Immutable: Constants, no synchronization is required, such as String and Integer.
  2. Unconditional thread safety: mutable but with enough internal synchronization without relying on external synchronization, such as AtomicLong and ConcurrentHashMap.
  3. Conditional thread safety: Similar to unconditional thread safety, but some methods require external synchronization to be used, such as the collection returned by the wrapper, whose iterators require external synchronization.
  4. Non-thread-safe: mutable, requires external synchronization to surround each method call, such as ArrayList and HashMap.
  5. Thread opposition: Even if each method call is surrounded by external synchronization, it is not thread-safe.

Be careful when recording a conditional thread-safe class, pointing out which calling methods require external synchronization. For example, when traversing the collection, you need to synchronize on the map:

Map<K, V> m = (new HashMap<>());
Set<K> s = (); // Needn't be in synchronized block
...
synchronized(m) { // Synchronizing on m, not s!
    for (K key : s)
        ();
}

In order to prevent users from maliciously holding public locks for a long time, private locks can be used and placed inside the methods provided to the outside:

// Private lock object idiom - thwarts denial-of-service attack
private final Object lock = new Object();
public void foo() {
    synchronized(lock) {
        ...
    }
}

The Lock field should always be declared final to prevent inadvertent modifications to it.

83. Use delayed initialization wisely

In most cases, regular initialization is better than delayed initialization. Delay initialization only does this if necessary.

Delay initialization is suitable for scenarios: If a field is accessed only on a small part of the class, and initializing the field is expensive, then deferred initialization can be considered. likeSingleton mode

In the case of multi-thread competition, using delayed initialization can easily lead to errors.

Here is an example of a regular initialization:

// Normal initialization of an instance field
private final FieldType field = computeFieldValue();

To change to a delayed initialization version, synchronized needs to be synchronized:

// Lazy initialization of instance field - synchronized accessor
private FieldType field;
private synchronized FieldType getField() {
    if (field == null)
        field = computeFieldValue();
    return field;
}

If you need to use delay initialization on static fields to improve performance, use delay initialization of the hold class mode:

// Lazy initialization holder class idiom for static fields
private static class FieldHolder {
    static final FieldType field = computeFieldValue();
}
private static FieldType getField() { return ; }

If you need to use delayed initialization on the instance field to improve performance, use the double check mode:

// Double-check idiom for lazy initialization of instance fields
private volatile FieldType field;
private FieldType getField() {
    FieldType result = field;
    if (result == null) { // First check (no locking)
        synchronized(this) {
            if (field == null) // Second check (with locking)
                field = result = computeFieldValue();
        }
    }
    return result;
}

If repeated initialization can be tolerated, you can change to single check mode:

// Single-check idiom - can cause repeated initialization!
private volatile FieldType field;
private FieldType getField() {
    FieldType result = field;
    if (result == null)
        field = result = computeFieldValue();
    return result;
}

84. Don't rely on thread scheduler

Any program that relies on a thread scheduler to ensure correctness or performance cannot guarantee portability.

Threads should not check for shared object state changes in loops. In addition to making the program susceptible to the impermanent effects of thread schedulers, loop checking state changes also greatly increases the load on the processor and affects other threads to obtain the processor for work. For example, the CountDownLatch version implemented below is very poor:

// Awful CountDownLatch implementation - busy-waits incessantly!
public class SlowCountDownLatch {

    private int count;

    public SlowCountDownLatch(int count) {
        if (count < 0)
            throw new IllegalArgumentException(count + " < 0");
         = count;
    }

    public void await() {
        while (true) {
            synchronized(this) {
                if (count == 0)
                return;
            }
        }
    }

    public synchronized void countDown() {
        if (count != 0)
            count--;
    }
}

When there are 1000 threads competing, the execution time of the above example is 10 times that of CountDownLatch in Java.

By calling to optimize the above program, you can barely make the program run, but it is not portable. A better approach is to refactor the code and reduce the number of concurrent threads.

Similarly, do not rely on adjusting thread priorities. Thread priority is one of the most unportable features in Java.

Serialization

85. Prioritize alternatives to Java serialization

When the serialization feature was introduced into Java, it has never appeared in production languages. The designers at the time considered that the benefits outweighed the risks.

Later history proved that this was not the case. Serialization has led to serious security vulnerabilities, and because its attack range is too large and difficult to prevent, problems are still increasing.

The following example demonstrates a "deserialization bomb" that takes a long time to perform deserialization:

// Deserialization bomb - deserializing this stream takes forever
static byte[] bomb() {
    Set<Object> root = new HashSet<>();
    Set<Object> s1 = root;
    Set<Object> s2 = new HashSet<>();
    for (int i = 0; i < 100; i++) {
        Set<Object> t1 = new HashSet<>();
        Set<Object> t2 = new HashSet<>();
        ("foo"); // Make t1 unequal to t2
        (t1); (t2);
        (t1); (t2);
        s1 = t1;
        s2 = t2;
    }
    return serialize(root); // Method omitted for brevity
}

The best way to avoid serialization vulnerabilities is to never deserialize anything. There is no reason to use Java serialization in any new system you write.

To replace Java serialization, the leading cross-platform structured data are JSON and Protobuf.

If you need to use serialization in an old system, never deserialize untrusted data.

An object deserialization filtering mechanism added in Java 9 allows certain classes to be received or rejected. Prefer whitelists over blacklists, as blacklists only protect you from known threats.

86. Implement Serializable with great caution

Implementing the Serializable interface will bring the following costs:

  1. Once an implementation of a class is published, it reduces the flexibility to change the implementation of that class. The serialized form needs to be always supported.
  2. Increased the possibility of bugs and security vulnerabilities.
  3. It increases the testing burden associated with new versions of the release class.

Implementing the Serializable interface is not an easy decision.

Classes designed for inheritance are rarely suitable for implementing Serializable interfaces, and interfaces are rarely suitable for inheriting Serializable.

Inner classes should not implement Serializable.

87. Consider using a custom serialization form

Do not accept the default serialization form before considering whether it is appropriate.

If the object's physical representation is the same as its logical content, the default serialization form may be appropriate. For example, the following class represents a person's name:

// Good candidate for default serialized form
public class Name implements Serializable {
    /**
    * Last name. Must be non-null.
    * @serial
    */
    private final String lastName;

    /**
    * First name. Must be non-null.
    * @serial
    */
    private final String firstName;

    /**
    * Middle name, or null if there is none.
    * @serial
    */
    private final String middleName;
    ... // Remainder omitted
}

Even if you think the default serialization form is appropriate, you usually have to provide the readObject method to ensure invariance and security.

The following example is not suitable for using default serialization because it mirrors all items in the linked list and two-way links between these items:

// Awful candidate for default serialized form
public final class StringList implements Serializable {
    private int size = 0;
    private Entry head = null;
    private static class Entry implements Serializable {
        String data;
        Entry next;
        Entry previous;
    }
    ... // Remainder omitted
}

When the physical representation of an object differs greatly from its logical data content, there are four disadvantages to using the default serialization form:

  1. Permanently bind the exported API to the current internal implementation.
  2. Takes up too much space.
  3. Too much time spent.
  4. May cause stack overflow.

The rational serialization form of StringList is the number of strings in the list and the string itself. This constitutes the logical data represented by a StringList, removing the details of its physical representation. Here is the modified version of StringList, where the transient modifier indicates that the instance field is to be omitted from the default serialization of the class:

// StringList with a reasonable custom serialized form
public final class StringList implements Serializable {
    private transient int size = 0;
    private transient Entry head = null;
    // No longer Serializable!

    private static class Entry {
        String data;
        Entry next;
        Entry previous;
    }
    // Appends the specified string to the list
    public final void add(String s) { ... }

    /**
    * Serialize this {@code StringList} instance.
    **
    @serialData The size of the list (the number of strings
    * it contains) is emitted ({@code int}), followed by all of
    * its elements (each a {@code String}), in the proper
    * sequence.
    */
    private void writeObject(ObjectOutputStream s) throws IOException {
        ();
        (size);
        // Write out all elements in the proper order.
        for (Entry e = head; e != null; e = )
            ();
    }

    private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
        ();
        int numElements = ();
        // Read in all elements and insert them in list
        for (int i = 0; i < numElements; i++)
            add((String) ());
    }

    ... // Remainder omitted
}

Any synchronization operation must be enforced for object serialization, just like any other method that reads the entire state of the object:

// writeObject for synchronized class with default serialized form
private synchronized void writeObject(ObjectOutputStream s) throws IOException {
    ();
}

Regardless of which serialization form is chosen, the serialized version UID of the explicit version is declared in each serializable class written:

private static final long serialVersionUID = randomLongValue;

Do not change the serialized version UID unless you want to break compatibility with all instances of existing serialization.

88. Defensively write readObject method

Article 50 writes an invariant date range class containing the Date field, which ensures its invariance by copying the Date object defensively:

// Immutable class that uses defensive copying
public final class Period {
    private final Date start;
    private final Date end;

    /**
    * @param start the beginning of the period
    * @param end the end of the period; must not precede start
    * @throws IllegalArgumentException if start is after end
    * @throws NullPointerException if start or end is null
    */
    public Period(Date start, Date end) {
         = new Date(());
         = new Date(());
        if (() > 0)
            throw new IllegalArgumentException(start + " after " + end);
    }

    public Date start () { return new Date(()); }

    public Date end () { return new Date(()); }

    public String toString() { return start + " - " + end; }

    ... // Remainder omitted
}

If we make this class support Java default serialization, then a vulnerability will occur: a problematic object can be listed from the byte stream in reverse sequence, with the end time less than the start time, bypassing the limitations made by the original constructor:

public class BogusPeriod {
// Byte stream couldn't have come from a real Period instance!
    private static final byte[] serializedForm = {
        (byte)0xac, (byte)0xed, 0x00, 0x05, 0x73, 0x72, 0x00, 0x06,
        0x50, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x40, 0x7e, (byte)0xf8,
        0x2b, 0x4f, 0x46, (byte)0xc0, (byte)0xf4, 0x02, 0x00, 0x02,
        0x4c, 0x00, 0x03, 0x65, 0x6e, 0x64, 0x74, 0x00, 0x10, 0x4c,
        0x6a, 0x61, 0x76, 0x61, 0x2f, 0x75, 0x74, 0x69, 0x6c, 0x2f,
        0x44, 0x61, 0x74, 0x65, 0x3b, 0x4c, 0x00, 0x05, 0x73, 0x74,
        0x61, 0x72, 0x74, 0x71, 0x00, 0x7e, 0x00, 0x01, 0x78, 0x70,
        0x73, 0x72, 0x00, 0x0e, 0x6a, 0x61, 0x76, 0x61, 0x2e, 0x75,
        0x74, 0x69, 0x6c, 0x2e, 0x44, 0x61, 0x74, 0x65, 0x68, 0x6a,
        (byte)0x81, 0x01, 0x4b, 0x59, 0x74, 0x19, 0x03, 0x00, 0x00,
        0x78, 0x70, 0x77, 0x08, 0x00, 0x00, 0x00, 0x66, (byte)0xdf,
        0x6e, 0x1e, 0x00, 0x78, 0x73, 0x71, 0x00, 0x7e, 0x00, 0x03,
        0x77, 0x08, 0x00, 0x00, 0x00, (byte)0xd5, 0x17, 0x69, 0x22,
        0x00, 0x78
    };

    public static void main(String[] args) {
        Period p = (Period) deserialize(serializedForm);
        (p);
    }

    // Returns the object with the specified serialized form
    static Object deserialize(byte[] sf) {
        try {
            return new ObjectInputStream(new ByteArrayInputStream(sf)).readObject();
        } catch (IOException | ClassNotFoundException e) {
            throw new IllegalArgumentException(e);
        }
    }
}

The solution is to check the validity of the deserialized object in the readObject method:

// readObject method with validity checking - insufficient!
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
    ();
    // Check that our invariants are satisfied
    if ((end) > 0)
        throw new InvalidObjectException(start +" after "+ end);
}

However, there are other problems. Attackers can access the original private Date field in the Period object through deserialization. By modifying these Date instances, an attacker can modify the Period instance:

public class MutablePeriod {
    // A period instance
    public final Period period;

    // period's start field, to which we shouldn't have access
    public final Date start;

    // period's end field, to which we shouldn't have access
    public final Date end;

    public MutablePeriod() {
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream out = new ObjectOutputStream(bos);

            // Serialize a valid Period instance
            (new Period(new Date(), new Date()));

            /*
            * Append rogue "previous object refs" for internal
            * Date fields in Period. For details, see "Java
            * Object Serialization Specification," Section 6.4.
            */
            byte[] ref = { 0x71, 0, 0x7e, 0, 5 }; // Ref #5
            (ref); // The start field
            ref[4] = 4; // Ref # 4
            (ref); // The end field

            // Deserialize Period and "stolen" Date references
            ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(()));
            period = (Period) ();
            start = (Date) ();
            end = (Date) ();
        } catch (IOException | ClassNotFoundException e) {
            throw new AssertionError(e);
        }
    }
}

On the author's machine, the following output can be generated:

Wed Nov 22 00:21:29 PST 2017 - Wed Nov 22 00:21:29 PST 1978
Wed Nov 22 00:21:29 PST 2017 - Sat Nov 22 00:21:29 PST 1969
public static void main(String[] args) {
    MutablePeriod mp = new MutablePeriod();
    Period p = ;
    Date pEnd = ;

    // Let's turn back the clock
    (78);
    (p);

    // Bring back the 60s!
    (69);
    (p);
}
Wed Nov 22 00:21:29 PST 2017 - Wed Nov 22 00:21:29 PST 1978
Wed Nov 22 00:21:29 PST 2017 - Sat Nov 22 00:21:29 PST 1969

When an object is deserialized, it is crucial to defensively copy any private fields. For example:

// readObject method with defensive copying and validity checking
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
    ();
    // Defensively copy our mutable components
    start = new Date(());
    end = new Date(());
    // Check that our invariants are satisfied
    if ((end) > 0)
        throw new InvalidObjectException(start +" after "+ end);
}

After modification, the following output will be generated:

Wed Nov 22 00:23:41 PST 2017 - Wed Nov 22 00:23:41 PST 2017
Wed Nov 22 00:23:41 PST 2017 - Wed Nov 22 00:23:41 PST 2017

Here are the guidelines for writing readObject methods:

  1. The object reference field must remain private and each object in the field should be copied defensively. Variable components of immutable classes fall into this category.
  2. Check for any invariant, if the check fails, throw an InvalidObjectException. The check action should follow any defensive copy.
  3. If the entire object graph must be verified after deserialization, use the ObjectInputValidation interface.
  4. Do not directly or indirectly call any overridable method in the class.

89. For instance control, enumeration type is better than readResolve

Article 3 writes the following singleton pattern code:

public class Elvis {
    public static final Elvis INSTANCE = new Elvis();
    private Elvis() { ... }
    public void leaveTheBuilding() { ... }
}

If the class implements a serialized interface, then it is no longer a singleton. Because the readObject method returns a new instance, which is different from the instance created when the class is initialized.

However, if the readResolve method is defined in the class, deserializing the newly created object will call this method and replace the newly created object with the object returned by this method. Therefore, the singleton can be implemented through the following code:

// readResolve for instance control - you can do better!
private Object readResolve() {
    // Return the one true Elvis and let the garbage collector
    // take care of the Elvis impersonator.
    return INSTANCE;
}

If you rely on readResolve for instance control, all instance fields with object reference type must be declared transient. Otherwise, some attackers may "steal" references to the deserialized object before running the readResolve method of the deserialized object and then launch an attack.

90. Consider replacing serialized instances with serialized proxy

Using a serialization proxy can reduce the risk of using normal serialization.

The following code implements a serialization proxy.

First, write a private static inner class, whose fields are the same as those of the peripheral class, and have a constructor with the peripheral class object as the parameter:

// Serialization proxy for Period class
private static class SerializationProxy implements Serializable {
    private final Date start;
    private final Date end;
    SerializationProxy(Period p) {
         = ;
         = ;
    }
    private static final long serialVersionUID =234098243823485285L; // Any number will do (Item 87)
}

Write a writeReplace method for peripheral classes that converts instances of peripheral classes to their serialization proxy before serialization:

// writeReplace method for the serialization proxy pattern
private Object writeReplace() {
    return new SerializationProxy(this);
}

In this way, the serialization system will never generate a serialized instance of the peripheral class, but an attacker may create an instance that attempts to violate the invariance of the class. To ensure that such an attack will fail, just add this readObject method to the peripheral class:

// readObject method for the serialization proxy pattern
private void readObject(ObjectInputStream stream) throws InvalidObjectException {
    throw new InvalidObjectException("Proxy required");
}

Finally, a readResolve method is provided on the SerializationProxy class, which returns a logical equivalent instance of the peripheral class. The existence of this method causes the serialization system to convert the serialization proxy back to an instance of the peripheral class when deserialized:

// readResolve method for 
private Object readResolve() {
    return new Period(start, end); // Uses public constructor
}

However, using a serializer is usually more expensive than using a protective copy.

Previous recommendations

  • "SpringBoot" EasyExcel implements the import and export of millions of data
  • "SpringBoot" The most complete SpringBoot commentary in history
  • Detailed explanation of Spring Framework IoC Core
  • A long article of ten thousand words will take you to explore all the expansion points in Spring
  • How to implement a general interface current limiting, heavy-weight and anti-shake mechanism
  • A long article of ten thousand words will take you into the underlying data structure of Redis
  • Analysis of the most comprehensive principle of volatile keyword