LAC WATCH

セキュリティとITの最新情報

RSS

株式会社ラック

メールマガジン

サイバーセキュリティや
ラックに関する情報をお届けします。

ラックピープル | 

ほとんどが非推奨メソッドとなったjava.util.Date、代替手段と非推奨メソッド利用していた場合の問題とは

Java8だとすでにjava.util.Dateのインスタンスを含めて殆どが非推奨になっています。
これに伴い、サブクラスであるjava.sql.Dateやjava.sql.Timestampにも非推奨のコンストラクタやメソッドが乱立しています。

非推奨のコンストラクタやメソッドへの対応はあるのでしょうか?
そもそも、非推奨クラスや非推奨メソッドを利用することには、どのような問題があるのでしょうか?

非推奨となったクラスやメソッドの代替手段

まずは、java.util.Dateを例に上げて、非推奨となったクラスやメソッドの代替手段を考えていきましょう。

非推奨となったクラスやメソッドには、このコンストラクタ/メソッドを使うべきという表記がJavaDocにあります。
コンパイル時のログに「非推奨メソッドを使っています」といったメッセージが出力されたら、適宜JavaDocを参照して適切なコンストラクタやメソッドを使うよう、実装を修正していってください。

java.util.Dateの代わりとなるクラス

Java8から導入されたjava.time.LocalDateTimejava.time.LocalDatejava.time.LocalTimeを使いましょう。
こちらなら、Java17でもまだ非推奨が付与されていないので、利用していても問題ありません。

java.time.LocalDate:
日付を操作・保持する。時刻データは保持していない。
java.time.LocalTime:
時刻を操作・保持する。日付データは保持していない。
java.time.LocalDateTime:
日付時刻を操作・保持する。

基本的な使い方

今まで使っていたjava.util.Dateとは使い方が大きく異なりますので、早く使い慣れるようにしましょう。

String⇔LocalDateTime/LocalDate/LocalTimeの相互変換

java.time.format.DateTimeFormatterを使って型変換ができます。
String型へ変換する場合のフォーマット指定は、概ねSimpleDateFormatと互換性はありますが、JavaDocで確認してください。

// LocalDateTime → String
System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
// String → LocalDateTime
System.out.println(LocalDateTime.parse("2022-02-28 12:34:21", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));

SimpleDateFormatを利用してフォーマットしていた場合もあると思います。
ただ、SimpleDateFormatはスレッドセーフではないので、実装の方法によってはアクセス数が多い場合に表示すべき値と異なる値が表示されるという不具合が発生する可能性があります。
原因不明な不具合を埋め込まないためにも、DateTimeFormatterを利用することをおすすめします。

現在日付取得

// LocalDateで現在日付取得
LocalDate localDate = LocalDate.now();
// LocalTimeで現在時刻取得
LocalTime localTime = LocalTime.now();
// LocalDateTimeで現在日付と現在時刻取得
LocalDateTime localDateTime = LocalDateTime.now();

日付指定してインスタンス作成

操作できるエリアを持っていれば、操作できるエリアに値を指定してのインスタンス作成が可能です。

どういう意味かと言いますと......
LocalDateなら年月日を保持しているため年月日を指定してのインスタンス作成はできますが、時刻を指定してのインスタンス作成はできません。
同じく、LocalTimeは日付を指定してのインスタンス作成はできません。
ということを言っているわけですね。

// 年月日を指定してLocalDateインスタンス作成
LocalDate localDate = LocalDate.of(2022, 1, 1);
// 時分秒を指定してLocalTimeインスタンスを作成
LocalTime localTime = LocalTime.of(12, 0, 0); // ナノ秒は0で作成される
// 年月日時分秒を指定してLocalDateTimeインスタンスを作成
LocalDateTime localDateTime = LocalDateTIme.of(2022, 1, 1, 12, 0, 0);

省略できる範囲

LocalDate:
年月日全て省略不可
LocalTime:
ナノ秒、秒までは省略可。省略した場合は「0」と判断される。
LocalDateTime:
ナノ秒、秒までは省略可。省略した場合は「0」と判断される。

加算

加算したいフィールドを指定してplusメソッドを呼び出すか、操作できるエリアが末尾についたplus○○を呼び出せば、加算完了となります。

// 明日日付のLocalDateインスタンス
LocalDate tommorow = LocalDate.now().plusDays(1);
// 翌月のLocalDateインスタンス
LocalDate nextMonth = LocalDate.now().plusMonths(1);
// 翌年のLocalDateインスタンス
LocalDate nextYear = LOcalDate.now().plus(1, ChronoUnit.YEARS);
// 1時間後のLocalTimeインスタンス
LocalTime nextHour = LocalTime.now().plusHours(1);

LocalDateTimeは、LocalDateやLocalTimeで使えるものが全部使えます。

減算

加算の時のplusplus○○minusminus○○に変更するだけで減算完了です。
加減算のコードが読みやすくなっています。

日付前後確認

インスタンス1.isAfter(インスタンス2)なら、インスタンス1がインスタンス2より未来日付なのかを確認します。
逆にインスタンス1.isBefore(インスタンス2)なら、インスタンス1がインスタンス2より過去日付なのかを確認します。

// 未来日確認
System.out.println(LocalDateTime.now().plusDays(1).isAfter(LocalDateTime.now()); // 「true」
// 過去日確認
System.out.println(LocalDateTime.now.plusDays(1).isBefore(LocalDateTime.now()); // 「false」

各エリアの値取得

DBなどから取ってきた日時を年月日個別に取りたい場合があります。
そんな時は、get○○で取得できます。

// 年
System.out.println(LocalDate.of(2022, 1, 2).getYear()); // 「2022」
// 月
System.out.println(LocalDate.of(2022, 1, 2).getMonthValue()); // 「1」
// 日
System.out.println(LocalDate.of(2022, 1, 2).getDayOfMonth()); // 「2」
// 時
System.out.println(LocalTime.of(12, 10, 11).getHour()); // 「12」
// 分
System.out.println(LocalTime.of(12, 10, 11).getMinute()); // 「10」
// 秒
System.out.println(LocalTime.of(12, 10, 11).getSecond()); // 「11」

少し注意点があります。

  • getMonthで取得できるのは列挙型のMonthオブジェクト。
    数値でなく、toStringで出力するとJanuaryなどの「月の名前」が取得されます。
  • getDayOfWeekで取得できるのは列挙型のDayOfWeekオブジェクト。
    これも数値でなくて、曜日の名前が取得されます。
    switchを使って何曜日なのか、を判断するのは重宝しそうです。
  • getDayOfYearで取得できるのは、1月1日を「1」として何日過ぎたかという値です。
    2022年4月22日だと「115」が取得できます。ちなみに2020年4月22日だと閏年なので「116」が取得できます。

便利な使い方

切り捨てする

時間しか指定できませんが、どこまで保持したいかを指定して、指定したエリア未満の値を最小値(秒なら0)にできます。
どこまで保持したいかを指定するにはChronoUnitを使えば簡単です。

// 当日00:00を指定する
System.out.println(LocalDateTime.of(2022, 5, 8, 12, 43, 6).truncatedTo(ChronoUnit.DAYS)); // 「2022/5/8 00:00:00」

LocalDateTime#nowを発行した後に、日付の切り替わりがすぐ取得できます。
当日の最後までをミリ秒単位で欲しい時(そこまで厳密にしたい場合はありますかね?)もかなり楽にできます。

System.out.println(LocalDateTime.of(2022, 5, 8, 12, 43, 6).truncatedTo(ChronoUnit.DAYS).plusDays(1).minusNanos(1)); // 「2022-05-08T23:59:59.999999999」

極限まで「当日判断したい!」ということがすぐに取得できます。

月末を取る

LocalDateにあるlengthOfMonthを使うと、当月の月末日付が取得できます。

LocalDate localDate = LocalDate.of(2020, 2, 4);
System.out.println("月末 -> " + localDate.lengthOfMonth());
localDate = LocalDate.of(2022, 2, 4);
System.out.println("月末 -> " + localDate.lengthOfMonth());

出力結果は下記となります。

月末 -> 29
月末 -> 28
 
プロセスは終了コード 0 で終了しました

年も正しく判断してくれていることがわかりますね。
月初は必ず1ですからね?

存在日付チェック

LocalDate#ofLocalDateTime#ofは、存在しない日付を入れるとDateTimeExceptionという例外をスローします。言い換えると、数値を入れてみて例外が出た=存在しない日付という判断が可能ということになります。
閏年も正しく判断してくれるので、かなり便利です。

年齢計算

ChronoUnitという別クラスを使うことになりますが、LocalDate同士やLocalDateTime同士の経過年というものが計算できます。これ、年齢計算しますよ、という意味でも使えることになります。
閏年もきっちり計算してくれるので、非常に使い勝手がいいです。

public class LocalDateTImeTest {
    public static void main(String[] args) {
        LocalDate birthDay = LocalDate.of(2000, 2, 29);
        LocalDate commingOfAgeDay = LocalDate.of(2022, 1, 10);
        LocalDate thisYearBirthDayBefore = LocalDate.of(2022, 2, 28);
        LocalDate thisYearBirthDay = LocalDate.of(2022, 3, 1);
        LocalDate christmas = LocalDate.of(2022, 12, 25);
 
        System.out.println("成人の日      -> " + ChronoUnit.YEARS.between(birthDay, commingOfAgeDay));
        System.out.println("誕生日前日    -> " + ChronoUnit.YEARS.between(birthDay, thisYearBirthDayBefore));
        System.out.println("閏年の誕生日? -> " + ChronoUnit.YEARS.between(birthDay, thisYearBirthDay));
        System.out.println("クリスマス    -> " + ChronoUnit.YEARS.between(birthDay, christmas));
    }
}

出力結果は下記となります。

成人の日      -> 21
誕生日前日    -> 21
閏年の誕生日? -> 22
クリスマス    -> 22
 
プロセスは終了コード 0 で終了しました

有効期限○日という判断も、現在時刻との差分をChronoUtnit.DAYSを使って取得すれば、すぐできます。

2月29日生まれの方の誕生日については法律によって扱いが異なるので、扱いには少し注意してください。閏年でない年における誕生日は、「年齢計算ニ関スル法律」では3月1日、道路交通法では2月28日となっています。

元号の操作

日本の場合(日本だけかもしれません)、元号という年月表記があります。
どう対応するのが早いでしょうか?
元号を保持するためのクラスがあると楽に処理できると思いませんか?

java.time.chrono.JapaneseDate

LocalDateやLocalDateTimeで保持している値もしくは、数値そのものを指定してインスタンスを作成すると、元号・元号年・月・日が取得できる、JapaneseDateというありがたいクラスが存在します。
Java17だと令和も対応しています。便利ですね!

public class LocalDateTImeTest {
    public static void main(String[] args) {
        LocalDateTime localDateTime = LocalDateTime.of(2022, 4, 25, 12, 0, 0);
        JapaneseDate japaneseDate =
                JapaneseDate.of(
                        localDateTime.getYear(),
                        localDateTime.getMonthValue(),
                        localDateTime.getDayOfMonth()
                );
 
        System.out.println("JapaneseDate -> " + japaneseDate);
        // 元号が英語で表示されてしまうのは避けたいという場合はgetDisplayNameを使えば日本語で表示される
        System.out.println(japaneseDate.getEra().getDisplayName(TextStyle.FULL, Locale.JAPANESE));
    }
}

上記コードを実行した結果は下記となります。

JapaneseDate -> Japanese Reiwa 4-04-25
令和
 
プロセスは終了コード 0 で終了しました

ソースコード内のコメントの中でも記載していますが、JapaneseDateをそのまま出力すると全部英語で出力されてしまいます。
JapaneseDateのgetEraというメソッドで取得できるJapaneseEraのメソッド、getDisplayNameへ「日本語で出力して欲しい」と指示すると漢字で出力されるので、必要に応じて変換して利用してください。

非推奨クラスやメソッドの問題

java.util.Dateを例に、非推奨クラスの代替実装を紹介してきました。
しかし、なぜそのような実装をしないといけないのか、と疑問を持ちませんか?

非推奨メソッドを使った実装をしていても、コンパイル時にWARNINGが出るだけです。
テストをしても期待通りに動きますし、何より本番環境でまったく問題なく動いていますしね。
java.util.Dateが非推奨になっているからと言っても、使い慣れたクラスだからそちらを使いたいでしょうし。
コンパイル時のログが長くなるだけなら特に問題ないのでは?と思われるかもしれません。

非推奨とは何でしょうか?

Javaでいう非推奨を示す@Deprecatedが付与されているクラスやメソッドを利用していても特に問題は発生していなければ、たとえ付与されていても使い慣れたクラスやメソッドを使ってしまうのは、人の性というものです。

ならば、なぜ非推奨が付与されているのかを理解してから利用したほうが、良い判断と言えるのではないでしょうか。
発生する可能性がある問題を把握しておけば、いざというときに対応できます。

非推奨 API とは、API の変更に伴い、もはや使用が推奨されなくなった API のことです。非推奨のクラス、メソッド、フィールドも引き続き実装されてはいますが、将来の実装で削除される可能性があります。そのため、新しく記述するコードではこれらを使用しないでください。また可能であれば、従来のコードもこれらを使用しないように書き直してください。

出典:JDK5のドキュメント(公式)、非推奨APIのページ

情報がちょっと古いですが、要は使わないことと言っているわけですね。

将来の実装での削除の可能性だけであれば問題ない?

将来の実装で削除される可能性があるだけであれば、将来の実装で削除されるまで(=使えるうち)なら使ってもいいと判断しそうです。しかし、それでは問題となる理由が1つあります。

削除する理由が記載されていません!

もしかすると、致命的な脆弱性が発生して、原因が@Deprecatedを付与されたクラスだからという理由で削除してしまうかもしれません。あくまで可能性の話ではありますが、この場合、脆弱性対応を行うためのマイナーバージョンアップでも非推奨が付与されたクラスやメソッドが削除されてしまいます。

削除された時に対応すればよい?

マイナーバージョンアップを行う場合は一通りの動作確認を実施してからのリリースですし、それなりの期間を取るから問題があれば対応可能となりますね。であれば、やはり削除された時に対応すればよいと判断しがちです。
しかし、本当にそれだけでしょうか?

さらに確認のため、Javaで非推奨だと教えてくれるjava.lang.Deprecatedに記載されているクラス概要を読んでみましょう。

@Deprecatedの注釈を付けられたプログラム要素は、プログラマが使用することを薦められていないプログラム要素です。要素はいくつかの理由のいずれかで非推奨にされる可能性があります。たとえば、その使用がエラーにつながる可能性があります。互換性のない形で変更されたり、将来のバージョンで削除される可能性があります。より新しい、通常は好ましい選択肢に置き換えられました;または非推奨にされました。

出典:Java17、DeprecatedクラスのJavaDoc

その使用がエラーにつながる可能性があります。

どういうことですかね?
もう一度読み直してみましょう。

その使用がエラーにつながる可能性があります。

java.lang.Deprecatedに記載されているクラス概要の文言です。
なにかの間違いと思いたいですが、本当に下記の文章が記載されています。

その使用がエラーにつながる可能性があります。

使うとエラーになる可能性があるそうです。
このアノテーションを付ける理由の一番目が、エラーにつながる可能性があるとは......

意外や意外、そんな理由を知らなかった人が100人中2,000人くらいいるのではないでしょうか。
非推奨のクラスやメソッドを使っていたら、思いもよらないところで期待通りの動作をしてくれなくなる可能性がある、ということになりますね。

最後に

今は動いているけど、何かしらのバージョンアップを適用したら即動かなくなることが出てくることがあり得ますし、それ以上に動いているように見えるけど、実は潜在的にエラーが発生する可能性があるのが非推奨クラス/メソッドということですね。
使うことはやめて、代替手段を取るようにしましょう。

この記事は役に立ちましたか?

はい いいえ