CodeStatesBootCamp/Review

Section 1 - Unit 7 : [Java] 컬렉션 Review

JKROH 2023. 3. 6. 11:49
반응형
Review 에서는 학습한 내용을 다시금 기록합니다.
Unit Review는 학습한 내용 중 기존에 알고 있었지만 정확하게 이해하지 못하던 정보와 새롭게 알게된 정보를 기록합니다. 추가적인 설명을 요하는 부분은 댓글로 남겨주세요.
Section Review는 전반적인 Section을 되돌아보고 학습했던 시간과 과정, 내용을 총괄하여 기록합니다.

<<컬렉션>>


열거형 (Enum)

* Enum 타입

  • enum 타입은 서로 연관성 있는 상수들을 모아놓을 때 사용한다.
  • 단순히 연관된 상수들이 많다고 enum을 사용하면 안된다. 해당 상수들에서 더 이상 변화가 없을 가능성이 높아야 사용해야한다.
    • 예를 들어, 모바일 OS 는 안드로이드, MAC OS 에서 이변이 없는 한 바뀌지 않을 것이다. 이럴 때 enum을 써야 한다.
    • 하드 코딩을 피하기 위해 문자열을 상수로 바꾼 것을 enum화 하면 안된다. 값이 언제 바뀔지도 모르며, 언제 어느 문자열이 추가될지도 모르기 때문이다.
  • enum을 사용하는 것은 다음의 장점이 있다.
    • 여러 상수들을 보다 편리하게 선언하고 관리할 수 있다.
    • 상수명의 중복을 피하고, 타입에 대한 안정성을 보장한다.
    • 간결하고 가독성이 좋은 코드를 작성할 수 있다.
  • enum 클래스는 다음과 같이 구현할 수 있다.
public enum GameResult {
    SUCCESS("성공"),
    FAIL("실패");

    private final String result;

    GameResult(String result) {
        this.result = result;
    }

    public String getValue() {
        return result;
    }
}

* Enum의 사용

  • Enum에서 사용할 수 있는 대표적인 메서드들은 다음과 같다.
반환 타입 메서드(매개변수) 설명
String name() Enum 객체를 정의할 때 사용한 상수 이름을 반환한다.
int ordinal() Enum 객체의 순서를 반환한다. 
int compareTo(비교값) 비교값과 비교한 순서 차이를 나타낸다
Enum valueOf(String name) name과 같은 이름의 enum을 반환한다.
Enum 배열 values() 모든 Enum 객체들을 배열로 반환한다.
  • 위의 메서드들을 사용한 예시와 결과는 아래와 같다.
public class Main {
    public static void main(String[] args) {
        for(GameResult result : GameResult.values()){
            System.out.printf("%s=%d\n", result.name(), result.ordinal());
        }
        System.out.println(GameResult.FAIL.compareTo(GameResult.SUCCESS));
        System.out.println(GameResult.valueOf("SUCCESS"));
        System.out.println(GameResult.valueOf("SUCCESS")==(GameResult.SUCCESS));
    }
}

====================================================================================

SUCCESS=0 // name(), ordial()
FAIL=1
1 //compareTo(Enum enum)
SUCCESS // valueOf(String name)
true // Enum타입은 == 로 비교가 가능하다.

제네릭

* 제네릭이란?

- 제네릭의 필요성

  • 제네릭을 사용하면 객체에 사용될 타입을 여러 형태로 지정할 수 있다.
  • 예를 들어, List는 List<E>의 형태로 지정된 interface이다. 그렇기 때문에 우리는 List에 어떠한 객체도 담을 수 있다.
  • 제네릭을 사용하면 컴파일 단계에서 타입의 일치성을 검증하여 중복 제거와 타입 안정성을 동시에 얻을 수 있다.

- 제네릭이란 무엇인가?

  • 제네릭은 사전적으로 '일반적인'이라는 의미이다.
  • 따라서 자바에서 제네릭은 메서드나 클래스의 타입을 구체적으로 지정하는 것이 아니라, 추후에 지정할 수 있도록 일반화 하는 것이다.

* 제네릭 클래스

- 제네릭 클래스 정의

  • 제네릭이 사용된 클래스를 제네릭 클래스라고 한다.
  • 제네릭 클래스를 정의하고 싶다면 클래스 이름 옆에 <타입 매개변수> 를 붙여주면 된다.
  • 이 때, 타입 매개 변수를 여러 개 사용한다면 <K, V>의 형태로 사용한다.
  • 타입 매개변수는 임의의 문자를 지정할 수 있다, 대표적으로 T, E, K, V 등이 사용된다.
class GenericClass<T> {
    private T genericValue;
    
    public GenericClass(T genericValue) {
        this.genericValue = genericValue;
    }
    
    public T getVlaue {
        return genericValue;
    }
}

 

- 제네릭 클래스를 정의할 때 주의할 점

  • 클래스 변수에는 타입 매개변수를 사용할 수 없다.
  • 클래스 변수는 프로그램이 실행되면서 정의되어야 하는, 타입 매개변수는 추후에 지정하기 때문이다. 

 

- 제네릭 클래스의 사용

GenericClass<String>  gc1 = new GenericClass<String>("Hello");
GenericClass<Integer> gc2 = new GenericClass<Integer>(10);
GenericClass<Double>  gc3 = new GenericClass<Double>(3.14);

GenericClass<String>  gc1 = new GenericClass<>("Hello");
GenericClass<Integer> gc2 = new GenericClass<>(10);
GenericClass<Double>  gc2 = new GenericClass<>(3.14);

* 제한된 제네릭 클래스

  • 제네릭 클래스의 타입 매개변수를 특정 클래스 / 인터페이스와 해당 클래스를 상속 받은 클래스 / 해당 인터페이스를 구현한 클래스로만 제한 할 수 있다.
    • GenericClass<T extends 클래스 명 또는 인터페이스 명> 을 사용하여 제네릭 클래스를 생성한다.
  • 특정 클래스를 상속 받으면서 동시에 특정 인터페이스를 구현한 타입으로만 타입을 제한할 수도 있다.
    • GenericClass<T extends 클래스 & 인터페이스 명> 을 사용하여 제네릭 클래스를 생성한다.
    • 이 때, 클래스 명이 인터페이스 명보다 앞에 와야 한다.

* 제네릭 메서드

  • 제네릭 메서드는 parameter를 타입 매개변수로 받아와서 사용할 수 있는 메서드이다.
  • 메서드의 반환 타입 아에 타입 매개변수를 선언해주면 제네릭 메서드로 사용할 수 있다.
  • public <T> void methodName(T parameter) 의 형태로 메서드를 선언하면 된다.
  • 제네릭 클래스 내에 제네릭 메서드를 사용한다면, 각각의 타입 매개변수는 다르게 정의할 수 있다.
  • 예를 들어 String을 사용한 제네릭 클래스 내부의 제네릭 메서드에는 Integer를 이용할 수 있다.
  • 제네릭 메서드는 static하게 사용할 수 있다.

* 와일드카드

  • 와일드카드는 어떤 타입으로든 대체될 수 있는 타입 매개변수를 의미하며, ?를 사용한다.
  • 일반적으로 와일드카드는 extends 또는 super와 조합하여 사용한다.
    • ? extends T는 와일드카드에 상한 제한을 둔다. T와 T를 상속받은 하위 클래스만이 타입 매개변수로서 사용될 수 있다.
    • ? super T는 와일드카드에 하한 제한을 둔다. T와 T의 상위 클래스만이 타입 매개변수로서 사용될 수 있다.
    • 예를 들어, Animal 을 상속받은 Mammalia와 Birds 클래스가 있고, 각각을 상속받은 Lion과 Dove가 있다고 가정해보자.
    • ? extends Mammalia를 사용하면, 타입 매개변수로 Mammalia와 Lion만 사용할 수 있다. Animal은 사용할 수 없다.
    • ? super Birds를 사용하면, 타입 매개변수로 Animal과 Birds만 사용할 수 있다. Dove는 사용할 수 없다.
  • extends 또는 super와 조합하지 않은 와일드카드는 ? extends Object 와 같은 의미이다. 즉, 모든 객체를 사용할 수 있다.

예외 처리

* 컴파일 에러와 런타임 에러

- 컴파일 에러

  • 컴파일 시 발생하는 에러를 뜻한다.
  • 주로 문법적인 문제를 가리키는 syntax오류로부터 발생한다.
  • 자바 컴파일러가 오류를 감지하여 알려주고 이를 IDE에서 빨간 줄로 나타내주기 때문에 발견하기 쉽다.

- 런타임 에러

  • 런타임 시 발생하는 에러를 뜻한다.
  • 프로그램이 실행될 때 JVM에 의해서 감지되기 때문에 프로그램을 실행해봐야 문제를 알 수 있다.

* 예외 클래스의 상속 계층도

- 일반 예외 클래스

  • 런타임 익셉션을 제외한 모든 예외 클래스들을 일반 예외 클래스로 정의한다.
  • 컴파일러가 예외 처리 코드 여부를 검사한다.
  • 즉, try-catch문 등으로 처리해주지 않으면 IDE에 빨간 줄로 나타난다.
  • 주로 잘못된 클래스명이나 데이터 형식 등 사용자의 실수로 인해 발생한다.

- 실행 예외 클래스

  • 컴파일러가 예외 처리 코드 여부를 검사하지 않는다.
  • 주로 클래스 간 형변환 오류, 배열 범위를 벗어난 인덱스 지정, 널 포인트 익셉션 등이 있다.

* try - catch 문

try {
 
    // 예외가 발생할 가능성이 있는 코드

}
catch (ExceptionType e) {

    // Exception 타입의 예외가 발생하면 실행할 코드

}
finally {

    // finally 블럭은 선택적으로 작성
    // 예외 발생 여부와 상관없이 항상 실행할 코드

}

* 예외 전가

  • throws를 통해 예외를 전가할 수 있다.
  • throw 를 통해 예의를 의도적으로 발생시킬 수 있다.
    • 예를 들어, 범위에 맞지 않는 숫자가 입력되었다면, throw new IllegalArgumentException(); 등을 이용할 수 있다.

컬렉션 프레임워크

* 컬렉션 프레임워크란?

- 컬렉션 프레임워크

  • 컬렉션은 여러 데이터들의 집합을 의미한다.
  • 컬렉션은 다루는데 있어 편리한 메서드들을 미리 정의해놓은 것을 컬렉션 프레임워크라고 한다.

- 컬렉션 프레임워크의 구조

- Collection 인터페이스

기능 반환 타입 메서드 설명
객체 추가 boolean addAll(Collection C) C의 모든 객체들을 컬렉션에 추가한다.
객체 검색 boolean continsAll(Collection C) C의 모든 객체들이 저장되어 있는지를 확인한다.
  boolean equals(Collection C) C와 컬렉션이 포함하고 있는 객체들이 같은지 확인한다.
객체 삭제 boolean removeAll(Collection C) 주어진 컬렉션을 삭제하고 성공 여부를 확인한다.
  boolean retainAll(Collection C) 주어진 컬렉션을 제외한 모든 객체를 삭제하고 변화가 있는지 확인한다.
객체 변환 Object[]  toArray() 컬렉션에 저장된 객체를 객체배열로 반환한다.
  Object[] toArray(Object[] a) 주어진 배열에 컬렉션의 객체를 저장해서 반환한다.
  • equals()메서드를 통해 비교할 때, 원시 타입 객체와 String 은 별도의 조건 없이 비교가 가능하다. 그러나 객체 간 비교를 하기 위해선 hashCode()와 equals() 메서드를 오버라이딩해서 사용해야 한다.
  • removeAll() 과 retainAll 을 사용할 때도 마찬가지다.
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

class Pokemon{
    String name;

    public Pokemon(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Pokemon pokemon = (Pokemon) o;
        return Objects.equals(name, pokemon.name);
    }

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

public class Main {
    public static void main(String[] args) {
        List<Pokemon>pokemonList1 = new ArrayList<>(List.of(new Pokemon("피카츄"), new Pokemon("라이츄"), new Pokemon("파이리"), new Pokemon("거북왕")));
        List<Pokemon>pokemonList2 = List.of(new Pokemon("피카츄"), new Pokemon("라이츄"), new Pokemon("파이리"));
        List<Pokemon> pokemonList3 = new ArrayList<>(pokemonList1);

//        Pokemon [] pokemons = pokemonList1.toArray();  컴파일 에러가 발생한다
        Pokemon [] pokemons = pokemonList1.toArray(new Pokemon[0]);

        System.out.println(pokemonList1.equals(pokemonList2));
        System.out.println(pokemonList1.equals(pokemonList3));
        System.out.println(pokemonList1.containsAll(pokemonList2));
        System.out.println(pokemonList1.containsAll(pokemonList3));
        for(Pokemon pokemon : pokemonList1){
            System.out.print(pokemon.name+" ");
        }
        System.out.println();
        System.out.println(pokemonList1.retainAll(pokemonList2));
        for(Pokemon pokemon : pokemonList1){
            System.out.print(pokemon.name+" ");
        }
    }
}


==============================================================================

false
true
true
true
피카츄 라이츄 파이리 거북왕 
true
피카츄 라이츄 파이리
  • 위의 코드에서 toArray()에 new Pokemon[0]를 argument로 넣어주고 있다.
  • 배열은 반드시 크기가 정해져야 하기 때문에 이를 해결하기 위한 인자로 넣어주는 것이다. 그런데, pokemonList1의 크기는 4인데 왜 0이 들어갈까. 그 이유는 다음과 같다.
    • List를 toArray 메서드에 인자로 넘어가는 배열 객체의 size만큼의 배열로 전환한다.
    • 단, 해당 List의 size가 인자로 넘어가는 배열 객체의 size보다 클때, 해당 List의 size로 배열이 만들어진다.
      • 즉, 0크기의 배열이 넘어갔기 때문에 pokemons 는 크기 4의 배열이 된다.
    • 반대로 해당 List size가 인자로 넘어가는 배열객체의 size보다 작을때는, 인자로 넘어가는 배열객체의 size로 배열이 만들어진다.
      • 만일 크기 10의 배열이 넘어갔다면 pokemons는 크기 10의 배열이 되었을 것이다.

* List<E>

- ArrayList vs LinkedList

  • 객체의 삽입과 삭제가 빈번하게 일어난다면 LinkedList를 사용하는 것이 좋다.
  • 사실 당연한데, LinkedList에는 다음 객체에 대한 주소값이 저장되어 있기 때문에 해당 값만 수정해준다면, 삽입 / 삭제시 객체를 복사해서 옮겨주는 과정이 필요가 없기 때문이다.
  • 이는 Hash에서 일어나는 삽입 / 삭제와 비슷하다. 
    • 결과적으로 ArrayList는 O(n)의 시간복잡도가, LinkedList는 O(1)의 시간복잡도가 발생한다.
  • 그러나, 객체를 탐색하는 경우가 빈번할 때는 ArrayList를 사용하는 것이 좋다.
  • LinkedList는 무조건 처음부터 순차적으로 데이터를 찾기 때문이다.
    • 결과적으로 ArrayList는 O(1)의 시간복잡도가, LinkedList는 O(n)의 시간복잡도가 발생한다.

* Iterator

  • iterator는 컬렉션에 저장된 요소들을 순차적으로 읽어오는 역할을 한다.
  • iterator 인터페이스에는 다음의 세 가지 메서드가 정의되어있다.
    • hasNext() : 읽어올 객체가 남아있으면 true를 반환한다.
    • next() : 하나의 객체를 읽어온다. next()를 호출하기 이전에 hasNext()를 통해 읽어올 객체가 있는지 확인해야 한다.
    • remove() : next()를 통해 읽어온 객체를 삭제한다. next()를 호출한 다음 사용해야 한다.
public class Main {
    public static void main(String[] args) {
        List<Pokemon>pokemonList1 = new ArrayList<>(List.of(new Pokemon("피카츄"), new Pokemon("라이츄"), new Pokemon("파이리"), new Pokemon("거북왕")));

        Iterator<Pokemon>pokemonIterator = pokemonList1.iterator();

        while(pokemonIterator.hasNext()){
            System.out.println(pokemonIterator.next().name);
        }
    }
}

====================================================================================

피카츄
라이츄
파이리
거북왕

* Set<E>

- LinkedHashSet vs TreeSet

  • LinkedHashSet은 저장 순서가 보장되 HashSet이다. 중복은 허락하지 않으면서 순서는 지키고 싶을 때 사용할 수 있다.
  • TreeSet은 이진 트리 형태로 저장되는 HashSet이다. 중복은 허락하지 않으면서 오름차순 또는 내림차순으로 값을 저장하고 싶을 때 사용한다.
  • 상황에 따라 맞는 구현체를 사용하자.

* Map<K,V>

- HashMap vs HashTable

  • 전반적인 성능은 HashMap이 더 뛰어나다. HashMap은 Key값으로 Null이 허용되어 더 다양한 방법으로 사용할 수 있고 보조 해시를 추가적으로 사용하기 때문에 해시 충돌의 가능성이 낮다.
  • 그러나 HashMap은 동기화가 없기 때문에 thread-unsafe하다. 즉, 멀티 쓰레드 환경에서는 예외가 발생할 수 있다.
  • 예를 들어, 두 개의 쓰레드에서 동시에 하나의 HashMap의 Key-Value 쌍을 수정하면, 동시성 문제가 발생할 수 있다. 책 한권에 두 명이 동시에 글을 쓰면 책이 찢어지지 않겠는가.
  • 해당 방법을 해결하기 위해 syncronized 키워드로 직접 동기화 시킬 수 있지만, HashTable 을 사용할 수도 있다.
  • HashTable은 동기화가 걸려있기 때문에 thread-safe하다. 즉, 먼저 해당 HashTable을 다루던 쓰레드의 처리가 완전히 끝나야 다음 쓰레드가 HashTable을 사용할 수 있다.
  • 따라서, 동시성 문제가 발생하지 않을 단일 쓰레드 환경이면 HashMap을 사용하고, 동시성 문제에 대한 우려가 있는 멀티 쓰레드 환경이라면 HashTable을 사용하자.
반응형