일일 정리

240613 자바(6) - 인터페이스/예외처리/try-catch-finally문/throws/기본 API/Wrapper/Boxing, Unboxing

햠__ 2024. 6. 13. 18:08

인터페이스(Interface)


인터페이스(Interface)

자바에서 클래스들이 구현해야 하는 동작을 지정하는 역할

 

인터페이스는 실행 코드와 객체가 통신하는 접점으로

실행 코드가 인터페이스의 메소드를 호출하면 인터페이스는 객체의 메소드를 호출시킨다.

 

인터페이스 선언

[접근 제한자] interface 인터페이스명 { ... }

필드 대신 상수를, 생성자는 만들 수 없고 메소드 대신 추상 메소드를 생성할 수 있다.

 

인터페이스 선언은 class 키워드 대신에 interface 키워드를 사용한다.

인터페이스는 선언된 필드는 모두 public static funal의 특성을 갖는다.

인터페이스에 선언된 메소드는 모두 public abstract의 특성을 갖는다.

자바 8부터 디폴트 메소드와 정적 메소드도 선언이 가능하다.

 

public interface Runable {
	void run(); // public abstract 생략 가능
}

 

 

 

인터페이스 구현

인터페이스를 구현하는 클래슨는 클래스 선언부에 implements 키워드를 추가하고 인터페이스명을 명시해야 한다.

인터페이스를 구현하는 클래스는 인터페이스에 정의된 추상 메소드를 반드시 오버라이딩 해야 한다.

 

// 인터페이스 구현 방법
public class Cat implements Runable {
  ...

  @Override
  public void run() {
    ...
  }
}

 

상속과 다르게 인터페이스는 다중 구현이 가능하다.

또한, 인터페이스를 구현하는 클래스로 객체를 생성 후 구현된 메소드를 호출할 수 있다.

 

인터페이스 상속

[접근 제한자] interface 하위인터페이스 extends 상위인터페이스1, 상위인터페이스2 { ... }

 

다른 인터페이스를 상속할 수 있다.

클래스와 달리 다중 상속을 허용한다.

 

// 다중 상속
public interface Basic extends Runable, Swimable {
  void eat();
}

 

하위 인터페이스를 구현하는 클래스는 하위 인터페이스의 추상 메소드 뿐만 아니라

상위 인터페이스들의 모든 추상 메소드들을 오버라이딩 해야 한다.


인터페이스 실습을 진행해보자!

이전에 다형성에서 진행했던 파일들을 그대로 가져왔다.

 

이후 Basic 이라는 이름으로 인터페이스를 하나 생성해주었다.

package com.beyond.inter.practice;

public interface Basic {
	/* public static final */ int num = 0;
	
	// 무조건 public static final로 선언되기 때문에 private를 줄 수 없음
//	private int num = 0;
	
	void turnOn() {
	}
	
	// 인터페이스에서 메소드는 무조건 추상 메소드이다.
	/* public abstract */ void turnOn();
	/* public abstract */ void turnOff();
	
	// default를 붙이면 작성 가능하긴 함. 
	// 인터페이스를 구현하는 메소드들들이 공통적으로 갖도록 함
//	public default void print() {}
}

 

작성한 코드는 위와 같다.

 

인터페이스로 작성된 코드 내에서는 모든 필드들이 public static final로 선언된다.

굳이 적지 않아도 기본으로 public static final로 적용이 된다.

그렇기 때문에 접근 제한자를 private로 작성하면 에러가 발생한다.

 

또한, interface에는 메소드도 추상 메소드만 작성할 수 있는데, 중괄호를 주어 메소드 내용을 작성하지는 못한다.

만약 default 값을 주면 작성이 가능하긴하다.

 

이후 Product 클래스에 Basic 인터페이스를 implemnets 해주었다.

 

public abstract class Product implements Basic {
	...
}

 

Basic을 implements 해주니 Product 클래스를 상속 받는 다른 3가지의 클래스가 에러가 발생했다.

그 이유는 뭘까?

 

왜냐면 Product 클래스는 abstract를 주어 추상 클래스로 작성되어서 interface를 implements 해도 재정의 안해도 되지만,

Product 클래스를 상속하는 나머지 세 클래스들은 인터페이스의 추상 메소드를 재정의 하지 않았기 때문에 에러가 발생한다.

 

그래서 나머지 세 클래스에 Basic 인터페이스에 생성된 메소드들을 추가하여 재정의 해주었다.

 

// Desktop 클래스
@Override
public void turnOn() {
	System.out.println("데스크톱을 켭니다.");
}

	@Override
	public void turnOff() {
		System.out.println("데스크톱을 끕니다.");
	}

 

// SmartPhone 클래스
@Override
public void turnOn() {
	System.out.println("스마트폰을 켭니다");		
}

	@Override
	public void turnOff() {
		System.out.println("스마트폰을 끕니다.");
	}

 

// Television 클래스
@Override
public void turnOn() {
	System.out.println("텔레비전을 켭니다.");
}

	@Override
	public void turnOff() {
		System.out.println("텔레비전을 끕니다.");
	}

 

위와 같이 세 클래스에 추상 메소드를 재정의해주었더니 에러가 사라졌다.

 

이후 메인 실행 클래스로 돌아와 간단한 출력 코드들을 작성해주었다.

package com.beyond.inter;

import com.beyond.inter.practice.Basic;
import com.beyond.inter.practice.Desktop;

public class Application {

	public static void main(String[] args) {
//		인터페이스는 객체 생성이 불가능하다.
//		Basic basic = new Basic();
		
//		인터페이스는 참조 변수로 사용이 가능하다.
		Basic basic = new Desktop("애플", "A1111", "아이맥 24인치", 1500000, true);
		
		basic.turnOn();
		basic.turnOff();
		System.out.println();
	}

}

 

여기서 주의할 점은 인터페이스에서 객체 생성이 불가능한 점이다.

인터페이스를 구현하는 클래스를 통해서 객체를 생성해야 한다.

 

출력 결과

 

이후 실행을 해보면 위와 같이 재정의한 추상 메소드가 올바르게 출력되는 것을 확인할 수 있었다.


아래는 배열의 다형성을 적용한 실습이다.

//		배열의 다형성
		Basic[] array = new Basic[4];
		
		array[0] = new Desktop("애플", "A1111", "아이맥 24인치", 1500000, true);
		array[1] = new Desktop("삼성", "S1111", "매직스테이션", 2000000, false);
		array[2] = new SmartPhone("애플", "A2222", "아이폰11", 1000000, "SKT");
		array[3] = new Television("엘지", "L1111", "스마트 TV", 2000000, 40);
		
		for (Basic product : array) {
			product.turnOn();
			product.turnOff();
			System.out.println();
		}

 

메인 실행 클래스에 위와 같은 코드를 작성하고 실행하였다.

실행 결과

 

배열에 새로 저장한 값들이 차례대로 올바르게 출력되는 것을 확인할 수 있었다.


다음은 인터페이스의 다중 구현 실습이다.

 

기존에 존재하는 Basic 인터페이스를 복사해서 Basic2로 붙여넣어주고, Prodcut 클래스에서 아래와 같이 작성한다.

public abstract class Product implements Basic, Basic2 {
	...
}

 

이렇게 두 인터페이스를 다중 구현해도 에러가 발생하지 않는다.

 

두 인터페이스 내에 동일한 메소드가 있어도 내용이 없는 추상 메소드이기 때문에

구현 클래스에서 한 번만 재정의 해주면 되기 때문에 다중 구현을 허용한다.

또한, 인터페이스 간의 상속 역시 가능하다.

 

 

예외처리


에러(Error)

프로그램 수행 시 치명적 상황이 발생하여 비정상 종료 상황이 발생한 것

 

- 컴파일 에러 : 소스코드 상의 문법 에러로 소스코드를 수정하여 해결할 수 있다.

- 런타임 에러 : 프로그램 실행 중에 발생하는 에러로 사용자로부터 잘못된 값을 입력받거나 계산식의 오류 등으로 발생할 수 있다.

- 시스템 에러 : 컴퓨터 하드웨어 오작동 또는 고장으로 인해 발생하는 에러로 소스코드를 수정하여 해결이 불가능하다.

 

예외(Exception)

사용자의 잘못된 조작 또는 개발자의 잘못된 코딩으로 인해 발생하는 프로그램 오류

 

예외의 경우 if문 또는 예외 처리 구문을 통해서 예측 가능한 예외 상황에 대해서 해결이 가능하다.

모든 예외는 Exception 클래스 (* 자바에서 모든 예외는 클래스로 관리한다)를 상속하고 있으며,

반드시 예외 처리해야 하는 Checked Exceoption과 해주지 않아도 되는 Unchecked Exception으로 나뉜다.

 

- CheckedException

컴파일 시 예외 처리 코드가 있는지 검사하는 예외

 

Exception을 상속하고 있는 예외들이다.

예외 처리가 되어있지 않으면 컴파일 에러를 발생시킨다. (try ~ catch, throws)

조건문이나 소스코드 수정으로는 해결되지 않는다. 주로 외부에 매개체와 입출력이 일어날 때 발생한다.

 

- UnCheckedException

컴파일 시 예외 처리 코드가 있는지 검사하지 않는 예외

 

RuntimeException을 상속하고 있는 예외들이다.

RuntimeException 같은 경우엔 프로그램을 실행할 때 문제가 발생되는 것이기 때문에 충분히 예측이 가능해 조건문들을 통해서 충분히 처리가 가능하다.

 

예외 처리

프로그램에서 예외가 발생했을 경우 프로그램의 갑작스러운 종료를 막고 정상 실행을 유지할 수 있도록 처리하는 코드

 

1. try-catch-finally 문

try 블록의 코드에서 예외가 발생하면 즉시 실행을 멈추고 catch 블록으로 이동하여 예외 처리 코드를 실행한다.

 

try 블록 - 예외가 발생할 기능이 있는 코드가 위치함

finally 블록 - 생략이 가능하고 예외 발생 여부와 상관없이 항상 실행할 내용이 있을 경우에 작성

 

try {
  ...
} catch (Exception e) {
  ... 
} finally {
  // 예외 발생 여부와 상관없이 실행해야 하는 코드  
}

 

2. throws

메소드 선언부 끝에 작성되어 메소드에서 처리하지 않는 예외를 호출한 곳으로 떠넘기는 역할

 

메소드 내부에서 예외가 발생할 수 있는 코드를 작성할 때 try-catch 블록으로 예외를 처리하는 것이 기본이지만,

throws 키워드를 통해서 메소드를 호출한 곳으로 예외를 떠넘길 수도 있다.

// BufferedReader 클래스의 readLine() 메소드
public String readLine() throws IOException {
  ...
}

 

3. 예외와 오버라이딩

부모 클래스의 메소드를 자식 클래스에서 오버라이딩 시 메소드가 throws 하는 Exception과 같거나 하위 클래스이어야 한다.

 

오버라이딩 시에는 throws를 작성하지 않아도 된다.

부모 클래스의 메소드와 같은 예외를 throws 하는 것은 가능하다.

부모 클래스의 메소드보다 더 하위 타입의 예외를 throws 하는 것은 가능하다.

부모 클래스의 메소드보다 더 상위 타입의 예외를 throws 하는 것은 불가능하다.


예외처리 실습을 진행해보자.

 

일부러 에러가 발생하도록 코드를 작성하였다.

package com.beyond.exception.practice;

public class A_TryCatch {
	public void method1() {
		int result = 10 / 0;
		
		System.out.println(result);
	}
}

 

그리고 메인 실행 메소드에 다음과 같이 작성하였다.

package com.beyond.exception;

import com.beyond.exception.practice.A_TryCatch;

public class Application {

	public static void main(String[] args) {
		System.out.println("프로그램 실행");
		
		new A_TryCatch.method1();
		
		System.out.println("프로그램 종료");
	}

}

 

그럼 아래와 같은 에러가 발생한다.

 

에러 발생

 

TryCatch문을 사용하여 코드를 수정해보자!

package com.beyond.exception.practice;

public class A_TryCatch {
	public void method1() {		
		try {
			// 예외가 발생할 가능성이 있는 코드
			int result = 10 / 0;
			
			System.out.println(result);
		} catch (NullPointerException e) {
			// try에서 발생한 예외를 처리하는 블록
			System.out.println("NullPointerException이 발생하였습니다.");
		} catch (ArithmeticException | ClassCastException e) {
			System.out.println("ArithmeticException 또는 ClassCastException이 발생하였습니다.");
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			// 예외 발생 여부와 상관없이 무조건 수행된다.
			System.out.println("finally 블록 실행");
		}
	}
}

 

출력 결과

 

출력 결과는 위와 같다.

프로그램 실행 후 에러가 발생하면 catch문에서 각각의 해당되는 에러가 있는 곳으로 가서 블록 내 코드를 실행한다.

 

두 번째 catch문처럼 여러 에러를 동시에 줄 수도 있고,

세 번째 catch문처럼 printStackTrace()를 통해서 에러는 출력하지만 실행을 거기서 멈추지 않고 다음 내용을 이어서 실행하도록 할 수도 있다. 

 

그래서 원래 같았으면 에러 발생하고 프로그램이 종료 되었어야 했는데 printStackTrace를 해주었기 때문에

"프로그램 종료"도 함께 출력이 된다.

 

그리고 finally 블록은 예외 발생 여부와 상관없이 무조건 수행되는 블록이다.


throws 실습을 진행해보겠다.

새 클래스 파일을 생성해주었다.

 

method1은 method2를, method2는 method3을 호출하도록 코드를 작성하였고, method3 내용을 추가해주었다.

package com.beyond.exception.practice;

import java.io.IOException;

public class B_Throws {
	public void method1() throws ClassNotFoundException, IOException {
		System.out.println("method1() 호출");
		method2();
		System.out.println("method1() 종료");
	}
	
	public void method2() throws ClassNotFoundException, IOException {
		System.out.println("method2() 호출");
		method3();
		System.out.println("method2() 종료");
	}
	
	// 에러가 발생하면 throws를 통해서 method3를 호출한 메소드가 에러를 해결하도록 함
	public void method3() throws ClassNotFoundException, IOException {
		System.out.println("method3() 호출");
		
		int random = (int)(Math.random() * 3 + 1);
		
		// Exception 코드를 상속하고 있으면 반드시 try catch문을 사용해주어야 함. 아니면 에러 발생
		if (random == 1) {
			throw new ClassNotFoundException();
		} else if (random == 2) {
			throw new IOException();
		}

		
		System.out.println("method3() 종료");
	}
	
}

 

위와 같이 코드를 작성하고 method3()에서 try-catch문을 작성하였을 때

코드가 Exception 클래스를 상속하고 있으면 반드시 try-catch문을 작성해주어야 한다. 아니면 에러가 발생한다.

 

이 에러 발생을 throws를 통해서 method3()를 호출한 메소드가 에러를 해결하도록 넘길 수 있다.

그리고 method2()에서도 throws를 통해서 에러 해결을 method1로 넘겨주었다.

 

thorws를 진행하면 해당 메소드를 호출한 메소드에 에러가 발생하고,

최종적으로 넘김을 받은 메소드에서 해결 코드를 작성해주어야한다..

 

만약 모든 메소드에서 throws 했다면 어떻게 될까?

메인 실행 메소드에서 에러가 발생한다.

그렇게 되면 메인 실행 메소드에서 오류를 수정할 코드를 작성해주어야 하고, 만약 메인 메소드 마저 throws를 진행해버린다면

자바 JVM에서 오류를 해결하고자 한다. 하지만 결국 에러가 발생하고 프로그램을 종료시킨다.

 

출력 결과

 

위와 같이 메인 실행 메소드 실행 시 에러가 발생한다.

 

메인 메소드를 아래와 같이 수정해보자.

public class Application {

	public static void main(String[] args) {
		System.out.println("프로그램 실행");

//		new B_Throws().method1();
		
		try {
			new B_Throws().method1();
		} catch (ClassNotFoundException e) {
			System.out.println("ClassNotFoundException 예외 발생");
		} catch (IOException e) {
			System.out.println("IOException 예외 발생");
		}
		
		System.out.println("프로그램 종료");
	}

}

출력 결과

 

아래와 같이 예외 발생 시 출력문이 출력되면서 프로그램 종료를 진행한다.


RunTimeException 실습을 진행해보자.

 

사용자로부터 배열의 길이를 입력받아와 출력해보자.

public class C_RunTimeException {
	public void method2() {
		int size = 0;
		int[] numbers = null;
		Scanner scanner = new Scanner(System.in);
		
		System.out.print("배열의 길이 > ");
		size = scanner.nextInt();
		
		numbers = new int[size];
		
		for (int i = 0; i <= numbers.length; i++) {
			System.out.print(numbers[i] + " ");
		}
	}
}

 

출력 결과

 

사용자로부터 입력을 잘못받아드리거나, 내가 미숙해서 코드를 잘못 짜서 생긴 오류들이고

이것들은 코드 수정으로 오류 해결이 가능하다.

 

코드 수정 후 결과

 

반복문의 조건에서 i < numbers.length로 바꿔주었더니 오류가 발생하지 않고 실행되었다.

 

근데 여기서 문제가 하나 더 있다.

뭐냐면 배열 값을 입력할 때 음수 값을 입력하면 그 때도 에러가 발생한다.

NegativeArraySizeException

 

이 경우에도 코드 수정을 통해서 에러를 해결할 수 있다.

public class C_RunTimeException {
	public void method2() {
		int size = 0;
		int[] numbers = null;
		Scanner scanner = new Scanner(System.in);
		
		System.out.print("배열의 길이 > ");
		size = scanner.nextInt();
		
		if (size <= 0) {
			System.out.println("0보다 큰 값을 입력해주세요.");
			return;
		}
		
		numbers = new int[size];
		
		for (int i = 0; i < numbers.length; i++) {
			System.out.print(numbers[i] + " ");
		}
	}
}

 

실행 결과

위와 같은 실행 결과를 받을 수 있었다.

 

이렇게 무조건 예외를 통해서 에러를 해결하는 방법이 아닌 코드를 수정하는 등의 간단한 코드 수정을 통해서

발생한 에러를 해결할 수 있는 에러를 RunTimeException 이라고 한다.

 

 

기본 API


Wrapper 클래스

기본 자료형을 객체로 포장해 주는 클래스

 

Boxing : 기본 자료형을 Wrapper 클래스로 포장해주는 것

Unboxing : Wrapper 클래스를 기본 자료형으로 변경해주는 것


Wrapper 클래스 실습을 진행해보자.

 

Wrapper 클래스를 이용해서 Boxing 하는 방법에는 3가지가 존재한다.

 

<Boxing>

1. Wrapper 클래스로 객체를 생성하는 방법

public void method1() {
	// 1. Wrapper 클래스로 객체를 생성하는 방법 (@Deprecated)
	
	// Deprecated 됐다는 의미.
	// 사용하지 말라는 의미임(?) 근데 이전 버전과의 호환성을 위해서 남겨놓았다.
	Integer integer = new Integer(10);
	Double double1 = new Double(3.14);
	Double double2 = new Double(3.14);
	
	System.out.println(integer);
	System.out.println(double1);
	System.out.println(double2.toString());
	System.out.println();
       
    // 데이터값이 아닌 주소값을 비교하기 때문에 false로 나옴
	System.out.println(double1 == double2);
	// 데이터값 비교
	System.out.println(double1.equals(double2));
	System.out.println();
}

 

 

맨 첫 줄의 Integer와 Double 타입들의 객체를 생성하였을 때에 아래처럼 단어 중간에 글이 그어져있다.

 

Deprecated 되었다.

 

이것은 Deprecated 됐다는 의미로 사용하지 말라고 경고하는 것이다. 이전 버전과의 호환성을 위해서 남겨놓았다고 한다.

객체를 생성하고 값을 대입하였을 때 올바르게 출력되는 것을 확인할 수 있었다.

 

출력 결과

 

또한, 값을 객체로 생성하였기 때문에 == 연산자를 사용하여 비교하면 데이터값이 아닌 주소값을 비교하기 때문에 false로 출력이 된다.

데이터 값을 비교하려면 equals() 메소드를 사용해주어야 한다.

 

실행 결과

 

2. 정적 메소드를 통해서 생성하는 방법

// 2. 정적 메소드를 통해서 생성하는 방법
	Integer integer2 = Integer.valueOf(3);
	Double double3 = Double.valueOf(3.14);
	Double double4 = Double.valueOf("3.14");
		
	System.out.println(integer2);
	System.out.println(double3);
	System.out.println(double4);
	System.out.println(double3.equals(double4));
	System.out.println();

 

실행 결과

 

3. Auto Boxing

// 3. Auto Boxing (같은 타입만 가능)
	Integer integer3 = 5;	
	Double double5 = 1.12345678;
	Double double6 = 1.12345678;
		
	System.out.println(integer3);
	System.out.println(double5);
	System.out.println(double6);
	System.out.println(double5 == double6);	
	System.out.println(double5.equals(double6));
	System.out.println();

 

객체를 생성할 때 값이 기본 타입을 적어주었는데 시스템에서 알아서 만들어준다.

단, 생성 시의 같은 타입만 가능하다. 

 

-

 

<Unboxing>

1. Wrapper 객체의 메소드를 이용하는 방법

int iNum1 = integer.intValue();
int iNum2 = integer2.intValue();
		
System.out.println(iNum1);
System.out.println(iNum2);
System.out.println(iNum1 + iNum2);
System.out.println();

 

Wrapper 클래스로 생성한 객체를 기본 타입으로 변경해준다.

위 예시는 int 타입으로 변경해주어 연산도 가능하다.

 

2. Auto Boxing

// 2. Auto Unboxing
	double dNum1 = double1;	
//	double dNum1 = integer; // 자동 형 변환이 일어나서 에러 안남
	double dNum2 = double2;
		
	System.out.println(dNum1);
	System.out.println(dNum2);
	System.out.println(dNum1 + dNum2);
	System.out.println();

	System.out.println(integer2 + integer3);
	System.out.println(double4 + double5 + integer);
	System.out.println();

 

위에서 선언된 double1과 double2는 double 타입으 ㅣ참조 변수이다.

참조하고 있는 주소값의 데이터 값을 대입해준다.

 

또한, 타입이 다른 경우에도 자동 형 변환이 일어나서 에러가 발생하지 않는다.


Date 클래스

날짜를 표현하는 클래스

 

주로 객체 간에 날짜 정보를 주고 받을 때 사용한다.

대부분이 Deprecated 되어있어서 보통은 시스템의 현재 시간을 가져올 때만 사용한다.


Date 클래스 실습을 진행해보자! 

public void method1() {
	Date today = new Date();
    Date when = new Date(1000);
    
    System.out.println(today);
    System.out.println(when);
    System.out.println();

 

기본 생성자로 객체를 생성하면 현재 시스템의 시간에 대한 정보를 가지고 객체를 생성한다.

 

출력 결과

 

매개변수로 숫자값을 받을 수 있는데 밀리 세컨드 단위로 받는다. 숫자값 1000이 1초이며,

Date 출력은 1970년 1월 1일 00시 기준으로 출력한다. (KST 기준이라서 9시로 출력된다.)

 

// 1970년 1월 1일 00시 기준으로 지나간 밀리 세컨드 단위의 시간
System.out.println(today.getTime());
		
System.out.println(today.getYear() + 1900);
System.out.println(today.getMonth() + 1);
System.out.println(today.getDate());
System.out.println();

 

getTime()은 1970년 1월 1일 00시 기준으로부터 지나간 시간을 밀리 세컨드 단위로 출력해준다.

 

getYear(), getMonth(), getDate()를 이용해서 년월일을 출력해주려면

년도는 +1900, 월은 +1 해줘야 한다.

이유는 출력 기준이 달라서 그렇다.


자바 1.8부터는 날짜와 시간을 나타내는 java.time 패키지를 제공한다.

Date 클래스보다 사용이 편하다!

 

그리고 내일 실습을 위해서 lombok을 설치해주었다.

lombok을 설치하면 Getter Setter 같은 메소드들을 자동으로 생성해준다고 한다.

사용하면 편하다고 한다.