Here is a list of 10 Java coding practices that are more subtle than Josh Bloch's Effective Java rules. Compared to Josh Bloch's list that is easy to learn and focuses on daily situations, this list will contain situations involving uncommon things in API/SPI design, which may have a big impact.
I've encountered these while writing and maintaining jOOQ (SQL modeled internal DSL in Java). As an internal DSL, jOOQ challenges Java compilers and generics to the greatest extent, combining generics, mutable parameters and overloads, which Josh Bloch may not recommend for such a broad API.
Let me share with you 10 subtle best practices for Java coding:
1. Remember C++'s destructor
Remember C++'s destructor? Don't remember? Then you are really lucky because you don't have to debug the memory leaks due to the memory allocated after object deletion is not released. Thanks to Sun/Oracle for the garbage collection mechanism implemented!
Nevertheless, the destructor provides an interesting feature. It understands the inverse allocation order to free memory. Remember that this is the case in Java, when you operate the class destructor syntax:
There are various other use cases. Here is a concrete example of how to implement some event listeners' SPI:
@Overridepublic void beforeEvent(EventContext e) { super.beforeEvent(e); // Super code before my code}@Overridepublic void afterEvent(EventContext e) { // Super code after my code super.afterEvent(e);} The infamous philosopher’s dining problem is another good example of why it matters. For questions about Philosopher dining, please check the link:
http://adit.io/posts/2013-05-11-The-Dining-Philosophers-Problem-With-Ron-Swanson.html
Rule : Whenever you use before/after, allocate/free, take/return semantics to implement logic, consider whether to perform after/free/return operations in reverse order.
Provide customers with SPI methods that make it easy for them to inject custom behavior into your library/code. Beware that your SPI evolution judgments may confuse you, making you think you (not) intend to need additional parameters. Of course, functions should not be added too early. But once you publish your SPI, once you decide to follow semantic versioning, you will really regret adding a stupid single parameter to your SPI when you realize that in some cases you might need another parameter:
interface EventListener { // Bad void message(String message);}What if you need a message ID and message source? API evolution will prevent you from adding parameters to the above types. Of course, with Java 8, you can add a defender method that "defends" your bad design decisions in the early days:
interface EventListener { // Bad default void message(String message) { message(message, null, null); } // Better? void message( String message, Integer id, MessageSource source );} Note that unfortunately, the defender method cannot use the final modifier.
But it would be much better to use context objects (or parameter objects) than to pollute your SPI using many methods.
interface MessageContext { String message(); Integer id(); MessageSource source();}interface EventListener { // Awesome! void message(MessageContext context);} You can evolve the MessageContext API more easily than EventListner SPI, because few users will implement it.
Rule : Whenever an SPI is specified, consider using a context/parameter object instead of writing methods with fixed parameters.
Note : It is also a good idea to swap results through a dedicated MessageResult type, which can be constructed using the builder API. This will greatly increase the flexibility of SPI evolution.
Swing programmers usually only press a few shortcut keys to generate hundreds or thousands of anonymous classes. In most cases, it doesn't hurt to do so as long as the interface is followed and the SPI subtype lifecycle is not violated. But don't use anonymous, local or internal classes frequently for a simple reason - they will save references to external classes. Because no matter where they go, external categories have to follow. For example, if the external operation of a local class is improper, the entire object graph will undergo subtle changes, which may cause memory leakage.
Rule: Before writing anonymous, local or internal classes, please think twice about whether you can convert it into static or ordinary top-level classes, thus avoiding ways to return their objects to an outer-level domain.
Note: Use double curly braces to initialize simple objects:
new HashMap<String, String>() {{ put("1", "a"); put("2", "b");}}This method utilizes the instance initializer described in the JLS §8.6 specification. It looks good on the surface, but it is not recommended in fact. Because if you use a completely independent HashMap object, the instance will not always hold references to external objects. In addition, this will allow the class loader to manage more classes.
Java8's pace is approaching. With Java 8 brings lambda expressions, whether you like it or not. Although your API users may like it, you'd better make sure they can use it as often as possible. So unless your API receives simple "scalar" types, such as int, long, String, and Date, let your API receive SAM as often as possible.
What is SAM? SAM is a single abstract method [type]. Also known as a function interface, it will soon be annotated as @FunctionalInterface. This matches Rule 2, EventListener is actually a SAM. The best SAM has only one parameter, as this will further simplify the writing of lambda expressions. Imagine writing
listeners.add(c -> System.out.println(c.message()));
To replace
listeners.add(new EventListener() { @Override public void message(MessageContext c) { System.out.println(c.message())); }});Imagine handling XML in JOOX. JOOX contains a lot of SAM:
$(document) // Find elements with an ID .find(c -> $(c).id() != null) // Find their children elements .children(c -> $(c).tag().equals("order")) // Print all matches .each(c -> System.out.println($(c)))Rules: Be nice to your API users, start writing SAM/function interfaces from now on .
Note: There are many interesting blogs about Java8 lambda expressions and improved Collections API:
I have written 1 or 2 articles about java NULLs, and also explained the introduction of new Optional classes in Java 8. From an academic or practical point of view, these topics are quite interesting.
Although Null and NullPointerException are still a flaw in Java at this stage, you can still design an API that will not have any problems. When designing APIs, you should avoid returning null as much as possible, because your users may call the method in a chain:
initialise(someArgument).calculate(data).dispatch();
As can be seen from the above code, no method should return null. In fact, using null is usually considered a comparable heterogeneity. Library like jQuery or jOOX has completely abandoned null on iterable objects.
Null is usually used in delayed initialization. In many cases, delayed initialization should also be avoided without severely affecting performance. In fact, if the data structure involved is too large, then delay initialization should be used with caution.
Rule: Methods should avoid returning null whenever they are. null is only used to represent semantics of "uninitialized" or "not existed".
Although it is OK to return a method with a null value in some cases, never return an empty array or an empty collection! Please see the java.io.File.list() method, it is designed like this:
This method returns an array of strings for all files or directories in the specified directory. If the directory is empty, the returned array is empty. Return null if the specified path does not exist or an I/O error occurs.
Therefore, this method is usually used like this:
File directory = // ...if (directory.isDirectory()) { String[] list = directory.list(); if (list != null) { for (String file : list) { // ... } }}Do you think null check is necessary? Most I/O operations will produce IOExceptions, but this method only returns null. Null cannot store I/O error messages. Therefore, such a design has the following three shortcomings:
If you look at the problem from the perspective of collections, then an empty array or collection is the best implementation of "not existed". Returning an empty array or collection is of little practical significance unless used for delayed initialization.
Rule: The returned array or collection should not be null.
The benefit of HTTP is stateless. All relevant states are transferred in each request and response. This is the essence of REST naming: state transfer (Representational state transfer). It's also great to do this in Java. Think about this from the perspective of rule 2 when the method receives the state parameter object. If the state is transmitted through such an object, rather than operating the state from outside, things will be easier. Take JDBC as an example. The following example reads a cursor from a stored program.
CallableStatement s = connection.prepareCall("{ ? = ... }");// Verbose manipulation of statement state:s.registerOutParameter(1, cursor);s.setString(2, "abc");s.execute();ResultSet rs = s.getObject(1);// Verbose manipulation of result set state:rs.next();rs.next();This makes the JDBC API so weird. Each object is stateful and difficult to operate. Specifically, there are two main problems:
Rules: More implementations in functional style. Transfer state through method parameters. Very few operational object states.
This is a relatively easy way to operate. In relatively complex object systems, you can achieve significant performance improvements, as long as you first make equal judgments in the equals() method of all objects:
@Overridepublic boolean equals(Object other) { if (this == other) return true; // Other equality judgment logic...}Note that other short-circuit checks may involve null value checks, so they should also be added:
@Overridepublic boolean equals(Object other) { if (this == other) return true; if (other == null) return false; // Rest of equality logic...}Rule: Use short circuits in all your equals() methods to improve performance.
Some people may disagree with this, because making the method default to final is contrary to the habit of Java developers. But if you have complete control over the code, it is certainly correct to make the method default to final:
This is especially suitable for static methods, in which case "overlay" (actually occlusion) hardly works. I recently came across an example of a very bad shading static method in Apache Tika. Take a look:
TikaInputStream extends TaggedInputStream to mask its static get() method with a relatively different implementation.
Unlike conventional methods, static methods cannot be overwritten with each other because the call is bound to the static method call at compile time. If you are unlucky, you may accidentally get the wrong method.
Rule: If you have complete control of your API, make as many methods as possible default to final.
In special occasions, you can use the "accept-all" variable parameter method to receive an Object... parameter:
void acceptAll(Object... all);
Writing such a method brings a little JavaScript feel to the Java ecosystem. Of course you may want to limit the actual type based on the actual situation, such as String…. Since you don't want to limit too much, you might think it's a good idea to replace Object with generic T:
void acceptAll(T... all);
But not. T will always be inferred as an Object. In fact, you may just think that generics cannot be used in the above methods. More importantly, you might think you can overload the above method, but you can't:
void acceptAll(T... all);void acceptAll(String message, T... all);
This looks like you can optionally pass a String message to the method. But what happens to this call?
acceptAll("Message", 123, "abc");The compiler infers T as <? extends Serializable & Comparable<?>>, which will make the call unclear!
So whenever you have a "accept-all" signature (even if it's a generic), you will never be able to type-safely overload it. API consumers may only let the compiler "accidentally" choose the "correct" method when they are lucky. But it is also possible to use the accept-all method or not to call any method.
Rule : Avoid "accept-all" signatures if possible. If not, don't overload such a method.
Java is a beast. Unlike other more idealistic languages, it slowly evolved to what it is today. This may be a good thing, because at the speed of Java development, there are already hundreds of warnings, and these warnings can only be grasped through years of experience.
Stay tuned for more of the top ten lists on this topic!
Original link: jooq Translation: ImportNew.com - liken
Translation link: http://www.importnew.com/10138.html
[Please keep the original source, translator and translation link when reprinting. ]