no image
[Java] 람다식
이 게시글은 이것이 자바다(저자 : 신용권, 임경균)의 책과 동영상 강의를 참고하여 개인적으로 정리하는 글임을 알립니다. 람다식의 개념 자바는 함수형 프로그래밍을 위해 Java 8부터 람다식을 지원한다. 자바는 객체지향 프로그래밍 언어로써 객체가 없이 함수가 존재할 수 없다. 클래스 내부에 무조건 함수가 있어야 하기 때문에 자바에서는 함수를 함수라 부르지 않고 메소드라고 부른다. 함수형 프로그래밍 함수형 프로그래밍이란 함수를 정의하고 이 함수를 데이터 처리부로 보내 데이터를 처리하는 기법을 가진다. 데이터 처리부는 데이터만 가지고 있을 뿐, 처리 방법이 정해져 있지 않아 외부에서 제공된 함수에 의존한다. 즉, 함수에 어떤 처리코드가 적혀있느냐에 따라 해당 데이터를 가지고 발생하는 실행결과가 달라질 수 있..
2023.08.05
no image
[Java] 컬렉션 프레임워크(LIFO & FIFO, 동기화된 컬렉션, 수정할 수 없는 컬렉션)
이 게시글은 이것이 자바다(저자 : 신용권, 임경균)의 책과 동영상 강의를 참고하여 개인적으로 정리하는 글임을 알립니다. LIFO & FIFO LIFO는 스택(Stack) 자료구조를 뜻하고, FIFO는 큐(Queue) 자료구조를 뜻한다. 컬렉션 프레임워크는 LIFO 자료구조를 제공하는 스택 클래스와 FIFO 자료구조를 제공하는 큐 인터페이스를 제공하고 있다. Stack Stack 클래스는 LIFO 자료구조를 구현한 클래스이다. 아래는 Stack 객체를 생성하는 방법이다. Stack stack = new Stack(); Stack stack = new Stack(); Stack 클래스는 Vector 클래스를 상속했다. 따라서 Stack 객체는 동기화 처리가 되어있다. 즉, 멀티스레딩 환경에서 안전하게 처리..
2023.08.04
no image
[Java] 컬렉션 프레임워크(List, Set, Map)
이 게시글은 이것이 자바다(저자 : 신용권, 임경균)의 책과 동영상 강의를 참고하여 개인적으로 정리하는 글임을 알립니다. 컬렉션 프레임워크(Collection Framework)자바는 널리 알려져 있는 자료구조를 바탕으로 객체들을 효율적으로 추가, 삭제 검색할 수 있도록 관련된 인터페이스와 클래스들을 java.util 패키지에 포함시켜 놓았다.이들을 총칭해서 컬렉션 프레임워크라고 부른다.컬렉션 프레임워크는 몇 가지 인터페이스를 통해서 다양한 컬렉션 클래스를 이용할 수 있도록 설계되어 있다.주요 인터페이스로는 List, Set, Map이 있다.컬렉션 프레임워크배열은 길이가 정해지면 바꿀 수 없었지만, 컬렉션 프레임워크는 길이가 가변적이다.컬렉션 프레임워크는 인터페이스이기 때문에 인터페이스에 정의된 메소드들..
2023.08.03
no image
[Java] 데몬 스레드와 스레드풀
이 게시글은 이것이 자바다(저자 : 신용권, 임경균)의 책과 동영상 강의를 참고하여 개인적으로 정리하는 글임을 알립니다. 데몬 스레드(Daemon Thread) 데몬 스레드는 주 스레드의 작업을 돕는 보조적인 역할을 수행하는 스레드이다. 주 스레드가 종료되면 데몬 스레드도 따라서 자동으로 종료된다. 스레드를 데몬으로 만들기 위해서는 주 스레드가 데몬이 될 스레드의 setDaemon(true)를 호출하면 된다. 주 스레드와 메인 스레드는 같을 수도 있고 다를 수도 있다. 메인 스레드에서, 다른 스레드를 데몬 스레드로 설정했다면 메인 스레드는 주 스레드가 된다. 하지만 메인 스레드가 아닌 A 스레드에서, B 스레드를 데몬 스레드로 설정했다면 B 스레드 관점에서는 A 스레드가 주 스레드가 된다. 아래의 코드에..
2023.08.02
no image
[Java] 멀티 스레드
이 게시글은 이것이 자바다(저자 : 신용권, 임경균)의 책과 동영상 강의를 참고하여 개인적으로 정리하는 글임을 알립니다. 2023.06.27 - [컴퓨터 구조 & 운영체제/운영체제] - [운영체제] 스레드(Thread), 멀티 프로세스와 멀티 스레드 [운영체제] 스레드(Thread), 멀티 프로세스와 멀티 스레드 이 글은 혼자 공부하는 컴퓨터 구조 + 운영체제 (저자 : 강민철)의 책과 유튜브 영상을 참고하여 개인적으로 정리하는 글임을 알립니다. 스레드 이 글에서 다루는 내용은 소프트웨어적 스레드이 rebugs.tistory.com 하나의 프로세스가 두 가지 이상의 작업을 처리할 수 있는 이유는 멀티 스레드가 있기 때문이다. 프로세스의 모든 스레드가 종료가 되어야 프로세스가 종료된다. 하나의 스레드에서 ..
2023.08.01
no image
[Java] 제네릭(Generic)
이 게시글은 이것이 자바다(저자 : 신용권, 임경균)의 책과 동영상 강의를 참고하여 개인적으로 정리하는 글임을 알립니다. 제네릭의 개념 제네릭이란 결정되지 않은 타입을 파라미터로 처리하고 실제 사용할 때 파라미터를 구체적인 타입으로 대체시기는 기능이다. 아래의 코드는 Box 클래스에서 결정되지 않은 content의 타입을 T라는 타입 파라미터로 정의한 것이다. public class Box { public T content; } 는 T가 타입 파라미터임을 뜻하는 기호로, 타입이 필요한 자리에 T를 사용할 수 있음을 알려주는 역할을 한다. Box 클래스는 T가 무엇인지 모르지만, Box 객체가 생성될 시점에 다른 타입으로 대체된다는 것을 알고 있다. Box의 내용물로 String 타입을 저장하고 싶으면 아..
2023.07.31
no image
[Java] 백준 17단계 - 11050번 문제 (이항 계수1)
문제설명 소스코드 import java.util.Scanner; public class Main { public static void main(String[] args) throws Exception{ Scanner sc = new Scanner(System.in); int N = sc.nextInt(); int K = sc.nextInt(); System.out.print(factorial(N) / (factorial(N-K)*factorial(K))); } static int factorial(int n) { long count = 1; if(n == 0) return (int)count; else { for(int i = 1; i
2023.07.30
no image
[Java] 랜덤 숫자(난수) 얻기
이 게시글은 이것이 자바다(저자 : 신용권, 임경균)의 책과 동영상 강의를 참고하여 개인적으로 정리하는 글임을 알립니다. math 클래스의 random() 메소드 이용 0.0과 1.0 사이(0.0 포함, 1.0 미포함) 범위에 속하는 하나의 double 타입의 값을 리턴 double v11 = Math.random(); //0.47464615326686044 1부터 n까지 랜덤 수를 뽑는 코드는 아래와 같다. int num = (int) (Math.random()*n) + 1; 아래의 예제는 1~45의 랜덤 숫자 중 6개 뽑는 예제이다. public class Main { public static void main(String[] args) { for(int i = 0; i < 6; ++i) { int ..
2023.07.30

이 게시글은 이것이 자바다(저자 : 신용권, 임경균)의 책과 동영상 강의를 참고하여 개인적으로 정리하는 글임을 알립니다. 


람다식의 개념

자바는 함수형 프로그래밍을 위해 Java 8부터 람다식을 지원한다.

자바는 객체지향 프로그래밍 언어로써 객체가 없이 함수가 존재할 수 없다.

클래스 내부에 무조건 함수가 있어야 하기 때문에 자바에서는 함수를 함수라 부르지 않고 메소드라고 부른다.

함수형 프로그래밍
함수형 프로그래밍이란 함수를 정의하고 이 함수를 데이터 처리부로 보내 데이터를 처리하는 기법을 가진다.
데이터 처리부는 데이터만 가지고 있을 뿐, 처리 방법이 정해져 있지 않아 외부에서 제공된 함수에 의존한다.
즉, 함수에 어떤 처리코드가 적혀있느냐에 따라 해당 데이터를 가지고 발생하는 실행결과가 달라질 수 있다는 뜻이다.

 

자바는 람다식을 익명 구현 객체로 변환한다.

 

아래와 같은 인터페이스가 정의 되었다고 하자.

@FunctionalInterface
public interface Calculable {
	//추상 메소드
	void calculate(int x, int y);
}

 

람다식은 인터페이스의 익명 구현 객체이므로 인터페이스 타입의 매개 변수에 대입될 수 있다.

public static void action(Calculable calculable) {
    //데이터
    int x = 10;
    int y = 4;
    //데이터 처리
    calculable.calculate(x, y);
}

action() 메소드를 호출할 때 매개값으로 아래와 같이 람다식을 제공할 수 있다.

action((x, y) -> {
    int result = x - y;
    System.out.println("result: " + result);
});

action 메소드에서 calculable.calculate(x, y)를 실행하면 람다식의 중괄호 블록이 실행되면서 데이터가 처리된다.

action() 메소드는 제공된 람다식을 이용해서 내부 데이터를 처리하는 처리부 역할을 한다.

해당 메소드에 어떤 람다식을 이용해서 데이터를 처리하느냐에 따라서 결과는 달라지게 된다.

 

데이터는 똑같고 어떤 람다식을 대입하느냐에 따라 처리 결과가 달라진다.

 

참고사항
-인터페이스의 익명 구현 객체를 람다식으로 표현하려면 인터페이스의 추상 메소드가 단 하나여야 한다.
-인터페이스가 단 하나의 추상 메소드를 가질 때, 이를 함수형 인터페이스라고 한다.
-인터페이스가 함수형 인터페이스임을 보장하기 위해서는 @FunctionalInterface 어노테이션을 붙이면 된다.

 

예제코드

더보기

Calculable.java

@FunctionalInterface
public interface Calculable {
	//추상 메소드
	void calculate(int x, int y);
}

 

LambdaExample.java

public class LambdaExample {
	public static void main(String[] args) {
		action((x, y) -> {
			int result = x + y;
			System.out.println("result: " + result);
		});

		action((x, y) -> {
			int result = x - y;
			System.out.println("result: " + result);
		});
	}

	public static void action(Calculable calculable) {
		//데이터
		int x = 10;
		int y = 4;
		//데이터 처리
		calculable.calculate(x, y);
	}
}
/*
result: 14
result: 6
*/

 


 

매개변수가 없는 람다식

 

아래의 코드들은 매개변수가 없는 람다식 예제이다.

 

Workable.java

@FunctionalInterface
public interface Workable {
	void work();
}

 

Person.java

public class Person {
	public void action(Workable workable) {
		workable.work();
	}
}

 

LambdaExample.java

public class LambdaExample {
	public static void main(String[] args) {
		Person person = new Person();

		//실행문이 두 개 이상인 경우 중괄호 필요
		person.action(() -> {
			System.out.println("출근을 합니다.");
			System.out.println("프로그래밍을 합니다.");
		});

		//실행문이 한 개일 경우 중괄호 생략 가능
		person.action(() -> System.out.println("퇴근합니다."));
	}
}
/*
출근을 합니다.
프로그래밍을 합니다.
퇴근합니다.
*/

 


아래의 코드들은 익명 구현 객체를 람다식으로 대체해 버튼의 클릭 이벤트를 처리하는 예제이다.

 

Button.java

public class Button {
	//정적 중첩 인터페이스
	@FunctionalInterface
	public static interface ClickListener {
		//추상 메소드
		void onClick();
	}
	
	//필드
	private ClickListener clickListener;
		
	//메소드
	public void setClickListener(ClickListener clickListener) {
		this.clickListener = clickListener;		
	}
	
	public void click() {
		this.clickListener.onClick();
	}
}

 

ButtonExample.java

public class ButtonExample {
	public static void main(String[] args) {
		//Ok 버튼 객체 생성
		Button btnOk = new Button();

		//Ok 버튼 객체에 람다식(ClickListener 익명 구현 객체) 주입
		btnOk.setClickListener(() -> {
			System.out.println("Ok 버튼을 클릭했습니다.");
		});
		
		//Ok 버튼 클릭하기
		btnOk.click();

		//Cancel 버튼 객체 생성
		Button btnCancel = new Button();

		//Cancel 버튼 객체에 람다식(ClickListener 익명 구현 객체) 주입
		btnCancel.setClickListener(() -> {
			System.out.println("Cancel 버튼을 클릭했습니다.");
		});
		
		//Cancel 버튼 클릭하기
		btnCancel.click();
	}
}
/*
Ok 버튼을 클릭했습니다.
Cancel 버튼을 클릭했습니다.
*/

 


 

매개변수가 있는 람다식

함수형 인터페이스의 추상 메소드에 매개변수가 있을 경우 람다식은 아래와 같이 작성할 수 있다.

매개변수를 선언할 때 타입은 생략할 수 있고, 구체적인 타입 대신에 var를 사용할 수도 있다.

출처 : 이것이 자바다 유튜브 동영상 강의

 

또한 매개변수가 하나일 경우에는 괄호를 생략할 수도 있다.

이때는 타입 또는 var를 붙일 수 없다.

출처 : 이것이 자바다 유튜브 동영상 강의

 

아래의 예제는 매개변수가 있는 람다식의 사용 예제이다.

Workable.java

@FunctionalInterface
public interface Workable {
	void work(String name, String job);
}

 

Speakable.java

@FunctionalInterface
public interface Speakable {
	void speak(String content);
}

 

Person.java

public class Person {
	public void action1(Workable workable) {
		workable.work("홍길동", "프로그래밍");
	}

	public void action2(Speakable speakable) {
		speakable.speak("안녕하세요");
	}
}

 

LambdaExample.java

public class LambdaExample {
	public static void main(String[] args) {
		Person person = new Person();

		//매개변수가 두 개일 경우
		person.action1((name, job) -> {
			System.out.print(name + "이 ");
			System.out.println(job + "을 합니다.");
		});
		person.action1((name, job) -> System.out.println(name + "이 " + job + "을 하지 않습니다."));
		
		//매개변수가 한 개일 경우
		person.action2(word -> {
			System.out.print("\"" + word + "\"");
			System.out.println("라고 말합니다.");
		});
		person.action2(word -> System.out.println("\"" + word + "\"라고 외칩니다."));
	}
}
/*
홍길동이 프로그래밍을 합니다.
홍길동이 프로그래밍을 하지 않습니다.
"안녕하세요"라고 말합니다.
"안녕하세요"라고 외칩니다.
*/

 


 

리턴값이 있는 람다식

함수형 인터페이스의 추상 메소드에 리턴값이 있을 경우 람다식은 아래와 같이 작성할  수 있다.

return 문 하나만 있을 경우 중괄호와 함께 return 키워드를 생략할 수 있다.

리턴값은 연산식 또는 리턴값 있는 메소드 호출로 대체할 수 있다.

출처 : 이것이 자바다 유튜브 동영상 강의

 

Calcuable.java

@FunctionalInterface
public interface Calcuable {
	double calc(double x, double y);
}

 

Person.java

public class Person {
	public void action(Calcuable calcuable) {
		double result = calcuable.calc(10, 4);
		System.out.println("결과: " + result);
	}
}

 

LambdaExample.java

public class LambdaExample {
	public static void main(String[] args) {
		Person person = new Person();

		//실행문이 두 개 이상일 경우
		person.action((x, y) -> {
			double result = x + y;
			return result;
		});

		//리턴문이 하나만 있을 경우(연산식)
		//person.action((x, y) -> {
		// return (x + y);
		//});
		//person.action((x, y) -> (x + y)); //이것도 가능
		person.action((x, y) -> x + y);

		//리턴문이 하나만 있을 경우(메소드 호출)
		//person.action((x, y) -> {
		// return sum(x, y);
		//});
		person.action((x, y) -> sum(x, y));
		
		}
	
	public static double sum(double x, double y) {
		return (x + y);
	}
}
/*
결과: 14.0
결과: 14.0
결과: 14.0
*/

 

 


 

메소드 참조

메소드 참조는 말 그대로 메소드를 참조해서 매개변수의 정보 및 리턴 타입을 알아내 람다식에서 불필요한 매개변수를 제거하는 것을 목적으로 한다.

 

예를 들어 두 개의 값을 받아 큰 수를 리턴하는 Math 클래스으 max() 정적 메소드를 호출하는 람다식은 아래와 같다.

(left, right) -> Math.max(left, right);

위 코드를 더 간결하게 아래의 코드로 줄일 수 있다.

Math::max;

 


 

정적 메소드와 인스턴스 메소드 참조

정적 메소드를 참조할 경우 클래스 이름 뒤에 :: 기호를 붙이고 정적 메소드 이름을 기술한다.

클래스 :: 메소드

 

인스턴스 메소드일 경우에는 먼저 객체를 생성한 다음 참조 변수 뒤에 :: 기호를 붙이고 인스턴스 메소드 이름을 기술한다.

참조변수 :: 메소드

 

더보기를 누르면 접힌 코드를 볼 수 있다.

더보기
@FunctionalInterface
public interface Calcuable {
	double calc(double x, double y);
}

 

public class Person {
	public void action(Calcuable calcuable) {
		double result = calcuable.calc(10, 4);
		System.out.println("결과: " + result);
	}
}

 

public class Computer {
	public static double staticMethod(double x, double y) {
		return x + y;
	}

	public double instanceMethod(double x, double y) {
		return x * y;
	}
}

MethodReferenceExample.java

public class MethodReferenceExample {
	public static void main(String[] args) {
		Person person = new Person();
		
		//정적 메소드일 경우
		//람다식
		//person.action((x, y) -> Computer.staticMethod(x, y));		
		//메소드 참조
		person.action(Computer :: staticMethod);
		
		//인스턴스 메소드일 경우
		Computer com = new Computer();
		//람다식
		//person.action((x, y) -> com.instanceMethod(x, y));		
		//메소드 참조
		person.action(com :: instanceMethod);
	}
}
/*
결과: 14.0
결과: 40.0
*/
매개변수의 위치를 바꿔야할 경우 메소드 참조를 사용할 수 없다.

 


매개변수의 메소드 참조

아래와 같이 람다식에서 제공되는 a 매개변수의 메소드를 호출해서 b 매개변수를 매개값으로 사용하는 경우도 있다.

(a, b) -> { a.instanceMethod(b); }

 

이를 메소드 참조로 표현하면 아래와 같다.

작성 방법은 정적 메소드 참조와 동일하지만, a의 인스턴스 메소드가 사용된다는 점에서 다르다.

클래스 :: instanceMethod

 

더보기를 누르면 접힌 코드를 볼 수 있다.

더보기
@FunctionalInterface
public interface Comparable {
	int compare(String a, String b);
}

 

public class Person {
	public void ordering(Comparable comparable) {
		String a = "홍길동";
		String b = "김길동";

		int result = comparable.compare(a, b);

		if(result < 0) {
			System.out.println(a + "은 " + b + "보다 앞에 옵니다.");
		} else if(result == 0) {
			System.out.println(a + "은 " + b + "과 같습니다.");
		} else {
			System.out.println(a + "은 " + b + "보다 뒤에 옵니다.");
		}
	}
}

 

MethodReferenceExample.java

public class MethodReferenceExample {
	public static void main(String[] args) {
		Person person = new Person();
		
		person.ordering((a, b) -> {
			return a.compareToIgnoreCase(b);
		});
		
		person.ordering((a, b) -> a.compareToIgnoreCase(b));
		
		person.ordering(String :: compareToIgnoreCase); //매개변수의 메소드 참조
		
		//위 세개의 코드 모두 같은 기능을 동작하는 코드
	}
}
/*
홍길동은 김길동보다 뒤에 옵니다.
홍길동은 김길동보다 뒤에 옵니다.
홍길동은 김길동보다 뒤에 옵니다.
*/
a.compareToIgnoreCase(b)
위 메소드는 String 클래스의 인스턴스 메소드이며, 리턴값은 a 문자열이 b 문자열보다 크면 양의 정수, 같으면 0, 작으면 의 정수 (대소문자 고려사항 무시)이다.

 

 


 

생성자 참조

생성자를 참조한다는 것은 객체를 생성하는 것을 의미한다.

람다식이 단순히 객체를 생성하고 리턴하도록 구성된다면 람다식을 생성자 참조로 대치할 수 있다.

아래의 코드에서 람다식은 단순히 객체를 생성한 후 리턴만 한다.

(a, b) -> {return new 클래스(a,b);}

 

이것을 생성자 참조로 표현하면 아래와 같다.

클래스 :: new

 

생성자가 오버로딩되어 여러 개가 있을 경우, 컴파일러는 함수형 인터페이스의 추상 메소드와 동일한 매개변수 타입과 개수를 가지고 있는 생성자를 찾아 실행한다.
만약 해당 생성자가 존재하지 않으면 컴파일 오류가 발생한다.

 

Creatable1.java

@FunctionalInterface
public interface Creatable1 {
	public Member create(String id);
}

 

Creatable2.java

@FunctionalInterface
public interface Creatable2 {
	public Member create(String id, String name);
}

 

Member.java

public class Member {
	private String id;
	private String name;
	
	public Member(String id) {
		this.id = id;
		System.out.println("Member(String id)");
	}
		
	public Member(String id, String name) {
		this.id = id;
		this.name = name;
		System.out.println("Member(String id, String name)");
	}
	
	@Override
	public String toString() {
		String info = "{ id: " + id + ", name: " + name + " }";
		return info;
	}
}

 

Person.java

public class Person {
	public Member getMember1(Creatable1 creatable) {
		String id = "winter";
		Member member = creatable.create(id);
		return member;
	}

	public Member getMember2(Creatable2 creatable) {
		String id = "winter";
		String name = "한겨울";
		Member member = creatable.create(id, name);
		return member;
	}
}

 

ConstructorReferenceExample.java

public class ConstructorReferenceExample {
	public static void main(String[] args) {
		Person person = new Person();
		//---------------------------------------------------------
		Member m1 = person.getMember1((id)->{
			Member m = new Member(id);
			return m;
		});
		
		 m1 = person.getMember1((id)->{
				return new Member(id);	
			});
		
		m1 = person.getMember1((id)-> new Member(id));
		
		m1 = person.getMember1( Member :: new );
		System.out.println(m1);
		System.out.println();
        //위 코드는 모두 같은 결과를 내는 코드이다.
		//---------------------------------------------------------
		Member m2 = person.getMember2((id, name)->{
			Member m = new Member(id, name);
			return m;
		});
		
		m2 = person.getMember2((id, name)->{
			return new Member(id, name);
		});
		
		m2 = person.getMember2((id, name)-> new Member(id, name));
		
		m2 = person.getMember2( Member :: new );
		System.out.println(m2);
        //위 코드는 모두 같은 결과를 내는 코드이다.
	}
}
/*
Member(String id)
Member(String id)
Member(String id)
Member(String id)
{ id: winter, name: null }

Member(String id, String name)
Member(String id, String name)
Member(String id, String name)
Member(String id, String name)
{ id: winter, name: 한겨울 }
*/

이 게시글은 이것이 자바다(저자 : 신용권, 임경균)의 책과 동영상 강의를 참고하여 개인적으로 정리하는 글임을 알립니다. 


LIFO & FIFO

LIFO는 스택(Stack) 자료구조를 뜻하고, FIFO는 큐(Queue) 자료구조를 뜻한다.

컬렉션 프레임워크는 LIFO 자료구조를 제공하는 스택 클래스와 FIFO 자료구조를 제공하는 큐 인터페이스를 제공하고 있다.

 

 

Stack

Stack 클래스는 LIFO 자료구조를 구현한 클래스이다.

아래는 Stack 객체를 생성하는 방법이다.

Stack<E> stack = new Stack<E>();
Stack<E> stack = new Stack<>();

 

Stack 클래스는 Vector 클래스를 상속했다.
따라서 Stack 객체는 동기화 처리가 되어있다.
즉, 멀티스레딩 환경에서 안전하게 처리 가능하다.

 

아래는 Stack 클래스의 주요 메소드이다.

리턴 타입 메소드 설명
E push(E item) 주어진 객체를 스택에 넣는다.
E pop() 스택의 맨 위 객체를 빼낸다.
E lastElement() 스택의 맨 위 객체를 빼내지 않고 리턴만 한다.

 

사용 예제

더보기

Coin.java

public class Coin {
	private int value;

	public Coin(int value) {
		this.value = value;
	}

	public int getValue() {
		return value;
	}
}

 

StackExample.java

import java.util.Stack;

public class StackExample {
	public static void main(String[] args) {
		//Stack 컬렉션 생성
		Stack<Coin> coinBox = new Stack<Coin>();
		
		//동전 넣기
		coinBox.push(new Coin(100));
		coinBox.push(new Coin(50));
		coinBox.push(new Coin(500));
		coinBox.push(new Coin(10));

		//동전을 하나씩 꺼내기
		while(!coinBox.isEmpty()) {
			Coin coin = coinBox.pop();
			System.out.println("꺼내온 동전 : " + coin.getValue() + "원");
		}
	}
}
/*
꺼내온 동전 : 10원
꺼내온 동전 : 500원
꺼내온 동전 : 50원
꺼내온 동전 : 100원
*/

 


 

Queue

Queue 인터페이스는 FIFO 자료구조에서 사용되는 메소드를 정의하고 있다.

아래는 Queue 인터페이스에 정의되어 있는 메소드이다.

리턴 타입 메소드 설명
boolean offer(E e) 또는 add(E e) 주어진 객체를 큐에 넣는다.
E poll() 큐에서 객체를 빼낸다.
E peek() 큐의 맨 앞의 객체를 빼내지 않고 리턴만 한다.

 

add()와 offer()의 차이
add()메소드와 offer() 메소드의 차이점은 문제 상황에서 예외를 발생시키는가 아니면 false를 리턴하느냐에 있다.
add()는 삽입에 실패할 경우 예외를 발생시키지만, offer는 false를 리턴한다.

 

Queue 인터페이스를 구현한 대표적인 클래스는 LinkedList이다.

그렇기 때문에 LinkedList 객체를 Queue 인터페이스 변수에 아래와 같이 대입할 수 있다.

Queue<E> queue = new LinkedList<E>();
Queue<E> queue = new LinkedList<>();

 

사용 예제

더보기

Message.java

public class Message {
	public String command;
	public String to;

	public Message(String command, String to) {
		this.command = command;
		this.to = to;
	}
}

 

QueueExample.java

import java.util.LinkedList;
import java.util.Queue;

public class QueueExample {
	public static void main(String[] args) {
		//Queue 컬렉션 생성
		Queue<Message> messageQueue = new LinkedList<>();
		
		//메시지 넣기
		messageQueue.offer(new Message("sendMail", "홍길동"));
		messageQueue.offer(new Message("sendSMS", "신용권"));
		messageQueue.offer(new Message("sendKakaotalk", "감자바"));
		
		//메시지를 하나씩 꺼내어 처리
		while(!messageQueue.isEmpty()) {
			Message message = messageQueue.poll();
			switch(message.command) {
				case "sendMail":
					System.out.println(message.to + "님에게 메일을 보냅니다.");
					break;
				case "sendSMS":
					System.out.println(message.to + "님에게 SMS를 보냅니다.");
					break;
				case "sendKakaotalk": 
					System.out.println(message.to + "님에게 카카오톡를 보냅니다.");
					break;
			}
		}
	}
}
/*
홍길동님에게 메일을 보냅니다.
신용권님에게 SMS를 보냅니다.
감자바님에게 카카오톡를 보냅니다.
*/

 


 

동기화된 컬렉션

컬렉션 프레임워크의 대부분의 클래스들은 싱글 스레드 환경에서 사용할 수 있도록 설계되어 있다.

Vector와 HashTable은 동기화된 메소드로 구성되어 있기 때문에 멀티 스레드 환경에서 안전하게 처리할 수 있다.

하지만 ArrayList와 HashSet, HashMap은 동기화된 메소드로 구성되어 있지 않아 멀티 스레드 환경에서 안전하지 않다.

 

경우에 따라서 ArrayList와 HashSet, HashMap을 멀티 스레드 환경에서 사용해야 할 때가 있다.

이런 경우를 대비해서 컬렉션 프레임워크는 비동기화된 메소드를 동기화된 메소드로 래핑하는 Collections의 synchronizedXXX() 메소드를 제공한다.

리턴 타입 메소드(매개변수) 설명
List<T> synchronized(List<T> list) List를 동기화된 List로 리턴
Map<K, V> synchronized(Map <K, V> m) Map을 동기화된 Map으로 리턴
Set<T> synchronized(Set<T> s) Set을 동기화된 Set으로 리턴

 

이 메소드들은 매개값으로 비동기화된 컬렉션을 대입하면 동기화된 컬렉션을 리턴한다.

 

사용 예제

더보기

SynchronizedMapExample.java

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class SynchronizedMapExample {
	public static void main(String[] args) {
		//Map 컬렉션 생성
		Map<Integer, String> map = Collections.synchronizedMap(new HashMap<>());
			
		//작업 스레드 객체 생성
		Thread threadA = new Thread() {
			@Override
			public void run() {
				//객체 1000개 추가
				for(int i=1; i<=1000; i++) {
					map.put(i, "내용"+i);
				}
			}
		};
			
		//작업 스레드 객체 생성
		Thread threadB = new Thread() {
			@Override
			public void run() {
				//객체 1000개 추가
				for(int i=1001; i<=2000; i++) {
					map.put(i, "내용"+i);
				}
			}
		};
		
		//작업 스레드 실행
		threadA.start();
		threadB.start();
		
		//작업 스레드들이 모두 종료될 때까지 메인 스레드를 기다리게 함
		try {
			threadA.join();
			threadB.join();
		} catch(Exception e) {
		}
			
		//저장된 총 객체 수 얻기
		int size = map.size();
		System.out.println("총 객체 수: " + size);
		System.out.println();
	}
}

 

 


 

수정할 수 없는 컬렉션

수정할 수 없는 컬렉션이란 요소를 추가 또는 삭제를 할 수 없는 컬렉션을 뜻한다.

 

정적 메소드 of()

List, Set, Map 인터페이스의 정적 메소드인 of()로 수정할 수 없도록 할 수 있다.

List<E> immutableList = List.of(E...elements);
Set<E> immutableSet = Set.of(E...elements);
Map<K, V> immutableMap = Map.of(K k1, V v1, K k2, V v2 ...);

 

정적 메소드 copyOf()

List, Set, Map 인터페이스의 정적 메소드인 copyOf()로 수정할 수 없도록 할 수 있다.

List<E> immutableList = List.copyOf(E...elements);
Set<E> immutableSet = Set.copyOf(E...elements);
Map<K, V> immutableMap = Map.copyOf(K k1, V v1, K k2, V v2 ...);

 

배열로부터 수정할 수 없는 List 컬렉션 만들기

String[] arr = {"A", "B", "C"};
List<String> immutableList = Arrays.asList(arr);

 

사용예제

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class ImmutableExample {
	public static void main(String[] args) {
		//List 불변 컬렉션 생성
		List<String> immutableList1 = List.of("A", "B", "C");
		//immutableList1.add("D"); (x)

		//Set 불변 컬렉션 생성
		Set<String> immutableSet1 = Set.of("A", "B", "C");
		//immutableSet1.remove("A"); (x)

		//Map 불변 컬렉션 생성
		Map<Integer, String> immutableMap1 = Map.of(
				1, "A",
				2, "B",
				3, "C"
				);
		//immutableMap1.put(4, "D"); (x)
		
		//List 컬렉션을 불변 컬렉션으로 복사
		List<String> list = new ArrayList< >();
		list.add("A");
		list.add("B");
		list.add("C");
		List<String> immutableList2 = List.copyOf(list);
		
		//Set 컬렉션을 불변 컬렉션으로 복사
		Set<String> set= new HashSet< >();
		set.add("A");
		set.add("B");
		set.add("C");
		Set<String> immutableSet2 = Set.copyOf(set);
			
		//Map 컬렉션을 불변 컬렉션으로 복사
		Map<Integer, String> map = new HashMap< >();
		map.put(1, "A");
		map.put(2, "B");
		map.put(3, "C");
		Map<Integer, String> immutableMap2 = Map.copyOf(map);
		
		//배열로부터 List 불변 컬렉션 생성
		String[] arr = { "A", "B", "C" };
		List<String> immutableList3 = Arrays.asList(arr);
	}
}

이 게시글은 이것이 자바다(저자 : 신용권, 임경균)의 책과 동영상 강의를 참고하여 개인적으로 정리하는 글임을 알립니다. 


컬렉션 프레임워크(Collection Framework)

자바는 널리 알려져 있는 자료구조를 바탕으로 객체들을 효율적으로 추가, 삭제 검색할 수 있도록 관련된 인터페이스와 클래스들을 java.util 패키지에 포함시켜 놓았다.

이들을 총칭해서 컬렉션 프레임워크라고 부른다.

컬렉션 프레임워크는 몇 가지 인터페이스를 통해서 다양한 컬렉션 클래스를 이용할 수 있도록 설계되어 있다.

주요 인터페이스로는 List, Set, Map이 있다.

컬렉션 프레임워크
배열은 길이가 정해지면 바꿀 수 없었지만, 컬렉션 프레임워크는 길이가 가변적이다.

컬렉션 프레임워크는 인터페이스이기 때문에 인터페이스에 정의된 메소드들의 사용 방법만 익히면 어떤 구현 클래스가 오던지 사용 방법이 모두 똑같기 때문에 편리하다.

 

 

List와 Set은 객체를 추가, 삭제, 검색하는 방법에 있어서 공통점이 있기 때문에 공통된 메소드만 따로 모아 Collection 인터페이스로 정의해 두고 이것을 상속하고 있다.

 

Map은 키와 값을 하나의 쌍으로 묶어서 관리하는 구조로 되어 있어 List 및 Set과는 사용 방법이 다르다.

 

아래의 표는 각 인터페이스 별로 사용할 수 있는 컬렉션의 특징을 정리한 표이다.

인터페이스 분류 특징 구현 클래스
Collection List -순서를 유지하고 저장
-중복 저장 가능
ArrayList, Vector
LinkedList
Set -순서를 유지하지 않고 저장
-중복 저장 안됨
HashSet, TreeSet
Map -키와 값으로 구성된 엔트리 저장
-키는 중복 저장 안됨
HashMap, HashTable, TreeMap, Properties

 

 


 

List 컬렉션

List 컬렉션은 객체를 인덱스로 관리하기 때문에 객체를 저장하면 인덱스가 부여되고 인덱스로 객체를 검색, 삭제할 수 있는 기능을 제공한다.

 

List 컬렉션에서 공통적으로 사용 가능한 List 인터페이스 메소드는 아래와 같다.

기능 메소드 설명
객체 추가 boolean add(E e) 객체를 맨 끝에 추가
void add(int index, E element) 인덱스에 객체를 추가
set(int index, E element) 인덱스의 객체를 새로운 객체로 바꿈
객체 검색 boolean contains(Object o) 주어진 객체가 저장되어 있는지 여부
E get(int index) 주어진 인덱스에 저장된 객체를 리턴
isEmpty() 컬렉션이 비어 있는지 여부
int size() 저장되어 있는 전체 객체 수를 리턴
객체 삭제 void clear() 저장된 모든 객체를 삭제
E remove(int index) 주어진 인덱스에 저장된 객체를 삭제
boolean remove(Object o) 주어진 객체를 삭제

 


 

ArrayList

ArrayList는 List 컬렉션에서 가장 많이 사용하는 컬렉션이다.

ArrayList에 객체를 추가하면 내부 배열에 객체가 저장된다.

일반 배열과 차이점은 제한 없이 객체를 추가할 수 있다는 점이다.

tip
List 컬렉션은 객체 자체를 저장하는 것이 아니라 객체의 번지를 저장한다.
또한 동일한 객체를 중복 저장할 수 있는데, 이 경우 동일한 번지가 저장된다.
null 또한 저장이 가능하다.

 

ArrayList 컬렉션은 아래와 같이 생성할 수 있다.

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

타입 파라미터 E에는 ArrayList에 저장하고 싶은 객체 타입을 지정하면 된다.

객체 타입을 모두 생략하면 Object 타입 객체를 저장할 수 있다.

효율성
빈번한 객체 삭제와 삽입이 일어나면 해당 인덱스부터 끝까지 인덱스가 당겨지거나 밀려나게 된다.
따라서 이런 경우에는 LinkedList를 사용하는 것이 좋다.

 

아래는 ArrayList에 객체를 추가, 검색, 삭제하는 예제이다.

더보기

Board.java

public class Board {
	private String subject;
	private String content;
	private String writer;

	public Board(String subject, String content, String writer) {
		this.subject = subject;
		this.content = content;
		this.writer = writer;
	}

	public String getSubject() { return subject; }
	public void setSubject(String subject) { this.subject = subject; }
	public String getContent() { return content; }
	public void setContent(String content) { this.content = content; }
	public String getWriter() { return writer; }
	public void setWriter(String writer) { this.writer = writer; }
}

 

ArrayListExample.java

import java.util.ArrayList;
import java.util.List;
	
public class ArrayListExample {
	public static void main(String[] args) {
		//ArrayList 컬렉션 생성
		List<Board> list = new ArrayList< >();
		
		//객체 추가
		list.add(new Board("제목1", "내용1", "글쓴이1"));
		list.add(new Board("제목2", "내용2", "글쓴이2"));
		list.add(new Board("제목3", "내용3", "글쓴이3"));
		list.add(new Board("제목4", "내용4", "글쓴이4"));
		list.add(new Board("제목5", "내용5", "글쓴이5"));

		//저장된 총 객체 수 얻기
		int size = list.size();
		System.out.println("총 객체 수: " + size);
		System.out.println();
				
		//특정 인덱스의 객체 가져오기
		Board board = list.get(2);
		System.out.println(board.getSubject() + "\t" + board.getContent() +
								"\t" + board.getWriter());
		System.out.println();
			
		//모든 객체를 하나씩 가져오기
		for(int i=0; i<list.size(); i++) {
			Board b = list.get(i);
			System.out.println(b.getSubject() + "\t" + b.getContent() +
								"\t" + b.getWriter());
		}
		System.out.println();
			
		//객체 삭제
		list.remove(2);
		list.remove(2);
			
		//향상된 for문으로 모든 객체를 하나씩 가져오기
		for(Board b : list) {
			System.out.println(b.getSubject() + "\t" + b.getContent() +
								"\t" + b.getWriter());
		}
	}
}
/*
총 객체 수: 5

제목3	내용3	글쓴이3

제목1	내용1	글쓴이1
제목2	내용2	글쓴이2
제목3	내용3	글쓴이3
제목4	내용4	글쓴이4
제목5	내용5	글쓴이5

제목1	내용1	글쓴이1
제목2	내용2	글쓴이2
제목5	내용5	글쓴이5
*/

 


 

Vector

Vector는 ArrayList와 동일한 내부 구조를 가지고 있다.

차이점은 동기화된 메소드로 구성되어 있기 때문에 멀티 스레드가 동시에 Vector() 메소드를 호출할 수 없다는 것이다.

그렇기 때문에 멀티 스레드 환경에서 안전하게 객체를 추가 또는 삭제할 수 있다.

 

Vector 컬렉션은 다음과 같이 생성할 수 있다.

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

타입 파라미터 E에는 Vector에 저장하고 싶은 객체 타입을 지정하면 된다.

객체 타입을 모두 생략하면 모든 종류의 객체를 저장할 수 있다.

 

아래의 예제는 쓰레드 A, B에서 동시에 객체를 벡터에 1000개씩 추가한 후 전체 저장된 수를 출력하는 예제이다.

벡터가 아니라 ArrayList로 구현하면 실행 결과가 달라진다. 

더보기

Board.java

public class Board {
	private String subject;
	private String content;
	private String writer;
	
	public Board(String subject, String content, String writer) {
		this.subject = subject;
		this.content = content;
		this.writer = writer;
	}

	public String getSubject() { return subject; }
	public void setSubject(String subject) { this.subject = subject; }
	public String getContent() { return content; }
	public void setContent(String content) { this.content = content; }
	public String getWriter() { return writer; }
	public void setWriter(String writer) { this.writer = writer; }
}

 

VectorExample.java

import java.util.List;
import java.util.Vector;

public class VectorExample {
	public static void main(String[] args) {
		//Vector 컬렉션 생성
		List<Board> list = new Vector<>();
	
		//작업 스레드 객체 생성
		Thread threadA = new Thread() {
			@Override
			public void run() {
				//객체 1000개 추가
				for(int i=1; i<=1000; i++) {
					list.add(new Board("제목"+i, "내용"+i, "글쓴이"+i));
				}
			}
		};
		
		//작업 스레드 객체 생성
		Thread threadB = new Thread() {
			@Override
			public void run() {
				//객체 1000개 추가
				for(int i=1001; i<=2000; i++) {
					list.add(new Board("제목"+i, "내용"+i, "글쓴이"+i));
				}
			}
		};
		
		//작업 스레드 실행
		threadA.start();
		threadB.start();
		
		//작업 스레드들이 모두 종료될때까지 메인 스레드를 기다리게함
		try {
			threadA.join();
			threadB.join();
		} catch(Exception e) {
		}
		
		//저장된 총 객체 수 얻기
		int size = list.size();
		System.out.println("총 객체 수: " + size);		
		System.out.println();		
	}
}
/*
총 객체 수: 2000
*/

 


 

LinkedList

LinkedList는 ArrayList와 사용 방법은 동일하지만 내부 구조는 완전히 다르다.

LinkedList는 인접 객체를 체인처럼 연결해서 관리한다.

연결 리스트는 특정 위치에서 객체를 삽입하거나 삭제하면 바로 앞뒤 링크만 변경하면 되므로 빈번한 객체 삭제와 삽입이 일어나는 곳에서 ArrayList보다 좋은 성능을 발휘한다.

LinkedList 컬렉션은 DLL(Double Linked List) 구조이다.

LinkedList 컬렉션은 아래와 같이 생성할 수 있다.

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

 

아래의 예제는 ArrayList와 LinkedList에 만 개의 객체를 삽입하는 데 걸린 시간을 측정한 것이다.

더보기

LinkedListExample.java

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class LinkedListExample {
	public static void main(String[] args) {
		//ArrayList 컬렉션 객체 생성
		List<String> list1 = new ArrayList<String>();

		//LinkedList 컬렉션 객체 생성
		List<String> list2 = new LinkedList<String>();

		//시작 시간과 끝 시간을 저장할 변수 선언
		long startTime;
		long endTime;

		//ArrayList 컬렉션에 저장하는 시간 측정
		startTime = System.nanoTime();
		for(int i=0; i<10000; i++) {
			list1.add(0, String.valueOf(i));
		}
		endTime = System.nanoTime();
		System.out.printf("%-17s %8d ns \n", "ArrayList 걸린 시간: ", (endTime-startTime) );

		//LinkedList 컬렉션에 저장하는 시간 측정
		startTime = System.nanoTime();
		for(int i=0; i<10000; i++) {
			list2.add(0, String.valueOf(i));
		}
		endTime = System.nanoTime();
		System.out.printf("%-17s %8d ns \n", "LinkedList 걸린 시간: ", (endTime-startTime) );
	}
}
/*
ArrayList 걸린 시간:   5813200 ns 
LinkedList 걸린 시간:    880900 ns 
*/

 

 


 

Set 컬렉션

Set 컬렉션은 저장 순서가 유지되지 않는다.

또한 객체를 중복해서 저장할 수 없고, 하나의 null만 저장할 수 있다. 

Set 컬렉션은 수학의 집합에 비유될 수 있다.

집합은 순서와 상관없고 중복이 허용되지 않기 때문이다.

 

Set 컬렉션에는 HashSet, LinkedHashSet, TreeSet 등이 있는데, Set 컬렉션에서 공통적으로 사용 가능한 Set 인터페이스의 메소드는 아래 표와 같다.

LinkedHashSet : 요소의 저장 순서를 유지하지 않는다.
TreeSet : 요소를 정렬된 순서로 유지한다.
기능 메소드 설명
객체 추가 boolean add(E e) 주어진 객체를 성공적으로 저장하면 true를 리턴하고 중복 객체면 false를 리턴
객체 검색 boolean contains(Object o) 주어진 객체가 저장되어 있는지 여부
isEmpty() 컬렉션이 비어 있는지 조사
Iterator<E> iterator() 저장된 객체를 한 번씩 가져오는 반복자 리턴
int size() 저장되어 있는 전체 객체 수 리턴
객체 삭제 void clear() 저장된 모든 객체를 삭제
boolean remove(Object o) 주어진 객체를 삭제
Iterator<E> iterator()
이 메소드는 Interface Iterable<E>의 추상 메소드이다.
Iterable을 한국어로 번역하면 '반복할 수 있는'이다.
향상된 for문에서 선언할 수 있는 객체는 이 인터페이스를 구현한 객체만 올 수 있다.
iterator() 메소드를 호출하면 해당 객체의 참조를 Iterator 타입으로 가져온다.

 


 

HashSet

Set 컬렉션 중에서 가장 많이 사용되는 것이 HashSet이다.

아래는 HashSet 컬렉션을 생성하는 방법이다.

Set<E> set = new HashSet<E>();
Set<E> set = new HashSet<>();
Set set new Hashset();

HashSet은 동일한 객체는 중복 저장하지 않는다.

여기서 동일한 객체란 동등 객체를 말한다.
HashSet은 다른 객체라도 HashCode() 메소드의 리턴값이 같고, equals() 메소드가 true를 리턴하면 동일한 객체라고 판단하고 중복 저장하지 않는다.

 

HashSet 컬렉션 생성 예제

더보기

HashSetExample.java

import java.util.*;

public class HashSetExample {
	public static void main(String[] args) {
		//HashSet 컬렉션 생성
		Set<String> set = new HashSet<String>();
		
		//객체 저장
		set.add("Java");
		set.add("JDBC");
		set.add("Servlet/JSP");
		set.add("Java");			//<-- 중복 객체이므로 저장하지 않음
		set.add("iBATIS");
		
		//저장된 객체 수 출력
		int size = set.size();
		System.out.println("총 객체 수: " + size);
	}
}
/*
총 객체 수: 4
*/

 

이름과 나이가 동일할 경우 Member 객체를 HashSet에 중복 저장하지 않는 예제

더보기

Member.java

public class Member {
	public String name;
	public int age;

	public Member(String name, int age) {
		this.name = name;
		this.age = age;
	}
				
	//hashCode 재정의
	@Override
	public int hashCode() {
		return name.hashCode() + age;
	}

	//equals 재정의
	@Override
	public boolean equals(Object obj) {
		if(obj instanceof Member target) {
			return target.name.equals(name) && (target.age==age) ;
		} else {
			return false;
		}
	}
}

 

HashSetExample.java

import java.util.*;
	
public class HashSetExample {
	public static void main(String[] args) {
		//HashSet 컬렉션 생성
		Set<Member> set = new HashSet<Member>();

		//Member 객체 저장
		set.add(new Member("홍길동", 30));
		set.add(new Member("홍길동", 30));

		//저장된 객체 수 출력
		System.out.println("총 객체 수 : " + set.size());
	}
}
/*
총 객체 수 : 1
*/

 

Set 컬렉션은 인덱스로 객체를 검색해서 가져오는 메소드가 없다.

대신 객체를 한 개씩 반복해서 가져와야 하는데, 두 가지 방법이 있다.

1. for 문을 이용하는 방법

for문에서 HashSet 객체를 직접적으로 추가 및 삭제를 하면 안 된다.
Set<E> set = new HashSet<E>();
for(E e : set)
{
    for(String element : set)
    {
        if(element.equals("JSP"))
        {
            set.remove(element);
        }
    }
}
위 코드에서 set의 객체의 개수가 4개라고 하면, 처음에 for문은 반복하는 횟수가 4번으로 정해져 있다.
하지만 중간에 remove나 add 메소드로 인해서 set 객체의 개수가 감소 또는 증가하면 4번 반복을 돌아야하는데 객체의 수가 4개 미만 또는 초과이므로 for문은 오류를 내뿜는다.
따라서 직접적인 add나 remove 메소드 호출은 지양해야 한다.

추가 또는 삭제를 하려면 iterator() 메소드로 반복자를 얻어서 작업을 하는 것이 안전하다.

 

2. iterator() 메소드로 반복자를 얻어 객체를 하나씩 가져오기

Set<E> set = new HashSet<E>();
Iterator<E> iterator = set.iterator();

iterator는 Set 컬렉션의 객체를 가져오거나 제거하기 위해 아래의 메소드를 제공한다.

리턴 타입 메소드명 설명
boolean hasNext() 가져올 객체가 있으면 true를 리턴하고 없으면 false를 리턴
E next() 컬렉션에서 하나의 객체를 가져온다.
void remove() next()로 가져온 객체를 Set 컬렉션에서 제거한다.

사용 방법은 아래와 같다.

while(iterator.hasNext()) {
    E e = iterator.next();
}

hasNext() 메소드로 가져올 객체가 있는지 먼저 확인하고, true를 리턴할 때만 next() 메소드로 객체를 가져온다.

만약, next()로 가져온 객체를 컬렉션에서 제거하고 싶다면 remove() 메소드를 사용한다.

 

HashSet 추가, 삭제, 제거 예제

import java.util.*;

public class HashSetExample {
	public static void main(String[] args) {
		//HashSet 컬렉션 생성
		Set<String> set = new HashSet<String>();
		
		//객체 추가
		set.add("Java");
		set.add("JDBC");
		set.add("JSP");
		set.add("Spring");
		
		//객체를 하나씩 가져와서 처리
		Iterator<String> iterator = set.iterator();
		while(iterator.hasNext()) {
			//객체를 하나 가져오기
			String element = iterator.next();
			System.out.println( element);
			if(element.equals("JSP")) {
				//가져온 객체를 컬렉션에서 제거
				iterator.remove();
			}
		}
		System.out.println();
		
		//객체 제거
		set.remove("JDBC");
		
		//객체를 하나씩 가져와서 처리
		for(String element : set) {
			System.out.println(element);
		}
	}
}
/*
Java
JSP
JDBC
Spring

Java
Spring
*/

 


 

Map 컬렉션

Map 컬렉션은 키(key)와 값(value)으로 구성된 엔트리(entry) 객체를 저장한다.

여기서 키와 값은 모두 객체이다.

키는 중복저장할 수 없지만, 값은 중복 저장할 수 있다.

기존에 저장된 키와 동일한 키로 값을 저장하면 기존의 값은 없어지고 새로운 값으로 바뀌게 된다.

Map 컬렉션에는 HashMap, HashTable, LinkedHash, Properties, TreeMap 등이 있다.

Map 컬렉션에서 공통적으로 사용 가능한 Map 인터페이스 메소드는 아래와 같다.

기능 메소드 설명
객체 추가 V put(K key, V value) 주어진 키와 값을 추가, 저장이 되면 값을 리턴
객체 검색 boolean containsKey(Object key) 주어진 키가 있는지 여부
boolean containsValue(object value) 주어진 값이 있는지 여부
Set<Map.Entry<K, V>> entrySet() 키와 값의 쌍으로 구성된 모든 Map.Entry 객체를 Set에 담아서 리턴
V get(Object key) 주어진 키의 값을 리턴
boolean isEmpty() 컬렉션이 비어있는지 여부
Set<K> keySet() 모든 키를 Set 객체에 담아서 리턴
int size() 저장된 키의 총 수를 리턴
Collection<V> values() 저장된 모든 값 Collection에 담아서 리턴
객체 삭제 void clear() 모든 Map.Entry(키와 값)를 삭제
V remove(Object key) 주어진 키와 일치하는 Map.Entry 삭제, 삭제가 되면 값을 리턴

위 표에서 K와 V는 타입 파라미터이고, K는 키 타입, V는 값 타입을 말한다.

 


 

HashMap

HashMap은 키로 사용할 객체가 hashCode() 메소드의 리턴값이 같고 equals() 메소드가 true를 리턴할 경우, 동일 키로 보고 중복 저장을 허용하지 않는다.

 

아래는 HashMap 컬렉션을 생성하는 방법이다. K와 V는 각각 키와 값의 타입을 지정할 수 있는 타입 파라미터이다.

Map<K, V>map = new HashMap<K, V>();

 

아래의 예제는 이름을 키로, 점수를 값으로 저장하는 해시맵 사용 방법을 보여준다.

더보기

HashMapExample.java

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

public class HashMapExample {
	public static void main(String[] args) {
		//Map 컬렉션 생성
		Map<String, Integer> map = new HashMap< >();

		//객체 저장
		map.put("신용권", 85);
		map.put("홍길동", 90);
		map.put("동장군", 80);
		map.put("홍길동", 95); //키가 같기 때문에 제일 마지막 값만 저장
		System.out.println("총 Entry 수: " + map.size());
		System.out.println();

		//키로 값 얻기
		String key = "홍길동";
		int value = map.get(key); //키를 매개값으로 주면 값을 리턴
		System.out.println(key + ": " + value);
		System.out.println();

		//키 Set 컬렉션을 얻고, 반복해서 키와 값을 얻기
		Set<String> keySet = map.keySet();
		Iterator<String> keyIterator = keySet.iterator();
		while (keyIterator.hasNext()) {
			String k = keyIterator.next();
			Integer v = map.get(k);
			System.out.println(k + " : " + v);
		}
		System.out.println();

		//엔트리 Set 컬렉션을 얻고, 반복해서 키와 값을 얻기
		Set<Entry<String, Integer>> entrySet = map.entrySet();
		Iterator<Entry<String, Integer>> entryIterator = entrySet.iterator();
		while (entryIterator.hasNext()) {
			Entry<String, Integer> entry = entryIterator.next();
			String k = entry.getKey();
			Integer v = entry.getValue();
			System.out.println(k + " : " + v);
		}
		System.out.println();
		
		//키로 엔트리 삭제
		map.remove("홍길동");
		System.out.println("총 Entry 수: " + map.size());
		System.out.println();
	}
}
/*
총 Entry 수: 3

홍길동: 95

홍길동 : 95
신용권 : 85
동장군 : 80

홍길동 : 95
신용권 : 85
동장군 : 80

총 Entry 수: 2
*/

 


 

HashTable

HashTable은 HashMap과 동일한 내부 구조를 가지고 있다. 차이점은 동기화된 메소드로 구성되어 있기 때문에 멀티 스레드가 동시에 HashTable의 메소드들을 실행할 수 없다는 것이다.

따라서 멀티 스레드 환경에서도 안전하게 객체를 추가, 삭제할 수 있다.

 

Map<K, V>map = new Hashtable<K, V>();

 


 

Properties

Properties는 HashTable의 자식 클래스이기 때문에 해시 테이블의 특징을 그대로 가지고 있다.

Properties는 키와 값을 String 타입으로 제한한 컬렉션이다.

또한 주로 확장자가 .properties인 프로퍼티 파일을 읽을 때 사용한다.

프로퍼티 파일은 키와 값이 = 기호로 연결되어 있는 텍스트 파일이다.

일반 텍스트 파일과는 다르게 ISO 8859-1 문자셋으로 저장되며, 한글일 경우에는 \u+유니코드로 표현되어 저장된다.

 

database.properties

driver=oracle.jdbc.OracleDirver
url=jdbc:oracle:thin:@localhost:1521:orcl
username=scott
password=tiger
admin=\uD64D\uAE38\uB3D9

 

Properties을 사용하면 위와 같은 프로퍼티 파일의 내용을 코드에서 쉽게 읽을 수 있다.

먼저 프로퍼티 객체를 생성하고, load() 메소드로 프로퍼티 파일의 내용을 메모리로 로드한다.

Properties properties = new Properties();
properties.load(Xxx.class.getResourceAsStream("database.properties"));

일반적으로 프로퍼티 파일은 클래스 파일들과 함께 저장된다.

따라서 클래스 파일을 기준으로 상대 경로를 이용해서 읽는 것이 편리하다.

Class 객체의 getResourceAsStream() 메소드는 주어진 상대 경로의 리소스 파일을 읽는 InputStream을 리턴한다.

 

PropertiesExample.java

import java.util.Properties;
	
public class PropertiesExample {
	public static void main(String[] args) throws Exception {
		//Properties 컬렉션 생성
		Properties properties = new Properties();

		//PropertiesExample.class와 동일한 ClassPath에 있는 database.properties 파일 로드
		properties.load(PropertiesExample.class.getResourceAsStream("database.properties"));

		//주어진 키에 대한 값 읽기
		String driver = properties.getProperty("driver");
		String url = properties.getProperty("url");
		String username = properties.getProperty("username");
		String password = properties.getProperty("password");
		String admin = properties.getProperty("admin");

		//값 출력
		System.out.println("driver : " + driver);
		System.out.println("url : " + url);
		System.out.println("username : " + username);
		System.out.println("password : " + password);
		System.out.println("admin : " + admin);
	}
}
/*
driver : oracle.jdbc.OracleDirver
url : jdbc:oracle:thin:@localhost:1521:orcl
username : scott
password : tiger
admin : 홍길동
*/

 


 

검색 기능을 강화시킨 컬렉션

TreeSet

TreeSet은 이진 트리를 기반으로 한 Set 컬렉션이다.

이진 트리는 여러 개의 노드가 트리 형태로 연결된 구조로, 루트 노드라고 불리는 하나의 노드에서 시작해 각 노드에 최대 2개의 노드를 연결할 수 있는 구조를 가지고 있다.

TreeSet에 객체를 저장하면 아래와 같이 자동으로 정렬된다.

부모 노드의 객체와 비교해서 낮은 것은 왼쪽 자식 노드에, 높은 것은 오른쪽 자식 노드에 저장한다.

 

아래는 트리셋 컬렉션을 생성하는 방법이다.

TreeSet<E> treeSet = new TreeSet<E>();
TreeSet<E> treeSet = new TreeSet<>();

Set 타입 변수에 대입을 하면 자식인 TreeSet 메소드를 사용할 수 없다.

 

아래는 TreeSet의 검색 관련 메소드들이다.

리턴 타입 메소드 설명
E first() 제일 낮은 객체를 리턴
last() 제일 높은 객체를 리턴
lower(E e) 주어진 객체보다 바로 아래 객체를 리턴
higher(E e) 주어진 객체보다 바로 위 객체를 리턴
floor(E e) 주어진 객체와 동등한 객체가 있으면 리턴, 만약 없다면 주어진 객체의 바로 아래의 객체를 리턴
ceiling(E e) 주어진 객체와 동등한 객체가 있으면 리턴, 만약 없다면 주어진 객체의 바로 위의 객체를 리턴
poolFirst() 제일 낮은 객체를 꺼내오고 컬렉션에서 제거
poolLast() 제일 높은 객체를 꺼내오고 컬렉션에서 제거
Iterator<E> decendingIterator() 내림차순으로 정렬된 Iterator를 리턴
NavigableSet<E>
decendingSet() 내림차순으로 정렬된 NavigableSet을 리턴
headSet(
E toElement,
boolean inclusive)
주어진 객체보다 낮은 객체들을 NavigableSet으로 리턴,
주어진 객체 포함 여부는 두 번째 매개값에 따라 달라짐
tailSet(
E fromElement,
boolean inclusive)
주어진 객체보다 높은 객체들을 NavigableSet으로 리턴,
주어진 객체 포함 여부는 두 번째 매개값에 따라 달라짐
headSet(
E fromElement,
boolean fromInclusive,
E toElement,
boolean toInclusive
)
시작과 끝으로 주어진 객체 사이의 객체들을 NavigableSet으로 리턴. 시작과 끝 객체의 포함 여부는 두 번째, 네 번째 매개값에 따라 달라짐

 

아래의 예제는 무작위로 저장한 점수를 검색하는 방법을 보여준다.

더보기

TreeSetExample.java

import java.util.NavigableSet;
import java.util.TreeSet;

public class TreeSetExample {
	public static void main(String[] args) {
		//TreeSet 컬렉션 생성
		TreeSet<Integer> scores = new TreeSet< >();

		//Integer 객체 저장
		scores.add(87);
		scores.add(98);
		scores.add(75);
		scores.add(95);
		scores.add(80);

		//정렬된 Integer 객체를 하나씩 가져오기
		for(Integer s : scores) {
			System.out.print(s + " ");
		}
		System.out.println("\n");

		//특정 Integer 객체를 가져오기
		System.out.println("가장 낮은 점수: " + scores.first());
		System.out.println("가장 높은 점수: " + scores.last());
		System.out.println("95점 아래 점수: " + scores.lower(95));
		System.out.println("95점 위의 점수: " + scores.higher(95));
		System.out.println("95점이거나 바로 아래 점수: " + scores.floor(95));
		System.out.println("85점이거나 바로 위의 점수: " + scores.ceiling(85) + "\n");

		//내림차순으로 정렬하기
		NavigableSet<Integer> descendingScores = scores.descendingSet();
		for(Integer s : descendingScores) {
			System.out.print(s + " ");
		}
		System.out.println("\n");
		
		//범위 검색( 80 <= )
		NavigableSet<Integer> rangeSet = scores.tailSet(80, true);
		for(Integer s : rangeSet) {
			System.out.print(s + " ");
		}
		System.out.println("\n");
		
		//범위 검색( 80 <= score < 90 )
		rangeSet = scores.subSet(80, true, 90, false);
		for(Integer s : rangeSet) {
			System.out.print(s + " ");
		}
	}
}
/*
75 80 87 95 98 

가장 낮은 점수: 75
가장 높은 점수: 98
95점 아래 점수: 87
95점 위의 점수: 98
95점이거나 바로 아래 점수: 95
85점이거나 바로 위의 점수: 87

98 95 87 80 75 

80 87 95 98 

80 87 
*/

 


 

TreeMap

TreeMap은 이진 트리를 기반으로 한 Map 컬렉션이다.

TreeSet과의 차이점은 키와 값이 저장된 Entry를 저장한다는 점이다.

TreeMap에 엔트리를 저장하면 키를 기준으로 자동 정렬되는데, 부모 키 값과 비교해서 낮은 것은 왼쪽, 높은 것은 오른쪽 자식 노드에 Entry 객체를 저장한다.

 

아래는 TreeMap 컬렉션을 생성하는 방법이다.

TreeMap<K, V> treeMap = new TreeMap<K, V>();
TreeMap<K, V> treeMap = new TreeMap<>();

Map 타입 변수에 대입해도 되지만 자식인 TreeMap 타입으로 대입해야 관련 메소드들을 사용할 수 있다.

 

아래는 TreeMap이 가지고 있는 검색 관련 메소드들이다.

리턴 타입 메소드 설명
Map.Entry<K, V> firstEntry() 제일 낮은 Map.Entry를 리턴
lastEntry() 제일 높은 Map.Entry를 리턴
lowerEntry(K key) 주어진 키보다 바로 아래 Map.Entry를 리턴
higherEntry(K key) 주어진 키보다 바로 위 Map.Entry를 리턴
flooerEntry(K key) 주어진 키와 동등한 키가 있으면 해당 Map.Entry를 리턴, 없다면 주어진 키 바로 아래의 Map.Entry를 리턴
ceilingEntry(K key) 주어진 키와 동등한 키가 있으면 해당 Map.Entry를 리턴, 없다면 주어진 키 바로 위의 Map.Entry를 리턴
poolFirstEntry(K key) 제일 낮은 Map.Entry를 꺼내오고 컬렉션에서 제거
poolLastEntry(K key) 제일 높은 Map.Entry를 꺼내오고 컬렉션에서 제거
NavigableSet<K> descendingKeySet() 내림차순으로 정렬된 키의 NavigableSet을 리턴
NavigableMap<K, V> descendingMap() 내림차순으로 정렬된 Map.Entry의 NavigableMap을 리턴
headMap(
K toKey,
boolean inclusive)
주어진 키보다 낮은  Map.Entry들을 NavigableMap으로 리턴, 주어진 키의 Map.Entry 포함 여부는 두 번째 매개값에 따라 달라짐
tailMap(
K fromKey,
boolean inclusive)
주어진 키보다 높은  Map.Entry들을 NavigableMap으로 리턴, 주어진 키의 Map.Entry 포함 여부는 두 번째 매개값에 따라 달라짐
subMap(
K fromKey,
boolean fromInclusive
K toKey,
boolean toInclusive
)
시작과 끝으로 주어진 키 사이의 Map.Entry들을 NavigableMap 컬렉션으로 반환. 시작과 끝 키의 Map.Entry 포함 여부는 두 번째, 네 번째 매개값에 따라 달라짐 

 

아래의 예제는 영어 단어와 페이지 번호를 무작위로 저장하고 검색하는 방법을 보여준다.

더보기

TreeMapExample.java

import java.util.Map.Entry;
import java.util.NavigableMap;
import java.util.Set;
import java.util.TreeMap;

public class TreeMapExample {
	public static void main(String[] args) {
		//TreeMap 컬렉션 생성
		TreeMap<String,Integer> treeMap = new TreeMap< >();

		//엔트리 저장
		treeMap.put("apple", 10);
		treeMap.put("forever", 60);
		treeMap.put("description", 40);
		treeMap.put("ever", 50);
		treeMap.put("zoo", 80);
		treeMap.put("base", 20);
		treeMap.put("guess", 70);
		treeMap.put("cherry", 30);

		//정렬된 엔트리를 하나씩 가져오기
		Set<Entry<String, Integer>> entrySet = treeMap.entrySet();
		for(Entry<String, Integer> entry : entrySet) {
			System.out.println(entry.getKey() + "-" + entry.getValue());
		}
		System.out.println();

		//특정 키에 대한 값 가져오기
		Entry<String,Integer> entry = null;
		entry = treeMap.firstEntry();
		System.out.println("제일 앞 단어: " + entry.getKey() + "-" + entry.getValue());
		entry = treeMap.lastEntry();
		System.out.println("제일 뒤 단어: " + entry.getKey() + "-" + entry.getValue());
		entry = treeMap.lowerEntry("ever");
		System.out.println("ever 앞 단어: " + entry.getKey() + "-" + entry.getValue() + "\n");
		
		//내림차순으로 정렬하기
		NavigableMap<String,Integer> descendingMap = treeMap.descendingMap();
		Set<Entry<String,Integer>> descendingEntrySet = descendingMap.entrySet();
		for(Entry<String,Integer> e : descendingEntrySet) {
			System.out.println(e.getKey() + "-" + e.getValue());
		}
		System.out.println();
		
		//범위 검색
		System.out.println("[c~h 사이의 단어 검색]");
		NavigableMap<String,Integer> rangeMap = treeMap.subMap("c", true, "h",	false);
		for(Entry<String, Integer> e : rangeMap.entrySet()) {
			System.out.println(e.getKey() + "-" + e.getValue());
		}
	}
}
/*
apple-10
base-20
cherry-30
description-40
ever-50
forever-60
guess-70
zoo-80

제일 앞 단어: apple-10
제일 뒤 단어: zoo-80
ever 앞 단어: description-40

zoo-80
guess-70
forever-60
ever-50
description-40
cherry-30
base-20
apple-10

[c~h 사이의 단어 검색]
cherry-30
description-40
ever-50
forever-60
guess-70
*/

 


 

Comparable과 Comparator

Comparable

TreeSet과 TreeMap에 저장되는 키 객체는 저장과 동시에 오름차순으로 정렬되지만, 어떤 객체든 정렬될 수 있는 것은 아니다.

객체가 Comparable 인터페이스를 구현하고 있어야 정렬을 할 수 있다.

사용자 정의 객체를 저장할 때에는 반드시 Comparable을 구현하고 있어야 한다.

 

Comparable 인터페이스에는 compareTo() 메소드가 정의되어 있다.

따라서 사용자 정의 클래스에서 이 메소드를 재정의해서 비교 결과를 정수 값으로 리턴해야 한다.

리턴 타입 메소드 설명
int compareTo(T o) 주어진 객체와 같으면 0을 리턴
주어진 객체보다 적으면 음수를 리턴
주어진 객체보다 크면 양수를 리턴

 

아래의 예제는 사용자 정의 객체를 정렬하기 위해 Comparable 인터페이스를 구현한 예제이다.

더보기

Person.java

public class Person implements Comparable<Person> {
	public String name;
	public int age;

	public Person(String name, int age) {
		this.name = name;
		this.age = age;
	}

	@Override
	public int compareTo(Person o) {
		if(age<o.age) return -1;
		else if(age == o.age) return 0;
		else return 1;
	}
}

 

ComparableExample.java

import java.util.TreeSet;

public class ComparableExample {
	public static void main(String[] args) {
		//TreeSet 컬렉션 생성
		TreeSet<Person> treeSet = new TreeSet<Person>();

		//객체 저장
		treeSet.add(new Person("홍길동", 45));
		treeSet.add(new Person("감자바", 25));
		treeSet.add(new Person("박지원", 31));
		
		//객체를 하나씩 가져오기
		for(Person person : treeSet) {
			System.out.println(person.name + ":" + person.age);
		}
	}
}
/*
감자바:25
박지원:31
홍길동:45
*/

 


 

Comparator

비교 기능이 있는 Comparable 구현 객체를 TreeSet에 저장하거나 TreeMap의 키로 저장하는 것이 원칙이다

하지만 비교 기능이 없는 Comparable 비구현 객체를 저장하고 싶다면 TreeSet과 TreeMap을 생성할 때 비교자(Comparator)를 아래와 같이 제공하면 된다.

TreeSet<K, V> treeSet = new TreeMap<E>(new ComparatorImpl());

TreeMap<K, V> treeMap = new TreeMap<K, V>(new ComparatorImpl());

 

비교자는 Comparator 인터페이스를 구현한 객체를 말하는데, Comparator 인터페이스에는 compare() 메소드가 정의되어 있다.

비교자는 이 메소드를 재정의해서 비교 결과를 정수 값으로 리턴하면 된다.

리턴 타입 메소드 설명
int compare(T o1, T o2) o1과 o2가 동등하다면 0을 리턴
o1이 o2 앞에 오게 하려면 음수를 리턴
o1이 o2 뒤에 오게 하려면 양수를 리턴

 

아래는 Comparable을 구현하고 있지 않은 사용자 정의 객체를 TreeSet에 저장하는 예제이다.

더보기

Fruit.java

public class Fruit {
	public String name;
	public int price;

	public Fruit(String name, int price) {
		this.name = name;
		this.price = price;
	}
}

 

FruitComparator.java

import java.util.Comparator;

public class FruitComparator implements Comparator<Fruit> {
	@Override
	public int compare(Fruit o1, Fruit o2) {
		if(o1.price < o2.price) return -1;
		else if(o1.price == o2.price) return 0;
		else return 1;
	}
}

 

ComparatorExample.java

import java.util.TreeSet;

public class ComparatorExample {
	public static void main(String[] args) {
		//비교자를 제공한 TreeSet 컬렉션 생성
		TreeSet<Fruit> treeSet = new TreeSet<Fruit>(new FruitComparator());
			
		//객체 저장
		treeSet.add(new Fruit("포도", 3000));
		treeSet.add(new Fruit("수박", 10000));
		treeSet.add(new Fruit("딸기", 6000));
		
		//객체를 하나씩 가져오기
		for(Fruit fruit : treeSet) {
			System.out.println(fruit.name + ":" + fruit.price);
		}
	}
}
/*
포도:3000
딸기:6000
수박:10000
*/

이 게시글은 이것이 자바다(저자 : 신용권, 임경균)의 책과 동영상 강의를 참고하여 개인적으로 정리하는 글임을 알립니다. 


데몬 스레드(Daemon Thread)

  • 데몬 스레드는 주 스레드의 작업을 돕는 보조적인 역할을 수행하는 스레드이다.
  • 주 스레드가 종료되면 데몬 스레드도 따라서 자동으로 종료된다.
  • 스레드를 데몬으로 만들기 위해서는 주 스레드가 데몬이 될 스레드의 setDaemon(true)를 호출하면 된다.
주 스레드와 메인 스레드는 같을 수도 있고 다를 수도 있다.
메인 스레드에서, 다른 스레드를 데몬 스레드로 설정했다면 메인 스레드는 주 스레드가 된다.
하지만 메인 스레드가 아닌 A 스레드에서, B 스레드를 데몬 스레드로 설정했다면 B 스레드 관점에서는 A 스레드가 주 스레드가 된다.

 

아래의 코드에서는 메인 스레드에서 또 다른 스레드를 데몬 스레드로 설정했기 때문에 메인 스레드가 주 스레드가 된다.

public class Main {
	public static void main(String[] args) {
		XXXThread thread = new XXXThread();
		thread.setDaemon(true);
		thread.start();
	}
}

따라서 메인 스레드가 종료되면 데몬 스레드도 종료된다.

 

아래의 예제는 메인 스레드에서 자동 저장 스레드를 데몬 스레드로 설정한다.

메인 스레드가 종료되면 자동 저장 스레드도 종료되는 것을 보여준다.

 

AutoSaveThread.java

public class AutoSaveThread extends Thread {
	public void save() {
		System.out.println("작업 내용을 저장함.");
	}

	@Override
	public void run() {
		while(true) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				break;
			}
			save();
		}
	}
}

 

DaemonExample.java

public class DaemonExample {
	public static void main(String[] args) {
		AutoSaveThread autoSaveThread = new AutoSaveThread();
		autoSaveThread.setDaemon(true);
		autoSaveThread.start();

		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
		}

		System.out.println("메인 스레드 종료");
	}
}
/*
작업 내용을 저장함.
작업 내용을 저장함.
메인 스레드 종료
*/

 


 

스레드풀(ThreadPool)

무분별하게 스레드를 늘리면 CPU 사용량이 증가하고 메모리 사용량이 늘어난다.

이에 따라 애플리케이션의 성능 또한 급격히 저하된다.

이렇게 병렬 작업 증가로 인한 스레드의 폭증을 막으려면 스레드풀을 사용하는 것이 좋다.

 

  • 스레드 풀은 작업 처리에 사용되는 스레드를 제한된 개수만큼 정해놓는다.
  • 이후 작업들은 작업 큐(Queue)에 들어가게 된다.
  • 작업 큐에 있는 작업들은 스레드가 하나씩 맡아서 처리한다.
예를 들어
작업이 20개 있고 최대 스레드 수가 5개라고 하면, 작업 1, 2, 3, 4, 5가 각각 스레드 1, 2, 3, 4, 5에 배정되고 스레드 1에 배정된 작업 1이 끝나면 스레드 1에 작업 6이 배정된다. 스레드 2에 배정된 작업 2가 끝나면 스레드 2에 작업 7이 배정된다.
이렇게 최대 스레드 수는 5개로 제한하고 먼저 작업이 끝나는 스레드에 작업 큐에 있는 다음 작업을 처리하도록 맡기는 것이다.

이렇게 하면 작업량이 증가해도 스레드의 개수가 늘어나지 않아 성능 저하가 발생하지 않는다.

출처 : 이것이 자바다 유튜브 동영상 강의

 


 

스레드풀 생성

자바는 스레드풀을 생성하고 사용할 수 있도록 java.util.concurrent 패키지에서 ExecutorService 인터페이스와 Executors 클래스를 제공하고 있다.

Executors의 아래의 두 정적 메소드를 사용하면 간단하게 스레드풀인 ExecutorsService 구현 객체를 만들 수 있다.

메소드명(매개변수) 초기 수  코어 수 최대 수
newCachedThreadPool() 0 0 int 범위 최대 값(이론상)
newFixedThreadPool(int n) 0 생성된 수 n
  • 초기 수 : 스레드풀이 생성될 때 기본적으로 생성되는 스레드 수
  • 코어 수 : 스레드가 증가된 후 사용되지 않는 스레드를 제거할 때 최소한 풀에서 유지하는 스레드 수
  • 최대 수 : 증가되는 스레드의 한도 수

 

newCachedThreadPool()

  • newCachedThreadPool()로 생성된 스레드풀의 초기 수와 코어 수는 0개이고, 작업 개수가 많아지면 새 스레드를 생성시켜 작업을 한다.
  • 이 메소드는 스레드 최대 수를 정하지 않는다. 따라서 이론상 int 범위 최대 값인 약 21억 개의 스레드가 생성될 수 있다.
  • 60초 동안 스레드가 아무 작업을 하지 않으면 스레드를 풀에서 제거한다.
ExecutorService executorService = Executors.newCachedThreadPool()

 

 

newFixedThreadPool(int n)

  • newFixedThreadPool(int n)로 생성된 스레드 풀의 초기 수는 0개이고, 작업 개수가 많아지면 최대 n개까지 스레드를 생성시켜 작업을 처리한다.
  • 이 스레드 풀의 특징은 생성된 스레드를 제거하지 않는다는 것이다. 
    즉, 최대가 n이고 n보다 작은 m개가 생성되었다면 코어수는 m이 된다.
ExecutorService executorService = Executors.newFixedThreadPool(5);
스레드풀 종료
newCachedThreadPool()와 달리 newFixedThreadPool(int n)은 스레드가 아무 작업을 하지 않아도 스레드는 제거가 되지 않는다

 

 

사용자 정의 스레드풀

  • 위 두 메소드를 사용하지 않고 직접 ThreadPoolExecutor로 스레드풀을 생성할 수도 있다.
  • 아래의 예시는 초기 수 0개, 코어 수 3개, 최대 수 100개인 스레드풀을 생성하는 코드이다.
  • 스레드가 120초 동안 놀고 있을 경우 해당 스레드를 풀에서 제거한다.
ExecutorService threadPool = new ThreadPoolExecutor(
    3, // 코어 스레드 개수
    100, // 최대 스레드 개수
    120L, // 최대 놀 수 있는 시간 (이 시간 넘으면 스레드 풀에서 쫓겨 남.)
    TimeUnit.SECONDS, // 놀 수 있는 시간 단위
    new SynchronousQueue<Runnable>() // 작업 큐
);

 


 

스레드풀 종료

스레드풀의 스레드는 기본적으로 데몬 스레드가 아니기 때문에 main 스레드가 종료되더라도 작업을 처리하기 위해 계속 실행 상태로 남아 있다.

스레드풀의 모든 스레드를 종료하려면 ExecutorService의 다음 두 메소드 중 하나를 실행해야 한다.

리턴 타입 메소드명(매개변수) 설명
void shutdown() 현재 처리 중인 작업뿐만 아니라 작업 큐에 대기하고 있는 모든 작업을 처리한 뒤에 스레드풀을 종료시킨다.
List(Runnable) shutdownNow() 현재 작업 처리 중인 스레드를 interrupt해서 작업을 중지시키고 스레드풀을 종료시킨다. 또한 작업 큐에 있는 미처리된 작업을 리턴한다.
  • shutdown() : 남아있는 작업을 마무리하고 스레드풀을 종료할 때
  • shutdownNow() : 남아있는 작업과 상관없이 강제 종료를 할 때

 

아래의 예제는 최대 5개의 스레드로 운영되는 스레드풀을 생성하고 종료한다.

ExecutorServiceExample.java

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
	
public class ExecutorServiceExample {
	public static void main(String[] args) {
		//스레드풀 생성
		ExecutorService executorService = Executors.newFixedThreadPool(5);
		//작업 생성과 처리 요청
		//스레드풀 종료
		executorService.shutdownNow();
	}
}

 


 

작업 생성과 처리 요청

작업 생성

하나의 작업은 Runnable 또는 Callable 구현 객체로 표현한다.

  • Runnable : 작업 처리 완료 후 리턴값 없음
  • Callable : 작업 처리 완료 후 리턴값 있음
  • 각각 run() 메소드와 call() 메소드를 호출시켜서 작업을 생성한다.

 

Runnable 익명 구현 객체

new Runnable(){
	@Override
	public void run(){
    //스레드가 처리할 작업 내용
    }
}

 

 

Callable 익명 구현 객체

new Callable<T>(){
	@Override
	public T call() throws Exception {
    //스레드가 처리할 작업 내용
    return T;
    }
}
call() 의 리턴 타입은 Callable <T>에서 지정한 T 타입 파라미터와 동일한 타입이어야 한다.(제네릭 개념)

 

 

작업 처리 요청

작업 처리 요청이란 ExcutorService의 작업 큐에 Runnable 또는 Callable 객체를 넣는 행위를 말한다.

작업 처리 요청을 위해 ExecutorService는 아래의 두 가지 메소드를 제공한다.

리턴 타입 메소드명(매개변수) 설명
void execute(Runnable command) -Runnable을 작업 큐에 저장
-작업 처리 결과를 리턴하지 않음
Future<T> submit(Callable<T> task) -Callable을 작업 큐에 저장
-작업 처리 결과를 얻을 수 있도록 Future를 리턴
  1. Runnable 또는 Callable 객체가 ExecutorService의 작업 큐에 들어간다.
  2. ExecutorService는 들어온 객체를 처리할 스레드가 있는지 보고, 없다면 스레드를 새로 생성시킨다.
  3. 스레드는 작업 큐에서 Runnable 또는 Callable 객체를 꺼내와 run() 또는 call() 메소드를 실행하면서 작업을 처리한다.
    즉, 작업 처리 요청을 한 뒤, 작업을 생성하고 작업을 처리한다.

 

아래의 예제에 대한 설명

  • 이메일을 보내는 작업으로, 1000개의 Runnable을 생성한 다음 execute() 메소드로 작업 큐에 넣는다.
  • ExcutorService는 최대 5개 스레드로 작업 큐에서 Runnable을 하나씩 꺼낸다.
  • 꺼낸 Runnable 객체의 run() 메소드를 실행하면서 작업을 처리한다.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class RunnableExecuteExample {

	public static void main(String[] args) {
		//1000개의 메일 생성
		String[][] mails = new String[1000][3];
		for(int i=0; i<mails.length; i++) {
			mails[i][0] = "admin@my.com";
			mails[i][1] = "member"+i+"@my.com";
			mails[i][2] = "신상품 입고";
		}
		
		//ExecutorService 생성
		ExecutorService executorService = Executors.newFixedThreadPool(5);
		
		//이메일을 보내는 작업 생성 및 처리 요청
		for(int i=0; i<1000; i++) {
			final int idx = i;
			executorService.execute(new Runnable() {
				@Override
				public void run() {
					Thread thread = Thread.currentThread();
					String from = mails[idx][0];
					String to = mails[idx][1];
					String content = mails[idx][2];
					System.out.println("[" + thread.getName() + "] " + from + " ==> " + to + ": " + content);
				}
			});
		}
			
		//ExecutorService 종료
		executorService.shutdown();
	}
}
/*
....
....
....
[pool-1-thread-4] admin@my.com ==> member987@my.com: 신상품 입고
[pool-1-thread-4] admin@my.com ==> member988@my.com: 신상품 입고
[pool-1-thread-4] admin@my.com ==> member989@my.com: 신상품 입고
[pool-1-thread-5] admin@my.com ==> member974@my.com: 신상품 입고
[pool-1-thread-5] admin@my.com ==> member991@my.com: 신상품 입고
[pool-1-thread-4] admin@my.com ==> member990@my.com: 신상품 입고
[pool-1-thread-4] admin@my.com ==> member993@my.com: 신상품 입고
[pool-1-thread-3] admin@my.com ==> member984@my.com: 신상품 입고
[pool-1-thread-3] admin@my.com ==> member995@my.com: 신상품 입고
[pool-1-thread-3] admin@my.com ==> member996@my.com: 신상품 입고
[pool-1-thread-2] admin@my.com ==> member983@my.com: 신상품 입고
[pool-1-thread-2] admin@my.com ==> member998@my.com: 신상품 입고
[pool-1-thread-1] admin@my.com ==> member981@my.com: 신상품 입고
[pool-1-thread-2] admin@my.com ==> member999@my.com: 신상품 입고
[pool-1-thread-3] admin@my.com ==> member997@my.com: 신상품 입고
[pool-1-thread-4] admin@my.com ==> member994@my.com: 신상품 입고
[pool-1-thread-5] admin@my.com ==> member992@my.com: 신상품 입고
*/
//이메일을 보내는 작업 생성 및 처리 요청에서의 final int
executorService.execute에 Runnable 인터페이스의 구현 객체를 대입해야 한다.
위 예제에서는 익명 구현 객체를 대입한다.
따라서 로컬 변수를 로컬 클래스에서 사용할 때 제한이 있다.

 

아래의 예제에 대한 설명

  1. 자연수 덧셈을 하는 작업으로 100개의 Callable을 생성하고 submit() 메소드로 작업 큐에 넣는다.
  2. ExecutorService는 최대 5개 스레드로 작업 큐에서 Callable 객체를 하나씩 꺼낸다.
  3. 꺼낸 객체의 call() 메소드를 실행하면서 작업을 처리한다.
  4. Future의 get() 메소드는 작업이 끝날 때까지 기다렸다가 call() 메소드가 리턴한 값을 리턴한다.
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
	
public class CallableSubmitExample {
	public static void main(String[] args) {
		//ExecutorService 생성
		ExecutorService executorService = Executors.newFixedThreadPool(5);

		//계산 작업 생성 및 처리 요청
		for(int i=1; i<=100; i++) {
			final int idx = i;
			Future<Integer> future = executorService.submit(new Callable<Integer>() {
				@Override
				public Integer call() throws Exception {
					int sum = 0;
					for(int i=1; i<=idx; i++) {
						sum += i;
					}
					Thread thread = Thread.currentThread();
					System.out.println("[" + thread.getName() + "] 1~" + idx + " 합 계산");
					return sum;
				}
			});

			try {
				int result = future.get();
				System.out.println("\t리턴값: " + result);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		
		//ExecutorService 종료
		executorService.shutdown();
	}
}
/*
...
...
...
[pool-1-thread-2] 1~92 합 계산
	리턴값: 4278
[pool-1-thread-3] 1~93 합 계산
	리턴값: 4371
[pool-1-thread-4] 1~94 합 계산
	리턴값: 4465
[pool-1-thread-5] 1~95 합 계산
	리턴값: 4560
[pool-1-thread-1] 1~96 합 계산
	리턴값: 4656
[pool-1-thread-2] 1~97 합 계산
	리턴값: 4753
[pool-1-thread-3] 1~98 합 계산
	리턴값: 4851
[pool-1-thread-4] 1~99 합 계산
	리턴값: 4950
[pool-1-thread-5] 1~100 합 계산
	리턴값: 5050
*/

이 게시글은 이것이 자바다(저자 : 신용권, 임경균)의 책과 동영상 강의를 참고하여 개인적으로 정리하는 글임을 알립니다. 


2023.06.27 - [컴퓨터 구조 & 운영체제/운영체제] - [운영체제] 스레드(Thread), 멀티 프로세스와 멀티 스레드

 

[운영체제] 스레드(Thread), 멀티 프로세스와 멀티 스레드

이 글은 혼자 공부하는 컴퓨터 구조 + 운영체제 (저자 : 강민철)의 책과 유튜브 영상을 참고하여 개인적으로 정리하는 글임을 알립니다. 스레드 이 글에서 다루는 내용은 소프트웨어적 스레드이

rebugs.tistory.com

 

 

  • 하나의 프로세스가 두 가지 이상의 작업을 처리할 수 있는 이유는 멀티 스레드가 있기 때문이다.
  • 프로세스의 모든 스레드가 종료가 되어야 프로세스가 종료된다.
  • 하나의 스레드에서 예외가 발생하면 모든 스레드가 종료되어 결국 프로세스도 종료되므로 예외 처리에 만전을 기해야 한다.
메인 스레드
모든 자바 프로그램은 메인 스레드가 main() 메소드를 실행하면서 시작된다.
메인 스레드는 main() 메소드의 첫 코드부터 순차적으로 실행하고, main() 메소드의 마지막 코드를 실행하거나 return문을 만나면 실행을 종료한다.

메인 스레드는 필요에 따라 추가 작업 스레드들을 만들어서 실행시킬 수 있다.
멀티 스레드에서는 실행 중인 스레드가 하나라도 있다면 프로세스는 종료되지 않는다.
즉, 메인 스레드가 작업 스레드보다 먼저 종료되더라도 작업 스레드가 하나라도 실행 중이라면 프로세스는 종료되지 않는다.
한 프로세스에 하나의 JVM이 실행된다.
여러 자바 프로세스가 실행중이라면 여러 JVM이 있는 것이다.

 

스레드 생성

중요한 점은 아래의 방법 중에 어떤 방법을 사용해서 생성을 하든 상관이 없지만, 작업 스레드를 실행하려면 스레드 객체의 start() 메소드를 호출해야 한다.

 

Runnable 구현 객체를 만들어서 생성

Runnable 구현 클래스 작성

작업 스레드 객체를 직접 생성하려면, java.lang 패키지에 있는 Thread 클래스로부터 작업 스레드 객체를 생성하면 된다.

객체를 생성할 때에는 Runnable 구현 객체를 매개값으로 갖는 생성자를 호출해야 한다.

Runnable 인터페이스
스레드가 작업을 실행할 때 사용하는 인터페이스이다.
Runnable에는 run() 메소드가 정의되어 있는데, 구현 클래스는 run()을 오버라이딩해서 스레드가 실행할 코드를 가지고 있어야 한다.

Runnable 구현 클래스는 작업 내용을 정의한 것이므로, 스레드에게 전달해야 한다.

Runnable 구현 객체를 생성한 후 Thread 생성자 매개값으로 Runnable 객체를 전달하면 된다.

 

Task.java

public class Task implements Runnable{
	@Override
	public void run() {
		//실행할 코드 작성
	}
}

 

Main.java

public class Main {
	public static void main(String[] args) {
		Runnable task = new Task();
		Task task2 = new Task();
		
		Thread thread = new Thread(task);
		Thread thread2 = new Thread(task2);
		
		thread.start();
		thread2.start();
	}
}

 

Runnable 익명 구현 객체로 생성

아래와 같이 익명 구현 객체를 생성하여 매개값으로 사용할 수도 있다.

public class Main {
	public static void main(String[] args) {
		Thread thread = new Thread(new Runnable() { //익명 구현 객체
			@Override
			public void run() {
				//실행할 코드 작성
			}
		});
		
		thread.start();
	}
}

 

Thread 자식 클래스로 생성

명시적인 자식 클래스 정의 후 생성

작업 스레드 객체를 생성하는 또 다른 방법은 Thread의 자식 객체로 만드는 것이다.

Thread 클래스를 상속한 다음 run() 메소드를 오버라이딩해서 스레드가 실행할 코드를 작성하고 객체를 생성하면 된다.

 

Task.java

public class Task extends Thread{
	@Override
	public void run() {
		//실행할 코드 작성
	}
}

 

Main.java

public class Main {
	public static void main(String[] args) {
		//방법1
		Thread thread = new Thread(new Task());
		thread.start();
		
		//방법2
		Task task = new Task();
		Thread thread2 = new Thread(task);
		thread2.start();
		
		//방법3
		Task task2 = new Task();
		task2.start();
	}
}

 

 

Thread 익명 자식 객체로 생성

명시적인 자식 클래스를 정의하지 않고, 아래와 같이 Thread 익명 자식 객체를 사용할 수도 있다.

public class Main {
	public static void main(String[] args) {
		Thread thread = new Thread() {
			@Override
			public void run() {
				//실행할 코드 작성
			}
		};
		thread.start();
	}
}

 


 

스레드 이름

  • 메인 스레드는 main이라는 이름을 가진다.
  • 작업 스레드는 자동적으로 Thread-n이라는 이름을 가진다.
  • 다른 이름으로 설정하고 싶다면 Thread 클래스의 setName() 메소드를 사용하면 된다.
  • 당연히 스레드를 실행하기 전에 스레드 이름을 바꿔야 한다.
thread.setName("스레드 이름");

 

어떤 스레드가 실행하고 있는지 확인하려면 정적 메소드인 CurrentThread()로 스레드 객체의 참조를 얻은 다음 getName() 메소드로 스레드의 이름을 얻고 출력해 보면 된다.

Thread thread = Thread.currentThread();
System.out.println(thread.getName());

 

 

ThreadNameExample.java

public class ThreadNameExample {
	public static void main(String[] args) {
		Thread mainThread = Thread.currentThread();
		System.out.println(mainThread.getName() + " 실행");

		for(int i=0; i<3; i++) {
			Thread threadA = new Thread() {
				@Override
				public void run() {
					System.out.println(getName() + " 실행");
				}
			};
			threadA.start();
		}

		Thread chatThread = new Thread() {
			@Override
			public void run() {
				System.out.println(getName() + " 실행");
			}
		};
		chatThread.setName("chat-thread");
		chatThread.start();
	}
}
/*
main 실행
Thread-0 실행
Thread-1 실행
Thread-2 실행
chat-thread 실행
*/

 

위 예제에서 ThreadA.start()를 시작한 순서대로 출력이 되지 않을 수 있다.

Thread-0 실행 -> Thread-1 실행 -> Thread-2 실행 -> chat-thread 실행

이 순서가 아니라 무작위 순서가 될 수 있다는 것이다.

이유는, 바로 스레드가 시작되는 것이 아니라 실행 대기 상태로 있다가 CPU 스케줄링에 의해서 스케줄링 큐에 있는 순서대로 시작되기 때문이다.

2023.06.30 - [컴퓨터 구조 & 운영체제/운영체제] - [운영체제] CPU 스케줄링의 개념

 

[운영체제] CPU 스케줄링의 개념

이 글은 혼자 공부하는 컴퓨터 구조 + 운영체제 (저자 : 강민철)의 책과 유튜브 영상을 참고하여 개인적으로 정리하는 글임을 알립니다. 모든 프로세스들은 CPU를 필요로 하고, 모든 프로세스들은

rebugs.tistory.com

 


 

스레드 상태

  • 스레드 객체를 생성(NEW)하고, start() 메소드를 호출하면 곧바로 스레드가 실행되는 것이 아니라 실행 대기 상태(RUNNABLE)가 된다.
  • 실행 대기하는 스레드는 CPU 스케쥴링에 따라 CPU를 점유하고 run() 메소드를 실행한다. 이때를 실행(RUNNING) 상태라고 한다.
  • 실행 스레드는 run() 메소드를 모두 실행하기 전에 스케줄링에 의해 다시 실행 대기 상태로 돌아갈 수 있다. 그리고 다른 스레드가 실행 상태가 된다.
  • 이렇게 스레드는 실행 대기 상태와 실행 상태를 번갈아 가면서 자신의 run() 메소드를 조금씩 실행한다.
  • 실행 상태에서 run() 메소드가 종료되면 더 이상 실행할 코드가 없기 때문에 스레드의 실행은 멈추게 된다. 이 상태를 종료 상태(TERMINATED)라고 한다.

출처 : 이것은 자바다 유튜브 동영상 강의

위 개념은 운영체제 CPU 스케줄링의 개념이다.

 

아래는 일시 정지로 가기 위한 메소드와 다시 실행 대기 상태로 가기 위한 메소드를 보여준다.

구분 메소드 설명
일시 정지로 보냄 sleep(long millis) 주어진 시간 동안 스레드를 일시 정지 상태로 만든다. 주어진 시간이 지나면 자동적으로 실행 대기 상태가 된다.
join() join() 메소드를 호출한 스레드는 일시 정지 상태가 된다. 실행 대기 상태가 되려면, join() 메소드를 가진 스레드가 종료되어야 한다.
wait() 동기화 블록 내에서 스레드를 일시 정지 상태로 만든다
일시 정지에서 벗어남 interrupt() 일시 정지 상태일 경우, interruptedException을 발생시켜 실행 대기 상태 또는 종료 상태로 만든다.
notify()
notifyAll()
wait() 메소드로 인해 일시 정지 상태인 스레드를 실행 대기 상태로 만든다.
실행 대기로 보냄 yield() 실행 상태에서 다른 스레드에게 실행을 양보하고 실행 대기 상태가 된다.

위 표에서 wait()과 notify(), notifyAll()은 Object 클래스의 메소드이고 그 외는 Thread 클래스의 메소드이다.

Object 클래스의 메소드들은 스레드 동기화에서 다룬다.

 

주어진 시간 동안 일시 정지

실행 중인 스레드를 일정 시간 멈추게 하고 싶다면 Thread 클래스의 정적 메소드인 sleep()을 이용하면 된다.

매개값에는 얼마 동안 일시 정지 상태로 있을 것인지 밀리세컨드(1/1000초) 단위로 시간을 주면 된다.

일시 정지 상태에서는 InterruptedException이 발생할 수 있기 때문에 sleep() 메소드는 예외 처리가 필요하다.

try {
        Thread.sleep(3000); //3초간 일시 정지
    } catch(InterruptedException e) {}

 

 

다른 스레드의 종료를 기다림

스레드는 다른 스레드와 독립적으로 실행하지만 다른 스레드가 종료될 때까지 기다렸다가 실행을 해야 하는 경우도 있다.

이를 위해 스레드는 join() 메소드를 제공한다.

  • 아래의 그림에서 ThreadA가 ThreadB의 join() 메소드를 호출하면 ThreadA는 ThreadB가 종료할 때까지 일시 정지 상태가 된다.
  • ThreadB의 run() 메소드가 종료되고 나서야 비로소 ThreadA는 일시 정지에서 풀려 다음 코드를 실행한다.
  • 일시 정지 상태에서는 InterruptedException이 발생할 수 있기 때문에 join() 메소드는 예외 처리가 필요하다.

 

SumThread.java

public class SumThread extends Thread {
	private long sum;
	
	public long getSum() {
		return sum;
	}

	public void setSum(long sum) {
		this.sum = sum;
	}

	@Override
	public void run() {
		for(int i=1; i<=100; i++) {
			sum+=i;
		}
	}
}

 

JoinExample.java

public class JoinExample {
	public static void main(String[] args) {
		SumThread sumThread = new SumThread();
		sumThread.start();
		try {
			sumThread.join();
		} catch (InterruptedException e) {
		}
		System.out.println("1~100 합: " + sumThread.getSum());
	}
}

 

join() 메소드를 호출하지 않으면 1~100의 합인 5050이 아니라 더 작은 값이 나올 수도 있다.

 

다른 스레드에게 실행 양보

스레드가 원하는 상태가 아니라면 다른 스레드에게 실행을 양보하고 자신은 실행 대기 상태로 가게 할 수도 있다.

이런 기능을 위해 Thread는 yield() 메소드를 제공한다.

yield()를 호출한 스레드는 실행 대기 상태로 돌아가고, 다른 스레드가 실행 상태가 된다.

 

  • 아래 예제에서는 처음 5초 동안은 ThreadA의 work 필드의 값이 false이기 때문에 실행 대기상태가 된다.
  • 5초 이후에는 work 필드의 값이 true로 바뀌어서 출력문이 계속 실행된다.
  • 다시 5초 이후에는 work 필드의 값이 false로 바뀌어서 다시 실행 대기상태가 된다.

 

WorkThread.java

public class WorkThread extends Thread {
	//필드
	public boolean work = false;
	
	//생성자
	public WorkThread(String name) {
		setName(name);
	}
	
	//메소드
	@Override
	public void run() {
		while(true) {
			if(work) {
				System.out.println(getName() + ": 작업처리");
			} else {
				Thread.yield();
			}
		}
	}
}

 

YieldExample.java

public class YieldExample {
	public static void main(String[] args) {
		WorkThread workThreadA = new WorkThread("workThreadA");
		workThreadA.start();
		
		try { Thread.sleep(5000); } catch (InterruptedException e) {}
		workThreadA.work = true;

		try { Thread.sleep(10000); } catch (InterruptedException e) {}
		workThreadA.work = false;
	}
}

 


 

스레드 동기화

동기화 메소드와 블록

스레드 A가 공유 자원(객체)을 사용 중인데 다른 스레드인 스레드 B가 접근하려고 하면 스레드 A가 사용 중인 객체를 다른 스레드가 변경할 수 없도록 하려면 스레드 작업이 끝날 때까지 객체에 잠금을 걸면 된다.

이를 위해 자바는 동기화 메소드와 블록을 제공한다.

  • 공유 객체의 동기화 메소드는 하나의 스레드만 사용할 수 있다.
  • 공유 객체의 동기화 블록은 하나의 스레드만 사용할 수 있다.
  • 공유 객체의 일반 메소드는 모든 스레드가 사용할 수 있다.

출처 : 이것은 자바다 유튜브 동영상 강의

 

동기화 메소드는 다음과 같이 synchronized 키워드를 붙이면 된다.

synchronized 키워드는 인스턴스와 정적 메소드 어디든 붙일 수 있다.

public synchronized void method(){
	//단 하나의 스레드만 실행하는 영역
}

스레드가 동기화 메소드를 실행하는 즉시 객체는 잠금이 일어나고, 메소드 실행이 끝나면 잠금이 풀린다.

 

메소드 전체가 아닌 일부 영역을 실행할 때만 객체 잠금을 걸고 싶다면 다음과 같이 동기화 블록을 만들면 된다.

public void method(){
	//여러 스레드가 실행할 수 있는 영역
    synchronized(공유 객체){
    	//단 하나의 스레드만 실행하는 영역
    }
    //여러 스레드가 실행할 수 있는 영역
}

 

  • 아래의 예제에서 공유 객체는 Calculator이다.
  • Calculator의 setMemory1()는 동기화 메소드이다.
  • Calculator의 setMemory2()는 동기화 블록을 포함하는 메소드이다.
  • setMemory1()과 setMemory2()는 하나의 스레드만 실행이 가능한 메소드가 된다.
  • 두 메소드는 동일하게 매개값을 memory 필드에 값을 저장을 하고 2초간 일시 정지 후 memory 필드의 값을 출력한다.
  • 두 개의 스레드가 공유 객체의 memory 값을 바꾼다.

 

Calculator.java

public class Calculator {
	private int memory;

	public int getMemory() {
		return memory;
	}

	public synchronized void setMemory1(int memory) {
		this.memory = memory;
		try {
			Thread.sleep(2000);
		} catch(InterruptedException e) {}
		System.out.println(Thread.currentThread().getName() + ": " + this.memory);
	}

	public void setMemory2(int memory) {
		synchronized(this) {
			this.memory = memory;
			try {
				Thread.sleep(2000);
			} catch(InterruptedException e) {}
			System.out.println(Thread.currentThread().getName() + ": " + this.memory);
		}
	}
}

 

User1Thread.java

public class User1Thread extends Thread {
	private Calculator calculator;

	public User1Thread() {
		setName("User1Thread");
	}

	public void setCalculator(Calculator calculator) {
		this.calculator = calculator;
	}

	@Override
	public void run() {
		calculator.setMemory1(100);
	}
}

 

User2Thread.java

public class User2Thread extends Thread {
	private Calculator calculator;
	
	public User2Thread() {
		setName("User2Thread");
	}

	public void setCalculator(Calculator calculator) {
		this.calculator = calculator;
	}

	@Override
	public void run() {
		calculator.setMemory2(50);
	}
}

 

SynchronizedExample.java

public class SynchronizedExample {
	public static void main(String[] args) {
		Calculator calculator = new Calculator();

		User1Thread user1Thread = new User1Thread();
		user1Thread.setCalculator(calculator);
		user1Thread.start();

		User2Thread user2Thread = new User2Thread();
		user2Thread.setCalculator(calculator);
		user2Thread.start();
	}
}
/*
User1Thread: 100
User2Thread: 50
*/

 

 

wait()과 notify()를 이용한 스레드 제어

경우에 따라서는 두 개의 스레드를 교대로 번갈아 가며 실행할 때도 있다.

정확한 교대 작업이 필요할 경우, 자신의 작업이 끝나면 상대방 스레드를 일시 정지 상태에서 풀어주고 자신은 일시 정지 상태로 만들면 된다.

 

이 방법의 핵심은 공유 객체에 있다.

  • 공유 객체는 두 스레드가 작업할 내용을 각각 동기화 메소드로 정해 놓는다.
  • 한 스레드가 작업을 완료하면 notify() 메소드를 호출해서 일시 정지 상태에 있는 다른 스레드를 실행 대기 상태로 만든다.
  • 자신은 두 번 작업을 하지 않도록 wait() 메소드를 호출하여 일시 정지 상태로 만든다.

출처 : 이것은 자바다 유튜브 동영상 강의

  • notify()는 wait() 에 의해 일시 정지된 스레드 중 한 개를 실행 대기 상태로 만들고, notifyAll()은 wait()에 의해 일시 정지된 모든 스레드를 실행 대기 상태로 만든다.
  • 주의할 점은 이 두 메소드는 동기화 메소드 또는 동기화 블록 내에서만 사용할 수 있다는 것이다.

 

아래의 예제는 공유 객체인 WorkObject에 동기화 메소드인 ThreadA와 ThreadB가 각 methodA와 methodB를 호출한다.

 

WorkObject.java

public class WorkObject {
	public synchronized void methodA() {
		Thread thread = Thread.currentThread();
		System.out.println(thread.getName() + ": methodA 작업 실행");
		notify();
		try {
			wait();
		} catch (InterruptedException e) {
		}
	}

	public synchronized void methodB() {
		Thread thread = Thread.currentThread();
		System.out.println(thread.getName() + ": methodB 작업 실행");
		notify();
		try {
			wait();
		} catch (InterruptedException e) {
		}
	}
}

 

ThreadA.java

public class ThreadA extends Thread {
	private WorkObject workObject;

	public ThreadA(WorkObject workObject) {
		setName("ThreadA");
		this.workObject = workObject;
	}

	@Override
	public void run() {
		for(int i=0; i<10; i++) {
			workObject.methodA();
		}
	}
}

 

ThreadB.java

public class ThreadB extends Thread {
	private WorkObject workObject;

	public ThreadB(WorkObject workObject) {
		setName("ThreadB");
		this.workObject = workObject;
	}

	@Override
	public void run() {
		for(int i=0; i<10; i++) {
			workObject.methodB();
		}
	}
}

 

WaitNotifyExample.java

public class WaitNotifyExample {
	public static void main(String[] args) {
		WorkObject workObject = new WorkObject();

		ThreadA threadA = new ThreadA(workObject);
		ThreadB threadB = new ThreadB(workObject);

		threadA.start();
		threadB.start();
	}
}
/*
ThreadA: methodA 작업 실행
ThreadB: methodB 작업 실행
ThreadA: methodA 작업 실행
ThreadB: methodB 작업 실행
ThreadA: methodA 작업 실행
ThreadB: methodB 작업 실행
ThreadA: methodA 작업 실행
ThreadB: methodB 작업 실행
ThreadA: methodA 작업 실행
ThreadB: methodB 작업 실행
ThreadA: methodA 작업 실행
ThreadB: methodB 작업 실행
ThreadA: methodA 작업 실행
ThreadB: methodB 작업 실행
ThreadA: methodA 작업 실행
ThreadB: methodB 작업 실행
ThreadA: methodA 작업 실행
ThreadB: methodB 작업 실행
ThreadA: methodA 작업 실행
ThreadB: methodB 작업 실행
*/

 


 

스레드 안전 종료

스레드는 자신의 run() 메소드가 모두 실행되면 자동적으로 종료되지만, 경우에 따라서는 실행 중인 스레드를 즉시 종료할 필요가 있다.

스레드를 강제 종료시키기 위해 Thread는 stop() 메소드를 제공하고 있으나 이 메소드는 사용하지 않는 것을 추천한다.

스레드를 갑자기 종료하게 되면 사용 중이던 리소스들이 불안전한 상태로 남겨지기 때문이다.

스레드를 안전하게 종료하는 방법은 리소스들을 정리하고 run() 메소드를 빨리 종료하는 것이다.

 

이를 위해 아래의 두 가지 방법을 사용한다.

  • 조건 이용
  • interrupt() 메소드 이용

 

조건 이용

  • 스레드가 while 문으로 반복 실행할 경우, 조건을 이용해서 run() 메소드의 종료를 유도한다.
public class XXXThread extends Thread {
	private boolean stop;

	public void setStop(boolean stop) {
		this.stop = stop;
	}

	@Override
	public void run() {
		while(!stop) {
			System.out.println("실행 중");
		}
		System.out.println("리소스 정리");
		System.out.println("실행 종료");
	}
}

 

 

interrupt() 메소드 이용

일시 정지를 이용한 방법

interrupt() 메소드는 스레드가 일시 정지 상태에 있을 때 InterruptedException 예외를 발생시키는 역할을 한다.

이것을 이용하면 예외 처리를 통해 run() 메소드를 정상 종료시킬 수 있다.

public void run() {
		try {
			while(true) {
				//...
				Thread.sleep(1); //일시 정지
			}
		} catch(InterruptedException e) {
		}
		//스레드가 사용한 리소스 정리
	}

즉, 이 방법은 스레드가 일시 정지 상태에 있을 때만 가능하다.

일시 정지 상태일 때 처리하는 InterruptedException 예외 처리를 이용한 방법이기 때문이다.

 

PrintThread.java

public class PrintThread extends Thread {
	public void run() {
		try {
			while(true) {
				System.out.println("실행 중");
				Thread.sleep(1);
			}
		} catch(InterruptedException e) {
		}
		System.out.println("리소스 정리");
		System.out.println("실행 종료");
	}
}

 

InterruptExample.java

public class InterruptExample {
	public static void main(String[] args) {
		Thread thread = new PrintThread();
		thread.start();

		try {
			Thread.sleep(100); //메인 스레드를 일시정지. 작업 스레드를 멈추는게 아님
		} catch (InterruptedException e) {
		}

		thread.interrupt(); //작업 스레드 인터럽트
	}
}
/*
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
리소스 정리
실행 종료
*/

 

 

일시 정지를 이용하지 않는 방법

Thread의 interrupted()와 isInterrupt() 메소드는 interrupt() 메소드 호출 여부를 리턴한다.

  • interrupted() : 정적 메소드 -> 객체 생성 없이 사용 가능
  • isInterrupt() : 인스턴스 메소드 -> 객체를 생성해야 사용 가능

이 방법은 위 메소드를 이용하여 interrupt() 메소드 호출 여부를 확인하고 호출되었다면,  while 블록을 빠져나가 리소스 정리를 하고 스레드를 종료하게끔 유도한다.

 

PrintThread.java

public class PrintThread extends Thread{
	public void run() {
		while(true){
			System.out.println("실행 중");
			if(Thread.interrupted()) { //interrupt() 메소드가 호출되었는지 확인
				break; //interrupt() 메소드가 호출되었다면 while문 탈출
			}
		}
		System.out.println("리소스 정리");
		System.out.println("실행 종료");
	}
}

 

Main.java

public class Main {
	public static void main(String[] args) {
		Thread thread = new PrintThread();
		thread.start();
		try {
			Thread.sleep(1); //메인 스레드를 일시정지, 작업 스레드를 일시 정지 하는것이 아님
		}catch (InterruptedException e) {
		}
		thread.interrupt(); //작업 스레드 인터럽트
	}
}
/*
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
리소스 정리
실행 종료
*/

이 게시글은 이것이 자바다(저자 : 신용권, 임경균)의 책과 동영상 강의를 참고하여 개인적으로 정리하는 글임을 알립니다. 


제네릭의 개념

제네릭이란 결정되지 않은 타입을 파라미터로 처리하고 실제 사용할 때 파라미터를 구체적인 타입으로 대체시기는 기능이다.

 

아래의 코드는 Box 클래스에서 결정되지 않은 content의 타입을 T라는 타입 파라미터로 정의한 것이다.

public class Box <T>{
	public T content;
}

<T>는 T가 타입 파라미터임을 뜻하는 기호로, 타입이 필요한 자리에 T를 사용할 수 있음을 알려주는 역할을 한다.

Box 클래스는 T가 무엇인지 모르지만, Box 객체가 생성될 시점에 다른 타입으로 대체된다는 것을 알고 있다.

 

Box의 내용물로 String 타입을 저장하고 싶으면 아래와 같이 코드를 작성하면 된다.

Box<String> box = new Box<String>();
box.content = "안녕하세요";
String con = box.content;

 

Box의 내용물로 int 타입을 저장하고 싶으면 아래와 같이 코드를 작성하면 된다.

Box<Integer> box = new Box<Integer>();
box.content = 100; //자동 박싱
int con = box.content;

<T>에서 타입 파라미터로 쓰이는 T는 A부터 Z까지 어떤 알파벳을 써도 상관이 없다.

또한 주의할 점은 타입 파라미터를 대체하는 타입은 클래스 및 인터페이스이다.

 

아래와 같이 변수를 선언할 때와 동일한 타입으로 호출하고 싶다면 생성자 호출시 생성자에는 타입명을 명시하지 않아도 된다.

Box<Integer> box = new Box<>();

 


 

제네릭 타입

public class 클래스명<A, B, ...>{}
public interface 인터페이스명<A, B, ...>{}
  • 제네릭 타입 : 결정되지 않은 타입을 파라미터로 가지는 클래스와 인터페이스
  • 선언부에 <> 부호가 붙고 그 사이에 타입 파라미터들이 위치
  • 타입 파라미터 : 일반적으로 대문자 알파벳 한 글자로 표현
  • 외부에서 제네릭 타입을 사용하려면 타입 파라미터에 구체적인 타입을 지정해야 함
  • 외부에서 제네릭 타입을 지정하지 않으면 암묵적으로 Object 객체가 사용된다.
타입 파라미터
타입 파라미터는 기본적으로 Object 타입으로 간주되므로 Object가 가지고 있는 메소드를 호출할 수 있다.
예를 들어, 타입 파라미터는 Object의 equals() 메소드를 호출할 수 있고, 타입 파라미터로 대입된 객체가 equals() 메소드가 재정의 되어 있다면 재정의된 equals() 메소드가 호출된다.

 

제네릭 타입 클래스

Product.java

//제네릭 타입
public class Product<K, M> {
	//필드
	private K kind;
	private M model;
	
	//메소드
	public K getKind() { return this.kind; }
	public M getModel() { return this.model; }
	public void setKind(K kind) { this.kind = kind; }
	public void setModel(M model) { this.model = model; }
}

 

GenericExample.java

 

public class GenericExample {
	public static void main(String[] args) {
		//K는 Tv로 대체, M은 String으로 대체
		Product<Tv, String> product1 = new Product<>();
		
		//Setter 매개값은 반드시 Tv와 String을 제공
		product1.setKind(new Tv());
		product1.setModel("스마트Tv");
		
		//Getter 리턴값은 Tv와 String이 됨
		Tv tv = product1.getKind();
		String tvModel = product1.getModel();
		//------------------------------------------------------------------------
		//K는 Car로 대체, M은 String으로 대체
		Product<Car, String> product2 = new Product<>();
			
		//Setter 매개값은 반드시 Car와 String을 제공
		product2.setKind(new Car());
		product2.setModel("SUV자동차");
		
		//Getter 리턴값은 Car와 String이 됨
		Car car = product2.getKind();
		String carModel = product2.getModel();
	}
}

 

제네릭 타입 인터페이스

 

제네릭 타입 인터페이스 선언

public interface Rentable<P> {
	P rent();
}

public class Home {
	public void turnOnLight() {
		System.out.println("전등을 켭니다.");
	}
}
public class Car {
	public void run() {
		System.out.println("자동차가 달립니다.");
	}
}

아래의 HomeAgency와 CarAgency는 Rentable의 타입 파라미터를 Home과 Car로 대체해서 구현하는 방법을 보여준다.

public class HomeAgency implements Rentable<Home> {
	@Override
	public Home rent() {
		return new Home();
	}
}
public class CarAgency implements Rentable<Car>{
	@Override
	public Car rent() {
		return new Car();
	}
}

public class GenericExample {
	public static void main(String[] args) {
		HomeAgency homeAgency = new HomeAgency();
		Home home = homeAgency.rent();
		home.turnOnLight();

		CarAgency carAgency = new CarAgency();
		Car car = carAgency.rent();
		car.run();
	}
}
/*
전등을 켭니다.
자동차가 달립니다.
*/

위에 예제의 실행 내용과 같이

HomeAgency와 CarAgency를 생성하면 각 Home 객체와 Car 객체가 리턴되고, 각 메소드를 호출할 수 있다.

 


 

제네릭 메소드

  • 제네릭 메소드 : 타입 파라미터를 가지고 있는 메소드
  • 타입 파라미터가 메소드 선언부에 정의
  • 타입 파라미터, 리턴타입, 메소드명(매개변수) 순으로 선언한다
  • 타입 파라미터는 메소드로 들어온 매개값이 어떤 타입이냐에 따라 컴파일 과정에서 구체적인 타입으로 대체된다.

아래의 boxing() 메소드는 타입 파라미터로 <T>를 정의하고 매개 변수 타입과 리턴 타입에서 T를 사용한다.

정확한 리턴 타입은 T를 내용물로 갖는 Box 객체이다.

 

public <T> Box<T> boxing(T t){...}​

 

Box.java

public class Box<T> {
	//필드
	private T t;
	
	//Getter 메소드
	public T get() {
		return t;
	}
	
	//Setter 메소드
	public void set(T t) {
		this.t = t;
	}
}

 

GenericExample.java

public class GenericExample {
	//제네릭 메소드
	public static <T> Box<T> boxing(T t) {
		Box<T> box = new Box<T>();
		box.set(t);
		return box;
	}

	public static void main(String[] args) {
		//제네릭 메소드 호출
		Box<Integer> box1 = boxing(100);
		int intValue = box1.get();
		System.out.println(intValue);

		//제네릭 메소드 호출
		Box<String> box2 = boxing("홍길동");
		String strValue = box2.get();
		System.out.println(strValue);
	}
}
/*
100
홍길동
*/

 


 

제한된 타입 파라미터

  • 제한된 타입 파라미터 : 모든 타입으로 대체할 수 없고, 특정 타입과 자식 또는 구현 관계에 있는 타입만 대체할 수 있는 타입 파라미터
  • 예를 들어 숫자를 연산하는 제네릭 메소드는 대체 타입으로 Number 또는 자식 클래스(Byte, Short, Integer, Long, Double)로 제한
  • 상위 타입은 클래스뿐만 아니라 인터페이스도 가능(인터페이스라고 해서 implements 를 사용하지 않음)
public class GenericExample {
	//제한된 타입 파라미터를 갖는 제네릭 메소드
	public static <T extends Number> boolean compare(T t1, T t2) {
		//T의 타입을 출력
		System.out.println("compare(" + t1.getClass().getSimpleName() + ", " +
				t2.getClass().getSimpleName() + ")"); //리플렉션

		//Number의 메소드 사용
		double v1 = t1.doubleValue(); //Number의 메소드 사용
		double v2 = t2.doubleValue(); //Number의 메소드 사용

		return (v1 == v2);
	}

	public static void main(String[] args) {
		//제네릭 메소드 호출
		boolean result1 = compare(10, 20);
		System.out.println(result1);
		System.out.println();

		//제네릭 메소드 호출
		boolean result2 = compare(4.5, 4.5);
		System.out.println(result2);
	}
}
/*
compare(Integer, Integer)
false

compare(Double, Double)
true
*/

 


 

와일드카드 타입 파라미터

출처 : 이것이 자바다 유튜브 영상

위와 같은 상속 관계가 있다고 가정해 보자.

 

Student와 자식 클래스인 HighStudent, middleStudent만 타입 파라미터의 대체 타입으로 가능하도록 하고 싶다면 아래와 같이 선언하면 된다.

리턴타입 메소드명(제네릭타입<? extends Student> 변수) {}

 

반대로 Worker와 부모 클래스인 Person만 가능하도록 매개변수를 아래와 같이 선언할 수 있다.

리턴타입 메소드명(제네릭타입<? super Worker> 변수) {}

 

 아래와 같이 어떤 타입이든 가능하도록 매개변수를 선언할 수도 있다.

리턴타입 메소드명(제네릭타입<?> 변수) {}

 

 

아래의 예제에 대한 설명

  • registerCourse1()은 모든 사람이 들을 수 있는 과정을 등록한다.
  • registerCourse2()는 학생만 들을 수 있는 과정을 등록한다.
  • registerCourse3() 은 직장인과 일반인만 들을 수 있는 과정을 등록한다.

Person.java

public class Person {
}

class Worker extends Person {
}

class Student extends Person {
}

class HighStudent extends Student {
}
	
class MiddleStudent extends Student{
}

 

Applicant.java

public class Applicant<T> {
	public T kind;

	public Applicant(T kind) {
		this.kind = kind;
	}
}

 

Course.java

public class Course {
	//모든 사람이면 등록 가능
	public static void registerCourse1(Applicant<?> applicant) {
		System.out.println(applicant.kind.getClass().getSimpleName() +
				"이(가) Course1을 등록함");
	}
	
	//학생만 등록 가능
	public static void registerCourse2(Applicant<? extends Student> applicant) {
		System.out.println(applicant.kind.getClass().getSimpleName() + 
				"이(가) Course2를 등록함");
	}
	
	//직장인 및 일반인만 등록 가능
	public static void registerCourse3(Applicant<? super Worker> applicant) {
		System.out.println(applicant.kind.getClass().getSimpleName() +
				"이(가) Course3을 등록함");
	}
}

 

GenericExample.java

public class GenericExample {
	public static void main(String[] args) {
		//모든 사람이 신청 가능
		Course.registerCourse1(new Applicant<Person>(new Person()));
		Course.registerCourse1(new Applicant<Worker>(new Worker()));
		Course.registerCourse1(new Applicant<Student>(new Student()));
		Course.registerCourse1(new Applicant<HighStudent>(new HighStudent()));
		Course.registerCourse1(new Applicant<MiddleStudent>(new MiddleStudent()));
		System.out.println();

		//학생만 신청 가능
		//Course.registerCourse2(new Applicant<Person>(new Person())); (x)
		//Course.registerCourse2(new Applicant<Worker>(new Worker())); (x)
		Course.registerCourse2(new Applicant<Student>(new Student()));
		Course.registerCourse2(new Applicant<HighStudent>(new HighStudent()));
		Course.registerCourse2(new Applicant<MiddleStudent>(new MiddleStudent()));
		System.out.println();
			
		//직장인 및 일반인만 신청 가능
		Course.registerCourse3(new Applicant<Person>(new Person()));
		Course.registerCourse3(new Applicant<Worker>(new Worker()));
		//Course.registerCourse3(new Applicant<Student>(new Student())); 		(x)
		//Course.registerCourse3(new Applicant<HighStudent>(new HighStudent())); 	(x)
		//Course.registerCourse3(new Applicant<MiddleStudent>(new MiddleStudent())); 	(x)
	}
}
/*
Person이(가) Course1을 등록함
Worker이(가) Course1을 등록함
Student이(가) Course1을 등록함
HighStudent이(가) Course1을 등록함
MiddleStudent이(가) Course1을 등록함

Student이(가) Course2를 등록함
HighStudent이(가) Course2를 등록함
MiddleStudent이(가) Course2를 등록함

Person이(가) Course3을 등록함
Worker이(가) Course3을 등록함
*/

 

문제설명

 

소스코드

import java.util.Scanner;
public class Main {
    public static void main(String[] args) throws Exception{
    	Scanner sc = new Scanner(System.in);
    	int N = sc.nextInt();
    	int K = sc.nextInt();
    	System.out.print(factorial(N) / (factorial(N-K)*factorial(K)));
    }
    static int factorial(int n)
    {
    	long count = 1;
    	if(n == 0) return (int)count;
    	else
    	{
    		for(int i = 1; i <= n; ++i) count *= i;
    		return (int)count;
    	}
    }
}

 

설명

  • 사실상 팩토리얼 구하는 문제이다. 왜 인지는 아래의 공식을 보면 알 수 있을 것이다.

이 게시글은 이것이 자바다(저자 : 신용권, 임경균)의 책과 동영상 강의를 참고하여 개인적으로 정리하는 글임을 알립니다. 


math 클래스의 random() 메소드 이용

0.0과 1.0 사이(0.0 포함, 1.0 미포함) 범위에 속하는 하나의 double 타입의 값을 리턴

double v11 = Math.random(); //0.47464615326686044

 

1부터 n까지 랜덤 수를 뽑는 코드는 아래와 같다.

 

int num = (int) (Math.random()*n) + 1;

 

아래의 예제는 1~45의 랜덤 숫자 중 6개 뽑는 예제이다.

public class Main {
	public static void main(String[] args) {
		for(int i = 0; i < 6; ++i) 
		{
			int num = (int) (Math.random()*45) + 1;
			System.out.print(num + " ");
		}
	}
}
/*
38 45 9 7 40 37
*/

 


 

java.util.random 클래스 이용

객체 생성 설명
Random() 현재 시간을 이용해서 종자값을 자동 설정
Random(long seed) 주어진 종자값을 사용
  • 종자값(seed)이란 난수를 만드는 알고리즘에 사용되는 값으로, 종자값이 같으면 같은 난수를 얻는다.
    종자값이 다른데 같은 난수를 얻는 확률은 매우 드물다.
  • 아래의 표는 Random 클래스가 제공하는 메소드이다.
리턴값 메소드 설명
boolean nextBoolean() boolean 타입의 난수를 리턴
double nextDouble() double 타입의 난수를 리턴(0.0 <= ~ < 1.0)
int nextInt() int 타입의 난수를 리턴(-2^32 <= ~ <= 2^32 -1)
int nextInt(int n) int 타입의 난수를 리턴(0 <= ~ < n)

 

진짜 난수, 의사 난수
로또에서 번호가 적힌 공을 하나하나 꺼내 당첨 번호를 결정하는 것과 같은 것이 '진짜 난수'를 생성하는 과정이다.
의사 난수는 실제와 비슷하다는 뜻이다.
컴퓨터가 생성하는 난수는 진짜 난수가 아니라 의사 난수이다.

그럼 Random 클래스가 생성하는 난수는 뭔데?
컴퓨터 과학에서 난수는 보통 특정 입력값이나 컴퓨터 환경에 따라 무작위로 선택한 것처럼 보이는 난수를 생성한다.
Random 클래스에서는 48비트의 seed를 사용하고, 이 seed는 선형 합동법이라는 계산법에 의해 특정 수(난수)로 바뀐다.
seed의 값과 컴퓨터 환경이 같다면 그 결과값은 항상 같다.
따라서 seed를 매번 바꿔줘야한다. seed를 매번 다르게 하기 위해 현재 시간을 이용하는 것이 일반적이다. 현재 시간은 매 순간 바뀌므로 이전에 발생한 의사 난수를 다시 생성하지는 않는다.
ex) seed 가 1인 경우 : 항상 1 -> 105 -> 999 -> 1002 ... 의 순서로 숫자를 생성
ex) seed 가 2인 경우 : 항상 2 -> 892 -> 7291 -> 10123 ... 의 순서로 숫자를 생성

이는 C언어에서도 마찬가지이다.
아래의 코드는 비주얼스튜디오 환경에서 C++로 작성된 코드이다.
#include<iostream>
using namespace std;
void main()
{
	int num = rand();
	int num2 = rand();
	cout << num << ", " << num2 << endl;
}​​

위 코드로 몇 번이고 계속 실행을 해도 같은 수만 출력이 된다.

 

아래의 코드는 랜덤으로 로또 번호를 선택하고, 랜덤으로 당첨번호를 추출한다. 이후 두 로또번호가 일치하는지 확인한다.

로또는 1~45 범위의 정수 숫자만 선택할 수 있으므로 nextInt(45) + 1 연산식을 사용한다

import java.util.Arrays;
import java.util.Random;

public class RandomExample {
	public static void main(String[] args) {
		//선택번호
		int[] selectNumber = new int[6];
		Random random = new Random(3); //seed = 3
		System.out.print("선택번호: ");
		for(int i=0; i<6; i++) {
			selectNumber[i] = random.nextInt(45) + 1;
			System.out.print(selectNumber[i] + " ");
		}
		System.out.println();

		//당첨번호
		int[] winningNumber = new int[6];
		random = new Random(5); //seed = 5
		System.out.print("당첨번호: ");
		for(int i=0; i<6; i++) {
			winningNumber[i] = random.nextInt(45) + 1;
			System.out.print(winningNumber[i] + " ");
		}
		System.out.println();

		//당첨여부
		Arrays.sort(selectNumber);
		Arrays.sort(winningNumber);
		boolean result = Arrays.equals(selectNumber, winningNumber);
		System.out.print("당첨여부: ");
		if(result) {
			System.out.println("1등에 당첨되셨습니다.");
		} else {
			System.out.println("당첨되지 않았습니다.");
		}
	}
}
/*
선택번호: 15 21 16 17 34 28 
당첨번호: 18 38 45 15 22 36 
당첨여부: 당첨되지 않았습니다.
*/

'Java Category > Java' 카테고리의 다른 글

[Java] 멀티 스레드  (0) 2023.08.01
[Java] 제네릭(Generic)  (0) 2023.07.31
[Java] StringBuilder 클래스, StringTokenizer 클래스  (1) 2023.07.29
[Java] 레코드, 롬복(lombok)  (0) 2023.07.28
[JAVA] API Document  (0) 2023.07.27