제 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 클래스 | 클래스 | |
---|---|---|---|
선언 시 사용하는 예약어 | interface | abstract class | class |
구현 안 된 메소드 포함 가능 여부 | 가능(필수) | 가능 | 불가 |
구현된 메소드 포함 가능 여부 | 불가 | 가능 | 가능(필수) |
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이라는 예약어를 사용해서 값을 변경하지 못하게 하는걸까?
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
이기 때문에 값을 할당할 수 없다는 에러
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로 선언할 필요는 없다.
enum
은 enumeration
이라는 뜻에서 “셈, 계산, 열거, 목록, 일람표“ 라는 뜻을 갖고 있음
- 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
라는 변수에 OverTimeValue2
의 FIVE_HOUR
라는 상수를 할당해 놓았다.
그렇다면 왜 이런 방법으로 처음부터 사용하지 않았을까 ?
위와 같이 enum클래스 선언하면 선언 자체는 간단해진다. 하지만 구현하는데 있어서 더 복잡해 진다.
➡️예를 들어 3시간 근무할 경우 야근 수당이 200원 오르면 어떻게 될까 ?
이 값이 항상 바뀔 수 있는 경우에는 원격 서버에 있는 값을 읽어오도록 하면 큰 문제는 없을 것이다.
하지만..
enum클래스의 경우 만약 야근 수당이 2000원 오르면 자바 프로그램을 수정한 후 다시 컴파일해서 실행중인 자바 프로그램을 중지했다가 다시 시작해야 한다는 단점이 존재한다.
물론 성능은 두번째 방법(enum 클래스 선언시 각 상수의 값을 지정)이 더 좋다.
enum 클래스의 부모는 무조건 java.lang.enum이어야 한다.
enum 클래스
는 무조건 java.lang.Enum
이라는 클래스의 상속을 받는다.
extends java.lang.Enum
이라는 문장을 enum 클래스
를 선언할 때 사용하지는 않지만, 컴파일러가 알아서 이 문자을 추가해서 컴파일한다.
따라서 마음대로 extends
를 하면 안 된다. 또한 누군가 만든 enum
을 extends
예약어를 사용하여 선언할 수 없다.
| 접근 제어자 | 메소드 | 설명 | | — | — | — | | protected | Enum(String name, int ordinal) | 컴파일러에서 자동으로 호출되도록 해놓은 생성자다. 하지만 개발자가 이 생성자를 호출할 수는 없다. |
name
은enum 상수
의 이름이다.ordinal
은enum 순서
이다. 상수가 선언된 순서대로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 | 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_HOUR
는 FIVE_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으로 처리할 수 있는지 고민해서 적용해보자.
회고
이 파트는 너무 길었다. 이해는 했는데 나의 것으로 만드는 일 너무 어렵다. 노트에도 써보고 코드도 짜보고 계속해봐야겠다.
정리 한번 쭉 했으니 그걸 토대로 누군가에게 설명 또는 혼자서 시험을 봐보자. 보고 틀린것들은 피드백… 하고 혼자 공부하기 참으로 힘들다.