반응형
반응형

필자의 주관적인 생각과 이해를 바탕으로 작성된 글입니다. 잘못된 부분이 있거나 있다면 댓글로 피드백 부탁드립니다!

 

개요

 

HashMap은 Key, Value 데이터쌍을 저장하는 자료구조로 익히 알고있다. 속도면에서 장점을 갖고 있어 코딩테스트에서도 많이 활용된다. 도대체 어떻게 생겨먹은 녀석이길래 이렇게 빠른지 알아보자.

 


HashMap = Hash + Map

 

Map
Key, Value 쌍으로 이루어진 자료형.
순서를 보장하지 않음.
키는 중복이 허용되지 않음.

 

Hash
해시 함수를 사용하여 임의의 길이를 가진 데이터를 고정된 길이를 가진 데이터로 매핑한 값

 

 

즉, HashMap이란 Map은 Map인데 Hash를 활용한 Map인 것이다.


Map은 어떻게 Key, Value 쌍으로 관리할 수 있을까? 🤔

HashMap을 이해하기 위해선 Map과 Hash에 대해 이해해야한다. 먼저 Map이 어떻게 Key와 Value 쌍으로 관리할 수 있는 이유는 내부적으로 Key와 Value를 담을 수 있는 배열 타입으로 데이터를 관리하고 있기 때문이다.

 

 먼저 일반적인 배열의 형태를 생각해보자. 인덱스마다 하나의 값을 넣을 수 있는 자료구조인데 Key와 Value를 둘 다 넣는다는 건 말이 되지 않아보인다. 그런데 배열의 인덱스에 Key값을 넣는다면 얘기가 된다. 0이라는 Key에 대한 Value는 010-1111-1111, 1이라는 Key에 대한 Value는 010-2222-2222로 관리한다고 가정한다면 아래와 같이 배열의 인덱스에는 Key 값을 넣어 관리할 수 있다.

 

배열의 Index를 키로 활용한다.


 

배열의 인덱스를 키로 활용한다고? 그럼 Key는 정수만 가능하잖아... 🤔

 그렇다. HashMap의 Key는 정수 뿐 아니라 모든 타입의 인스턴스가 들어올 수 있다. 그럼 인스턴스를 정수로 변환할 수 있다면 어떨까? 그럼 Key로 들어온 모든 인스턴스는 Key로 사용 가능하게 된다. HashMap의 Hash 의미를 여기서 알 수 있다. 키로 들어온 값을 해시함수를 통해 해시화 시키고, 이를 배열의 인덱스로 사용하는 것이다! 여기서 사용되는 해시함수는 들어온 인스턴스의 hashCode() 메서드이다.

 


 

그럼 HashMap 은 이렇게 생겼나요? (상상)

hashMap.put("Sim","010-1111-1111");
hashMap.put("Park","010-2222-2222");
hashMap.put("Hong","010-3333-3333");

 

String 타입의 Key와 Value를 저장하기 위해 String 타입의 HashMap을 생성하고 위 코드를 실행시킨다고 가정해보자. Sim, Park, Hong에 대한 해시 값이 아래와 같다.

해시 값

 

지금까지의 설명을 토대로 HashMap의 구조를 상상해보면 다음과 같을것이다. 만약 새로운 Key, Value 쌍이 들어온다면 해시값과 Size 나머지 연산을 통해 구한 인덱스에 Value가 추가될것이다.

 

상상속의 HashMap

 


 

갑자기 % Size는 뭐야? 🤔

 Hash Func, 즉 해시함수를 통해 해시 코드를 구하고 이를 Size 로 나머지 연산(실제로는 시프트 연산)을 한다. 나머지 연산을 하는 이유는 배열의 인덱스 중 하나로 매핑시키기 위함이다.

 예를들어 Size가 10인 배열은 0~9까지의 인덱를 갖는다. 해시 값은 hashCode() 메서드 뿐 아니라 부가적인 연산도 함께 수행되어 구해지는데 아래와 같이 큰 숫자의 정수형이 리턴된다.

특정 값에 대한 해시값

 

  만약 3288449라는 값을 배열의 인덱스로 사용한다면 최소 3288449 크기의 배열을 생성해야한다. 메모리를 많이 차지할것이다. 때문에 이 값을 배열의 Size로 나눈 나머지를 구하고 이를 Index로 사용하는 것이다. 만약 HashMap 내부 배열 Size가 10이라면 3288449 % 10 = 9. 즉 9라는 인덱스를 갖게 된다.


실제로는 이렇지 않아요. 배열은 배열인데 Node 타입의 배열이랍니다. 🤭

 

실제로 데이터가 저장되는 곳은 제네릭 타입, Object 타입의 배열일까? 모두 아니다 Node 타입의 배열에 저장된다. HashMap에 선언된 Node 타입 변수 및 클래스이다.

 

transient Node<K,V>[] table; // hashMap 클래스 내에 선언되어있어요

 

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
    
    ...
    
}

 

 멤버필드로 hash, key, value, nextNode 가 존재한다. 사실 Index와 Value로만 Key, Value 쌍을 관리하면 문제가 많다. 해시가 충돌될 경우 처리도 못하고, 리사이징과 해시 재배치 시 문제가 된다. (문제가 되는 이유는 아래에서 설명하도록 하겠다!)


Index랑 value 만 관리하면 될 줄 알았는데 아니네요? 🤔

hash, key, value, nextNode 필드를 갖는 Node 타입의 인스턴스로 관리되는 이유를 HashMap의 리사이징과 재배치, 충돌 우회 전략과 함께 이해해보자.


리사이징

리사이징
새로운 길이의 배열을 생성한 후 데이터를 이관시킴으로써 결과적으로 배열의 사이즈를 변경시키는 작업

 

 배열의 사이즈는 초기화 시 정해진다. 기본 생성자를 통해 HashMap을 생성한 후 put 메서드를 실행하면 기본사이즈인 16 사이즈의 Node 배열이 생성되고 리사이징 임계 값으로 12(0.75*16)가 결정된다. 여기서 리사이징 임계값이란 배열을 리사이징하는 기준값을 뜻하며 현재 배열길이의 두 배로 리사이징한다. 12개를 초과할 경우 배열을 16의 2배인 32 사이즈로 리사이징 하는것이다.

  ArrayList도 내부적으로 배열을 사용하고, 배열에 더 이상 들어갈 공간이 없을 경우 현재 사이즈의 절반 사이즈를 추가한 새로운 배열로 리사이징하는데 이와 같은 이치이다.

최초 put 메서드 실행 시

 


재배치

재배치
배열의 사이즈가 변경될 때 기존 데이터들의 Index를 재배치하는 작업

 

 리사이징과 이어지는 내용이다. 리사이징을 할 경우 두 배 사이즈로 배열을 재생성하고 데이터를 이관시킨다고 했다. 이는 기존에 저장되어 있던 Node들의 Index가 재배치되어야 함을 의미한다. Index를 구하는 공식은 hashCode % Size 이므로  Size가 바뀐다면 Index도 바뀌어야하기 때문이다. 예를들어 사이즈가 10 일때 해시값 5755151를 통해 구한 Index는 1이지만, 사이즈가 20일 경우 Index는 11이 된다.


Node에서 hash 값이 관리되는 이유

 이 재배치 작업 때 해시 값을 가져와 연산을 해야하는데 해시 값을 hash에 저장해놨기 때문에 나머지 연산만 하면 된다. 만약 해시 값이 없다면 해시 값 추출을 위해 존재하는 데이터 수 만큼의 해시함수 연산을 해야할것이다.

 


충돌 우회 전략 - Separate Chaining

 지금 Index 기반으로 값을 넣고 있는데 과연 Index가 충돌할 확률은 대략적으로 어느정도일까? 사이즈에 대한 나머지를 Index로 사용하므로 배열 사이즈가 20이라면, Index가 중복될 확률은 최소 20분의 1이 된다.

어찌됐든 충돌이 일어날 수 있는 상황이다. HashMap은 이런 충돌에 대한 우회 전략으로 Separate Chaining 방식을 사용하며, 이 전략을 위해 nextNode를 사용한다.

Separate Chaning 
동일한 해시값이 이미 존재할경우 LinkedList로 관리한다. 즉, Node에 있는 필드 중 nextNode를 활용하여 중복된 해시 값에 대한 Value를 관리하는 것이다.

 

 

 그런데 만약 동일한 Key 값이 들어왔다면 어떨까? Sim이라는 Key값이 들어있는 HashMap에 Sim이라는 Key 값으로 다른 Value를 넣는 것은 전혀 문제되지 않는다. 이 경우 Index에 대한 Value가 덮어씌워져야한다. 즉, 동일한 Key가 들어왔는지를 확인하려면 Index 값만 비교하는 게 아니라 실제 Key 값도 비교해봐야한다.

 


Node에서 key, nextNode 값이 관리되는 이유

  동일한 Key가 들어왔는지 확인하고, Linked List 형태로 우회하는 Separate Chaining 전략을 사용하기 위해 key와 nextNode가 관리된다.


Separate Chaining 동작원리

충돌 발생! 비이상!

 

 

 해시함수로 해시값을 구하고 나머지 연산으로 추출한 Index 가 충돌할 경우를 가정했다.

충돌이 일어나면 들어온 Kim에 대한 해시 값과 키 값을 충돌한 Node의 값과 비교한다. 다를 경우 Separate Chaining 전략에 따라 key, value, hash, nextNode를 갖는 Node 인스턴스를 생성하여 NextNode에 할당한다. 만약 비교한 Key와 Hash 값이 같았다면 중복된 Key가 들어온 것이므로 해당 Node의 Value 값을 새로 들어온 Value 값으로 수정한다.


그럼 NextNode에 추가된 Kim을 조회할 땐 어떻게 동작할까?🤔

HashMap.get("Kim")

 

 

NextNode에 추가된 Kim에 대한 Value 값 조회를 시도하면 다음 과정을 수행하게 된다.

 

1. Hash Func % Size 연산을 통해 Index를 구한다.

2. Index 에 매핑된 노드가 존재하는지 확인한다.

3. 매핑된 노드가 존재하므로(충돌) 해당 노드의 Key, Hash 값과 요청으로 들어온 Key, Hash 값을 비교한다.

4. Kim과 Sim의 Key와 Hash 값이 다르므로 해당 노드의 nextNode가 있는지 존재한다.

5. nextNode가 존재하므로 해당 Node를 참조한다.

6. nextNode에 저장된 Key와 Hash 값이 들어온 Key와 Hash 값과 일치하므로 이 노드에 대한 Value 값을 리턴한다.

 


내부 구조를 이해한 후 다시 생각해본 HashMap의 장점 

 

1. 조회가 빠르다.

조회 시 Key 값에 해시 및 나머지 연산만 하면 Index를 구할 수 있고, Index 기반으로 접근하니 당연히 조회 속도가 빠를수밖에 없다.

 

2. 저장, 삭제도 ArrayList보다 빠르다.

저장은 Key에 대한 Index를 구한 후 값을 넣기만 하면 되고, 삭제도 Key에 대한 Index를 구하고 삭제하면 된다. ArrayList의 경우 순서를 유지해야 하기 때문에 중간에 값을 삭제할 경우 빈자리를 채우기 위한 이동 연산이, 등록할 경우 빈자리를 만들기 위한 이동 연산이 수행되어야하는데 말이다.

 


내부 구조를 이해한 후 다시 생각해본 HashMap의 단점

 

1. 너무 많은 저장이 일어날 경우 오히려 속도가 느려진다.

 저장이 많아지면 그만큼 리사이징과 재배치작업이 많아지기 때문이다. 만약 저장해야할 데이터가 많다면 HashMap의 사이즈를 너프하게 잡는것도 좋은 방법이다.

반응형
반응형

1. 개요

 코딩테스트를 하면 자주 나왔던 Iterator. 이게 무엇인지, 또 왜 사용하는지 알아보았다.


2. Iterator란?

 Iterator란 자바의 컬렉션(Collection)에 저장되어 있는 요소들을 순회하는 인터페이스이다.


3. Collection?

 Collection이란 자바에서 제공하는 자료구조들의 인터페이스로 List, ArrayList, Stack, Quque, LinkedList 등이 이를 상속받고있다. 즉, 이러한 컬렉션 인터페이스를 상속받는 클래스들에 대해 Iterator 인터페이스 사용이 가능하다.

Collection 구조 / 출처 : 위키백과


4. 사용 이유

 컬렉션 프레임워크에 대해 공통으로 사용이 가능하고 사용법이 간단하기 때문이다.

 저 위 그림에 나와있는 클래스, 인터페이스에서 모두 사용이 가능하다.

 

 Iterator를 사용하려면 정의 방법과 메서드 3개만 알면 된다.

 

 정의방법은 Iterator<T> iterator = Collection.iterator(); 이고,

 메서드는 다음 요소가 있는지 판단하는 hasNext(), 다음 요소를 가져오는 next(),  가져온 요소를 삭제하는 remove()가 끝이다. 아래 예제를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class IteratorTest {
 
    public static void main(String[] args) {
        
        List<Integer> list = new ArrayList<Integer>();
 
        for(int i = 0;i <= 100; i++) {
            list.add(i);
        }
        
        Iterator<Integer> iter = list.iterator();
        
        while(iter.hasNext()) {
            int data = iter.next();
            System.out.print(data);
        }
        
    }
    
}
cs

5~9번 라인에서 Collection 인터페이스를 상속받는 ArrayList 객체를 생성하고 0부터 100 값을 add 한다.

11번 라인에서 Iterator<T> iterator = Collection.iterator(); 형식에 맞게 Iterator<Integer> iter = list.iterator(); 를 사용하여 Iterator를 참조한다.

13번 라인에서 hasNext() 메서드를 사용하여 다음 요소가 있는지 확인한다. (있으면 true, 없으면 false를 반환)

14번 라인에서 next() 메서드를 사용해 다음 요소의 값을 조회한다.


5. Iterator과 반복문

 Iterator를 통한 순회는 반복문을 통한 순회와는 메모리적으로 중요한 차이가 있다.

LinkedList를 통해 예를 들어보겠다.

 

1
2
3
4
5
6
7
8
9
10
11
    public void linkedListTest() {
        LinkedList<Integer> list = new LinkedList<Integer>();
        
        for(int i = 0;i <= 100; i++) {
            list.add(i);
        }
        
        for(int i = 0; i<= 100; i++) {
            list.get(i);
        }
    }
cs

 

Linked List의 메모리구조

add 메서드를 이용해 데이터 입력이 다 끝나면 위 그림과 같은 구조가 된다.

그리고 get(0)부터 get(100)까지를 수행하게 되는데 이는 0부터 100까지 총 101번의 요소를 조회하는게 아니다.

get(int index) 메서드는 시작 주소부터 index 만큼 요소들을 밟아가며 조회하는 메서드이기 때문이다.

만약 5번째 값을 조회한다면 처음 시작주소부터 시작하여 다음주소를 타고... 타고.. 를 총 5번 반복해야한다.

get 메서드가 실행되며 i 값이 증가할 때마다 메모리적으로 조회해야 하는 요소는 1번, 2번, 3번, 4번... 101번까지 증가하는 것이다. 총 5151번을 조회해야 한다.

 

이에반해 Iterator는 1부터 101번째까지의 요소에 대해 내부적으로 객체로 생성한 후 순차적으로 조회한다.

처음 주소로 돌아갈 필요가 없기때문에 next 메서드를 통해 조회 시 요소의 개수인 101번만 조회를 하게된다.

 

그렇다면 드는 생각. 속도면에서 훨씬 빠르지않을까?

 

훨씬 빠를것이라고 생각했으나... Iterator를 구현하기 위해 객체를 생성하는 부분에서 시간이 더 걸린다고 한다.

물론 그 차이는 크지 않지만...

 

결론.

 Iterator는 컬렉션 프레임워크에 대한 인터페이스이고, 사용법이 쉽다.

 하지만 반복문보다 속도면에서 조금 느리다는 평이 있다.

반응형
반응형

1. 개요

 - List에 대해 알고, 배열과 연결리스트의 개념과 차이를 이해한다.

 

2. 리스트(List)란?

 - 순서가 의미를 갖는 데이터들의 집합이다.

 - 삽입, 삭제, 검색 등의 기본적인 연산이 가능한 자료구조이다.

 - 리스트를 구현하는 대표적인 두가지 방법은 배열과 연결리스트이다.

 

즉, 리스트란 첫번째에는 A가, 두번째에는 B가 세번째에는 C가 들어있는 것처럼 순서마다 특정 데이터를 갖고 있고, 이 순서를 통해 삽입, 수정, 삭제, 검색을 할 수 있는 자료구조를 말한다.

 

3. 배열이란?

 - 같은 종류의 데이터들이 메모리상에 순차적으로 저장되어 있는 자료구조이다.

 - 같은 종류의 데이터들이기때문에 각각 데이터들의 크기가 같고, 메모리상에 순차적으로 저장되어 있기 때문에 주소 값 계산이 쉽고, 랜덤 액세스가 가능하다.

 - 조회할때는 빠르나 중간 데이터를 삭제, 추가 시에는 시간이 많이 걸린다.

 - 배열의 구조는 다음과 같다.

배열의 구조

 - int 형은 정수형 자료형으로 4Byte이다. 주소를 보면 4byte 간격이란 것을 알 수 있다. 내가 배열의 5번째 값을 조회하고 싶다면 처음 주소 값에서 자료형 크기 * 5 해준 값(주소값)을 구한 후 바로 접근하면 된다. 즉, 배열에서 특정 데이터를 조회하려면 자료형과 인덱스를 통해 주소 값 계산 후 바로 접근한다. (참고로 이 방식이 랜덤 액세스 방식이다.) 대신 중간에 데이터를 추가하거나 삭제 한다면 빈 자리를 매꾸기 위해 아주 많은 데이터들이 움직여야한다. 예를들면, 3번째 인덱스에 위치한 4000 값을 삭제한다면, 5000이 그 그 자리로 이동해야하고, 5000 자리로는 6000이 이동해야하고... 이게 끝까지 반복되어야한다. 만약 100000개 크기의 배열에서 1번째 값을 삭제한다면 100000-1번 움직여야한다.

 

4. 연결리스트

 - 같은 종류의 데이터들이 메모리상에 비순차적으로 저장되어 있는 자료구조이다.

 - 크기의 제한이 없다.

 - 다른 데이터의 이동 없이 중간에 삽입, 삭제가 가능하다. 

 - 랜덤 엑세스가 불가능하다.

 - 연결리스트의 구조는 다음과 같다.

 - 연결리스트의 값에는 총 2가지 정보가 들어간다. 하나는 메모리 주소에 해당하는 데이터. 하나는 다음 주소 값이다. 그리고 이 주소 값을 거칠때마다 인덱스가 증가한다. 예를들어 이 연결리스트의 2번째 인덱스에 해당하는 값을 조회하려면 시작주소인 00ffff00에 저장된 다음 주소인 00ffff08이 된다. 만약 100000개 크기의 연결리스트에서 100000번째 값을 조회하려면 주소 계산이 되지 않기 때문에 무작정 메모리 시작주소부터 다음 주소를 100000번 까야한다. 즉 조회에 시간이 오래걸린다. 대신, 어떤 값을 삭제하거나 추가할때에는 메모리를 찾은 후 다음주소만 바꿔주면 되기 때문에 배열에 비해 시간이 적게 걸린다. 예를들어 3번째와 4번째 사이에 3500을 추가하고싶다면 먼저 3번째 주소를 찾은 후(00ffff18) 메모리 빈 공간(00ffff10)에 3500값을 만들고 3500의 다음 주소를 3번째 인덱스의 다음 주소로 저장한다. 그리고 3번째 인덱스의 다음 주소에는 3500값의 주소로 저장한다.

 

3500 추가

 

5. 마치며

 가려운 등이 긁힌 느낌이다.. java에서 왜 배열 길이를 선언하고 후에 못늘리는지,(메모리에 순차적으로 들어가기 때문에 나중에 길이를 늘리게 되면 현재 다른 데이터를 위해 할당된 메모리 주소를 침범할 수 있기 때문) 굳이 길이 제한이 없는 ArrayList같은 연결리스트를 놔두고 왜 배열을 쓰는지 (랜덤 액세스로 조회 시간이 엄청 짧음), 그리고 이 둘의 차이, 메모리 구조 등을 확실히 이해할 수 있는 좋은 공부였다. 너무좋다...ㅎㅎ

반응형
반응형

1. 개요

Generic 프로그래밍, Generic 프로그래밍을 사용하는 이유를 예제를 통해 알아보자.


2. Generic 프로그래밍이란?

 제네릭 프로그래밍이란 하나의 데이터가 특정 데이터 타입에만 종속되지 않고 여러 데이터 타입을 가질 수 있는 기술에 중점을 두어 재사용성을 높일 수 있는 프로그램 방식이다. - 위키백과

 

 무슨 말인지 이해가 잘 안간다. 일단 Generic이 무슨 의미를 갖는지 확인해보자.

 

 Generic의 사전적 의미는 '포괄적인, 총칭, 일반적인' 이다.

 

 이를 Generic 프로그래밍의 정의와 혼합하여 생각해보니 '데이터를 포괄적으로 사용할수 있도록 하는 프로그래밍, 어떤 데이터 타입도 가질 수 있도록 일반화시키는 프로그래밍' 으로 이해를 해보았다.

 

 그렇다면 데이터를 포괄적으로 사용한다는 것이 프로그래밍에서 어떤 의미를 가질까?

 어떤 데이터가 A가 될수도, B가 될수도, C가 될수도 있다라고 생각이 되는데... 예제를 통해 이해해보자.


3. 예제

Box.java

1
2
3
4
5
6
7
8
9
10
11
12
public class Box<T> {
 
    private T t;
    
    public void set(T t) {
        this.t = t;
    }
    
    public T get() {
        return t;
    }
}
cs

이 Box 클래스는 제네릭한 클래스이다. T란 녀석이 중간 중간 껴있는 것을 확인할 수 있는데,

T는 내가 생성한 클래스도, 타입도 아니다. Generic 한 클래스를 만들기 위해 사용하는 제네릭 변수이다.

 

Box 객체 생성시 제네릭 변수 T로 들어온 데이터 형을 아래와 같이 변환(?) 시켜준다.

main 메서드의 예제를 보면 이해할 수 있을 것이다.

Generic Class

 

 

Main.java

1
2
3
4
5
6
7
8
9
10
11
public class Main {
 
    public static void main(String[] args) {
        Box<Integer> box = new Box<Integer>();
        box.set(10);
        
        Integer i = box.get();
        System.out.println(i.intValue());
    }
}
 
cs

다음과 같이 Box<Integer> box = new Box<Integer>() 로 생성하면, 앞서 이미지의 T 값이 Integer가 되는 것이다.

그럼 Box 클래스는 Integer 타입의 데이터를 관리할 수 있는 객체로 활용되게 된다.

 

Box<String> box = new Box<String>() 으로 생성하면 Box 클래스는 String 타입의 데이터를 관리하고, Human 으로 생성하면 Human 타입의 데이터를 관리하게 된다. 이제 Box라는 클래스가 특정 데이터 타입에 종속되지 않는 Generic한 클래스가 된 것이다.

 

그래도 이해가 잘 안된다면 ArrayList를 생각해봐도 좋을 것 같다.

ArrayList 생성 시 new ArrayList<String>, new ArrayList<Integer>, new ArrayList<Human> 형태로 작성하게 되는데, 입력한 데이터 타입에 따라 add할 수 있는 데이터 타입이 정해지게 된다. Generic과 비슷한 형태와 성질을 띄는 것 같지 않는가?

 

덧붙이면 예제에는 제네릭 변수를 T로 사용했는데 굳이 T가 아니어도 된다. K, V, A 등의 값으로 사용해도 된다.

 

Pair.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Pair<K,V> {
 
    private K key;
    private V value;
    
    public void set(K key, V value) {
        this.key = key;
        this.value = value;
    }
    
    public K getKey() {
        return key;
    }
    
    public V getValue() {
        return value;
    }
}
cs

Pair 클래스의 제네릭 변수를 K, V로 하여 클래스를 작성하였다.

그리고 main 메서드에서 K에는 String 타입을, V에는 Integer 타입으로 Pair 객체를 생성하였다.

 

1
2
3
4
5
6
7
8
9
10
public class Main {
 
    public static void main(String[] args) {
 
        Pair<String, Integer> pair = new Pair<String, Integer>();
        pair.set("test"9);
        System.out.println(pair.getKey()); // test
        System.out.println(pair.getValue()); // 9
    }
}
cs

 

그런데 생각해보니 Object로 대체해도 Generic한 클래스가 되지 않을까?

Box 클래스의 T를 빼버리고, Object 타입으로 넣으면 Object 타입은 모든 클래스의 슈퍼클래스이기 때문에 정형화시킬 수 있지 않는가? 그런데 왜 굳이 Object를 쓰지 않고 Generic을 사용할까?

 


4. Generic 사용 이유

Box.java

1
2
3
4
5
6
7
8
9
10
11
12
public class Box {
 
    private Object t;
    
    public void set(Object t) {
        this.t = t;
    }
    
    public Object get() {
        return t;
    }
}
cs

제네릭 변수를 빼고 Object로 치환하였다.

 

Main.java

1
2
3
4
5
6
7
8
9
10
11
12
public class Main {
 
    public static void main(String[] args) {
 
        Box box = new Box();
        box.set(10);
        
        //Integer i = box.get(); // 컴파일 에러
        Integer i = (Integer) box.get();
        System.out.println(i.intValue());
    }
}
cs

그리고 main 메서드에서 Box 객체를 생성하였더니 box.get() 부분에서 컴파일 에러가 발생하였다.

Integer i = box.get() 은 슈퍼클래스인 Object를 서브 클래스인 Integer가 참조하려는 형태이기 때문에 에러가 발생한다.

그래서 강제로 캐스팅하는 코드를 추가해주고 있다.

 

반면에 Generic 클래스를 사용하면 캐스팅하는 코드가 없다. Object를 사용하는 것보다 효율적이다.

 

추가적으로 Generic을 사용하면 컴파일 시점에 잡을 수 없었던 타입 에러를 검출 할 수 있다. 

 

Main.java

1
2
3
4
5
6
7
8
9
10
11
12
public class Main {
 
    public static void main(String[] args) {
 
        Box box = new Box();
        box.set(10);
        
        //Integer i = box.get(); // 컴파일 에러
        String i = (String) box.get();
        System.out.println(i);
    }
}
cs

다음과 같이 9행을 String으로 수정하였다.

box.get 이라는 메서드는 Object 형태의 데이터를 return 하기 때문에 컴파일 시점에서는 Integer로 받아도, String으로 받아도 상관없다. 그래서 컴파일 에러가 발생하지 않는다. 하지만 런타임 시 10이라는 정수 타입의 변수라 들어가게 되고, 9번 라인에서 Integer를 String 타입으로 변환하려 하니 castException이 발생하게 된다.

castException

Generic은 캐스팅 할 필요가 없기 때문에 이러한 문제가 발생하지도 않는다.

 

이처럼 Generic은 Object를 사용하는 방법보다 효율적임을 알 수 있다.

 


5. 마치며

오늘은 Generic에 대한 개념을 잡자는 목적으로 포스팅을 하였는데, 목적을 달성했다는 느낌이 든다.

기부니가좋다.

 

반응형
반응형

1. 개요

 추상클래스와 인터페이스에 대해 알아보자.


2. 추상클래스란?

 - 추상(abstract) 클래스란 추상 메서드를 포함한 클래스이다.

 - 추상(abstract) 메서드란 선언만 있고, 구현이 없는 메서드이다.

 - 추상 메서드가 하나라도 선언되어있으면 객체를 만들 수 없기때문에 서브클래스를 만드는 용도로 사용된다.

 - 추상 메서드와 추상 클래스는 abstract 키워드로 표시한다.

 - 추상 클래스를 상속받는 클래스는 추상메서드를 구현해야한다.


3. 추상 클래스 사용 이유?

 객체도 못만들고, 번거롭게 abstract 키워드 추가하고, 구현하지 않는 추상메서드를 굳이 선언... 왜 쓸까?

 라는 의문이 들기 마련이다. 말보다는 직접 코드로 경험하면 이해할 수 있을 것이다. 다음 상황을 생각해보자.


4. 상황

 찰흙을 빚어 도형을 만들고, 만든 도형을 전시하는 프로그램을 구현한다. 이에 대해 요구사항을 부여하였다.

 1) 삼각형, 사각형, 원 모양의 도형을 만들 수 있다.

 2) 내가 만든 도형에 대해 관리 및 조회할 수 있다.

 3) 도형 전시를 위해 해당 도형의 넓이를 알아야 한다.

 

 직접 코딩을 하기 전에 생각해보자. 

 1번을 처리하기 위해서는 삼각형, 사각형, 원에 대한 클래스가 필요하다.

 2번을 처리하기 위해서는 내가 만든 도형을 배열이나 리스트로 관리해야한다. 삼각형 배열에는 삼각형 객체를, 사각형 배열에는 사각형 객체를, 원 배열에는 원 객체를 넣어 관리해도되지만, 3개의 배열을 각각 선언해야 하니 효율적으로 느껴지진 않는다. 그래서 도형이라는 클래스를 만들고 삼각형, 사각형, 원 클래스가 이를 상속하도록 구현한다. 이렇게 되면 도형이라는 자료형 하나로 모든 클래스를 관리할 수 있다.

 3번을 처리하기 위해서는 각 클래스마다 자신의 넓이를 구하는 메서드가 필요하다.

 이제 직접 예제를 통해 이를 구현해보겠다.


5. 예제

  

5.1. 클래스 생성

 - Triangle, Square, Circle, Figure(도형), Exhibition(전시), Main 클래스를 생성한다.

 - Triangle, Square, Circle은 Figure 클래스를 상속받는다.

 - 삼각형은 밑변, 높이. 사각형은 가로, 세로. 원은 반지름 값을 나타내는 멤버변수를 정의하고, 생성자를 생성한다.

 

5.2. 1번 요건 처리

 - 세 클래스를 다음과 같이 구현한다.

 - 클래스를 구현했으니 각 도형에 대한 객체를 만들 수 있게 되었다.

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
32
33
34
35
public class Triangle extends Figure{
 
    private int base;
    private int height;
    
    public Triangle( int baseint height) {
        this.base = base;
        this.height = height;
    }
}
 
 
/////////////////////////////////////
 
public class Square extends Figure{
 
    private int horizontal;
    private int vertical;
    
    public Square( int horizontal, int vertical) {
        this.horizontal = horizontal;
        this.vertical = vertical;
    }
}
 
/////////////////////////////////////
 
public class Circle extends Figure{
 
    private int redius;
    
    public Circle(int redius) {
        this.redius = redius;
    }
}
cs

 

5.3. 2번 요건 처리

 - 조회에 사용할 toString 메서드를 각 클래스에 추가한다.

 - 도형들을 관리할 Exhibition 클래스를 정의한다.

 

Trigangle.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Triangle extends Figure{
 
    private int base;
    private int height;
    
    public Triangle( int base, int height) {
        this.base = base;
        this.height = height;
    }
    
    public String toString() {
        return "class : Triangle , base : "+base + ", height : "+height; 
    }
}
cs

 

Square.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Square extends Figure{
 
    private int horizontal;
    private int vertical;
    
    public Square( int horizontal, int vertical) {
        this.horizontal = horizontal;
        this.vertical = vertical;
    }
    
    public String toString() {
        return "class : Square , horizontal : "+horizontal + ", vertical : "+vertical; 
    }
}
cs

 

Circle.java

1
2
3
4
5
6
7
8
9
10
11
12
public class Circle extends Figure{
 
    private int redius;
    
    public Circle(int redius) {
        this.redius = redius;
    }
    
    public String toString() {
        return "class : Circle , redius : "+redius; 
    }
}
cs

 

Exhibition.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Exhibition {
 
    private List<Figure> list;
    
    public void setList( List<Figure> list ) {
        this.list = list;
    }
    
    public void showList() {
        for(Figure figure : list) {
            System.out.println(figure.toString());
        }
    }
}
cs

 

이제 Main 클래스에 main 메서드를 생성하여 1,2 번 요건이 만족되도록 정의 후 실행시켜보자.

 

Main.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Main {
 
    public static void main(String[] args) {
        //삼각형, 사각형, 원 객체 생성
        Triangle triangle = new Triangle(4,3);
        Square square = new Square(5,5);
        Circle circle = new Circle(10);
                
        // 관리에 사용될 리스트 생성 및 추가
        List<Figure> list = new ArrayList<Figure>();
                
        list.add(triangle);
        list.add(square);
        list.add(circle);
                
        Exhibition exhibition = new Exhibition();
        exhibition.setList(list);
        exhibition.showList();
                
    }
}
cs

5~7번 라인에서 삼각형, 사각형 원 객체를 만든다.

10번 라인에서 도형을 관리할 list를 생성한다.

12~14번 라인에서 생성한 도형들을 list에 넣는다.

16~17번 라인에서 전시 객체를 생성하고 그 안에 만든 도형들을 set 한다.

18번 라인에서 도형들을 조회한다.

 

출력 결과를 확인해보면 다음과 같이 조회된다.

showList()

 

이제 마지막 요건인 도형의 넓이를 구하면 끝이다. 그리고 여기서 추상 클래스의 사용 이유에 대해 조금은 와닿을 수 있을 것이다.

 

5.4 3번 요건 처리

 - 각 도형마다 구하는 넓이 공식이 다르기때문에 삼각형, 사각형, 원 클래스에 넓이를 구하는 메서드를 각각 구현한다.

 - Exhibition 클래스에서는 구현한 메서드를 출력하는 메서드를 생성한다.

 

Triangle.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Triangle extends Figure{
 
    private int base;
    private int height;
    
    public Triangle( int base, int height) {
        this.base = base;
        this.height = height;
    }
    
    public String toString() {
        return "class : Triangle , base : "+base + ", height : "+height; 
    }
    
    public double getArea() {
        return base * height * 0.5;
    }
}
cs

 

Square.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Square extends Figure{
 
    private int horizontal;
    private int vertical;
    
    public Square( int horizontal, int vertical) {
        this.horizontal = horizontal;
        this.vertical = vertical;
    }
    
    public String toString() {
        return "class : Square , horizontal : "+horizontal + ", vertical : "+vertical; 
    }
    
    public double getArea() {
        return horizontal * vertical;
    }
}
cs

 

Circle.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Circle extends Figure{
 
    private int redius;
    
    public Circle(int redius) {
        this.redius = redius;
    }
    
    public String toString() {
        return "class : Circle , redius : "+redius; 
    }
    
    public double getArea() {
        //파이 값은 임의로 3.14로 가정한다.
        return redius * redius * 3.14;
    }
}
cs

 

Exhibition.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Exhibition {
 
    private List<Figure> list;
    
    public void setList( List<Figure> list ) {
        this.list = list;
    }
    
    public void showList() {
        for(Figure figure : list) {
            System.out.println(figure.toString());
        }
    }
    
    public void getFigureArea() {
        for(Figure figure : list) {
            //컴파일 발생
            System.out.println(figure.getClass().getSimpleName()+ " : "+ figure.getArea());
        }
    }
}
cs

 

 이로써 넓이를 구하는 것까지 구현했다고 생각했으나, Exhibition.java 코드의 18번째 라인에서 에러가 발생하게 된다.

발생 원인은 figure 객체에 getArea() 메서드를 찾지 못해서 발생한다.

하지만 분명 figure는 삼각형, 사각형, 원 클래스 중 한 객체를 참조하고 있고, getArea 메서드가 정의되어 있는데 왜 찾지 못하는 걸까? 바로 다이나믹 바인딩의 처리가 런타임시점에서 진행되기 때문이다.

 실제 코드를 돌리면 16번째의 figure 객체는 삼각형, 사각형, 원 객체 중 하나겠지만, 컴파일러 입장에서는 컴파일 시점에 이를 확인할 방법이 없기 때문에 에러가 뜨는것이다.

 

그럼 해결방안으로 Figure 클래스에 getArea 메서드를 생성하는 방법이 있다.

그렇게 되면 컴파일 시점에는 에러가 발생한 지점에서 getArea 메서드를 Figure 클래스의 메서드로 인식하여 에러를 떨구지 않을 것이고, 런타임 시에는 다이나믹 바인딩으로 인해 참조 객체의 getArea 메서드를 호출할 것이다.

 

하지만 Figure 클래스의 getArea가 전혀 사용되지 않는 상황인데 저렇게 메서드를 정의해두면, 불필요한 코드일 뿐더러, 타 개발자가 이를 봤을 때 당연히 이 메서드와 클래스가 어디서 사용되고 있을 것이라고 생각할 수 있다. 하지만 코드를 보면 실제로 Figure 클래스는 참조객체로써 사용되지 않고있다. 뭔가 혼란스럽지 않는가? 이러한 혼란 해소할 수 있는 것이 바로 추상 메서드, 추상 클래스이다.

 

 추상 메서드를 사용했기 때문에 Figure 클래스는 getArea 메서드를 선언만 해놓으면 된다. 그럼 타 개발자가 이 코드를 봤을 때 이 클래스가 객체로 생성되어 어디선가 사용되고 있지 않을까에 대해 생각할 필요가 없다. 개요에서 설명했듯이 추상클래스는 서브클래스를 위한 클래스이고, 객체로 생성이 불가능하기 때문이다. 또한 자식 클래스에서 정의해서 쓰고 있다라는 사실을 명확하게 알 수 있다.

 

Figure.java

1
2
3
4
5
public abstract class Figure {
 
    public abstract double getArea();
}
 
cs

 

실행화면

 

실행 결과를 통해 모든 요건을 만족하는것을 확인할 수 있다.


 

 

설명이 좀 중구난방 한것같지만 그래도 쥐어짜면서 글을 써내려가니 머릿속에서 확실히 조금 더 정리된 느낌이고, 이전에 공부했던 정형화나 다이나믹 바인딩도 복습할 수 있었다.

반응형
반응형

1. 개요

 상속 관계인 두 클래스의 메서드를 오버라이딩 한 후, 다형성의 원리를 적용하여 자료형과 참조 객체를 다르게 설정하였때 어떤 결과가 나올지 알아보았다.

 

 말이 조금 어렵게 느껴지니 예로 설명하면 다음과 같다.


 Parent라는 부모 클래스와 이를 상속받는 Child 자식 클래스가 있다.

 Parent 클래스에는 hi라는 메서드가 있기때문에 Child에서도 호출이 가능하나, 메서드를 재정의하고 싶어 메서드 오버라이딩을 하였다.

 객체 생성 시점에 Child child = new Child(); 형태로 생성 후 child.hi() 메서드를 호출해도 되지만, 다형성의 원리를 사용하여 Parent child = new Child(); 형태로 생성하였다.

 만약, 이 상황에서 child.hi() 메서드를 호출하면 Parent 클래스와 Child 클래스의 hi() 중 어떤 메서드가 실행될까?


 

 Child 객체를 참조하고있으니 Child 객체의 메서드가 호출될 것 같지만 자료형을 Parent이니 Parent의 메서드가 호출될 것 같기도 하다. 다형성에 대해 알아보고 이 고민을 해소해보자.


2. 다형성(Ploymorphism)이란?

 슈퍼클래스 타입의 변수가 서브클래스 타입의 객체를 참조할 수 있다는 성질이다.

 Child 클래스가 Parent 클래스를 상속받고 있다면 슈퍼 클래스 타입의 변수인 Parent가 Child 클래스를 참조할 수 있다는 의미인데, 예제를 보면 이해가 빠를 것이다. 다형성이란 말이 어렵지 사실 되게 익숙한 개념이다.

 

 그럼 예제를 통해 상속관계, 메서드 오버라이딩, 다형성이 적용된 객체를 생성해보도록 하겠다.


3. 예제

3.1. Parent.java

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Parent {
    
    protected String name;
    protected int age;
    
    public Parent(String name, int age) {
        this.name = name;
        this.age = age;
    }
    protected void hi() {
        System.out.println("안녕하십니까 올해로 "+age+"살인 "+name+"입니다.");
    }
}
cs

 

3.2. Child.java

1
2
3
4
5
6
7
8
9
10
11
public class Child extends Parent{
 
    public Child(String name, int age) {
        super(name, age);
    }
    
    @Override
    public void hi() {
        System.out.println("안녕? 내 이름은 "+name+"이고, "+age+"살이야! 만나서반가워~");
    }
}
cs

Parent 클래스를 상속받고 있으며 부모 클래스의 hi() 메서드를 오버라이드하여 재정의하였다.

 

3.3. Main.java

1
2
3
4
5
6
7
public class Main {
 
    public static void main(String[] args) {
        Parent child = new Child("심드류 아들",10);
        child.hi();
    }
}
cs

main 메서드에서 다음과 같이 자료형이 Parent인 child 변수에 Child 객체를 생성하여 초기화해주었다.

슈퍼클래스 타입(:Parent)인 변수(:child)가 서브클래스의 객체(new Child)를 참조하고 있다. 바로 이게 다형성이다.

그럼 이 상태에서 hi() 메서드를 실행하면 어떤 결과가 나올까?


4. 결과

Child 객체의 hi 메서드가 실행됨

 결과는 바로! Child 객체의 hi 메서드가 실행된다. 즉 자료형이 아닌, 참조 객체의 메서드가 실행되는 것이다.

 다형성의 원리를 사용하여 객체를 생성하고, 오버라이드된 메서드를 호출했을 때에는 슈퍼클래스의 메서드가 아닌, 서브클래스의 메서드가 호출된다. 즉, 참조 객체가 무엇이냐에 따라 호출하는 메서드가 동적으로 바뀔 수 있다는 뜻이다.

이러한 내용을 동적 바인딩(dynamic binding) 이라고 한다.


5. 정리

 상속관계에서 서브 클래스의 메서드를 오버라이딩 하고 다형성의 원리에 입각하여 객체 생성 후 메서드를 호출하면, 해당 메서드는 동적 바인딩된다.

 

반응형
반응형

1. 개요

 자주 쓰는 상속과 생성자의 개념에 대해 간단히 정리하고, 상속관계에서 발생할 수 있는 문제에 대해 알아보자.


2. 상속이란?

 상속이란 부모님에게 재산을 물려받듯이 부모 클래스의 멤버필드와 메서드를 자식클래스가 물려받는 것이다.

 상속을 통해 불필요한 중복을 제거할 수 있다.


3. 생성자란?

 객체에 대한 초기화 메서드이다. 멤버 변수를 초기화하거나 자원을 할당할 수 있다.

 클래스 내 생성자가 정의되어 있지 않을 경우 자바가 자동으로 no-parameter 생성자를 생성한다.

 

 여기까지가 많은 사람들이 알고 있는 생성자에 대한 간단한 정의이다. 하지만 다음과같은 내용도 있다.

더보기

 자식클래스의 생성자는 부모클래스의 생성자를 먼저 호출한다.

 명시적으로 호출하지 않을 경우 부모클래스의 no-parameter 생성자가 자동적으로 호출된다.

별게 아닌 것 같지만, 이 내용을 알지 못했을 때 발생할 수 있는 문제 상황이 있다. 한번 알아보자.


4. 상속, 생성자 예제

 상속 관계로 부모 클래스는 술(Alcohol), 자식 클래스는 럼(Rum)으로 하겠다. (럼: 해적들이 마시던 술)

 

4.1. Alcohol.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Alcohol {
 
    protected double frequency;
    protected double price;
    protected String flavor;
    
    public Alcohol(double frequency, double price, String flavor) {
        this.frequency = frequency;
        this.price = price;
        this.flavor = flavor;
    }
    
    public String whatFlavor() {
        return "flavor is "+flavor;
    }
}
cs

모든 술에는 도수, 가격, 맛이 존재한다. 그에 맞게 frequency, price, flavor 멤버필드를 선언해주었고, 모든 멤버필드의 값을 초기화해주는 생성자도 생성하였다. 추가적으로 whatFlavor라는 간단한 메서드도 정의하였다.

 

4.2. Rum.java

1
2
3
4
5
6
7
8
9
10
11
public class Rum extends Alcohol{
 
    private String rumType;
 
    public Rum(String rumType, double frequency, double price, String flavor) {
        this.rumType = rumType;
        this.frequency = frequency;
        this.price = price;
        this.flavor = flavor;
    }
}
cs

Alcohol 클래스를 상속받는 Rum 클래스를 정의하였다.

Alcohol 클래스를 상속받았기때문에 이에 대한 멤버필드와 메서드를 Rum 클래스에서 할당받게 된다. 즉, Rum 클래스의 멤버필드는 rumType, frequency, price, flavor가 된다. 마찬가지로 Rum 클래스의 생성자도 정의하였다.

...

...

...

혹시 이 예제에서 문제점을 발견했는가? 발견했다면 당신은 멋진사람...

 

4.3. 컴파일 에러

 이 예제는 언뜻보면 이상이 없어보이나 다음과 같이 컴파일 에러가 발생하는 코드이다.

컴파일 에러

 에러 내용은 다음과 같다. 

더보기

 Implicit super constructor Alcohol() is undefined.

 암시 적 슈퍼 생성자 Alcohol ()이 정의되지 않았습니다.

 바로 이게 앞서 언급했던 "자식클래스의 생성자는 부모클래스의 생성자를 먼저 호출한다." , "명시적으로 호출하지 않을 경우 부모클래스의 no-parameter 생성자가 자동적으로 호출된다." 라는 생성자의 정의에 의해 발생한 에러이다.

 

 자식클래스의 생성자는 호출 전에 부모클래스의 생성자를 호출해야하는데, 현재 Rum 생성자를 보면 알수있듯이 부모 클래스의 생성자를 호출하는 메서드인 super(...)가 없다. 즉, 명시적으로 호출하지 않았기 때문에 자바가 Rum 생성자 내에서 부모 클래스의 no-parameter 생성자를 자동적으로 호출하려했으나 현재 부모클래스에는 no-parameter 생성자가 없기 때문에 위 에러가 발생한 것이다.

 

그렇다면 해결은 어떻게 할까?


5. 해결

이를 해결하기 위해서는 두가지 방법이 있다.

첫번째, 부모클래스에 no-parameter 생성자를 생성한다.

두번째, 자식클래스의 생성자에 부모클래스의 생성자를 명시적으로 호출한다.

첫번째 방법은 에러 제거만을 목적으로 불필요한 코드를 추가하기 때문에 추천하지 않는 방법이다. 자식 클래스 생성자 내에서 부모 클래스의 생성자를 명시적으로 호출하는 두번째 방법을 사용하자.

 

5.1. Rum.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Rum extends Alcohol{
 
    private String rumType;
 
    public Rum(String rumType, double frequency, double price, String flavor) {
        super(frequency, price, flavor);
        this.rumType = rumType;
    }
    
    public static void main(String[] args) {
        Rum rum = new Rum("DarkRum"40.069000"bitter");
        System.out.println(rum.whatFlavor());
    }
}
cs

Rum 생성자 안에서 super() 메서드를 통해 부모 클래스의 생성자를 명시적으로 호출해주고, 부모클래스에 없는 rumType은 this를 통해 초기화하였다.

 

main 메서드에서 생성자를 통해 Rum 객체를 생성하여 테스트하면 정상적으로 작동됨을 확인할 수 있다.

 

 

오늘 자료구조를 공부하며 알게 된 새로운 내용을 정리할 수 있어 기쁘다.

반응형

+ Recent posts