Lazy Loading and Caching via Sticky Cactoos Primitives
You obviously know what lazy loading is, right? And you no doubt know about caching. To my knowledge, there is no elegant way in Java to implement either of them. Here is what I found out for myself with the help of Cactoos primitives.
Let’s say we need an object that will encrypt some text. Speaking in a more object-oriented way, it will encapsulate the text and become its encrypted form. Here is how we will use it (let’s create tests first):
interface Encrypted { String asString() throws IOException; } Encrypted enc = new EncryptedX("Hello, world!"); System.out.println(enc.asString());
Now let’s implement it, in a very primitive way, with one primary constructor. The encryption mechanism will just add +1
to each byte in the incoming data, and will assume that the encryption won’t break anything (a very stupid assumption, but for the sake of this example it will work):
class Encrypted1 implements Encrypted { private final String text; Encrypted1(String txt) { this.data = txt; } @Override public String asString() { final byte in = this.text.getBytes(); final byte[] out = new byte[in.length]; for (int i = 0; i < in.length; ++i) { out[i] = (byte) (in[i] + 1); } return new String(out); } }
Looks correct so far? I tested it and it works. If the input is "Hello, world!"
, the output will be "Ifmmp-!xpsme\""
.
Next, let’s say that we want our class to accept an InputStream
as well as a String
. We want to call it like this, for example:
Encrypted enc = new Encrypted2( new FileInputStream("/tmp/hello.txt") ); System.out.println(enc.toString());
Here is the most obvious implementation, with two primary constructors (again, the implementation is primitive, but works):
class Encrypted2 implements Encrypted { private final String text; Encrypted2(InputStream input) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); while (true) { int one = input.read(); if (one < 0) { break; } baos.write(one); } this.data = new String(baos.toByteArray()); } Encrypted2(String txt) { this.text = txt; } // asString() is exactly the same as in Encrypted1 }
Technically it works, but stream reading is right inside the constructor, which is bad practice. Primary constructors must not do anything but attribute assignments, while secondary ones may only create new objects.
Let’s try to refactor and introduce lazy loading:
class Encrypted3 { private String text; private final InputStream input; Encrypted3(InputStream stream) { this.text = null; this.input = stream; } Encrypted3(String txt) { this.text = txt; this.input = null; } @Override public String asString() throws IOException { if (this.text == null) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); while (true) { int one = input.read(); if (one < 0) { break; } baos.write(one); } this.text = new String(baos.toByteArray()); } final byte in = this.text.getBytes(); final byte[] out = new byte[in.length]; for (int i = 0; i < in.length; ++i) { out[i] = (byte) (in[i] + 1); } return new String(out); } }
Works great, but looks ugly. The ugliest part is these two lines of course:
this.text = null; this.input = null;
They make the object mutable and they’re using NULL. It’s ugly, trust me. Unfortunately, lazy loading and NULL references always come together in classic examples. However there is a better way to implement it. Let’s refactor our class, this time using Scalar
from Cactoos:
class Encrypted4 implements Encrypted { private final IoCheckedScalar<String> text; Encrypted4(InputStream stream) { this( () -> { ByteArrayOutputStream baos = new ByteArrayOutputStream(); while (true) { int one = stream.read(); if (one < 0) { break; } baos.write(one); } return new String(baos.toByteArray()); } ); } Encrypted4(String txt) { this(() -> txt); } Encrypted4(Scalar<String> source) { this.text = new IoCheckedScalar<>(source); } @Override public String asString() throws IOException { final byte[] in = this.text.value().getBytes(); final byte[] out = new byte[in.length]; for (int i = 0; i < in.length; ++i) { out[i] = (byte) (in[i] + 1); } return new String(out); }
Now it looks way better. First of all, there is only one primary constructor and two secondary ones. Second, the object is immutable. Third, there is still a lot of room for improvement: we can add more constructors which will accept other sources of data, for example File
or a byte array.
In a nutshell, the attribute that is supposed to be loaded in a “lazy” way is represented inside an object as a “function” (lambda expression in Java 8). Until we touch that attribute, it’s not loaded. Once we need to work with it, the function gets executed and we have the result.
There is one problem with this code though. It will read the input stream every time we call asString()
, which will obviously not work, since only the first time will the stream have the data. On every subsequent call the stream will simply be empty. Thus, we need to make sure that this.text.value()
executes the encapsulated Scalar
only once. All later calls must return the previously calculated value. So we need to cache it. Here is how:
class Encrypted5 implements Encrypted { private final IoCheckedScalar<String> text; // same as above in Encrypted4 Encrypted5(Scalar<String> source) { this.data = new IoCheckedScalar<>( new StickyScalar<>(source) ); } // same as above in Encrypted4
This StickyScalar
will make sure that only the first call to its method value()
will go through to the encapsulated Scalar
. All other calls will receive the result of the first call.
The last problem to solve is about concurrency. The code we have above is not thread safe. If I create an instance of Encrypted5
and pass it to two threads, which call asString()
simultaneously, the result will be unpredictable, simply because StickyScalar
is not thread-safe. There is another primitive to help us out though, called SyncScalar
:
class Encrypted5 implements Encrypted { private final IoCheckedScalar<String> text; // same as above in Encrypted4 Encrypted5(Scalar<String> source) { this.data = new IoCheckedScalar<>( new SyncScalar<>( new StickyScalar<>(source) ) ); } // same as above in Encrypted4
Now we’re safe and the design is elegant. It includes lazy loading and caching.
I’m using this approach in many projects now and it seems convenient, clear, and object-oriented.
You may also find these related posts interesting: Why InputStream Design Is Wrong; Try. Finally. If. Not. Null.; Each Private Static Method Is a Candidate for a New Class; How I Would Re-design equals(); Object Behavior Must Not Be Configurable;
Published on Java Code Geeks with permission by Yegor Bugayenko, partner at our JCG program. See the original article here: Lazy Loading and Caching via Sticky Cactoos Primitives Opinions expressed by Java Code Geeks contributors are their own. |
The javax.inject.Provider and java.util.function.Supplier interfaces provide lazy loading and caching semantics without all these hoops to jump through and are built into the JDK and well documented. Nothing new to learn.
add in the @Memoize annotation on the implementations and you get the caching for free as well.