Meine Best Practices für Java- & Webentwicklung

Best Practices aus "Effective Java (3rd Edition)"

Exzerpt aus "Effective Java (3rd Edition) External Link" by Joshua Bloch

1. Return empty collections or arrays, not nulls

Wenn die Methode ein Set zurückgibt, kein return null; bei leerem Result, sondern return Set.of();

2. Consider static factory methods instead of constructors

public static Boolean valueOf(boolean b) {
    return b ? Boolean.TRUE : Boolean.FALSE;
}

Nicht immer besser, können aber in manchen Anwendungsfällen zahlreiche Vorteile bieten:

Nachteile:

3. Consider a builder when faced with many constructor parameters

Static factories and constructors share a limitation: they do not scale well to large numbers of optional parameters. This one hits some projects I worked on very hard.

4. Enforce the singleton property with a private constructor or an enum type

Alles was nicht mehrere Instanzen braucht, soll nur einmal instanziiert werden. Sei es mit @ApplicationScoped oder einem anderen Singleton Pattern.

5. Enforce noninstantiability with a private constructor

Occasionally you’ll want to write a class that is just a grouping of static methods and static fields. Such classes have acquired a bad reputation because some people abuse them to avoid thinking in terms of objects, but they do have valid uses.

private MyStaticClass() {
    // private constructor to prevent instantiation
}

6. Avoid creating unnecessary objects

String s = new String("bikini");  // DON'T DO THIS!

oder:

// Hideously slow! Can you spot the object creation?
private static long sum() {
    Long sum = 0L;
    for (long i = 0; i <= Integer.MAX_VALUE; i++)
        sum += i;
    return sum;
}

Auflösung: This program gets the right answer, but it is much slower than it should be, due to a one-character typographical error. The variable sum is declared as a Long instead of a long, which means that the program constructs about 2^31 unnecessary Long instances (roughly one for each time the long i is added to the Long sum).

7. Avoid finalizers and cleaners

Finalizers are unpredictable, often dangerous, and generally unnecessary. Their use can cause erratic behavior, poor performance, and portability problems.

8. Prefer try-with-resources to try-finally

// try-with-resources - the best way to close resources!
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
    return br.readLine();
}

9. Consider implementing Comparable

By implementing Comparable, you allow your class to interoperate with all of the many generic algorithms and collection implementations that depend on this interface.

10. Minimize the accessibility of classes and members

The single most important factor that distinguishes a well-designed component from a poorly designed one is the degree to which the component hides its internal data and other implementation details from other components. A well-designed component hides all its implementation details, cleanly separating its API from its implementation. Components then communicate only through their APIs and are oblivious to each other’s inner workings. This concept, known as information hiding or encapsulation, is a fundamental tenet of software design.

11. In public classes, use accessor methods, not public fields

Occasionally, you may be tempted to write degenerate classes that serve no purpose other than to group instance fields:

// Degenerate classes like this should not be public!
class Point {
    public double x;
    public double y;
}

Because the data fields of such classes are accessed directly, these classes do not offer the benefits of encapsulation. You can’t change the representation without changing the API, you can’t enforce invariants, and you can’t take auxiliary action when a field is accessed. Hard-line object-oriented programmers feel that such classes are anathema and should always be replaced by classes with private fields and public accessor methods (getters) and, for mutable classes, mutators (setters):

// Encapsulation of data by accessor methods and mutators
class Point {

    private double x;
    private double y;

    public Point(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public double getX() { return x; }
    public double getY() { return y; }

    public void setX(double x) { this.x = x; }
    public void setY(double y) { this.y = y; }
}

Certainly, the hardliners are correct when it comes to public classes: if a class is accessible outside its package, provide accessor methods to preserve the flexibility to change the class’s internal representation. If a public class exposes its data fields, all hope of changing its representation is lost because client code can be distributed far and wide.

However, if a class is package-private or is a private nested class, there is nothing inherently wrong with exposing its data fields—assuming they do an adequate job of describing the abstraction provided by the class.

12. Minimize mutability

An immutable class is simply a class whose instances cannot be modified. All the information contained in each instance is fixed for the lifetime of the object, so no changes can ever be observed. The Java platform libraries contain many immutable classes, including String, the boxed primitive classes, and BigInteger and BigDecimal. There are many good reasons for this: Immutable classes are easier to design, implement, and use than mutable classes. They are less prone to error and are more secure. To make a class immutable, follow these five rules:

  1. Don’t provide methods that modify the object’s state (known as mutators).
  2. Ensure that the class can’t be extended. This prevents careless or malicious subclasses from compromising the immutable behavior of the class by behaving as if the object’s state has changed. Preventing subclassing is generally accomplished by making the class final, for instance.
  3. Make all fields final. This clearly expresses your intent in a manner that is enforced by the system. Also, it is necessary to ensure correct behavior if a reference to a newly created instance is passed from one thread to another without synchronization, as spelled out in the memory model.
  4. Make all fields private. This prevents clients from obtaining access to mutable objects referred to by fields and modifying these objects directly. While it is technically permissible for immutable classes to have public final fields containing primitive values or references to immutable objects, it is not recommended because it precludes changing the internal representation in a later release.
  5. Ensure exclusive access to any mutable components. If your class has any fields that refer to mutable objects, ensure that clients of the class cannot obtain references to these objects. Never initialize such a field to a client-provided object reference or return the field from an accessor. Make defensive copies in constructors, accessors, and readObject methods.

13. Favor composition over inheritance

Inheritance is a powerful way to achieve code reuse, but it is not always the best tool for the job. Used inappropriately, it leads to fragile software. It is safe to use inheritance within a package, where the subclass and the superclass implementations are under the control of the same programmers. It is also safe to use inheritance when extending classes specifically designed and documented for extension. Inheriting from ordinary concrete classes across package boundaries, however, is dangerous.

Unlike method invocation, inheritance violates encapsulation. You might think that it is safe to extend a class if you merely add new methods and refrain from overriding existing methods. While this sort of extension is much safer, it is not without risk. If the superclass acquires a new method in a subsequent release and you have the bad luck to have given the subclass a method with the same signature and a different return type, your subclass will no longer compile. If you’ve given the subclass a method with the same signature and return type as the new superclass method, then you’re now overriding it, so you’re subject to the problems described earlier. Furthermore, it is doubtful that your method will fulfill the contract of the new superclass method, because that contract had not yet been written when you wrote the subclass method.

Luckily, there is a way to avoid all the problems described above. Instead of extending an existing class, give your new class a private field that references an instance of the existing class. This design is called composition because the existing class becomes a component of the new one. Each instance method in the new class invokes the corresponding method on the contained instance of the existing class and returns the results. This is known as forwarding, and the methods in the new class are known as forwarding methods. The resulting class will be rock solid, with no dependencies on the implementation details of the existing class. Even adding new methods to the existing class will have no impact on the new class.

14. Prefer interfaces to abstract classes

Java has two mechanisms to define a type that permits multiple implementations: interfaces and abstract classes. Since the introduction of default methods for interfaces in Java 8, both mechanisms allow you to provide implementations for some instance methods.

A major difference is that to implement the type defined by an abstract class, a class must be a subclass of the abstract class. Because Java permits only single inheritance, this restriction on abstract classes severely constrains their use as type definitions. Any class that defines all the required methods and obeys the general contract is permitted to implement an interface, regardless of where the class resides in the class hierarchy. Existing classes can easily be retrofitted to implement a new interface.

15. Generics: Don't use raw types

You don’t discover the error until runtime, long after it has happened, and in code that may be distant from the code containing the error. Once you see the ClassCastException, you have to search through the codebase looking for the method invocation that put the coin into the stamp collection. The compiler can’t help you.

16. Generics: Eliminate unchecked warnings

When you program with generics, you will see many compiler warnings:

The more experience you acquire with generics, the fewer warnings you’ll get, but don’t expect newly written code to compile cleanly. Many unchecked warnings are easy to eliminate. For example, suppose you accidentally write this declaration:

Set<Lark> exaltation = new HashSet();

Change this to:

Set<Lark> exaltation = new HashSet<>();

Some warnings will be much more difficult to eliminate. When you get warnings that require some thought, persevere! Eliminate every unchecked warning that you can.

If you eliminate all warnings, you are assured that your code is typesafe, which is a very good thing. It means that you won’t get a ClassCastException at runtime, and it increases your confidence that your program will behave as you intended. If you can’t eliminate a warning, but you can prove that the code that provoked the warning is typesafe, then (and only then) suppress the warning with an @SuppressWarnings("unchecked") annotation. If you suppress warnings without first proving that the code is typesafe, you are giving yourself a false sense of security. The code may compile without emitting any warnings, but it can still throw a ClassCastException at runtime.

Every time you use a @SuppressWarnings("unchecked") annotation, add a comment saying why it is safe to do so.

17. Generics: When it makes sense

Ouch - a class badly in need of generics:

Example of a class in need of generics

So etwas sollte spätestens beim Merge Request abgefangen werden!

18. Generics: How not to do it

Sorry, no words needed. Please NEVER merge this:

public abstract class JpaLearningCardBaseContentService<LC extends LearningCardBaseContent<L, LC, AC>, J extends JpaLearningCardBaseContent<L, LC, AC>, L extends
        LearningCardBase<LC, A>, A extends AnswerOptionBase<L, AC>, AC extends AnswerOptionBaseContent<LC, AC, A>, AS extends AnswerOptionBaseService<A, AC, L, LC>, ACS extends
        AnswerOptionBaseContentService<AC, A, L, LC>, LS extends LearningCardBaseService<L>>
    extends JpaLightPersistenceService<LC, J, Long> implements LearningCardBaseContentService<LC, L, A, AC> {
    // ...
}

A mere class declaration should never take 5 minutes to understand.

19. Prefer lists to arrays

Arrays differ from generic types in two important ways. First, arrays are covariant. This scary-sounding word means simply that if Sub is a subtype of Super, then the array type Sub[] is a subtype of the array type Super[]. Generics, by contrast, are invariant: for any two distinct types Type1 and Type2, List<Type1> is neither a subtype nor a supertype of List<Type2>.

20. Prefer lambdas to anonymous classes

// Anonymous class instance as a function object - obsolete!
Collections.sort(words, new Comparator<String>() {
    public int compare(String s1, String s2) {
        return Integer.compare(s1.length(), s2.length());
    }
});

Instead, do this:

// Lambda expression as function object (replaces anonymous class)
Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));

21. 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...

22. Prefer for-each loops to traditional for loops

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

23. Know and use the libraries

Suppose you want to generate random integers between zero and some upper bound. Faced with this common task, many programmers would write a little method that looks something like this:

// Common but deeply flawed!
static Random rnd = new Random();

static int random(int n) {
    return Math.abs(rnd.nextInt()) % n;
}

This method may look good, but it has three flaws. The first is that if n is a small power of two, the sequence of random numbers will repeat itself after a fairly short period. The second flaw is that if n is not a power of two, some numbers will, on average, be returned more frequently than others. If n is large, this effect can be quite pronounced.

The third flaw in the random method is that it can, on rare 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 a non-negative int by calling Math.abs. If nextInt() returns Integer.MIN_VALUE, then Math.abs will also return Integer.MIN_VALUE, and the remainder operator (%) will return a negative number, assuming nis not a power of two. This will almost certainly cause your program to fail, and the failure may be difficult to reproduce.

24. Avoid float and double if exact answers are required

BigDecimal für Geldbeträge verwenden. float und double erzeugen Rundungsfehler.

For example, suppose you have $1.03 in your pocket, and you spend 42¢. How much money do you have left? Here’s a naive program fragment that attempts to answer this question:

System.out.println(1.03 - 0.42);

Unfortunately, it prints out 0.6100000000000001. This is not an isolated case. Suppose you have a dollar in your pocket, and you buy nine washers priced at ten cents each. How much change do you get?

System.out.println(1.00 - 9 * 0.10);

According to this program fragment, you get $0.09999999999999998.

You might think that the problem could be solved merely by rounding results prior to printing, but unfortunately, this does not always work. For example, suppose you have a dollar in your pocket, and you see a shelf with a row of delicious candies priced at 10¢, 20¢, 30¢, and so forth, up to a dollar. You buy one of each candy, starting with the one that costs 10¢, until you can’t afford to buy the next candy on the shelf.

25. Prefer primitive types to boxed primitives

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

26. Refer to objects by their interfaces

Good example:

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

Bad example:

LinkedHashSet<Son> sonSet = new LinkedHashSet<>();

27. Prefer interfaces to reflection

If not:

28. Adhere 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 [JLS, 6.1].

29. Use exceptions only for exceptional conditions

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

What does this code do? It’s not at all obvious from inspection, and that’s reason enough not to use it . It turns out to be a horribly ill-conceived idiom for looping through the elements of an array. The infinite loop terminates by throwing, catching, and ignoring an ArrayIndexOutOfBoundsException when it attempts to access the first array element outside the bounds of the array. It’s supposed to be equivalent to the standard idiom for looping through an array, which is instantly recognizable to any Java programmer:

for (Mountain m : range) {
    m.climb();
}

Exceptions are, as their name implies, to be used only for exceptional conditions; they should never be used for ordinary control flow.

30. Use checked exceptions for recoverable conditions and runtime exceptions for programming errors

Use checked exceptions for conditions from which the caller can reasonably be expected to recover. By confronting the user with a checked exception, the API designer presents a mandate to recover from the condition. The user can disregard the mandate by catching the exception and ignoring it, but this is usually a bad idea.

Use runtime exceptions to indicate programming errors. The great majority of runtime exceptions indicate precondition violations. A precondition violation is simply a failure by the client of an API to adhere to the contract established by the API specification. For example, the contract for array access specifies that the array index must be between zero and the array length minus one, inclusive. ArrayIndexOutOfBoundsException indicates that this precondition was violated.

31. Avoid unnecessary use of checked exceptions

In summary, when used sparingly, checked exceptions can increase the reliability of programs; when overused, they make APIs painful to use.

Pair<Boolean, LearningCardGroup> answerLearningCard(@NotNull LearningCardGroup learningCardGroup,
                                                      LearningCardAssignment learningCardAssignment,
                                                      @NotNull Collection<AnswerOption> answers)
      throws InvalidTrainingStateException, PersistenceException, LearningCardGroupAlreadyCompletedException,
             ConsecutiveCorrectCountExceededException,
             PreviousLearningCardGroupUncompletedException, LearningCardGroupCooldownException,
             LearningCardGroupHasUncompletedLearningCardsException, InvalidParticipantException, TrainingNotSignedException; // but why ???
     // this is a mistake seen a lot with junior devs
     // exceptions are exceptionally slow and should be avoided for mere validation
     // the performance gets even worse if a stacktrace is printed, since logging and printing is one of the slowest possible actions

32. Favor the use of standard exceptions

An attribute that distinguishes expert programmers from less experienced ones is that experts strive for and usually achieve a high degree of code reuse.

Exceptions are no exception to the rule that code reuse is a good thing. The Java libraries provide a set of exceptions that covers most of the exception-throwing needs of most APIs. Reusing standard exceptions has several benefits. Chief among them is that it makes your API easier to learn and use because it matches the established conventions that programmers are already familiar with. A close second is that programs using your API are easier to read because they aren’t cluttered with unfamiliar exceptions. Last (and least), fewer exception classes means a smaller memory footprint and less time spent loading classes.

The most commonly reused exception type is IllegalArgumentException. This is generally the exception to throw when the caller passes in an argument whose value is inappropriate. For example, this would be the exception to throw if the caller passed a negative number in a parameter representing the number of times some action was to be repeated. Another commonly reused exception is IllegalStateException. This is generally the exception to throw if the invocation is illegal because of the state of the receiving object. For example, this would be the exception to throw if the caller attempted to use some object before it had been properly initialized.

Arguably, every erroneous method invocation boils down to an illegal argument or state, but other exceptions are standardly used for certain kinds of illegal arguments and states. If a caller passes null in some parameter for which null values are prohibited, convention dictates that NullPointerException be thrown rather than IllegalArgumentException. Similarly, if a caller passes an out-of-range value in a parameter representing an index into a sequence, IndexOutOfBoundsException should be thrown rather than IllegalArgumentException.

Do not reuse Exception, RuntimeException, Throwable, or Error directly. Treat these classes as if they were abstract. Though RuntimeException may be used if you want to convert a checked exception into a runtime exception, and you really know what you are doing.

33. Don’t ignore exceptions

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

An empty catch block defeats the purpose of exceptions, which is to force you to handle exceptional conditions. Ignoring an exception is analogous to ignoring a fire alarm – and turning it off so no one else gets a chance to see if there’s a real fire.