Javaでよく発生する例外:NullPointerException と ArrayIndexOutOfBoundsException の原因と予防法
勉強ちゃん
いろいろ勉強
Java学習のステップとして、前回までは「入力」「ファイル読み書き」「クラス化」などを取り上げてきました。
今回は、より実用的なアプリケーション開発に欠かせない 「スレッド(並行処理)」 について解説していきます。
スレッドとは、プログラムの処理を「並行して実行する仕組み」です。
通常のJavaプログラムは 1つのメインスレッド 上で順番に実行されますが、スレッドを使うことで 複数の処理を同時に進める ことができます。
これらを同時進行できると、アプリの動作がスムーズになります。
スレッドを導入するメリットは大きく4つあります。
Javaでスレッドを扱う方法はいくつかあります。
Threadクラスを拡張し、run()
メソッドをオーバーライドするシンプルな方法です。
class TestThread extends Thread {
public void run() {
System.out.println("スレッドが実行されました: " + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
TestThread testThread = new TestThread();
testThread.start(); // 新しいスレッドを開始
}
}
クラスの多重継承ができないJavaでは、こちらの方がよく使われます。
class TestRunnable implements Runnable {
public void run() {
System.out.println("Runnableを使ったスレッド実行: " + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new TestRunnable());
thread.start();
}
}
コードを短く書きたい場合、匿名クラスやラムダ式でスレッドを生成できます。
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("ラムダ式でスレッド実行");
});
thread.start();
}
}
前回の買い物アプリの主な処理は次の4つでした。
このうち (2) 入力 と (3) 計算+(4) 出力 を 別スレッド に分け、
安全なキューでデータを受け渡し(プロデューサ/コンシューマ)します。
BlockingQueue
(待機と通知を自動でやってくれる安全なキュー)class ProductLoader {
// ファイル形式例: リンゴ,100 のような「商品名,単価」
public Map<String, Integer> loadPrices(String fileName) throws IOException {
Map<String, Integer> priceMap = new HashMap<>();
try (BufferedReader br = new BufferedReader(new FileReader(fileName))) {
String line;
int row = 0;
while ((line = br.readLine()) != null) {
row++;
if (line.isBlank()) continue;
String[] data = line.split(",");
if (data.length != 2) {
throw new IOException("価格ファイルの形式エラー(" + row + "行目): " + line);
}
String name = data[0].trim();
int price = Integer.parseInt(data[1].trim());
priceMap.put(name, price);
}
}
return priceMap;
}
}
class PurchaseCommand {
public static final PurchaseCommand POISON = new PurchaseCommand("__END__", 0);
private final String productName;
private final int quantity;
public PurchaseCommand(String productName, int quantity) {
this.productName = productName; this.quantity = quantity;
}
public String getProductName() { return productName; }
public int getQuantity() { return quantity; }
}
BlockingQueue
は put()
(空きを待つ)と take()
(データを待つ)で、待ち合わせを安全にやってくれます。POISON
をキューに入れて、コンシューマ側に「終わり」を伝えます。class InputProducer implements Runnable {
private final BlockingQueue<PurchaseCommand> queue;
private final Map<String, Integer> priceMap;
public InputProducer(BlockingQueue<PurchaseCommand> queue, Map<String, Integer> priceMap) {
this.queue = queue; this.priceMap = priceMap;
}
@Override
public void run() {
try (Scanner sc = new Scanner(System.in)){
while (true) {
System.out.println("商品名を入力してください(終了は end):");
String name = sc.nextLine().trim();
if ("end".equalsIgnoreCase(name)) {
queue.put(PurchaseCommand.POISON);
break;
}
if (!priceMap.containsKey(name)) {
System.out.println("指定された商品は存在しません。");
continue;
}
System.out.println("数量を入力してください: ");
while (!sc.hasNextInt()) {
System.out.println("整数で入力してください: ");
sc.next();
}
int q = sc.nextInt();
sc.nextLine();
queue.put(new PurchaseCommand(name, q));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
class PurchaseConsumer implements Runnable {
private final BlockingQueue<PurchaseCommand> queue;
private final Map<String, Integer> priceMap;
private final ResultWriter writer;
private final String outputFile;
private final List<Purchase> lines = new ArrayList<>();
private final AtomicInteger total = new AtomicInteger(0);
public PurchaseConsumer(BlockingQueue<PurchaseCommand> queue,
Map<String, Integer> priceMap,
ResultWriter writer,
String outputFile) {
this.queue = queue;
this.priceMap = priceMap;
this.writer = writer;
this.outputFile = outputFile;
}
@Override
public void run() {
try {
while (true) {
PurchaseCommand cmd = queue.take(); // データ到着まで待機
if (cmd == PurchaseCommand.POISON) break;
int unitPrice = priceMap.get(cmd.getProductName());
Item item = new Item(cmd.getProductName(), unitPrice);
Purchase line = new Purchase(item, cmd.getQuantity());
lines.add(line);
total.addAndGet(line.getSubtotal());
}
// 全投入完了後にまとめて出力
writer.write(outputFile, lines, total.get());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (IOException io) {
System.out.println("書き込みエラー: " + io.getMessage());
}
}
}
AtomicInteger
を使っていますが、この例では単一スレッドだけが更新するため必須ではありません(実戦では並列更新時に有効)。class ResultWriter {
public void write(String fileName, List<Purchase> lines, int total) throws IOException {
try (BufferedWriter bw = new BufferedWriter(new FileWriter(fileName, true))) {
for (Purchase line : lines) {
bw.write(line.toLine());
bw.newLine();
}
bw.write("合計金額:" + total);
bw.newLine();
}
}
}
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.List;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
try {
// 価格表読み込み
ProductLoader loader = new ProductLoader();
Map<String, Integer> priceMap = loader.loadPrices("productUnitPrice");
// 並行パイプ(安全なキュー)
BlockingQueue<PurchaseCommand> queue = new LinkedBlockingQueue<>();
// スレッド起動
Thread producer = new Thread(new InputProducer(queue, priceMap), "InputProducer");
Thread consumer = new Thread(new PurchaseConsumer(queue, priceMap, new ResultWriter(), "productInfo"),
"PurchaseConsumer");
producer.start();
consumer.start();
// 終了待ち
producer.join();
consumer.join();
System.out.println("計算結果を productInfo に保存しました。");
} catch (Exception e) {
System.out.println("エラー: " + e.getMessage());
}
}
}
実行の流れ
入力スレッドがコマンド(商品名・数量)をキューに積む → 計算スレッドが取り出して小計・合計を更新 → 最後にまとめてファイル出力します。
次は JFrame(GUI)入門。今回の並行処理の発想を活かし、UI スレッドとワーカースレッドを分けて、安全に画面更新する基本を学びます。