Java/Java

Java - Formatter

Dlise 2023. 10. 23. 20:40

깔끔한 코드 작성에 String.format()의 필요를 느껴 이와 관련된 내용을 공부하고자 한다.

 

String Docs

https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/String.html

 

String (Java SE 17 & JDK 17)

All Implemented Interfaces: Serializable, CharSequence, Comparable , Constable, ConstantDesc The String class represents character strings. All string literals in Java programs, such as "abc", are implemented as instances of this class. Strings are constan

docs.oracle.com

 

Oracle의 String docs를 보면 format 관련 메서드는 아래 3가지가 있다.

 

format은 Java 1.5, formatted는 Java 15부터 지원을 한다.

 

위의 메서드는 Formatter 클래스와 연관이 있다는 공통점이 있다.

 

 

Formatter 클래스

https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Formatter.html

 

Formatter (Java SE 17 & JDK 17)

All Implemented Interfaces: Closeable, Flushable, AutoCloseable An interpreter for printf-style format strings. This class provides support for layout justification and alignment, common formats for numeric, string, and date/time data, and locale-specific

docs.oracle.com

 

여러 출력 양식을 지원하는 Formatter 클래스는 c언어의 printf와 유사하지만 좀 더 엄격하다고 docs에서 소개한다.

정렬, 숫자, 문자열, 날짜, 시간 등 다양한 출력 형식을 지원한다.

 

사용법을 알아보자.

 

 

형식 문자열 구문

출력 형식은 format string과 하나 이상의 argument를 가진다.

 

String.format("Sample : %d %d", n, m);를 예로 보면

  • "Sample : %d " - format string
  • n, m - argument

이다.추가로 %d를 format specifiers(형식 지정자)라고 한다.

 

여기서 가장 중요한 것이 형식 지정자이다. 형식 지정자는 타입에 따라 다음과 구문을 가진다.

  • General, Character, Numeric 타입:
    • %[argument_index$][flags][width][.precision]conversion
  • Date / Time 타입:
    • %[argument_index$][flags][width]conversion
  • 그 외
    • %[flags][width]conversion

 

각 구문의 역할은 아래와 같다.

  • (선택) argument_index: 인수 위치를 나타내는 10진수 정수
  • (선택) flags: 출력 형식을 수정하는 문자 집합
  • (선택) width: 출력할 최소 문자 수(10진수 양수)
  • (선택) precision: 제한 문자 수(음수가 아닌 10진수 정수)
  • (필수) conversion: 인수 형식을 지정하는 문자

 

 

Conversion

필수인 conversion을 먼저 살펴보자.

conversion은 아래와 같이 자료형에 따른 구분을 가지며 이 구분을 기반으로 각각에 맞게 동작한다.

  • General(일반)
    모든 인수에 적용 가능

  • Character(문자)
    유니코드 문자를 나타내는 기본 유형에 적용 가능(char, Character, byte, Byte, short, Short 형)
    int 혹은 Integer 형은 Character.isValidCodePoint(int) 결과가 true일 때 가능

  • Numeric(숫자)
    • Integral(정수) - byte, Byte, short, Short, int, Integer, long, Long, BigInteger
    • Floating Point(부동 소수점) - float, Float, double, Double, BigDecimal

  • Date/Time (날짜/시간)
    날짜 혹은 시간에 적용 가능(long, Long, Calender, Date, TemporalAccessor)

  • Percent(백분율) 
    % 결과를 반환

  • Line Separator(줄 구분)
    줄 구분 기호 생성

 

각각에 대응하는 알파벳은 아래와 같다.

일반적으로 대문자 conversion은 결과를 대문자로 출력한다는 점을 제외하면 소문자와 결과가 같다.

이제 위의 Conversion에서 생소한 것들을 다뤄보자.

 

%b, %B:

Boolean.toString(boolean)의 반환대로 true or false를 생성한다.

 

argument가 boolean형일 경우, 해당 값에 따라 결과를 반환하고

argument가 boolean형이 아닐 경우 null이면 false, 그 외엔 true를 반환한다.

public class Main {
    public static void main(String[] args) {
        System.out.println(String.format("%b, %b", "", null)); //true, false
        System.out.println(String.format("%b, %b", true, false)); //true, false
        System.out.println(String.format("%b, %B", "test", "test")); //true, TRUE
    }
}

 

%h, %H:

해시 코드 값을 나타내는 문자열을 출력하는 것이다.

Integer.toHexString(arg.hashCode())의 결과를 반환한다.

public class Main {
    public static void main(String[] args) {
        System.out.println(String.format("%h, %h", "", null)); //0, null
        System.out.println(String.format("%h, %h", true, false)); //4cf, 4d5
        System.out.println(String.format("%h, %H", "test", "test")); //364492, 364492
    }
}

 

%g, %G: 

일반 과학 표기법(?)이라고 한다.

public class Main {
    public static void main(String[] args) {
        System.out.println(String.format("%g", 1.29));
        System.out.println(String.format("%g", 1.29123745345));
    }
}

precision의 기본값이 6이기에 출력 결과는 아래와 같다.

 

%a, %A:

16진수 지수 형식으로 출력한다.

public class Main {
    public static void main(String[] args) {
        System.out.println(String.format("%a", 1.29));
        System.out.println(String.format("%A", 1.29));
    }
}

 

%%:

%를 출력하기 위한 것으로 매칭되는 argument가 없어야 한다. width를 사용할 수 있다.

public class Main {
    public static void main(String[] args) {
        System.out.println(String.format("10%5%"));
    }
}

만약 %%가 아닌 %를 사용하면 UnknownFormatConversionException 오류가 발생한다.

 

%n:

줄 구분 기호이다. flag, width, precision이 모두 없어야 한다. 만약 있다면 각각 IllegalFormatFlagsException, IllegalFormatWidthException, IllegalFormatPrecisionException을 발생시킨다.

 

매칭되는 argument도 없어야 한다.

public class Main {
    public static void main(String[] args) {
        System.out.println(String.format("%d%n%d", 1, 2));
        System.out.println();
        System.out.println(String.format("%d\n%d", 1, 2));
    }
}

결과를 보면 %n과 \n의 동작이 같다.

그럼에도 %n이 있는 이유는 플랫폼이 달라도 항상 올바르게 줄 구분을 하기 때문이다.

 

이에 대한 내용은 아래 페이지에서 확인할 수 있다.

https://stackoverflow.com/questions/1883345/whats-up-with-javas-n-in-printf

 

What's up with Java's "%n" in printf?

I'm reading Effective Java and it uses %n for the newline character everywhere. I have used \n rather successfully for newline in Java programs. Which is the 'correct' one? What's wrong with \n ? ...

stackoverflow.com

 

 

다음으로 아래는 날짜/시간과 관련된 conversion이다.

  • 시간 형식 지정자

 

  • 날짜 형식 지정자

 

  • 날짜/시간 형식 지정자

 

 

Flag

다음으로 Flag에 대해 살펴보자.

 

Flag가 없는 경우(기본값)

  • 출력은 오른쪽 정렬이 된다.
  • 음수는 '-'가 붙어서 나온다.
  • 양수와 0에는 부호, 선행 공백이 없다.
  • 구분 기호가 없다.

 

Flag가 있는 경우

docs에서 Flag는 다음과 같이 설명이 되어있다.

각 Flag마다 y가 적혀있는 타입만 지원하는 것이며, 그중 일부만 지원하는 경우 첨자를 붙여 설명해 놓았다.

 

이를 간단히 다뤄보자.

 

'-' Flag: 결과를 왼쪽으로 정렬하며 모든 타입을 지원한다.

width가 함께 있어야 하며 그렇지 않은 경우 MissingFormatWidthException을 발생시킨다.

public class Main {
    public static void main(String[] args) {
        System.out.println(String.format("%-5d %d", 1, 1));
    }
}

 

'#' Flag: 형식 지정자에 대해 대체 형식으로 형식을 지정한다.

16진법인 x와 함께라면 0x를, 8진법인 o와 함께라면 0을 시작으로 한다.

public class Main {
    public static void main(String[] args) {
        System.out.println(String.format("%#x", 1));
    }
}

 

'+' Flag: 기호를 함께 출력한다. 해당 Flag가 없으면 음수에만 부호가 붙는다.

오류가 발생하면 IllegalFormatFlagsException을 발생시킨다.

public class Main {
    public static void main(String[] args) {
        System.out.println(String.format("%+d, %+d", 1, -1)); //+1, -1
    }
}

 

' ' Flag: 공백을 포함한다. 오류가 발생하면 IllegalFormatFlagsException을 발생시킨다.

public class Main {
    public static void main(String[] args) {
        System.out.println(String.format("% d, % 5d", 1, 1));
    }
}

 

'0' Flag: width에서 빈 공간을 0으로 채운다. width가 없으면 MissingFormatWidthException을 발생시킨다.

public class Main {
    public static void main(String[] args) {
        System.out.println(String.format("%05d", 1));
    }
}

 

',' Flag: ,로 단위를 구분한다.

public class Main {
    public static void main(String[] args) {
        System.out.println(String.format("%,d", 1111111111));
    }
}

 

'(' Flag: 음수일 경우 괄호로 묶는다.

public class Main {
    public static void main(String[] args) {
        System.out.println(String.format("%(d, %(d", 1, -1));
    }
}

 

 

Width

width는 출력할 때의 최소 문자 수로 기호, 숫자, 그룹, 괄호 등도 포함한다. 값의 길이가 width의 길이보다 작으면 공백으로 채우며 왼쪽 패딩이 기본값이다. '-' Flag가 있으면 오른쪽 패딩이 된다.

 

width의 범위는 1 ~ Integer.MAX_VALUE이다. 이를 벗어나면 IllegalFormatWidthException을 발생시킨다.

precision은 적용이 되지 않는다. 만약 적용하려 하면 IllegalFormatPrecisionException을 발생시킨다.

 

 

Precision

부동소수점 등을 출력할 때의 최대 문자 수이다. 반올림을 해 결과를 출력한다.

 

precision의 범위는 0 ~ Integer.MAX_VALUE이다. 이를 벗어나면 IllegalFormatPrecisionException을 발생시킨다.

public class Main {
    public static void main(String[] args) {
        System.out.println(String.format("%.5f", 1.23847514));
    }
}

소수점 5자리까지 반올림되어 출력된 모습이다.

 

 

Argument Index

argument의 위치를 정하는 10진수 정수이다. 설명은 아래와 같다.

아래의 예시 코드를 보면 바로 이해할 수 있을 것이다.

public class Main {
    public static void main(String[] args) {
        System.out.println(String.format("%1$s %3$s %2$s %4$s", "1", "2", "3", "4"));
    }
}

즉, 위치를 정할 수 있다.

 

추가로  <를 활용하는 방법도 있다. 이 방법은 이전의 형식 지정자를 재사용하는 것이다.

아래 코드를 보면 동작을 이해할 수 있을 것이다.

public class Main {
    public static void main(String[] args) {
        System.out.println(String.format("%1$d %<d %<d %d %d", 1, 2));
        System.out.println(String.format("%2$d %<d %<d %d %d", 1, 2));
    }
}

만약 앞서 사용한 인수가 없으면 MissingFormatArgumentException을 발생시킨다.

 

 

ps. 만약 형식 지정자보다 argument가 더 많으면 예외를 발생시키는 것이 아니라 남은 argument를 무시한다.

public class Main {
    public static void main(String[] args) {
        System.out.println(String.format("%d %d %d", 1, 2, 3, 4, 5));
    }
}