Java Effective Chapter 9 - mariamaged/Java-Android-Kotlin GitHub Wiki
The most powerful technique for minimizing the scope of a local variable is to declare it where it is
first used
.
- If a variable is declared before it is used, it is just clutter - one more thing to distract the reader who is trying to figure out what the program does.
- By the time the variable is used, the reader might not remember the variable's type or initial value.
- Declaring a local variable prematurely can cause its scope not only to begin too early but also to end too late.
- The scope of a local variable extends from the point where it is declared to the end of the enclosing block.
- If a variable is declared outside of the block in which it is used, it remains visible after the
program exists that block
.
-
Nearly every local variable declaration should contain an initializer.
- If you don't have enough information to initialize a variable sensibly, you should postpone the declaration until you do.
- One exception to this rule concerns
try catch statements
. - If a variable is initialized to an
expression
whose evaluation throws achecked exception
, the variable must be initialized inside a try block. - If the value must be used outside of the try block, then it must be declared before the try block, where it cannot yet be "sensibly" initialized.
- Keep methods small and focused.
- These idioms are better than while loops, but they aren't perfect.
- The iterator and index variables are both a clutter-all you need are the elements.
- Furthermore, they represent opportunities for error.
- The for-each loop solves all of these problems.
- It gets rids of the clutter and the opportunity for error by
hiding the iterator or index variable
. - The resulting idiom applies equally to
collections
andarrays
, easing the process of switching the implementation type of a container from one to the other:
- The advantages of the for-each loop over the traditional for loops are even greater when it comes to
nested iteration
. - Here is a common mistake that people make when doing nested iteration:
enum Suit {CLUB, DIAMOND, HEART, SPADE}
enum Rank {ACE, DEUCE, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, TEN, JACK, QUEEN, KING}
public class Effective {
static Collection<Suit> suits = Arrays.asList(Suit.values());
static Collection<Rank> ranks = Arrays.asList(Rank.values());
public static void main(String[] args) {
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()));
}
}
// Solution - NOT THE BEST
for(Iterator<Suit> i = suits.iterator(); i.hasNext();) {
Suit suit = i.next();
for(Iterator<Rank> j = ranks.iterator(); j.hasNext();)
deck.add(new Card(suit, j.next());
}
// Solution - BEST USING FOREACH LOOP
```java
for(Suit suit : suits)
for(Rank rank: ranks)
deck.add(new Card(suit, rank);
- Consider this ill-conceived attempt to print all the possible rolls of a pair of dice:
enum Face {ONE, TWO, THREE, FOUR, FIVE, SIX}
public class Effective1 {
public static void main(String[] args) {
Collection<Face> faces = EnumSet.allOf(Face.class);
for(Iterator<Face> i = faces.iterator(); i.hasNext();)
for(Iterator<Face> j = faces.iterator(); j.hasNext();)
System.out.println(i.next() + " "+ j.next());
}
}
- Unfortunately, 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 itsremove
method.- You can often avoid explicit traversal by using Collection's removeIf, added in Java 8.
- Transforming- If you want 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 iterators or index variables can be advanced in lockstep.
-
Destructive filtering- If you need to traverse a collection removing selected elements, then you need to use an
- 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.
public interface Iterable<E> {
// Returns an iterator over the elements in this iterable
Iterator<E> iterator;
}
- Suppose you want to generate
random integers
betweenzero
andsome upper bound
.
// Common but deeply flawed
static Random rnd = new Random();
static int random(int n) {
return Math.abs(rnd.nextInt()) % n;
}
- First fault: if n is a small power of 2, the sequence of random numbers will repeat itself after a fairly short period.
-
Second fault: if n is not a power of 2, some numbers will, on average, be returned more frequently than others.
- If n is large, this effect can be quite pronounced.
- This is powerfully demonstrated by the following program, which generates a million random numbers in a carefully chosen range and then prints out how many of the numbers fell in the lower half of the range.
- If the random method worked properly, the program would print a number close to half a million, but if you run it, it doesn't happen.
-
Third fault: it can, on random occasions, fail catastrophically, returning a number outside the specified range.
- This is so because the method attempts to map the value returned by
rnd.nextInt()
to anon-negative int
by callingMath.abs()
. - If
nextInt()
returnsInteger.MIN_VALUE
,Math.abs()
will also returnInteger.MIN_VALUE
, and the remainder operator will also return a negative number, assuming n is not a power of 2.
- This is so because the method attempts to map the value returned by
public static void main(String[] args) {
int n = 2 * (Integer.MAX_VALUE / 3);
int low = 0;
for(int i = 0; i < 1000000; i++)
if(random(n) < n/2)
low++:
System.out.println(low);
}
- The solution is Random.nextInt(int).
- As of Java 7, you should no longer use Random.
- For most uses, the random number generator of choice is now ThreadLocalRandom.
- It produces higher quality random numbers, and it's very fast.
- A
second advantage
of using standard libraries is that you don't have to waste your time writingad hoc solutions
to problems that are onlymarginally 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.- If the library is missing something, the developer community will make it known, and the missing functionality may get added in subsequent release.
- A
final advantage
of using the standard libraries is that you place your code in the mainstream.- Such code is more easily readable, maintainable, and reusable by the multitude of developers.
The float and double types are designed primarily for
scientific
andengineering
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.
- For example, suppose you have $1.03 in your pocket, and you spend 42c.
- How much money do you have left?
- Unfortunately, it prints out 0.6100000000000001.
System.out.println(1.03 - 0.42);
- Suppose you have a dollar in your pocket, and you buy nine washers priced at ten cents each.
- How many change do you get?
- According to this program fragment, you get $0.09999999999999998.
System.out.println(1.00 - 9 * 0.10);
- Suppose you have a dollar in your pocket, and you see a shelf with a row of delicious candies priced at
10c
,20c
,30c
, and so forth. - You buy one of each candy, starting with the one that costs 10c, until you can't afford to buy the next candy on the shelf.
- How many candies do you buy, and how much change do you get?
- You'll find that you can afford
3
pieces of candy, and you have$0.3999999999999999
left.
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++;
}
System.out.println(itemsBought + " items bought.");
System.out.println("Change: $" + funds);
}
- Solution using BigDecimal:
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);
}
Disadvantages of BigDecimal
- It is a lot less convenient than using a primitive arithmetic type.
- It is a lot slower.
Every primitive type has a corresponding reference type, called a boxed primitive, called a
boxed primitive
.
- The boxed primitives corresponding to
int
,double
, andboolean
areInteger
,Double
, andBoolean
. -
First: Two boxed primitives instances can
have the same value
anddifferent identities
. -
Second: Primitive types have only
fully functional values
, whereas each boxed primitive has onenonfunctional value
,which isnull
, in addition to all the functional values of the corresponding primitive type. -
Last: Primitives are more
time-and space-efficient
than boxed primitives.
Applying the == operator to boxed primitives is almost always wrong.
Applying the < operator causes the Integer instances to be auto-unboxed.
- i is an Integer, not an int, and like all nonconstant object reference fields, its initial value is null.
In nearly every case when you mix primitives and boxed primitives in an operation, the boxed primitive is auto-unbocked.
- If a null object reference is auto-unboxed, you get a
NullPointerException
.
- This program is much slower than it should be because it accidentally declares a local variable (sum) to be the boxed primitive type Long instead of the primitive type long*.
- The program compiles without error or warning, and the variable is
repeatedly boxed and unboxed
, causing theobserved performance degradation
.
Uses of boxed primtivies
- As elements, keys, and values in collections.
- You can't put primitives in collections, so you're forced to use boxed primitives.
- You must use boxed primitives as type parameters in
parameterized types and methods
, because the language does not permit you to use primitives.- For example, you cannot declare a variable to be of type ThreadLocal, so you must use ThreadLocal<Integer> instead.
Because strings are so common and so well supported by the language, there is a natural tendency to use strings for purposes other than those for which they were designed.
- Strings are poor substitutes for other value types.
- When a piece of data comes into a program from a
file
, from thenetwork
, or fromkeyboard input
, it is often used in String form. - There is natural tendency to leave it that way, but this tendency is justified only if the data really is
textual
in nature.
-
Numeric data: should be translated into the appropriate numeric type, such as
int
,float
, orBigInteger
. -
Answer to a yes-no question data: should be translated into an appropriate
enum type
or aboolean
.
-
String are poor substitutes for enum types.
- Enums make far better enumerated type constants than strings.
-
Strings are poor substitutes for aggregate types.
- If an entity has multiple components, it is usually a bad idea to represent it as a single string.
- If an entity has multiple components, it is usually a bad idea to represent it as a single string.
- If the character used to separate fields occurs in one of the fields, chaos may result.
- To access individual fields, you have to parse the string, which is slow, tedious, and error-prone.
- A better approach is simply to write a class to represent the aggregate, often a
private static member class
.
- It is fine for generating a single line of output or constructing the string representation of a
small, fixed size object
,but it doesn't scale
.
Using the string concatenation operator repeatedly to concatenate n strings requires time quadratic in n.
- This is an unfortunate consequence of the fact that strings are
immutable.
String concatenation
// Inappropriate use of string concatenation - performs poorly!
public String statement() {
String result = "";
for(int i = 0; i < numItems(); i++) {
result += lineForItem(i);
}
return result;
}
- The method performs abysmally if the number of items is large.
StringBuilder
public String statement() {
StringBuilder b = new StringBuilder(numItems() * LINE_WIDTH);
for(int i = 0; i< numItems(); i++) {
b.append(lineForItem(i));
}
return b;
}
- A lot of work has gone into making string concatenation faster since
Java 6
. - If new
newItems
returns100
andlineForItem
returns an80-character string
.- The second method runs
6.5
times faster than the first one. - The second is linear.
- Note that the second method preallocates a StringBuilder large enough to hold the entire result, eliminating the need for
automatic growth
. - Even if it is detuned to use a default-sized StringBuilder, it is still
5.5
times faster.
- The second method runs
If an appropriate interface types exist, then parameters, return values, variables, and fields should all be declared using interface type.
- Get in the habit of writing this:
Set<Son> sonSet = new LinkedHashSet<>();
- Not this:
LinkedHashSet<Son> sonSet = new LinkedHashSet<>();
-
If you get into the habit of using interfaces as types, your program will be much more flexible.
- If you want to switch implementations, all you have to do is to change the class name in the constructor (or use a different static factory).
Set<Son> sonSet = new HashSet<>();
- There is one caveat:
- If the original implementation offered some special functionality not required by the original contract of the interface and the code depended on that functionality, then it is critical that the new implementation
provides the same functionality
. -
LinkedHashSet
-->HashSet
change loses theoriginal ordering policy
.
- If the original implementation offered some special functionality not required by the original contract of the interface and the code depended on that functionality, then it is critical that the new implementation
- Why would you want to change an implementation type?
- The second implementation offers
better performance
than the original, or because it offersdesirable functionality that the original implementation lacks.
-
HashMap
-->EnumMap
change provides better performance and iteration order consistent with the natural order of the keys, but you can only use an EnumMap if the key is an enum type.
- The second implementation offers
- If an object belongs to such a class-based framework, it is preferable to refer to it by the relevant base class which is often abstract, rather than by its implementation class.
-
Many java.io classes such as OutputStream fall into this category
.
- A final case in which there is no appropriate interface type is that of classes that implement
but also provide extra methods not found in the interface
. - For example, PriorityQueue has a comparator method that is not present on the Queue interface.
- Such a class should be used to refer to its instances only if the program relies on the extra methods, and this should be very rare.
The core reflection facility, java.lang.reflect, offers programmatic access to arbitrary classes.
- Given a Class object, you can obtain Constructor, Method, and Field instances representing the constructors, methods, and fields of the class represented by the Class instance.
- These objects provide programmatic access to the class's member names, field types, method signatures, and so on.
- Moreover, Constructor, Method, and Field instances let you manipulate their underlying counterparts reflectively:
- You can construct instances.
- Invoke methods.
- Access fields. by invoking methods on the Constructor, Method, and Field instances.
This power, however, comes at a price.
-
You lose all the benefits of compile-time type checking, including exception checking.
- If a program attempts to invoke a non existent or inaccessible method reflectively, it will fail at runtime unless you've taken special precautions.
-
The code required to perform reflective access is clumsy and verbose.
- It is
tedious
towrite
anddifficult
toread
.
- It is
-
Performance suffers.
- Reflective method invocation is much slower than normal method invocation.
public static void main(String[]) {
// Translate the class name into a Class object.
Class<? extends Set<String>> cl = null;
try {
// Unchecked class.
cl = (Class<? extends Set<String>>) Class.forName(args[0]);
}
catch(ClassNotFoundException e) {
fatalError("Class not found.");
}
// Get the constructor.
Constructor<? extends Set<String>> cons = null;
try {
cons = cl.getDeclaredConstructor();
}
catch (NoSuchMethodException e) {
fatalError("No parameterless constructor");
}
// Instantiate the set.
Set<String> s = null;
try{
s = cl.newInstance();
}
catch(IllegalAccessException e) {
fatalError("Constructor not accessible");
}
catch(InstantiationException e) {
fatalError("Class not instantiable");
}
catch(InvocationTargetException e) {
fatalErorr("Constructor threw " + e.getCause());
}
catch(ClassCastException e) {
fatalError("Class does not implement Set");
}
// Exercise the set
s.addAll(Arrays.asList(args).subList(1, args.length));
}
private static void fatalError(String msg) {
System.err.println(msg);
System.exit(1);
}
- This toy program could easily be turned into a
generic set tester
thatvalidates
the specifiedSet implementation
by aggressively manipulating one or more instances and checking that they obey the Set contract.
Disadvantages
- The example can generate six different exception at runtime, all of which would have been compile-time errors if reflective instantiation were not used.
- It takes 25 lines of tedious code to generate an instance of a class from it name.
- If you compile this program, you'll get an
unchecked cast
warning. - This warning is legitimate, in that the cast to
Class<? extends Set<String>>
will succeed even if the named class isnot a Set implementation
, in which case the program will throw aClassCastException
when it instantiates the class.