객체를 만드는 방법, 상태를 두는 위치(static vs 인스턴스), 객체끼리 협력하는 모양, null과 공유 참조를 다루는 법을 조금씩 바꿔 가며 연습합니다.
예제 1은 생성자를 여러 개 두고 this(...)로 연결했습니다. 생성자를 하나만 private으로 두고 of, withoutEmail, guest 같은 이름 있는 static 메서드로 객체를 만들면 "무엇을 만드는지"가 호출부에 드러납니다. 생성자가 셋 이상 생기거나 같은 매개변수 타입 조합이 겹칠 때 이 방식을 택합니다.
public class PersonFactory {
static class Person {
String name;
int age;
String email;
private Person(String name, int age, String email) { // 생성자는 하나만
this.name = name;
this.age = age;
this.email = email;
}
static Person of(String name, int age, String email) { return new Person(name, age, email); }
static Person withoutEmail(String name, int age) { return new Person(name, age, "unknown@example.com"); }
static Person guest() { return new Person("손님", 0, "guest@example.com"); }
@Override
public String toString() { return name + "(" + age + ", " + email + ")"; }
}
public static void main(String[] args) {
System.out.println(Person.of("김민수", 18, "kim@shop.com")); // 출력: 김민수(18, kim@shop.com)
System.out.println(Person.withoutEmail("이서연", 30)); // 출력: 이서연(30, unknown@example.com)
System.out.println(Person.guest()); // 출력: 손님(0, guest@example.com)
// 다른 클래스에서 new Person(...) → 컴파일 오류: private
}
}상태가 필요 없는 계산은 static 메서드로 두고 값을 매번 넘깁니다. 같은 비율을 여러 번 쓴다면 비율을 필드로 기억하는 객체가 편합니다. 둘 중 하나를 고르는 기준은 "이 값이 호출마다 바뀌는가, 한 번 정해지면 고정인가"입니다.
public class DiscountCalc {
// 방식 A: 상태 없음 → static. 비율을 매번 넘긴다
static int applyRate(int price, int percent) {
return price - price * percent / 100;
}
// 방식 B: 비율을 객체가 기억 → 같은 정책을 여러 상품에 적용
static class Discount {
final int percent;
Discount(int percent) { this.percent = percent; }
int apply(int price) { return applyRate(price, percent); } // A를 재사용
}
public static void main(String[] args) {
System.out.println(applyRate(10_000, 15)); // 출력: 8500
System.out.println(applyRate(10_000, 0)); // 출력: 10000
Discount vip = new Discount(30);
int[] cart = {12_000, 5_500, 30_000};
int total = 0;
for (int p : cart) total += vip.apply(p);
System.out.println("VIP 합계: " + total); // 출력: VIP 합계: 33250
}
}예제 5의 재고 관리처럼, 주문(Order)이 항목(OrderItem) 배열을 갖고 합계 계산을 항목에게 맡깁니다. 주문 번호는 static 카운터로 발급하고, 각 객체는 자기 데이터만 책임집니다.
public class OrderDemo {
static class OrderItem {
String product;
int price;
int qty;
OrderItem(String product, int price, int qty) {
this.product = product;
this.price = price;
this.qty = qty;
}
int subtotal() { return price * qty; }
}
static class Order {
static int nextId = 1; // 모든 주문이 공유
final int id;
final String customer;
OrderItem[] items = new OrderItem[5];
int count = 0;
Order(String customer) {
this.id = nextId++;
this.customer = customer;
}
void add(OrderItem item) {
if (count < items.length) items[count++] = item;
}
int total() {
int sum = 0;
for (int i = 0; i < count; i++) sum += items[i].subtotal(); // 계산은 항목에게
return sum;
}
@Override
public String toString() { return "주문#" + id + " " + customer + " " + count + "건 " + total() + "원"; }
}
public static void main(String[] args) {
Order o1 = new Order("김민수");
o1.add(new OrderItem("키보드", 89_000, 1));
o1.add(new OrderItem("마우스", 25_000, 2));
Order o2 = new Order("이서연");
o2.add(new OrderItem("모니터", 350_000, 1));
System.out.println(o1); // 출력: 주문#1 김민수 2건 139000원
System.out.println(o2); // 출력: 주문#2 이서연 1건 350000원
System.out.println(Order.nextId); // 출력: 3
}
}객체 배열은 만들자마자 모든 칸이 null이고, 중간 칸이 비어 있을 수도 있습니다. 순회할 때 null 칸을 건너뛰고, 찾지 못했을 때 null을 돌려주는 메서드의 결과를 바로 쓰지 않도록 합니다. Objects.equals, Objects.toString은 null이어도 예외를 내지 않는 도우미입니다.
import java.util.Objects;
public class NullSafety {
static class Product {
String code;
String name;
Product(String code, String name) { this.code = code; this.name = name; }
}
static Product find(Product[] items, String code) {
for (Product p : items) {
if (p != null && p.code.equals(code)) return p; // null 칸 건너뛰기
}
return null; // 못 찾음
}
static String nameOf(Product p) {
return p == null ? "(없음)" : p.name; // null을 쓰기 전에 검사
}
public static void main(String[] args) {
Product[] items = new Product[4]; // 4칸 모두 null
items[0] = new Product("A1", "노트북");
items[2] = new Product("C3", "모니터"); // 1번 칸은 비어 있음
System.out.println(nameOf(find(items, "C3"))); // 출력: 모니터
System.out.println(nameOf(find(items, "Z9"))); // 출력: (없음)
Product p = null;
System.out.println(Objects.equals(p, null)); // 출력: true (p.equals(null)은 NPE)
System.out.println(Objects.toString(p, "기본값")); // 출력: 기본값
try {
System.out.println(p.name);
} catch (NullPointerException e) {
System.out.println("NPE: null의 필드를 읽으려 함"); // 출력: NPE: null의 필드를 읽으려 함
}
}
}예제 4에서 본 것처럼 참조를 대입하면 같은 객체를 가리킵니다. 독립된 사본이 필요하면 다른 객체를 받아 값을 베끼는 생성자(복사 생성자)를 만듭니다. 안에 배열이나 다른 객체가 있으면 그것도 clone이나 새 객체로 복사해야 진짜 독립됩니다.
import java.util.Arrays;
public class CopyConstructor {
static class Wallet {
int balance;
int[] coins;
Wallet(int balance, int[] coins) {
this.balance = balance;
this.coins = coins;
}
Wallet(Wallet other) { // 복사 생성자
this(other.balance, other.coins.clone()); // 배열도 새로 복사
}
@Override
public String toString() { return balance + " " + Arrays.toString(coins); }
}
public static void main(String[] args) {
Wallet original = new Wallet(1000, new int[]{500, 100});
Wallet alias = original; // 같은 객체를 가리킴
Wallet copy = new Wallet(original); // 독립된 새 객체
alias.balance = 0;
alias.coins[0] = 0;
System.out.println("원본: " + original); // 출력: 원본: 0 [0, 100]
System.out.println("복사: " + copy); // 출력: 복사: 1000 [500, 100]
System.out.println(original == alias); // 출력: true
System.out.println(original == copy); // 출력: false
}
}