Chapter 12 컬렉션 프레임워크

Ruinak·2021년 6월 11일
0

Java

목록 보기
12/15
post-thumbnail

1. 제네릭

1-1 제네릭이란?

  • 프로그램에서 변수를 선언할 때 모든 변수는 자료형이 있습니다.
  • 메서드에서 매개변수를 사용할 때도 자료형을 가지고 있습니다.
  • 대부분은 하나의 자료형으로 구현하지만, 변수나 메서드의 자료형을 필요에 따라 여러 자료형으로 바꿀 수 있다면 훨씬 유연할 것입니다.
  • 제네릭(Generic) 프로그래밍은 어떤 값이 하나의 참조 자료형이 아닌 여러 참조 자료형을 사용할 수 있도록 프로그래밍하는 것입니다.
  • '컬렉션 프레임워크'도 많은 부분이 제네릭으로 구현되어 있습니다.

1-2 제네릭의 필요성

  • 3D 프린터의 예입니다.
  • 아래는 재료로 파우더를 쓰는 경우입니다.
  • 아래는 재료로 플라스틱을 쓰는 경우입니다.
  • 재료만 바뀌었을 뿐 프린터 기능이 동일하다면 프린터 클래스를 두 개 만드는 것은 비 효율적입니다.
  • 위와 같은 경우에 어떤 재료든 쓸 수 있또록 material 변수의 자료형을 Object로 사용할 수 있습니다.
  • Object는 모든 클래스의 최상위 클래스이므로 모든 클래스는 Object로 변환할 수 있기 때문입니다.
  • material 변수의 자료형을 Object로 선언한 ThreeDPrinter에 파우더를 재료료 사용한다면 아래와 같은 코드를 구현할 수 있습니다.
  • setMaterial( ) 메서드를 활용하여 Powder를 재료로 선택할 때는 매개변수 자료형이 Object이므로 자동으로 형 변환이 됩니다.
  • Object 클래스인 getMaterial( ) 메서드로 Powder 자료형 변수를 반환받을 때는 반드시 형 변환을 해줘야 합니다.
  • 어떤 변수가 여러 참조 자료형을 사용할 수 있도록 Object 클래스를 사용하면 다시 원래 자료형으로 반환해 주기 위해 매번 형 변환을 해야하는 번거로움이 있습니다.
  • 제네릭의 방식은 여러 참조 자료형이 쓰일 수 있는 곳에 특정한 자료형을 지정하지 않고, 클래스나 메서드를 정의한 후 사용하는 시점에 어떤 자료형을 사용할 것인지 지정하는 방식입니다.

1-3 제네릭 클래스 정의하기

  • 제네릭에서는 여러 참조 자료형을 사용해야 하는 부분에 Object가 아닌 하나의 문자로 표현합니다.
  • 제네릭 클래스로 ThreeDPrint 정의하면 아래와 같습니다.
  • 여러 자료형으로 바꾸어 사용할 material 변수의 자료형을 T로 사용했습니다.
  • T를 자료형 매개변수(type parameter)라고 합니다.
  • 클래스 이름을 GenericPrint<T>라고 정의하고 나중에 클래스를 사용할 때 T 위치에 실제 사용할 자료형을 지정합니다.
  • 클래스의 각 메서드에서 해당 자료형이 필요한 부분에는 모두 T 문자를 사용하여 구현합니다.

다이아몬드 연산자 < >

  • 자바 7부터는 제네릭 자료형의 클래스를 생성할 때 생성자에 사용하는 자료형을 명시하지 않을 수 있습니다.
  • 위에서 < >를 다이아몬드 연산자라고 합니다.
  • 선언된 자료형을 보고 생략된 부분이 String임을 컴파일러가 유추할 수 있기 때문에 생성 부분에서는 생략할 수 있습니다.

자료형 매개변수 T와 static

  • static 변수나 메서드는 인스턴스를 생성하지 않아도 클래스 이름으로 호출할 수 있습니다.
  • static 변수는 인스턴스 변수가 생성되기 이전에 생성됩니다.
  • static 메서드에서는 인스턴스 변수를 사용할 수 없습니다.
  • T의 자료형이 정해지는 순간은 제네릭 클래스의 인스턴스가 생성되는 순간입니다.
  • 즉 T의 자료형이 결정되는 시점보다 빠르기 때문에 static 변수의 자료형이나 static 메서드 내부 변수의 자료형으로 T를 사용할 수 없습니다.
  • 자료형 매개변수로 T 외의 다른 문자도 사용할 수 있습니다.
  • E = element, K = key, V = value를 의미합니다.
  • A, B 등 아무 문자나 사용해서 정의할 수도 있습니다.

1-4 제네릭 클래스 사용하기

  • 파우더가 재료인 프린터는 아래와 같이 선언하여 생성합니다.
  • T로 정의한 클래스 부분에 Powder형을 넣어주고, T형 매개변수가 필요한 메서드에 Powder 클래스를 생성하여 대입해 줍니다.
  • GenericPrinter<Powder>에서 어떤 자료형을 사용할지 명시했으므로 getMaterial( ) 메서드에서 반환할 때 형 변환을 하지 않습니다.
  • 실제 제네릭 클래스를 사용할 때 T 위치에 사용한 Powder형을 '대입된 자료형'이라 하고, Powder를 대입해 만든 GenricPrinter<Powder>를 '제네릭 자료형'이라고 합니다.
  • 제네릭 클래스를 사용하면 컴파일러는 일단 대입된 자료형이 잘 쓰였는지 확인하고, class 파일을 생성할 때 T를 사용한 곳에 지정된 자료형 자료형에 따라 컴파일하므로 형 변환을 하지 않아도 됩니다.
  • 제네릭을 사용하면 컴파일러가 자료형을 확인해 주기 때문에 안정적이면서 형 변환 코드가 줄어듭니다.

제네릭 클래스 사용 예제

예제 12-1 Powder 클래스 정의하기

public class Powder {
	public void doPrinting() {
		System.out.println("Powder 재료로 출력합니다.");
	}

	public String toString() {
		return "재료는 Powder입니다.";
	}
}

예제 12-2 Plastic 클래스 정의하기

public class Plastic {
	public void doPrinting() {
		System.out.println("Plastic 재료로 출력합니다.");
	}

	public String toString() {
		return "재료는 Plastic입니다.";
	}
}

예제 12-3 GenericPrinter<T> 클래스 정의하기

public class GenericPrinter<T> {	

	// T 자료형으로 선언한 변수
	private T material; 

	public void setMaterial(T material) {
		this.material = material;
	}

	// T 자료형 변수 material을 반환하는 제네릭 메서드
	public T getMaterial() { 
		return material;
	}

	public String toString() {
		return material.toString();
	}
}
  • GenericPrinter<T> 클래스의 인스턴스 변수 material은 자료형 매개변수 T로 선언했습니다.
  • getMaterial( ) 메서드는 T 자료형 변수 material을 반환합니다.
  • 메서드 선언부나 메서드의 매개변수로 자료형 매개변수 T를 사용한 메서드를 '제네릭 메서드(generic method)'라고 합니다.
  • 제네릭 메서드는 일반 메서드뿐 아니라 static 메서드에서도 활용할 수 있습니다.

예제 12-4 GenericPrinter<T> 클래스 사용하기

public class GenericPrinterTest {
	public static void main(String[] args) {
		
		// Powder형으로 GenericPrinter 클래스 생성
		GenericPrinter<Powder> powderPt = new GenericPrinter<Powder>();
		
		powderPt.setMaterial(new Powder());
		Powder powder = powderPt.getMaterial();
		System.out.println(powderPt);
		System.out.println(powder);
		
		// plastic형으로 GenericPrinter 클래스 생성
		GenericPrinter<Plastic> plasticPt = new GenericPrinter<Plastic>();
		
		plasticPt.setMaterial(new Plastic());
		Plastic plastic = plasticPt.getMaterial();
		System.out.println(plasticPt);
	}	
}
  • 사용할 참조자료형을 지정하여 GenericPrinter 클래스를 생성합니다.
  • 새로운 재료가 추가되면 추가된 재료 클래스를 만들고 T 대신 해당 클래스를 대입하여 GenericPrinter를 생성하면 됩니다.

제네릭에서 대입된 자료형을 명시하지 않는 경우

  • 제네릭 클래스는 사용할 때는 GenericPrinter<Powder>의 Powder와 같이 대입된 자료형을 명시해야 합니다.
  • 위처럼 클래스에 대입된 자료형을 명시하지 않는 경우 컴파일 오류는 아니지만, 사용할 자료형을 명시하라는 의미의 노란색 경고 줄이 나타납니다.
  • 컴파일러가 어떤 자료형을 사용할건지 알 수 없으므로 getMaterial( ) 메서드에서 강제로 형 변환을 해줘야합니다.
  • 제네릭 클래스를 사용하는 경우에는 되도록이면 대입된 자료형으로 사용할 참조 자료형을 지정하는 것이 좋습니다.
  • 여러 자료형을 동시에 사용하려면 Object 클래스를 사용할 수도 있습니다.

1-5 T 자료형에 사용할 자료형을 제한하는 <T extends 클래스>

  • 제네릭 클래스에서 T 자료형에 사용할 자료형에 제한을 둘 수 있습니다.
  • 사용할 클래스에 자료형 제한을 두는 방식으로 extends 예약어를 사용할 수 있습니다.

예제 12-7 extends 예약어를 사용해서 제한 두기

public abstract class Material {
	public abstract void doPrinting();
}

public class Powder extends Material {
	public void doPrinting() {
		System.out.println("Powder 재료로 출력합니다.");
	}

	public String toString() {
		return "재료는 Powder입니다.";
	}
}

public class Plastic extends Material {
	public void doPrinting() {
		System.out.println("Plastic 재료로 출력합니다.");
	}

	public String toString() {
		return "재료는 Plastic입니다.";
	}
}
  • Material 클래스를 추상 클래스로 정의했습니다.
  • 상속받은 클래스는 doPrinting( ) 추상 메서드를 반드시 구현해야 합니다.
  • Powder와 Plastic은 Material을 상속 받습니다.

예제 12-8 GenericPrinter<T extends Material> 클래스

public class GenericPrinter<T extends Material> {	
			   // extends 예약어로 사용할 수 있는 자료형에 제한을 둠

	// T 자료형으로 선언한 변수
	private T material; 

	public void setMaterial(T material) {
		this.material = material;
	}

	// T 자료형 변수 material을 반환하는 제네릭 메서드
	public T getMaterial() { 
		return material;
	}

	public String toString() {
		return material.toString();
	}
	
	// 상위 클래스 Material의 메서드 호출
	public void printing() {		
		material.doPrinting();		
	}
}
  • 클래스 이름에 <T extends Material>이라고 명시하여 사용할 수 있는 자료형에 제한을 둡니다.
  • T 위치에 특정 인터페이스를 구현 클래스만 사용하려는 경우에도 extends 예약어를 사용할 수 있습니다.

<T extends 클래스>로 상위 클래스 메서드 사용하기

  • <T extends Material>로 선언하면 제네릭 클래스를 사용할 때 상위 클래스 Material에서 선언한 메서드를 사용할 수도 있습니다.
  • T는 컴파일할 때 Object 클래스로 변환되므로, Object 클래스가 기본으로 제공하는 메서드만 사용할 수 있습니다.
    - 자료형을 알 수 없기 때문입니다.
  • <T extends 클래스>를 사용하면 어떻게 될까?
    - material에 사용할 수 있는 메서드에 doPrinting( )이 추가된 것을 확인할 수 있습니다.
  • 상위 클래스 Material에서 선언하거나 구현한 메서드를 모두 사용할 수 있습니다.

예제 12-9 <T extends 클래스> 사용하기

public class GenericPrinter<T extends Material> {	

	private T material; 

	public void setMaterial(T material) {
		this.material = material;
	}

	public T getMaterial() { 
		return material;
	}

	public String toString() {
		return material.toString();
	}
	
	// 상위 클래스 Material의 메서드 호출
	public void printing() {		
		material.doPrinting();		
	}
}
  • T형 material 변수에서 doPrinting() 메서드를 호출할 수 있습니다.

예제 12-10 <T extends 클래스> 테스트하기

public class GenericPrinterTest2 {
	public static void main(String[] args) {
		
		GenericPrinter<Powder> powderPt = new GenericPrinter<Powder>();
		powderPt.setMaterial(new Powder());
		powderPt.printing();
		
		GenericPrinter<Plastic> plasticPt = new GenericPrinter<Plastic>();
		plasticPt.setMaterial(new Plastic());
		plasticPt.printing();
	}
}

1-6 제네릭 메서드 활용하기

  • 제네릭 메서드의 일반 형식은 아래와 같습니다.
  • 반환형 앞에 사용하는 <자료형 매개변수>는 여러 개일 수 있으며, 이는 메서드 내에서만 유효합니다.

예제 12-11 자료형 매개변수를 두 개 사용하는 클래스

public class Point<T, V> {
	T x;
	V y;
	
	Point(T x, V y) {
		this.x = x;
		this.y = y;
	}

	// 제네릭 메서드
	public T getX() {	
		return x;
	}

	// 제네릭 메서드
	public V getY() {	
		return y;
	}
}
  • 한 점을 나타내는 Point 클래스의 두 좌표 x, y는 정수일 수도 있고 실수일 수도 있으므로, T와 V라는 자료형 매개변수로 표현했습니다.
  • 변수들을 위한 메서드 getX( ), getY( )는 T와 V를 반환하고 있으므로 제네릭 메서드입니다.
// < > 다이아몬드 연산자만 사용하고 자료형을 명시하지 않음
Point<Integer, Double> p1 = new Point<>(0, 0.0);
Point<Integer, Double> p2 = new Point<>(10, 10.0);
  • 두 점의 위치를 표현할 때 x 좌표는 Integer를 사용하였고, y 좌표는 Double을 사용했습니다.
  • 컴파일러는 선언된 자료형을 보고 생성되는 인스턴스의 자료형을 유추할 수 있으므로 <> 다이아몬드 연산자에는 자료형을 명시하지 않아도 됩니다.

예제 12-12 제네릭 메서드 구현하기

public class GenericMethod {
	// 제네릭 메서드
	public static <T, V> double makeRectangle(Point<T, V> p1, Point<T, V> p2) {
		double left = ((Number)p1.getX()).doubleValue();
		double right = ((Number)p2.getX()).doubleValue();
		double top = ((Number)p1.getY()).doubleValue();
		double bottom = ((Number)p2.getY()).doubleValue();
		
		double width = right - left;
		double height = bottom - top;
		
		return width * height;
	}
	public static void main(String[] args) {
		Point<Integer, Double> p1 = new Point<Integer, Double>(0, 0.0);
		Point<Integer, Double> p2 = new Point<Integer, Double>(10, 10.0);
		
		double rect = GenericMethod.<Integer, Double>makeRectangle(p1, p2);
		System.out.println("두 점으로 만든 사각형의 넓이는 " + rect + "입니다.");
	}
}

  • GenericMethod 클래스는 제네릭 클래스가 아닙니다.
  • 제네릭 클래스가 아니라도 내부에 제네릭 메서드를 구현할 수 있습니다.
  • 제네릭 메서드인 makeRectangle( ) 메서드는 static으로 구현했습니다.
  • makeRectangle( ) 메서드에서 사용하는 T와 V는 makeRectangle( ) 메서드 내부에서만 유효하게 사용할 수 있습니다.

1-7 컬렉션 프레임워크에서 사용하는 제네릭

  • 배열은 요소를 가지므로 T보다는 Element를 뜻하는 E를 더 많이 사용합니다.

2. 컬렉션 프레임워크

2-1 컬렉션 프레임워크란?

  • 원하는 건물을 지으려면 구조를 잘 잡아야하듯이 프로그램을 개발할 때도 사용하는 자료를 어떤 구조로 관리할 것인지가 중요합니다.
  • 프로그램의 기능을 효과적으로 구현하기 위해 사용하는 것이 자료 구조(data structure)입니다.
  • 자료 구조는 프로그램 실행 중 메모리에 자료를 유지·관리하기 위해 사용합니다.
  • 자바에서는 필요한 자료 구조를 미리 구현하여 java.util 패키지에서 제공하고 있는데, 이를 컬렉션 프레임워크(collection framework)라고 합니다.
  • 자료 구조는 개발자가 필요할 때 직접 만들어 사용할 수도 있지만, 자바 컬렉션 프레임워크를 사용하면 직접 개발하는 수고를 덜 수 있을 뿐만 아니라 잘 만들어진 자료 구조 클래스를 활용할 수 있습니다.
  • 자바 컬렉션 프레임워크에는 여러 인터페이스가 정의되어 있고, 그 인터페이스를 구현한 클래스가 있습니다.
  • 컬렉션 프레임 워크의 전체 구조는 Collection 인터페이스와 Map 인터페이스 기반으로 이루어져 있습니다.
  • Collection 인터페이스는 하나의 자료를 모아서 관리하는데 필요한 기능을 제공합니다.
  • Map 인터페이스는 쌍(pair)으로 된 자료들을 관리하는데 유용한 기능을 제공합니다.

2-2 Collection 인터페이스

  • Collection 인터페이스는 하위에 List 인터페이스와 Set 인터페이스가 있습니다.
  • List를 구현한 클래스는 순차적인 자료를 관리하는데 사용하는 클래스입니다.
  • Set 인터페이스는 우리가 수학 시간에 배운 집합을 생각하면 됩니다.
    - 집합은 순서와 상관없이 중복을 허용하지 않습니다.
    - Set 계열의 클래스는 아이디처럼 중복되지 않는 객체를 다루는데 사용합니다.
  • Collection 인터페이스에 선언된 메서드 중 자주 사용하는 메서드
  • add( )나 remove( ) 메서드가 boolean형으로 결과 값을 반환하는 것은 객체가 잘 추가되었는지, 컬렉션에서 객체가 잘 제거되었는지 여부를 반환하는 것입니다.
  • Collection 인터페이스를 구현한 클래스는 위 메서드를 모두 제공합니다.

2-3 Map 인터페이스

  • Map 인터페이스는 하나가 아닌 쌍(pair)으로 되어있는 자료를 관리하는 메서드들이 선언되어 있습니다.
  • key-value 쌍이라고 표현하는데 이때 키 값은 중복될 수 없습니다.
  • 학번과 학생 이름처럼 쌍으로 되어있는 자료를 관리할때 사용하면 편리합니다.

MAP 인터페이스를 구현한 대표 클래스

  • Map은 기본적으로 검색용 자료 구조입니다.
  • 어떤 key 값을 알고 있을 때 value를 찾기 위한 자료 구조입니다.

Map 인터페이스에 선언된 메서드 중 주요 메서드

2-4 실습 패키지 구조

  • 회원 관리 프로그램에서 회원 추가, 회원 삭제, 전체 회원 정보 출력 기능을 구현합니다.

예제 12-13 Member 클래스 구현하기

package study.collection;

public class Member {
	
	// 속성
	private int memberId;		// 회원 아이디
	private String memberName;	// 회원 이름
	
	// 풀 생성자
	public Member(int memberId, String memberName) {	
		this.memberId = memberId;
		this.memberName = memberName;
	}

	public int getMemberId() {
		return memberId;
	}

	public void setMemberId(int memberId) {
		this.memberId = memberId;
	}

	public String getMemberName() {
		return memberName;
	}

	public void setMemberName(String memberName) {
		this.memberName = memberName;
	}

	@Override
	public String toString() {		// toString( ) 메서드 재정의
		return memberName + " 회원님의 아이디는 " + memberId + "입니다.";
	}
}
  • 속성으로 사용한 id와 name은 private 변수로 선언하고 get( ), set( ) 메서드를 public으로 제공합니다.
  • 회원 정보를 출력하기 위해 toString( ) 메서드를 재정의하여 구현했습니다.

3. List 인터페이스

  • List 인터페이스에는 객체를 순서에 따라 저장하고 유지하는데 필요한 메서드가 선언되어 있습니다.
  • 순차 자료 구조의 대표적인 예는 배열입니다.
  • 자바에서 배열을 구현한 대표 클래스로는 ArrayList, Vector가 있고, 배열과 구현 방식은 다르지만 순차 자료 구조를 구현한 LinkedList가 있습니다.

3-1 ArrayList 클래스

  • ArrayList는 객체 배열을 구현한 클래스이며 켈렉션 인터페이스와 그 하위 List 인터페이스를 구현했습니다.
  • 객체 순서를 기반으로 순차적으로 자료를 관리하는 프로그램을 구현할 때 사용합니다.

ArrayList를 활용해 회원 관리 프로그램 구현하기

  • ArrayList를 활용한 MemberArrayList 클래스에는 메서드를 3개를 제공합니다.
    - addMember( ) 메서드 : 회원을 추가
    - removeMember( ) 메서드 : 회원을 삭제
    - showAllMember( ) 메서드 : 전체 회원을 출력
  • 각 메서드의 코드를 보면 Collection 인터페이스에서 선언하고 ArrayList에서 구현한 add( ), get( ) 등의 메서드를 사용한 것을 알 수 있습니다.

예제 12-14 ArrayList 활용하기

// Member 클래스는 collection 패키지에 있으므로 사용하려면 import 해야함
import collection.Member;	
import java.util.ArrayList;

public class MemberArrayList {
	// ArrayList 선언
	private ArrayList<Member> arrayList; 	
	
	public MemberArrayList() {
		// Member형으로 선언한 ArrayList 생성
		arrayList = new ArrayList<Member>();
	}
	
	// arrayList에 회원을 추가하는 메서드
	public void addMember(Member member) {
		arrayList.add(member);
	}
	
	// 해당 아이디를 가진 회원을 ArrayList에서 찾아 제거함
	public boolean removeMember(int memberId) {	
		for(int i = 0; i < arrayList.size(); i++) {
			// get() 메서드로 회원을 순차적으로 가져옴
			Member member = arrayList.get(i);
			int tempId = member.getMemberId();
			// 해당 아이디가 매개변수와 일치하면
			if(tempId == memberId) {
				// 해당 회원을 삭제
				arrayList.remove(i);
				return true;
			}
		}
		// 반복문이 끝날 때까지 해당 아이디를 찾지 못한 경우
		System.out.println(memberId + "가 존재하지 않습니다.");
		return false;
	}
	
	// 전체 회원을 출력하는 메서드
	public void showAllMember() {
		for(Member member : arrayList) {
			System.out.println(member);
		}
		System.out.println();
	}
}
  • ArrayList를 사용하려면 import.java.util.ArrayList를 선언해줘야 합니다.
  • addMember( ) 메서드는 매개변수로 전달된 회원을 ArrayList의 맨 뒤에 추가합니다.
  • removeMember( ) 메서드는 매개변수로 전달받은 아이디(memberId) 회원을 ArrayList에서 찾아서 제거합니다.
  • showAllMember( ) 메서드는 모든 회원을 출력합니다.

MemberArrayList 테스트 클래스 구현하기

예제 12-15 ArrayList 활용하기

public class MemberArrayListTest {
	public static void main(String[] args) {
		MemberArrayList memberArrayList = new MemberArrayList();
		
		// 새로운 회원 인스턴스 생성
		Member memberLee = new Member(1001, "이작가");
		Member memberKim = new Member(1002, "김독자");
		Member memberHong = new Member(1003, "홍길동");
		Member memberSung = new Member(1004, "성춘향");
		
		// ArrayList에 회원 추가 & 전체 회원 출력
		memberArrayList.addMember(memberLee);
		memberArrayList.addMember(memberKim);
		memberArrayList.addMember(memberHong);
		memberArrayList.addMember(memberSung);
		System.out.println("============== 전체 회원 출력 ==============");
		memberArrayList.showAllMember();	
		
		// 홍길동 회원 삭제 & 전체 회원 출력
		memberArrayList.removeMember(memberHong.getMemberId());
		System.out.println("=============== 홍길동 삭제 ===============");
		memberArrayList.showAllMember(); 
		
		// 홍길동 회원 추가 & 장영실 회원 생성, 추가 & 전체 회원 출력
		Member memberJang = new Member(1005, "장영실");
		memberArrayList.insertMember(memberHong, 2);
		memberArrayList.insertMember(memberJang, 4);
		System.out.println("============ 홍길동, 장영실 추가 ============");
		memberArrayList.showAllMember(); 
		
		// 척준경 회원 생성, 추가 & 전체 회원 출력
		Member memberC = new Member(1010, "척준경");
		memberArrayList.insertMember(memberC, 5);
		System.out.println("================ 척준경 추가 ================");
		memberArrayList.showAllMember(); 
	}	
}

  • ArrayList의 요소가 추가되는 add( )나 insert( ) 등의 메서드는 용량이 부족하면 큰 용량의 배열을 새로 만들고 기존 항목을 복사합니다.

3-2 ArrayList와 Vector 클래스

  • ArrayList와 Vecor의 가장 큰 차이는 동기화 지원 여부입니다.
  • 동기화(synchronization)란 두 개 이상의 스레드가 동시에 Vector를 사용할 때 오류가 나지 않도록 실행 순서를 보장하는 것입니다.

스레드와 멀티스레드 프로그래밍

  • 스레드란 간단히 말하면 작업 단위입니다.
  • 프로그램이 메모리에서 수행되려면 스레드 작업이 생성되어야 합니다.
  • 하나의 스레드만 수행되면 단일 스레드(single thread)라고 하고 두 개 이상의 스레드가 동시에 실행되는 경우를 멀티 스레드(multi-thread)라고 합니다.
  • 두 개 이상의 스레드가 동시에 실행되면 같은 메모리 공간(리소스)에 접근하기 때문에 변수 값이나 메모리 상태에 오류가 생길 수 있습니다.
    - 이때 메모리에 동시에 접근하지 못하도록 순서를 맞추는 것이 동기화입니다.
  • 두 작업이 동시에 실행되는 멀티스레드 환경이 아닌 경우에는 ArrayList를 사용하도록 권장합니다.
    - 동기화를 구현하기 위해서는 동시에 작업이 이루어지는 자원에 대해 잠금(lock)을 수행하기 때문입니다.
    - 메서드를 호출할 때 배열 객체에 잠금을 하고, 메서드 수행이 끝나면 잠금을 해제한다는 뜻입니다.
  • Vector의 모든 메서드는 호출될 때마다 잠금과 해제가 일어나므로 ArrayList보다 수행 속도가 느립니다.
  • ArrayList를 사용해서 구현했는데 나중에 프로그램에서 동기화가 필요하다면 Vector로 바꾸지 않고 아래와 같이 ArrayList 생성 코드를 쓰면 됩니다.

3-3 LinkedList 클래스

  • 배열은 처음 배열을 생성할 때 정적 크기로 선언하고, 물리적 순서와 논리적 순서가 동일합니다.
  • 배열은 중간에 자료를 삽입하거나 삭제할 때 나머지 자료를 이동시켜 빈 공간을 만들지 않고 연속된 자료 구조를 구현합니다.
  • 처음 선언한 배열 크기 이상으로 요소가 추가되는 경우에는 크기가 더 큰 배열을 새로 생성하여 각 요소를 복사해야하는 번거로움이 있습니다.
  • 이런 점을 개선한 자료 구조를 링크드 리스트(linked list)라고 합니다.

링크드 리스트 구조

  • 링크드 리스트의 각 요소는 다음 요소를 가리키는 주소 값을 가집니다.
  • 물리적인 메모리는 떨어져 있어도 논리적으로는 앞뒤 순서가 있습니다.
  • 같은 List 인터페이스를 구현한 ArrayList에 비해 중간에 자료를 넣고 제거하는데 시간이 적게 걸린다는 장점이 있고, 크기를 동적으로 증가시킬 수 있습니다.
  • 링크드 리스트의 각 요소는 아래 그림처럼 요소의 자료와 다음 요소의 주소를 저장하는 부분으로 구현됩니다.
  • 아래 그림은 링크드 리스트에 세 요소 A, B, D가 순차적으로 저장된 상태입니다.
  • 각 요소는 물리적으로 다른 메모리에 생성되어 있지만, 다음 요소를 가리키는 순서에 따라 A 다음은 B, 그 다음은 D가 됩니다.
  • D의 다음은 가리키는 요소가 없기 때문에 null 값이나 0을 저장합니다.

링크드 리스트에 요소 추가하기

  • 3번째 위치에 'C' 요소를 추가할 때, 배열이라면 D 요소를 뒤로 밀고 공간을 비워서 그 자리에 C를 놓습니다.
  • 링크드 리스트는 서로 가리키고 있는 주소 값만 변경해주면 됩니다.
  • 자료 이동이 발생하는 배열에 비해 훨씬 효율적입니다.
  • B가 가리키던 다음 위치를 C로 변경하고 C는 D를 가리키면 됩니다.
  • 이렇게 하면 논리적으로 A → B → C → D 순서가 됩니다.

링크드 리스트의 요소 제거하기

  • 제거해야 하는 요소가 있는 경우에도 각 요소가 가리키는 주소 값만 변경하면 됩니다.
  • B를 제거한다고 할 때 A의 다음 요소를 C로 변경하기만 하면 A → C → D로 순서가 변경됩니다.
  • 제거된 B의 메모리는 나중에 자바의 가비지 컬렉터에 의해 수거됩니다.

배열과 링크드 리스트의 다른 점

  • 배열은 생성할 때 용량을 지정하고, 용량보다 더 많은 요소가 추가된 경우에 용량을 늘려 가며 수행합니다.
  • 링크드 리스트는 요소를 추가할 때마다 동적으로 요소의 메모리를 생성하기 때문에 배열처럼 용량을 늘리고 요소 값을 복사하는 번거로움이 없습니다.
  • 링크드 리스트는 자료를 중간에 추가하거나 삭제할 때 자료의 이동이 배열보다 적습니다.
  • 배열은 물리적으로 연결된 자료 구조이므로 i번째 요소 메모리 위치르 바로 계산할 수 있어 접근이 빠르고, 링크드 리스트보다 구현하기 쉽습니다.

배열과 링크드 리스트의 효율적인 사용

  • 사용하는 자료의 변동(삽입·삭제)이 많은 경우에는 링크드 리스트
  • 자료 변동이 거의 없는 경우에는 배열

LinkedList 클래스 사용하기

  • LinkedList는 ArrayList보다 다양한 메서드를 제공합니다.

예제 12-16 LinkedList 테스트하기

public class LinkedListTest {
	public static void main(String[] args) {
		
		LinkedList<String> myList = new LinkedList<String>();
		
		// 링크드 리스트에 요소 추가
		myList.add("A");		
		myList.add("B");
		myList.add("C");
		
		// 리스트 전체 출력
		System.out.println(myList);	
		
		// 링크드 리스트의 첫번째 위치에 D 추가
		myList.add(1, "D");			
		System.out.println(myList);
		
		// 링크드 리스트의 맨 앞에 0 추가
		myList.addFirst("0");
		System.out.println(myList);	
		
		// 연결 리스트의 맨 뒤 요소 삭제 후 해당 요소를 출력
		System.out.println(myList.removeLast());	
		System.out.println(myList);
	}
}

  • LinkedList 클래스에는 링크드 리스트의 맨 앞 또는 맨 뒤에 요소를 추가·삭제하는 addFirst( ), addLast( ), removeFirst( ), removeLast( ) 등의 메서드가 있습니다.
  • ArrayList 클래스보다 훨씬 다양합니다.
  • 위 메서드들은 이후에 알아볼 스택(stack)이나 큐(queue)에서 다양하게 활용할 수 있습니다.

3-4 ArrayList로 스택과 큐 구현하기

  • 프로그램을 개발할 때 가장 많이 사용하는 자료 구조에 스택과 큐가 있습니다.
  • 스택은 상자를 쌓듯이 자료를 관리하는 방식입니다.
  • 상자가 쌓인 상태에서 하나의 상자를 꺼내려면 맨 나중에 올린 상자를 먼저 꺼내야합니다.
  • 스택은 맨 나중에 추가된 데이터를 먼저 꺼내는 방식(Last In Frist Out : LIFO)입니다.
  • 큐는 일상생활에서 가장 많이 사용하는 방식의 자료 구조로 '선착순'을 생각하면 됩니다.
  • 줄을 선 대기열 처럼 먼저 추가된 데이터부터 꺼내서 사용하는 방식(First In Frist Out : FIFO)입니다.
  • Stack 클래스는 자바 1부터 제공되었습니다.
  • Queue는 인터페이스로 정의되어 있고 Priority Queue 등이 구현되어 있습니다.

ArrayList로 스택 구현하기

  • 스택은 가장 최근에 추가된 자료부터 반환해줍니다.
  • 가장 최근에 검색한 단어를 찾는 경우나 장기, 체스 같은 게임에서 수를 무를 때도 응용할 수 있습니다.
  • 스택에 자료를 추가하는 것을 push( )라고 하고, 자료를 꺼내는 것은 pop( )이라고 합니다.
  • 스택에 가장 최근에 추가된 자료의 위치를 top이라고 합니다.

예제 12-17 스택 구현하기

class MyStack {
	private ArrayList<String> arrayStack = new ArrayList<String>();
	
	// 스택의 맨 뒤에 요소를 추가
	public void push(String data) {	
		arrayStack.add(data);
	}
	
	// 스택의 맨 뒤에서 요소 꺼냄
	public String pop() {	
		// ArrayList에 저장된 유효한 자료의 개수
		int len = arrayStack.size();	
		if(len == 0) {
			System.out.println("스택이 비어 있습니다.");
			return null;
		}
		// 맨 뒤에 있는 자료 반환하고 배열에서 제거
		return arrayStack.remove(len-1);	
	}
}

public class StackTest {
	public static void main(String[] args) {
		MyStack stack = new MyStack();
		stack.push("A");
		stack.push("B");
		stack.push("C");
		
		System.out.println(stack.pop());
		System.out.println(stack.pop());
		System.out.println(stack.pop());
		System.out.println(stack.pop());
	}
}

  • push( )에서는 add 메서드를 사용하여 ArrayList 맨 뒤에 요소를 추가합니다.
  • pop( ) 메서드의 arrayStack.remove(len-1)를 사용해 가장 최근에 추가된 마지막 항목(요소)을 ArrayList에서 제거하고 반환해 줍니다.
  • 테스트 프로그램을 통해 추가된 순서와 반대로 최근 항목부터 pop( )이 수행됨을 알 수 있습니다.

ArrayList로 큐 구현하기

예제 12-18 큐 구현하기

class MyQueue {
	private ArrayList<String> arrayQueue = new ArrayList<String>();
	
	// 큐의 맨 뒤에 추가
	public void enQueue(String data) {	
		arrayQueue.add(data);
	}
	
	// 큐의 맨 앞에서 꺼냄
	public String deQueue() {	
		int len = arrayQueue.size();
		if(len == 0) {
			System.out.println("큐가 비었습니다.");
			return null;
		}
		// 맨 앞의 자료 반환하고 배열에서 꺼냄
		return arrayQueue.remove(0);	
	}
}

public class QueueTest {
	public static void main(String[] args) {
		MyQueue queue = new MyQueue();
		
		queue.enQueue("A");	
		queue.enQueue("B");
		queue.enQueue("C");
		
		System.out.println(queue.deQueue());
		System.out.println(queue.deQueue());
		System.out.println(queue.deQueue());
	}
}

  • enQueue( )에서는 add( ) 메서드를 사용하여 ArrayList 맨 뒤에 요소를 추가합니다.
  • 큐에서 자료를 꺼내는 deQueue( ) 메서드는 ArrayList의 맨 앞에 있는 요소부터 제거하고 반환합니다.
  • 출력결과를 보면 추가된 순서대로 요소가 반환되는 것을 알 수 있습니다.

3-5 Collection 요소를 순회하는 Iterator

  • MemberArrayList.java의 removeMember( ) 메서드를 보면 for문과 get(i) 메서드를 사용하여 회원을 순차적으로 하나씩 꺼내면서 매개변수와 같은 아이디를 찾습니다.
  • 순서가 없는 Set 인터페이스를 구현한 경우에는 get(i) 메서드를 사용할 수 없으므로, 이 때 Iterator를 사용합니다.
  • Iterator는 Collection 인터페이스를 구현한 객체에서 미리 정의되어 있는 iterator( ) 메서드를 호출하여 참조합니다.
  • 예를 들어 Collection을 구현한 ArrayList에 iterator( ) 메서드를 호출하면 Iterator 클래스가 반환되므로 아래 처럼 Iterator형 변수에 대입해 사용합니다.

Iterator를 사용하여 요소를 순회할 때 사용하는 메서드

예제 12-19 MemberArrayList 클래스의 removeMember( ) 메서드 수정

...
// 해당 아이디를 가진 회원을 ArrayList에서 찾아 제거함
	public boolean removeMember(int memberId) {			
//		for(int i = 0; i < arrayList.size(); i++) {
//			// get() 메서드로 회원을 순차적으로 가져옴
//			Member member = arrayList.get(i);
//			int tempId = member.getMemberId();
//			// 해당 아이디가 매개변수와 일치하면
//			if(tempId == memberId) {
//				// 해당 회원을 삭제
//				arrayList.remove(i);
//				return true;
//			}
//		}

		// Iterator 반환
		Iterator<Member> ir = arrayList.iterator();
		// 요소가 있는 동안
		while(ir.hasNext()) {
			// 다음 회원을 반환받음
			Member member = ir.next();
			int tempId = member.getMemberId();
			// 회원 아이디가 매개변수와 일치하면
			if(tempId == memberId) {
				// 해당 회원을 삭제하고
				arrayList.remove(member);
				// true 반환
				return true;
			}
		}
		// 반복문이 끝날 때까지 해당 아이디를 찾지 못한 경우
		System.out.println(memberId + "가 존재하지 않습니다.");
		return false;
	}
    ...
  • arrayList.iterator( ) 메서드를 호출하여 Iterator를 가져옵니다.
  • Iterator<Member>와 같이 제네릭 자료형으로 Iterator가 순회할 요소의 자료형을 지정합니다.
  • Iterator는 각 요소를 순회하기 때문에 hashNext( )의 결과가 true이면 다음 요소를 가져오는데 next( ) 메서드를 호출합니다.
  • 순서가 없는 클래스도 Iterator를 사용하면 요소를 순회할 수 있습니다.

4. Set 인터페이스

  • 순서와 상관없이 중복을 허용하지 않는 경우에는 Set 인터페이스를 구현한 클래스를 사용함
  • 우리가 사용하는 데이터 중 회원 아이디, 주민등록번호, 사번, 홈쇼핑 주문 번호 등은 중복되면 안될 것임

4-1 HashSet 클래스

  • HashSet 클래스는 집합 자료 구조를 구현하며 중복을 허용하지 않음

예제 12-20 HashSet 테스트

  • hashSet에 동일한 자료 '유중혁'을 추가했지만, 같은 자료는 중복되어 출력되지 않음
  • HashSet에 중복된 값은 추가되지 않음
  • ArrayList는 순서가 있는 자료 구조이기 때문에 추가한 순서대로 출력되지만, HashSet은 자료가 추가된 순서와 상관없이 출력됨

HashSet를 활용해 회원 관리 프로그램 구현하기

예제 12-21 HashSet 활용하기(1)

  • 회원을 삭제할 때 사용하는 remove( ) 메서드가 ArrayList와 좀 다름
  • ArrayList에서는 get(i) 메서드를 사용해 i번째에 해당하는 항목을 삭제함
  • HashSet에서는 해당하는 아이디를 가진 회원을 찾기 위해 Iterator를 사용하며, 만약 아이디가 같으면 HashSet의 remove( ) 메서드를 사용하여 해당하는 회원을 삭제함

예제 12-22 HashSet 활용하기(2)

  • 출력 결과를 보면 같은 아이디 1003을 가진 유중혁 회원과 한수영 회원이 그대로 출력됨
  • 같은 회원이라는 것은 회원 아이디가 같다는 뜻인데 원래 HashSet의 정의대로라면 한수영 회원이 추가되지 않아야 함
  • 앞 예제 HashSetTest에서는 같은 문자열은 두 번 추가되지 않았었음
  • String이 두 번 추가되지 않은 이유는 String 클래스에 객체가 동일한 경우에 대한 처리 방법이 이미 구현이 되어있기 때문임

객체가 동일함을 구현하기

  • 기본적으로 인스턴스 주소가 같으면 같은 객체임
  • 하지만 여기서는 회원 아이디가 같아도 같은 회원이 됨
  • Object 클래스에서 논리적으로 같은 객체를 구현하기 위해 equals( ) 메서드와 hashCode( ) 메서드를 재정의했음
    - 그러므로 Member 클래스에도 equals( ) 메서드와 hashCode( ) 메서드를 재정의하여 회원 아이디가 같으면 같은 회원임을 구현해줘야 함

예제 12-23 HashSet 활용하기(3)

  • Member 클래스에 equals( )와 hashCode( ) 메서드를 재정의하고 MemberHashsetTest를 수행해 출력 결과를 보면 아이디가 같은 회원은 추가되지 않는 것을 알 수 있음

4-2 TreeSet 클래스

  • 자바의 Collection 인터페이스나 Map 인터페이스를 구현한 클래스 중 Tree로 시작하는 클래스는 데이터를 추가한 후 결과를 출력하면 결과 값이 정렬이 됨
  • TreeSet은 자료의 중복을 허용하지 않으면서 출력 결과 값을 정렬하는 클래스

예제 12-24 TreeSet 테스트하기

  • TreeSet에 한수영, 유중혁, 김독자 순으로 요소를 추가했지만, 결과 값은 정령되어 출력됨

이진 검색 트리

  • 트리는 자료 사이의 계층 구조를 나타내는 자료 구조임
  • 트리 자료 구조에서 각 자료가 들어가는 공간을 노드라고 함
  • 위 아래로 연결된 노드의 관계를 부모-자식 노드(parent-child node)라고 함
  • 이진 검색 트리는 노드에 저장되는 자료의 중복을 허용하지 않고, 부모가 가지는 자식 노드의 수가 2개 이하임
  • 왼쪽에 위치하는 자식 노드는 부모 노드보다 항상 작은 값을 가짐
  • 오른쪽에 위치하는 자식 노드는 부모 노드보다 항상 큰 값을 가짐
  • 어떤 특정 값을 찾으려고 할 때 한 노드와 비교해 비교한 노드보다 작은 값이면 왼쪽 자식 노드 방향으로, 큰 값이면 오른쪽 자식 노드 방향으로 이동함
  • 이진 검색 트리를 맨 왼쪽 노드부터 시작해서 왼쪽 → 부모 → 오른쪽 순으로 순회하면 오름차순이 됨
    - 반대로 오른쪽 → 부모 → 왼쪽 순으로 순회하면 내림차순이 됨
  • 어떤 기준으로 값의 크기를 비교할 것인지는 프로그래머가 직접 구현해야 함

TreeSet를 활용해 회원 관리 프로그램 구현하기

  • TreeSetTest.java 예제에서 별도의 코드를 구현하지 않아도 요소들이 정렬되었던 이유는 String 클래스 안에 정렬 방식이 이미 구현되어 있기 때문임

예제 12-25 TreeSet 활용하기(1)

  • MemberHashSet에서 HashSet 대신에 TreeSet만 선언하여 생성. 나머지 코드는 동일

예제 12-26 TreeSet 활용하기(2)

  • 오류 내용을 보면 Member 클래스가 Comparable 인터페이스를 구현하지 않았다는 의미임

4-3 Comparable 인터페이스와 Comparator 인터페이스

  • Member 클래스가 가진 회원 아이디를 기준으로 하여 오름차순으로 정렬할 것
    - Comparable과 Comparator는 이러한 정렬을 구현할 수 있게 해주는 인터페이스임
  • 정렬 방식은 정렬 기준 값이 있는 Member 클래스에 구현하면 됨

자기 자신과 전달받은 매개변수를 비교하는 + Comparable 인터페이스

  • Comparable 인터페이스에는 compartTo( ) 추상 메서드가 포함되어 있음
  • Comparable 인터페이스를 구현하는 Member 클래스에서 CompareTo( ) 메서드를 구현해야 함

예제 12-27 Comparable 인터페이스 구현하기

  • CompereTo( ) 메서드의 의미 : 비교 대상은 this의 회원 아이디, 즉 새로 추가한 회원의 아이디와 CompareTo( ) 메서드의 매개변수로 전달된 회원 아이디임
  • 두 값을 비교하여 새로 추가한 회원 아이디가 더 크면 양수, 그렇지 않으면 음수, 같으면 0을 반환하도록 만듬
  • 이렇게 구현하면 출력 결과 값은 오름차순으로 정렬 됨
  • 아이디가 오름차순으로 정렬되어 있음
  • 내림차순으로 정렬하려면 ( -1)을 member.memberId에 곱해주면 됨

두 매개변수를 비교하는 Comparator 인터페이스

  • Comparator 역시 정렬을 구현하는데 사용하는 인터페이스임
  • Comparator 인터페이스는 compare( ) 메서드를 구현해야 함

예제 12-28 Comparator 인터페이스 구현하기

  • Member2 클래스를 새로 만들어 Comparator를 구현
  • Comparator 인터페이스는 compare( ) 메서드를 구현해야 하는데, 이 메서드에는 매개변수가 2개 전달됨
  • compareTo( ) 메서드는 this와 전달된 매개변수를 비교했다면, compare( ) 메서드는 전달되는 두 매개변수를 비교함
  • 첫번째 매개변수가 더 클 때 양수를 변환하여 오름차순으로 정렬됨
  • Comparator를 사용할 때 유의할 점은 TreeSet 생성자에 Comparator를 구현한 객체를 매개변수로 전달한다는 것임
  • 일반적으로 Comparator 인터페이스보다 Comparable 인터페이스를 더 많이 사용함

예제 12-29 Comparator 인터페이스 사용하기

  • TreeSet 클래스를 생성할 때 생성자에 매개변수를 넣지 않으면 원래 String 클래스에 정의된 Comparable 인터페이스의 compareTo( ) 메서드 구현 내용대로 오름차순으로 정렬됨

5. Map 인터페이스

  • Map 인터페이스는 자료를 쌍(pair)으로 관리하는데 필요한 메서드가 정의되어 있음
  • key-value 쌍으로 이루어진 객체의 key 값은 유일하며 value 값은 중복될 수 있음
  • Map 인터페이스를 구현한 클래스는 내부적으로 해시 알고리즘에 의해 구현되어 있음

5-1 HashMap 클래스

  • HashMap은 Map 인터페이스를 구현한 클래스 중 가장 많이 사용함
  • HashMap에서 자료를 관리하는 방식은 해시 방식임
  • 해시 방식의 자료를 저장하는 공간을 해시 테이블이라고 함
  • key 값이 정해지면 그에 대응하는 해시 테이블의 저장위치가 정해지는데 이런 위치를 계산하는 함수가 '해시 함수'임
  • 해시함수 표현 방법
  • 새로운 key-value 자료가 입력되거나, key를 알고 있는 상태에서 value를 검색하는데 걸리는 시간은 산술적으로 계산할 수 있음
    - 자료 추가 속도가 검색 속도가 상당히 빠르다는 장점이 있음
  • 해시 함수를 어떻게 만드느냐는 key 값 특성이나 개발 프로그램 성격에 따라 다를 수 있음
  • 서로 다른 key 값에 같은 index가 반환되는 충돌(collision)이 발생하는 경우도 있음
    - 해시 테이블에 데이터를 꽉 채우지 않고 적정 수준이 되면 테이블을 확장해 충돌 발생 확률을 낮춤
  • Map 인터페이스에서 사용하는 key 값은 중복될 수 없으므로 equals( ) 메서드와 hashcode( ) 메서드를 재정의해서 사용하는 것이 좋음

HashMap을 활용해 회원 관리 프로그램 구현하기

예제 12-30 HashMap 활용하기(1)

  • key 값은 회원 아이디, value는 회원 클래스로 구현함
  • Map 인터페이스는 모든 자료를 한 번에 순회할 수 있는 방법이 없음
    - 모든 자료를 순회하려면 key 값을 먼저 가져와서 key 값에 해당하는 value를 찾아야 함
  • HashMap의 value( ) 메서드를 사용하면 key 값 없이 모든 value 값을 Collection 자료형으로 반환해 줌
  • key는 중복될 수 없으므로 반환형이 Set이고, value는 중복 가능하므로 Collection이 됨

예제 12-31 HashMap 활용하기(2)

  • 모든 회원이 잘 추가되고 회원 아이디가 1004인 이지혜 회원이 삭제됨
  • 쌍으로 된 자료는 HashMap을 사용하여 관리하면 편리함

HashMap과 Hashtable

  • HashMap과 Hashtable 클래스는 모두 쌍으로 이루어진 자료를 관리하는데 사용됨
  • Hashtable 클래스는 자바 1부터 사용했고 Vector 클래스와 마찬가지로 멀티스레드를 위한 동기화를 제공함
  • 멀티스레드 환경이 아니라면 Hashtable보다는 HashMap을 사용하는 것을 권장함

5-2 TreeMap 클래스

  • Map 인터페이스를 구현한 클래스 중 key 값으로 자료를 정렬하려면 TreeMap을 사용할 수 있음
  • TreeMap은 TreeSet과 마찬가지로 이진 검색 트리로 구현되어있음
  • key 값으로 정렬하므로 key 값에 해당하는 클래스에 Comparable이나 Comparator 인터페이스를 구현해야 함

예제 12-32 TreeMap 사용하기

  • Integer 클래스를 보면 이미 Comparable 인터페이스가 구현되어 있으므로 따로 Comparable 인터페이스를 구현하지 않아도 됨

예제 12-33 TreeMap 활용하기

  • 추가되는 순서와 상관없이 key 값인 회원 아이디를 기준으로 정렬됨
profile
Nil Desperandum <절대 절망하지 마라>

0개의 댓글