Align Text in Columns in Java: Print a Table with printf

Align text in columns in Java with printf widths, widths computed from the data, cut long values and a small TextTable class that prints Markdown tables.

Three playlist columns where each width is the longest value or header, turned into the format string with the widths %-10s, %-13s and %5s

To align text in columns in Java, we give every column a fixed width in a format string, such as “%-12s%-15s%6d”, and print each row of the table with System.out.printf() or String.format(). Text columns get the – flag for left alignment, and number columns keep the default right alignment, so the digits line up.

We print data in tabular format in command-line tools, test reports, log summaries and plain-text emails, where a reader scans rows and compares values.

The following example prints a small playlist as a table with three columns.

String rowFormat = "%-12s%-15s%6s";
String header = rowFormat.formatted("Title", "Artist", "Plays");     // "Title       Artist          Plays"
String row = rowFormat.formatted("Yesterday", "Beatles", 1520);      // "Yesterday   Beatles          1520"
String line = "-".repeat(33);                                        // 12 + 15 + 6 dashes

Notice that one format string serves the header and every row, so the columns stay in place. The sections that follow compute the widths from the data, cut values that are too long, and build a reusable TextTable class without any library.

1. Printing Columns with printf() and Fixed Widths

A format string holds one specifier per column, and the width of each specifier is the width of that column. The specifier %-12s left aligns a value in 12 characters, while %6d right aligns an integer in 6 characters. Because the widths add up to the line length, the separator line is “-“.repeat(33) for 12 + 15 + 6.

Three playlist columns where each width is the longest value or header, turned into the format string with the widths %-10s, %-13s and %5s
Each column is as wide as its longest value or header, and those widths go into the format string

With printf(), we end the pattern with %n, which prints the line separator of the operating system. The \n escape always prints a line feed, which matters only for files that Windows tools read.

String rowFormat = "%-12s%-15s%6s%n";
System.out.printf(rowFormat, "Title", "Artist", "Plays");
Title       Artist          Plays
---------------------------------
Yesterday   Beatles          1520
Hallelujah  Leonard Cohen     980
Hey         Pixies             75

The left, right or center align tutorial explains each flag of a single field. For a table, two rules matter most. Text columns use %-Ns, and numbers or amounts use %Nd or %N.2f, so the last digits sit under each other.

2. Computing Column Widths from the Data

Fixed widths work for data we know in advance. A command-line export in a music app prints song titles from 3 to 40 characters long, so a fixed width of 12 either wastes space or breaks the layout at the first long title. We compute each width as the length of the longest value in the column, or of its header if that is longer.

List<String> titles = List.of("Yesterday", "Hallelujah", "Hey");
int titleWidth = titles.stream().mapToInt(String::length).max().orElse(0);   // 10
String rowFormat = "%-" + (titleWidth + 2) + "s%6d";                         // "%-12s%6d"
String first = rowFormat.formatted("Yesterday", 1520);                       // "Yesterday     1520"

The + 2 adds a gap between this column and the next one. The orElse(0) call handles an empty list, so an empty playlist prints only the header.

2.1. Cutting Values That Are Too Long

A width in %-12s is a minimum, not a maximum, so one long value shifts every column to its right. When a column must keep its width, for example in a fixed-width file, we cut the value and mark the cut with three dots.

static String shorten(String value, int max) {
    return value.length() <= max ? value : value.substring(0, max - 3) + "...";
}
String shifted = String.format("%-12s|", "Bohemian Rhapsody");           // "Bohemian Rhapsody|"
String kept = String.format("%-12s|", shorten("Bohemian Rhapsody", 12));  // "Bohemian ...|"
String cutOnly = String.format("%-12.12s|", "Bohemian Rhapsody");         // "Bohemian Rha|"

The precision .12 in the last line cuts the value without the dots. Either way, the reader loses part of the text, so cutting fits fixed-width files better than console reports, where computed widths keep every value.

3. Align Text in Columns using Custom TextTable

Copying the width calculation into every report is repetitive, and every copy is one more place for a bug. The TextTable class collects the rows first, computes all widths in build() and prints each column with its own alignment. It uses only the JDK, with a StringJoiner for the separators between cells.

3.1. Using TextTable

We create a table with a list of Column records, each with a header and an alignment, and add one row per song. The add() method accepts any objects and converts them with String.valueOf(), so numbers and null values need no conversion.

TextTable table = new TextTable(List.of(
        new Column("Title", Alignment.LEFT),
        new Column("Artist", Alignment.LEFT),
        new Column("Plays", Alignment.RIGHT),
        new Column("Rating", Alignment.CENTER)));
table.add("Yesterday", "Beatles", 1520, "4.8");
table.add("Hallelujah", "Leonard Cohen", 980, "4.9");
table.add("Hey", "Pixies", 75, "4.1");
String printed = table.build();                                    // 5 lines
int lineCount = (int) printed.lines().count();                     // 5
| Title      | Artist        | Plays | Rating |
|:-----------|:--------------|------:|:------:|
| Yesterday  | Beatles       |  1520 |  4.8   |
| Hallelujah | Leonard Cohen |   980 |  4.9   |
| Hey        | Pixies        |    75 |  4.1   |

The second line marks the alignment of each column with colons. That is the Markdown table syntax, so the output reads well in a console and also renders as a real table when we paste it into a README or a pull request comment.

3.2. TextTable Class

The class is short because each method has one job. The fit() method pads one cell, build() computes the widths, line() joins the cells of one row, and separator() writes the Markdown alignment row.

enum Alignment { LEFT, CENTER, RIGHT }
record Column(String header, Alignment alignment) {}
static String fit(String value, int width, Alignment alignment) {
    int gap = Math.max(0, width - value.length());
    return switch (alignment) {
        case LEFT -> value + " ".repeat(gap);
        case RIGHT -> " ".repeat(gap) + value;
        case CENTER -> " ".repeat(gap / 2) + value + " ".repeat(gap - gap / 2);
    };
}
final class TextTable {
    private final List<Column> columns;
    private final List<List<String>> rows = new ArrayList<>();

    TextTable(List<Column> columns) {
        this.columns = List.copyOf(columns);
    }

    void add(Object... values) {
        if (values.length != columns.size()) {
            throw new IllegalArgumentException(
                    "Expected " + columns.size() + " values, got " + values.length);
        }
        rows.add(Arrays.stream(values).map(String::valueOf).toList());
    }

    String build() {
        int[] widths = new int[columns.size()];
        for (int i = 0; i < widths.length; i++) {
            widths[i] = columns.get(i).header().length();
            for (List<String> row : rows) {
                widths[i] = Math.max(widths[i], row.get(i).length());
            }
        }
        StringJoiner table = new StringJoiner("\n");
        table.add(line(columns.stream().map(Column::header).toList(), widths));
        table.add(separator(widths));
        rows.forEach(row -> table.add(line(row, widths)));
        return table.toString();
    }

    private String line(List<String> values, int[] widths) {
        StringJoiner line = new StringJoiner(" | ", "| ", " |");
        for (int i = 0; i < widths.length; i++) {
            line.add(fit(values.get(i), widths[i], columns.get(i).alignment()));
        }
        return line.toString();
    }

    private String separator(int[] widths) {
        StringJoiner line = new StringJoiner("|", "|", "|");
        for (int i = 0; i < widths.length; i++) {
            Alignment a = columns.get(i).alignment();
            String left = a == Alignment.RIGHT ? "-" : ":";
            String right = a == Alignment.LEFT ? "-" : ":";
            line.add(left + "-".repeat(widths[i]) + right);
        }
        return line.toString();
    }
}

A row with the wrong number of values fails early with IllegalArgumentException, instead of printing a broken table later. The List.copyOf() call stores an unmodifiable copy of the columns, so a caller cannot change them after the widths are computed.

TextTable songs = new TextTable(List.of(
        new Column("Title", Alignment.LEFT),
        new Column("Plays", Alignment.RIGHT)));
songs.add("Yesterday");                 // IllegalArgumentException: Expected 2 values, got 1

4. Writing the Table to a File or a Log

Since build() returns a String, the same table goes to any output. A nightly job can attach it to an email, a test can compare it with an expected text block, and a logger can write it in one call so other log lines do not end up between the rows.

TextTable table = new TextTable(List.of(new Column("Title", Alignment.LEFT)));
table.add("Yesterday");
Path report = Files.createTempFile("playlist", ".md");
Path written = Files.writeString(report, table.build());           // the table as a Markdown file
boolean saved = Files.exists(written);                             // true

For tables with borders drawn in box characters, cell wrapping or colors, a library such as picocli’s TextTable saves the work in command-line apps. For a report with a few columns, printf or the class from section 3 is enough.

5. Tabular Output FAQs

The format syntax is rarely the problem in a console table, whereas borders, libraries and line endings often are.

5.1. How do I print a table in the Java console without a library?

We use one printf format string for all rows, with a width per column, as in section 1. The JDK has no table class, but Formatter handles the padding, and computing the widths takes one stream per column.

5.2. How do I print a table with borders?

We put the border characters in the format string and draw the horizontal lines with repeat().

String bordered = String.format("| %-10s | %5d |", "Hey", 75);   // "| Hey        |    75 |"
String border = "+" + "-".repeat(12) + "+" + "-".repeat(7) + "+";   // "+------------+-------+"

5.3. Why do my columns shift when one value is longer?

The width in a specifier is a minimum. A value longer than the width is printed in full and pushes the rest of the row to the right. We either compute the widths from the data or cut long values, as in section 2.1.

5.4. Should I use \n or %n in printf()?

We use %n for console and file output, because it prints the line separator of the platform. The \n escape prints a line feed on every platform, which is the right choice for files that must use Unix line endings, such as shell scripts. Protocols such as HTTP and SMTP need \r\n, which we write out explicitly.

5.5. How do I right align decimal numbers in a column?

We give the column a width and a fixed number of decimals, and pass a locale so the decimal separator does not change between machines.

String price = String.format(Locale.ROOT, "%8.2f|", 3.5);    // "    3.50|"

6. Conclusion

Printing text in columns comes down to one format string with a width per column, the – flag for text and the default right alignment for numbers.

When the data varies, we compute each width from the longest value, and when a column must stay fixed, we cut values with a precision or with dots. The TextTable class wraps these steps in about 50 lines of plain Java and prints a table that is also valid Markdown. The Java String tutorial lists more String formatting tasks.

7. References

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.