【Java】Java SE 8 Gold挑戦にあたってのJava8言語仕様勉強メモ
Java SE 8 programmer Ⅱ受験にあたってjava8の言語仕様等を勉強して覚えた結果等をここに書き記す。
「Java1.6くらいで言語仕様が止まっているレガシー人間が1.8を学ぼうと自己学習した記録」に近く、ほぼほぼ自分用のメモである。
世間的に言っても大分時代遅れの内容であると思われ、その点ご留意くださいといいますか…
Generics
イマイチ良く分かってなかったので基礎的なところから学習した(今更)。
すぐ忘れそうだが…
不変・共変について
型パラメータを指定したからといって、お互いは独立していて何の関係もありませんというのが「不変」で、型を指定したからにはその関係性に従ってもらうぜというのが「共変」、のようだ。
List<Object>とList<Stringt>は指定している型パラメータ間に親子関係があるが、Listとしてのそれぞれのオブジェクトは親子関係にはならないというのが「不変」、ObjectとStringは配列とはいえObjectはStringの親なので親子関係は維持する、というのが「共変」。
腹違いの子同士は血は繋がってるけど結局のところ赤の他人ですよねというのが「不変」で、血がつながっているならどんなに形を違えても親子に変わりはないというのが「共変」?
もういいか。
共変、不変についてはこちらのブログに詳しい。
ただ、型パラメータ指定した場合は型パラメータの間での親子関係の記述が妥当かどうかはコンパイル時に厳密にチェックされる。
例えばこんな↓クラスがあったとして
static class TestObject<T> { private T t; public T getT() { return this.t; } public void setT(T t) { this.t = t; } } static class TestParent {} static class TestChild extends TestParent {}
それぞれ以下のようになる。
No | コード | コンパイルできる? |
---|---|---|
1 |
TestObject<? super TestChild> to1 = new TestObject |
できる |
2 |
TestObject<? super TestChild> to2 = new TestObject<TestParent>(); |
できる |
3 |
TestObject<? extends TestParent> to3 = new TestObject<TestChild>(); |
できる |
4 |
TestObject<? extends TestParent> to4 = new TestObject<TestParent>(); |
できる |
5 |
TestObject<TestChild> to5 = new TestObject<TestChild>(); |
できる |
6 |
TestObject<TestChild> to6 = new TestObject<TestParent>(); |
できない |
7 |
TestObject<TestParent> to7 = new TestObject<TestChild>(); |
できない |
8 |
TestObject<TestParent> to8 = new TestObject<TestParent>(); |
できる |
9 |
TestObject<TestChild> to9 = new TestObject<>(); |
できる |
10 |
TestObject<TestParent> to10 = new TestObject<>(); |
できる |
11 |
TestObject<?> to11 = new TestObject<TestChild>(); |
できる |
12 |
TestObject<?> to12 = new TestObject<TestParent>(); |
できる |
13 |
TestObject<?> to13 = new TestObject<? extends TestParent>(); |
できない |
14 |
TestObject<?> to14 = new TestObject<? super TestChild>(); |
できない |
No.6は、変数宣言の型パラメータをTestChildに指定してるにも関わらず、TestParent(TestChildの親)を型パラメータにしたインスタンスを生成しようとしてるので、型の不一致でコンパイルエラー。
No.7はその逆で、クラスの型パラメータをTestParentに指定してるにも関わらず、TestChild(TestParentの子)を型パラメータにしたインスタンスを生成しようとしてるので、これも型の不一致でコンパイルエラー。
No.6は左辺と右辺で型の親子関係が逆転してるので、まあコンパイルエラーになるのも分からなくはないが、No.7はポリモーフィズム的に考えればいけるんじゃないのか、と思ったが、だめのようだ。
extendsもsuperも付けずに、特定のクラスかインタフェースをばっちりそのまま指定した場合は、インスタンス生成時の型パラメータもそれに則っている必要がある、ということのようだ。
逆にNo.1~4のように、変数宣言時に「具体的にはまだわからないが、とりあえずTestChildが継承している何か(<? super TestChild>)」や、「具体的にはまだわからないが、とりあえずTestParentを継承している何か(<? extends TestParent>)という書き方をしていると、右辺側はTestChildだろうとTestParentだろうとコンパイル通る(?の境界に位置するクラスも含むようなので、イメージ的には数学記号の⊇(=super)や⊆(=extends)に近い)
No.9やNo.10のように、右辺に型パラメータを何も指定しないでおくと、上のように細かいことをいちいちチェックされず、素通りでコンパイルは通る。
だが例えばこの後、to9.setT("a");などと、変数宣言側と全く異なるクラスを指定してメソッド使おうとするとコンパイルエラーになる(No.9で言えば、変数宣言時の型パラメータはTestChildなので、setも引数にTestChildをもらうことを前提にしている。にも関わらずjava.lang.String(="a"のこと)を渡してるので、コンパイルエラーになる、という理屈だ)
要するにNo.9やNo.10はインスタンス生成時にかかるコンパイルのチェックを後回しにしているだけにすぎん。。
また、No.13やNo.14は右辺にワイルドカード(?)を使ってるのでコンパイルエラーになる(右辺側にはワイルドカードを使えない)。
そもそもGenericsで指定する「型」とは
Genericsクラスの定義をするときに指定する、APIでよく出てくる「T」とは、特定のクラスを指しているわけではなく、「なんらかの型」を指していて、クラス定義の段階ではTの詳細はわかっていない(Tが何なのかについては問わない)というのが基本的な考え方である。
なのでここにStringとか、既知のクラス名つけると逆に混乱する。
例えばComparator<String>をimplementsしてcompareを実装しようとした場合、
class MyComparator<String> implements Comparator<String> { public int compare(String t1,String t2) { return t1.compareTo(t2); } }
こういう定義はコンパイルエラーになる。
なぜならcompareメソッドが引数でもらうことになっている型は「String」という名前の「なんかのクラス」でしかなく、この「String」は、たまたま偶然java.lang.Stringと同じ名前なだけでjava.lang.Stringとは全然違うクラスを指している(同じかどうかすらこの時点ではわからんし把握する気もない)からだ。にも関わらずjava.lang.Stringを指していると勘違いして、お互いString同士だからcompareToで比較できるぜ、と安易に考えて実装しちまってるから怒られる。
赤太字の部分が、Comparatorの定義に従って繋がっており、見た目は「String」だがjava.lang.Stringとはまるで別物である(同じかもしれないけど、この時点ではGenericsの型に書いた記述はあくまで単なる識別子に過ぎず、java.lang.Stringと同じ意味合いを持たない)ということを理解しなくてはならない。
だからここに既知のクラスと同名のクラスを指定してしまうと混乱する。
そのためこの定義は
class MyComparator<T> implements Comparator<T> { public int compare(T t1,T t2) { return t1.compareTo(t2); } }
と書いてくれた方がまだ分かりやすい。
なんなのか正体不明の謎クラス「T」が、「compareTo」メソッドを持っているかどうか等知る由もないので、コンパイルエラーになる、というのは、Stringを使った時に比べて理解しやすい(と個人的に思う)。
これを無理やり実装するなら
class MyComparator<T> implements Comparator<T> { public int compare(T t1,T t2) { return ((Comparable)t1).compareTo((Comparable)t2); } }
って感じになるのか。(無検査キャストをしてるので警告がでるがコンパイルは通る)
これがラムダ式だとその辺のモヤモヤした部分を一発で解消してくれるのでわかりやすい。
Comparator<String> mc = (s1,s2) -> s1.compareTo(s2);
ラムダ式が実装しているs1.compareTo(s2)は、記述している時点でs1やs2が少なくともComparableを実装しているクラスである前提になってるが、上のように細かいことをぐちぐち言われずそのままコンパイル通る。(まあラムダ式の場合は引数に型指定できるからもっと分かりやすいけど)
というより、Genericsクラスは、クラス定義時点では正確な型パラメータが不明のまま定義し、インスタンス生成時(ラムダ式を含む)に「型」をある程度正確に(呼び出し元から)決めてあげる、ということなのだろう。
だからMyComparatorを定義した時点ではまだ「T」の正体が判明しないが、MyComparatorのインスタンスを生成するときに
MyComparator<String> mc = new MyComparator<String>();
と指定することで、初めてこの「MyComparator」がStringを型パラメータに持つクラスであることが決定し、オブジェクトの性質が固まる(もちろんString以外を指定してもいいが、いずれにせよインスタンス生成時に型の詳細が決定される)
ラムダ式はインスタンスの生成を簡易記述しているだけで、本質的なところはこれと同じといえるのだろう。
と言いつつ型パラメータ付のクラスをexntendsしたりinterfaceをimplementsして型パラメータ無しのクラス宣言する場合はTの明記が必要
見出しの通りなのだが。。
例えば以下のようなinterfaceがあったとする
static interface TestI<T> { public T get(); }
これはこの時点では「なんだかよくわからん謎クラスT」である…それは間違ってない。
だがこのinterfaceをimplementsするクラスは「なんだかよくわからん謎クラスT」では困るので、明示的にクラスを指定してあげる(型パラメータのふるまいを決定してあげる)必要がある。
static class TestC1 implements TestI<T> { // ここと public T get() { // ここでコンパイルエラー return 1; } }
こういうクラスはコンパイルエラーになる。
TestIの持つ「謎クラスT」の特性は、implementsするクラスが責任をもって明示的に指定してあげる必要がある。
static class TestC2 implements TestI<Integer> { public Integer get() { return 2; } }
これならコンパイル通る。
TestI<Integer>の"Integer"の部分は、れっきとしたjava.lang.Integerを指しており、内部的にもIntegerとして扱うことができる。
同様のことはクラスを継承(extends)するケースにおいても言える。
例えば以下のようなクラスがあったとして
static class TestC0<T> { private T t; public T get() { return this.t; } public void set(T t) { this.t = t; } }
これもこのクラスの時点では「なんだかよくわからん謎クラスT」だが、これを継承したクラスを定義する場合
static class TestC3 extends TestC0<T> {} // これはコンパイルエラー static class TestC4 extends TestC0<Integer> {} // これはコンパイル通る
という感じになる。
TestC3はTestC0の型パラメータTを「なんだかよくわからないまま」扱おうとしており、クラスの定義があやふやなのでコンパイルエラー。
TestC4はTestC0の型パラメータがIntegerであることを明記して継承しているので、取り扱える。
メソッドに型パラメータ
メソッドに型パラメータ付ける場合は戻り値の型の前に型パラメータを記述する
public class Test<T> {
public <T> void test() {
System.out.println("test");
}
}
この例だと型パラメータを指定する意味が全くない(型パラメータTが一切何の関係もしていない)のだが、一応定義できるっちゃできる。
Tを絡めると以下のようなメソッド定義が可能になる。
public <T> List<T> test2() { return new ArrayList<T>(); }
とか。
例えばTestクラスを型パラメータIntegerでインスタンス生成した場合、test2の戻り値はList<Integer>になる。
逆にInteger以外の型を指定すると(ワイルドカード"?"以外は)コンパイルエラーになる。
Test<Integer> test = new Test<Integer>(); List<Integer> list = test.test2(); // これはOK List<String> list = test.test2(); // これはだめ(コンパイルエラー)
また、型パラメータ指定したメソッドは呼び出し側からも型パラメータが動的に指定できる。
Test<Integer> test = new Test<Integer>();
List<String> list2 = test.<String>test2();
慣れてないせいか、個人的にかなり変な書き方に見えるのだが、これもコンパイルは通る。
クラス自体をIntegerの型パラメータでインスタンス作っておきながら、メソッドは別の型パラメータStringで指定して結果を取り出している。
ちなみにメソッドの型パラメータは、所属するクラスがGenericsクラスでなくても、メソッドだけを個別に型パラメータ化できる。
つまり
class Test2 { public <T> List<T> test() { return new ArrayList<T>(); } }
というのが定義できる。
この場合は
Test2 test2 = new Test2(); List<String> list = test2.<String>test();
として使うことができる。
ラムダ式
基本の4つのラムダ式は確実に抑えておく必要がある。
特にFunction、Consumer、Predicateに関しては後述するStream APIも交えて実際に使用して使い方を覚えたほうが良いと思う
基本の4つのラムダ式
java.util.function.Function、Predicate、Consumer、Suuplierの4つ
Functionはapplyで、Predicateはtestで、Consumerはacceptで、Supplierはgetで、それぞれ定義した関数を実行する。
Function<String,String> func1 = (a) -> a.concat("!!"); System.out.println(func1.apply("function")); // function!!Predicate<String> pred1 = (a) -> a.startsWith(“p”);
System.out.println(pred1.test(“predicate”)); // trueConsumer<StringBuilder> cons1 = (a) -> a.append("!?");
StringBuilder sb = new StringBuilder();
sb.append(“consumer”);
cons1.accept(sb);
System.out.println(sb.toString());// consumer!?Supplier<String> supp1 = () -> “supplier”;
System.out.println(supp1.get()); //supplier
ちなみに大体ラムダ式で書くケースしか扱われないが、これは匿名クラスの書き方を省略しているだけで、例えばFunctionの例で言えば以下と同じである。
Function<String,String> func = new Function<String,String>() { @Override public String apply(String str) { return str + "!!"; } };System.out.println(func.apply(“function”));
関数型インタフェース
default及びstaticを除くメソッドが一つだけのinterface(上記のFunctionやConsumer等も含め)を関数型インタフェースと呼ぶらしい。
関数型インタフェースはインスタンス生成をラムダ式で記述できる。
「default及びstaticを除く」がポイントで、決して「メソッド一つだけ」が条件ではない。
だから以下のようなinterfaceも関数型interfaceである
static interface Test { int execute(); default int defaultExecute() {return -1;}; static int staticExecute() {return 0;}; }
これはラムダ式を使って
Test test = () -> {return 100;};
System.out.println(test.execute()); // 100
で記述できる。
他によく出てくる関数型インタフェース
java.util.function.BiFunction。
Functionの仲間(サブクラス)で、引数を2つとる。(Bi(バイ)というのが"2"を意味するんだそうだ)
それと返却値の型を含めて合計3つの型パラメータを指定する。
BiFunction<Integer,Integer,Number> bif = (a,b) -> {return a + b;}; System.out.println(bif.apply(10,20)); // 30
java.util.function.BiConsumer。
Consumerの仲間(サブクラス)で、引数を2つとる(この点はBiFunctionと同じ)
Consumerなので返却値はないので、型パラメータは2つだけ。
BiConsumer<Integer,StringBuilder> bic = (a,b) -> {b.append(String.valueOf(a + 1));}; StringBuilder sb = new StringBuilder(); sb.append("BiConsumerTest:"); bic.accept(999,sb); System.out.println(sb); // BiConsumerTest:1000
java.util.function.UnaryOperator。
Functionの仲間(サブクラス)で、引数1つ・かつ戻り値の型も引数と同じ、という限定条件付きのFunction。
なので型指定パラメータは1つだけになる。
UnaryOperator<Integer> uo = (a) -> {return a + 10;};
System.out.println("UnaryOperator#apply=" + uo.apply(100));
java.util.function.BinaryOperator。
BiFuctionの仲間であり、「Binary」という名前の通り引数を2つ取るが、戻り値含めて型指定は1つだけ。
~Operator系はそういう傾向にあるようだ。
BinaryOperator<Integer> bo = (a,b) -> {return a + b;};
System.out.println("BinaryOperator#apply=" + bo.apply(1,2));
DoubleFunction、LongFunctionなどの、プリミティブ型ラッパークラスの名前が先頭にくっついたやつら。
全部java.util.function配下にいる。
ここでいう「Double」とか「Long」とかのプリミティブ型ラッパークラスの型名は実際に実行する際に渡す引数の型を指しており、戻り値の型を指してはいない。
要するに引数の型が決め打ちになってる省略形のFunctionという感じか。
一方で、型パラメータには戻り値の型を指定する。
なので、たとえばDoubleFunctionなら、型パラメータにStringとか全然Doubleでもなんでもないクラスを指定しても良く、この場合は「Doubleの値を引数にもらって最終的にString型を返すFunction」になる。
以下のようなものがそう。
DoubleFunction<String> df = () -> String::valueOf;
System.out.println(df.apply(10.0)); // 10.0
これがLongConsumerとか、戻り値のないタイプの関数型インタフェースになってくると、話が違くてややこしくなるから注意が必要だ。
ただ基本4つの関数型インタフェースの名前の前にくっついてるプリミティブ型ラッパークラスの名前は、あくまで「実行時引数の型」だということを理解しておきなさい俺。
逆に、ToIntFunctionとかToDoubleFunctionとか、先頭に"To"がくっついてその後にプリミティブ型ラッパークラス名がつくやつは、戻り値がIntegerやDoubleになっていて、Genericsで指定するのは引数の型になる。
また、これらのFunctionは、他のFunctionと異なり、applyメソッドで実行するのではなく、applyAsXxxメソッドという専用の別メソッドが用意されていることを忘れてはならない。
ToIntFunctionならapplyAsIntメソッドを使う。
なので例えばToIntFunction<String>だったら、「Stringを引数に、最終的にintを返す関数」になる。
以下実例
ToIntFunction<String> tif = String::length; tif.applyAsInt("test"); // 4
ラムダ式の中からローカル変数はいじれない
ラムダ式の中から関数定義外の変数いじろうとすると、文句言われてコンパイルできない。
int a; Consumer<Integer> cons = (i) -> { a = i.intValue(); };
この例はコンパイルエラー。(Consumerじゃなくて他でも全部同じだが)
エラー: ラムダ式から参照されるローカル変数は、finalまたは事実上のfinalである必要があります
a = i.intValue();
逆の(?)考え方で、以下のような例もコンパイルエラーになる
int b = 10; Consumer<Integer> con2 = (i) -> System.out.println(b); b = 11; con2.accept(b);
ラムダ式で定義している引数iを使わずに、外にある変数bを使っている例だが、ラムダ式の中から外にある変数bに少しでも触れた時点で、bがfinal扱いになるので、直後にb = 11;と値の中身を書き換える指示がこれに払拭してコンパイルエラーになる。
メソッド参照
ラムダ式で定義した引数の型・個数と、ラムダ式で定義している関数内で実行する引数の型・個数が一致している場合、ラムダ式の中身の記述をさらに省略できる。
例えば
Consumer<String> cons = (a) -> System.out.println(a);
const.accept("STRAIGHTENER"); // 標準出力に「STRAIGHTENER」と表示
は、
Consumer<String> cons = System.out::println; const.accept("STRAIGHTENER"); // 標準出力に「STRAIGHTENER」と表示
で省略できる。
また、コンストラクタをメソッド参照で記述する場合は、クラス名::newと書く。
Supplier<String> s = String::new;
メソッド参照2
staticメソッドをつかう場合はString::valueOf等のようにクラス名を直書きするが、インスタンスメソッドを使う場合は実際に変数名.メソッド名の要領で記述する。
逆に、インスタンスメソッドをstaticメソッド呼び出し風に書くとコンパイルエラーになる。
たとえば以下のような感じ。
String s = "test";Function<String,String> f1 = String::concat; // コンパイルエラー
Function<String,String> f2 = s::concat; // コンパイル通る
とかいいながらちょっと微妙な例外もあり、後述するStream APIでメソッド参照を使う場合で、特にStreamの格納要素が独自オブジェクトの場合だと、インスタンスメソッドもstaticメソッド風に書かないとコンパイルエラーになる。
例えば
class Test { private String name; public Test(String name) { this.name = name; } public String getName() { return this.name; } public void setName(String name) { this.name = name; } }
こういうクラスがあったとして、このクラスのインスタンスをいくつか生成してStreamに格納してStream APIを使うケースを考えたとき、
Stream<Test> stream = Stream.of( new Test("aaa"), new Test("bbb"), new Test("ccc"), new Test("ddd") );stream.map(t::getName).forEach(System.out::println); // コンパイルエラー
この書き方はコンパイルエラーになる。
コンパイラからすると「t::getNameのtってなんだよ?どこから出てきたんだ?」って言いたくなるのだろう。
例外とは書いたが、これはまあ、そう言われてみれば正論な気もする。
この場合は、インスタンスメソッドを呼ぶのだが、staticメソッド風に書かないとだめで、以下のように書く。
stream.map(Test::getName).forEach(System.out::println);
ちなみにメソッド参照を使わないで以下のようにラムダ式で記述する分には、どこから出てきたんだかわからない謎の変数tを使ってても問題なくコンパイル通る。(というかそれがラムダ式の特徴でもあるから当然だ)
stream.map(t -> t.getName()).forEach(System.out::println);
Stream API(基本編)
良く言われてることだが、Streamというキーワードと、FileInputStreamとかを混在してはいけない。
全く別の存在である。
数学で言うところの「集合」全体に対する操作をするためのAPIという感じ。
Streamのつくりかた
- Stream#ofを使う。
Sream<String> stream = Stream.of("A","B","C");
ちなみにIntStreamやDoubleStream等の、プリミティブ型特化型のStreamもあるが、これは上の方法では作れない。
Stream#ofで生成できるStreamは、あくまでofの引数に渡す型Tを型パラメータに持つStream<T>だからである。
なので、以下の例はコンパイルエラーになる。
IntStream is = Stream.of(1,2,3,4); //コンパイルエラー
これ↓ならコンパイル通る。
Stream<Integer> is = Stream.of(1,2,3,4); //コンパイル通る
IntStream等のプリミティブ型特化Streamにはそれぞれに特化したofメソッドがある。
IntStream is = IntStream.of(1,2,3,4);
- java.util.List#streamというメソッドがあって、これでListをそのままStreamにできる。
List<Integer> list = Arrays.asList(1,2,3,4,5); Stream<Integer> stream = list.stream();
同じような種類にjava.util.Set#streamというのもあるので同じように使える。
Set<Integer> set = new HashSet<Integer>(); set.add(1); set.add(2); set.add(3); Stream<Integer> stream = set.stream();
ちなみにMapにはstreamはない!
Map#entrySetで一度Setにしてから上の方法でStreamにするしかない。 - Files#linesを使う。これはテキストファイル全体を改行コード区切りで各要素に分割したStreamにして返してくれるメソッドである。
try (Stream<String> stream = Files.lines(Paths.get("hogehoge.txt"))){ stream.forEach(System.out::println); }
ちなみにStreamはAutoCloseableを実装してるのでtry-with-resource文の中に書くことができる。
似たようなのにFiles#readAllLinesっていうのがあるが、これは戻り値がListなので少し扱いが違う(まあそのあとlist#streamしたら同じだけど)
よく出てくる中間操作と終端操作
Sreamには中間操作と終端操作があって、中間操作は終端操作が実行されるまでは実行されない(遅延評価となる)。
終端操作なしで放置して「どうなる?」って問題があるが、この場合、間にどんな中間操作をいれていたとしても、何も実行されないで終わる。
なんか正確に言うとこの「終端操作」にもいくつか種類があるらしいが、とりあえず大分別でそうとだけ捉えるだけにしておく。
中間操作は基本的に全部、メソッド実行の結果として中間操作適用後の新たなStreamを返し、それにさらに中間操作を加えることで全体の加工や整形を行っていく。
StringBuilder#appendを連続で実行して文字列繋げまくるのと感覚的には似ている。
メソッド | 中間/終端 | 内容 |
---|---|---|
Stream#filter | 中間操作 | Streamの各要素に 引数のPredicateを適用し、 結果がtrueの要素のみ残した 新たなStreamをつくる。 |
Stream#map | 中間操作 | Streamの各要素に 引数のFunctionを適用した 新たなStreamを作る。 |
Stream#mapToInt | 中間操作 | Streamの各要素に 引数のToIntFunctionを適用した 新たなIntStreamを作る。 仲間にmaptoDoubleとかmapToLongとか、 プリミティブ型特化のメソッドがある。 |
Stream#flatMap | 中間操作 | 入れ子構造になっているStreamの各要素を バラして平坦なStreamにする。 「バラし方」を引数で Functionで指定する。 Streamの各要素がListになってて Listの中にさらに要素が入ってる、 というときに使用する。 これもmapToXxxと同じで、 flatmapToIntとかがある。 |
Stream#sorted | 中間操作 | 中身をソートする。 引数なしだと自然順序に基づいてソート。 Comparatorを引数にとるバージョンもあり こっちはComparatorに従ってソートされる |
Stream#peek | 中間操作 | 中間操作の途中の 適用具合を確認するための デバッグ用として 用意されたメソッド 引数にConsumerをとる。 大体System.out::printlnを使う。。 |
Stream#distinct | 中間操作 | Stream内の重複を除く。 SQLのdistinctと同じ。 ただ、格納順序は元の順序を 出来る限り維持する(API曰く) ソート機能は持っていない、 という意味。 |
BaseStream#parallel | 中間操作 | Stream APIの実行状況を並列モードに変える。 ただ、並列処理にしたからといって、 直列処理より早いとは限らない(らしいよ) これはStreamじゃなく BaseStreamに定義がある。 |
Stream#count | 終端操作 | Streamの要素数を返す。 多分一番単純な終端処理。 とりあえず終わらせたければ これ使っておけばいい |
Stream#max Stream#min |
終端操作 | Stream内の最大もしくは 最小の要素を探して返す。 引数にComparatorと取る。 戻り値はOptionalなので、 結果の取り出しは Optional#getとか Optional#ifPresentとかで 行うことになる。 |
Stream#forEach Stream#forEachOrdered |
終端操作 | Streamの全要素に対して 引数のConsumerを実行する。 大体System.out::printlnを使う、これも。。 |
Stream#reduce | 終端操作 | 引数のBinaryOperatorの各引数に Streamの各要素を一つずつ当てはめて Streamの中身を集約する。 API見ると小難しいこと書いてあるが 要するに自作の集約処理を適用する、 ときに使うらしい。 引数の数違いでいくつか種類がある。 それぞれ戻り値も違うので注意 |
Stream#collect | 終端操作 | Collectorを引数に、 Streamの中身を集約する。 SQLのgroup byとイメージ近い。 Collectors#groupingBy、 Collectors#partioningBy、 Collectors#joining、 あたりはよく出てくる |
Stream#findAny Stream#findFirst |
終端操作 | Stream内からなんか一つ探して返す。 基本的にそんだけ。 何が返ってくるか指定できないし、 保証もできないという、 なんのためにあるんだか よくわからないメソッド。 「とりあえず終わらせたい」用か? |
Stream#anyMatch | 終端操作 | 引数のPredicateがtrueになる要素が Stream内に一つでもいたらtrueを返す。 性質上、trueが見つかったら即終了。 つまりStreamの全要素を見ることを 保証していない。 |
Stream#allMatch | 終端操作 | 引数のPredicateがtrueになる要素が Stream内全要素でOKならtrueを返す。 性質上、falseが見つかったら即終了。 つまりStreamの全要素を見ることを 保証していない。 |
処理のイメージをつかむ
個人的には、中間操作を定義したら、一度Streamの全要素にその中間操作を適用しきってから次の中間操作に進む、と思ってたんだが、実際はそうではないようだ。
各要素ごとに中間1→中間2→…×n→終端、と一度全て適用しきってから、次の要素を取り出してまた最初の中間→中間2→…と進む。
例えば以下のようなコードを実装してみる。
List<Integer> list = Arrays.asList(1,2,3,4,5); Stream<Integer> stream = list.stream(); System.out.println( stream .filter((i)->i>2) // (1) .peek(System.out::println) // (2) .map((i)->i*2) // (3) .peek(System.out::println) // (4) .anyMatch((i)->i==8) // (5) );
これを実行すると、以下のような標準出力になる。
3 6 4 8
これは1~5までの数値のStreamに対して、
(1)要素が2より大きい数だけでフィルターし
(2)(1)の結果を見てみて
(3)各要素を2倍して
(4)(3)の結果を見てみて
(5)値が8の要素の有無を探して結果を返す。
が、結果を見ると3→6→4→8、でここで終了となっている。
これは
まず最初の要素1を取り出す→(1)を適用した結果消えてなくなる
→次の要素2を取り出す→(1)を適用した結果消えてなくなる
→次の要素3を取り出す→(1)を適用して生き残る→(2)で「3」が見える→(3)で2倍する→(4)で「6」が見える→(5)の判定でNG
→次の要素4を取り出す→(1)を適用して生き残る→(2)で「4」が見える→(3)で2倍する→(4)で「8」が見える→(5)の判定でOK
→終了
という流れになっている。
つまり要素を一つずつ取り出して、中間~終端までの各操作を一通り適合しきる、ということが確認できる。
ただ、これは直列で処理をしているからで、並列処理にすると内容が変わる。
System.out.println( stream .parallel() .filter((i)->i>2) // (1) .peek(System.out::println) // (2) .map((i)->i*2) // (3) .peek(System.out::println) // (4) .anyMatch((i)->i==8) // (5) );
Stream#parallelを使うと、以後のStream APIの実行を並列処理で回すことができる。
このときの実行結果は以下の通りになる
5 4 3 6 8 10 true
前回は登場しなかった「10(5の2倍)」などが登場していることがわかる。
Stream API(実践編)
はじめに(データソースの準備)
これは実際にモノを使ったほうがわかりやすいと思い、適当なCSVを作った。
これをソースとして用いていろいろ実験する。
CSVはこれ↓(趣味丸出し)。
ストレイテナー,ホリエアツシ,1998-01-01 ストレイテナー,ナカヤマシンペイ,1998-01-01 ストレイテナー,日向秀和,2004-03-01 ストレイテナー,大山純,2008-10-01 Nothing's Carved In Stone,生形真一,2008-11-15 Nothing's Carved In Stone,日向秀和,2008-11-15 Nothing's Carved In Stone,大喜多崇規,2008-11-15 Nothing's Carved In Stone,村松拓,2008-11-15 the HIATUS,細美武士,2009-01-01 the HIATUS,ウエノコウジ,2009-01-01 the HIATUS,masasucks,2009-01-01 the HIATUS,柏倉隆史,2009-01-01 the HIATUS,伊澤一葉,2009-01-01 ELLEGARDEN,細美武士,1998-01-01 ELLEGARDEN,生形真一,1998-01-01 ELLEGARDEN,高田雄一,1998-01-01 ELLEGARDEN,高橋宏貴,1998-01-01 FULLARMOR,ホリエアツシ,2002-12-01 FULLARMOR,日向秀和,2002-12-01 FULLARMOR,大喜多崇規,2002-12-01 FULLARMOR,井澤惇,2006-12-01
要するに「バンド別メンバー別加入年度CSV」ってところか。
加入年度は年以外は基本、適当。
Wikipediaで調べた結果を載せただけである。
※ひなっちのテナー正式加入は2004年だったはずだが何月かは知らないし、OJのテナー正式加入も2008年だった以外、月が不明でわかっていない。FULLARMORの井澤氏加入はWikipedia曰く2006年末とのことなので、とりあえず上の通りにした。他バンド、つまりハイエイタス・エルレ・ナッシングスの加入年度は全メンバー同じ、つまり「バンド結成日」を意味するが、例えばナッシングスはNovember 15thのイメージが強いためこうしてるだけで正式なバンド加入日は知らない(明かされてない)し、とか、まあ、その辺含めて適当という意味だ。あくまでJava勉強用のちょっとした教材だから情報の精緻さは求めてない
「ひなっちが所属しているバンド一覧」を抽出する
まずは非常に簡単な例だ。
Path path = Paths.get("StreamMaterial1.csv"); try(Stream<String> stream = Files.lines(path)) {stream .filter((line)->line.contains("日向秀和")) .map((line)->line.split(",")[0]) .forEach(System.out::println);
} catch(IOException ioe){
ioe.printStackTrace();
}
Stream#filterでひなっち(日向秀和)の行のみにして、Stream#mapで各行をカンマ(,)区切りしたときの1番目の項目のみを抽出した新たなStreamにして、最後にSream#forEachで内容を全部標準出力する。
結果は以下の通りになる。
ストレイテナー Nothing's Carved In Stone FULLARMOR
「ひなっちが一番最初に加入したバンド」を探す
なんかちょっと不思議だが、このCSVだとFULLARMORになる(テナー正式加入の2004年より前の2002年にFULLARMORを結成していることになるからだ)
Path path = Paths.get("StreamMaterial1.csv"); try(Stream<String> stream = Files.lines(path)) {Optional<String> result = stream .filter((line)->line.contains("日向秀和")) .min(new Comparator<String>() { @Override public int compare(String s1,String s2) { LocalDate joinDate1 = LocalDate.parse(s1.split(",")[2]); LocalDate joinDate2 = LocalDate.parse(s2.split(",")[2]); return joinDate1.compareTo(joinDate2); } }); result.ifPresent(System.out::println);
} catch(IOException ioe){
ioe.printStackTrace();
}
まずFiles#linesでファイル内の各行を要素とするStreamインスタンスとして取り出す。
その後、filter*1でひなっちだけに絞り込んだStreamにして、
最後にminメソッドで(加入年度が)「一番小さい」要素を探し出す。
加入年度はCSVのカンマ区切りで3番目(配列要素で言えば2番目)に位置するので、compareメソッド内でそれを取り出し、LocalDateにparse。
あとはLocalDateに備え付けのcompareToで比較するだけ。
結果はOptional<Sting>で返ってくるので、ifPresentの引数にSystem.out::printlnのメソッド参照を引き渡し、標準出力する。
結果はこうなる↓
FULLARMOR,日向秀和,2002-12-01
Stream APIは勿論、try-with-resource文、Localdate、ラムダ式、メソッド参照等を幅広く使えて個人的に満足している習作である(何)
余談だが、minのところをmaxに変えるとナッシングス(2008-11-15加入が最大)になる。
「一番多くのバンドに属している(兼任している)人」を探す
これもひなっちなのだが(というかひなっちになるように意図的にCSVをそう作ったのだが)、いくつか探したけど、一発でドーンと取れるようないい感じのメソッドが見当たらなかったので、streamを2回使ってやりくりしている。
恐らく俺が知らないだけで他にもいろいろなやり方があると思われる。
Path path = Paths.get("StreamMaterial1.csv"); try(Stream<String> stream = Files.lines(path)) { // (1)元データ全体を「メンバー別参加バンド数」で集約する Map<String,Integer> map = stream .collect(Collectors.groupingBy( (line)->line.split(",")[1] , Collectors.summingInt( (line)->{return 1;})) );<span class="c-green">// (2)「参加バンド数」の最大を探す</span> Integer max = map.values().stream().max((i1,i2)->i1.compareTo(i2)).get(); <span class="c-green">// (3)(1)のMap全要素に対して(2)の値を持っているentryを標準出力</span> map.forEach((k,v)->{ if(v.equals(max)){ System.out.println(k + "," + v); <span class="c-green">//日向秀和,3 </span> } });
} catch(IOException ioe){
ioe.printStackTrace();
}
(1)やりたかったのはStream#collectで引数にCollectors#groupingByを使っているところだ。
Collectors#groupingByはCSV各行のカンマ区切りで1番目、つまり「メンバー名」で、Collectors#summingIntで、集約するたび毎回1を返し、それをサマリする。これにより「メンバー数のカウント」を実現する→これが「参加バンド数」になる。
これはSQLでいうところの
select member_name,sum(1) join_band_count from csv group by member_name
これとだいぶ感覚が近いといえるだろう。
(個人的にはgroup byでcountとるときってsum(1)よりcount(1)使うことのほうが多いんだけど、まあいいや)
(2)では、(1)の結果として受け取ったMapのうち、「参加バンド数」にあたる値の要素群だけを取り出して(Map#values)、streamにして(Set#stream)、最大値を算出している(Stream#max)。
これで「参加バンド数」の最大値を得る。
(3)で、最後に(1)のMapをforEachで全要素ナメて、(2)の最大値に該当する要素を標準出力する。
結果はこうなる↓
日向秀和,3
ちなみに余談だがCSVに以下の行
MONOEYES,細美武士,2015-01-01 MONOEYES,toddy,2015-01-01 MONOEYES,一瀬正和,2015-01-01 MONOEYES,スコット,2015-01-01
を加えると、細美武士の参加バンド数が3になって、ひなっちの参加バンド数と1位タイになり、結果にひなっちと細美武士が2つ表示されることになる。
(実験のため、結果が一つになるようにしたかったので、あえてMONOEYESは用意しなかった)
余談ついでにさらに言うならば
KILLING BOY,木下理樹,2010-01-01 KILLING BOY,日向秀和,2010-01-01 KILLING BOY,大喜多崇規,2010-01-01 KILLING BOY,伊東真一,2010-01-01
を加えると今度はひなっちの参加バンド数が4になり突出して1位に返り咲く。
もっと言うならばHH&MM(松下マサナオ)、ゴールドシルバーブロンズ(ホリエアツシ、柏倉隆史)、と加えていくともうキリがなくなってくる…のでやめる。(本旨ともずれるw)
UNIXコマンドでパイプ繋ぎまくるのと似てるなと思った件
Stream APIは、「パイプライン」という単語が公式APIに出てくることからも、UNIXコマンドでパイプを繋げて標準出力を整形していくのと、感覚的にはちょっと近い気がした。(これよりもう少し融通の利くバージョンの簡易実装法というか)
たとえば
cat temp.txt | grep "hogehoge" | cut -d',' -f2 | sort -u
は、Stream APIで書くと
try (Stream<String> stream = Files.lines(Paths.get("temp.txt"))) { stream.filter( (line) -> line.contains("hogehoge")) .map( (line) -> line.split(",")[1]) .sorted() .distinct() .forEach(System.out::println); } catch(Exception e) { e.printStackTrace(); }
と(基本的には)同様である。
grep "hogehoge"がStream#filter、cut -d',' -f2がString#split+Stream#map、sort -uがStream#sorted、Stream#distinctに相当する。
UNIXコマンドのほうは標準出力を次のコマンドの入力にしているが、Stream APIは標準出力等使わずAPIの連鎖でそれを実現しており、当然だがこの点において両者は全く別のものである。
しかし、一つのソース(元データ)全体に対して、特定の操作を行うことで別のデータを導き出す、という一連の操作と、そのための「絞り込み」や「切り出し」の観点は、UNIXコマンドとStream APIで良く似てるな、と感覚的に思ったのである。
アサーション
条件式には「こうあるべき」を書き、メッセージには「こうあるべきのくせに、こうなってなかった場合のメッセージ」を書く。
つまり条件式がfalseになったとき、メッセージが発行されるのだ。
例えば
int a = 10; assert a == 10 : "int a is not 10"; // aが10かどうかチェックし、10じゃないときメッセージ assert a != 10 : "int a is 10"; // aが10じゃないかチェックし、10のときメッセージ
上記のコードだと発行されるアサーションはa!=10のほうになる(a==10はすり抜ける)
条件にはa==10って書いておきながら、メッセージをその真逆にしなければならないという、一行の間でtrueとfalseが混在する点が直感的な理解に結びつかず、最初は「???」だった。
まあ原点に立ち戻ってみれば当たり前のことだったが。(というかメッセージをこういう風に書いてなければ多分「???」にもならなかったと思うんだが)
なお、実行に際しては、-eaオプション(Enable Assertの頭文字を付けた意味らしい)をつけないとアサーションは機能しない、ということに気を付けなければならない
(上のコードの例でいえば、-eaオプションをつけないと、2行目のassertにすらヒットせず、完全スルーされて先に進む)
java -ea AssertTest
ちなみにどうでもいいことだが、「絶対ひっかかる条件」をハードコーディングで書いてもコンパイルできるし、実行したら(-ea付けてる前提だが)絶対そこでAssertionErrorが飛んでくる。
assert 1 == 2 : "1 != 2... of cource!";
これは以前試した「Exceptionを強制throwさせて絶対そこで処理を中断させる」の新しい形式かもしれない(Assertionをその目的に使うのはどうなの、っていう気もするが。。。)
日付/時刻API
java.util.Dateとjava.text.SimpleDateFormatで時が停滞しているレガシー人間なので、苦労した。
実態として、未だにそっちのレガシー形式のほうが馴染みが強い…
これを学んでて唯一いいなと思ったのは、日付/時刻系APIはjava.timeパッケージ配下に属しているため、「ああ、これでjava.sql.Dateとjava.util.Dateで混在してコンパイラに文句言われないで済むな」と思ったことくらいか。。。
(RDBと接続するツールとか作ってるとしばしばあって、そのときだけjava.util.Dateもしくはjava.sql.Dateといちいちフルパッケージで書かないといけないことにモヤモヤがあった)
LocalDate
java.util.Dateと違ってnewしない。
現在日付を取りたい場合はLocaldate#nowを使う。
LocalDate now = LocalDate.now();
任意の日付を指定したい場合はLocalDate#ofを使う。
LocalDate ld = LocalDate.of(2019,12,25);
余談だが、javascriptでDateオブジェクトをnewするときに似た形式を指定できるが、こっちはなぜか月が0~11という仕様のため、例えば10月の日付オブジェクトを取得したい場合に9と記述する必要があったが、LocalDateにはそれがない(10月なら10を渡せばよい)ので、わかりやすくて良かったね、という余談。
Localdate#parse
文字列をparseしてLocalDateを生成する場合、デフォルトではyyyy-MM-dd形式で変換をかける。
この形式以外は例外が発生する。
LocalDate ld1 = LocalDate.parse("2019-12-25"); System.out.println("ld1=" + ld1);
これはOKだが
LocalDate ld1 = LocalDate.parse("2019/12/25"); System.out.println("ld1=" + ld1);
これはNG。
ちなみに発生するExceptionはjava.time.format.DateTimeParseException: である。
自由形式の日付を変換かける場合、java.time.format.DateTimeFormatterというクラスを使う。
LocalDate ld1 = LocalDate.parse("2019/12/25" , DateTimeFormatter.ofPattern("yyyy/MM/dd"));
System.out.println("ld1=" + ld1);
日付の加減算
java.util.Dateのときには、足したい日数をミリ秒にして、同じく元日付のほうもミリ秒にして、両者を足して、あとでオフセット分引いて、もう一回Dateにする、とか面倒なことやったのだが(参考)、こっちは簡単で直感的なメソッドが用意されている。
加算はLocalDate#plusDays、減算はLocalDate#minusDaysで行う。いずれも引数はlong型。
LocalDate ld1 = LocalDate.of(2019,12,26); LocalDate ld1p = ld1.plusDays(3L);System.out.println(ld1p); // 2019-12-29
LocalDate ld2 = LocalDate.of(2019,12,26);
LocalDate ld2m = ld2.minusDays(3L);System.out.println(ld2m); // 2019-12-23
仲間にLocalDate#plusWeeks、LocalDate#plusMonths、LocalDate#plusYearsがある。(minusXxxxも同様にある)
日時も扱えるLodalDateTimeクラスにも同様のメソッドがあり、加えてplusHoursとかplusMinutesとかplusSecodsとかも揃っている。
ちなみに、Oracle DBのADD_MONTHS関数が変に気を使ってくれるせいでミスった過去があるので、近い名前のLocalDate#plusMonthsには少し警戒が必要と思って実験してみた。
LocalDate ld3 = LocalDate.of(2019,2,28).plusMonths(1L); System.out.println(ld3); // 2019-03-28 ←3/31にならないLocalDate ld4 = LocalDate.of(2019,3,31).plusMonths(1L);
System.out.println(ld4); // 2019-04-30 ←4/30のままにしてくれる
oracleのADD_MONTHS関数は、「引数が月末日の場合、1か月後を指定すると、翌月の月末日が返ってくる」という仕様のため、2019-02-28+1か月でなぜか2019-03-31が返ってくるという動きだったが、1つ目の例を見る限りでは、そんなことはなく、安心した。
2つ目の例は、2019-03-31+1か月=2019-04-30となり、これについては「翌月末日」になってはいるが、「翌月に同日が存在しない場合の動作」であり、上述したADD_MONTHS関数の変な仕様を指しているものではないし、これは個人的に直感と合っているのでわかりやすい。
期間の計算
LocalDate#untilもしくはPeriod#betweenというのを使う。
LocalDate ld1 = LocalDate.of(2008,11,15); LocalDate ld2 = LocalDate.of(2009, 2,27);System.out.println(ld1.until(ld2)); // P3M12D
System.out.println(Period.between(ld1,ld2));// P3M12D
これはナッシングスがバンドを結成したと思われる2008-11-15(November 15th)から初ライブとなる2009-02-27までの間の期間を求めている(何度も言うがJavaの勉強教材として取り上げただけで情報の精緻さは求めていない)
しかしPeriodオブジェクトをそのまま標準出力に渡すと、結果の内容が若干分かりづらい。
3Mというのが「3か月」、12Dというのが「12日」を示しているようで、要するに「3か月と12日間」ということのようだ。
まあ、これでもなんとなくわかるから、いっか、というのもある(DateもDateFormatで加工しなくてもなんとなく日時を知る分には十分の標準出力が出る)が、少しわかりやすく加工してみると
Period p = ld1.until(ld2);
System.out.println(
p.getMonths() + "か月と" +
p.getDays() + "日"
); //3か月と12日
という風にもできる。
旧時代のAPIと比較する相互変換
旧時代はDateFormat#xxxが変換のキモだったが、新時代においては、いずれもLocalDate(Time)#xxxが変換のキモを担っている点が相違点である。
変換 | 上段:旧API実例 下段:新API実例 |
備考 |
---|---|---|
文字列→日時 | ■旧APItry { DateFormat df = new SimpleDateFormat("yyyyMMddHHmmss"); Date date = df.parse("20191229123456"); } catch(ParseException pe) { pe.printStackTrace(); } ■新API DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); LocalDateTime ld = LocalDateTime.parse("20191229123456",df);; |
旧APIは上述の通り DateFormat#parseの周りを try-catchで囲わないと コンパイルエラーになったが 新APIのほうは必要ない (新APIのほうも DateTimeParseExceptionという Exceptionを投げるが これはRuntimeExceptionの 仲間なので catchは必須ではない) |
日時→文字列 | ■旧APIDateFormat df = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); String dtstr = df.foramt(new Date()); ■新API DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"); String dtstr = LocalDateTime.now().format(df); |
ちなみに、JDKのバグがあるらしく、「SSS」をつなぎ文字なしでそのまま使うと、ミリ秒として取り扱ってくれない。(実行時に謎のExceptionが出る)
参考:https://qiita.com/Pekotaro/items/5b65718a27f47a42148d
例えば以下の例はNG。
DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"); LocalDateTime ld = LocalDateTime.parse("20191229123456789",df);
こんな↓Exceptionを投げつけられてしまう。
java.time.format.DateTimeParseException: Text '20191229123456789' could not be parsed at index 0 at java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:1949) at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1851) at java.time.LocalDateTime.parse(LocalDateTime.java:492) at LocalDateStudy4.main(LocalDateStudy4.java:17)
index 0とかいうからyyyyのほうを探ったのだが全然違った。
わけのわからんことを。。
下記のように、「SSS」の手前に別の文字をいれておくと、ちゃんとミリ秒として扱ってくれる。
DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyyMMddHHmmss.SSS"); LocalDateTime ld = LocalDateTime.parse("20191229123456.789",df);
java.nio.file.うんたら
java.io.*ばっかり使っていた人だったので、このパッケージのことはまるでわからん。
未だにjava.io.*の方が使い勝手がいい人である。
とりあえず基本的なところから使い方を学習した。
java.nio.file.Path
最初、java.io.Fileの代わりに出てきた新しいクラスかなあ、程度の理解でいたが、全然違うようだ。
java.io.file.Pathはjava.io.Fileよりもっと手前の状況(ファイルアクセスする前)に応じるためのクラス、という感じだ。
java.io.Fileのように、既にファイルかディレクトリを指すことを前提としているのと異なり、java.nio.file.Pathはあくまで「ファイルかディレクトリに至るまでのパス文字列」が主軸であり、Pathそのものに何か具体的なファイルやディレクトリの存在を仄めかす意味を保持しないようだ。
だからPathにはFile#existsみたいなファイルやディレクトリに直接アクセスするメソッドは持たず、パスを組み立てたり操作したりするメソッドだけが提供されている。
…という理解である。
PathはPaths#getでオブジェクトを生成する。
Path path = Paths.get("/hoge/huga.txt");
Pathの作り方にはほかにjava.nio.file.FileSystemsを使って
FileSystem fs = FileSystems.getDefault(); Path path = fs.getPath("/hoge/huga.txt");
というのもあり、Paths#get(前者)とFilesystem#getPath(後者)で生成されたPathインスタンスはequalsメソッドでお互いtrueを返す。
Path#iterator、Path#getNameCount
最初見たとき「おっ、もしかしてPathオブジェクトにワイルドカード使えば、それに見合うPathを見つけてきてくれたりするのか」と適当なこと考えていたがそんなことはなかった。
パス文字列をパスセパレータで区切って反復してくれるだけだ。
Path path = Paths.get("D:\\test\\test.java");for(Iterator<Path> it = path.iterator(); it.hasNext(); ){
System.out.println(it.next());
}
// test
// test.java
Windowsのパスを示す場合、ドライブを示す文字列、上記で言えば「D:\\」が返ってこない。
そして、Iteratorの要素数はPath#getNameCountと等しい。
つまり上記の例の場合、Path#getNameCountは2を返す。
一方、これがLinux系のパス文字列の場合は、ルート直下のディレクトリも含めて全部返ってくる。
要するに以下のような例の場合
Path path = Paths.get("/mnt/d/test/test.java");for(Iterator<Path> it = path.iterator(); it.hasNext(); ){
System.out.println(it.next());
}
// mnt
// d
// test
// test.java
となる。
この例だと、Path#getNameCountも4が返ってくる。
基本的には「パスを示しているだけの単なる文字列情報」に過ぎないようなので(この辺はjava.io.Fileと同じ)、上記のコードもWindows OS上で全然問題なく実行可能である。
java.nio.file.Files
java.io.Fileのパワーアップ版という感じ。
使ってみて思ったが、なかなか便利である。
速攻でInputStreamとかを返してくれるメソッドが標準用意されているのはありがたい。
PathsはPathをつくるためのgetメソッドしか存在していない非常にサッパリとしたクラスのため、同じ関連で、FilesもFileつくるためのサッパリクラスなのかと思いきや、API見ると圧倒される。
Paths→Pathの関係と異なり、FilesはFileを作るだけではない(というかむしろそれができなくなっていて、返してくれるのは大体Pathになっている)、最早その枠を超えて「すごい便利なユーティリティ」というイメージである。
パス文字列を渡して一度生成したオブジェクトでいろいろやるFileと異なり、インスタンスは生成できず、やりたいことの度に毎回メソッドにPathオブジェクト(っていうか要するにパス文字列)を渡す、というのがFileと決定的に違うところである。
例えばBufferedReaderを例にとると、旧時代だと
File file = new File("hoge.txt"); Bufferedeader br = new BufferedReader(new InputStreamReader(new FileInputStream(file),"UTF-8"));
とか、いちいちこんなことして生成していたBufferedReaderが
Path path = Paths.get("hoge.txt");
Bufferedeader br = Files.newBufferedReader(path);
これで済むようになっている。
とても便利だ!(今更、て感じなんだろうが)
なお、Files#newBufferedReaderで生成するBufferedReaderは、API曰く、デフォルトで文字コードにUTF-8が設定されるらしい。
この辺にもちょっと時代の流れを感じる(Node.jsはデフォルトUTF-8だったりするし)
java.nio.file.Files#readAllLines、java.nio.file.Files#lines
java8からはFiles#readAllLinesやlinesという便利なメソッドが追加されており、これ一発使うだけでファイルの全内容を読み込んで返してくれる。
readAllLinesは戻り値がList<String>、linesは戻り値がStream<String>という違いがある。
「改行コード指定箇所がないから、どうせWindowsでやったらLFオンリーの行は改行してるとみなしてくれないんだろうな…」と思ってたらAPIにしっかり書いてあってスイマセンデシタという気持ちである。
CRLFもLFのみもCRのみも全部そこで改行してるとみなしてくれる。
例えば以下のようなCRLF、LF、CR混在のファイルも、それぞれを「1行」とみなしてListにしてくれる
文字 | 改行コード |
---|---|
あいうえお | [CRLF] |
かきくけこ | [LF] |
さしせそ | [CR] |
便利になったもんですなあ~。(?)
Files#walk、Files#list、Files#walkFileTree
Files#walk、Files#listは両方ともStream<Path>を返すが、walkのほうは自分自身及び配下のサブディレクトリとそこに格納されているすべてのファイルを再帰的に読み込んでくれるが、listのほうは自分自身を含まず、かつ直下にあるファイルやディレクトリだけしか取得しない。
File#walkFileTreeは他の2つと違いPath単品を返すが、第二引数にFileVisitorインタフェースを実装したクラスを渡すことで、指定したパスにアクセスした瞬間の動作を細かく指定できる。
というか簡単に実装してみた感じでは"いちいち指定しないとメソッド自体呼び出せない"という煩わしさのほうが際立つ(せめて関数型インタフェースだったらラムダ式で簡略できるのだが、このインタフェースは4メソッド持っているのでラムダ式が使えないし、4つ全部なんか実装しないとコンパイル自体できないのだ)
そんな人のために(?)SimpleFileVisitorというできもののクラスが既に用意されているので、普通はこっち使ったほうが楽だと思われる。
とはいえ、結局のところ「Pathオブジェクト返すだけのメソッド」に過ぎず、裏側でどんな処理をガリガリしてようが結果はただのPathなのだ。
例えば引数にディレクトリを指定すると、戻ってくるのはディレクトリを指すPathだが、メソッドの実行過程で、ディレクトリ配下にどんなディレクトリ/ファイルがあるか?を探ることができる、っちゃあできるが、それ必要か?というのが第一印象だった。
個人的にはちょっと使いどころがわからんメソッドである(まあこれは使いこなせていないだけだろうが…)
java.io.Console
標準入力からデータ受け付けるアプリケーション作ってた時は、set /p(Windows)とかread [環境変数名](Linux)で読み込めばいいや、と、結局Javaの外に逃がしてしまってることが多かったが、それ専用のクラスが用意されていたようだ。
java.io.Consoleは、System#consoleでインスタンスを生成する(コンストラクタを呼んでつくるわけではない)
Console con = System.console(); String line = con.readLine(); System.out.println(line);char[] password = con.readPassword();
for (char c : password) {
System.out.print(c);
}
Console#readLineやConsole#readPasswordを記述している行では、処理が止まり、標準入力からの何かしらの入力を待つ。
また、Console#readPasswordでは標準入力の内容を入力中に表示させないようになっている。
なお、Console#readLineはStringを返すが、Console#readPasswordはchar[]を返す点に注意が必要である(なんで違うんだろう…)
これ自体は1.6からあったらしいが、知らなかったなあ…
ず~っとset /pとかに頼っていた(;'∀')
並行処理
ExecutorService
Executors#newFixedThreadPoolの引数に、実行するスレッド数を指定して、ExecutorServiceを生成する、というのはわかったのだが、たとえば引数に2を指定して3つのスレッドをexecuteとかさせても全然文句言われず、「なんの意味があるんだこれは?」と一瞬迷ったが、API見て解決した。
要するにそのスレッド数分の処理が完了するまでは次のスレッドの処理開始に着手しない、ということのようだ。
例えば以下のようなコードではr3(3番目にexecuteするRunnableクラス)はr1かr2の実行完了まで実行開始しない。
public static void main(String... args) {ExecutorService es = null; try { Runnable r1 = new CustomRunnable("Runnable1"); Runnable r2 = new CustomRunnable("Runnable2"); Runnable r3 = new CustomRunnable("Runnable3"); es = Executors.newFixedThreadPool(2); es.execute(r1); es.execute(r2); es.execute(r3); } catch(Exception e) { e.printStackTrace(); } finally { es.shutdown(); }
}
static class CustomRunnable implements Runnable{
DateTimeFormatter df = DateTimeFormatter.ofPattern(“yyyy/MM/dd HH:mm:ss.SSS “);
String name;
public CustomRunnable(String name){
this.name = name;
}
@Override
public void run() {
System.out.println(LocalDateTime.now().format(df)+ this.name + " START”);
try {
Thread.sleep(2000);
} catch(Exception e) {
//nothing to do
}
System.out.println(LocalDateTime.now().format(df)+this.name + " END”);
}
}
ちなみに以下のような実行例になる。
初期スレッド数の2に収まっているr1とr2は同時開始だが、r3はr2の終了後にようやく開始していることがわかる。
2019/12/15 21:43:45.391 Runnable2 START 2019/12/15 21:43:45.391 Runnable1 START 2019/12/15 21:43:47.394 Runnable2 END 2019/12/15 21:43:47.394 Runnable3 START 2019/12/15 21:43:47.395 Runnable1 END 2019/12/15 21:43:49.396 Runnable3 END
余談だがExecutors#newFixedThreadPoolの引数に0以下の負数を指定するとIllegalArgumentExceptionが発生する。
Callable
Runnableの仲間?にCallableがある。
両者の違いは主に以下。
- Runnableはjava.langにいるのでimport句不要だが、Callableはjava.util.concurrent配下にいるのでimportするかフルパッケージで記述するかしないと使えない。(まあ普通はimportするんだろう)
- Runnable#runはvoidなので戻り値が指定できず、かつ例外をスローできないが、Callable#callは戻り値を型パラメータで指定可能で、かつ例外もスローできる。
Callableのほうが後から出てきている分柔軟に使えそうではある。
Fork/Join
ForkJoinPoolのインスタンスを作って、ForkJoinPool#submitにForkJinTask/ForkJoinActionを渡す、というのが基本的な流れのようだ。
ForkJoinTask、ForkJoinActionは両方ともabstractクラスで、それぞれにさらにRecursiveTask、RecursiveAcionという子供のabstractクラスがある。
ただし、ForkJoin~系はcompute()、exec()、getRawResult()、setRawResult()合計4つのabstractメソッドを持つが、Recursive~はcomputeだけがabstractメソッドになっている(他3つのメソッドはfinal付で定義済になっている。finalなのでこれ継承したクラス作っても定義を上書きできない)。
computeメソッドがスレッド処理の実態になる。
まあスレッドを動かしたいだけなら最低限コレ(compute)さえあれば事足りるだろ?ってことなのか。
要するにちょっと試したい、程度の軽い気持ちで使うならRecursive~継承するほうが楽。
RecursiveTaskとRecursiveActionの違いは、戻り値の定義。RecursiveTaskはGenericsで指定できるが、RecursiveActionはvoidになっていて、戻り値が指定できない。
この差異はCallableとRunnableの関係性に似ている。
とりあえずRecursiveAcitonで実行してみた結果
private static final DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss.SSS"); class CustomFJTask<T> extends RecursiveTask<T> { private T name; private String id; public void setId(String id) { this.id = id; } public String getId() { return this.id; } protected T compute() { try { System.out.println(LocalDateTime.now().format(df) + ":" + getId() + ":START:"); Thread.sleep(1000); System.out.println(LocalDateTime.now().format(df) + ":" + getId() + ":E N D:"); } catch(Exception e){ e.printStackTrace(); } return this.name; } }
これがForkJoinTaskの定義。
別に大したことは一切していない。
呼ばれたら標準出力にログ出して、1秒待って、またログ出して終了。
一方このForkJoinTaskの実行コードは
List<CustomFJTask<String>> taskList = new ArrayList<CustomFJTask<String>>(); for (Integer id : Arrays.asList(1,2,3)) { CustomFJTask<String> task = new CustomFJTask<String>(); task.setId(String.valueOf(id)); taskList.add(task); }ForkJoinPool fjp = new ForkJoinPool(3);
System.out.println(LocalDateTime.now().format(df) + “MAIN:SUBMIT START:”);
List<Future<String>> futureList = new ArrayList<Future<String>>();
for(CustomFJTask<String> t : taskList) {
futureList.add(fjp.submit(t));
}
System.out.println(LocalDateTime.now().format(df) + “MAIN:SUBMIT E N D:”);
System.out.println(LocalDateTime.now().format(df) + “MAIN:FUTURE START:”);
for(Future<?> f : futureList) {
try {
f.get();
} catch(Exception e) {
e.printStackTrace();
}
}
System.out.println(LocalDateTime.now().format(df) +“MAIN:FUTURE E N D:”);
こんなかんじ。
ここでは3つのForkJoinTaskをつくってListに詰め込み、作り終わった後に拡張for文で回しながら連続でForkJoinPool#submitする(ForkJoinPool#submitの引数にForkJoinTaskを渡すのだ)
ForkJoinPool#submitは戻り値がFutureというオブジェクトなので、submitの実行後に返ってくるそのFutureオブジェクトをまたListに詰め込む。
ForkJoinPool#submitは渡したタスクの処理完了を待たずにFutureを返してくるので、submitは一瞬で終わるのだが、処理結果を取り出すFuture#getはタスクの処理が完了するまで待機する。
結果的に実行結果は以下の通りとなる。
2020/01/08 15:44:01.071MAIN:SUBMIT START: 2020/01/08 15:44:01.074MAIN:SUBMIT E N D: 2020/01/08 15:44:01.074:2:START: 2020/01/08 15:44:01.074:1:START: 2020/01/08 15:44:01.074MAIN:FUTURE START: 2020/01/08 15:44:01.074:3:START: 2020/01/08 15:44:02.075:1:E N D: 2020/01/08 15:44:02.075:2:E N D: 2020/01/08 15:44:02.077:3:E N D: 2020/01/08 15:44:02.078MAIN:FUTURE E N D:
ちょっと時刻表示のフォーマット整形に失敗したがw、要するにsubmitは一瞬で終わって、Future#getは全てのスレッドが終わるまで完了しないことを確認している。
submitの時点で処理を並列実行する形になっているので、実際の表示は実行するたびに変わる。
CyclicBarrier
並行して動いているいろんなスレッドの足並みを一度ここで揃えましょうよ、という処理機構。
並行して動くスレッド内にCyclicBarrier#awaitを仕込むことで、事前に定義した数が出そろうまで、各スレッドがそこで止まる。
例えば以下のようなクラスを考えてみる。
Runnable barrierAction = () -> System.out.println(LocalDateTime.now().format(df) + "Barrier Action Execute!"); CyclicBarrier cb = new CyclicBarrier(5,barrierAction);class MyRunnable implements Runnable {
private long waitTime;
private String name;
public MyRunnable(long waitTime,String name) {
this.waitTime = waitTime;
this.name = name;
}
public long getWaitTime() {
return this.waitTime;
}
public String getName() {
return this.name;
}
public void run() {
try {
System.out.println(LocalDateTime.now().format(df) + getName() + “:START”);
Thread.sleep(getWaitTime());
cb.await();
System.out.println(LocalDateTime.now().format(df) + getName() + “:E N D”);
} catch(Exception e){
e.printStackTrace();
}
}
}
上の例だと「5つのスレッドが出そろう(new CyclicBarrier(5,..))まではここで待機する(cb.await)処理」を作っている。
このMyRunnableクラスは、コンストラクタでlong値(=待機時間)とString(=名前、ログ用であってあまり深い意味はない)をもらってインスタンスを生成する作りになっており、呼び出し側から複数の異なる「待機時間」を渡すことで、それぞれCyclicBarrierにたどり着くまでの処理時間を調整することを想定している。
実際以下のような形で実行してみる
List<Thread> list = new ArrayList<Thread>(); for (Long l : Arrays.asList(1L,2L,3L,4L,5L)) { Thread t = new Thread(new MyRunnable(l*1000,"No."+String.valueOf(l))); list.add(t); t.start(); } }
上のコードでは、「処理時間1秒のNo.1」から「処理時間5秒のNo.5」まで、全部で5本のスレッドを作っている。
それぞれのスレッドを生成する際に渡すlong値により、「バラバラの処理時間」を仮想的に実現する(実際には↑のクラスの実装に基づき単にThread#sleepしているだけ)
実行すると以下のような感じになる(並列処理のため、実行するたび結果は変わる可能性がある)
2019/12/28 22:38:21.925 No.4:START 2019/12/28 22:38:21.925 No.5:START 2019/12/28 22:38:21.925 No.2:START 2019/12/28 22:38:21.925 No.1:START 2019/12/28 22:38:21.925 No.3:START 2019/12/28 22:38:26.927 Barrier Action Execute! 2019/12/28 22:38:26.930 No.2:E N D 2019/12/28 22:38:26.929 No.3:E N D 2019/12/28 22:38:26.928 No.4:E N D 2019/12/28 22:38:26.928 No.5:E N D 2019/12/28 22:38:26.930 No.1:E N D
作った5つのスレッドの中で一番処理時間がかかるのはNo.5(5秒)なので、他の4つは、No.5がCyclicBarrierにたどり着くまでは先に進めず、待ち状態になる。
結果、処理開始の22:38:21から約5秒後の22:38:26に「5つ全部が到着した」ことを確認して、CyclicBarrie生成時の第二引数に渡したBarrierActionが発行され、5つが無事に完了した。
CyclicBarrierは引数に「待ちスレッド数」だけを指定するものと、「待ちスレッド数」+「待ちスレッド数を満たすスレッドが到着したときに実行するアクション」の2つを指定する、2種類のコンストラクタがある。
上記は後者。
第二引数のRunnnableは(ラムダ式で書いてるが)バリア突破を示す標準出力を出すだけの簡単なアクションとして定義している。
実際やってみてなんとなく感じたのは、「ちゃんとバリアを突破したことがわかる」程度のすげー簡単なログ用のアクションは仕込んでおいてもいいんじゃないかなと思った(つまり後者のコンストラクタを個人的にはお勧めする)
ちなみに「待ちスレッド数」以下しかスレッドが来なかった場合は永久に待ち続けて処理が終わらなくなる。
例えば「待ちスレッド数」を5にして3つしかスレッド作って流さなかった場合、残り2つが来るまで永久に待ち続けてしまう。
また、一度「待ちスレッド数」まで到達してバリア突破した後は「待ちスレッド数」が0に戻ってまたカウントし直しになるので、例えば「待ちスレッド数」を3にして、4つのスレッドを流すと、最初の3つはバリアで同期してその後無事に完了するが、最後の1つは来るはずもない残りの2つのスレッドを永久に待ち続けてしまう。
気をつけましょう。
その他諸々知識集
実のところこれが一番大事だったりして…
- メソッド内に内部クラスを宣言できる
public void test() { class In { private int id; public In(int id) { this.id = id; } public void setId(int id) { this.id = id; } public int getId() { return this.id; } } In in = new In(); System.out.println(in.getId()); }
- ComparableとComparatorについて。
TreeSetやTreeMapのインスタンスを生成するとき(コンストラクタの引数)に使うのがComparatorで、TreeSetに格納する値やTreeMapのKeyに用いる変数がimplementsしておかないといけないのがComparable。
TreeSetやTreeMapのKeyに格納する変数がComparableをimplementsしてないと、addやputの時点でClassCastExceptionが飛ぶ(コンパイルエラーではなく実行エラーになる点がポイントである)
一方、インスタンス生成時にコンストラクタにComparatorを与えておけば、Comparableをimplementsしてないオブジェクトでも、TreeSetやTreeMapに格納してもExceptionは発生せず、正常に格納できる(その際、コンストラクタに与えたComparatorに従ってソートされる)。
なお、Comparatorは関数型インタフェースなのでラムダ式が使えるが、Comparableは使えない(API見ると関数型インタフェースっぽく見えるんだが…明記がないし、実際使えなかった)
名前 パッケージ 実装メソッド名 用途 Comparable java.lang compareTo TreeMapのキーに使用する値は
これを実装しているクラスである
必要がある
(実装してないと実行時エラー
※コンパイルエラーではない)Comparator java.util compare TreeSet、TreeMapの
コンストラクタに
引数で渡す。
Arrays#sortとか
Stream#sortとかに
引数で渡す。 - Comparator#thenComparingは、元のComparatorのcompareメソッドの比較結果が等しい(元のComparatorの比較ではソートが一意に決定づけられない)場合に呼ぶComparatorを指定する。戻り値もComparatorなので、続けて指定できる。要するに第二(、三、四…)ソートキーを指定してるのと同じ。SQLでいえば
select * from TEST order by A ASC,B DESC,C ASC
のBとかCとかのことを指す。(AがthenComparingを呼び射してる自分自身そのものを指す)
これは
Comparator<String> a = (e1,e2) -> e1.compareTo(e2); Comparator<String> b = (e1,e2) -> e1.compareTo(e2); Comparator<String> c = (e1,e2) -> e1.compareTo(e2);
a.thenComparing(b.reversed()).thenComparing(c);
とイメージ的には同じである。(A,B,CがDB上文字系の項目であることを暗黙の前提にしているが…まあイメージのすり合わせなので、細かいことはいいか) なお、Comparator#reversedは元のComparatorの逆順序を表すComparatorを返す。 - 覚えておこう系メソッドまとめ
カテゴリ メソッド 引数 使い方 備考 基本的なやつだから
覚えておこうね系Arrays#asList 可変長配列 可変長引数で
java.util.Listを生成Arrays#stream 可変長配列 引数にいろんな配列や
Stream化の対象範囲をintで指定し
Streamオブジェクトを取得ArrayListの
コンストラクタSet<T> ArrayListのコンストラクタに
Setを指定できる
出来上がるListの型パラメータはTStream#of 可変長配列 可変長引数で
java.util.stream.Streamを生成Functionや
Consumerを使う
既存クラスの
追加メソッドMap#forEach BiConsumer Map内の各要素(=entrySet)に
引数のBiConsumerを適用する。Map#compute
Map#computeIfPresent
Map#computeIfAbsentK key,
BiFunction第一引数のキーに
対応する値を取り出し、
その値に第二引数の
BiFunctionを適用し、
適用した値で書き換える。各メソッドの違いは、
キーに対応する値が
いるかいないか。
- computeは、
キーに対応する値が
いようがいまいが
問答無用で適用 - computeIfAbsentは、
キーに対応する値が
いなければ適用 - computeIfPresentは、
キーに対応する値が
いれば適用
Map#merge K key,
V value,
BiFunctionキーKに対応する値が
存在しなければVで置き換え
存在すれば値にBiFunctionを適用した
結果で置き換えする。
BiFunction#apply実行時の
第一引数は既存の値
第二引数はmergerの第二引数のVになる「存在しない」には、
「valueがnull」を含むList#replaceAll UnaryOperator List内の各要素を、
引数のUnaryOperatorを
適用した結果で置き換える。UnaryOperatorの型Eは
Listの型パラメータと
一致している必要があるMap#replaceAll BiFunction Map内の各要素を、
引数のBiFunctionを
適用した結果で置き換える。
2つの引数はそれぞれ
各EntryのKeyとValue。List#replaceAllは
UnaryOperatorが引数だが
こっちはBiFunctionである
点に注意。default、
staticメソッド系Predicate#and
Predicte#or
Predicate#negatePredicate Predicate同士を結合した
新たなPredicateを得る。
andが論理積
orが論理和
negateが否定Predicateのdefaultメソッド Function#andThen
Function#composeFunction Function同士を結合する。
andThenは引数のFunctionを
自分の「後」に、
composeは引数のFunctionを
自分の「前」に、
それぞれ結合する。Functionのdefaultメソッド Function#identity なし 常に引数と同じ値を返す。
Comparator#comparingと一緒に
よく使われる。Functionのstaticメソッド - computeは、
- Map#removeは第一引数にKeyだけとる(そのキーのEntryを削除する)ものと、加えて第二引数にValueをとる(そのキーのバリューが引数のValueと一致していたら削除する)ものがある。前者しか使ったことないから知らなかったよ。。
- <? super T>は概念的に?>Tに近い。?が示すのは「Tより親世代の何か」
<? extends T>は概念的に?<Tに近い。?が示すのは「Tより子世代の何か」 - Stream#mapToDoubleとかflatMapToIntとかの類のメソッドの「Double」とか「Int」の部分は戻り値のStreamの型特化型クラスを指している。mapToInt、flatMapToIntの戻り値はIntStreamになる。mapToDouble、flatMapToDoubleだったら戻り値はDoubleStreamになる。
- Files#linesは戻り値Stream<String>、File#readAllLinesは戻り値List<String>
- Integer(とかその辺のプリミティブ型ラッパークラス)はComparableを実装している(→だからTreeMapのキーに利用できる)
- Properties#getPropertyは第二引数に指定したキーに対応する値が取得できなかった場合のデフォルト値を指定することができる。
あと、getPropertyで値が取得できなかった場合でも、nullが返ってくるので落ちたりしません。 - Statement#createStatementは引数0か引数2つのいずれかしかない。
また、引数を取る場合、固定値で決まっていて、
第一引数はResultSet.TYPE_FORWARD_ONLY、ResultSet.TYPE_SCROLL_INSENSITIVE、ResultSet.TYPE_SCROLL_SENSITIVE
第二引数はResultSet.CONCUR_READ_ONLY、ResultSet.CONCUR_UPDATABLE
第一引数はカーソルの移動制御を表し
第二引数はResultSetからのデータ更新の反映有無を表す
第一引数のINSENSITIVEは「スクロール可能だが、基本的に更新反映しない」SENSITIVEは「スクロール可能だが、基本的に更新反映する」 - abstractとstatic、defaultを同時に宣言することはできない(コンパイルエラーになる)。よく考えりゃそりゃそうか。
- 列挙型はinterfaceをimplementsできる。ただし別クラスをextendsはできない
- BasicFileAttributesの作り方。APIより。第二引数にBasicFileAttributes.classを付けるのがポイントだ!
BasicFileAttributes attrs = Files.readAttributes(file, BasicFileAttributes.class);
- Comparator#comparing、reversedはComparatorを戻り値にもつので、メソッドの後ろに続けて書ける(StringBuilder#appendみたいなかんじ)
- try-with-resource文は書いた順の逆にcloseが走る。つまり
try (ResourceA ra = new ResourceA(); ResourceB rb = new ResourceB() ) { ... }
だった場合、rb→raの順番にcloseメソッドが呼び出される。 - InputStreamReader#readは引数なしで呼べるやつ(A)と引数にchar型の配列渡すやつ(B)と2つある。
(A)では戻り値がそのまま1文字(のコード値を表す値)として取り出せるが、(B)で読み込んだ結果は引数に渡した配列に格納される。
ずーっと(B)しか使ったことなかったので(A)を知らなかった。 - ExecutorService#submitはFutureを返すが、戻り値の型を指定できないRunnableも引数に渡せる。この場合Futureの中身はnullになる。
- staticブロックはクラスのロード時に一回だけ実行され、インスタンス生成のたびに実行されるわけではない
- メソッドオーバーライド時、元メソッドと同じExceptionしかthrows宣言できない。ちがうExceptionをthrows宣言するとコンパイルエラーになる
- closeしたReaderに対してReader#readyするとIOExceptionが飛ぶ。
なぜかcloseしててもreadyなら動くと勘違いしていた。そうじゃないのだ。 - Path#resolveは、引数が相対パスなら元パス+相対パス、引数が絶対パスなら引数のパスで上書き
- 列挙型のコンストラクタは暗黙的にprivateになるので外部からは呼び出せない(そうなんだ…)
publicのコンストラクタを定義するとコンパイルエラーになる。 - ResultSet#updateString等で更新をかけたものはResultSet#updateRowを使わないと更新反映されない。
updateString等がトランザクション処理で、updateRowがcommitって感じか。
DBの更新にはいつもStatement#executeUpdate使って(UPDATE文やINSERT文等のDMLを直接投げて)いたので、この辺使ったことない。
処理的にはDMLとは全く違う形式の更新を走らせるんだろうなあ、これ…興味はあるけどまあ今はいいか - Reader#markは引数にint型をとる(マーク以降に読み込める文字数の上限を指定する)。引数なしで呼び出せるmarkメソッドは存在しない。
- LocalDate、LocalTime、LocalDateTimeはimmutable(不変)クラスなので、配下メンバーのメソッドは「メソッド適用後の新しいオブジェクト」を返却するが、元のオブジェクトを変更しない。Function的な挙動をするメソッドだけがあり、Consumerっぽい動きをするメソッドは存在しない、というイメージ。
- IntStreamやDoubleStreamの各Stream APIに引数で渡すFunctionやConsumerは、それぞれのプリミティブ型に特化したヴァージョンの必要がある。IntStreamだったらIntFunctionだし、DoubleStreamだったらDoublePredicateだったり、と。Stream<Integer>でいいじゃん、という理屈が通用しない、結構ガチガチなルールが存在している。
- Stream#flatMapは入れ子になっている各要素を一回呼ぶごとに一段階バラす。というか「一段階しか」バラしてくれない。このため複数入れ子になっている要素を持っている場合、その数だけflatMapを呼ばないと、本当の意味で「平坦」にはならない。
Stream<List<List<List<List<String>>>>> stream = Stream.of( Arrays.asList( Arrays.asList( Arrays.asList( Arrays.asList( "AAA", "BBB", "CCC", "DDD", "EEE" ) ) ) ) );
stream.flatMap(v->v.stream()).forEach(System.out::println);
// [AAA, BBB, CCC, DDD, EEE]
この場合、「Stringの要素を持つListを要素に持つListを要素に持つListを要素に持つList」がStreamの各要素になっており(もう意味が分からん)、一回のflatMapで分解できるのは一番外側のListの分のみなので、forEachで標準出力してみると、まだ3段階入れ子のListがあることがわかる。 - メソッドのオーバーライドは、元クラスよりアクセス修飾子を広げることはできるが、狭くすることはできない。強さの順はprivate→デフォルト→protected→public。この順番でならオーバーライドで可視性を広げることができる。
なのだが、privateの場合は(基本的に)継承して可視性を広げるのは無理。同一クラス内にInnerクラスとして両者の定義があって、横並びで中身が見れるようになってる場合のみ、広げられる。(というかそういうケースではprivateにする意味があまりないような気もするが…)
例えば
public static void main(String[] args) { class Test1 { int get1() {return 1;} private int get2() {return 2;} protected int get3() {return 3;} public int get4() {return 4;} } class Test2 extends Test1 { public int get1() {return super.get1();} public int get2() {return super.get2();} public int get3() {return super.get3();} public int get4() {return super.get4();} }
System.out.println(new Test2().get2());
}
こういうのは可能。Test1#get2はprivateだが、Test2#get2がオーバーライドしてpublicおっぴろげ状態にしてしまっており、Test1#get2のprivateが実質意味をなさくなくなっている。
Test1とTest2が横並びで同じJavaファイル内のInnerクラスとして定義があるのでこういうのも問題なくコンパイルできる。
Test1とTest2が物理的に違うクラスファイルとして分離されていると、当たり前だがprivateを少しでも広げようとする行為は許されず、コンパイルエラーになる。
余談だが、上記のケースにおいて、Test1にprivateのフィールド変数(仮にここではtとする)があっても、new Test1().tとかで普通に覗くことができる(privateの意味が全くない…)
*1:line)->line.contains("日向秀和"