User Guide

What is LJV?

LJV is a tool for visualizing Java data structures, using Reflection API and Graphviz. Its original version was developed by John Hamer in 2004 and released under GNU GPL (see the original project page).

This project aims to upgrade this tool to modern Java and make it an open source library in the modern sense of the word.

Getting started

The tool requires Java 11 or later version. You can use Maven or Gradle to import the LJV dependency:

<dependency>
  <groupId>org.atp-fivt</groupId>
  <artifactId>ljv</artifactId>
  <version>1.03</version>
</dependency>

Then use drawGraph method of LJV class to get graphviz representation of any Java Object:

import org.atpfivt.ljv.LJV;

...

System.out.println(new LJV().drawGraph("Hello"));

The printed result will look like following:

digraph Java {
    rankdir="TB";
    node[shape=plaintext]
    . . .
}

This is a graph definition in DOT graph description language. This script can be converted into a bitmap or vector image using dot utility from graphviz package:

dot -Tpng myfile.dot > myfile.png

Or, even simpler, the resulting script can be pasted to GraphViz Online and visualized like this:

Diagram

Or, even more simpler, you may use the following utility function in order to save some tedious copy-pasting:

public static void browse(LJV ljv, Object obj) {
    try {
        var dot = URLEncoder.encode(
              ljv.drawGraph(obj), "UTF8").replaceAll("\\+", "%20");
        Desktop.getDesktop().browse(
              new URI("https://dreampuf.github.io/GraphvizOnline/#"
                      + dot));
    } catch (Exception e) {
        throw new IllegalStateException(e);
    }
}

The diagram can be interpreted as following:

  • The String object that we are visualizing is represented as a 'root' node of the graph

  • This object has two fields of primitive types (coder and hash), with values set to zero.

  • This object through the field value references another object of an array type. This array has 5 elements.

  • Elements of the array are primitive values 72, 101, 108, 108 and 111.

Diagram customization

LJV has the ability to customize the view of the graph.

For example, you can specify any class to be treated as a primitive type with setTreatAsPrimitive method. In this case, the result of toString call will be used as its value:

String graph = new LJV()
        .setTreatAsPrimitive(String.class)
        .drawGraph(
                new Object[]{new String[]{"a", "b", "c"}, new int[]{1, 2, 3}}
        );
Diagram

You can also change the drawing direction:

ArrayList<Object> a = new ArrayList<>();
        a.add(new Person("Albert", true, 35));
        a.add(new Person("Betty", false, 20));
        a.add(new java.awt.Point(100, -100));

String graph = new LJV()
        .setTreatAsPrimitive(String.class)
        .setDirection(Direction.LR)
        .drawGraph(a);

Direction is an enum with four values that define the graph drawing direction (top to bottom, left to right, etc):

Diagram

It is also possible to set colors and styles for the components of the graph:

Node n1 = new Node("A");
n1.level = 1;
AnotherNode n2 = new AnotherNode("B");
n2.level = 2;
AnotherNode n3 = new AnotherNode("C");
n3.level = 2;

n1.left = n2;
n1.right = n3;
n1.right.left = n1;
n1.right.right = n1;

String graph = new LJV()
    .addFieldAttribute("left", "color=red,fontcolor=red")
    .addFieldAttribute("right", "color=blue,fontcolor=blue")
    .addClassAttribute(Node.class, "color=pink,style=filled")
    .addIgnoreField("level")
    .addIgnoreField("ok")
    .setTreatAsPrimitive(String.class)
    .setShowFieldNamesInLabels(false)
    .drawGraph(n1);
Diagram

Usage

java.util.String before and after Java 9

The most widely used type of data in Java is, of course, String. Starting from Java 9, the internal representation of String has changed: char[] was replaced by byte[], and coder flag was introduced in order to switch between 8-bit and 16-bit character representation. This allowed significant memory optimization for strings that contain only LATIN-1 charset characters:

/*Java 8-: one 16-bit char per character*/
new LJV().drawGraph("abcαβγ");
Diagram
/*Java 9+: coder set to 0 and one byte per LATIN-1 character*/
new LJV().drawGraph("abc");
Diagram
/*Java 9+: coder set to 1
and 2 bytes per character
if there are symbols outside
LATIN-1 set*/
new LJV().drawGraph("abcαβγ");
Diagram

String creation

One rarely needs to create String using new operator, however it’s worth noticing that String(String original) constructor reuses the internal byte array of its argument. Concatenation (even with an empty string!) always produces a full new copy:

String x = "Hello";
new LJV().drawGraph(new String[]{
    x, new String(x),
    new String(x.toCharArray()),
    x + ""});
Diagram

String interning

Calling intern() deduplicates all the String objects and reduce them to a single value kept in the String pool (compare with the previous example):

String x = "Hello";
new LJV().drawGraph(new String[]{
  x, new String(x).intern(),
  new String(x.toCharArray()).intern(),
  (x + "").intern()}));
Diagram

Boxed primitives caching

Usually we create boxed primitives via autoboxing. In rare cases when we do need to create e. g. Integer object explicitly, the correct way to do this is with Integer.valueOf method. This method deduplicates values in the range from -128 to 127 or -XX:AutoBoxCacheMax value.

Values outside this range will not be deduplicated even when autoboxing is used.

Integer created with constructor will never be deduplicated, and this constructor is deprecated since Java 9.

new LJV().drawGraph(new Integer[]{
    42, Integer.valueOf(42),
    new Integer(42),
    -4242, -4242
});
Diagram

LinkedList

Linked list is a data structure with theoretical O(1) efficiency for adding/removing its random node that can acts both as List and Deque. In Java practice, however, LinkedList is superceded by ArrayList and ArrayDeque in all the cases, and it’s questionable whether this class is needed in standard library at all.

List<Integer> list = new LinkedList<>();
list.add(1); list.add(42); list.add(21);

new LJV()
  .setTreatAsPrimitive(Integer.class)
  .setDirection(Direction.LR)
  .drawGraph(list);
Diagram

ArrayDeque

If not LinkedList, then what? Java has a number of high-performant array-based data structures. ArrayList is well-known, but there are also ArrayDeque based on looped array and PriorityQueue based on balanced binary heap, which is actually also an array.

Let’s see, for example, how looped buffer of ArrayDeque works.

This structure implements queue capabilities. If maximum number of elements in the queue does not grow over time, this data structure works very fast and memory efficient, with constant time for every operation.

LJV ljv = new LJV().setTreatAsPrimitive(Integer.class);

//note that this sets initial capacity to 5
Deque<Integer> arrayDeque = new ArrayDeque<>(4);
arrayDeque.add(1); arrayDeque.add(2); arrayDeque.add(3);

ljv.drawGraph(arrayDeque)
Diagram
arrayDeque.poll(); //returns 1
arrayDeque.poll(); //returns 2

ljv.drawGraph(arrayDeque);
Diagram

Here we reach the end of the buffer and start writing from the beginning:

arrayDeque.add(4); arrayDeque.add(5); arrayDeque.add(6);

ljv.drawGraph(arrayDeque);
Diagram

HashMap

HashMap is a widely used data structure in Java. For many people, implementation details of HashMap is also a favorite topic of discussion in a Java programmer job interview.

There are a number of ways to implement hash collisions resolution in a hash map, developers of Java platform chose linked lists:

Map<String, Integer> map = new HashMap<>();
map.put("one", 1);   map.put("two", 2);
map.put("three", 3); map.put("four", 4);

new LJV()
    .setTreatAsPrimitive(Integer.class)
    .setTreatAsPrimitive(String.class)
    .drawGraph(map);
Diagram

Collision

While the number of collisions on a single HashMap bucket is small, the linked list keeps growing:

List<String> collisionString = new HashCodeCollision().genCollisionString(3);
Map<String, Integer> map = new HashMap<>();

for (int i = 0; i < collisionString.size(); i++) {
    map.put(collisionString.get(i), i);
}

new LJV()
    .setDirection(Direction.LR)
    .setTreatAsPrimitive(Integer.class)
    .setTreatAsPrimitive(String.class)
    .setIgnoreNullValuedFields(true)
    .drawGraph(map);
Diagram

'Treeified' collision

However, if a single bucket becomes overloaded with collisions, and keys implement Comparable interface, the linked list turns to a tree.

This reduces the search time in a bucket from O(N) to O(log(N)) and mitigates a certain kind of DDoS attacks:

List<String> collisionString = new HashCodeCollision().genCollisionString(6);
Map<String, Integer> map = new HashMap<>();

for (int i = 0; i < collisionString.size(); i++) {
    map.put(collisionString.get(i), i);
}

String graph = new LJV()
    .setTreatAsPrimitive(String.class)
    .setTreatAsPrimitive(Integer.class)
    .setIgnoreNullValuedFields(true)
    .drawGraph(map);
Diagram

LinkedHashMap

One of the features of HashMap is that this data structure completely 'forgets' the order of insertion of its elements. Also, iteration over HashMap is not very effective from performance point of view. When insertion order matters, we can use LinkedHashMap, which is actually a HashMap combined with linked list. One of the possible use cases for LinkedHashMap is LRU cache implementation.

Map<String, Integer> map = new HashMap<>();
map.put("one", 1);   map.put("two", 2);
map.put("three", 3); map.put("four", 4);

new LJV().setDirection(LR)
    .setTreatAsPrimitive(Integer.class)
    .setTreatAsPrimitive(String.class)
    .drawGraph(map);
Diagram

TreeMap

TreeMap in Java is a Red-Black tree that implements NavigableMap. This implementation provides guaranteed O(log(N)) time cost for get/put/remove operations, which in practice is inferior to HashMap.

We use TreeMap when we need lowerKey(..), higherKey(..) and other NavigableMap capabilities not provided by a simple Map.

Map<String, Integer> map = new TreeMap<>();
map.put("one", 1);         map.put("two", 2);
map.put("three", 3);       map.put("four", 4);
new LJV().setDirection(LR)
    .setTreatAsPrimitive(Integer.class)
    .setTreatAsPrimitive(String.class)
    .setIgnoreNullValuedFields(true)
    .drawGraph(map);
Diagram

ConcurrentSkipListMap

ConcurrentSkipListMap is a thread-safe NavigableMap implementation, that uses quite a complex non-blocking algorithm involving random numbers generator. That’s why for a given input its internal representation never looks the same from one run to another:

ConcurrentSkipListMap<String, Integer> map = new ConcurrentSkipListMap<>();

map.put("one", 1);
map.put("two", 2);
map.put("three", 3);
map.put("four", 4);

String actualGraph = new LJV()
        .setTreatAsPrimitive(Integer.class)
        .setTreatAsPrimitive(String.class)
        .drawGraph(map);

First run

Diagram

Second run

Diagram