本気のJavaチュートリアル

## はじめに

Javaは、世界中で広く使われているプログラミング言語です。その汎用性と強力な機能により、ウェブアプリケーション、モバイルアプリ、大規模システムなど、さまざまな分野で活躍しています。この記事では、Javaの基礎から応用まで、20の章に分けて詳しく解説します。各章では、概念の説明とともに、実践的なコード例を提供します。

## 第1章: Javaの基本構造

Javaプログラムの基本構造を理解することは、言語習得の第一歩です。Javaでは、すべてのコードはクラスの中に書かれます。メインメソッドは、プログラムの実行開始点となります。

```java
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("こんにちは、世界！");
    }
}
```

このシンプルな例では、`HelloWorld`というクラスを定義し、その中に`main`メソッドを記述しています。`System.out.println()`は、コンソールに文字列を出力するための標準的な方法です。

## 第2章: 変数と基本データ型

Javaには、様々なデータ型があります。基本データ型には、整数型（int, long）、浮動小数点型（float, double）、文字型（char）、真偽値型（boolean）などがあります。

```java
public class DataTypes {
    public static void main(String[] args) {
        int age = 30;
        double height = 175.5;
        char grade = 'A';
        boolean isStudent = true;
        String name = "田中太郎";

        System.out.println("名前: " + name);
        System.out.println("年齢: " + age);
        System.out.println("身長: " + height + "cm");
        System.out.println("成績: " + grade);
        System.out.println("学生ですか？ " + isStudent);
    }
}
```

この例では、異なるデータ型の変数を宣言し、値を代入しています。`String`は基本データ型ではなく、オブジェクト型ですが、非常によく使用されます。

## 第3章: 制御構造

Javaの制御構造には、条件分岐（if-else）、繰り返し（for, while）などがあります。これらを使用することで、プログラムの流れを制御できます。

```java
public class ControlStructures {
    public static void main(String[] args) {
        int score = 85;

        // if-else文
        if (score >= 90) {
            System.out.println("優秀です！");
        } else if (score >= 80) {
            System.out.println("良好です。");
        } else {
            System.out.println("もう少し頑張りましょう。");
        }

        // for文
        System.out.println("1から5までの数を表示します：");
        for (int i = 1; i <= 5; i++) {
            System.out.print(i + " ");
        }
        System.out.println();

        // while文
        int count = 0;
        while (count < 3) {
            System.out.println("カウント: " + count);
            count++;
        }
    }
}
```

この例では、if-else文を使用して成績を評価し、for文で1から5までの数を表示し、while文でカウンターを使用しています。

## 第4章: 配列

配列は、同じデータ型の要素を複数格納できるデータ構造です。Javaでは、配列の宣言、初期化、アクセスが簡単に行えます。

```java
public class ArrayExample {
    public static void main(String[] args) {
        // 整数の配列を宣言と初期化
        int[] numbers = {1, 2, 3, 4, 5};

        // 配列の要素にアクセス
        System.out.println("3番目の要素: " + numbers[2]);

        // 配列の長さを取得
        System.out.println("配列の長さ: " + numbers.length);

        // 配列の全要素を表示
        System.out.println("配列の全要素:");
        for (int number : numbers) {
            System.out.print(number + " ");
        }
        System.out.println();

        // 2次元配列
        int[][] matrix = {
            {1, 2, 3},
            {4, 5, 6},
            {7, 8, 9}
        };

        System.out.println("2次元配列の要素:");
        for (int[] row : matrix) {
            for (int element : row) {
                System.out.print(element + " ");
            }
            System.out.println();
        }
    }
}
```

この例では、1次元配列と2次元配列の宣言、初期化、アクセス方法を示しています。拡張for文（foreach）を使用して、配列の全要素を簡単に走査する方法も紹介しています。

## 第5章: メソッド

メソッドは、特定のタスクを実行するためのコードブロックです。メソッドを使用することで、コードの再利用性と可読性が向上します。

```java
public class MethodExample {
    public static void main(String[] args) {
        greet("田中さん");
        int sum = add(5, 3);
        System.out.println("5 + 3 = " + sum);
        
        double circleArea = calculateCircleArea(5.0);
        System.out.println("半径5の円の面積: " + circleArea);
    }

    // 引数を受け取り、戻り値のないメソッド
    public static void greet(String name) {
        System.out.println("こんにちは、" + name + "！");
    }

    // 2つの引数を受け取り、整数を返すメソッド
    public static int add(int a, int b) {
        return a + b;
    }

    // doubleの引数を受け取り、doubleを返すメソッド
    public static double calculateCircleArea(double radius) {
        return Math.PI * radius * radius;
    }
}
```

この例では、異なる種類のメソッドを定義しています。`greet`メソッドは引数を受け取りますが戻り値はありません。`add`メソッドは2つの整数を受け取り、その和を返します。`calculateCircleArea`メソッドは円の半径を受け取り、面積を計算して返します。

## 第6章: オブジェクト指向プログラミング（クラスとオブジェクト）

オブジェクト指向プログラミング（OOP）は、Javaの中心的な概念です。クラスはオブジェクトの設計図であり、オブジェクトはクラスのインスタンスです。

```java
// 学生クラスの定義
class Student {
    // フィールド（属性）
    private String name;
    private int age;
    private String studentId;

    // コンストラクタ
    public Student(String name, int age, String studentId) {
        this.name = name;
        this.age = age;
        this.studentId = studentId;
    }

    // メソッド
    public void introduce() {
        System.out.println("こんにちは、私は" + name + "です。" + age + "歳で、学生番号は" + studentId + "です。");
    }

    // getter メソッド
    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public String getStudentId() {
        return studentId;
    }
}

public class OOPExample {
    public static void main(String[] args) {
        // Studentクラスのオブジェクトを作成
        Student student1 = new Student("佐藤花子", 20, "S12345");
        Student student2 = new Student("鈴木一郎", 22, "S67890");

        // オブジェクトのメソッドを呼び出す
        student1.introduce();
        student2.introduce();

        // getterメソッドを使用してオブジェクトの属性にアクセス
        System.out.println(student1.getName() + "の年齢: " + student1.getAge());
        System.out.println(student2.getName() + "の学生番号: " + student2.getStudentId());
    }
}
```

この例では、`Student`クラスを定義し、名前、年齢、学生番号の属性を持たせています。コンストラクタを使用してオブジェクトを初期化し、メソッドを通じてオブジェクトの振る舞いを定義しています。メインクラスでは、`Student`オブジェクトを作成し、そのメソッドを呼び出しています。

## 第7章: 継承

継承は、既存のクラスを基に新しいクラスを作成する機能です。これにより、コードの再利用性が高まり、階層構造を持つクラス設計が可能になります。

```java
// 基底クラス（親クラス）
class Animal {
    protected String name;

    public Animal(String name) {
        this.name = name;
    }

    public void eat() {
        System.out.println(name + "が食事をしています。");
    }

    public void sleep() {
        System.out.println(name + "が眠っています。");
    }
}

// 派生クラス（子クラス）
class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }

    public void bark() {
        System.out.println(name + "がワンワン吠えています。");
    }

    // メソッドのオーバーライド
    @Override
    public void eat() {
        System.out.println(name + "が骨を食べています。");
    }
}

// 別の派生クラス
class Cat extends Animal {
    public Cat(String name) {
        super(name);
    }

    public void meow() {
        System.out.println(name + "がニャーと鳴いています。");
    }

    // メソッドのオーバーライド
    @Override
    public void eat() {
        System.out.println(name + "が魚を食べています。");
    }
}

public class InheritanceExample {
    public static void main(String[] args) {
        Dog dog = new Dog("ポチ");
        Cat cat = new Cat("タマ");

        dog.eat();  // オーバーライドされたメソッド
        dog.sleep(); // 親クラスから継承したメソッド
        dog.bark();  // Dog クラス固有のメソッド

        cat.eat();   // オーバーライドされたメソッド
        cat.sleep(); // 親クラスから継承したメソッド
        cat.meow();  // Cat クラス固有のメソッド
    }
}
```

この例では、`Animal`という基底クラスを定義し、`Dog`と`Cat`クラスがそれを継承しています。継承により、`Dog`と`Cat`は`Animal`のメソッドを使用できます。また、`eat`メソッドをオーバーライドして、各動物に固有の振る舞いを定義しています。

## 第8章: インターフェース

インターフェースは、クラスが実装すべきメソッドの仕様を定義します。これにより、異なるクラス間で共通の振る舞いを保証できます。

```java
// インターフェースの定義
interface Flyable {
    void fly();
    void land();
}

interface Swimmable {
    void swim();
    void dive();
}

// 複数のインターフェースを実装するクラス
class Duck implements Flyable, Swimmable {
    private String name;

    public Duck(String name) {
        this.name = name;
    }

    @Override
    public void fly() {
        System.out.println(name + "が空を飛んでいます。");
    }

    @Override
    public void land() {
        System.out.println(name + "が着陸しました。");
    }

    @Override
    public void swim() {
        System.out.println(name + "が水面を泳いでいます。");
    }

    @Override
    public void dive() {
        System.out.println(name + "が水中に潜りました。");
    }
}

// Flyableインターフェースのみを実装するクラス
class Airplane implements Flyable {
    private String model;

    public Airplane(String model) {
        this.model = model;
    }

    @Override
    public void fly() {
        System.out.println(model + "が高度30,000フィートで飛行中です。");
    }

    @Override
    public void land() {
        System.out.println(model + "が滑走路に着陸しました。");
    }
}

public class InterfaceExample {
    public static void main(String[] args) {
        Duck duck = new Duck("カモ");
        Airplane airplane = new Airplane("ボーイング747");

        // Duckの能力を試す
        duck.fly();
        duck.land();
        duck.swim();
        duck.dive();

        // Airplaneの能力を試す
        airplane.fly();
        airplane.land();

        // インターフェースを型として使用
        Flyable flyingObject = duck;
        flyingObject.fly();  // Duckのflyメソッドが呼ばれる

        flyingObject = airplane;
        flyingObject.fly();  // Airplaneのflyメソッドが呼ばれる
    }
}
```

この例では、`Flyable`と`Swimmable`という2つのインターフェースを定義しています。`Duck`クラスは両方のインターフェースを実装し、`Airplane`クラスは`Flyable`インターフェースのみを実装しています。インターフェースを使用することで、異なるクラス間で共通の振る舞いを定義できます。また、インターフェースを型として使用することで、多態性を実現できます。

## 第9章: 例外処理

例外処理は、プログラム実行中に発生する予期せぬエラーを適切に管理するための仕組みです。Javaでは、try-catch-finally構文を使用して例外を処理します。

```java
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;

public class ExceptionHandlingExample {
    public static void main(String[] args) {
        try {
            // ファイルを読み込む
            File file = new File("example.txt");
            Scanner scanner = new Scanner(file);

            while (scanner.hasNextLine()) {
                String line = scanner.nextLine();
                System.out.println(line);
            }
            scanner.close();

            // 配列の範囲外アクセスを試みる
            int[] numbers = {1, 2, 3};
            System.out.println(numbers[10]);  // この行で例外が発生

        } catch (FileNotFoundException e) {
            System.out.println("ファイルが見つかりません: " + e.getMessage());
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("配列の範囲外にアクセスしました: " + e.getMessage());
        } catch (Exception e) {
            System.out.println("予期せぬエラーが発生しました: " + e.getMessage());
        } finally {
            System.out.println("プログラムの実行を終了します。");
        }
    }
}
```

この例では、ファイル読み込みと配列操作の2つの潜在的な例外を処理しています。`try`ブロック内でコードを実行し、発生する可能性のある例外を`catch`ブロックで捕捉します。`finally`ブロックは、例外の発生有無に関わらず必ず実行されます。

## 第10章: ジェネリクス

ジェネリクスは、型安全性を保ちながら、汎用的なクラスやメソッドを作成するための機能です。これにより、コードの再利用性が高まり、型キャストの必要性が減少します。

```java
import java.util.ArrayList;
import java.util.List;

// ジェネリッククラスの定義
class Box<T> {
    private T content;

    public void put(T item) {
        this.content = item;
    }

    public T get() {
        return content;
    }
}

// ジェネリックメソッドの定義
class Utilities {
    public static <E> void printArray(E[] array) {
        for (E element : array) {
            System.out.print(element + " ");
        }
        System.out.println();
    }
}

public class GenericsExample {
    public static void main(String[] args) {
        // ジェネリッククラスの使用
        Box<Integer> intBox = new Box<>();
        intBox.put(10);
        System.out.println("整数ボックスの中身: " + intBox.get());

        Box<String> stringBox = new Box<>();
        stringBox.put("こんにちは、ジェネリクス！");
        System.out.println("文字列ボックスの中身: " + stringBox.get());

        // ジェネリックメソッドの使用
        Integer[] intArray = {1, 2, 3, 4, 5};
        String[] stringArray = {"りんご", "バナナ", "オレンジ"};

        System.out.println("整数配列:");
        Utilities.printArray(intArray);

        System.out.println("文字列配列:");
        Utilities.printArray(stringArray);

        // ジェネリックコレクションの使用
        List<Double> doubleList = new ArrayList<>();
        doubleList.add(3.14);
        doubleList.add(2.71);
        System.out.println("ダブルのリスト: " + doubleList);
    }
}
```

この例では、ジェネリッククラス`Box<T>`を定義し、異なる型のデータを格納できるようにしています。また、ジェネリックメソッド`printArray`を定義し、任意の型の配列を表示できるようにしています。さらに、ジェネリックコレクション（`ArrayList<Double>`）の使用例も示しています。

## 第11章: コレクションフレームワーク

Javaのコレクションフレームワークは、データを効率的に格納、操作するための一連のクラスとインターフェースを提供します。主要なインターフェースには、List、Set、Mapがあります。

```java
import java.util.*;

public class CollectionFrameworkExample {
    public static void main(String[] args) {
        // Listの使用例（ArrayList）
        List<String> fruits = new ArrayList<>();
        fruits.add("りんご");
        fruits.add("バナナ");
        fruits.add("オレンジ");
        fruits.add("バナナ");  // 重複を許可

        System.out.println("フルーツリスト: " + fruits);
        System.out.println("2番目のフルーツ: " + fruits.get(1));

        // Setの使用例（HashSet）
        Set<String> uniqueFruits = new HashSet<>(fruits);
        System.out.println("ユニークなフルーツセット: " + uniqueFruits);  // 重複が除去される

        // Mapの使用例（HashMap）
        Map<String, Integer> fruitInventory = new HashMap<>();
        fruitInventory.put("りんご", 50);
        fruitInventory.put("バナナ", 30);
        fruitInventory.put("オレンジ", 40);

        System.out.println("フルーツ在庫: " + fruitInventory);
        System.out.println("りんごの在庫: " + fruitInventory.get("りんご"));

        // イテレーションの例
        System.out.println("フルーツリストの反復処理:");
        for (String fruit : fruits) {
            System.out.println(fruit);
        }

        System.out.println("フルーツ在庫の反復処理:");
        for (Map.Entry<String, Integer> entry : fruitInventory.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }

        // ソートの例
        Collections.sort(fruits);
        System.out.println("ソート後のフルーツリスト: " + fruits);

        // 検索の例
        boolean hasMango = fruits.contains("マンゴー");
        System.out.println("リストにマンゴーが含まれていますか？ " + hasMango);
    }
}
```

この例では、`List`、`Set`、`Map`インターフェースの実装を使用しています。`ArrayList`は順序付きのリストを、`HashSet`は重複を許可しないセットを、`HashMap`はキーと値のペアを格納するマップを提供します。また、コレクションの反復処理、ソート、検索などの一般的な操作も示しています。

## 第12章: ラムダ式と関数型インターフェース

Java 8以降で導入されたラムダ式は、関数型プログラミングの概念をJavaに取り入れたものです。ラムダ式を使用することで、より簡潔で読みやすいコードを書くことができます。

```java
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

public class LambdaAndFunctionalInterfaceExample {
    public static void main(String[] args) {
        // ラムダ式を使用した関数型インターフェースの実装
        Runnable runnable = () -> System.out.println("こんにちは、ラムダ式！");
        runnable.run();

        // 引数を持つラムダ式
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        
        // 偶数をフィルタリング
        System.out.println("偶数:");
        numbers.stream()
               .filter(n -> n % 2 == 0)
               .forEach(System.out::println);

        // カスタム関数型インターフェース
        StringProcessor reverser = (str) -> new StringBuilder(str).reverse().toString();
        System.out.println("文字列の反転: " + reverser.process("こんにちは"));

        // java.util.function パッケージの関数型インターフェースの使用
        Predicate<String> isLongWord = (s) -> s.length() > 5;
        System.out.println("'プログラミング'は長い単語ですか？ " + isLongWord.test("プログラミング"));
        System.out.println("'Java'は長い単語ですか？ " + isLongWord.test("Java"));

        // メソッド参照
        List<String> names = Arrays.asList("田中", "佐藤", "鈴木", "高橋");
        names.forEach(System.out::println);
    }
}

// カスタム関数型インターフェース
@FunctionalInterface
interface StringProcessor {
    String process(String input);
}
```

この例では、ラムダ式を使用して様々な関数型インターフェースを実装しています。`Runnable`インターフェースの実装、ストリームAPIでのフィルタリング、カスタム関数型インターフェース`StringProcessor`の定義と使用、`java.util.function`パッケージの`Predicate`インターフェースの使用、そしてメソッド参照の例を示しています。

## 第13章: ストリームAPI

Java 8で導入されたストリームAPIは、コレクションを操作するための強力な機能を提供します。ストリームを使用することで、データの処理をより宣言的に、そして多くの場合並列に行うことができます。

```java
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class StreamAPIExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("田中", "佐藤", "鈴木", "高橋", "伊藤", "渡辺", "山本");

        // フィルタリング
        List<String> longNames = names.stream()
                                      .filter(name -> name.length() > 2)
                                      .collect(Collectors.toList());
        System.out.println("3文字以上の名前: " + longNames);

        // マッピング
        List<Integer> nameLengths = names.stream()
                                         .map(String::length)
                                         .collect(Collectors.toList());
        System.out.println("名前の長さ: " + nameLengths);

        // ソート
        List<String> sortedNames = names.stream()
                                        .sorted()
                                        .collect(Collectors.toList());
        System.out.println("ソートされた名前: " + sortedNames);

        // 集計
        long count = names.stream()
                          .filter(name -> name.startsWith("佐"))
                          .count();
        System.out.println("「佐」で始まる名前の数: " + count);

        // 数値ストリームの操作
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        int sum = numbers.stream()
                         .reduce(0, Integer::sum);
        System.out.println("合計: " + sum);

        // 並列ストリーム
        long evenCount = numbers.parallelStream()
                                .filter(n -> n % 2 == 0)
                                .count();
        System.out.println("偶数の数: " + evenCount);

        // flatMap の使用
        List<List<Integer>> nestedList = Arrays.asList(
            Arrays.asList(1, 2, 3),
            Arrays.asList(4, 5, 6),
            Arrays.asList(7, 8, 9)
        );
        List<Integer> flattenedList = nestedList.stream()
                                                .flatMap(List::stream)
                                                .collect(Collectors.toList());
        System.out.println("フラット化されたリスト: " + flattenedList);
    }
}
```

この例では、ストリームAPIの様々な機能を示しています。フィルタリング、マッピング、ソート、集計などの基本的な操作に加え、`reduce`メソッドを使用した集約、並列ストリームの使用、そして`flatMap`を使用したネストされたコレクションのフラット化も示しています。ストリームAPIを使用することで、複雑なデータ処理を簡潔に記述できます。

## 第14章: 入出力（I/O）処理

Javaの入出力（I/O）APIは、ファイルの読み書きやネットワーク通信など、様々な入出力操作を行うための機能を提供します。ここでは、ファイルの読み書きを中心に説明します。

```java
import java.io.*;
import java.nio.file.*;

public class IOExample {
    public static void main(String[] args) {
        String fileName = "example.txt";
        String content = "これはファイルに書き込むテキストです。\n改行も含まれています。";

        // ファイルへの書き込み（従来のI/O）
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName))) {
            writer.write(content);
            System.out.println("ファイルへの書き込みが完了しました。");
        } catch (IOException e) {
            System.out.println("ファイルの書き込み中にエラーが発生しました: " + e.getMessage());
        }

        // ファイルからの読み込み（従来のI/O）
        try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) {
            String line;
            System.out.println("ファイルの内容:");
            while ((line = reader.readLine()) != null) {
System.out.println(line);
            }
        } catch (IOException e) {
            System.out.println("ファイルの読み込み中にエラーが発生しました: " + e.getMessage());
        }

        // NIO（New I/O）を使用したファイルの書き込み
        String newContent = "これはNIOを使用して書き込まれたテキストです。";
        try {
            Files.write(Paths.get("nio_example.txt"), newContent.getBytes());
            System.out.println("NIOを使用してファイルに書き込みました。");
        } catch (IOException e) {
            System.out.println("NIOでのファイル書き込み中にエラーが発生しました: " + e.getMessage());
        }

        // NIOを使用したファイルの読み込み
        try {
            String readContent = new String(Files.readAllBytes(Paths.get("nio_example.txt")));
            System.out.println("NIOで読み込んだファイルの内容:");
            System.out.println(readContent);
        } catch (IOException e) {
            System.out.println("NIOでのファイル読み込み中にエラーが発生しました: " + e.getMessage());
        }

        // ディレクトリの作成と一覧表示
        try {
            Files.createDirectory(Paths.get("newDirectory"));
            System.out.println("新しいディレクトリを作成しました。");

            System.out.println("ディレクトリの内容:");
            Files.list(Paths.get(".")).forEach(System.out::println);
        } catch (IOException e) {
            System.out.println("ディレクトリ操作中にエラーが発生しました: " + e.getMessage());
        }
    }
}
```

この例では、従来のI/OとNIO（New I/O）の両方を使用してファイルの読み書きを行っています。また、ディレクトリの作成と一覧表示も示しています。try-with-resources構文を使用してリソースの自動クローズを行い、例外処理も適切に行っています。

## 第15章: マルチスレッディング

マルチスレッディングは、複数の処理を並行して実行するためのJavaの機能です。これにより、プログラムの効率と応答性を向上させることができます。

```java
class Counter {
    private int count = 0;

    // synchronized キーワードを使用して、メソッドをスレッドセーフにする
    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

class CounterThread extends Thread {
    private Counter counter;

    public CounterThread(Counter counter) {
        this.counter = counter;
    }

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            counter.increment();
        }
    }
}

public class MultithreadingExample {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        // 複数のスレッドを作成
        Thread thread1 = new CounterThread(counter);
        Thread thread2 = new CounterThread(counter);

        // スレッドの開始
        thread1.start();
        thread2.start();

        // メインスレッドが他のスレッドの終了を待つ
        thread1.join();
        thread2.join();

        System.out.println("最終的なカウント: " + counter.getCount());

        // Runnableインターフェースを使用したスレッド
        Runnable runnable = () -> {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + ": " + i);
                try {
                    Thread.sleep(1000); // 1秒間スリープ
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        Thread thread3 = new Thread(runnable, "Thread-A");
        Thread thread4 = new Thread(runnable, "Thread-B");

        thread3.start();
        thread4.start();
    }
}
```

この例では、`Counter`クラスを使用して、複数のスレッドが共有リソースにアクセスする状況を示しています。`synchronized`キーワードを使用して、メソッドをスレッドセーフにしています。また、`Thread`クラスの拡張と`Runnable`インターフェースの実装という2つの方法でスレッドを作成しています。`join()`メソッドを使用して、メインスレッドが他のスレッドの終了を待つようにしています。

## 第16章: ネットワークプログラミング

Javaのネットワークプログラミング機能を使用すると、ネットワーク通信を行うアプリケーションを簡単に作成できます。ここでは、簡単なクライアント-サーバーアプリケーションの例を示します。

```java
import java.io.*;
import java.net.*;

// サーバークラス
class SimpleServer {
    public static void main(String[] args) {
        try (ServerSocket serverSocket = new ServerSocket(5000)) {
            System.out.println("サーバーが起動しました。クライアントからの接続を待っています...");
            
            while (true) {
                try (Socket clientSocket = serverSocket.accept();
                     PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
                     BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()))) {
                    
                    System.out.println("クライアントが接続しました。");
                    
                    String inputLine;
                    while ((inputLine = in.readLine()) != null) {
                        System.out.println("クライアントからのメッセージ: " + inputLine);
                        out.println("サーバーからの応答: " + inputLine.toUpperCase());
                    }
                }
            }
        } catch (IOException e) {
            System.out.println("サーバーでエラーが発生しました: " + e.getMessage());
        }
    }
}

// クライアントクラス
class SimpleClient {
    public static void main(String[] args) {
        try (Socket socket = new Socket("localhost", 5000);
             PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
             BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
             BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in))) {
            
            System.out.println("サーバーに接続しました。メッセージを入力してください（終了するには 'exit' と入力）:");
            
            String userInput;
            while ((userInput = stdIn.readLine()) != null) {
                out.println(userInput);
                if (userInput.equalsIgnoreCase("exit")) {
                    break;
                }
                System.out.println("サーバーからの応答: " + in.readLine());
            }
        } catch (UnknownHostException e) {
            System.out.println("ホストが見つかりません: " + e.getMessage());
        } catch (IOException e) {
            System.out.println("I/Oエラーが発生しました: " + e.getMessage());
        }
    }
}
```

この例では、簡単なエコーサーバーとクライアントを実装しています。サーバーは5000番ポートでクライアントからの接続を待ち、接続されたクライアントからのメッセージを大文字に変換して送り返します。クライアントはユーザーからの入力を受け取り、サーバーに送信し、サーバーからの応答を表示します。

これらのクラスを別々のファイルに保存し、別々のターミナルで実行することで、クライアント-サーバー通信をシミュレートできます。

## 第17章: データベース接続（JDBC）

JDBCは、JavaからデータベースにアクセスするためのAPIです。以下の例では、MySQLデータベースに接続し、基本的なCRUD（Create, Read, Update, Delete）操作を行います。

```java
import java.sql.*;

public class JDBCExample {
    // JDBCドライバ名とデータベースURL
    static final String JDBC_DRIVER = "com.mysql.cj.jdbc.Driver";
    static final String DB_URL = "jdbc:mysql://localhost/testdb";

    // データベースの認証情報
    static final String USER = "username";
    static final String PASS = "password";

    public static void main(String[] args) {
        Connection conn = null;
        Statement stmt = null;
        try {
            // JDBCドライバの登録
            Class.forName(JDBC_DRIVER);

            // データベースへの接続
            System.out.println("データベースに接続中...");
            conn = DriverManager.getConnection(DB_URL, USER, PASS);

            // ステートメントオブジェクトの作成
            stmt = conn.createStatement();

            // テーブルの作成
            String sql = "CREATE TABLE IF NOT EXISTS employees " +
                         "(id INTEGER not NULL, " +
                         " name VARCHAR(255), " +
                         " age INTEGER, " +
                         " PRIMARY KEY ( id ))";
            stmt.executeUpdate(sql);
            System.out.println("テーブルが作成されました");

            // データの挿入
            sql = "INSERT INTO employees VALUES (1, '山田太郎', 30)";
            stmt.executeUpdate(sql);
            sql = "INSERT INTO employees VALUES (2, '佐藤花子', 25)";
            stmt.executeUpdate(sql);
            System.out.println("データが挿入されました");

            // データの取得
            sql = "SELECT id, name, age FROM employees";
            ResultSet rs = stmt.executeQuery(sql);

            // 結果の表示
            while(rs.next()){
                int id  = rs.getInt("id");
                String name = rs.getString("name");
                int age = rs.getInt("age");

                System.out.print("ID: " + id);
                System.out.print(", 名前: " + name);
                System.out.println(", 年齢: " + age);
            }

            // データの更新
            sql = "UPDATE employees SET age = 31 WHERE id = 1";
            stmt.executeUpdate(sql);
            System.out.println("データが更新されました");

            // データの削除
            sql = "DELETE FROM employees WHERE id = 2";
            stmt.executeUpdate(sql);
            System.out.println("データが削除されました");

            // リソースのクローズ
            rs.close();
            stmt.close();
            conn.close();
        } catch(SQLException se) {
            se.printStackTrace();
        } catch(Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if(stmt!=null) stmt.close();
            } catch(SQLException se2) {
            }
            try {
                if(conn!=null) conn.close();
            } catch(SQLException se) {
                se.printStackTrace();
            }
        }
        System.out.println("さようなら！");
    }
}
```

この例では、MySQLデータベースに接続し、テーブルの作成、データの挿入、取得、更新、削除を行っています。実際に使用する際は、適切なJDBCドライバをクラスパスに追加し、データベースの接続情報を正しく設定する必要があります。

## 第18章: Java FX

JavaFXは、デスクトップアプリケーションやリッチインターネットアプリケーション（RIA）を作成するためのJavaのGUIフレームワークです。以下は、簡単なJavaFXアプリケーションの例です。

```java
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class JavaFXExample extends Application {

    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle("JavaFX Example");

        // ラベルの作成
        Label nameLabel = new Label("名前:");

        // テキストフィールドの作成
        TextField nameField = new TextField();

        // ボタンの作成
        Button submitButton = new Button("送信");

        // 結果表示用のラベル
        Label resultLabel = new Label();

        // ボタンのアクションの設定
        submitButton.setOnAction(e -> {
            String name = nameField.getText();
            if (name.isEmpty()) {
                resultLabel.setText("名前を入力してください。");
            } else {
                resultLabel.setText("こんにちは、" + name + "さん！");
            }
        });

        // レイアウトの作成
        VBox vbox = new VBox(10);
        vbox.setPadding(new Insets(20));
        vbox.getChildren().addAll(nameLabel, nameField, submitButton, resultLabel);

        // シーンの作成
        Scene scene = new Scene(vbox, 300, 200);

        // ステージにシーンを設定
        primaryStage.setScene(scene);

        // ステージの表示
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}
```

この例では、テキストフィールドに名前を入力し、ボタンをクリックすると挨拶メッセージが表示される簡単なGUIアプリケーションを作成しています。JavaFXを使用するには、適切な依存関係とモジュール設定が必要です。

## 第19章: ユニットテスト（JUnit）

ユニットテストは、コードの品質を保証し、バグを早期に発見するために重要です。JUnitは、Javaで広く使用されているユニットテストフレームワークです。以下は、JUnitを使用したテストの例です。

```java
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

class Calculator {
    public int add(int a, int b) {
        return a + b;
    }

    public int subtract(int a, int b) {
        return a - b;
    }

    public int multiply(int a, int b) {
        return a * b;
    }

    public int divide(int a, int b) {
        if (b == 0) {
            throw new IllegalArgumentException("0で割ることはできません");
        }
        return a / b;
    }
}

public class CalculatorTest {
    private Calculator calculator;

    @BeforeEach
    void setUp() {
        calculator = new Calculator();
    }

    @Test
    void testAdd() {
        assertEquals(5, calculator.add(2, 3), "2 + 3 は 5 になるはずです");
    }

    @Test
    void testSubtract() {
        assertEquals(1, calculator.subtract(3, 2), "3 - 2 は 1 になるはずです");
    }

    @Test
    void testMultiply() {
        assertEquals(6, calculator.multiply(2, 3), "2 * 3 は 6 になるはずです");
    }

    @Test
    void testDivide() {
        assertEquals(2, calculator.divide(6, 3), "6 / 3 は 2 になるはずです");
    }

    @Test
    void testDivideByZero() {
        assertThrows(IllegalArgumentException.class, () -> {
            calculator.divide(1, 0);
        }, "0で割ると例外がスローされるはずです");
    }

    @Test
    @Disabled("この機能はまだ実装されていません")
    void testSquareRoot() {
        // 平方根の計算機能がまだ実装されていないと仮定
    }

    @Test
    void testAddWithNegativeNumbers() {
        assertEquals(-1, calculator.add(-2, 1), "-2 + 1 は -1 になるはずです");
    }

    @Test
    void testMultiplyWithZero() {
        assertEquals(0, calculator.multiply(5, 0), "5 * 0 は 0 になるはずです");
        assertEquals(0, calculator.multiply(0, 5), "0 * 5 は 0 になるはずです");
    }
}
```

この例では、簡単な`Calculator`クラスとそのテストケースを実装しています。JUnit 5の機能を使用して、以下のようなテスト手法を示しています：

- `@BeforeEach`: 各テストメソッドの前に実行される設定メソッド
- `assertEquals`: 期待値と実際の値を比較
- `assertThrows`: 例外がスローされることを確認
- `@Disabled`: 特定のテストを一時的に無効化
- 境界値や特殊なケース（負の数、ゼロなど）のテスト

JUnitを使用するには、プロジェクトの依存関係にJUnitを追加し、適切なテストランナーを設定する必要があります。

## 第20章: デザインパターン

デザインパターンは、ソフトウェア設計における一般的な問題に対する再利用可能な解決策です。Javaでよく使用されるデザインパターンの一つに、Singletonパターンがあります。以下は、Singletonパターンの実装例です。

```java
// スレッドセーフなSingletonパターンの実装
public class Singleton {
    // privateで静的な唯一のインスタンス
    private static volatile Singleton instance;

    // privateコンストラクタで外部からのインスタンス化を防ぐ
    private Singleton() {
        // 初期化処理
    }

    // インスタンスを取得するためのpublicな静的メソッド
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

    // Singletonクラスのその他のメソッド
    public void doSomething() {
        System.out.println("Singletonインスタンスのメソッドが呼び出されました。");
    }
}

// Singletonパターンの使用例
public class SingletonExample {
    public static void main(String[] args) {
        Singleton singleton1 = Singleton.getInstance();
        Singleton singleton2 = Singleton.getInstance();

        // 両方の変数が同じインスタンスを参照していることを確認
        System.out.println("singleton1 == singleton2: " + (singleton1 == singleton2));

        singleton1.doSomething();
    }
}
```

この例では、スレッドセーフなSingletonパターンを実装しています。以下の特徴があります：

1. privateコンストラクタ: 外部からのインスタンス化を防ぎます。
2. 静的なインスタンス変数: クラスの唯一のインスタンスを保持します。
3. publicな静的ファクトリメソッド: インスタンスへのアクセスを提供します。
4. ダブルチェックロッキング: マルチスレッド環境での効率的な実装を提供します。

Singletonパターンは、リソースの共有、設定の一元管理、ロギングなど、アプリケーション全体で唯一のインスタンスが必要な場合に使用されます。

以上で、Javaプログラミングの主要な概念と実践的な例を20章にわたって解説しました。これらの知識と例を基に、さらに学習を深め、実践的なJavaアプリケーションの開発に取り組んでいくことができます。