프로그램 오류의 종류

프로그램이 실행 중 어떤 원인에 의해서 오작동을 하거나 비정상적으로 종료되는 경우를

프로그램 에러 또는 오류라고 한다.

이를 종류로는 3가지 종류로 나눌 수 있다.

  • 컴파일 에러 : 컴파일 시에 발생하는 에러
  • 런타임 에러 : 실행 시에 발생하는 에러
  • 논리적 에러 : 실행은 되지만, 의도와 다르게 동작하는 것

자바에서는 이러한 에러중 런타임 에러에 대해서 다음과 같이 2가지로 구분하였다.

  • 에러 : 프로그램 코드에 의해서 수습될 수 없는 심각한 오류
  • 예외 : 프로그램 코드에 의해서 수습될 수 있는 다소 미약한 오류

에러는 프로그램 실행 도중 발생하면 복구 할 수 없는 심각한 오류들을 말한다.

예를들어 우리가 StackOverFlow같은 것이나 메모리누수 이러한 것들을 말한다.

예외는 프로그램에 의해 수습을 할 수 있다.

예외 클래스의 계층구조

이러한 에러와 오류는 Throwable이라는 클래스에 상속되어 있다.

Throwable은 결과적으로 클래스이기 때문에 Object에 상속이 되어있다.

ExceptionThrowableObject

ErrorThrowableObject

와 같은 계층 구조로 이루어져 있다.

이중 예외의 최고조상은 Exception클래스 이며 이러한 예외 클래스는 두 그룹으로 나누어진다.

  1. RuntimeException클래스와 그 자식들
  2. RuntimeException을 제외한 Exception클래스와 그 자식들

첫번 째 그룹은 보통 프로그래머의 실수로 발생하는 예외이다. 예를들어 NullpointerException같은 오류들이 있다.

두번 째 그룹은 보통 사용자의 실수와 같은 외적인 요인에 의해 발생하는 예외이다.

예를들어 프로그램을 사용하고있는 사용자가 입력 데이터형식을 잘못해서 들어가서 DataFormatException을 발생하거나 하는 것이다.

예외 처리하는 법

이러한 에러가 아닌 예외는 프로그래머가 미리 이에 대한 처리를 해줄 수 있다.

이러한 예외가 일어나면 프로그램이 종료되는데 이러한 것을 방직하고 정상적인 실행상태를 유지할 수 있게 하는 것이다.

이러한 예외를 처리하기 위해서는 try-catch문을 다음과 같이 사용한다.

try{
    //예외가 발생할 가능성이 있는 문장을 삽입
} catch (Exception1 e1){
        //Exception1 이 발생하면 이를 처리할 문장을 적는다.
} catch (Exception2 e2){

}

이러한 catch블럭은 여러개가 붙을 수 있지만 발생한 종류와 일치하는 단 한개의 catch블럭만 실행이 된다.

또한, try-catch문은 {}를 생략할 수 없다는 것을 알아두자

try{
    try{

    }catch(Exception e1){

    }
}catch (Exception e){

}

그리고 이렇게 try-catch문 안에 다시 반복해서 try-catch문을 넣는 행위도 금지한다.

또한, Exception의 참조변수는 중복될 수 없다. 그렇기 때문에 e를 썼다면 다른 예외의 참조변수는 다른 단어를 써야 할 것 이다.

try-catch문 예제

public static void main(String[] args) {

        int number = 100;
        int result = 0;
        System.out.println(number / result);

}

이러한 경우 다음과 같은 에러가 발생하여 프로그램이 강제 종료된다.

Exception in thread "main" java.lang.ArithmeticException: / by zero

이러한 ArithmeticException을 해결하기 위해서 try-catch문을 써보자

public static void main(String[] args) {
        int number = 100;
        int result = 0;
        try{
            System.out.println(number / result);
        }catch (ArithmeticException e){
            System.out.println(e);
            System.out.println("오류가 발생하여 0으로 출력됩니다.");
        }
}

이렇게 try-catch문으로 ArithmeticException을 예외처리해주면 catch 블럭 안에 있는 코드가 실행이 된다.

만약에 오류가 없다면 catch문장을 굳이 수행하지는 않는다.

그렇다고 오류가 있어도 무조건 들어가는 것이 아니라 **catch에 선언되어 있는 오류만 예외처리 할 수 있다.**

또한, 참조변수 e는 오류 내용을 그대로 가지고 있다. 그렇기 때문에 출력하면 다음과 같은 결과를 가진다.

java.lang.ArithmeticException: / by zero

이렇게 System.out.println이 있지만 이를 출력하는 다른 방법도 있다.

이중 오류 메세지인 / by zero만 출력하고 싶으면 getMessage()메서드를 사용하면 되고

모든 것을 다 출력하고 싶다면 printStackTrace()를 출력하면 된다.

catch (ArithmeticException e){
    e.printStackTrace();// java.lang.ArithmeticException: / by zero at codewords02.main(codewords02.java:12)
    System.out.prinln(e.getMessage()); // / by zero
}

이렇게 출력이 된다.

멀티 catch블럭

예외처리마다 똑같은 행위를 하고싶다면 예를들어 단순히 출력하는것이다.

이러한 경우 예외의 수가 많아지면 catch블럭 줄이 엄청 길어지게 되기 마련이다.

이를 해결하기위해서 JDK1.7부터 |를 사용해 예외들을 묶을 수 있게 되었다.

try{
    //...
}catch(Exception1 | Exception2 e){
    /..
    e.printStackTrace();
}

이런식으로 하나의 참조변수에 예외들을 묶을 수 있다.

하지만 이것도 규칙이 있다.

만약 Exception1Exception2가 서로 부모와 자식 관계에 있다면 컴파일 에러가 발생한다

왜냐면 부모와 자식관계에 있다면 부모 클래스만 적어도 자식클래스의 예외처리를 해줄 수 있기 때문이다.

사실 허용할 수 있는 정도지만 불필요한 코드를 제거하라는 의미에서 에러가 발생한다.

또한, 주의해야할 점이 참조변수가 같아진것 이기 때문에

어떤 오류가 발생할지 모르는 시점이기 때문에 자신의 멤버를 사용할 수 없고 부모의 멤버만 사용한다.

물론 장점도 있지만 이러한 점이 있기 때문에 겹치는 멤버나 메서드에 한에서 사용하기를 권장한다.

예외를 발생시키는 법

프로그래머는 다음과 같은 과정으로 예외를 고의로 발생시킬수 있다.

  1. 연산자 new를 이용하여 Exception객체를 생성하는 것이다
    • Exception e = new Exception("오류 내용");
  2. 키워드 throw를 사용하여 예외를 발생시킨다.
    • throw e;
    • throw new Exception();

하지만 이러한 예외중에서도 RuntimeException을 고의로 발생시킬 때는 컴파일이 잘되는 것을 볼 수 있다.

왜냐하면 RuntimeException과 그 자식 클래스들을 프로그래머에 의해 실수로 발생하는 것들이기 떄문에 예외처리를 강제하지 않는다는 것이다.

이들은 unchecked예외라고 불린다. 나머지들은 checked예외라고 불리낟.

메서드에 예외 선언하는 법

메서드에 예외를 선언하기 위해서는 throws를 사용하여 처리할 수 있다.

void method() throws Exception1, ....[

}

다 귀찮다 하면 예외의 최고 조상인 Exception클래스를 메서드에 선언하면 모든종류의 예외가 발생할 가능성이 있다는 것을 표시한다.

이렇게 예외를 선언한다면 자식 클래스가 상속하여 오버라이딩할 때도 예외가 발생할 수 있다는 점에 주의해야한다.

그렇기 때문에 자바는 메서드를 작성할 때 메서드 내에서 발생할 가능성이 있는 예외를 메서드의 선언부에 명시하여 이 메서들ㄹ 사용하는 쪽에서는 이에 대한 처리를 하도록 강요하기 때문에 좀더 견고한 프로그램 코드를 작성하게 도와준다.

그렇기 때문에 메서드를 사용할 때 API를 보고 throws목록을 확인하고 예외를 처리해줘야할 생각을 하는 것이 좋다.

예외의 호출 스택(Call Stack)

예외는 처리를 하지 않으면 호출한 메서드에게 넘겨주게된다.

public static void main(String[] args) throws Exception
{
    method();
} 

public static void method() throws Exception{
    throw new Exception();
}

이러면 다음과 같은 오류 메세지가 등장하게된다.

image

오류가 등장하는것을 보면 mehtod()에서 먼저 오류가 발생한뒤 처리를 해주지 않아 method를 호출한 main에게 넘겨주고 main에서도 처리를 안해주어 결국 예외가 발생한것으로 보인다.

이를 해결하기위해서는 두가지 방법이있다.

try-catch를 이용하여

  1. main메서드에서 처리해주는것
  2. mehtod()에서 처리를 해주는것

둘중 어느 것을 사용해도 무방하다

하지만 첫번째 방법은 보통 메서드 내에서 자체적으로 해결이 안되는 경우에 사용한다. 예를들어 호출값을 다시 받아서 처리해주는 경우가 있다.

그게 아니라면 보통은 예외가 발생한 해당 메서드에서 처리해주는 것이 좋다.

Finally블럭

Finally블럭은 예외 발생여부에 상관없이 실행되어야할 코드를 포함시킬 목적으로 사용된다.

try-catch문의 끝에 선택적으로 덧붙여 사용하면된다.

try{

}catch(Exception e1){

}finally{

}

중요한것은 예외가 발생하지 않아도 실행되고 혹은 예외가 발생해도 실행된다는 것이다.

try-with-recsoorces문

JDK 1.7부터 새로 추가된 기능으로 보통 파일 입출력에서 사용한다.

왜 입출력에서 자주쓰이는지는 다음 예를 살펴보자

try{
    fileInputStream = new FileInpuStrem("score.bat");
    dataInputStream = new DataInputStream(fileInputStream);
    //...
} catch(IOException e){
    e.printStackTrace();
} finally{
    dataInputStream.close();
}

이러한 파일 입출력은 사용한 후에 꼭 close()를 해주어야 사용된 자원이 반환된다.

이러한 특성을 지니고 있기 때문에 만약 예외가 발생하면 close가 되지않아 자원이 반환되지 않기 때문에 finally에서 항상 close()를 해주고 있다.

하지만 이 코드에는 문제점이 있다. close()라는 메서드에서도 오류가 발생할 수 있다는 것이다.

프로그래머가 잘못해서 다른 곳에서 close()이미 했다던지 혹은, 파일이 없어서 close()할 파일도 없다던지 하는 오류가 발생할 수 있다. 그래서 다음과 같이 작성해야한다.

 try{
    fileInputStream = new FileInpuStrem("score.bat");
    dataInputStream = new DataInputStream(fileInputStream);
    //...
} catch(IOException e){
    e.printStackTrace();
} finally{
    try{
        if(dataInputStream != null)
            dataInputStream.close();
    }catch (IOException e){
            e.printStackTrace();
    }
}

이런식으로 finally블럭에 try-catch문을 추가하여 close()에서 발생할 수 있는 예외를 처리하도록 변경하였다.

이러한 문장을 try-with-resources문으로 고친다면 다음과 비슷하게 사용될 수 있다.

try (FileInpuStream fileInputStream = new FileInputStream("score.bat");
        DataInputStream dataInputStream = new DataInputStream(fileInputStream)){
    while(true){
        score = dataInputStream.readInt();
        System.out.prinln(score);
        sum += score;
    } catch (EOFException e) {
        System.out.println("점수의 총합은 " sum + "입니다.");
    } catch (IOException ie){
        ie.printStackTrace();
    }
}

이렇게 변경하면 앞선 코드보다 복잡하지 않고 좋다.

현재 try의 괄호 안에 객체를 생성하는 문장을 넣었는데 이는 try를 벗어나자마자 자동적으로 close()를 호출하게 된다.

하지만 사용하기 위해서는 객체의 클래스가 AutoCloseable이라는 인터페이스를 구현한 것이어야만 한다.

사용자 정의 예외 만들기

기존 정의된 예외말고 자신이 필요한 예외클래스를 새롭게 정의 할 수 있다.

Exception클래스나 RuntimeException클래스로부터 보통은 상속받아 클래스를 작성한다.

// 예시 1
class MyException extends Exception{
    private final int ERR_CODE;

    MyException(String message){
        super(message, 100);
    }

    MyException(String message, int errCode){
        super(message);
        ERR_CODE = errCODE;
    }

    public int getErrCode(){
            return ERR_CODE;
    }        
}

클래스를 만약 Exception을 하면 checked예외로 처리가 된다.

만약 RuntimeException을 상속 받았다면 nonchecked예외로 처리할 수 있어서 더 환영받고 있다.

예외 되던지기

만약, 한 메서드에서 발생할 수 있는 예외가 여럿인 경우는 다음과 같이 처리할 수 있다.

  • 몇 개는 try-catch문을 이용하여 메서드 내에서 자체로 처리한다.
  • 나머지는 선언부에 지정하여 호출한 메서드에서 처리한다.

이런식으로 양쪽을 통해 처리할 수 있다. 또한, 이런 방법으로도 예외가 단 하나인 상황에서도 처리할 수 있다.

이렇게 예외가 단 하나인 경우에는 예외를 처리한 후에 다시 발생시키는 방법을 통해서 가능하다.

이를 예외 되던지기라고 한다.

예외 되던지기를 사용하는 방법은 다음과 같다.

  • 예외가 발생할 가능성이 있는 메서드에서 try-catch문을 사용하여 예외를 처리해준다.
  • catch문에서 필요한 작업을 행한 후 throw문을 이용하여 예외를 다시 발생시킨다.
  • 다시 발생한 예외를 이 메서드를 호출한 메서드에게 전달되고 호출한 메서드의 try-catch문에서 예외를 또 다시 처리한다.

예외 되던지기를 사용할 때는 하나의 예외에 대해서 예외가 발생한 메서드와 이를 호출한 메서드 양쪽 모두에서 처리해줘야할 작업이 있을 때 사용한다.

// 예제
public static void main(Stirng[] args){

    try{
        method();
    } catch (Exception e){
        System.out.println("main 메서드에서 예외가 처리되었습니다");
    }
} 

static void method() throws Exception{
    try{
        throw new Exception();
    } catch (Exception e){
        System.out.println("method 에서 예외가 처리되었습니다.");
        throw e;
    }
}

이런식으로 main에서도 try-catch를 이용하여 처리를 해주고 method에서 예외를 처리하고 예외를 방생시켜서 사용한다.

연결된 예외(chained exception)

한 예외는 다른 예외를 발생시킬 수 있다.

다음 예시는 SpaceException을 원인 예외로하는 InstallException을 발생시키는 예시이다.

public class ChainedException {
    public static void main(String[] args) {
        try{
            install();
        } catch (InstallException e){
            e.printStackTrace();
        }
    }

    static void install() throws InstallException{
        try{
            startInstall();
            copyFile();
        } catch (SpaceException se){
            InstallException ie = new InstallException("설치중 예외 발생");
            ie.initCause(se);
            throw ie;
        } catch (MemoryException me){
            InstallException ie = new InstallException("설치중 예외 발생");
            ie.initCause(me);
            throw ie;
        } finally {
            deleteTempFiles();
        }
    }

    static void startInstall(RuntimeException ) throws SpaceException, MemoryException{
        if(!enoughSpace()){
            throw new SpaceException("설치할 공간이 부족합니다.");
        }

        if(!enoughMemory()){
            throw new MemoryException("메모리가 부족합니다.");
            throw new RuntimeException(new MemoryException("메모리가 부족합니다."));
        }
    }

    static void copyFile(){

    }

    static void deleteTempFiles(){

    }

    static boolean enoughSpace(){

        return false;
    }

    static boolean enoughMemory(){

        return true;
    }
}

class InstallException extends Exception{
    InstallException(String message){
        super(message);
    }
}

class SpaceException extends Exception{
    SpaceException(String message){
        super(message);
    }
}

class MemoryException extends Exception{
    MemoryException(String message){
        super(message);
    }
}

일단 initCase()는 지정한 예외를 원인 예외로 지정하는 메서드이다. 또한, getCause()는 원인 예외를 반환하는 메서드이다.

원인 예외로 왜 등록해서 다시 예외를 발생하는지는 이유가 있다.

그냥 예외만 처리하면되는데 왜이렇게 복잡하게 하는지 궁금할텐데

그냥 예외만 바로 처리해버린다면 실제로 발생한 예외가 무엇인지 모르기 때문에 함부로 할 수 없다.

또한, checked예외라면 이를 unchecked에외로 바꿀 수 있기 떄문이다.

startInstall()메서드를 확인해보면 자세히 알 수 있다.

static void startInstall(RuntimeException ) throws SpaceException, MemoryException{
        if(!enoughSpace()){
            throw new SpaceException("설치할 공간이 부족합니다.");
        }

        if(!enoughMemory()){
            throw new MemoryException("메모리가 부족합니다.");
            throw new RuntimeException(new MemoryException("메모리가 부족합니다."));
        }
    }

이처럼 throw new RuntimeException으로 한번 감싸준다면 unchecked예외로 설정할 수 있다.

Reference

  • 자바의 정석

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

[Java/Study] 지네릭 타입  (0) 2021.09.19
[Java/Study] java.time 패키지  (0) 2021.09.18
[Java/Study] 내부 클래스  (0) 2021.09.11
[Java/Study] 인터페이스  (0) 2021.09.11
[Java/Study] 추상클래스  (0) 2021.09.11

파이참을 실행하니 다음과 같은 오류가 생겼다.

 

Internal error. Please refer to https://jb.gg/ide/critical-startup-errors

검색해보니 다음과 같은 명령어를 수행하면 쉽게 해결되는 것 같다.

 

Window에서 윈도우 키를 누르고 cmd를 검색 후 관리자 권한 실행을 한다.

그 후 명령창에서 다음과 같은 명령어를 입력한다.

net stop winnat
net start winnat

그러면 실행되는 것을 볼 수 있을 것이다.

내부 클래스란?

내부 클래스는 말 그대로 안에 선언된 클래스이다.

클래스 내에 선언된 클래스를 내부 클래스라고 한다.

내부 클래스를 사용하는 이유는 다음과 같다.

  • 내부 클래스에서 외부 클래스의 멤버들을 쉽게 접근할 수 있다.
  • 코드의 복잡성을 줄일 수 있다. (캡슐화)

이 때 사용하는 내부 클래스는 다른 클래스에서는 사용하지 않는 내용이여야 한다.

내부 클래스의 종류

  • 인스턴스 클래스
    • 외부 클래스의 메멉변수 선언위치에 선언한다.
    • 외부 클래스의 인스턴스멤버 처럼 다루어진다.
    • 주로 외부 클래스의 인스턴스멤버들과 관련된 작업에 사용될 목적으로 선언된다.
  • 스태틱 클래스
    • 외부 클래스의 멤버변수 선언위치에 선언한다.
    • 외부 클래스의 static멤버 처럼 다루어진다.
    • 특히 static메서드에서 사용될 목적으로 선언된다.
  • 지역 클래스
    • 외부 클래스의 메서드나 초기화블럭 안에 선언한다.
    • 선언된 영역 내부에서만 사용될 수 있다.
  • 익명 클래스
    • 클래스의 선언과 객체의 생성을 동시에 하는 이름없는 클래스이다. (일회용)

이러한 내부 클래스의 선언위치는 다음과 같다.

class InnnerClassTest{
    class 인스턴스클래스 {}
    static class 스태틱 클래스 {}

    void method(){
        class 내부클래스{}
    }

}

내부 클래스의 제어자와 접근성

내부클래스는 앞에 선언위치와 같이 그냥 Class 내부에 선언되는 것이다.

이러한 내부 클래스도 클래스이기 때문에 abstractfinal같은 제어자를 안에서 사용할 수 있다. 또한, 접근 제어자도 마음대로 사용할 수 있다.

내부 클래스에서 static을 사용하기 위해서는 static클래스로 정의를 해야한다.

그렇지 않으면 static을 사용해야하는데 클래스가 안읽혀서 static을 사용하지 못하는 모순이 생긴다.

하지만 final static은 상수로서 사용이 가능하다.

또한, static멤버는 인스턴스 멤버를 직접호출을 할 수 없다.

하지만 인스턴스 멤버는 인스턴스 멤버와 static멤버를 모두 직접호출이 가능하다.

static클래스는 외부의 클래스의 인스턴스 멤버를 객체 생성없이 사용할 수 없지만

인스턴스 멤버는 클래스의 인스턴스멤버를 객체 생성 없이 사용할 수 있다.

지역 클래스는 외부 클래스의 인스턴스 멤버와 static 멤버를 모두 사용할 수 있다.

또한, 지역클래스가 포함된 메서드에 정의된 지역변수도 사용할 수 있다.

final이 붙은 지역변수만 접근이 가능하다. 하지만 JDK1.8이후로는 final을 컴파일러가 자동적으로 붙여준다.

컴파일을 하면 외부 클래스명$내부 클래스명.class형식으로 된다. 하지만 안에 중복된 이름의 클래스가 있을 수도 있기 때문에 외부 클래스명$숫자 내부 클래스명.class로 이루어진다.

마지막으로 내부 클래스와 외부 클래스에 선언된 변수의 이름이 같을 떄가 있다.

이럴 때는 외부 클래스명.this를 붙여서 서로 구별 할 수 있다.

익명 클래스(anonoymous class)

익명 클래스는 내부 클래스와 다르게 일므이 없다.

클래스의 선언과 객체의 생성을 동시에 하기 때문에 단 한번만 사용될 수 있고 오직하나의 객체만 생성할 수 있는 일회용 클래스이다.

이는 다음과 같이 생성된다.

new 부모 클래스이름(){

}

new 구현 인터페이스이름(){

}

Object ob = new Object(){ void method() };

이는 이름이 없어서 생성자를 가질 수 없다.

또한, 구현하고자 하는 부모 클래스이름구현 인터페이스 이름을 사용하기 때문에

둘이상의 인터페이스를 구현할 수 없다.

하나의 클래스를 상속 받거나 단 하나의 인터페이스만을 구현할 수 있다.

익명 클래스를 사용하면 다음과 같은 형태로 클래스파일이 생성이된다.

외부 클래스명&숫자.class

Reference

  • 자바의 정석

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

[Java/Study] java.time 패키지  (0) 2021.09.18
[Java/Study] 예외처리  (0) 2021.09.14
[Java/Study] 인터페이스  (0) 2021.09.11
[Java/Study] 추상클래스  (0) 2021.09.11
[Java/Study] 다형성  (0) 2021.09.10

인터페이스란?

인터페이스는 일종의 추상클래스이다.

인터페이스는 추상클래스보다 추상화 정도가 높다.

그래서 오직 추상메서드상수만을 멤버로 가질 수 있다.

추상클래스가 미완성 설계도이면 인터페이스는 그냥 설계도이다.

인터페이스의 작성법

인터페이스를 작성하기 위해서는 다음과 같이 작성한다.

interface 인터페이스이름{
        public static final 타입 상수이름 = 값;
        public abstract 메서드이름(매개변수);
}

인터페이스의 이러한 작성법을 본다면 특이한 사항이 있다.

인터페이스를 작성하기 위해서는 지켜야할 제약사항이 존재한다.

  • 모든 멤버변수는 public static final이어야 하며 이를 생략할 수는 있다.
  • 모든 메서드는 public abstract이어야 하며 이를 생략할 수 있다.
  • static메서드와 default메서드는 JDK1.8부터 예외로 한다.

만약 이러한 public static final이나 public abstract를 생략을 한다면 알아서 컴파일러가 컴파일시 자동으로 추가해 준다.

인터페이스의 특징들

인터페이스의 상속

인터페이스는 인터페이스 끼리만 상속이 가능하다. 또한, 클래스와 달리 다중상속을 허용한다.

또한, 인터페이스는 클래스와 달리 Object같은 최고 부모가 존재하지 않는다.

인터페이스의 구현

인터페이스를 클래스에 사용할 때 구현이라는 말이 쓰인다.

인터페이스는 추상클래스와 비슷하다고 하지 않았는가?

그렇기 때문에 추성클래스와 비슷하게 인스턴스를 그 자체로 생성이 불가능하다.

클래스 간의 상속은 extends확장이라는 의미로 사용하지만

인터페이스는 달리 이러한 설계도를 완성을 해야한다는 의미이기 때문에 구현이라는 의미로 implements를 사용하여 클래스를 사용한다.

class 클래스이름 implements 인터페이스 이름{

}

//예시
class Dog implements Animal{
        /...
}

인터페이스를 implements한다면 이 인터페이스에 있는 모든 메서드를 구현해야 한다.

하지만 일부 메서드만 구현하고 싶을때도 있다. 그럴 때는 abstract를 붙여서 추상클래스로 만들면 가능하다.

abstract class Dog implements Animal{
        /...
}

지금까지 인터페이스구현이라는 단어를 사용하고 클래스확장이라는 단어를 사용하기 때문에

단어의 의미가 달라서 인터페이스를 사용하면 부모가 없다고 생각할 수도 있지만 그것은 아니다.

인터페이스로 부터 상속받은 추상메서드를 구현하는 것이기 때문에 지금까지 사용했던 부모 라는 의미와는 100% 같지는 않지만 조금 다른의미의 조상이라고 볼 수 있다.

접근제어자의 사용

다음과 같은 class와 인터페이스가 있다.

interface Animal{
    void feed(); // public abstract void feed();
}

class Dog implements Animal{
        public void feed(){

        }
}

이때 Dog클래스에서 feed()메서드를 사용하기 위해서는 반드시 public으로 선언을 해야 사용할 수 있다.

왜냐하면 오버라이딩 할때는 부모의 메서드보다 넓은 범위의 접근 제어자를 지정해야하기 때문에 public abstract보다 넓은 범위를 사용하기 위해서 public을 반드시 사용해야 한다.

인터페이스를 사용한 다중상속

자바에서는 다중 상속을 금지하는 이유는 만약 두 부모로 부터 상속받는 멤버변수중 겹치는 것이있다면 컴파일러는 어떤 조상의 것을 상속받아서 사용했는지 알수가 없기 때문에 이를 금지하였다.

하지만 인터페이스는 다중 상속이 된다는것이 이상하지 않는가?

왜냐하면 인터페이스static상수만 정의할 수 있기 떄문에 부모클래스의 멤버변수와 충돌하는 겨우는 거의 없다.

그리고 static상수는 보통 클래스 이름을 붙여서 사용하기 때문에 충돌되더라도 괜찮다.

만약, 메서드가 같아서 걱정이라면 당연히 부모 클래스의 메서드를 상속받으면되기 때문에 문제가 없다.

이렇게 인터페이스는 부모클래스와 충돌하지 않기 때문에 이를 이용하여 단일상속만 가능한 자바에서 다중상속을 구현할 수 있다.

다음과 같은 예제를 살펴보자

public class Apple{
    protected int sweet;
    protected int growLevel;    

    public void feed(){
        growLevel++;
    }

public class Dessert{
        protected int price;

        public void eat(){
            //.. 먹는것을 구현
        }
        public int getPrice(){
            return price;
        }
        public void setPrice(int price){
            this.price = price;
        }
    }
}

이러한 Apple이라는 클래스와 Dessert라는 클래스를 다중상속하여 AppleDessert라는 클래스를 만들고 싶을 떄 단일상속을 원칙으로 하는 자바에서는 구현을 못할 것이라고 생각하지만

인터페이스를 사용하여 구현할 수 있다. 다음과 같은 인터페이스를 구현한다.

public interface InterfaceAppleDessert{
        public abstract void eat(); 
        public abstract int getPrice();
        public abstract void setPrice(int price);
} 

InterfaceAppleDessert라는 인터페이스는 Dessert클래스에 정의된 메서드와 일치하는 추상메서드를 가지는 인터페이스다 이를 이용하여 실제로 만들 AppleDessert라는 클래스를 구현해보자

public class AppleDessert extends Apple implements InterfaceAppleDessert{
        Dessert des = new Dessert();
        public void eat(){
            des.eat();
        }        
        public int getPrice(){
            return des.getPrice();
        }
        public void setPrice(int price){
            desc.setPrice(price);
        }
}

이렇게 InterfaceAppleDessert인터페이스 때문에 메서드를 반드시 오버라이딩 할 수 밖에 없다.

이러한 메서드를 Dessert인스턴스를 만들어 Dessert 메서드를 각각의 오버라이딩된 메서드에 호출하면 다중상속을 구현할 수 있다.

인터페이스를 이용한 다형성

다형성에서는 자식클래스의 인스턴스를 부모타입의 참조변수로 참조가 가능하다는 것을 배웠다.

인터페이스 또한, 이를 구현한 클래스의 부모라고도 볼 수 있기 때문에 이를 형변환하거나

인터페이스 타입의 참조변수로 이를 구현한 클래스의 인스턴스를 참조할 수 있다.

이전 예시를 찾아보자 Dog클래스와 Animal인터페이스를 살펴보면 다음과 같다.

// 예시 1번
Animal animal = (Animal)new Dog();

//예시 2번
Aniaml animal = new Dog();

또한, 클래스의 다형성과 같이 매개변수와 리턴타입에도 사용이 가능하다.

void bark(Animal animal){
    //...
}

Animal method(){
        Dog dog = new Dog();
        return dog;
}

리턴타입이 인터페이스이면 메서드가 해당 인터페이스를 구현한 클래스의 인스턴스를 반환한다는 것을 의미한다.

분명 method()의 반환타입은 Animal이라는 인터페이스인데 implementsDog클래스의 인스턴스가 반환되는 것을 볼 수 있다.

인터페이스의 장점

  • 개발시간을 단축시킬 수 있다.
    • 인터페이스가 작성되면 여러명이서 각각 인터페이스를 구현할 수 있다. 예를들어서 Animal이라는 인터페이스를 개발해놓으면 2명이서 Dog클래스를 만들필요없이 분담하여 Cat클래스도 만들 수 있는 것이다.
  • 표준화가 가능하다
    • 프로젝트의 기본틀을 인터페이스로 작성하여 다른 개발자들이 개발할 떄 또한, 일관되고 정형화된 프로그램의 개발이 가능하다.
  • 서로 관계없는 클래스들에게 관계를 맺어 줄 수있다.
    • 아무런 관계를 가지지 않고 부모클래스도 다른 클래스에게 여러개를 implements가 가능한 인터페이스를 사용하여 하나의 인터페이스를 공통적으로 구현하도록 함으로써 관계를 맺어 줄 수 있다.
  • 독립적인 프로그래밍이 가능하다
    • 인터페이슨느 클래스의 선언과 구현을 분리시킬 수 있기 때문에 실제구현에 독립적인 프로그램을 작성하는 것이 가능하다. 클래스와 클래스의 직접적인 관계를 인터페이스를 이용하여 간접적인 관계로 변경하면, 한 클래스의 변경이 관련된 다른 클래스에 영향을 미치지 않는 독립적인 프로그래밍이 가능하다.

인터페이스의 이해

인터페이스를 이해하기 위해서는 다음과 같은 두가지 사항을 염두해둬야한다.

  • 클래스를 사용하는 쪽과 클래스를 제공하는 쪽이 있다.
  • 메서드를 사용하는 쪽에서는 사용하려는 메서드의 선언부만 알면 된다.

다음과 같은 예시를 살펴보자

class A {
    public void methodA(B b){
        b.methodB();
    }
}

class B{
    public void methodB(){
        System.out.println("this is method B");
    }
}

public static void main(String[] args) {
        A a = new A();
        a.methodA(new B());
}

현재상황은 A클래스가 B의 인스턴스를 생성하고 메서드를 호출한다.

이때 A를 사용하는 쪽이라하고 B를 제공하는 쪽이라하자.

이렇게 직접적으로 연결되어 있다면 만약 B라는 클래스가 변경이된다면 예를들어 methodB()가 사라지거나 혹은 B라는 클래스가 변경이되면 A라는 클래서의 메서드도 변경을 반드시 해야한다.

이렇게 직접적으로 관계되어있는 둘의 관계를 바꾸기 위해서는 인터페이스를 사용하여 클래스의 선언과 구현을 분리해야한다. 다음과 같은 인터페이스를 생성해보자

interface I {
    public abstract void methodB();
}

class B implements I{
    System.out.println("this is method B");
}

class A{
        public void methodA(I i){
            i.methodB();
        }
}

이런식으로 인터페이스 I를 만들어서 B의 클래스를 implements하였다.

선언은 인터페이스가 맡아서 하였기 때문에 내가 만약에 이제 클래스 B를 쓰지 않는다고 해도

C라는 클래스를 생성하여 대체한다고 하여도 implementsC클래스에 한다면 문제가 없어진다.

혹은 다음과 같은 예제를 보자

interface PayInterface{
    public abstract void paymentMethod(); 
}

class Card implements PayMethod{
    public void paymentMethod(){
        System.out.println("카드로 결제하겠습니다.");
    }
}

class Cash implements PayMethod{
    public void paymentMethod(){
        System.out.println("현금으로 결제하겠습니다.");
    }
}

class PayService(){
    public void buy(PayInterface i){
            i.paymentMethod();
    }
}

public static void main(Stirng[] args){
    PayService service = new PayService();

    service.buy(new Card()); //카드로 결제하겠습니다.
     service.buy(new Cash()); //현금으로 결제하겠습니다.
}

인터페이스를 사용하여 내가 원하는 방식대로 인터페이스만 implements한다면 사이트에서 결제방식이 바뀌더라도 직접적으로 PayService와 결제방식 클래스를 둘다 안바꾸어도 되고 그냥 PayInterfaceimplements한 하나의 클래스만 바로 생성하면 바로 쓸수있다.

디폴트 메서드와 static메서드

이부분은 앞선 설명으로 알듯이 JDK1.8버전 이후에 생긴 것이다.

원래는 인터페이스는 추상 메서드만 선언이 가능하다.

java.util.Collection 인터페이스는 static메서드를 선언을 할 수 없었어서 Collections라는 킆래스에 들어가게 되었다.

static메서드는 많이 들어봤지만 디폴트 메서드는 처음일 것이다.

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

추상 메서드가 아니기 때문에 디폴트 메서드가 새로 추가된 해당 인터페이스를 구현한 클래스를 변경하지 않아도 된다. 굳이 메서드를 오버라이딩 할 필요가 없기 때문이다.

// 선언방법
default void newMethod() {}

앞에 default를 붙이면된다.

디폴트 메서드는 추상 메서드와 달리 ;만 붙여서 끝나는 것이 아닌 {}부분이 필요하다.

하지만 이러한 디폴트 메서드도 구현하다 보면 만약에 기존 메서드와 이름이 충돌되는 경우가 있을 것이다. 이럴 때는 다음과 같은 규칙을 지켜야한다.

  1. 여러 인터페이스의 디폴트 메서드 간의 충돌
    • 인터페이스를 구현한 클래스에서 디폴트 메서드를 오버라이딩해야 한다.
  2. 디폴트 메서드와 부모 클래스의 메서드 간의 충돌
    • 부모 클래스의 메서드가 상속되고, 디폴트 메서드는 무시된다.

그냥 편하게 하려면 디폴트 메서드라도 오버라이딩을 하면 그만이다.

Reference

  • 자바의 정석

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

[Java/Study] 예외처리  (0) 2021.09.14
[Java/Study] 내부 클래스  (0) 2021.09.11
[Java/Study] 추상클래스  (0) 2021.09.11
[Java/Study] 다형성  (0) 2021.09.10
[Java/Study] 제어자  (0) 2021.09.09

추상클래스란?

추상 클래스는 이전에도 설명했듯이 미완성 설계도에 비유할 수 있다.

미완성 메서드를 가지고 있기 때문에 이에 비유한다.

추상 클래스는 인스턴스를 생성할 수 없다.

상속을 통한 자식클래스에서 메서드의 내용을 구현하게 된다.

추상클래스를 사용하기 위해서는 다음과 같이 abstract키워드를 사용하면된다.

abstract class 클래스이름{

}

추상 메서드가 없더라도 앞에 abstract를 넣으면 추상 클래스로 사용이 가능하다.

추상메서드란?

추상 메서드는 선언만 해놓고 구현을 안해놓은 메서드라 할 수 있다.

실제 수행될 내용은 적어놓지 않았지만 이런 기능이 있을거다 라고 모호하게 작성한 것을 말한다.

왜 구현을 안하고 남겨 놓는 이유는 상속받는 클래스에 따라 메서드 내용이 달라질 수 있기 때문이다.

예를들어 Animal이라는 클래스에 추상클래스인 talk()가 있다면

이를 CatDog가 상속을 받았다면 서로 다른 내용으로 짖기 때문에 다를것 이다.

추상 클래스와 메서드는 다음과 같이 사용된다.

abstract class Animal{
    abstract void talk();
}

class Cat extends Animal{
    void talk(){
        System.out.println("미야옹");
    }
}

class Dog extends Animal{
        void talk(){
            System.out.println("멍멍");
        }
}

추상 메서드는 특이할 때 선언할때 abstract를 붙이고 마지막은 꼭 메서드와 다르게 ;를 붙여주는 것이 특징이다.

이러한 추상 클래스를 통해서 각 클래스에 공통된 메서드가 있다면 추상 클래스를 만들어 재사용하기 쉽게 만드는 것도 한가지 방법이다.

이렇게 공통된 부분으로 묶어서 추상 클래스를 만들어 상속시키면 다양한 장점이 생긴다.

  • 같은 부모를 가져 배열로 묶어서 관리할 수 있다.
  • 코드가 깔끔해진다.

Reference

  • 자바의 정석

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

[Java/Study] 내부 클래스  (0) 2021.09.11
[Java/Study] 인터페이스  (0) 2021.09.11
[Java/Study] 다형성  (0) 2021.09.10
[Java/Study] 제어자  (0) 2021.09.09
[Java/Study] Package 와 Import 및 터미널로 자바 실행방법  (0) 2021.09.08

자바에서 문자열의 앞뒤에 공백이 들어가는 경우가있다.

이런경우에 없애는 쉬운 메서드가 있다. trim()이다 사용방법은 다음과 같다.

String hello = " Hello World ";
System.out.println(hello.trim()); // "Hello World" 로 출력됨

이런식으로 가운데에 있는 공백은 지우지 않고 양 끝에 있는 공백만 지우게 된다.

'Java > 짜잘한팁' 카테고리의 다른 글

[Java/Tip] 문자열자르기  (0) 2021.09.11

보통 코딩테스트에서 문장을주고 단어를 재배열하거나 이런 문제를 많이 낸다.

문장에서 단어는 보통 띄어쓰기로 구분이 되어있는데 이를 배열에 저장하는 법이 있다.

split을 사용하면 쉽게 가능하다.

String[] result = words.split(" ");

이를 실행하면 자동으로 String배열에 띄어쓰기마다 잘라서 각 단어를 저장해준다.

이렇게 문자열을 자를 때 사용하는 메서드가 하나더 있다.

substring메서드를 사용하는 것이다.
substring(시작위치) 또는, substring(시작위치, 끝위치)로 할 수 있다.

String str = "hello";

String result = str.substring(1);
System.out.println(result); // ello 출력

String result2 = str.substring(0,1);
System.out.println(result2) // h 출력

'Java > 짜잘한팁' 카테고리의 다른 글

[Java/Tip] 문자열 앞뒤 공백 지우기  (0) 2021.09.11

다형성한 타입의 조변수로 여러 타입의 객체를 참조할 수 있도록 하는 것이다.

다시말하자면

조상클래스 타입의 참조변수로 자손클래스의 인스턴스를 참조할 수 있도록 하는것

다음과 같은 예제를 보자

class Car {
    private String engine;

    void turnOn()
    void turnOff()
    String getEngine()
    void setEngine(String engine)
}

class ElectroCar extends Car{
    String battery;
    void chargeBattery()
}

이런경우는 ElectorCarCar를 상속하고있다.

이럴때 인스턴스 객체를 다음과 같이 생성할 수 있다.

// 1번
Car car = new ElectroCar();
// 2번
ElectroCar= new ElectroCar();

이렇게 상속하는 관계일 경우에는 부모 클래스 타임의 참조변수로 자식 클래스의 인스턴스를 참조하도록 하는 것도 가능하다.

이렇게 상속하는 경우는 어떤 차이점이 있냐면

둘 다 같은 타입의 인스턴스지만 참조변수의 타입에 따라 사용할 수 있는 멤버의 개수가 달라진다.

저 예시로 cartesla는 사용할 수 있는 멤버의 개수가 다르다.

  • teslaElectroCar의 멤버인 batterychangeBatttery()메서드를 사용이 가능하다.
  • carbatterychageBattery()의 메서드 사용이 불가능하다.

그렇다면 이렇게 생각할 수도 있을 것이다.

자식 타입이 참조변수로 조상타입의 인스턴스를 참조하는 케이스이다.

ElectroCar car = new Car();

이러한 경우에는 항상 생각해야할 것이 있다.

클래스는 상속을 통해서 확장이 가능하다 하지만 축소는 될 수가 없다.
부모 인스턴스의 개수는 자식 인스턴스의 멤버의 개수보다 같거나 적어야한다.

부모타입의 참조변수 → 자식타입의 인스턴스 참조 가능

자손타입의 참조변수 → 부모타입의 인스턴스 참조 불가능

참조변수의 형변환

참조변수도 형변환이 가능하다 형변환할 수 있는 조건은 다음과 같다.

  • 서로 상속관계에 있는 클래스만 가능하다.
  • 자식타입의 참조변수를 부모타입의 참조변수로 하는것이 가능하다.
  • 부모 타입의 참조변수를 자식타입의 참조변수로 형변환이 가능하다.
  • 자식타입의 참조변수를 부모타입의 참조변수로 형변환 할 때에는 작은 자료형에서 큰 자료형으로 형변환 하는 것이므로 생략이 가능하다.

형변환의 예시를 보자

Car car = null;
ElectroCar tesla1 = new ElectroCar();
ElectroCar tesla2 = null;

car = tesla1; // 작은것에서 큰것을 형변환 함으로 형변환 생략 (업캐스팅)
tesla2 = (ElectroCar)car; // 큰것에서 작은것으로 변환이므로 형변환 필요 (다운캐스팅)

이러한 두번째처럼 작은 곳에서 큰곳으로의 형변환은 생략할 수 없기 때문에

instanceof연산자를 사용하여 인스타입이 어떤것인지 확인하는 것이 안전하다.

이러한 형변환을 했을 때 틀리기 쉬운 오류들이 있다. 다음 예시를 보자

Car car = null;
ElectroCar tesla = new ElectroCar();

car = tesla;
car.chargeBattery(); // -> 불가능 하다. 

이러한 결과가 불가능한 이유는 형변환은 단지 참조변수의 타입을 변환하는 것이지 인스턴스를 변환하는 것은 아니기 때문이다

carCar인스턴스이기 때문에 메서드 호출이 불가능한것이다.

만약에 이런경우를 보자

Car car = null;
ElectroCar tesla1 = new ElectroCar();
ElectroCar tesla2 = null;

car = tesla;

tesla2 = (ElectroCar)car;
tesla2.chargeBattery(); // -> 가능하다.

이는 인스턴스가 tesla2가 형변화된 car를 받고 ElectroCar의 인스턴스이기 때문에 메서드를 사용 가능하다.

다운캐스팅을 함으로서 car에서 사용할 수 없었던 ElectroCar 의 기능을 사용할 수 있는 것이다.

정리하자면

  • 서로 상속관계에 있는 타입간의 형변환은 양방향 자유롭게 수행될 수 있다.
  • 참조 변수가 가리키는 인스턴스의 자손타입으로 형변환은 허용되지 않는다.
  • 참조변수가 가리키는 인스턴스의 타입이 무엇인지 instanceof 를 사용해서 확인하는 것이 중요한다.
  • 컴파일 시에는 참조변수간의 타입만 확인하기 때문에 오류가 없지만 메서드를 호출하거나 ㅎ라때는 에러가 발생할 수 있다.

Instanceof연산자

instaceof연산자는 참조변수가 참조하고 있는 인스턴스의 실제 타입을 알아볼 수 있는 연산자이다.

bool형으로 연산의 결과를 반환한다.

  • ture가 나왔을 경우는 참조변수가 검사한 타입으로 형변환이 가능하다.
  • false가 나왔을 경우는 형변환이 불가능하다는 것이다.
ElectroCar tesla = new ElectroCar();

System.out.println(tesla instanceof ElectroCar); //true

System.out.println(tesla instanceof Car); //true

System.out.println(tesla instanceof Object); //true

tesla는 당연히 CarObject클래스를 다중상속 하였기 때문에 부모타입의 연산에 true를 결과로 얻는다.

참조변수와 인스턴스의 연결

우리는 형변환을 배워서 부모클래스던지 자식클래스로 참조변수의 타입을 변경할 수 있다.

만약에, 자식클래스와 부모클래스에 똑같은 변수가 있다고 생각해보자

인스턴스는 다음과 같이 2개를 생성하였다.

class Car{
    String name = "Parent car";
    void whoseCar(){
        System.out.println("Parent car");
    }
}

class ElectroCar extends Car{
    String name = "Child car";
    void whoseCar(){
        System.out.println("Child car");
    }
}

public static void main(String args){
    Car car = new ElectroCar();
    ElectroCar tesla = new ElectroCar();

    car.whoseCar(); // Child Car
    tesla.whoseCar(); // Child Car

    System.out.println(car.name); // Parent Car
    System.out.println(tesla.name); // Child Car
}

조금신기한 결과가 나타났다.

결과적으로 부모 클래스와 자식 클래스에 중복으로 정의됬을 경우

부모타입의 참조변수를 사용했을 경우는 조상클래스의 선언된 멤버변수가 사용된다.

하지만 메서드의 경우에는 참조변수의 타입에 관계 없이 인스턴스의 메서드(오버라이딩된 메서드)가 호출된다.

매개변수의 다형성

다형성은 클래스에만 국한하지 않고 메서드의 매개변수에서도 적용이 된다.

물품들을 구매하는 예제를 보자.

class Product{
    int price;
    int bonusPoint;
}

class Tv extends Product {}
class Computer extends  Product {}
class Audio extends Product {}

class Buyer{
    int money = 10000;
    int bonusPoint = 0;
}

여기서 구매를 한다는 표시로 buy()라는 메서드를 다음과 같이 만들것이다.

buy()는 일반적으로 Tv, Computer, Audio마다 각각 price가 다르기 때문에 이러한 buy()메서드를 총 3개를 만들어야할것이다.

class Buyer{
    int money = 10000;
    int bonusPoint = 0;

    void buy(Tv t){
        money -= t.price;
        bonusPoint += t.bonusPoint;
    }

    void buy(Computer c){
        money -= c.price;
        bonusPoint += c.bonusPoint;
    }

    void buy(Audio a){
        money -= a.price;
        bonusPoint += a.bonusPoint;
    }
}

이런식으로 총 3개의 매개변수가 다른 buy매서드를 오버로딩 해야할 것이다.

하지만 매개변수의 다형성을 이용한다면 다음과 같이 하나의 메서드로 줄일 수 있다.

class Buyer{
    int money = 10000;
    int bonusPoint = 0;

    void buy(Product p){
        money -= p.price;
        bonusPoint += p.bonusPoint;
    }

}

왜냐하면 제품들은 모두 Product를 상속받았기 때문이다.

이런방식으로 모든 클래스의 부모클래스는 Object이기 때문에 이를 활용한 예제도 가능하다는 것을 잊지 말자

또한, 이러한 객체들을 배열로 다룰수 있다.

Product p[] = new Product[3];
p[0] = new Tv();
p[1] = new Computer();
p[2] = new Audio();

이런식으로 부모타입인 Product 타입의 배열을 사용함으로서 상속한 자식들을 하나의 배열로 쉽게 이용할 수 있다.

Reference

  • 자바의 정석

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

[Java/Study] 인터페이스  (0) 2021.09.11
[Java/Study] 추상클래스  (0) 2021.09.11
[Java/Study] 제어자  (0) 2021.09.09
[Java/Study] Package 와 Import 및 터미널로 자바 실행방법  (0) 2021.09.08
[Java/Study] 오버라이드  (0) 2021.09.07

+ Recent posts