さよなら「あなたとJAVA」

みんなから愛された「あなたとJAVA」の役割が終わったようです。

Java」で検索するとjava.comのサイトがひっかかるのですが、このサイトは古いまま放置されていて、Javaの学習を始める人にとっての罠になっていました。
https://www.java.com/ja/

「あなたとJAVA」というキャッチコピーの脱力感と、「ダウンロー」で改行され「ド」だけが目立ってしまう間のヌケかたから大人気のサイトでしたが、かっこいいものではない・・・

もともとはJAVA+YOUで、2008年JavaOneのキャッチコピーでした。これは大文字だけのデザインだからよかったのだけど、日本語訳するときJAVAだけ大文字で残ってしまい「JAVAではなくJava」の説得力をなくさせてくれていました。

それに、ほとんどの人がJavaのプログラミングの勉強をしようとして「Java」を検索するのにJREの配布サイトにたどりついてニッチもサッチもとなっていたので、学習を阻害していました。

「ダウンロー」に関しては一昨年9月ごろに修正されていたので、ちょっとずつ手が入ったりしていたようではあるけど、抜本的な更新は されていなかった。
https://arigato-java.download/newline/

ところが、いま見たところデザインが更新されて、OpenJDKやOracle JDKへのリンクもついて、問題が解消されている。

4/24には「あなたとJAVA」だったので、この1カ月の間に変わったっぽい。
これで万事解決ではあるけども、「あなたとJAVA」が見れなくなったのは少し寂しいものがありますね。

do whileやwhileなど繰り返しの補足 - 「プロになるJava」ボツ原稿

「プロになるJava」ボツ原稿、今回は9章「繰り返し」の補足です。
書籍ではwhileやdo-while構文について簡潔な説明を載せていますが、多めの説明をしています。
また、繰り返し構文にまつわる話題とサンプルを2つ載せています。

繰り返しの構文

while文

while文は「条件が成り立つ間、処理が繰り返される」という構文です。for文から「初期化」と「繰り返し時の処理」を省いたものとも言えます。
構文は次のようになります。

while (繰り返し条件) {
  繰り返す処理
}

ここまで単純だと、最初は使い方のイメージがつかないかもしれません。そこでまずはfor文をwhile文に置き換えてみましょう。

for (int i = 0; i < 5; i++) {
    System.out.println(i);
}

これもIntelliJ IDEAが変換してくれます。forに入力カーソルを持っていって[Alt / Opt]+[Enter]を押すとメニューが開くので「Replace with 'while'」を選ぶとfor文がwhile文に置き換えられます。

コードは次のようになります。

int i = 0;
while (i < 5) {
    System.out.println(i);
    i++;
}

i < 5という条件が成り立っている間、変数iの値の表示とi++を繰り返します。i++によって変数iの値は1ずつ増えていくので、そのうちi < 5という条件は成り立たなくなります。そのときに繰り返しが終わって次の処理にいきます。ここでは次の処理はないのでプログラムは止まります。


練習

  1. projava.WhileSampleという名前でクラスを作ってここでのサンプルコードを動かしてみましょう 次のコードをwhile文で書いてみましょう
for (int i = 3; i > 0; i--) {
    System.out.println(i);
}
  1. 次のコードをfor文で書いてみましょう
int i = 1;
while(i <= 10) {
    System.out.println(i);
    i += 2;
}

do while文

もうひとつの繰り返し構文はdo while文です。 do while文は繰り返す処理を1度行って、条件を満たしていればもう一度繰り返すという構文です。

do {
  繰り返す処理
} while (条件);

あまり出番は多くないですが、処理を行ってよい結果がでてなかったらもう一回という場合に使います。分かりやすい例でいえば、通信を行ってエラーになったら一回のようなリトライ処理ですね。

var r = new Random();
int score;
do {
    score = r.nextInt(100);
} while(score < 80);
System.out.println("%d点!".formatted(score));

動かすと84点!のように表示されます。 ここで使っているRandomクラスは疑似乱数を生成するためのクラスで、java.utilパッケージに属しています。「乱数」は予測不能な数を返すものです。「疑似」とついているのは、計算によって生成するので計算式やパラメータさえわかっていれば次の値が予測できるため真の乱数ではないということです。 nextIntメソッドは与えた整数のひとつ下の値までを生成します。ここでは0から99までの数字を生成します。

score = r.nextInt(100);

80点が取れてなかったらもう一回、となっています。

while(score < 80)

while句で使われる変数はループの前までに宣言しておく必要があります。

int score;

練習 1. projava.DoSampleという名前でクラスを作ってここでのサンプルコードを動かしてみましょう。import文が必要になるので気をつけてください。 2. 80点が表示されることがあるかどうか考えてみましょう


「繰り返し」に関する話題

適切なプログラム

練習の4番目について、多くの人が次のようにしたのではないかと思います。

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

※ 「1から10まで表示するようにしてみましょう」という問題です。

次のようにした人もいるかもしれません。

for (var n = 1; n < 11; n++) {
    System.out.println(n);
}

どちらも動きとして間違っていないので、「正しいプログラム」です。
けれども、「適切なプログラム」は次のようなものになります。

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

ここで「適切なプログラム」はやりたいことを表すプログラムのことを指します。動きや性能など他の要素が同じであれば、やりたいことをうまく表すのがいいプログラムです。
この練習問題でやりたいことは「1から10まで表示」です。であれば「1」と「10」が使われているものが「適切」です。

また、for文を使いこなせるようになるには、基本的な形を覚えたあとで、少しずつ形を変えていくことも大事です。ここでは繰り返し条件で<演算子ではなく<=演算子を使うようにしました。

プログラムはコンピュータに指示を与えて処理を行うだけではなく、ほかのプログラマに「このような処理を記述している」と伝えるものでもあります。
ただし、今回のように、これが他よりうまくやりたいことを表していると言えるようなプログラムばかりではありません。だいたい同じだけどどちらがより適切なプログラムか悩むというのも、プログラマの仕事です。

空ループ問題

次のようなコードを実行して、「Hello」を5回表示したいのになぜか1回しか表示されない、ということがあります。

src/main/java/projava/LoopSample.java

public class LoopSample {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++); {
            System.out.println("Hello");
        }
    }
}

入力して試しても、うっかり5回表示されてしまう人もいるかもしれません。そのくらいわかりにくいミスです。
よく見るとfor文の直後に、セミコロンが入っています。

for (int i = 0; i < 5; i++); {
                           ^

IntelliJ IDEAでは黄色く強調表示されます。これはなにか警告があるという合図です。このような警告は確認するようにしましょう。

セミコロン(;)を空ブロック({})に置き換えるとわかりやすいと思います。

for (int i = 0; i < 5; i++){} {
    System.out.println("hello");
}

つまり、このコードでは「なにもしない」を5回くりかえして、printlnを1度実行することになります。
もし何もしないループを書きたい場合はこのような空のブロックを書くほうが、意図した空ループであることがわかりやすいでしょう。
空ループが必要な理由をコメントで記述しておくと戸惑いにくいです。

もう少しループの練習

格子の描画

まずは縦線をたくさん引いてみます。 「projava.GridSample」という名前でクラスを作成して次のコードを入力してください。

src/main/java/projava/GridSample.java

package projava;

import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;

public class GridSample {
    public static void main(String[] args) {
        var image = new BufferedImage(600, 400, BufferedImage.TYPE_INT_RGB);
        var g = image.createGraphics();
        for (int x = 50; x <= 500; x += 30) {
            g.drawLine(x, 10, x, 370);
        }

        var f = new JFrame("格子");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.add(new JLabel(new ImageIcon(image)));
        f.pack();
        f.setVisible(true);
    }
}

実行すると次のように縦線が引かれます。

このコードでは、600px×400pxの画像領域を用意して、その画像に描画するGraphicsを変数gで扱えるようにしています。

var image = new BufferedImage(600, 400, BufferedImage.TYPE_INT_RGB);
var g = image.createGraphics();

画像はImageIconを経由してJLabelで表示するようにして、JFrameウィンドウに追加しています。

f.add(new JLabel(new ImageIcon(image)));

ウィンドウの表示などに関する説明は省略するので、「SwingでのGUI」の章を復習してください。

ループの処理としてX座標が50から500まで30pxごとに処理を行います。

for (int x = 50; x <= 500; x += 30) {

始点と終点のX座標が同じで、Y座標だけ変わっているので縦線が引かれます。

g.drawLine(x, 10, x, 370);

このループを次のループで置き換えてみましょう。

for (int y = 10; y <= 370; y += 30) {
    g.drawLine(50, y, 500, y);
}

そうするとここでは横線が引かれます。

for (int y = 10; y <= 370; y += 30) {

練習として、両方あわせて格子にしてみましょう。

モアレ

「projava.Moire」という名前でクラスを作成して、「GridSample」のループ以外の部分をコピーしてください。 クラス名の部分がGridSampleではなくMoireになっているか注意してください。

ループ部分に次のように入力します。

src/main/java/projava/Moire.java

for (int x = 0; x < 600; x += 5) {
    g.drawLine(x, 0, 600 - x, 400);
}

実行すると次のような模様が描かれます。

ここではX座標を0から600まで5ずつ処理を行います。

for (int x = 0; x < 600; x += 5) {

縦線を引くときは始点と終点で同じ座標を使っていましたが、ここでは始点は0から離れていく方向、終点は600から離れていく方向で座標を指定しているので、斜めの線が引かれていきます。

g.drawLine(x, 0, 600 - x, 400);

このとき、描画のドットの関係で模様が現れます。このような描画誤差による模様を モアレ(Moire) といいます。

練習として、左右に線が引かれていない領域があるので、埋めてみてください。

アルゴリズムと計算量 - 「プロになるJava」ボツ原稿

「プロになるJava」ボツ原稿、今回は「13章 処理の難しさの段階」に入れようと思っていた、「アルゴリズムと計算量」の話です。
こういう話題でよくでる「こんな難しいプログラム組まないのでは?」という疑問についても最後にまとめています。

アルゴリズムと計算量

ここまでいろいろな処理の計算手順について紹介しました。こういった計算手順のことを アルゴリズム といいます。
同じ処理をするアルゴリズムはいろいろ考えられます。そのとき気になるのは実行性能の問題です。
ただ、実行性能はコンピュータによってクセがあるので、アルゴリズムそのものについてを考えるときにはそういったクセを取り除いて考えたいものです。
そこで、アルゴリズムの性能について考えるときには、データが増えるごとに計算時間や使用メモリがどのように増えていくかということを考えます。
この増え方のことを 計算量 といいます。計算時間に関する計算量を 時間計算量 、使用メモリに関する計算量を 空間計算量 といいますが、単に計算量と言ったときには時間計算量のことを表します。

リストの探索

アルゴリズムによる計算量の違いについて見てみましょう。 例えば次のように数値が昇順に並べられたListがあるとします。昇っていく順なので小さい数字が前にくる順番です。

var data = List.of(9, 15, 21, 24, 37, 47, 52, 78, 83, 87);

この配列に16や78といった数値が含まれているかどうか判定するプログラムを作ることを考えます。

線型探索

まず考えられるのが、Listの要素を先頭から順に値を確認して、求める値がListに含まれるかどうか確認する方法です。

src/main/java/projava/SearchSample.java

package projava;

import java.util.List;

public class SearchSample {

    static boolean search(List<Integer> data, int target) {
        for (int n : data) {
            if (n == target) {
                return true;
            }
        }
        return false;
    }

    public static void main(String[] args) {
        var data = List.of(9, 15, 21, 24, 37, 47, 52, 78, 83, 87);

        System.out.println(search(data, 16));
        System.out.println(search(data, 78));
    }
    
}

16を探すとList中にないのでfalseになり、78を探すとList中にあるのでtrueが表示されます。

false
true

拡張for文でデータを先頭から処理をして、該当する値があればtrueを返し、値がみつからないままループが終わった時にはfalseを返しています。

for (int n : data) {
    if (n == target) {
        return true;
    }
}
return false;

このように頭から順にひとつずつデータを確認するような探索方法を 線形探索 といいます。

データが昇順に並んでることを利用すると、前から順に探していってデータが求める値より大きければ、それ以降には求めるデータがないことがわかります。
そこで次のように、途中で探索を打ち切ることで少し計算が速くなります。

src/main/java/projava/SearchSample.java

static boolean search(List<Integer> data, int target) {
    for (int n : data) {
        if (n == target) {
            return true;
        } else if (n > target) {
            return false;
        }
    }
    return false;
}

また、この処理はStreamで書くことができます。Streamで書くと次のようになります。

src/main/java/projava/SearchSample.java

static boolean search(List<Integer> data, int target) {
    return data.stream()
               .anyMatch(n -> (n == target));
}

二分探索

データが昇順に並んでいるので、もっと効率のよい探し方があります。
先ほど、求める値よりデータが大きければそれより後に該当する値は存在しないことを使って、処理を少し速くしました。
同じように、求める値よりデータが小さければそれより前に該当する値は存在しないとも言えます。

そうすると、真ん中の値を取り出してどちら側にデータがあるかを見ていくことができます。
データが半分になったら、そのデータに対して同様に処理を行っていくと、最終的にデータがあるかどうかの判定ができます。

このような探索方法を 2分探索(にぶんたんさく) といいます。
2分探索は次のようなコードになります。

src/main/java/projava/SearchSample.java

static boolean search(List<Integer> data, int target) {
    if (data.isEmpty()) {
        return false;
    } else if (data.size() == 1) {
        return data.get(0) == target;
    }
    int midIndex = data.size() / 2;
    int midData = data.get(midIndex);
    if (midData == target) {
        return true;
    } else if (target < midData) {
        return search(data.subList(0, midIndex), target);
    } else {
        return search(data.subList(midIndex + 1, data.size()), target);
    }
}

Listに対する再帰の処理では、データがないときや1件のときの処理を先に済ませておくのがコツです。

if (data.isEmpty()) {
    return false;
} else if (data.size() == 1) {
    return data.get(0) == target;
}

ここではListが空であればfalse、要素が1件であればその要素が求める値に一致するかどうかを返します。

真ん中の位置を計算して、その要素を得ます。

int midIndex = data.size() / 2;
int midData = data.get(midIndex);

真ん中の要素が求める値であればtrueを返して処理を終わります。

if (midData == target) {
    return true;

真ん中の要素が求める値よりも大きければ、求める値は前半部分にあるはずなので、subListメソッドで前半部分を切り出してsearchメソッドを再び呼び出します。

} else if (target < midData) {
    return search(data.subList(0, midIndex), target);

残ったのは真ん中の要素が求める値よりも小さい場合なので、求める値は後半部分にあるはずなので、subListメソッドで後半部分を切り出してsearchメソッドを再び呼び出します。

} else {
    return search(data.subList(midIndex + 1, data.size()), target);

こうして求める値がListに含まれているかどうかを判定できます。


練習 1. このサンプルではコードが単純化できるようにListを使っていますが、実際には整数については配列で扱うほうが効率がよいです。配列にして作り直してみましょう。
配列の分割を行う適切なメソッドはないので、Arrays.copyOfRangeメソッドを使って配列の部分コピーを作成することで同様の処理ができます。
2. 配列のコピーを行うのは効率が悪いので、コピーを行わずに処理できるようにしてみましょう
3. 前節の再帰を使ったループで24000を超えるとStackOverFlowErrorが発生していました。エラーが発生する回数を正確にみつけるときにも2分探索の考え方が使えます。実際に2分探索の考え方を使ってエラーが発生する回数をみつけてみましょう。


計算量

それではリストの探索の計算量を考えてみましょう。 線形探索では、データが増えれば増えた分だけ処理量が増えます。データが100倍になれば100倍、1000倍になれば1000倍の処理量がかかります。
データ量と処理量の関係は次のようになります。

線形探索で探索を途中で打ち切るようにする改良を紹介しましたが、この場合は処理量は平均すると半分くらいになるものの、データ量が増えたときの処理量の増え方は変わりません。
線形探索ではデータが増えれば増えただけ処理量が増えます。このとき、前処理にかかる時間をA、1回の処理時間をB、結果出力にかかる時間をCとして、データ量をnであらわすと、処理時間tはt = A + Bn + Cで表せます。

ただ、ここでデータ数が大きくなったときには前処理の時間Aや結果出力の時間Cはどんどん無視できるようになります。また、1回の処理時間Bは実装方法でも変わり、ここではデータ数によって処理時間がどのように増えるかを知りたいので、処理時間Bも無視できます。
このように、式を単純化して計算時間を見積もる方法を 漸近的解析といいます。漸近的解析で求められた大ざっぱな計算時間は 漸近的記法 で表します。線形探索の計算時間を漸近的記法で表すとt=O(n)となります。計算時間を大文字のOを使って表す記法を big-O記法 といいます。
アルゴリズムの計算量を表すときは、big-O記法を使って「線形探索の計算量はO(n)だ」のようにいいます。

2分探索では、データが2倍になったときに処理が1回増えるので、データが1000倍になっても処理は10回増えるだけです。データが増えてもデータが増えたほどに処理量は増えません。
2分探索でのデータ量と処理量の関係は次のようになります。

2分探索の計算量はO(log n)になります。logなんてよくわからない、という人も多いと思いますが、処理が進むごとにデータ件数が一定の比率で減っていく場合に計算量がO(log n)になると覚えておけばいいと思います。2分探索では処理が進むごとにデータ件数が1/2に減っていきます。

このように、同じ処理でもやりかたによってデータが増えたときに遅くなる度合いが変わります。
移動平均の処理では、データ件数をn、ウィンドウサイズをwとすると、最初の処理の場合の計算量はO(n × w)、効率化した場合の計算量はウィンドウサイズに関わらなくなるのでO(n)となります。

よく見かける計算量には次のようなものがあります。

計算量 処理の傾向 実際の処理例
O(1) ループがない 配列から指定要素を得る
O(log n) 処理が進むごとに処理件数が一定割合で減る 2分探索
O(n) 処理が進むごとの処理件数が一定件数減る。データ件数分のループ 線形探索
O(n log n) 処理が進むごとに一定割合で減ったデータに対して件数分のループ 昇順並べ替え
O(n2) データ件数分の二重ループ 平面上の点から、一番距離が近い組み合わせを得る
O(2n) 複数の要素のすべての組み合わせを得る 容量の決まったナップサックに重さの違う複数の品物をギリギリまで詰める組み合わせを得る
O(n!) 複数の要素のすべての順列組み合わせを得る セールスマンが複数の会社に営業に行くときどの順番が最も移動距離が短いか判定する

処理したいデータ量が多くなるときは、データ量に対してどのように処理量が増えていくか、計算量を考えることが大切になります。
特に、計算量がO(2n)やO(n!)になると少しデータが増えただけで計算が終わらなくなってしまいます。手元で少ないデータで動かすときには大丈夫なのに、実際の運用を始めてデータが増えると重くて使い物にならないということにもなりかねません。
データ量に対する計算時間の伸びについて意識できるようになりましょう。

こんな難しいプログラム組まないのでは?

実際には昇順に並んだ配列から要素を探すにはArrays.binarySearchメソッドがあります。難しい処理はライブラリやフレームワークが提供さていることが多く、自分で実装する必要性は高くありません。
それではこのような難しいことは勉強する必要がないのでしょうか?

一部に難しい処理が現れることがある

アプリケーションを作るとき、おそらくプログラムのほとんどは簡単な処理として書くことができます。ただ、プログラムが大きくなってくると、その中での一部に難しい処理が現れてきます。
そういった、一部に現れる難しい処理を適切に書けることは、安定したアプリケーションを作るうえで大切になります。

それぞれの処理が簡単でも全体では難しい処理をすることがある

データ中から重複を省く処理を書くことはありませんが、Twitterのような投稿機能を実装するときに直前と同じ投稿を無視するような処理を書くことはあります。
ループの中で状態遷移するプログラムを書くことはありませんが、注文を行ったら「注文中」、運営者が商品発送を行ったら「配送中」、商品が届いたら「完了」のように状態が変わるシステムを組むことは多くあります。
システム内の状態が変わるのに日数がかかったりフレームワークに隠されたりして、それぞれの状態ごとの処理を別の場所に書くことも多いです。そうすると、個別の処理は簡単に見えます。
例えば状態遷移のプログラムのcase句を抜き出せば、条件によって値を変えるかメソッドを抜けるという簡単な処理になっています。

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

目の前の処理の簡単さにだまされて状態遷移に気づかず実装を進めたとき、いきあたりばったりなコードを書いてしまい、運用を始めると条件によって変な動きをすることがわかり、その場しのぎの修正をすると必要以上に複雑になる、ということもよく見かけます。
「こんな難しいプログラム組まないのでは?」という疑問の答えとしては、「個々の処理で難しいプログラムを書くことは少ないけど、プログラム全体としてこういった難しさがでてくることは多い」となります。
簡単に見える処理を積み上げていくといつのまにか難しい処理になっているのがプログラムです。

ほかの人が勉強していないので差別化できる

普通の入門書にはこういった難しい話は載っていません。つまり、ほとんどの入門者はこういったことを勉強しないのです。勉強しようと思ったときには難しめの本が必要になってギャップがおおきく、手を出しづらいということもあります。
プログラマはデキル人になるほど人数がすごく少なくなるという状況があります。
今後、単純なWebサイトや企業システムの構築はどんどん簡単になり、そのような簡単なサイトやシステムを組める人はどんどん増えていきます。その裏では難しい部分も増えていきますが、難しいプログラムが組める人はそこまでは増えないと思っています。
つまり、作るものや作る人について、難しいものと簡単なもの、難しいことができる人と簡単なことだけできる人が二極化していくことが予想されます。 そういった場合、ここに書いたことをしっかり理解できれば、プログラマ全体としてかなり上位に入ることができるようになります。

カッコが対応しているかどうか判定する - 「プロになるJava」ボツ原稿

「プロになるJava」ボツ原稿、今回は「13章 処理の難しさの段階」の「隠れた状態を扱う処理」のもうひとつの例として、カッコの対応を判定する処理です。13.1.2のあとに入る想定です。
書籍にのせたランレングス圧縮よりもカッコの対応の判定のほうが「隠れた状態」がわかりやすいのですが、重複を省く処理からの流れでランレングス圧縮を残しました。

隠れた状態を扱う処理

カッコの対応の判定

((test(data))test)」のような文字列で、カッコ「(~)」が正しく対応しているかどうかを判定するプログラムを考えます。このとき、単純に前の要素を見ても、正しく対応しているかどうかの判断はできません。
最初に思いつくのは、開きカッコと閉じカッコが同じ数だけあればいいというものです。確かに先にあげた例では開きカッコが3つ、閉じカッコが3つで正しく対応しています。しかし、「)test(」では開きカッコと閉じカッコの数は正しいものの、正しく対応してるとは言えませんね。

開きカッコの数を数えるという方針は正しいのですが、閉じカッコは開きカッコと別に数えるのではなく、閉じカッコが来ると開きカッコの数を減らしていくようにすれば対応を判定できます。最終的に0になれば全ての開きカッコが正しく閉じられ、途中でマイナスになるなら閉じカッコが多すぎると判断できます。

src/main/java/projava/CheckParentheses.java

package projava;

public class CheckParentheses {

    static boolean check(String data) {
        int count = 0;
        for (var ch : data.toCharArray()) {
            switch (ch) {
                case '(' -> count++;
                case ')' -> {
                    count--;
                    if (count < 0) {
                        return false;
                    }
                }
            }
        }
        return count == 0;
    }

    public static void main(String[] args) {
        System.out.println(check("((test(data))test)" ));
        System.out.println(check("((test(data)test)" ));
        System.out.println(check("()test)data(test" ));
    }

}

実行すると、mainメソッドでのカッコの対応が取れているのは最初の行だけなので、次のように表示されます。

true
false
false

処理はcheckメソッドとして書いていきます。判定する文字列を引数dataで受け取ります。カッコの対応が正しければtrueを返すことにします。

static boolean check(String data) {

変数countで開きカッコの数を管理します。

int count = 0;

文字列中の文字の数だけ繰り返します。

for (var ch : data.toCharArray()) {

取り出した文字で処理を振り分けるようswitch文で分岐します。

switch (ch) {

開きカッコの場合は変数countの値を増やします。

case '(' -> count++;

閉じカッコの場合は変数countの値を減らします。

case ')' -> {
    count--;

このとき、もし変数countの値が負になっていれば、対応が取れていないことをあらわすfalseを返して処理を終了します。

if (count < 0) {
    return false;
}

すべての文字を処理したあと、変数countが0であれば対応が取れていることをあらわすtrueが返るようにして処理を終了します。

return count == 0;

mainメソッドで、カッコが正しく対応している文字列、閉じカッコの数が足りない文字列、開きカッコと閉じカッコの数は同じでも順番がよくない文字列の判定をしています。

public static void main(String[] args) {
    System.out.println(check("((test(data))test)" ));
    System.out.println(check("((test(data)test)" ));
    System.out.println(check("()test)data(test" ));
}

移動平均とスライディングウィンドウ - 「プロになる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でなにか作るぞっていう場合の足掛かりになるようになっていますね。