sormuras.github.io

Java Records - Records.toTextBlock

Let’s enhance the upcoming Java programming language enhancement record with a toTextBlock method. This is the second part of the mini series about records.

Proposed Methods

Records.toTextBlock(Record)

For record types, a public String toString() method implementation is generated for us. It returns a “string representation of all the record components, with their names”. That’s nice. All in one line. That’s not so nice. Especially when your record contains many components. And components are sometimes records, as well. Thus, their components contribute to the same line.

The proposed Records.toTextBlock(Record) method also produces a string representation. But with new line separators and tab characters inserted to achieve a tree-like view of all record components.

Pseudo-Code Usage Sample

Given the record declartion R:

record R(T0 n0, T1 n1, ... Tn nn) {}

The generated toString() method produces:

"R[n0=v0, n1=v1, ... nn=vn]"

The string representation produced by toTextBlock(Record) looks like:

""" 
R
\tn0 = v0
\tn1 = v1
...
\tnn = vn
"""

Values of type record are printed as indented text blocks. Let’s assume that type T2 is declared as a record.

While Record.toString() represents that within the same line:

"R[n0=v0, n1=v1, n2=T2[nA=w0, nB=w1, ..., nz=wz] ... nn=vn]"
"""
R
\tn0 = v0
\tn1 = v1
\tn2 -> T2
\t\t\tnA = w0
\t\t\tnB = w1
...
\tnn = vn
"""

Proof Of Concept Implementation

class Records {
  /** Returns a multi-line string representation of the given object. */
  public static String toTextBlock(Record record) {
    return toTextBlock(0, record, "\t");
  }

  /** Returns a multi-line string representation of the given object. */
  private static String toTextBlock(int level, Record record, String indent) {
    var lines = new ArrayList<String>();
    if (level == 0) lines.add(record.getClass().getSimpleName());

    var components = record.getClass().getRecordComponents();
    Arrays.sort(components, Comparator.comparing(RecordComponent::getName));

    for (var component : components) {
      var name = component.getName();
      var shift = indent.repeat(level);
      try {
        var value = component.getAccessor().invoke(record);
        var nested = value.getClass();
        if (nested.isRecord()) {
          lines.add(String.format("%s%s%s -> %s", shift, indent, name, nested.getSimpleName()));
          lines.add(toTextBlock(level + 2, (Record) value, indent));
          continue;
        }
        lines.add(String.format("%s%s%s = %s", shift, indent, name, value));
      } catch (ReflectiveOperationException e) {
        lines.add("// Reflection over " + component + " failed: " + e);
      }
    }
    return String.join(System.lineSeparator(), lines);
  }
}

Java 9+ variant w/o --enable-preview

class Records {

  /**
   * An informative annotation type used to indicate that a class type declaration is intended to be
   * transmuted into a {@code record} as defined by JEP 359, soon.
   */
  @Target(ElementType.TYPE)
  @Retention(RetentionPolicy.RUNTIME)
  public @interface Record {}

  /** Returns a multi-line string representation of the given object. */
  public static String toTextBlock(Object object) {
    return toTextBlock(0, object, "\t");
  }

  private static String toTextBlock(int level, Object object, String indent) {

    var lines = new ArrayList<String>();
    if (level == 0) lines.add(object.getClass().getSimpleName());

    var fields = object.getClass().getDeclaredFields();
    Arrays.sort(fields, Comparator.comparing(Field::getName));

    for (var field : fields) {
      // if not a "private final field" continue
      var name = field.getName();
      var method = object.getClass().getDeclaredMethod(name);
      // if not a "matching component accessor" continue
      try {
        var shift = indent.repeat(level);
        var value = method.invoke(object);
        var nested = value.getClass();
        if (nested.isAnnotationPresent(Record.class)) {
          lines.add(String.format("%s%s%s -> %s", shift, indent, name, nested.getSimpleName()));
          lines.add(toTextBlock(level + 2, value, indent));
          continue;
        }
        lines.add(String.format("%s%s%s = %s", shift, indent, name, value));
      } catch (ReflectiveOperationException e) {
        lines.add("// Reflection over " + method + " failed: " + e);
      }
    }
    return String.join(System.lineSeparator(), lines);
  }
}