- JAVA
Java getter/setterの書き方|カプセル化とprivate
getter/setterとは、privateにしたフィールドの値を、外から読み取る(getter)・書き換える(setter)ためのpublicメソッドです。 フィールドを private で隠し、決まったメソッドだけを入り口にする考え方をカプセル化と呼びます。入り口を1か所にすることで、値のチェックを入れたり、後から中身の持ち方を変えたりしやすくなります。
この記事では、privateフィールドに直接アクセスしてコンパイルエラーになるところから始め、getter/setterで解決するまでを実行結果つきで追います。getXxx / setXxx / isXxx の命名規則の早見表、setterでの値チェック、配列やListを返すときの防御的コピーまで、コピーしてそのまま動く形で載せました。Eclipse・IntelliJ IDEA・VS Codeでの自動生成の手順と、Lombok・record(Java 16以降)で書いた場合の比較表も付けています。実行環境は OpenJDK 21.0.10 です。
getter/setterの早見表
基本の形はこの表のとおりです。フィールド名の先頭を大文字にして、前に get / set / is を付けます。
| 種類 | 役割 | 書き方(フィールドが private int age; の場合) |
命名 |
|---|---|---|---|
| getter | 値を返す | public int getAge() { return age; } |
get + 先頭大文字のフィールド名 |
| setter | 値を受け取って入れる | public void setAge(int age) { this.age = age; } |
set + 先頭大文字のフィールド名 |
| booleanのgetter | true/falseを返す | public boolean isDone() { return done; } |
is + 先頭大文字のフィールド名 |
| フィールド | getter | setter |
|---|---|---|
private String name; |
getName() |
setName(String name) |
private int price; |
getPrice() |
setPrice(int price) |
private boolean done; |
isDone() |
setDone(boolean done) |
private Boolean archived; |
getArchived() |
setArchived(Boolean archived) |
private List<String> tags; |
getTags() |
setTags(List<String> tags) |
private final String id; |
getId() |
なし(変更させない) |
| 書き方 | getterの名前 | 行数の目安 | 使えるバージョン |
|---|---|---|---|
| 手書き・IDEの自動生成 | getName() |
フィールド1つにつき6行前後 | すべて |
Lombok(@Getter @Setter) |
getName() |
アノテーション1〜2行 | Lombokの導入が必要 |
| record | name() |
宣言の1行 | Java 16以降(setterなし) |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java getter/setterの書き方|カプセル化とprivate」
privateフィールドに直接アクセスするとどうなるか
コンパイルエラーを再現する
まず、フィールドを private にして、別のクラスから直接さわってみます。以下のサンプルは1つのファイルに複数のクラスを書いています。javac Direct.java でコンパイルし、java Direct で実行してください。
class Member {
private String name;
private int age;
}
public class Direct {
public static void main(String[] args) {
Member m = new Member();
m.name = "佐藤";
m.age = 30;
System.out.println(m.name + " " + m.age);
}
}
実行結果(OpenJDK 21.0.10、コンパイルエラー):
Direct.java:8: error: name has private access in Member
m.name = "佐藤";
^
Direct.java:9: error: age has private access in Member
m.age = 30;
^
Direct.java:10: error: name has private access in Member
System.out.println(m.name + " " + m.age);
^
Direct.java:10: error: age has private access in Member
System.out.println(m.name + " " + m.age);
^
4 errors
name has private access in Member は「nameはMemberクラスの中でprivateとして宣言されています」という意味です。private を付けたフィールドは、そのクラスの中からしか読み書きできません。 代入(8〜9行目)だけでなく、読み取り(10行目)もエラーになっています。
publicにすれば動くが、どんな値でも入る
エラーを消すいちばん手早い方法は public に変えることです。
class Member {
public String name;
public int age;
}
public class PublicField {
public static void main(String[] args) {
Member m = new Member();
m.name = "佐藤";
m.age = -5; // ありえない値もそのまま入る
System.out.println(m.name + " " + m.age + "歳");
}
}
実行結果:
佐藤 -5歳
動きはしますが、年齢に-5が入ってもそのまま通ってしまいます。 フィールドがpublicだと、どのクラスのどの行からでも自由に書き換えられるため、「値がおかしい」と気づいたときに原因の行を探す範囲が広くなります。
getter/setterで解決する
フィールドは private のままにして、読み書き用のpublicメソッドを用意します。
class Member {
private String name; // 外から直接さわれない
private int age;
// getter: 値を返す
public String getName() {
return name;
}
// setter: 値を受け取ってフィールドに入れる
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
public class Basic {
public static void main(String[] args) {
Member m = new Member();
m.setName("佐藤");
m.setAge(30);
System.out.println(m.getName() + " " + m.getAge() + "歳");
}
}
実行結果:
佐藤 30歳
m.name = "佐藤" が m.setName("佐藤") に、m.name が m.getName() に変わりました。 この時点ではpublicフィールドと動きは同じですが、値の出入り口がメソッドになったことで、後からチェックや変換を足せるようになります(次の章以降で扱います)。
getter/setterの書き方を1行ずつ確認する
書く手順
- フィールドに
privateを付ける - getterを書く:
public 型 getフィールド名() { return フィールド名; } - setterを書く:
public void setフィールド名(型 引数) { this.フィールド名 = 引数; }
| 部品 | getter | setter |
|---|---|---|
| アクセス修飾子 | public |
public |
| 戻り値の型 | フィールドと同じ型 | void(何も返さない) |
| メソッド名 | get + 先頭大文字のフィールド名 |
set + 先頭大文字のフィールド名 |
| 引数 | なし | フィールドと同じ型を1つ |
| 中身 | return フィールド名; |
this.フィールド名 = 引数; |
this.を付け忘れると値が入らない
setterでつまずきやすいのが this. の付け忘れです。
class Member {
private String name;
public String getName() { return name; }
public void setName(String name) {
name = name; // this. を付け忘れた
}
}
public class NoThis {
public static void main(String[] args) {
Member m = new Member();
m.setName("佐藤");
System.out.println("name = " + m.getName());
}
}
実行結果:
name = null
setName("佐藤") を呼んだのに、getName() はnullを返しました。引数とフィールドが同じ名前のとき、メソッドの中で単に name と書くと引数のほうを指します。name = name; は「引数を引数自身に代入する」だけで、フィールドは変わりません。コンパイルエラーにならない(javac -Xlint:all でも警告が出ないことを確認しました)ため、setterでは必ず this.name = name; と書く、と覚えておくのが確実です。IDEの自動生成を使えば、この書き間違いは起きません。
命名規則の早見表(getXxx / setXxx / isXxx)
基本のルール
getter/setterの名前には、JavaBeansと呼ばれる慣習のルールがあります。多くのライブラリやフレームワークは、この名前を手がかりに「このクラスにはどんな項目(プロパティ)があるか」を判断します。
| フィールド | getter | setter | プロパティ名 |
|---|---|---|---|
String name |
getName() |
setName(String) |
name |
int itemCount |
getItemCount() |
setItemCount(int) |
itemCount |
boolean done(プリミティブ) |
isDone() |
setDone(boolean) |
done |
boolean inStock |
isInStock() |
setInStock(boolean) |
inStock |
Boolean archived(ラッパー型) |
getArchived() |
setArchived(Boolean) |
archived |
String url |
getUrl() |
setUrl(String) |
url |
get/set/isの後ろは、フィールド名の先頭1文字を大文字にする(camelCase)isを使うのはプリミティブ型のbooleanのgetterだけ。ラッパー型のBooleanはgetを使う- booleanのフィールド名に
isを付けてisDoneとすると、getterがisIsDone()かisDone()かで揺れやすい。フィールド名はdoneのようにしておくと迷わない
booleanのgetterを書く
class Task {
private boolean done; // boolean(プリミティブ型)
private Boolean archived; // Boolean(ラッパー型)
private boolean hasDeadline;
public boolean isDone() { return done; } // boolean は isXxx
public void setDone(boolean done) { this.done = done; }
public Boolean getArchived() { return archived; } // Boolean は getXxx
public void setArchived(Boolean archived) { this.archived = archived; }
public boolean hasDeadline() { return hasDeadline; } // 読みやすさ重視の名前(規約の外)
public void setHasDeadline(boolean v) { this.hasDeadline = v; }
}
public class Bool {
public static void main(String[] args) {
Task t = new Task();
t.setDone(true);
System.out.println("isDone() = " + t.isDone());
System.out.println("getArchived() = " + t.getArchived());
System.out.println("hasDeadline() = " + t.hasDeadline());
}
}
実行結果:
isDone() = true
getArchived() = null
hasDeadline() = false
boolean の初期値はfalse、Boolean の初期値はnullです。「未設定」を表したいときは Boolean、true/falseの2つだけでよいときは boolean を選びます。
命名がプロパティとして認識されるかを確かめる
命名規則どおりに書くとどう認識されるのかを、Java標準の java.beans.Introspector で確かめます。このクラスは、getter/setterの名前からプロパティの一覧を取り出します。
import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
class Item {
private String name;
private boolean inStock;
private Boolean reserved;
private String url;
private int xCount;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public boolean isInStock() { return inStock; }
public void setInStock(boolean inStock) { this.inStock = inStock; }
public Boolean isReserved() { return reserved; } // Boolean に is を使った場合
public void setReserved(Boolean reserved) { this.reserved = reserved; }
public String getURL() { return url; }
public void setURL(String url) { this.url = url; }
public int getXCount() { return xCount; }
public void setXCount(int xCount) { this.xCount = xCount; }
public boolean hasStock() { return inStock; } // has は規約の外
public int getTotal() { return 100; } // フィールドがなくても getter があればプロパティ
}
public class Props {
public static void main(String[] args) throws Exception {
BeanInfo info = Introspector.getBeanInfo(Item.class, Object.class);
System.out.printf("%-10s %-14s %-14s%n", "プロパティ", "getter", "setter");
for (PropertyDescriptor pd : info.getPropertyDescriptors()) {
String r = pd.getReadMethod() == null ? "-" : pd.getReadMethod().getName();
String w = pd.getWriteMethod() == null ? "-" : pd.getWriteMethod().getName();
System.out.printf("%-10s %-14s %-14s%n", pd.getName(), r, w);
}
}
}
実行結果(OpenJDK 21.0.10):
プロパティ getter setter
URL getURL setURL
XCount getXCount setXCount
inStock isInStock setInStock
name getName setName
reserved - setReserved
total getTotal -
この結果から、次のことがわかります。
| 書いたもの | 認識された結果 | わかること |
|---|---|---|
getName() / setName() |
プロパティ name、読み書き可 |
規則どおりの基本形 |
boolean isInStock() |
プロパティ inStock、読み書き可 |
boolean の is は認識される |
Boolean isReserved() |
reserved のgetterは「-」 |
ラッパー型 Boolean の is はgetterとして認識されない |
getURL() |
プロパティ名が URL |
大文字が2つ続くと、先頭が小文字にならない |
getXCount() |
プロパティ名が XCount |
xCount のような「1文字目だけ小文字」の名前は揺れやすい |
hasStock() |
一覧に出てこない | has や can は規約の対象外 |
getTotal()(フィールドなし) |
プロパティ total、読み取り専用 |
プロパティはフィールドではなくメソッド名で決まる |
フィールド名は url、itemCount のように「小文字で始まり、2文字目も小文字」の形にしておくと、どのツールでも同じ名前で認識されます。JSONへの変換や画面との値の受け渡しを行うライブラリの多くもgetter/setterの名前を手がかりにするため、命名をそろえておくことが動作の安定につながります(各ライブラリでの認識結果は本記事では未実行です)。
setterで値をチェックする
不正な値を受け付けないsetter
getter/setterの大きな利点は、値が入る直前にチェックを入れられることです。条件を満たさない値は IllegalArgumentException で知らせます。
class Product {
private String name;
private int price;
public String getName() { return name; }
public void setName(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("name は空にできません");
}
this.name = name.trim();
}
public int getPrice() { return price; }
public void setPrice(int price) {
if (price < 0) {
throw new IllegalArgumentException("price は0以上で指定してください: " + price);
}
this.price = price;
}
}
public class Check {
public static void main(String[] args) {
Product p = new Product();
p.setName(" ノート ");
p.setPrice(300);
System.out.println("[" + p.getName() + "] " + p.getPrice() + "円");
try {
p.setPrice(-100);
} catch (IllegalArgumentException e) {
System.out.println("例外: " + e.getMessage());
}
try {
p.setName(" ");
} catch (IllegalArgumentException e) {
System.out.println("例外: " + e.getMessage());
}
System.out.println("変更後も元の値のまま: [" + p.getName() + "] " + p.getPrice() + "円");
}
}
実行結果:
[ノート] 300円
例外: price は0以上で指定してください: -100
例外: name は空にできません
変更後も元の値のまま: [ノート] 300円
ポイントは3つです。
- チェックを先に行い、通った値だけをフィールドに代入する。 例外を投げた場合、フィールドは元の値のまま保たれます。
- 例外のメッセージに、受け取った値を含める。 「-100が渡された」とわかれば原因を追いやすくなります。
trim()のような値の整形もsetterの中にまとめられる。前後の空白を取り除いた "ノート" が保存されています。
コンストラクタでも同じチェックを使う
setterにチェックを入れても、コンストラクタで直接フィールドに代入すると、そこだけチェックを通りません。コンストラクタからsetterを呼ぶと、チェックを1か所にまとめられます。
class Member {
private final String id; // 作成後に変えない値: final + getter のみ
private String name; // 後から変える値: getter + setter
public Member(String id, String name) {
this.id = id;
setName(name); // チェックを setter に寄せる
}
public String getId() { return id; }
public String getName() { return name; }
public final void setName(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("name は空にできません");
}
this.name = name;
}
}
public class Ctor {
public static void main(String[] args) {
Member m = new Member("M001", "佐藤");
m.setName("鈴木");
System.out.println(m.getId() + " " + m.getName());
try {
new Member("M002", "");
} catch (IllegalArgumentException e) {
System.out.println("例外: " + e.getMessage());
}
}
}
実行結果:
M001 鈴木
例外: name は空にできません
setName に final を付けているのは、コンストラクタから呼ぶメソッドを子クラスでオーバーライドされないようにするためです。id のように作成後に変えない値は、final を付けてgetterだけを用意します。
setterを用意しないという選択
すべてのフィールドにsetterを付ける必要はありません。「何をしたいか」を表すメソッドだけを公開すると、クラスの使い方がはっきりします。
class Counter {
private int count;
public int getCount() { return count; }
// setCount は用意せず、意味のある操作だけを公開する
public void increment() { count++; }
public void reset() { count = 0; }
}
public class Count {
public static void main(String[] args) {
Counter c = new Counter();
c.increment();
c.increment();
System.out.println("count = " + c.getCount());
c.reset();
System.out.println("count = " + c.getCount());
}
}
実行結果:
count = 2
count = 0
setCount(999) のような書き換えはできず、「1増やす」「0に戻す」だけが許されます。
| フィールドの性質 | 用意するもの |
|---|---|
| 作成後に変えない(ID、作成日時など) | final + getterのみ |
| 自由に変えてよい | getter + setter |
| 条件つきで変えてよい | getter + チェック入りのsetter |
| 決まった操作でだけ変わる(カウンター、状態など) | getter + 操作を表すメソッド(increment() など) |
| クラスの内部でだけ使う | getterもsetterも用意しない |
配列やListを返すときの防御的コピー
getterから中身が書き換えられる例
フィールドをprivateにしてsetterを用意しなくても、配列やListをそのまま返すgetterがあると、外から中身を書き換えられます。
import java.util.ArrayList;
import java.util.List;
class Team {
private int[] scores = {80, 90};
private List<String> members = new ArrayList<>(List.of("佐藤", "鈴木"));
public int[] getScores() { return scores; } // 中身そのものを返している
public List<String> getMembers() { return members; }
}
public class Leak {
public static void main(String[] args) {
Team t = new Team();
t.getScores()[0] = -1; // setter を通らずに書き換わる
t.getMembers().add("部外者");
t.getMembers().clear();
System.out.println("scores[0] = " + t.getScores()[0]);
System.out.println("members = " + t.getMembers());
}
}
実行結果:
scores[0] = -1
members = []
setterを1つも書いていないのに、点数が-1になり、メンバーが空になりました。getterが返しているのは配列やListの「コピー」ではなく同じものへの参照なので、受け取った側の変更がそのままフィールドに届きます。
コピーを返す・コピーして受け取る
防御的コピーとは、受け取るときと返すときにコピーを作り、クラスの中のデータと外のデータを切り離すことです。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class Team {
private int[] scores;
private final List<String> members = new ArrayList<>();
// 受け取るとき: コピーして持つ
public void setScores(int[] scores) {
this.scores = scores.clone();
}
// 返すとき: コピーを返す
public int[] getScores() {
return scores.clone();
}
// List は変更できないコピーを返す
public List<String> getMembers() {
return List.copyOf(members);
}
// 変更は専用のメソッドだけで受け付ける
public void addMember(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("name は空にできません");
}
members.add(name);
}
}
public class Defensive {
public static void main(String[] args) {
Team t = new Team();
int[] src = {80, 90};
t.setScores(src);
t.addMember("佐藤");
t.addMember("鈴木");
src[0] = -1; // 渡した元の配列を書き換えても
t.getScores()[1] = -1; // 受け取った配列を書き換えても
System.out.println("scores = " + Arrays.toString(t.getScores()));
try {
t.getMembers().add("部外者");
} catch (UnsupportedOperationException e) {
System.out.println("add は UnsupportedOperationException");
}
System.out.println("members = " + t.getMembers());
}
}
実行結果:
scores = [80, 90]
add は UnsupportedOperationException
members = [佐藤, 鈴木]
渡した配列を後から書き換えても、受け取った配列を書き換えても、Teamの中の値は [80, 90] のままです。Listは List.copyOf で変更できないコピーを返しているので、add を呼ぶと例外になります。
コピーの方法早見表
| 型 | 返すとき(getter) | 受け取るとき(setter・コンストラクタ) |
|---|---|---|
| 配列 | return arr.clone(); |
this.arr = arr.clone(); |
| List | return List.copyOf(list); |
this.list = new ArrayList<>(list); または List.copyOf(list) |
| Set | return Set.copyOf(set); |
this.set = new HashSet<>(set); |
| Map | return Map.copyOf(map); |
this.map = new HashMap<>(map); |
| String・Integer・LocalDateなど | そのまま返してよい | そのまま受け取ってよい |
| int・booleanなどのプリミティブ型 | そのまま返してよい | そのまま受け取ってよい |
StringやLocalDateは一度作ると中身を変えられない(不変の)型なので、コピーは不要です。List.copyOf などはJava 10以降で使えます。要素にnullが含まれるとNullPointerExceptionになる仕様です(一般に知られた仕様で、nullを含むケースは本記事では未実行です)。
読み取り専用ビューとコピーの違い
Collections.unmodifiableList も「変更できないList」を返しますが、List.copyOf とは動きが違います。
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class ViewVsCopy {
public static void main(String[] args) {
List<String> inner = new ArrayList<>(List.of("A"));
List<String> view = Collections.unmodifiableList(inner); // 読み取り専用のビュー
List<String> copy = List.copyOf(inner); // その時点のコピー
inner.add("B");
System.out.println("view = " + view);
System.out.println("copy = " + copy);
}
}
実行結果:
view = [A, B]
copy = [A]
| 方法 | 外からの変更 | 元のListが変わったとき | 向いている場面 |
|---|---|---|---|
Collections.unmodifiableList(list) |
できない | 変更が見える(元と連動) | 常に最新の内容を見せたいとき。コピーのコストがかからない |
List.copyOf(list) |
できない | 変わらない(その時点の写し) | 取得した時点の内容を固定して渡したいとき |
new ArrayList<>(list) |
できる(コピー側だけ変わる) | 変わらない | 受け取った側で自由に加工してよいとき |
なお、ここで紹介したコピーはすべて浅いコピーです。Listの要素が書き換え可能なオブジェクト(たとえばsetterを持つクラス)の場合、要素そのものは共有されます。要素も守りたいときは、要素を不変のクラスやrecordにしておくのが簡潔です。
setterを持たない不変クラスという選択
すべてのフィールドを final にしてsetterを用意しないクラスを、不変(イミュータブル)クラスと呼びます。作った後に値が変わらないので、どこに渡しても安心して使えます。
final class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
// 値を変えたいときは、新しいインスタンスを返す
public Point withX(int x) {
return new Point(x, this.y);
}
}
public class Immutable {
public static void main(String[] args) {
Point p1 = new Point(1, 2);
Point p2 = p1.withX(10);
System.out.println("p1 = (" + p1.getX() + ", " + p1.getY() + ")");
System.out.println("p2 = (" + p2.getX() + ", " + p2.getY() + ")");
}
}
実行結果:
p1 = (1, 2)
p2 = (10, 2)
withX(10) を呼んでも p1 は変わらず、新しい p2 が作られています。この形をもっと短く書けるのが、次に紹介するrecordです。
IDEでgetter/setterを自動生成する手順
getter/setterは形が決まっているので、IDEに生成させると this. の付け忘れや命名のゆれが起きません。以下は各IDEで一般に知られている操作手順です(本記事の検証環境にはIDEがないため、操作は未実行です。メニューの名称はバージョンや日本語化の有無で少し異なる場合があります)。
| IDE | 手順 | ショートカット |
|---|---|---|
| Eclipse | フィールドを書いたクラスを開く → メニュー「ソース」→「getter および setter の生成」→ 対象のフィールドにチェック → 生成 | Alt + Shift + S を押してから R |
| IntelliJ IDEA | クラスの中にカーソルを置く → メニュー「Code」→「Generate」→「Getter and Setter」→ フィールドを選択 → OK | Alt + Insert(macOSは Command + N) |
| VS Code | 拡張機能「Extension Pack for Java」を入れる → クラスの中で右クリック →「ソース アクション」→「Generate Getters and Setters」→ フィールドを選択 | 右クリックメニュー、またはクイックフィックス(Ctrl + .) |
自動生成を使うときのコツです。
- 先にフィールドをすべて書いてから生成すると、1回の操作で済みます。
- すべてのフィールドにsetterを付けるのではなく、変更してよいフィールドだけにチェックを入れます(getterだけを生成するメニューもあります)。
- 生成されるsetterは代入だけの形です。値のチェックや防御的コピーは、生成後に自分で書き足します。
Lombok・recordで書いた場合との比較
Lombokで書く
Lombokは、アノテーションを付けるとコンパイル時にgetter/setterなどを生成してくれるライブラリです。
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
class Product {
private String name;
private int price;
private boolean inStock;
}
public class Lom {
public static void main(String[] args) {
Product p = new Product();
p.setName("ノート");
p.setPrice(300);
p.setInStock(true);
System.out.println(p.getName() + " " + p.getPrice() + "円 在庫=" + p.isInStock());
System.out.println(p);
}
}
実行結果(OpenJDK 21.0.10 + Lombok 1.18.24。javac -cp lombok.jar Lom.java でコンパイル):
ノート 300円 在庫=true
Product(name=ノート, price=300, inStock=true)
ソースにはgetter/setterを1つも書いていませんが、getName() や setPrice() が使えています。boolean のフィールドには、命名規則どおり isInStock() が生成されました。
今回動かしたのはUbuntuのパッケージに含まれるLombok 1.18.24です。これより新しい版での動作は未実行なので、導入時は使用するJDKに対応したLombokの版を配布元で確認してください。IDEで使うには、IDE側のLombok対応(プラグインや設定)も必要になります。
recordで書く(Java 16以降)
recordは、値を持つだけの不変なクラスを1行で宣言できる機能です。
import java.util.List;
record Product(String name, int price, List<String> tags) {
// コンパクトコンストラクタ: 値のチェックと防御的コピー
Product {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("name は空にできません");
}
if (price < 0) {
throw new IllegalArgumentException("price は0以上で指定してください: " + price);
}
tags = List.copyOf(tags);
}
}
public class Rec {
public static void main(String[] args) {
Product p = new Product("ノート", 300, List.of("文具", "新着"));
System.out.println(p.name() + " " + p.price() + "円 " + p.tags()); // getName() ではなく name()
System.out.println(p); // toString も自動
System.out.println(p.equals(new Product("ノート", 300, List.of("文具", "新着"))));
try {
new Product("ノート", -1, List.of());
} catch (IllegalArgumentException e) {
System.out.println("例外: " + e.getMessage());
}
}
}
実行結果(OpenJDK 21.0.10):
ノート 300円 [文具, 新着]
Product[name=ノート, price=300, tags=[文具, 新着]]
true
例外: price は0以上で指定してください: -1
recordの読み取りメソッドは getName() ではなく name() です。privateでfinalなフィールド、コンストラクタ、toString()、equals()、hashCode() も自動で用意されます。値のチェックは、引数リストを書かない「コンパクトコンストラクタ」に書きます。
recordにはsetterがありません。書き換えようとするとコンパイルエラーになります。
record Product(String name, int price) { }
public class RecSet {
public static void main(String[] args) {
Product p = new Product("ノート", 300);
p.price = 500;
p.setPrice(500);
}
}
実行結果(コンパイルエラー):
RecSet.java:5: error: price has private access in Product
p.price = 500;
^
RecSet.java:6: error: cannot find symbol
p.setPrice(500);
^
symbol: method setPrice(int)
location: variable p of type Product
2 errors
3つの書き方の比較表
| 比較項目 | 手書き・IDE自動生成 | Lombok | record |
|---|---|---|---|
| 読み取りメソッドの名前 | getName() / isDone() |
getName() / isDone() |
name() / done() |
| setter | 書ける | @Setter で生成 |
なし(不変) |
| ソースの量 | 多い | 少ない | もっとも少ない |
| 値のチェック | setterやコンストラクタに自由に書ける | 必要なメソッドだけ手書きで足す | コンパクトコンストラクタに書く |
toString / equals / hashCode |
自分で書く、またはIDEで生成 | @ToString @EqualsAndHashCode @Data で生成 |
自動 |
| 追加の準備 | 不要 | ライブラリの追加とIDEの対応 | 不要(Java 16以降) |
| 継承 | できる | できる | ほかのクラスを継承できない(インターフェースの実装は可) |
| 今回の確認 | OpenJDK 21.0.10で実行 | Lombok 1.18.24で実行 | OpenJDK 21.0.10で実行 |
どれを選ぶかの判断表
| 状況 | おすすめ |
|---|---|
| Javaを学び始めたところ | まず手書きで仕組みを理解し、慣れたらIDEの自動生成 |
| 値を運ぶだけで、作った後に変えない(検索結果の1行、APIの応答など) | record |
| 作った後に値を変える必要がある | 手書き・IDE生成、またはLombok |
| フィールドが多く、getter/setterでソースが長くなっている | Lombok(チームで導入を決めている場合)またはrecord |
| setterに細かいチェックや整形を入れたい | 手書き(必要なものだけLombokに任せる併用も可) |
フレームワークやライブラリが getXxx / setXxx の形を前提にしている |
手書き・IDE生成・Lombok(recordに対応しているかはライブラリごとに確認) |
よくあるつまずきと直し方
| 症状 | 原因 | 直し方 |
|---|---|---|
name has private access in Member |
privateフィールドに外から直接アクセスした | getName() / setName() を経由する |
| setterを呼んでも値がnullや0のまま | name = name; と書いて this. が抜けている |
this.name = name; にする |
| ライブラリが項目を認識しない | Boolean に is を使った、has で始めた、先頭の大文字・小文字が規則と違う |
getXxx / setXxx、boolean だけ isXxx にそろえる |
| setterがないのに中身が変わる | 配列やListをそのまま返している | clone() や List.copyOf でコピーを返す |
| チェックを入れたのに不正な値が入る | コンストラクタで直接代入している | コンストラクタからsetterを呼ぶ、または同じチェックを通す |
cannot find symbol ... setPrice |
recordにsetterを期待した | recordは不変。新しいインスタンスを作る |
recordで getName() が見つからない |
recordの読み取りメソッドは name() |
p.name() と書く |
getter/setterを書くときのチェックリスト
| 確認すること | チェック |
|---|---|
フィールドに private を付けた |
□ |
getterは get + 先頭大文字、boolean だけ is にした |
□ |
setterの中は this.フィールド = 引数; になっている |
□ |
変更させたくないフィールドは final にして、setterを作っていない |
□ |
| setterで値の範囲やnull・空文字をチェックした | □ |
| コンストラクタでも同じチェックが通る | □ |
| 配列・List・Mapを返すgetterはコピーを返している | □ |
| 値を運ぶだけのクラスはrecordにできないか検討した | □ |
よくある質問
Q. getter/setterを付けるだけなら、publicフィールドと同じではありませんか? A. 代入と取り出しだけのgetter/setterは、動きとしてはpublicフィールドとほぼ同じです。違いは「後から変えられるかどうか」にあります。メソッドにしておけば、呼び出し側のコードを変えずに、値のチェック、整形、ログ出力、内部の持ち方の変更を足せます。あわせて、すべてのフィールドにsetterを付けず、必要なものだけ公開することがカプセル化のポイントです。
Q. booleanのgetterは getDone() と書いてもよいですか?
A. プリミティブ型の boolean は isDone() が慣習です。本記事の確認では、boolean isInStock() はプロパティとして認識され、ラッパー型の Boolean isReserved() はgetterとして認識されませんでした。boolean は is、Boolean は get と覚えておくと、ツールやライブラリとの相性がよくなります。
Q. カプセル化とgetter/setterは同じ意味ですか?
A. 同じではありません。カプセル化は「データと、そのデータを扱う処理を1つのクラスにまとめ、外からは決められた入り口だけを使わせる」という考え方です。getter/setterはその入り口の1つの形です。increment() のように操作を表すメソッドだけを公開するのも、setterを持たない不変クラスにするのも、カプセル化の手段です。
Q. recordがあれば、getter/setterはもう書かなくてよいですか?
A. 値を運ぶだけで、作った後に変えないデータであればrecordが簡潔です。作った後に値を書き換える必要があるクラスや、getXxx / setXxx の形を前提にしたライブラリと組み合わせるクラスでは、これまでどおりgetter/setterを使います。用途に合わせて選び分けるのがおすすめです。
まとめ
- getterは値を返すメソッド、setterは値を受け取ってフィールドに入れるメソッド。フィールドを
privateにして、入り口をメソッドに絞るのがカプセル化 - privateフィールドに外から直接アクセスすると
has private accessのコンパイルエラーになる。getter/setterを経由して解決する - 命名は
getXxx/setXxx、プリミティブのbooleanだけisXxx。規則どおりに書くとツールやライブラリが正しく認識する - setterの中は
this.フィールド = 引数;。this.が抜けると値が入らず、エラーも警告も出ない - setterに値のチェックを入れ、通った値だけを代入する。変更させたくないフィールドは
finalにしてsetterを作らない - 配列やListを返すgetterは
clone()やList.copyOfでコピーを返す。そのまま返すと外から中身を書き換えられる - 形が決まった部分はIDEの自動生成・Lombok・recordに任せられる。recordは
name()形式で、setterを持たない
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java getter/setterの書き方|カプセル化とprivate」
株式会社RJCでは、Javaのgetter/setterやカプセル化の考え方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。