クラス設計・カプセル化・static・初期化
アクセス修飾子の判定法・カプセル化・staticとインスタンスの本質・初期化順序・varargs・イミュータブル設計を完全解説
Chapter 03 ─ クラス設計・カプセル化・static・初期化
OOP(オブジェクト指向)ドメインは試験の33%を占める。この章から5章までが試験の根幹。 クラス・カプセル化・static・初期化順序は「何度問われても確実に答えられる」状態にしておく。
3-1. クラスとオブジェクトの関係 ─ 「設計図とモノ」
クラスとは
クラスは「設計図(テンプレート)」。オブジェクトを作るための型定義。
オブジェクトは「設計図から作った実体」。new するたびに独立した実体がメモリ上に生まれる。
たとえば「Person クラス」は「人間の設計図」。new Person("田中") とすると「田中さんという実体のオブジェクト」が作られる。new Person("鈴木") とすれば「鈴木さん」という別のオブジェクトが作られる。田中さんと鈴木さんは同じ設計図から作られたが、独立したオブジェクトとしてメモリ上に別々に存在する。
クラスの3要素
public class Person {
// 1. フィールド(状態): このオブジェクトが持つデータ
private String name;
private int age;
// 2. コンストラクタ(生成処理): new されたときに実行される初期化処理
public Person(String name, int age) {
this.name = name; // this.name = フィールド, name = 引数
this.age = age; // 名前が同じとき this で区別する
}
// 3. メソッド(振る舞い): このオブジェクトができること
public String getName() { return name; }
public int getAge() { return age; }
public void greet() {
System.out.println("こんにちは、" + name + "(" + age + "歳)です");
}
}
this とは何か
this はそのオブジェクト自身への参照。コンストラクタの引数名とフィールド名が同じとき、this.name(フィールド)と単なる name(引数)を区別するために使う。引数名がフィールド名と被らなければ this は不要だが、同名にして this を使うのが慣習。
// this の使用例
public Person(String name, int age) {
this.name = name; // 左辺: フィールド、右辺: 引数
this.age = age;
}
// this なしで別の引数名を使う場合(あまり一般的でない)
public Person(String personName, int personAge) {
name = personName;
age = personAge;
}
3-2. アクセス修飾子 ─ 「どこから見えるか」の4段階
アクセス修飾子はフィールド・メソッド・コンストラクタ・クラスに付けて、「どこからアクセスできるか」を制限する。
4種類の一覧
| 修飾子 | 同クラス | 同パッケージ | 別パッケージのサブクラス | その他(無関係なクラス) |
|---|---|---|---|---|
public | ○ | ○ | ○ | ○ |
protected | ○ | ○ | ○ | ✗ |
| (なし)package-private | ○ | ○ | ✗ | ✗ |
private | ○ | ✗ | ✗ | ✗ |
public ─ 全公開
どこからでもアクセス可能。外部に公開する API に使う。
protected ─「同じ家族(パッケージ)OR 血縁(継承)ならOK」
同パッケージ内のクラスと、別パッケージでもサブクラス(継承している子クラス)ならアクセスできる。これが試験の引っかけポイント。
// パッケージ: com.example.base
package com.example.base;
public class Base {
protected int value = 42; // protected フィールド
protected void show() {
System.out.println(value);
}
}
// パッケージ: com.example.child(別パッケージ)
package com.example.child;
import com.example.base.Base;
public class Child extends Base {
public void test() {
System.out.println(value); // ✅ OK: サブクラスなのでアクセス可
show(); // ✅ OK: 継承したメソッド
}
}
// 別パッケージの無関係なクラス
package com.example.other;
import com.example.base.Base;
public class Other {
public void test() {
Base b = new Base();
System.out.println(b.value); // ❌ エラー: 別パッケージで継承もしていない
b.show(); // ❌ エラー: 同様
}
}
試験での引っかけ
別パッケージのChildクラスの中でBaseクラスのインスタンスを作り、そのインスタンスのprotectedフィールドにアクセスしようとするとエラーになる(継承して得た自分のフィールドとしてなら OK)。
public class Child extends Base {
public void test() {
System.out.println(value); // ✅ OK: 継承した自分のフィールド
Base b = new Base();
System.out.println(b.value); // ❌ エラー: Base のインスタンスに直接アクセスは別パッケージでは不可
}
}
package-private(修飾子なし)─「同じ職場の人だけ」
修飾子を何も書かないと package-private になる。同じパッケージ内でだけ使える。サブクラスでも別パッケージなら見えない。
class Helper { // public なし → package-private
void assist() { } // public なし → package-private メソッド
}
private ─「自分だけの秘密」
同クラス内からのみアクセス可能。サブクラスからも見えない。フィールドは基本的に private にするのが OOP の原則(カプセル化)。
トップレベルクラスのアクセス修飾子
ファイル直下に書くトップレベルクラスに付けられる修飾子は public か なし(package-private)の2択のみ。private や protected はトップレベルクラスに使えない(ネストクラスには使える)。
public class TopLevel { } // OK
class TopLevelPkg { } // OK(package-private)
// private class TopLevel { } // ❌ コンパイルエラー
また、1つの .java ファイルに public クラスは1つだけ。複数の public クラスを同じファイルに書くとコンパイルエラー。ファイル名と public クラスの名前は完全に一致しなければならない。
3-3. カプセル化 ─「公開 API と実装の分離」
カプセル化とは「フィールドを private にして、アクセスはメソッド経由にする設計」。
目的は2つ: ① 不正な値を防ぐバリデーション ② 内部実装を変えても外部に影響しない
// フィールドが public だと誰でも好き勝手に書き換えられる(危険)
public class BadAccount {
public double balance; // 直接変更可能!
}
BadAccount a = new BadAccount();
a.balance = -999999; // 残高マイナスにできてしまう
// フィールドを private にしてメソッドでアクセス(安全)
public class GoodAccount {
private double balance;
public GoodAccount(double initial) {
if (initial < 0) throw new IllegalArgumentException("初期残高は0以上");
this.balance = initial;
}
public double getBalance() { return balance; } // 読み取りのみ
public void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException("入金額は正の数で");
balance += amount;
}
public void withdraw(double amount) {
if (amount <= 0) throw new IllegalArgumentException("出金額は正の数で");
if (amount > balance) throw new IllegalStateException("残高不足");
balance -= amount;
}
}
「setter を全部作ればいいんじゃないの?」という疑問もよくある。setBalance(double) を作ればマイナスも設定できてしまう。カプセル化の本質は「ドメイン(業務)にあった操作として API を設計すること」。残高を「セットする」操作ではなく「入金する」「出金する」という操作にすることで、意図しない使われ方を防げる。
3-4. static の本質 ─「クラスに属するか、インスタンスに属するか」
static なし = インスタンスメンバー
class Person {
String name; // インスタンスフィールド: Person オブジェクトごとに独立
int age;
}
Person p1 = new Person(); p1.name = "田中";
Person p2 = new Person(); p2.name = "鈴木";
// p1.name と p2.name は別々の領域に存在する
static あり = クラスメンバー
class Person {
static int count = 0; // クラス変数: クラス自体に1つだけ。全インスタンスで共有
String name;
Person(String name) {
this.name = name;
count++; // 生成されるたびに全体カウントが増える
}
}
new Person("田中");
new Person("鈴木");
System.out.println(Person.count); // 2(全インスタンスで共有)
イメージ: クラスを「会社」、インスタンスを「社員」に例えると、
インスタンス変数 = 社員ごとの「名刺(名前・部署)」
クラス変数 = 会社全体の「従業員数カウンター」
static メソッドはオブジェクトなしで呼べる
class MathUtils {
static int square(int n) { return n * n; } // static メソッド
int multiplied(int n) { return n * 2; } // インスタンスメソッド
}
MathUtils.square(5); // ✅ オブジェクト不要(クラス名で直接呼べる)
// MathUtils.multiplied(5); // ❌ インスタンスメソッドはオブジェクトが必要
new MathUtils().multiplied(5); // ✅
Math.abs() や Integer.parseInt() など、Java の標準ライブラリの多くが static メソッド。
static メソッドからインスタンスメンバーにアクセス不可
static メソッドは「どのオブジェクトか」という文脈(this)を持たない。そのため、インスタンス変数やインスタンスメソッドにアクセスしようとするとコンパイルエラー。
class Sample {
int instanceVar = 10;
static int staticVar = 20;
static void staticMethod() {
System.out.println(staticVar); // ✅ OK
// System.out.println(instanceVar); // ❌ コンパイルエラー!
// instanceMethod(); // ❌ コンパイルエラー!
// System.out.println(this); // ❌ static に this はない
}
void instanceMethod() {
System.out.println(instanceVar); // ✅ OK
System.out.println(staticVar); // ✅ OK(インスタンスから static は参照可)
staticMethod(); // ✅ OK
}
}
試験で最も多い static エラーパターン
class Sample { int x = 10; public static void main(String[] args) { System.out.println(x); // ❌ コンパイルエラー! // main は static。x はインスタンス変数 } }これを直すには
new Sample().xかSampleのインスタンスを作ってアクセスする。 またはxをstaticにする。
static 変数への非推奨なアクセス方法
static 変数はインスタンス変数経由でもアクセスできてしまうが、非推奨(コンパイル警告が出る)。
Sample s = new Sample();
s.staticVar = 100; // ⚠️ 動くが警告。「クラス名でアクセスすべき」
Sample.staticVar = 100; // ✅ 正しい書き方
試験では「s.staticVar と Sample.staticVar は同じ変数を指すか?」という問いが出ることがある。答えは「同じ変数を指す(同一)」。
3-5. コンストラクタの詳細
コンストラクタの特徴
public class Dog {
public Dog() { // コンストラクタ
// 戻り値型を書かない(void も書かない)
// 名前はクラス名と完全に一致
}
// 戻り値型を書くとコンストラクタではなくメソッドになる
public void Dog() { } // これはメソッド(コンストラクタではない)
}
コンストラクタを「普通のメソッドと同じ」と思いがちだが、戻り値型がない点が決定的に違う(void も書かない)。void を書いてしまうとメソッドとして扱われる。
コンストラクタのオーバーロード
引数の数や型が異なるコンストラクタを複数定義できる(メソッドのオーバーロードと同じルール)。
public class Rectangle {
private int width;
private int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
public Rectangle(int size) { // 正方形(width = height)
this(size, size); // this() で別のコンストラクタに委譲
}
public Rectangle() { // デフォルト(1x1)
this(1, 1);
}
}
this(引数) は「同じクラスの別コンストラクタを呼ぶ」構文(コンストラクタチェーン)。
this() の制約(試験頻出)
this() によるコンストラクタ呼び出しには厳しい制約がある。
- コンストラクタの最初の行でなければならない
public Rectangle(int size) {
System.out.println("start"); // ❌ コンパイルエラー!this() より前に書けない
this(size, size);
}
public Rectangle(int size) {
this(size, size); // ✅ 最初の行なら OK
System.out.println("done"); // ✅ this() の後なら OK
}
super()とthis()は同じコンストラクタに両方書けない
どちらも「コンストラクタの最初の行」でなければならないため、両立できない。
public Child(int x) {
super(x); // ❌ または
this(x, 0); // ❌ どちらか一方のみ
}
デフォルトコンストラクタ
コンストラクタを1つも定義しないと、コンパイラが引数なしのコンストラクタ(デフォルトコンストラクタ)を自動生成する。ただし引数ありコンストラクタを1つでも書くと自動生成されなくなる。
class A {
// コンストラクタなし
// → コンパイラが A() {} を自動生成
}
new A(); // ✅ OK(自動生成された A() が使われる)
class B {
B(int n) { } // 引数ありのコンストラクタを定義
// → B() は自動生成されない!
}
new B(); // ❌ コンパイルエラー(B() が存在しない)
new B(10); // ✅ OK
試験頻出
「親クラスに引数ありコンストラクタだけ定義して、子クラスのコンストラクタを何も書かない」というパターンがエラーになる。 子クラスのコンストラクタは自動でsuper()を呼ぶが、親クラスにA()がないためエラー。
super() の自動挿入
コンストラクタの先頭に this() も super() も書かない場合、コンパイラが自動的に super() を先頭に挿入する。
class Parent {
Parent() {
System.out.println("Parent()");
}
}
class Child extends Parent {
Child() {
// ← コンパイラが super() を自動挿入
System.out.println("Child()");
}
}
new Child();
// Parent() ← super() が先に呼ばれる
// Child()
親クラスに引数なしコンストラクタがない場合:
class Parent {
Parent(String name) { } // 引数あり。Parent() は存在しない
}
class Child extends Parent {
Child() {
// 自動挿入された super() が → super() を呼ぶが Parent() は存在しない!
}
// ❌ コンパイルエラー
}
// 修正: 明示的に super(...) を書く
class Child extends Parent {
Child() {
super("defaultName"); // ✅ OK
}
}
3-6. 初期化の順序 ─ 「いつ、誰が、何を初期化するか」
初期化には複数の仕組みが関わる。実行順序は決まっている。
全体の流れ
【クラスが初めてロードされるとき(JVM起動後、最初のアクセス時。1回のみ)】
① static フィールドを宣言時の初期値で初期化
② static 初期化ブロック(上から順に実行)
【new するたびに(インスタンス生成のたびに)】
③ インスタンスフィールドを宣言時の初期値で初期化
④ インスタンス初期化ブロック(上から順に実行)
⑤ コンストラクタ本体を実行
static 初期化ブロック
static { } で囲む。クラスがロードされた時点で一度だけ実行される。
class Config {
static final int MAX;
static {
// 複雑な計算が必要な定数の初期化
MAX = Runtime.getRuntime().availableProcessors() * 2;
System.out.println("static ブロック実行: MAX=" + MAX);
}
}
// Config クラスに最初にアクセスしたとき static ブロックが実行される
System.out.println(Config.MAX);
// 出力: static ブロック実行: MAX=X
// X
インスタンス初期化ブロック
{ } のみで書く(static なし)。すべてのコンストラクタの前に毎回実行される。複数のコンストラクタに同じ前処理を書きたいときに便利。
class Sample {
int x;
{
x = 10;
System.out.println("インスタンスブロック: x=" + x);
}
Sample() {
System.out.println("コンストラクタ(): x=" + x);
}
Sample(int y) {
System.out.println("コンストラクタ(int): x=" + x);
}
}
new Sample(); // インスタンスブロック実行 → Sample() 実行
new Sample(5); // インスタンスブロック実行 → Sample(int) 実行
順序の確認例(試験対策)
class Order {
static int s = 1; // ①
static {
System.out.println("static block 1: s=" + s); // ②
s = 100;
}
static {
System.out.println("static block 2: s=" + s); // ③
}
int i = s; // ④(s はすでに 100)
{
System.out.println("instance block: i=" + i); // ⑤
}
Order() {
System.out.println("constructor: i=" + i); // ⑥
}
}
new Order();
new Order();
出力:
static block 1: s=1
static block 2: s=100
instance block: i=100
constructor: i=100
instance block: i=100
constructor: i=100
- static 関連(①②③): クラスロード時に1回のみ
- インスタンス関連(④⑤⑥):
newするたびに実行
複数 static ブロックの順序
static ブロックは**書いた順番(上から下)**に実行される。
class Foo {
static int x;
static { x = 1; System.out.println(x); } // 1: 実行順①
static { x = 2; System.out.println(x); } // 2: 実行順②
static { x = 3; System.out.println(x); } // 3: 実行順③
}
// 1, 2, 3 の順に出力
3-7. varargs(可変長引数)
基本
引数の型の後ろに ... を付けると、その引数は0個以上の値を受け取れる。メソッド内部では配列として扱われる。
static int sum(int... nums) {
int total = 0;
for (int n : nums) total += n;
return total;
}
sum(1, 2, 3); // nums = [1, 2, 3] → 6
sum(10, 20); // nums = [10, 20] → 30
sum(); // nums = [](空配列)→ 0
sum(new int[]{1,2,3}); // 配列を直接渡すことも可
varargs の制約(試験頻出)
ルール1: varargs は最後の引数でなければならない
void method(int... a, String b) { } // ❌ varargs が最後ではない
void method(String label, int... vals) { } // ✅ 最後の引数のみ
ルール2: varargs は1つのメソッドに1つだけ
void method(int... a, String... b) { } // ❌ varargs が複数
ルール3: 配列型と varargs は区別される(オーバーロードに注意)
void print(int[] arr) { System.out.println("array"); }
void print(int... vals) { System.out.println("varargs"); }
// ❌ これはコンパイルエラー(int[] と int... は同じシグネチャとして扱われる)
より具体的なオーバーロードが優先される
void show(int n) { System.out.println("int"); }
void show(int... ns) { System.out.println("varargs"); }
show(42); // "int"(より具体的な int が優先)
show(1, 2, 3); // "varargs"(int 単体では対応できない)
show(); // "varargs"(引数なし)
3-8. イミュータブル(不変)クラス設計
なぜイミュータブルか
String が不変なのと同じ理由: スレッドセーフ・セキュリティ・hashCode キャッシュが可能。
java.time.LocalDate・BigDecimal など多くの標準クラスがイミュータブルに設計されている。
イミュータブルクラスの4条件
- クラスを
finalにする(サブクラスで可変に「上書き」されないよう) - フィールドをすべて
private finalにする(直接変更不可) - setter を提供しない(メソッド経由の変更もできないよう)
- コンストラクタでのみ値を設定する
public final class Point { // ① final クラス
private final int x; // ② private final フィールド
private final int y;
public Point(int x, int y) {
this.x = x; // ④ コンストラクタのみで設定
this.y = y;
}
public int getX() { return x; } // ③ getter あり(setter なし)
public int getY() { return y; }
// 「移動した新しい座標」を返す(自分は変えない)
public Point translate(int dx, int dy) {
return new Point(x + dx, y + dy); // 新しいオブジェクトを返す
}
}
Point p1 = new Point(1, 2);
Point p2 = p1.translate(3, 4); // p1 は変わらない
System.out.println(p1); // Point{x=1, y=2}
System.out.println(p2); // Point{x=4, y=6}
可変オブジェクトのフィールドは「防衛的コピー」が必要
フィールドに List や配列などの可変オブジェクトが含まれる場合、参照を共有すると外部から内容を変更できてしまう。
// 危険なパターン(見た目はイミュータブルっぽいが実は可変)
public final class BadNames {
private final List<String> names;
public BadNames(List<String> names) {
this.names = names; // ← 外部リストへの参照をそのまま保持!
}
public List<String> getNames() {
return names; // ← この参照を使って外部が変更できる!
}
}
List<String> original = new ArrayList<>(Arrays.asList("Alice", "Bob"));
BadNames bad = new BadNames(original);
original.add("Charlie"); // original と bad.names は同じリストなので変更される!
System.out.println(bad.getNames()); // [Alice, Bob, Charlie](意図せず変わった)
// 正しい防衛的コピー
public final class GoodNames {
private final List<String> names;
public GoodNames(List<String> names) {
this.names = new ArrayList<>(names); // コピーを保持
}
public List<String> getNames() {
return Collections.unmodifiableList(names); // 変更不可ビューを返す
}
}
3-9. パッケージと import
パッケージ宣言
クラスをどのパッケージに属させるかを宣言する。ファイルの一番最初の行(コメントを除く)に書く。
package com.example.shop; // この行が最初
import java.util.List;
public class ShopService { }
パッケージ名はディレクトリ構造に対応。com.example.shop なら com/example/shop/ShopService.java。
import 宣言
他のパッケージのクラスを使うときに必要。package 宣言の後、クラス定義の前に書く。
import java.util.ArrayList; // ArrayList クラスだけインポート
import java.util.List; // List インターフェースだけインポート
import java.util.*; // java.util パッケージ全体をインポート(ワイルドカード)
インポート不要のもの:
java.langパッケージ(String,Integer,System,Math,Objectなど)は自動インポート- 同パッケージ内のクラス
// java.lang は自動インポートなので明示不要(書いてもよい)
import java.lang.String; // 書かなくてよい
// 同パッケージなら不要
// com.example.shop パッケージの別クラスは import 不要
ワイルドカードインポートの注意点
import java.util.* はサブパッケージを含まない。
import java.util.*; // java.util パッケージのクラスのみ
// java.util.concurrent などのサブパッケージは含まれない
// import java.util.concurrent.*; を別途書く必要がある
static import
クラスの static メンバーを、クラス名なしで使えるようにする。
import static java.lang.Math.PI;
import static java.lang.Math.sqrt;
import static java.lang.Math.*; // Math の全 static メンバー
double area = PI * 5 * 5; // Math.PI と書かなくてよい
double root = sqrt(16); // Math.sqrt と書かなくてよい
テストコードでよく使う:
import static org.junit.jupiter.api.Assertions.*;
assertEquals(4, add(2, 2)); // Assertions.assertEquals を省略
assertTrue(result);
✏️ 練習問題
Q: 次のコードの出力を答えよ。
public class InitTest {
static int x = 1;
int y = 10;
static {
x = 2;
System.out.println("S1: x=" + x);
}
{
y = 20;
System.out.println("I1: y=" + y);
}
static {
x = 3;
System.out.println("S2: x=" + x);
}
InitTest() {
System.out.println("C: y=" + y);
}
public static void main(String[] args) {
new InitTest();
new InitTest();
}
}
答え
S1: x=2
S2: x=3
I1: y=20
C: y=20
I1: y=20
C: y=20
- クラスロード時(1回のみ): static フィールド x=1 初期化 → S1 実行(x=2)→ S2 実行(x=3)
- 1回目の
new: インスタンスフィールド y=10 初期化 → I1 実行(y=20)→ C 実行 - 2回目の
new: 同様に I1 → C
Chapter 03 まとめチェックリスト
-
public>protected> package-private >privateの順でアクセス範囲が狭くなる -
protected= 同パッケージ OR サブクラス(別パッケージでも継承していればOK) - トップレベルクラスに使えるのは
publicと package-private のみ -
static= クラスに属する。インスタンスに属さない - static メソッドからインスタンス変数・インスタンスメソッドに直接アクセス不可
- 引数ありコンストラクタを1つ定義するとデフォルトコンストラクタが消える
- コンストラクタに戻り値型(void も含む)は書かない
-
this()はコンストラクタの最初の行のみ。super()と共存不可 - コンストラクタ先頭に何も書かないと
super()が自動挿入される - 初期化順序: static フィールド→ static ブロック→(インスタンスごとに)インスタンスフィールド→インスタンスブロック→コンストラクタ
- varargs は最後の引数のみ。1つのメソッドに1つのみ
- イミュータブルの4条件: final クラス・private final フィールド・setter なし・コンストラクタのみ初期化
- 可変フィールドは防衛的コピーが必要(コンストラクタで受け取ったリストをそのまま持たない)