Java:クラスとオブジェクト
1学習の目的
- クラスを定義してオブジェクトを作れるようになる。バラバラだった配列を1つにまとめ、データの対応がずれる危険を根本から解消する。
- コンストラクタとメソッドを理解する。Javaがオブジェクト指向言語と呼ばれる理由が、ここで初めて実感できる。
2基礎解説
JV06・JV07では名前・価格・在庫を3つの配列で管理していました。しかしこの方法は、配列の長さがずれると破綻します。「ノートPCの価格は128000円」という関係が、コード上のどこにも書かれていないからです。
| 用語 | 意味 | 例 |
|---|---|---|
| クラス | 設計図 | class Product { … } |
| オブジェクト | 実際のモノ | new Product(…) |
| フィールド | 持つデータ | String name; |
| コンストラクタ | 作るときの処理 | Product(…) { … } |
- クラスは設計図、オブジェクトは実物。class Product が設計図で、new Product("ノートPC", 128000, 5) が実際の商品1つ。1つの設計図から何個でも作れる。
- コンストラクタはクラス名と同じ名前で、戻り値の型を書かない。new したときに自動で呼ばれ、初期値を設定する。
- this は「このオブジェクト自身」。引数と同じ名前のフィールドを区別するために this.name = name; と書く。
- データと処理を同じ場所に置ける。在庫金額の計算は、商品自身が知っているべき情報——product.stockValue() と書けるのが自然。
- toString() を定義すると出力の形を決められる。定義しないと Product@1b6d3586 のような意味不明な表示になる。
現場使用例:商品・顧客・注文・社員など、業務で扱うあらゆるデータ。実務のJavaプログラムは、ほぼすべてクラスの組み合わせでできている。
public class Main {
// ① クラス(設計図)を定義する
static class Product {
// フィールド(持つデータ)
String name;
int price;
int stock;
// コンストラクタ(作るときの処理)
Product(String name, int price, int stock) {
this.name = name; // this は「自分自身」
this.price = price;
this.stock = stock;
}
// メソッド(できること)
int stockValue() {
return price * stock; // 自分のフィールドが使える
}
String label() {
if (stock == 0) return "品切れ";
if (stock < 10) return "残りわずか";
return "在庫あり";
}
// 出力の形を決める
@Override
public String toString() {
return name + "(" + price + "円)";
}
}
public static void main(String[] args) {
// ② オブジェクトを作る
Product p = new Product("ノートPC", 128000, 5);
// ③ 使う
System.out.println(p.name); // ノートPC
System.out.println(p.stockValue()); // 640000
System.out.println(p.label()); // 残りわずか
System.out.println(p); // toString が使われる
// オブジェクトの配列(名前・価格・在庫が常にセット)
Product[] items = {
new Product("ノートPC", 128000, 5),
new Product("マウス", 3200, 42),
};
for (Product item : items) {
System.out.println(item.name + ":" + item.stockValue());
}
}
}
3基本ドリル(10問)
出力 == ノートPC
static class Product { String name; int price; Product(String name, int price) { this.name = name; ... } }
public class Main {
static class Product {
String name;
int price;
Product(String name, int price) {
this.name = name;
this.price = price;
}
}
public static void main(String[] args) {
Product p = new Product("ノートPC", 128000);
System.out.println(p.name);
}
}クラスが設計図、new で実物を作る。この Product という型は、Javaが最初から持っている int や String と同じように使える。
A. 戻り値の型を書く B. クラス名と同じ名前で戻り値の型を書かない C. main と同じ名前 D. void を書く 選択★☆☆無料
解答 == B
new したときに呼ばれる特別なメソッド。
B
コンストラクタは戻り値の型を書かない。もし void Product(...) と書くと、ただのメソッドとして扱われてしまう。
出力 == 640000
メソッドの中では、自分のフィールドをそのまま使える。
public class Main {
static class Product {
String name;
int price;
int stock;
Product(String name, int price, int stock) {
this.name = name;
this.price = price;
this.stock = stock;
}
int stockValue() {
return price * stock;
}
}
public static void main(String[] args) {
Product p = new Product("ノートPC", 128000, 5);
System.out.println(p.stockValue());
}
}メソッドの中では this を省略できる。price * stock は this.price * this.stock と同じ意味になる。
public class Main {
static class Item {
String name;
Item(String name) {
____.name = name;
}
}
public static void main(String[] args) {
System.out.println(new Item("テスト").name);
}
}出力 == テスト
「これ・自分自身」を意味する4文字。
this
引数とフィールドが同じ名前のとき、this で区別する。書かないと引数に引数を代入するだけになり、フィールドが空のままになる。
A. エラー B. クラス名と英数字が表示される C. null D. 空行 選択★☆☆無料
解答 == B
Product@1b6d3586 のような表示になる。
B
メモリ上の位置が表示されるので、人間には意味が読み取れない。toString() を定義して、読める形にするのが実務の作法。
出力 == ノートPC(128,000円)
@Override public String toString() { return ...; } と書く。
public class Main {
static class Product {
String name;
int price;
Product(String name, int price) {
this.name = name;
this.price = price;
}
@Override
public String toString() {
return name + "(" + String.format("%,d", price) + "円)";
}
}
public static void main(String[] args) {
Product p = new Product("ノートPC", 128000);
System.out.println(p);
}
}println にオブジェクトを渡すと toString() が自動で呼ばれる。デバッグや一覧表示のとき、この定義があると格段に楽になる。
public class Main {
static class Item {
int value;
Item(int value) { this.value = value; }
}
public static void main(String[] args) {
Item i = ____ Item(42);
System.out.println(i.value);
}
}出力 == 42
「新しく作る」を意味する3文字。
new
new でメモリ上に実物が作られる。クラスは設計図にすぎず、new しない限り実体は存在しない。
A. 変わらない B. 999になる C. エラー D. 0になる 選択★★☆広告解放
解答 == B
オブジェクトも配列と同じく参照(JV06)。
B
オブジェクトも参照が渡される。同じ実物を2つの名前で指しているだけなので、片方を変えればもう片方も変わる。
出力 == 774400
Product[] items = { new Product(...), new Product(...) }; で作れる。
public class Main {
static class Product {
String name;
int price;
int stock;
Product(String name, int price, int stock) {
this.name = name;
this.price = price;
this.stock = stock;
}
int stockValue() {
return price * stock;
}
}
public static void main(String[] args) {
Product[] items = {
new Product("ノートPC", 128000, 5),
new Product("マウス", 3200, 42),
};
int total = 0;
for (Product item : items) {
total += item.stockValue();
}
System.out.println(total);
}
}配列が1つで済む。JV06では3つの配列を添字で対応させていたが、今は名前・価格・在庫が常に一体で動く。
出力 == 残りわずか
早期リターンで書くと読みやすい。
public class Main {
static class Product {
String name;
int stock;
Product(String name, int stock) {
this.name = name;
this.stock = stock;
}
String label() {
if (stock == 0) return "品切れ";
if (stock < 10) return "残りわずか";
return "在庫あり";
}
}
public static void main(String[] args) {
Product p = new Product("ノートPC", 5);
System.out.println(p.label());
}
}判定ロジックを商品自身に持たせている。JV07では引数で在庫を渡していたが、商品が自分の在庫を知っているのだから、自分で判定できるほうが自然。
4実践シナリオ(5問)
出力 == 田中商事(東京)1,500pt
toString の中で String.format("%,d", points) を使う。
public class Main {
static class Customer {
String name;
String area;
int points;
Customer(String name, String area, int points) {
this.name = name;
this.area = area;
this.points = points;
}
@Override
public String toString() {
return name + "(" + area + ")"
+ String.format("%,d", points) + "pt";
}
}
public static void main(String[] args) {
Customer c = new Customer("田中商事", "東京", 1500);
System.out.println(c);
}
}表示の形をクラス自身が知っている。使う側は println するだけでよく、書式を組み立てる手間がなくなる。
出力3行
stockValue() メソッドを使う。番号は添字+1。
public class Main {
static class Product {
String name;
int price;
int stock;
Product(String name, int price, int stock) {
this.name = name;
this.price = price;
this.stock = stock;
}
int stockValue() {
return price * stock;
}
}
public static void main(String[] args) {
Product[] items = {
new Product("ノートPC", 128000, 5),
new Product("マウス", 3200, 42),
new Product("モニター", 45000, 0),
};
for (int i = 0; i < items.length; i++) {
System.out.println((i + 1) + ". " + items[i].name + ":"
+ String.format("%,d", items[i].stockValue()) + "円");
}
}
}データがずれる心配がない。JV06の3配列方式では、1つの配列に追加し忘れると破綻したが、この形なら起こりえない。
出力 == 999
Product b = a; のあと b.price を変更する。
public class Main {
static class Product {
int price;
Product(int price) { this.price = price; }
}
public static void main(String[] args) {
Product a = new Product(100);
Product b = a;
b.price = 999;
System.out.println(a.price);
}
}配列と同じく、オブジェクトも参照。「コピーしたつもりが元も変わった」という不具合の原因になるので、常に意識しておく。
出力 == ノートPC × 2 = 256,000円
Order のフィールドに Product 型を持たせる。amount() は product.price * qty。
public class Main {
static class Product {
String name;
int price;
Product(String name, int price) {
this.name = name;
this.price = price;
}
}
static class Order {
Product product;
int qty;
Order(Product product, int qty) {
this.product = product;
this.qty = qty;
}
int amount() {
return product.price * qty;
}
@Override
public String toString() {
return product.name + " × " + qty + " = "
+ String.format("%,d", amount()) + "円";
}
}
public static void main(String[] args) {
Product p = new Product("ノートPC", 128000);
Order o = new Order(p, 2);
System.out.println(o);
}
}オブジェクトの中にオブジェクトを持てる。「注文には商品が含まれる」という現実の関係が、そのままコードの構造になっている。
出力 == 927,400円
static int totalValue(Product[] items) の形。中で stockValue() を呼ぶ。
public class Main {
static class Product {
String name;
int price;
int stock;
Product(String name, int price, int stock) {
this.name = name;
this.price = price;
this.stock = stock;
}
int stockValue() {
return price * stock;
}
}
static int totalValue(Product[] items) {
int total = 0;
for (Product item : items) {
total += item.stockValue();
}
return total;
}
public static void main(String[] args) {
Product[] items = {
new Product("ノートPC", 128000, 5),
new Product("マウス", 3200, 42),
new Product("キーボード", 8500, 18),
};
System.out.println(String.format("%,d", totalValue(items)) + "円");
}
}引数が1つで済む。JV07では names と values の2つを渡していたが、Product 型なら1つの配列にすべての情報が入っている。
5仕上げ課題
Product クラス
・フィールド:name(String)、price(int)、stock(int)
・コンストラクタ:3つの値を受け取る
・int stockValue() … 在庫金額(price × stock)
・String label() … 0なら「品切れ」、10未満なら「残りわずか」、それ以外「在庫あり」
・toString() … 「ノートPC:640,000円(残りわずか)」の形式
Main クラスの static メソッド
・int totalValue(Product[] items) … 在庫総額
・Product maxItem(Product[] items) … 在庫金額が最大の商品オブジェクトを返す
データ:ノートPC/128000/5、マウス/3200/42、モニター/45000/0、キーボード/8500/18
期待される出力(6行)
1. ノートPC:640,000円(残りわずか)
2. マウス:134,400円(在庫あり)
3. モニター:0円(品切れ)
4. キーボード:153,000円(在庫あり)
---
在庫総額:927,400円(最高:ノートPC) 総合★★★広告解放
出力6行が完全一致
toString に整形を任せれば、出力は (i+1) + ". " + items[i] だけで済む。maxItem は Product 型を返すので、呼び出し側で .name を取り出せる。
public class Main {
static class Product {
String name;
int price;
int stock;
Product(String name, int price, int stock) {
this.name = name;
this.price = price;
this.stock = stock;
}
int stockValue() {
return price * stock;
}
String label() {
if (stock == 0) return "品切れ";
if (stock < 10) return "残りわずか";
return "在庫あり";
}
@Override
public String toString() {
return name + ":" + String.format("%,d", stockValue())
+ "円(" + label() + ")";
}
}
static int totalValue(Product[] items) {
int total = 0;
for (Product item : items) {
total += item.stockValue();
}
return total;
}
static Product maxItem(Product[] items) {
Product max = items[0];
for (Product item : items) {
if (item.stockValue() > max.stockValue()) {
max = item;
}
}
return max;
}
public static void main(String[] args) {
Product[] items = {
new Product("ノートPC", 128000, 5),
new Product("マウス", 3200, 42),
new Product("モニター", 45000, 0),
new Product("キーボード", 8500, 18),
};
for (int i = 0; i < items.length; i++) {
System.out.println((i + 1) + ". " + items[i]);
}
System.out.println("---");
System.out.println("在庫総額:" + String.format("%,d", totalValue(items))
+ "円(最高:" + maxItem(items).name + ")");
}
}JV06からJV08まで、同じ在庫レポートを3回書いてきた。その変化を振り返ってほしい。
JV06では3つの配列を添字で対応させ、main にすべての処理が詰まっていた。JV07でメソッドに分けて main は短くなったが、maxName(names, values) のように関連するデータを別々に渡す不自然さが残っていた。そしてJV08で、ようやく「商品」という単位でデータと処理がまとまった。
最も大きな変化は maxItem の戻り値だ。JV07では名前という文字列しか返せなかったが、今は商品オブジェクトそのものを返せる。だから呼び出し側で .name でも .price でも、必要な情報を後から取り出せる。「一番売れた商品の在庫数も知りたい」と言われても、メソッドを直す必要がない。
出力部分も (i + 1) + ". " + items[i] だけになった。toString() が整形を引き受けているので、使う側は「何を表示するか」だけ考えればよい。
これがオブジェクト指向の出発点だ。データと、そのデータに対する処理を同じ場所に置く——たったそれだけの原則が、プログラムの構造を大きく変える。
ただし今の Product には弱点がある。フィールドが外から自由に書き換えられるので、p.stock = -100; のような不正な値も入れられてしまう。次章のカプセル化で、この穴を塞ぐ。