Java好き

カテゴリ:JavaSE標準ライブラリ > java.util

JavaSE8から加わったOptional。
ストリームAPIやラムダ式に隠れてしまっているがとても便利。

ただ慣れてないと使いにくい部分はあるので、
使い方をまとめる。

Optionalがよくわからない場合は
こちら→Optional - nullを扱う新たな方法

生成

3つのファクトリメソッドが用意されているので
いずれかを使う。

値がnullであっても空のOptionalにすればよいので、
基本的にはofNullable()、empty()を中心に考える。

メソッド 概要
of() nullの場合、NullPointerExceptionになる。
ofNullable() nullの場合、空のOptionalを返す。
empty() 空のOptionalを返す。

Immutableなので1度中身のオブジェクトを決めてしまったら
入れ替えるのではなく、新たに生成する。

中身のオブジェクトの取得

Optionalから中身のオブジェクトを取得する場合、
それがnullだったときの方針を決めておかないと
うまく取得することができない。

代替値を設定するや例外を投げるといった方針によって、
メソッドを選ぶ。

アンチパターン

JavaDocを見るとget()が目につくので、
isPresent() + get() とやりたくなってしまうが
これをやってしまってはnullチェックしているのと大差ない。

Optional#orElse(null)とする方がマシ。
まずはorElseXXX()の中から検討する。

例 isPresent() + get()はなるべく避ける
String value = null;
Optional<String> optional = Optional.ofNullable(value);
 
String displayValue = null;
if (optional.isPresent())
    displayValue = optional.get();

orElse()

中身がnullの場合でも、そのときの値を指定できる。
orElse(null)も可。

String value = null;
Optional<String> optionalValue = Optional.ofNullable(value);
String displayValue = optionalValue.orElse("(デフォルト)");

orElseGet()

orElseとほぼ同じであるが、(ファクトリ)関数オブジェクトを指定できる。
(毎回違うインスタンスを生成できる)

String value = null;
Optional<String> optionalValue = Optional.ofNullable(value);
String displayValue = optionalValue.orElseGet(() -> "(デフォルト)");

orElseThrow()

中身がnullの場合、生成した例外をスローする。

String value = null;
Optional<String> optionalValue = Optional.ofNullable(value);
String displayValue = optionalValue.orElseThrow(RuntimeException::new);

中身のオブジェクトのメソッドを実行する

中身をいちいち取得しなくても、
中身のオブジェクトのメソッドを実行できる。

ifPresent()

consumerを渡すことで、副作用をともなう処理ができる。
もちろん、中身がnullの場合は何も行われないだけ。

isPresent()とメソッド名が酷似しているので注意。

String value = null;
Optional<String> optionalValue = Optional.ofNullable(value);
optionalValue.ifPresent(System.out::println);

中身のオブジェクトが保持しているオブジェクト(値)を
取得する

例えば、つぎのような場合

Optional_サンプルクラス図

これも中身のオブジェクトを取得しなくても、
処理することができる。

map()

戻り値がOptionalなので、
そのOptionalに対してまたメソッドを実行できる。

Person person = new Person("田中", "tanaka@example.com", Optional.empty());
Optional<Person> optionalPerson = Optional.ofNullable(person);
 
Optional<String> name = optionalPerson.map(Person::getName);
String displayedName = name.orElse("(未登録)");

flatMap()

mapとの違いは、保持しているオブジェクトがOptionalの場合
そのまま取り出せるというところ。

Person person = new Person("田中", "tanaka@example.com", Optional.empty());
Optional<Person> optionalPerson = Optional.ofNullable(person);
 
Optional<String> twitterAccount = optionalPerson.flatMap(Person::getTwitterAccount);
String displayeTtwitterAccount = twitterAccount.orElse("(未登録)");

主にStreamAPI用の関数型インタフェースが
定義されているパッケージ。

このパッケージには43個のインタフェースが存在するが、
4種類の基本形以外は拡張や派生となっている。
その拡張や派生も一定の規則がある。

4種類の基本形

ここだけ押さえておけばOK。

基本形 メソッド 概要
Supplier<T> T get() 引数なしで、指定された型の値を返す。
supplierは供給者という意味。
Predicate<T> boolean test(T t) 引数1つで、booleanの値を返す。
predicateは真理値を返す関数でよく使われる名詞。
Function<T,R> R apply(T t) 1つの引数で、指定された型の値を生成する(返す)。
functionは関数の意味。
Consumer<T> void accept(T t) 引数1つで、結果を返さない。
何らかの副作用(オブジェクトの状態変化)を行う。

その他のインタフェース

基本形以外の残り39個をまとめると
次のようになる。

基本形 引数の変化 プリミティブ型の固定
Supplier系 Supplier BooleanSupplier DoubleSupplier IntSupplier LongSupplier
Predicate系 Predicate DoublePredicate IntPredicate LongPredicate
BiPredicate
Function系 Function DoubleFunction IntFunction LongFunction
ToDoubleFunction ToIntFunction ToLongFunction
DoubleToIntFunction DoubleToLongFunction LongToDoubleFunction
BiFunction ToDoubleBiFunction ToIntBiFunction ToLongBiFunction
(UnaryOperator系) UnaryOperator DoubleUnaryOperator IntUnaryOperator LongUnaryOperator
BinaryOperator DoubleBinaryOperator IntBinaryOperator LongBinaryOperator
Consumer系 Consumer DoubleConsumer IntConsumer LongConsumer
ObjDoubleConsumer,ObjIntConsumer,ObjLongConsumer
BiConsumer ToDoubleBiConsumer ToIntBiConsumer ToLongBiConsumer

基本形の拡張

Functionでは、引数の型と生成する値の型が同じになる
UnaryOperatorという特殊な拡張が定義される。

基本形の拡張 メソッド 概要
UnaryOperator<T> T apply(T t) 特殊なFunctionで引数の型と生成する値の型が同じ。
unaryは単項という意味。

引数の変化

基本形に対して2つの引数とれるようにしたものが定義される。
命名規則は次のようになる。

接頭辞 説明
Bi,Binary 引数を2つ取るように変更したものにつけられる。

命名規則からいうと、UnaryOperatorはBiUnaryOperatorとしたいところだが、
これだと意味不明なのでBinaryOperatorとなっていると思う。

BinaryOperatorはUnaryOperatorの2つの引数を取れる版であるが
基本的にはBiFunctionの拡張である。

Supplierは引数を取るとFunctionになってしまうので、引数の変化はない。

プリミティブ型への固定

基本形は型パラメータによりあらゆるオブジェクト型に対応できるが、
それゆえにプリミティブに対応できない。

引数や戻り値をラッパー型ではなくプリミティブ型にしたい場面は多いので、
個別のインタフェースとして定義される。

命名規則は次のようになる。

接頭辞 説明
BooleanXX,DoubleXX,IntXX,LongXX 引数がプリミティブ型で固定されたもの。
ToDoubleXX,ToIntXX,ToLongXX 戻り値がプリミティブ型で固定されたもの。
DoubleToIntXX,DoubleToLongXX,LongToDoubleXX 引数、戻り値共にプリミティブ型で固定されたもの。
ObjDoubleXX,ObjIntXX,ObjLongXX 引数が参照型とプリミティブ型に固定されたもの。(Consumer形のみ存在)

「欲張らない繰り返し」ぐらいまで理解しよう。

正規表現はクラスの使い方より、
パターンを作る方が本題であるし難しい。

ここでは正規表現について、「繰り返し」が理解できるぐらいにまとめる。
「欲張らない繰り返し」ぐらいまで理解できていれば、大体困らない。

単語の一致

pen

単純な単語に思えるが、正規表現上の解釈としては次のように考える。
「まずpがあり、その直後にeがあり、その直後にnがある場合にマッチする」

単語だけの正規表現は実際にはあまり使わない。
単語のマッチであればString.contains()などで十分。
ここでは、正規表現の考え方の基本をおさえておく。

ドット

p.n

「.」は、任意の1文字とマッチさせる。

1文字というところが重要。

複数の任意文字としたい場合は、
繰り返しと組み合わせなくてはならない。

「.」自体はあくまでも1文字。

この1文字は単語、空白、記号など何でもいい。

文字クラス

p[ea]n

「[]」は、かっこ内で指定したテキスト1文字にマッチさせる。

この文字クラスも、
1文字というところが重要。
文字クラス内がどんなに長くなろうと、
表しているのは1文字。

また、文字クラス内は「orつなぎ」で
考えなくてはいけないことも重要。

通常の正規表現は「andつなぎ」である。

単語の一致の例パターンでは、
「まずpがある」and「その直後にeがある」and「その直後にnがある」場合にマッチする
という解釈と考えられる。

一方、ここ(文字クラス)の例パターンで考えると、
「まずpがある」 and (「その直後にeがある」 or 「その直後にaがある」)and 「その直後にnがある」場合にマッチする
という解釈をする。

範囲を示す「-」

[0-9]

「-」を使うことで範囲を示せる。
[0123456789]と書かなくてよい。

文字クラス内と文字クラス外では意味が変わるメタ文字

これを知っていないと、はまる可能性がある。

例えば、「.」は文字クラス外では先ほど説明したように任意の1文字である。
ただこれが文字クラス内に入ると、ただの記号としての「.」ということになる。
(任意の1文字ではなくなる。)

文字 通常(文字クラス外) 文字クラス内 備考
. 任意の1文字 「.」記号
| 選択 「|」記号
( 範囲の始まり 「(」記号
) 範囲の終わり 「)」記号
- 「-」記号 範囲を示す

選択

pan|pen

「|」は、またはの意味になる。

選択の対象になった部分は、
通常の正規表現として扱われる。

()で範囲を限定

p(a|e)n

「()」で選択対象を限定できるので、
普通は利用する。

「()」がない場合は、範囲が全般に及んでしまう。

繰り返し

pe*n

「*」は、繰り返しを意味する。
繰り返すのは直前の要素である。

繰り返しの回数によって、
記号が変わる。

記号 繰り返し回数
? 0回以上-1回
* 0回以上-無限
+ 1回以上-無限
X{n} n回
X{n,} n回以上
X{n,m} n回以上-m 回以下

欲張らない繰り返し

例えば、次のテキストから<li>で囲まれた部分の抽出を考える。

<ul><li>baseball</li><li>soccer</li></ul>

先ほどの繰り返しを利用すると、
次のように考えられる。

<li>.*</li>

この正規表現で抽出すると、
次の結果になる。

<li>baseball</li><li>soccer</li>

これでは、最初の<li>で囲まれた部分だけを抽出したいときに困る。
この場合は、繰り返しに「?」を加えることで解決する。

<li>.*?</li>

この正規表現で抽出すると、
次の結果になる。

<li>baseball</li>

この2つの例からわかるように、
通常の繰り返しではできる限りたくさんマッチさせようとする。
(いわゆる欲張りな繰り返し)

よって、部分抽出など繰り返しの範囲を狭くしたい場合は
「?」(最短一致数量子)を指定する。

意外と難しいPatternとMatcherの基本的な使い方を
まとめました。

               

基本

java.util.regexパッケージは、
例外クラスとインタフェースを含めても4つというシンプルな構成。
Regexというクラスはなく、Javaではパッケージ名となっている。

基本的には、PatternクラスとMatcherクラスだけを使って
正規表現処理をする。

regexパッケージクラス図

Patternクラスは、コンパイル済みの正規表現を保持するクラス。

Matcherクラスは、正規表現(Patternオブジェクト)と対象文字列を関連づけるクラス。
正規表現を適用するためのメソッドを提供する。
また、抽出や置換用のメソッドも提供する。

正規表現の処理手順としては、次のようになる。

  1. 正規表現をコンパイルして、Patternオブジェクトを生成。
  2. Patternオブジェクトと対象文字列を関連づけるMatcherオブジェクトを生成。
  3. 正規表現を適用するメソッドを呼び出して、文字列の抽出や置換を行う。

正規表現のコンパイル

ファクトリメソッドが用意されているので、それを使う。

オプションをいくつか指定できる。
また、スレッドセーフなのでフィールドに保持することも可能。

String regex = "pen";
Pattern pattern = Pattern.compile(regex);

検索処理

Matcherオブジェクトのfind()を呼び出すことで、
正規表現を適用してマッチしたものが見つかったかどうかをbooleanで返す。
その後に、group()を呼び出すことでマッチした文字列を返してくれる。

複数マッチする場合は、またfind()を呼び出す。

Matcherの内部でマッチした位置を保持しているので、
group()は前回の次にマッチした文字列を返すことになる。
(JDBCのResultSetのような感じ)

String regex = "\\d+\\w+";
Pattern pattern = Pattern.compile(regex);
 
String input = "1st 2nd";
Matcher matcher = pattern.matcher(input);
 
if (matcher.find())
    log.debug("first match is [" + matcher.group() + "]");
 
if (matcher.find())
    log.debug("second match is [" + matcher.group() + "]");
 
// マッチしないはず
if (matcher.find())
    log.debug("third match is [" + matcher.group() + "]");
実行結果
first match is [1st]
second match is [2nd]

matches()とlookingAt()

正規表現を適用するメソッドはfind()だけではなく、この2つのメソッドでも可能。
違いは次のようになっている。

  • matches()
    対象文字列の「全体と」マッチするかどうか。
    暗黙的に正規表現が「\A…\z」で囲まれて判定されるようなもの。
  • lookingAt()
    対象文字列の「先頭から」マッチするかどうか。
    暗黙的に正規表現が「\A」が先頭に付加されて判定されるようなもの。

対象文字列に改行が含まれない(1行)であれば、
使い勝手はよいかもしれない。

ただ、改行が含まれる場合にMULTI_LINEオプションのことを考えると
直感的ではなくなる。

よってこれらを利用するより、
find()で正規表現をしっかり作った方がわかりやすいと考えられる。

置換処理

appendReplacement()とappendTail()を
うまく利用することで処理できる。

String regex = "\\d+\\w+";
Pattern pattern = Pattern.compile(regex);
 
String input = "1st 2nd";
Matcher matcher = pattern.matcher(input);
 
StringBuffer replacedStrBuffer = new StringBuffer();
while(matcher.find()){
    matcher.appendReplacement(replacedStrBuffer, "This is " + matcher.group() + " match.");
}
matcher.appendTail(replacedStrBuffer);
 
log.debug(replacedStrBuffer.toString());
実行結果
This is 1st match. This is 2nd match.

replaceAll()とreplaceFirst()もある

もっと単純な置換であれば、この2つのメソッドでもよい。

Matcherも使い回せる

reset()を用いることで、
Matcherを何度も生成せずに使い回せる。

最初に空文字でMatcherを生成して、
reset()で対象文字列を入れ替えていく使い方も可能。

ただし、スレッドセーフということでもないので
そこは要注意。

スポンサードリンク

基本

for文で回すときは、Map.Entry

Mapをイテレートするときは、Map.Entryというネストされたクラスがあるので利用する。
keySet()を回して、毎回get()するようなことは効率が悪いだけ。

ちなみに、Entryは「登録されたもの」「参加者」という意味。
コンテストのエントリーとかと同じ。

Map<String, Object> map = new HashMap<>();
  
for (Map.Entry<String, Object> entry : map.entrySet()) {
    String key = entry.getKey();
    Object value = entry.getValue();
}