𝗪𝗵𝘆 𝗶𝘀 𝗠𝗮𝗽 𝗡𝗼𝘁 𝗜𝘁𝗲𝗿𝗮𝗯𝗹𝗲 𝗶𝗻 𝗝𝗮𝘃𝗮?— Most Asked Interv

“Why is Map not directly Iterable in Java?”

blog.stackademic.com

 

왜 Map은 반복 가능하지 않은가?

이 글에서는 Map의 반복 가능하지 않도록 설계된 이유에 대해서 말합니다.
이를 확장해 class, interface 상속에 대한 내용도 추가해 놓았습니다.


Java Collection 프레임워크의 구조를 보면 알 수 있듯이, MapCollection이 아니며 Iterable 또한 상속받지 않습니다.
(Iterable 을 상속의 의미는 Enhanced For Loop 를 사용할 수 있음을 말합니다)

위의 글에서는 왜 "MapIterable 하지 않은가?" 에 대해 말합니다.
주된 주장은 MapCollection, 즉 요소의 집합이 아니며 Pair의 묶음이라는 것 입니다.
Map은 기본적으로 key-value 형태의 쌍이기 때문에, 명시하지 않는다면 어떤 원소값의 반복인지 알 수 없습니다.

  1. key 값을 반복
  2. value를 반복
  3. key-value 쌍을 반복

이렇게 다양한 옵션들이 있기 때문에 정확히 무엇을 순회할건지를 알 수 없는 모호함이 발생한다는 것으로
정확히 어떤것을 순회할지를 나타낸 후에야 Iterable해지는 것을 알 수 있습니다.

Map<String, Integer> scores = new HashMap<>();
scores.put("Madhavi", 90);
scores.put("Kiran", 85);
scores.put("Anita", 88);

// Iterate over entrySet
for (Map.Entry<String, Integer> entry : scores.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}
// Iterate over keys
for (String key : scores.keySet()) {
    System.out.println("Key: " + key);
}
// Iterate over values
for (Integer value : scores.values()) {
    System.out.println("Value: " + value);
}

class를 여러개 상속 받을 수 없는 비슷한 이유

이것과 비슷한 개념으로 class의 상속이 존재하는데요
class는 기본적으로 상태를 가지게 됩니다.

상태를 가진다는 것은 Field 값이 존재한다는 것이고, method 를 통해 이를 조작하게 됩니다.
재밌는 상상을 해볼까요?

만약 dragon, bird 두 개의 클래스를 상속받는다 가정하고
각각이 fly() 라는 메서드와 ,wings 라는 필드가 존재한다고 하겠습니다.

class Bird{

    int wings;

    public void fly(){
        // do something
    }

}

class Dragon{
    int wings;

    public void fly(){
        // do something
    }
}

class bat extends Dragon, Bird{
    // 누구의 날개를 사용해서 어떻게 날 것인가?
    // error 
}

이를 상속받은 클래스에서는 과연 누구의 wings를 가지고 어떻게 fly() 연산을 수행해야 할까요?
이렇게 직접적인 구현을 가지고 있는 클래스는 여러개를 상속받게 하는 순간 모호함에서 오는 이상이 발생할 수 있기 때문에
자바에서는 원칙적으로 막아두는 것 입니다.

반면에 interface는 상태를 가지지않고, 그저 어떤 연산이 가능한지만 명시해놓는 명세서입니다.
따라서 여러개를 상속받아도 모호함에서 오는 이상이 없는 것 이죠

그렇기 때문에 interface의 경우는 여러개를 상속받는 것을 막아두지 않습니다.

Why Abstraction??

우리는 이전의 글을 통해 Abstraction의 개념을 간략하게 이해했습니다.

 

Abstraction(추상화) 기본 개념 - 1편

Abstraction(추상화)란? 자바에서는 추상클래스 , 추상메서드 , 추상화 등 "추상"이라는 말이 자주 쓰입니다. 또한 Abstract으로 선언된 클래스 , 메서드 등을 보고 추상화시켰다 합니다. 그렇다면 추상

masiljangajji-coding.tistory.com

추가적으로 다음 글을 읽는 것을 추천드립니다.

 

Specification 기본 개념 및 활용

Specification 프로그램에서 터지는 버그는 대부분 동작에 대한 오해로 발생합니다. 이러한 동작의 오해를 줄이기 위한 대표적인 방법으로 Spec(명세)이 존재합니다. 또한 완성된 프로그램은 필수적

masiljangajji-coding.tistory.com

 

이 글에서는 이전 글과 비슷한 내용을 다루되 더욱 세세한 부분을 보려고 합니다.


Abstraction을 하게되면 얻을 수 있는 핵심 이점은 다음과 같습니다.

  1. 시스템을 구성 요소 또는 모듈로 나누어 재사용할 수 있다록 한다.(Modularity 모듈성)
  2. 모듈 주의에 벽을 구축하여 모듈이 자체적으로 책임지고 시스템의 다른 부분에서 발생한 버그가 모듈의 무결성을 손상시킬 수 없도록 한다.(Encapsulation 캡슐화)
  3. 모듈의 구현 세부사항을 숨겨 세부사항을 변경해도 시스템의 나머지 부분을 변경하지 않아도 된다. (Imformation Hiding)
  4. 기능을 모듈의 책임으로 만들어 여러 모듈에 걸쳐 분산되지 않도록 한다. (Separation of Concerns 관심사분리)

어떻게 이런 일이 가능한 것일까요??

 

자바에서 제공하는 List에 대해서 보겠습니다.

ArrayList , LinkedList 둘 다 List Interface를 상속받아 구현하는 방식으로 설계돼있습니다.

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

ArrayList

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable

LinkedList

 

 

지금부터 이러한 구조의 이점을 알아보겠습니다.

 

List Interface

List라는 타입이 필수적으로 가져야 하는 연산을 명시한 것입니다.
연산에대한 간단한 Specification을 명시한 것이죠

 

따라서 우리는 앞으로 List라는 타입이 가져야하는 연산을 알 수 있습니다.

(List에 명시돼있는 size , isEmpty , contains 연산의 Specification)

 

사용자는 List의 구현체를 보지 않아도 , 즉 내부적인 구현이 어떤식으로 이루어져있는지 몰라도
간단한 Spec만으로 충분한 사용이 가능해집니다. (Imformation Hiding)

 

사용자가 List 내부구현에 의존하지 않고 사용하기 떄문에 갑작스럽게 List 구현체의 내부구현을 바꾸더라도
아무런 문제가 발생하지 않을 것 입니다. (Encapsulation)

 

Spec을 명시함으로써 자연스럽게 연산의 구조를 알 수 있게됐고 이를통해 세세한 메서드 분리가 가능해집니다.
이는 PreCondition , PostCondition 같은 조건들을 제공하고 Unit Test를 돕습니다.

 

후에 배열구조나 노드구조를 사용하지 않는 새로운 구조의 List를 만든다고 했을때도
List Interface를 재사용 함으로써 코드의 재사용성을 증가시킬 것 입니다. (Modularity)

 

List이외에도 Collection FrameWork 안에있는 Abstract Data Type은 Interface를 두고 구현체를 따로 두는 방식을 채택합니다.

 

 

이러한 방법은 쓸대없는 일을 여러번 하는 것 처럼 보이지만 실제로는 위에서 기술 한 것처럼 많은 이점이 있는것이죠

 

Collection 의 Super Type으로 Iterable가 존재하는 것도 같은 이치입니다.

 

계속해서 강조하자면 Abstraction은 매우 큰 범위를 갖고있는 말입니다.

 

사용자에게 내부구현 정보를 숨기는 것 ... Abstraction

새로운 구조를 갖는 List가 필요할때 implements 받아 연산 정의 절차를 줄이는 것 ... Abstraction

 

무언가를 "간추린다" 면 전부다 Abstraction에 해당하기 떄문이죠 

 

이 글에서는 Abstraction이 갖는 이점과 보다 세세한 관점에 대해서 다뤘습니다.
도움이 되셨다면 좋겠습니다.

Collection FramWork

자바는 Collection FramWork를 제공합니다

이 안에는 대부분의 자료구조가 존재합니다. List, Map, Set, Tree...

또 기본적으로 Collection은 Parameterized Type을 사용합니다.

 

Collection을 한마디로 정리하면 자료를 다루는 방법과 저장하는 구조가 다른 Abstract Data Type의 집합입니다.
따라서 용도에 따라 어떤 자료구조를 사용할 것인지 잘 이해했다면 Collection FrameWork를 이해했다고 볼 수 있습니다.

 

 

List

List Interface를 상속받는 구현체는 대표적으로 ArrayList, LinkedList 가 존재합니다.

둘다 같은 List를 상속받아 구현체로써 사용되지만 , 구현할떄 사용되는 Representation이 다릅니다.

이런식으로 Collection Interface를 상속받은 구현체는 구현시 사용되는 자료구조가 다르거나 정렬의 유무에서 차이가 나는 경우게 대부분입니다.

 

데이터 구조

  1. ArrayList 배열 사용
  2. LinkedList node구조 사용

이렇게 구조에서 차이가 나지만 실제 동작에서는 ArrayList가 할 수 있는 일은 LinkedList가 전부 할 수 있으며

LinkedList 가 할 수 있는 일은 ArrayList가 전부 수행 가능합니다.

 

그럼 왜 나눠서 쓰는 걸까요??
그냥 하나로 통일해서 쓰면 안될까요??

 

ArrayList는 배열을 사용합니다. 따라서 주소가 sequential하게 연결돼있습니다.

따라서 특정 index에 접근하는게 매우 빠릅니다 시작주소 + 3 하면 3번쨰 index에 접근 가능하게 되는 것이죠

반면에 원소를 더하거나 삭제하는 경우에는 배열의 요소들을 한 칸씩 뒤로 이동시키거나 앞으로 당겨야 하기 때문에 상당히 느린 연산을 수행하게됩니다.

 

LinkedList는 node 구조를 사용합니다 배열이 아님으로 주소가 sequential하게 연결돼있지 않습니다.
따라서 특정 index에 접근하는게 ArrayList에 비해 느립니다.

 

하지만 원소를 더하거나 삭제하는 연산을 할때 node들 사이에 연결만 이어주고 끊어주면 되기때문에 ArrayList보다 연산이 빠릅니다.

정리하자면 다음과 같습니다.

 

  • ArrayList : 리스트 크기 변경 안될때 , 데이터 접근이 빈번할 때
  • LinkedList : 리스트 크기 변경이 자주 일어날떄 , 요소의 삽입/삭제가 빈번할 때

이렇게 같은 List자료구조지만 구현돼있는 내부 구조가 다르기 떄문에 용도에 맞게 사용할 수 있어야 합니다.

 

 

Set

Set은 중복을 허용하지 않는 대표적인 자료구조입니다.

크게 HashSet , TreeSet 으로 나뉩니다.

Hash는 이름그대로 HashCode를 사용하며 Tree는 내부적으로 BinarySearchTree 구조를 사용합니다.

 

이점은 중복을 어떻게 확인할 것인지에 큰 차이가 생깁니다.

  • HashSet
    1. HashCode 비교
    2. Equals 비교
    3. 중복없음

 

  • TreeSet
    1. 요소들이 이미 정렬돼있음
    2. Tree를 순회하면서 직접 Element 비교 (Comparable Implements 필요)
    3. 중복없음

HashSet은 HashCode와 Equals만 이용하면 되기 떄문에 원소를 추가하는 연산이 빠릅니다.

반면에 TreeSet은 직접적인 순회를 해야하기 떄문에 HashSet에 비해 느립니다. 하지만 정렬된 상태를 유지하기 떄문에

 

정렬된 상태가 필요한 경우에는 TreeSet을 그렇지 않다면 HashSet쓰는게 일반적으로 유리합니다.

public class Main {

    public static void main(String[] args) {

        Set<Person> set = new HashSet<>();

        set.add(new Person(20, "홍길동"));
        set.add(new Person(20, "홍길동"));

        for (Person p : set) {
            System.out.println(p);
        }

    }
}

class Person {


    int age;
    String name;

    public Person(int age, String name) {
        this.age = age;
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        Person person = (Person) o;

        return age == person.age && Objects.equals(name, person.name);
    }

//    @Override
//    public int hashCode() {
//        return Objects.hash(age, name);
//    }

    @Override
    public String toString() {
        return "Person{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }

}

Equals 구현 , HashCode 주석처리

따라서 중복체크를 똑바로 못합니다.

 

 

HashCode의 주석처리를 지워주면 중복체크가 정상적으로 작동합니다.

public class Main {

    public static void main(String[] args) {

        Set<Person> set = new TreeSet<>();

        set.add(new Person(20, "홍길동"));
        set.add(new Person(20, "홍길동"));

        for (Person p : set) {
            System.out.println(p);
        }

    }
}

class Person implements Comparable<Person> {


    int age;
    String name;

    public Person(int age, String name) {
        this.age = age;
        this.name = name;
    }

    @Override
    public String toString() {
        return "Person{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }

    @Override
    public int compareTo(Person o) {

        return this.age - o.age;

    }
}

TreeSet은 해시코드와 Equals를 쓰지 않습니다.


대신 Comparable의 compareTo 를 사용합니다

지금은 age를 기준으로 compareTo가 구현이 돼있습니다.

public static void main(String[] args) {

        Set<Person> set = new TreeSet<>();

        set.add(new Person(20, "홍길동"));
        set.add(new Person(21, "홍길동"));

        for (Person p : set) {
            System.out.println(p);
        }

    }

이 코드에서는 나이가 다르면 이름이 같아도 다른 객체기 떄문에 2개가 들어갑니다.

 

Map

Map은 Key : Value 쌍으로 이루어져 있습니다.

여기서는 Key가 해시코드 역할을 합니다.

Map 자료구조도 대표적으로 HashMap , TreeMap 이 존재합니다.

또 내부적으로 HashMap은 배열과 연결리스트 기반 , Tree는 노드기반 구조를 사용합니다

기존과 똑같이 Hash는 HashCode , Equals 를 사용합니다.

public class Main {

    public static void main(String[] args) {

        Map<Person, Integer> map = new HashMap();

        map.put(new Person(20, "홍길동"), 1);
        map.put(new Person(20, "홍길동"), 2);


        for (Person p : map.keySet()) {
            System.out.println(p);
        }

    }
}

class Person {


    int age;
    String name;

    public Person(int age, String name) {
        this.age = age;
        this.name = name;
    }

    @Override
    public String toString() {
        return "Person{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        Person person = (Person) o;

        return age == person.age && Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(age, name);
    }


}

 

 

마찬가지로 TreeMap은 compareTo를 구현해 써야 합니다.

public class Main {

    public static void main(String[] args) {

        Map<Person, Integer> map = new TreeMap<>();

        map.put(new Person(20, "홍길동"), 1);
        map.put(new Person(20, "홍길동"), 2);


        for (Person p : map.keySet()) {
            System.out.println(p);
        }

    }
}

class Person implements Comparable<Person> {


    int age;
    String name;

    public Person(int age, String name) {
        this.age = age;
        this.name = name;
    }

    @Override
    public String toString() {
        return "Person{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }


    @Override
    public int compareTo(Person o) {
        return this.age - o.age;
    }
}

 

Collection FramWork를 이해하고 응용한다는 것은

자료구조를 용도에 맞게 문제 해결에 더 적합한 것으로 선택해서 사용할 수 있음을 의미합니다.

 

도움이 되셨다면 좋겠습니다.

+ Recent posts