Comparator와 Comparable
이전 예제에서 Arrays.sort()를 호출만 하면 컴퓨터가 알아서 배열을 정렬하는 것처럼 보이지만, 사실 Character클래스의 Cmpatable의 구현에 의해 정렬되었던 것이다.
Comparator와 Comparable은 모두 인터페이스로 컬렉션을 정렬하는데 필요한 메서드를 정의하고 있으며, Comparable을 구현하고 있는 클래스들은 같은 타입의 인스턴스 끼리 서로 비교할 수 있는 클래스들, 주로 Integer와 같은 wrapper클래스와 String, Date, File과 같은 것들이며 기본적으로 오름차순, 즉 작은 갑셍서부터 큰 값의 순으로 정렬되도록 구현되어 있다. 그래서 Comparable을 구현한 클래스는 정렬이 가능하다는 것을 의미한다.
Comparator와 Comparable의 실제 소스는 다음과 같다.
public interface Comparator{
int compare(Object o1, Object o2);
boolean equals(Object obj);
}
public interface Comparable{
public int compareTo(Object o);
}
- Comparable은 java.lang 패키지에 있고, Comparator은 java.util패키지에 있다.
compare()와 compareTo는 선언형태와 이름이 약간 다를 뿐 두 객체를 비교한다는 같은 기능을 목적으로 고안된 것이다. compareTo()의 반환값은 int이지만 실제로는 비교하는 두 객체가 같으면 0, 비교하는 값보다 작으면 음수, 크면 양수를 반환하도록 구현해야 한다. 이와 마찬가지로 compare()도 객체를 비교해서 음수, 0, 양수 중의 하나를 반환하도록 구현해야 한다.
equals메서드는 모든 클래스가 가지고 있는 공통적인 메서드이지만, Comparator를 구현하는 클래스는 오버라이딩이 필요할 수도 있다는 것을 알리기 위해서 정의한 것일 뿐, 그냥 compare(Object o1, Object o2)만 구현하면 된다.
public fianl class Integer extends Number implements Comparable{
...
public int compareTo(Object o){
reutnr compareTo((Integer)o);
}
public int compareTo(Integer anotherInteger){
int thisVal = this.value;
int anotherVal = anotherInteger.value;
//비교하는 값이 크면 -1, 같으면 0, 작으면 1을 반환한다.
reutnr (thisVal<anotherVal ? -1: (thisVal == anotherVal ? 0: 1));
}
...
}
위의 코드는 Integer클래스의 일부이다. Comparable의 compareTo(Obejct o)를 구현해 놓은 것을 볼 수 있다. 두 Integer객체에 저장된 int값(value)을 비교해서 같으면 0, 크면 -1, 작으면 1을 반환한다는 것을 알 수 있다. TreeSet에 Integer인스턴스를 저장했을 때 정렬되는 기준이 바로 compareTo메서드에 의한 것이다.
Comparable을 구현한 클래스들이 기본적으로 오름차순으로 정렬되어 있지만 , 내림차순으로 정렬한다던가 아니면 다른 기준에 의해서 정렬되도록 하고 싶을 때 Comparator를 구현해서 정렬기준에 제공할 수 있다.
Comparable : 기본 정렬기준을 구현하는데 사용
Comparator : 기본 정렬기준 외에 다른 기준으로 정렬하고자 할 때 사용
import java.util.*;
public class CompartorEx {
public static void main(String[] args) {
String[] strArr = {"cat", "Dog", "lion", "tiger"};
Arrays.sort(strArr); // String의 comrable구현에 의한 정렬
System.out.println("strArr="+Arrays.toString(strArr));
Arrays.sort(strArr, String.CASE_INSENSITIVE_ORDER); //대소문자 구분안함
System.out.println("strArr="+Arrays.toString(strArr));
Arrays.sort(strArr, new Desending()); //역순정렬
System.out.println("strArr="+Arrays.toString(strArr));
}
}
class Desending implements Comparator{
public int compare(Object o1, Object o2){
if(o1 instanceof Comparable && o2 instanceof Comparable){
Comparable c1 = (Comparable)o1;
Comparable c2 = (Comparable)o2;
return c1.compareTo(c2) * -1; //-1을 곱해서 기본 정렬 방식의 역으로 변경한다.
//또는 c2.compareTo(c1)와 같이 순서를 바꿔도 된다.
}
return -1;
}
}
=======================
strArr=[Dog, cat, lion, tiger]
strArr=[cat, Dog, lion, tiger]
strArr=[tiger, lion, cat, Dog]
전에 배운것과 같이 Arrays.sort()는 배열을 정렬할 때 Comparator를 지정해주지 않으면 객체(주로 Comparable을 구현한 클래스의 객체)에 구현된 내용에 따라 정렬된다.
static void sort(Object[] a) //객체 배열에 저장된 객체가 구현한 Comparable에 의한 정렬
static void sort(Object[] a, Compartor c) //지정한 Comparator에 의한 정렬
String의 Comparable 구현은 문자열이 사전 순으로 정렬되도록 작성되어 있다. 문자열의 오름차순 정렬은 공백, 숫자, 대문자, 소문자의 순으로 정렬되는 것을 의미한다. 정확히 얘기하면 문자의 유니코드의 순서가 작은 값에서부터 큰 갑승로 정렬되는 것이다.
그리고 아래와 같이 대소문자를 구분하지 않고 비교하는 Compartor를 상수의 형태로 제공한다.
public static final Comparator CASE_INSENSITIVE_ORDER
이 Compartor를 이용하면 , 문자열을 대소문자 구분없이 정렬할 수 있다.
Arrays.sort(strArr, String.CASE_INSENSITIVE_ORDER); //대소문자 구별없이 정렬
String의 기본 정렬을 반대로 하는 것, 즉 문자열을 내림차순(descending order)을 구현핳는 것은 아주 간단하다. 단지 String에 구현된 compareTo()의 결과에 -1을 곱하기만 하면된다. 또는 비교하는 객체의 위치를 바꿔서 c2.compareTo(c1)과 같이 해도 된다.
다만 compare()의 매개변수가 Object타입이기 때문에 compareTo()를 바로 호출할 수 없으므로 먼저 Comparable로 형변환해야 한다는 것만 확인하자.
class Descending implements Compartor{
public int compare(Object o1, Object o2){
if(o1 instanceof Comparable && o2 instanceof Comparable){
Comparable c1 = (Comparable)o1;
Comparable c2 = (Comparable)o2;
return c1.compareTo(c2) * -1; //기본 정렬 방식의 역으로 변경한다.
}
return -1;
}
}
HashSet
HashSer은 Set인터페이스를 구현한 가장 대표적인 컬렉션이며, Set인터페이스의 특징대로 HashSet은 중복된 요소를 저장하지 않는다.
HashSet에 새로운 요소를 추가할 때는 add메서드나 addAll메서드를 사용하는데, 만일 HashSet에 이미 저장되어 있는 요소와 중복된 요소를 추가하고자 한다면 이 메서드들은 false를 반환함으로써 중복된 요소이기 때문에 추가에 실패했다는 것을 알린다.
이러한 HashSet의 특징을 이용하면, 컬렉션 내의 중복요소들을 쉽게 제거할 수 있다.
ArrayList와 같이 List인터페이스를 구현한 컬렉션과 달리 HashSet은 저장순서를 유지하지 않으므로 저장순서를 유지하고자 한다면 LinkedHashSet()을 사용해야 한다.
- HashSet은 내부적으로 hashMap을 이용해서 만들어어 졌으며, HashSet이라는 이름은 해싱(hashing)을 이용해서 구현했기 때문에 붙여진 것이다.
HashSet의 메서드
HashSet() : HashSet객체를 생성한다.
HashSet(Collection c) : 주어진 컬렉션을 포함하는 HashSet객체를 생성한다.
HashSet(int initialCapacity) : 주어진값을 초기용량으로 하는 HashSet객체를 생성한다.
HashSet(int initialCapacity, float loadFactor) : 초기 용량과 load factor를 지정하는 생성자
boolean add(Object o) : 새로운 객체를 저장한다.
boolean addAll(Collection c) : 주어진 컬렉션에 저장된 모든 객체들을 추가한다.(합집합)
void clear() : 저장된 모든 객체를 삭제한다.
Object clone() : HashSEt을 복제해서 반환한다. (얕은복사)
boolean contains(Object o) : 지정된 객체를 포함하고 있는지 알려준다.
boolean containsAll(Collection c) : 주어진 컬렉션에 저장된 모든 객체들을 포함하고 있는지 알려준다.
boolean isEmpty() : HashSet이 비어있는지 알려준다.
Iterator iterator() : Iterator를 반환한다.
boolean remove(Object o) : 지정된 객체를 HashSet에서 삭제한다.(성공하면 true, 실패하면 false)
boolean removeAll(Collection c) : 주어진 컬렉션에 저장된 모든 객체와 동일한 것들을 HashSet에서 모두 삭제한다(차집합)
boolea retainAll(Collection c) : 주어진 컬렉션에 저장된 객체와 동일한 것만 남기고 삭제한다.(교집합)
int size() : 저장된 객체의 개수를 반환한다.
Object[] toArray() : 저장된 객체들을 객체배열의 형태로 반환한다.
Object[] toArray(Object[] a) : 저장된 객체들을 주어진 객체배열(a)에 담는다.
- load factor는 컬렉션 클래스에 의해 저장공간이 가득 차기전에 미리 용량을 확보하기 위한 것으로, 이 값을 0.8로 지정하면 , 저장공간의 80%가 채워졌을 떄 용량이 두배로 늘어난다. 기본값은 0.75, 즉 75%이다.
import java.util.*;
public class HashSetEx1 {
public static void main(String[] args) {
Object[] objArr = {"1", new Integer(1),"2","2","3","3","4","4","4"};
Set set = new HashSet();
for(int i=0; i<objArr.length; i++){
//set.add(objArr[i]); //HashSet에 objArr요소들을 저장한다.
System.out.println(set.add(objArr[i])+ " : 저장할 요소 : "+objArr[i]);
}
//HashSet에 저장된 요소들을 출력한다.
System.out.println(set);
}
}
=================================
true : 저장할 요소 : 1
true : 저장할 요소 : 1
true : 저장할 요소 : 2
false : 저장할 요소 : 2
true : 저장할 요소 : 3
false : 저장할 요소 : 3
true : 저장할 요소 : 4
false : 저장할 요소 : 4
false : 저장할 요소 : 4
[1, 1, 2, 3, 4]
결과에서 알 수 있듯이 중복된 값은 저장되지 않았다. add메서드는 객체를 추가할 때 HashSet에 이미 같은 객체가 있으면 중복으로 간주하고 저장하지 않는다. 그리고는 작업이 실패했다는 의미로 false를 반환한다.
'1'이 두번 출력되었는데, 둘 다 '1'로 보이기 때문에 구별이 안되지만, 사실 하나는 String인스턴스이고 다른 하나는 Integer 인스턴스로 서로 다른 객체이므로 중복으로 간주하지 않는다.
Set을 구현한 컬렉션 클래스는 List를 구현한 컬렉션 클래스와 달리 순서를 유지하지 않기 때문에 저장한 순서와 다를 수 있다.
만일 중복을 제거하는 동시에 저장한 순서를 유지하고자 하면 HashSet대신 LinkedHashSet을 사용해야 한다.
import java.util.*;
public class HashSetLotto {
public static void main(String[] args) {
Set set = new HashSet();
for(int i=0; set.size()<6; i++){
int num = (int)(Math.random()*45)+1;
set.add(new Integer(num));
}
List list = new LinkedList(set); //LinkedList(Collection c)
Collections.sort(list); //Collections.sort(List list)
System.out.println(list);
}
}
========================
[11, 13, 17, 19, 33, 42]
중복된 값은 저장되지 않는 HashSet의 성질을 이용해서 로또번호를 만드는 예제이다.
Math.random()을 사용했기 때문에 실행할 때마다 결과가 다를 것이다.
번호를 크기순으로 정렬하기 위해서 Collections클래스의 sort(List list)를 사용했다. 이 메서드는 인자로 List인터페이스 타입을 필요로 하기때문에 LinkedLIst클래스의 생성자 LinkedList(Collection c)를 이용해서 HashSet에 저장된 객체들을 LinkedList에 담아서 처리했다.
실행결과의 정렬기준은, 컬렉션에 저장된 객체가 Integer이기 때문에 Integer클래스에 정의된 기본정렬이 사용되었다.
- Collection은 인터페이스고, Collections는 클래스임에 주의하자.
import java.util.*;
public class Bingo {
public static void main(String[] args) {
Set set = new HashSet();
//Set set2 = new LinkedHashSet();
int[][] board = new int[5][5];
for (int i = 0; set.size() < 25; i++) {
set.add((int) (Math.random() * 50) + 1 + "");
}
Iterator it = set.iterator();
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[i].length; j++) {
board[i][j] = Integer.parseInt((String) it.next());
System.out.print((board[i][j] < 10 ? " " : " ") + board[i][j]);
}
System.out.println();
}
}
}
================================
23 46 47 26 48
27 49 28 50 30
31 10 13 36 14
15 16 19 3 4
6 42 20 21 43
1~50 사이의 숫자 중에서 25개를 골라서 5x5크기의 빙고판을 만드는 예제이다. next()는 반환값이 Object타입이므로 형변환해서 원래의 타입으로 되돌려놔야 한다. 이 예제 역시 random()을 사용했기 때문에 실행할 때마다 다른 결과를 얻을 것이다.
그런데 몇번 실행해보면 같은 숫자가 비슷한 위치에 나온다는 사실을 발견할 수 있을 것이다. 앞서 언급한 것과 같이 HashSet은 저장된 순서를 보장하지 않고 자체적인 저장방식에 따라 순서가 결정되기 때문이다. 이 경우에는 HashSet보다 LinkedHashSet이 더 나은 선택이다.
import java.util.*;
public class HashSetEx3 {
public static void main(String[] args) {
HashSet set = new HashSet();
set.add("abc");
set.add("abc");
set.add(new Person("David",10));
set.add(new Person("David",10));
System.out.println(set);
}
}
class Person{
String name;
int age;
Person(String name, int age){
this.name = name;
this.age = age;
}
public String toString(){
return name + ":"+age;
}
}
===================================
[abc, David:10, David:10]
Person클래스는 name과 age를 멤버변수로 갖는다. 이름(name)과 나이(age0가 같으면 같은 사람으로 인식하도록 하려는 의도로 작성하였다. 하지만 실행결과를 보면 두 인스턴스의 name과 age의 값이 같음에도 불구하고 서로 다른 것으로 인식하여 'David:10'이 두번 출력되었다.
클래스의 작성의도대로 이 두 인스턴스를 같은 것으로 인식하게 하려면 어떻게 해야 하는 걸까?
import java.util.*;
public class HashSetEx4 {
public static void main(String[] args) {
HashSet set = new HashSet();
set.add(new String("abc"));
set.add(new String("abc"));
set.add(new Person2("David",10));
set.add(new Person2("David",10));
System.out.println(set);
}
}
class Person2{
String name;
int age;
Person2(String name, int age){
this.name = name;
this.age = age;
}
public boolean equals(Object obj){
if(obj instanceof Person2){
Person2 tmp = (Person2)obj;
return name.equals(tmp.name) && age==tmp.age;
}
return false;
}
public int hashCode(){
return (name+age).hashCode();
}
public String toString(){
return name + ":"+age;
}
}
================================
[abc, David:10]
HashSet의 add메서드는 새로운 요소를 추가하기 전에 기존에 저장된 요소와 같은 것인지 판별하기 위해 추가하려는 요소의 equals()와 hashCode()를 호출하기 때문에 equlals()와 hashCode()를 목적에 맞게 오버라이딩해야 한다.
그래서 String클래스에서 같은 내용의 문자열에 대한 equlas()의 호출결과가 true인것과 같이 , Person2클래스에서도 두 인스턴스의 name과 age가 서로 같으면 true를 반환하도록 equals()를 오버라이딩했다. 그리고 hashCode()는 String클래스의 hashCode()를 이용해서 구현했다.
String의 hashCode()는 잘 구현되어 있기 때문에 이를 활용하면 간단히 처리할 수 있다.
public int hashCode(){
return (name+age).hashCode();
}
위의 코드를 JDK1.8부터 추가된 java.util.Object클래스의 hash()를 이용해서 작성하면 아래와 같다. 이 메서드의 괄호 안에 클래스의 인스턴스변수들을 넣으면 된다. 이전의 코드와 별반 다르지 않지만, 가능하면 아래의 코드를 쓰자.
public int hashCode(){
return Objects.hash(name,age); //int hash(Object ...values)
}
오버라이딩을 통해 작성된 hashCode()는 다음의 세가지 조건을 만족시켜야 한다.
- 실행중인 애플리케이션 내의 동일한 객체에 대해서 여러 번 hashCode()를 호출해도 동일한 int값을 반환해야한다. 하지만 실행시마다 동일한 int값을 반환할 필요는없다.(단, equals메서드의 구현에 사용된 멤버변수의 값이 바뀌지 않았다고 가정한다.)
예를들어 Person2클래스의 equals메서드에 사용된 멤버변수 name과 age의 값이 바뀌지 않는 한 , 하나의 Person2인스턴스에 대해 hashCode()를 여러 번 호출했을 때 항상 같은 int값을 얻어야 한다.
Person2 p = new Person2("David", 10);
int hashCode1 = p.hashCode();
int hashCode2 = p.hashCode();
p.age = 20;
int hashCode3 = p.hashCode();
위의 코드에서 hashCode1의 값과 hashCode2의 값은 항상 일치해야 하지만 이 두값이 매번 실행될 때마다 반드시 같은 값일 필요는 없다. hashCode3은 equlas메서드에 사용된 멤버변수 age를 변경한 후에 hashCode메서드를 호출한 결과이므로 hashCode1이나 hashCode2와 달라도 된다.
- String클래스는 문자열의 내용으로 해시코드를 만들어 내기 때문에 내용이 같은 문자열에 대한 hashCode()호출은 항상 동일한 해시코드를 반환한다. 반면에 Object클래스는 객체의 주소로 해시코드를 만들어 내기 때문에 실행할 때마다 해시코드값이 달라질 수 있다.
2. equlas메서드를 이용한 비교에 의해서 tru를 얻은 두 객체에 대해 각각 hashCode()를 호출해서 얻은 결과는 반드시 같아야 한다.
인스턴스 p1과 p2에 대해서 equals메서드를 이용한 비교의 결과인 변수 b의 값이 true라면, hashCode1과 hashCode2의 값은 같아야 한다는 뜻이다.
Person2 p1= new Person2("David",10);
Person2 p2= new Person2("David",10);
boolean b = p1.equlas(p2);
int hashCode1 = p1.hashCode();
int hashCode2 = p1.hashCode();
3. equals메서드를 호출했을 때 false를 반환하는 두 객체는 hashCode()호출에 대해 같은 int값을 반환하는 경우가 있어도 괜찮지만, 해싱(hashing)을 사용하는 컬렉션의 성능을 향상시키기위해서는 다른 int값을 반환하는 것이 좋다.
위의 코드에서 변수 b의 값이 false일지라도 hashCode1과 hashCode2의 값이 같은 경우가 발생하는 것을 허용한다. 하지만 , 해시코드를 사용하는 Hashtable이나 HashMap과 같은 컬렉션의 성능을 높이기 위해서는 가능한 한 서로 다른 값을 반환하도록 hashCode()를 잘 작성해야 한다는 뜻이다.
서로 다른 객체에 대해서 해시코드값(hashCode()를 호출한 결과)이 중복되는 경우가 많아질수록 해싱을 사용하는 Hashtable, HashMap과 같은 컬렉션의 검색속도가 떨어진다.
두 객체에 대해 equals메서드를 호출한 결과가 true이면, 두 객체의 해시코드는 반드시 같아야 하지만 , 두 객체의 해시코드가 같다고 해서 equals메서드의 호출결과가 반드시 true이어야 하는 것은 아니다.
사용자정의 클래스를 작성할 때 equlas메서드를 오버라이딩 해야 한다면 hashCode()도 클래스의 작성의도에 맞게 오버라이딩하는것이 원칙이지만, 경우에 따라 위의 예제에서와 같이 간단히 구현하거나 생략해도 별 문제가 되지 않으므로 hashCode()를 구현하는데 부담을 가지지 않아도 된다.
import java.util.*;
public class HashSetEx5 {
public static void main(String[] args) {
HashSet setA = new HashSet();
HashSet setB = new HashSet();
HashSet setHab = new HashSet();
HashSet setKyo =new HashSet();
HashSet setCha = new HashSet();
setA.add("1"); setA.add("2"); setA.add("3");
setA.add("4"); setA.add("5");
System.out.println("A ="+setA);
setB.add("4"); setB.add("5"); setB.add("6");
setB.add("7"); setB.add("8");
System.out.println("B ="+setB);
Iterator it = setB.iterator();
while(it.hasNext()){
Object tmp =it.next();
if(setA.contains(tmp))
setKyo.add(tmp);
}
it = setA.iterator();
while(it.hasNext()){
Object tmp =it.next();
if(!setB.contains(tmp))
setCha.add(tmp);
}
it = setA.iterator();
while(it.hasNext())
setHab.add(it.next());
it = setB.iterator();
while(it.hasNext())
setHab.add(it.next());
System.out.println("A ∪ B ="+ setKyo);
System.out.println("A ∩ B ="+setHab);
System.out.println("A - B ="+setCha);
}
}
=======================================
A =[1, 2, 3, 4, 5]
B =[4, 5, 6, 7, 8]
A ∪ B =[4, 5]
A ∩ B =[1, 2, 3, 4, 5, 6, 7, 8]
A - B =[1, 2, 3]
이 예제는 두 개의 HashSet에 저장된 객체들을 비교해서 합집합, 교집합, 차집합을 구하는 방법을 보여준다. 사실 Set은 중복을 허용하지 않으므로 HashSEt의 메서드를 호출하는 것만으로도 간단하기 합칩합(addAll), 교집합(retainAll), 차집합(removeAll)을 구할 수 있다.
'Back-end' 카테고리의 다른 글
04 정확하고 다양한 결과 출력 WHERE절과 연산자 (0) | 2021.07.10 |
---|---|
11 컬렉션 프레임웍(5) (0) | 2021.07.09 |
11 컬렉션 프레임웍(3) (0) | 2021.07.07 |
11 컬렉션 프레임웍(2) (0) | 2021.07.06 |
03 SELECT문의 기본 형식 (0) | 2021.07.06 |