General Programming

Item57: Minimize the scope of local variables

The most powerful technique for minimizing the scope of a local variable is to declare it where it is first used. Declaring a local variable prematurely can cause its scope not only to begin too early but also to end too late.

Nearly every local variable declaration should contain an initializer. If you don't yet have enough information to initialize a variable sensibly, you should postpone the declaration until you do. If a variable is initialized to an expression whose evaluation can throw a checked exception, the variable must be initialized inside a try block.

Loops present a special opportunity to minimize the scope of variables. The for loop, in both its traditional and for-each forms, allows you to declare loop variables, limiting their scope to the exact region where they're needed. We should always prefer for loops to while loops.

A final technique to minimize the scope of local variables is to keep methods small and focused.

Item58: Prefer for-each loops to traditional for loops

The traditional for loops are redundant, we can replace them with for-each loops.

for (Element e : elements) {
	... // Do something with e
}

A hard-to-find 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 = Arrays.asList(Suit.values());
static Collection<Rank> ranks = Arrays.asList(Rank.values());

List<Card> deck = new ArrayList<>();
for (Iterator<Suit> i = suits.iterator(); i.hasNext(); )
	for (Iterator<Rank> j = ranks.iterator(); j.hasNext(); )
		deck.add(new Card(i.next(), j.next()));

The bug is, that the external iterator has been invoked so many times, and it should be invoked only once in the intrnal loop. This could be a common typo, which is really hard to find with the first glance. In this way, we should use for-each loop to replace it:

for (Suit suit : suits)
	for (Rank rank : ranks)
		deck.add(new Card(suit, rank));

There are three common situations where you can't use for-each:

  • Destructive filtering—If you need to traverse a collection removing selected elements, then you need to use an explicit iterator so that you can call its remove method.
  • Transforming—If you need to traverse a list or array and replace some or all of the values of its elements, then you need the list iterator or array index in order to replace the value of an element.
  • Parallel iteration—If you need to traverse multiple collections in parallel, then you need explicit control over the iterator or index variable so that all iterators or index variables can be advanced in lockstep

Not only does the for-each loop let you iterate over collections and arrays, it lets you iterate over any object that implements the Iterable interface, which consists of a single method.

Item59: Know and use the libraries

By using a standard library, you take advantage of the knowledge of the experts who wrote it and the experience of those who used it before you. For example, the random number generator of choice is now ThreadLocalRandom.

A second advantage of using the libraries is that you don't have to waste your time writing ad hoc solutions to problems that are only marginally related to your work.

A third advantage of using standard libraries is that their performance tends to improve over time, with no effort on your part.

A fourth advantage of using libraries is that they tend to gain functionality over time.

A final advantage of using the standard libraries is that you place your code in the mainstream.

Numerous features are added to the libraries in every major release, and it pays to keep abreast of these additions, and the libraries are too big to study all the documentation, but every programmer should be familiar with the basics of java.lang, java.util, and java.io, and their subpackages.

Item60: Avoid float and double if exact answers are required

The float and double types are designed primarily for scientific and engineering calculations. They perform binary floating-point arithmetic, which was carefully designed to furnish accurate approximations quickly over a broad range of magnitudes. They do not, however, provide exact results and should not be used where exact results are required. The float and double types are particularly ill-suited for monetary calculations because it is impossible to represent 0.1 (or any other negative power of ten) as a float or double exactly.

The right way to solve this problem is to use BigDecimal, int, or long for monetary calculations.

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; funds.compareTo(price) >= 0; price = price.add(TEN_CENTS)) {
    funds = funds.subtract(price);
    itemsBought++;
  }
  System.out.println(itemsBought + " items bought.");
  System.out.println("Money left over: $" + funds);
}

An alternative to using BigDecimal is to use int or long, depending on the amounts involved, and to keep track of the decimal point yourself.

Item61: Prefer primitive types to boxed primitives

Java has a two-part type system, consisting of primitives, such as int, double, and boolean, and reference types, such as String and List. Every primitive type has a corresponding reference type, called a boxed primitive. The boxed primitives corresponding to int, double, and boolean are Integer, Double, and Boolean.

Three major differences:

  1. Primitives have only their values, whereas boxed primitives have identities distinct from their values.
  2. Primitive types have only fully functional values, whereas each boxed primitive type has one nonfunctional value.
  3. Primitives are more time- and space-efficient than boxed primitives.

Working with reference types could lead to unexpected errors:

Comparator<Integer> naturalOrder = (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);

The == operator should not be used to compare two different objects for their values' equality, while programmers could easily forget this because the reference types look so similar to their primitive types. Applying the == operator to boxed primitives is almost always wrong.

Another point: when you mix primitives and boxed primitives in an operation, the boxed primitive is auto-unboxed. In this way, if a reference type has not been initialized, a NullPointerException would be thrown.

Last but not least, frequent and too many autoboxing and autounboxing could harm the performance badly, which should be avoided.

So only use boxed primitives when necessary. For example, we must use boxed primitives as type parameters in parameterized types and methods.

Item62: Avoid strings where other types are more appropriate

  • Strings are poor substitutes for other value types. If there's an appropriate value type, whether primitive or object reference, we should use it; if there isn't, we should write one, instead of using string values directly or leave it that way.

  • Strings are poor substitutes for enum types.

  • Strings are poor substitutes for aggregate types.
  • Strings are poor substitutes for capabilities.

To summarize, avoid the natural tendency to represent objects as strings when better data types exist or can be written. Used inappropriately, strings are more cumbersome, less flexible, slower, and more error-prone than other types. Types for which strings are commonly misused include primitive types, enums, and aggregate types.

Item63: Beware the performance of string concatenation

Using the string concatenation operator repeatedly to concatenate n strings requires time quadratic in n. To achieve acceptable performance, use a StringBuilder in place of a String.

The moral is simple: Don't use the string concatenation operator to combine more than a few strings unless performance is irrelevant.

Item64: Refer to objects by their interfaces

If appropriate interface types exist, then parameters, return values, variables, and fields should all be declared using interface types.

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

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

If we get into the habit of using interfaces as types, our program will be much more flexible. But there is one caveat: if the original implementation offered some special functionality not required by the general contract of the interface and the code depended on that functionality, then it is critical that the new implementation provide the same functionality.

It is entirely appropriate to refer to an object by a class rather than an interface if no appropriate interface exists. And a second case in which there is no appropriate interface type is that of objects belonging to a framework whose fundamental types are classes rather than interfaces. The final case in which there is no appropriate interface type is that of classes that implement an interface but also provide extra methods not found in the interface.

If there is no appropriate interface, just use the least specific class in the class hierarchy that provides the required functionality.

Item65: Prefer interfaces to reflection

The core reflection facility, java.lang.reflect, offers programmatic access to arbitrary classes. This power, however, comes at a price:

  • You lose all the benefits of compile-time type checking, including exception checking.
  • The code required to perform reflective access is clumsy and verbose.
  • Performance suffers.

You can obtain many of the benefits of reflection while incurring few of its costs by using it only in a very limited form. For example, you can create instances reflectively and access them normally via their interface or superclass.

Item66: Use native methods judiciously

The Java Native Interface (JNI) allows Java programs to call native methods, which are methods written in native programming languages such as C or C++.

It is rarely advisable to use native methods for improved performance, while the use of native methods has serious disadvantages. Native languages are not safe, so that applications using native methods are no longer immune to memory corruption errors. Other factors include less portable because the dependency of platforms, harder to debug and even the decrease on performance.

Item67: Optimize judiciously

Generally speaking, there are more evils than benefits that optimization could bring to us. Don't sacrifice sound architectural principles for performance. Strive to write good programs rather than fast ones. 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 remainder of the system.

So how to improve performance? The answer is to strive to avoid design decisions that limit performance. Implementation problems can be fixed by later optimization, but pervasive architectural flaws that limit performance can be impossible to fix without rewriting the system.

We should also consider the performance consequences of your API design decisions. Luckily, it is generally the case that good API design is consistent with good performance. This means it is always a bad idea to warp an API to achieve good performance.

To avoid useless optimization, we should measure performance before and after each attempted optimization.

Item68: Adgere to generally accepted naming conventions

The Java platform has a well-established set of naming conventions, many of which are contained in The Java Language Specification. Loosely speaking, naming conventions fall into two categories: typographical and grammatical.

For quick reference, the following table shows examples of typographical conventions:

Identifier Type Example
Package or module org.junit.jupiter.api, com.google.common.collect
Class or Interface Stream, FutureTask, LinkedHashMap, HttpClient
Method or Field remove, groupingBy, getCrc
Constant Field MIN_VALUE, NEGATIVE_INFINITY
Local Variable i, denom, houseNum
Type Parameter T, E, K, V, X, R, U, V, T1, T2

Grammatical naming conventions are more flexible and more controversial than typographical conventions.