JAVA Basic) 컬렉션 프레임워크 - Set 인터페이스
Programming/Java 기초

JAVA Basic) 컬렉션 프레임워크 - Set 인터페이스

반응형

이해가 되지 않을 경우 이전 포스팅을 참고하세요.

2021.10.05 - [Programming/JAVA 기초] - JAVA Basic) 컬렉션 프레임워크

 

JAVA Basic) 컬렉션 프레임워크

목차 <컬렉션 프레임워크란?> 흔히 프로그래밍을 건축에 비유를 한다. 원하는 건물을 지으려면 구조를 잘 잡아야하듯, 프로그램 개발도 사용하는 자료를 어떤 구조로 관리할 것인지가 중요하기

montoo.tistory.com

2021.10.06 - [Programming/JAVA 기초] - JAVA Basic) 컬렉션 프레임워크 - List 인터페이스

 

JAVA Basic) 컬렉션 프레임워크 - List 인터페이스

'컬렉션 프레임워크'가 뭔지 모른다면, 아래 포스트부터 확인할 것 흔히 프로그래밍을 건축에 비유를 한다. 원하는 건물을 지으려면 구조를 잘 잡아야하듯, 프로그램 개발도 사용하는 자료를 어

montoo.tistory.com

목차

     


     

    <Set 인터페이스>

    순서와 상관 없이 중복을 허용하지 않는 경우에는 'Set 인터페이스'를 구현한 클래스를 사용한다.

    • 중복이 허용되지 않는 데이터 : 회원 ID, 주민등록번호, 사번, 주문번호 등

    'Set 인터페이스'를 구현한 대표 클래스는 'HashSet''TreeSet'이 있다.

     

    <HashSet 클래스>

    'HashSet 클래스'집합 자료 구조를 구현하며 중복을 허용하지 않는다

    중복을 허용하지 않는 것을 보여주는 간단한 HashSetTest 클래스를 만들어보자.

    package collection.hashset;
    
    import java.util.HashSet;
    
    public class HashSetTest {
    	public static void main(String[] args) {
    		HashSet<String> hashset = new HashSet<String>();
    		hashset.add(new String("이상순"));
    		hashset.add(new String("신동엽"));
    		hashset.add(new String("유재석"));
    		hashset.add(new String("이효리"));
    		hashset.add(new String("강호동"));
    		hashset.add(new String("강호동")); // +1
    		
    		System.out.println(hashset);
    	}
    }

    "강호동"을 두번 입력해도 한 번만 들어감

    동일한 문자열 자료 "강호동"을 추가해도, 중복되어 출력되지 않는다. 여기서 알 수 있는 두 가지 사실은.

    1. 'HashSet' 중복된 값은 추가되지 않는다.
    2. 'HashSet'은 ArrayList처럼 순서가 있는 자료 구조가 아니기 때문에 추가 순서와 상관없이 출력

     

     HashSet 활용

    이제 HashSet을 활용해 회원 관리 프로그램을 구현해 보도록 하자.

    HashSet 클래스를 생성하고 addMember(), removeMember() showAllMember()를 구현해 보자.

    package collection.hashset;
    
    import java.util.HashSet;
    import java.util.Iterator;
    import collection.Member;
    
    public class MemberHashSet {
    	private HashSet<Member> hashSet; //HashSet선언
    	
    	public MemberHashSet() {
    		hashSet = new HashSet<Member>(); //HashSet생성
    	}
    	
    	//HashSet에 회원 추가
    	public void addMember(Member member) {
    		hashSet.add(member);
    	}
    	
    	//매개변수로 받은 회원 ID에 해당되는 회원 삭제
    	public boolean removeMember(int memberID) {
    		Iterator<Member> ir = hashSet.iterator();//Iterator를 활용해 순회
    		
    		while (ir.hasNext()) {
    			Member member = ir.next();
    			int tempId = member.getMemberId();
    			if(tempId == memberID) {
    				hashSet.remove(member);
    				return true;
    			}
    		}
    		System.out.println(memberID+"가 존재하지 않습니다.");
    		return false;
    	}
    	
    	//모든 회원 출력
    	public void showAllMember() {
    		for(Member member : hashSet) {
    			System.out.println(member);
    		}
    		System.out.println();
    	}
    }
    • 회원을 삭제할 때 사용하는 remove()메서드가 ArrayList처럼 get(i) 메서드를 사용해 i번째 항목을 삭제하는 것이 아닌, 해당되는 ID를 찾기 위해 Iterator를 사용했다.
      Iterator는 HashSet을 순회하며 ID가 true면 HashSet의 remove()메서드를 사용해 해당 회원을 삭제한다.
    메서드 설명
    boolean remove(Object o) 매개변수로 받은 객체를 삭제하고 삭제 여부를 true, false로 반환

     

    이제 테스트 프로그램을 실행해 확인해 보자.

    먼저 MemberHashSet을 생성해 회원 집합을 추가하고, 기존에 추가된 회원과 아이디가 같은 회원을 추가해 보자.

    package collection.hashset;
    
    import collection.Member;
    
    public class MemberHashSetTest {
    	public static void main(String[] args) {
    		MemberHashSet memberHashSet = new MemberHashSet();
    		
    		Member memberLee = new Member(1001, "이상순");
    		Member memberShin = new Member(1002, "신동엽");
    		Member memberYu = new Member(1003, "유재석");
    		Member memberLee2 = new Member(1004, "이효리");
    		Member memberKang = new Member(1005, "강호동");
    		
    		memberHashSet.addMember(memberLee);
    		memberHashSet.addMember(memberShin);
    		memberHashSet.addMember(memberYu);
    		memberHashSet.addMember(memberLee2);
    		memberHashSet.addMember(memberKang);
    		memberHashSet.showAllMember();
    		
    		//ID 중복 회원 추가
    		Member memberFake = new Member(1005, "짭호동"); 
    		memberHashSet.addMember(memberFake);
    		memberHashSet.showAllMember();
    	}
    }

    아이디가 중복된 회원이 추가된 모습

    출력 결과를 보면 같은 ID '1005'를 가진 '강호동'과 '짭호동'이 그대로 출력되었다.

    앞서 HashSetTest 클래스에서 테스트했을 때는 String("강호동")이 중복되지 않았는데 그 이유는 String 클래스에는 객체가 동일할 경우 처리방법이 이미 구현되어 있기 때문이다.

    Member클래스에도 같은 객체를 처리하는 방법을 구현해보자.

     

    객체가 동일함을 구현하기

    기본적으로 '인스턴스 주소'가 같으면 '같은 객체'다. 하지만 여기서는 '회원 ID'가 같으면 '같은 회원'

    Object 클래스에서 논리적으로 같은 개체를 구현하기 위해 equals() 메서드hashCode() 메서드를 재정의했다.
    그러므로 Member 클래스에서도 equals() 메서드와 hashCode() 메서드를 재정의회원 ID가 같으면 같은 회원임을 구현해야 한다.

    package collection;
    
    public class Member {
    	private int memberId; //회원 ID
    	private String memberName; //이름
    	
    	@Override
    	public boolean equals(Object obj) {
    		if(obj instanceof Member) {
    			Member member = (Member)obj;
    			if(this.memberId == member.memberId) { 
    				return true;
    			} //매개변수로 받은 회원ID가 this.의 ID와 같다면 true반환
    		}
    		return false;
    	}
    
    	@Override
    	public int hashCode() {
    		return memberId; //hashCode() 메서드가 회원 ID를 반환하도록 재정의
    	}
    }
    • Member 클래스에 equals(), hashCode() 메서드를 재정의.

    테스트 출력 결과를 보면 중복되었던 ID 1005 짭호동이 추가되지 않은것을 볼 수 있다.

    중복된 ID 1005 짭호동이 추가되지 않았다

     

    <TreeSet 클래스>

    자바의 Collection 인터페이스나 Map 인터페이스를 구현한 클래스 중 'Tree로 시작하는 클래스'는 데이터를 추가한 후 '결과를 출력하면 값이 정렬'된다.

    'TreeSet'자료의 중복을 허용하지 않으면서 출력 결과 값을 정렬하는 클래스.

    아래 코드를 보자.

    package collection.treeset;
    
    import java.util.TreeSet;
    
    public class TreeSetTest {
    	public static void main(String[] args) {
    		TreeSet<String> treeSet = new TreeSet<>();
    		treeSet.add("홍길동");
    		treeSet.add("강감찬");
    		treeSet.add("이순신");
    		
    		for(String str : treeSet) {
    			System.out.println(str);
    		}
    	}
    }

    오름차순으로

    TreeSet에 홍길동, 강감찬, 이순신 순으로 요소를 추가했으나 결과 값은 오름차순으로 정렬되어 출력되었다.
    어떤 기준으로 이루어지는지 알아보자.

    자바는 정렬을 구현하기 위해 '이진 트리(binary tree)'를 사용한다.

     

    이진 검색 트리

    '트리'자료 사이의 계층 구조를 나타내는 자료 구조.

    '이진 검색 트리(Binary Search Tree : BST)' 그림을 보면

    이진 검색 트리 기본 구조

    • 노드 : 트리 자료 구조에서 각 자료가 들어가는 공간
    • 부모-자식 노드 : 위아래로 연결된 노드의 관계성

    이진 검색 트리는 노드에 저장되는 자료의 중복을 허용하지 않고, 부모가 가지는 자식의 노드가 2개 이하.
    왼쪽에 위치하는 자식 노드는 부모 노드보다 항상 작은 값을 가짐.
    오른쪽에 위치하는 자식 노드는 부모 노드보다 항상 큰 값을 가짐.
    따라서 어떤 특정 값을 찾으려 할 때 한 노드와 비교한 노드보다 작은 값이면 왼쪽 자식 노드 방향, 큰 값이면 오른쪽 자식 노드 방향으로 이동한다.

    따라서 비교 범위가 평균 1/2만큼씩 줄어 효과적인 자료 검색이 가능하다.

    이진 검색 트리의 자료 구성

    그럼 간단한 이진 검색 트리를 만들어 보자.

    [23, 10, 48, 15, 7, 22, 56]

    위 순서로 숫자를 입력한다고 할 때 트리가 만들어지는 모양은 다음과 같다.

    트리가 만들어지는 순서

    이렇게 만들어진 이진 검색 트리를 맨 왼쪽 노드 부터 '왼쪽-부모-오른쪽 순'으로 순회하면 오름차순이 된다.

    [7, 10, 15, 22, 23, 48, 56]

    그 반대로 오른쪽-부모-왼쪽 순으로 순회하면 내림차순이 된다.

    자바의 TreeSet은 이진 검색 트리를 활용하여 자료를 정렬한다.
    어떤 기준으로 값의 크기를 비교할 것인지는 프로그래머가 직접 구현해야 한다.

     

    TreeSet 활용

    TreeSetTest.java 예제에서 별도의 코드를 구현하지 않아도 요소들이 정렬되어있던 이유는 String 클래스 안에 정렬 방식이 이미 구현되어 있기 때문이다.

    이제 패키지를 새로 만들고 TreeSet을 활용해 회원 관리 프로그램을 구현해보자.
    동일한 Set 인터페이스를 구현한 클래스이므로 HashSet대신에 TreeSet만 선언해 생성하면 나머지 코드는 같다.

    회원 정렬 기준은 회원 ID순으로 한다.

    package collection.treeset;
    
    import java.util.Iterator;
    import java.util.TreeSet;
    import collection.Member;
    
    public class MemberTreeSet {
    	private TreeSet<Member> treeSet;
    	
    	public MemberTreeSet() {
    		treeSet = new TreeSet<Member>();
    	}
    	
    	//TreeSet에 회원을 추가하는 메서드
    	public void addMember(Member member) {
    		treeSet.add(member);
    	}
    	
    	//회원을 삭제하는 메서드
    	public boolean removeMember(int memberId) {
    		Iterator<Member> ir = treeSet.iterator();
    		
    		while(ir.hasNext()) {
    			Member member = ir.next();
    			int tempId = member.getMemberId();
    			if(tempId == memberId) {
    				treeSet.remove(member);
    				return true;
    			}
    		}
    		System.out.println(memberId + "가 존재하지 않습니다.");
    		return false;
    	}
    	
    	//회원 전체를 출력하는 메서드
    	public void showAllMember() {
    		for(Member member : treeSet) {
    			System.out.println(member);
    		}
    		System.out.println();
    	}
    }

    그리고 TreeSet을 테스트하여 정렬이 어떻게 되는지 확인하자.

    package collection.treeset;
    
    import collection.Member;
    
    public class MemberTreeSetTest {
    	public static void main(String[] args) {
    		MemberTreeSet memberTreeSet = new MemberTreeSet();
    		
    		Member memberPark = new Member(1003, "박서훤");
    		Member memberLee = new Member(1001, "이지원");
    		Member memberSon = new Member(1002, "손민국");
    	
    		memberTreeSet.addMember(memberLee);
    		memberTreeSet.addMember(memberSon);
    		memberTreeSet.addMember(memberPark);
    		memberTreeSet.showAllMember();
    		
    		Member memberHong = new Member(1003, "홍길동");
    		memberTreeSet.addMember(memberHong);
    		memberTreeSet.showAllMember();
    	}
    }

    죽어버린(?) 자바..

    위 출력 화면에서 오류메시지를 잘 확인하면 Member 클래스가 Comparable 인터페이스를 구현하지 않았다는 내용이 있다.

    Member클래스가 Comparable를 구현하지 않았다!

    Comparable을 구현하지 않았다는 말
    우리가 만든 Member클래스를 TreeSet의 요소로 추가할 때 '어떤 기준으로 노드를 비교하여 트리를 형성'해야 하는지를 구현하지 않았다는 뜻.

    따라서 회원을 TreeSet에 추가할 때 어떤 기준으로 비교할 것인지를 구현해야 한다.

     

    <Comparable / Comparator 인터페이스>

    Member 클래스가 가진 회원 아이디를 기준으로 하여 오름차순으로 정렬할 것이다.
    Comparable과 Comparator는 이러한 정렬을 구현해 주는 인터페이스다.

    정렬 기준 값이 있는 Member 클래스에 구현하면 된다.

    먼저 Comparable 인터페이스를 활용하여 구현해보자.

    public class Member implements Comparable<Member> {
    	...
    }

     

    Comparable 인터페이스

    Comparable 인터페이스에는 compareTo() 추상 메서드가 포함되어 있다.
    따라서 이 인터페이스를 구현하는 Member 클래스에서 compareTo() 메서드를 구현해야 한다.

    CompareTo() 메서드를 구현한 Member 클래스 코드는 이렇다.

    package collection;
    
    public class Member implements Comparable<Member> {
    	private int memberId; //회원 ID
    	private String memberName; //이름
    	...
        
    	//compareTo() 메서드 재정의. 추가되는 회원ID와 매개변수로 받은 회원ID를 비교.
    	@Override
    	public int compareTo(Member member) {
    		return (this.memberId - member.memberId);
    	}
    }

    여기서 재정의한 compareTo() 메서드의 의미는,
    비교 대상은 새롭게 추가되는 this의 memberId, 즉 새로 추가한 회원의 ID와
    compareTo() 메서드의 매개변수로 전달된 회원ID, 즉 트리의 값이 비교한다.

    새로 추가되는 ID가 더 크면 양수, 그렇지 않으면 음수, 같으면 0을 반환하도록 만들었다.

    이렇게 구현하면 출력 결과 값은 오름차순으로 정렬된다.
    즉, compareTo() 메서드에서는 '새로 추가되는 값'이 this. '비교되는 값은 트리에 있는 값'이 매개변수로 전달된다.

    comparTo()는 프로그래머가 호출하는 메서드가 아닌 객체가 TreeSet에 요소를 추가할 때 호출되는 메서드다.
    그리고 어떤 매개변수가 전달될지는 기존 TreeSet에 어떤 요소가 들어있는지에 따라 달라진다.

    회원의 ID가 오름차순으로 정렬되었다.

     

    내림차순으로 정렬하려면, Member 클래스의 compareTo()메서드를 다음처럼 수정하면 된다.

    	@Override
    	public int compareTo(Member member) {
    		return (this.memberId - member.memberId)*(-1);
    	} //내림차순으로 정렬하기 위해 반환 값을 음수로 만든다.

    반환 값에 -1을 곱하여 음수로 바꾸면 내림차순으로 정렬된다.

    -1을 곱해 반환값을 음수로 만들어 내림차순

     

    Comparator 인터페이스

    'Comparator' 역시 정렬을 구현하는 데 사용하는 인터페이스다.  Comparator 인터페이스Compare() 메서드를 구현해야한다.

    Member2 클래스를 새로 만들어 Comparator를 구현한 코드를 보자.

    package collection;
    
    import java.util.Comparator;
    
    public class Member2 implements Comparator<Member2> {
    	private int memberId; //회원 ID
    	private String memberName; //이름
    	...
    
    	// compare()메서드 재정의. 전달받은 두 매개변수를 비교.
    	@Override
    	public int compare(Member2 mem1, Member2 mem2) {
    		return mem1.getMemberId() - mem2.getMemberId();
    	}
    }

    Comparator 인터페이스는 'compare() 메서드'를 구현해야 하는데, 이 메서드에는 매개변수 2개가 전달된다.
    compareTo() 메서드는 this와 전달된 매개변수를 비교했다면, compare() 메서드는 전달되는 두 매개변수를 비교한다.

    첫 번째 매개변수가 더 클 때 양수를 반환하여 오름차순으로 정렬된다.

    Comparator를 사용할 때 유의할 점은 TreeSet 생성자에 Comparator를 구현한 객체를 매개변수로 전달한다는 것.
    즉 다음과 같이 코드를 구현해야 한다.

    TreeSet<Member> treeSet = new TreeSet<Member>(new Member());

    일반적으로 Comparator 인터페이스 보다는 Comparable 인터페이스를 더 많이 사용한다.

    다만 어떤 클래스가 이미 Comparable 인터페이스를 구현한 경우에 이 클래스의 정렬방식을 정의할 때 Comparator 인터페이스를 사용할 수 있다. 

    예를 들어 String 클래스는 Comparable 인터페이스를 이미 구현해다고 하자. 만약 정렬 방식을 내림차순으로 바꾸고 싶은 경우.
    String클래스일 경우 final로 선언되어 있어서 상속받아 compareTo() 메서드를 재정의할 수도 없다. 이런 경우 Comparator를 사용한다.

    package collection.treeset;
    
    import java.util.Comparator;
    import java.util.Set;
    import java.util.TreeSet;
    
    class MyCompare implements Comparator<String> {
    
    	//내림차순으로 정렬
    	@Override
    	public int compare(String s1, String s2) {
    		return (s1.compareTo(s2)) * -1;
    	}
    }
    
    public class ComparatorTest {
    	public static void main(String[] args) {
    		Set<String> set = new TreeSet<String>(new MyCompare()); //TreeSet 생성자의 매개변수로 정렬 방식을 지정
    		set.add("aaa");
    		set.add("ccc");
    		set.add("bbb");
    		
    		System.out.println(set);
    	}
    }

    내림차순으로 정렬됨

    TreeSet 클래스를 생성할 때 생성자에 매개변수를 넣지 않으면 원래 String 클래스에 정의된 Comparable 인터페이스의 compareTo() 메서드 구현 내용대로 오름차순으로 정렬된다.

    이 예제에서는 TreeSet 클래스 생성자에 Comparator 인터페이스를 구현한 MyCompare 인스턴스를 매개변수로 넣었기 때문에, 재정의한 compare() 메서드 방식에 따라 내림차순으로 정렬 방식이 바뀌는 것.

     

    반응형