Skip to main content

Current Situation

Java has supported immutable record types since Java 14, and Kotlin's data classes can easily implement immutable classes, even supporting copy functions. However, they are designed for simple immutable objects, and have problems dealing with complex deep data structure.

The reason why deep, complex immutable objects are difficult to handle is not how to create a brand new data structure from scratch, which is very simple for any programming language. The real difficulty is wanting to make some local adjustments to an existing data structure and build a new data structure. This is very difficult for current Java and Kotlin, please see

First define an immutable tree node

TreeNode.java
record TreeNode(
String name,
List<TreeNode> childNodes
) {}

Prepare an old object

var oldTreeNode = ...blabla... 

Let's implement three data change operations in order from simple to complex to create a brand new object

  1. Change the name property of the root node

    TreeNode newTreeNode = new TreeNode(
    "Hello", // Set root node name
    oldTreeNode.childNodes()
    );

    This case is very simple, and the problems of Java/Kotlin are not obvious yet.

  2. Change the name property of the first level child node

    Breadcrumb conditions:

    • Position of first level child node: pos1
    TreeNode newTreeNode = new TreeNode(
    oldTreeNode.name(),
    IntStream
    .range(0, oldTreeNode.childNodes().size())
    .mapToObj(index1 -> {
    TreeNode oldChild1 = oldTreeNode.childNodes().get(index1);
    if (index1 != pos1) {
    return oldChild1;
    }
    return new TreeNode(
    "Hello", // Set name of first level child node
    oldChild1.childNodes()
    );
    })
    .toList()
    );
  3. Change the name property of the second level child node

    Breadcrumb conditions:

    • Position of first level child node: pos1
    • Position of second level child node: pos2
    TreeNode newTreeNode = new TreeNode(
    oldTreeNode.name(),
    IntStream
    .range(0, oldTreeNode.childNodes().size())
    .mapToObj(index1 -> {
    TreeNode oldChild1 = oldTreeNode.childNodes().get(index1);
    if (index1 != pos1) {
    return oldChild1;
    }
    return new TreeNode(
    oldChild1.name(),
    IntStream
    .range(0, oldChild1.childNodes().size())
    .mapToObj(index2 -> {
    TreeNode oldChild2 = oldChild1.childNodes().get(index2);
    if (index2 != pos2) {
    return oldChild2;
    } else {
    return new TreeNode(
    "Hello", // Set name of second level child node
    oldChild2.childNodes()
    );
    }
    })
    .toList()
    );
    })
    .toList()
    );
info

It can be seen that as long as the object tree has some depth, creating a new immutable object based on another immutable object (i.e. secondary "modification") will be a nightmare.