【Java】メソッド参照とラムダ式の関係
Java 8以降には、メソッド参照というものがある。全くなじみがなく今まで触れずにいたが、調べてみるとラムダ式をさらに簡略化した記法らしい。
引数をパススルーしているかどうか
Javaをメソッド参照に置き換えられるのは、主にパラメータをパススルーしているかどうかで判断できる。
例えば e -> System.out.println(e)
というラムダ式があったとき、パラメータeはそのまま System.out.println
の引数として渡されている。このとき、eに対しては何の処理もされておらず、ラムダ式はただ単にeを受け取ってそのまま別のメソッドに引数として受け渡すことしかしていない。
このような場合、ラムダ式をメソッド参照で置き換えられることが多い。上記の例であれば、 System.out::println
と書くことができる。パラメータの値に対して何か変化がもたらされるわけではないので、これだけで何をしているか十分に理解することができる。
コンストラクタをメソッド参照に置き換える
メソッド参照に置き換えられるのは、staticメソッドだけではない。パラメータをパススルーしていれば、コンストラクタも置き換えることができる。
例えば、 i -> new Card(i)
というように、受け取ったパラメータを使って新しいオブジェクトを作るラムダ式があった場合、それは Card::new
というように書くことができる。
jqで少し複雑なjsonを検索+ソートする
以前の記事で、jqを使って小さな困りごとを解決したことを書いたが、あれから実際にjqをいろいろな場面で使い始めている。とはいえ最初はけっこう試行錯誤したので、実際に使ったコマンドを忘れないように記録しておきたい。
jqとは
以前の記事でも書いたが、jqはJSONデータに特化したsedコマンドのようなもので、JSONの特定のキーの値を使った検索・ソート・置換などが簡単にできる。(以前同じようことをsedやawkでやろうとしたことがあるが、かなり手間がかかったのでおすすめはしない)
JSONで吐かれる大量の生データ・ログデータを集計したいときなどに非常に重宝する。
今回はサンプルとして以下のようなJSONファイルを作ってみた。1行ごとにJSONが1つ吐き出される形式だ。
# test.json {"x":"hoge","y":"foo","s":{"a":true,"timestamp":"1557626945"}} {"x":"fuga","y":"bar","s":{"a":false,"timestamp":"1557626721"}} {"x":"hoge","y":"bar","s":{"a":true,"timestamp":"1557625712"}} {"x":"fuga","y":"foo","s":{"a":false,"timestamp":"1557626978"}} {"x":"hoge","y":"bar","s":{"a":true,"timestamp":"1557624928"}}
特定の値で検索・置換する
基本的に、jqを使うときは操作の基準となるキーを指定すれば良い。例えば、上のファイルでyの値がbarである行だけを取り出すときは以下のようになる。
$ jq -c '. | select(.y == "bar")' test.json {"x":"fuga","y":"bar","s":{"a":false,"timestamp":"1557626721"}} {"x":"hoge","y":"bar","s":{"a":true,"timestamp":"1557625712"}} {"x":"hoge","y":"bar","s":{"a":true,"timestamp":"1557624928"}}
入れ子の場合も、感覚的に指定できる。以下はaがfalseのもののみを取り出している。boolean型などもよしなに変換してくれるようだ。
$ jq -c '. | select(.s.a == false)' test.json {"x":"fuga","y":"bar","s":{"a":false,"timestamp":"1557626721"}} {"x":"fuga","y":"foo","s":{"a":false,"timestamp":"1557626978"}}
値を置き換える場合、新しい値を指定するだけでよい。
$ jq -c '. | select(.s.a == false) | .s.a = true' test.json {"x":"fuga","y":"bar","s":{"a":true,"timestamp":"1557626721"}} {"x":"fuga","y":"foo","s":{"a":true,"timestamp":"1557626978"}}
特定の値でソートする
ソートする場合、やり方が少し複雑になる。jqでは sort_by
を使うことで特定のキーの値でソートできるのだが、普通にやろうとするとエラーになる。
$ jq -c '. | sort_by(.s.timestamp)' test.json jq: error (at test.json:1): Cannot index string with string "s" jq: error (at test.json:2): Cannot index string with string "s" jq: error (at test.json:3): Cannot index string with string "s" jq: error (at test.json:4): Cannot index string with string "s" jq: error (at test.json:5): Cannot index string with string "s"
これは、sort_by
を使うときの入力値は配列でなければいけないため。
そこで、ファイルを指定する際に --slurp
オプションをつけることで、入力値を配列にでき、ソートが可能になる。その後に.[]
と指定すれば、今度は配列を外すことができ、出力を1行ごとに戻すことができる。
$ jq -c '. | sort_by(.s.timestamp) |.[]' --slurp test.json {"x":"hoge","y":"bar","s":{"a":true,"timestamp":"1557624928"}} {"x":"hoge","y":"bar","s":{"a":true,"timestamp":"1557625712"}} {"x":"fuga","y":"bar","s":{"a":false,"timestamp":"1557626721"}} {"x":"hoge","y":"foo","s":{"a":true,"timestamp":"1557626945"}} {"x":"fuga","y":"foo","s":{"a":false,"timestamp":"1557626978"}}
逆に、selectを配列の状態で使うことはできない。
$ jq -c '. | select(.y == "bar") |.[]' --slurp test.json jq: error (at test.json:5): Cannot index array with string "y"
検索とソートを一緒にやりたいときは、まずソートを行ってから、配列を外して検索すると良いだろう。
$ jq -c '. | sort_by(.s.timestamp) |.[] | select(.y=="bar")' --slurp test.json {"x":"hoge","y":"bar","s":{"a":true,"timestamp":"1557624928"}} {"x":"hoge","y":"bar","s":{"a":true,"timestamp":"1557625712"}} {"x":"fuga","y":"bar","s":{"a":false,"timestamp":"1557626721"}}
これらを組み合わせることで、JSONを簡単に加工することができる。出力を確認するテスト等の時に大いに役立つだろう。
Javaのジェネリクスを理解する
最近Javaのドキュメントを読む機会が多いが、APIの中では普段の仕事であまり使わないジェネリクスが使われていることが多く、たちまち読めなくなってしまう。時間のあるうちに、ジェネリクスの理解を深めておきたい。
ジェネリクスはなぜ生まれたか
今では当たり前のように使っている List<String>
のような表記だが、最初からあったわけではない。Java5以前は、コレクション内の要素の型を1つに指定する術はなかった。コレクションのadd/getメソッドは、それぞれの引数や戻り値をObject型として取り扱うようになっていたため、要素を取り出すときはキャストするようにしていたそうだ。
しかしその場合、コレクション内に別の型の要素を混入させることができる。さらに悪いことにコンパイルは通ってしまい、実行時にエラーが起こることになる。せっかくの型情報を活かせず、Object型としてしか扱えないことを、当時は「型の損失」と言っていたようだ。
public class Main { public static void main(String[] args) { List students = new ArrayList(); students.add(new Student(1, "John")); students.add("hoge"); // Student型でない要素を混入させられる Student s0 = (Student) students.get(0); Student s1 = (Student) students.get(1); // 実行時にキャストに失敗して落ちる } } class Student { int studentId; String name; public Student(int studentId, String name) { this.studentId = studentId; this.name = name; } }
この問題を解決するために、ジェネリクスが導入された。宣言に一つ以上の型パラメータを持つクラスやインターフェースが登場したのである。例えばListインターフェースの正式な名前は List<E>
となっており、型パラメータEを指定することで、List<String>
やList<Student>
などを定義できる。
非境界ワイルドカード
List<?>
のように定義すると、任意の型のリストを取り扱うことができる。
public class Main { public static void main(String[] args) { List<String> strList = new ArrayList<>(Arrays.asList("a", "b", "c")); List<Integer> integerList = new ArrayList<>(Arrays.asList(1, null)); System.out.println(getSize(strList)); // 3 System.out.println(getSize(integerList)); // 2 } static int getSize(List<?> list) { //strList, integerListの両方を引数に渡せる return list.size(); } }
単にList
とすることとの違いは、このList<?>
にはnull以外の一切の値を追加できないことにある。このため、異なる型の要素が混入することはない。そのかわり、List<?>
に何の型が入るのかコンパイラは知らないため、Object型以外の型を指定して値を取り出そうとするとコンパイルエラーとなる。size()
などリスト内の要素の型に関わらない、List
境界ワイルドカード
List<? extends Student>
とすると、リストに入る型がStudent自身かそのサブタイプに制限される(すべての型は自分自身のサブタイプでもあるため、Student型も含まれる)。例えば、StudentのサブタイプであるJuniorとSeniorという型があるとき、以下のようにStudent型で要素を受け取ることが可能となる。ただし 依然としてリストへの追加は不可能であることに注意。
public class Main { public static void main(String[] args) { List<Junior> juniorStudents = new ArrayList<>(Arrays.asList( new Junior(1, "John"), new Junior(2, "Jack") )); List<Senior> seniorStudents = new ArrayList<>(Arrays.asList( new Senior(3, "Jay"), new Senior(4, "Jude") )); System.out.println(getFirstStudent(juniorStudents).name); // John System.out.println(getFirstStudent(seniorStudents).name); // Jay } static Student getFirstStudent(List<? extends Student> students) { return students.get(0); //要素をStudent型で受け取れる } } class Student { int studentId; String name; public Student(int studentId, String name) { this.studentId = studentId; this.name = name; } } class Junior extends Student { public Junior(int studentId, String name) { super(studentId, name); } } class Senior extends Student { public Senior(int studentId, String name) { super(studentId, name); } }
なぜList<Student>
だけでサブタイプを扱えないかというと、List<Junior>
はList<Student>
のサブタイプとはみなされないため。もしそうでない場合、同じList<Student>
型のリストにJunior型とSenior型を混在させることができてしまう。 境界ワイルドカードは、一つのリストに型を混在させないようにしつつ、複数のリストを抽象的に扱う工夫がなされた方法といえる。
なお、逆に List<? super Junior>
という書き方をすれば、Junior自身かそのスーパータイプのみに要素の型を制限できる。
ジェネリックメソッド
先程の境界つきワイルドカードは引数の型を柔軟に扱うためのものだったが、メソッド定義の時点でジェネリック型を扱いたいときもある。最も単純なジェネリックメソッドは以下のようになる。
public class Main { public static void main(String[] args) { List<String> strList = new ArrayList<>(Arrays.asList("a", "b", "c")); List<Integer> integerList = new ArrayList<>(Arrays.asList(1, null)); System.out.println(getSize(strList)); // 3 System.out.println(getSize(integerList)); // 2 } static <T> void printElem(List<T> list) { for (T e in list): System.out.Println(e); } }
ジェネリックメソッドは、ジェネリックではないクラスで使われる前提があるため、まず<T>
で型パラメータを宣言する。このメソッドは、list
の要素の型に関わらず、要素のサイズを取得して返している。
なお、クラスの定義の時点でジェネリック型を使いたい場合は、 class StudentList<T extends Student>
のように書くことができる。
?とTの違い
ここまで見ると、ワイルドカードと型パラメータは似た働きをするように思えるが、いくつか違いもある。
例えば、以下のようなジェネリックメソッドは動作する。
public <T> List<T> moveFirstElem(List<T> list, List<T> anotherList) { anotherList.add(list.get(0)); return anotherList; }
しかし、以下は動作しない。ワイルドカードが使われたリストに要素を追加することはできないためである。
public List<? extends Student> moveFirstElem(List<? extends Student> list, List<? extends Student> anotherList) { anotherList.add(list.get(0)); return anotherList; }
逆に、型パラメータを使う場合は <T super SuperType>
のようにsuper
を使った指定はできない。
きれいに整形されたjsonを1行に戻す方法
JSONを読みやすく整形するツールはいろいろなIDEやウェブサイトにあるが、元に戻すツールはあまりない。しかし、例えばJSON形式のデータを1行ごとに処理するシステムのテストデータを作るときなどは、可読性の高い状態でJSONを作っても、それを1行に戻さなければいけない。
例えば以下のようなファイルがあったとする。
# sample.json { "userId": 1, "id": 1, "title": "delectus aut autem", "completed": false }
このとき、jq -c
を使うと1行の状態に戻すことができる。jqコマンドがない場合は brew install jq
でインストールできる。
-c
は--compact-output
の略であり、出力を1行にまとめてくれる。
$ jq -c < sample.json {"userId":1,"id":1,"title":"delectus aut autem","completed":false}
jqコマンドは、JSONを扱いやすくする多機能なコマンドであり、-c
オプションを使う機会はあまりないかもしれない。しかし自分の場合、これをきっかけにjqコマンドに入門し、今ではAPIを叩くときやJSONの特定の値を置換するときなどにフル活用している。JSONに手を加えたいという人はぜひ試してほしい。
「ワンストップ見積もり本」で見積もりのやり方を学んだ
先日の技術書典6で、親方Projectさんが販売されていた本です。 ちょうどこの前、見積もりが全然できておらず目標を達成できなくて怒られたばかりなので、買ったその日に真剣に読みました。
この本は、プロジェクトやタスクにかかる時間・工数をどう見積もるかについて書かれた本です。15人の著者の方が、それぞれの工夫や経験談などを寄稿されています。
なぜ自分が今まで見積もりができなかったかというと、見積もりというものをこんな風に捉えていたからでした。
- 何が起こるかわからないのに、かかる時間なんてわかるわけなくない?
- 結局適当な時間を答えることになるんだから、あんまり意味ないよね
しかしこの本を読んで、自分の認識がいかに甘かったかに気付かされました。確かにどれほど考えても想定外の事態は起こりうるのですが、だからといって誰も何も考えていないわけではないのです。
見積もりの要点
この本を一通り読んだ今、見積もりの要点は以下の4つかなと考えています。
- 執拗なまでにタスクを細分化する
- 最短時間・最長時間の両方を考える
- 実際に何にどれくらい時間がかかったか記録する
- 1~3のログを貯めることで見積もりの精度を上げる
まずタスクの細分化。これは普段作っているチケットよりも細かい粒度で考えます。例えば「ある機能を実装する」というタスクであれば、「必要な処理の流れを書き出して整理する」「整理した流れに沿ってコードを書く」「テストコードを書いて実行・修正」などに分けられます。しつこいくらいに細かくすることで、時間を見積もりやすくなるうえ、次に何をすればいいかがわかりやすくなり、すぐに取りかかることができます。
また、時間の見積もりも単にエイヤ!で行うのではなく、超うまくいったときの時間(達成確率20%)と、これを超えたら恥ずい時間(達成確率80%)の2つを考えると良いです。これを二点見積法と言うそうです。実際にかかった時間が早すぎず遅すぎず、二点の中間くらいになると、見積もりが良かったと言えます。
最後に、実際にかかった時間を記録して振り返ることで、次からの見積もりの精度を上げていくことができます。私は3章で紹介されていた、erukitiさんの見積もりシートをほぼそのまま使い始めました。まだ1週間ほどですが、見積もり時間を大きく超過したときの振り返りが溜まってきており、自分にとってどんな行動がアンチパターンなのかが少しずつ見えてくるようになっています。
この本をきっかけに、見積もりの技術を考えるようになり、自分の仕事の仕方が大きく変わりつつあります。早くも良い収穫を得ることができました。
@kakakakakkuさんのブログメンタリングを卒業しました
1月から3月まで、@kakakakakkuさん(カックさん)のブログメンタリングを受けました。
メリークリスマス!新規ブログメンティを募集しまーす!今回の募集人数は1-3名です.メンタリング開始日は 1/1 (火) です.
— カック@ブロガー / k9u (@kakakakakku) 2018年12月16日
・ネタをどう探すか?
・どう習慣化するか?
・どう読まれる記事を書くか?
・などなど
を話しながらブログ以外のアウトプットにも挑戦してもらおうと思っています.
3ヶ月間、週1回など事前に決めたペースで記事を公開し、カックさんからアドバイスをいただくことができます。 今のところなんと無料。
ちなみに以下がカックさんのブログです。質の高い記事が定期更新されていてとても勉強になります。
メンタリングを受けた背景
自分がブログを続けている最大の理由は、アウトプットを習慣化することで今までの道のりが見えるようにし、自分に自信をつけたいと思っているからです。
もともと、自分の意思だけで習慣化するのが難しいということはよくわかっていました。そのため、ブログ執筆者が集まるSlackグループ(2週間記事を書かなければ退会になる)に加入し、強制力が働く環境で記事を書いていました。
ただ、2週間を意識するあまり、続けることだけが目的になるのもよくないと思い、記事を書くときの意識・技術を高めたいと考えていました。そのときカックさんのメンタリングを知る機会があり、思い切って応募した次第です。
3ヶ月のメンタリング期間では、執筆の心構えからちょっとした小技まで、多くのことを学びました。中でも、これから自分がブログを続けていくうえで大きな糧となった学びをいくつか書いていきます。
見える化がモチベーションにつながる
メンタリングが始まってまず最初にやったことは、Search ConsoleとGoogle Analyticsの設定でした。そして、毎週末にPV数やブクマ数・Twitterフォロワー数などのKPIを確認して送ることになりました。
今思えば、これはとても大事な作業でした。今までは設定作業が面倒くさかったこともあり、PV数などはあまり気にしておらず、自分をやる気にさせるものは自分の成長意欲と締切だけだったのです。それが、今週は500PVだった、というようにアクセスが見える化され、それが増えていく推移を目にすることで、自分の記事を誰かが見てくれているんだという事実を実感でき、他者への貢献という外向きのモチベーションが生まれました。 もちろん、イベント参加レポなど自分の出来事を報告する記事も書きますが、「将来同じことを学ぶ誰かのために」と思って記事を書いている時が一番楽しいです。
ちなみに、KPIを確認するのも面倒くさい... という人のために、同じ時期のメンティである@budougumi0617さんがスクリプトを作ってくださいました。神。
仕事と執筆を両立させるために
ブログを書けなかったときの言い訳として最も多いのが「仕事が忙しかったから」だと思います。しかし、この言い訳はメンタリング期間中絶対に言ってはいけません。ブログというのは学んだことのアウトプットであり、学びって仕事と関係なく常にやるべきだよね?というのがカックさんの考え方です。
そのため、どの時間を使って記事を書くか?というのを意識するようになりました。今までは、まとまった時間ができたら「どれ、記事でも書くか...」という感じだったのですが、今は電車に乗っている時間やちょっとした待ち時間で、モバイルアプリから記事を書くようになりました。記事の構成が頭にあれば、PCの前にいなくてもコード以外の部分は充分書けます。
自ら学びに行き、ネタをつかむ
ブログをやる上で最もきついのがネタ探しです。今回のメンタリングでは、最初に20個分ネタ出しをすることで、小さな学びでも記事にできることを体得する取り組みがありました。
しかしそれ以上に得られたのが、ネタを探しに行く姿勢です。メンタリング期間中、カックさんもご自身のブログを更新され続けていたのですが、その多くは書籍のレビュー・最近使ったツールの紹介・新しい技術の学習メモなどでした。どうしてカックさんはこんなにいろいろなものと巡り会えるんだろうと不思議だったのですが、自分がネタを探しているうちに、ネタが降ってくるのを待つのではなく、意識して新しいことに挑戦しネタを掴みに行くことが大事なんだと気付きました。もちろん平日は仕事で忙しいのですが、例えば繰り返しの作業をシェルスクリプトでやってみたり、社内で先輩に質問したことを記事のテーマにしたりなど、日々の仕事からもネタを掘り出しに行く姿勢が身につきつつあります。
メンタリングで執筆習慣を確立させよう
振り返ればとても充実した3ヶ月間でした。ブログを書きたいけどなかなか続かないという人には、このメンタリングをぜひおすすめしたいです。記事を書く習慣とモチベーションがきっと確立すると思います。
4-6月募集は終わってしまいましたが、3ヶ月に1度のペースで募集がかかっているので、興味のある方はカックさんのTwitterアカウントをフォローしておくと良いと思います。
KafkaをDockerの外から叩けないときに確認すべき設定
Dockerで立ち上げたKafkaを、ホスト環境でビルドしたJavaアプリケーションから叩こうとしたのだが、なぜかエラーになって叩けず。
java.lang.IllegalStateException: No entry found for connection 21729123512
よく調べてみると、advertised_listenerの設定を忘れていたことに気がついた。
advertised_listenerとは
KafkaのコアであるKafka brokerはListenerを持っている。クライアントからListenerに接続があると、そのListenerは自身がアクセスできるIPアドレスまたはホスト名を返し、それを使ってその後の処理が行われる。
しかし、Kafkaとアプリケーションが同じ環境にあれば全く問題ないが、Dockerを使っているとこれが問題になりうる。ListenerはDockerのインターナルネットワークの中にあるので、返ってくるホスト名もインターナルネットワークのものになってしまうのである。
現に、エラーが起きているKafkaに、ホスト環境からkafkacatで接続してみると以下のようになった。(kafkacatは、Kafka版netcatとして開発されたCLIであり、こういった接続確認などを行うときとても便利に使える)
$ kafkacat -b localhost:9092 -L Metadata for all topics (from broker -1: localhost:9092/bootstrap): 1 brokers: broker 0 at cd572956787b:9092 29 topics: ...
broker 0 at cd572956787b:9092
に注目。このcd572956787b
とはKafkaのDockerコンテナのホスト名である。Dockerコンテナのインターナルネットワークのホスト名が返ってきてしまうために、ホスト環境ではこの名前解決ができず、その後のKafka brokerとの接続に失敗するわけだ。
具体例のついたより詳しい説明が、この記事に書かれている。
advertised_listenerを設定する
対応としては、Kafkaに外部ネットワークからアクセスできるような設定をすることが必要となる。server.properties
内のadvertised.listeners
がそれにあたる。これは外部ネットワークからアクセスする場合用に、Zookeeperに公開されるListenerの設定をするための項目になっている。
ここにadvertised.listeners=PLAINTEXT://<IP>:9092
と設定すれば良い。なおこのIPはlocalhostでも192.168始まりのプライベートIPアドレスでも動作したが、kafka-dockerのREADMEによると、localhostだと複数のbrokerを動かすときに不具合が発生するとのことなので、プライベートIPアドレスを使う方が良いかもしれない。
landoop/fast-data-devを使っている場合はもっと簡単で、READMEにあるとおり、環境変数としてADV_HOST=<IP>
を指定して起動させれば良い。
プライベートIPアドレスを指定して起動させ、再びkafkacatを叩いてみる。
$ kafkacat -b localhost:9092 -L Metadata for all topics (from broker -1: localhost:9092/bootstrap): 1 brokers: broker 0 at 192.168.58.109:9092 29 topics: ...
broker 0 at 192.168.58.109:9092
に変わっていることが確認できた。