Java力をあげるための指針

また「プロになるJava」の宣伝か、と思われるので、今回は「プロになるJava」の宣伝は自粛します。
Java力をあげるためには最適な「プロになるJava」がオススメなんですが、そうするとこのエントリもこのあたりで終わってしまうので、今回は自重します。

ということで、よく「Java力をあげるにはどうしたらいいか」という質問をみかけます。どうしましょうね、という話。
ここで、「Java力をあげたい」と言ってるときの大半はプログラミング力をあげたいという話です。
もちろん「プロになるJava」もプログラミング力をあげるのにとても役に立つのですが、今回は「プロになるJava」以外で攻めてみましょう。

そうすると実のところJavaにこだわる必要がなくて、そして最近はPythonで無償のテキストがたくさん手に入るので、そういうのを見るといいんではないかと思います。
たとえば「Think Python: How to Think Like a Computer Scientist」の日本語訳「Think Python:コンピュータサイエンティストのように考えてみよう」が公開されています。
https://cauldron.sakura.ne.jp/thinkpython/thinkpython/ThinkPython.pdf

売ってる書籍だと「独学プログラマー」とか。面接対策みたいな話まであっておもしろいです。

あとは手を動かすんですが、LeetCodeやりましょう。
https://leetcode.com/
easyをたくさんやるのがいいです。英語が苦手であれば、問題をみるとExampleとして入出力例があるので、そこから何をやるか想像できるものをやりましょう。想像できなければ飛ばして別の問題を見ます。

と、Java力をあげたいといいつつ実はプログラミング力をあげたいという想定で話を進めましたが、ほんとにJava力をあげたいという場合もあると思います。
文法については何か本を一冊読みましょう。 「プロになるJava」がオススメなんですが、今回は禁じ手なので別の本・・・まあなにか読んでください。

文法はわかったとして、見るといいと思うのは標準APIJavadocです。
java.langパッケージとjava.utilパッケージにあるものは一通り見て、用途がぜんぜんわからないものは無視するとして、なんとなくわかるものは全部確認しておきましょう。
https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/lang/package-summary.html
https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/util/package-summary.html

文法とAPIがひととおりわかったら、バイトコードもちょっとは知っておくといいです。簡単なコンパイラを作りましょう。コンパイラというと腰が引けるかもしれませんが、「数字単体か足し算だけ」とすればそこまで難しくないと思います。
数字だけ出力するものはここにあるので、ここに足し算の処理を付け加えてみましょう。
https://nowokay.hatenablog.com/entry/20080609/1213025229

これで言語とAPIバイトコードがわかったので、あとはJVMのふるまいです。
JVMのふるまいは、JITコンパイラGCがわかればとりあえずいいと思うので、これらが何をしているかくらいは勉強するといいと思います。
ただ、このあたりに関して手頃な資料がすぐには思い浮かばなかったので、探してそのうち追記します。

追記: 「Javaパフォーマンス」がよさそう。スレッドの話も載っているし

プログラミングは論理的思考の訓練になるか - 「プロになるJava」ボツ原稿

プログラミングと論理的思考の関係、「プログラマに大切なのは日本語だ」の実際に意味するところの話です。
「プロになるJava」でページ数などの関係でボツにした原稿で、結構ちゃんと書いたのですが、この先に日の目を見る機会もなさそうなので公開します。

4/8補足:こういう章を入れようとした背景としては、論理的思考とかロジカルシンキングとかはすでにマーケティング用語になっていて、主に情報整理術を扱う本にこういう言葉が使われていることが多いので、そういうマーケティング用語として使われてる無定義なものではなく、論理学を勉強しようよという狙いでこういう話題を含めようとしたのでした。

論理演算子

排他的論理和はあとの話題にも出ますが「プロになるJava」で扱ってないのと、これもボツ原稿なので、まず紹介。

排他的論理和

日常会話でも「または」と言うことがありますが「りんご、またはオレンジ」と言ったときには「りんごかオレンジのどちらか一方」になります。論理和の場合には両方trueでも成り立つので注意が必要です。
日常会話の「または」は「xor(えっくすおあ)」という論理演算になり、Javaでは「^」で表します。

どちらか一方だけtrueの場合にtrueを返します。

jshell> true ^ false
$1 ==> true

両方trueや両方falseの場合にはfalseを返します。

jshell> true ^ true
$2 ==> false

この演算は!=と同じ結果になるので、実際のコードでは!=を使うほうがわかりやすいでしょう。
表にまとめると次のようになります。

true false
true false true
false true false

この演算を排他的論理和といいます。

プログラミングは論理的思考の訓練になるか

論理的思考とはなにかという話にはいろいろな見方がありますが、ひとつに「正しく推論ができる」ということがあります。推論というのは、なにか情報が与えられたときに、その情報を組み合わせて結論を導き出すことです。

メソッド呼び出しと論理の関係

プログラミングと論理的思考の関連を考えるひとつの材料として、メソッドを組み合わせてプログラムを実装することと論理的な推論が対応しているという理論があります。

ここで論理的な推論というのは次のようなものです。 次のふたつの前提があるとします。

  • サカナは泳ぐ
  • マグロはサカナである

このふたつの前提から次の結論を導きだします。

  • マグロは泳ぐ

この推論とプログラムの関係を考えてみましょう。 「XはYである」を「型Xを引数にとって型Yを返すメソッドがある」を置き換えます。 そうすると、最初の前提条件「サカナは泳ぐ」「マグロはサカナである」は次の2つのメソッドp1とp2になります。

Oyogu p1(Sakana s);
Sakana p2(Maguro m);

そして結論「マグロは泳ぐ」は次のメソッドc1になります。

Oyogu c1(Maguro m);

この結論が2つの前提条件から導きだせることと、メソッドc1がメソッドp1、p2を使って実装できることが対応しています。 ここでc1は次のように実装ができるので、先ほどの推論も妥当だということになります。

Oyogu c1(Maguro m) {
  Sakana s = p2(m);
  return p1(s);
}

間違った推論の例も見てみましょう。 次の2つの前提があるとします。

  • サカナは泳ぐ
  • イルカは泳ぐ

この2つの前提から次の結論が導き出せるか考えてみます。

  • イルカはサカナである

前提「サカナは泳ぐ」はメソッドp1として使いまわせるので、「イルカは泳ぐ」に対応するメソッドp3を宣言します。

Oyogu p1(Sakana s);
Oyogu p3(Iruka i);

結論「イルカはサカナである」に対応する次のメソッドが、2つのメソッドp1、p3を使って実装できれば、この推論は正しいということになります。

Sakana c2(Iruka i);

このメソッドを実装するには、Sakana型を返すメソッドが必要になりますが、今回の前提条件にはありません。そのため、このメソッドc2はメソッドp1とp3を使って実装できないということになります。
このことが、「サカナは泳ぐ」「イルカは泳ぐ」という前提条件から「イルカはサカナである」という結論は導きだせないということにつながります。

このように、メソッドを組み合わせて実装できることと、前提条件を組み合わせて結論を導きだすことは同じ形をしています。この対応はカリーハワード同型対応といいます。
このことを考えると、メソッドを組み立てて新しいメソッドを作ることは論理的思考の練習にもなってるということになります。その点では、プログラミングは論理思考の訓練になっていると言えます。

間違った推論から導いた間違った結論をもとに行動してしまうと、損してしまうことになります。
また、結論が間違っている場合に、推論が間違っているのか前提が間違っているのかわからなくなります。

命題論理

プログラマに必要な言語は日本語だ!」「まず日本語を勉強しろ」のような発言を見ることがありますが、日本語で日常生活を送れているのであれば、それ以上に日本語を勉強するというのは、漢字をたくさん覚えるとか「すべからく」で始まる文は「べし」で終わるというような係り受けを正しく使うとか、小説をたくさん読んで表現を覚えるということになります。
でも、プログラマには日本語が必要といっている人の求めているのは、そういった日本語能力ではありません。
実際には、文章の論理構造を読み取れるようになってほしいとか、文章を書くときの論理構造を正しく扱えるようになって欲しいということを求めています。
そのときに勉強しないといけないのは論理学ということになります。

日本語の「または」の論理構造が実際にはあいまいであることを「排他的論理和」の説明で示しました。
ほかには「ならば」もあいまいです。たとえば「晴れならば遊びにいく」というとき、日常会話では「雨のときは遊びにいかないんだな」と思いますが、実際には雨の場合はなにも言っていません。晴れのときに遊びに行かなかったら、この文は成り立たなかったことになりますが、論理構造としては雨の日は遊びにいってもいかなくても「晴れならば遊びにいく」という文は成り立つことになります。
説明や議論のための文章を書くときには、こういった実際はあいまいな論理になるような文を使ってしまわないことが大切です。

Twitterでよくみられるのが「晴れならば遊びにいくと言ったのに雨でも遊びに行ってるじゃないか」のような、「ならば」を誤読したそこに書かれていない条件を基準にした批判です。
こういった論理構造からはずれるやりとりを防ぐためには、文章の論理構造を式にして確認する練習を一度やっておくといいと思います。

「AならばB」を論理式として考えると、AがtrueのときにBがfalseの場合だけfalseになる式ということになります。Javaに該当する演算子はありませんが、記号としては→を使います。
表にすると次のようになります。

A B A→B
True True True
True False False
False True True
False False True

ここで、「サカナは泳ぐ」は「サカナならば泳ぐ」と言い換えることができます。ふたつの前提条件がどちらも成り立つということは「且つ」でつなぐことができます。また、前提条件がなりたつ「ならば」結論がなりたつということになります。
まとめると、さきほどの推論の例は次の論理式で表せるということになります。

(サカナ→泳ぐ && マグロ→サカナ) → (マグロ→泳ぐ)

この「マグロ」「サカナ」「泳ぐ」それぞれが成り立つとき成り立たないときのすべての組み合わせについて、式を確認していくと次のような表になります。

マグロ サカナ 泳ぐ サカナ→泳ぐ マグロ→サカナ & マグロ→泳ぐ 結論の「→」
True True True True True True True True
False True True True True True True True
True False True True False False True True
False False True True True True False True
True True False False True False True True
False True False False True False False True
True False False True False False True True
False False False True True True True True

これを見ると、結論はすべてTrueになっているので、どのような値の組み合わせでも成り立つことになります。常にTrueになる式をトートロジーといいます。推論を論理式に置き換えたときにトートロジーになるとき、その推論は正しいといえます。そのため、この推論は正しいということができます。

間違った推論の例を論理式で表すと次のようになります。

(サカナ→泳ぐ && イルカ→泳ぐ) → (イルカ→サカナ)

この論理式についてすべての組み合わせを確認すると次の表のようになります。

イルカ サカナ 泳ぐ サカナ→泳ぐ イルカ→泳ぐ & イルカ→サカナ 結論の「→」
True True True True True True True True
False True True True True True True True
True False True True True True False False
False False True True True True True True
True True False False False False True True
False True False False True False True True
True False False True False False False True
False False False True True True True True

今度は結論にFalseがあることがわかります。つまりこの論理式はトートロジーになっていません。そのため、この推論は正しくないということになります。(※イルカでありサカナではなく泳ぐというときFalseになっています)

こういった論理は命題論理といい、正しい説明を書くためには必要な考え方です。
いくらJavaを使いこなせるようになって高度なプログラムが書けるようになっても、作ろうと思うもの自体に誤りがあっては、正しいプログラムは作れません。
ぜひ、論理学を勉強してみてください。

補足

ボツ原稿はここまで。 プログラミングに論理が強く関係してそうなことがちょっと見えたんではないかなと思います。論理学は数学の一分野でもあるので「プログラミングに数学は必要か」という話の答えでもありますね。

あとは、論理学の参考書籍を

いきなり「論理学」と言うと抵抗のある人には、文章の論理構造から論理学に入門するこちらの本がいいと思います。
プログラマはまず日本語を勉強すべし」と言ってる人が勉強させたいものは、この本のようなことだと思います。

もうすこし形式的に論理学を勉強する場合はこちらを。パラドックス不完全性定理など、難しめの話題も取り扱っていますが、まずは述語論理まで読めば十分かと。

プログラミングとの関係はこちらが。論理を拡張していくことで関数型プログラミングになっていくことが説明されています。カリーハワード同型対応のちゃんとした説明もこの本にあります。

カリーハワード同型対応にどのような意味があるか、というもっとつっこんだ話は、この本にあります。

「メソッドのシグネチャ」はJava言語とJava仮想マシンで違う

プロになるJava」でシグネチャのことを次のように説明しています。

メソッドの名前と受け取る引数、戻り値の種類をあわせたものを シグネチャ といいます。

戻り値は含まないのでは?という話になり、結論から言えばJava言語では名前と引数でメソッドを区別、Java仮想マシンでは名前と引数、戻り値まで含めてメソッドを区別しているけど、ここではJava言語の話をしているので含まないほうが正しい、です。

シグネチャの定義

Java言語仕様では、「シグネチャとは」という定義はありませんが、「シグネチャが同じとは」という説明があります。ジェネリクスの型パラメータまで考慮はするけど、名前と引数が同じとみなせれば同じシグネチャだよという感じですね。

Two methods or constructors, M and N, have the same signature if they have the same name, the same type parameters (if any) (§8.4.4), and, after adapting the formal parameter types of N to the type parameters of M, the same formal parameter types.
https://docs.oracle.com/javase/specs/jls/se17/html/jls-8.html#jls-8.4.2

一方でJava 7のJava仮想マシン仕様では、戻り値やthrowsまで含めるような記述があります。そしてよく見るとメソッド名はありませんね。

A method signature, defined by the production MethodTypeSignature, encodes the (possibly parameterized) types of the method's formal arguments and of the exceptions it has declared in its throws clause, its (possibly parameterized) return type, and any formal type parameters in the method declaration.
https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3.4

そしてJava 8からは、この記述があった4.3.4. Signaturesごと消えています。

f:id:nowokay:20220402232630p:plain

Java言語とJava仮想マシンでのメソッド特定方法の違い

クラスFooから、クラスBarvoid print(int)なメソッドを呼び出します。

public class Foo {
    public static void main(String[] args) {
        Bar.print(3);
    }
}

メソッド定義はこんな感じ。

public class Bar {
    static void print(int a) {
        System.out.println(a);
    }
}

javacでコンパイルして実行できます。

C:\Users\naoki\Desktop>dir *.class

ファイルが見つかりません

C:\Users\naoki\Desktop>javac Foo.java

C:\Users\naoki\Desktop>java Foo
3

このprintメソッドの戻り値をintに変更します。クラスFooはそのまま。

public class Bar {
    static int print(int a) {
        System.out.println(a);
        return 0;
    }
}

コンパイルしなおす前にclassファイルを退避しておきます。

C:\Users\naoki\Desktop>copy Bar.class Bar.class_
        1 個のファイルをコピーしました。

一旦classファイルを全部消して、コンパイル・実行するとちゃんと実行できます。

C:\Users\naoki\Desktop>del *.class

C:\Users\naoki\Desktop>javac Foo.java

C:\Users\naoki\Desktop>java Foo
3

このことから、Java言語では戻り値の型が変わってもメソッドの特定に問題がないことがわかります。

では、先ほど退避したBarクラスのclassファイルを戻して実行します。

C:\Users\naoki\Desktop>copy Bar.class_ Bar.class
Bar.class を上書きしますか? (Yes/No/All): y
        1 個のファイルをコピーしました。

C:\Users\naoki\Desktop>java Foo
Exception in thread "main" java.lang.NoSuchMethodError: 'int Bar.print(int)'
        at Foo.main(Foo.java:3)

そうすると、NoSuchMethodErrorが発生しました。そしてここで、見つからなかったメソッドとして戻り値の型まで含んでいます。 このことから、Java仮想マシンでは戻り値の型が変わるとメソッドが特定できなくなることがわかります。

ということで、「Java」の用語の話をしていても言語かVMかで扱いが変わることがあるんだなということでした。

いいわけ

ただ、「プロになるJava」で戻り値まで含める説明をしたのは、JShellでメソッドの「シグネチャ」として戻り値まで出ているので、その説明をしやすくするためというのがあります。

jshell> "test".toUpperCase(
シグネチャ:
String String.toUpperCase(Locale locale)
String String.toUpperCase()

<ドキュメントを表示するにはタブを再度押してください>

シグネチャというのはなにかを特定するための情報なので、メソッドのシグネチャという場合にJava言語では戻り値は含めないのですが、実際の「シグネチャ」の使い方はメソッドの「概形」をさすことも多いので、うまい説明を考えないといけない。

あの火事から2年 - 漂流開発者の日記(WEB+DB PRESS VOL.22, 2004-8-17)

2004年のWEB+DB PRESS VOL.22に掲載された記事です。絵もかいてます。
火事はその2年前、2002年ですね。

暑いですね

夏です。暑いですね。
夜、暑くて眠れないこともあります。
それはそうと、暑くて眠れないといえば、2年前の夏の火事を思い出します。
どうしてそうつながるかは、まぁ、続きを読んでもらうことにして。

おっちゃんとの出会い

話はさらにその前の冬、2001年12月3日にさかのぼります。 夕方、出かけようと思って外に出たら、おっちゃんに呼び止められました。同じアパートのおっちゃんで、後ろの駐車場&田んぼを指差して「ここにマンション建つんやけど、そこで3ヵ月後店出すけん手伝ってくれんか?」と言われました。
仕事があるので、と断ると「あんた仕事しよったと?」と言われました。大きなお世話です。っていうか、マンションて3ヶ月で建つの?
次の日、12月4日のこと。
朝、部屋で作業してたら、外から変な声が聞こえます。外を見ると、昨日のおっちゃんが上半身はだかで、道のまん中に座り込んでいます。12月です。外は寒いです。
おっちゃんの周りには、ストーブやら自転車やら服やらがちらばってます。
周りにいる人が心配そうに声を掛けると「大丈夫やけん」といいながら腕立て伏せを始めました。ぜんぜん大丈夫じゃないね。
ぼくはそのまま出かけたので、その後どうなったか見てませんが、かなりあとで聞いた話によると、このことがきっかけで精神病院に入れられたらしいです。

どろぼう?

そしてその次の夏。まずは2002年8月3日。
部屋で作業してると、外で男の人と女の人の会話が聞こえました。 「出て来られたんですか?」と女の人。
「えぇ、月末、出てきました。」と男の人。
どうやらおっちゃん3日前に精神病院から出てきたらしい。 出てくんなよぉ、と思いながら作業を続けました。
そしてその4日後。8月7日。
その日は、遅くまで仕事してました。
で、家に帰ると、どの部屋にも明かりがついていません。 あれ?今日はみんないないのかな、と思いながら階段をあがると、なんかガラスがちらばってます。
2階まであがると、ぼくの部屋の扉がだぁーんと開いてます。「うわっ、どろぼう!」
見てみると、鍵がバールのようなものでこじ開けられてました。そんな強引などろぼうなら仕方ないね。
でも、なんにも盗られてないし、電器つかんし、部屋びちょびちょだし、なんか様子が変です。とりあえず、懐中電灯買おうと外にでました。ガラスが全部割られてます。

火事でした

階段降りると、おっちゃんの部屋のまわりに黄色いテープが。「POLICE KEEP OUT」
おっちゃんの部屋、真っ黒こげ。
さてはおっちゃん、暴れてガラス割りまくりつつ火つけてくたばったか?と思ったんですが、警察に電話すると、ただの火事だと。
次の日不動産やさんに聞くと、おっちゃん暑くて眠れなくてむしゃくしゃして火をつけたら、思いのほか燃え広がって、怖くなってイヌ連れて逃げた、と。放火じゃん。
引越し先は、燃えないこと、燃やす人がいないこと、を条件に決めました。
ぼくの部屋はススまみれ水びたしにはなっていましたが、大事なものは無事でした。
それでも火災保険が切れてたのは痛すぎ。
みなさん、ちゃんと確認しましょうね。

Javaでその場でだせる例外まとめ

プロになるJava」では、かなり最初のほうでゼロによる除算を行って例外の説明をしています。

jshell> 3/0
|  例外java.lang.ArithmeticException: / by zero
|        at (#1:1)

JShellでゼロ除算を行ったときの例外は、その場で発生しているためスタックトレースが出ません。
最初に体験する例外として最適です。

で、他には何があるだろうって考えてみました。

まずはその場で例外オブジェクトの生成

jshell> throw new Exception()
|  例外java.lang.Exception
|        at (#2:1)

定番のぬるぽ

jshell> (int)(Integer)null
|  例外java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because "null" is null
|        at (#3:1)

throw nullのほうが手軽で美しいですね( @iso2022jp さんから)

jshell> throw null
|  例外java.lang.NullPointerException: Cannot throw exception because "null" is null
|        at (#1:1)

ダウンキャストの失敗

jshell> (int) new Object()
|  例外java.lang.ClassCastException: class java.lang.Object cannot be cast to class java.lang.Integer (java.lang.Object and java.lang.Integer are in module java.base of loader 'bootstrap')
|        at (#4:1)

配列シリーズで、要素からはみだしたアクセス

jshell> (new int[0])[0]
|  例外java.lang.ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0
|        at (#5:1)

マイナスの要素数をもった配列を生成しようとする

jshell> new int[-1]
|  例外java.lang.NegativeArraySizeException: -1
|        at (#6:1)

メモリが足りない

jshell> new int[Integer.MAX_VALUE]
|  例外java.lang.OutOfMemoryError: Requested array size exceeds VM limit
|        at (#7:1)

そして @mick_neckさんによる、配列の型システムの不具合をついたやつ。

jshell> ((Object[])(new Integer[1]))[0] = "foo"
|  例外java.lang.ArrayStoreException: java.lang.String
|        at (#8:1)

@YujiSoftwareさんに言語仕様上のまとめおしえてもらいました。
https://docs.oracle.com/javase/specs/jls/se18/html/jls-15.html#jls-15.6

あとはメソッド定義を削除してそのクラスだけコンパイルしなおすとか、不正なバイナリを作ったときに出る例外かな。

2023/2/3 追記 assertがあった!

「プロになるJava」 第4部「高度なプログラミング」の練習問題解答

「プロになるJava」の第4部「高度なプログラミング」の練習問題の解答です。

「プロになるJava」 第2部「Javaの基本」の練習問題解答 - きしだのHatena
「プロになるJava」 第3部「Javaの文法」の練習問題解答 - きしだのHatena
「プロになるJava」 第4部「高度なプログラミング」の練習問題解答 - きしだのHatena

第12章 ファイルとネットワーク

ファイルアクセスと例外

try句で例外に対処する

  1. WriteFileサンプルからthrows IOExceptionを消して、try~catchでの例外処理を行ってみましょう。処理はe.printStackTrace()にします。
public static void main(String[] args) {
    var message = """
        test
        message
        """;
    try {
        var p = Path.of("test.txt");
        Files.writeString(p, message);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

messageの宣言もtry句に含めて構いませんが、ここでは定数の定義は処理とは区別して扱うという考え方でtryの外にしています。

検査例外と非検査例外

  1. InterruptedExceptionは検査例外か非検査例外か、図から考えてみてください。

Exceptionに含まれており、RuntimeExceptionには含まれていないので検査例外です。

  1. UncheckedIOExceptionは検査例外か非検査例外か、図から考えてみてください。

RuntimeExceptionに含まれているので非検査例外です。

ネットワークでコンピュータの外の世界と関わる

ソケット通信とTCP/IP

サーバーへの接続
  1. ポート番号を1600から1700に変えて試してみましょう

SimpleServerではServerSocketのコンストラクタのパラメータを1700にします。

public class SimpleServer {
    public static void main(String[] args) throws IOException {
        var server = new ServerSocket(1700);

SimpleClientではSocketのコンストラクタのパラメータを1700にします。

public class SimpleClient {
    public static void main(String[] args) throws IOException {
        var soc = new Socket("localhost", 1700);

try-with-resource

  1. SimpleServerを起動せずにSimpleClientを実行すると例外java.net.ConnectExceptionが発生します。例外処理をして「サーバーが起動していません」とメッセージを出すようにしてみましょう
public class SimpleClient {
    public static void main(String[] args) throws IOException {
        try (var soc = new Socket("localhost", 1600);
             OutputStream is = soc.getOutputStream()) 
        {
            is.write(234);
        } catch (ConnectException e) {
            System.out.println("サーバーが起動していません");
        }
    }
}

第13章 処理の難しさとアルゴリズム

処理の難しさ

他のデータを参照する

1. 奇数番目の文字を、続く偶数番目の文字と入れ替えて出力するようにしてみましょう。続く文字がない場合はそのまま出力します。例えば"abcde"に対して"badce"と出力します。

package projava;

public class ExExchange {
    public static void main(String[] args) {
        var data = "abcde";
        
        var builder = new StringBuilder();
        for (int i = 0; i < data.length(); i += 2) {
            if (i + 1 < data.length()) {
                builder.append(data.charAt(i + 1));
            }
            builder.append(data.charAt(i));
        }
        var result = builder.toString();
        System.out.println(data);
        System.out.println(result);
    }
}
  1. ひとつ後の要素と比べて大きいほうを格納した配列を作ってみましょう。最後の要素は最後にそのまま出力されます。例えば{3, 6, 9, 4, 2, 1, 5}に対して{6, 9, 9, 4, 2, 5, 5}が生成されます。

ここでは結果の格納をどうするか考える必要があります。まずは結果になる配列をあらかじめ用意する方法を考えてみます。結果は入力データと同じサイズになります。

package projava;

import java.util.Arrays;

public class ExMax {
    public static void main(String[] args) {
        var data = new int[]{3, 6, 9, 4, 2, 1, 5};
        
        var result = new int[data.length];
        for (int i = 0; i < data.length; ++i) {
            if (i < data.length - 1) {
                result[i] = Math.max(data[i], data[i + 1]);
            } else {
                // 最後の要素
                result[i] = data[i];
            }
        }
        
        System.out.println(Arrays.toString(data));
        System.out.println(Arrays.toString(result));
    }
}

文字列ではStringBuilderで文字を追加していくことができました。 数値を追加して最終的に配列を構築する場合には、IntStream.builder()を使うと便利です。 IntStream.builder()についてはボツ原稿の「移動平均とスライディングウィンドウ」で解説しています。もともとはこの例題を踏まえたものでした。 https://nowokay.hatenablog.com/entry/2022/04/25/130057

package projava;

import java.util.Arrays;
import java.util.stream.IntStream;

public class ExMax2 {
    public static void main(String[] args) {
        var data = new int[]{3, 6, 9, 4, 2, 1, 5};
        
        var builder = IntStream.builder();
        for (int i = 0; i < data.length; ++i) {
            if (i < data.length - 1) {
                builder.add(Math.max(data[i], data[i + 1]));
            } else {
                // 最後の要素
                builder.add(data[i]);
            }
        }
        
        var result = builder.build().toArray();
        System.out.println(Arrays.toString(data));
        System.out.println(Arrays.toString(result));
    }
}

IntStream.builder()のようなビルダーを使って結果を構築すると、最終的な要素数をあらかじめ考える必要がなくなります。実際の処理では結果の要素数があらかじめわからないことも多いので、そのような場合には便利です。

隠れた状態を扱う処理

  1. 受け取った文字列のアルファベットを、最初は小文字で出力し、0を受け取ったら次からのアルファベットは大文字に、1を受け取ったら次からのアルファベットを小文字で出力してみましょう。 例: aa0bcd1efg1gg0abc -> aaBCDefgggABC
package projava;

public class ExSwitchUpperLower {
    public static void main(String[] args) {
        var input = "aa0bcd1efg1gg0abc";
        
        var buf = new StringBuilder();
        var lower = true;
        for (char ch : input.toCharArray()) {
            switch (ch) {
                case '0' -> lower = false;
                case '1' -> lower = true;
                default -> {
                    if (lower) {
                        buf.append(Character.toLowerCase(ch));
                    } else {
                        buf.append(Character.toUpperCase(ch));
                    }
                }
            }
        }
        var result = buf.toString();
        System.out.println(input);
        System.out.println(result);
    }
}

このコードでは小文字を出力するか大文字を出力するかという状態が必要になります。

var lower = true;

各文字について処理を行いますが、ここでは文字数分繰り返すループではなく拡張for文を使っています。

for (char ch : input.toCharArray()) {

文字が0であれば大文字を出力するようlowerfalseに、1であれば小文字を出力するようlowertrueに切り替えます。

switch (ch) {
    case '0' -> lower = false;
    case '1' -> lower = true;

それ以外での文字では大文字小文字変換を行ってbufに追加していきますが、ここでCharacter.toLowerCaseメソッドなどを使って変換を行ってます。
「そんなメソッドなんか習ってないけど?」と言いたくなるかもしれませんが、こういった「ありそうなメソッド」は用意されていることが多いです。どういうメソッドが「ありそうなメソッド」かというと、知ってるメソッドの内部処理で使っていそうで単独で使っても便利そうな処理です。StringクラスのtoUpperCaseメソッドで文字を1文字ずつ変換しているのであれば、その1文字ごとの変換処理はメソッドになってるはずと考えて、Character.toとして補完候補を出してみるとあった、という感じです。
Java 8まではそういった「内部でやってる処理をぼくたちにも使わせてよ」というものが多く埋まっていたのですが、Java 9以降に半年ごとにバージョンアップされることになって細かい改善も入りやすくなり、便利メソッドがちゃんと使えることが多くなってます。

  1. 文字列を受け取って、数字以外はそのまま出力し、数字が来たら直前の文字をその数字に1を足した文字数分出力してください。  例:ab0c1ba2bc9cd1 -> abbcccbaaaabccccccccccccddd
package projava;

public class ExExpand {
    public static void main(String[] args) {
        var input = "ab0c1ba2bc9cd1";

        var buf = new StringBuilder();
        var pre = '0';
        for (var ch : input.toCharArray()) {
            if (ch >= '0' && ch <= '9') {
                if (pre == '0') { // 0のときは先頭文字なので何も出力しない
                    continue;
                }
                for (int i = 0; i < ch - '0' + 1; i++) {
                    buf.append(pre);
                }
            } else {
                pre = ch;
                buf.append(ch);
            }
        }
        
        var result = buf.toString();
        System.out.println(input);
        System.out.println(result);
    }
}

状態遷移と正規表現

状態遷移とenum

  1. 小数部の最後が0で終わると不適になるように判定を変更してみましょう。「 12.30」や「12.0」は不適です。

小数部で出てくる0に関する状態FRAC_ZEROを用意します。

FRAC_STARTFRACで0が入力されたときはFRAC_ZEROに移行、FRAC_ZEROからは1-9のときにFRACへ、0ならそのままというふうに状態遷移します。FRAC_ZEROからは終了状態に移行できないので、もしそこで文字列が終わると不適ということになります。

enumに追加します。

enum FloatState {
    START, INT, FRAC_START, FRAC, ZERO, FRAC_ZERO
}

そうすると、結局FRAC_STARTでもFRACでもFRAC_ZEROでも、0ならFRAC_ZEROへ、1-9ならFRACへという処理になるので、まとめて書けます。

case FRAC_START, FRAC, FRAC_ZERO -> {
    if (ch == '0') {
        state = FloatState.FRAC_ZERO;
    } else if (ch >= '1' && ch <= '9') {
        state = FloatState.FRAC;
    } else {
        return false;
    }
}
System.out.println(check("12.304")); // true
System.out.println(check("12.3004")); // true
System.out.println(check("12.300")); // false
System.out.println(check("12.30")); // false
System.out.println(check("12.0")); // false

コードを見てみると、結局FRAC_STARTFRAC_ZEROは扱いがまったく同じなので、まとめていいのではとなります。

case FRAC_START, FRAC -> {
    if (ch == '0') {
        state = FloatState.FRAC_START;
    } else if (ch >= '1' && ch <= '9') {
        state = FloatState.FRAC;
    } else {
        return false;
    }
}

ただ、こうった状態の最適化を行うと、あとあとの改変で場合分けがおきたりするので、なるべく素直な状態遷移を保つほうがいいと思います。状態をまとめるときは「これらの状態は本質的に同じである」つまり「これらの状態に対して変更が起きる時は常に同じ変更になる」という自信があるときだけにしましょう。

  1. 先頭に負の符号を表すを付けることができるように判定を変更してみましょう。「123」はOKですが「123」や「123」は不適です。

先頭でマイナスが来た時の状態を増やします。

enumにも追加します。

    enum FloatState {
        START, MINUS, INT, FRAC_START, FRAC, ZERO
    }

START状態からのMINUSへの遷移と、MINUSでの状態遷移を記述します。

switch (state) {
    case START -> {
        if (ch == '0') {
            state = FloatState.ZERO;
        } else if (ch >= '1' && ch <= '9') {
            state = FloatState.INT;
        } else if (ch == '-') {
            state = FloatState.MINUS;
        } else {
            return false;
        }
    }
    case MINUS -> {
        if (ch == '0') {
            state = FloatState.ZERO;
        } else if (ch >= '1' && ch <= '9') {
            state = FloatState.INT;
        } else {
            return false;
        }                    
    }
System.out.println(check("-12.304")); // true
System.out.println(check("--12.3004")); // false
System.out.println(check("1-2.3004")); // false
System.out.println(check("-.3004")); // false

14章 クラスとインタフェース

インタフェース

  1. record Staff(String name, String job) {}Namedインタフェースをimplementsしてみましょう。
record Staff(String name, String job) implements Named {}
  1. 次の2つのレコードのwidthheightを統一的に扱うためのインタフェースFigureを定義して、それぞれのレコードにimplementsしてみましょう。
record Box(int width, int height) {}
record Oval(int width, int height) {}
interface Figure {
  int width();
  int height();
}

record Box(int width, int height) implements Figure {}
record Oval(int width, int height) implements Figure {}

「プロになるJava」 第3部「Javaの文法」の練習問題解答

「プロになるJava」の第3部「Javaの文法」の練習問題の解答です。
※ 2023/10/03 Streamの練習問題の答えが抜けていたので追加しています。

「プロになるJava」 第2部「Javaの基本」の練習問題解答 - きしだのHatena
「プロになるJava」 第3部「Javaの文法」の練習問題解答 - きしだのHatena
「プロになるJava」 第4部「高度なプログラミング」の練習問題解答 - きしだのHatena

第7章 条件分岐

論理型

1. 「test」に「st」が含まれているかどうかcontainsメソッドで確認してみましょう
jshell> "test".contains("st")
$1 ==> true

値の比較

1. 12と35を<演算子を使って大小比較を行ってみましょう
jshell> 12 < 35
$2 ==> true
2. 12と35を<=演算子を使って等しいかどうか比較を行ってみましょう
jshell> 12 <= 35
$3 ==> true
3. 12と35を==演算子を使って等しいかどうか比較を行ってみましょう
jshell> 12 == 35
$4 ==> false
4. 12と35を!=演算子を使って等しいかどうか比較を行ってみましょう
jshell> 12 != 35
$5 ==> true

オブジェクトの大小比較

1. "test""TEST"compareToメソッドで比較してみましょう。
jshell> "test".compareTo("TEST")
$6 ==> 32

解説: 一致しない最初の文字の差を結果として返します。

jshell> "test".compareTo("tdST")
$9 ==> 1

片方の文字列がもう片方の文字列の先頭部分の場合、長さの差を返します。

jshell> "test".compareTo("tester")
$14 ==> -2
2. 今日の日付と2022年3月15日をcompareToメソッドで比較してみましょう
jshell> LocalDate.now().compareTo(LocalDate.of(2022, 3, 15))
$11 ==> 15

Javadocでの明確な記述は見つけれませんでしたが、日数の差が返ってきますね。 これを書いているのは3月30日です。

jshell> LocalDate.now()
$12 ==> 2022-03-30
3. 今日の日付が2022年3月15日よりも前かどうかisBeforeメソッドで確認してみましょう
jshell> LocalDate.now().isBefore(LocalDate.of(2022, 3, 15))
$13 ==> false

オブジェクトが等しいかどうかの比較

1. 文字列「hello」にtoUpperCaseメソッドを呼び出した結果が「HELLO」であるかどうか確認してみましょう
jshell> "hello".toUpperCase().equals("HELLO")
$16 ==> true

equalsメソッドを使わず==で判定すると、内容は同じだけど別オブジェクトであるということでfalseになります。

jshell> "hello".toUpperCase() == "HELLO"
$17 ==> false
2. 2021年9月14日をLocalDateで表したときに、plusDaysメソッドで10日足した結果が2021年9月24日であるかどうか確認してみましょう
jshell> LocalDate.of(2021, 9, 14).plusDays(10).equals(LocalDate.of(2021, 9, 24))
$18 ==> true

equalsメソッドを使わず==で判定すると、内容は同じだけど別オブジェクトであるということでfalseになります。

jshell> LocalDate.of(2021, 9, 14).plusDays(10) == LocalDate.of(2021, 9, 24)
$19 ==> false

if文での条件分岐

if文

1. 変数aに割り当てる値を変えてみて、表示が変わることを確認しましょう。

変数aに0を割り当てると、0は3よりも小さいので「小さい」が表示されます。

var a = 0;
if (a < 3) {
    System.out.println("小さい");
}

変数aに0を割り当てると、0は3よりも小さいので「小さい」が表示されます。

var a = 0;
if (a < 3) {
    System.out.println("小さい");
}

変数aに5を割り当てると、5は3よりも小さくないので何も表示されなくなります。

var a = 5;
if (a < 3) {
    System.out.println("小さい");
}

変数aに3を割り当てると、3は3よりも小さくないので何も表示されなくなります。

var a = 3;
if (a < 3) {
    System.out.println("小さい");
}

else

1. 変数aに割り当てる値を変えてみて、表示が変わることを確認しましょう。

変数aに0を割り当てると、0は3よりも小さいので「小さい」が表示されます。

var a = 0;
if (a < 3) {
    System.out.println("小さい");
} else {
    System.out.println("大きい");
}

変数aに5を割り当てると、5は3よりも小さくないので「大きい」が表示されます。

var a = 5;
if (a < 3) {
    System.out.println("小さい");
} else {
    System.out.println("大きい");
}

変数aに3を割り当てると、3は3よりも小さくないので「大きい」が表示されます。

var a = 3;
if (a < 3) {
    System.out.println("小さい");
} else {
    System.out.println("大きい");
}

else if

1. 変数aに割り当てる値を変えてみて、表示が変わることを確認しましょう。

変数aに0を割り当てると、0は3よりも小さいので「小さい」が表示されます。

var a = 0;
if (a < 3) {
    System.out.println("小さい");
} else if (a < 7) {
    System.out.println("中くらい");          
} else {
    System.out.println("大きい");
}

変数aに5を割り当てると、5は3よりも小さくなく、7よりも小さいので「中くらい」が表示されます。

var a = 5;
if (a < 3) {
    System.out.println("小さい");
} else if (a < 7) {
    System.out.println("中くらい");          
} else {
    System.out.println("大きい");
}

変数aに10を割り当てると、10は3よりも小さくなく、7よりも小さくないので「大きい」が表示されます。

var a = 10;
if (a < 3) {
    System.out.println("小さい");
} else if (a < 7) {
    System.out.println("中くらい");          
} else {
    System.out.println("大きい");
}

変数aに7を割り当てると、7は3よりも小さくなく、7よりも小さくないので「大きい」が表示されます。

var a = 7;
if (a < 3) {
    System.out.println("小さい");
} else if (a < 7) {
    System.out.println("中くらい");          
} else {
    System.out.println("大きい");
}

変数aに3を割り当てると、3は3よりも小さくなく、7よりも小さいので「中くらい」が表示されます。

var a = 3;
if (a < 3) {
    System.out.println("小さい");
} else if (a < 7) {
    System.out.println("中くらい");          
} else {
    System.out.println("大きい");
}

switchでの条件分岐

switch

1. 変数aの値が5だった場合に「five」と表示するようにcase句を追加してみましょう
switch (a) {
    case 1, 2 -> System.out.println("one-two");
    case 3 -> System.out.println("three");
    case 4 -> System.out.println("four");
    case 5 -> System.out.println("five");
}

第8章 データ構造

Listで値をまとめる

List

1. LocalDate型であらわした2021年9月14日と2021年3月15日が格納されたListを用意してみましょう

(importが必要になるので注意してください)

jshell> import java.time.*

jshell> var dates = List.of(LocalDate.of(2021, 9, 14), LocalDate.of(2021, 3, 15))
dates ==> [2021-09-14, 2021-03-15]
2. 用意したListから2番目の要素を表示してみましょう(2021-03-15が表示されるはずです)
jshell> dates.get(1)
$3 ==> 2021-03-15

変更のできるList

1. authorsに「hosoya」を追加してみましょう
jshell> authors.add("hosoya")
$6 ==> true

jshell> authors
authors ==> [yamamoto, naoki, sugiyama, hosoya]
2. authorsの2番目の要素を「kishida」に戻してみましょう
jshell> authors
authors ==> [yamamoto, naoki, sugiyama, hosoya]

jshell> authors.set(1, "kishida")
$8 ==> "naoki"

jshell> authors
authors ==> [yamamoto, kishida, sugiyama, hosoya]
3. LocalDateを格納できるArrayListを用意してdatesという変数に割り当ててみましょう
jshell> var dates = new ArrayList<LocalDate>()
dates ==> []
4. 変数datesに割り当てたArrayListに2021年9月14日を追加してみましょう
jshell> dates.add(LocalDate.of(2021, 9, 14))
$11 ==> true

jshell> dates
dates ==> [2021-09-14]

配列

配列の要素の利用

1. 要素が5つのint型の配列を用意してみましょう
jshell> var nums = new int[5]
nums ==> int[5] { 0, 0, 0, 0, 0 }
2. 用意した配列の3番目の要素に2を入れてみましょう
jshell> nums[2] = 2
$14 ==> 2

jshell> nums
nums ==> int[5] { 0, 0, 2, 0, 0 }
3. [2, 3, 5, 7]が入ったint型の配列を用意してみましょう
jshell> var nums2 = new int[]{2, 3, 5, 7}
nums2 ==> int[4] { 2, 3, 5, 7 }
4. 用意した配列の4番目の要素を得てみましょう(7が入っているはずです)
jshell> nums2[3]
$17 ==> 7

レコードで違う種類の値を組み合わせる

レコードのオブジェクトを生成する

1. String型のenglish、String型のjapaneseをコンポーネントにもったレコードWordを定義しましょう。
jshell> record Word(String english, String japanese){}
|  次を作成しました: レコード Word
2. Wordレコードのオブジェクトをいくつか作ってみましょう。
jshell> var apple = new Word("apple", "りんご")
apple ==> Word[english=apple, japanese=りんご]

jshell> var grape = new Word("grape", "ぶどう")
grape ==> Word[english=grape, japanese=ぶどう]
3. LocalDate型のdate、int型のprice、String型のmemoをコンポーネントにもったレコードSpendingを定義しましょう。
jshell> record Spending(LocalDate date, int price, String memo) {}
|  次を作成しました: レコード Spending
4. Spendingレコードのオブジェクトをいくつか作ってみましょう。
jshell> var s1 = new Spending(LocalDate.of(2022, 4, 6), 200, "たまご")
s1 ==> Spending[date=2022-04-06, price=200, memo=たまご]

jshell> var s2 = new Spending(LocalDate.of(2022, 3, 19), 3278, "プロになるJava")
s2 ==> Spending[date=2022-03-19, price=3278, memo=プロになるJava]

Mapで辞書をつくる

Map

変更可能なMap

1. 「dog」に対応する値をgetメソッドで取ってみましょう。
jshell> animals.get("dog")
$25 ==> "いぬ"
2. 「horse」に対して「うま」をputメソッドで格納してみましょう。
jshell> animals.put("horse", "うま")
$26 ==> null

jshell> animals
animals ==> {horse=うま, dog=いぬ, fox=きつね, cat=猫}

HashMapは順番を気にしないため、順番はここで示したものと違うことがあります。もし追加した順を保ちたい場合にはLinkedHashMapを使います。

3. sizeメソッドで件数を確認してみましょう。
jshell> animals.size()
$28 ==> 4

第9章 繰り返し

ループ構文

for文の基本

1. 3回「Hello」と表示するプログラムをForHelloというクラス名で作ってみましょう。
package projava;

public class ForHello {
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            System.out.println("Hello");
        }
    }
}
2. 今回のサンプルプログラムの変数iの名前をnに変えてみましょう。
public static void main(String[] args) {
    for (int n = 0; n < 5; n++) {
        System.out.println(n);
    }
}

こういった場合、変数iに入力カーソルを置いて[Shift] + [F6]を押すと、利用箇所を含めまとめて名前を変更することができます。

3. 今回のサンプルプログラムを0から9まで表示するようにしてみましょう。
for (int n = 0; n <= 9; n++) {
    System.out.println(n);
}

次のようにしても動作は同じですが、やりたいことに出てくる数字がそのままコードに出てくるほうが望ましいです。

for (int n = 0; n < 10; n++) {
    System.out.println(n);
}
4. 今回のサンプルプログラムを1から10まで表示するようにしてみましょう。
for (int n = 1; n <= 10; n++) {
    System.out.println(n);
}

次のようにしても動作は同じですが、やりたいことに出てくる数字がそのままコードに出てくるほうが望ましいです。

for (int n = 0; n < 10; n++) {
    System.out.println(n + 1);
}

for文の応用

1. 0から35まで5ずつ増やしながら表示してみましょう
for (int i = 0; i <= 35; i += 5) {
    System.out.println(i);
}
2. 20から0まで3ずつ減らしながら表示してみましょう
for (int i = 20; i >= 0; i -= 3) {
    System.out.println(i);
}

結果は次のようになります。

20
17
14
11
8
5
2

ループのcontinueとbreak

continue文
1. 0から9まで表示してください。ただし3は表示を飛ばしてください
for (int i = 0; i <= 9; i++) {
    if (i == 3) {
        continue;
    }
    System.out.println(i);
}
2. 0から9まで表示してください。ただし3と5は表示を飛ばしてください
for (int i = 0; i <= 9; i++) {
    if (i == 3 || i == 5) {
        continue;
    }
    System.out.println(i);
}

switchを使うと次のようになります。

for (int i = 0; i <= 9; i++) {
    switch(i) {
        case 3, 5: continue;
    }
    System.out.println(i);
}

->を使って書く場合には、continue文は中カッコで囲む必要があります。

for (int i = 0; i <= 9; i++) {
    switch(i) {
        case 3, 5 -> {
            continue;
        }
    }
    System.out.println(i);
}
3. 0から9まで表示してください。ただし3から6は表示を飛ばしてください
for (int i = 0; i <= 9; i++) {
    if (i >= 3 && i <= 6) {
        continue;
    }
    System.out.println(i);
}
break文

ここでの練習問題は、編集作業の不整合で意味のないものになってしまっています。本来の意図になるように書き換えています。

1. projava.BreakSampleという名前でクラスを作って次のループを動かしてみましょう。
for (int i = 0; i < 5; i++) {
    System.out.println(i);
    if (i < 3) {
        continue;
    }
    System.out.println("finish");
    break;
}
2. この例を動かすとどうなるか考えてみましょう。
3. 実際に動かしてみましょう。

ループに慣れる

デバッガでループを覗く

1. ほかのコードについてもデバッガ―で動作を確認してみましょう

解答略

2重ループ

1. この表は5x9までしか表示されていません。9x9が表示されるようにしてみましょう。
for (int i = 1; i <= 9; i++) {
    for (int j = 1; j <= 9; j++) {
        System.out.printf("%2d | ", i * j);
    }
    System.out.println();
}

次のように表示されます。

 1 |  2 |  3 |  4 |  5 |  6 |  7 |  8 |  9 | 
 2 |  4 |  6 |  8 | 10 | 12 | 14 | 16 | 18 | 
 3 |  6 |  9 | 12 | 15 | 18 | 21 | 24 | 27 | 
 4 |  8 | 12 | 16 | 20 | 24 | 28 | 32 | 36 | 
 5 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 
 6 | 12 | 18 | 24 | 30 | 36 | 42 | 48 | 54 | 
 7 | 14 | 21 | 28 | 35 | 42 | 49 | 56 | 63 | 
 8 | 16 | 24 | 32 | 40 | 48 | 56 | 64 | 72 | 
 9 | 18 | 27 | 36 | 45 | 54 | 63 | 72 | 81 | 
内側のループ回数が変わる場合
1. 次のように表示されるようにしてみましょう
OOOOO
OOOO
OOO
OO
O
for (int i = 5; i >= 1; i--) {
    for (int j = 0; j < i; j++) {
        System.out.print("O");
    }
    System.out.println();
}

もう少しループの練習

丸を並べる
1. 上から4番目を赤くしてみましょう。

for (int x = 0; x < 12; x++) {
    for (int y = 0; y < 12; y++) {
        if (y == 3) {
            g.setColor(Color.RED);
        } else {
            g.setColor(Color.WHITE);
        }                
        g.fillOval(x * 30 + 50, y * 30 + 20, 25, 25);
    }
}
2. ななめに赤くしてみましょう。

for (int x = 0; x < 12; x++) {
    for (int y = 0; y < 12; y++) {
        if (x == y) {
            g.setColor(Color.RED);
        } else {
            g.setColor(Color.WHITE);
        }                
        g.fillOval(x * 30 + 50, y * 30 + 20, 25, 25);
    }
}

迷路ゲームをつくる

1. 右上がゴールになるようにしてみましょう

変数goalの初期値を次のようにします。

var goal = new Position(4, 1);
2. 左下がスタートになるようにしてみましょう

変数currentの初期値を次のようにします。

var current = new Position(1, 3);
3. もっと大きい迷路を定義してみましょう

4. waszが上左右下になっていますが、uhjnを上左右下になるようにしてみましょう。

switch式を次のように変更します。

var next = switch(ch) {
    case 'h' -> new Position(current.x()-1, current.y());
    case 'u' -> new Position(current.x()  , current.y()-1);
    case 'j' -> new Position(current.x()+1, current.y());
    case 'n' -> new Position(current.x()  , current.y()+1);
    default -> current;
};
5. ゴールの位置にGと表示するようにしてみましょう

変数goalと変数xyを比較するelse ifを挿入します。

} else if (map[y][x] == 1) {
    System.out.print("*");
} else if (y == goal.y() && x == goal.x()) {
    System.out.print("G");
} else {
    System.out.print(".");
}

次のようになります。

******
*.*.G*
*...**
*o*..*
******
6. 一歩進むごとに現在位置の表示を「o」と「O」で切り替えるようにしてみましょう。

これは少し難しいです。作戦には2通りあるのですが、まずは正攻法を紹介します。

現在の状態をあらわす変数を用意します。ここでは大文字かどうかをあらわすということでupperとします。初期値にはfalseを割り当てます。

var current = new Position(1, 3);
var upper = false;

表示のとき、uppertrueであればOfalseであればoを表示するようにします。ここでは条件演算子を使っています。

if (y == current.y() && x == current.x()) {
    System.out.print(upper ? "O" : "o");

そして、移動するときにupperを反転するようにします。

if (map[next.y()][next.x()] == 0) {
    if (!current.equals(next)) {
        upper = !upper;
    }
    current = next;
}

これで移動するごとにoOが切り替わります。

さてもうひとつの方法です。
今回は1マス動くごとに切り替わり、斜めには動けないので、マップ上でoOのどちらが表示されるかは固定で、次のような感じになります。

oOoOoOo
OoOoOoO
oOoOoOo
OoOoOoO
oOoOoOo

そうすると、横位置と縦位置の合計が偶数のときにo、奇数のときにOを表示すれば移動ごとに切り替わるようになります。

System.out.print((x + y) % 2 == 0 ? "o" : "O");

7. 現在地のまわり2マスだけ表示するようにしてみましょう。つまり5x5マスが表示されるようにします。

ループの範囲をcurrentの前後2つになるようにします。
そしてmapからはみ出したときの処理を加えます。なにも表示しないという手もありますが、なにかを表示するようにすると自分が必ず中心になるので、よりゲームらしさが出ます。ここでは#を表示します。

for (int y = current.y() - 2; y <= current.y() + 2; ++y) {
    for (int x = current.x() - 2; x <= current.x() + 2; ++x) {
        if (y < 0 || y >= map.length || x < 0 || x >= map[y].length) {
            System.out.print("#");
        }else if (y == current.y() && x == current.x()) {

mapのデータを次のように壁を厚くして、mapからはみ出す部分が表示されることがないようにすると、「mapからはみ出したときの処理」を省けます。処理スピードが求められる場合に使われるテクニックです。

int[][] map = {
    {1, 1, 1, 1, 1, 1, 1, 1},
    {1, 1, 1, 1, 1, 1, 1, 1},
    {1, 1, 0, 1, 0, 0, 1, 1},
    {1, 1, 0, 0, 0, 1, 1, 1},
    {1, 1, 0, 1, 0, 0, 1, 1},
    {1, 1, 1, 1, 1, 1, 1, 1},
    {1, 1, 1, 1, 1, 1, 1, 1}
};

このように、処理がはみださないように置かれるデータを番兵と呼ぶことがあります。番兵を置くことで処理を効率化します。

8. なにも入力せずに「Enter]キーを押したり、2文字入力して[Enter]キーを押したりすると、[z]キーなどを押しても移動しなくなります。どのような操作をすれば移動が行えるようになるか考えてみましょう。

[Enter]キーを押します。

次のように、文字を取得したあと[Enter]キーを表す\nを受け取った場合に処理を飛ばすようにすると、文字を入力しても動かないということが起きなくなります。

int ch = System.in.read();
if (ch == '\n') continue;

第10章 データ構造の処理

データ構造を拡張for文で扱う

基本for文でのListの要素の処理

1. 次のように用意されたListのすべての要素を表示するプログラムを基本for文を使って書いてみましょう。
var names = List.of("yusuke", "kis", "sugiyama");
package projava;

import java.util.List;

public class ExListForBasic {
    public static void main(String[] args) {
        var names = List.of("yusuke", "kis", "sugiyama");
        
        for (int i = 0; i < names.size(); i++) {
            System.out.println(names.get(i));
        }
    }
}

拡張for文でのListの要素の処理

1. 次のように用意されたListのすべての要素を拡張for文を使って表示するプログラムを書いてみましょう。
var names = List.of("yusuke", "kis", "sugiyama");
package projava;

import java.util.List;

public class ExListForExtended {
    public static void main(String[] args) {
        var names = List.of("yusuke", "kis", "sugiyama");
        
        for (String name : names) {
            System.out.println(name);
        }
    }
}

拡張for文での配列の要素の処理

1. 次の配列のすべての要素を表示するプログラムを拡張for文を使って書いてみましょう。
var names = new String[]{"yusuke", "kis", "sugiyama"};
package projava;

public class ExArrayForExtended {
    public static void main(String[] args) {
        var names = new String[]{"yusuke", "kis", "sugiyama"};
        
        for (String name : names) {
            System.out.println(name);
        }
    }
}

値の集合の処理のパターン

共通するパターン
1. List.of("apple", "banana", "grape")について、次の処理を考えてみましょう。
  • 5文字ちょうどの文字列を表示する
var data = List.of("apple", "banana", "grape");
for (var fruit : data) {
    if (fruit.length() == 5) {
        System.out.println(fruit);
    }
}
  • 5文字ちょうどの文字列を取り出した新たなListを作る
var data = List.of("apple", "banana", "grape");
var result = new ArrayList<String>();
for (var fruit : data) {
    if (fruit.length() == 5) {
        result.add(fruit);
    }
}
System.out.println(result);
  • 5文字ちょうどの文字列の個数を数える
var data = List.of("apple", "banana", "grape");
var result = 0;
for (var fruit : data) {
    if (fruit.length() == 5) {
        result++;
    }
}
System.out.println(result);
  • 5文字ちょうどの文字列のすべてが「p」を含むか確認する

まず「共通するパターン」に沿って書くと次のようになります。

var data = List.of("apple", "banana", "grape");
var result = true;
for (var fruit : data) {
    if (!fruit.contains("p")) {
        result &= false;
    }
}
System.out.println(result);

「初期値」がtrue、「結果を加える処理」が&=になっています。 初期値には、計算の結果に影響がない値を設定することになります。足し算の場合は、0を足しても計算の結果に影響を与えません。掛け算の場合は1を掛けても計算の結果に影響を与えません。Listの場合は空のリストを追加しても影響がない、という考え方です。
このように、計算の結果に影響を与えない値を、その計算の単位元 といいます。 この場合、&演算での単位元trueになります。

「加える処理」なので&=にしましたが、この場合は=で構いません。 また、一度pが含まれない文字列が来ると次以降のデータを確認しなくても「すべてがpを含む」が満たされないことになるので、次以降のループの処理は不要になります。

for (var fruit : data) {
    if (!fruit.contains("p")) {
        result = false;
        break;
    }
}
  • 5文字ちょうどの文字列のどれがひとつでも「p」を含むか確認する
var data = List.of("apple", "banana", "grape");
var result = false;
for (var fruit : data) {
    if (fruit.contains("p")) {
        result |= true;
    }
}
System.out.println(result);

「初期値」がfalseで、「結果を加える処理」が|=になっています。
|演算の単位元falseになります。

また、一度pが含まれる文字列が来ると次以降のデータを確認しなくても「どれがひとつでもpを含む」が満たされることになるので、次以降のループの処理は不要になります。

for (var fruit : data) {
    if (fruit.contains("p")) {
        result = true;
        break;
    }
}

Stream

全体に対する処理
1. var strs = List.of("apple", "banana", "orange", "pineapple");があるとき、次の処理をStreamを使って書いてみましょう。

・6文字以上のものを大文字にして表示

jshell> strs.stream().
   ...> filter(s -> s.length() >= 6).
   ...> map(String::toUpperCase).
   ...> forEach(System.out::println)
APPLE
BANANA
ORANGE
PINEAPPLE

・6文字以上のものの文字数の合計を表示

数値での合計にはCollectors.summingIntを使います。

jshell> strs.stream().
   ...> filter(s -> s.length() >= 6).
   ...> collect(Collectors.summingInt(String::length))
$2 ==> 21

String::lengths -> s.length()をメソッド参照の形にしたものです。
あとの章ででてくるmapToIntを使うこともできます。

jshell> strs.stream().
   ...> mapToInt(String::length).
   ...> filter(l -> l >= 6).
   ...> sum()
$3 ==> 21

・すべての文字列がaを含んでるかどうか判定

jshell> strs.stream().allMatch(s -> s.contains("a"))
$4 ==> true

・cを含むものがひとつでもあるかどうか判定

jshell> strs.stream().anyMatch(s -> s.contains("c"))
$5 ==> false

基本型でのStream

StreamとIntStreamの行き来

1. StringクラスのrepeatメソッドはJava 11で導入されたためJava 8では使えません。IntStreamを利用して"test"を3回連結して"testtesttest"を出力する処理を実装してみましょう。
package projava;

import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class ExRepeat {
    public static void main(String[] args) {
        var result = IntStream.range(0, 3)
                .mapToObj(n -> "test")
                .collect(Collectors.joining());
        System.out.println(result);
    }
}

第11章 メソッド

メソッドの宣言

JShellでのメソッド宣言

基本的なメソッド
1. 「Hi!」と表示するhiメソッドを宣言してみましょう
jshell> void hi() { System.out.println("Hi!");}
|  次を作成しました: メソッド hi()
2. 宣言したhiメソッドを呼び出してみましょう。
jshell> hi()
Hi!
引数のあるメソッド
1. greetingメソッドとまったく同じく、"Hello "に続いて受け取った引数を表示するメソッドを`void salutation(String person)に続けて宣言してみましょう。

salutationもgreetingと同じく挨拶という意味です。

jshell> void salutation(String person) { System.out.println("Hello " + person);}
|  次を作成しました: メソッド salutation(String)

jshell> salutation("kis")
Hello kis
2. 引数として数値を受け取って、その回数だけ「Hello」と表示するメソッドを宣言してみましょう。hellohello(1)として呼び出すと「Hello」、hellohello(2)として呼び出すと「hellohello」が表示されます。
jshell> void hellohello(int count) { System.out.println("hello".repeat(count));}
|  次を作成しました: メソッド hellohello(int)

jshell> hellohello(1)
hello

jshell> hellohello(2)
hellohello
3. hellohello(3)として呼び出して動きを確認してみましょう。
jshell> hellohello(3)
hellohellohello
戻り値のあるメソッド
1. 与えられた数字を2倍するメソッドを int dbl(int n)から始めて宣言してみましょう。(doubleは「予約語」となっていてメソッド名に使えません)
jshell> int dbl(int n) { return n * 2;}
|  次を作成しました: メソッド dbl(int)
2. 宣言したメソッドdblを呼び出してみましょう。
jshell> dbl(3)
$10 ==> 6

jshell> dbl(5)
$11 ==> 10
3. 与えられた数字を3倍するメソッドtripleを宣言して呼び出してみましょう。
jshell> int triple(int n) { return n * 3;}
|  次を作成しました: メソッド triple(int)

jshell> triple(3)
$13 ==> 9

jshell> triple(5)
$14 ==> 15
4. 与えられた文字列を2回繰り返すメソッドを宣言して呼び出してみましょう。
jshell> String twice(String s) { return s.repeat(2);}
|  次を作成しました: メソッド twice(String)

jshell> twice("Hello")
$16 ==> "HelloHello"
5. 与えられた2つの整数のうち大きいほうを返すメソッドmax2を宣言してみましょう。条件演算子を使います。
jshell> int max2(int n, int m) { return n > m ? n : m;}
|  次を作成しました: メソッド max2(int,int)

jshell> max2(5, 3)
$18 ==> 5

jshell> max2(1, 4)
$19 ==> 4
6. 与えられた3つの整数のうち一番大きい数値を返すメソッドmax3を宣言してみましょう。
jshell> int max3(int n, int m, int l) { return max2(n, max2(m, l));}
|  次を作成しました: メソッド max3(int,int,int)

jshell> max3(1, 4, 5)
$21 ==> 5

jshell> max3(5, 1, 4)
$22 ==> 5

jshell> max3(4, 5, 1)
$23 ==> 5

インスタンスメソッドの宣言

インスタンスメソッドを宣言する
1. 「(名前)さんの平均点は(平均)点です」と表示するshowResultメソッドをStudentレコードに用意してみましょう。
record Student(String name, int englishScore, int mathScore){
    int average() {
        return (this.englishScore() + this.mathScore()) / 2;
    }
    void showResult() {
        System.out.println("%sさんの平均点は%d点です".formatted(name(), average()));
    }
}

ラムダ式とメソッド参照

ラムダ式

1. 次のメソッドをラムダ式で表してみましょう。
boolean check(String s) {
  return s.contains("y");
}
s -> s.contains("y")
2. 次のメソッドをラムダ式で表してみましょう。
void print(String s) {
  System.out.println(s);
}
s -> System.out.println(s);
3. 次のラムダ式upperという名前のメソッドにしてみましょう。引数と戻り値の型はどちらもStringです。
s -> s.toUpperCase()
String upper(String s) {
    return s.toUpperCase();
}
4. 次のラムダ式をemptyという名前のメソッドにしてみましょう。引数の型はString、戻り値の型はbooleanです。
s -> s.isEmpty()
boolean empty(String s) {
    return s.isEmpty();
}

メソッド参照

1. 次のコードをメソッド参照を使って書き換えてみましょう
IntStream.of(nums).mapToObj(n -> "*".repeat(n)).toList()
IntStream.of(nums).mapToObj("*"::repeat).toList()

IntelliJ IDEAでラムダ式とメソッド参照の変換

1. 次のラムダ式をメソッド参照を使って書き換えましょう
names.stream().map(s -> s.toUpperCase()).toList()
names.stream().map(String::toUpperCase).toList()
2. 次のラムダ式をメソッド参照を使って書き換えましょう
names.stream().map(s -> "%sさん".formatted(s)).toList()
names.stream().map("%sさん"::formatted).toList()
3. メソッド参照をラムダ式を使って書き換えましょう
names.stream().map(String::toLowerCase).toList()
names.stream().map(s -> s.toLowerCase()).toList()

メソッド使いこなし

メソッド呼び出しの組み合わせ

変数を使ってメソッド呼び出しの組み合わせを分解する
1. "three times".repeat("abc".length())を変数を使って分解してみましょう。
var length = "abc".length();
"three times".repeat(length);

再帰とスタック

再帰によるループ
1. 次のforループでの処理を再帰に書き換えてみましょう。
for (int i = 3; i > 0; i--) {
    System.out.println(i);
}

次のようになります。

public class ExRecLoop {
    public static void main(String[] args) {
        loop(3);
    }
    
    static void loop(int i) {
        if (i <= 0) {
            return;
        }
        System.out.println(i);
        loop(i - 1);
    }
}