본문 바로가기
TIL/JAVA

23.05.24

by J1-H00N 2023. 5. 24.

문제 발생

Tire kiaSampleTire = new KiaTire("KIA");

Car car1 = new Car(kiaSampleTire);

car1.tire.rideComfort(); // KIA 타이어 승차감은 60

위에서 이해한 내용대로라면 kiaSampleTire도 KiaTire을 통해 생성했더라도 자동 형변환에 의해 Tire로 변환되었을 텐데 

KiaTire kiaSampleTire = new KiaTire("KIA")로 생성 했을 때와 같은 결과가 나온다. 제대로 이해하지 못했다는 증거.

 

1. kiaSampleTire가 KiaTire의 객체로 생성되었고, KiaTire가 rideComfort를 오버라이딩 했기에 실행되는건가??

2. KiaTire와 Tire에 있는 super, this로 인해 Tire의 객체가 KiaTire로 연결 된건가?? <-- 이건 아니다 싶음

 

해결

kiaSampleTire가 KiaTire의 객체로 생성되었고 Tire 변수이므로, 부모 클래스인 Tire로 자동 형변환되어 KiaTire가 Tire로부터 상속받은 필드와 메서드만 사용가능 하다. 하지만 KiaTire만 가지고 있는 메서드에 접근 못할 뿐 해당 객체의 클래스는 여전히 KiaTire이다.

 

 

반환 타입에서의 다형성

Tire hankookTire = car1.getHankookTire(); // Tire hankkokTire = new HanKookTire("HANKOOK")
KiaTire kiaTire = (KiaTire) car2.getKiaTire();  // KiaTire kiaTire = new KiaTire("KIA")
Tire getHankookTire() {
        return new HankookTire("HANKOOK");
    }

Tire getKiaTire() {
        return new KiaTire("KIA");
    }
}

반환 타입의 다형성을 확인하는 위 코드에서 return만 생각해 첫번째 줄은 HanKookTire가 Tire로 자동 형변환 된거라고 이해하고 넘어갔다가 아래줄에서 KiaTire 객체를 KiaTire 변수로 생성하는데 왜 (KiaTire)를 통한 강제 형변환이 필요한지 이해가 안됐었는데, get~~ 메서드의 반환타입이 Tire이므로, return을 HankookTire나 KiaTire로 설정해도 자동 형변환이 일어나 Tire로 반환됐던 것이다. 그래서 첫 줄 코드는 Tire 객체를 Tire 변수로 생성한 것이였고, 둘째 줄 코드는 Tire 객체를 KiaTire 변수로 생성하려고 했기에 강제 형변환이 필요했던 것이다.

메서드를 사용할 때 메서드의 반환타입, 매개변수 등을 면밀히 체크할 필요성을 다시금 되새기는 계기가 되었다.

 

위와 같은 상황처럼 상속과 자동 형변환 등을 통해 꼬이다 보니 내가 생성한 객체가 내가 의도한 클래스의 객체인지, 해당 클래스 객체명의 원래 클래스 명을 체크하는 명령어가 instance of이다.

 

{대상 객체} instance of {클래스 이름} 과 같이 쓰며 반환값은 boolean이다.

더보기
package sparta_nbc.Syntax._instanceOf;

class Parent { }
class Child extends Parent { }

public class InstanceOf {
    public static void main(String[] args) {
        Parent p = new Parent();

        System.out.println(p instanceof Object); // true 출력
        System.out.println(p instanceof Parent); // true 출력
        System.out.println(p instanceof Child);  // false 출력

        Parent pc = new Child();

        System.out.println(pc instanceof Object); // true 출력
        System.out.println(pc instanceof Parent); // true 출력
        System.out.println(pc instanceof Child);  // true 출력 // pc가 Parent로 형변환이 되긴 했지만 원래 클래스명은 Child이므로

        Child c = new Child();

        System.out.println(c instanceof Object); // true 출력
        System.out.println(c instanceof Parent); // true 출력
        System.out.println(c instanceof Child); // true 출력
    }
}

 

추상 클래스

미완성된 클래스를 의미하며, 혼자서는 아무것도 못한다. 그래서 자식클래스가 상속받아 변수나 메서드를 추가하며 완성해야 객체를 생성할 수 있다.

추상 클래스는 추상 메서드를 포함할 수 있는데, 마찬가지로 미완성상태라 이 메서드는 혼자서 호출할 수 없고 자식 클래스가 상속받아 완성한 후에야 호출 가능

기존 부모 클래스와 자식 클래스의 관계는 부모 클래스가 먼저 만들어진 뒤 그 기능을 공유하는 자식 클래스가 만들어지는 것이였다면, 추상 클래스와 자식 클래스의 관계는 자식 클래스들 중 공통되는 부분을 모아 완성되지 않은 형태로 껍데기만 짜서 추상 클래스를 만드는 형식이다.

 

추상 메서드는 다른 메서드들 처럼 반환타입, 메서드 이름, 매개변수 등이 존재하지만, {}을 통해 구현하는 부분이 없다.

public abstract class 추상클래스명 {
	abstract 리턴타입 메서드이름(매개변수, ...);
}

그리고 이 추상클래스를 상속받는 자식 클래스는 추상 메서드를 반드시 오버라이딩해야 한다.

↓ 추상 클래스 사용 예시

더보기
public class BenzCar {
    String company; // 자동차 회사 : GENESIS
    String color; // 자동차 색상
    double speed;  // 자동차 속도 , km/h

    public double gasPedal(double kmh) {
        speed = kmh;
        return speed;
    }

    public double brakePedal() {
        speed = 0;
        return speed;
    }

    public void horn() {
        System.out.println("Benz 빵빵");
    }

}

 

public class AudiCar {
    String company; // 자동차 회사 : GENESIS
    String color; // 자동차 색상
    double speed;  // 자동차 속도 , km/h

    public double gasPedal(double kmh) {
        speed = kmh;
        return speed;
    }

    public double brakePedal() {
        speed = 0;
        return speed;
    }

    public void horn() {
        System.out.println("Audi 빵빵");
    }

}

 

public class ZenesisCar {
    String company; // 자동차 회사 : GENESIS
    String color; // 자동차 색상
    double speed;  // 자동차 속도 , km/h
    
    public double gasPedal(double kmh) {
        speed = kmh;
        return speed;
    }

    public double brakePedal() {
        speed = 0;
        return speed;
    }

    public void horn() {
        System.out.println("Zenesis 빵빵");
    }

}

 

public abstract class Car {
    String company; // 자동차 회사 : GENESIS
    String color; // 자동차 색상
    double speed;  // 자동차 속도 , km/h

    public double gasPedal(double kmh) {
        speed = kmh;
        return speed;
    }

    public double brakePedal() {
        speed = 0;
        return speed;
    }

    public abstract void horn();
}

 

인터페이스

인터페이스는 두 객체를 연결해주는 역할을 하는데, 상속 관계가 없는 다른 클래스들이 동일한 메서드를 구현해야할 때 유용

인터페이스의 구현 클래스들은 추상 클래스를 상속받은 클래스와 같이 인터페이스 내 메서드를 구현해야 하며, 여기서도 다형성을 적용할 수 있다.

  • 선언 : public interface 인터페이스명 {}
  • 인터페이스의 멤버변수는 모두 public static final이어야 하며, 생략 가능
  • 인터페이스의 메서드는 모두 public abstract이어야 하며, 생략 가능
  • 추상 클래스와 마찬가지로 단독으로 객체 생성을 할 수 없기 때문에 다른 클래스에서 구현해야 한다.
  • 인터페이스의 메서드는 추상클래스와 같이 abstract이므로 꼭 오버라이딩해야 하며, 일부만 오버라이딩해야 한다면 해당 클래스를 추상 클래스로 만들어야 한다.
  • 구현 : public class 클래스명 implements 인터페이스명 {}
  • 인터페이스 간의 상속도 가능하며, 이때는 extends로 상속한다.
  • 인터페이스 간의 상속은 다중상속이 가능하다.
더보기
public class Main implements C {

    @Override
    public void a() {
        System.out.println("A");
    }

    @Override
    public void b() {
		System.out.println("B");
    }
}

interface A {
    void a();
}
interface B {
    void b();
}
interface C extends A, B { }

여기서 인터페이스 C는 a(), b()를 가지고 있어 C를 구현하는 클래스는 a()와 b()를 오버라이딩한다.

 

아래와 같이 인터페이스의 구현은 상속과 같이할 수 있다.

public class Main extends D implements C {

    @Override
    public void a() {
        System.out.println("A");
    }

    @Override
    public void b() {
        System.out.println("B");
    }

    @Override
    void d() {
        super.d();
    }

    public static void main(String[] args) {
        Main main = new Main();
        main.a();
        main.b();
        main.d();
    }
}

interface A {
    void a();
}

interface B {
    void b();
}

interface C extends A, B {
}

class D {
    void d() {
        System.out.println("D");
    }
}

 

default(디폴트) 메서드

디폴트 메서드는 추상 클래스의 기본 구현을 제공하는 메서드이다.

앞에 default를 붙이고 추상 메서드와 달리 {}을 붙여야 한다. 그리고 추상 메서드가 아니기 때문에 상속받는 클래스가 꼭 재정의 할 필요는 없다.

 

static(스태틱) 메서드

인터페이스에서 스태틱 메서드 구현이 가능하다.

기존의 스태틱 메서드와 같이 객체없이 호출이 가능하며, 선언도 기존과 동일하다.

접근 제어자는 생략하면 컴파일러가 자동으로 public을 추가해준다.

interface A {
    static void aaa() {
        System.out.println("static method");
    }
}

public class Main implements A {
	public static void main(String[] args) {
        // static 메서드 aaa() 호출
        A.aaa();
    }
}

 

인터페이스의 다형성에서도 크게 다른 건 없다

자동 형변환 : 부모클래스 변수명 = new 자식클래스 처럼 인터페이스 변수명 = new 구현객체

더보기
public class Main {
    public static void main(String[] args) {
        // A 인터페이스에 구현체 B 대입
        A a1 = new B(); 
    }
}

interface A { }
class B implements A {}

강제 형변환 : 자식클래스 변수명 = (자식클래스) 부모클래스 처럼 구현객체 변수명 = (구현객체) 인터페이스변수명

더보기
public class Main {
    public static void main(String[] args) {

        // A 인터페이스에 구현체 B 대입
        A a1 = new B();
        a1.a();
        // a1.b(); // 불가능

        // B 강제 타입 변환
        B b = (B) a1;
        b.a();
        b.b(); // 강제 타입변환으로 사용 가능
    }
}

interface A {
    void a();
}
class B implements A {
    @Override
    public void a() {
        System.out.println("B.a()");
    }

    public void b() {
        System.out.println("B.b()");
    }
}

 

3주차 숙제 계산기 만들기

 

문제 발생 

추상 클래스 생성자 abstract와 static은 같이 못 쓴다고 한다.

 

시도1. 추상 메서드를 굳이 만들지 않고 추상 클래스에 static 메서드로 생성한다. <= 오버라이딩도 안되므로 추상 클래스를 만드는 의미가 없어짐

시도2. 기존 메서드에서 static을 없앰 <= static의 성질 때문에 따로 변수를 만들지 않아서 번거로움이 있음

 

해결 방안

기존 메서드들의 static을 없애고 Calculator 클래스에 변수를 만들어 operator클래스들과 상관관계를 새로 만듦

 

알게 된 것

abstract와 static은 같이 못 쓴다

 

↓완성코드

더보기
package sparta_nbc.HW.HW_03;

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Calculator calculator = new Calculator();
        Scanner scanner = new Scanner(System.in);
        String operator = scanner.nextLine();
        int firstNumber = scanner.nextInt();
        int secondNumber = scanner.nextInt();
        System.out.println(firstNumber + " " + operator + " " + secondNumber + " = " + calculator.calculate(operator, firstNumber, secondNumber));
    }
}

 

package sparta_nbc.HW.HW_03;

/*
더하기, 빼기, 나누기, 곱하기 연산을 수행할 수 있는 Calculator 클래스를 만듭니다.

Step1
1. Calulator 클래스는 연산을 수행하는 반환타입이 double 인 calculate 메서드를 가지고 있습니다.
2. calculate 메서드는 String 타입의 operator 매개변수를 통해 연산자 매개값을 받습니다.
3. int 타입의 firstNumber, secondNumber 매개변수를 통해 피연산자 값을 받습니다.
4. calculate 메서드는 전달받은 피연산자, 연산자를 사용하여 연산을 수행합니다.

Step2
나머지 연산자(%)를 수행할 수 있게 Calculator 클래스 내부코드를 변경합니다.

Step3
AddOperation(더하기), SubstractOperation(빼기), MultiplyOperation(곱하기), DivideOperation(나누기)
연산 클래스를을 만든 후 클래스간의 관계를 고려하여 Calculator 클래스와 관계를 맺습니다.
- 관계를 맺은 후 필요하다면 Calculator 클래스의 내부코드를 변경합니다.

Step4
AddOperation(더하기), SubstractOperation(빼기), MultiplyOperation(곱하기), DivideOperation(나누기)
연산 클래스들을 AbstractOperation(추상 클래스)를 사용하여 추상화하고 Calculator 클래스의 내부 코드를 변경합니다.
*/

public class Calculator {
    String operator;
    int firstNumber;
    int secondNumber;
    //Step1
    public double calculate(String operator, int firstNumber, int secondNumber) {
        double result = 0.0; // result 값 초기화
        this.operator = operator;
        this.firstNumber = firstNumber;
        this.secondNumber = secondNumber;
        AddOperation add = new AddOperation();
        SubstractOperation sub = new SubstractOperation();
        MultiplyOperation multi = new MultiplyOperation();
        DivideOperation div = new DivideOperation();
        // Step2
//        if (operator.equals("%")) {
//            result = (double) (firstNumber % secondNumber);
//        }
        // Step3
//        if (operator.equals("+")) {
//            result = AddOperation.addOperation(firstNumber, secondNumber); // static 클래스이므로 가능
//        } else if (operator.equals("-")) {
//            result = SubstractOperation.substractOperation(firstNumber, secondNumber);
//        } else if (operator.equals("*")) {
//            result = MultiplyOperation.multiplyOperation(firstNumber, secondNumber);
//        } else if (operator.equals("/")) {
//            result = DivideOperation.divideOperation(firstNumber, secondNumber);
//        }
        // 모든 조건이 .equals이므로 조건문을 switch 로 변경
        switch (operator) {
            case "+" :
                result = add.operate(firstNumber, secondNumber);
                break;
            case "-" :
                result = sub.operate(firstNumber, secondNumber);
                break;
            case "*" :
                result = multi.operate(firstNumber, secondNumber);
                break;
            case "/" :
                result = div.operate(firstNumber, secondNumber);
                break;
            default:
                System.out.println("'+', '-', '*', '/' 중 하나를 입력해 주세요");
                break;
        }
        return result;
    }
}

// Step3
class AddOperation extends AbstractOperation {
    @Override
    public double operate(int firstNumber, int secondNumber) {
        double result = firstNumber + secondNumber;
        return result;
    }
}

class SubstractOperation extends AbstractOperation {
    @Override
    public double operate(int firstNumber, int secondNumber) {
        double result = firstNumber - secondNumber;
        return result;
    }
}

class MultiplyOperation extends AbstractOperation {
    @Override
    public double operate(int firstNumber, int secondNumber) {
        double result = firstNumber * secondNumber;
        return result;
    }
}

class DivideOperation extends AbstractOperation {
    @Override
    public double operate(int firstNumber, int secondNumber) {
        double result = (double) firstNumber / secondNumber;
        return result;
    }
}

//Step4
abstract class AbstractOperation {
    abstract double operate(int firstNuber, int secondNumber);
}

문제 발생

문제 조건에서 매개변수를 변경하는 부분을 못 봤음.

↓해결 후 수정 코드

더보기
package sparta_nbc.HW.HW_03;

public class Main {
    public static void main(String[] args) {
        Calculator cal1 = new Calculator(new AddOperation());
        System.out.println(cal1.calculate(2, 3));

        Calculator cal2 = new Calculator(new SubstractOperation());
        System.out.println(cal2.calculate(5,2));

        Calculator cal3 = new Calculator(new MultiplyOperation());
        System.out.println(cal3.calculate(4,6));

        Calculator cal4 = new Calculator(new DivideOperation());
        System.out.println(cal4.calculate(7,2));
    }
}

 

package sparta_nbc.HW.HW_03;

/*
더하기, 빼기, 나누기, 곱하기 연산을 수행할 수 있는 Calculator 클래스를 만듭니다.

Step1
1. Calulator 클래스는 연산을 수행하는 반환타입이 double 인 calculate 메서드를 가지고 있습니다.
2. calculate 메서드는 String 타입의 operator 매개변수를 통해 연산자 매개값을 받습니다.
3. int 타입의 firstNumber, secondNumber 매개변수를 통해 피연산자 값을 받습니다.
4. calculate 메서드는 전달받은 피연산자, 연산자를 사용하여 연산을 수행합니다.

Step2
나머지 연산자(%)를 수행할 수 있게 Calculator 클래스 내부코드를 변경합니다.

Step3
AddOperation(더하기), SubstractOperation(빼기), MultiplyOperation(곱하기), DivideOperation(나누기)
연산 클래스를을 만든 후 클래스간의 관계를 고려하여 Calculator 클래스와 관계를 맺습니다.
- 관계를 맺은 후 필요하다면 Calculator 클래스의 내부코드를 변경합니다.

Step4
AddOperation(더하기), SubstractOperation(빼기), MultiplyOperation(곱하기), DivideOperation(나누기)
연산 클래스들을 AbstractOperation(추상 클래스)를 사용하여 추상화하고 Calculator 클래스의 내부 코드를 변경합니다.
*/

public class Calculator {
    AbstractOperation operation;

    public Calculator(AbstractOperation operation) {
        this.operation = operation;
    }
    //Step1
    public double calculate(int firstNumber, int secondNumber) {
        double result = 0.0; // result 값 초기화
        // Step2
//        if (operator.equals("%")) {
//            result = (double) (firstNumber % secondNumber);
//        }
        // Step3
//        if (operator.equals("+")) {
//            result = AddOperation.addOperation(firstNumber, secondNumber); // static 클래스이므로 가능
//        } else if (operator.equals("-")) {
//            result = SubstractOperation.substractOperation(firstNumber, secondNumber);
//        } else if (operator.equals("*")) {
//            result = MultiplyOperation.multiplyOperation(firstNumber, secondNumber);
//        } else if (operator.equals("/")) {
//            result = DivideOperation.divideOperation(firstNumber, secondNumber);
//        }
        // 모든 조건이 .equals이므로 조건문을 switch 로 변경
        result = operation.operate(firstNumber, secondNumber);
        return result;
    }
}

// Step3
class AddOperation extends AbstractOperation {
    @Override
    public double operate(int firstNumber, int secondNumber) {
        double result = firstNumber + secondNumber;
        return result;
    }
}

class SubstractOperation extends AbstractOperation {
    @Override
    public double operate(int firstNumber, int secondNumber) {
        double result = firstNumber - secondNumber;
        return result;
    }
}

class MultiplyOperation extends AbstractOperation {
    @Override
    public double operate(int firstNumber, int secondNumber) {
        double result = firstNumber * secondNumber;
        return result;
    }
}

class DivideOperation extends AbstractOperation {
    @Override
    public double operate(int firstNumber, int secondNumber) {
        double result = (double) firstNumber / secondNumber;
        return result;
    }
}

//Step4
abstract class AbstractOperation {
    abstract double operate(int firstNuber, int secondNumber);
}

 

오류와 예외에 대한 이해

 

오류(Error)는 일반적으로 회복이 불가능한 문제

  • 시스템 레벨 또는 환경적인 요인으로 발생
  • 코드의 문제로 발생하는 경우도 있으나 일반적으로 회복 불가능

예외(Exception)는 일반적으로 회복이 가능한 문제

  • 회복이 가능한 전제는 예외가 발생할 수 있다는 것을 인지하고, 대응했을 것
  • 현실적으로 코드레벨에서 할 수 있는 문제상황에 대한 대응은 “예외처리”에 속한다

예외의 종류

  • 컴파일 에러(예외)
    • 대부분 자바 프로그램의 규칙을 어겼을 때(문법 실수, 오타 등) 발생
    • 이 때는 그저 문법에 맞게 코드를 수정하기만 하면 됨
  • 런타임 에러(예외)
    • 이제부터 주로 다를 예외
    • 문법적인 오류가 아닌 프로그램이 실행되는 도중 맞닥뜨리게 되는 예외

예외의 종류

  • 확인된 예외
    • 컴파일 시점에서 확인되는 예외
    • 이미 해당 예외가 일어날 것을 인지해 정의했기 때문에 컴파일 과정에서 확인할 수 있는 것이지, 컴파일 예외는 아니다.
  • 미확인된 예외
    • 런타임 시점에 확인되는 예외
    • 예외 처리가 반드시 필요하지 않는 예외

'TIL > JAVA' 카테고리의 다른 글

23.05.26  (0) 2023.05.26
23.05.25  (0) 2023.05.25
23.05.23  (0) 2023.05.23
23.05.22  (0) 2023.05.22
23.05.04  (0) 2023.05.04