티스토리 뷰

Java/Class

11. Collections Framework : 객체 비교

알 수 없는 사용자 2018. 11. 4. 00:30


이번 포스팅에서는

Collections Framework에서 객체 간의 비교가

어떤 과정에 따라 이루어지고

객체를 정렬하는 방법을

살펴보도록 하겠습니다.

 

======================================================================


Collections Framework : 객체 비교




1. Object Equals : What?


  객체는 변수와 메소드를 하나의 틀로 묶은 것으로 생각할 수 있습니다. 그렇기 때문에 객체를 비교할 때 우리의 의도와는 다르게 결과가 나타나게 됩니다.

 쉽게 말하면, 객체 안에 특정 데이터로 객체를 비교하고 싶은데 컴퓨터는 객체가 가지고 있는 데이터가 아닌 실제 객체 틀을 보고 비교한다는 것입니다.


 예제 코드를 보겠습니다.

Integer 객체 두 개를 생성하는데 그 안의 값은 같습니다.

Integer i1 = new Integer(31);
Integer i2 = new Integer(31);


사람이 봤을 때는 두 객체 안에 포함된 값이 같고 비교하는 값이 31이라고 생각하기 때문에 당연히 같은 객체라고 생각할 수 있습니다.

하지만 컴퓨터는 다릅니다.

System.out.println(i1 == i2);

위와 같이 i1과 i2가 같은지 출력하면 false 값을 얻게 됩니다.


 사람이 봤을 때만 31이라는 값을 비교하는 것이고 컴퓨터 입장에서는 31이라는 값이 아닌 참조하는 객체 값 자체를 비교하게 되는 것입니다.



 앞에서 말한 것과 같이 객체를 하나의 틀이라고 생각한다면 31을 박스에 담아 i1에 저장하는 것이고, i2에 저장된 31은 앞과 다른 박스에 담겨 있는 것입니다. 그렇기 때문에 다른 컴퓨터는 i1이 참조하는 박스(객체 값)와 i2가 참조하는 박스(객체 값)를 비교하게 되는 것입니다.


그렇기 때문에 객체로만 본다면 서로 다른 객체로 new 연산자를 통해 만들어졌기 때문에 컴퓨터는 당연히 다르다고 생각하는 것입니다.



그렇다면 Collection Framework에서는 중복된 객체는 자동으로 저장하지 않는 Set 컬렉션도 있었고 객체를 기준에 따라 정렬하는 List의 sort() 메소드도 있었습니다. 또한 객체가 포함되어 있는지 확인하고, 특정 객체를 삭제하는 기능도 있었습니다.

생각해보면 이러한 기능들은 모두 저장되어있는 객체와 특정 객체를 비교하여 찾아내는 작업이 필요하게 되는데 어떤 방식으로 찾아내는 것일까요?






2. Object Equals : HashSet?



다음과 같이 Position 클래스를 정의하고 이를 HashSet을 이용하여 Position 객체를 저장하겠습니다.

class Position {
int x;
int y;

public Position(int x, int y) {
// TODO Auto-generated constructor stub
this.x = x;
this.y = y;
}

@Override
public String toString() {
return "[x= " + x + ", y= " +y + "]";
}
}
public class Source08_Duplicate {
public static void main(String[] args) {
Set<Position> ps = new HashSet<>();
Position p1 = new Position(1, 1);
Position p2 = new Position(1, 1);
ps.add(p1);
ps.add(p2);
System.out.println(ps.size());

System.out.println(ps.toString());
}
}


 x=1, y=1인 Position 객체를 따로 생성하여 p1, p2에 저장하였습니다. 

그리고 Set의 사이즈와 저장된 객체의 값을 찍어보도록 하겠습니다.


2
[[x= 1, y= 1], [x= 1, y= 1]]

Set이라면 중복된 객체를 포함하지 않아야 하므로 p2의 add 작업은 일어나지 않아야 원하는 결과를 얻을 수 있습니다.

하지만 앞에서 본 Integer 객체와 같이 컴퓨터는 서로 다른 객체로 판단하여 중복되지 않았다 판단하고 Set 컬렉션에 저장한 것입니다.


이처럼 개발자의 입맛?에 따라서 객체의 비교를 설정해 주고 싶다면 HashSet 클래스에서는 저장되는 객체의 클래스에 hashCode() 메소드와 equals() 메소드를 오버라이드 해주어야합니다.


- hashCode() : hashCode는 객체를 구별하기 위해 설정된 고유한 값을 출력시켜주는 메소드입니다. 

- equals() : 실제 객체 안에 비교하고자 하는 데이터를 비교하여 결과를 반환해주는 메소드입니다.



## HashSet 클래스의 객체를 비교하는 과정은 hashCode()의 반환값이 같은지 체크하고, equals() 메소드의 반환 값이 같은지 체크하는 과정이라 생각하면 편합니다.

때문에 equals()에서 반환하는 값이 true이여도 hashCode()의 반환 값이 다르다면 다른 객체로 인식하는 것입니다. 반대로 hashCode() 반환 값이 같더라도 equals() 반환값이 true가 아니라면 다른 객체로 판단하는 것입니다.



위의 코드를 hashCode()와 equals() 코드를 오버라이드하여 수정해봅시다.

// 1. hashCode()의 리턴값을 수정해야한다.
@Override
public int hashCode() {
System.out.println("hashCode called ....!!!");
return x*y;
}

// 2. hashCode()가 같을 때 데이터 값을 비교하기위해 equals메소드 오버라이드
@Override
public boolean equals(Object obj) {
System.out.println("equals called ....!!!");
if(obj instanceof Position) {
Position other = (Position)obj;
return this.x == other.x && this.y == other.y;
} else {
return false;
}
}


그리고 똑같이 메인 문을 실행해보면 다음과 같은 결과를 얻습니다.

hashCode called ....!!!
hashCode called ....!!!
equals called ....!!!
1
[[x= 1, y= 1]]


결과를 보면 p1이 저장될 때는 hashCode() 메소드만 호출되고 p2가 저장될 때는 hashCode() 메소드와 equals() 메소드가 둘 다 호출된 것을 볼 수 있습니다.

p1이 저장될 때는 hashCode가 같은 객체가 없기 때문에 equals() 메소드를 애초에 호출하지 않고 같은 객체가 없다고 판단하여 Set에 저장한 것입니다.

반면에 p2가 저장될 때는 hashCode가 같은 객체를 발견하고 equals() 메소드까지 호출하여 같은 객체가 있다고 판단하고 Set에 저장하지 않은 것이죠.


이처럼 hashCode() 와 equals()를 이용하여 객체의 비교를 개발자가 수정해 줄 수 있습니다. 

물론 add 뿐만 아니라 삭제 같은 기능도 결국 특정 객체를 찾아 삭제하는 것이기 때문에 위와 같은 설정을 해주어야 합니다.


hashCode() 메소드의 반환 값을 정할 때는 객체마다 같은 값을 반환해도 큰 지장은 없지만 해싱을 사용하는 컬렉션의 성능을 향상시키기 위해서는 다른 int 값을 반환하는 것을 권장합니다.






3. Object Equals : TreeSet?


 TreeSet의 경우 특성이 크기를 비교하여 정렬하여 객체를 저장하는 것이었습니다.

때문에 hashCode() 메소드와 equals() 메소드를 사용하지 않고 Comparable 인터페이스를 구현하여 CompareTo() 메소드를 오버라이드 하여 사용합니다.

이 CompareTo() 메소드는 정렬뿐만 아니라 객체 비교를 위해서도 사용됩니다.


예제를 다루기 위해 Card 클래스를 설계해보겠습니다.

정해진 카드의 이름과 타입을 랜덤으로 뽑아서 TreeSet에 저장하는 것입니다.

 

class Card implements Comparable<Card>{
static String[] made;
static {
made = new String[] {"탱커","딜러","힐러","서포터"};
}

String name;
int type;

public Card(String n, int t) {
name = n;
type = t;
}

@Override
public String toString() {
return "{" + name + "(" + made[type] + ")}";
}

// Treeset 객체의 크기 비교 판단을 위해 생성
@Override
public int compareTo(Card o) {
// 이름이 작은애가 작은 객체, 이름이 같다면 type이 작은 애가 작은객체
int i = this.name.compareTo(o.name);
if(i == 0){
return this.type - o.type;
} else {
if(i<0)
return -1;
else
return 1;
}
}
}

class CardShop {
static Card random() {
String[] name = "루피,조로,나미,우솝,상디".split(",");

Card t = new Card(name[(int)(Math.random()*name.length)], (int)(Math.random()*4));
return t;
}
}

public class Exercise02_Card {
public static void main(String[] args) {
Set<Card> cards = new TreeSet<>();
System.out.println("7연속 카드 뽑기! 시작!! ");
for(int cnt=1; cnt<=7; cnt++) {
Card t = CardShop.random();
String result = cnt + " ... " + t.toString();
if(!cards.contains(t)) {
result += " NEW!";
}
System.out.println(result);
cards.add(t);
}
for(Card m : cards) {
System.out.println("-- " + m);
}
}
}


TreeSet은 HashSet과 다르게 CompareTo() 메소드를 오버라이드했습니다.

CompaerTo는 인자로 전달되는 객체를 가지고 변수를 비교하여 작은지 큰지를 반환해주는 메소드입니다.

메소드를 호출하는 객체를 기준으로 크기가 작다면 음수. 같다면 0, 크다면 양수가 반환되게 해주면 됩니다.

크기가 작은데 음수를 반환한다는 것은 오름차순을 의미하고 크기가 작은데 양수를 반환하면 내림차순으로 정렬한다는 것이 되겠죠.



7연속 카드 뽑기! 시작!!
1 ... {상디(딜러)} NEW!
2 ... {조로(힐러)} NEW!
3 ... {조로(서포터)} NEW!
4 ... {루피(딜러)} NEW!
5 ... {조로(서포터)}
6 ... {상디(딜러)}
7 ... {우솝(딜러)} NEW!
-- {루피(딜러)}
-- {상디(딜러)}
-- {우솝(딜러)}
-- {조로(힐러)}
-- {조로(서포터)}

위의 결과로 다음과 같이 나온 것을 볼 수 있습니다. 물론 random() 메소드를 사용했기 때문에 결과는 달라지겠죠.


하지만 여기서 봐야할 것은 NEW표시가 없는 카드가 뽑혔지는지 확인하고 실제 TreeSet에 중복된 객체가 들어갔는지 확인하는 것입니다.

또한 이름을 기준으로 오름차순으로 정렬했기 때문에 내부적으로 어떻게 정렬되었는지 확인도 해야합니다.





4. Object Equals : List?


 List는 객체 비교와 정렬의 방법이 Set과 유사합니다.

다만 객체의 비교는 equals()에 의해서 일어나고, 정렬은 Comparator 인터페이스를 구현한 클래스를 이용하여 진행된다는 것입니다.


 TreeSet에서 사용한 Comparable 인터페이스는 Comparator 인터페이스와 유사합니다.

 - Comparator : 기본 정렬기준 외에 다른 기준으로 정렬하고자 할 때 사용합니다.

 - Comparable : 기본 정렬기준을 구현하는 데 사용합니다.


예제로 ArrayList에 String 형 객체를 저장하여 정렬이 어떤 방식으로 이루어지는지 알아보겠습니다.

String과 같이 자바에서 기본적으로 제공하는 메소드는 대부분 hashCode() 메소드와 equals() 메소드가 처리되어 있기 때문에 자동으로 객체 비교가 가능합니다.


class CustomComparator implements Comparator<String> {

@Override
public int compare(String o1, String o2) {
// o1 객체가 작다고 한다면 -1 , 크다고하면 1 , 같으면 0
int c = o1.compareTo(o2);
return c < 0 ? -1 : c > 0 ? 1 : 0;
}

}

class ParseComparator implements Comparator<String> {

@Override
public int compare(String o1, String o2) {
int a = Integer.parseInt(o1);
int b = Integer.parseInt(o2);
int c = a - b;
return c < 0 ? -1 : c > 0 ? 1 : 0;
}

}
public class Source11_List {
public static void main(String[] args) {
List<String> li = new ArrayList<>();
li.add("11");
li.add("101");
li.add("10");
li.add("100");
li.add("1001");
li.add("111");
System.out.println(li.toString());
System.out.println("contains? " + li.contains("101")); // true
String data = new String("10");
System.out.println("contains? " + li.contains(data)); // true
// List계열은 contains indexOf 객체 판별할 때 equals만 사용 (hashCode 판단안함)
System.out.println(li.indexOf(data)); // 2
System.out.println(li.get(2) == data); // false
System.out.println(li.get(2).equals(data)); // true

Comparator<String> c = new CustomComparator();
li.sort(c); // c에 정의한 대로 정렬

// 문자열로 바라보고 정렬하는 것과 실제 숫자 정렬과는 다르다.
for(int idx =0; idx < li.size(); idx ++) {
String m = li.get(idx);
System.out.println("--" + m + " / " + Integer.parseInt(m, 2));
}

System.out.println();

Comparator<String> t = new ParseComparator();
li.sort(t);

for(Iterator<String> s = li.iterator(); s.hasNext();) {
String d = s.next();
System.out.println("--" + d + " / " + Integer.parseInt(d, 2));
}

}
}


TreeSet과 다른 점은 별도의 클래스를 Comparator 인터페이스를 이용하여 구현한 후 sort() 메소드에 인자로 넘겨주었다는 것입니다.

Comparator 클래스 안에 존재하는 compare() 메소드는 Comparable 인터페이스에서 구현한 compareTo() 메소드와 구현 방식은 매우 비슷합니다.


이처럼 정렬할 때 주의해야 할 점은 문자열과 실제 숫자의 정렬이 다르다는 것입니다.

만약 2진수를 문자열로 표기한다면 정렬의 방식은 CustomComparator의 방식처럼 될 것입니다.

--10 / 2
--100 / 4
--1001 / 9
--101 / 5
--11 / 3
--111 / 7

왜냐하면 문자열의 크기비교는 앞의 자리부터 문자를 하나씩 비교하기 때문에 두 번째 자릿수가 1인 문자가 뒤로가게 되는 것입니다.



하지만 실제로 우리가 적은 값은 2진수 값으로 11은 10 뒤, 1001은 마지막에 위치해야합니다.

때문에 parseComparator와 같이 정렬해주어야합니다.

--10 / 2
--11 / 3
--100 / 4
--101 / 5
--111 / 7
--1001 / 9


'Java > Class' 카테고리의 다른 글

13. Networking : UDP,TCP  (1) 2018.11.16
12. Thread : 쓰레드  (16) 2018.11.14
10. Collections Framework : Map  (0) 2018.10.31
9. Collections Framework : Queue, Deque  (0) 2018.10.30
8. Collections Framework : List  (0) 2018.10.29
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함