Java 落とし穴と注意事項
Javaでよくあるバグ、メモリリーク、スレッドセーフの問題をまとめます。
メモリリーク
Javaにはガベージコレクションがありますが、メモリリークは発生します。GCは「参照されていないオブジェクト」を回収しますが、不要な参照が残っているとリークします。
コレクションへの追加のみ
// NG: 追加するだけで削除しない
public class EventHistory {
private static final List<Event> events = new ArrayList<>();
public void addEvent(Event event) {
events.add(event); // 永遠に増え続ける
}
}
// OK: 上限を設けるか、古いものを削除
public class EventHistory {
private static final int MAX_SIZE = 1000;
private final Queue<Event> events = new LinkedList<>();
public void addEvent(Event event) {
events.add(event);
if (events.size() > MAX_SIZE) {
events.poll(); // 古いものを削除
}
}
}
staticフィールドでの参照保持
// NG: staticフィールドに大きなオブジェクトを保持
public class Cache {
private static final Map<String, byte[]> cache = new HashMap<>();
public void put(String key, byte[] data) {
cache.put(key, data); // アプリ終了までメモリに残る
}
}
// OK: WeakHashMap、サイズ制限、有効期限を設ける
public class Cache {
private final Map<String, byte[]> cache = new LinkedHashMap<>() {
@Override
protected boolean removeEldestEntry(Map.Entry<String, byte[]> eldest) {
return size() > 100; // 100件を超えたら古いものを削除
}
};
}
リスナー・コールバックの登録解除忘れ
// NG: 登録したまま解除しない
public class UserView {
public UserView(EventBus eventBus) {
eventBus.register(this); // 登録
// 解除を忘れると、UserViewがGCされない
}
}
// OK: 明示的に解除
public class UserView implements AutoCloseable {
private final EventBus eventBus;
public UserView(EventBus eventBus) {
this.eventBus = eventBus;
eventBus.register(this);
}
@Override
public void close() {
eventBus.unregister(this); // 必ず解除
}
}
リソースのclose忘れ
// NG: closeを忘れるとリソースリーク
public String readFile(String path) throws IOException {
FileInputStream fis = new FileInputStream(path);
// 例外が発生するとcloseされない
byte[] data = fis.readAllBytes();
fis.close();
return new String(data);
}
// OK: try-with-resources
public String readFile(String path) throws IOException {
try (FileInputStream fis = new FileInputStream(path)) {
return new String(fis.readAllBytes());
} // 自動的にclose
}
スレッドセーフ
マルチスレッド環境では、データ競合やデッドロックに注意が必要です。
可変オブジェクトの共有
// NG: 複数スレッドで可変オブジェクトを共有
public class Counter {
private int count = 0;
public void increment() {
count++; // 読み取り→加算→書き込みの3操作(原子的でない)
}
}
// OK: AtomicIntegerを使う
public class Counter {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // 原子的操作
}
}
// OK: synchronizedを使う
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
}
check-then-act(競合状態)
// NG: チェックと操作の間に他スレッドが割り込む可能性
public class UserCache {
private final Map<String, User> cache = new HashMap<>();
public User getOrCreate(String id) {
if (!cache.containsKey(id)) { // チェック
cache.put(id, new User(id)); // 操作(この間に他スレッドが...)
}
return cache.get(id);
}
}
// OK: ConcurrentHashMapのcomputeIfAbsent
public class UserCache {
private final Map<String, User> cache = new ConcurrentHashMap<>();
public User getOrCreate(String id) {
return cache.computeIfAbsent(id, User::new); // 原子的操作
}
}
スレッドセーフでないクラス
以下のクラスはスレッドセーフではありません。複数スレッドで共有しないでください。
// NG: SimpleDateFormatを共有
private static final SimpleDateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd");
public String format(Date date) {
return FORMAT.format(date); // スレッドセーフでない!
}
// OK: DateTimeFormatterを使う(スレッドセーフ)
private static final DateTimeFormatter FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
public String format(LocalDate date) {
return FORMAT.format(date); // スレッドセーフ
}
// OK: ThreadLocalで各スレッドに固有のインスタンス
private static final ThreadLocal<SimpleDateFormat> FORMAT =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
スレッドセーフでない主なクラス:
SimpleDateFormatHashMap,ArrayList,HashSet(→ConcurrentHashMap,CopyOnWriteArrayList)StringBuilder(→StringBuffer、ただし通常は同期不要)Random(→ThreadLocalRandom)MessageDigest,Cipher,Signature(java.security系全般)javax.xml.parsers.DocumentBuilderjava.text.NumberFormat
HashMapの無限ループ
Java 7以前では、複数スレッドでHashMapを操作すると無限ループが発生することがありました。
// NG: 複数スレッドでHashMap
private final Map<String, String> map = new HashMap<>();
// スレッド1とスレッド2が同時にput → 無限ループの可能性
// OK: ConcurrentHashMapを使う
private final Map<String, String> map = new ConcurrentHashMap<>();
ダブルチェックロッキングの誤り
// NG: volatileがないと壊れる
public class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 部分的に初期化されたオブジェクトが見える可能性
}
}
}
return instance;
}
}
// OK: volatileを付ける
public class Singleton {
private static volatile Singleton instance;
// ...
}
// OK: ホルダーイディオム(推奨)
public class Singleton {
private Singleton() {}
private static class Holder {
static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}