[Java 회고 03] 클래스와 객체 생성 : 생성자, 참조와 얕은/깊은 복사, 임시 객체와 String

글 정보
카테고리java/study/basic
작성일2025-12-18
게시 여부true
seriesJava 회고
series-order3
제목[Java 회고 03] 클래스와 객체 생성 : 생성자, 참조와 얕은/깊은 복사, 임시 객체와 String

클래스, 객체, 인스턴스

클래스 문법

클래스 생성자

// 1. 반환 타입(void 등)을 적지 않습니다.
// 2. 메서드 이름은 반드시 '클래스 이름'과 같아야 합니다.
public Car() {
    // new Car(); 를 할 때 자동으로 실행되는 코드
    System.out.println("자동차가 생성되었습니다.");
}
class Box {
    int size;
    
    // 1. 매개변수가 없는 생성자 (기본값 설정)
    Box() { this.size = 10; }

    // 2. 매개변수가 있는 생성자 (값 지정)
    Box(int size) { this.size = size; }
}

// 사용: new Box() 또는 new Box(50) 가능

생성자

public class InitTest {
	// 1. 필드 명시적 초기화
	int value = 10; 

	public InitTest() {
		// 2. 생성자에서 초기화 (이것이 덮어씌움)
		this.value = 20; 
	}
	
	public static void main(String[] args) {
		InitTest t = new InitTest();
		System.out.println(t.value); // 결과: 20
	}
}

DeepCopy VS Swallow Copy


public class ShallowCopyExample {
    public static void main(String[] args) {
        // 원본 객체 생성
        Person original = new Person("홍길동", new Address("서울"));
        
        // 얕은 복사 (생성자에서 주소값만 그대로 전달)
        Person shallowCopied = new Person(original);
        
        // 복사본의 주소를 "부산"으로 변경
        shallowCopied.address.city = "부산";
        
        // [Side Effect 발생]
        // 복사본을 바꿨는데 원본인 '홍길동'의 주소도 "부산"으로 바뀜!
        System.out.println("원본 주소: " + original.address.city); // 부산 (의도치 않은 변경)
    }
}

class Address {
    String city;
    Address(String city) { this.city = city; }
}

class Person {
    String name;
    Address address;

    Person(String name, Address address) {
        this.name = name;
        this.address = address;
    }

    // 얕은 복사 생성자
    Person(Person other) {
        this.name = other.name;
        this.address = other.address; // [위험] 주소값(참조)만 복사함
    }
}
public class DeepCopyExample {
    public static void main(String[] args) {
        Person original = new Person("홍길동", new Address("서울"));
        
        // 깊은 복사 (내부 Address도 새로 생성)
        Person deepCopied = new Person(original);
        
        // 복사본의 주소 변경
        deepCopied.address.city = "부산";
        
        // [안전함]
        // 원본은 여전히 "서울"을 유지함. 서로 다른 주소 객체를 가짐.
        System.out.println("원본 주소: " + original.address.city); // 서울
    }
}

class Person {
    String name;
    Address address;

    // 깊은 복사 생성자
    Person(Person other) {
        this.name = other.name;
        // [핵심] new 키워드로 아예 새로운 Address 인스턴스를 생성해서 할당
        this.address = new Address(other.address.city); 
    }
    
    // 일반 생성자 생략 (위 예제와 동일) ...
}

참조자

참조자

public class ReferenceIdentity {
    public static void main(String[] args) {
        
        // 1. 인스턴스 생성
        // 힙 메모리에 "익명의 Member 인스턴스"가 생성되고, 그 주소를 p1이 가짐
        Member p1 = new Member("Alice");

        // 2. 참조자 복사 (포인터 복사)
        // 인스턴스가 복사되는 것이 아니라, '주소값'만 p2에 복사됨
        Member p2 = p1; 

        // 3. 동일성 확인
        // p2를 통해 내용을 바꾸면, p1이 가리키는 대상도 바뀜 (같은 곳을 보니까)
        p2.name = "Bob";
        System.out.println(p1.name); // Bob 출력
        
        // p1과 p2는 서로 다른 변수지만, 같은 주소(인스턴스)를 가리킴
        System.out.println(p1 == p2); // true (주소값이 같음)
    }
}

class Member {
    String name;
    Member(String name) { this.name = name; }
}
// 1. Cloneable 인터페이스를 반드시 구현해야 함 (마커 인터페이스)
class Item implements Cloneable {
    int value;
    
    // 2. protected인 clone()을 public으로 오버라이딩 해야 함
    // 3. 예외(CloneNotSupportedException) 처리가 강제됨
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone(); // 기본적으로 얕은 복사(Shallow Copy) 수행
    }
}

public class CloneProblem {
    public static void main(String[] args) {
        Item original = new Item();
        
        try {
            // 4. 리턴 타입이 Object이므로 매번 형변환(Casting) 필요
            Item copy = (Item) original.clone();
            
        } catch (CloneNotSupportedException e) {
            // 5. 불필요해 보이는 예외 처리 로직 작성 필요
            e.printStackTrace();
        }
    }
}

깊은 복사를 하는 법

public class DeepCopyConstructor {
    public static void main(String[] args) {
        // 1. RHS (Right Hand Side) : 원본 객체 생성
        Member rhs = new Member("홍길동", new Address("서울"));

        // 2. LHS (Left Hand Side) : 복사 생성자를 통해 생성
        //    rhs를 넘겼지만, 내부적으로는 새로운 Address를 만들어 가짐
        Member lhs = new Member(rhs);

        // 3. 독립성 확인
        //    lhs의 주소를 바꿔도 rhs는 영향받지 않음
        lhs.addr.city = "부산"; 

        System.out.println("RHS(원본): " + rhs.addr.city); // 서울 (유지됨!)
        System.out.println("LHS(복사): " + lhs.addr.city); // 부산
    }
}

// 참조 타입 필드 (이 녀석도 복사가 필요함)
class Address {
    String city;
    
    Address(String city) { this.city = city; }

    // Address용 복사 생성자 (재귀적인 깊은 복사를 위해 권장)
    Address(Address rhs) {
        this.city = rhs.city; // 값만 베껴옴
    }
}

class Member {
    String name;
    Address addr; // 참조형 변수 (Deep Copy 대상)

    // 일반 생성자
    Member(String name, Address addr) {
        this.name = name;
        this.addr = addr;
    }

    // ★ 복사 생성자 (Copy Constructor) 구현 ★
    // 파라미터로 넘어온 rhs(원본)의 데이터를 읽어 this(LHS)에 채운다.
    Member(Member rhs) {
        this.name = rhs.name; // String은 불변객체라 참조 복사도 무관
        
        // [중요] 참조형 필드는 반드시 'new'를 사용해 새로 만들어야 함!
        // rhs.addr의 주소를 넣는게 아니라, rhs.addr의 '값'을 가진 새 객체를 연결
        this.addr = new Address(rhs.addr); 
    }
}

임시 객체

public class Main {
    public static void main(String[] args) {
        
        // 1. 일반적인 방식 (참조자 O)
        // 'p'라는 참조 변수가 힙 영역의 실제 Point 인스턴스를 가리킵니다.
        Point p = new Point(10, 20);
        p.print(); 

        // 2. 임시 객체 사용 (참조자 X)
        // new Point(30, 40)으로 인스턴스는 생성되지만, 이름이 없습니다.
        // print() 실행 직후, 더 이상 접근할 수 없어 가비지 컬렉션(GC) 대상이 됩니다.
        new Point(30, 40).print();
    }
}

// 간단한 Point 클래스 (설명용)
class Point {
    int x, y;
    Point(int x, int y) { this.x = x; this.y = y; }
    void print() { System.out.println("좌표: " + x + ", " + y); }
}

String

public class StringMemory {
    public static void main(String[] args) {
        
        // 1. new 연산자 사용 (Heap 메모리)
        // 매번 새로운 객체가 힙 영역에 생성됩니다. 같은 문자열이라도 주소값이 다릅니다.
        String strHeap = new String("Java"); 

        // 2. 리터럴 사용 (Runtime Constant Pool)
        // JVM이 관리하는 풀(Pool)에 저장됩니다. 이미 같은 문자열이 있다면 재사용합니다.
        String strPool = "Java";

        // 주소값 비교 (==)
        // strHeap은 힙, strPool은 상수 풀에 있으므로 서로 다른 주소를 가집니다.
        System.out.println(strHeap == strPool); // false 출력
    }
}
public class StringConcatProblem {
    public static void main(String[] args) {
        
        // 코드상으로는 한 줄이지만, 내부적으로는 여러 임시 객체가 생성됩니다.
        String result = "Hello" + "World" + "!!";
        
        // [내부 동작 과정]
        // 1. "Hello"와 "World"가 리터럴로 존재
        // 2. 둘을 합친 "HelloWorld"라는 **임시 객체(Instance)**가 힙에 생성됨 (사용자가 볼 수 없음)
        // 3. 생성된 "HelloWorld"와 "!!"를 합쳐서 최종 결과 "HelloWorld!!" 생성
        // 4. 중간에 생겼던 "HelloWorld"는 참조자가 없어 가비지(Garbage)가 됨
    }
}

연관 문서