DEV Community

loading...

Java - reduce redundant objects with Flyweight Design pattern

Abhinav Pandey
All things Java!!
Updated on ・4 min read

Object creation is the most fundamental operation in OOP. It would be hard to count the number of objects we create(knowingly or behind the scene) even in the most trivial of use cases.

Each object is created on the heap and will take up some space until it is garbage collected. Long running programs will keep the heap occupied. Similarly, concurrently running threads will multiply the memory in use.

Let's look at a simple example:
I have an application which returns me a large number of data points to plot on a graph. The data point contains two informations - the data and how the point looks on the graph

private static class DataPoint {
        private final double data;
        private final Point point;

        public DataPoint(double data, Point point) {
            this.data = data;
            this.point = point;
        }
    }
Enter fullscreen mode Exit fullscreen mode

Each Point in turn has a shape and a color:

class Point {
    private String color;
    private String shape;

    public Point(String color, String shape) {
        this.color = color;
        this.shape = shape;
    }
}
Enter fullscreen mode Exit fullscreen mode

So lets create a consumer which will create some data points for me.

        DataPoint[] dp = new DataPoint[N];
        for(int i=0; i<N; i++) {
            double data = Math.random(); // or whatever data source
            Point point = data > 0.5 ? new Point("Green", "Circle") : new Point("Red", "Cross");
            dp[i] = new DataPoint(data, point);
        }   

Enter fullscreen mode Exit fullscreen mode

Looks simple and works fine. Let's look at the amount of memory we used while creating this DataPoint array.

  1. DataPoint object -> 2 references + Padding => ~24 bytes.
  2. In turn, each DataPoint object has a Point object which takes up memory of its own -> 2 references + the string pool literals(negligible) + Padding => ~24 bytes

So the total memory used by our array becomes (24+24)*N = 48*N bytes. Not much? - Well, depends on N and depends on the number of concurrent threads. For N = 1000 this means 48 KBs. Add 100 threads to it => 4.8 MBs.

The problem

There are practically only two variations of points - Green circle and Red cross but we created N Point objects.

The solution - Flyweights

Alt Text

The principle is simple - avoid redundant values in objects. To define our solution, lets define two terms:

  1. Repeating properties - The properties that are likely to remain the same for many instances of the object.
  2. Unique properties - Properties that change with every instance of the object.

In our scenario each half of the data point objects contain the same value for Point (Probabilistically).

The flyweight design pattern suggests that parts of the object which are likely to repeat among large number of objects should be shared/reused among them rather than being repeated. Some important use cases when we should consider using flyweights:

  1. The repeating properties are heavy - The Point object is heavy in this case.
  2. There are a limited number of values that the repeating properties can take. - One example is the Boolean class. It can take only two values true or false.

There are many ways to implement this. Let's look at a few ways to implement the Flyweight pattern.

Method 1 - static factories

We expose a static factory method for each of the two possible instances of Point object.

class Point {
    private String color;
    private String shape;
    private static Point GREEN_CIRCLE = new Point("Green", "Circle");
    private static Point RED_CROSS = new Point("Red", "Cross");

    private Point(String color, String shape) {
        this.color = color;
        this.shape = shape;
    }

    public static Point getGreenCircle() {
        return GREEN_CIRCLE;
    }
    public static Point getRedCross() {
        return RED_CROSS;
    }
}
Enter fullscreen mode Exit fullscreen mode

Features:

  1. Named methods which describe the type of object being returned.
  2. Private static instances - immutable and only one copy.
  3. Private constructor - to disallow object creation from outside.

Method 2 - Enums

enum Point {
    GREEN_CIRCLE("Green", "Circle"),
    RED_CROSS("Red", "Cross");

    private final String color;
    private final String shape;

    Point(String color, String shape) {
        this.color = color;
        this.shape = shape;
    }
}
Enter fullscreen mode Exit fullscreen mode

Features:

  1. Constructor is implicitly private.
  2. An enum conveys the purpose clearly that only a few variations are possible.
  3. Immutable data.

Both static factories and enums will create only 2 copies of Point object no matter how many times they are required.

Method 3 - Caching

The above two examples work well when all variations are already known. Another scenario can be when one of the fields can take more values than anticipated. However, the other values of the object do not change unless that varying field changes.

Let's take a different example in this case. We are creating a Product object. You know that for one product id only one value of a Product object is possible. Creating the Product object again is heavy as you need to set a lot of properties. It is better to keep a copy of the product cached so that object creation is not required twice for a single product. Let's look at the code for it.

class ProductCache {
    public static Map<String, Product> productMap = new HashMap<>();

    public Product getProduct(String productId) {
        Product product;
        if(productMap.containsKey(productId)) {
            product = productMap.get(productId);
        } else {
            product = new Product(/*properties*/);
            productMap.put(productId, product);
        }
        return product;
    }
}
Enter fullscreen mode Exit fullscreen mode

Once we create a Product object, we keep it cached into the map against its product id so that we never initialize same the product again.

Hope you enjoyed this introduction to Flyweight pattern and find ways to implement it in your applications.
Thanks for reading.

Discussion (4)

Collapse
tehmoros profile image
Piotr "MoroS" Mrożek • Edited

Nicely explained. :)

One thing though: when you take data deserialization into account (whether it's "JSON/XML/gRPC to object" or ORM), then only the enum-based solution truly works "out of the box". That of course is a basic scenario.

Luckily most solutions (JPA/Hibernate, Jackson, etc.) offer the possibility of implementing a custom deserializer (again, whether it's a data deserializer or a ORM converter), so with little effort you can implement other strategies as well.

Collapse
ramanbedi1989 profile image
Raman Bedi

Hi Abhinav,
Thanks for the article, it helps to understand Flyweight DP with examples.
Please clarify one doubt that I have. For the ProductCache, don’t we need to add the newly instantiated “product” to the “productMap” before returning it so that the following lookups always return the cached instance?

Collapse
abh1navv profile image
Abhinav Pandey Author

Correct. Thanks for the correction.

Collapse
gorynych profile image
Gorynych

Wow! It's a Design Pattern now... Okay... )))