Java の知識をアップデートしようとして、Essential Java の第三刷を読んでいるのだが、Flyweight
パターンが出てきた。そういえばこのパターンはちゃんとやったことがないので、理解して試してみることにした。
Flyweight パターン
Flyweight パターンは、GoFのデザインパターンの一つで、次のような問題を解くためのパターンである。
- 多くのオブジェクトが効率的にサポートされる
- 多くのオブジェクトの生成を避けたい
定義
- 本質的なステートがシェアできる
- (本質的なステートを区別できる)ステートを投入するインターフェイスがある。
何かこれだけだとよくわからない。
シーケンスを見ると、どうやらキャッシュするパターンのようだ。歴史の項目を見てみると、ドキュメントエディタで使われたようで、フォントの情報とか、例えばアルファベットが26文字だとすると、その都度文字のインスタンスを生成していたら、インスタンスが大量に生成されてしまう。だから、26個だけインスタンスを生成するようにして、それをキャッシュして返す仕組みのようだ。
ギターフレットの音のサンプル
サンプルを作って理解してみよう。要はファクトリーのキャッシュしたものであるので、簡単だ。今回のお題は若干強引だが、ギターの弦の番号と、フレットの番号を入力すると、その場所の音を返すアプリケーションだ。音は12個しかないので、インスタンスは、12個でよいはず。デザインパターンのダイアグラムでいう Flyweight
のオブジェクトとして、Sound
を使ってみる。
Sound.java : Flyweight
package com.company;
public interface Sound {
void Play();
}
SoundImpl : Flywieght1
package com.company;
public class SoundImpl implements Sound{
private String note;
public SoundImpl(String note) {
this.note = note;
}
@Override
public void Play() {
System.out.println(note + "- ♪");
}
public String getNote() {
return note;
}
}
SoundFactory : FlywieghtFactory
package com.company;
import java.util.HashMap;
public class SoundFactory {
private static Map<String, Sound> sounds = new HashMap<String, Sound>();
public static Sound getSound(String note) {
if (sounds.containsKey(note)){
return sounds.get(note);
} else {
Sound sound = new SoundImpl(note);
sounds.put(note, sound);
return sound;
}
}
}
Main が膨れ上がってかっこ悪いが、ともかく、動作するものが出来た。単純でポイントは、Factoryに Map を持たせて、キャッシュしておけば良い。簡単だ。
Main.java
package com.company;
import java.util.HashMap;
public class Main {
private static HashMap<Integer, Integer> stringMap = new HashMap<Integer, Integer>();
private static HashMap<Integer, String> notes = new HashMap<Integer, String>();
private static void setup() {
stringMap.put(1, 4);
stringMap.put(2, 11);
stringMap.put(3, 7);
stringMap.put(4, 2);
stringMap.put(5, 9);
stringMap.put(6, 4);
notes.put(0, "C");
notes.put(1, "C#");
notes.put(2, "D");
notes.put(3, "D#");
notes.put(4, "E");
notes.put(5, "F");
notes.put(6, "F#");
notes.put(7, "G");
notes.put(8, "G#");
notes.put(9, "A");
notes.put(10, "A#");
notes.put(11, "B");
}
public static void main(String[] args) {
setup();
while(true) {
java.io.Console con = System.console();
if (con != null) {
String input = con.readLine("string:fret:");
if (input.contains("exit")) {
System.out.println("Closing ...");
System.exit(0);
} else {
String[] stringFret = input.split(":");
int openNote = stringMap.get(Integer.parseInt(stringFret[0]));
int fret = Integer.parseInt(stringFret[1]);
int note = openNote + fret;
if (note >= 12) {
note = note - 12;
}
String noteString = notes.get(new Integer(note));
Sound sound = SoundFactory.getSound(noteString);
sound.Play();
}
}
}
}
}
Concurrent だとどうなるのか?
本編が簡単だったので、Javaにあまり慣れていないので、もうステップやってみることにした。コンカレントな状態だと、どうすればいいのだろう?HashMap
は、Thread Safe ではないらしい。Thread Safe にしたかったら HashTable
もしくは、ConcurrentHashMap
を使うと良いらしい。いったい何が違うのだろうか?
-
Difference between HashMap and ConcurrentHashMap in Java Collection
こちらの記事を元に整理してみたい。
HashTable と ConcurrentHashMap
-
HashMap
とConcurrentHashMap
の違いは、スレッドセーフか否か。つまりコンカレントの環境で使えるか否か。HashTable
が提供するレベルの同期レベルを提供できるではないが、実用では十分なれべる -
HashMap
は、Collections.synchronizedMap
でラップするとHashTable
とほぼ同等のコレクションが返される。それは、すべての更新イベントで、Map
全体がロックされる。ConcurrentHashMap
の場合は、スレッドセーフを全体のMapをパーティション分割して、一部だけロックするような仕組みになっている。 -
ConcurrentHashMap
は、よりスケーラブルで、パフォーマンスもSynchronized HashMap
より良い。シングルスレッドの環境下では、HashMap
の方が、ConcurrentHashMap
よりパフォーマンスが良い
これらの比較を見てみると、明らかに ConcurrentHashMap
を使うのが妥当なケースが多そうだ。じゃあ実装してみよう。
Micronaut を使って実装する
簡単そうだ。
Micronaut install
私の環境では、chocolatey
のソースが書き換わっていたので、デフォルトに戻す。 Administrator 権限で PowerShellを起動するとインストールできる。
$ choco isntall micronaut -s https://chocolatey.org/api/v2/
Generate project via template
次の感じで Maven のプロジェクトを生成できる。
$ mn create-app flyweight-server --build maven
まずはこれで、サーバーは上がるようになる。自分がハマったポイントは、自分は、IntelliJ を使ってコードを書くのだが、Terminal の JAVA_HOME が設定されていなくて、Caused by: java.lang.IllegalArgumentException: invalid target release: 11
というエラーメッセージが出た。もともとは、Java8 の JDK だけパスにあったので、それが問題なのだが、Java の基本は、JAVA_HOME の設定と、%JAVA_HOME%/bin をパスに通すことだと改めて思いだした。そうでないと、ちゃんと maven が動作しない。
$ cd flywieght-server
$ mvn clean package
$ java -jar .\target\flyweight-server-0.1.jar
## コントローラの追加
コントローラを追記してみる。Micronautは、どちらかというと、Spring の軽量版みたいな感じで様々な機能をサポートしている様子。私は今回 Http Server が欲しいだけだったので、マニュアルを見てコードを書いてみる。コントローラーとルーディングをするのは、単にコントローラを書けばよいみたい。とても簡単でござる。
FlyweightController.java
package flyweight.server;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
@Controller("/flyweight")
public class FlyweightController {
@Get(value = "/{string}/{fret}", produces = MediaType.TEXT_PLAIN)
public String index(Integer string, Integer fret){
return "String: " + string + " Fret: "+ fret;
}
}
これで動いたので、コントローラーの知りたい挙動は理解できた。次に Flyweight のアプリを改造してみよう。これは、マルチスレッドで動作すると思うので、先ほどの ConcurrentHashMap
を使ってみよう。
なんと先ほどのより短くなってしまったではないか。computeIfAbsent
メソッドが、関数を受け取るようになってきて、アトミックな動作を保証してくれる。これは、C# の ConcurrentDictionary と同じ雰囲気だ。だから、もし、なかったら新たに作って返すし、そうでなければ、既存のものを返してくれる。最高!
SoundFactory.java
package flyweight.server;
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public class SoundFactory {
private static ConcurrentMap<String, Sound> sounds = new ConcurrentHashMap<String, Sound>();
public static Sound getSound(String note) {
return sounds.computeIfAbsent(note, n -> new SoundImpl(n));
}
}
全体像
FlyweightController.java
package flyweight.server;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
@Controller("/flyweight")
public class FlyweightController {
@Get(value = "/{string}/{fret}", produces = MediaType.TEXT_PLAIN)
public String index(Integer string, Integer fret){
Integer openNote = Constants.stringMap.get(string.intValue());
int note = openNote.intValue() + fret.intValue();
if (note >= 12) {
note = note - 12;
}
Sound sound = SoundFactory.getSound(Constants.notes.get(note));
return "String: " + string + " Fret: "+ fret + " Sound: " + sound.Play();
}
}
Constants.java
package flyweight.server;
import java.util.HashMap;
public class Constants {
static HashMap<Integer, Integer> stringMap = new HashMap<Integer, Integer>();
static HashMap<Integer, String> notes = new HashMap<Integer, String>();
static void setup() {
stringMap.put(1, 4);
stringMap.put(2, 11);
stringMap.put(3, 7);
stringMap.put(4, 2);
stringMap.put(5, 9);
stringMap.put(6, 4);
notes.put(0, "C");
notes.put(1, "C#");
notes.put(2, "D");
notes.put(3, "D#");
notes.put(4, "E");
notes.put(5, "F");
notes.put(6, "F#");
notes.put(7, "G");
notes.put(8, "G#");
notes.put(9, "A");
notes.put(10, "A#");
notes.put(11, "B");
}
}
Sound.java
package flyweight.server;
public interface Sound {
String Play();
}
SoundImpl.java
package flyweight.server;
public class SoundImpl implements Sound {
private String note;
public SoundImpl(String note) {
this.note = note;
}
public String Play() {
return this.note + "- note";
}
public String getNote() {
return this.note;
}
}
Application.java
package flyweight.server;
import io.micronaut.runtime.Micronaut;
public class Application {
public static void main(String[] args) {
Constants.setup();
Micronaut.run(Application.class, args);
}
}
実行結果
[INFO] Replacing original artifact with shaded artifact.
[INFO] Replacing C:\Users\tsushi\Code\java\spike\micronaut\flyweight-server\target\flyweight-server-0.1.jar with C:\Users\tsushi\Code\java\spike\micronaut\flyweight-server\target\flyweight-server-0.1-shaded.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 7.444 s
[INFO] Finished at: 2020-10-26T14:16:48-07:00
[INFO] ------------------------------------------------------------------------
PS C:\Users\tsushi\Code\java\spike\micronaut\flyweight-server> java -jar .\target\flyweight-server-0.1.jar
←[36m14:17:24.022←[0;39m ←[1;30m[main]←[0;39m ←[34mINFO ←[0;39m ←[35mio.micronaut.runtime.Micronaut←[0;39m - Startup completed in 1154ms. Server Running: http://localhost:8080
今日は軽く Flyweight
パターンと、ConcurrentHashMap
を学べたので良しとしましょう。