Javaにはデフォルト引数の機能がないため、それに取って代わる実装方法についてまとめてみました。
TL;DR
以下のような方針で実装していれば問題ないと思います。
デフォルトにしたい引数の個数 | 解決方法 |
---|---|
~ 4 | オーバーロード |
5 ~ | Builderパターン |
デフォルト引数とは
この記事でいうデフォルト引数とは、メソッドを呼び出す際に実引数として値が未設定であれば、
代わりにデフォルト値で初期化する仕組みのことです。
(最近の言語ではほとんど導入されている構文だと思います)
以下、Groovyで書いたデフォルト引数のサンプルです。
def foo(a = 0, b = "undefined") {
println "a = $a, b = $b"
}
foo(23, "hoge") // => a = 23, b = hoge
foo() // => a = 0, b = undefined
Javaでの実装方法
※以降、Java11で動作確認しています。
1. オーバーロード | デフォルト引数の数が少なければ
最もよく見るパターンで手軽に実装できます。
static void foo(int a, String b) {
System.out.println("a = " + a + ", b = " + b);
}
static void foo(int a) { foo(a, "undefined"); } // 引数bだけデフォルト引数
static void foo(String b) { foo(0, b); } // 引数aだけデフォルト引数
static void foo() { foo(0, "undefined"); } // 引数a, bの両方ともデフォルト引数
public static void main(String[] args) {
foo(23, "hoge"); // => a = 23, b = hoge
foo(23); // => a = 23, b = undefined
foo("hoge"); // => a = 0, b = hoge
foo(); // => a = 0, b = undefined
}
サンプルでは、foo(int a, String b)
で宣言されている引数がそれぞれ違う型同士のため、
オーバーロードによって全ての引数にデフォルト値を設定できています。
仮に引数aとbが同じ型であった場合、「引数aだけデフォルト引数」と「引数bだけデフォルト引数」を同時に宣言することはできません。
その場合、後述のBuilderパターンかNull渡しで実装するしかありません。
###(補足)可変長引数 | 配列型の仮引数が未指定の場合、空配列のデフォルト値が入る
可変長引数となる部分はメソッド引数の一番最後になる制約がありますが、
一番最後の引数に何も指定しなかった場合、自動的に空配列(length = 0)が渡ることになります。
そのため、そういったケースであれば、わざわざオーバーロードする必要はありません。
static void foo(String a, int... b) {
int bSum = IntStream.of(b).sum();
System.out.println("a = " + a + ", sum = " + bSum);
}
public static void main(String[] args) {
foo("hoge", 23, 42); // => a = hoge, sum = 65
foo("hoge"); // => a = hoge, sum = 0
}
2. Builderパターン | 多岐に使用可。ただ、クラス作成する手間が...
オーバーロードのほかによく使用されるパターンでBuilderパターンを用いてデフォルト引数を設定する方法があります。
オーバーロードは手軽にデフォルト値を設定できるのでいいですが、引数の数が多くなってしまうと
Builderパターンに置き換えたほうがコードが読みやすくなります。
閾値としては引数の数が4つ以内が望ましいとSonarQubeのルールで書いてありました。
Java: Methods should not have too many parameters | SonarQube - Rules
サンプルでは引数2個のメソッドに対してBuilderパターンでデフォルト値を設定するように書いています。
import lombok.Builder;
import lombok.Value;
public class BuilderPattern {
static void foo(Foo foo) {
System.out.println("foo = " + foo);
}
public static void main(String[] args) {
var specifiedFoo = Foo.builder().a(23).b("hoge").build();
var defaultFoo = Foo.builder().build();
foo(specifiedFoo); // => foo = Foo(a=23, b=hoge)
foo(defaultFoo); // => foo = Foo(a=0, b=undefined)
}
}
@Value
@Builder
class Foo {
@Builder.Default
Integer a = 0;
@Builder.Default
String b = "undefined";
}
###(補足)Map型 | これをするならBuilder使って欲しい
MapでもBuilderパターンと同じようなことはできますが、これをするのであればBuilderで書いた方がいいと思ってます。
static void foo(Map<Foo, Object> params) {
var a = (int) params.getOrDefault(Foo.A, 0);
var b = (String) params.getOrDefault(Foo.B, "undefined");
System.out.println("a = " + a + ", b = " + b);
}
public static void main(String[] args) {
foo(Map.of(Foo.A, 23, Foo.B, "hoge")); // => a = 23, b = hoge
foo(Map.of()); // => a = 0, b = undefined
}
enum Foo {
A, B
}
3. Null渡し | デフォルト値を設定する引数が1番目だけだったり2番目だけだったりする場合
オーバーロードの解説で書いたように、宣言されている引数がそれぞれ同じ型同士の場合、
引数それぞれにデフォルト値を設定できないケースがありました。
その回避策として、メソッド内でデフォルト値を設定する方法があります。
ただ、メソッドのシグネチャだけでどの変数にデフォルト値が設定されているのか判断できないため、
メソッド利用者はJavadocを見るか、メソッドの実装を見る必要があり、あまり使いたくないパターンではあります。
static void foo(String a, Integer b, Integer c) {
b = Objects.requireNonNullElse(b, 0);
c = Objects.requireNonNullElse(c, 0);
System.out.println("a = " + a + ", b = " + b + ", c = " + c);
}
public static void main(String[] args) {
foo("hoge", 23, 42); // => a = hoge, b = 23, c = 42
foo("hoge", null, null); // => a = hoge, b = 0, c = 0
}
###(補足)Optional型 | ダメ。ゼッタイ。
前述のNull渡しパターンの問題点として
「メソッドのシグネチャだけでどの変数にデフォルト値が設定されているのか判断できない」とあり、
「あれ、もしかしてOptional型が使えるのでは?」と思ってしまいがちですが、これは結構批判を浴びる書き方となるようです。
というとも、Optional型は本来メソッドから返却される値がnullかもしれないことを呼び出し側に伝えるための仕組みになるため、
引数にOptional型を使ってしまうとコードが余計に冗長になったり、意味がないコードが増えたりしてしまいます。
よって、下のような書き方は厳禁です。
static void foo(String a, Optional<Integer> bOpt) {
var b = bOpt.orElse(0);
System.out.println("a = " + a + ", b = " + b);
}
public static void main(String[] args) {
foo("hoge", Optional.of(23)); // => a = hoge, b = 23
foo("hoge", Optional.empty()); // => a = hoge, b = 0
foo("hoge", null); // => NullPointerException
}
※Optional型の詳細を書くと本題から外れてしまうので、必要であればググってください。m(_ _)m
参考文献
Java optional parameters | Stack Overflow
Java: Methods should not have too many parameters | SonarQube - Rules
Why should Java 8's Optional not be used in arguments | Stack Overflow