Hammer L Under construction

4.2. Equals and hashCode

About the author

Sandra L Sandra is the best. Toevoegen: iets over Sandra en over wat maakt dat ze plezier heeft in haar werk.

The first blog of the ‘MetaFactory Developer Series’ showed how easy it is to generate Java POJO classes by means of Metaprogramming. We implemented two methods: ‘equals’ and ‘hashCode’.

In this edition we’ll discuss the Freemarker snippets that are responsible for these implementations and how Metaprogramming forms the basis for these snippets. Next, we focus at how MetaFactory’s Code Composer handles metadata. Last, we demonstrate how we can very easily switch to other implementations of these methods and how the developer is in full control of the generated code.

4.2.1. Freemarker snippets

We left you with 2 Java POJO classes with some fields and two methods: equals and hashCode. To refresh your memory, here is the piece of code instruction responsible for the methods:

Listing 4.11 equals and hashCode methods from previous blog
 1<method name=”equals”
 2        visibility=”public”>
 3    <annotation>@Override</annotation>
 5    <parameter name=”return”>
 6        <apicommentline>Return true if object is the same as the argument object,
 7                        otherwise return false</apicommentline>
 8        <datatype>boolean</datatype>
 9    </parameter>
11    <parameter name=”other”>
12        <apicommentline>The reference object with which to compare</apicommentline>
13        <datatype>Object</datatype>
14    </parameter>
16    <body>${fmsnippet.java.pojo.method.equals_by_bk}</body>
19<method name=”hashCode”
20        visibility=”public”>
21    <annotation>@Override</annotation>
23    <parameter name=”return”>
24        <apicommentline>A hash code value for this object</apicommentline>
25        <datatype>integer</datatype>
26    </parameter>
28    <body>${fmsnippet.java.pojo.method.hash_code_by_bk}</body>

In Pojo Classes we already demonstrated the use of expressions for the bodies of the methods. Let’s now take a closer look at the snippet for the equals method:

Listing 4.12 Freemarker snippet for equals
 1<#import "/library/pattern_functions.ftl" as function />
 3<#--stop if modelObject is null-->
 4<#if !(modelObject)??>
 5        <#stop "modelObject not found in context" />
 8<#--stop if generatedJavaMethod is null-->
 9<#if !(generatedJavaMethod)??>
10        <#stop "generatedJavaMethod not found in context. It contains the ast of the created method" />
13<#-- Here we show an example of an equals implementation with use of Java 8. -->
15<#-- Add import for Objects to the class imports -->
18<#-- Retrieve the ast object of the method created by the code instruction and add some extra text -->
19<#assign apicomment = generatedJavaMethod.apiComment />
20<#assign apicommentText>
21        ${apicomment}
22        Fields used as business key:
25<#assign class = function.createPojoClassName(modelObject.name) />
26<#assign compareObject = "other${class}" />
28<#-- Here starts the actual equals implementation -->
29if (this == other) {
30        return true;
33if (!(other instanceof ${class})) {
34        return false;
37final ${class} ${compareObject} = (${class}) other;
39boolean result;
41<@generateEqualsForBKAttributes />
43return result;
45<#--Now set the apicomment we created in this template to the ast object of the method -->
46<#assign apicommentText = "${apicommentText}." />
51<#macro generateEqualsForBKAttributes>
52        <#local key = "businesskey" />
53        <#local bkAttributes = modelObject.findAttributesByMetaData(key) />
54        <#if bkAttributes?size == 0>
55                <#stop "No metadata on attributes was found for modelObject
56                  ${modelObject.name}. Please add <businesskey> with an integer
57                  that indicates the order of processing to an attribute that
58                  should be used for the implementation of the equals method. " />
59        </#if>
60        <#local comparator = comparatorFactory.createMetaDataComparator(key) />
61        <#local sortedBkAttributes = metafactory.sort(bkAttributes, comparator) />
62        <#list sortedBkAttributes as attribute>
63                <#local attributeName = attribute.name />
64                <#local attributeType = attribute.type />
65                <#local attributeNameFU = attributeName?cap_first />
66                <#if metafactory.getJavaType(attributeType) == "boolean">
67                        <#local getter = "is${attributeNameFU}" />
68                <#else>
69                        <#local getter = "get${attributeNameFU}" />
70                </#if>
71                <#local value = attribute.getMetaData(key)?number />
72                <#if value == 1>
73                        result = Objects.equals(${getter}(), ${compareObject}.${getter}());
74                <#else>
75                        result = result && Objects.equals(${getter}(), ${compareObject}.${getter}());
76                </#if>
78                <#--Add this attribute to the apicomment -->
79                <#local previousComment = apicommentText />
80                <#assign apicommentText = " ${previousComment} ${value})
81                  ${attributeName}" >
82        </#list>

4.2.2. Control over the generation process

Listing 4.13 Model
1<!--I need to be here.
2    But I am not yet -->

Even if you are not familiar with the Freemarker language, you will be able to recognize and read it as any other code language. Compared to Listing 4.11 we made significant changes in this version of the file. Before we used all attributes of a model object in the implementations of the equals and hashCode methods. Now we only include attributes with specific metadata in the implementations (see also toString in blog Organizing code instructions).

This gives us full control of which attributes of the model object are included. In this example we added the metadata <businesskey>1</businesskey> to the name attribute of the model object Book. The number value of the businesskey metadata indicates the order in which the attributes are to be processed. This way we also control that in the case of Book that the name is processed first, followed by the description attribute. Even more convenient, we can control this without making any adjustments to the implementation in the snippet file.

4.2.3. CodeComposer and metadata

We have now seen an example of a snippet with an implementation of the equals method. We have introduced metadata in the model object to control which attributes are used in this implementation. But how does the Code Composer deal with required, but missing metadata? Let’s take a closer look at our current implementation:

Listing 4.14 Model
1<#local key = "businesskey" />
2<#local bkAttributes = modelObject.findAttributesByMetaData(key) />
3<#if bkAttributes?size == 0>
4        <#stop "No metadata on attributes were found for modelObject
5               ${modelObject.name}. Please add <businesskey> with an integer
6               that indicates the order of processing to an attribute that
7               should be used for the implementation of the equals method." />

First, we choose the name of the metadata we are going to search for. In our example: businesskey. Then we use a predefined method findAttributesByMetaData on the modelObject which takes the metadata key as an argument. We expect to retrieve a list with one or more attributes that have this metadata defined. Otherwise, our generated Java code will not compile.

To prevent this from happening, it is good to immediately stop the generation process when such a situation occurs. In this example we stop the generation process with the Code Composer when the list of attributes is 0. In the stop condition we can enter a useful message with information on why the generation process has been stopped and what is needed to successfully continue generation.

Another solution might be to automatically add the metadata with a default value to the data model. When we try to retrieve some metadata on a model object and that metadata does not exist yet, it is possible to have the Code Composer directly add that metadata to the data model with some default value. This is very convenient when you create a new code instruction and all model objects should have this metadata. Instead of having to add it yourself to every object, the CodeComposer will add it for you.

4.2.4. Alternative equals and hashCode implementations

In our current example we have shown method implementations that are based on Java 8, which might be outdated for the code you are working with today. It does not matter what language or what kind of architecture you use in your application. You as a developer are in control of the code that is generated by the CodeComposer. By writing your own code instructions and snippets, you can write any kind of implementation that suits your needs. Lombok is used by many developers and can also be used to create implementations for our methods.

This is an example of a code instruction with a Lombok implementation: