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을 사용하자.
반응형