JAVA言語の文法解説
基本文法からオブジェクト指向、例外処理、コレクション、データ構造・アルゴリズム、マルチスレッド、JVMの仕組みまで、基本情報技術者試験に頻出するCS基礎トピックをJavaのコードで確認しながら解説します。
文法解説
1. コメント
Javaには1行コメント・複数行コメント・ドキュメンテーションコメント(Javadoc)の3種類があります。JavadocコメントはjavadocコマンドでAPIドキュメントを自動生成できます。
// 1行コメント
/*
* 複数行コメント
* 複数行にまたがる説明を書ける
*/
/**
* Javadocコメント。クラスやメソッドの説明に使う。
* @param name 名前
* @return 挨拶文
*/
public String greet(String name) {
return "こんにちは、" + name + "さん";
}
2. 変数と基本データ型/参照型・静的型付け
Javaは静的型付け言語で、変数は宣言時に型が確定し、コンパイル時に型チェックが行われます。データ型は、値そのものをスタックに保持するプリミティブ型(基本データ型)と、ヒープ上のオブジェクトへの参照を保持する参照型に大別されます。
// プリミティブ型(8種類):値そのものを保持
byte b = 127; // 8bit整数
short s = 32000; // 16bit整数
int age = 20; // 32bit整数(最もよく使う)
long population = 1_400_000_000L; // 64bit整数
float f = 3.14f; // 32bit浮動小数点
double height = 172.5; // 64bit浮動小数点(デフォルト)
char initial = 'A'; // 16bit Unicode文字
boolean isStudent = true; // true/false
// 参照型:オブジェクトへの参照(メモリアドレス相当)を保持
String name = "太郎"; // Stringは参照型(実体はヒープ上)
int[] scores = {90, 80}; // 配列も参照型
final int MAX_SCORE = 100; // finalを付けると再代入不可(定数)
// 静的型付けなので、後から異なる型の値を代入しようとするとコンパイルエラーになる
// age = "20歳"; // コンパイルエラー:int型にString型は代入できない
3. 演算子とキャスト(型変換)
算術演算子・比較演算子・論理演算子に加え、型の互換性を確認するinstanceof演算子があります。数値型同士の変換には、精度が上がる暗黙的な変換(自動型変換)と、精度が落ちる可能性がある明示的なキャストがあります。
int i = 10;
double d = i; // 暗黙的な変換(int→double、情報が失われない)
double pi = 3.14;
int truncated = (int) pi; // 明示的なキャスト(double→int、小数点以下切り捨て)
System.out.println(truncated); // 3
Object obj = "文字列です";
if (obj instanceof String str) { // パターンマッチング(Java16〜)
System.out.println(str.length());
}
System.out.println(7 / 2); // 3(整数同士の除算は切り捨て)
System.out.println(7 % 2); // 1(剰余)
System.out.println(7.0 / 2); // 3.5(片方がdoubleなら浮動小数点演算)
4. 制御構文(if / switch / for / while)
条件分岐にはif-elseとswitch、繰り返しにはfor・while・do-while・拡張for文があります。Java14以降はswitch式(->構文)も使えます。
int score = 75;
if (score >= 80) {
System.out.println("優");
} else if (score >= 60) {
System.out.println("良");
} else {
System.out.println("再履修");
}
// 従来のswitch文(fall-throughに注意。breakを忘れると次のcaseまで実行される)
switch (score / 10) {
case 10:
case 9:
case 8:
System.out.println("優");
break;
default:
System.out.println("優以外");
}
// switch式(Java14〜):矢印記法でfall-throughせず値も返せる
String grade = switch (score / 10) {
case 10, 9, 8 -> "優";
case 7, 6 -> "良";
default -> "再履修";
};
for (int i = 0; i < 3; i++) {
System.out.println(i);
}
int n = 0;
while (n < 3) {
System.out.println(n);
n++;
}
int m = 0;
do {
System.out.println(m); // 最低1回は実行される
m++;
} while (m < 3);
5. メソッド(オーバーロード・可変長引数・再帰)
同じ名前で引数の型や数が異なるメソッドを複数定義することをオーバーロード(多重定義)と呼びます。引数の数を可変にする可変長引数や、自分自身を呼び出す再帰呼び出しも基本情報試験の頻出テーマです。
// オーバーロード:引数の型・数が異なれば同名メソッドを定義できる
static int add(int a, int b) {
return a + b;
}
static double add(double a, double b) {
return a + b;
}
static int add(int a, int b, int c) {
return a + b + c;
}
// 可変長引数(内部的には配列として扱われる)
static int sum(int... numbers) {
int total = 0;
for (int n : numbers) {
total += n;
}
return total;
}
// sum(1, 2, 3, 4) のように任意個の引数を渡せる
// 再帰呼び出し:階乗を求める例(基底条件を忘れると無限再帰でスタックオーバーフロー)
static long factorial(int n) {
if (n <= 1) {
return 1; // 基底条件(これがないと無限に再帰してしまう)
}
return n * factorial(n - 1); // 自分自身を呼び出す
}
// factorial(5) => 120
6. クラスとカプセル化
Javaは全ての処理をクラスの中に書く必要があります。フィールドをprivateにして外部から直接変更できないようにし、publicなメソッド(getter/setter)経由でアクセスさせることをカプセル化と呼びます。アクセス修飾子はprivate < パッケージプライベート(無指定) < protected < publicの順に公開範囲が広がります。
public class BankAccount {
private long balance; // privateで外部からの直接変更を禁止
public BankAccount(long initialBalance) {
if (initialBalance < 0) {
throw new IllegalArgumentException("初期残高は0以上にしてください");
}
this.balance = initialBalance;
}
// getter:値の取得のみ許可
public long getBalance() {
return balance;
}
// setterの代わりに、業務ルールを伴う操作用メソッドを公開する
public void deposit(long amount) {
if (amount <= 0) {
throw new IllegalArgumentException("入金額は正の数にしてください");
}
balance += amount;
}
}
7. 継承とポリモーフィズム
extendsで親クラスを継承し、@Overrideでメソッドを再定義します。親クラス型の変数に子クラスのインスタンスを代入し、呼び出し時に実際の型のメソッドが実行される性質をポリモーフィズム(多態性)と呼びます。
class Animal {
protected String name;
Animal(String name) { this.name = name; }
public String speak() {
return name + "が鳴きました";
}
}
class Dog extends Animal {
Dog(String name) { super(name); } // super()で親のコンストラクタを呼ぶ
@Override
public String speak() {
return name + ":ワン!";
}
}
class Cat extends Animal {
Cat(String name) { super(name); }
@Override
public String speak() {
return name + ":ニャー!";
}
}
// ポリモーフィズム:Animal型の配列に異なるサブクラスを混在させられる
Animal[] animals = { new Dog("ポチ"), new Cat("タマ") };
for (Animal a : animals) {
System.out.println(a.speak()); // 実際の型(Dog/Cat)のspeak()が呼ばれる
}
8. 抽象クラスとインターフェースの違い
どちらも「実装を子クラスに強制する」仕組みですが、性質が異なります。抽象クラス(abstract class)はフィールド(状態)やコンストラクタ、実装済みメソッドを持てますが単一継承しかできません。インターフェースは基本的に定数と抽象メソッド(+Java8以降はdefaultメソッド)のみを持ち、多重実装が可能です。
// 抽象クラス:共通の状態と一部実装を持たせつつ、一部は子クラスに強制する
abstract class Shape {
protected String color;
Shape(String color) { this.color = color; } // コンストラクタを持てる
public String describe() { // 実装済みメソッド
return color + "の" + getClass().getSimpleName();
}
public abstract double area(); // 抽象メソッド:子クラスで必ず実装する
}
class Circle extends Shape {
private double radius;
Circle(String color, double radius) {
super(color);
this.radius = radius;
}
@Override
public double area() {
return radius * radius * Math.PI;
}
}
// インターフェース:状態を持たず、複数同時に実装できる(多重実装)
interface Drawable {
void draw();
}
interface Resizable {
void resize(double factor);
}
class Box implements Drawable, Resizable { // 複数インターフェースを実装可能
public void draw() { System.out.println("四角を描画"); }
public void resize(double factor) { System.out.println(factor + "倍にリサイズ"); }
}
9. ジェネリクス
<T>のような型パラメータを使うことで、型安全性を保ちながら様々な型に対応できるクラス・メソッドを定義できます。実行時のキャスト漏れによるClassCastExceptionを、コンパイル時に検出できるようになります。
// ジェネリッククラス:型パラメータTで扱う型を抽象化する
class Box {
private T content;
public void set(T content) { this.content = content; }
public T get() { return content; }
}
Box stringBox = new Box<>();
stringBox.set("こんにちは");
String value = stringBox.get(); // キャスト不要で安全に取り出せる
// 境界型パラメータ:Comparableを実装した型のみ受け付ける
static > T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
System.out.println(max(3, 7)); // 7
System.out.println(max("abc", "xyz")); // xyz
// ワイルドカード:Numberおよびそのサブクラスのリストを受け付ける
static double sumAll(List extends Number> list) {
double total = 0;
for (Number n : list) {
total += n.doubleValue();
}
return total;
}
10. 例外処理(検査例外・非検査例外・try-with-resources)
Javaの例外は大きく2種類に分かれます。検査例外(checked exception)はExceptionを継承(RuntimeExceptionを除く)し、throws宣言やtry-catchでの処理をコンパイラが強制します(例:IOException)。非検査例外(unchecked exception)はRuntimeExceptionを継承し、処理は強制されません(例:NullPointerException、ArrayIndexOutOfBoundsException)。リソースの後始末にはtry-with-resourcesを使うとclose()忘れを防げます。
// 非検査例外(unchecked):コンパイラは処理を強制しない
try {
int result = 10 / 0; // ArithmeticExceptionはRuntimeExceptionのサブクラス
} catch (ArithmeticException e) {
System.out.println("エラー: " + e.getMessage());
} finally {
System.out.println("finallyは例外の有無に関わらず必ず実行される");
}
// 検査例外(checked):呼び出し元でtry-catchまたはthrows宣言が必須
void readFile(String path) throws java.io.IOException {
// IOExceptionはExceptionのサブクラスなのでchecked exception
java.nio.file.Files.readString(java.nio.file.Path.of(path));
}
// 独自の検査例外を定義する場合はExceptionを継承する
class InsufficientBalanceException extends Exception {
InsufficientBalanceException(String message) { super(message); }
}
// try-with-resources:AutoCloseableを実装したリソースは自動でclose()される
try (var reader = new java.io.BufferedReader(new java.io.FileReader("data.txt"))) {
System.out.println(reader.readLine());
} catch (java.io.IOException e) {
System.out.println("読み込み失敗: " + e.getMessage());
} // ここでreaderのclose()が自動的に呼ばれる(finallyでclose()を書く必要がない)
11. コレクションフレームワーク(List・Map・Set)
Javaの標準コレクションは主に3系統です。List(順序あり・重複可、ArrayList/LinkedList)、Set(重複不可、HashSet/TreeSet)、Map(キーと値のペア、HashMap/TreeMap)です。用途に応じて実装クラスを選ぶことが性能面でも重要です。
import java.util.*;
// List:順序があり重複を許容する(ArrayListはランダムアクセスが速い:平均O(1))
List fruits = new ArrayList<>();
fruits.add("apple");
fruits.add("banana");
fruits.add("apple"); // 重複OK
// Set:重複を許容しない(HashSetは順序なし、追加・検索は平均O(1))
Set uniqueFruits = new HashSet<>(fruits);
System.out.println(uniqueFruits.size()); // 2(appleの重複が除去される)
// Map:キーと値のペアを保持する(HashMapは検索が平均O(1))
Map scores = new HashMap<>();
scores.put("math", 90);
scores.put("english", 80);
System.out.println(scores.get("math")); // 90
for (Map.Entry entry : scores.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// Comparatorでソート順をカスタマイズ
List sorted = new ArrayList<>(fruits);
sorted.sort(Comparator.reverseOrder());
12. データ構造の実装:連結リストで作るスタックとキュー
基本情報技術者試験では、ライブラリを使わずにスタック(後入れ先出し:LIFO)やキュー(先入れ先出し:FIFO)を自前で実装できるかが問われます。ここでは単方向連結リストを使って両方を実装します。
// 連結リストのノード
class Node {
T value;
Node next;
Node(T value) { this.value = value; }
}
// スタック(LIFO):先頭への追加・削除だけで実現できる(push/popともにO(1))
class MyStack {
private Node top;
private int size = 0;
public void push(T value) {
Node node = new Node<>(value);
node.next = top;
top = node;
size++;
}
public T pop() {
if (top == null) throw new NoSuchElementException("スタックが空です");
T value = top.value;
top = top.next;
size--;
return value;
}
public boolean isEmpty() { return top == null; }
public int size() { return size; }
}
// キュー(FIFO):先頭で取り出し、末尾に追加する(先頭・末尾の参照を保持しO(1)を実現)
class MyQueue {
private Node head, tail;
public void enqueue(T value) {
Node node = new Node<>(value);
if (tail == null) {
head = tail = node;
} else {
tail.next = node;
tail = node;
}
}
public T dequeue() {
if (head == null) throw new NoSuchElementException("キューが空です");
T value = head.value;
head = head.next;
if (head == null) tail = null;
return value;
}
}
13. アルゴリズムと計算量(Big-O):探索と整列
探索アルゴリズムの代表が線形探索(O(n))と二分探索(O(log n)、事前にソート済みが前提)です。整列アルゴリズムでは、実装が単純なバブルソート(平均・最悪ともにO(n²))に対し、クイックソート(平均O(n log n)、最悪O(n²))は実用上高速です。計算量はデータ件数nに対して処理時間・回数がどう増えるかを表す指標で、試験でも頻出です。
// 二分探索:計算量 O(log n)(探索範囲を毎回半分に絞り込む)
// 前提:配列 arr はソート済みであること
static int binarySearch(int[] arr, int target) {
int low = 0, high = arr.length - 1;
while (low <= high) {
int mid = (low + high) / 2;
if (arr[mid] == target) {
return mid; // 発見
} else if (arr[mid] < target) {
low = mid + 1; // 右半分を探索
} else {
high = mid - 1; // 左半分を探索
}
}
return -1; // 見つからなかった
}
// クイックソート:計算量 平均O(n log n)、最悪O(n²)(分割統治法)
static void quickSort(int[] arr, int low, int high) {
if (low >= high) return;
int pivot = arr[high]; // 基準値(ピボット)
int i = low - 1;
for (int j = low; j < high; j++) {
if (arr[j] < pivot) {
i++;
int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp;
}
}
int tmp = arr[i + 1]; arr[i + 1] = arr[high]; arr[high] = tmp;
int pivotIndex = i + 1;
quickSort(arr, low, pivotIndex - 1); // 左側を再帰的にソート
quickSort(arr, pivotIndex + 1, high); // 右側を再帰的にソート
}
// 呼び出し例: quickSort(arr, 0, arr.length - 1);
14. マルチスレッド(Thread・Runnable・synchronized)
Javaは言語レベルでマルチスレッドをサポートします。Threadクラスの継承、またはRunnableインターフェースの実装で並行処理を書けます(クラス継承の制約がないためRunnable推奨)。複数スレッドが同じデータを同時に更新すると不整合(競合状態)が起きるため、synchronizedで排他制御します。
// Runnableインターフェースを実装する方法(推奨)
class Counter {
private int count = 0;
// synchronizedを付けないと、複数スレッドから同時にincrementされた際に
// カウントの取りこぼし(競合状態)が発生する可能性がある
public synchronized void increment() {
count++;
}
public int getCount() { return count; }
}
public class Main {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start(); // start()で新しいスレッドとして実行開始(run()を直接呼ぶと並行実行にならない)
t2.start();
t1.join(); // t1の終了を待つ
t2.join(); // t2の終了を待つ
System.out.println(counter.getCount()); // synchronizedがあれば正しく2000になる
}
}
15. JVMの仕組み(バイトコード・クラスローディング)とガベージコレクション
Javaのソースコード(.java)はjavacコンパイラによりバイトコード(.classファイル)に変換されます。これはOS非依存の中間コードで、実行時にJVM(Java仮想マシン)がプラットフォームごとの機械語に変換・実行します(これが「一度書けばどこでも動く」を実現する仕組みです)。クラスは実行時にクラスローダー(ブートストラップ→拡張/プラットフォーム→アプリケーションの階層構造)によって必要になったタイミングで読み込まれます。メモリ管理はガベージコレクション(GC)が自動で行い、どこからも参照されなくなったオブジェクトをヒープから回収します(世代別GCでは新しいオブジェクトを扱うYoung領域と、生存期間が長いオブジェクトを扱うOld領域に分けて効率化します)。
// コンパイルと実行の流れ(コマンドライン)
// javac Main.java → Main.class(バイトコード)が生成される
// java Main → JVMがMain.classを読み込み実行する
public class Main {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("初期値");
sb = null; // これ以降、元のStringBuilderオブジェクトはどこからも参照されなくなる
// → ガベージコレクションの回収対象になる
// System.gc()はGC実行を「示唆」するのみで、実行を保証するものではない
System.gc();
}
}
16. パッケージとビルド(Maven / Gradleの基礎)
関連するクラスをまとめる仕組みがパッケージで、ディレクトリ構成とパッケージ名を一致させる必要があります。実務では依存ライブラリの管理やビルドの自動化にMaven(pom.xmlで設定するXMLベースのビルドツール)やGradle(build.gradleで設定する、より柔軟なビルドツール)を使うのが一般的です。
// com/example/app/Main.java
package com.example.app; // パッケージ宣言(ディレクトリ構成と一致させる:com/example/app/)
import java.util.List; // 他パッケージのクラスをインポート
import static java.lang.Math.PI; // staticインポートでMath.PIをPIとして使える
public class Main {
public static void main(String[] args) {
System.out.println(PI);
}
}
<project>
<groupId>com.example</groupId>
<artifactId>my-app</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
// Gradle (build.gradle) の場合はこう書く:
// dependencies {
// testImplementation 'junit:junit:4.13.2'
// }
処理速度改善のポイント
JavaはJVM上でJIT(実行時コンパイル)により最適化されますが、書き方によって性能に大きな差が生まれます。
- 文字列連結にはStringBuilderを使う:ループ内で
+による連結を繰り返すと、その都度新しいStringオブジェクトが生成され非効率です。あらかじめ容量を指定(new StringBuilder(1000))するとさらに再確保コストを減らせます。 - 用途に合ったコレクションを選ぶ:ランダムアクセスが多いならArrayList(平均O(1))、挿入・削除が多いならLinkedList、検索が多いならHashMap(平均O(1))というように使い分けます。
- オートボクシング/アンボクシングを避ける:Integerなどのラッパークラスとint等のプリミティブ型との自動変換にはオーバーヘッドがあるため、大量の数値計算にはプリミティブ型や配列(
int[])を使いましょう。 - ループ内でのオブジェクト生成を避ける:可能であればループの外でオブジェクトを生成し再利用することで、GCの負荷(オブジェクト生成・回収のコスト)を減らせます。
- JITコンパイルの特性を理解する:JVMは起動直後はバイトコードをインタプリタ実行し、頻繁に呼ばれるメソッドを検出すると実行時にネイティブコードへコンパイル(JIT)して高速化します。ベンチマーク時は「ウォームアップ」させてから計測しましょう。
- アルゴリズムの計算量を意識する:データ件数が多い場合、O(n²)のバブルソートよりO(n log n)のクイックソート、線形探索O(n)よりソート済みデータに対する二分探索O(log n)を選ぶなど、根本的なアルゴリズム選定が最も効果的な高速化になることが多いです。
- try-with-resourcesでリソースを適切に解放する:ファイルやDB接続などを開いたままにするとメモリやハンドルを圧迫します。速やかに解放することでリソース枯渇やGC負荷増大を防げます。
- プロファイラで計測してから最適化する:推測で最適化するのではなく、VisualVMやJFR(Java Flight Recorder)などでボトルネックを特定してから手を入れましょう。