Chapter 4. Java Interop and Polymorphism
As already mentioned, Clojure runs on the Java virtual machine, and it uses this to its advantage. Not only is the JVM a production-hardened platform to run on, being a JVM language gives Clojure access to many different Java libraries as well as its own. We will look at how Clojure talks to Java classes in this chapter. We will also look at another way that it benefits from using Java classes for some types of polymorphism and how Clojure handles this polymorphism more generally as well.
First, we will explore Java interop.
Handling Interop with Java
When a new language comes into being, it faces the library problem. That is, to be useful in everyday situations, a language needs to do all the things that current dominant languages do. These current dominant languages have a full array of libraries that support things like parsing JSON and logging.
Clojure solved this new language library problem by running on the JVM and having interoperability with Java classes. When you use Clojure, you can use Java classes and Java libraries. Clojure builds on the strength of the production-hardened and tested JVM and existing Java libraries. In fact, many of the popular Clojure libraries in use today utilize Java libraries as fundamental building blocks. We are going to cover the most common areas that you will encounter: how to import Java libraries/classes, how to create new instances of them, and how to interact with their methods.
Note
Don’t sweat it if you don’t have a Java background. We are just dipping our toes in. The water is fine here.
Clojure uses the new and dot special form to interact with Java classes, but provides more idiomatic forms that use them under the covers. We can take a look at this with one of Clojure’s strings. For example, let’s use the string "caterpillar"
, which is one of the characters that Alice met in Wonderland. A string is really just a string from Java—it is a java.lang.String
.
Note
A String in Java is an instance of java.lang.String
. A string in Clojure is the exact same thing.
(
class
"caterpillar"
)
;; -> java.lang.String
We can transform this string to uppercase using the String’s method toUpperCase
.
The way to call toUpperCase
in Java, would be to call it on the string itself with a dot:
String
cString
=
new
String
(
"caterpillar"
);
cString
.
toUpperCase
();
We do this in Clojure by using a dot followed by the object and the object’s method that we wish to invoke:
(
.
"caterpillar"
toUpperCase
)
;; -> "CATERPILLAR"
There is also a shorthand dot prefix way to do the same thing by using a dot followed by the object’s method that we wish to invoke:
(
.toUpperCase
"caterpillar"
)
;; -> "CATERPILLAR"
If the Java method takes arguments, they are included after the object. For example, if we wanted to find the index of the substring “pillar” using the string’s indexOf
method (which takes a character as a parameter), in Java we would do something like:
String
c1String
=
new
String
(
"caterpillar"
);
String
c2String
=
new
String
(
"pillar"
);
c1String
.
indexOf
(
c2
);
In Clojure, the first argument is the string we want to call the method on, and the second is the argument:
(
.indexOf
"caterpillar"
"pillar"
)
;; -> 5
We can create instances of Java objects with new
:
(
new
String
"Hi!!"
)
;; -> "Hi!!"
Another way to create an instance of a Java class from Clojure is to use a shorthand form for creation by using a dot right after the class name:
(
String.
"Hi!!"
)
;; -> "Hi!!"
What if we wanted to work with a Java object that we needed to import? Let’s take an example of needing to reach in and do some interop networking with Java. In particular, we need to work with a java.net.InetAddress
that represents an IP. How do we create one? The first thing we need to do is import the Java class. We can do this by using :import
in the namespace with the package name and the class that we wish to import:
(
ns
caterpillar.network
(
:import
(
java.net
InetAddress
)))
We can now create an instance of InetAddress
. The way to create a new InetAddress
in Java is to use a static method called getByName
that takes a string of the hostname and resolves the matching IP address. To execute static methods on Java classes from Clojure, we use a forward slash:
(
InetAddress/getByName
"localhost"
)
;; -> #<Inet4Address localhost/127.0.0.1>
Now we have a Java object that we act on and get a property off of with the dot notation:
(
.getHostName
(
InetAddress/getByName
"localhost"
))
;; -> "localhost"
We can also use Java classes without importing them by using their fully qualified names:
(
java.net.InetAddress/getByName
"localhost"
)
;; -> #<Inet4Address localhost/127.0.0.1>
There is also a doto
macro, which allows us to take a Java object and then act on it in succession with a list of operations. This is useful if we have a Java object that we need to mutate in a series of steps.
We can show this with Java’s StringBuffer
object, which is a class that helps build strings. It takes an initial string as an argument. Then, if we call the method append
with a string, it will change the object by adding that string to it:
(
def
sb
(
doto
(
StringBuffer.
"Who "
)
(
.append
"are "
)
(
.append
"you?"
)))
(
.toString
sb
)
;; -> "Who are you?"
This doto
syntax is much nicer to read than the alternative nested version:
(
def
sb
(
.append
(
.append
(
StringBuffer.
"Who "
)
"are "
)
"you?"
))
(
.toString
sb
)
;; -> "Who are you?"
Table 4-1 shows the code equivalents of using interop with Java compared to Clojure.
Java | Clojure |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
The ability to use Java classes and libraries in such an easy way is a real advantage in Clojure. As the popularity of Clojure has spread, there are now more Clojure libraries than ever to choose from. For example, you can use Java classes to generate a universally unique identifier (UUID). Because they are very common in generating IDs for orders, customers, or images in computer programs, here is how you can use a UUID in your Clojure program:
(
import
'java.util.UUID
)
(
UUID/randomUUID
)
;; -> #uuid "f9877259-2cc1-4e5a-8c6f-8b51499cb9f8"
Importing the Java class for UUID.
Calling the method on the Java class to give us a unique and random UUID.
You now have the power to interact with Java’s classes.
It is time to look at another way that Java’s classes help out Clojure: polymorphism. We will take a closer look at the different ways Clojure achieves polymorphism next.
Practical Polymorphism
In an object-oriented language like Java, there are a large amount of types for every situation. Clojure takes another approach. It has a small amount of types and many different functions for them. However, being pragmatic, Clojure realizes that polymorphism is flexible and useful for some situations. Let’s take a look at a few ways that Clojure can flex its polymorphic muscles.
If we wanted to have a function that would behave differently based on the kind of input we had, we could use a case like statement. This example uses a function called cond
that behaves differently depending on whether the argument is a keyword, string, or number, and returns the caterpillar’s questions to Alice:
(
defn
who-are-you
[
input
]
(
cond
(
=
java.lang.String
(
class
input
)
)
"String - Who are you?"
(
=
clojure.lang.Keyword
(
class
input
)
)
"Keyword - Who are you?"
(
=
java.lang.Long
(
class
input
)
)
"Number - Who are you?"
)
)
(
who-are-you
:alice
)
;; -> "Keyword - Who are you?"
(
who-are-you
"alice"
)
;; -> "String - Who are you?"
(
who-are-you
123
)
;; -> "Number - Who are you?"
(
who-are-you
true
)
;; -> nil
The class input is compared, and if it is a string it will return
"String - Who are you?"
If it is a keyword, it will return
"Keyword - Who are you?"
If it is a number (class of
Long
), it will return"Number - who are you?"
When called with a keyword, returns the clause that matched the keyword
class
.When called with a string, returns the clause that matched the string class.
When called with a number, returns the clause that matched the number class.
When called with a boolean, returns
nil
because there is no matchingcond
clause.
We can express this with polymorphism in Clojure with multimethods. We first need to define the
multimethod and a function that specifies how it is going to dispatch; that is, how it is going to decide which of the following methods to use. In the case of our who-are-you
function, the dispatch is
going to be on the class of the input:
(
defmulti
who-are-you
class
)
(
defmethod
who-are-you
java.lang.String
[
input
]
(
str
"String - who are you? "
input
)
)
(
defmethod
who-are-you
clojure.lang.Keyword
[
input
]
(
str
"Keyword - who are you? "
input
)
)
(
defmethod
who-are-you
java.lang.Long
[
input
]
(
str
"Number - who are you? "
input
)
)
(
who-are-you
:alice
)
;; -> "Keyword - who are you? :alice"
(
who-are-you
"Alice"
)
;; -> "String - who are you? Alice"
(
who-are-you
123
)
;; -> "Number - who are you? 123"
(
who-are-you
true
)
;; -> IllegalArgumentException No method in multimethod
;;'who-are-you' for dispatch value: class java.lang.Boolean
We are declaring that the
who-are-you
function is going to be a multimethod with a single argument. The function that will be used for choosing what method to use is theclass
function. This class dispatch function takes only a single argument.Using
defmethod
, we say that if the class of the input is a String, then we will pass the original value of the input to astr
function that will construct the"String - who are you .."
return value.We do a similar
defmethod
dispatching on theKeyword
class.And another
defmethod
dispatching on theLong
class.When we call the
who-are-you
function with a keyword, it not only uses the method defined for keywords, it also returns the value of the:alice
input in the return string.Calling with a string results in the function defined for the string class along with the “Alice” value in the return string.
Calling with a number also shows that the function defined for the
Long
class was used along with the number 123.Calling with a boolean throws an error because it couldn’t find a matching dispatch method.
We could also provide a default dispatch method using the :default
keyword, so if we don’t have a matching one it will use that instead of throwing an exception:
(
defmethod
who-are-you
:default
[
input
]
(
str
"I don't know - who are you? "
input
))
(
who-are-you
true
)
;; -> "I don't know - who are you? true"
In the previous example, the dispatch
function is called first, which is the class of the input. Then, using that value,
it decides what method to use.
Really, any function can be given to dispatch on. So, we can even inspect the value of a map as input. What if we wanted to have a multimethod to control the conversation of the caterpillar based on the value of Alice’s question?
In this example, we are going to create a multimethod that is dispatched on a function of her height, so that she knows which side of the mushroom to eat from.
First, we declare that the function named eat-mushroom
is going to be a multimethod with defmulti
. This
time, instead of using the class function, we are going to define our own. It is a function that takes a one parameter, height.
If the height is less than 3, then the :grow
keyword will be returned; otherwise, the :shrink
keyword will be returned:
(
defmulti
eat-mushroom
(
fn
[
height
]
(
if
(
<
height
3
)
:grow
:shrink
)))
The :grow
and :shrink
keywords that we are choosing to dispatch on now need defmethods
for each of them.
For the :grow
keyword, we will simply return a helpful string that tells the user to eat the right side to grow:
(
defmethod
eat-mushroom
:grow
[
_
]
"Eat the right side to grow."
)
Then the :shrink
keyword will do something similar, only it will return a helpful string to eat the other side of the mushroom:
(
defmethod
eat-mushroom
:shrink
[
_
]
"Eat the left side to shrink."
)
Note
You will notice that we are using an underscore instead of using a name for the parameter in the defmethods
. This is
an idiomatic way to say that we don’t care about the value of the input here—we are not going to use it, and effectively
ignore it.
When we try call the eat-mushroom
function with a small height, it will tell us the hint to grow:
(
eat-mushroom
1
)
;; -> "Eat the right side to grow."
Likewise
,when
we
call
it
with
a
big
height
,it
will
tell
us
the
hint
to
shrink.
(
eat-mushroom
9
)
;; -> "Eat the left side to shrink."
Another way to use polymorphism in Clojure is to use protocols. Where multi-methods are great using polymorphism on one function, sometimes protocols can handle polymorphism elegantly with groups of functions.
Let’s take a look at this with the eat-mushroom
example using a String
, Keyword
, and a Long
. First, we need to define the protocol:
(
defprotocol
BigMushroom
(
eat-mushroom
[
this
]))
Next, we implement the protocol for all our types at once using extend-protocol
.
The parameter this
is the thing that we are going to perform the function on:
(
extend-protocol
BigMushroom
java.lang.String
(
eat-mushroom
[
this
]
(
str
(
.toUpperCase
this
)
" mmmm tasty!"
))
clojure.lang.Keyword
(
eat-mushroom
[
this
]
(
case
this
:grow
"Eat the right side!"
:shrink
"Eat the left side!"
))
java.lang.Long
(
eat-mushroom
[
this
]
(
if
(
<
this
3
)
"Eat the right side to grow"
"Eat the left side to shrink"
)))
Now, we can call the function with the data types quite naturally:
(
eat-mushroom
"Big Mushroom"
)
;; -> "BIG MUSHROOM mmmm tasty!"
(
eat-mushroom
:grow
)
;; -> "Eat the right side!"
(
eat-mushroom
1
)
;; -> "Eat the right side to grow"
We have been using protocols to add methods to existing data structure. However, what if we want to add our own?
Clojure’s answer to this is data types. There are two solutions depending on what you are looking for.
If you need structured data, the answer is to use defrecord
, which actually creates a class with a new type.
The defrecord
form defines the fields that the class will hold. To demonstrate, we will make a defrecord
to describe the mushroom
that the caterpillar was sitting on when Alice met him. It had a color and a height:
(
defrecord
Mushroom
[
color
height
])
;; -> caterpillar.network.Mushroom
Now we can create a new mushroom object with a dot notation:
(
def
regular-mushroom
(
Mushroom.
"white and blue polka dots"
"2 inches"
))
;; -> #'caterpillar.network/regular-mushroom
(
class
regular-mushroom
)
;; -> caterpillar.network.Mushroom
Notice that the class type that was produced was the same as the one defined by defrecord
. We can get the values
with the dot-dash that is preferred over the dot-prefix form for accessing fields:
(
.-color
regular-mushroom
)
;; -> "white and blue polka dots"
(
.-height
regular-mushroom
)
;; -> "2 inches"
We can combine the structured data and type that defrecord
gives us with protocols to implement interfaces.
The mushroom that Alice encountered in Wonderland was special. If she ate from one side of the mushroom it
made her grow big, and the other side made her grow small. Let’s define a protocol for a mushroom to be edible.
Of course, it will work differently on different types of mushrooms. The protocol will be called Edible
and it will consist of two functions: one called bite-right-side
and one called bite-left-side
. Each of these functions takes this
as an argument, which is the record itself that we will call it with later:
(
defprotocol
Edible
(
bite-right-side
[
this
])
(
bite-left-side
[
this
]))
Now that we have a protocol defined, we can start having records that implement it. The type of record that we will make is a WonderlandMushroom
:
(
defrecord
WonderlandMushroom
[
color
height
]
Edible
(
bite-right-side
[
this
]
(
str
"The "
color
" bite makes you grow bigger"
)
)
(
bite-left-side
[
this
]
(
str
"The "
color
" bite makes you grow smaller"
)
)
)
Creates a
WonderlandMushroom
record that takes arguments that set the color and height.Implements the
Edible
protocol.Defines the implementation for the
bite-right-side
function.Defines the implementation for the
bite-left-side
function.
Next, we define a record for a RegularMushroom
. It is very similar to the WonderlandMushroom
.
It has the same constructor, and implements the Edible
protocol. The main difference is in
what the functions do. The bites of the mushroom don’t make you grow bigger or smaller.
They just taste bad:
(
defrecord
RegularMushroom
[
color
height
]
Edible
(
bite-right-side
[
this
]
(
str
"The "
color
" bite tastes bad"
))
(
bite-left-side
[
this
]
(
str
"The "
color
" bite tastes bad too"
)))
Finally, we can construct our mushrooms with the record dot syntax:
(
def
alice-mushroom
(
WonderlandMushroom.
"blue dots"
"3 inches"
))
(
def
reg-mushroom
(
RegularMushroom.
"brown"
"1 inches"
))
When we take bites from the WonderlandMushroom
, they give us the growing messages:
(
bite-right-side
alice-mushroom
)
;; -> "The blue dots bite makes you grow bigger"
(
bite-left-side
alice-mushroom
)
;; -> "The blue dots bite makes you grow smaller"
And when we take bites from the RegularMushroom
, they taste bad:
(
bite-right-side
reg-mushroom
)
;; -> "The brown bite tastes bad"
(
bite-left-side
reg-mushroom
)
;; -> "The brown bite tastes bad too"
We have gone through a fun example with protocols and Alice in Wonderland. But we will stop for a moment to talk about when to use protocols in a practical setting.
A real-world example of protocols is implementing different types of persistence. It is common in a business setting to want to write information to a data source.
The information that we write stays the same, but we are writing it to different types of data sources. We could have one defrecord
type persist the
result to a database and another could persist the result to an Amazon S3 bucket. We can easily adapt the same technique we used with mushrooms to store information.
In the previous example, we were using records that held structured data values. Sometimes we don’t really care about the structure or the map lookup features provided by defrecord
, we just need an object with a type to save memory. In this case, we should reach for deftype
. We can show this using the
mushroom example, except this time, we don’t care what color the mushroom is, or how
tall it is.
The protocol itself doesn’t change:
(
defprotocol
Edible
(
bite-right-side
[
this
])
(
bite-left-side
[
this
]))
The difference is that instead of using defrecord
, we are now going to use deftype
:
(
deftype
WonderlandMushroom
[
]
Edible
(
bite-right-side
[
this
]
(
str
"The bite makes you grow bigger"
)
)
(
bite-left-side
[
this
]
(
str
"The bite makes you grow smaller"
)
)
)
Use
deftype
to define aWonderlandMushroom
with no arguments.It implements the
Edible
protocol.The function for
bite-right-side
is simply a string telling you that it makes you bigger.The function for
bite-left-side
likewise tells you that it will make you smaller.
The RegularMushroom
looks the same as the WonderlandMushroom
(with less magic, of course):
(
deftype
RegularMushroom
[]
Edible
(
bite-right-side
[
this
]
(
str
"The bite tastes bad"
))
(
bite-left-side
[
this
]
(
str
"The bite tastes bad too"
)))
We construct the mushrooms the same way as before with the dot notation:
(
def
alice-mushroom
(
WonderlandMushroom.
))
(
def
reg-mushroom
(
RegularMushroom.
))
Tasting the mushrooms gives the growing response for the WonderlandMushroom
and the taste
bad response for the RegularMushroom
:
(
bite-right-side
alice-mushroom
)
;; -> "The bite makes you grow bigger"
(
bite-left-side
alice-mushroom
)
;; -> "The bite makes you grow smaller"
(
bite-right-side
reg-mushroom
)
;; -> "The bite tastes bad"
(
bite-left-side
reg-mushroom
)
;; -> "The bite tastes bad too"
The main difference between using protocols with defrecord
and deftype
is how you want your data organized. If you want structured data, choose defrecord
. Otherwise, use deftype
. Why? Because with records, you get type-based dispatch and you can still manipulate your data like maps (which is great for reuse). Sometimes, when this structured data isn’t needed, you can use deftype
to avoid paying for the overhead for something you don’t want.
Clojure protocols and data types are powerful solutions when you need them, but beware! Many people who come from an object-oriented background tend to reach for them and use them just because they are similar to how they are used to modeling and thinking about code.
Caution
Think before you use protocols.
In the example we just did using protocols, we could have actually used other ways to get the same result. Instead of using a protocol, we could have used a simple map to distinguish what kind of mushroom it was.
We could define the bite-right-side
function to take a mushroom as an argument. This argument would be a map containing a key of :type
. If the :type
key value is equal to the string "wonderland"
, then we could know that it was a special mushroom that could make you grow bigger. Otherwise, it would just be considered a regular mushroom:
(
defn
bite-right-side
[
mushroom
]
(
if
(
=
(
:type
mushroom
)
"wonderland"
)
"The bite makes you grow bigger"
"The bite tastes bad"
)
)
We could then define a similar function for the left side:
(
defn
bite-left-side
[
mushroom
]
(
if
(
=
(
:type
mushroom
)
"wonderland"
)
"The bite makes you grow smaller"
"The bite tastes bad too"
))
When we bite into the wonderland mushroom, with the map key :type
set to "wonderland"
, it will give us the growing messages:
(
bite-right-side
{
:type
"wonderland"
})
;; -> "The bite makes you grow bigger"
(
bite-left-side
{
:type
"wonderland"
})
;; -> "The bite makes you grow smaller"
And of course, when we bite into the regular mushroom, it tastes bad:
(
bite-right-side
{
:type
"regular"
})
;; -> "The bite tastes bad"
(
bite-left-side
{
:type
"regular"
})
;; -> "The bite tastes bad too"
As you can see, there are multiple ways to get functions to behave differently based on values and types.
Protocols should be used sparingly. In most situations, a pure function or multimethod can be used instead. A nice thing about Clojure is that it is easy to move from just maps to records when you need to. This allows you to delay the decision of whether or not to use protocols.
You can now handle real-world state and concurrency with atoms, refs, and agents. You can also handle polymorphism in a practical way. The tools to conquer stuctured data, types, and interfaces where pure functional approaches don’t work are in your hands. You have all the skills you need to start creating your own Clojure projects and explore the ecosystem in the next chapter.
Get Living Clojure now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.