はじめに
Javaのバージョン番号について、ところどころで1.8のように表記されている個所が出てきます。
リリース当初のJavaは1.3、1.4のようにマイナーバージョンがアップされていましたが、1.5のタイミングで5.0とメジャーバージョンを変えていくようになりました。
Java内のフォルダー名や表記など至るところで「1.X」と表記されている個所がありますが、実際にはXのバージョンを差すので混乱しないよう気をつけてください。
#Java SE 8
##当時の社会
Java SE 8は2014年3月18日にリリースされました。Java5から10年経っています。
1年前ですが何があったかは覚えておらず。齢をとりましたな・・・。Wikipediaを頼ることに。
まさにこの日、ロシアのプーチン大統領がクリミアの編入を表明したり、中台間で結ばれたサービス協定に反対する学生が国会を占拠したりと世界が激しく動いていました。
ITの世界では直後の4月にDockerがリリースされ、コンテナ型仮想化技術、Immutable Infrastructureなどいう言葉が流行しました。
しかし英語圏においては「Immutable Infrastructure」というバズワードは存在していないとのこと。
他にはニュースでも耳にするようになったIoT(Internet of Things = モノのインターネット)が2014年のホットワードといえそうです。
##ラムダ式(クロージャ)
5年前静岡のエンジニアが自発的に集まって始まった「静岡ITDevelopers勉強会」の最初の教本が「Haskell」で、その理由が**「今関数型プログラミング言語が熱い」**だったのですが、Javaも8で少なくないユーザーが渇望していた関数型言語が採用するラムダ式が追加されました。
ラムダ式を使うとコードを書く効率も上がり、読みやすくなります。また何より関数型言語は並列処理に強く、マルチコアのCPUが当たり前である現在、速度を追求するためにも関数型の考え方が必要とされています。
しかし関数型プログラミング言語は、手続き型プログラミング言語に慣れた人にとっては「なんでわざわざ分かりにくい書き方するんだ」というのが正直な感想です。
おそらくJava8は「ラムダ式」のためのバージョンです。
ラムダ式のためにAPIに数多くのクラスやインターフェースやメソッドが追加されていて、その多さに拒否感を持ってしまう人もいるかと思います。私もです。
しかし、これから新規で書かれるJavaのコードにはラムダ式が当たり前のように入ってくるでしょう。プログラムを読んで「???」となるのは辛い。頑張りましょう。
かつてのオブジェクト指向、さらに言えば手続き型言語だって、生まれた当初はそういう部分があったはずです。
ラムダ式は下記の基本文法になります。
( 実装するメソッドの引数 ) -> { 処理 } ※引数は複数でも引数なしでも可能
- 左辺に実装するメソッドの引数を記述
- 「->」を記述
- 右辺に実装する処理(ターゲット式)を記述
基本的なコードで説明します。
public class LamdaSample1 {
public static void main(String[] args){
SampleInterface sampleInterface = (name) ->{
System.out.println("Hello, " + name);
};
sampleInterface.say("World");
}
@FunctionalInterface
private interface SampleInterface {
public void say(String s);
}
}
- 左辺に実装するメソッドの引数を記述 = "name"
- 「->」を記述
- 右辺に実装する処理(ターゲット式)を記述 = System.out.println() と function.say()
ラムダ式を取り入れるメリットの1つは、今まで関数型インターフェースの実装のために書いていた冗長なソースを簡潔に書けるようになる点です。
関数型インタフェースとは、大雑把に言って、定義されている抽象メソッドが1つだけあるインターフェースのことで、staticメソッドやデフォルトメソッドは含まれていても構いません。
また、Objectクラスにあるpublicメソッドがインターフェース内に抽象メソッドとして定義されている場合、そのメソッドが含まれていても1つとはカウントしません。
FunctionalInterfaceアノテーションを付与すると、関数型インターフェースの条件を満たしていない場合にコンパイルエラーになってくれますし、関数型インターフェースを目的としていることが分かりやすくなります。
Java SE 8より前のバージョンでは、その関数型インターフェースを実装するのに「無名内部クラス(匿名クラス)」を使うなどしなければいけませんでした。
「無名内部クラス(匿名クラス)」については以下の記事が分かりやすく説明しています。
@IT:あなたの知らない、4つのマニアックなJava文法 (3/3)
ローカル内で1度だけ使うようなクラスをいちいち名前をつけて定義するとコードが長くなるので、無名内部クラス(匿名クラス)を使うことがあります。
特にGUIアプリケーションなどでは、そこそこ複雑な初期化や処理を持つものの、コンストラクタ内でしか必要のないクラスがありますから、そこで使用します。
ラムダ式を使わず、匿名クラスを使うと、先のコードはこのように書き換えることが出来ます。
public class LamdaSample0 {
public static void main(String[] args){
SampleInterface sampleInterface = new SampleInterface() {
@Override
public void say(String s) {
System.out.println("Hello, " + s);
}
};
sampleInterface.say("World");
}
@FunctionalInterface
private interface SampleInterface {
public void say(String s);
}
}
たしかに、匿名クラスを使う代わりにラムダ式を使うと、コードが短くなりました。ほぼ全ての匿名クラスはラムダ式で置き換えられます。
###ラムダ式の省略
ラムダ式は様々な省略が使えるため、元々短く書けるコードをさらに短く書くことが出来ます。
@FunctionalInterface
public interface IsNumberInterface {
boolean check(String value);
}
このようなインタフェースがあり、それをラムダ式を使用して実装するとします。何も省略しない場合は以下のように書くことが出来ます。
import java.math.*;
public class LamdaSample2 {
public static void main(String[] args) {
IsNumberInterface isNumberInterface = (String value) -> {
try {
new BigDecimal(value);
return true;
} catch (Exception e) {
return false;
}
};
System.out.println(isNumberInterface.check("10"));
}
}
- 引数の型の省略(型推論)
インタフェース宣言時にboolean check(String value)と書いている為、Stringは省略可能です。
IsNumberInterface isNumberInterface = (value) -> {
try {
new BigDecimal(value);
return true;
} catch (Exception e) {
return false;
}
};
- 丸括弧の省略
引数が1つの場合は丸括弧も省略できます。
引数がない場合は省略できず、 () -> { … }のように記述します。
IsNumberInterface isNumberInterface = value -> {
try {
new BigDecimal(value);
return true;
} catch (Exception e) {
return false;
}
};
- 波括弧{}とreturnの省略
何らかの計算を行う関数型インタフェースCalcInterfaceがあったとします。
@FunctionalInterface
public interface CalcInterface {
BigDecimal calc(BigDecimal value1, BigDecimal value2);
}
import java.math.BigDecimal;
public class LamdaSample11 {
public static void main(String[] args) {
CalcInterface calcInterface = (value1, value2) -> {
return value1.add(value2);
};
System.out.println(calcInterface.calc(BigDecimal.valueOf(10.1),BigDecimal.valueOf(20.3)));
}
}
通常はこのように記述します。
しかし、ラムダ式では処理の記述であるターゲット式が1つの文のみで表現できる場合、波括弧{}を外して記述することができ、その場合は戻り値の有無にかかわらず「return」は記述しません。
import java.math.BigDecimal;
public class LamdaSample12 {
public static void main(String[] args) {
CalcInterface calcInterface = (value1, value2) -> value1.add(value2);
System.out.println(calcInterface.calc(BigDecimal.valueOf(10.1),BigDecimal.valueOf(20.3)));
}
}
この1つの文で表現するために三項演算子を使うことが出来ます。三項演算子は今やExcel関数でも使用されているので説明は不要でしょう。
上記のメソッドにNULLチェックを入れた場合は以下のようになります。
import java.math.BigDecimal;
public class LamdaSample13 {
public static void main(String[] args) {
CalcInterface calcInterface = (value1, value2) ->
(value1 != null && value2 != null) ? value1.add(value2) : BigDecimal.ZERO;
System.out.println(calcInterface.calc(BigDecimal.valueOf(10.1),BigDecimal.valueOf(20.3)));
}
}
ラムダ式に関しては知識を読むよりたくさん書いて覚えたほうがよさそうですね・・・。
【ここが便利】
ラムダ式は様々なメリットがありますが、まずは「記述量が減る」というメリットがあります。
##ForEachメソッドとメソッド参照
Java8はこのラムダ記法を使うことを前提に様々な機能が追加されました。それを今から説明します。
以下のmain内の5つのforループは、いずれも同じ「hello」「world」を改行して出力します。
import java.util.Arrays;
import java.util.List;
public class ForEachSample {
public static void main(String[] args) {
List<String> stringList = Arrays.asList("hello","world");
//通常for文
for(int i=0;i<stringList.size();i++){
System.out.println(stringList.get(i));
}
//拡張for文(Java5から)
for(String s:stringList){
System.out.println(s);
}
//ラムダ式(Java8から) 省略せず
stringList.forEach((String s) -> System.out.println(s));
//ラムダ式(Java8から) 省略
stringList.forEach(s -> System.out.println(s));
//メソッド参照(Java8から)
stringList.forEach(System.out::println);
}
}
「ラムダ式」の2つのコードについては、ここで初めて登場したforEach(この後説明します)を除き説明済みだから大丈夫でしょう。
しかし「メソッド参照とforEach」は理解しにくいです。
「何をprintlnするか、書いてないじゃないか」と思いませんでしたか?私はそう思いました。
まずラムダ式のほうから説明しましょう。ForEachメソッドは、Iterableインタフェースに新たに用意されたメソッドで、用途としては拡張Forループと同じです。
Iterableを実装したオブジェクトの要素を先頭から1つずつ読みます。拡張For文よりコーディング量が少なくなります。そして
str -> System.out.println(str)
というラムダ式のように、
- ラムダ式の処理が1つのメソッドを実行するだけ
- ラムダ式の引数とラムダ式の処理で実行しているメソッドに渡す引数が同じ
この場合、「メソッド参照」を使うことでさらに記述を簡潔にすることができます。
クラス名::メソッド名
で、『forEachで読み込んでいる要素1つ1つに対して、それを使ったメソッドを実行するよ』という意味を持ちます。
stringList.forEach(s -> System.out.println(s));
が、先ほど揚げた2つの条件を満たします。forEachが走査しているただひとつの要素sと、ただ1つを引数として使用しているprintln(s)の"(s)"を省略して
stringList.forEach(System.out::println);
となるのです。値をそのまま別のメソッドに渡すという場合にはメソッド参照が使えます。
たしかに書くのは早くなりそうですが、読む場合にも書く場合にも、慣れが必要だと思われます。
staticメソッドを参照する場合は
クラス名::メソッド名
でメソッド参照を行うことが出来ます。
インスタンスメソッド名を参照する場合は
インスタンスの変数::メソッド名
でメソッド参照を行うことが出来ます。
さらに、コンストラクタも呼び出すことが出来ます。その場合は
クラス名::new
このように記述します。
さらに、配列型のオブジェクトを生成する場合には
配列の要素型[]::new
を使います。
【ここが便利】
「記述量が減る」というメリットがあります。
##Stream API
forEach()でコレクションの各値を取得することが出来ますが、Java8ではこれを使用してとても便利なことのできるAPIが追加されました。それがStream API。百聞は一見に如かず、使ってみました。
@ITラムダ式で本領を発揮する関数型インターフェースとStream APIの基礎知識 (2/3)
ただし1,000,000回ループは10,000回にしてあります。
1,000,000ループでで動かしたところノートPCが異音を出し始めたので恐ろしくて強制終了させました(笑)
import java.util.Calendar;
import java.util.Date;
import java.util.List;
public class SalesData {
private Date date; // 売上日
private int salesAmount; // 売上金額
private String location; // 地域
public SalesData(Date date, int salesAmount, String location){
this.date = date;
this.salesAmount = salesAmount;
this.location = location;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
public int getSalesAmount() {
return salesAmount;
}
public void setSalesAmount(int salesAmount) {
this.salesAmount = salesAmount;
}
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
private static final int[] SALES_AMOUNTS = {
1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000
};
private static final String[] LOCATIONS = {
"東京", "千葉", "埼玉", "神奈川", "茨城", "栃木", "群馬"
};
public static List<SalesData> createSalesDateList() {
List<SalesData> salesDataList = new ArrayList();
// 2014/1/1のDateを生成
Calendar calendar = Calendar.getInstance();
calendar.clear();
calendar.set(2014, 1, 1);
// 10,000日分のデータを作成する
for (int i = 0; i < 10000; i++) {
Date salesDate = calendar.getTime();
// 売上金額ごとのデータを作成する
for (int salesAmount : SALES_AMOUNTS) {
// 地域ごとのデータを作成する
for (String location : LOCATIONS) {
salesDataList.add(new SalesData(salesDate, salesAmount, location));
}
}
calendar.add(Calendar.DAY_OF_MONTH, 1); // 日付を1日加算
}
return salesDataList;
}
}
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
public class StreamAPISample1 {
public static void main(String[] args) {
// サンプルデータの作成
List<SalesData> salesDataList = SalesData.createSalesDateList();
int maxSalesAmount = -1;
long start = System.nanoTime();
for (SalesData salesData : salesDataList) {
// 地域が「東京」のもので絞る
if (salesData.getLocation().equals("東京")) {
// 最大の売り上げを取得する
if (maxSalesAmount < 0) {
maxSalesAmount = salesData.getSalesAmount();
} else if (maxSalesAmount < salesData.getSalesAmount()) {
maxSalesAmount = salesData.getSalesAmount();
}
}
}
System.out.println("maxSalesAmount = " + maxSalesAmount);
long end = System.nanoTime();
System.out.println("process time ="+(end-start));
}
}
コードそのものはかなり長いですが、やっていることは
[2014年1月1日,1000,東京]
[2014年1月1日,1000,千葉]
・・・
[2014年1月1日,1000,群馬]
[2014年1月1日,2000,東京]
・・・
[2014年1月1日,9000,群馬]
[2014年1月2日,1000,東京]
・・・
こういうデータを1,000,000個入れています。(自分の場合は10,000個ですが)
実行結果は以下のようになりました。結果は当然ですよね。
maxSalesAmount = 9000
process time =53388108
これをStream APIを使って書き換えると次のようになります。
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
public class StreamAPISample2 {
public static void main(String[] args) {
// サンプルデータの作成
List<SalesData> salesDataList = SalesData.createSalesDateList();
long start = System.nanoTime();
//Stream APIを使用
int maxSalesAmount = salesDataList.stream()
.filter(salesData -> salesData.getLocation().equals("東京"))
.mapToInt(salesData -> salesData.getSalesAmount())
.max()
.getAsInt();
System.out.println("maxSalesAmount = " + maxSalesAmount);
long end = System.nanoTime();
System.out.println("process time ="+(end-start));
}
}
各行の説明は先のリンクに任せ、このコードには関数型プログラミング言語の特徴が出ています。
関数型プログラミング言語は、『前の関数処理によって得た出力をまた入力として次の処理を行う』という考え方があり、このコードも言葉に直すと
- salesDataList配列をstream型オブジェクトに変換して
- そのstreamオブジェクト型オブジェクトからlocationが東京のものだけにフィルタリングして
- その中から売り上げ量を取得して
- その中の最大値を取得して
- その最大値を左辺であるmaxSalesAmountに格納する
という意味になります。先ほどのStreamAPIを使用したプログラムの結果は以下になります。
maxSalesAmount = 9000
process time =192870693
maxSalesAmountは同じですが、process timeはむしろStream APIの方が遅いです。
これを高速にするために、直列処理であるstreamメソッドではなく並列処理のparallelStreamメソッドを使ってみます。
int maxSalesAmount = salesDataList.parallelStream()
13行目のstreamをparallelStreamに代えただけです。そのうえでそれぞれのケースを5回実行してみました。
import java.util.Calendar;
import java.util.Date;
import java.util.List;
public class StreamAPISample5 {
public static void main(String[] args) {
// テストデータの生成
List<SalesData> salesDataList = SalesData.createSalesDateList();
// 5回実行
for (int i = 1; i <= 5; i++) {
System.out.println(i + "回目 ------------------------------");
// forループの場合
int maxSalesAmount = -1;
long start = System.currentTimeMillis();
for (SalesData salesData : salesDataList) {
if (salesData.getLocation().equals("東京")) {
if (maxSalesAmount < 0) {
maxSalesAmount = salesData.getSalesAmount();
} else if (maxSalesAmount < salesData.getSalesAmount()) {
maxSalesAmount = salesData.getSalesAmount();
}
}
}
long end = System.currentTimeMillis();
System.out.println("forループ: time=" + (end - start) + "ms");
// streamメソッドの場合
maxSalesAmount = -1;
start = System.currentTimeMillis();
maxSalesAmount = salesDataList.stream()
.filter(salesData -> salesData.getLocation().equals("東京"))
.mapToInt(salesData -> salesData.getSalesAmount())
.max()
.getAsInt();
end = System.currentTimeMillis();
System.out.println("stream: time=" + (end - start) + "ms");
// parallelStreamメソッドの場合
maxSalesAmount = -1;
start = System.currentTimeMillis();
maxSalesAmount = salesDataList.parallelStream()
.filter(salesData -> salesData.getLocation().equals("東京"))
.mapToInt(salesData -> salesData.getSalesAmount())
.max()
.getAsInt();
end = System.currentTimeMillis();
System.out.println("parallelStream: time=" + (end - start) + "ms");
}
}
}
1回目 ------------------------------
forループ: time=56ms
stream: time=78ms
parallelStream: time=762ms
2回目 ------------------------------
forループ: time=97ms
stream: time=9ms
parallelStream: time=6ms
3回目 ------------------------------
forループ: time=3ms
stream: time=12ms
parallelStream: time=4ms
4回目 ------------------------------
forループ: time=5ms
stream: time=11ms
parallelStream: time=4ms
5回目 ------------------------------
forループ: time=4ms
stream: time=11ms
parallelStream: time=1ms
最初の1回目は時間を取られていますが、それ以降は直列処理のforループやstreamメソッドよりもparallelStreamの方が速いことが分かります。
10,000回でこのレベルですから、1,000,000回ではもっと著しい違いが出るのでしょう。ただし、
- 対象となる要素が少ない場合は、並列処理の方が遅くなる
- コア数によってパフォーマンスは変わる
- 元データの並び順は保障されない
これはJavaのみならず、並列処理のお約束ですよね。
stream APIは今回紹介したfilter()以外にも、要素が全て条件を満たすallMatch()やいずれか1つ以上が条件を満たすanyMatch()、全ての要素が条件を満たさないnoneMatch()、合計を返すsum(),平均を返すaverage()など便利なメソッドが用意されています。
【ここが便利】
これまではfor文や匿名クラスで実現していたコードを短く書くことが出来ます。
また、parallelStreamを使用することで並列処理が高速化できます。
##Optionalクラス
Optionalクラスは、Nullになる可能性がある値を格納するのに使用します。
なぜか(笑)IT系の人以外にも知名度が高いNullPointerException。通常try-catchしますが比較を忘れて実行時に例外を発生させてしまうケースは起こりえます。
NULLになる可能性のある値をOptionalオブジェクトに格納することで、『値がNULLでない場合』のチェック文を書くことが出来ます。
「それは if (xxx != null) { ・・・}else {} と何が違うのか」
と思うのは当然ですが、ifで囲うとネストが深くなって可読性が下がりますし、そもそもif文が防御的なコードなので、記述量を減らせるものならば減らしたい・・。
Optionalクラスへの値の格納は以下のように行います。
Optional<String> name = Optional.ofNullable("hello");
Optionalから値を取り出す場合は、get()を使用します。
System.out.println(name.get());
この際値を持たないオブジェクトに対してgetメソッドを呼び出すと例外が発生するため、getメソッドを使う時はisPresentメソッドによるチェックが必要です。
isPresentメソッドは、nullが格納されていない場合はtrue,nullが格納されている場合はfalseを返します。
if(name.isPresent()){
System.out.println( name.get() );
}
しかし、これだけではif文でnull判定するのとコーディング量は大差ないですよね。
実はこのOptionalクラスもStream APIの最終的なメソッドの処理結果を受け取る際に使用します。つまりラムダ式で多用されます。ラムダ式を使うと
hogeOpt.ifPresent(hoge -> System.out.println(hoge.length())); // 値が存在する場合のみ実行
このようになります。
Optionalクラスについて更なる情報は、以下のページを参考にしてください。
きしだのはてな:Java8でのプログラムの構造を変えるOptional、ただしモナドではない
Java 8 "Optional" ~ これからのnullとの付き合い方 ~
##インタフェースのデフォルトメソッド
未知との遭遇であったラムダ式とはうってかわり、従来のJavaの知識がある人にとっては驚きの仕様変更がありました。
Java8で最もインパクトのある「言語拡張」とされるインタフェースのデフォルトメソッド。
その意味は「インタフェースにメソッドを実装できる」というこれまでの常識を覆すもの。
インタフェースのメソッド名の前にdefaultキーワードを付与することで、実装したクラスがそのメソッドを実装しない場合、デフォルトで使用されるようになります。
public interface DefaultInterfaceSample {
public int getId();
public default String getName(){
return "匿名希望";
}
}
public class CallDefaultInterfaceSample implements DefaultInterfaceSample{
public static void main(String[] args) {
}
@Override
public int getId() {
return 100;
}
}
気になるのは当然、インタフェースは多重継承が可能であり(だからこそインタフェースのメソッドは実装が出来なかったわけで)そこはどうなっているのか。
メソッド名と引数の型が同じメソッドを持つ別々のインターフェースを継承しようとすると、コンパイルエラーになります。オーバーライドが必要です。
「インタフェースにメソッドが定義できるウレシイヤッター!」で終わりだとあんまりなので、なぜ今更にインタフェースのメソッド定義が可能になったのかを調べてみました。
参考にしたのは以下のページです。ありがとうございます。
Yuji Blog:Java8新機能 ラムダ式とデフォルトメソッドの導入理由
ギークを目指して:Java8のインタフェース実装から多重継承とMixinを考える
ラムダ式(というか並列処理)のためにCollection InterfaceにforEachとかを追加したけど、そのままだとCollectionを実装している各クラスでその処理を実装する必要がでてくる。
そうなると、下位互換もできなくなってしまうため、その解決方法としてデフォルトメソッドが追加された
なるほど分かりやすい。インタフェースのデフォルトメソッドもラムダ式と関係があったのですね。
【ここが便利】
Javaでも多重継承が可能になりました。これにより柔軟な実装が出来るようになりました。
##新Time API
Java 8では、それより前まで日時を扱っていた「java.util.Date」「java.util.Calendar」などとは異なる、新しい日時を扱うAPIが追加されました。
java.util.Dateやjava.util.Calendar、java.text.DateFormatなどのクラスは時間が経つと共に開発の手法や運用の環境が変わっていったため、さまざまな要求や問題が出てきました。
時刻のみや日付のみを扱えなかったのも問題ですが、並列処理のこの時世の最大の問題点は旧来の時間系クラスはスレッドセーフではないため、マルチスレッドの環境でそれらのクラスが扱われる場合、実行時に意図した結果にならない場合がある点でした。
新Date-Time APIで用意されたクラスは生成後に状態が変わらないImmutableなクラスになっており、スレッドセーフになっています。
新TimeAPIを使用してプログラムを書いてみました。
import java.time.LocalDateTime;
import java.time.Month;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.chrono.JapaneseDate;
import java.time.chrono.JapaneseEra;
public class DateTimeAPISample {
public static void main(String[] args){
//タイムゾーンやUTCから時差の設定がない西暦の日付と時刻を扱うクラス
//(年、月、日、時、分、秒、ナノ秒)
LocalDateTime localDateTime = LocalDateTime.of(2015, Month.MAY, 10, 15, 35, 55);
System.out.println("localDateTime=" + localDateTime);
//UTC(世界標準時)からの時差つきの日付時刻をあらわすクラス
//それぞれ2桁の数字を使って表現。日本はUTCから+9時間の時差
OffsetDateTime offsetDateTime1 = OffsetDateTime.of(localDateTime, ZoneOffset.of("+09:00"));
System.out.println("offsetDateTime1=" + offsetDateTime1);
//java.time.chronoパッケージが西暦以外のカレンダーにも対応。これは日本暦
JapaneseDate japaneseDate = JapaneseDate.of(JapaneseEra.HEISEI, 27, 5, 5);
System.out.println("japaneseDate="+japaneseDate);
//タイムゾーンとして指定した地域での、夏時間などの補正も含めた日付時刻を扱うクラス
ZonedDateTime zonedDateTimeOfRegion = ZonedDateTime.of(2014, 3, 9, 1, 59, 0, 0, ZoneId.of("America/Los_Angeles"));
System.out.println("zonedDateTimeOfRegion="+zonedDateTimeOfRegion);
}
}
実行結果は以下のようになりました。
localDateTime=2015-05-10T15:35:55
offsetDateTime1=2015-05-10T15:35:55+09:00
japaneseDate=Japanese Heisei 27-05-05
zonedDateTimeOfRegion=2014-03-09T01:59-08:00[America/Los_Angeles]
LocalDateTimeクラス、ZonedDateTimeクラス、OffsetDateTimeクラスはそれぞれが変換するメソッドが用意されていますが、一方で旧来のjava.util.Dateとの変換はかなり面倒と聞きます。
これまで紹介したクラスは日付と時刻を持つクラスでしたが、日付だけを持つDateクラス、時刻だけを持つTimeクラスもあります。
新DateAPIは日付、時間に関する便利なメソッドを持っています。月や曜日を取得する便利なenum(列挙型)も用意されています。
これから書くプログラムは(Java1.8以降の場合は)、新TimeAPIを使うようにしましょう。
【ここが便利】
時間に関するプログラムが便利に、安全になりました。
いかがだったでしょうか。
駆け足&ざっくりな感じで、Java5からJava8までの追加機能について説明してきました。
便利な機能が追加されていたり、旧来のDateTimeは今では非推奨(代替のクラスが存在する)だったりと、すっかり浦島太郎状態でした。
しかし問題は、企業が長くサポートしているサービス・システムのJavaのバージョンが古いままで、せっかくの新機能も使用できない、という点だったりして・・・。