Programing/Java

Effective Java - 객체 비교, 복제

안중환 2016. 3. 4. 16:29
  • 객체 비교
    - 1_equals 재정의

    package kr.co.ioacademy; //iocademy 윤찬식 강사님
    
    import java.util.Objects;
    
    // 1. Object.equals()를 재정의하지 않을 경우
    // 모든 객체는 오직 자기 자신과 동일하다.
    
    // 2. 객체 동일성이 아닌 논리적 동일성의 개념을 제공하기 위해서는
    // equals()를 재정의해야 한다.
    
    class Point {
      private int mX;
      private int mY;
    
      public Point(int x, int y) {
        this.mX = x;
        this.mY = y;
      }
    
      @Override
      public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + mX;
        result = prime * result + mY;
        return result;
      }
    
      @Override
      public boolean equals(Object obj) {
        // 1. 자기 자신인지 검사 - 성능
        if (this == obj) return true;
        // 2. null 인지 체크 (모든 객체는 null과 동치 관계가 있지 않다.)
        if (obj == null) return false;
        // 3. 인자의 자료형이 정확한지 검사.
        if (!(obj instanceof Point)) return false;
        // 4. 자료형 변환
        Point p = (Point) obj;
    
        // 5. 중요 필드 점검
        return mX == p.mX && mY == p.mY;
      }
    }
    
    
    class Unit {
      private Point position;
      private Point start;
    
      // 1. 객체에 대한 참조는 null이 될 수 있다.
      // 필드가 많아지면 Objects.equal을 고려하자.(Google Guava / 1.7)
    
    
      @Override
      public boolean equals(Object obj) {
        if (obj == this) return true;
        if (obj == null) return false;
        if (!(obj instanceof Unit)) return false;
    
        Unit p = (Unit) obj;
        // if (position == null) {
        // if (p.position != null) return false;
        // } else if (!position.equals(p.position)) return false;
        // return true;
    
        return Objects.equals(position, p.position) && Objects.equals(start, p.start);
      }
    }
    
    public class Example5 {
      public static void main(String[] args) {
        Point p1 = new Point(10, 20);
        Point p2 = new Point(10, 20);
    
        if (p1.equals(p2)) {
          System.out.println("Same");
        } else {
          System.out.println("Not Same");
        }
      }
    
    }
    


    - 2_BigDecimal, 배열 비교
    : 컴퓨터는 태생적으로 부동소수점을 정확히 표현할 수 없다.(2진수로 표현하기 때문에...)
    : float, double은 == 로 비교하면 안된다. -> BigDecimal 사용
    : 배열의 내용 비교 -> Arrays.equals()

    package kr.co.ioacademy; //iocademy 윤찬식 강사님
    
    import java.math.BigDecimal;
    import java.util.Arrays;
    
    // 1. float이나 double은 ==으로 비교하면 안된다.
    // 2. 정밀한 연산을 필요로 한다면 BigDecimal 을 사용해야 한다.
    public class Example6 {
      public static void main(String[] args) {
    
        double value1 = 2.0 - 1.1;
        double value2 = 0.9;
    
        // if (value1 == value2) {
        if (Math.abs(value1 - value2) < 0.001) {
          System.out.println("same");
        } else {
          System.out.println("not same");
        }
    
        BigDecimal v = new BigDecimal(2.0).subtract(new BigDecimal(1.1));
        System.out.println(v);;
    
        // 주의 사항 : BigDecimal(String)의 생성자를 사용해야 한다.
        BigDecimal v2 = new BigDecimal("2.0").subtract(new BigDecimal("1.1"));
        System.out.println(v2);;
    
        //-------------------------------------------------
        // 3. 배열 내용을 비교하려면 Object.equals() 가 아닌 Arrays.equals()
        //    사용해야 한다.
        int[] arr1 = new int[20];
        int[] arr2 = new int[20];
    
        System.out.println(arr1.equals(arr2));
    
        System.out.println(Arrays.equals(arr1, arr2));
      }
    }

  • - 3_hashCode
     : Google Guava를 사용하여 toString 재정의
    package kr.co.ioacademy; //iocademy 윤찬식 강사님
    
    import java.util.HashMap;
    import java.util.Map;
    import java.util.Objects;
    
    import com.google.common.base.MoreObjects;
    
    
    // 핵심 : 같은 객체는 동일한 해시 코드 값을 가져야 한다.
    // 즉 equals를 재정의한 클래스는 반드시 hashCode도 재정의해야 한다.
    // 그래야 HashMap, HashSet, HashTable 등 hash 기반 컬렉션과 함께 사용하면 오동작 하지 않는다.
    
    public class Example7 {
      public static void main(String[] args) {
        Map<Person, String> m = new HashMap<>();
    
        m.put(new Person("Tom", 42), "Google");
    
        System.out.println(m.get(new Person("Tom", 42)));
    
        // toString()을 잘 만들어 놓으면 편리하다.
        System.out.println(new Person("IU", 42));
      }
    }
    
    
    class Person {
      private String name;
      private int age;
    
      public Person(String name, int age) {
        this.name = name;
        this.age = age;
      }
    
      @Override
      public String toString() {
        return MoreObjects.toStringHelper(this).add("name", name).add("age", age).toString();
      }
    
      @Override
      public int hashCode() {
        // equals 에 이용된 중요 필드를 이용해서 hash를 생성하면 된다.
        return Objects.hash(name, age);
      }
    
      @Override
      public boolean equals(Object o) {
        if (o == this) return true;
        if (o instanceof Person) {
          Person p = (Person) o;
          return Objects.equals(name, p.name) && age == p.age;
        }
    
        return false;
      }
    }
    
    


  • 객체 복제
    : 인자 전달 방식
     -> Call-by-Value vs. Call-by-Reference

    : 객체 복사 방식
     -> 얕은 복사(Shallow Copy) vs. 깊은 복사(Deep Copy)

    class Num {
    	public int num;
    	
    	public Num(int num) {
    		this.num = num;
    	}
    }
    
    public class CallBy {
    	
    	// 값을 복사해서 인자 전달
    	// value는 객체에 대한 레퍼런스 값, 프리미티브 타입의 값
    	public void callByValue(int a, int b) { 
    		System.out.println("callByValue in method: " + a + " " + b);
    		int swap = a;
    		a = b;
    		b = swap;
    		System.out.println("callByValue out method: " + a + " " + b);
    		
    	}
    	
    	// 해당 객체를 참조하는 객체를 복사해서 인자 전달(얕은 복사)
    	// 해당 객체의 주소값을 직접 넘기는 것이 아니라 객체를 가리키고 있는 또 다른 주소값을 만들어서 넘긴다
    	void callByReference(Num a, Num b) {
    		System.out.println("callByReference in method: " + a.num + " " + b.num);
    		Num swap = a;
    		a = b;
    		b = swap;
    		System.out.println("callByReference out method: " + a.num + " " + b.num);
    	}
    	
    	// 객체의 멤버 필드값에 대한 복사가 필요(깊은 복사)
    	void callByReference2(Num a, Num b) {
    		System.out.println("callByReference2 in method: " + a.num + " " + b.num);
    		int swap = a.num;
    		a.num = b.num;
    		b.num = swap;
    		System.out.println("callByReference2 out method: " + a.num + " " + b.num);
    	}
    	
    	public static void main(String[] args) {
    		CallBy call = new CallBy();
    		
    		int a = 5;
    		int b = 10;
    		
    		Num n1 = new Num(5);
    		Num n2 = new Num(10);
    		
    		call.callByValue(a, b);
    		System.out.println("callByValue main method: " + a + " " + b + "\n");
    		//callByValue in method: 5 10
    		//callByValue out method: 10 5
    		//callByValue main method: 5 10
    		
    		call.callByReference(n1, n2);
    		System.out.println("callByReference main method: " + n1.num + " " + n2.num + "\n");
    		//callByReference in method: 5 10
    		//callByReference out method: 10 5
    		//callByReference main method: 5 10
    		
    		call.callByReference2(n1, n2);
    		System.out.println("callByReference2 main method: " + n1.num + " " + n2.num);
    		//callByReference2 in method: 5 10
    		//callByReference2 out method: 10 5
    		//callByReference2 main method: 10 5
    		
    	}
    }
    


    - 1_clone, Cloneable
     : 객체에 대한 깊은 복사
     : 
    상속을 해주기 위한 클래스를 설계할 때, 잘 동작하는 protected clone 메소드를 그 클래스에 두지 않는다면 서브 클래스에서 Cloneable 인터페이스를 제대로 구현할 수 없다.

    package kr.co.ioacademy; //iocademy 윤찬식 강사님
    
    import com.google.common.base.MoreObjects;
    
    class Point implements Cloneable {
      private int x;
      private int y;
    
      public Point(int x, int y) {
        this.x = x;
        this.y = y;
      }
    
      public int getX() {
        return x;
      }
    
      public void setX(int x) {
        this.x = x;
      }
    
      public int getY() {
        return y;
      }
    
      public void setY(int y) {
        this.y = y;
      }
    
      @Override
      public String toString() {
        return "(" + x + ", " + y + ")";
      }
    
      @Override
      public Point clone() {
        try {
          return (Point) super.clone();
        } catch (CloneNotSupportedException e) {
          e.printStackTrace();
        }
    
        return null;
      }
    }
    
    // 객체 복제 하기
    // 1. clone() 함수를 오버라이드 한다. (protected -> public)
    // : 오버라이딩할 메소드는 부모의 접근 제한자와 같거나 접근하기 더 쉬워야 한다.
    
    // 2. Cloneable 인터페이스를 구현해야 한다.
    // : 어떤 객체가 복제를 허용한다는 사실을 알리는데 쓰이는 용도이다.
    
    
    // 객체가 Cloneable 인터페이스를 구현하고 있으면, Object.clone() 은
    // 객체가 가지고 있는 모든 멤버를 복사한다.
    class Unit implements Cloneable {
      private String name;
      private int age;
    
      private Point position;
      // 중요 : 변경 가능 객체에 대한 참조를 가지고 있으면 문제가 발생한다.
    
      public Unit(String name, int age, Point pos) {
        this.name = name;
        this.age = age;
        this.position = pos;
      }
    
      public void setPos(int x, int y) {
        position.setX(x);
        position.setY(y);
      }
    
      
      // public Object clone() {
      // 공변 반환형 : 재정의 메소드의 리턴 타입은 재정의 되는 메소드의
      // 리턴 타입의 하위 클래스가 될 수 있다.(1.5)
      //  @Override
      //  public Unit clone() {
      //    try {
      //      return (Unit) super.clone();
      //    } catch (CloneNotSupportedException e) {
      //      e.printStackTrace();
      //    }
      //
      //    return null;
      //  }
    
      @Override
      public Unit clone() {
        try {
          // 1. 전체 복사 후
          Unit result = (Unit) super.clone();
    
          // 2. 변경 가능 객체 복제
          result.position = position.clone();
          return result;
    
        } catch (CloneNotSupportedException e) {
          e.printStackTrace();
        }
    
        return null;
      }
    
      @Override
      public String toString() {
        return MoreObjects.toStringHelper(this).add("name", name).add("age", age)
            .add("pos", position).toString();
      }
    }
    
    
    public class Example8 {
      public static void main(String[] args) {
        Unit p1 = new Unit("Tom", 42, new Point(0, 0));
    
        // Unit p2 = (Unit) p.clone();
        Unit p2 = p1.clone();
    
        p2.setPos(10, 20); // !!!
    
        System.out.println(p1);
        System.out.println(p2);
      }
    }
    


    - 2_생성자 방어 복사
     : 접근자를 이용하여 클라이언트가 값을 변경할 수 있다. -> 캡슐화가 깨짐
     : 객체를 복사할 때 객체의 참조를 리턴하는 것이 아니라 객체의 복사본을 리턴한다. pos.setX(9999)로 x값을 변경해도 복사한 객체의 값을 바꾸므로 기존의 객체의 값은 그대로 유지된다.

    package kr.co.ioacademy; //iocademy 윤찬식 강사님
    
    import com.google.common.base.MoreObjects;
    
    public class Example9 {
      public static void main(String[] args) {
        
        // 1. 생성자 방어 복사가 필요하다.
        //  : 인자의 유효성을 검사하기 전에 복사하고 나서, 
        //    원본이 아닌 복사본의 유효성을 검사해야 한다.
        Point pos = new Point(100, 200);
        // Unit unit = new Unit(pos);
        
        
        String name = "Tom";
        Unit unit = new Unit(pos, name);
        
        pos.setX(9999);
        
        // 2. 접근자 메소드에서도 참조를 방어 복사하여 리턴해야 한다.
        pos = unit.position();
        pos.setX(9999);
    
        System.out.println(unit);
      }
    }
    
    
    class Unit {
      private Point position;
      private String name;
      
      public Unit(Point pos, String name) {
        position = pos.clone();
        // position = pos;
        this.name = name;
      }
    
      public String name() {
        return name;
      }
      
      public Point position() {
        return position.clone();
      }
    
      @Override
      public String toString() {
        return MoreObjects.toStringHelper(this)
            .add("pos", position).add("name", name).toString();
      }
    
    }
    
    
    class Point implements Cloneable {
      private int x;
      private int y;
    
      public Point(int x, int y) {
        this.x = x;
        this.y = y;
      }
    
      public int getX() {
        return x;
      }
    
      public void setX(int x) {
        this.x = x;
      }
    
      public int getY() {
        return y;
      }
    
      public void setY(int y) {
        this.y = y;
      }
    
      @Override
      public String toString() {
        return "(" + x + ", " + y + ")";
      }
    
      @Override
      public Point clone() {
        try {
          return (Point) super.clone();
        } catch (CloneNotSupportedException e) {
          e.printStackTrace();
        }
    
        return null;
      }
    }