概要
本記事ではOptionalのApple公式ドキュメントの概要に掲載されている項目を元にして、SwiftのOptional型とJava 8のOptional型を比較します。若干今更感のある内容にはなりますが個人的な備忘録も兼ねて投稿に至りました。
記事の最後にはそれぞれの記法の比較表も掲載しています。あわせてご覧ください。
なお本記事に掲載してあるスニペットは多少簡略化されているので、そのままコピペしても動かないものがあります。ご了承ください。
Nil結合演算子(Nil-Coalescing Operator)
Swift
中身があるならそれを、なければ指定したデフォルト値を返すようにOptinalをアンラップします。??という演算子を使います。複数のOptional型をオペランドとしてつなぐこともできます。
let wrappedValue: String? = nil
let defaultValue = "default"
let value = wrappedValue ?? defaultValue
print(value)
// "default"
let wrappedValue: String? = nil
let defaultValue = "default"
let otherWrappedValue: String? = nil
// 右オペランドがOptionalである限り??でつなぐことができる
let value = wrappedValue ?? otherWrappedValue ?? defaultValue
print(value)
// "default"
Java 8
Java 8では演算子ではなくorElse、orElseGetというメソッドです。orElseではデフォルト値そのものを、orElseGetではデフォルト値を提供するSupplier<? extends T>型のオブジェクトを渡します。しかしいずれのメソッドもラップしている型を返すので、SwiftのようにorElseで複数のOptional型をつなぐことはできません。
Optional<String> wrappedValue = Optional.empty();
String defaultValue = "default";
String value = wrappedValue.orElse(defaultValue);
System.out.println(value);
// "default"
String otherValue = wrappedValue.orElseGet( () -> defaultValue );
System.out.println(otherValue);
// "default"
Optional<String> otherWrappedValue = Optional.of("other");
// これはできない
// String unwrappedValue = wrappedValue.orElse(otherWrappedValue).orElse(defaultValue);
短絡評価
Swift
SwiftのNil結合演算子は短絡評価します。したがって左オペランドがnilでない限り右オペランドが評価されることはありません。
func hoge() -> String {
print("called")
return "hoge"
}
let value = nil ?? hoge()
print(value)
// "called"
// "hoge"
// 短絡評価なのでhoge()は呼ばれない
let value = "non nil" ?? hoge()
print(value)
// "non nil"
Java 8
Java 8のorElseGet も短絡評価ですが、orElseはしません。デフォルト値そのものを渡しているので当然と言えば当然ですね。
public String hoge() {
System.out.println("called");
return "hoge";
}
String value = Optional.<String>empty().orElseGet(this::hoge);
System.out.println(value);
// "called"
// "hoge"
// 短絡評価なのでhoge()は呼ばれない
String value = Optional.of("non null").orElseGet(this::hoge);
System.out.println(value);
// "non null"
// orElseは短絡評価しない
String value = Optional.of("non null").orElse(hoge());
System.out.println(value);
// "called"
// "non null"
使い分けについてですが、デフォルト値生成にコストがかかる場合などには短絡評価するorElseGetを使うと良いでしょう。
public String fetchValue() {
// 時間のかかる処理
return result;
}
// △ - 必ずfetchValue()が呼ばれるので少なくともパフォーマンスに影響
String value = wrappedValue.orElse(fetchValue());
// ◯ - 短絡評価なので中身があれば呼ばれない
String value = wrappedValue.orElseGet(this::fetchValue);
// △ - 中身があると無駄なオブジェクト生成となる
Person person = wrappedPerson.orElse(new Person());
// ◯ - 短絡評価なので中身があればオブジェクトは生成されない
Person person = wrappedPerson.orElseGet(Person::new);
強制アンラップ(Unconditional Unwrapping)
Swift
中身があるかどうかにかかわらず値をアンラップします。演算子!を使います。もしnilであった場合は実行時エラーを引き起こします。
let number = Int("42")!
print(number)
// 42
Java 8
Java 8ではgetメソッドを使います。もしnullであった場合は非検査例外であるNoSuchElementExceptionが投げられます。
※ なお、intFromStringは説明の便宜上Optional<Integer>型を返していますが、通常整数を包む場合はOptionalInt型を使う方が良いでしょう。
public Optional<Integer> intFromString(String string) {
try {
return Optional.of(Integer.parseInt(string));
} catch (NumberFormatException ignored) {
return Optional.empty();
}
}
int number = intFromString("42").get();
System.out.println(number);
// 42
変換
Swift
値を変換します。mapというメソッドを使います。中身があれば与えられた(Wrapped) -> U型のクロージャに従って値を変換し、nilであればnilのまま流します。したがってOptional型を返します。
let intString: String? = "42"
let percentage: String? = intString.map { "\($0)%" }
print(String(describing: percentage))
// Optional("42%")
クロージャ内でOptional型を返したいときは、(Wrapped) -> Optional<U>型のクロージャを引数にとるflatMapメソッドを使います。
let intString: String? = "42"
// Int(String)はInt?を返す
let integer: Int? = intString.flatMap { Int($0) }
print(String(describing: integer))
// Optional(42)
Java 8
Java 8でも同様にmap、flatMapというメソッドを使います。使い分けも同様です。
Optional<String> intString = Optional.of("42");
Optional<String> percentage = intString.map( str -> str + "%" );
System.out.println(percentage);
// Optional[42%]
Optional<String> intString = Optional.of("42");
Optional<Integer> integer = intString.flatMap(this::intFromString);
System.out.println(integer);
// Optional[42]
オプショナルバインディング(Optional Binding)
Swift
安全なアンラップ手段として馴染み深いことと思います。ifやguardといったキーワードと併せることで、中身がある場合とない場合とで処理を分岐することができます。
let wrappedValue: String? = "value"
if let value: String = wrappedValue {
print(value)
} else {
print("no value")
}
// "value"
let wrappedValue: String? = "value"
guard let value: String = wrappedValue else {
print("no value")
return
}
print(value)
// "value"
Java 8
Java 8ではifPresentというメソッドを介して安全にアンラップすることができます。Consumer<? super T>型のオブジェクトを引数にとり、その中に中身があった場合の処理を記述します。
Optional<String> wrappedValue = Optional.of("value");
wrappedValue.ifPresent( str -> {
System.out.println(str);
});
// "value"
しかしながら中身がない場合の処理を記述できるメソッドはありません。したがって両方の処理を記述したい場合はisPresentを使って中身の有無を確認する方法をとる必要があります。ただし、中身がある場合明示的に強制アンラップgetを記述しなければいけないためSwiftと比較すると冗長です。
Optional<String> wrappedValue = Optional.of("value");
if (wrappedValue.isPresent()) {
// 冗長だが明示的に強制アンラップする必要がある
String value = wrappedValue.get();
System.out.println(value);
} else {
System.out.println("no value");
}
// "value"
Optional<String> wrappedValue = Optional.of("value");
// カード節的に記述する
if (!wrappedValue.isPresent()) {
System.out.println("no value");
return;
}
// 冗長だが明示的に強制アンラップする必要がある
String value = wrappedValue.get();
System.out.println(value);
// "value"
Optional<String> wrappedValue = Optional.of("value");
wrappedValue.ifPresent( str -> {
System.out.println(str);
})// .orElse( () -> { // これはできない
// System.out.println("no value");
// })
;
またifPresentに渡す処理のスコープはラムダ式内であるため、その処理内で呼び出し元のメソッドのリターンはできません。したがって早期リターンなどを行いたい場合はisPresentを使った方法をとる他ありません。
public boolean foo(Optional<String> wrappedValue) {
// 値があるなら早期リターンする
if (wrappedValue.isPresent) {
String str = wrappedValue.get();
// 何かする
return true;
}
// 値がない場合の処理
return result;
}
public boolean foo(Optional<String> wrappedValue) {
// 値があるなら早期リターンする
wrappedValue.ifPresent( str -> {
// 何かする
// return true; // スコープがこのラムダ式内にあるため、これはできない
});
// 値がない場合の処理
return result;
}
オプショナルチェイニング(Optional Chaining)
Swift
中身があるときのみプロパティやメソッドへのアクセスを行います。後置演算子?を使います。
let wrappedValue: String? = "value"
let uppercased: String? = wrappedValue?.uppercased()
print(String(describing: uppercased))
// Optional("VALUE")
class Hoge {
func hoge() { print("hoge") }
}
let wrappedValue: Hoge? = Hoge()
wrappedValue?.hoge()
// "hoge"
Java 8
Java 8では先に紹介したmapメソッドが使えます。対象がメソッドであればメソッド参照を用いて簡潔に書くことができます。ただし、対象がOptional型を返す場合にはflatMapを使う必要があります。
Optional<String> wrappedValue = Optional.of("value");
Optional<String> uppercased = wrappedValue.map(String::toUpperCase);
System.out.println(uppercased);
// Optional[VALUE]
class User {
final String id;
Optional<String> mail = Optional.empty();
User(String id) { this.id = id; }
}
Optional<User> wrappedUser = Optional.of(new User("user1"));
Optional<String> mail = wrappedUser.flatMap( user -> user.mail );
System.out.println(mail);
// Optional.empty
使い分けを含め変換のときと全く同じ記法ですね。Java 8ではラップしている値のフィールドやメソッドにアクセスするためだけの特別な記法やメソッドが用意されているわけではありません。
値を返さないメソッドへのアクセスは先に紹介したifPresentを使います。
class Hoge {
void hoge() { System.out.println("hoge"); }
}
Optional<Hoge> wrappedValue = Optional.of(new Hoge());
wrappedValue.ifPresent(Hoge::hoge);
// "hoge"
まとめ
本記事ではSwiftでのOptional型の扱いを元に、Java 8とOptional型の比較を行いました。以下にそれぞれの記法の比較表を掲載します。特にオプショナルバインディングとオプショナルチェイニングについては両者の違いがよくあらわれています。Java 8のオプショナルバインディングでは分岐と同時にアンラップできない分Swiftより少し冗長です。またJava 8にはSwiftの?演算子のようなラップしている値にアクセスするための専用手段は用意されていません。
| Swift | Java | |
|---|---|---|
| Nil結合演算子 | ?? |
orElse ※ 非短絡評価 orElseGet
|
| 強制アンラップ | ! |
get |
| 変換 |
map flatMap
|
map flatMap
|
| オプショナルバインディング |
if let guard let
|
ifPresent もしくは isPresentで分岐した後にgetで強制アンラップ |
| オプショナルチェイニング | ? |
Optional型を返すならflatMap それ以外を返すなら map 値を返さないなら ifPresent
|
もし何か間違いなどございましたらコメントにてご指摘ください。
オマケ - Java 9のOptional
ここではJava 9で追加されたOptionalのメソッド、orとifPresentOrElseを紹介します。Java 8の話は出てきません。完全なオマケです。
or
中身がない場合に与えられたSupplier<? extends Optional<? extends T>>型のオブジェクトを実行してOptional型のデフォルト値を得ます。これはちょうどSwiftのNil結合演算子で右オペランドにOptional型を持ってきた場合と対応しています。
また短絡評価をするため、中身がある限り与えられたSupplierオブジェクトが実行されることはありません。
let wrappedValue: String? = nil
let defaultValue = "default"
let otherWrappedValue: String? = nil
let value = wrappedValue ?? otherWrappedValue ?? defaultValue
print(value)
// "default"
// available Java 9 or later
Optional<String> wrappedValue = Optional.empty();
String defaultValue = "default";
Optional<String> otherWrappedValue = Optional.empty();
// Java 9以降ならデフォルト値にOptional型を指定できる
String value = wrappedValue.or( () -> otherWrappedValue ).orElse(defaultValue);
System.out.println(value);
// "default"
ifPresentOrElse
中身がある場合とない場合とで処理を分岐することができます。中身がある場合にはConsumer<? super T>型のオブジェクトが、ない場合にはRunnable型のオブジェクトが実行されます。nullだった場合の処理も記述できるので、Java 8と比べるとSwiftのオプショナルバイディングにかなり近づいています。
let wrappedValue: String? = "value"
if let value = wrappedValue {
print(value)
} else {
print("no value")
}
// "value"
// available Java 9 or later
Optional<String> wrappedValue = Optional.<String>empty();
wrappedValue.ifPresentOrElse( str -> {
System.out.println(str);
}, () -> {
System.out.println("no value");
});
// "no value"