Itsukaraの日記

最新IT技術を勉強・実践中。最近はDeep Learningに注力。

Java8のラムダ式/ストリームはCoffeeScript/Jqueryの真似?

最近のJavaの仕様を確認中です。Java8でラムダ式とストリームが入ったらしいですが、Javascript/CoffeeScript/Jqueryを先に知ったためか、Javaがこれらを真似たように感じました。

Javascript/CoffreeScriptでは、元々、関数を記述可能でしたが、Java7までは、クラスと結びついた形での関数であるメソッドしか書けなかったということですね。Java8で、初めてクラスとは独立した関数であるラムダ式を書けるようになったということと理解しました。

ラムダ式の書き方は、CoffeeScriptの関数の書き方を真似たように感じました。また、ストリームによるチェイニングは、Jqueryのチェイニングを真似たように感じました。

Javascriptでは引数の型を気にせずに関数を書けるので簡単ですが、Javaでは型に縛られているので、ちょっとした関数も、何らかの型を持たせる必要があり、少し面倒ですね。Java8では、Consumer、Function、Predicateなどのクラスが導入されていますが、Javascriptの文化で見ると、非常に奇異に感じました。ただ、型を厳密に扱いながら、簡潔に書けるようにするためには、これらが必要なのですね。

練習のために、ラムダ式やストリームを使ったプログラムを書いてみました。Java7までならばforループを記載するために行数が増えそうなところが、非常に簡潔に書けるところが良いですね。なお、実際の処理は短くなっていますが、宣言や、Getter、toStringなどの定型的な部分(Eclipsedで自動生成可能)は、結構多くなってしまいましたが...

CoffeeScriptJqueryなら、もっと簡潔に書けそうな気もしますが、Javaではしっかりと型をチェックするので、コンパイル時にエラーを発見できる点が良いですね。実際、下記を書いているときに、コンパイルエラーで修正した部分が結構あります。そのようなエラーをCoffeeScriptJqueryで実行時にデバッグするのは大変かもしれません。

なお、下記のプログラムはEclipseの環境下で書きましたが、Eclipseは変数や式の型を理解しているので、それぞれの変数や式の途中で、どのメソッドが呼び出せるかが直ぐに分かるので、非常に便利でした。

import static java.util.function.Function.*;
import static java.util.stream.Collectors.*;

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

enum Gender {
	man,
	woman,
	genderless
};

class Person implements Comparable<Person>, Cloneable {
	private String name;
	private String furigana;
	private int	   age;
	private Gender gender;

	// Person中の以下のコードは、Eclipseの機能活用で自動生成したもの(一部加筆)
	public Person(String name, String furigana, int age, Gender gender) {
		super();
		this.name = name;
		this.furigana = furigana;
		this.age = age;
		this.gender = gender;
	}

	public String getName() {
		return name;
	}

	public String getFurigana() {
		return furigana;
	}

	public int getAge() {
		return age;
	}

	public Gender getGender() {
		return gender;
	}

	@Override
	public String toString() {
		return "[name=" + name + ", furigana=" + furigana + ", age=" + age + ", gender=" + gender + "]";
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + age;
		result = prime * result + ((furigana == null) ? 0 : furigana.hashCode());
		result = prime * result + ((gender == null) ? 0 : gender.hashCode());
		result = prime * result + ((name == null) ? 0 : name.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Person other = (Person) obj;
		if (age != other.age)
			return false;
		if (furigana == null) {
			if (other.furigana != null)
				return false;
		} else if (!furigana.equals(other.furigana))
			return false;
		if (gender != other.gender)
			return false;
		if (name == null) {
			if (other.name != null)
				return false;
		} else if (!name.equals(other.name))
			return false;
		return true;
	}

	@Override
	public int compareTo(Person paramT) {
		// 加筆したコード
		return this.furigana.compareTo(paramT.furigana);
	}

	@Override
	protected Object clone() throws CloneNotSupportedException {
		// TODO 自動生成されたメソッド・スタブ
		return super.clone();
	}
}

public class Test_Stream {

	public static void main(String[] args) {
		// Arrayからstream(その2)
		Person p1 = new Person("佐藤", "さとう",   28, Gender.man);
		Person p2 = new Person("鈴木", "すずき",   22, Gender.woman);
		Person p3 = new Person("高橋", "たかはし", 35, Gender.man);
		Person p4 = new Person("田中", "たなか",   41, Gender.man);
		Person p5 = new Person("伊藤", "いとう",   23, Gender.man);
		Person p6 = new Person("渡辺", "わたなべ", 53, Gender.man);
		Person p7 = new Person("山本", "やまもと", 27, Gender.woman);
		Person p8 = new Person("中村", "なかむら", 38, Gender.man);
		Person p9 = new Person("小林", "こばやし", 29, Gender.genderless);

		Person[] personArray = {p1, p2, p3, p4, p5, p6, p7, p8, p9};
//		List<Person> personList = Arrays.asList(personArray);
//		上記2行は下記1行で十分
//		List<Person> personList = Arrays.asList(p1, p2, p3, p4, p5, p6, p7, p8, p9);
//		無理やりストリームを使って書くと下記のようになる。実際は途中でsortもできるが...
		List<Person> personList = Arrays.stream(personArray).collect(Collectors.toList());
		personList.sort(Person::compareTo);

		// 各種ラムダ式の定義例
		Function<Person, String> person2String = p -> p.getName()+"さんは"+p.getAge()+"歳、("+(p.getGender()==Gender.man?"男性":"女性")+")です。";
		Consumer<Person> printPerson = p -> System.out.println(p);
		Consumer<String> printString = System.out::println;

//		Comparator<Person> compPersonAge = (person1, person2) -> person1.getAge() - person2.getAge();
//		上記は、下記のようにも書ける
		Comparator<Person> compPersonAge = Comparator.comparingInt(Person::getAge);

		BiPredicate<Person, Gender> genderp = (p, g) -> p.getGender() == g;
		Predicate<Person> manp = p -> genderp.test(p, Gender.man);
		Predicate<Person> womanp = p -> genderp.test(p, Gender.woman);

		// 以下、ラムダ式の使い方例
		System.out.println("\nstreamの使い方1: personList.stream().forEach(printPerson)");
		personList.stream().forEach(printPerson);

		System.out.println("\nstreamの使い方2: Arrays.stream(personArray).sorted(compPersonAge).forEach(printPerson)");
		Arrays.stream(personArray).sorted(compPersonAge).forEach(printPerson);

		System.out.println("\nstreamの使い方3: personList.stream().filter(predicate).map(person2String).forEach(printString)");
		System.out.println("predicate = manp");
		personList.stream().filter(manp)  .sorted(compPersonAge).map(person2String).forEach(printString);
		System.out.println("predicate = womanp");
		personList.stream().filter(womanp).sorted(compPersonAge).map(person2String).forEach(printString);

		// 以下、ストリームを活用したハッシュの作成、検索例
		System.out.println("\nMAPの作成、検索例");
//		SortedMap<String, Integer> person2AgeMap = new TreeMap<>();
//		personList.stream().forEach(p -> person2AgeMap.put(p.getName(), p.getAge()));
// 		上記2行は下記1行に纏められる。どちらが良いか...
		Map<String, Integer> person2AgeMap = personList.stream().collect(Collectors.toMap(p -> p.getName(), p -> p.getAge()));

		Stream.of(p3, p8).map(Person::getName).forEach(name -> {
			int age = person2AgeMap.get(name);
			System.out.println(name+"さんの年齢は"+age+"歳です。");
		});

		// flatmap, distinct, collect, entrySet利用例
		System.out.println("\nflatmap, distinct, collect, entrySet利用例");
		String sentence = "With the first gust of wind the ends of the cloak whipped about the Traveler's body."
				         +"But he immediately wrapped it closely around him, and the harder the Wind blew, "
				         +"the tighter he held it to him.";

		System.out.println("\n例文:"+sentence);
		System.out.print("\n含まれる単語:");
		List<String> wordlist = Stream.of(sentence.split("\n")) // split to lines
								.map(line -> line.split("\\W+")) // split to words
								.flatMap(Arrays::stream) // flatten to stream of words
								.filter(s -> !s.equals("")) // filter out ""
								.map(String::toLowerCase)
								.distinct() // unique words
								.collect(toList());
		wordlist.stream().forEach(word -> System.out.print(word+" "));

		System.out.println("\n各文字の出現頻度:");
		Stream.of(sentence.split("\\W+")) // split to words
		.map(String::toLowerCase)
		.map(word -> word.split("")) // split to letters
		.flatMap(Arrays::stream) // flatten to stream of letters
		.collect(groupingBy(identity(), counting())) // 頻度計算。型はMap<String,long)
		.entrySet().stream() // Map<String,long) => Set<Etnry<String,long>>
		.sorted((e1, e2) -> (int)(e2.getValue() - e1.getValue())) // 頻度の大きい順にソート。intにキャスト必要
		.forEach(e -> System.out.println(e.getKey()+":"+e.getValue()));
	}
/*
 * 実行結果:

streamの使い方1: personList.stream().forEach(printPerson)
[name=伊藤, furigana=いとう, age=23, gender=man]
[name=小林, furigana=こばやし, age=29, gender=genderless]
[name=佐藤, furigana=さとう, age=28, gender=man]
[name=鈴木, furigana=すずき, age=22, gender=woman]
[name=高橋, furigana=たかはし, age=35, gender=man]
[name=田中, furigana=たなか, age=41, gender=man]
[name=中村, furigana=なかむら, age=38, gender=man]
[name=山本, furigana=やまもと, age=27, gender=woman]
[name=渡辺, furigana=わたなべ, age=53, gender=man]

streamの使い方2: Arrays.stream(personArray).sorted(compPersonAge).forEach(printPerson)
[name=鈴木, furigana=すずき, age=22, gender=woman]
[name=伊藤, furigana=いとう, age=23, gender=man]
[name=山本, furigana=やまもと, age=27, gender=woman]
[name=佐藤, furigana=さとう, age=28, gender=man]
[name=小林, furigana=こばやし, age=29, gender=genderless]
[name=高橋, furigana=たかはし, age=35, gender=man]
[name=中村, furigana=なかむら, age=38, gender=man]
[name=田中, furigana=たなか, age=41, gender=man]
[name=渡辺, furigana=わたなべ, age=53, gender=man]

streamの使い方3: personList.stream().filter(predicate).map(person2String).forEach(printString)
predicate = manp
伊藤さんは23歳、(男性)です。
佐藤さんは28歳、(男性)です。
高橋さんは35歳、(男性)です。
中村さんは38歳、(男性)です。
田中さんは41歳、(男性)です。
渡辺さんは53歳、(男性)です。
predicate = womanp
鈴木さんは22歳、(女性)です。
山本さんは27歳、(女性)です。

MAPの作成、検索例
高橋さんの年齢は35歳です。
中村さんの年齢は38歳です。

flatmap, distinct, collect, entrySet利用例

例文:With the first gust of wind the ends of the cloak whipped about the Traveler's body.But he immediately wrapped it closely around him, and the harder the Wind blew, the tighter he held it to him.

含まれる単語:with the first gust of wind ends cloak whipped about traveler s body but he immediately wrapped it closely around him and harder blew tighter held to 
各文字の出現頻度:
e:21
t:19
h:16
i:12
d:11
a:8
o:8
r:8
l:7
w:6
n:5
s:5
b:4
m:4
p:4
u:4
f:3
y:3
c:2
g:2
k:1
v:1

 */
}