- 投稿日:2019-12-14T22:16:37+09:00
ArrayList
個人的な勉強のためまとめました
ArrayList
何らかの集合のことをコレクションと呼び、配列もそのうちの一つである
Javaには、もっと簡単にコレクションを扱うために、コレクションAPIやコレクション・フレームと呼ばれる複数のインターフェイスや、クラスで構成されるクラス群がある
これらのインターフェイスやクラスの多くはjava.utilパッケージに配置されており、java.util.ArrayListクラスは動的配列とも呼ばれ、配列のように使えるArrayListクラスの特徴
①オブジェクトであればどのような型でも使える
②必要に応じて要素数を自動的に増やす
③追加した順に並ぶ
④nullも値として追加できる
⑤重複した値も追加できる
⑥スレッドセーフではない①、②の特徴は、配列の問題点をカバーするもの
③は配列のように値を番号で管理するため、追加した順に値が並ぶリスト構造である。ただし、任意の場所に挿入することもできる
⑥は並行処理したときに意図しない結果になることを防ぐ機能がついているかどうか
※スレッドセーフなリストを使いたい場合はjava.util.Vectorを使うジェネリックス
ArrayListの値を追加するaddメソッドの引数や、値を取り出すgetメソッドの戻り値はObject型になっているため、何でも扱うことができる
しかし、値が混在するコレクションはダウンキャストするとき例外が発生する可能性がある
そこで、型を指定することで、コレクションが扱える型を制限するジェネリックスという機能がある例)ジェネリックス
ArrayList<String> list=new ArrayList<>;
指定している型のことを型パラメータ(例だとStringの部分)、ジェネリックスをより簡潔に書けるようにダイヤモンド演算子<>(例だと右の<>)という機能がある
この機能により、、インスタンスへの参照を保持する変数がどのような型変数で宣言しているかをコンパイラが判断し、同じ型を使ってインスタンス生成時の型変数を決めれる。これを型推論と呼ぶ
- 投稿日:2019-12-14T21:43:25+09:00
JavaでExpression Problemを解決する方法
Expression Problemとは
http://maoe.hatenadiary.jp/entry/20101214/1292337923
静的型付き言語で、再コンパイルなしで、新しいデータ型を追加したり、新しい関数を追加できるようにするにはどうしたらよいか
という問題である。
何も考えずにクラスを設計すると、
1. データ構造は追加しやすいが操作が追加しにくい
2. 操作は追加しやすいがデータ構造は追加しにくい
のどちらかになりやすい。
データ構造も操作も追加すくするにはどうしたらいいか、というのがExpression Problemの問題意識である。Javaでの解決方法
HaskellやOCamlやScalaなどで高度な言語機能を用いた解法が既に有名(?)だが、Javaの言語機能でも同様にExpression Problemが解決できることを示す。
- サブタイピング+戻り値の共変性を使う方法
- サブタイピング+Genericsを使う方法
上記2種類の方法が存在する。この記事では2の方法を紹介する。
例題
まずは単純なインタプリタを例にして考える。コード簡略化のためLombokを使用しているので注意。
import lombok.AllArgsConstructor; import lombok.Data; interface Exp { int eval(); } @Data @AllArgsConstructor class Add<E extends Exp> implements Exp { private E e1; private E e2; @Override public int eval() { return e1.eval() + e2.eval(); } } @Data @AllArgsConstructor class Lit implements Exp { private int x; @Override public int eval() { return x; } }AddクラスのフィールドがEという型パラメータになっているところがミソ。
操作の追加
このクラスに対してprint操作の追加を行う。
interface ExpP extends Exp { String print(); } class LitP extends Lit implements ExpP { LitP(int x) { super(x); } @Override public String print() { return String.valueOf(getX()); } } class AddP<E extends ExpP> extends Add<E> implements ExpP { AddP(E e1, E e2) { super(e1, e2); } @Override public String print() { return getE1().print() + " + " + getE2().print(); } }既存クラスをいじらずに問題なく追加できた。
クラスの追加
Sub(引き算)を表すクラスを追加する。
@Data @AllArgsConstructor class Sub<E extends Exp> implements Exp { private E e1; private E e2; @Override public int eval() { return e1.eval() - e2.eval(); } } class SubP<E extends ExpP> extends Sub<E> implements ExpP { SubP(E e1, E e2) { super(e1, e2); } @Override public String print() { return getE1().print() + " - " + getE2().print(); } }どちらの場合でも、既存クラスを修正せずに操作の追加/クラスの追加が行えている。
これらを使う側のクラスは以下のようになる。public class Main { public static void main(String[] args) { new Add<Exp>(new Lit(1), new Lit(3)).eval(); new Sub<Exp>(new Lit(1), new Lit(3)).eval(); new AddP<ExpP>(new LitP(1), new LitP(3)).print(); new SubP<ExpP>(new LitP(1), new LitP(3)).print(); } }高い拡張性が必要な状況(データ構造と操作の両方を拡張することが予想される)では、このデザインパターン(名称不明?)を覚えておいて損はないのではないか、と思う。
なお「サブタイピング+戻り値の共変性を使う」方法の方にも興味がある方は、下記の参考文献を参照されたし。(※この記事の内容は下記論文の丸パクリです)参考文献
- 投稿日:2019-12-14T18:04:22+09:00
Processingで作った作品をProcessingで作ったプレゼンスライドで動かす
はじめに
Processingで作った作品を,Processingで作ったプレゼンスライドにそのまま埋め込んでプレゼンしたくないですか?
本記事で実現したいと思います(需要があるのかは知らない).
ここでいう「プレゼンスライド」は,記事Processingで作るプレゼンスライドで作ったものを指します.
この記事を読む前にご覧ください.こんなのを作ります.
Processingで作ったスライドに作品を埋め込むことができた(4ページ目) pic.twitter.com/fFtBqLiINz
— ohayota (@ohayoooota) December 14, 2019プレゼンスライドのソースコード
今回作ったプログラムをGitHubに上げています.
ご自由にお使いください.
PresentationSlideProcessing実装
Processingで作るプレゼンスライドのプログラムに,クラスを追加実装して実現しようと思います.
(全クラスに微変更を加えていますが,時間と余力がないため割愛します.GitHubに上げているプログラムをご参照ください.)
(後でリライトします)Canvasクラスの実装
作品のコードを実行する部分は,Canvasクラスとして実装します.
1作品1関数として実装したいと思います....そして作っておいたものがこちらです(唐突)(不親切).
Canvas.pdeclass Canvas { int workNum; // 描画する作品を指定する番号 float sizeX; float sizeY; float x; float y; color back; boolean isBaseDrew = false; // work1用の変数 float[] r = {750, 750, 750}; Canvas(int workNum, float sizeX, float sizeY, float x, float y, color back) { this.workNum = workNum; this.sizeX = sizeX; this.sizeY = sizeY; this.x = x; this.y = y; this.back = back; } void resetValues() { isBaseDrew = false; // work1用の変数初期化 for(int i = 0; i < r.length; i++) r[i] = 750; // work2用の変数初期化... } // 透明な円を重ねていく void work1() { noStroke(); if (r[0] > 0) { float randomX = random(-sizeX/6, sizeX/6); float randomY = random(-sizeY/6, sizeY/6); fill(255, 30); ellipse(randomX, randomY, r[0], r[0]); r[0] -= 75; } if (r[1] > 0) { float randomX = random(-sizeX/6, sizeX/6); float randomY = random(-sizeY/6, sizeY/6); fill(0, 0, 0, 10); ellipse(randomX, randomY, r[1], r[1]); r[1] -= 90; } if (r[2] > 0) { float randomX = random(-sizeX/6, sizeX/6); float randomY = random(-sizeY/6, sizeY/6); fill(228, 0, 127, 20); ellipse(randomX, randomY, r[2], r[2]); fill(0, 161, 233, 20); ellipse(-randomX, -randomY, r[2], r[2]); r[2] -= 50; } } //void work2() { // // 作品のプログラムを移植する(置換: width->sizeX & height->sizeY) //} void drawBase() { fill(0); noStroke(); rect(x-sizeX/2, y-sizeY/2, sizeX, sizeY); } void drawFrame() { noFill(); strokeWeight(5); stroke(mainColor); rect(x-sizeX/2, y-sizeY/2, sizeX, sizeY); } void draw() { if (!isBaseDrew) { drawBase(); isBaseDrew = true; } drawFrame(); // 表示する作品をcaseで指定する pushMatrix(); translate(x, y); switch(workNum) { case 1: work1(); break; //case 2: // work2(); // break; default: } popMatrix(); } }
work1()
内に,自分で適当に書いたコードを入れておきました.
作品を1つの関数内に移植して,作品のグローバル変数を,Canvasクラスの変数として定義しました.
Canvasクラスのインスタンス生成時に指定した番号workNum
をもとに対応する関数が呼ばれ,作品が描画されます.KeyEventに対応する関数の修正
従来の関数では,スライドを1回描画した直後にループを止めていました.
今回は描画を続ける必要があるので,Canvasクラスだけ描画を続けるようにしました.keyEvent.pdevoid keyPressed() { if (!isKeyTyped) { // 表示していたスライドのCanvasをリセットする ArrayList<Canvas> canvases = slides.get(slideNum).textField.canvases; for (Canvas c: canvases) c.resetValues(); switch (keyCode) { case LEFT: if (0 < slideNum) { loop(); slideNum--; } break; case RIGHT: if (slideNum < slides.size()-1) { loop(); slideNum++; } break; default: } isKeyTyped = true; } noLoop(); } void keyReleased() { loop(); isKeyTyped = false; isSlideDrew = false; }
isSlideDrew
でスライドが描画されたかを管理しています.完成したスライド
こちらのツイートに載っている動画のように,4ページ目に作品を載せることができました(枠をオーバーしてますが).
Processingで作ったスライドに作品を埋め込むことができた(4ページ目) pic.twitter.com/fFtBqLiINz
— ohayota (@ohayoooota) December 14, 2019作品コード移植時の注意点
Canvasクラス内に作品のコードを元のまま移植すると,正しい位置に表示されません.
それは元のウィンドウサイズをwidth
やheight
で取得しているためです.
Canvasクラス内では,sizeX
とsizeY
がウィンドウサイズとして対応します.// rect(0, 0, width, height); rect(0, 0, sizeX, sizeY);移植後には,移植コード内の
size()
やbackground()
関数を削除してください.
background()
の部分はrect()
等で代用してください.// size(600, 600); // background(0); fill(0); rect(0, 0, sizeX, sizeY);現状の課題
スライドに作品を描画しましたが,枠を超えて描画された部分が隠せていません...
知見のある方にご教示いただきたいです!おわりに
Processingで作った作品を,Processingで作ったプレゼンスライドに埋め込んでみました.
ぜひ,プレゼンで使ってみてください!
- 投稿日:2019-12-14T18:04:22+09:00
Processingで作った作品を,Processingで作ったプレゼンスライドで動かす
はじめに
Processingで作った作品を,Processingで作ったプレゼンスライドにそのまま埋め込んでプレゼンしたくないですか?
本記事で実現したいと思います(需要があるのかは知らない).
ここでいう「プレゼンスライド」は,記事Processingで作るプレゼンスライドで作ったものを指します.
本記事を読む前にご一読ください.こんなのを作ります.
Processingで作ったスライドに作品を埋め込むことができた(4ページ目) pic.twitter.com/fFtBqLiINz
— ohayota (@ohayoooota) December 14, 2019プレゼンスライドのソースコード
今回作ったプログラムをGitHubに上げています.
ご自由にお使いください.
PresentationSlideProcessing実装
Processingで作るプレゼンスライドのプログラムに,クラスを追加実装して実現しようと思います.
(全クラスに微変更を加えていますが,時間と余力がないため割愛します.GitHubに上げているプログラムをご参照ください.)
(後でリライトします)Canvasクラスの実装
作品のコードを実行する部分は,Canvasクラスとして実装します.
1作品1関数として実装したいと思います....そして作っておいたものがこちらです(唐突)(不親切).
Canvas.pdeclass Canvas { int workNum; // 描画する作品を指定する番号 float sizeX; float sizeY; float x; float y; color back; boolean isBaseDrew = false; // work1用の変数 float[] r = {750, 750, 750}; Canvas(int workNum, float sizeX, float sizeY, float x, float y, color back) { this.workNum = workNum; this.sizeX = sizeX; this.sizeY = sizeY; this.x = x; this.y = y; this.back = back; } void resetValues() { isBaseDrew = false; // work1用の変数初期化 for(int i = 0; i < r.length; i++) r[i] = 750; // work2用の変数初期化... } // 透明な円を重ねていく void work1() { noStroke(); if (r[0] > 0) { float randomX = random(-sizeX/6, sizeX/6); float randomY = random(-sizeY/6, sizeY/6); fill(255, 30); ellipse(randomX, randomY, r[0], r[0]); r[0] -= 75; } if (r[1] > 0) { float randomX = random(-sizeX/6, sizeX/6); float randomY = random(-sizeY/6, sizeY/6); fill(0, 0, 0, 10); ellipse(randomX, randomY, r[1], r[1]); r[1] -= 90; } if (r[2] > 0) { float randomX = random(-sizeX/6, sizeX/6); float randomY = random(-sizeY/6, sizeY/6); fill(228, 0, 127, 20); ellipse(randomX, randomY, r[2], r[2]); fill(0, 161, 233, 20); ellipse(-randomX, -randomY, r[2], r[2]); r[2] -= 50; } } //void work2() { // // 作品のプログラムを移植する(置換: width->sizeX & height->sizeY) //} void drawBase() { fill(0); noStroke(); rect(x-sizeX/2, y-sizeY/2, sizeX, sizeY); } void drawFrame() { noFill(); strokeWeight(5); stroke(mainColor); rect(x-sizeX/2, y-sizeY/2, sizeX, sizeY); } void draw() { if (!isBaseDrew) { drawBase(); isBaseDrew = true; } drawFrame(); // 表示する作品をcaseで指定する pushMatrix(); translate(x, y); switch(workNum) { case 1: work1(); break; //case 2: // work2(); // break; default: } popMatrix(); } }
work1()
内に,自分で適当に書いたコードを入れておきました.
作品を1つの関数内に移植して,作品のグローバル変数を,Canvasクラスの変数として定義しました.
Canvasクラスのインスタンス生成時に指定した番号workNum
をもとに対応する関数が呼ばれ,作品が描画されます.KeyEventに対応する関数の修正
従来の関数では,スライドを1回描画した直後にループを止めていました.
今回は描画を続ける必要があるので,Canvasクラスだけ描画を続けるようにしました.keyEvent.pdevoid keyPressed() { if (!isKeyTyped) { // 表示していたスライドのCanvasをリセットする ArrayList<Canvas> canvases = slides.get(slideNum).textField.canvases; for (Canvas c: canvases) c.resetValues(); switch (keyCode) { case LEFT: if (0 < slideNum) { loop(); slideNum--; } break; case RIGHT: if (slideNum < slides.size()-1) { loop(); slideNum++; } break; default: } isKeyTyped = true; } noLoop(); } void keyReleased() { loop(); isKeyTyped = false; isSlideDrew = false; }
isSlideDrew
でスライドが描画されたかを管理しています.完成したスライド
こちらのツイートに載っている動画のように,4ページ目に作品を載せることができました(枠をオーバーしてますが).
Processingで作ったスライドに作品を埋め込むことができた(4ページ目) pic.twitter.com/fFtBqLiINz
— ohayota (@ohayoooota) December 14, 2019作品コード移植時の注意点
Canvasクラス内に作品のコードを元のまま移植すると,正しい位置に表示されません.
それは元のウィンドウサイズをwidth
やheight
で取得しているためです.
Canvasクラス内では,sizeX
とsizeY
がウィンドウサイズとして対応します.// rect(0, 0, width, height); rect(0, 0, sizeX, sizeY);移植後には,移植コード内の
size()
やbackground()
関数を削除してください.
background()
の部分はrect()
等で代用してください.// size(600, 600); // background(0); fill(0); rect(0, 0, sizeX, sizeY);現状の課題
スライドに作品を描画しましたが,枠を超えて描画された部分が隠せていません...
知見のある方にご教示いただきたいです!おわりに
Processingで作った作品を,Processingで作ったプレゼンスライドに埋め込んでみました.
ぜひ,プレゼンで使ってみてください!
- 投稿日:2019-12-14T16:28:08+09:00
API呼ぶ【コール編】
API呼ぶ
以下4つのパートに分かれています。(〇〇編は適当につけましたゆるして)
API呼ぶ【準備編】
API呼ぶ【コア編】
API呼ぶ【ハンドリング編】
API呼ぶ【コール編】コール編
前回までで、Facadeを呼べばAPI関連の処理まとめてやってくれるように作れました
じゃあFacade呼ぼうservice.javapackage service; import exception.NantokaApiBadResultException; import exception.NantokaApiUnknownException; import facade.Facade; import model.RequestBody; import model.RequestDto; import model.ResponseBody; import model.ResponseDto; public class Service { /** APIFacade */ private static Facade facade = new Facade(); public static void main(String[] args) { System.out.println(service("")); } public static String service(String str) { // ロジック RequestBody body = new RequestBody(); body.setId("28"); body.setName("なんとかさん"); body.setInfo("おなかすきました"); RequestDto request = new RequestDto(); request.setRequest(body); try { // API実行 ResponseDto response = facade.nantokaApiFacade(request); ResponseBody responseBody = response.getBody(); return responseBody.getInfo() + responseBody.getComment(); } catch (NantokaApiUnknownException e) { return e.getMessage(); } catch (NantokaApiBadResultException e) { return e.getMessage(); } } }エラーがシンプルでいいですね
FacadeでExceptionでくくったものと、処理結果がnullの場合を一つのエラーにまとめることができています
あとは結果が正常じゃない場合のエラーをとっています
結果の種類に応じて処理分けたかったら、Exceptionを増やして分岐させていけば対応できますねまとめ
★Client(コア層)はただたんにAPIたたくだけ
★Facade(ハンドリング層)でExceptionを好きなように吐かせる
★Service(コール層)で、分岐した時の処理を実装なかなか綺麗なんじゃないでしょうか!
- 投稿日:2019-12-14T16:27:56+09:00
API呼ぶ【ハンドリング編】
API呼ぶ
以下4つのパートに分かれています。(〇〇編は適当につけましたゆるして)
API呼ぶ【準備編】
API呼ぶ【コア編】
API呼ぶ【ハンドリング編】
API呼ぶ【コール編】ハンドリング編
コア層で実際にAPIをたたいたので、
今回の階層では出てきたエラーをハンドリングしますここでいうエラーとは、Exceptionだけでなく
処理結果が不正である場合も含まれますFacade.javapackage facade; import client.Client; import exception.NantokaApiBadResultException; import exception.NantokaApiUnknownException; import model.RequestDto; import model.ResponseDto; public class Facade { /** 処理結果コード正常 */ private static final String SUCCESS = "0"; /** APIクライアント */ private Client client = new Client(); /** * APIを実行した結果を取得する * * @param request リクエストDTO * @return レスポンスDTO * @throws NantokaApiUnknownException 通信エラーや結果が存在しなっかった場合 * @throws NantokaApiBadResultException 処理結果不正の場合 */ public ResponseDto nantokaApiFacade(RequestDto request) throws NantokaApiUnknownException, NantokaApiBadResultException { ResponseDto response = new ResponseDto(); try { response = client.nantokaApiClient(request); } catch (Exception e) { // 通信系エラーの場合 throw new NantokaApiUnknownException(e.getMessage()); } if (response == null) { throw new NantokaApiUnknownException("なんも返ってこなかったよ"); } if (!SUCCESS.equals(response.getResultCode())) { throw new NantokaApiBadResultException("処理結果不正"); } return response; } }通信エラー系のExceptionの場合も、処理結果がなぜかnullだった場合も、
同じくUnknownException
で返すようにしてみましたまぁこっちの気持ちとしてはなんかよくわかんないエラーなので、まとめたいよね
あと処理結果が不正の場合も、こっちからしたらエラーみたいなものなのでExceptionとして返しましたここまでで、APIを呼ぶ機能は完成したと言えます
あとはこいつを呼び出すだけなので、次回実際に使ってみましょう
- 投稿日:2019-12-14T16:27:44+09:00
API呼ぶ【コア編】
API呼ぶ
以下4つのパートに分かれています。(〇〇編は適当につけましたゆるして)
API呼ぶ【準備編】
API呼ぶ【コア編】
API呼ぶ【ハンドリング編】
API呼ぶ【コール編】コア編
実際にAPIにPOST送信をなげる、コアとなる部分を実装します
client.javapackage client; import javax.xml.ws.http.HTTPException; import json.JsonUtil; import model.RequestDto; import model.ResponseDto; public class Client { /** * APIを実行する * * @param request リクエストDTO * @return レスポンスDTO */ public ResponseDto nantokaApiClient(RequestDto request) { String requestJson = JsonUtil.requestDtoToJson(request); String response = post(requestJson, "http://nantoka.com"); return JsonUtil.responseJsonToDto(response); } /** * POST処理 * * @param requestJson Json変換したリクエスト * @param uri 送信先URI * @return レスポンスJson * @throws 通信エラー系 */ private final String post(String requestJson, String uri) { try { // なんかPOST用のライブラリのやつ return "ClientBuilder.newClient().path(uri).request().post(requestJson);"; } catch (HTTPException e) { throw e; } } }postメソッド内部の実際の
ClientBuilder
のライブラリは今回は省略しました
ハンドリングはもっと表面部分の階層の処理でやりたいため、
今回みたいなコア層では通信系Exceptionをすべてそのままスローするようにしていますやってることはリクエストを投げてレスポンスを受け取る、ただそれだけですね
外部のメソッドを通信して使ってるだけです
通信の際にはJson形式に直して送り、戻ってきたJson結果をDTOに直して返してます
- 投稿日:2019-12-14T16:27:32+09:00
API呼ぶ【準備編】
API呼ぶ
以下4つのパートに分かれています。(〇〇編は適当につけましたゆるして)
API呼ぶ【準備編】
API呼ぶ【コア編】
API呼ぶ【ハンドリング編】
API呼ぶ【コール編】準備編
まずは必要なモデル、Json変換、自作Exceptionを用意します。
- モデル:リクエスト/レスポンスを入れる箱
- Json変換:API通信用にリクエスト/レスポンスをJsonに変換する
- Exception:結果に応じたExceptionを作り、処理の分岐の準備をする
モデル
Getter/Setterは省略してます
リクエスト.javapackage model; public class RequestDto { /** 外枠 */ private RequestBody request; }リクエスト中身.javapackage model; public class RequestBody { /** id */ private String id; /** 名前 */ private String name; /** 内容 */ private String info; }レスポンス.javapackage model; public class ResponseDto { /** 処理結果 */ private String resultCode; /** 内容 */ private ResponseBody body; }レスポンス中身.javapackage model; public class ResponseBody { /** 結果内容 */ private String info; /** コメント */ private String comment; }Json変換
JsonUtil.javapackage json; import model.RequestDto; import model.ResponseBody; import model.RequestBody; import model.ResponseDto; public class JsonUtil { /** * リクエストDTOをJsonに変換 * * @param request リクエストDTO * @return Json変換した文字列 */ public static String requestDtoToJson(RequestDto request) { RequestBody body = request.getRequest(); String json = "{" + "\"request\": {" + "\"id\": " + body.getId() + "," + "\"name\": " + body.getName() + "," + "\"info\": " + body.getInfo() + "}" + "}"; return json; } /** * レスポンスJsonをDTOに変換 * * @param json レスポンスJson * @return 変換したDTO */ public static ResponseDto responseJsonToDto(String json) { json = "{" + "\"resultCode\": 0," + "\"body\": {" + "\"info\": 結果内容これだよ('ω')," + "\"comment\": コメントだよ('ω')" + "}" + "}"; ResponseBody body = new ResponseBody(); body.setInfo(getContent(json, "info")); body.setComment(getContent(json, "comment")); ResponseDto response = new ResponseDto(); response.setBody(body); response.setResultCode(getContent(json, "resultCode")); return response; } /** * Json文字列から情報を切り出す * <p> * XXX 内容にresultCodeの文字列やカンマが入っている場合だめだよねこれw * * @param json Json文字列 * @param content 切出し対象のパラメータ名 * @return 切出した値 */ private static String getContent(String json, String content) { return json.split("\"" + content + "\": ")[1].split(",")[0].split("}")[0].trim(); } }レスポンスのとこはやりやすさ重視ってことで、今回は固定値を入れなおしてます。
まぁライブラリがあると思うのでこれはぶっちゃけ必要ないかも?自作Exception
基底Exception.javapackage exception; public class NantokaApiException extends Exception{ private static final long serialVersionUID = 1L; public NantokaApiException(String msg){ super(msg); } }予期せぬエラー系をまとめるException.javapackage exception; public class NantokaApiUnknownException extends NantokaApiException { private static final long serialVersionUID = 1L; public NantokaApiUnknownException(String msg){ super(msg); } }API処理結果はあるけどエラー結果だったときのException.javapackage exception; public class NantokaApiBadResultException extends NantokaApiException { private static final long serialVersionUID = 1L; public NantokaApiBadResultException(String msg){ super(msg); } }ということで下準備でした
次回こいつらを使ってAPIをたたいていきます
- 投稿日:2019-12-14T06:09:36+09:00
漏れのある抽象化の法則 - False Sharing
テックタッチアドベントカレンダー14日目を担当する@ihirokyです。
13日目は @oyoshikeita による 言葉を使ってプロダクトに生命を吹き込む でした。
何気なく眺めているプロダクトがなぜこんなデザインになっているのかが垣間見れて「あーなるほど」と納得してしまいました。自分は何となくに頼りがちな部分がありますが、デザインするにあたりしっかり考えた裏付けをもちプロダクト開発に当たっていきたいです。さて、テックタッチエンジニア勢の記事はTypeScript/Goが大勢を占めていますが、ネタが無いので心機一転Javaを使ったハードウェアを考えるお話です。6日目の話は忘れました。
今回作成したものは https://github.com/ihiroky/false-sharing に置いてあります。
漏れのある抽象化の法則
私の胸にぶっ刺さっている言葉の一つに、Stackoverflowでお馴染みの Joel Spolsky 氏の名言「漏れのある抽象化の法則」というものがあります1。いろいろな物事は抽象化され、細かいことを気にしなくても扱い安いレベルで機能が提供されています。ただ、その抽象化の内側にいるなにかが特殊な振る舞いをしたとき、一見理解しがたい現象が発生します。これに対処するには結局抽象化がどのように機能し、何を抽象化しているのかを学ぶ必要があります。つまり、抽象化は作業時間を短縮するが学ぶ時間までは短縮してくれません。つまるところ、問題を解決できるエンジニアはすべてを勉強する必要がある、というお話です。ということで、並行処理界隈ではとても有名な Flase Sharing を通して高級なプログラミング言語の上にのっても結局ハードウェアのことを理解していなければその性能はいかせない例を見てみたいと思います。
例題
一つの配列をマルチスレッドで突っつくプログラムを考えます2:
src/main/java/com/ihiroky/FalseSharingArray.javapackage com.ihiroky; import java.util.concurrent.atomic.AtomicLongArray; public class FalseSharingArray implements Runnable { private AtomicLongArray array_; private final int arrayIndex_; private final long iteration_; public FalseSharingArray(AtomicLongArray array, int arrayIndex, long iteration) { array_ = array; arrayIndex_ = arrayIndex; iteration_ = iteration; } @Override public void run() { for (long i = 0; i < iteration_; i++) { array_.set(arrayIndex_, i); } } }src/main/java/com/ihiroky/App.javapackage com.ihiroky; import java.util.concurrent.atomic.AtomicLongArray; public class App { private void runArray(String name, int width) throws Exception { final int NUM_THREADS = 3; final long ITERATION = 500_000_000; var array = new AtomicLongArray(NUM_THREADS * width); Thread[] threads = new Thread[NUM_THREADS]; for (int i = 0; i < threads.length; i++) { threads[i] = new Thread(new FalseSharingArray(array, i * width, ITERATION)); } long start = System.nanoTime(); for (Thread t : threads) { t.start(); } for (Thread t : threads) { t.join(); } System.out.println(name + " - duration: " + ((System.nanoTime() - start) / 1000_000) + "ms."); } public static void main(String[] args) throws Exception { App app = new App(); System.out.println(" === Start warm up === "); app.runArray("array without pad", 1); app.runArray("array with pad", 8); System.out.println(" === End warm up === "); app.runArray("array without pad", 1); app.runArray("array with pad", 8); } }warm upは置いておくと、App#runArray()メソッドを
width
を 1 にした場合と 8 にした場合の二通り呼び出しています。このwidth
は、AtomicLongArrayの長さと、FalseSharingArrayクラスがAtomicLongArrayのどこを参照するかを指定するindexに影響しています。src/main/java/com/ihiroky/App.java (抜粋)// arrayの要素のうち使うのはNUM_THREADS個だがとびとびに使った場合飛ばし多分長さが必要 var array = new AtomicLongArray(NUM_THREADS * width); Thread[] threads = new Thread[NUM_THREADS]; for (int i = 0; i < threads.length; i++) { // FalseSharingArrayが参照するarrayの要素位置をwidthによりまばらにできる threads[i] = new Thread(new FalseSharingArray(array, i * width, ITERATION)); }つまり、FalseSharingArrayがAtomicLongArrayを隙間無く(普通に)使うか、8個おきに使うかの指定です。これを実行してみると:
ihiroky@pclz750hs:~/projects/false-sharing$ gradle run > Task :run === Start warm up === array without pad - duration: 37374ms. array with pad - duration: 13922ms. === End warm up === array without pad - duration: 24741ms. array with pad - duration: 7299ms.となり、widthを8にした場合、widthを1にした場合と比べて3倍以上速くなっています。widthが1か8かに関係なくプログラム上配列要素の共有による競合はありません。ただ間をあけただけなのこの差は不思議ですよね。私は不思議でなりませんでした。
こんなイメージ:
何が漏れている?
この現象は、ハードウェアレベルでみつめ直すとメモリの競合が起きていることが分かります。以下はCPUがメモリ上のデータを使う様子を模式的に表しています:
CPUはデータを扱う際に、メモリより高速なキャッシュ上にデータを一時的にロードします。メモリ上のデータをキャッシュに載せる際に、キャッシュラインを単位としてデータをキャッシュ読み込んだりメモリに書き込んだりしています。また、キャッシュラインのサイズはCPUによりいろいろですが、普段触るx64系は大抵64バイトです。つまりキャッシュラインサイズ分まとめてメモリに対してデータの読み書きが行われます。このため、プログラム上では複数のスレッドによる要素の共有はなくとも、その要素がキャッシュライン上に共存していれば複数のCPUからみると変数の共有が行われている状態になります。ここで、同一キャッシュライン上に存在する二つの数値を2つのCPUがそれぞれキャッシュに読み込んでいる状態で片方がその数値を更新した場合、CPU間で整合性をとる取るためにもう片方はキャッシュの読み込みなおしが必要になる、更に更新が起きた場合は…となると、競合が発生します。これが頻発すると、上記プログラムのように顕著な差となって現れます。
このように、本来共有していないデータをキャッシュ上の同一ラインで共有してしまう状況を「False Sharing」と言います。widthを8にしてFalse Sharingを回避できたのは、キャッシュラインが64バイト、longが8バイトだからです(8バイト x 8個 = 64バイト)。
学びはエンジニアの基礎体力
このようにハードウェアがどうなっているかを理解していないとプログラムの動きを予測することは難しいです。一見遠回りに見えても基本的な計算機の仕組みを含めた基本的な事項、流行りの技術の裏側もしっかり学んでおきたいです。それゆえ工学系出身のエンジニアを非常に羨ましく思う今日この頃。
落穂?拾い
AtomicLongArrayを使っている理由
今回配列を持ち出すのにあえて AtomicLongArray を用いました。今回のネタは通常の配列を用いると(通常の方法では)発生しません。キャッシュの一貫性を保つ処理を挟み込まないからです。ざっくりいうと AtomicほんにゃかArray は配列の各要素に対してvolatile相当の処理もできるようになっているところがミソです。
試した環境のCPUと利用するCPUコア
Intel(R) Core(TM) i7-3517U CPU @ 1.90GHz です。古いですね。2コア/ハイパースレッディング有(4スレッド)です。CPUののキャッシュが分かれていることがことの原因なので、プログラム上で1スレッドしか起こさないようにする、(最近はほぼ見ないですが)1コアしかないCPUで試すと、触る要素の間隔を広げても無駄にメモリを消費するだけでスループットは変わりません。
ちなみにテックタッチに買ってもらった開発PCのCPUは i9-9900K です。爆速です。@jdk.internal.vm.annotation.Contended (旧@sun.misc.Contented)
今回はCPUのキャッシュラインサイズが64バイトと分かっていたので空のlongを7個並べて対処しました。キャッシュラインのサイズはあたりまですが使うハードウェアに依存するため、真っ向から対処しようとすると都度CPUのスペックを確認してコードを書き直す必要があります。都度 configure/make みたいな事をしてもいいんですが、JDKには
@Contented
という建前上JDK内部使用を前提とした変数のメモリ上の配置を制御するアノテーションが存在しています。これをメンバ変数につけておくとJVMがよしなにレイアウト(パディング)してくれます。何もしないと処理系が変数の並びを変えてくる可能性もあるため、真っ向勝負すると JCTools バリに継承とパディングを駆使する必要が出てきます。@Contented
にいては https://www.javaspecialists.eu/archive/Issue249.html あたりが参考になります。そういえばマイクロベンチの JMH もひたすらパディング仕込んできますね。終わりに
複雑化するシステムを駆使して価値を生み出すエンジニアは抽象化された生産性の高い技術を駆使しつつも、問題が発生すれば何がどのように抽象化されているかを把握しなぜ目に見える現象がおこっているかを理解し解決していきます。テックタッチのメンバーは最良のプロダクトを生み出すために常に考えつづけ、技術的な壁を乗り越えるために皆学びがすごいです(あせるくらい)。それゆえ困難も乗り越えられているのかなと感じる次第です。
15日目の担当は @terunuma です。何がでるかな〜。