タイトル: staticクラスとシングルトンの違い
SEOタイトル: static クラス vs Singleton 完全比較 (Java / C# / Python の実装視点)
| この記事の要点 |
|
結論を先に
| 観点 | static クラス | Singleton |
|---|---|---|
| インスタンス | 無し (クラス自体) | 1 つ (this あり) |
| 状態 | クラスフィールド (グローバル) | インスタンスフィールド |
| 呼び出し | ClassName.method() | ClassName.getInstance().method() |
| インターフェース実装 | 不可 | 可 |
| 継承 | 不可 (Java の static、C# の static class) | 可 |
| 多態性 | 不可 | 可 |
| Mock | 困難 (PowerMock 等) | 容易 (DI で差替) |
| Lazy 初期化 | クラスロード時に確定 | 初回 getInstance() 時に生成可 |
| 用途 | ユーティリティ / 純粋関数 | 状態 / リソース管理 |
static クラス (Java)
// Java の static utility クラス例
public final class MathUtils {
public static final double PI = 3.14159265;
private MathUtils() { } // インスタンス化禁止
public static double square(double x) {
return x * x;
}
public static double sum(double[] arr) {
double s = 0;
for (double v : arr) s += v;
return s;
}
}
// 使用 (インスタンス化せずクラス名から呼ぶ)
double s = MathUtils.square(5);
double pi = MathUtils.PI;
Java の標準 API では java.lang.Math, java.util.Collections, java.util.Arrays がこのパターンです。
Singleton (Java)
public class Logger {
private static volatile Logger instance;
private final List<String> buffer = new ArrayList<>(); // 状態を持つ
private Logger() { }
public static Logger getInstance() {
if (instance == null) {
synchronized (Logger.class) {
if (instance == null) instance = new Logger();
}
}
return instance;
}
public void log(String msg) {
buffer.add(LocalDateTime.now() + " " + msg);
}
public List<String> getBuffer() {
return Collections.unmodifiableList(buffer);
}
}
// 使用
Logger logger = Logger.getInstance();
logger.log("started");
違い 1: インスタンス・状態
- static はクラス全体で 1 つの「クラスレベルのデータ」。
thisが存在しない - Singleton は 1 つの「インスタンスレベルのデータ」。
thisが使える
状態を持たず純粋計算するだけなら static、状態 (リスト・接続・設定) を保持して操作するなら Singleton が向きます。
違い 2: インターフェース実装
interface Notifier {
void send(String msg);
}
// ❌ static クラスは interface を実装できない
public final class StaticMailNotifier {
public static void send(String msg) { ... }
}
// → 「StaticMailNotifier implements Notifier」ができない
// ✅ Singleton なら interface 実装可能
public class MailNotifier implements Notifier {
private static final MailNotifier INSTANCE = new MailNotifier();
public static MailNotifier getInstance() { return INSTANCE; }
@Override public void send(String msg) { ... }
}
// 使用側はインターフェース型で受けられる → 差替え可能
Notifier notifier = MailNotifier.getInstance();
notifier.send("hi");
違い 3: 継承
Java の static フィールド・メソッドは継承できない (オーバーライドできない)。C# の static class も継承不可。Singleton はインスタンスメソッドのため通常のクラスと同じく継承・オーバーライド可能です。
// Singleton は継承で振る舞いを切り替えられる
public class BaseLogger {
public void log(String msg) { System.out.println(msg); }
}
public class SlackLogger extends BaseLogger {
@Override public void log(String msg) {
super.log(msg);
sendToSlack(msg);
}
}
// テスト環境 / 本番環境で getInstance() の中身を切り替え可能
違い 4: Mock しやすさ (テスト)
// ❌ static は Mock しにくい
public class OrderService {
public void notify(String msg) {
MailUtils.sendMail(msg); // ← static 呼び出し、テストで差替できない
}
}
// ✅ Singleton + interface で差替容易
public class OrderService {
private final Notifier notifier;
public OrderService(Notifier notifier) { // コンストラクタ注入
this.notifier = notifier;
}
public void notify(String msg) {
notifier.send(msg);
}
}
// テストでは Mock 注入
Notifier mock = mock(Notifier.class);
OrderService svc = new OrderService(mock);
svc.notify("test");
verify(mock).send("test");
違い 5: Lazy 初期化
static フィールドは クラスロード時に初期化されるため、使わなくても初期化コストがかかります。Singleton は getInstance() 初回呼び出しまで遅延 (Lazy) できます:
// 初期化に 5 秒かかる重い処理
public class HeavyConfig {
private static final HeavyConfig INSTANCE = new HeavyConfig();
private HeavyConfig() {
// 5 秒かかる初期化
}
public static HeavyConfig getInstance() { return INSTANCE; }
}
// → クラス参照時に 5 秒待たされる
// Lazy 版
public class HeavyConfig {
private static HeavyConfig instance;
private HeavyConfig() { /* 5秒 */ }
public static synchronized HeavyConfig getInstance() {
if (instance == null) instance = new HeavyConfig();
return instance;
}
}
// → 初回 getInstance() でのみ 5 秒
C# での比較
// static クラス
public static class MathUtil {
public const double PI = 3.14159;
public static double Square(double x) => x * x;
}
// Singleton (Lazy<T> 推奨)
public sealed class Logger {
private static readonly Lazy<Logger> lazy = new(() => new Logger());
public static Logger Instance => lazy.Value;
private List<string> buffer = new();
private Logger() {}
public void Log(string m) => buffer.Add(m);
}
Python での比較
Python は module 自体がシングルトンなので、static と singleton の区別は実装上ほぼ無く、書き方の問題:
# static 相当 (module-level 関数)
# math_utils.py
PI = 3.14159
def square(x): return x * x
# Singleton 相当 (クラス + __new__)
class Logger:
_inst = None
def __new__(cls):
if cls._inst is None:
cls._inst = super().__new__(cls)
cls._inst.buf = []
return cls._inst
def log(self, m): self.buf.append(m)
# 実用上、状態を持たないなら module-level 関数 (= static 相当)
# 状態を持つなら singleton クラスか、module-level の変数
使い分けの指針
| シーン | 推奨 |
|---|---|
| 純粋関数の集約 (数学関数、文字列操作) | static |
| 状態 (キャッシュ・接続プール・ロガー) | Singleton |
| 差替・テスト容易性が必要 | Singleton + interface |
| 定数の集約 | static final / const |
| DI フレームワーク利用環境 | DI コンテナの Singleton スコープ |
| Lazy 初期化が必要 | Singleton |
| サブクラスで振る舞い変更 | Singleton |
アンチパターン: 状態を持つ static クラス
// ❌ 状態を持つ static クラス (グローバル変数と同じ問題)
public final class SessionStore {
private static Map<String, User> sessions = new HashMap<>();
public static void put(String id, User u) { sessions.put(id, u); }
public static User get(String id) { return sessions.get(id); }
}
// 問題:
// - テストごとに sessions がリークする
// - スレッドセーフでない (ConcurrentHashMap でも問題は残る)
// - Mock しにくい
// - 依存先がコードに埋め込まれる
// ✅ Singleton か DI コンテナで管理
public class SessionStore {
private final Map<String, User> sessions = new ConcurrentHashMap<>();
// ... DI コンテナで Singleton として登録
}
現代的な妥協点: DI コンテナ + Singleton スコープ
Spring、Laravel、NestJS 等のフレームワークは、コンテナでオブジェクトのライフサイクルを管理します。Singleton パターンを自前で書くのではなく、コンテナの Singleton スコープに登録するのが現代の正解です。
- テストで容易に差替 (Mock 注入)
- スコープ切替 (Singleton / Request / Prototype) が柔軟
- 循環依存の早期検知
getInstance()をコードから消せる (コンストラクタ注入)
FAQ
Q: ユーティリティクラスは static で書くべきか?
A: 状態を持たない純粋関数群なら static で OK (Math / Collections)。テストもしやすい。状態が入る瞬間に Singleton か DI コンテナへ移行を。
Q: static メソッドはテストできない?
A: テスト対象が static を呼んでいると Mock 困難 (PowerMockito / Mockito 5+ の static モックで可能だが推奨されない)。Singleton + DI で差替が王道。
Q: Singleton と static、メモリ消費に差は?
A: ほぼ同じ。どちらもプロセス内に 1 つ。Lazy 初期化なら Singleton の方が起動直後のメモリ少なめ。
Q: 言語によってどちらが好まれる?
A: Java は両方使われる (Math は static、ロガーは Singleton)。C# は static class が言語サポートで Java より使いやすい。Python は module-level が一般的。PHP は近年 DI コンテナ (Laravel / Symfony) 経由が主流。