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

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

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