Edited at

Javaバージョン別の改元(新元号)対応まとめ


はじめに

改元が2019年5月1日、新元号が「令和」と4月1日に発表されました。新元号の発表が1ヵ月前の4月1日に予定されています (4/1見直し)

関連する記事は「Javaで新元号に対応する」などがありますが、Java SE 8(以下、単にJava 8のように表記)からのDate and Time APIに対応していなかったり、それ以外のブログなどでも古い情報が多いため、改めてなるべく網羅的かつ実践的となるよう、まとめてみることにしました。

※本記事は改元対応が完了するであろう2019年5月くらいまでは随時更新する予定です。

 誤りや補足などあれば適宜コメントなどでご指摘ください。

(3/24追記)

2019年3月21日(米国時間)にOracle社のブログ記事「A new (Japanese) era for Java!」(日本語翻訳)が公開され、2019年4月16日(米国時間)に予定されているアップデートリリースで新元号に対応することが発表されました。

本記事は、バージョンアップできない場合でも、できるだけ対応する方法について説明します。

(4/27見直し)

商用ライセンスが必要となるOracle JDK 7u221, 8u211, 11.0.3がリリースされ、OpenJDK 7u221, 8u212-b03, 11.0.3+7にも新元号「令和」の対応が反映されました。また、現状最新版でnon-LTS(短期サポート)であるOracle JDK/OpenJDK 12.0.1にも反映されました。

そのため、これら以降にバージョンアップすることで、それほど考慮せずに対応可能なはずです。

※実質的なLTS(長期サポート)リリースとなったOracle JDK 8u212/11.0.3とOpenJDK LTS 8u212/11.0.3以降は異なる修正が含まれる可能性があるため、念のためご注意ください。


前提

(4/17見直し)


結論(対応方針)

以降は長くなるので、改元に対する標準APIでの対応方針としては、バージョン別に次のようになると思います。(4/17見直し)


  • Java 6, 7 (7u221以降は修正済)


    • java.util.Date/Calendarを使う場合、calendars.propertiesを修正



  • Java 8 (Oracle JDK 8u211, OpenJDK 8u212-b03以降は修正済)


    • java.util.Date/Calendarを使う場合、calendars.propertiesを修正

    • Date and Time APIを使う場合、calendars.propertiesの修正が反映されるよう最新版を利用 (元年表記は別途実装)



  • Java 9, 10


    • java.util.Date/Calendarを使う場合、jdk.calendar.japanese.supplemental.era, java.locale.providers システムプロパティを指定

    • Date and Time APIを使う場合、jdk.calendar.japanese.supplemental.era システムプロパティを指定 (元年表記は別途実装)



  • Java 11以降 (Oracle JDK 11.0.3/12.0.1以降, OpenJDK 11.0.3+7/12.0.1以降は修正済)


    • java.util.Date/Calendarを使う場合、java.locale.providers システムプロパティを指定し、改元対応が正式に入った4月以降のリリースにバージョンアップ

    • Date and Time APIを使う場合、改元対応が正式に入った4月以降のリリースにバージョンアップするか、jdk.calendar.japanese.supplemental.era システムプロパティを変則的に指定



(4/17見直し)

実際には標準APIだけではなく、例えば「4月中は、(5月以降の日付でも) 新元号を印字してはならない!」など、業務要件に応じて独自実装が必要となる可能性も多々あるでしょうが、原則として本記事では扱いません。


実行例

和暦の対応のうち、一般的によく使われそうな日付の整形(フォーマット)や解析(パース)が基本的に行えることを目指します。

(4/1見直し)

新元号は「令和」、アルファベットは「R」と発表されましたので、これをもとに実行例も含め見直しました。

$ echo 令和 | native2ascii

\u4ee4\u548c

となるため、こちらを基本的に{JRE_HOME}/lib/calendars.propertiesに指定するものとします。

※異体字の「令」\uf9a8もありますが、CJK互換漢字でありUnicode正規化により、CJK統合漢字の「令」\u4ee4に正規化されます。そのため本記事では\u4ee4\u548cのみを扱います。

(4/27追記) なお、経済産業省からも「新元号名で使用する文字コードについて(周知)」が4/5付けで出ており、これと同様です。

calendar.japanese.eras: \

name=Meiji,abbr=M,since=-3218832000000; \
name=Taisho,abbr=T,since=-1812153600000; \
name=Showa,abbr=S,since=-1357603200000; \
name=Heisei,abbr=H,since=600220800000; \
name=\u4ee4\u548c,abbr=R,since=1556668800000

※「1556668800000」は2019年5月1日(ローカル時間を指定するらしく9:00)を示すエポックミリ秒です。


java.util.Date/Calendarを使った例

旧来からの使いづらい実装とも言えるjava.util.Datejava.util.Calendarを使った例は次のようにしてみました。


DateCalendarTest.java

import java.text.*;

import java.util.*;

public class DateCalendarTest {

public static void main(String... args) throws ParseException {
Locale locale = new Locale("ja", "JP", "JP");
DateFormat kanjiFormat = new SimpleDateFormat("GGGGy年M月d日", locale);
DateFormat asciiFormat = new SimpleDateFormat("Gyy.MM.dd", locale);

// 現在日付出力
Calendar now = Calendar.getInstance();
System.out.println(kanjiFormat.format(now.getTime()));
System.out.println(asciiFormat.format(now.getTime()));

Calendar cal = Calendar.getInstance();
cal.clear();

// 改元前の日付出力
cal.set(2019, Calendar.APRIL, 30, 23, 59, 59);
Date lastHeiseiDate = cal.getTime();
System.out.println(kanjiFormat.format(lastHeiseiDate));
System.out.println(asciiFormat.format(lastHeiseiDate));

// 改元後の日付出力
cal.set(2019, Calendar.MAY, 1, 0, 0, 0);
Date firstNewEraDate = cal.getTime();
System.out.println(kanjiFormat.format(firstNewEraDate));
System.out.println(asciiFormat.format(firstNewEraDate));

// 厳密でない日付解析
System.out.println(kanjiFormat.parse("平成31年5月1日"));
System.out.println(asciiFormat.parse("H31.05.01"));

// 厳密に解析時はエラーになる
kanjiFormat.setLenient(false);
try {
kanjiFormat.parse("平成31年5月1日");
} catch (ParseException e) {
System.err.println(e.getMessage());
}
asciiFormat.setLenient(false);
try {
asciiFormat.parse("H31.05.01");
} catch (ParseException e) {
System.err.println(e.getMessage());
}
}
}


(3/26追記) 参考までにLocale("ja", "JP", "JP")が和暦を表すロケールですが、これは互換性を維持するための特例であり、Java 7からはLocale.forLanguageTag("ja-JP-u-ca-japanese")のように指定できます。

このとき期待する動作としては次のような出力となるはずです。

平成31年4月1日

H31.04.01
平成31年4月30日
H31.04.30
令和1年5月1日
R01.05.01
Wed May 01 00:00:00 JST 2019
Wed May 01 00:00:00 JST 2019
Unparseable date: "平成31年5月1日"
Unparseable date: "H31.05.01"

なお、厳密でない日付解析(デフォルト)は、"13月"や"-1日"など明らかにおかしい日付フィールドでも許容してしまいますので、業務要件に応じて上記のようにDateFormat#setLenient(false)したり、別途フィールドごとに有効範囲をチェックする必要もあるでしょう。

それでは、バージョン別に動作や注意点を見てみましょう。


Java 6

基本的にどのマイナーバージョンでも期待通りに動作します。

ただし、Locale("ja", "JP", "JP")を指定していても、OSによっては漢字が文字化けする可能性があります。試した限り、Windowsの言語設定が日本語の場合は正しく動作しました。Windowsの言語設定を英語にしていたり、macOS X環境では??31?4?30?のようになってしまいました。

これは全般的にコンソールに日本語を出力する際の問題のようですが、念のためご注意ください。


Java 7, 8

基本的にどのマイナーバージョンでも期待通りに動作します。

(4/27見直し) Oracle JDK 7u221/8u211, OpenJDK 7u221, 8u212-b03以降で正式対応されました。


Java 9以降

JDK-8048123による変更で、calendars.propertiesがなくなり、システムプロパティ jdk.calendar.japanese.supplemental.era を設定することで代替とするようになっています。

次のように実行します。

$ java -Djdk.calendar.japanese.supplemental.era="name=令和,abbr=R,since=1556668800000" DateCalendarTest

(4/27見直し)

後述するDate and Time APIと同様に、Java 11以降では上記のようなシステムプロパティの指定は無視されます。また、Oracle JDK 11.0.3, OpenJDK 11.0.3+7以降は正式対応されましたので指定は不要です。

実行すると次のような結果となります。

平成31年4月1日

平成31.04.01
平成31年4月30日
平成31.04.30
令和1年5月1日
R01.05.01
Wed May 01 00:00:00 JST 2019
Exception in thread "main" java.text.ParseException: Unparseable date: "H31.05.01"
at java.base/java.text.DateFormat.parse(DateFormat.java:388)
at DateCalendarTest.main(DateCalendarTest.java:33)

例外が発生してしまいましたね。

実際にその前の出力を見ると"H31.04.30"となるべき箇所が"平成31.04.30"となっています。

(明示的にシステムプロパティを指定した"R01.05.01"は出力されています)

(1/8見直し)

Java 12-ea+25や13-ea+1(Early Accessでビルド番号まで表記)まで一通り試しましたが同様の結果になるので、Bugとして報告しておきましたが、現状では「NYY.MM.DD」のような形式 (JIS X 0301表記)は、Java 9以降はSimpleDateFormatでは整形・解析できないと考えたほうがよいでしょう。

元号の漢字のみを利用するのであれば問題ありません。

また、次のDate and Time APIを使うのが、当面の回避策になると考えられます。


"[JDK-8216204] Wrong SimpleDateFormat behavior with Japanese Imperial Calendar" でコメントいただいたのですが、不具合ではなくJava 9からの仕様変更が原因でした。

Java 8以前と同様の出力にしたい場合は、java.locale.providersシステムプロパティを「COMPAT,CLDR」のように設定すればよいとのことです。

$ java -Djava.locale.providers=COMPAT,CLDR -Djdk.calendar.japanese.supplemental.era="name=令和,abbr=R,since=1556668800000" DateCalendarTest

(1/13追記)

Java 8ではCLDRがデフォルト有効ではなく、互換設定としては-Djava.locale.providers=COMPAT,SPIとなるため、気になる場合はそのように設定してもよいでしょう。


参考情報


Date and Time APIを使った例

Java 8で導入されたDate and Time APIは、旧来のjava.util.Dateやjava.util.Calendarを置換したり移行するために、ISO 8601をベースとして設計されたものです。

これを使った例は次のようにしてみました。(6/15見直し)


DateAndTime8Test.java

import java.util.Locale;

import java.time.LocalDate;
import java.time.chrono.*;
import java.time.format.*;

public class DateAndTime8Test {

public static void main(String... args) {
DateTimeFormatter kanjiFormat = DateTimeFormatter.ofPattern("GGGGy年M月d日", Locale.JAPAN);
DateTimeFormatter asciiFormat = DateTimeFormatter.ofPattern("GGGGGyy.MM.dd", Locale.JAPAN);

// 現在日付出力
JapaneseDate today = JapaneseDate.now();
System.out.println(kanjiFormat.format(today));
System.out.println(asciiFormat.format(today));

// 改元前の日付出力
JapaneseDate lastHeiseiDate = JapaneseDate.of(2019, 4, 30);
System.out.println(kanjiFormat.format(lastHeiseiDate));
System.out.println(asciiFormat.format(lastHeiseiDate));

// 改元後の日付出力
JapaneseDate firstNewEraDate = JapaneseDate.of(2019, 5, 1);
System.out.println(kanjiFormat.format(firstNewEraDate));
System.out.println(asciiFormat.format(firstNewEraDate));

// 厳密でない日付解析
DateTimeFormatter kanjiParseFormat = kanjiFormat.withChronology(JapaneseChronology.INSTANCE)
.withResolverStyle(ResolverStyle.LENIENT);
System.out.println(kanjiParseFormat.parse("平成31年5月1日", LocalDate::from));
// ※英字表記では"y"を1桁でパースしないと元号11年までが +100年扱いされる
DateTimeFormatter asciiParseFormat = DateTimeFormatter.ofPattern("GGGGGy.MM.dd", Locale.JAPAN)
.withChronology(JapaneseChronology.INSTANCE)
.withResolverStyle(ResolverStyle.LENIENT);
System.out.println(asciiParseFormat.parse("H31.05.01", LocalDate::from));
// System.out.println(asciiParseFormat.parse("R01.05.01", LocalDate::from)); // 正式リリース後のみ

// 厳密に解析時はエラーになる
kanjiParseFormat = kanjiParseFormat.withResolverStyle(ResolverStyle.STRICT);
try {
kanjiParseFormat.parse("平成31年5月1日");
} catch (DateTimeParseException e) {
System.err.println(e.getMessage());
}
asciiParseFormat = asciiParseFormat.withResolverStyle(ResolverStyle.STRICT);
try {
asciiParseFormat.parse("H31.05.01");
} catch (DateTimeParseException e) {
System.err.println(e.getMessage());
}
}
}


念のためですが、JapaneseEra.HEISEIのようにハードコーディングされている箇所は、改元対応が正式に入るリリースまで対応できません。JapaneseEra.REIWAは定義済ですがpublicになるのは9月にリリースされる予定のJava 13からです。(4/27追記)

ちなみに、DateTimeFormatterではLocaleを省略できますが、OSや言語設定によっては"平成"の代わりに"Heisei"と出力されてしまうため、明示的に指定することをお勧めします。

(6/15追記)

GGGGGyy.MM.dd のようなアルファベット表記のパターンでは、整形は正しく動作しますが、解析は正式リリースとなった4月以降のリリースでないと、新元号での日付に対してはDateTimeParseExceptionが発生してしまいます。

また、@kazuki43zoo さんにコメントや記事で解説いただいたようにGGGGGy.MM.dd"y"を1桁にしないと元号11年までが +100年扱いされてしまうので、ご注意ください。

直観に反する動作ではあるのですが、有識者にも確認したところ、これはBugではなくIsoChronologyを基準として、仕様が策定されたためであろうとの結論となりました。

もし、桁数まで含めた厳密な解析が必要な場合は、正規表現などを併せて用いるのがよいかもしれません。

このとき期待する動作としては次のような出力となるはずです。

平成31年4月1日

H31.04.01
平成31年4月30日
H31.04.30
令和1年5月1日
R01.05.01
2019-05-01
2019-05-01
Text '平成31年5月1日' could not be parsed: year, month, and day not valid for Era
Text 'H31.05.01' could not be parsed: year, month, and day not valid for Era

こちらも先のjava.util.Date/Calendarの例と同様に、解析には注意が必要です。

デフォルトはResolverStyle.SMARTであり、"13月"や"-1日"などは許容しませんが、"平成32年"なども解析できなくなってしまうため、業務要件には合わないことも多いと思います。

ここでは厳密でない日付解析(LENIENT)と厳密な日付解析(STRICT)のみ指定していますが、やはり状況に応じてもっと細かいチェックが必要になるでしょう。

それでは、バージョン別に動作や注意点を見てみましょう。

なお、calendars.propertiesやJava 9以降のjdk.calendar.japanese.supplemental.eraシステムプロパティは、java.util.Date/Calendarを使った例と同様に適宜指定するものとします。


Java 8

(4/27見直し) Oracle JDK 8u221, OpenJDK 8u222-b03で正式対応されました。


8GA~8u31

Exception in thread "main" java.lang.NullPointerException

at java.time.chrono.JapaneseEra.privateEraFrom(JapaneseEra.java:271)
at java.time.chrono.JapaneseDate.toPrivateJapaneseDate(JapaneseDate.java:502)
at java.time.chrono.JapaneseDate.<init>(JapaneseDate.java:333)
at java.time.chrono.JapaneseDate.now(JapaneseDate.java:197)
at java.time.chrono.JapaneseDate.now(JapaneseDate.java:166)
at DateAndTime8Test.main(DateAndTime8Test.java:13)

いきなり例外が発生してしまいましたね。

(1/15見直し)Java 8リリース当初は和暦を正しく扱えなかったようです。

(JapaneseDate.ofやJapaneseDate.fromでも同様にNullPointerExceptionが発生します。)


Java 8リリース当初は、JDK-8044671の不具合により、calendars.propertiesに元号を追加すると、和暦を正しく扱えなくなってしまうようです。


8u40~8u151

平成31年4月1日

H31.04.01
平成31年4月30日
H31.04.30
31年5月1日
301.05.01
2019-05-01
2019-05-01
Text '平成31年5月1日' could not be parsed: year, month, and day not valid for Era
Text 'H31.05.01' could not be parsed: year, month, and day not valid for Era

"令和1年5月1日"となってほしいのに"31年5月1日"となったり、"R01.05.01"となってほしいのに"301.05.01"となっていますね。calendars.propertiesのsinceは使われているようですが、それ以外は正しく読み込めず元号が"3"として扱われています。

(1/15見直し) JDK-8054214の不具合により、追加された元号を数値として出力してしまっているようです。


8u152~8u212

平成31年4月1日

H31.04.01
平成31年4月30日
H31.04.30
令和1年5月1日
R01.05.01
2019-05-01
2019-05-01
Text '平成31年5月1日' could not be parsed: year, month, and day not valid for Era
Text 'H31.05.01' could not be parsed: year, month, and day not valid for Era

期待通りの出力となりました!

このため、Java 8のDate and Time APIで和暦を扱うには、なるべく新しいバージョンを利用したほうがよさそうです。

ただし、1つ注意点があります。サンプルコードではDateTimeFormatter.ofPattern("GGGGy年M月d日")と"G"が4つのフル形式で指定し、"令和1年5月1日"のように出力されました。

"G"が3つまでだと短縮形式扱いとなるのですが、"R1年5月1日"と出力されてしまいます。("平成"までは問題なし)


8u221~

(6/15追記) Oracle JDK 8u221, OpenJDK 8u222-b03で正式対応されました。

このため、Date and Time APIで和暦を用いる場合は、これ以降のバージョンを基本的には利用したほうがよいでしょう。

GGGGGy.MM.dd のようなアルファベット表記のパターンでの解析では、これ以降のリリースでないと新元号での日付 (例:R01.05.01) に対しては正しく動作しないためです。

ではJava 9以降で動作を見ていきましょう。次のように実行します。

$ java -Djdk.calendar.japanese.supplemental.era="name=令和,abbr=R,since=1556668800000" DateAndTime8Test


Java 9

基本的にどのマイナーバージョンでも期待通りに動作します。

ただし、Java 9は先の8u152~と同様の注意点が存在するため、漢字表記をする際はDateTimeFormatterで"GGGG"と4つ指定しましょう。


Java 10

基本的にどのマイナーバージョンでも期待通りに動作します。

DateTimeFormatterで"G"が3つまでの短縮形式でも、"令和1年5月1日"と出力されるように修正されています。


Java 11, 12, 13-ea

(4/27見直し) Oracle JDK 11.0.3/12.0.1, OpenJDK 11.0.3+7/12.0.1で正式対応され、プレースホルダ実装は置き換えられましたので、以下は該当しません。

平成31年4月1日

H31.04.01
平成31年4月30日
H31.04.30
元号1年5月1日
N01.05.01
2019-05-01
2019-05-01
Text '平成31年5月1日' could not be parsed: year, month, and day not valid for Era
Text 'H31.05.01' could not be parsed: year, month, and day not valid for Era

システムプロパティで指定した「令和」や「R」の代わりに、"元号"や"N"が出力されていますね。

これはプレースホルダとして実装された内容によりオーバーライドされているためです。

(逆に言えば、jdk.calendar.japanese.supplemental.eraシステムプロパティを指定しなくても同じです。)

(4/27見直し) 新元号が発表後に修正されて、Oracle JDK 11.0.3/12.0.1, OpenJDK 11.0.3+7/12.0.1で正式対応されました。そのため、次のTipsも不要です。


Tips (裏技?)

今まではシステムプロパティ jdk.calendar.japanese.supplemental.era に、2019年5月1日開始とみなされる「1556668800000」を設定するのを前提としてきましたが、1ミリ秒でも先を指定すると、プレースホルダ実装をさらにオーバーライドすることができました。

(4/27追記) @mazkasa さんからコメントをいただき、表示にはCalendarNameProviderを実装するのがロケールも考慮した正攻法であり、プレースホルダ実装も置き換えられるとのことです。

JShellで動作を見てみましょう。次のように起動します。

$ jshell -s -R-Djdk.calendar.japanese.supplemental.era="name=令和,abbr=R,since=1556668800001"

-> import java.time.chrono.*

-> import java.time.format.*
-> var kanjiFormat = DateTimeFormatter.ofPattern("GGGGy年M月d日", Locale.JAPAN)
-> var asciiFormat = DateTimeFormatter.ofPattern("GGGGGyy.MM.dd", Locale.JAPAN)
-> var firstNewEraDate = JapaneseDate.of(2019, 5, 1)
-> System.out.println(kanjiFormat.format(firstNewEraDate))
令和151
-> System.out.println(asciiFormat.format(firstNewEraDate))
R01.05.01
-> /exit

なお、旧来のjava.util.Date/Calendarではオーバーライドできず、プレースホルダ実装のままとなりました。(もちろん翌日の5月2日を指定すれば動きますが意味はありません)

Date and Time APIとしても、このようなイレギュラーなシステムプロパティの指定を考慮している訳ではないはずです。そのため、基本的には、正式に元号が発表された後の修正リリースを待ったほうがよいでしょうが、どうしても正式対応していない(かつJava 11以降の)リリースを使いたい場合には、応急処置的に利用することは可能かもしれません。

(4/27見直し) Oracle JDK 11.0.3/12.0.1, OpenJDK 11.0.3+7/12.0.1で正式対応されましたので、こちら以降を使う場合は不要です。


補足


元年表記について

先の実行例では"令和1年"のように出力されていました。ただ漢字で出力する際は"令和元年"のように出力したい場合も多いと思います。


java.util.Date/Calendarの場合

旧来のjava.text.DateFormatではFULLスタイルを指定した日付フォーマッタを利用することで実現できます。

(4/17追記) またGGGGyyyy年のように"y"を4つ以上指定することでもFULLスタイルと同様となります。


FirstYearTest.java

import java.text.*;

import java.util.Calendar;
import java.util.Locale;

public class FirstYearTest {
public static void main(String... args) throws ParseException {
DateFormat fullFormat = DateFormat.getDateInstance(DateFormat.FULL, new Locale("ja", "JP", "JP"));
Calendar cal = Calendar.getInstance();
cal.clear();
cal.set(2019, Calendar.MAY, 1);
System.out.println(fullFormat.format(cal.getTime()));
fullFormat.parse("平成元年1月8日"); // 解析も問題ないが平成を便宜的に利用
}
}


上記を実行すると "令和元年5月1日" のように出力されます。

(4/27見直し)

Java 11以降は先に示した実行例と同様に、以前はプレースホルダが優先され "元号元年5月1日" となりましたが、Oracle JDK 11.0.3/12.0.1, OpenJDK 11.0.3+7/12.0.1で正式対応されました。

ちなみにSHORTスタイルなどを指定すると "R1.05.01" と年が1桁となるため、JIS X 0301表記とは異なります。


Date and Time APIの場合

(1/13 見直し)

Java 8からのDate and Time APIでもFULLスタイルを指定することが可能なのですが、残念ながら現状は"元年"に対応しておらず、またバージョンによって出力もまちまちです。JDK-8068571により"元年"には対応しておらず、代わりにDateTimeFormatterBuilderで対応するとのことです。

(4/27見直し)

基本的に、整形はJava 11以降解析はJava 12以降での対応となります。

2019年7月中旬にリリースされるであろう8u221や11.0.4以降にはバックポートされそうです。

Oracle JDK 8u211/11.0.3, OpenJDK 8u212-b03/11.0.3+7でいずれもバックポートされました。


FirstYear8Test.java

import java.util.*;

import java.time.chrono.*;
import java.time.format.*;
import java.time.LocalDate;
import java.time.temporal.ChronoField;

public class FirstYear8Test {
public static void main(String... args) {
DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder();
builder.appendText(ChronoField.ERA, TextStyle.FULL);
builder.appendText(ChronoField.YEAR_OF_ERA, Collections.singletonMap(1L, "元"));
builder.appendLiteral("年");
builder.appendValue(ChronoField.MONTH_OF_YEAR);
builder.appendLiteral("月");
builder.appendValue(ChronoField.DAY_OF_MONTH);
builder.appendLiteral("日");
DateTimeFormatter formatter = builder.toFormatter(Locale.JAPAN)
.withChronology(JapaneseChronology.INSTANCE);

System.out.println(formatter.format(JapaneseDate.of(2019, 5, 1)));
formatter.parse("平成元年1月8日", LocalDate::from);
}
}


(4/27見直し) このため、単純に実行するとJava 11以降のプレースホルダ実装では "元号元年5月1日" のように出力されていましたが、Oracle JDK 11.0.3/12.0.1, OpenJDK 11.0.3+7/12.0.1で正式対応され "令和元年5月1日" と出力されます。

古いバージョンでは、Date and Time APIだけでは"元年"に対応できないため、"令和1年5月1日" のようなパターンから、正規表現などを利用して置換したほうが楽だとは思います。

(3/24追記)

上記ではCollections.singletonMap(1L, "元")と元年のみ変換するよう指定しています。

元年以外("平成31年"など)を扱う際、整形時はこれで問題ありませんが、解析時はその分もMapに追加する必要があります。

例えば該当箇所を次のように変更します。(必要なimport文は別途指定しているものとします。)

Map<Long, String> yearMap = LongStream.rangeClosed(1, 100).boxed()

.collect(Collectors.toMap(Function.identity(), String::valueOf));
yearMap.put(Long.valueOf(1), "元");
builder.appendText(ChronoField.YEAR_OF_ERA, yearMap);


漢数字への対応

(3/3 追記)

先のコード例で、DateTimeFormatterBuilderの箇所を変更すれば、元年以外の漢数字にも対応できます。

ひとまず元号100年目まで対応する場合は、次のように置き換えることができます。

// 0(〇)は今回の例では利用されないが便宜的に設定

String[] kanjiNumBase = {"〇", "一", "二", "三", "四", "五", "六", "七", "八", "九"};

// 1(一)から99(九十九)まで漢数字を設定
Map<Long, String> kanjiNumMap = new HashMap<>();
for (int num = 1; num < 100; num++) {
if (num < 10) {
kanjiNumMap.put(Long.valueOf(num), kanjiNumBase[num]);
continue;
}

int tens = num / 10;
int ones = num % 10;
StringBuilder kanjiNum = new StringBuilder();
if (tens > 1) {
kanjiNum.append(kanjiNumBase[tens]);
}
kanjiNum.append("十");
if (ones > 0) {
kanjiNum.append(kanjiNumBase[ones]);
}
kanjiNumMap.put(Long.valueOf(num), kanjiNum.toString());
}

// 年は元年・百年に対応
Map<Long, String> yearMap = new HashMap<>(kanjiNumMap);
yearMap.put(Long.valueOf(1), "元");
yearMap.put(Long.valueOf(100), "百");

DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder();
builder.appendText(ChronoField.ERA, TextStyle.FULL);
builder.appendText(ChronoField.YEAR_OF_ERA, yearMap);
builder.appendLiteral("年");
builder.appendText(ChronoField.MONTH_OF_YEAR, kanjiNumMap);
builder.appendLiteral("月");
builder.appendText(ChronoField.DAY_OF_MONTH, kanjiNumMap);
builder.appendLiteral("日");

DateTimeFormatter formatter = builder.toFormatter(Locale.JAPAN)
.withChronology(JapaneseChronology.INSTANCE)
.withResolverStyle(ResolverStyle.LENIENT);

Java 11以降であれば、これを利用して「平成三十一年四月三十日」のように出力できます。

また、Java 12以降であれば「平成元年一月八日」や「平成三十一年四月三十日」など正常な日付に加えて、DateTimeFormatterに変換する際にwithResolverStyle(ResolverStyle.LENIENT)を指定することで「平成九十九年十二月三十一日」や「昭和百年一月一日」など存在しない日付も解析できるようになります。


Java 8/11へのバックポート

(4/17見直し)

2019年4月16日(米国時間)のリリースにより、改元対応のバックポートが実施されました。

このため、これ以降のリリースにバージョンアップできる場合は、それほど考慮せずに改元対応もできるはずです。


合字の対応

(6/1見直し)

"㍻"のような合字は、新元号ではUnicodeコードポイント「U+32FF」が割り当てられ、Java 13以降でUnicode 12.1の対応により、Unicode正規化にも対応し、次のようなコードで確認できるようになります。


LigatureNormalizeTest.java

import java.text.Normalizer;

public class LigatureNormalizeTest {
public static void main(String... args) {
System.out.println("\u337b"); // ㍻
System.out.println(Normalizer.normalize("\u337b", Normalizer.Form.NFKD));
System.out.println(Normalizer.normalize("\u337b", Normalizer.Form.NFKC));

System.out.println("\u32ff"); // ㋿ (令和の合字)
System.out.println(Normalizer.normalize("\u32ff", Normalizer.Form.NFKD));
System.out.println(Normalizer.normalize("\u32ff", Normalizer.Form.NFKC));
}
}


Oracle OpenJDK 13-ea+23 で実行した結果は次のようになります。

> java -version

openjdk version "13-ea" 2019-09-17
OpenJDK Runtime Environment (build 13-ea+23)
OpenJDK 64-Bit Server VM (build 13-ea+23, mixed mode, sharing)

> java LigatureNormalizeTest

平成
平成
?
令和
令和

筆者の環境では、コンソールで正しく令和の合字「㋿」が表示できなかったため「?」となっていますが、正規化されている様子が確認できます。(合字の表示はOSのアップデートに依存します)

LTSリリースであるJDK 8/11にもバックポートされる可能性はありますが、現状はどうなるか不明です。


まとめ

上記以前のバージョンで注意点も含め、表にまとめると次のようになります。(4/18見直し)

バージョン
java.util.Date/Calendar
Date and Time API
共通の備考・注意点

6
○: OSによっては漢字が文字化け
N/A (非対応)
2019年4月より前のリリースはcalendars.properties要修正

7

N/A (非対応)
2019年4月より前のリリースはcalendars.properties要修正

8

△: 最新バージョンを利用すれば◎
2019年4月より前のリリースはcalendars.properties要修正で、元年表記は別途実装

9
◎: アルファベット表記はjava.locale.providers要指定
○: 漢字表記は"GGGG"とパターン指定、元年表記は別途実装
jdk.calendar.japanese.supplemental.era要指定

10
◎: アルファベット表記はjava.locale.providers要指定
○: 元年表記は別途実装
jdk.calendar.japanese.supplemental.era要指定

11以降
○: 初期はプレースホルダ実装、アルファベット表記はjava.locale.providers要指定
○: 元年表記に対応、プレースホルダ実装は上書き可能
2019年4月以降のリリースにバージョンアップすればいずれも◎

(6/1追記)令和の合字「㋿」のUnicode正規化に対応するには、2019年9月にリリースされるJDK 13を現状待つことになりそうです。


おわりに

ご利用のJDK/JREを改元後の最新版にバージョンアップいただくのが本筋ですが、Oracle JDK/JRE 8を利用している場合、商用ユーザーに対するPublic Updatesは2019年1月で終了しました。そのため、Oracle Java SE Subscriptionや、別のJDKディストリビューションへの切替が必要になるケースもあると想定されます。

もっと早く(遅くても2019年始に)新元号が発表されれば、2019年1月のPublic Updatesまでに正式な改元対応が入ったかもしれず、この記事を書く必要もなかったかもしれません。

Javaには限りませんが、和暦を扱っているシステムの改元対応が無事に完了することを願っております。