Contents
Java - Interface, 추상클래스, enum
   Oct 26, 2022     15 min read

제 13장 인터페이스와 추상클래스, enum

인터페이스와 abstract 클래스를 사용하는 이유

  • 설계시 선언해 두면 개발할 때 기능을 구현하는 데에만 집중할 수 있다.
  • 개발자의 역량에 따른 메소드의 이름과 매개 변수 선언의 격치를 줄일 수 있다.
  • 공통적인 인터페이스와 abstract 클래스를 선언해 놓으면, 선언과 구현을 구분할 수 있다.

인터페이스 실습

public interface MemberManager {
  public boolean addMember(MemberDTO member);
  public boolean removeMember(String name, String phone);
  public boolean updateMember(MemberDTO member);
}
public class MemberMangerImpl implements MemberManager{

}

위의 내용을 컴파일해보면 컴파일이 안된다. 그리고 에러가 뜬다.

에러 메시지 : MemberMangerImpl is not abstract and does not override abstract method updateMember(MemberDTO) in MemberManger

해석하면 “MemberMangerImpl 클래스는 abstract 클래스도 아니고, MemberManger에 정의되어 있는 updateMember()라는 abstrace 메소드도 구현하지 않았다.” 라고 한다.

해결책 : 인터페이스를 구현할 경우(implements)에는 반드시 인터페이스에 정의된 메소드들의 몸통을 만들어 주어야만 한다. 즉, 메소드들을 구현해야한다.

public class MemberMangerImpl implements MemberManager{

    @Override
    public boolean addMember(MemberDTO member) {
        return false;
    }

    @Override
    public boolean removeMember(String name, String phone) {
        return false;
    }

    @Override
    public boolean updateMember(MemberDTO member) {
        return false;
    }
}

메소드들을 구현해야지 컴파일 에러가 뜨지않는다.


예제)

public class InterfaceExample {
    public static void main(String[] args) {
        MemberManager member = new MemberManager();
    }
}

위와 같은 코드를 작성시 컴파일 에러가 뜬다.. 왜일까 ?

에러 메시지 : MemberManager is abstract; cannot be instantiated

해석하면 MemberManager가 abstract이기 때문에 초기화가 되지 않는다는 메시지가 출력된다.

public class InterfaceExample {
    public static void main(String[] args) {
        MemberManager member = new MemberManagerImpl();

    }
}

위 코드는 member의 타입은 MebmerManager이다. 그리고, MemberManagerImpl 클래스에는 인터페이스에 선언되어 있는 모든 메소드들이 구현되어 있다. 따라서 실제 member 의 타입은 MemberManager 가 되기 때문에, member에 선언된 메소드들을 실행하면 MemberManagerImpl 에 있는 메소드들이 실행된다.

일부 완성되어 있는 abstract클래스

abstract 클래스는 자바에서 마음대로 초기화하고 실행할 수 없도록 되어있다.

abstract 클래스를 구현해 놓은 클래스로 초기화 및 실행이 가능하다.

  • abstract 클래스 형식
접근제어자 abstract class 클래스이름{
    접근제어자 abstract boolean 메소드이름(); // 추상 메소드 형식
}
public abstract class MemberManagerAbstract {
    public abstract boolean addMember(MemberDTO member);
    public abstract boolean removeMember(String name, String phone);
    public abstract boolean updateMember(MemberDTO member);

    public void printLog(String data) {
        System.out.println("Data =" + data);
    }

}

abstract 클래스는 abstract으로 선언한 메소드가 하나라도 있을 때 선언한다.

abstract 정리

  • abstract 클래스는 클래스 선언시 abstract이라는 예약어가 클래스 앞에 추가되면 된다.
  • abstract 클래스는 안에는abstract으로 선언된 메소드가 0개 이상 있으면 된다.
  • abstract 으로 선언된 메소드가 하나라도 있으면, 그 클래스는 반드시 abstract 으로 선언되어야만 한다.
  • abstract 클래스는 몸통이 있는 메소드가 0개 이상 있어도 전혀 상관 없으며, static이나 final 메소드가 있어도 된다.

어떤 예약어를 사용하여 이 abstract 클래스를 구현한다고 선언할까?

  • extends라는 예약어 를 사용하여 그 뒤에 확장할 클래스 이름을 명시
  • abstract 클래스에는 구현된 메소드가 있을 수 있기 때문에 확장해서 사용한다고 이야기해 주어야만 한다.

abstract 클래스를 구현하는 예시

public class MemberManagerImpl2 extends MemberManagerAbstract{
}

위의 코드를 쓰고 컴파일 할 경우 에러가 난다.

이유는 abstract 으로 선언되어 있는 메소드들을 구현하지 않았기 때문이다.

public class MemberManagerImpl2 extends MemberManagerAbstract{

    public boolean addMember(MemberDTO member){
        return false;
    }

    public boolean removeMember(String name, String phone){
        return false;
    }

    public boolean updateMember(MemberDTO member){
        return false;
    }

}

왜? abstract 클래스가 있는 것일까?

인터페이스를 선언하면, 어떤 메소드는 미리 만들어 놓아도 전혀 문제가 없는 경우 발생한다. 그렇다고, 해당 클래스를 만들기는 좀 애매할 때가 있다. 그 중 아주 공통적인 기능을 미리 구현해 놓으면 많은 도움이 된다. 이럴때 사용하는 것이 바로 abstract 클래스다.

 인터페이스abstract 클래스클래스
선언 시 사용하는 예약어interfaceabstract classclass
구현 안 된 메소드 포함 가능 여부가능(필수)가능불가
구현된 메소드 포함 가능 여부불가가능가능(필수)
static 메소드 선언 가능 여부불가가능가능
final 메소드 선언 가능 여부불가가능가능
상속(extends) 가능불가가능가능
구현(implements) 가능가능불가불가

예약어 - final

final은 클래스(class), 메소드(Method), 변수(variable)에 선언할 수 있다.

final이 클래스, 메소드, 변수에 사용될 때 어떻게 달라지는지 알아보자.

클래스에 final을 선언할 때

  • 클래스가 final로 선언되어 있으면 상속을 해줄 수 없다.

  • 클래스에 final 형식

접근제어자 final class 메소드이름{
}

➡️예제) 클래스에 final을 선언할 때 예제

//FinalClass
public final class FinalClass{
}
//FinalChildClass
public class FinalChildClass extends FinalClass{
}

➡️FinalChildClass 을 컴파일해보면 에러가 난다.

에러 메시지 :
cannot inherit from final FinalClass
번역 : final인 FinalClass에서 상속을 받을 수는 없다."

➡️클래스에 final을 사용하는 것은 언제인가?

예를들어 String 클래스 상속을 방아서 toString() 메소드에 무조건 1을 리턴하게 한다면 String이라는 클래스에 대한 기본 속성을 변경하는 것이다.

더 이상 확장해서는 안 되는 클래스, 누군가 이 클래스르 상속 받아서 내용을 변경해서는 안되는 클래스를 선언할 때 final로 선언하면 된다.

메소드를 final로 선언할 때

  • 메소드를 final로 선언할 경우 Overriding을 할 수 없게 된다.

예제를 통해서 알아보자.

➡️예제) 메소드에 final선언할 경우

public abstract class FinalMehtodClass{
    public final void printLog(String data){
        System.out.println("Data=" + data);
    }
}

➡️FinalMehtodClass 안에 있는 final로 선언된 메소드 printLog() 메소드를 Overriding하려고 할 때

public class FinalMehtodChildClass extends FinalMehtodClass{
    public final void printLog(String data){
        System.out.println("Data=" + data);
    }
}

➡️컴파일할 경우 에러가 뜬다.

에러 메시지 :
printLog(String) in FinalMethodChildClass cannot override printLog(String) in FinalMethodClass
해석 : FinalMethodChildClass 클래스에 있는 printLog() 메소드는 final이기 때문에 override할 수 없다.

메소드에 final을 넣을 경우 다른 개발자가 그 메소들르 덮어 쓰는 것을 막을 수 있다.

변수에서 final을 선언할 때

  • 변수에 final을 선언하는 것은 클래스,메소드에 final을 넣는것과는 조금 다르다.
  • '더 이상 바꿀 수 없다'의 의미를 내포하고 있다.
  • 인스턴스 변수static으로 선언된 클래스 변수는 선언과 함께 값을 지정해야만 한다.

➡️예제) 인스턴스 변수에 final을 선언

public class FinalVariable{
    final int instanceVariable;
}

➡️ 위의 코드를 컴파일 할 경우 어떻게 되는가? 에러가 뜬다.

에러 메시지 :
variable instanceVariable not initialized in the default constructor
해석 : 변수 instanceVariable이 기본 생성자에서 초기화되지 않았습니다.

➡️컴파일이 안되는 이유 : final로 선언되어 있는 변수이기 때문이다.

➡️ 해결법 : 변수 생성과 동시에 초기화를 해야만 컴파일시 에러가 발생하지 않는다. 또한 생성자나 메소드에서 초기화하면 되지 않을까? 라는 생각이 있을 수 있지만 중복되어 변수값이 선언될 수도 있기 때문에 final의 기본 의도를 벗어난다.

아래와 같이 선언하면 된다.

public class FinalVariable{
   final int instanceVariable = 1;
}

이제 매개 변수나 지역 변수에 final에 대해서 알아보자.

➡️매개 변수나 지역변수 final

public class FinalVariable{
    final int instanceVariable = 1;

    public void method (final int parameter){
      final int localVariable;
    }
}

매개 변수나 지역 변수를 final로 선언한 경우 반드시 선언할 때 초기화 할 필요는 없다.

➡️이유:

  • 매개 변수는 이미 초기화가 되어서 넘어 왔다.
  • 지역 변수는 메소드를 선언하는 중괄호 내에서만 참조되므로 다른 곳에서 변경할 일이 없다.

※주의

  • 아래와 같이 하면 처음 localVariable = 2로 선언할 때는 상관없다.
  • 다음 줄 loclaVariable = 3; 으로 다시 선언 한다면 컴파일 에러가 난다.
  • 매개 변수 parameter 는 매개 변수로 넘어오기 전에 이미 값을 정해 놓았고 final로 선언되어 있기 때문에 다시 값을 할당하면 컴파일 에러가 난다.
public class FinalVariable{
    final int instanceVariable = 1;

    public void method (final int parameter){
      final int localVariable;
      loclaVariable = 2;
      loclaVariable = 3;
      parameter=4;
    }
}

왜 final이라는 예약어를 사용해서 값을 변경하지 못하게 하는걸까?

Untitled

1월과 12월은 31일까지 있다. 어떤 일이 있든 달력은 바뀌지 않는다. 이런 경우 final을 사용하면 너무 유용하다.

1월과 12월을 가져다가 쓸 수 있다. 하지만 final이 붙어 바꿀 수는 없다.

중요한것은 final을 남발해서 사용하면 안된다.

참조 자료형 final

import main.Chapter08.MemberDTO;

public class FinalReferenceType {
    final MemberDTO dto = new MemberDTO();

    public static void main(String[] args) {
        FinalReferenceType referenceType= new FinalReferenceType();
        referenceType.checkDTO();

    }

    public void checkDTO(){
        System.out.println(dto);
        dto = new MemberDTO();
    }


}
  • 결과는 컴파일 에러가 난다.

➡️에러 메시지

  • Cannot assign a value to final variable 'dto’
  • ‘dto’final 이기 때문에 값을 할당할 수 없다는 에러

Untitled 1

dto라는 변수를 final로 선언한 것을 알 수 있다.

기본 자료형과 마찬가지로 참조 자료형도 두 번 이상 값을 할당하거나 새로 생성자를 사용하여 초기화할 수 없다.

package main.Chapter13;

import main.Chapter08.MemberDTO;

public class FinalReferenceType {
    final MemberDTO dto = new MemberDTO();

    public static void main(String[] args) {
        FinalReferenceType referenceType= new FinalReferenceType();
        referenceType.checkDTO();

    }

    public void checkDTO(){
        System.out.println(dto);
//        dto = new MemberDTO();
        dto.name = "Dante";
        System.out.println(dto);
    }

}
결과 :
Name =null phone=null eMail=null
Name =Dante phone=null eMail=null

정리해보자면 final로 선언된 dto 객체는 FinalReferenceType 에서 두 번 이상 생성할 수 없다.

하지만, 그 객체의 안에 있는 객체들은 fianl로 선언된 것이 아니기 때문에 그러한 제약을 받지않는다.

제약을 받지 않는 이유는 MemberDTO 에 선언되어 있는 name, phone, email 모두 final이 아니기 때문이다.

해당 클래스가 final이라고 해서 그 안에 있는 인스턴스 변수나 클래스 변수가 final은 아니라는 것이다.


enum 클래스

final과 String과 같은 문자열이나 숫자들을 나타내는 기본 자료형의 값을 고정할 수 있다. 이렇게 고정된 값을 ‘상수(constant)’ 라고 한다.

어떤 클래스가 상수만으로 만들어져 있을 경우에는 반드시 class로 선언할 필요는 없다.

enumenumeration 이라는 뜻에서 “셈, 계산, 열거, 목록, 일람표“ 라는 뜻을 갖고 있음

  • enum 클래스는 일종의 클래스이다.

➡️예시를 통해서 알아보자.

public enum OverTimeValues {
    THREE_HOUR,
    FIVE_HOUR,
    WEEKEND_FOUR_HOUR,
    WEEKEND_EIGHT_HOUR;
}
  • 급여
    • 평일 3 시간 이상 ~ 5시간 미만일 경우
    • 주말 4시간 이상 8시간 미만일 때의 휴일 근무 수당
    • 8시간 이상일 때의 야근 수당이 있다고 가정
public class OverTimeManager {

    public int getOverTimeAmount(OverTimeValues value){
        int amount = 0;
        System.out.println(value);
        switch(value){
            case THREE_HOUR:
                amount = 18000;
                break;

            case FIVE_HOUR:
                amount = 30000;
                break;

            case WEEKEND_FOUR_HOUR:
                amount = 4000;
                break;

            case WEEKEND_EIGHT_HOUR:
                amount = 6000;
                break;
        }
        return amount;
    }
}

getOverTimeAmount() 메소드를 보면 OverTimeValues라는 enum 타입을 매개 변수로 받고, 변수명은 value로 지정했다. 야근 수당을 리턴할 amount라는 int 타입을 선언하고 0을 기본값으로 선언하고, 가장 마지막에 그 값을 리턴함.

이제 OverTimeValue 라는 enum 타입을 어떻게 getOverTimeAmount() 메소드에 전달할까?

public class getOverTimeAmount {
    public static void main(String[] args) {
        OverTimeManager manager = new OverTimeManager();
        int myAmount = manager.getOverTimeAmount(OverTimeValues.THREE_HOUR);
        System.out.println(myAmount);
    }
}
결과 :
THREE_HOUR
18000

밑에서 두 번째 줄을 보면, 별도의 생성자도 필요없고, enum을 넘겨주는것이 아니라는 것을 알 수 있다.

“enum 클래스이름.상수 이름” 을 지정함으로써 클래스의 객체 생성이 완료된다고 생각하면된다.

int myAmount = manager.getOverTimeAmount(OverTimeValues.THREE_HOUR);

위의 코드를 풀어서 쓰면

OverTimeValues value = OverTimeValues.THREE_HOUR;
int myAmount = manager.getOverTimeAmount(value);

여기서 value 변수는 OverTimeValues라는 enum 클래스의 객체라고 생각하면 된다.

  • enum 클래스는 생성자를 만들 수 있지만, 생성자를 통하여 객체를 생성할 수는 없다.
  • THREE_HOUR로 출력된 결과가 “THREE_HOUR”라는 문자열 값을 getOverTimeAmount()라는 메소드에 매개 변수로 넘기면 컴파일 에러가 난다.

Enum 제대로 사용하기

  • enum은 값을 할당하는 것은 불가능하다.

왜 ? enum 클래스 선언시 각 상수의 값을 저장하면 더 좋은게 아닌가?

➡️예제를 통해서 알아보자.

public enum OverTimeValue2{
    THREE_HOUR(18000),
    FIVE_HOUR(30000),
    WEEKEND_FOUR_HOUR(40000),
    WEEKEND_EIGHT_HOUR(60000);

    private final int amount;

    OverTimeValues2(int amount){
      this.amount = amount;
    }
    public int getAmount(){
      return amount;
    }
}

amount final로 선언 되어 있다. 그 변수는 OverTimeValue2의 생성자에서 매개 변수로 넘겨 받은 값을 할당할 때 사용된다.

각 상수를 enum 클래스는 생성자도 없는데 어떻게 돌아 갈까 ?

왜? enum클래스는 생성자도 없는데 어떻게 이상없이 작동했을까 ???

➡️enum클래스는 일반 클래스와 마찬가지로, 컴파일할 때 생성자를 자동으로 만들어 준다.

public class OverTimeManager2 {

    public static void main(String[] args) {
        OverTimeValue2 value2 = OverTimeValue2.FIVE_HOUR;
        System.out.println(value2);
        System.out.println(value2.getAmount());

    }
}
결과:
FIVE_HOUR
30000

위 실행은 문제 없이 컴파일이 되었고 실행도 되었다.

value2 라는 변수에 OverTimeValue2FIVE_HOUR라는 상수를 할당해 놓았다.

그렇다면 왜 이런 방법으로 처음부터 사용하지 않았을까 ?

위와 같이 enum클래스 선언하면 선언 자체는 간단해진다. 하지만 구현하는데 있어서 더 복잡해 진다.

➡️예를 들어 3시간 근무할 경우 야근 수당이 200원 오르면 어떻게 될까 ?

이 값이 항상 바뀔 수 있는 경우에는 원격 서버에 있는 값을 읽어오도록 하면 큰 문제는 없을 것이다.

하지만..

enum클래스의 경우 만약 야근 수당이 2000원 오르면 자바 프로그램을 수정한 후 다시 컴파일해서 실행중인 자바 프로그램을 중지했다가 다시 시작해야 한다는 단점이 존재한다.

물론 성능은 두번째 방법(enum 클래스 선언시 각 상수의 값을 지정)이 더 좋다.


enum 클래스의 부모는 무조건 java.lang.enum이어야 한다.

enum 클래스는 무조건 java.lang.Enum 이라는 클래스의 상속을 받는다.

extends java.lang.Enum이라는 문장을 enum 클래스를 선언할 때 사용하지는 않지만, 컴파일러가 알아서 이 문자을 추가해서 컴파일한다.

따라서 마음대로 extends를 하면 안 된다. 또한 누군가 만든 enumextends 예약어를 사용하여 선언할 수 없다.

| 접근 제어자 | 메소드 | 설명 | | — | — | — | | protected | Enum(String name, int ordinal) | 컴파일러에서 자동으로 호출되도록 해놓은 생성자다. 하지만 개발자가 이 생성자를 호출할 수는 없다. |

  • nameenum 상수의 이름이다.
  • ordinalenum 순서이다. 상수가 선언된 순서대로 0부터 증가한다.

Enum 클래스의 부모 클래스는 Object 이다. 따라서 Object의 메소드들을 사용할 수 있다.

하지만…

Enum 클래스는 개발자들이 Object 클래스 중 4개의 메소드Overriding하지 못하도록 막아놨다.

  • Object 클래스의 메소드
메소드내용
clone( )객체를 복제하기 위한 메소드이다. 하지만, 이 메소드는 enum 클래스에서 사용하지 안 된다. 만약 호출될 경우엔 CloneNotSupportedException이라는 예외를 발생시키도록 되어 있다.
finalize( )GC(Garbage Collection)가 발생할 때 처리하기 위한 메소드다.
hashCode( )int 타입의 해시 코드 값을 리턴하는 메소드다.
equals( )두 개의 객체가 동일한지를 확인하는 메소드다.

➡️hashCode()equals()메소드는 사용해도 된다. 사용하면 안되는 것은 clone(), finalize() 메소드이다.

toString()

➡️toString() 메소드는 Enum 클래스에서 Overriding한 Object 클래스의 메소드 중에서 유일하게 final로 선언되어 있지 않다.

➡️ toString() 메소드는 enum 변수에 toString을 호출하면 상수 이름을 출력한다.

메소드내용
compareTo(E e)매개 변수로 enum 타입과의 순서(ordinal) 차이를 리턴한다.
getDeclaringClass()클래스 타입의 enum의 리턴한다.
name()상수의 이름을 리턴한다.
ordinal()상수의 순서를 리턴한다.
valueOf(Class enumType, String name)static메소드다. 첫 번째 매개 변수로는 클래스 타입의 enum을, 두 번째 매개 변수로는 상수의 이름을 넘겨주면 된다.

CompareTo()

compareTo() 메소드는 순서가 같은지, 다른지를 비교하는 데 사용된다. 만약 같은 상수라면 0을, 그렇지 않고 다르면 순서의 차이를 출력한다.

순서의 차이는 메소드의 매개 변수로 넘기는 상수 기준으로 앞에 있으면 음수(-)를, 뒤에 있으면 양수(+)를 리턴한다.

public class OverTimeManager2 {

    public static void main(String[] args) {
        OverTimeValue2 value2 = OverTimeValue2.FIVE_HOUR;
        System.out.println(value2);
        System.out.println(value2.getAmount());

       OverTimeValues2 value3 = OverTimeValues2.THREE_HOUR;
       System.out.println(value2.compareTo(value3));
    }
}
결과 :
FIVE_HOUR
30000
1

1이라는 결과는 매개 변수로 넘긴 THREE_HOURFIVE_HOUR 바로 안에 선언되어 있다.

  • 0번 : THREE_HOUR
  • 1번 : FIVE_HOUR

따라서 1번을 출력한다.

API문서에도 없는 values()

enum 클래스에는 API 문서에도 없는 특수한 메소드가 있다.

values() 라는 메소드이다. 이 메소드를 호출하면 enum 클래스에 선언되어 있는 모든 상수를 배열로 리턴한다.

어떤 상수가 어떤 순서로 선언되었는지 확인하기 어려운 경우에 이 메소드를 사용하면 된다.

public class OverTimeManager3 {
    public static void main(String[] args) {
        OverTimeValue2[] valueList= OverTimeValue2.values();
        int i = 0;
        for (OverTimeValue2 value : valueList) {
            System.out.println(i + " " +  value);
            i++;
        }
    }
}
결과 :
0 THREE_HOUR
1 FIVE_HOUR
2 WEEKEND_FOUR_HOUR
3 WEEKEND_EIGHT_HOUR

enum은 여기까지 알아보자. 개발하면서 계속 나오는 것을 enum으로 처리할 수 있는지 고민해서 적용해보자.

회고

이 파트는 너무 길었다. 이해는 했는데 나의 것으로 만드는 일 너무 어렵다. 노트에도 써보고 코드도 짜보고 계속해봐야겠다.
정리 한번 쭉 했으니 그걸 토대로 누군가에게 설명 또는 혼자서 시험을 봐보자. 보고 틀린것들은 피드백… 하고 혼자 공부하기 참으로 힘들다.