본문 바로가기
  • 비둘기다
  • 비둘기다
  • 비둘기다
코딩/JAVA Basics

[자바 JAVA] 제네릭과 컬렉션 프레임워크

by parzival56 2022. 12. 7.

제네릭 타입

제네릭이라는 말은 영어 general에서 나온 말로 '포괄적인'이라는 뜻입니다. 즉, 여태까지 하던 방법처럼 계속 타입을 선언하고 변환하는 과정을 거치는 것이 아니라 이 모든 과정이 필요 없게 한 가지 키워드에 모든 것을 함축해놓은 타입을 의미합니다. 

제네릭 타입이란, 타입을 파라미터로 가지는 클래스와 인터페이스를 의미합니다. 제네릭 타입은 < > 표시로 선언합니다.

제네릭은 기존의 타입들을 포괄적인 범위로 변경해주는 역할이기 때문에 완전히 생소한 단어는 없습니다.

기본 타입이라고 한다면 int, double, char, float 등이 있습니다. 그러면 제네릭 타입에서의 파라미터는 Integer, Double, Character, Float 등이 됩니다. 이들은 모두 기본 타입의 풀네임과 첫 글자를 대문자로 선언하여 사용하죠.

String은 그 자체가 기본 타입과 제네릭 파라미터의 특성을 모두 갖추고 있어 특별한 형태는 존재하지 않습니다. 

 

위에서 말씀드린 Integer, Double, String 등은 모두 이들의 기본 타입이 하는 역할과 더불어 메서드들까지 담고 있기 때문에 제네릭 타입들은 모두클래스나 인터페이스라고 할 수 있습니다. 이들은 모두 java.lang.Object라는 가장 상위에 있는 파일에 모두 담겨있으나 Object는 원래 모든 파일에서 생략된 채로 생성되기 때문에 굳이 import를 하지 않아도 됩니다. 

 

제네릭 타입을 사용할 때에는 <> 안에 클래스 혹은 인터페이스가 들어가게 됩니다. 앞서 말씀드린 Integer, String도 클래스, 인터페이스의 일종이고 우리가 임의로 만들어주는 class Person에서의 Person도 클래스이기에 <> 안에 들어갈 수 있습니다. 

"아니 Integer, String 같은 애들은 자바 IDE에서 제공하는 Object안에 들어있으니까 <>안에 넣으면 메서드의 기능들을 수행하는 건 알겠는데 그럼 Person을 넣으면 뭐가 되나"라고 질문한다면 간단하게 답할 수 있습니다.

이는 쉽게 말해 제네릭 타입을 가지는 객체에 해당 클래스가 가지는 모든 내용을 상속해주는 것과 같습니다.

엄밀히 말하면 상속은 아니죠. 예를 들어 후에 나오겠지만 Person같은 개인이 선언한 클래스를 타입으로 설정하면 해당 클래스를 타입으로 가지는 객체는 Person의 필드 값 (변수들)과 메서드를 모두 사용할 수 있게 됩니다. 설령 필드에 private형이 있어도 상속과는 다른 방식의 접근이기 때문에 별도의 getter, setter 없이 사용이 가능하죠.

 

 

이제 기존의 타입 사용법을 제네릭 타입을 사용하는 방법과 함께 비교해보도록 하겠습니다.

// 기준 클래스
public class Box<T> {
    private T t;
    public T get{} { return t; }
    public void set(T t) { this.t = t; }
}

// 기존 방법
public class Box<String> {
    private String t;
    public void set(String t) {
        this.t = t;
    }
    public String get() { return t; }

// 제네릭 타입을 사용하면
Box<String> box = new Box<String>();
box.set("hello");
String str = box.get();

위의 기준 클래스에서 <T>의 의미는 아무 타입이나 들어갈 수 있는 임의의 타입 T를 넣어 후에 String 등을 넣어 처리하는 것입니다. 후에 와일드 카드 타입 부분에 나오는 내용입니다.

제네릭 타입은 <1, 2, 3, .....>처럼 사용하여 두 개 이상의 파라미터가 사용 가능합니다. 

 

제네릭 타입은 클래스 뿐만 아니라 메서드에도 적용이 가능합니다.

public<타입 파라미터> 리턴타입 메서드명(매개변수) {}

 제네릭 메서드는 위와 같이 선언합니다. 

 

제네릭 타입에는 와일드카드 타입이라는 것이 존재합니다. 거창한 것이 아니라 와일드카드의 정의대로 지정되지 않은 아무거나를 의미 합니다.

와일드카드 타입에는 크게 3가지가 존재하는데 이들은 각각

1. <?> : 제한 없음

2. <? extends 상위 타입> : 상위 클래스 제한

3. <? super 하위 타입> : 하위 클래스 제한

1번은 파라미터로 모든 타입, 클래스, 인터페이스가 올 수 있습니다. 2, 3번은 자신보다 상위 혹은 하위의 타입들을 제한 두는 방법입니다. 

 

제네릭 타입을 상속받을 시에는 상속받는 자식은 무조건 제네릭 타입으로 선언해야 합니다. 자식의 파라미터는 반드시 부모의 파라미터를 담고 있어야 하며 필요에 따라 파라미터를 추가해도 됩니다.

public class Child<T, M, C> extends Parent<T, M> {}

 

컬렉션 프레임워크

컬렉션 프레임워크에서 컬렉션이란 요소들을 수집해 저장하는 것을 의미합니다. 배열과 같은 맥락을 가지죠.

 

그러면 예를 들어 만들어진 배열에서 값을 삭제하는 과정을 생각해봅시다.

먼저, 삭제하고자 하는 값이 있는 인덱스에 접근하여 값을 0이나 null 같은 기본값으로 바꿔줍니다. 그다음은 이제 배열의 인덱스 순서를 앞당겨야 하는데 거의 불가능이죠. 솔직히 상상도 하기 싫은 과정입니다. 

 

만약 이렇게 값을 삭제할 때 인덱스를 앞당겨주지 않고 그대로 내버려 둔다면 어떻게 될까요? 

컴파일을 하는데 시간이 굉장히 오래 걸릴 것입니다. 필요도 없는 인덱스 구간을 계속 가지고 있는 것이기 때문입니다. 쉬운 예제로 코딩하는 것이라면 하나쯤 빠지는데 시간이 얼마나 더 걸리겠냐 생각하실 수도 있지만 규모가 커져버리면 딜레이 시간은 배가 됩니다. 그래서 이러한 고생을 하지 않고 편하게 배열을 편집할 수 있는 가능이 어디 없을까 하다가 탄생한 개념이 컬렉션입니다.

 

기존의 배열의 문제점이라고 한다면 저장할 수 있는 객체의 수를 배열을 선언하면서 결정을 해버립니다. 그러면 추가, 삭제를 할 때 문제가 생기죠. 그리고 객체를 삭제했을 때 인덱스가 비게 됩니다. 아까 말씀드린 단점이죠. 만약 100개의 객체가 배열에 저장되어 있을 때 특정 값을 지닌 인덱스를 삭제하고 싶을 때 100개의 인덱스를 모두 뒤져가며 찾아야 삭제가 가능하죠. 

 

컬렉션 프레임워크는 위의 문제들을 보완해줍니다. 객체들을 효율적으로 추가, 삭제, 검색할 수 있게 제공되는 라이브러리죠. 이는 java.util 패키지에 포함되어 있습니다. 컬렉션에는 set, list, map이 있는데 먼저 list부터 살펴보겠습니다.

 

List 컬렉션

리스트의 특징으로는 배열과 비슷하게 인덱스로 객체들을 관리한다는 점과 객체의 값이 중복 저장이 가능합니다. 

구현된 클래스로는 ArraytList, Vector, LinkedList가 있습니다. 

list 메서드

위는 리스트라는 컬렉션에 내재되어 있는 메서드입니다. 즉, 별도의 과정 필요 없이 위의 메서드를 사용하면 추가, 검색, 삭제가 가능하죠.

1. ArrayList

먼저 ArrayList입니다. 이 리스트의 특징이라고 하면 저장 용량이 정해져 있지 않습니다. 용량을 지정하지 않을 시 초기 용량인 10으로 자동으로 바뀌지만 객체가 들어오면 늘어나고 중간에 객체가 삭제되면 그 뒤의 인덱스가 앞으로 앞당겨지기 때문에 배열의 문제점을 보완한 컬렉션이라고 할 수 있습니다. 

import java.util.*;

public class ArrayListEx {
    public static void main(String[] args) {
        // 문자열만 삽입가능한 ArrayList 컬렉션 생성
        ArrayList<String> a = new ArrayList<String>();
        
        // 키보드로부터 4개의 이름 입력받아 ArrayList에 삽입
        Scanner scanner = new Scanner(System.in); 
        for(int i=0; i<4; i++) {
            System.out.print("이름을 입력하세요>>");
            String s = scanner.next(); // 키보드로부터 이름 입력
        
            a.add(s); // ArrayList 컬렉션에 삽입
        }
        
        // ArrayList에 들어 있는 모든 이름 출력
        for(int i=0; i<a.size(); i++) {
            // ArrayList의 i 번째 문자열 얻어오기
            String name = a.get(i); 
            System.out.print(name + " ");
        }
    scanner.close();
}

위는 이름을 입력받아 ArratList에 저장하는 예시 코드입니다. 별도의 크기 선언 없이 ArrayList를 선언하고 for문으로 값을 입력받아줍니다. 값을 입력 받음과 동시에 .add를 통해 즉시 리스트에 넣어주고 이를 출력문으로 출력하는 코드입니다.

그리고 두 번째 for문은 a.size()라고 되어있는데 이는 배열로 for문을 돌릴 때와 똑같이 리스트의 크기만큼 반복해주는 것이라고 생각하시면 됩니다. 그리고 밑의 get은 위의 표에 나온 대로 값을 반환문 없이 자동으로 반환해주는 메서드입니다.

 

2. Vector

List<E> list = new Vector<E>();

위와 같이 선언하고 벡터의 특징이라고 하면 스레드와 동기화된다는 것입니다. 벡터는 스레드를 통해 접근받는데 다수의 스레드가 접근해 벡터에 영향을 주더라도 실제 스레드에는 영향이 없어 자유롭게 조작이 가능한 것이 특징입니다.

 

import java.util.Vector;
class Point {
  private int x, y;
  public Point(int x, int y) {
    this.x = x;
    this.y = y;
  }
  public String toString() {
    return "(" + x + "," + y + ")"; 
  }
}

public class PointVectorEx {
  public static void main(String[] args) {
  // Point 객체를 요소로만 가지는 벡터 생성
  Vector<Point> v = new Vector<Point>(); 
  
  // 3 개의 Point 객체 삽입
  v.add(new Point(10, 20));
  v.add(new Point(-5, 10));
  v.add(new Point(30, -8));
  
  v.remove(1); // 인덱스 1의 Point(-5, 10) 객체 삭제
  
  // 벡터에 있는 Point 객체 모두 검색하여 출력
  for(int i=0; i<v.size(); i++) {
    Point p = v.get(i); // 벡터에서 i 번째 Point 객체 얻어내기
    System.out.println(p); // p.toString()을 이용하여 객체 p 출력
  }
 }
}

위는 벡터의 이름에 걸맞게 좌표값을 표시하는 예제 코드입니다. 이 예제는 제네릭 타입을 Point로 하는 것이 중요합니다.

어러한 코딩이 제네릭의 강점 중 하나인데 가장 위 제네릭을 말씀드리면서 <> 안에는 Integer 같은 Object의 클래스뿐만 아니라 내가 생성해주는 모든 클래스, 인터페이스들이 다 들어갈 수 있다고 말씀드렸고, 그 역할은 클래스, 인터페이스를 제네릭 타입으로 가지는 객체가 선언한 제너럴 타입의 모든 필드, 메서드를 사용할 수 있게 되는 것입니다.

 

위의 코드를 해석해보자면 v는 Vector를 자료형으로 가지기에 스레드 동기화를 이루는 일종의 배열 형태로 자리잡게 됩니다. 그리고 제네릭 타입을 Point로 하여 int x, y와 toString메서드를 사용할 수 있게 됩니다. 

여기서 포인트는 Point의 생성자는 (int x, int y)를 요구하기 때문에 Point타입인 v도 마찬가지로 Point형 객체를 생성할 시에 이를 따라야 하는 것입니다. 그리고 Point의 필드 값이 private인데 이를 접근할 수 있을까 하실 수 있습니다.

만약 private이 필드에 있는 클래스를 상속받는다면 getter, setter로 대응을 하겠지만 으러한 경우는 별도의 처리 없이 바로 private에 접근하여 사용할 수 있습니다.

Point의 객체를 생성하면서 add를 해주는 것이 특징입니다. 그리고 remove를 통해 간단하게 해당 인덱스에 있는 값을 삭제시킵니다. 마지막은 toString을 이용하여 (x, y)로 출력하는 것이 가능합니다. (Point 타입이기 때문에)

 

3. Iterator<E>

이는 Vector, ArrayList, LinkedList가 상속받는 인터페이스입니다.

iterater 메서드

 위와 iterater의 메서드들인데 iterator는 위의 두 가지와 달리 추가, 삭제보다는 순차 검색에 중점을 준 메서드들이 주를 이룹니다. 

 

4. LinkedList

linkedlist는 arraylist와 비슷하지만 객체들을 다루는 방법이 다릅니다. linkedlist는 인접한 참조를 링크하여 체인처럼 관리합니다. 그래서 특정 객체를 추가, 삭제하게 되면 참조 링크만 변경하여 자동으로 이어 주기만 하면 끝이기 때문에 주로 빈번한 삭제와 삽입이 일어나는 곳에서는 arraylist보다 유용합니다. 

 

 

Collections 클래스

⚫java.util 패키지에 포함

⚫컬렉션에 대해 연산을 수행하고 결과로 컬렉션 리턴

⚫모든 메서드는 static 타입

⚫주요 메서드

          ▪ 컬렉션에 포함된 요소들을 소팅하는 sort() 메서드

          ▪ 요소의 순서를 반대로 하는 reverse() 메서드

          ▪ 요소들의 최대, 최솟값을 찾아내는 max(), min() 메서드

          ▪ 특정 값을 검색하는 binarySearch() 메서드

collections 클래스를 이용한 문자열 정렬, 검색 예시입니다. 한글까지 sort로 정렬되는 것을 확인할 수 있습니다. 

 

Set 컬렉션

set은 list와 유사한 부분이 많습니다. 그러나 차이점이라고 한다면 객체는 중복 저장이 불가능하며 저장 순서가 랜덤입니다. 구현 클래스로는 HashSet, LinkedHashSet, TreeSet이 있습니다.

위의 list의 메서드와 유사한 메서드들을 많이 찾아볼 수 있습니다. 

 

HashSet

hashset은 보통 동일한 객체를 판단하는데 많이 사용합니다. 말씀드린 대로 값을 중복 저장하지 않기 때문에

!이름.add(객체)를 통해 중복된 값을 쉽게 골라낼 수 있습니다.

 

Map 컬렉션

map은 파이썬의 딕셔너리와 유사한 개념입니다. 유일무이한 key값이 있고 이에 대응하는 value가 하나 존재합니다. key, value는 모두 객체로 처리되고, key는 중복 불가지만 value는 중복이 가능합니다. 

구현 클래스로는 HashMap, Hashtable, LinkedHashMap, Properties, TreeMap이 있습니다. 

map의 메서드이며 key와 value를 가지기 때문에 메서드의 수가 많을 것을 확인할 수 있습니다. 

 

 

이상으로 제네릭 타입과 컬렉션 프레임워크에 대해 알아봤습니다.

'코딩 > JAVA Basics' 카테고리의 다른 글

[자바 JAVA] 예외처리  (0) 2022.12.06
[자바 JAVA] 자바의 람다식  (0) 2022.12.06
[자바 JAVA] 중첩 클래스  (0) 2022.12.06
[자바 JAVA] 추상클래스와 인터페이스  (0) 2022.12.06
[자바 JAVA] 다형성  (0) 2022.12.05

댓글