Java | オブジェクト指向:クラス定義の構文

Java Java
スポンサーリンク

クラス定義の全体像

クラスは「状態(フィールド)と振る舞い(メソッド)」をひとまとめにする設計図です。基本構文は「パッケージ宣言→import→クラス宣言→フィールド→コンストラクタ→メソッド→ネスト型・初期化子・定数」の順で書くと読みやすく、IDEの補完とも相性が良いです。重要なのは「可視性(アクセス修飾子)」「不変性と初期化」「static とインスタンスの違い」を正しく押さえることです。


クラス宣言の基本構文

パッケージ・import・クラスヘッダ

package com.example.app;             // 所属パッケージ

import java.util.List;               // 使う型の import

public final class User {            // アクセス+修飾子+class+名前
    // フィールド・コンストラクタ・メソッドが続く
}
Java
  • アクセス修飾子: public(どこからでも)、なし(同一パッケージ内のみに公開)
  • 修飾子: final(継承不可)、abstract(抽象クラス)、sealed(継承を限定)
  • 継承・実装: extends 親クラスimplements インターフェース, ...
public class Employee extends User implements Comparable<Employee> { /* ... */ }
Java

フィールド・メソッド・定数の定義

フィールド(状態)と可視性

public final class User {
    private final String id;     // 不変フィールド(コンストラクタで確定)
    private String name;         // 可変フィールド(検証付きで変更)

    // 定数(共有の不変値)
    public static final int MAX_NAME_LEN = 100;
}
Java
  • private にする: 外から直接いじらせず、整合性を守る
  • final を好む: 不変にするとバグと並行性の問題が減る
  • 定数は public static final: マジックナンバー排除、意味ある名前で再利用

コンストラクタ(初期化の入口)

public User(String id, String name) {
    if (id == null || id.isBlank()) throw new IllegalArgumentException("id");
    this.id = id;
    this.name = normalize(name);
}

// コンストラクタのオーバーロード
public User(String id) { this(id, ""); }
Java
  • 検証はここで: 不正な初期状態を拒否して、以後の安全を高める
  • this(…) で共通化: 似た初期化は一箇所にまとめる
  • super(…): 親クラスがある場合は最初に呼ぶ

メソッド(振る舞い)

public String id() { return id; }                // ゲッター(不変の公開)
public String name() { return name; }

public void rename(String newName) {             // 変更は検証付き
    var x = normalize(newName);
    if (x.isEmpty() || x.length() > MAX_NAME_LEN) return;
    this.name = x;
}

private static String normalize(String s) {      // 共通処理は private static で
    return s == null ? "" : s.trim().replaceAll("\\s+", " ");
}
Java
  • 公開最小: 必要な操作だけ public。内部は private で隠す
  • static とインスタンス: その個体に依存しない処理は static にすると明確

初期化の流れと static/インスタンスの違い(重要ポイントの深掘り)

初期化の順序

  • static 初期化: クラスロード時に一度だけ実行(static フィールド、static ブロック)
  • インスタンス初期化: new 時に「フィールド初期化→インスタンス初期化子→コンストラクタ」
public final class Config {
    public static final java.util.Locale DEFAULT_LOCALE; // static フィールド
    static { DEFAULT_LOCALE = java.util.Locale.JAPAN; }  // static 初期化ブロック

    private final java.time.Instant createdAt = java.time.Instant.now(); // インスタンス初期化

    { /* インスタンス初期化ブロック(滅多に使わない) */ }
}
Java
  • 指針: 複雑な初期化はコンストラクタに集約。static ブロックは最小限に

static メンバーとインスタンスメンバー

public final class Counter {
    private int value;                 // 個体ごとに異なる
    private static int created = 0;    // クラス全体で共有

    public Counter() { created++; }
    public void inc() { value++; }
    public int value() { return value; }
    public static int createdCount() { return created; }
}
Java
  • インスタンス: それぞれのオブジェクトの状態に紐づく
  • static: クラス全体の共有(設定、定数、工場メソッドなど)。多用しすぎるとテスト困難になるので節度を

継承・インターフェース・特殊なクラス表現

継承とメソッドオーバーライド

public abstract class Shape {
    public abstract double area();
    public String label() { return "shape"; }  // 共通の振る舞い
}
public final class Circle extends Shape {
    private final double r;
    public Circle(double r) { this.r = r; }
    @Override public double area() { return Math.PI * r * r; }
    @Override public String label() { return "circle"; }
}
Java
  • abstract: 抽象メソッドで「契約」を提示、子クラスが実装
  • @Override: 上書きの意図を明示し、ミスを防ぐ

インターフェース(できることの契約)

public interface Discount { int apply(int subtotal); }
public final class RateDiscount implements Discount {
    private final double rate;
    public RateDiscount(double rate) { this.rate = rate; }
    @Override public int apply(int subtotal) { return (int) Math.round(subtotal * (1 - rate)); }
}
Java
  • 複数実装が可能: 柔軟な差し替えとテストがしやすい

record・enum(特化構文)

public record Money(int amount, String currency) {        // 不変の値クラス
    public Money add(int delta) { return new Money(amount + delta, currency); }
}

public enum UserRole { ADMIN, EDITOR, VIEWER }            // 限定された列挙
Java
  • record: equals/hashCode/toString が自動。値オブジェクトに最適
  • enum: 取りうる値を型で限定し、switch などで安全に扱える

例題で身につける「フル装備のクラス」

完成度の高いクラス定義例

package com.example.domain;

import java.util.Objects;

public final class Email {
    private final String value;

    private Email(String value) { this.value = value; }

    public static Email of(String raw) {
        var v = normalize(raw);
        if (!isValid(v)) throw new IllegalArgumentException("invalid email: " + raw);
        return new Email(v);
    }

    public String value() { return value; }

    @Override public String toString() { return value; }
    @Override public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Email e)) return false;
        return value.equals(e.value);
    }
    @Override public int hashCode() { return Objects.hash(value); }

    private static String normalize(String s) {
        return s == null ? "" : s.trim().toLowerCase(java.util.Locale.ROOT);
    }
    private static boolean isValid(String s) {
        int at = s.indexOf('@');
        return at > 0 && at == s.lastIndexOf('@') && at < s.length() - 1 && !s.contains(" ");
    }
}
Java
  • ファクトリメソッド(of): 生成ルールを一箇所に集約、整合性を担保
  • 不変: 外へは値だけ公開、内部状態は変更不可
  • equals/hashCode: 値オブジェクトとして正しく比較・ハッシュ化

よくあるつまずきと回避

可視性を甘くする(public フィールド)

  • 問題: 直接代入で整合性が崩れる
  • 回避: 必ず private+検証付きメソッドで操作。必要な値だけゲッターで公開

コンストラクタで検証しない

  • 問題: 不正な初期状態が後でバグになる
  • 回避: コンストラクタ(または of)で必ずチェックし、例外で拒否

static を乱用する

  • 問題: 共有状態が増えてテスト困難に
  • 回避: 共有すべきもの(定数・設定)に限定。振る舞いはインスタンスへ

継承で無理に共通化

  • 問題: 階層が複雑化、変更がつらい
  • 回避: まずインターフェース+委譲で設計できないか検討。抽象化は「自然な共通点」に限定

仕上げのアドバイス(重要部分のまとめ)

クラスは「アクセス修飾子+適切な修飾子」で定義し、フィールドは private/可能なら不変、生成はコンストラクタ(または of)で検証、振る舞いは小さく明確に。static とインスタンスの責務を分け、初期化の順序を理解して安全に設計する。継承は慎重に、インターフェースと record/enum を使い分ける——この型を守るだけで、読みやすく壊れにくいクラス定義が手に入ります。

タイトルとURLをコピーしました