移動平均とスライディングウィンドウ - 「プロになるJava」ボツ原稿

「プロになるJava」ボツ原稿、今回は「13章 処理の難しさの段階」の「他のデータを参照する」のもうひとつの例として、移動平均やスライディングウィンドウの話題です。
13.1.1のあとに入る想定です。

他のデータを参照する

移動平均

隣のデータを参照しないといけない処理としてもうひとつ、 移動平均 を見てみましょう。移動平均は、日々の売り上げや来店者数など、バラツキのあるデータをなめらかにして傾向を見るときによく使われます。
一定区間の要素を取り出して平均をとり、その区間を移動させながら全体のグラフを書いていくので移動平均といいます。

src/main/java/projava/MovingAverage.java

package projava;

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

public class MovingAverage {

    public static void main(String[] args) {
        int[] data = {3, 6, 9, 4, 2, 1, 5};
        
        var builder = IntStream.builder();
        for (int i = 0; i < data.length; i++) {
            int sum = Arrays.stream(data, Math.max(0, i - 2), i + 1)
                    .sum();
            builder.add(sum / 3);
        }
        int[] result = builder.build().toArray();
        System.out.println(Arrays.toString(data));
        System.out.println(Arrays.toString(result));
    }
}

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

[3, 6, 9, 4, 2, 1, 5]
[1, 3, 6, 6, 5, 2, 2]

ここではIntStream.Builderを使うことでint型の配列を構築しようと思います。
IntStream.Builderを使った配列の構築は次のようになります。まずIntStream.builderメソッドでIntStream.Builderオブジェクトを得ます。

var builder = IntStream.builder();

IntStream.Builderオブジェクトに対してaddメソッドでint型の要素を追加します。

builder.add(data[i]);

要素を追加し終わったら、buildメソッドでIntStreamを得れます。ここではそのままtoArrayメソッドで配列を得ます。

int[] result = builder.build().toArray();

このような、ビルダーオブジェクトをまず用意して要素を構築してからbuildメソッドで目的のオブジェクトを得るというパターンを ビルダーパターン といいます。イミュータブルなオブジェクトを得るときによく使われています。あとで紹介するStringBuilderは文字列を構築するために使います。
「ファイルとネットワーク」の章で紹介したHttpRequestもビルダーを使って構築していました。

HttpRequest req = HttpRequest.newBuilder(uri).build();

さて、移動平均の処理をみていきましょう。 処理を要素数分くり返します。このループはdata.foriと入力して[Tab]キーを押すと補完できます。

for (int i = 0; i < data.length; i++) {

Arrays.streamメソッドは配列からStreamを得るメソッドです。int型の配列を渡すとIntStreamを得れます。また、引数を3つ与えた場合は、最初の引数に配列、2番目の引数に開始位置、3番目の引数に終了位置の次の値を指定します。

int sum = Arrays.stream(data, Math.max(0, i - 2), i + 1)
        .sum();

i番目までをStreamで処理したいので、第3引数にi + 1を渡しています。
第2引数のMath.max(0, i - 2)のようなMath.maxの使い方は、値が0より小さくならないようにするときによく使います。

また、ここで変数sumfor文の処理のブロック内で定義しています。こうすると、この変数sumfor文の処理のブロック内だけで有効な変数になります。変数の有効範囲を スコープ といいます。変数は、定義された中カッコの中でだけ使えると覚えておくといいでしょう。

そうすると、3つの要素の合計を得ることができるので、3で割って平均をとってビルダーに追加します。

builder.add(sum / 3);

処理が終わってビルダーから配列を取り出したら、元の配列と結果の配列をArrays.toStringメソッドを使って整形して表示します。

int[] result = builder.build().toArray();
System.out.println(Arrays.toString(data));
System.out.println(Arrays.toString(result));

移動平均のように、配列などの一部を取り出して処理して、そのあとは取り出す部分をずらして同じ処理を繰り返すような手続きを スライディングウィンドウ といいます。

スライディングウィンドウの処理を素直に実装すると、ウィンドウのサイズに比例した処理時間がかかります。今回はウィンドウのサイズは3件だけなのであまり問題になりませんが、28日分や180日分などウィンドウのサイズが大きくなってくると問題が出てくることがあります。
スライディングウィンドウでは、ウィンドウに入ってくる要素と出ていく要素だけを考えると処理ができることがあります。移動平均の場合は、ウィンドウに入ってくる要素を合計に足して、出ていく要素を合計から引けば、区間の合計を求めることができます。
そのような考えを取り入れたコードは次のようになります。

src/main/java/projava/MovingAverage.java

int sum = 0;
for (int i = 0; i < data.length; i++) {
    sum += data[i];
    if (i >= 3) {
        sum -= data[i - 3];
    }
    builder.add(sum / 3);
}

合計を覚えておく変数sumを用意します。

int sum = 0;

合計に、入ってくる要素を足します。

sum += data[i];

4番目以降の要素を処理するとき、つまり変数iが3以上のときはウィンドウから出ていく要素があるので合計から引きます。

if (i >= 3) {
    sum -= data[i - 3];
}

このようにすると、ウィンドウサイズに関わらず、データ件数だけに比例して処理時間がかかるようになります。

Java Date Time APIでの和暦の扱い、ロケール、タイムゾーン - 「プロになるJava」 ボツ原稿

今回のボツ原稿は和暦を扱うJapaneseDateクラスと各地の時差を反映した時刻を扱うZonedDateTimeについてです。
P.89に5.1.8として続く想定です。

和暦の扱い

日付を扱えるようになると、元号を含んだ日付も扱いたいですよね。 java.time.chrono.JapaneseDateクラスで和暦を扱うことができます。
ではJapaneseDateクラスを使って、元号の付いた日付を表示してみましょう。

JapaneseDate.now()を実行してみます。このとき、新たなimportが必要になりますが、自分で入力せずにJShellの補完機能を使ってみます。
JapaneseDateと入力した状態で[Shift]+[Tab]を押したあと、[i]キーを押します。

jshell> JapaneseDate
0: 何もしない
1: import: java.time.chrono.JapaneseDate
選択:

選択肢が表示されるので、ここで[1]キーを押してみます。 「imported」と表示されて次のようになったはずです。

Imported: java.time.chrono.JapaneseDate
jshell> JapaneseDate

これはimport java.time.chrono.JapaneseDateを実行したのと同じ効果があります。ここで先ほどと違い*ではなくJapaneseDateクラスを指定しているので、java.time.chronoパッケージの中のJapaneseDateクラスだけパッケージ名を省略できるようになります。

続きの.now()を入力して実行してみましょう。

jshell> JapaneseDate.now()
$20 ==> Japanese Reiwa 3-06-18

「Reiwa」と表示されて和暦での年が表示されました。

サブパッケージ

ところで、JapaneseDateクラスが所属しているjava.time.chronoパッケージは、java.timeパッケージに対して「サブパッケージ」という関係になっています。
すでにjava.time.*に対してimportを行っているのでchrono.JapaneseDateと入力できそうですが、残念ながらそれはできません。サブパッケージは、プログラムから見たときの特別扱いは何もないので、利用するときは名前の前半がかぶってる全く別のパッケージだと考えましょう。

ロケール

ところで、曜日を表示するとどうなるでしょうか?%tAを使って曜日を表示してみます。

jshell> "%tA".formatted(today)
$6 ==> "日曜日"

日本語で「日曜日」と表示されました。もしかしたら「Sunday」と表示された人もいるかもしれません。これは、地域や言語によって表記を変える仕組みによるもので、このような仕組みを ロケール と言います。

言語の設定を変えて実行してみましょう。formattedメソッドでは言語の設定を変更できないので、同じ動きをするString.formatメソッドを使ってみます。

例えば"%tA".formatted(today)String.formatメソッドを使って書き換えると次のようになります。

jshell> String.format("%tA", today)
$10 ==> "日曜日"

formatメソッドの最初の引数にロケールを渡すことができます。英語を設定するときにはLocale.ENGLISHを指定します。

jshell> String.format(Locale.ENGLISH, "%tA", today)
$9 ==> "Sunday"

「Sunday」と表示されました。
日本語を指定する場合にはLocale.JAPANESEを指定します。

jshell> String.format(Locale.JAPANESE, "%tA", today)
$8 ==> "日曜日"

このように、ロケールとして言語を設定すると、それに従って表示がかわります。


練習 1. Locale.CHINESEを指定して曜日を表示してみましょう。


タイムゾーン付日付時刻

Java 17のリリースは14時30分だった」という話を聞いたとき、ちょっとおかしいと思いませんでしたか?Javaは世界中で使われています。どの地域でいう14時30分だったかは気になるところです。
日付時刻を使ってある瞬間を表すには、それがどの地域での日付時刻かを表す タイムゾーン の指定も必要になります。タイムゾーン付きの日付時刻としてZonedDateTimeクラスが用意されています。
ZonedDateTime.now()として現在時刻を得てみましょう。

jshell> ZonedDateTime.now()
$136 ==> 2021-09-19T06:05:21.549662100+09:00[Asia/Tokyo]

LocalDateTimeの場合と比べてみましょう。

jshell> LocalDateTime.now()
$137 ==> 2021-09-19T06:06:38.967300300

ZonedDateTimeの値には現在時刻のあとに時差を表す「+09:00」とタイムゾーンを表す「Asia/Tokyo」がついていることがわかります。
UTC(協定世界時) は世界の時間の基準になる時間です。世界の時間はUTCからの時差をつかって表現されます。日本ではUTCから9時間の差があるので「+09:00」が表示されていました。
日本人が多く訪問している国・地域のタイムゾーンには次のようなものがあります。

ZoneId 国・地域
Asia/Tokyo 日本
Asia/Seoul 韓国
Asia/Shanghai 中国
Asia/Taipei 台湾
Asia/Bangkok タイ
Asia/Singapore シンガポール
Asia/Ho_Chi_Minh ベトナム
Asia/Manila フィリピン
US/Pacific アメリカ西海岸
US/Eastern アメリ東海岸
Europe/Berlin ドイツ
Etc/UTC 協定世界時

それでは、タイムゾーンを指定して今の時刻を得てみましょう。アメリ東海岸でのいまの時刻を見てみます。アメリ東海岸タイムゾーンは「US/Eastern」で表します。

jshell> ZonedDateTime.now(ZoneId.of("US/Eastern"))
$130 ==> 2021-09-18T13:07:29.331452500-04:00[US/Eastern]

アメリ東海岸なのでニューヨークあたりの現在時刻がわかりました。
タイムゾーンZoneIdクラスの値として指定します。ZoneIdの値を得るにはZonedId.ofメソッドにタイムゾーンを指定して呼び出します。

それではJava 17リリース日時をタイムゾーン付きで表してみましょう。Java 17はUTCでいう14時30分にリリースされました。そこでタイムゾーンUTCを指定してみましょう。 「Etc/UTC」として指定します。

jshell> var java17 = ZonedDateTime.of(java17dateTime, ZoneId.of("Etc/UTC"))
java17 ==> 2021-09-14T14:30Z[Etc/UTC]

これを日本時間で表してみましょう。 withZoneSameInstantメソッドで、同じ瞬間の別のタイムゾーンでの時刻を表すことができます。

jshell> java17.withZoneSameInstant(ZoneId.of("Asia/Tokyo"))
$94 ==> 2021-09-14T23:30+09:00[Asia/Tokyo]

つまり、Java 17は日本時間では夜の23時半にリリースされたわけですね。
コンピュータのタイムゾーンの設定を日本時間にしている人も多いと思います。 ZoneId.systemDefaultメソッドで動かしてるコンピュータのタイムゾーンを得ることができます。

jshell> java17.withZoneSameInstant(ZoneId.systemDefault())
$92 ==> 2021-09-14T23:30+09:00[Asia/Tokyo]

タイムゾーン付きの日付時刻は、夏の間に1時間早くなる夏時間などを考えると、日付と時刻には密接な関係があるためZonedDateTimeだけが用意されていて、日付だけをあらわすZonedDateや時刻だけを表すZonedTimeはありません。
プログラムの勉強をしていると、日付時刻の表し方についても詳しくなっていきますね。


練習 1. いまの時刻がアメリカ西海岸では何時になっているか確認してみましょう。
2. 日本時間での2021年7月23日20時00分がシンガポールで何時だったか確認してみましょう。


Jakarta EE StarterでJava EE/Jakarta EEを始める

最近のフレームワークにはナントカStarterが用意されていて、簡単に最初のプロジェクトを作れるようになっています。
Java EE/Jakarta EEの場合、コンテナを起動してデプロイするという2段階だったりして、最初に動かすところまでが大変でした。

そういう問題に対処するために、Eclipse starter for Jakarta EEが用意されました。
https://start.jakarta.ee/

他のstarterと違って、Webでいろいろ選んで初期ファイルを構成するのではなく、単なるMavenArchetypeです。

mvn archetype:generate -DarchetypeGroupId="org.eclipse.starter" -DarchetypeArtifactId="jakarta-starter" -DarchetypeVersion="1.0.0"

実行はこんな感じ・・・

mvn clean package payara-micro:start

しかしテストでこけるので、テストを飛ばしましょう。
f:id:nowokay:20220418104644p:plain

-DskipTests=trueをつけて実行します。

mvn clean package payara-micro:start -DskipTests=true

準備が整うまで結構時間がかかります。REST Endpointsの表示が出たら準備完了。
f:id:nowokay:20220418110922p:plain

localhost:8080に接続すると次のようになります。
f:id:nowokay:20220418104854p:plain

ソースコードとしてはJAX-RSのエンドポイントとしてCafeResource.javaJPAのエンティティとしてCoffee.java、クエリーを投げるレポジトリとしてCafeRepository.javaができています。 f:id:nowokay:20220418111507p:plain

CafeResource.javaはこんな感じ

@Path("coffees")
public class CafeResource {

    @Inject
    private CafeRepository cafeRepository;

    @GET
    @Produces({ MediaType.APPLICATION_JSON })
    public List<Coffee> getAllCoffees() {
        return this.cafeRepository.getAllCoffees();
    }

    @POST
    @Consumes({ MediaType.APPLICATION_JSON })
    public Coffee createCoffee(Coffee coffee) {
        try {
            return this.cafeRepository.persistCoffee(coffee);
        } catch (PersistenceException e) {
        ....

Coffee.javaはこんな感じ

@Entity
@NamedQuery(name = "findAllCoffees", query = "SELECT o FROM Coffee o ORDER BY o.name")
public class Coffee implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank(message = "Name cannot be blank.")
    protected String name;

    @NotNull(message = "Price must be set.")
    @PositiveOrZero(message = "Price must be greater than or equal to zero.")
    protected Double price;
        ...

CafeRepository.javaはこんな感じ

@Stateless
public class CafeRepository {

    @PersistenceContext
    private EntityManager entityManager;

    public List<Coffee> getAllCoffees() {
        logger.log(Level.INFO, "Finding all coffees.");

        return this.entityManager.createNamedQuery("findAllCoffees", Coffee.class).getResultList();
    }

    public Coffee persistCoffee(Coffee coffee) {
        logger.log(Level.INFO, "Persisting the new coffee {0}.", coffee);
        this.entityManager.persist(coffee);
        return coffee;
    }

サーブレットも動かせます。

@WebServlet(urlPatterns = "/NewServlet")
public class NewServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("text/html;charset=UTF-8");
        try ( PrintWriter out = response.getWriter()) {
            out.println("""
                        <!DOCTYPE html>
                        <html><head>
                        <title>Servlet NewServlet</title>
                        </head>
                        <body>
                        <h1>Servlet NewServlet at %s</h1>
                        %s
                        </body></html>
                        """.formatted(request.getContextPath(), LocalDateTime.now()));
        }
    }

}

f:id:nowokay:20220418113923p:plain

ということで、Java EE / Jakarta EEでなにか作るぞっていう場合の足掛かりになるようになっていますね。

初めて買った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言語では戻り値は含めないのですが、実際の「シグネチャ」の使い方はメソッドの「概形」をさすことも多いので、うまい説明を考えないといけない。