在Java中漂亮地打印二维数组

14
我正在寻找一个工具,可以将一个String[][]打印成一个易读的表格,并正确控制列长度。
5个回答

25

如果你想要类似于MySQL命令行客户端的输出,你可以使用类似下面这样的东西:

import java.io.PrintStream;

import static java.lang.String.format;
import static java.lang.System.out;

public final class PrettyPrinter {

    private static final char BORDER_KNOT = '+';
    private static final char HORIZONTAL_BORDER = '-';
    private static final char VERTICAL_BORDER = '|';

    private static final String DEFAULT_AS_NULL = "(NULL)";

    private final PrintStream out;
    private final String asNull;

    public PrettyPrinter(PrintStream out) {
        this(out, DEFAULT_AS_NULL);
    }

    public PrettyPrinter(PrintStream out, String asNull) {
        if ( out == null ) {
            throw new IllegalArgumentException("No print stream provided");
        }
        if ( asNull == null ) {
            throw new IllegalArgumentException("No NULL-value placeholder provided");
        }
        this.out = out;
        this.asNull = asNull;
    }

    public void print(String[][] table) {
        if ( table == null ) {
            throw new IllegalArgumentException("No tabular data provided");
        }
        if ( table.length == 0 ) {
            return;
        }
        final int[] widths = new int[getMaxColumns(table)];
        adjustColumnWidths(table, widths);
        printPreparedTable(table, widths, getHorizontalBorder(widths));
    }

    private void printPreparedTable(String[][] table, int widths[], String horizontalBorder) {
        final int lineLength = horizontalBorder.length();
        out.println(horizontalBorder);
        for ( final String[] row : table ) {
            if ( row != null ) {
                out.println(getRow(row, widths, lineLength));
                out.println(horizontalBorder);
            }
        }
    }

    private String getRow(String[] row, int[] widths, int lineLength) {
        final StringBuilder builder = new StringBuilder(lineLength).append(VERTICAL_BORDER);
        final int maxWidths = widths.length;
        for ( int i = 0; i < maxWidths; i++ ) {
            builder.append(padRight(getCellValue(safeGet(row, i, null)), widths[i])).append(VERTICAL_BORDER);
        }
        return builder.toString();
    }

    private String getHorizontalBorder(int[] widths) {
        final StringBuilder builder = new StringBuilder(256);
        builder.append(BORDER_KNOT);
        for ( final int w : widths ) {
            for ( int i = 0; i < w; i++ ) {
                builder.append(HORIZONTAL_BORDER);
            }
            builder.append(BORDER_KNOT);
        }
        return builder.toString();
    }

    private int getMaxColumns(String[][] rows) {
        int max = 0;
        for ( final String[] row : rows ) {
            if ( row != null && row.length > max ) {
                max = row.length;
            }
        }
        return max;
    }

    private void adjustColumnWidths(String[][] rows, int[] widths) {
        for ( final String[] row : rows ) {
            if ( row != null ) {
                for ( int c = 0; c < widths.length; c++ ) {
                    final String cv = getCellValue(safeGet(row, c, asNull));
                    final int l = cv.length();
                    if ( widths[c] < l ) {
                        widths[c] = l;
                    }
                }
            }
        }
    }

    private static String padRight(String s, int n) {
        return format("%1$-" + n + "s", s);
    }

    private static String safeGet(String[] array, int index, String defaultValue) {
        return index < array.length ? array[index] : defaultValue;
    }

    private String getCellValue(Object value) {
        return value == null ? asNull : value.toString();
    }

}

并且像这样使用:

final PrettyPrinter printer = new PrettyPrinter(out);
printer.print(new String[][] {
        new String[] {"FIRST NAME", "LAST NAME", "DATE OF BIRTH", "NOTES"},
        new String[] {"Joe", "Smith", "November 2, 1972"},
        null,
        new String[] {"John", "Doe", "April 29, 1970", "Big Brother"},
        new String[] {"Jack", null, null, "(yes, no last name)"},
});

上面的代码将产生以下输出:

+----------+---------+----------------+-------------------+
|FIRST NAME|LAST NAME|DATE OF BIRTH   |NOTES              |
+----------+---------+----------------+-------------------+
|Joe       |Smith    |November 2, 1972|(NULL)             |
+----------+---------+----------------+-------------------+
|John      |Doe      |April 29, 1970  |Big Brother        |
+----------+---------+----------------+-------------------+
|Jack      |(NULL)   |(NULL)          |(yes, no last name)|
+----------+---------+----------------+-------------------+

1
@user230137 是的,但这就是MySQL输出的工作原理。如果您在本机MySQL命令行客户端中进行SELECT查询,结果是带有换行符的行,则输出也会被打破,因为输出字符没有以任何方式处理。我所知道的唯一区别是,我的实现更紧凑一些,因为它没有像MySQL命令行输出那样有1个空格水平填充。 - Lyubomyr Shaydariv

9
你可以尝试。
System.out.println(Arrays.deepToString(someRectangularStringArray));

如果没有特定的代码,这就是最好的效果了。


2
@DD. 我知道这不是最好的方法,但由于原始问题缺乏细节,我想提出这个(据我所知)可能是唯一的无代码解决方案,也许还可以应付得了。 - fvu
这是个好的解决方案,但为了让它更美观,我必须像这里建议的那样对结果使用进一步的 replace - Steve Chambers
这既不是表格,也不是真正的人类可读的。如果你的二维数组中有许多浮点数,甚至很难弄清楚列是什么。 - Felix Crazzolara

9

我不知道是否有一个实用程序库可以做到这一点,但是您可以使用String.format函数来格式化字符串以按照您想要显示的方式显示它们。这是我快速编写的示例:

String[][] table = {{"Joe", "Bloggs", "18"},
        {"Steve", "Jobs", "20"},
        {"George", "Cloggs", "21"}};

    for(int i=0; i<3; i++){
        for(int j=0; j<3; j++){
            System.out.print(String.format("%20s", table[i][j]));
        }
        System.out.println("");
    }

这将产生以下输出:
            Joe               Bloggs                  18
          Steve               Jobs                    20
         George               Cloggs                  21

2

当我在寻找一台现成的打印机(不仅仅是为了打印字符串)时,我改变了Lyubomyr Shaydariv被接受的答案编写的代码。由于这可能增加了问题的价值,所以我分享一下:

import static java.lang.String.format;

public final class PrettyPrinter {

    private static final char BORDER_KNOT = '+';
    private static final char HORIZONTAL_BORDER = '-';
    private static final char VERTICAL_BORDER = '|';

    private static final Printer<Object> DEFAULT = new Printer<Object>() {
        @Override
        public String print(Object obj) {
            return obj.toString();
        }
    };

    private static final String DEFAULT_AS_NULL = "(NULL)";

    public static String print(Object[][] table) {
        return print(table, DEFAULT);
    }

    public static <T> String print(T[][] table, Printer<T> printer) {
        if ( table == null ) {
            throw new IllegalArgumentException("No tabular data provided");
        }
        if ( table.length == 0 ) {
            return "";
        }
        if( printer == null ) {
        throw new IllegalArgumentException("No instance of Printer provided");
        }
        final int[] widths = new int[getMaxColumns(table)];
        adjustColumnWidths(table, widths, printer);
        return printPreparedTable(table, widths, getHorizontalBorder(widths), printer);
    }

    private static <T> String printPreparedTable(T[][] table, int widths[], String horizontalBorder, Printer<T> printer) {
        final int lineLength = horizontalBorder.length();
        StringBuilder sb = new StringBuilder();
        sb.append(horizontalBorder);
        sb.append('\n');
        for ( final T[] row : table ) {
            if ( row != null ) {
                sb.append(getRow(row, widths, lineLength, printer));
                sb.append('\n');
                sb.append(horizontalBorder);
                sb.append('\n');
            }
        }
        return sb.toString();
    }

    private static <T> String getRow(T[] row, int[] widths, int lineLength, Printer<T> printer) {
        final StringBuilder builder = new StringBuilder(lineLength).append(VERTICAL_BORDER);
        final int maxWidths = widths.length;
        for ( int i = 0; i < maxWidths; i++ ) {
            builder.append(padRight(getCellValue(safeGet(row, i, printer), printer), widths[i])).append(VERTICAL_BORDER);
        }
        return builder.toString();
    }

    private static String getHorizontalBorder(int[] widths) {
        final StringBuilder builder = new StringBuilder(256);
        builder.append(BORDER_KNOT);
        for ( final int w : widths ) {
            for ( int i = 0; i < w; i++ ) {
                builder.append(HORIZONTAL_BORDER);
            }
            builder.append(BORDER_KNOT);
        }
        return builder.toString();
    }

    private static int getMaxColumns(Object[][] rows) {
        int max = 0;
        for ( final Object[] row : rows ) {
            if ( row != null && row.length > max ) {
                max = row.length;
            }
        }
        return max;
    }

    private static <T> void adjustColumnWidths(T[][] rows, int[] widths, Printer<T> printer) {
        for ( final T[] row : rows ) {
            if ( row != null ) {
                for ( int c = 0; c < widths.length; c++ ) {
                    final String cv = getCellValue(safeGet(row, c, printer), printer);
                    final int l = cv.length();
                    if ( widths[c] < l ) {
                        widths[c] = l;
                    }
                }
            }
        }
    }

    private static <T> String padRight(String s, int n) {
        return format("%1$-" + n + "s", s);
    }

    private static <T> T safeGet(T[] array, int index, Printer<T> printer) {
        return index < array.length ? array[index] : null;
    }

    private static <T> String getCellValue(T value, Printer<T> printer) {
        return value == null ? DEFAULT_AS_NULL : printer.print(value);
    }

}

以及 Printer.java:

public interface Printer<T> {
    String print(T obj);
}

使用方法:

    System.out.println(PrettyPrinter.print(some2dArray, new Printer<Integer>() {
        @Override
        public String print(Integer obj) {
            return obj.toString();
        }
    }));

为什么要改用 T?因为 String 不够用,但我并不总是想要与 obj.toString() 相同的输出,所以我使用了接口来随意更改。第二个变化是不提供类的 outstream,如果你想要像 (Log4j // Logback) 这样的 Logger 使用该类,使用流就肯定会出问题。为什么要改为只支持静态调用?我想将其用于记录器,只进行一次调用似乎是显而易见的选择。

嗨。感谢您指出T——我只考虑了String。关于返回String的静态方法:我宁愿提供一个编写器回调注入或模仿PrintStream以写入日志记录器,因为通常不需要获取此类打印的结果,但是有必要将其写入某个地方,而无需担心委托结果给可以编写的适当对象。 - Lyubomyr Shaydariv
请注意,String 是一种通用的表示类型。而 T 则只涵盖一个字段的单个类型,而不是记录。因为您可能想要处理一个对象列表,其中每个字段都可以转换为 String。如果您想要漂亮地打印由 List<Person>Person[] 组成的行——那么如何在 T 中将它们分别转换为 firstNameString)、lastNameString)和 dateOfBirthDate)字段以进行打印呢?您可能会提出一个返回 String[] 或其他内容的打印机接口,但结果总是变成 String[][] - Lyubomyr Shaydariv
我不同意你关于PrintStream的第一个评论,因为它与slf4j记录器不兼容,但这可能是一场我不想参与的圣战(就像所提议的静态方法一样)。回答你的问题,对于你放入PrettyPrinter的每个对象,都会调用Printer接口,在那里你可以定义从T到String的任何转换,这比原始方法更加灵活。如果每个字段都保存了Person列表,则可以迭代该列表。如果你没有指定打印机,则会调用T.toString()。 - Sebastian van Wickern

1
JakWharton提供了一个很好的解决方案https://github.com/JakeWharton/flip-tables,使用String[]和对象列表,通过属性名称和结果集进行反射。例如:
List<Person> people = Arrays.asList(new Person("Foo", "Bar"), new Person("Kit", "Kat"));
System.out.println(FlipTableConverters.fromIterable(people, Person.class));

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接