Discussion about general Java programming craftsmanship, dealing with local variables, libraries, control structures and other language features and good practices.
Check the rest of the series here.
Item 57: Minimize the scope of local variables
Minimizing the local variable scope will improve code readability and maintainability and will reduce potential errors.
“The most powerful technique for minimizing the scope of a local variable is to declare it where it is first used.”
Besides declaring it where it is first used, it should also be initialized whenever it is possible. Keeping our methods small and focused, will ease our task of reducing the scope of a local variable.
Item 58: Prefer for-each loops over traditional loops
When we do not need an iterator (when iterating over collection) or an index (when iterating over an array) we should prefer using for-each loops (officially called “enhanced for loop”) over traditional for loop. For-each loop removes the clutter for us end gives us only the collection elements to work with and thus improving our code readability (this is even more apparent in nested loops).
Besides all of the above, the for-each loop comes with no performance impact over the traditional one.
Item 59: 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.”
In simple words, we should not reinvent the wheel (we will only lose time and the end result will not be as performant and robust as the one implemented by the expert in the field). If something that we need looks like it is reasonably common, chances are that it as already implemented, if not in Java’s standard library, then maybe in Google’s Guava, some Apache library or somewhere else. Only when there is some special case that we need, then we do not have the choice besides implementing it ourselves.
“Every programmer should be familiar with the basics of java.lang, java.util, and java.io, and their sub-packages.”
Item 60: Avoid float and double if exact answers are required
Because float and double data types use binary floating-point arithmetic, which provides accurate approximations quickly over a broad range of magnitudes, they are not well suited for calculation that needs exact results. “The float and double types are particularly ill-suited for monetary calculations.”
When exact answers are needed, we should use BigDecimal, int (when quantity is less than nine decimal digits) or long (when quantity is less than eighteen decimal digits).
BigDecimal is more convenient but it is slower than using int and long primitives, but if the performance is important and we do not mind keeping track of decimal point ourselves, then int and long are our options.
Item 61: Prefer primitive types to boxed primitives
We should always prefer primitives over boxed primitives whenever it is possible. Primitives are faster than boxed primitives and can not be null.
Autoboxing and unboxing blur the line between the types, but it does not make it less dangerous. E.g. it is easy to get NullPointerException when unboxing boxed primitive which is null or to create unnecessary objects which will reduce the performance of our software. Also comparing boxed primitives with ==
operator will certainly always be false because it is an identity comparison and not the values itself.
Item 62: Avoid strings where other types are more appropriate
Strings are designed to represent text, but are poor substitutes for other types, mainly value, enum and aggregate types. When some data comes into our program (from network, file or keyboard input), we should always convert that data into its appropriate type, if we do not have an appropriate type, we should create one (often with a private static member class).
If the incoming data is some value type, numeric or yes/no answer, we should convert it into an int (float, long..) or boolean value. If it is used for enumeration, we can use enums instead, and if the is some aggregate type (an entity with multiple components), instead of concatenating multiple strings, we should define some type that will represent that aggregate.
Item 63: Beware the performance of string concatenation
Strings are immutable, so when we concatenate (using ‘+’ operator) two strings, contents of them both are copied to form a new one.
“Using the string concatenation operator repeatedly to concatenate n strings requires time quadratic in n.”
When we need to combine more than a few strings and performance is of any importance, we should use StringBuilder instead of string concatenation. With StringBuilder, we get linear time complexity, and to improve performance, even more, we can preallocate the size of the StringBuilder object in order to eliminate automatic growth.
Item 64: 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.” By using interfaces to refer to objects, our code is more flexible and stylish. We can easily switch the implementation if we find that the different one has better performance or some other functionality that we could utilize. There is one caveat with this, and that is that if we rely on some functionality that one implementation offers, the new implementation should have the same functionality (e.g. if we use the insertion order that LinkedHashMap provides, then we can not simply swap it for HashMap).
In cases where there is no interface which we can refer to, like using value classes like String or BigInteger, or some framework class that does not provide an interface, it is appropriate to refer to an object by its class.
“If there is no appropriate interface, just use the least specific class in the class hierarchy that provides the required functionality.”
Note: When using var keyword (introduced with Java 11), this rule is a bit broken, because when using var, the compiler will resolve that variable to the type on the right side. But these variables are local variables and if we keep their scope small (which we should), it is going to be fine.
Item 65: Prefer interfaces to reflection
Reflection (java.lang.reflect) allows us to programmatically access arbitrary classes which then provides us with objects representing constructors, methods, and fields. It is is a very powerful tool that enables us to have frameworks like Spring, but with that power also comes responsibility and, in this case, great pain of writing and reading code that performs reflective access.
If we are in doubt whether our application requires reflection then we probably do not need it. We should always avoid it if possible, but in a case where we really need some classes not known at compile-time, then we should try to use reflection only to instantiate objects and then access them using interface or superclass that is known at compile time.
Item 66: Use native methods judiciously
The Java Native Interface (JNI) enables Java code to call and be called by native applications and libraries written in other languages like C, C++ or assembly.
Like Reflection API, this is pretty powerful, but we should think twice before using native methods because it has serious disadvantages, like reduced memory safety.
In cases where we must use some low-level resources or native libraries, we should use native methods as little as possible and test rigorously.
In early releases, prior to Java 3, it was often necessary to use native methods for performance gains, but JVMs today are pretty fast and for most tasks provide comparable performance with lower-level languages.
Item 67: Optimize judiciously
“Premature optimization is the root of all evil. ” – Donald E. Knuth
The simple truth about optimization is that it is easy to do more harm than good, especially when done prematurely. We should “strive to write good programs rather than fast ones” because good programs are also generally performant enough and, besides that, they embody principles and design decisions that allow us to improve the performance if needed without changing the whole system. That’s why we should not ignore concerns until everything is done and should “strive to avoid design decisions that limit performance”. Decisions like exposing a concrete class as a public API would probably haunt us forever.
Once we have a system with clear, concise and well-structured implementation and we need to improve on performance, it is crucial to measure before and after every improvement attempt and use profilers to pinpoint our slow code.
Item 68: Adhere to generally accepted naming conventions
The Java platform has a well-established naming convention which can be referred to in chapter 6 of the JLS (Java Language Specification). Generally speaking, naming conventions fall into two categories: typographical and grammatical.
Grammatical naming conventions are more flexible than typographical and they specify things like class names should use nouns and method names verbs.
The typographical convention is well defined and we should never violate them. They cover packages, classes, interfaces, methods, fields, and type variables.
Here is a quick reference for the typographical convention:
Identifier Type | Examples | Description |
---|---|---|
Package or module |
com.hazelcast.cache, org.springframework.boot |
Reverse domain name scheme. Components should use only alphanumeric chars and generally only single word or abbreviation. |
Class, Interface, Enum, Annotation type | FutureTask, LinkedHashMap, HttpUrl | One or more words with PascalCase. Avoid abbreviations except well know ones like min or max. Acronyms should have only the first letter capitalized. |
Method or Field | remove, setCountDirection | Except using camelCase instead of PascalCase, the guides are the same as for classes or interfaces. |
Constant Field | MAX_VALUE, COUNT_DOWN_OPERATION | Static final fields have special treatment, every word should be capitalized and separated only with an underscore. |
Local Variable | i, count, joinAllowed, targetAddress | The same convention as for other member names except abbreviations are permitted. Input params are special kind of local variables but should be named more carefully because of their public nature. |
Type Parameter | T, E, K, V, X, R, U, V, T1, T2 | Only consist of a single uppercase letter, e.g. Map<K, V>. Generally, T for type, K for a key, V for value, R function return type and so on. |
You liked this, then you can share it so others can like it as well 🙂