Javaオブジェクト指向の核心:クラス設計、メモリ割り当て、およびメソッドの設計

クラスとオブジェクトの基本概念

Javaにおけるオブジェクト指向プログラミング(OOP)の基盤は、「クラス」と「オブジェクト」の関係を理解することにあります。

  • クラス:抽象的な概念であり、特定のグループに属する事物の設計図(テンプレート)として機能します。データ型としての役割も果たします。
  • オブジェクト:クラスという設計図から生成された具体的な実体(インスタンス)です。

インスタンス変数(フィールド)

クラスが持つデータは、インスタンス変数(フィールドまたはプロパティ)として定義されます。これらは基本データ型だけでなく、配列や他のオブジェクトなどの参照型も保持できます。

class Vehicle {
    String model;       // 車種名
    double cost;        // 価格
    String bodyColor;   // 車体色
    String[] owners;    // 所有者リスト(参照型)
}

インスタンス変数に明示的な初期値が与えられない場合、配列と同様のルールに従ってデフォルト値が割り当てられます(例:int0booleanfalseStringなどの参照型はnull)。

public class DefaultValuesDemo {
    public static void main(String[] args) {
        Employee emp = new Employee();
        
        System.out.println("勤続年数: " + emp.yearsOfExperience);
        System.out.println("氏名: " + emp.fullName);
        System.out.println("基本給: " + emp.baseSalary);
        System.out.println("試用期間クリア: " + emp.isProbationPassed);
    }
}

class Employee {
    int yearsOfExperience;
    String fullName;
    double baseSalary;
    boolean isProbationPassed;
}

オブジェクトのフィールドへのアクセスは、オブジェクト参照.フィールド名(例:emp.fullName)という構文で行います。

オブジェクトの生成とメモリモデル

オブジェクトの生成は、宣言と初期化を分けて行うことも、一行で完結させることも可能です。

// 1. 宣言と生成を分ける
Vehicle v1;
v1 = new Vehicle();

// 2. 同時に宣言と生成を行う
Vehicle v2 = new Vehicle();

JVMのメモリ割り当てメカニズム

Javaのメモリ構造は主に以下の3つの領域で構成されています。

  1. スタック:ローカル変数やメソッドの呼び出し履歴(フレーム)が格納されます。
  2. ヒープnew演算子によって生成されたオブジェクトや配列の実体が格納されます。
  3. メソッド領域:クラスのメタデータ、定数プール(文字列リテラルなど)、静的変数が格納されます。

newを使用したオブジェクト生成時の内部的なフローは以下の通りです。

  1. メソッド領域にクラス情報がロードされているか確認し、未ロードであればロードする(クラスローダー)。
  2. ヒープ領域にオブジェクト用のメモリを確保し、フィールドをデフォルト値で初期化する。
  3. 確保したヒープ領域のメモリアドレスを、スタック上のローカル変数(参照変数)に代入する。
  4. コンストラクタや明示的な初期化ブロックが実行され、指定された値でフィールドが上書きされる。

メソッドの設計と振る舞い

メソッドはオブジェクトの「振る舞い」を定義します。コードの再利用性を高め、実装の詳細をカプセル化するために不可欠です。

public class RobotDemo {
    public static void main(String[] args) {
        Robot bot = new Robot();
        bot.announceStatus();
        bot.enterSleepMode();
        
        String report = bot.executeTask("データ集計");
        System.out.println("タスク結果: " + report);
    }
}

class Robot {
    public void announceStatus() {
        System.out.println("システム正常稼働中。");
    }

    public void enterSleepMode() {
        System.out.println("スリープモードに移行します...");
    }

    public String executeTask(String taskName) {
        System.out.println("タスク開始: " + taskName);
        return "タスク完了。結果をサーバーにアップロードしました。";
    }
}

メソッド定義のルール

  • シグネチャアクセス修飾子 戻り値の型 メソッド名(パラメータリスト)
  • パラメータ:仮引数(定義時)と実引数(呼び出し時)の型、個数、順序は一致している必要があります。
  • 戻り値void以外の場合は、すべての実行パスで適切な型の値をreturnする必要があります。
  • 制約:Javaではメソッド内に別のメソッドを定義する(ネストする)ことはできません。

パラメータの受け渡しメカニズム

Javaのパラメータ受け渡しは、厳密にはすべて「値渡し(Pass by Value)」です。ただし、基本型と参照型でその振る舞いが異なります。

基本データ型の場合:値そのものがコピーされるため、メソッド内での仮引数の変更は呼び出し元の実引数に影響しません。

class ValueSwapper {
    public void swap(int x, int y) {
        int temp = x;
        x = y;
        y = temp; // この変更はmainメソッドの変数には影響しない
    }
}

参照型(オブジェクトや配列)の場合:参照(メモリアドレス)の値がコピーされます。メソッド内で参照先のオブジェクトのフィールドを変更すると、呼び出し元にも影響します。ただし、仮引数自体に新しいオブジェクトを再代入しても、呼び出し元の参照は変わりません。

class ReferenceModifier {
    public void modifyArray(int[] data) {
        data[0] = 999; // 呼び出し元の配列の要素が変更される
    }

    public void modifyObject(UserAccount account) {
        account.balance = 5000; // 呼び出し元のオブジェクトのフィールドが変更される
        
        account = new UserAccount(); // ローカル参照の再代入
        account.balance = 100;       // 呼び出し元のオブジェクトには影響しない
    }
}

再帰的アルゴリズム

再帰とは、メソッドが自分自身を呼び出すプログラミング手法です。複雑な問題を簡潔なコードで解決できる一方で、スタックオーバーフロー(StackOverflowError)を防ぐために適切な終了条件(ベースケース)の設定が必須です。

ケース1:フィボナッチ数列

class MathOperations {
    public long computeFibonacci(int n) {
        if (n <= 1) {
            return n;
        }
        return computeFibonacci(n - 1) + computeFibonacci(n - 2);
    }
}

ケース2:グリッド上の経路探索(迷路)

2次元配列をグリッドとし、再帰とバックトラックを用いて出口までの経路を探索します。

class PathFinder {
    // 0: 未踏破, 1: 壁, 2: 経路, 3: 行き止まり
    public boolean findPath(int[][] grid, int row, int col) {
        int targetRow = grid.length - 2;
        int targetCol = grid[0].length - 2;

        if (grid[targetRow][targetCol] == 2) {
            return true;
        }

        if (grid[row][col] == 0) {
            grid[row][col] = 2; // 仮に経路とする
            
            // 探索優先順: 右 -> 下 -> 左 -> 上
            if (findPath(grid, row, col + 1)) return true;
            if (findPath(grid, row + 1, col)) return true;
            if (findPath(grid, row, col - 1)) return true;
            if (findPath(grid, row - 1, col)) return true;
            
            grid[row][col] = 3; // すべて失敗した場合は行き止まりとする
        }
        return false;
    }
}

ケース3:ハノイの塔

class DiskTransfer {
    public void moveDisks(int count, char source, char auxiliary, char target) {
        if (count == 1) {
            System.out.println("ディスク 1 を " + source + " から " + target + " へ移動");
            return;
        }
        moveDisks(count - 1, source, target, auxiliary);
        System.out.println("ディスク " + count + " を " + source + " から " + target + " へ移動");
        moveDisks(count - 1, auxiliary, source, target);
    }
}

ケース4:Nクイーン問題

バックトラッキングを用いて、N×Nのチェス盤にN個のクイーンを互いに攻撃し合わないよう配置する問題を解きます。1次元配列で盤面を表現し、インデックスを行、値を列として扱います。

class NQueensSolver {
    private int[] board;
    private int size;
    private int solutionCount = 0;

    public NQueensSolver(int n) {
        this.size = n;
        this.board = new int[n];
    }

    public void solve(int row) {
        if (row == size) {
            solutionCount++;
            printBoard();
            return;
        }

        for (int col = 0; col < size; col++) {
            board[row] = col;
            if (isSafe(row, col)) {
                solve(row + 1);
            }
        }
    }

    private boolean isSafe(int row, int col) {
        for (int i = 0; i < row; i++) {
            if (board[i] == col || Math.abs(board[i] - col) == Math.abs(i - row)) {
                return false;
            }
        }
        return true;
    }
    
    private void printBoard() {
        for (int i : board) System.out.print(i + " ");
        System.out.println();
    }
}

メソッドのオーバーロード

オーバーロード(Overloading)とは、同じクラス内でメソッド名を同一にしつつ、パラメータリスト(型、個数、順序)を変更して複数のメソッドを定義する機能です。これにより、似たような機能に対して直感的なAPIを設計できます。戻り値の型だけではオーバーロードとして認識されません。

class GeometryCalculator {
    // 正方形の面積
    public double calculateArea(double side) {
        return side * side;
    }

    // 長方形の面積
    public double calculateArea(double width, double height) {
        return width * height;
    }

    // 円の面積
    public double calculateArea(double radius, boolean isCircle) {
        return Math.PI * radius * radius;
    }
}

可変長引数(Varargs)

パラメータの数が不定である場合、可変長引数を使用することで、複数のオーバーロードメソッドを定義する手間を省けます。内部では配列として処理されます。

class StatisticsAnalyzer {
    // double... は配列として扱われる
    public double calculateAverage(double... values) {
        if (values.length == 0) return 0.0;
        
        double sum = 0;
        for (double val : values) {
            sum += val;
        }
        return sum / values.length;
    }
}

public class VarargsDemo {
    public static void main(String[] args) {
        StatisticsAnalyzer analyzer = new StatisticsAnalyzer();
        System.out.println(analyzer.calculateAverage(10.5, 20.0));
        System.out.println(analyzer.calculateAverage(1.0, 2.0, 3.0, 4.0, 5.0));
        
        // 配列を直接渡すことも可能
        double[] data = {100.0, 200.0, 300.0};
        System.out.println(analyzer.calculateAverage(data));
    }
}

可変長引数はパラメータリストの最後に配置する必要があり、1つのメソッドにつき1つしか定義できません。また、固定パラメータと組み合わせて使用することも可能です。

タグ: Java OOP MemoryManagement Recursion MethodOverloading

8月7日 23:54 投稿