안녕하세요, 여행벌입니다.

오늘은 대표적인 DP 문제 LIS에 대해서 포스팅해보겠습니다.


LIS ( Longest Increasing Subset )

 LIS는 이름 그대로 증가 부분 수열 중에서 가장 긴 부분 수열을 찾아내는 문제입니다.

 예를 들어, { 1 4 2 3 5 7 9 10 } 이 있다고 해봅시다.

 부분 수열이란 { 4 3 9 10 } 과 같이 수열에서 0개 이상의 숫자를 지우고 남은 수열을 말합니다.

 그 중, 증가 부분 수열이란 부분 수열의 원소들이 증가하고 있는 상태를 유지하는 것을 말합니다.

 { 1 2 3 5 7 9 } , { 4 5 7 9 10 } 등이 부분 증가 수열입니다.

  어떻게 증가 부분 수열 중 가장 길이가 긴 수열을 찾을 수 있을까요??

1) 완전 탐색

 가장 쉬운 방법은 모든 부분 증가 수열을 찾아보고, 그 중 가장 길이가 긴 수열을 찾는 방법입니다. 수열의 모든 부분에서 시작해서 모든 증가 수열을 DFS 탐색을 하듯이 탐색할 수 있습니다.

#include<vector>
#include<algorithm>

using namespace std;

int LIS(vector<int> A) {
	if (A.size() == 0) return 0;
	int ans = 0;
	for (int i = 0; i < A.size(); i++) {
		vector<int> B;
		// 시작점인 i보다 큰 애들을 다 입력하고 이어서 진행해본다.
		for (int j = i + 1; j < A.size(); j++)
			if (A[i] < A[j])
				B.push_back(A[j]);
		ans = max(ans, 1 + LIS(B));
	}
}

 다음과 같이 재귀적으로 코드를 구현할 수 있습니다.

 예를 들어 { 3, 1, 2, 5, 4 } 수열이 있다면, 다음과 같은 증가 부분 수열이 있습니다.

 { 3, 5 } / { 3, 4 } / { 1, 2 } / { 1, 2, 5 } / { 1, 2, 4 } / { 2, 5 } / { 2, 4 } / { 5 } / { 4 }

 이런 모든 경우의 수를 다 계산해서 가장 큰 값을 return 해주면 LIS 를 구할 수 있습니다. 완전 탐색이므로 당연히 굉장히 좋지 않은 시간 복잡도를 가지게 됩니다.

2) O(N^2) 시간 복잡도를 가지는 알고리즘

 메모리제이션을 이용하면 더 괜찮은 알고리즘을 구현할 수 있습니다.

 길이가 N인 수열이 있을 때,

 Dp[K] := (0 ~ K번 째 원소) 부분 수열 중 가장 큰 증가 부분 수열의 길이 라고 해보겠습니다

int LIS(vector<int> A) {
	int N = A.size();
	vector<int> Dp(N);
	Dp[0] = 1; 
	for (int i = 1; i < N; i++) {
		int m = 0;
		for (int j = 0; j < i; j++)
			if (A[j] < A[i])
				m = max(m, Dp[j]);
		Dp[i] = m + 1;
	}
	int ans = 0;
	for (int i = 0; i < N; i++)
		ans = max(ans, Dp[i]);
	return ans;
}

list =  { 3, 1, 2, 5, 4 } 수열을 생각해보겠습니다.

Dp[0] := { 3 } 에서 얻을 수 있는 가장 큰 증가 부분 수열의 길이이므로 1이 됩니다.

Dp[1] := { 3, 1 } 에서 가장 큰 증가 부분 수열의 길이이므로 1이 됩니다.

Dp[2] := { 3, 1, 2 } 에서 얻을 수 있는 가장 큰 증가 부분 수열의 길이이므로 2가 됩니다.

Dp[3] := { 3, 1, 2, 5 } 에서얻을 수 있는 가장 큰 증가 부분 수열의 길이이므로 3이 됩니다.

Dp[4] : = { 3, 1, 2, 5, 4 } 에서  얻을 수 있는 가장 큰 증가 부분 수열의 길이이므로 2가 됩니다.

 

 Dp 배열은 정의에 의해 다음과 같은 점화식이 성립합니다.

 Dp[K] := max(Dp[i], where 0 <= i < K and list[i] < list[K] ) + 1

 Dp[K]는 기존에 구한 가장 긴 증가 부분 수열에 list[K]를 뒤에 이어서 만들 수 있습니다. 따라서, list[K] 보다 값이 작아서 list[K]를 뒤에 이을 수 있는 증가 부분 수열 중에서 Dp 값이 가장 큰 값( 가장 길이가 긴 수열 )을 찾으면 Dp[K] 를 구할 수 있습니다.

 

 이중포문으로 Dp 배열을 채울 수 있으므로 O(N^2) 복잡도로 해결할 수 있습니다. 완전탐색보다는 많이 개선되었지만, 아직 N이 조금만 커져도 계산이 어렵다는 것을 알 수 있습니다.

3) O(NlogN) 시간 복잡도를 가지는 알고리즘

 Dp 배열의 정의를 조금 바꾸고, Binary Search를 이용해 Dp 배열을 채워나가면 훨씬 더 좋은 알고리즘을 구현할 수 있습니다.

 

 Dp[ x ] := 길이가 x 인 증가 부분 수열 중 가장 작은 마지막 값 이라고 정의하겠습니다.

 

 예를 들어, [ 4 2 6 3 1 5 ] 에서 길이가 2인 증가 부분 수열은 4 - 6 / 4 - 5 / 2 - 6 / 2 - 3 / 2 - 5 / 3 - 5 / 1 - 5 처럼 여러가지가 있지만, 이 중 가장 작은 마지막 값은 3이므로 Dp[2] = 3 이 된다.

 

 이때, Dp 배열은 항상 증가 상태를 유지합니다. 왜냐하면, Dp[1] 은 길이가 1인 가장 작은 마지막 값이고, Dp[2] 는 Dp[1] 의 원소에 더 큰 원소를 이어서 얻은 값이고, Dp[3] 은 Dp[2] 의 원소에 더 큰 원소를 이은 값이기 때문입니다. 따라서, 우리는 Dp 배열에 대해서 Binary_Search 를 이용할 수 있고, 시간 복잡도를 O(NlogN) 으로 줄일 수 있습니다.

 

 지금 알고 있는 가장 큰 증가 부분 수열의 길이가 k이고 Dp[k] 까지 알고 있다고 해보겠습니다. 다음에 순회하는 원소가 Dp[k] 보다 크다면, 지금 구한 가장 큰 증가 부분 수열의 뒤에 원소를 이을 수 있으므로, Dp[k + 1] 을 갱신할 수 있습니다. 아니라면, 순회하는 원소를 Binary_Search를 이용해 Dp[1] ~ Dp[k] 중에 적절한 위치에 갱신을 하면 됩니다. 

int LIS(vector<int> v) {
	int N = v.size();
	vector<int> Dp(N + 1, INF);
	int longest = 1;
	Dp[0] = -INF;
	Dp[1] = v[0];
	for (int cur : v) {
		if (Dp[longest] < cur) {
			longest++;
			Dp[longest] = cur;
		}
		else {
			vector<int>::iterator it = lower_bound(Dp.begin(), Dp.end(), cur);
			*it = cur;
		}
	}
	int ans = 0;
	for (int i = 1; i <= N; i++)
		if (Dp[i] != INF) ans = i;
	return ans;
}

 배열의 길이가 N일 때, 최대 증가 부분 수열의 길이는 N이므로 Dp 배열의 크기는 (N + 1) 이 되고, 초기화는 INF 로 초기화해둡니다. Dp[ 0 ] 은 길이가 0인 증가 부분 수열이므로 존재하지 않고, 이분 탐색을 위해 Dp[0] = -INF 로 초기화해두겠습니다. Dp[1] 은 길이가 1인 증가 부분 수열이므로 일단은 v[0] 으로 채워둡니다. longest는 현재 찾은 가장 긴 증가 부분 수열의 길이라고 하겠습니다.

 

 배열의 원소들을 앞에서부터 순회하며 현재 찾은 가장 긴 증가 부분 수열의 마지막 값인 Dp[longest] 보다 큰 값이라면 가장 긴 증가 부분 수열의 뒤에 값을 이어서 LIS 를 만들 수 있으므로, longest는 1 증가하고, Dp 값을 갱신할 수 있습니다. 아니라면, 현재 순회 중인 원소를 Binary_Search를 통해서 Dp 배열을 갱신하면 됩니다.

 

 예를 들어, { 3, 1, 4, 2, 5, 6 } 이 있다고 하겠습니다.

순서 DP[0] DP[1] DP[2] DP[3] DP[4] DP[5] DP[6]
초기값 -INF 3 INF INF INF INF INF
1) 3을 순회 -INF 3 INF INF INF INF INF
2) 1을 순회 -INF 1 INF INF INF INF INF
3) 4를 순회 -INF 1 4 INF INF INF INF
4) 2를 순회 -INF 1 2 INF INF INF INF
5) 5를 순회 -INF 1 2 5 INF INF INF
6) 6을 순회 -INF 1 2 5 6 INF INF

 N개의 원소를 순회하면서, Binary_Search를 진행하므로 O(NlogN) 시간 복잡도를 가지는 것을 확인할 수 있습니다.


 

안녕하세요, 여행벌입니다.

저번 포스팅에 이어서 예외 클래스에 대해서 포스팅해보겠습니다.


예외 클래스 ( Throwable 클래스 )

 저번 포스팅에서 예외가 발생하면 해당 예외 클래스의 인스턴스를 생성한다고 했습니다. 그럼 예외 클래스는 어떤 클래스들이 있고, 어떤 메소드가 있고, 어떤 구조로 이루어져 있는지 알아보겠습니다.

 

 자바의 최상위 클래스인 Object 클래스를 제외하고 예외 클래스의 최상위 클래스는 Throwable 클래스 입니다. Throwable 클래스에는 여러 가지 메소드가 정의되어 있는데, 자주 쓰이는 메소드 2가지만 알아보겠습니다.

// 예외의 원인을 담고 있는 문자열을 반환
public String getMessage()

// 예외가 발생한 위치와 호출된 메소드의 정보를 출력
public void printStackTrace()

 위의 2가지 메소드는 예외 상황을 파악할 때 자주 쓰이는 메소드이므로 알아두시면 좋습니다.

예외 클래스 구분

 예외 클래스의 최상위 클래스는 항상 Throwable 클래스 이지만, 세부적으로 들어가 보면 이를 상속하는 3가지 부류로 나뉩니다.

 예외 클래스의 상속 관계는 다음과 같습니다. 우리는 이를 기반으로 크게 3가지 부류로 예외 클래스를 나눌 수 있습니다.

 

1) Error 클래스를 상속하는 예외 클래스

2) RuntimeException 클래스를 상속하는 예외 클래스( 당연히 Excpetion 클래스도 상속 )

3) RuntimeException 클래스를 상속하지 않고 Excpetion 클래스만 상속하는 예외 클래스

1) Error 클래스를 상속하는 예외 클래스

 Error 클래스를 상속하는 예외 클래스는 'VirtualMachineError', 'IOError' 와 같이 우리가 처리할 수 없는 예외 상황을 의미하기 때문에 그냥 프로그램이 종료되도록 놔두고 이후에 원인을 파악해야 합니다.

 'VirtualMachineError' 는 자바 가상머신에 심각한 오류가 발생한 것이고, 'IOError' 는 입출력 관련해서 코드 수준 복구가 불가능한 오류가 발생한 상황입니다. 자바 프로그램이 임의의 파일에 데이터를 저장하는 중에 하드디스크에 물리적 오류가 생긴다는 등 우리가 해결할 수 없는 상황들을 뜻합니다.

2) RuntimeException 클래스를 상속하는 예외 클래스

 RuntimeException 클래스를 상속하는 예외 클래스 같은 경우는 우리가 자주 보게 되는 예외 상황들입니다. 대표적인 클래스는 다음과 같습니다.

 

1. java.lang.ArithmeticException // 0으로 나누기 등의 산술 오류

2. java.lang.InputMismatchException // 잘못된 입력을 한 경우

3. java.lang.ArrayIndexOufOfBoundsException // 배열의 인덱스를 잘못 참조한 경우

4. java.lang.NegativeArraySizeException // 배열 생성 시 길이를 음수로 지정하는 경우

5. java.lang.NullPointerException // null 객체를 잘못 참조한 경우

6. java.lang.ArrayStoreException // 배열에 적절치 않은 인스턴스를 저장하는 경우

 

 이 외에도 다양한 예외 클래스들이 있지만, 대체로 프로그래머가 예외 처리를 해야 하는 상황보다는 코드를 수정해야 하는 상황입니다.

3) Exception 클래스만 상속하는 예외 클래스

 3종류 중 Exception 클래스만 상속하는 예외 클래스가 가장 많습니다. 종류가 너무 많기 때문에 예외 상황을 마주칠 때마다 정리해두는 것을 추천합니다. 다른 예외 클래스와의 차이점은 Exception 클래스만을 상속하는 예외 클래스는 try ~ catch 문으로 처리하거나 다른 영역으로 처리를 넘긴다고 반드시 명시해야 한다는 점입니다. 그렇지 않으면 컴파일을 할 수 없습니다.

 

 정리하면, Error를 상속하거나 RuntimeException을 상속하는 예외의 발생은 코드 작성 과정에서 특별히 무언가를 하지 않아도 되지만, Exception 을 상속하는 예외의 발생에 대해서는 try ~ catch 문을 통해서 예외를 처리하거나 예외의 처리를 다른 영역으로 넘겨야 한다.

throws 키워드

 위에서 계속 예외의 처리를 다른 영역으로 넘긴다고 표현하고 있다. throws 키워드를 이용하면 현재 영역에서 발생한 예외를 다른 영역으로 처리해달라고 넘길 수 있다. 먼저, 간단한 코드를 통해서 예외가 발생할 때 호출 흐름이 어떻게 되는지 알아보자.

package Hello;

public class test{
	public static void method1() {
		method2();
	}
	public static void method2() {
		System.out.println(5 / 0); // 예외 발생
	}
	
	public static void main(String args[]) {
		try {
			method1();
		}
		catch(Throwable e) {
			e.printStackTrace();
		}
		System.out.println("프로그램이 정상적으로 종료");
	}
}

 메인 메소드에서 method1 메소드를 호출하고, method1 메소드에서는 method2 메소드를 호출하고, method2 에서는 0으로 나누는 ArithmeticException 이 발생합니다. printStackTrace 메소드를 이용해서 예외가 발생한 위치와 호출된 메소드의 정보를 출력해보았습니다.

[Output]
java.lang.ArithmeticException: / by zero
	at HelloWorld/Hello.test.method2(test.java:8)
	at HelloWorld/Hello.test.method1(test.java:5)
	at HelloWorld/Hello.test.main(test.java:13)
프로그램이 정상적으로 종료

  method2 에서 에러가 발생했지만, method1 을 호출하고, 다시 main을 호출한 것을 확인할 수 있습니다. 즉, 예외는 처리되지 않으면 그 책임이 넘어가고 그 끝은 main입니다. 위의 코드는 main 함수에서 try ~ catch 구문을 이용해 예외를 처리하였으므로, try~catch 문 뒤에 있는 출력이 정상적으로 돌아간 것을 확인할 수 있습니다.

 throws 키워드를 이용하면 특정 예외 상황에 대해서 처리를 넘길 수 있습니다.

	public static void method2() throws ArithmeticException {
		System.out.println(5 / 0); // 예외 발생
	}

 다음과 같이 throws 키워드를 이용하면 ArithmeticException이 발생했을 때, 예외 처리를 메소드를 호출한 영역으로 넘길 수 있습니다. 또, 다음과 같이 여러 가지 예외 상황은 넘긴다고 명시할 수도 있습니다.

	public static void method2() throws ArithmeticException, IOException{
		System.out.println(5 / 0); // 예외 발생
	}

직접 정의하는 예외 클래스

 프로그래머가 직접 예외 클래스를 정의하고 이를 기반으로 특정 상황에서 예외가 발생하도록 할 수도 있습니다. 핵심은 우리가 직접 정의하는 예외 클래스는 모두 Exception 클래스를 상속해야 합니다.

class MyError extends Exception{
	public MyError() {
		super("내가 정의한 에러발생!");
	}
}

 예외 클래스를 정의하는 방법은 간단합니다. 그럼 어떻게 사용하는지 알아보겠습니다.

package Hello;

import java.io.IOException;

class MyError extends Exception{
	public MyError() {
		super("내가 정의한 에러발생! 양수를 입력해주세요!");
	}
}
public class test{
	public static void method1(int n) throws MyError{
		if(n < 0)
			throw new MyError();
	}
	public static void main(String args[]) {
		try {
			method1(-5);
		}
		catch(Throwable e){
			e.printStackTrace();
		}
	}
}

 method1 에서 입력받은 수가 음수라면 throw new MyError() 를 통해 내가 정의한 예외 상황을 발생시킬 수 있습니다.

[Output]
Hello.MyError: 내가 정의한 에러발생! 양수를 입력해주세요!
	at HelloWorld/Hello.test.method1(test.java:13)
	at HelloWorld/Hello.test.main(test.java:17)

이상으로 예외 처리 및 예외 클래스 포스팅을 마무리해보겠습니다.

예외 처리는 항상 필요하지만, 너무 과한 예외 처리는 프로그램의 성능 저하를 초래하므로

조심하셔야 됩니다!

 

안녕하세요, 여행벌입니다.

오늘은 자바에서 예외란 무엇이고, 어떻게 처리하는지에 대해서 포스팅해보겠습니다.


예외

 자바에서 예외란 "예외적인 상황"을 의미합니다. 단순한 문법 오류 뿐만 아니라, 실행 중간에 발생하는 "정상적이지 않은 상황"을 모두 뜻하는 표현입니다. 자바 가상머신은 예외가 발생하면 그 내용을 간단히 출력하고 프로그램을 종료합니다.

예외 처리(try ~ catch)

 자바는 예외 상황 별로 그 상황을 알리기 위한 클래스를 정의하고 있습니다. 이러한 클래스를 가리켜 "에외 클래스" 라고 합니다. 자바에서는 예외가 발생하면 해당 예외 클래스의 인스턴스를 생성합니다. 이때, 이 인스턴스를 프로그래머가 처리해준다면, 예외는 처리된 것으로 간주하여 프로그램을 종료하지 않습니다.

 

 예외가 발생했을 때, 예외를 처리하고 프로그램을 정상적으로 실행하기 위해서는 try ~ catch 구문을 이용해야 합니다. 기본 구조는 다음과 같습니다.

try{
	// 관찰영역
}
catch(Exception name){
	// 예외 처리 영역
}
// try ~ catch 다음 영역

 예외가 발생할 수 있는 코드는 try 로 관찰 영역 안에 포함시키고, try 영역 안에서 발생하는 예외를 catch 영역에서 처리해주면 예외가 발생했을 때, 프로그램을 종료하지 않아도 됩니다.

 쉽게 정리하면 try 영역에서 발생하는 예외를 catch 영역에서 해결한다!! 라고 생각하시면 될 것 같습니다.

 

 구체적으로 예외가 발생하면 어떤 일들이 일어나는지 알아보겠습니다.

먼저, try 영역에서 에러가 발생합니다. 그러면 에러에 해당하는 인스턴스가 생성됩니다. 이 인스턴스를 catch 구문에 전달하게 되고, catch 구문 안에서 예외를 처리한 후, try ~ catch 다음 영역부터 다시 프로그램을 실행합니다. 이때, catch 구문 안에서 예외를 어떻게 처리하는지는 가상머신이 신경쓰지 않습니다.

 

 예시를 통해서 익혀보겠습니다.

package Hello;

public class test{
	public static void main(String args[]) {
		System.out.println("프로그램시작");
		System.out.println(5 / 0); // 예외 발생
		System.out.println("예외발생");
		System.out.println("예외가 발생하지만 시스템은 정상적으로 실행");
	}
}

우리는 0으로 나누면 에러가 발생하는 것을 알고 있습니다.

따라서, 위에서 얘기한 대로 예외가 발생한 지점에서 ArithmeticException 이라는 예외 내용을 간단히 출력하고 종료되는 것을 확인할 수 있습니다. 

[Output]
프로그램시작
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at HelloWorld/Hello.test.main(test.java:6)

 이번에는 try ~ catch 구문을 이용해 예외가 발생하는 영역을 try 영역 안에 넣어보았습니다.

package Hello;

public class test{
	public static void main(String args[]) {
		System.out.println("프로그램시작");
		try {
			System.out.println(5 / 0); // 예외 발생
		}
		catch(Exception e) {
			System.out.println("예외발생");
		}
		System.out.println("예외가 발생하지만 시스템은 정상적으로 실행");
	}
}
[Output]
프로그램시작
예외발생
예외가 발생하지만 시스템은 정상적으로 실행

 프로그램이 시작하고 예외가 발생했지만, 프로그램이 정상적으로 끝까지 실행되는 것을 확인할 수 있습니다. 또, catch 구문에서 예외를 처리하지않고 무슨 일을 하던 가상머신은 신경쓰지 않고 try ~ catch 다음 구문부터 프로그램을 시작하는 것을 확인할 수 있습니다.

다중 예외 처리

 예외가 발생하는 종류가 여러가지면 어떻게 해야할까요?? 

 정수 2개를 입력받고 나누기를 하는 프로그램을 생각해보겠습니다.

package Hello;

import java.util.Scanner;

public class test{
	public static void main(String args[]) {
		Scanner kb = new Scanner(System.in);
		System.out.println("첫 번째 정수를 입력해주세요");
		int n1 = kb.nextInt();
		System.out.println("두 번째 정수를 입력해주세요");
		int n2 = kb.nextInt();
		System.out.println(n1 / n2);
	}
}

 1. 정수가 아닌 다른 문자가 입력으로 들어온다면??

 2. 나누는 수가 0이 들어온다면??

 다음과 같은 2가지 상황에서 에러가 발생할 수 있습니다. 그러면 에러가 발생하는 상황을 강제로 만들어서 어떤 에러가 발생하는지 보겠습니다.

[Output]
첫 번째 정수를 입력해주세요
a
Exception in thread "main" java.util.InputMismatchException
	at java.base/java.util.Scanner.throwFor(Scanner.java:939)
	at java.base/java.util.Scanner.next(Scanner.java:1594)
	at java.base/java.util.Scanner.nextInt(Scanner.java:2258)
	at java.base/java.util.Scanner.nextInt(Scanner.java:2212)
	at HelloWorld/Hello.test.main(test.java:9)

 첫 번째 정수를 'a' 라는 문자를 입력했더니, InputmismatchException이 발생한 것을 확인 할 수 있습니다.

 두 번째 정수에 0을 입력하면, 0으로 나누는 상황이 되므로 위에서 다뤘던 ArithmeticException이 발생하겠죠?? 그럼 각각 다른 에러 상황에 다른 조치를 취하려면 어떻게 해야될까요?

package Hello;

import java.util.InputMismatchException;
import java.util.Scanner;

public class test{
	public static void main(String args[]) {
		try {
			Scanner kb = new Scanner(System.in);
			System.out.println("첫 번째 정수를 입력해주세요");
			int n1 = kb.nextInt();
			System.out.println("두 번째 정수를 입력해주세요");
			int n2 = kb.nextInt();
			System.out.println(n1 / n2);
		}
		catch(InputMismatchException e) {
			System.out.println("잘못된 입력을 했습니다.");
		}
		catch(ArithmeticException e) {
			System.out.println("잘못된 나누기 연산입니다.");
		}
	}
}

 다음과 같이 catch 구문을 2개 만들고, 각각 발생할 수 있는 에러 인스턴스를 넘겨주면 됩니다.

[Output]
첫 번째 정수를 입력해주세요
a
잘못된 입력을 했습니다.
[Output]
첫 번째 정수를 입력해주세요
5
두 번째 정수를 입력해주세요
0
잘못된 나누기 연산입니다.

 문자열을 입력해도 정상적으로 프로그램이 진행되고, 다음과 같이 0으로 나눠도 프로그램이 잘 진행되는 것을 확인할 수 있습니다.

 

 자바7에서부터는 다음과 같이 한 번에 여러가지 예외를 처리할 수 있도록 묶는 것도 가능합니다.

package Hello;

import java.util.InputMismatchException;
import java.util.Scanner;

public class test{
	public static void main(String args[]) {
		try {
			Scanner kb = new Scanner(System.in);
			System.out.println("첫 번째 정수를 입력해주세요");
			int n1 = kb.nextInt();
			System.out.println("두 번째 정수를 입력해주세요");
			int n2 = kb.nextInt();
			System.out.println(n1 / n2);
		}
		catch(InputMismatchException | ArithmeticException e) {
			System.out.println("잘못된 입력 혹은 잘못된 나누기 연산입니다.");
		}
	}
}

 따라서, 상황 별 예외의 처리 방식이 다르지 않은 경우에는 위와 같이 한 번에 catch 구문 안에서 여러 예외가 처리될 수 있또록 묶는 것도 좋은 방법이 될 수 있습니다.

예외 처리(try ~ catch ~ finally)

 try ~ catch 문을 이용해 예외 처리를 진행하면, 예외가 발생한 시점에 catch 문으로 넘어가고, catch 문이 실행되고 나서 try ~ catch 문 다음부터 다시 시작합니다. 그럼, 다음과 같이 try 문에서 예외가 발생한 시점보다 뒤에 꼭 실행되야하는 코드가 있으면 어떻게 해야될까요?

try{
	// 예외발생
	// 꼭 실행해야되는 코드
}
catch(Exception e){

}

 예외가 발생하면 꼭 실행해야되는 코드가 실행되지 않고 try ~ catch 문이 종료될 것입니다.

 그럼 또 문제가 발생하겠죠?? 이를 막기 위해 finally 가 추가되었습니다.

try{
	// 관찰영역
}
catch(Exception e){
	// 에러처리영역
}
finally{
	// 꼭 실행되야하는 영역
}

 finally 영역은 try 영역이 실행된다면 무조건 실행되는 영역입니다. 따라서, try 문 안에서 예외가 발생해 catch 영역으로 넘어가더라도 마지막에는 finally 영역을 실행하게 됩니다.


 

 

안녕하세요, 여행벌입니다.

오늘은 자바의 인터페이스(interface) 가 무엇인지, 문법 구조가 어떻게 되는지, 언제 사용하는지 알아보겠습니다.


인터페이스(interface)

 인터페이스의 사전적 의미는 "연결점" 또는 "접점"으로

자바에서는 둘 사이를 연결하는 매개체의 역할을 합니다.

인터페이스(interface) 기본 문법

 인터페이스는 문법적인 면에서 클래스와 굉장히 비슷합니다.

interface 인터페이스이름{
	// 변수들
	// 메소드들
}

 class 대신 interface 선언이 붙어있고, 마찬가지로 안에는 각종 변수와 메소드가 존재합니다. 하지만, 기본적으로 메소드는 몸체가 비어 있는 "추상 메소드" 입니다.  아래와 같이 몸체가 비어 있는 메소드를 추상 메소드라고 합니다.

public void example(int a);

 그럼 인터페이스의 메소드는 몸체가 비어 있는데 어떤 기능을 할까요?? 클래스에서 인터페이스를 '상속' 이 아닌 "구현"을 하고, 이때 비어있는 메소드인 "추상 메소드" 를 클래스에서 구현해서 사용합니다. 즉, 저번 포스팅에서 다룬 오버라이딩을 통해 추상 메소드를 이용할 수 있습니다.

 

인터페이스에는 다음과 같은 특징이 있습니다.

 

● 인터페이스는 인스턴스 생성이 불가능하다.

● 인터페이스형을 대상으로 참조 변수 선언은 가능하다.

● 인터페이스는 클래스가 "상속" 하는게 아닌 "구현"을 하고 implements 키워드를 사용한다.

● 한 클래스가 둘 이상의 인터페이스를 동시에 구현할 수 있다.

● 추상메소드와 이를 구현하는 메소드 사이에 오버라이딩이 성립하고, @Override 어노테이션을 사용할 수 있다.

● 인터페이스 안에 있는 모든 "추상 메소드"를 구현하지 않으면, 해당 클래스를 대상으로 인스턴스 생성은 불가능하다.

 

 그럼 간단한 예시를 통해서 인터페이스의 특징을 살펴보겠습니다.

package Hello;

interface print{
	public void print(String name);
}

class printName implements print{
	@Override
	public void print(String name) {
		System.out.println(name + "님 환영합니다.");
	}
}

public class test{
	public static void main(String args[]) {
		printName ex1 = new printName();
		ex1.print("travelbeeee");
		
		print ex2 = new printName();
		ex2.print("travelbeeee2");
		
	}
}
[Output]
travelbeeee님 환영합니다.
travelbeeee2님 환영합니다.

 printName 이라는 클래스에서 print 인터페이스를 구현해, 추상 메소드를 사용하는 모습을 볼 수 있습니다. 또, 인터페이스 형을 대상으로 참조 변수 선언을 하고 메소드를 이용하는 모습도 볼 수 있습니다.

인터페이스 변수

 인터페이스 안에도 변수를 선언할 수 있습니다. 클래스와는 다르게 변수는 다음과 같은 특징을 가집니다.

 

● 선언과 동시에 값을 초기화 해야 한다.

● 모든 변수는 public, static, final 이 선언된 것으로 간주한다. 즉, 고정된 값 "상수" 가 된다.

 

 인터페이스 변수는 "상수"로 선언되므로, 인터페이스 변수명은 관례적으로 대문자로 짓습니다.

interface print{
	string INTERFACE_NAME = "print_Interface";
	int INTERFACE_AGE = 9999;
	public void print(String name);
}

인터페이스 메소드

 인터페이스 안에는 "추상 메소드" / "디폴트(default) 메소드" / "클래스(static) 메소드" 를 선언할 수 있습니다.

 추상 메소드는 위에서 다뤘듯이 몸체가 비어 있는 메소드를 의미하고, 추상 메소드는 기본적으로 public이 선언된 것으로 간주합니다. 추상 메소드는 인터페이스를 구현하는 클래스 내에서 오버라이딩을 통해 전부 구현해줘야하고, 구현하지 않은 추상 메소드가 존재하면 안됩니다.

 

 디폴트 메소드는 자바8에서 처음 소개된 기능입니다. 추상 메소드와 다르게 자체로 완전한 메소드이고, 이를 구현하는 클래스가 오버라이딩 하지 않아도 됩니다. 이미 기존에 구현한 인터페이스에 메소드를 추가해야되는 경우에 디폴트 메소드를 이용하면, 이를 구현하고 있는 모든 클래스들에게 영향을 주지 않고 메소드를 추가할 수 있습니다.

 

 클래스 메소드도 자바8에서 처음 소개된 기능입니다. 클래스에 정의하는 static 메소드와 유사하게, 자체로 완전한 메소드이고 따로 구현하지 않아도 되고, 클래스의 static 메소드 호출 방법과 동일합니다.

package Hello;

interface print{
	public void print1( ); // 추상메소드
	default void print2() { // 디폴트메소드
		System.out.println("디폴트메소드입니다.");
	}
	public static void print3() { // 클래스 메소드
		System.out.println("클래스메소드입니다.");
	}
}

class printName implements print{
	@Override
	public void print1() {
		System.out.println("추상메소드입니다.");
	}
}

public class test{
	public static void main(String args[]) {
		print.print3();
		printName ex1 = new printName();
		ex1.print1();
		ex1.print2();
	}
}
[Output]
클래스메소드입니다.
추상메소드입니다.
디폴트메소드입니다.

 클래스 메소드는 인스턴스 생성 없이도 사용이 가능하고, 디폴트 메소드는 클래스 내에서 따로 구현하지 않고 사용하는 것을 볼 수 있습니다.

인터페이스 상속

 인터페이스도 클래스처럼 서로 상속이 가능합니다. extends 키워드를 사용해야하고, 클래스의 상속과 동일하게 작동합니다.

interface print{
	public void print1( ); // 추상메소드
	default void print2() { // 디폴트메소드
		System.out.println("디폴트메소드입니다.");
	}
	public static void print3() { // 클래스 메소드
		System.out.println("클래스메소드입니다.");
	}
}

interface colorPrint extends print{
	public void colorPrint1();
	default void colorPrint2() {
		System.out.println("칼라프린트의 디폴트메소드");
	}
	public static void colorPrint3() {
		System.out.println("칼라프린트의 클래스메소드");
	}
}

class printName implements colorPrint{
	@Override
	public void print1() {
		System.out.println("추상메소드입니다.");
	}
	@Override
	public void colorPrint1() {
		System.out.println("칼라프린트의 추상메소드");
	}
}

 마찬가지로 추상 메소드는 클래스에서 모두 구현해줘야합니다.


인터페이스를 사용하면 서로 다른 클래스에게 접점을 만들어 줄 수 있습니다.

다르지만 공통된 기능이 필요한 경우에 인터페이스를 만들고,

클래스마다 구현해 사용한다면 훨씬 효율적으로 코드를 구현할 수 있습니다.

이처럼, 인터페이스는 자바에서 많은 역할을 해줍니다.

 

 

 

 

+ Recent posts