「プロになるJava」の第4部「高度なプログラミング」の練習問題の解答です。
「プロになるJava」 第2部「Javaの基本」の練習問題解答 - きしだのHatena
「プロになるJava」 第3部「Javaの文法」の練習問題解答 - きしだのHatena
「プロになるJava」 第4部「高度なプログラミング」の練習問題解答 - きしだのHatena
第12章 ファイルとネットワーク
ファイルアクセスと例外
try句で例外に対処する
- 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
の外にしています。
検査例外と非検査例外
InterruptedException
は検査例外か非検査例外か、図から考えてみてください。
Exceptionに含まれており、RuntimeExceptionには含まれていないので検査例外です。
UncheckedIOException
は検査例外か非検査例外か、図から考えてみてください。
RuntimeExceptionに含まれているので非検査例外です。
ネットワークでコンピュータの外の世界と関わる
サーバーへの接続
- ポート番号を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
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("サーバーが起動していません");
}
}
}
処理の難しさ
他のデータを参照する
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);
}
}
- ひとつ後の要素と比べて大きいほうを格納した配列を作ってみましょう。最後の要素は最後にそのまま出力されます。例えば
{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()
のようなビルダーを使って結果を構築すると、最終的な要素数をあらかじめ考える必要がなくなります。実際の処理では結果の要素数があらかじめわからないことも多いので、そのような場合には便利です。
隠れた状態を扱う処理
- 受け取った文字列のアルファベットを、最初は小文字で出力し、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であれば大文字を出力するようlower
をfalse
に、1であれば小文字を出力するようlower
をtrue
に切り替えます。
switch (ch) {
case '0' -> lower = false;
case '1' -> lower = true;
それ以外での文字では大文字小文字変換を行ってbuf
に追加していきますが、ここでCharacter.toLowerCase
メソッドなどを使って変換を行ってます。
「そんなメソッドなんか習ってないけど?」と言いたくなるかもしれませんが、こういった「ありそうなメソッド」は用意されていることが多いです。どういうメソッドが「ありそうなメソッド」かというと、知ってるメソッドの内部処理で使っていそうで単独で使っても便利そうな処理です。String
クラスのtoUpperCase
メソッドで文字を1文字ずつ変換しているのであれば、その1文字ごとの変換処理はメソッドになってるはずと考えて、Character.to
として補完候補を出してみるとあった、という感じです。
Java 8まではそういった「内部でやってる処理をぼくたちにも使わせてよ」というものが多く埋まっていたのですが、Java 9以降に半年ごとにバージョンアップされることになって細かい改善も入りやすくなり、便利メソッドがちゃんと使えることが多くなってます。
- 文字列を受け取って、数字以外はそのまま出力し、数字が来たら直前の文字をその数字に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') {
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);
}
}
- 小数部の最後が0で終わると不適になるように判定を変更してみましょう。「 12.30」や「12.0」は不適です。
小数部で出てくる0に関する状態FRAC_ZERO
を用意します。
FRAC_START
やFRAC
で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_START
とFRAC_ZERO
は扱いがまったく同じなので、まとめていいのではとなります。
case FRAC_START, FRAC -> {
if (ch == '0') {
state = FloatState.FRAC_START;
} else if (ch >= '1' && ch <= '9') {
state = FloatState.FRAC;
} else {
return false;
}
}
ただ、こうった状態の最適化を行うと、あとあとの改変で場合分けがおきたりするので、なるべく素直な状態遷移を保つほうがいいと思います。状態をまとめるときは「これらの状態は本質的に同じである」つまり「これらの状態に対して変更が起きる時は常に同じ変更になる」という自信があるときだけにしましょう。
- 先頭に負の符号を表すを付けることができるように判定を変更してみましょう。「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章 クラスとインタフェース
インタフェース
record Staff(String name, String job) {}
にNamed
インタフェースをimplements
してみましょう。
record Staff(String name, String job) implements Named {}
- 次の2つのレコードの
width
とheight
を統一的に扱うためのインタフェース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 {}