clone() 메서드 : 객체의 복사본을 생성하기 위해 사용되는 메서드. 객체의 얕은 복사(shallow copy)를 수행한다.

얕은 복사는 객체 내부의 복합 객체에 대한 참조만을 복사하므로, 복제된 객체와 원본 객체가 같은 데이터를 가리키게 될 수 있다. 이로 인해 복제된 객체를 변경할 때 원본 객체도 영향을 받을 수 있다.

 

clone 메서드를 사용할 때 주의할 점은 얕은 복사가 되다는 점이다. 이로 인해  클래스가 가변 객체를 참조하는 순간 오류가 발생할 수 있다.

import java.util.Arrays;
import java.util.EmptyStackException;

public class Stack implements Cloneable {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
    }

    public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        Object result = elements[--size];
        elements[size] = null; // 다 쓴 참조 해제
        return result;
    }

    private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }

    @Override
    protected Stack clone() {
        try {
            Stack result = (Stack) super.clone();
            result.elements = elements.clone(); // 배열을 깊은 복사한다.
            return result;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(); // 이 예외가 발생할 일은 없다.
        }
    }
}

저번 아이템 7에서 사용한 예제에 가변상태를 참조하는 clone 메서드를 추가해주었다. 만약 가변상태를 참조하는 clone 메서드를 없다고 가정하고 clone 메서드가 Stack 클래스를 복제한다면 Stack인스턴스의 size 필드는 올바른 값을 갖겠지만, elements 필드는 원본 Stack 인스턴스와 똑같은 배열을 참조할것이다. 원본이나 복제본 중 하나를 수정하면 다른 하나도 수정되어 불변식이 해친다는 것이다. 이를 해결하기 위해 가변상태를 참조하는 clone 메서드 추가한다면 불변식을 해치지 않는다.

 @Override
    protected Stack clone() {
        try {
            Stack result = (Stack) super.clone();
            result.elements = elements.clone(); // 배열을 깊은 복사한다.
            return result;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(); // 이 예외가 발생할 일은 없다.
        }

clone 메서드는 사실상 생성자와 같은 효과를 낸다. 즉, clone은 원본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야 한다. 이처럼 배열을 복제할 때는 배열의 clone 메서드를 사용하라고 권장한다. 사실, 배열은 clone 기능을 제대로 사용하는 유일한 예라 할 수 있다.

 

새로운 인터페이스를 만들 때는 절대 Cloneable을 확장해서는 안 되며, 새로운 클래스도 이를 구현해서는 안된다. final 클래스라면 Cloneable을 확장해서는 안되며, 새로운 클래스도 이를 구현해서는 안된다. final 클래스라면 위험이 크지는 않지만 권장하지는 않는다.

 

결론 : 복제기능의 기본원칙은 '복제기능은 복사 생성자와 복사 팩터리 메서드 를 이용하는게 BEST ' 라는 것이다. 단, 배열만은 clone 메서드 방식이 가장 깔끔하다.

 

 

추가 설명 - 복사 생성자와 복사 팩토리 메서드란?

복사 생성자 :
복사 생성자는 해당 클래스의 인스턴스를 인자로 받는 생성자다.
이 생성자는 전달받은 인스턴스의 상태를 복사하여 새로운 인스턴스를 초기화한다.
복사 생성자는 객체의 깊은 복사를 명시적으로 제어할 수 있게 해준다.

public class MyClass {
    private int data;

    public MyClass(MyClass source) {
        this.data = source.data;
    }
}

복사 팩토리 메서드 : 
복사 팩토리 메서드는 복사 생성자와 유사한 기능을 제공하지만, 메서드를 통해 구현된다. 
이 방식은 클래스의 인스턴스를 인자로 받아, 그 인스턴스의 상태를 복사하여 새로운 인스턴스를 반환한다.
public class MyClass {
    private int data;

    public static MyClass newInstance(MyClass source) {
        return new MyClass(source.data);
    }
}

https://www.acmicpc.net/problem/16234

 

16234번: 인구 이동

N×N크기의 땅이 있고, 땅은 1×1개의 칸으로 나누어져 있다. 각각의 땅에는 나라가 하나씩 존재하며, r행 c열에 있는 나라에는 A[r][c]명이 살고 있다. 인접한 나라 사이에는 국경선이 존재한다. 모

www.acmicpc.net

 

[정답 코드]

import java.io.*;
import java.util.*;

public class Main {
    static int[][] graph;
    static boolean[][] visited;
    static int[] dx = {-1, 1, 0, 0};
    static int[] dy = {0, 0, -1, 1}; // 상하좌우순서
    static int N, L ,R;
    static ArrayList<int[]> list;
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        StringTokenizer st = new StringTokenizer(br.readLine(), " ");

        N = Integer.parseInt(st.nextToken());
        L = Integer.parseInt(st.nextToken());
        R = Integer.parseInt(st.nextToken());

        graph = new int[N][N];
        for (int i = 0; i < N; i++) {
            st = new StringTokenizer(br.readLine(), " ");
            for (int j = 0; j < N; j++) {
                graph[i][j] = Integer.parseInt(st.nextToken());
            }
        }
        System.out.println(move());
    }

    public static int move() { // 더이상 인구 이동이 일어나지 않을때 까지 반복
        int result = 0;
        while (true) {
            boolean isMove = false;
            visited = new boolean[N][N];
            for (int i = 0; i < N; i++) {
                for (int j = 0; j < N; j++) {
                    if (!visited[i][j]) {
                        int sum = dfs(i, j); // 방문하지 않은 곳 dfs 탐색
                        if (list.size() > 1) {
                            changePopulation(sum); // 국경이 열린 노드끼리 인구 이동
                            isMove = true;
                        }
                    }
                }
            }
            if (!isMove) {
                return result;
            }
            result++;
        }
    }

    public static void changePopulation(int sum) {
        int avg = sum / list.size();
        for (int i = 0; i < list.size(); i++) {
            int x = list.get(i)[0];
            int y = list.get(i)[1];
            graph[x][y] = avg;
        }
    }

    public static int dfs(int x, int y) {
        Queue<int[]> queue = new LinkedList<>();
        queue.offer(new int[]{x, y});

        list = new ArrayList<>();
        list.add(new int[]{x, y});

        visited[x][y] = true;
        int sum = graph[x][y];

        while (!queue.isEmpty()) {
            int tmp[] = queue.poll();
            for (int i = 0; i < 4; i++) { // 4방탐색
                int nx = tmp[0] + dx[i];
                int ny = tmp[1] + dy[i];
                if (nx >= 0 && nx < N && ny >= 0 && ny < N) { // graph 범위 내
                    // 방문한적 없고, L과 R 사이일 때
                    if (!visited[nx][ny] && L <= Math.abs(graph[tmp[0]][tmp[1]] - graph[nx][ny]) && Math.abs(graph[tmp[0]][tmp[1]] - graph[nx][ny]) <= R) {
                        visited[nx][ny] = true; // 방문한곳으로 변경
                        queue.offer(new int[]{nx, ny}); // 새로 큐에 담아줌
                        list.add(new int[]{nx, ny}); // 연결된 나라끼리는 따로 좌표를 담아줌
                        sum += graph[nx][ny];
                    }
                }
            }
        }
        return sum;
    }
}

 

[설명]

dfs와 구현을 동시에 해야하는 문제이다. 

문제를 세분화해서 풀어야 한다.

1. 순회를하며 방문하지 않은 노드를방문한다. 모든 노드를방문할 때 까지 반복된다.
2. 방문한 노드에서 dfs 알고리즘을 구현. 

  • dfs 알고리즘 : 연결된 노드들의 좌표를 list에 담아줌. 연결된 노드들의 값을 모두 더함.

3. 연결된 것 노드의 체크가 끝나면 인구 이동을 시작.

  • 인구이동 로직 : 연결된 노드들의 좌표를 담았던 list를 하나씩 꺼내며 연결된 노드들의 값을 모드 더했던것을 적절히 나눠 값을 배분

4. 모든 노드들을 방문했다면 1일 증가

5. 1 ~ 5를 반복, 인구이동이 더이상 일어나지 않을 때 까지 반복

'코딩테스트' 카테고리의 다른 글

백준 1068번: 트리[JAVA]  (1) 2024.03.15
백준 1991번: 트리 순회[JAVA]  (1) 2024.03.15
백준 2493번: 탑[JAVA]  (0) 2024.03.12
백준 7682번: 틱택토[JAVA]  (0) 2024.03.11
백준 9372번: 상근이의 여행[JAVA]  (0) 2024.03.09

toString() 메서드객체를 문자열 표현으로 반환해주는 메서드이다.

toString() 메서드는 기본적으로 '클래스이름@hashcode' 문자열을 반환해준다. 예를들면 PhoneNumber@abbaa 이런식이다.

이런 형태는 객체의 상태나 값에 대한 유용한 정보를 제공하지 않기때문에 재정의해 의미있게 표현해야한다.

 

예를들어 이러한 클래스가 있다고 예시를 들어보자 : 

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    /**
     * 객체의 문자열 표현을 반환합니다.
     * 이 문자열 표현은 객체의 주요 정보를 요약하여 제공하며,
     * 디버깅과 로깅 목적으로 유용합니다.
     * 
     * 반환되는 문자열의 포맷은 "Person{name='이름', age=나이}" 입니다.
     * 여기서, '이름'과 '나이'는 각각 객체의 name과 age 필드의 현재 값을 나타냅니다.
     * 
     * @return 객체의 문자열 표현
     */
    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + "}";
    }
}

// 사용 예:
Person person = new Person("John Doe", 30);
System.out.println(person.toString()); // 출력: Person{name='John Doe', age=30}

 

예시처럼 toString()을 재정의 했을 경우 Person{name='John Doe', age=30} 같이 반환된다.

Person 객체에 담긴 name, age같은 정보가 출력되어 디버깅, 로깅시 유용하다.

만약 이를 재정의하지 않았을경우 출력값은 Person@b58a 이런식으로 출력됬을것이다.

 

그리고 예시처럼 반드시 반환값의 포맷을 명시하든 아니든 재정의 하려는 의도를 반드시 명확하게 밝혀야 한다.

구글의 AutoValue같은 프레임워크로 toString 재정의시 각 필드의 내용등을 설명해주긴 한다. 하지만 개발자가 원하는 클래스의 '의미'까지는 알지는 못. 개발자가 원하는 의도에 맞게 작성해주자.

 

결론 : toString() 메서드는 재정의해주자. 재정의시 의도를 명확하게 작성해주자. 재정의해 디버깅시 유용하게 사용하자. 재저

hashcode : 객체를 대표하는 정수 값. hashCode() 메서드를 통해 이 값을 얻는다. 객체의 hashcode는 객체를 저장하거나 검색할때 hash table같은 데이터 구조의 로 사용된다. 

 

equals를 재정의한 클래스 모두에서 hashCode도 재정의해야 한다.

그렇지 않으면 hashCode 일반 규약을 어기게 되어 오류가 발생할 수 있다.

 

1. 프로그램 실행 중 동일한 객체에 대해 여러 번 hashCode() 메서드를 호출하면, 객체가 수정되지 않았다면 메서드는 항상 동일한 정수를 반환해야 한다. 이 값은 객체가 수정되지 않는 한 프로그램 재실행 간에도 일관되어야 한다.

 

2. equals(Object) 메서드가 두 객체를 동등하게 판단한다면, 두 객체의 hashCode() 메서드는 동일한 정수 값을 반환해야 한다.

 

3. equals(Object) 메서드가 두 객체를 다르게 판단한다 하더라도, 두 객체의 hashCode() 메서드가 서로 다른 정수 값을 반환할 필요는 없다. 그러나, 다른 객체에 대해 다른 해시 코드를 생성하는 것이 해시 테이블의 성능을 향상시킨다.

 

 

결론 : equals를 재정의할 때는 hashcode도 반드시 재정의해야 한다.

하지만 AutoValue 프레임워크를 사용하면 equals와 hashCode를 자동으로 만들어주니 hashCode를 직접 재정의하는 방법을 외울 필요는 없는것 같다.

equals 메서드는 Java의 Object 클래스에 정의된 메서드로, 두 객체가 "같음(equal)"을 결정하기 위해 사용된다. 모든 클래스에서 상속받으므로, 필요에 따라 오버라이드(재정의)하여 객체 간의 동등성 비교 방식을 커스텀할 수 있다.

 

기본 동작 : 

public boolean equals(Object obj) {
    return (this == obj);
}

기본적으로 Object 클래스의 equals 메서드는 두 객체의 참조가 같은지 확인한다. 즉, 두 참조가 메모리 상에서 같은 객체를 가리키는 경우에만 true를 반환한다.

 

equals를 재정의할 때 다음 중 하나라도 해당한다면 재정의하지 않는것이 좋다.

1. 각 인스턴스가 본질적으로 고유하다.

  • 예를들어 Thread 클래스가 있다. Thread 클래스의 인스턴스는 각각 고유한 실행 스레드를 대표한다. 따라서, equals 메서드를 재정의하여 스레드 간의 '동등성'을 비교하는것은 의미가 없다.

2. 인스턴스의 논리적 동치성을 검사할 일이 없다.

  • 해당 인스턴스가 대표하는 값이나 상태가 같은 다른 인스턴스와 "동등"하다고 판단할 필요가 없다는 의미이다. 즉, 객체의 동일성(identity)이 중요할 뿐, 두 개의 서로 다른 객체가 같은 값을 가진다는 개념이 의미가 없거나 관련이 없는 경우를 말합니다.
public class Session {
    private final String sessionId;

    public Session(String sessionId) {
        this.sessionId = sessionId;
    }

    // 세션 ID에 기반한 동치성 비교가 의미가 없는 경우, equals 메서드를 재정의하지 않음
}
  • 이처럼 세션ID가 같다고해서 두 세션의 인스턴스가 논리적으로 동등하다고 판단할 필요 자체가 없을경우 이다.

3. 상위 클래스에서 재정의한 equals가 하위 클래스에도 딱 들어맞는다.

  • 상위 클래스에서 재정의한 equals 메서드가 하위 클래스에서도 모든 면에서 적절하게 동작한다면, 하위 클래스에서 equals 메서드를 다시 재정의할 필요가 없다. 이 경우는 일반적으로 하위 클래스가 상위 클래스의 "동등성" 개념을 변경하지 않고, 추가적인 상태 정보 없이 상위 클래스의 동작을 확장할 때 발생한다.

4. 클래스가 private이거나 package-private이고 equals 메서드를 호출할 일이 없다.

  • 클래스가 private이거나 package-private이고 그 인스턴스 간에 equals 메서드를 호출할 일이 없다면, 이는 해당 클래스가 매우 특정한 용도로 사용되며, 그 범위와 목적 내에서 인스턴스들의 동등성 비교가 로직적으로 의미가 없거나 필요하지 않다는 것을 의미할 경우 equals 메서드를 호출할 필요 자체가 없기때문에 재정의할 필요도 없는것이다.

 

이런 상황에 해당하지 않고 eqauls를 재정의 해야할 경우에는 반드시! 일반 규약을 따라야 한다.

 

1. 반사성(Reflexivity): 객체는 자기 자신과 동등해야 한다.

  • 어떤 객체 x에 대해, x.equals(x)true를 반환해야 한다.

2. 대칭성(Symmetry): 두 객체의 동등성 검사는 방향에 무관해야 한다.

  • 어떤 객체 x와 y에 대해, x.equals(y)가 true를 반환한다면, y.equals(x)도 true를 반환해야 한다.

3. 추이성(Transitivity): 첫 번째 객체가 두 번째 객체와 동등하고, 두 번째 객체가 세 번째 객체와 동등하면, 첫 번째 객체도 세 번째 객체와 동등해야 한다.

  • 어떤 객체 x, y, z에 대해, x.equals(y)와 y.equals(z)가 true를 반환한다면, x.equals(z)도 true를 반환해야 한다.

4. 일관성(Consistency): 두 객체의 상태가 변하지 않는 한, equals 메서드의 호출 결과는 변경되지 않아야 한다.

  • 어떤 객체 x와 y에 대해, 여러 번 x.equals(y)를 호출하더라도 항상 true를 반환하거나 항상 false를 반환해야 한다.

5. null에 대한 비동등성: 모든 객체는 null과 비교했을 때 false를 반환해야 한다.

  • 어떤 객체 x에 대해, x.equals(null)은 false를 반환해야 한다.

어려워 보이지만 하나하나 읽어보면 당연한 말이다.

 

eqauls를 재정의를 해야만 하는 경우에는 5가지 일반규약을 따르고 검색해가며 단계별로 재정의를 하자. 하지만 일반 규약을 따르며 재정의해도 오류가 발생할 수 있다. equals 재정의를 했다면 대칭적인지, 추이성이 있는지, 일관적인지 단위 테스트를 작성해보는것이 좋다.

 

결론 : 정말 꼭 필요한 경우가 아니라면 equals를 재정의하지 말자. 대부분 equals가 개발자가 원하는 비교를 정확히 준다.

그냥 재정의 하지말자.

+ Recent posts