자바 - 컬렉션 프레임워크

신범철·2022년 1월 23일
0

머리말

이글은 자바를 빠르게 끝내기 위해 내가 헷갈리는 부분만을 정리해논 글입니다. Do it! 자바 프로그래밍 입문을 정리하지만 내용을 많이 건너뜀

목차

제네릭

컬렉션 프레임워크

제네릭(Generic)

정의

제네릭은 클래스 / 인터페이스 / 메서드 등의 타입을 파라미터로 사용할 수 있을게 해주는 역할을 한다.
'데이터 형식에 의존하지 않고, 하나의 값이 여러 다른 데이터 타입들을 가질 수 있도록 하는 방법'

제네릭 예시

우리가 흔히 쓰는 ArrayList, LinkedList등을 생성할 때 제네릭이 사용가능하다.
객체<타입> 객체명 = new 객체<타입>();

    ArrayList<Integer> list1 = new ArrayList<Integer>();
    ArrayList<String> list2 = new ArrayList<String>();
    
    LinkedList<Double> list3 = new LinkedList<Double>();
    LinkedList<Character> list4 = new LinkedList<Character>();

이렇게 <>괄호 안에 들어가는 타입을 지정해준다.

제네릭 사용 이유

우리가 어떤 자료 구조(Class)를 만들어 배포하려고 한다. 그런데 String 타입도 지원하고 싶고 Integer 타입도 지원하고 싶고 많은 타입을 지원하고 싶다. 그러면 String에 대한 클래스, Interger에 대한 클래스 등 하나하나 타입에 따라 만들 것인가? 그건 너무 비효율적이다. 이런 문제를 해결하기 위해 제네릭을 사용한다.

제네릭은 클래스 내부에서 지정하는 것이 아닌 외부에서 사용자에 의해 지정되는 것을 의미한다.
-> 정확하게 얘기하면 지정된다는 것 보다는 타입의 경계를 지정하고, 컴파일때 해당 타입으로 캐스팅하여 매개변수화된 유형을 삭제하는 것이다.

제네릭(Generic)의 장점

  • 제네릭을 사용하면 잘못된 타입이 들어올 수 있는 것을 컴파일 단계에서 방지할 수 있다.
  • 클래스 외부에서 타입을 지정해주기 때문에 따로 타입을 체크하고 변환해줄 필요가 없다. 즉 관리가기가 편리하다.
  • 비슷한 기능을 지원하는 경우 코드의 재사용성이 높아진다.

제네릭 사용 방법

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

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

제네릭 클래스 및 인터페이스 선언

    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으로 쓰는 이유가 바로 위와 같은 이유이다.

Reference Type와 primitive Type에 대해서는 아래 링크를 참고하자
참조타입 원시타입 참고 링크

참조타입이 가능하므로 사용자가 정의한 클래스도 타입으로 올 수 있다는 것이다.

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

제네릭 클래스 활용

// 제네릭 클래스
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());
	}
}

결과는 다음과 같다

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

제네릭 메서드

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

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이란 객체를 생성할 때 <>안에 타입 파라미터를 지정한다.

그러면 a객체의 ClassName의 E 제네릭 타입은 String으로 모두 변환된다.
반대로 b객체의 ClassName의 E 제네릭 타입은 Integer으로 모두 변환된다.
geneicMethod()는 파라미터 타입에 따라 T 타입이 결정된다.

코드 실행 결과

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

위와 같은 방식이 왜 필요한가? 바로 정적 메소드로 선언할 때 필요 하기 때문이다.
static 메서드는 객체가 생성되기 이전 시점에서 메모리에 먼저 올라가기 때문에 제네릭이 사용되는 메소드를 정적 메소드로 두고 싶은 경우 제네릭 클래스와 별로도 독립적인 제네릭이 사용되어야한다.

더 자세한 내용은 참고문헌을 보자

컬렉션 프레임워크

컬렉션 프레임 워크란?

  • 프로그램에 필요한 자료구조와 알고리즘을 구현해 놓은 라이브러리
    - 자료구조와 알고리즘은 데이터들을 어떤 구조로 관리했을 때 가장 효율적인 알고리즘으로 적용해서 최적의 퍼포먼스를 보일 것인 것에 대한 내용
  • java.util 패키지에 구현되어 있음
  • 컬렉션 프레임워크를 사용함으로 개발에 소요되는 시간을 절약하고 최적화된 라이브러리를 사용할 수 있다.
  • 크게 Colletion 인터페이스와 Map 인터페이스로 구성된다.

Colletion 인터페이스

  • 하나의 객체의 자료 구조를 관리를 위해 선언된 인터페이스로 필요한 기본메서드가 선언되어 있다.
  • 하위에 List인터페이스와 Set인터페이스가 있다.

  • Collection 인터페이스에서 제공하는 주요 메서드

Map 인터페이스

  • 쌍으로 이루어진 객체를 관리하는데 필요한 여러 메서드가 선언되어 있다.
    - Colletion은 하나, Map은 쌍이니까 두개
  • Map을 사용하는 객체는 Key-Value 쌍으로 되어 있고 Key는 중복될 수 없지만 Value는 중복이 가능하다.
  • Map 인터페이스에서 제공하는 주요 메서드

Map 인터페이스와 Hashtable 클래스의 차이

자료구조에 대한 전반적인 설명

  • 배열은 선형 자료구조, 논리적인 구조와 물리적인 자료구조가 동일한 형태
    - 장점 : 인덱스로 값을 찾기 편함
    • 단점 : 중간에 자료가 빠지면 메모리를 앞으로 땡겨오는 작업필요, 배열의 크기가 정해져 있다.
    • 얘를 구현해 놓은 애가 ArrayList와 Vector, 근데 ArrayList가 최적화 되어 있기 때문에 전자를 사용
  • Linked List : 논리적으로는 선형으로 되어 있지만 물리적인 위치가 동떨어진 형태
    - 장점 : 중간에 자료가 빠져도 링크가 조정해주면됨, 크기가 정해져 있지 않다.
    • 단점 : 인덱스로 찾으려면 첫번째부터 링크를 찾아야 된다.
    • LinkedList 라이브러리 제공된다.
  • Stack 구조
    - LIFO : last in first out
  • Queue 구조
    - FIFO : first in first out
  • hash 구조
    - 검색을 위한 자료구조
    - key를 입력하여 value를 도출
  • 바이너리 트리
    - 부모 노드 아래 자식 노드가 두개보다 작거나 같은 구조

ArrayList vs Vector

  • 객체 배열 클래스
  • Vector는 자바 2부터 제공된 클래스이지만 일반적으로 최적화가 잘되어 있는 ArrayList를 더 많이 사용한다.
  • Vector는 멀티 쓰레드 프로그램에서 동기화를 지원
    - 동기화(synchronization) : 두 개의 쓰레드가 동시에 하나의 리소스에 접근할 때 순서를 맞추어서 데이터의 오류가 방지하지 않도록 한다.
  • capacity와 size는 다은 의미이다.
    - capacity는 배열의 용량(크기)
    • size는 그 안에 들어가 있는 데이터의 량
      • ex) 10개 짜리 배열에 3개가 들어가 있으면 capacity는 10 size는 3

++ arrayList의 default_capacity는 10이다.
용량이 넘치면 더블링을 한다.

참고 문헌

제네릭 관련 문헌

profile
https://github.com/beombu

0개의 댓글