본문 바로가기

학원/복기

0412 - 인터페이스, 기본메소드, Enum 자료형, 중첩 클래스

인터페이스


• 인터페이스(Interface) : 현실에 존재하는 대상을 클래스보다 더 추상적으로 표현하기 위한 자료형 (참조형)
인터페이스 내부에는 상수필드(Constant Field)추상메소드(Abstract Method)만 선언이 가능하다 
=> 버전 업데이트 후, 정적메소드(Static Method)와 기본메소드(Default Method)도 선언 가능해졌다 (JDK11 이후 사용되기 시작)

 

형식)[public] interface 인터페이스명 {
        [public static final] 자료형 필드명 = 값;  // public static final 생략가능 
        [public abstract] 반환형 메소드명(자료형 매개변수, ...); // public abstract 생략가능
    }

 

-> 인터페이스의 이름은 파스칼 표기법을 사용하여 작성하는 것을 권장

 

 

• 인터페이스는 클래스가 상속받아 사용하기 위한 자료형 - 다중 상속 가능 (인터페이스는 부모 인터페이스가 여러개 있을 수 있다)

형식) public class 클래스명 implements 인터페이스명, 인터페이스명, ... { }

 

• 인터페이스를 상속받은 클래스는 무조건 상속받은 인터페이스의 모든 추상메소드를 오버라이드 선언해야한다. 
• 인터페이스로 객체 생성은 불가능하지만 참조변수를 생성하여 인터페이스를 상속받은 자식클래스로 객체를 생성하여 저장이 가능하다. -> 참조변수로 자식 클래스의 메소드를 호출할 수 있다. (묵시적 객체 형변환이 일어나기 때문에) 

 

 인터페이스는 다른 인터페이스를 상속받아 사용이 가능하다 

형식) public interface 인터페이스명 extends 인터페이스명, 인터페이스명, ... { }

 

-> 자료형이 같으면 extends 자료형이 다르면 implements 사용

 

 

인터페이스를 선언하여 클래스가 상속받아 사용하는 이유

 

  1. 클래스의 단일 상속 관련 문제를 일부 보완하기 위해 인터페이스를 사용한다. 

 

ex) public class 늑대인간 extends 인간, 늑대  => 클래스는 단일상속이 되지 않기 때문에 불가능
    public class 늑대인간 extends 인간 implements 늑대  => 가능 
    public class 흡혈늑대인간 extends 인간 implements 늑대, 흡혈귀  => 가능


  2. 클래스에 대한 작업지시서의 역할을 제공하기 위해 인터페이스를 사용한다.

 

ex) TV or RADIO or SMARTPHONE >> 볼륨 증가, 볼륨 감소 기능 (공통 기능) - 인터페이스로 만들어준다
=> 인터페이스를 상속받은 모든 자식클래스에 동일한 형태의 메소드가 선언되도록 규칙을 제공 
=> 클래스간의 결합도를 낮추어 시스템 변경에 따른 유지보수의 효율성 증가  
   (-> 시스템 변경에 따라 클래스가 바뀌었을때 유지보수를 쉽게 해주기 위해 사용하는 것이다)

 

 인터페이스와 추상클래스의 차이

: 추상클래스는 추상메소드가 없어도 상관 없다. 하지만 인터페이스 안에는 추상메소드가 무조건 존재해야 한다. 
따라서 인터페이스를 상속받은 클래스는 인터페이스가 시키는 대로 무조건 오버라이드 해야한다.  

 


 

인터페이스 예시 1)

사람의 정보가 담긴 Human 클래스와 늑대의 정보가 담긴 Wolf 인터페이스를 상속받는 WolfHuman 클래스 생성

-> WolfHuman 클래스가 부모클래스를 한개만 상속받을 수 있기때문에 Wolf는 인터페이스로 생성한다.

 

Human 클래스와 Wolf 인터페이스를 생성한다.

 

//Human 클래스

package realization;

public class Human {
	//인간이 할 수 있는 행위 
	public void speak() {
		System.out.println("[인간]대화할 수 있는 능력");
	}
	
	public void walk() {
		System.out.println("[인간]두발로 걸을 수 있는 능력");		
	}
	
	public void smile() {
		System.out.println("[인간]활짝 웃을 수 있는 능력");		
	}
}

// Wolf 인터페이스
package realization;

//인터페이스에는 상수필드와 추상메소드만 선언 가능 
public interface Wolf {
	//인터페이스에는 추상메소드만 선언 가능하므로 public abstract 키워드 생략 가능 
	//추상메소드는 명령을 작성하지 않는다 
	void fastWalk(); 
	void cryLoudly(); 
}

 

Human 클래스와 Wolf 인터페이스를 상속받는 WolfHuman 클래스를 생성한다.

 

인터페이스를 상속받는 자식클래스인 WolfHuman은 인터페이스에 선언된 모든 추상메소드를 반드시 오버라이드 선언해야 한다. -> 실체화

package realization;

//클래스 작성시 부모클래스를 상속받기 위해 extends 키워드 사용 - 단일상속
//클래스 작성시 부모인터페이스를 상속받기 위해 implements 키워드 사용 - 다중상속
public class WolfHuman extends Human implements Wolf {

	//늑대가 할 수 있는 행위
	@Override
	public void fastWalk() {
		System.out.println("[늑대]네 발로 빠르게 달릴 수 있는 능력");
	}

	@Override
	public void cryLoudly() {
		System.out.println("[늑대]큰소리로 울부짖을 수 있는 능력");	
	}  
	
	//늑대인간이 할 수 있는 행위
	public void change() {
		System.out.println("[늑대인간]변신할 수 있는 능력");
	}
	
}

 

자식클래스로 객체 생성해 메소드 호출 

 

public class WolfHumanApp {
	public static void main(String[] args) {
		WolfHuman wolfhuman = new WolfHuman(); 
		
		wolfhuman.speak(); //[인간]대화할 수 있는 능력
		wolfhuman.walk(); //[인간]두발로 걸을 수 있는 능력
		wolfhuman.smile(); //[인간]활짝 웃을 수 있는 능력
		wolfhuman.change(); //[늑대인간]변신할 수 있는 능력
		wolfhuman.fastWalk(); //[늑대]네 발로 빠르게 달릴 수 있는 능력
		wolfhuman.cryLoudly(); //[늑대]큰소리로 울부짖을 수 있는 능력

 

부모클래스로 참조변수를 생성하여 자식클래스의 객체 저장 => 참조변수는 기본적으로 부모클래스의 메소드만 호출 가능

하지만, 객체 형변환을 이용하면 참조변수로 자식클래스의 메소드 호출이 가능하다 

 

 

Human human = new WolfHuman();
		
human.speak(); //[인간]대화할 수 있는 능력
human.walk(); //[인간]두발로 걸을 수 있는 능력
human.smile(); //[인간]활짝 웃을 수 있는 능력

//명시적 객체 형변환을 사용하여 자식클래스의 메소드 호출 
((WolfHuman)human).change(); //[늑대인간]변신할 수 있는 능력

 

자식클래스가 같은 클래스와 인터페이스는 명시적 객체 형변환을 이용하여 자식클래스의 객체를 공유하여 사용 가능하다 

 

Wolf wolf = (Wolf)human;
		
//묵시적 객체 형변환에 의해 자동으로 자식클래스의 메소드가 호출
wolf.fastWalk(); //[늑대]네 발로 빠르게 달릴 수 있는 능력
wolf.cryLoudly(); //[늑대]큰소리로 울부짖을 수 있는 능력

 

 

인터페이스 예시 2)

 

//Boat 인터페이스 
package realization;

public interface Boat {
	void navigate();
}

//Car 인터페이스
package realization;

public interface Car {
	void run();
}

//Boat와 Car를 상속받는 BoatCar 인터페이스
package realization;

//인터페이스가 다른 인터페이스를 상속받기 위해서는 extends 키워드 사용 - 다중상속 
public interface BoatCar extends Car, Boat {
	void floating();
}

// BoatCar 인터페이스를 상속받는 BoatCarReal 클래스
package realization;

public class BoatCarReal implements BoatCar {
	
	//모든 인터페이스의 메소드를 오버라이드 해줘야 한다
	@Override
	public void run() {
		System.out.println("땅위를 달리는 능력");
	}

	@Override
	public void navigate() {
		System.out.println("바다를 항해하는 능력");
	}

	@Override
	public void floating() {
		System.out.println("공중에 떠 있는 능력");	
	}
	
}

 

 

자식클래스가 상속받은 모든 부모 인터페이스로 참조변수를 생성하여 자식클래스의 객체 저장이 가능하다.
=> 묵시적 객체 형변환에 의해 오버라이드 선언된 자식클래스의 메소드 호출 
=> 인터페이스에 따라 호출 가능한 메소드가 다른 경우 발생 

 

package realization;

public class BoatCarRealApp {
	public static void main(String[] args) {
    
		BoatCar boatCar = new BoatCarReal();
        
		boatCar.run(); //땅위를 달리는 능력
		boatCar.navigate(); //바다를 항해하는 능력
		boatCar.floating(); //공중에 떠 있는 능력
	}
}

 

인터페이스를 사용하지 않을 때 클래스 간의 결합도가 높아진다. >> 결합도를 낮추기 위해 인터페이스를 이용한다.

 

 


기본메소드

JDK11 이상에서는 인터페이스에 명령을 작성할 수 있는 기본메소드를 선언할 수 있다.

 

 기본메소드(Default Method) : 인터페이스를 상속받은 자식클래스에서 오버라이드 선언 하지 않아도 되는 메소드 
 >> 원래 인터페이스를 상속받는 메소드는 모두 오버라이드 해야하지만 기본메소드는 오버라이드를 해도 되고 안해도 된다 

 

오버라이드 선언하지 않은 경우 기본메소드를 호출한다.
 >> 오버라이드 하면 자식메소드 호출, 오버라이드 하지 않으면 부모메소드 호출

 

형식) default 반환명 메소드명(자료형 매개변수, ...) { 명령; ... }

 

예시) 

package realization;

//Printable 인터페이스 
public interface Printable {
	//추상메소드(Abstract Method) : 자식클래스에서 반드시 오버라이드 선언 
	void print();
	
	//기본메소드(Default Method) : 인터페이스를 상속받은 자식클래스에서 오버라이드 선언하지 않아도 된다
	default void scan() {
		System.out.println("[에러]스캔 기능을 제공하지 않습니다.");
	}
}

//PrintSingle 클래스 
package realization;

public class PrintSingle implements Printable {

	@Override
	public void print() {
		System.out.println("[프린트]문서를 출력합니다.");
	}

}

//PrintMulti 클래스
package realization;

public class PrintMulti implements Printable {

	@Override
	public void print() {
		System.out.println("[복합기]문서를 출력합니다.");
	}
	
	//default 메소드는 오버라이드 해도되고 안해도 된다
	@Override
	public void scan() {
		System.out.println("[복합기]문서를 스캔합니다.");
	}

}

 

출력결과

 

package realization;

public class PrintableApp {
	public static void main(String[] args) {
		//기본메소드를 사용하기 위해서는 참조변수를 인터페이스로 선언 
		Printable printOne = new PrintSingle();
		
		printOne.print(); //[프린트]문서를 출력합니다.
		printOne.scan(); //[에러]스캔 기능을 제공하지 않습니다.
		// => PrintSingle에서 scan 메소드를 오버라이드하지 않았기 때문에 부모 인터페이스의 기본 메소드가 호출
		
		System.out.println("==============================================================");
		Printable printTwo = new PrintMulti();
		
		printTwo.print(); //[복합기]문서를 출력합니다.
		printTwo.scan(); //[복합기]문서를 스캔합니다. 
		//=> PrintMulti에서 메소드를 오버라이드 선언했기 때문에 자식클래스의 메소드 호출
		System.out.println("==============================================================");
	}
}

 

 

Enum 자료형 


 상수(Constant) : 프로그램에서 값(리터럴) 대신 사용하기 위한 의미있는 단어 

 

  클래스 또는 인터페이스의 상수필드를 선언한 경우 발생될 수 있는 문제점 

 → 상수필드를 값을 대표하는 단어로 사용하기 부적절한 경우가 발생할 수 있다

 → 상수필드가 선언된 클래스 또는 인터페이스는 아무런 의미 없이 접근 용도로만 사용

 

 

예시)

상수필드(Constant Field) 선언 -  public static final 키워드 생략 가능
=> 클래스에도 상수필드를 선언 가능하지만 보다 쉬운 상수 선언을 위해 인터페이스에 선언

//InterfaceOne 인터페이스 생성 후 상수 선언 
package enumerate;

public interface InterfaceOne {
	int INSERT = 1, UPDATE = 2, DELETE = 3, SELECT = 4;
	
}

//InterfaceTwo 인터페이스 생성 후 상수 선언 
package enumerate;

public interface InterfaceTwo {
	int ADD = 1, MODIFY = 2, REMOVE = 3, SEARCH = 4;
}

 

인터페이스에 선언된 상수필드값을 출력하면 필드에 저장된 값을 제공받아 출력된다.

 

public class InterfaceApp {
	public static void main(String[] args) {
		//인터페이스에 선언된 상수필드값 출력 - 필드에 저장된 값을 제공받아 출력 
		System.out.println("삽입 = " + InterfaceOne.INSERT); //삽입 = 1
		System.out.println("변경 = " + InterfaceOne.UPDATE); //변경 = 2
		System.out.println("삭제 = " + InterfaceOne.DELETE); //삭제 = 3
		System.out.println("검색 = " + InterfaceOne.SELECT); //검색 = 4
		
		System.out.println("삽입 = " + InterfaceTwo.ADD); //삽입 = 1
		System.out.println("변경 = " + InterfaceTwo.MODIFY); //변경 = 2
		System.out.println("삭제 = " + InterfaceTwo.REMOVE); //삭제 = 3
		System.out.println("검색 = " + InterfaceTwo.SEARCH); //검색 = 4

 

상수필드의 자료형과 동일한 자료형의 변수를 생성하여 상수 저장이 가능하다. 

 

		int choice = InterfaceOne.INSERT; //int choice = 1;
		System.out.println("choice = " + choice); //choice = 1
		
        
		switch(choice) {
		//case InterfaceOne.INSERT:
		case InterfaceTwo.ADD:
			System.out.println("# 학생정보를 삽입합니다. #" );
			break;
		case InterfaceOne.UPDATE:
			System.out.println("# 학생정보를 변경합니다. #" );
			break;	
		case InterfaceOne.DELETE:
			System.out.println("# 학생정보를 삭제합니다. #" );
			break;
		case InterfaceOne.SELECT:
			System.out.println("# 학생정보를 검색합니다. #" );
			break;
		}
		//# 학생정보를 삽입합니다. #
	}
}

 

Java에서는 클래스 또는 인터페이스에 상수필드를 선언해서 발생될 수 있는 문제점을 해결하기 위해 열거형(enum)이라는 자료형(참조형)을 제공한다.

 

문제점들을 해결하기 위해 상수필드를 enum이라는 자료형에 만들어준다.

 


 

열거형(EnumerateType) : 상수만을 선언하기 위한 자료형 

형식) public enum 열거형명 { 상수명, 상수명, ... ; } 
 => 열거형의 이름은 파스칼 표기법을 이용하여 작성하는 것을 권장

 

예시)

//EnumOne enum 생성 

package enumerate;
public enum EnumOne {
	INSERT, UPDATE, DELETE, SELECT; //상수필드 선언
}
 
//EnumTwo enum 생성
package enumerate;

public enum EnumTwo {
	ADD, MODIFY, REMOVE, SEARCH; //상수필드 선언
}

=> 상수필드를 선언할 때, public static final int 키워드 생략이 가능하다.

=> 열거형의 상수필드에는 0부터 1씩 증가되는 정수값이 기본값으로 자동 저장

 

 

열거형에 선언된 상수필드값 출력하면, 상수필드의 이름이 제공되어 출력된다.
=> 프로그램에서 상수가 값을 대표하는 이름으로 사용 가능하다.

 

package enumerate;

public class EnumApp {
	public static void main(String[] args) {
		System.out.println("삽입 = " + EnumOne.INSERT); //삽입 = INSERT
		System.out.println("변경 = " + EnumOne.UPDATE); //변경 = UPDATE
		System.out.println("삭제 = " + EnumOne.DELETE); //삭제 = DELETE
		System.out.println("검색 = " + EnumOne.SELECT); //검색 = SELECT	
	   
	    System.out.println("삽입 = " + EnumTwo.ADD); //삽입 = ADD
		System.out.println("변경 = " + EnumTwo.MODIFY); //변경 = MODIFY
		System.out.println("삭제 = " + EnumTwo.REMOVE); //삭제 = REMOVE
		System.out.println("검색 = " + EnumTwo.SEARCH); //검색 = SEARCH

 

실직적으로 저장된 0,1,2,3 이 출력되는 것이 아니라 상수필드의 이름이 제공되어 출력된다.
따라서 열거형에 선언된 상수들은 명확하게 값을 대표하는 단어가 될 수 있다 => 상수 자체를 하나의 의미있는 단어로 사용할 수 있다 

 

 

열거형에 선언된 상수를 저장하기 위해서는 반드시 열거형(참조형)을 이용하여 변수 선언해야 한다.
=> 상수필드가 선언된 열거형을 하나의 자료형으로 사용할 수 있다.

//int choice = EnumOne.INSERT; //에러 발생 
//choice 변수는 EnumOne 자료형에 선언된 상수만 저장이 가능하다 
EnumOne choice = EnumOne.INSERT; 
System.out.println("choice = " + choice); //choice = INSERT

//나열형으로 선언된 변수에 비교값은 같은 나열형에 선언된 상수만 사용하여 비교 가능 
switch(choice) {
case INSERT:
//case ADD;
System.out.println("# 학생정보를 삽입합니다. #" );
break;
case UPDATE:
System.out.println("# 학생정보를 변경합니다. #" );
break;	
case DELETE:
System.out.println("# 학생정보를 삭제합니다. #" );
break;
case SELECT:
System.out.println("# 학생정보를 검색합니다. #" );
break;
}

//# 학생정보를 삽입합니다. #

 


 

열거형에 선언된 상수필드는 기본적으로 정수형(int)으로 설정된다.

 

package enumerate;

public enum Compass {
	EAST, WEST, SOUTH, NORTH;
    //상수필드를 선언하면 열거형의 기본생성자를 이용하여 초기값이 상수필드에 저장 
    //상수필드에는 0부터 1씩 증가되는 정수값이 기본값으로 저장 
}

 

상수를 만들어 기본값으로 사용하는 것 외에도, 0,1,2,3 ... 이 아닌 다른 값 or 정수값이 아닌 문자열을 집어 넣을 수도 있다 

 

package enumerate;

public enum Compass {
	//매개변수가 있는 생성자를 이용하여 상수필드를 생성하여 초기값 저장 
	EAST("동"), WEST("서"), SOUTH("남"), NORTH("북");
	
	//상수필드의 자료형 또는 저장값을 변경하기 위한 필드 선언 
	// => private final 제한자를 사용하여 필드 선언 
	// => 필드에 저장된 값을 변경하기 위해 반드시 매개변수가 있는 생성자를 선언해줘야 한다 
	
	//private final int value; //상수필드값을 변경하기 위한 필드 
	private final String value; //상수필드의 자료형을 변경하기 위한 필드
    
	
	//상수필드에 초기값을 저장하기 위한 매개변수가 있는 생성자 선언 
	// => 상수필드의 자료형 또는 저장값을 변경하기 위한 필드에 매개변수의 값 저장 
	// => 반드시 생성자는 은닉화 선언
	//생성자를 선언하면 매개변수가 없는 기본 생성자는 미제공된다 
	private Compass(String value) {
		this.value = value;
	}
	
	//상수필드의 자료형 또는 저장값을 변경하기 위한 필드값 반환 
	// => 상수필드에 저장된 값을 반환하기 위한 메소드 
	public String getValue() {
		return value;
	}
}

 

상수값 출력

 

package enumerate;

public class CompassApp {
	public static void main(String[] args) {
		//열거형에 선언된 상수필드값 출력 - 상수필드명을 제공받아 출력 
		System.out.println("동쪽 = " + Compass.EAST); //동쪽 = EAST
		System.out.println("서쪽 = "+ Compass.WEST); //서쪽 = WEST
		System.out.println("남쪽 = " + Compass.SOUTH); //남쪽 = SOUTH
		System.out.println("북쪽 = " + Compass.NORTH); //북쪽 = NORTH
		
		System.out.println("동쪽 = " + Compass.EAST.getValue()); //동쪽 = 동
		System.out.println("서쪽 = "+ Compass.WEST.getValue()); //서쪽 = 서
		System.out.println("남쪽 = " + Compass.SOUTH.getValue()); //남쪽 = 남
		System.out.println("북쪽 = " + Compass.NORTH.getValue()); //북쪽 = 북
		
		//EnumType.values() : 열거형에 선언된 모든 상수필드를 배열로 변환하여 반환하는 메소드 
		for(Compass compass : Compass.values()) {
			//EnumType.ordinal() : 상수필드를 구분하기 위한 첨자(Index)를 반환하는 메소드 
			System.out.println(compass + " = " + compass.getValue() + " >> " + compass.ordinal() );
		}
		
//		EAST = 동 >> 0
//		WEST = 서 >> 1
//		SOUTH = 남 >> 2
//		NORTH = 북 >> 3
		
	}
}

 

EnumType.values() : 열거형에 선언된 모든 상수필드를 배열로 변환하여 반환하는 메소드 
EnumType.ordinal() : 상수필드를 구분하기 위한 첨자(Index)를 반환하는 메소드 

 

 


중첩 클래스(Nested Class)


•  중첩 클래스 : 클래스(OuterClass : 외부클래스) 내부에 클래스(InnerClass : 내부클래스)를 선언 
•  중첩 클래스는 두개의 클래스가 밀접한 관계에 있을 경우 사용한다. - 클래스의 캡슐화를 강화할 목적으로 선언 

 

•  객체 내부 클래스 - 컴파일 결과를 [외부클래스$내부클래스.class] 파일로 제공  
   => 객체 내부클래스에서는 static 제한자를 사용하여 필드 또는 메소드 선언 불가능

 

 

예시)

package nested;

import javax.swing.SpinnerDateModel;

//외부 클래스
public class OuterOne {
	private int outerNum;
	
	public OuterOne() {
		// TODO Auto-generated constructor stub
	}

	public OuterOne(int outerNum) {
		super();
		this.outerNum = outerNum;
	}

	public int getOuterNum() {
		return outerNum;
	}

	public void setOuterNum(int outerNum) {
		this.outerNum = outerNum;
	}
	
	public void outerDisplay() {
		System.out.println("outerNum = " + outerNum);
		
		//외부클래스에서는 객체 내부 클래스의 필드 또는 메소드에 대한 직접적인 참조 불가능 
		//System.out.println("innerNum = " + innerNum);
		//innerDisplay();
		
		//외부클래스는 객체 내부클래스로 객체를 생성하여 접근제한자에 상관없이 객체 내부클래스
		//필드 또는 메소드 참조 가능 
		//InnerOne innerOne = new InnerOne();
		//System.out.println("innerNum = " + innerOne.innerNum);
		//innerOne.innerDisplay();
	}
	
    //객체 내부 클래스 
	public class InnerOne{
		private int innerNum;
		
		public InnerOne() {
			// TODO Auto-generated constructor stub
		}

		public InnerOne(int innerNum) {
			super();
			this.innerNum = innerNum;
		}

		public int getInnerNum() {
			return innerNum;
		}

		public void setInnerNum(int innerNum) {
			this.innerNum = innerNum;
		}
		
		public void innerDisplay() {
			System.out.println("innerNum = " + innerNum);
			
			//객체 내부 클래스에서는 외부클래스의 필드 또는 메소드를 접근제한자에 상관없이 
			//직접 참조 가능 
			System.out.println("outerNum = " + outerNum);
			outerDisplay();
		}
	}
}

 

출력

 

package nested;

import nested.OuterOne.InnerOne;

public class OuterOneApp {
	public static void main(String[] args) {
		OuterOne outerOne = new OuterOne(100);
		outerOne.outerDisplay(); //outerNum = 100

 

객체 내부클래스의 생성자를 호출하여 객체 생성하는 것은 불가능하다.
- 원래 객체 내부클래스를 사용하는 이유는 외부클래스에서만 내부클래스 사용하려고 생성  

 

//InnerOne innerOne = new InnerOne(200); 
//InnerOne.innerDisplay(); 

>> 불가능

 

 

외부클래스의 객체를 사용하여 객체 내부클래스의 생성자를 호출하여 객체 생성 가능하지만 권장하지 않는다.

 

InnerOne innerOne = outerOne.new InnerOne(200);
innerOne.innerDisplay();