본문 바로가기
TIL/Java | Spring Boot

2021.12.31 TIL : [Java] 컬렉션과 제네릭 (Collection & Generic) - 2

by yeon_zoo 2022. 1. 1.

지난 번에 쓴 글에서 컬렉션과 제네릭의 기초에 대해 정리했다. 이번에는 HashMap과 LinkedList, 그리고 Collections 클래스 활용에 대해서 그리고 사용자가 직접 만드는 제네릭에 대해 알아보았다.

 

HashMap<K, V> 컬렉션

HashMap<K, V>컬렉션의 경로명은 java.util.HashMap이며 키와 값의 쌍으로 이루어졌다. K는 '키'로 사용할 데이터 타입을, V는 값으로 사용할 데이터 타입을 넣어주면 된다. 새로운 키-값 쌍을 넣어줄 때에는 put, 키에 대한 값을 불러올 때는 get을 이용해주면 된다.

HashMap<String, String> h = new HashMap<String, String>();
h.put("apple","사과");
String kor = h.get("apple");
System.out.println(kor);

해시맵은 해시 함수를 이용해서 각 키와 값이 저장되는 위치를 결정하므로, 사용자 입장에서는 그 위치를 알 수 없고 삽입되는 순서와 들어있는 위치도 관계가 없다. 이는 해시 구조에 대해서 배우면서도 사용한 개념을 미리 만들어둔 클래스라고 이해하면 된다. 

 

 

해시맵의 장단점

해시맵은 Vector<E>나 ArrayList<E>와는 다른 점이 있다. 먼저 요소의 삽입과 삭제의 시간이 굉장히 빠르다. 요소의 위치를 결정하는 해시 함수가 간단한 코드로 이루어지며, Vector<E>나 ArrayList<E>와 달리 요소의 삽입 삭제 시 다른 요소들을 밀거나 당겨 움직일 필요가 없기 때문이다. 

두번째로 요소 검색은 더욱 빠르다. get(key) 메소드가 호출되면 해시 함수가 key가 저장된 위치를 단번에 찾아내므로 Vector<E>나 ArrayList<E>에서처럼 모든 요소들을 하나씩 비교할 필요가 없다. 

하지만 해시맵은 인덱스를 이용해서 요소를 찾을 수 없고 오직 '키'만을 이용해야 한다는 단점이 있다. 

 

다음은 해시맵에 사용될 수 있는 메소드이다.

메소드 사용법
void clear() 해시맵의 모든 요소 삭제
boolean conatinsKey(Object key) 지정된 키를 포함하고 있으면 true
boolean containsValue(Object value) 지정된 값에 일치하는 키가 있으면 true
V get(Object key) 지정된 키의 값 리턴, 키가 없으면 null 리턴
boolean isEmpty() 해시맵이 비어있으면 true 리턴
Set<K> keySet() 해시맵의 모든 키를 담은 Set<K> 컬렉션 리턴
V put(K key, V value) key와 value 쌍을 해시맵에 저장
V remove(Object key) 지정된 키(key)를 찾아 키와 값 모두 삭제
int size() HashMap에 포함된 요소의 개수 리턴

해시맵을 활용한 예시는 다음과 같이 될 수 있다.

import java.util.*;
class Student{
    private int id;
    private String tel;
    public Student(int id, String tel) {this.id = id; this.tel = tel;}
    public int getId() {return id;}
    public String getTel() {return tel;}
}

public class practice_7 {
    public static void main(String args[]){
        var map = new HashMap<String, Student>();
        map.put("김민규", new Student(1, "010-1111-1111"));
        map.put("이석민", new Student(2, "010-2222-2222"));
        map.put("윤정한", new Student(3, "010-3333-3333"));
        map.put("홍지수", new Student(4, "010-4444-4444"));

        Scanner scanner = new Scanner(System.in);
        while(true){
            System.out.print("검색할 이름?");
            String name = scanner.nextLine();
            if (name.equals("exit"))
                break;
            Student student = map.get(name);
            if (student == null)
                System.out.println(name + "은 없는 사람입니다");
            else
                System.out.println("id:"+student.getId() + ", 전화:" +student.getTel());
        }
    }
}

 


Collections 클래스 활용

java.util 패키지에 포함된 collections 클래스는 컬렉션을 다루는 유용한 메소드를 다음과 같이 지원한다.

sort() 컬렉션에 포함된 요소들의 정렬
reverse() 요소를 반대 순으로 정렬
max(), min() 요소들의 최대값, 최소값 찾아내기
binarySearch() 이진 검색

Collections 클래스의 메소드는 모두 static 타입이므로 Collections 객체를 생성할 필요는 없다. 이 유틸리티 메소드들은 인자로 컬렉션 객체를 전달받아 처리한다. 

 

Collections 클래스의 활용 코드

import java.util.*;
public class practice_8 {
    static void printList(LinkedList<String> l){
        Iterator<String> iterator = l.iterator();
        while(iterator.hasNext()){
            String e = iterator.next();
            String separator;
            if (iterator.hasNext())
                separator = "->";
            else
                separator = "\n";
            System.out.print(e+separator);
        }
    }
    public static void main(String args[]){
        var myList = new LinkedList<String>();
        myList.add("아이언맨2");
        myList.add("스파이더맨");
        myList.add("토르-라그나로크");
        myList.add(0, "아이언맨1");
        myList.add(2, "아이언맨3");
        printList(myList);

        Collections.sort(myList);
        printList(myList);

        Collections.reverse(myList);
        printList(myList);

        int index = Collections.binarySearch(myList, "아이언맨3") + 1;
        System.out.println(index);
    }
}

제네릭 만들기

제네릭 클래스를 프로그래머가 직접 작성하는 방법은 기존의 클래스 작성 방법과 비슷하다. 클래스 이름 다음에 일반화된 타입의 매개변수를 <> 사이에 추가한다는 차이가 있다. 다음은 간단한 제네릭 클래스 작성 예시이다.

public class MyClass<T>{ //T는 타입 매개변수
    T val;
    void set(T a){
        val = a; // T 타입의 값 a를 val에 지정
    }
    T get(){
        return val;
    }
}

제네릭 클래스의 레퍼런스 변수를 선언할 때 다음과 같이 타입 매개변수에 구체적인 타입을 적으면 된다. 

MyClass<String> s; // <T>를 String으로 구체화

제네릭 객체 생성할 때, 클래스에 구체적인 타입을 대입하여 구체적인 객체를 생성하는 과정을 구체화라고 한다.이 과정은 자바 컴파일러에 의해 이루어진다. MyClass<T>에 T에 구체적인 타입을 지정하여 객체를 작성하는 예는 다음과 같다. 

MyClass<String> s = new MyClass<String>();
s.set("hello");
System.out.println(s.get()); // "hello" 출력

이런 코드를 이용하면 public class MyClass<T>의 정의된 부분에서 T가 String이라고 생각하면 된다. 마찬가지로 여기서도 기본 데이터 타입 (int 등)으로 사용할 수 없고 Integer 이런 식으로 표현해야 한다. 

 

제네릭 클래스 생성 및 활용 예시 코드

class GStack<T>{
    int tos;
    Object [] stck;
    public  GStack(){
        tos = 0;
        stck = new Object[10];
    }
    public void push(T item){
        if (tos == 10)
            return;
        stck[tos] = item;
        tos++;
    }
    public T pop(){
        if (tos == 0)
            return null;
        tos--;
        return (T)stck[tos]; // 타입 매개변수 타입으로 캐스팅
    }
}

public class practice_9{
    public static <T> GStack<T> reverse (GStack<T> a){
        GStack<T> s = new GStack<T>();
        while(true){
            T tmp;
            tmp = a.pop();
            if (tmp == null)
                break;
            else
                s.push(tmp);
        }
        return s;
    }
    public static void main(String args[]){
        var gs = new GStack<Double>();

        for (int i=0; i<5; i++){
            gs.push((double) i);
        }
        gs = reverse(gs);
        
        for (int i=0; i<5; i++){
            System.out.println(gs.pop());
        }
    }
}

 

제네릭은 동적으로 타입이 결정되지 않고 컴파일 시에 타입이 결정되므로 보다 안전한 프로그래밍이 가능하다는 장점이 있다. 또 런타임 타입 충돌 문제를 방지할 수 있고, 개발 시 타입 캐스팅 절차가 불필요하다. 

댓글