初めて買ったWindowsパソコンをバラした

初めて買ったWindowsパソコンが使わないのになぜかそのまま置いてあったので、そろそろ捨てようとバラすことにしました。

「例の火事」というのはこれ
https://nowokay.hatenablog.com/entry/2022/03/30/074816

持ってたのはEPSONのvividy TOWER VM516Tです。

IntelPentiumシールとかDesigned for Windows 95シールとか貼ってある。

スペックとしてはこんな感じ

項目 オリジナル 現状
CPU Pentium 166MHz K6-2 400MHz
メモリ 16MB 64MB
マザーボード P/I-XP55T2P4 <-
ビデオカード S3 Virge <-
サウンド SoundBlusterAWE32 <-
HDD 2GB ?
CD 6倍速4連装 ?
モデム 28.8kbps 56k
その他 SCSI <-

背面はこう

まだUSBがない時代ですね。SCSI刺さってるくらいだし。
ボードは上から56kモデム、ビデオ、LAN、SCSI、28.8kモデム、サウンドです。
LANカードに同軸の端子があるのも時代を感じますね。 SCSIにはMOがつながっていました。MOってなにって言われそうですが、3.5インチの光磁気ディスクです。230MBとかの大容量。

マザーボード、こんな感じ。

チップセットも、ノースブリッジ(左)、サウスブリッジ(右)に分かれてますね。
f:id:nowokay:20220416205907p:plain
というか、最近はチップひとつにまとまってるけど、チップセットという言葉だけ残ってますね。
CPUに近いノースブリッジがメモリにつながって、離れたサウスブリッジが拡張カードにつながっています。

CPU。ファン小さいですね。そしてCPU以外にはヒートシンクすらついてない。 f:id:nowokay:20220416211733p:plain

メモリはSIMMで、16MBが4枚。チップが日立ですね。そういう時代もあった。

ビデオカードはこれ

奥のサウンドブラスタは、端子をみるとPCIバスじゃなくISAバスなのがわかります。

なかなか懐かしかった。マザーボード+CPU+メモリはそのまま残して、他は捨てます。

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 {}