My Best Practices – Java

1. Pattern.compile() instead of String.replaceAll() in a loop

public String replaceAll(String regex, String replacement) {
    return Pattern.compile(regex).matcher(this).replaceAll(replacement);
}

The Pattern always has to be compiled first. If the same Pattern runs often against short strings, performance is noticeably better when you reuse the compiled Pattern.

2. StringBuilder instead of + in a loop

new StringBuilder() performs better than + in a loop.

3. Using equals() and hashCode() correctly

Sometimes all fields get included in equals() or hashCode(). That’s handy for checking whether an entity has changed, but it’s wrong. The purpose of equals() is not to check whether an entity was modified, but to determine whether it’s still the same entity, even if it’s not the same object instance. In other words: Use only the primary key!

You need this for any kind of collection. Call .contains() on a List and Java will use the equals method. HashMaps also need hashCode(). The two should always be overridden together.

A common pitfall is unsaved entities without an ID. Be careful here and persist early to avoid problems.

See also:

The contract of these methods must always be honored:

4. DateTime pitfalls when mixing operating systems

Some operating systems have an internal clock with nanosecond precision, others with milliseconds. So ZonedDateTime.now() doesn’t behave the same on both platforms and breaks equals(). Instead, ALWAYS adjust the precision!

/**
 * truncating the precision, because it relies on the internal clock of linux/macOs/windows.
 * windows and older macOS versions use only milliseconds, whereas linux and new macOS versions use microsecond precision.
 */
public static ZonedDateTime getNow() {
    return ZonedDateTime.now().truncatedTo(DateTimeUtil.PRECISION);
}

The following JPA annotations also rely on ZonedDateTime.now() and should never be used plain:

5. Don’t declare complex data types inline

Always create dedicated classes for complex data types instead of nesting Pairs, Lists etc.

Bad:

// unlesbar
public Triple<List<Pair<List<List<ChatMessage>>, ZonedDateTime>>, Language, ChatRoom> getChatMessagesGroupedByDay();

Better:

public class FTOChatMessageCollection {

    private List<FTOChatMessageCollectionPerDay> messagesPerDay;
    private Language language;
    private ChatRoom chatRoom;

    // constructor, getter & setter ...
}
public class FTOChatMessageCollectionPerDay {

    private List<FTOChatMessage> messages;
    private ZonedDateTime day;

    // constructor, getter & setter ...
}

Advantages:

Exceptions:

For simple relations, Pair is great! For example:

Pair<User, Training> pair = this.addCurrentUserToTraining(training);

6. Caching – done right (minimal setup guide)

The cache (pronounced /kæʃ/, like “cash”) is basically just a map that holds entries. More complex things like time-based invalidation and the like were left out.

It doesn’t always have to be a library when a simple map will do! Many developers don’t get this and reach for libraries out of fear, as a matter of principle. The most important aspect of caching is invalidating at the right moment.

Pitfall #1: Memory Leaks

Memory leaks happen very easily when caching. Forget to override equals() and hashCode() properly, and before you know it you’ve got a HashMap that leaks memory. One approach that’s always safe is building the map keys by hand. Ideally, include only the ID of an entity – otherwise the map already leaks as soon as you change a single field. Which makes sense, too: just because a user’s first name changes, it’s still the same user. See: 3. Using equals() and hashCode() correctly

Pitfall #2: Thread Safety

Ideally, the cache uses a ConcurrentHashMap, which offers atomic, thread-safe operations. The map is only ever modified through a single call – computeIfAbsent() – which is how thread safety is achieved:

private ConcurrentMap<String, T> cache = new ConcurrentHashMap<>();

protected T getOrCompute(final String key, final Supplier<T> computeFunction) {
    // thread safe, because `computeIfAbsent` is atomic, so no need for `synchronized` here.
    // if you extend this to more than 1 map operation, you should synchronize it
    return this.getCacheMap().computeIfAbsent(key, k -> computeFunction.get());
}

If the code were longer and there were several calls, the whole method would have to be synchronized.

7. Serialization – is it still needed?

My question: There is this Sonar rule called Fields in a “Serializable” class should either be transient or serializable. It comes up when you add a non-serializable field to your class. The rule states:

For instance, under load, most J2EE application frameworks flush objects to disk, and an allegedly Serializable object with non-transient, non-serializable data members could cause program crashes, and open the door to attackers.

This sounds outdated to me. In Effective Java 3rd Edition it is stated that Serialization is not relevant, when developing new software. Also, Removing Serialization from Java Is a 'Long-Term Goal' at Oracle. The Java chief architect himself, Mark Reinhold, stated

“Serialization was a horrible mistake in 1997.”

The question, if the Spring framework may flush objects, was already asked on here:

So I ask the question:

Do any modern application servers, frameworks or the J2EE implementation itself (especially Java components that interact with CDI, EJB, JPA) flush objects to disk under heavy load?

Or is this statement outdated?

Answer:

TL;DR If you're not implementing a stateful distributable web application, you do not enable session persistence, and you only use in-memory caches, you might get away without needing Serializable anywhere.

Explanation:

For session replication, Java Serialization is still a must for the latest Servlet Specification: https://jakarta.ee/specifications/servlet/5.0/jakarta-servlet-spec-5.0.html#distributed-environments

A common use case is if you have multiple servers behind a load balancer, and you log in on one server, you get a JSESSIONID cookie. If the load balancer sends you to a different server for the next request, your login attributes and other session-scoped attributes will be serialized and replicated on the new server, so your JSESSIONID is still valid there.

Another use case where you need Serialization for your session-scoped attributes, is when the application server decides to swap sessions to disk. For e.g. Tomcat 8, this can be configured to activate when there are too many sessions to keep in memory, or when the server is restarted, and you want to keep the sessions alive. See https://tomcat.apache.org/tomcat-9.0-doc/config/manager.html

Every Servlet-5.0 compatible application server must support Serializable objects in Http Sessions by default for these kinds of use cases.

For some servers, this can be tweaked or overridden by using e.g. Jackson-JSON serialization or something similar. See e.g. https://stackoverflow.com/questions/46459707/how-to-use-jackson-instead-of-jdkserializationredisserializer-in-spring

Another common use case is caching. This is used to keep objects accessible as fast as possible, ideally in memory, but possibly temporarily offloaded to disk or to an external cache server.
There is a standard API for it (JSR-107) that explicitly chose not to require Serializable objects, but most implementations still use Java Serialization for the offloading part as a default.

You can again tweak these to support other serialization mechanisms, e.g. for ehcache: https://www.ehcache.org/documentation/3.8/serializers-copiers.html

8. Hibernate: fetching strategy

In light of EAGER fetching is a code smell, written by an exceedingly charismatic smart-ass who happens to be a Hibernate contributor, LAZY is the preferred FetchType. Even if some data types make no sense without a foreign key, these types very much do get queried on their own (getCount(), data tables, etc.).

Here are all the scenarios, ranked by how satisfying the solution is, starting with the least satisfying:

1. Worst Case: LazyInitializationException

No words needed. We all share this pain.

2. Quick fix

public void updateChatRoom(ChatMessage message) {
    // reload here or LazyInitializationException would be thrown
    message = this.chatMessageService.reload(message); // not optimal: second roundtrip to the database - could easily take 100ms!
    // we missed the chance to unproxy without a performance hit.

    ChatRoom chatRoom = message.getChatRoom().getUpdatedAt(DateTimeUtil.getNow());
}

3. Unproxy directly in the service

public ChatMessage getChatMessage(final Long id, boolean withRoom) {
    // assume this is the same service + the same transaction.
    // so we are in the same persistence context + everything will be performed together at the transaction flush.
    // perfect: no additional roundtrip like in example 2.!

    ChatMessage message = this.chatMessageService.getById(id);

    if (withRoom) {
        ChatRoom chatRoom = this.chatMessageService.unproxy(message.getChatRoom());
    }
   
    // we should not call message.setChatRoom() -> this getter should not even exist as Hibernate would 
    // map this relation for us! setting it again would perform an update on our foreign key!!
    // so we have no option to set the ChatRoom back to the message and have to pass it everywhere in a Tuple or something similar
    return ???; 
}

4. Fetch directly in the service (do this!)

public ChatMessage getChatMessage(final Long id, boolean withRoom) {
    // all aforementioned problems solved
    return this.executeSingleResultQuery((query, cb, root) -> {
            if (withRoom) {
                root.fetch(ChatRoomEntity_.partition).fetch(ChatPartitionEntity_.chatModule);
            }

            return cb.equal(root.get(ChatRoomEntity_.id), id);
        });
}

9. Hibernate Search

Hibernate Search integrates seamlessly with Hibernate JPA. It notices when entities change and is easy to configure:

@Entity
@Indexed(interceptor = SearchableChatMessageInterceptor.class) // hier können Einträge von der Indexierung ausgeschlossen werden
@FullTextFilterDef(name = "SearchableChatMessageFilter", impl = SearchableChatMessageFilter.class) // hier wird der Key festgelegt
public class ChatMessageEntity implements Searchable { // das Searchable dient als Marker-Interface

    @Column
    @Field  // dieses Feld wird indexiert
    private String message;

    @ManyToOne(targetEntity = ChatRoomEntity.class, fetch = FetchType.LAZY, optional = false)
    @IndexedEmbedded(includeEmbeddedObjectId = true, targetElement = ChatRoomEntity.class)   // indexiert den Fremschlüssel mit.
                                                                                             // Über dieses Entity wird auch die Message gesucht,
                                                                                             // wie in @FullTextFilterDef -> SearchableChatMessageFilter definiert
    private ChatRoom chatRoom;

    // ...

}

Search is switched off locally:

// DefaultInstanceInitializer.java

// we don't need the global search everywhere, givin an option to override this setting here
protected boolean isBuildSearchIndexes() {
    return !Constants.Mode.isDebug();
}

Fuzzy search & search behavior

if ((needle.split(" ").length > 1 || needle.contains("\"")) && !searchExact) {
  /*
   * Simple Query String. Provides basic functionality like:
   *
   *     boolean (AND using “+”, OR using “|”, NOT using “-“)
   *     prefix (prefix*)
   *     phrase (“some phrase”)
   *     precedence (using parentheses)
   *     fuzzy (fuzy~2)
   *     near operator for phrase queries (“some phrase”~3)
   */
  query = qb.simpleQueryString()
            .onField(field)
            .andFields(moreFields)
            .withAndAsDefaultOperator()
            .matching(needle)
            .createQuery();
} // ...

Lucene

Debugging Lucene with Luke

Our version of Hibernate Search has to be debugged with Luke version 6.6.0. It only works with this version!

ElasticSearch

Hibernate Search also supports ElasticSearch (it then replaces Lucene and offers better fuzzy search / more of a Google feel).