제네릭 프로그래밍
제네릭 프로그래밍(generic programming)이란 다양한 종류의 데이터를 처리할 수 있는 클래스와 메소드를 작성하는 기법이다.
Object 타입의 변수를 사용하는 것보다 안전하고 사용하기 쉽다.
제네릭은 클래스를 정의할 때, 클래스 안에서 사용되는 자료형(타입)을 구체적으로 명시하지 않고 T와 같이 기호로 적어놓는 것이다.
객체를 생성할 때 T자리에 구체적인 자료형을 적어주면 된다.
이전의 방법
모든 객체는 궁극적으로 Object의 자손이다. 따라서 다형성에 의하여 Object 참조 변수는 어떤 객체든지 참조할 수 있는 것이다.
public class Box {
private Object data;
public void set(Object data) {
this.data = data;
}
public Object get() {
return data;
}
이것은 상당히 편리한 기능이지만 데이터를 꺼낼 때마다 항상 형변환을 하여야 한다는 단점이 있다.
Box b = new Box();
b.set("Hello World!"); // 문자열 객체를 저장
String s = (String) b.get(); // Object 타입을 String 타입으로 형변환
b.set(new Integer(10)); // 정수형 객체를 저장
Integer i = (Integer)b.get(); // Object 타입을 Integer 타입으로 형변환
제네릭을 이용한 방법
제네릭 기법을 이용하게 되면 앞의 단점을 해결할 수 있다.
Box 클래스를 제네릭으로 다시 작성하여 보면 다음과 같다.
class Box<T> { // T는 타입을 의미한다
private T data;
public void set(T data) {
this.data = data;
}
public T get() {
return data;
}
앞의 코드와 비교하여 보면 제네릭 클래스에서는 자료형을 표시하는 자리에 Object 대신에 T가 사용되고 있음을 알 수 있다.
일반적으로 대문자를 이용하여 타입 변수를 표시한다.
타입 매개 변수의 값은 객체를 생성할 때 구체적으로 결정된다.
문자열을 저장하는 Box 클래스의 객체를 생성하려면 T 대신에 String을 사용하면 된다.
Box<String> b = new Box<String>(); // = new Box();
정수를 저장하는 Box 클래스의 객체를 생성하려면 T 대신에 Integer를 사용하면 된다.
Box<Integer> b = new Box<Integer>(); // new Box();
컬렉션이란?
컬렉션은 자료를 저장하기 위한 구조이다.
많이 사용되는 자료구조로는 리스트(list), 스택(stack), 큐(queue), 집합(set), 해쉬 테이블(hash table) 등이 있다.
컬렉션은 앞에서 설명한 제네릭 기법으로 구현되어 있기 때문에 어떠한 타입의 데이터도 저장할 수 있다.
컬렉션의 종류

컬렉션의 특징
- 컬렉션은 제네릭을 사용한다.
- 컬렉션에서는 기초 자료형을 저장할 수 없고 클래스만 저장 가능하다.
- 기초 자료형을 클래스로 감싼 랩퍼 클래스는 사용할 수 있다.
- 기본 자료형을 저장하면 자동으로 랩퍼 클래스의 객체로 변환된다. 이것을 오토박싱(auto boxing)이라고 한다.
컬렉션 인터페이스의 주요 메소드
메소드 | 설명 |
boolean add(E e) | 해당 컬렉션에 전달된 요소를 추가 |
void clear() | 해당 컬렉션의 모든 요소를 제거 |
boolean contains(Object o) | 해당 컬렉션이 전달된 객체를 포함하고 있는지 확인 |
boolean equals(Object o) | 해당 컬렉션과 전달된 객체가 같은지를 확인 |
boolean isEmpty() | 해당 컬렉션이 비어있는지 확인 |
Iterator<E> iterator() | 해당 컬렉션의 iterator를 반환 |
boolean remove(Object o) | 해당 컬렉션에서 전달된 객체를 제거 |
int size() | 해당 컬렉션의 요소의 총 개수를 반환 |
Object[] toArray() | 해당 컬렉션의 모든 요소를 Object 타입의 배열로 반환 |
벡터
벡터(Vector) 클래스는 가변 크기의 배열을 구현하고 있다.
기존의 배열은 크기가 고정되어 있어서 사용하기 불편하다.
하지만 벡터는 요소의 개수가 늘어나면 자동으로 배열의 크기가 늘어난다.
또한 벡터는 제네릭 기법을 사용하고 있으므로 어떤 타입의 객체라도 저장할 수 있다.
정수와 같은 기초형 데이터도 오토박싱 기능을 이용하여서 객체로 변환되어 저장할 수 있다.
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Vector;
public class VectorExample1 {
public static void main(String[] args) {
List<String> vec = new Vector();
System.out.println("요소 추가");
vec.add("Apple");
vec.add("Orange");
vec.add("Mango");
System.out.println("\n크기");
System.out.println(vec.size());
System.out.println("\n인덱스 접근");
System.out.println(vec.get(1));
System.out.println("\n정렬 (오름차순)");
Collections.sort(vec);
for (String s : vec) {
System.out.print(s + " ");
}
System.out.println();
System.out.println("\n정렬 (내림차순)");
Collections.sort(vec, Collections.reverseOrder());
for (String s : vec) {
System.out.print(s + " ");
}
System.out.println();
// 정렬 다른 방법
// Arrays.sort();
System.out.println("\n삭제");
String result = vec.remove(2);
System.out.println(result);
System.out.println(vec.size());
System.out.println("\n값 찾기");
boolean search = vec.contains("mango");
System.out.println(search);
System.out.println("\n대소문자 구분 없이 값 찾기");
String a = "Mango";
boolean check = a.equalsIgnoreCase("mAngo");
System.out.println(check);
}
}
ArrayList
ArrayList도 가변 크기의 배열을 구현하는 클래스이다.
Vector는 스레드 간의 동기화를 지원하는데 반하여 ArrayList는 동기화를 하지 않기 때문에 성능은 ArrayList가 우수하다.
멀티 스레드 상황이라면 Vector를 사용하는 것이 좋다.
ArrayList의 기본 연산
생성
ArrayList<String> list = new ArrayList<String>(); // new ArrayList();
생성된 ArrayList 객체에 데이터를 저장하려면 add() 메소드를 사용한다.
list.add("MILK");
list.add("BREAD");
list.add("BUTTER");
만약에 기존의 데이터가 들어 있는 위치를 지정하여서 add()를 호출하면 새로운 데이터는 중간에 삽입된다.
list.add(1, "APPLE"); // 인덱스0 : MILK, 인덱스1 : APPLE, 인덱스2 : BREAD, ...
만약 특정한 위치에 있는 원소를 바꾸려면 set() 메소드를 사용한다.
list.set(2, "GRAPE"); // 인덱스2의 원소를 "GRAPE"로 대체
데이터를 삭제하려면 remove() 메소드를 사용한다.
list.remove(3); // 인덱스3의 원소를 삭제한다.
ArrayList 객체에 저장된 객체를 가져오는 메소드는 get()이다.
get()은 인덱스를 받아서 그 위치에 저장된 원소를 반환한다.
String s = list.get(1); // "APPLE" 반환
현재 저장된 원소의 개수를 알려면 size() 메소드를 이용한다.
list.size();
어떤 값이 리스트에 포함되어 있는지를 검사하려면 contains() 메소드를 사용한다.
if (list.contains("APPLE")) {
System.out.println("APPLE이 리스트에서 발견되었습니다.");
배열을 리스트로 변경하기
Arrays.asList() 메소드는 배열을 받아서 리스트 형태로 반환한다.
List<String> list = Arrays.asList(new String[size]);
LinkedList
ArrayList의 중간에서 데이터의 삽입이나 삭제가 빈번하게 발생하는 경우에는 큰 문제가 된다.
왜냐하면 삽입이나 삭제 위치의 뒤에 있는 원소들을 이동하여야 하기 때문이다.
이런 경우에는 연결 리스트로 구현된 LinkedList를 사용하는 것이 좋다.
import java.util.LinkedList;
public class LinkedListTest {
public static void main(String[] args) {
LinkedList<String> list = new LinkedList<String>();
list.add("MILK");
list.add("BREAD");
list.add("BUTTER");
list.add(1, "APPLE"); // 인덱스 1에 "APPLE"을 삽입
list.set(2, "GRAPE"); // 인덱스 2에 원소를 "GRAPE"으로 대체
list.remove(3); // 인덱스 3의 원소를 삭제한다.
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i) + " ");
}
}
}
Set
순서에는 상관없이 데이터만 저장하고 싶은 경우에는 집합(Set)을 사용할 수 있다.
데이터의 중복을 막도록 설계되어 있다.
- HashSet
- 해쉬 테이블에 원소를 저장하기 때문에 성능면에서 가장 우수
- 원소들의 순서가 일정하지 않다.
- TreeSet
- 레드-블랙 트리(red-black tree)에 원소를 저장
- 값에 따라 순서가 결정되지만 HashSet보다는 느리다.
- LinkedHashSet
- 해쉬 테이블과 연결 리스트를 결합한 것
- 원소들의 순서는 삽입되었던 순서와 같다.
- 약간의 비용을 들여서 HashSet의 문제점인 순서의 불명확성을 제거한 방법이다.
HashSet을 사용하여 문자열을 저장해보면
import java.util.HashSet;
public class SetTest {
public static void main(String[] args) {
HashSet<String> set = new HashSet<>();
set.add("Milk");
set.add("Bread");
set.add("Butter");
set.add("Cheese");
set.add("Ham");
set.add("Ham");
System.out.println(set);
if (set.contains("Ham")) {
System.out.println("Ham도 포함되어 있음");
}
}
}
/*
실행결과
[Ham, Butter, Cheese, Milk, Bread]
Ham도 포함되어 있음
*/
LinkedHashSet을 사용한다면 다음과 같은 결과가 얻어진다. 입력된 순서대로 출력됨에 주의
import java.util.LinkedHashSet;
public class SetTest {
public static void main(String[] args) {
LinkedHashSet<String> set = new LinkedHashSet<>();
set.add("Milk");
set.add("Bread");
set.add("Butter");
set.add("Cheese");
set.add("Ham");
set.add("Ham");
System.out.println(set);
if (set.contains("Ham")) {
System.out.println("Ham도 포함되어 있음");
}
}
}
/*
실행결과
[Milk, Bread, Butter, Cheese, Ham]
Ham도 포함되어 있음
*/
만약 TreeSet을 사용한다면 다음과 같은 결과가 얻어진다. 알파벳 순서대로 정렬되는 것에 주의
import java.util.TreeSet;
public class SetTest {
public static void main(String[] args) {
TreeSet<String> set = new TreeSet<>();
set.add("Milk");
set.add("Bread");
set.add("Butter");
set.add("Cheese");
set.add("Ham");
set.add("Ham");
System.out.println(set);
if (set.contains("Ham")) {
System.out.println("Ham도 포함되어 있음");
}
}
}
/*
실행결과
[Bread, Butter, Cheese, Ham, Milk]
Ham도 포함되어 있음
*/
합집합과 교집합
자바 Set에도 addAll()과 retainAll()이라는 메소드가 있어서 합집합, 교집합을 구현한다.
Set<Integer> s1 = new HashSet<>(Arrays.asList(1, 2, 3, 4, 5, 7, 9));
Set<Integer> s2 = new HashSet<>(Arrays.asList(2, 4, 6, 8));
s1.retainAll(s2); // 교집합을 계산한다.
System.out.println(s1)
Map
Map은 키-값을 하나의 쌍으로 묶어서 저장하는 자료구조이다.
Map은 중복된 키를 가질 수 없다.
각 키는 오직 하나의 값에만 매핑될 수 있다.
키가 제시되면 Map은 값을 반환한다.
Map은 List와 같은 자료구조와는 상당히 다르기 때문에 Collection 인터페이스를 사용하지 않고 별도의 Map이라는 이름의 인터페이스가 제공되고 이 인터페이스를 구현한 HashMap이라는 클래스가 제공된다.
HashMap은 해싱 테이블에 데이터를 저장한다.
데이터를 저장하려면 put() 메소드를 사용한다.
키들은 중복되지 않아야 한다.
Map<Integer, String> freshman = new HashMap<>(); // 생성
freshman.put("kim", "1234"); // 저장
값을 추출하려면 get() 메소드를 사용하면 된다.
value = freshman.getr("kim"); // "1234"를 반환
한 줄의 문장을 사용하여 HashMap을 초기화할 수 있다.
Map<Integer, String> map = Map.of("kkim", "1234", "park", "pass", "lee", "word");
// "kim" "1234"
// "park" "pass"
// "lee" "word"
Queue
큐(queue)는 데이터를 처리하기 전에 잠시 저장하고 있는 자료구조이다.
큐는 선입선출이다.
예제
import java.util.LinkedList;
import java.util.Queue;
public class QueueTest {
public static void main(String[] args) {
Queue<Integer> q = new LinkedList<>();
for (int i = 0; i < 5; i++) {
q.add(i);
}
System.out.println("큐의 요소 : " + q);
int e = q.remove();
System.out.println("삭제된 요소 : " + e);
System.out.println(q);
}
}
/*
실행결과
큐의 요소 : [0, 1, 2, 3, 4]
삭제된 요소 : 0
[1, 2, 3, 4]
*/
Share article