When defining a class, write formatTo as well as toString (how to use Formattable)

In 3 lines

--It is convenient to define toString () and formatTo () when creating a class. --toString has a developer-friendly format for debugging --For formatTo, use an easy-to-understand format from the user's perspective.

Introduction

At JJUG CCC 2018 fall, Edson Yanaga appeared in a session called "Revisiting Effective Java in 2018". I tried to organize the story about formatTo that was impressive to me.

The session was very interesting because it was like talking about the know-how of Effective Java 3rd Edition + α while live coding. Among them, regarding formatTo, I was surprised that "Is there such a useful thing in the java.lang package? ", But even if I searched with Qiita, there were almost no articles, so I decided to write it myself.

toString method

First of all, the familiar toString method.

Overview

A method that returns a string representation of an object. It is called in the following situations.

--When you combine a string and an object --When passed to a method that outputs as a character string, such as System.out.println () --When displaying variables in the debugger

It is defined in the Object class as class name +" @ "+ a hexadecimal representation of the hash value, but it's not very manageable and it is recommended to override it in a subclass.

reference Object#toString

Example

Override toString


class Person {
  private String name;
  private int age;

  public Person(String name, int age) {
    this.name = name;
    this.age = age;
  }

  @Override
  public String toString() {
    return name + ", " + age + " years old.";
  }
}

By overriding ʻObject # toString` like this, you can output a formatted character string just by passing the variable as it is when outputting as standard.

Example of using toString


Person marty = new Person("Marty", 17);
Person doc = new Person("Doc", 65);

System.out.println(marty); // Marty, 17 years old.
System.out.println(doc); // Doc, 65 years old.

Benefits of using toString

--No need to create getters (no need to expose fields to the outside world) --You can hide the process of converting an object into a string.

In particular, I think it is very "object-oriented" to know how the object itself should be stringified, regardless of how it holds the specific data.

Trouble with toString

--Stringized in the same format everywhere --Private variables that are not used as user output but are convenient to see during debugging cannot be output.

In the above example, the debugger and log output will all be output in the form Marty, 17 years old..

I want to use toString for user output, but I want more detailed information for debugging. That's where the formatTo method comes in.

formatTo

About the format string before formatTo

Format string

It is a mechanism that can do something similar to printf or sprintf in C language.

System.out.printf("%s, %d years old.%n", "Marty", 17); // --> Marty, 17 years old.
String str = String.format("%s, %d years old.", "Marty", 17); // --> "Marty, 17 years old."Is generated

How to specify the format is quite complicated, so please see Javadoc of Formatter class. However, all you need to know for the time being is that the format is specified by the following parameters.

Formatter format specification method


 %[argument_index$][flags][width][.precision]conversion

The meaning of each is as follows.

//
//conversion (argument conversion method. Integer value is%d, the real number is%Specify the conversion according to the type such as f)
//
String.format("%d", 12); // ---> "12"
System.out.printf("%f", Math.PI); // ---> 3.141593

//
//width (minimum number of characters, missing space is filled with spaces)
//
String.format("%5d", 12); // ---> "   12"

//
// flag (-, +, #Flags for literally formatting option control, etc.)
//
//Add a minus to align left
String.format("%-5d", 12); // ---> "12   "
//Add 0 to fill with zeros instead of spaces
String.format("%05d", 12); // ---> "00012"
//At the beginning+Display the sign with
System.out.printf("%+5d", 12); // ---> "+12"
System.out.printf("%+5d", -12); // ---> "-12"

//
//precision (basically used to mean the maximum number of characters)
//
//In the case of a real number, the number of digits after the decimal point (in this case, it seems to be treated as "the maximum number of characters after the decimal point")
System.out.printf("%4.2f", Math.PI); // ---> 3.14

For example, % -4.2f has the following values.

conversion flag width precision
f - 4 2

Processing when an object is passed as an argument

Person doc = new Person("Doc", 65);
String.format("%30s", doc);
String.format("%30s", doc.toString()); //The output will be the same as this

In this way, if you pass an object as an argument, the toString method of that class will be called and the result will be treated as a string.

However, if the class passed as an argument implements the Formattable interface, it is possible to control the output method, not just the result of toString.

Formattable interface

An interface that defines the following methods.

Formattable interface


void formatTo(Formatter formatter,
            int flags,
            int width,
            int precision)

If you pass an instance of a class that implements this interface to a printf method, the formatTo method will be called to format the corresponding part.

Basic usage

class Person implements Formattable {
  private String name;
  private int age;

  public Person(String name, int age) {
    this.name = name;
    this.age = age;
  }

  @Override
  public String toString() {
    return "{name=" + name + ",age=" + age + "}";
  }

  @Override
  public void formatTo(Formatter formatter, int flags, int width, int precision) {
    //Formatter is performed by manipulating formatter in the formatTo method.
    //How to use String#Same as format
    formatter.format("%s, %d years old.", name, age);
  }
}

Usage example of class that implements formatTo


Person marty = new Person("Marty", 17);
Person doc = new Person("Doc", 65);

System.out.printf("%s%n", marty); // Marty, 17 years old.
System.out.printf("%s%n", doc); // Doc, 65 years old.

System.out.println(marty); // {name=Marty,age=17}
System.out.println(doc); // {name=Doc,age=65}

In this way, by leaving the output for the user to the formatTo method, the output for debugging can be freely handled so that it is easy to use from the developer's point of view.

Multilingual

Even more convenient, the formatTo method can handle locale information. For example, multilingual support is possible as follows.

@Override
public void formatTo(Formatter formatter, int flags, int width, int precision) {
  if (formatter.locale() == Locale.JAPAN) {
    formatter.format("%s is%I'm d years old.", name, age);
  } else {
    formatter.format("%s, %d years old.", name, age);
  }
}

By default, it is the system locale, but you can switch the display by passing the locale as an argument of printf.

System.out.printf("%s%n", doc); //Doc is 65 years old.
System.out.printf(Locale.ENGLISH, "%s%n", doc); // Doc, 65 years old.

Application example ... 1

Since the display width is passed to the argument of formatTo, it is possible to switch automatically depending on the output width.

Example of automatically switching the era notation


private enum Gengou implements Formattable {
  MEIJI("Meiji", 'M'), TAISHO("Taisho", 'T'), SHOWA("Showa", 'S'), HEISEI("Heisei", 'H');

  private String name;
  private char initialLetter;

  private Gengou(String japaneseName, char initial) {
    this.name = japaneseName;
    this.initialLetter = initial;
  }

  /**
   *If the display width is 2 characters or more, the official name is output, and if it is 1 character, the alphabet is output as 1 character.
   */
  @Override
  public void formatTo(Formatter formatter, int flags, int width, int precision) {
    if (width >= 2) {
      formatter.format("%s", name);
    } else {
      formatter.format("%s", initialLetter);
    }
  }
}
System.out.printf("This year%3s 30 years.%n", Gengou.HEISEI); // This year平成 30 年です。
System.out.printf("This year%1s 30 years.%n", Gengou.HEISEI); // This yearH 30 年です。

Application example ... 2

Since the argument of formatTo can be handled freely, it is possible to control the output of the class representing the paragraph (Paragraph) by using precision as the number of ellipsis characters and width as the total number of characters.

private static class Paragraph implements Formattable {
  private String content;

  public Paragraph(String content) {
    this.content = content;
  }

  @Override
  public void formatTo(Formatter formatter, int flags, int width, int precision) {
    StringBuilder sb = new StringBuilder();

    if (content.length() < width) {
      sb.append(content);
    } else {
      sb.append(content.substring(0, width - precision));

      if (precision > 0) {
        for (int i = 0; i < precision; i++) {
          sb.append(".");
        }
      }
    }

    formatter.format(sb.toString());
  }
}
Paragraph p = new Paragraph("This is very very long text!");
System.out.printf("%15.3s%n", p); // This is very...
System.out.printf("%10.4s%n", p); // This i....

Where to use

As I wrote the article, I noticed that if the output is text like the Person class mentioned in the example, I think it is simpler to create a separate class for message molding.

On the contrary, in the case of a value object-like class that has a meaning as a value by itself, such as "amount" and "user ID", I feel that it is always good to implement Formattable. ..

Recommended Posts

When defining a class, write formatTo as well as toString (how to use Formattable)
[Rails] How to write when making a subquery
How to use java class
How to use the wrapper class
How to write a ternary operator
How to use class methods [Java]
[Java] How to use Math class
A memorandum on how to use Eclipse
[Java] How to use the File class
[Basic] How to write a Dockerfile Self-learning ②
[Java] How to use the HashMap class
[Introduction to Java] How to write a Java program
How to display error messages and success messages when registering as a user
[Processing × Java] How to use the class
How to use Java Scanner class (Note)
[Java] How to use the Calendar class
[SpringBoot] How to write a controller test
Rails: How to write a rake task nicely
How to convert a solidity contract to a Java contract class
[Java] How to use Calendar class and Date class
[Java] How to use compareTo method of Date class
I want to use Combine in UIKit as well.
How to run the SpringBoot app as a service
How to use an array for a TreeMap key
How to write a unit test for Spring Boot 2
java: How to write a generic type list [Note]
How to write a date comparison search in Rails
How to write a core mod in Minecraft Forge 1.15.2
A memo to check when you try to use Lombok
How to use and apply Java's JFrame / Canvas class
How to use Map
How to write Rails
How to use rbenv
How to use letter_opener_web
How to use with_option
How to use fields_for
How to use map
How to use collection_select
How to use Twitter4J
How to use active_hash! !!
How to use MapStruct
How to use hidden_field_tag
How to use TreeSet
How to write dockerfile
[How to use label]
How to write docker-compose
How to use identity
How to use hashes
How to write Mockito
How to use JUnit 5
How to write migrationfile
How to use Dozer.mapper
How to use Gradle
How to use org.immutables
How to use java.util.stream.Collector
How to use VisualVM
How to use Map
How to request a CSV file as JSON with jMeter
[1st] RSpec beginners tried to write ModelSpec as a beginner
How to use a foreign key with FactoryBot ~ Another solution
How to use ArgumentMatchers such as Mockito's any () in Kotlin