いやー、やっと Java8 が正式リリースになりましたね。 せっかくなので拙者もそれに関連しそうな記事を書こうかと。 ただし、あんまり直接関係するわけではないですが。
Java には実質的に仕様と言ってもいいような「プロパティ」というものがありますね。 これは、もともとはクラスにフィールドを定義する際に、フィールド自体はプライベートにして直接アクセス出来ないようにし、代わりに getter/setter (もしくは mutator/accessor)と呼ばれるメソッドを定義してそれらを介してフィールドにアクセスするようにしよう設計です。 プロパティはこれを少し進めて、たとえフィールドがクラスに定義されてなくても、getter/setter があればあたかもフィールドを持っているクラス設計かのように見做そう、みたいな話です。 このプロパティは JavaBeans の定義の一部にも入ってるし*1、標準 API 内の java.beans パッケージ内のクラスなどを使って操作できるし、大抵の IDE では getter/setter を自動生成できるし、Groovy などではクラスにフィールドを定義すると暗黙のうちに getter/setter メソッドを定義してプロパティにしてしまったりもします。
さて、このプロパティってのは1つのオブジェクトをフィールドとして持つかのようなクラス設計に対してのものですが、同様にして(同型の)複数のオブジェクトを保持したい場合に集約プロパティというものを考え、getter/setter メソッドのような標準的なアクセスメソッドとしてどんなものがあると最低限の処理ができそうかな、というの考えたいと思います。 えぇ、Java8 とか直接関係ありません。 Java8 から導入された java.util.Stream や java.util.function パッケージの型を少々使いますよ、って程度です。
関数型言語ではモナド理論がどうとか、filter, map, flatMap がどうとかという話を持ち出してきて、これらのメソッドが定義されてれば必要充分だってな話ができるんでしょうけど、Java8 ではラムダ式が導入されて関数型プログラミングが行いやすくなったとは言え、状態や副作用のあるなしをコードから簡単に区別できないので、状態ありきのクラス設計でいきます。 accessor だけじゃなく mutator も考えますよ、ということ。
集約プロパティにアクセスするメソッド ドラフト
集約プロパティにアクセスするメソッドを考えていきたいんですが、あれやこれやと機能を突き詰めていくと(集約の仕方、用途によって)List や Set みたいになるだろうということで(そしてむやみにメソッド数を増やすと実装が面倒なので)、簡単に実装できそうなメソッドを数個ずつ3段階(必須・オプショナル・お試し)に分けて観ていきましょう。 選定は特に何らかの根拠・裏打ちがあるって訳ではありませんが、データベースの
CRUD (Create/Read/Update/Delete) を元にしてみました:
CRUD |
必須 |
オプショナル |
お試し |
Create (add) |
addElement(E) |
addElements(Collection<E>) |
addElements(E[]) addElements(Supplier<E>, int) addElements(IntFunction<E>, int) |
Read (get/forEach) |
forEachElement(Consumer<E>) |
getElements() |
elementStream() |
Update (set) |
- |
setElements(Collection<E>) |
mapElement(UnaryOperator<E>) |
Delete (remove) |
removeElements(Predicate<E>) |
removeElement(E) clearElements() |
- |
- E は集約プロパティの要素の型です。 通常、メソッド引数に E のコレクションを指定する場合、Collection<? extends E> や Collection<? super E> のような型を指定すべきですが(PECS もしくは Get&Put 原則*2)、宣言が長くなるので、今回はとりあえずおいておきます*3。
- メソッド名に付けている Element は、1つの(コンテナ)クラスに集約プロパティが複数種ある場合に区別するためのものです。 1種類の集約プロパティしかない場合は付けなくてもいいんじゃないでしょうかね*4。 付けない場合は addElements() などは addAll() などとしておくのが標準 API に続く慣習です*5。
- 各メソッドの返り値は以下のメソッド別の項目の箇所で見ていきます。
これらのメソッドの他には、要素数を返す
- size() / getElementCount()
というメソッドも必要そうだけど、これは実装とかが大して難しくなさそうなので以下では考えません。
必須
まずは集約プロパティにアクセスするために必須のメソッド群。
CRUD |
メソッド |
返り値 |
Create |
addElement(E) |
void (or boolean) |
Read |
forEachElement(Consumer<E>) |
void |
Update |
- |
- |
Delete |
removeElements(Predicate<E>) |
Collection<E> |
CRUD を元にするとか言って、早速 Update に対応するものを挙げてないんですがそのあたりはご愛敬w
各メソッドを見ていく前に、まずこれらの使い方を見てみましょう。 ラムダ式に慣れてれば大して難しくありません、慣れてれば。 例えば Number オブジェクトを、順序を保ったまま List のように格納する NumberContainer クラスを作成したとして、以下のように使えるようにします:
NumberContainer<Integer> ints = new NumberContainer<>();
IntStream.range(0, 10).forEach( i ->
ints.addNumber(i)
);
ints.forEachNumber( i -> System.out.print(i) )
System.out.println();
ints.removeNumbers( i -> i % 2 != 0 );
ints.forEachNumber( System.out::print )
System.out.println();
NumberContainer 型の ints が Integer オブジェクトを集約プロパティとして持ちます。 この例では要素(Integer オブジェクト)を追加・列挙・削除しています。 では、各メソッドを見ていきましょう。
addElement(E) メソッド
これは引数のオブジェクトを集約プロパティに追加するメソッドです。 まぁ基本的な動作は問題ないかと。 ただし
返り値の型はいくつか選択肢があります。
- boolean : Java 標準 API では Collection#add() の返り値は boolean になっているので、それに合わせておく。 この返り値はコレクションの要素が変化した場合に true を、変化しなかった場合には false を返すという仕様です。 要素が変化しない場合というのは、例えば Set オブジェクトに既に保持している要素を加えたときなどです。
- void : 集約プロパティの要素を List のように保持する場合には同じオブジェクトでも常に要素が追加されるので、特に要素が変化したかどうかを条件分岐する必要がありません。 このように、必要がないなら返り値を void にしておいた方が実装は簡単かと思います。
- Optional<E> : Java8 から java.util パッケージに追加された Optional オブジェクトを使うこともできます。 Optional はオブジェクトのラップみたいなもので、null の代わりに空の Optional (Optional#empty() で取得可)があって、NullPointerException を投げずにあれこれ処理を行えるメソッドが定義されています(詳しくは JavaDoc などを参照)。 ただし、addElement() メソッドによって追加されなかったオブジェクトを取得したい場合は、標準 API の Collection#add の返り値の boolean と意味が逆っぽくなるのでご注意を。
さて、このメソッドの簡単な実装を見てみましょう。 作るのは上記で見た、Number のサブタイプを集約プロパティとして保持する NumberContainer クラスです。 集約プロパティの要素は List として保持することにし、PECS は無視してます:
public class NumberContainer<E extends Number>{
private final List<E> numbers = new LinkedList<>();
public boolean addNumber(E e){
return this.numbers.add(e);
}
}
返り値は boolean にしてます。 まぁ、しょーもない実装。 後からどんどんメソッドを追加していきます。
forEachElement(Consumer<E>) メソッド
Java8 で導入された
ラムダ式によって最もよく使い、また強力になるのはこの forEach
Element() メソッドでしょう。 もし Java7 以前で集約プロパティの値を取得・列挙したい場合は要素を何らかのコレクション(大抵 List か Set でしょうけど)に入れて返すことになります。 この場合、セキュリティのために防御的コピーを行う必要があります
*6。 しかし、
ラムダ式と forEach() メソッドを使えばその必要がなくなります。 コレクションの
次の段階、next generation と言っていいかと思います
*7。 引数には、要素を受け取り返り値を返さない関数型である
Consumer 型を指定します。
実装の仕方はいろいろあると思いますが、せっかく Java8 もリリースされたことだし、なるべく Stream を使ってやってみましょう:
import java.util.function.Consumer;
public class NumberContainer<E extends Number>{
private final List<E> numbers = new LinkedList<>();
public boolean addNumber(E e){ ... }
public void forEachNumber(Consumer<E> consumer){
this.numbers.stream().forEach(consumer);
}
}
コレクションから Stream オブジェクトを生成するのにどの程度コストがかかるのか分かりませんが、今後どんどん最適化されていくでしょうから、素直に Stream 使うのが無難かと。 for 文を使うなら
public void forEachNumber(Consumer<E> consumer){
for(E e : this.numbers){
consumer.accept(e);
}
}
みたいな感じですかね。
removeElements(Predicate<E>) メソッド
必須メソッドの最後は要素を削除する remove
Elements() メソッド。 個人的にはこの削除メソッドが Java8 を使って一番すっきり書けるようになったんではないかと思います。 Java7 までのコレクション
API では、要素を削除するメソッド remove() に削除したいオブジェクトを指定する必要がありました。 この場合、String のように
リテラルが定義されている型の削除は特に問題ないんですが、そうでない型の場合には「削除したいオブジェクトを取得する」という、なんとなく虚しさ(と思ってるのは拙者だけ?)作業が必要でした。 Java8 で導入された
Predicate 型を使えばこの虚しさをなくすことができます。 Predicate 型は要素を受け取り boolean 値を返す関数型で、フィルターの役目をします
*8。
オブジェクトを直接指定して削除しない場合、削除したオブジェクトを参照できるようにそれらのオブジェクトを返り値として返しておく方がいいでしょう。 使用するコレクションの型は List や Set など、適切な型にしておいていいと思います。
Stream をなるべく使った実装はこんな感じ:
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class NumberContainer<E extends Number>{
private final List<E> numbers = new LinkedList<>();
public boolean addNumber(E e) { ... }
public void forEachNumber(Consumer<E> consumer){ ... }
public List<E> removeNumbers(Predicate<E> predicate) {
List<E> removes = this.numbers.stream()
.filter(predicate)
.collect(Collectors.toList());
this.numbers.removeAll(removes);
return removes;
}
}
- Stream#filter() メソッドとメソッド引数の Predicate オブジェクトによって、削除する要素を取得
- Stream#collect() メソッドと Collectors#toList() で返されるコレクタによって、Stream オブジェクトを List に変換
という処理を最初にしています。 このメソッドの実装は Stream を使わずに書こうとするともっと面倒になりそうです(言うほど複雑にはなりませんが)。
実装まとめ(必須版)
結局、必須メソッドをまとめた実装はこんな感じになります:
import java.util.List;
import java.util.LinkedList;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class NumberContainer<E extends Number>{
private final List<E> numbers = new LinkedList<>();
public boolean addNumber(E e) {
return this.numbers.add(e);
}
public void forEachNumber(Consumer<E> consumer){
this.numbers.stream().forEach(consumer);
}
public List<E> removeNumbers(Predicate<E> predicate) {
List<E> removes = this.numbers.stream()
.filter(predicate)
.collect(Collectors.toList());
this.numbers.removeAll(removes);
return removes;
}
}
これくらいのコード量なら、集約プロパティを持たせたいクラスそれぞれに実装するのも不可能ではないかと。 面倒さはあるでしょうけど。
オプショナル
さて、次はオプショナルなメソッド。 よく使いそうなものは定義しておくと便利かもね、的なメソッドです。
CRUD |
メソッド |
返り値 |
Create |
addElements(Collection<E>) |
void (or boolean) |
Read |
getElements() |
Collection<E> |
Update |
setElements(Collection<E>) |
void |
Delete |
removeElement(E) clearElements() |
Optional<E> void |
これらを使ったコードはこんな感じになります:
NumberContainer<Integer> ints = new NumberContainer<>();
ints.addNumbers(Arrays.asList(0, 1, 2, 3));
for(Integer i : ints.getElements()){
System.out.print(i);
}
System.out.println();
ints.setNumbers(Arrays.asList(0, 1, 2, 3, 4, 5));
ints.forEachNumber( System.out::print )
System.out.println();
ints.removeNumber(5);
ints.forEachNumber( System.out::print )
System.out.println();
ints.clearNumbers();
これらの処理は必須メソッドを使って行うことも出来ますが、よく使う処理は別途定義しておくと楽。 ではそれぞれのメソッドを見ていきましょう。
addElements(Collection<E>) メソッド
これは複数の要素をまとめて追加したいときに使うメソッド。 Collection#addAll() に対応するものですね。 引数の Collection 型は場合によって List や Set に限定しておいていいと思います。 返り値は add
Element() の場合と同じくいくつかの候補があります:
- boolean : 集約プロパティの要素が変更された場合に true、そうでない場合に false を返す
- void : 要素の変更を知る必要がないなら void で充分
- Collection<E> : 追加した要素、もしくは追加されなかった要素を知りたい場合はそれらを返す。 addElement() のときの Optional<E> にあたるもの。
要素をコレクションで保持するなら実装は簡単:
public class NumberContainer<E extends Number>{
private final List<E> numbers = new LinkedList<>();
public boolean addNumbers(List<E> es){
return this.numbers.addAll(es);
}
}
必須メソッドの addElement() メソッドだけを使って実装することもできますが、boolean を返す場合には見た目がちょっと複雑に:
public boolean addNumbers(List<E> es){
boolean modified = false;
for(E e : es){
modified |= addNumber(e);
}
return modified;
}
ちょっとやってみただけ。
getElements() メソッド
場合によっては要素をコレクションに格納して取得したい場合もあるかもしれません。 よく使うなら
防御的コピーを行った get メソッドを定義しておくのもアリかと。 返り値のコレクションは List や Set にしておいても良いでしょう。
各要素を列挙して処理したい場合に、1つ forEachElement() メソッドが使いにくいことがあります。 それは各要素に対する処理が例外を投げる場合です*9。 この例外処理が定型なら forEach に組み込んでおけばいいんですが、メソッド使用者に例外処理を書かせる場合には getElement() でコレクションを返してしまう方が楽な場合もあります。 場合by場合ですが。
実装はまぁ、普通の防御的コピーで:
public class NumberContainer<E extends Number>{
private final List<E> numbers = new LinkedList<>();
public boolean addNumbers(List<E> es){ ... }
public List<E> getNumbers(){
return new ArrayList<>(this.numbers);
}
}
setElements(Collection<E>) メソッド
要素を完全に入れ替えることがよくあるなら add&remove より一括更新:
public class NumberContainer<E extends Number>{
private final List<E> numbers = new LinkedList<>();
public boolean addNumbers(List<E> es){ ... }
public List<E> getNumbers(){ ... }
public void setNumbers(List<E> es){
this.numbers.clear();
this.numbers.addAll(es);
}
}
addElements() と removeElements() (もしくは後で見る clearElements())で書くこともできます。
removeElement(E) メソッド
remove
Element() メソッドは要素を直接指定して削除する昔ながらの削除メソッド。 必須メソッドの remove
Elements() を使っても1行で書けます:
NumberContainer<Integer> ints = ...;
Integer i = ...;
ints.removeNumbers( j -> j.equals(i));
要素の型によっては equals() メソッドじゃなく == 演算子で十分なときもありますが(列挙型のときとか)。
では実装。 まぁ削除の処理自体は簡単ですが、返り値として削除が実行されたかどうかの boolean もしくは Optional オブジェクトを返すようにする場合はちょっとコーディンが必要、といってもそんなに量はないですが:
import java.util.Optional;
public class NumberContainer<E extends Number>{
private final List<E> numbers = new LinkedList<>();
public boolean addNumbers(List<E> es){ ... }
public List<E> getNumbers(){ ... }
public void setNumbers(List<E> es){ ... }
public Optional<E> removeNumber(E e){
boolean result = this.numbers.remove(e);
return result ? Optional.of(e) : Optional.empty();
}
}
ちなみに Stream (と必須メソッドの removeElements())を使って書くとこんな風になります:
public Optional<E> removeNumber(E e){
return removeNumbers(f -> f.equals(e)).stream().findAny();
}
Stream#findAny() メソッドが丁度いい Optional を返してくれるので使ってます。 ただ、パッと見はちょっとわかりにくい実装ですかね。
clearElements() メソッド
clear
Elements() メソッドも必須の remove
Elements() を使って書けます:
NumberContainer<Integer> ints = ...;
ints.removeNumbers( i -> true );
実装はまぁ、removeElement() メソッド使ってもよし、コレクションの Collection#clear() メソッドが使えるならそれもよし、って感じです:
public class NumberContainer<E extends Number>{
private final List<E> numbers = new LinkedList<>();
public boolean addNumbers(List<E> es){ ... }
public List<E> getNumbers(){ ... }
public void setNumbers(List<E> es){ ... }
public Optional<E> removeNumber(E e){ ... }
public void clearNumbers(){
this.numbers.clear();
}
}
お試し
なんか思いの外、記事が長くなってきたので、お試しメソッドはサラッと行っちゃいましょう。 お試しメソッドは「まぁいらんだろうけど、もしかしたら・・・」程度のメソッド。
CRUD |
メソッド |
返り値 |
Create |
addElements(E[]) addElements(IntFunction<E>, int) addElements(Supplier<E>, int) |
void (or boolean) Collection<E> Collection<E> |
Read |
elementStream() forEachElement(Predicate<E>, Consumer<E>) getElements(Predicate<E>) |
Stream<E> void Collection<E> |
Update |
mapElement(UnaryOperator<E>) |
void |
Delete |
- |
- |
forEachElement() と getElements() のオーバーロードメソッドは最初の表に載せてませんでしたが、フィルタリングをする Predicate オブジェクトを指定できるようにしたものです。 これらを使ったサンプルは各メソッドの項で見ていきましょう。
addElements(E[]) メソッド
オプショナルメソッドの箇所で見た add
Elements() メソッドはコレクションを引数にとって一括追加を行うメソッドでした。
Java では List オブジェクトを生成する
リテラル表記がないので、List を生成するのは結構面倒です(上記の例では Arrays#asList() メソッドを使ってた)。 そこで可変長引数をとる add
Elements() メソッドを定義しておくと便利な場合があります。 使い方は
NumberContainer<Integer> ints = new NumberContainer<>();
ints.addNumbers(2014, 3, 26);
のようになります。 実装はオプショナルな addElements() メソッドを使って
@SuppressWarnings("unchecked")
public boolean addNumbers(E... es){
return addNumbers(Arrays.asList(es));
}
とできます。 悲しいのは、型パラメータの配列(E...)を使うとコンパイル時に警告が出るところ。 理由は以下を参照:
警告を抑えるには @SuppressWorning("unchecked") もしくは @SafeVarargs をメソッドに付加しておく必要があります。 要素の型を具体的な型にしておいて、型パラメータを使わないという手もありますが。
addElements(IntFunction<E>, int) メソッド
Java8 で導入された
java.util.function パッケージの IntFunction 型を使って要素を追加するメソッド。 IntFunction<E> 型は、int を引数にとり E 型を返すメソッドを1つ持つ関数型です。 使用例はこんな感じ
NumberContainer<Interger> ints = new NumberContainer<>();
ints.addNumbers( i -> i*i, 4)
この例では0から3までの整数(0から4つの整数)に対して、その自乗 0, 1, 4, 9 を要素として追加しています。 実装は以下のような感じ:
public List<E> addNumbers(IntFunction<E> f, int n){
return IntStream.range(0, n)
.mapToObj(i -> f.apply(i))
.peek(e -> addNumber(e))
.collect(Collectors.toList());
}
最後の Stream#collect() は返り値のためだけのものです。 返り値を返す必要がなければ、Stream#peek() ではなく Stream#forEach() で充分です。
addElements(Supplier<E>, int) メソッド
次も上記の add
Elements() メソッドに似ていて、Java8 で導入された
java.util.function パッケージの Supplier 型を使って要素を追加するメソッド。 Supplier<E> 型は、引数をとらずに E 型を返すメソッドを1つ持つ関数型です。 使用例としては、例えばランダムな整数を10個追加したい場合
NumberContainer<Interger> ints = new NumberContainer<>();
Random random = new Random();
ints.addNumbers( () -> random.nextInt(), 10)
のようにします。 実装は上記の addElements() を使うと簡単:
public List<E> addNumbers(Supplier<E> supplier, int n){
return addNumbers(i -> supplier.get(), n);
}
elementStream() メソッド
elementStream() メソッドは、要素を Stream オブジェクトとして返すメソッド。 これがあれば要素を取得するメソッドは何でもできます。 それ以上のこと(フィルタリングやオブジェクト変換)が簡単にできます。 ただ、そういう処理がよく必要になるなら、集約プロパティとして
モデリングしたのが間違ってないかどうかを検討した方がいい気がします。
public Stream numberStream(){
return this.numbers.stream();
}
forEachElement(Predicate<E>, Consumer<E>) / getElements(Predicate<E>) メソッド
要素を取得する際にフィルタリングをかけるメソッド。 フィルターの役目をするのは Predicate 型。 使い方は(forEach
Element() の場合のみ):
NumberContainer<Interger> ints = new NumberContainer<>();
ints.addNumbers(o, 1, 2, 3, 4, 5)
ints.forEachNumber( i%2 == 0, System.out::print);
実装は1行ずつ:
public void forEachNumber(Predicate<E> filter, Consumer<E> consumer){
this.numbers.stream().filter(filter).forEach(consumer);
}
public List<E> getNumbers(Predicate<E> filter){
return stream().filter(filter).collect(Collectors.toList());
}
必須、オプショナルの forEachElement(), getElements() メソッドの実装に filter() メソッドを挟み込んだだけです。
mapElement(UnaryOperator<E>) メソッド
最後は、集約プロパティの要素を一括変換するするメソッド。
java.util.function パッケージの UnaryOperator 型を使っています。 UnaryOperator<E> は E 型を E 型に変換する(つまり E 型を1つとって E 型を返す)メソッドを持つ関数型です。 使い方は
NumberContainer<Interger> ints = new NumberContainer<>();
ints.addNumbers(o, 1, 2, 3)
ints.mapNumbers( i -> i*i*i );
ints.forEachNumber(System.out::print)
まぁ、あんまり使わないでしょうね。
最後に
集約プロパティを扱う上で、それは必要不可欠だ、ってものからお試しですら必要なさそうだ、ってものまでいろいろなメソッドを見てきましたが、大抵の場合には必須メソッドの3つでいいんじゃないかなぁと。 無理矢理持ち出してきたようなメソッドは、Java8 から導入された
java.util.function パッケージのいろいろな型を使ってみようと思った結果出てきたものもあります。
- (Int)Function ・・・ addElements()
- (Unary)Operator ・・・ mapElements()
- Predicate ・・・ removeElements()
- Consumer ・・・ forEachElement()
- Supplier ・・・ addElements()
java.util.function パッケージについては「java.util.function パッケージ」も参照のこと。 それはともかく、ゆくゆくは集約プロパティにアクセスするメソッドが IDE で自動生成できるようになればいいなと願います。
Scalaスケーラブルプログラミング第2版
プログラミングGROOVY