본문 바로가기

Study/개발일지

[백엔드스터디WIL]3주차 학습일지

자바에 대한 전반적인 개념을 복습하였다. (제네릭, map)

 

  • Generic(제네릭)의 장점

 

 

 

1. 제네릭을 사용하면 잘못된 타입이 들어올 수 있는 것을 컴파일 단계에서 방지할 수 있다.

2. 클래스 외부에서 타입을 지정해주기 때문에 따로 타입을 체크하고 변환해줄 필요가 없다. 즉, 관리하기가 편하다.

3. 비슷한 기능을 지원하는 경우 코드의 재사용성이 높아진다.

 

 









  • Generic(제네릭) 사용방법



 

 

보통 제네릭은 아래 표의 타입들이 많이 쓰인다. 

 

타입 설명
<T> Type
<E> Element
<K> Key
<V> Value
<N> Number

 

 

물론 반드시 한 글자일 필요는 없다. 또한 설명과 반드시 일치해야 할 필요도 없다. 예로들어 <Ele>라고 해도 전혀 무방하다. 다만 대중적으로 통하는 통상적인 선언이 가장 편하기 때문에 위와같은 암묵적(?)인 규칙이 있을 뿐이다.

 

 

그럼 각 상황별 선언 및 생성 방법을 알아보자.

 

 

 

 

1. 클래스 및 인터페이스 선언

 

 

 
public class ClassName <T> { ... }
 
public Interface InterfaceName <T> { ... }

 

기본적으로 제네릭 타입의 클래스나 인터페이스의 경우 위와같이 선언한다.

T 타입은 해당 블럭 { ... } 안에서까지 유효하다.

 

또한 여기서 더 나아가 제네릭 타입을 두 개로 둘 수도 있다. (대표적으로 타입 인자로 두 개 받는 대표적인 컬렉션인 HashMap을 생각해보자.)

 

 
public class ClassName <T, K> { ... }
 
public Interface InterfaceName <T, K> { ... }
 
 
 
// HashMap의 경우 아래와 같이 선언되어있을 것이다.
 
public class HashMap <K, V> { ... }

 

이렇듯 데이터 타입을 외부로부터 지정할 수 있도록 할 수 있다.

 

그럼 이렇게 생성된 제네릭 클래스를 사용하고 싶을 것이다. 즉, 객체를 생성해야 하는데 이 때 구체적인 타입을 명시를 해주어야 하는 것이다.

 
public class ClassName <T, K> { ... }
 
 
 
public class Main {
 
public static void main(String[] args) {
 
ClassName<String, Integer> a = new ClassName<String, Integer>();
 
}
 
}

 

위 예시대로라면 T는 String이 되고, K는 Integer가 된다.

 

이 때 주의해야 할 점은 타입 파라미터로 명시할 수 있는 것은 참조 타입(Reference Type)밖에 올 수 없다. 즉, int, double, char 같은 primitive type은 올 수 없다는 것이다. 그래서 int형 double형 등 primitive Type의 경우 Integer, Double 같은 Wrapper Type으로 쓰는 이유가 바로 위와같은 이유다.

 

또한 바꿔 말하면 참조 타입이 올 수 있다는 것은 사용자가 정의한 클래스도 타입으로 올 수 있다는 것이다.

 
public class ClassName <T> { ... }
 
 
 
public class Student { ... }
 
 
 
public class Main {
 
public static void main(String[] args) {
 
ClassName<Student> a = new ClassName<Student>();
 
}
 
}

 

 

 

 

 

2. 제네릭 클래스

 

그러면 클래스 및 인터페이스를 제네릭으로 받는 방법을 알아봤으니 본격적으로 활용해보자.

 

 

[제네릭 클래스]

 
// 제네릭 클래스
 
class ClassName<E> {
 
 
 
private E element; // 제네릭 타입 변수
 
 
 
void set(E element) { // 제네릭 파라미터 메소드
 
this.element = element;
 
}
 
 
 
E get() { // 제네릭 타입 반환 메소드
 
return element;
 
}
 
 
 
}
 
 
 
class Main {
 
public static void main(String[] args) {
 
 
 
ClassName<String> a = new ClassName<String>();
 
ClassName<Integer> b = new ClassName<Integer>();
 
 
 
a.set("10");
 
b.set(10);
 
 
 
System.out.println("a data : " + a.get());
 
// 반환된 변수의 타입 출력
 
System.out.println("a E Type : " + a.get().getClass().getName());
 
 
 
System.out.println();
 
System.out.println("b data : " + b.get());
 
// 반환된 변수의 타입 출력
 
System.out.println("b E Type : " + b.get().getClass().getName());
 
 
 
}
 
}

 

 

보면 ClassName이란 객체를 생성할 때 <> 안에 타입 파라미터(Type parameter)를 지정한다.

 

그러면 a객체의 ClassName의 E 제네릭 타입은 String으로 모두 변환된다.

반대로 b객체의 ClassName의 E 제네릭 타입은 Integer으로 모두 변환된다.

 

실제로 위 코드를 실행시키면 다음과 같이 출력된다.

 

 

 

 

 

 

 

 

 

 

만약 제네릭을 두 개 쓰고 싶다면 이렇게 할 수도 있다.

 

 
// 제네릭 클래스
 
class ClassName<K, V> {
 
private K first; // K 타입(제네릭)
 
private V second; // V 타입(제네릭)
 
 
 
void set(K first, V second) {
 
this.first = first;
 
this.second = second;
 
}
 
 
 
K getFirst() {
 
return first;
 
}
 
 
 
V getSecond() {
 
return second;
 
}
 
}
 
 
 
// 메인 클래스
 
class Main {
 
public static void main(String[] args) {
 
 
 
ClassName<String, Integer> a = new ClassName<String, Integer>();
 
 
 
a.set("10", 10);
 
 
 
 
 
System.out.println(" fisrt data : " + a.getFirst());
 
// 반환된 변수의 타입 출력
 
System.out.println(" K Type : " + a.getFirst().getClass().getName());
 
 
 
System.out.println(" second data : " + a.getSecond());
 
// 반환된 변수의 타입 출력
 
System.out.println(" V Type : " + a.getSecond().getClass().getName());
 
}
 
}

 

 

결과는 다음과 같다.

 

이렇게 외부 클래스에서 제네릭 클래스를 생성할 때 <> 괄호 안에 타입을 파라미터로 보내 제네릭 타입을 지정해주는 것. 이 것이 바로 제네릭 프로그래밍이다.

 

 

 

 

 

3. 제네릭 메소드

 

 

 

위 과정까지는 클래스 이름 옆에 예로들어 <E>라는 제네릭타입을 붙여 해당 클래스 내에서 사용할 수 있는 E 타입으로 일반화를 했다.

그러나 그 외에 별도로 메소드에 한정한 제네릭도 사용할 수 있다.

 

일반적으로 선언 방법은 다음과 같다. 

 

 
public <T> T genericMethod(T o) { // 제네릭 메소드
 
...
 
}
 
 
 
[접근 제어자] <제네릭타입> [반환타입] [메소드명]([제네릭타입] [파라미터]) {
 
// 텍스트
 
}

 

클래스와는 다르게 반환타입 이전에 <> 제네릭 타입을 선언한다.

위에서 다룬 제네릭 클래스에서 활용해보도록 하자.

 

 

 

[제네릭 클래스]

 
// 제네릭 클래스
 
class ClassName<E> {
 
 
 
private E element; // 제네릭 타입 변수
 
 
 
void set(E element) { // 제네릭 파라미터 메소드
 
this.element = element;
 
}
 
 
 
E get() { // 제네릭 타입 반환 메소드
 
return element;
 
}
 
 
 
<T> T genericMethod(T o) { // 제네릭 메소드
 
return o;
 
}
 
 
 
 
 
}
 
 
 
public class Main {
 
public static void main(String[] args) {
 
 
 
ClassName<String> a = new ClassName<String>();
 
ClassName<Integer> b = new ClassName<Integer>();
 
 
 
a.set("10");
 
b.set(10);
 
 
 
System.out.println("a data : " + a.get());
 
// 반환된 변수의 타입 출력
 
System.out.println("a E Type : " + a.get().getClass().getName());
 
 
 
System.out.println();
 
System.out.println("b data : " + b.get());
 
// 반환된 변수의 타입 출력
 
System.out.println("b E Type : " + b.get().getClass().getName());
 
System.out.println();
 
 
 
// 제네릭 메소드 Integer
 
System.out.println("<T> returnType : " + a.genericMethod(3).getClass().getName());
 
 
 
// 제네릭 메소드 String
 
System.out.println("<T> returnType : " + a.genericMethod("ABCD").getClass().getName());
 
 
 
// 제네릭 메소드 ClassName b
 
System.out.println("<T> returnType : " + a.genericMethod(b).getClass().getName());
 
}
 
}

 

 

보면 ClassName이란 객체를 생성할 때 <> 안에 타입 파라미터(Type parameter)를 지정한다.

 

그러면 a객체의 ClassName의 E 제네릭 타입은 String으로 모두 변환된다.

반대로 b객체의 ClassName의 E 제네릭 타입은 Integer으로 모두 변환된다.

genericMethod()는 파라미터 타입에 따라 T 타입이 결정된다.

 

실제로 위 코드를 실행시키면 다음과 같이 출력된다.

 

 

 

즉, 클래스에서 지정한 제네릭유형과 별도로 메소드에서 독립적으로 제네릭 유형을 선언하여 쓸 수 있다.

 

 

그럼 위와같은 방식이 왜 필요한가? 바로 '정적 메소드로 선언할 때 필요'하기 때문이다.

생각해보자. 앞서 제네릭은 유형을 외부에서 지정해준다고 했다. 즉 해당 클래스 객체가 인스턴스화 했을 때, 쉽게 말해 new 생성자로 클래스 객체를 생성하고 <> 괄호 사이에 파라미터로 넘겨준 타입으로 지정이 된다는 뜻이다.

 

하지만 static 은 무엇인가? 정적이라는 뜻이다. static 변수, static 함수 등 static이 붙은 것들은 기본적으로 프로그램 실행시 메모리에 이미 올라가있다.

 

이 말은 객체 생성을 통해 접근할 필요 없이 이미 메모리에 올라가 있기 때문에 클래스 이름을 통해 바로 쓸 수 있다는 것이다.

 

 

근데, 거꾸로 생각해보자면 static 메소드는 객체가 생성되기 전에 이미 메모리에 올라가는데 타입을 어디서 얻어올 수 있을까? 

 

 
class ClassName<E> {
 
 
 
/*
 
* 클래스와 같은 E 타입이더라도
 
* static 메소드는 객체가 생성되기 이전 시점에
 
* 메모리에 먼저 올라가기 때문에
 
* E 유형을 클래스로부터 얻어올 방법이 없다.
 
*/
 
static E genericMethod(E o) { // error!
 
return o;
 
}
 
 
 
}
 
 
 
class Main {
 
 
 
public static void main(String[] args) {
 
 
 
// ClassName 객체가 생성되기 전에 접근할 수 있으나 유형을 지정할 방법이 없어 에러남
 
ClassName.getnerMethod(3);
 
 
 
}
 
}

 

위 내용을 보면 이해가 갈 것이다.

 

그렇기 때문에 제네릭이 사용되는 메소드를 정적메소드로 두고 싶은 경우 제네릭 클래스와 별도로 독립적인 제네릭이 사용되어야 한다는 것이다.

 

 

 

 

 
// 제네릭 클래스
 
class ClassName<E> {
 
 
 
private E element; // 제네릭 타입 변수
 
 
 
void set(E element) { // 제네릭 파라미터 메소드
 
this.element = element;
 
}
 
 
 
E get() { // 제네릭 타입 반환 메소드
 
return element;
 
}
 
 
 
// 아래 메소드의 E타입은 제네릭 클래스의 E타입과 다른 독립적인 타입이다.
 
static <E> E genericMethod1(E o) { // 제네릭 메소드
 
return o;
 
}
 
 
 
static <T> T genericMethod2(T o) { // 제네릭 메소드
 
return o;
 
}
 
 
 
}
 
 
 
public class Main {
 
public static void main(String[] args) {
 
 
 
ClassName<String> a = new ClassName<String>();
 
ClassName<Integer> b = new ClassName<Integer>();
 
 
 
a.set("10");
 
b.set(10);
 
 
 
System.out.println("a data : " + a.get());
 
// 반환된 변수의 타입 출력
 
System.out.println("a E Type : " + a.get().getClass().getName());
 
 
 
System.out.println();
 
System.out.println("b data : " + b.get());
 
// 반환된 변수의 타입 출력
 
System.out.println("b E Type : " + b.get().getClass().getName());
 
System.out.println();
 
 
 
// 제네릭 메소드1 Integer
 
System.out.println("<E> returnType : " + ClassName.genericMethod1(3).getClass().getName());
 
 
 
// 제네릭 메소드1 String
 
System.out.println("<E> returnType : " + ClassName.genericMethod1("ABCD").getClass().getName());
 
 
 
// 제네릭 메소드2 ClassName a
 
System.out.println("<T> returnType : " + ClassName.genericMethod1(a).getClass().getName());
 
 
 
// 제네릭 메소드2 Double
 
System.out.println("<T> returnType : " + ClassName.genericMethod1(3.0).getClass().getName());
 
}
 
}

 

 

결과는 다음과 같다.

 

 

보다시피 제네릭 메소드는 제네릭 클래스 타입과 별도로 지정된다는 것을 볼 수 있다.

<> 괄호 안에 타입을 파라미터로 보내 제네릭 타입을 지정해주는 것. 이 것이 바로 제네릭 프로그래밍이다.

 

 

Map 컬렉션 클래스 

Map 인터페이스는 Collection 인터페이스와는 다른 저장 방식을 가집니다.

Map 인터페이스를 구현한 Map 컬렉션 클래스들은 키와 값을 하나의 쌍으로 저장하는 방식(key-value 방식)을 사용합니다.

여기서 키(key)란 실질적인 값(value)을 찾기 위한 이름의 역할을 합니다.

Map이란?

- Map은 리스트나 배열처럼 순차적으로(sequential) 해당 요소 값을 구하지 않고 key를 통해 value를 얻는다.

- 맵(Map)의 가장 큰 특징이라면 key로 value를 얻어낸다는 점이다. 

 

-특징

1. 요소의 저장 순서를 유지하지 않습니다.

2. key :  중복을 허용  X 

   value :  중복은 허용 O

 

put

자바의 맵(Map)중 가장 간단한 HashMap에 대해서 알아보자.

 

HashMap<String, String> map = new HashMap<String, String>();
map.put("people", "사람");
map.put("baseball", "야구");

 

key와 value가 String 형태인 HashMap을 만들고 위에서 보았던 예제의 항목값들을 입력해 보았다. key와 value는 위 예제에서 보듯이 put메소드를 이용하여 입력한다.

※ HashMap 역시 제네릭스를 이용한다. 위의 HashMap 의 제네릭스는 Key, Value 모두 String 타입이다.

MAP TABLE

key value
people 사람
baseball 야구

Map은 리스트나 배열처럼 순차적으로(sequential) 해당 요소 값을 구하지 않고 key를 통해 value를 얻는다. 맵(Map)의 가장 큰 특징이라면 key로 value를 얻어낸다는 점이다. baseball이란 단어의 뜻을 찾기 위해서 사전의 내용을 순차적으로 모두 검색하는 것이 아니라 baseball이라는 단어가 있는 곳만을 펼쳐보는 것이다.

get

key에 해당되는 값을 얻기 위해서는 다음과 같이 한다.

 

System.out.println(map.get("people"));

 

위와같이 get 메소드를 이용하면 value값을 얻을 수 있다. 위 예제는 결과로 "사람"이라는 문자열을 출력할 것이다.

containsKey

containsKey 메소드는 맵(Map)에 해당 키(key)가 있는지를 조사하여 그 결과값을 리턴한다.

System.out.println(map.containsKey("people"));

"people"이라는 키는 존재하므로 true가 출력될 것이다.

remove

remove 메소드는 맵(Map)의 항목을 삭제하는 메소드로 key값에 해당되는 아이템(key, value)을 삭제한 후 그 value 값을 리턴한다.

 

System.out.println(map.remove("people"));

 

"people"에 해당되는 아이템(people:사람)이 삭제된 후 "사람"이 출력될 것이다.

size

size 메소드는 Map의 갯수를 리턴한다.

 

System.out.println(map.size());

 

"people", "baseball" 두 값을 가지고 있다가 "people"항목이 삭제되었으므로 1이 출력될 것이다.

 

다음은 테스트 시 사용되었던 코드 전체이다.

 

TestMap.java

 

import java.util.HashMap;

public class TestMap {
    public static void main(String[] args) {
        HashMap<String, String> map = new HashMap<String, String>();
        map.put("people", "사람");
        map.put("baseball", "야구");

        System.out.println(map.get("people"));
        System.out.println(map.containsKey("people"));
        System.out.println(map.remove("people"));
        System.out.println(map.size());
    }
}

 


LinkedHashMap과 TreeMap

Map의 가장 큰 특징은 순서에 의존하지 않고 key로 value를 가져오는데 있다. 하지만 가끔은 Map에 입력된 순서대로 데이터를 가져오고 싶은 경우도 있고 때로는 입력된 key에 의해 소트된 데이터를 가져오고 싶을 수도 있을 것이다. 이런경우에는 LinkedHashMap과 TreeMap을 사용하는 것이 유리하다.

  • LinkedHashMap은 입력된 순서대로 데이터가 출력되는 특징을 가지고 있다.
  • TreeMap은 입력된 key의 소트순으로 데이터가 출력되는 특징을 가지고 있다.

대표적인 Map 컬렉션 클래스에 속하는 클래스

 

1. HashMap<K, V> 클래스

-Map 컬렉션 클래스에서 가장 많이 사용되는 클래스 중 하나입니다.

-HashMap은 Map을 구현한다. key와 value를 묶어 하나의 entry로 저장한다는 특징을 갖는다.

-해시 알고리즘(hash algorithm)을 사용하여 많은 양의 데이터를 검색하는데 검색 속도가 매우 빠르다.

-HashMap 클래스는 Map 인터페이스를 구현하므로, 중복된 키로는 값을 저장할 수 없다.

-value에 null값도 사용 가능하다.

-멀티쓰레드에서는 HashTable을 사용한다.

( 같은 값을 다른 키로 저장하는 것은 가능 )

 

HashMap <k,v> 주요 메소드

void clear() 해당 맵(map)의 모든 매핑(mapping)을 제거함.
boolean containsKey(Object key) 해당 맵이 전달된 키를 포함하고 있는지를 확인함.
boolean containsValue(Object value) 해당 맵이 전달된 값에 해당하는 하나 이상의 키를 포함하고 있는지를 확인함.
V get(Object key) 해당 맵에서 전달된 키에 대응하는 값을 반환함.
만약 해당 맵이 전달된 키를 포함한 매핑을 포함하고 있지 않으면 null을 반환함.
boolean isEmpty() 해당 맵이 비어있는지를 확인함.
Set<K> keySet() 해당 맵에 포함되어 있는 모든 키로 만들어진 Set 객체를 반환함.
V put(K key, V value) 해당 맵에 전달된 키에 대응하는 값으로 특정 값을 매핑함.
V remove(Object key) 해당 맵에서 전달된 키에 대응하는 매핑을 제거함.
boolean remove(Object key, Object value) 해당 맵에서 특정 값에 대응하는 특정 키의 매핑을 제거함.
V replace(K key, V value) 해당 맵에서 전달된 키에 대응하는 값을 특정 값으로 대체함.
boolean replace(K key, V oldValue, V newValue) 해당 맵에서 특정 값에 대응하는 전달된 키의 값을 새로운 값으로 대체함.
int size() 해당 맵의 매핑의 총 개수를 반환함.

 

 

2. Hashtable<K, V>

-HashMap 클래스와 같은 동작을 하는 클래스.

-Hashtable 클래스는 HashMap 클래스와 마찬가지로 Map 인터페이스를 상속받음.

-기존 코드와의 호환성을 위해서만 남아있으므로, Hashtable 클래스보다는 HashMap 클래스를 사용하는 것이 좋다.

 

3. TreeMap<K, V>

-키와 값을 한 쌍으로 하는 데이터를 이진 검색 트리(binary search tree)의 형태로 저장합니다.

-이진 검색 트리는 데이터를 추가하거나 제거하는 등의 기본 동작 시간이 매우 빠릅니다.

-TreeMap 클래스는 NavigableMap 인터페이스를 기존의 이진 검색 트리의 성능을 향상시킨 레드-블랙 트리(Red-Black tree)로 구현합니다.

-Map 인터페이스를 구현하므로, 중복된 키로는 값을 저장할 수 없습니다.

( 같은 값을 다른 키로 저장하는 것은 가능 )

 

TreeMap<K, V> 주요 메소드

Map.Entry<K, V> ceilingEntry(K key) 해당 맵에서 전달된 키와 같거나, 전달된 키보다 큰 키 중에서 가장 작은 키와 그에 대응하는 값의 엔트리를 반환함. 만약 해당하는 키가 없으면 null을 반환함.
K ceilingKey(K key) 해당 맵에서 전달된 키와 같거나, 전달된 키보다 큰 키 중에서 가장 작은 키를 반환함.
만약 해당하는 키가 없으면 null을 반환함.
void clear() 해당 맵(map)의 모든 매핑(mapping)을 제거함.
boolean containsKey(Object key) 해당 맵이 전달된 키를 포함하고 있는지를 확인함.
boolean containsValue(Object value) 해당 맵이 전달된 값에 해당하는 하나 이상의 키를 포함하고 있는지를 확인함.
NavigableMap<K, V> descendingMap() 해당 맵에 포함된 모든 매핑을 역순으로 반환함.
Set<Map.Entry<K, V>> entrySet() 해당 맵에 포함된 모든 매핑을 Set 객체로 반환함.
Map.Entry<K, V> firstEntry() 해당 맵에서 현재 가장 작은(첫 번째) 키와 그에 대응하는 값의 엔트리를 반환함.
K firstKey() 해당 맵에서 현재 가장 작은(첫 번째) 키를 반환함.
Map.Entry<K, V> floorEntry(K key) 해당 맵에서 전달된 키와 같거나, 전달된 키보다 작은 키 중에서 가장 큰 키와 그에 대응하는 값의 엔트리를 반환함. 만약 해당하는 키가 없으면 null을 반환함.
K floorKey(K key) 해당 맵에서 전달된 키와 같거나, 전달된 키보다 작은 키 중에서 가장 큰 키를 반환함.
만약 해당하는 키가 없으면 null을 반환함.
V get(Object key) 해당 맵에서 전달된 키에 대응하는 값을 반환함.
만약 해당 맵이 전달된 키를 포함한 매핑을 포함하고 있지 않으면 null을 반환함.
728x90