※ 최근 자바 개발자들끼리 checked exception을 unchecked exception으로 전환시키는 것이 좋다는 결론을 내림
1) 예외 복구
// 일반적으로 예외를 던지는 경우
public void exceptionTest() throws SomeException {
}
// 어느정도 처리하고 예외를 던지는 경우
public void exceptionTest() throws SomeException {
try {
...
} catch (SomeException e) {
// 예외 처리의 필요성이 있을 때 어느정도 처리하고 던지는 경우
throw e;
}
}
- 예외 발생 시 다른 작업 흐름으로 유도 ( try ~ catch ~ finally )
- 예외가 발생해도 어플리케이션은 정상적인 흐름으로 진행됨
- 예외 발생 시 재시도를 통해 정상 흐름으로 진행되게 하거나 미리 예측하여 다른 흐름으로 유도시키도록 구현하면 예외가 발생하더라도 정상적으로 작업 종료 가능
2) 예외 처리 회피
public void exceptionTest() {
try {
...
} catch (SQLException e) {
// 더 명확하게 인지할 수 있도록 다른 예외로 전환해서 던지는 방법
throw new DuplicationUserIdException(message);
}
}
- 예외 처리를 직접 담당하지 않고 호출한 쪽으로 던져 회피하는 방법
- 무책임하게 상위 메서드로 throw를 던지는 행위는 상위 메서드의 책임이 증가하기 때문에 적절한 경우에만 사용하는 것이 좋음
3) 예외 전환
- 예외를 잡아 다른 적절한 예외로 전환하여 자신을 호출한 메서드로 던져버리는 방법
- 호출한 쪽에서 예외를 받아 처리 시 좀 더 명확하게 인지할 수 있도록 돕기 위한 방법
- 어쩔 수 없이 API에서 가져다 쓰는 기능(Thread.sleep() 등)은 checked exception이 강제되는 경우 어떤 로직을 호출하거나 실행하려 하는지 명시되는 문제가 발생
→ try ~ catch를 쓰되 catch 블럭에서 다른 runtimeException을 throw
→ 예외를 유발했던 요소가 아닌 catch 블럭에서 일으킨 예외가 대신 콘솔에 찍힘
→ throw 되는 예외를 바꿔서 어떤 로직이 예외를 유발했는지 추론하기 어렵게 만들어 캡슐화 유지
- 자바 소스를 컴파일하는 과정에서 예외 처리 코드를 검사하여 예외 처리 코드가 없다면 컴파일 오류 발생
- 실행하지 않고도 오류가 발생한 것을 알 수 있음
- Checked Exception은 필요하지 않다고 결론이 남
2) 실행 예외 (Runtime Exception)
- 컴파일하는 과정에서 예외처리 코드를 검사하지 않는 예외
- 개발자들 사이에서 실행 예외만 신경쓰기로 합의, 오로지 개발자의 경험에 의해 처리
- 실행 예외에 대한 예외처리 코드를 넣지 않았을 때 해당 예외가 발생하면 프로그램 즉시 종료
RuntimeException을 상속받는 예외들은 모두 Unchecked Exception이다.
둘 다 확인 시점은 실행 단계이고, rollback 여부는 설정에 따라 달라진다.
2. 실행 예외 (Unchecked Exception = Runtime Exception)
1) NullPointerException
→ 객체 참조가 없는 상태, null 값을 갖는 참조 변수로 객체 접근 연산자인 "."을 사용했을 때 발생
public class NullPointerEx {
public static void main(String[] args) {
String str = null;
//str = "HELLO";
// .toLowercase()는 모든 문자를 소문자로 만들어줌
System.out.println(str.toLowerCase());
}
}
참조형 변수로 선언한 str이 null값이므로 위와 같은 오류가 발생하는 것을 확인할 수 있다.
2) ArrayIndexOutOfBoundsException
→ 배열에서 인덱스 범위를 초과하여 사용할 경우 발생
public class ArrayIndexEx {
public static void main(String[] args) {
int [] arr = {3, 6, 9};
// 있지도 않은 인덱스 번호 조회하기, 그러나 문법상 오류는 없음
System.out.println(arr[3]);
}
}
배열의 길이가 3인데 존재하지 않는 3번 인덱스 값을 조회했으므로 위와 같은 오류가 발생하는 것을 확인할 수 있다.
3) NumberFormatException
→ 문자열로 되어 있는 데이터를 숫자로 변경하는 경우 발생
public class NumberFormatEx {
public static void main(String[] args) {
String a = "35";
String b = "21";
System.out.println(a + b);
// str -> int 변환
int i = Integer.parseInt(a); // 문자 35를 숫자 35로 변환
int j = Integer.parseInt(b); // 문자 21을 숫자 21로 변환
System.out.println(i + j);
// parseInt는 문자열 내부에 순수한 정수가 들어있어야 변환을 실행하며
// 정수값이 아니라면 NumberFormatException이 발생
String s = "Hello";
System.out.println(Integer.parseInt(s));
}
}
s 변수는 String 타입으로 정수값이 아닌데 parseInt()는 문자열 내부에 순수한 정수가 있어야함 변환을 실행하기 때문에 위와 같은 오류가 발생하는 것을 확인할 수 있다.
4) ClassCastException
→ 상속 관계나 인터페이스 관계가 없는 클래스들을 억지로 형 변환할 경우 발생
☆ 형 변환은 부모클래스 -자식클래스, 구현클래스-인터페이스 간에 발생 → 이 관계가 아니라면 다른 클래스로 타입 변환 불가
//하나의 클래스 파일에 2개 이상의 클래스 선언 가능 (자주 사용하지는 않음)
// 상속관계 : 부모 Animal을 상속한 자식 Dog, Cat
class Animal{}
class Dog extends Animal{}
class Cat extends Animal{}
public class ClassCastEx {
public static void main(String[] args) {
Dog d = new Dog();
Animal da = d;
d = (Dog)da;
System.out.println("타입 변환 성공 : Animal -> Dog");
Animal c = new Cat();
Dog d2 = (Dog)c; // Cat은 Dog 타입으로 변환 불가
System.out.println("타입 변환 성공? : Dog -> Cat");
}
}
Cat과 Dog는 상속 관계에 있는 클래스가 아니므로 위와 같은 오류가 발생하는 것을 확인할 수 있다.
3. 예외처리코드
1) try ~ catch
- 예외 발생 시 갑작스러운 종료를 막고 정상 실행을 유지할 수 있도록 처리하는 코드
- try~catch~finally 블록 : 생성자나 메서드 내부에서 작성되어 컴파일 예외나 실행 예외가 발생할 경우 예외처리
- try 블록 : 예외 발생 가능성이 있는 코드 작성 → 예외 발생 시 실행을 멈추고 catch 블록으로 이동
- finally 블록 : 예외 발생 여부와 상관없이 항상 실행 → 필수는 X, 로직이 블럭과 관련 있을 시 작성
※ finally 구문이 실행되지 않는 경우
① finally 구문 이전에 System.exit() 구문 호출 시
② 컴퓨터가 꺼져서 시스템이 멈추었을 시
③ finally 블록 내부에서 예외가 발생했을 시
public class TryCatchEx1 {
public static void main(String[] args) {
int i = 10;
int j = 5;
try { // 예외가 발생할 가능성이 있는 코드를 넣는 구역
System.out.println(i / j); // 예외 발생 가능성이 있음
System.out.println("예외 발생하지 않을 때만 실행됨");
} catch(Exception e) { //catch 블럭에는 Exception의 종류를 기입
System.out.println("0으로 나눠서 catch 블럭으로 넘어왔습니다.");
} finally { // try, catch 둘 중 어느 블럭이라도 실행되면 마무리 블럭 실행
System.out.println("어쨌든 잘 마무리 했습니다.");
}
}
}
변수 j가 5라면 try 블럭 안의 내용과 finally 블럭 내용이 출력된다.
변수 j가 0이라면 예외가 발생하여 catch 블럭으로 넘어가고 해당 내용과 finally 블럭 내용이 출력된다.
2) 다중 catch
- 여러 가지 예외가 발생한다면 다중 catch 블록 작성
- 상위 예외 클래스가 하위 예외 클래스보다 아래쪽에 위치해야 함
- 상위 예외 클래스의 catch 블록이 위에 있다면 하위 예외 클래스의 catch 블록은 실행되지 않음
- 자바 7 버전부터 하나의 catch 블록에서 여러 개의 예외 처리 가능
- catch() 괄호 안에 동일하게 처리하고 싶은 예외를 |로 연결 (※ 두 예외가 상속 관계에 있으면 안됨)
ublic class MultiCatchEx {
public static void main(String[] args) {
String data1 = "30";
String data2 = "11";
try {
// NumberFormatException 발생 가능 (정수가 아닌 값이 들어갈 경우)
int i = Integer.parseInt(data1);
int j = Integer.parseInt(data2);
// ArithmeticException 발생 가능 (j에 0이 들어갈 경우)
int result = i / j;
System.out.println("i / j = " + result);
// NullPointerException 발생 가능
String str = null;
str.charAt(0); // 0번째 문자 얻기인데 null값인 경우
} catch(NumberFormatException | NullPointerException e) {
System.err.println("데이터를 숫자만 넣어주세요.");
System.err.println("혹은 문자를 제대로 만들어주세요.");
} catch(ArithmeticException e) {
System.err.println("0으로 나눌 수 없습니다.");
} catch(Exception e) { // 범용 에러 처리 (대부분의 에러를 다 커버)
System.err.println("알 수 없는 에러가 발생했습니다.");
System.err.println("복구중입니다.");
}
}
}
catch 블록은 위에서부터 차례대로 검색되고, 예외 발생 시 해당하는 예외처리를 할 수 있게 된다.
위 예제의 경우 NullPointerException 예외만 발생하므로 첫번째 catch 블록의 내용만 출력하는 것을 확인할 수 있다.
3) throws
- 메서드나 생성자를 호출한 곳으로 예외를 던지는 방법 → 예외 처리를 직접 수행하지 않고 메서드 호출자에게 던짐
- try 블록 내부에서 호출되어야 하고, catch 블록에서 떠넘겨 받은 예외 처리를해야 함
- throws가 붙어있는 메서드는 반드시 try 블록 내부에서 호출되어야 하고, catch 블록에서 떠넘겨 받은 예외를 처리해야 함
- main() 메서드에서 throws를 사용하는 것은 예외처리를 JVM에게 넘기겠다는 의미
※ JVM은 예외를 직접 처리해주지 않고 예외 메시지만 출력하고 프로그램 종료
public class ThrowsEx {
public static String[] greetings = {"안녕", "싸왓디", "헬로"};
// 예외의 원인이 메서드 선언부가 아닌 호출부에 있을 경우
// 메모리 영역이 다르므로 예외처리를 메서드 호출지역으로 떠넘겨줘야 함
// 이를 throws라고 하고, 메서드 혹은 생성자 호출 시 예외처리를 강요할 때 사용
public static void greet(int idx) {
try {
System.out.println(greetings[idx]);
} catch(ArrayIndexOutOfBoundsException e){
// 코드 안적어도됨
}
}
public static void main(String[] args) {
// throws가 붙어 있는 메서드나 생성자 호출 시에는
// 해당 메서드를 try 블록 내부에서 호출해야 예외처리를 진행해줌
greet(3);
}
}
위 예제에서 greetings 배열의 크기가 3인데 4번째 인덱스를 조회하고 있으므로 ArrayIndexOutOfBoundsException이 발생하고, 이 경우 위와 같이 greet 함수에서 예외처리를 해줄 수 있다. 하지만, 객체 지향 측면에서 해당 메서드의 책임이 많아지므로 위 코드는 좋은 코드가 아니다.
public class ThrowsEx {
public static String[] greetings = {"안녕", "싸왓디", "헬로"};
// 예외의 원인이 메서드 선언부가 아닌 호출부에 있을 경우
// 메모리 영역이 다르므로 예외처리를 메서드 호출지역으로 떠넘겨줘야 함
// 이를 throws라고 하고, 메서드 혹은 생성자 호출 시 예외처리를 강요할 때 사용
// 해당 예외가 발생하면 호출부(여기서는 main)에게 처리를 떠넘기는 것
public static void greet(int idx) throws Exception {
System.out.println(greetings[idx]);
}
public static void main(String[] args) {
// throws가 붙어 있는 메서드나 생성자 호출 시에는
// 해당 메서드를 try 블록 내부에서 호출해야 예외처리를 진행해줌
try {
greet(3);
} catch(Exception e) {
// .printSackTrace()는 예외발생 경로를 추적하는 메세지 출력
// 주로 개발 과정에서 예외의 원인을 역추적할 때 유용
e.printStackTrace();
}
System.out.println("프로그램 정상 종료!");
}
}
위와 같이 throws가 붙은 greet() 메서드 호출 시 해당 메서드가 호출되고 있는 메인함수 쪽에서 예외처리를 진행할 수 있도록 하는 것이 바람직하다.
위와 같이 예외처리가 잘 이루어지고, 프로그램이 정상 종료된 것을 확인할 수 있다.
public class ThrowsEx2 {
public void aaa(int n) throws Exception {
System.out.println("aaa 호출");
int i = 10 / n;
System.out.println("계산 결과 : " + i);
System.out.println("aaa 실행 중");
}
public void bbb() throws Exception {
System.out.println("bbb 호출");
aaa(0);
System.out.println("bbb 실행 중");
}
public void ccc() throws Exception {
System.out.println("ccc 호출");
bbb();
System.out.println("ccc 실행 종료");
}
public ThrowsEx2() throws Exception {
System.out.println("생성자 호출");
ccc();
System.out.println("생성자 실행 종료");
}
public static void main(String[] args) {
try {
ThrowsEx2 te = new ThrowsEx2();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("진짜 코드 호출 끝!");
}
}
4) throw
- 예외를 강제로 발생시키려면 throw 키워드 이용 → 개발자가 직접 예외 클래스를 정의하여 만들 수 있음
public class ThrowEx {
public static int calcSum(int n) throws Exception {
// 프로그램이 throw 구문을 만나는 순간 예외를 즉시 발생시키고,
// 해당 예외를 처리해줄 catch 블록이 있는지 검색
if(n <= 0) {
throw new Exception(); // 예외도 클래스로 정의되기 때문에 인스턴스 생성
}
int sum = 0;
for(int i = 1; i <= n; i++) {
sum += i;
}
return sum;
}
public static void main(String[] args) {
try {
int result1 = calcSum(100);
System.out.println("1~100까지의 누적합 : " + result1);
int result2 = calcSum(-100);
System.out.println("1~100까지의 누적합 : " + result2);
} catch (Exception e) {
e.printStackTrace();
System.err.println("매개값을 양수로 전달해주세요.");
}
}
}
throw 키워드가 없다면 위 코드에서 논리적으로는 문제가 없기 때문에 정상적으로 작동한다.
하지만, 반복문 사용 시 i 값에 음수가 들어간다면 반복문을 실행하지 않기 때문에 위 예시에서는 throw를 사용하여 강제로 예외처리를 해주는 것이 좋다.
객체 생성 시 throw 키워드를 붙여주면 위와 같이 강제로 설정한 예외처리를 해주는 것을 확인할 수 있다.
5) 사용자 정의 예외
- 자바 표준 API에서 제공하는 예외 클래스만으로 다양한 종류의 예외를 표현할 수 없는 문제가 발생
- 일반 예외로 선언할 경우 Exception 클래스 상속, 실행 예외로 선언할 경우 RuntimeException 클래스 상속
- 사용자 정의 예외 클래스의 이름은 Exception으로 끝나는 것이 좋음
// 잔고 불충분 예외
// 사용자 정의 예외 클래스 생성 시 Exception 클래스나 RuntimeException 클래스 상속
public class BalanceInsufficientException extends RuntimeException {
// 일반적으로 사용자 정의 예외 클래스를 만들 때는
// 기본 생성자와 예외원인 메세지를 받는 생성자를 두 개 오버로딩하여 선언만 해줌
public BalanceInsufficientException() {};
public BalanceInsufficientException(String message) {
super(message);
}
}
// 음수값 입금 예외
public class DepositMinusMoneyException extends RuntimeException {
public DepositMinusMoneyException() {}
public DepositMinusMoneyException(String message) {
super(message);
}
}
public class Account {
private long balance;
public long getBalance() {
return this.balance;
}
// 음수로 입금할 경우 강제 예외처리
public void deposit(int money) throws DepositMinusMoneyException {
if (money < 0) {
throw new DepositMinusMoneyException("음수로 입금할 수 없습니다.");
}
this.balance += money;
}
// 인출 시 잔액이 부족한 경우 강제 예외처리
public void withdraw(int money) throws BalanceInsufficientException {
if(this.balance < money) {
throw new BalanceInsufficientException("잔고가 부족합니다.");
}
this.balance -= money;
}
}
public class MainClass {
public static void main(String[] args) {
Account acc = new Account();
try {
acc.deposit(100000);
System.out.println("입금 후 잔액 : " + acc.getBalance() + "원");
acc.withdraw(100000);
} catch(BalanceInsufficientException e) {
// 예외 클래스가 제공하는 getMessage() 메서드는 예외의 원인 메세지를 String 타입으로 리턴
// 자바 표준 API에서 제공하는 다양한 예외클래스들은 각각의 예외 원인 메세지가 기본적으로 객체 안에 저장
e.printStackTrace();
// 생성자에서 제공해준 메세지 그대로 출력
System.err.println(e.getMessage());
} catch(DepositMinusMoneyException e) {
e.printStackTrace();
System.err.println(e.getMessage());
}
System.out.println("출금 후 잔액 : " + acc.getBalance() +"원");
}
}
위와 같이 어떤 종류의 예외가 발생했는지 정확히 알 수 있기 때문에 유지보수성을 높일 수 있다.
- 객체의 사용 방법을 정의한 타입으로 객체의 교환성을 높여줌 → 다형성을 구성하는 매우 중요한 역할
- interface 키워드로 선언, 클래스 이름 뒤에 implements 키워드로 구현
- 상수와 메서드만을 구성멤버로 가짐
- 데이터를 저장할 수 없기 때문에 데이터를 저장할 객체 또는 정적 변수 선언 불가능
- 인터페이스 선언된 변수는 public static final을 생략하더라도 자동으로 붙음
- 인터페이스의 메서드를 추상메서드 형식으로 선언하면 abstract를 생략하더라도 자동으로 붙음
- extends 키워드를 사용하여 인터페이스 간의 상속 구현 가능, 다중 상속 표현 가능
public interface RemoteController {
// 최대 배터리량, 최소 배터리량을 상수로 지정
int MAX_BATTERY = 100;
int MIN_BATTERY = 0;
// 리모콘이 가져야 하는 필수 기능에 대해서만 정의
public void turnOn();
public void turnOff();
public void showStatus();
}
public class TVRemoteController implements RemoteController {
private final int inch;
private int channel;
public TVRemoteController(int inch) {
this.inch = inch;
this.channel = 1;
}
@Override
public void turnOn() {
System.out.println("TV를 켭니다.");
}
@Override
public void turnOff() {
System.out.println("TV를 끕니다.");
}
@Override
public void showStatus() {
System.out.println("화면크기 : " + this.inch);
System.out.println("채널 : " + this.channel);
}
public void setChannelDown() {
// 1번까지만 채널이 있음
if(this.channel - 1 < 1) {
this.channel = 1;
} else {
this.channel--;
}
}
public void setChannelUp() {
this.channel++;
}
}
public class RobotCleanerRemoteController implements RemoteController{
public String modelName;
public String price;
// 로봇청소기 생성자
public RobotCleanerRemoteController(String modelName, String price) {
this.modelName = modelName;
this.price = price;
}
@Override
public void turnOn() {
System.out.println("로봇청소기를 켭니다.");
}
@Override
public void turnOff() {
System.out.println("로봇청소기를 끕니다.");
}
// 로봇청소기에 맞는 정보조회
@Override
public void showStatus() {
System.out.println("모델명 : " + this.modelName);
System.out.println("가격 : " + this.price);
}
}
public class MainClass {
public static void main(String[] args) {
// 인터페이스 역시 구현체를 다형성 형식으로 받을 수 있음
RemoteController rc = new TVRemoteController(50);
//RemoteController rc = new RobotCleanerRemoteController("imou", "280000");
rc.turnOn();
rc.showStatus();
rc.turnOff();
}
}
위와 같이 인터페이스를 이용하여 해당하는 타입의 객체를 생성할 수 있고, 해당 객체에서 구현한 함수에 따라 내용이 다르게 출력, 즉 다형성이 활용되는 것을 확인할 수 있다.
2. has - a 관계
- 상속의 전제는 is - a 관계
- 현실에서는 is - a 관계가 아니더라도 다른 객체의 기능을 쓸 수 있는 경우가 많음
→ has - a 관계로 설정해 객체 간 사용 구현
※ 현업에서는 상속보다 합성을 많이 이용한다.
public class Gun {
private int bullet;
private String modelName;
private String gunNumber;
public Gun(String modelName, String gunNumber) {
this.bullet = 5;
this.modelName = modelName;
this.gunNumber = gunNumber;
}
public void shoot() {
if(this.bullet > 0) {
this.bullet--;
System.out.println("총을 쐈습니다.");
} else {
System.out.println("방아쇠를 당겼지만 총알이 없습니다.");
}
}
public void reload() {
this.bullet = 5;
}
}
public class Police {
// 상속 없이 Gun 기능을 사용하기 위해 멤버변수도 Gun을 가짐
private Gun gun;
private String name;
private int height;
public Police(Gun gun, String name, int height) {
this.gun = gun;
this.name = name;
this.height = height;
}
public void shoot() {
this.gun.shoot();
}
public void showStatus() {
System.out.println("소유총기 : " + this.gun);
System.out.println("총기이름 : " + this.name);
System.out.println("총기크기 : " + this.height);
}
}
public class MainClass {
public static void main(String[] args) {
// Gun을 new 키워드로 생성해야 Police 생성자에 전달 가능
Gun gun = new Gun("M-16", "369486");
// Gun을 사전에 생성하지 않으면 넘길 방법이 없음
Police police = new Police(gun, "나경찰", 180);
police.shoot();
police.shoot();
police.shoot();
police.shoot();
police.shoot();
police.shoot();
}
}
public class PopupStore {
public void orderApple() {
System.out.println("착즙 사과주스를 내줍니다.");
}
public void orderOrange() {
System.out.println("착즙 오렌지주스를 내줍니다.");
}
public void orderGrape() {
System.out.println("착즙 포도주스를 내줍니다.");
}
}
public class Store extends PopupStore{
@Override
public void orderApple() {
System.out.println("착즙 사과주스를 팝니다. 가격은 20000원");
}
@Override
public void orderOrange() {
System.out.println("착즙 오렌지주스를 팝니다. 가격은 24000원");
}
// 실수로 포도주스 가격 업데이트를 누락
}
public class MainClass {
public static void main(String[] args) {
// 2가지 문제점 체크
// 1. 정식매장이 존재하는데 팝업스토어 생성 가능
PopupStore ps = new PopupStore();
// 2. 팝업스토어 클래스 내부에서 오버라이딩이 필수인 메서드가 누락될 수도 있음
PopupStore s = new Store();
s.orderApple();
s.orderOrange();
s.orderGrape();
}
}
위 예시와 같이 abstract 키워드를 이용하지 않았을 때 위의 2가지 문제점이 발생할 수 있다.
포도주스를 오버라이딩 하지 않으면 경고를 띄우게 해야 하지 않을까? → 추상화 적용
PopupStore(부모타입)으로 Store(자식) 객체 형변환이 이루어짐
부모타입으로 선언한 객체에서도 자식이 오버라이딩했던 함수의 내용이 출력되는 것을 확인할 수 있다. (Promotion-자동형변환)
1) abstract class
- new 키워드를 이용하여 객체 생성 불가
- 오직 상속을 통해서 자식클래스로 구체화 시켜야 함
- new를 사용하여 직접 생성자를 호출할 수는 없지만 자식 객체가 생성될 때 super()를 호출하여 추상 클래스 객체를 생성하므로 추상 클래스도 생성자가 반드시 있어야 함
- 추상 메서드를 하나 이상 보유하고 있다면 해당 클래스는 추상 클래스 → 무조건 abstract 키워드를 붙여줘야 함
2) abstract method
- 추상 클래스 내에서만 선언 가능
- 추상 메서드는 메서드의 선언부만 있고, 메서드 실행 내용이 들어가는 중괄호 {}가 없는 메서드를 말함
- 추상 클래스 설계 시 자식 클래스가 반드시 실행 내용을 채우도록 강요하고 싶은 메서드가 있을 경우 해당 메서드를 추상 메서드로 선언
- 자식 클래스에서 반드시 부모 추상 클래스의 추상 메서드를 재정의하여 실행 내용을 작성해야 함
3) abstract 사용 예제
public abstract class PopupStore {
// 1. 메서드에 abstract를 붙이면 해당 메서드는 추상메서드가 되고, 이 메서드는 반드시 오버라이딩 해야 함
// 2. 추상메서드는 상속을 목적으로 선언한 메서드
// 실행을 목적으로 선언된 메서드가 아니므로 메서드의 몸체({}) 부분이 없고 선언 마무리도 ;로 함
// 3. 일반 클래스에는 추상 메서드 선언 불가
// 추상메서드가 하나 이상 존재하면 무조건 추상클래스로 선언
// 4. 추상클래스 내부에서는 추상메서드가 하나 이상 존재한다면 일반메서드 선언도 여전히 가능
public abstract void orderApple();
public abstract void orderOrange();
public abstract void orderGrape();
public void refund() {
System.out.println("제품에 문제가 있어서 환불합니다.");
}
}
public class Store extends PopupStore {
@Override
public void orderApple() {
System.out.println("착즙 사과주스를 20000원에 팝니다.");
}
@Override
public void orderOrange() {
System.out.println("착즙 오렌지주스 24000원에 팝니다.");
}
@Override
public void orderGrape() {
System.out.println("가격은 못 정했습니다.");
}
}
public class ConvenientStore extends PopupStore {
@Override
public void orderApple() {
System.out.println("가당 사과주스 4000에 팝니다.");
}
@Override
public void orderOrange() {
System.out.println("가당 오렌지주스 5000에 팝니다.");
}
@Override
public void orderGrape() {
System.out.println("가당 포도주스 3500에 팝니다.");
}
}
public class MainClass {
public static void main(String[] args) {
// PopupStore 클래스는 직접 객체 생성 불가능
// PopupStore ps = new PopupStore();
// PopupStore s = new Store();
PopupStore s = new ConvenientStore();
// 객체 종류에 따라 실행구문이 다르게 정의되었지만 명세는 같은 메서드
s.orderApple();
s.orderOrange();
s.orderGrape();
// 어떤 객체가 와도 공통적으로 실행되는 메서드
s.refund();
}
}
※ 템플릿 메서드 패턴 활용
public abstract class Lottery {
// 템플릿 메서드 패턴은 큰 틀에서 호출구문은 구현메서드(실행문이 있는 메서드)로 정의해놓고
// 구현메서드가 호출하는 추상메서드들은 상속 후에 특징을 정하도록 만들어서
// 호출 순서는 그대로 가져갈 수 있도록 하되, 사용자가 특징만 정의하도록 하는 디자인 패턴
// 구현메서드는 큰 틀은 같지만, 세부사항이 달라질 수 있는 내용을 먼저 작성
public void lotteryCycle() {
// 1. 어디서 사는가?
buyLottery();
// 2. 당첨 여부 확인
checkWinLottery();
// 3. 당첨 시 수령
getLotteryMoney();
}
// 세부사항은 상속받은 주체가 무엇인지에 따라 다르게 정의할 수 있도록
// 추상메서드만 정의해놓고, 추가적인 작업은 하지 않음
abstract void buyLottery();
abstract void checkWinLottery();
abstract void getLotteryMoney();
}
public class KoreanLotto extends Lottery {
@Override
public void buyLottery() {
System.out.println("한 게임에 천원짜리 로또를 삽니다.");
}
@Override
public void checkWinLottery() {
System.out.println("45C6의 확률을 뚫고 1등에 당첨되었습니다.");
}
@Override
public void getLotteryMoney() {
System.out.println("1등 상금으로 대략 수십억을 받았습니다.");
}
}
public class StatesSuperball extends Lottery {
@Override
public void buyLottery() {
System.out.println("미국 가서 슈퍼볼 복권을 삽니다.");
}
@Override
public void checkWinLottery() {
System.out.println("69C5 * 26C1 분의 1의 확률로 당첨되었습니다.");
}
@Override
public void getLotteryMoney() {
System.out.println("당첨 금액은 최소 수천억원입니다.");
}
}
public class MainClass {
public static void main(String[] args) {
//Lottery lottery = new KoreanLotto();
Lottery lottery = new StatesSuperball();
lottery.lotteryCycle();
}
}
위와 같이 상속은 가능하지만 final 키워드가 붙은 method2는 자식 측에서 재정의할 수 없는 것을 확인할 수 있다.
3) final 변수
- 한 번 값을 할당하면 그 값을 변경할 수 없음
- 선언 시 초기화하는 방법과 생성자를 통하여 초기화하는 방법이 있는데, 초기화하지 않고 남겨두면 컴파일 에러 발생
public class Person {
// final 변수는 단 한 번 초기화될 수 있고 이후에는 변경이 불가능함
// 선언 시에 아예 직접 초기화를 해주거나 생성자에서 초기화를 해줘야 함
public final String nationality = "대한민국"; // 선언부 초기화
public final String name; // 이렇게 선언부에서 초기화를 안하면 생성자 초기화
public int age; // final이 안 붙은 멤버변수는 초기화 의무 X
public Person(String name) {
this.name = name;
}
}
public class MainClass {
public static void main(String[] args) {
Person kim = new Person("김자바");
//kim.nationality = "일본"; // final 변수 변경 불가
//kim.name = "채자바"; // final 변수 변경 불가
kim.age = 25;
System.out.println("국적: " + kim.nationality); // public이므로 호출 가능
System.out.println("이름: " + kim.name); // public이므로 호출 가능
System.out.println("나이: " + kim.age); // public이므로 호출 가능
}
}
위와 같이 final 키워드로 가장 처음 생성했던 값이 출력되는 것을 확인할 수 있다.
즉, final 키워드를 이용하여 변수를 생성하면 변수의 값은 변경할 수 없지만 호출하는 것에는 문제가 없다.
- final 배열 예시
public class Collector {
// 참조형 변수를 가진 경우 변수 자체의 주는 final이지만,
// 참조형 변수의 내부자료까지 바뀌지 않음을 보장하지 않음
public final String[] stickers = {"피카츄", "꼬부기", "미뇽"};
}
import java.util.Arrays;
public class MainClass2 {
public static void main(String[] args) {
Collector c1 = new Collector();
System.out.println(Arrays.toString(c1.stickers));
c1.stickers[0] = "파이리";
System.out.println(Arrays.toString(c1.stickers));
}
}
final 키워드로 생성한 c1 객체는 해당 배열이 있는 주소를 변경하는 것이 불가능할 뿐이고,
해당 배열이 가리키고 있는 참조형 변수의 내부자료까지는 변경 방지를 보장해주지 않는다.
따라서 위와 같이 배열 내부 요소가 변경된 값이 오류 없이 출력되는 것을 확인할 수 있다.
4) 상수(static final)
- 상수 : 불변의 값을 저장하는 필드
- 상수는 객체마다 저장할 필요가 없는 공용성을 가져야 하고,
여러가지 값으로 초기화될 수 없기 때문에 static과 final 제한자를 동시에 붙여 선언해야 함
- 상수 이름은 모두 대문자로 작성하는 것이 관례, 연결된 단어라면 "_"로 연결
// 상수를 선언할 때는 상수 집합을 만드는 목적으로 아래와 같이 클래스 선언
public class CountrySizes {
// 상수는 보통 public으로 풀고 사용 (값 변경이 불가능하고, 공용성을 띔)
public final static int KOREA_SIZE = 100431;
public final static int STATES_SIZE = 9833519;
public final static int THAILAND_SIZE = 513120;
}
public class MainClass {
public static void main(String[] args) {
// 상수만 모아둔 클래스 특성 상 클래스명이 곧 집합을 대표하는 이름이 됨
System.out.println(CountrySizes.KOREA_SIZE);
System.out.println(CountrySizes.STATES_SIZE);
System.out.println(CountrySizes.THAILAND_SIZE);
// 이를 잘 사용하는 예시는 자바의 Math 클래스가 있음
System.out.println(Math.PI);
System.out.println(Math.E);
}
}