인터페이스의 역할
사전적인 의미로는, 두 장치를 연결하는 접속기를 말한다.
=> 두 장치를 서로 다른 객체로 본다면, 인터페이스는 이 두 객체를 연결하는 역할을 한다.
간단히 말해, 개발 코드를 변경하지 않고도 사용 중인 객체의 정보를 변경하기 위해 인터페이스를 사용한다.
이러한 특징으로 인해, 인터페이스는 다형성 구현에 주된 기술로 이용된다.
설명보단 코드로 보는게 이해가 편하니 밑에서 코드를 살펴보자.
인터페이스를 구현하는 클래스 선언
인터페이스는 '~.java' 형태의 소스 파일로 작성되고, '~. class' 형태로 컴파일되기 때문에 물리적인 형태는 클래스와 동일하다.
인터페이스 선언
인터페이스 선언은 class 대신 interface 키워드를 사용한다.
접근 제한자로는 클래스와 동일하게 default, public을 붙일 수 있다.
public interface 인터페이스명 {
//인터페이스가 가지는 멤버들 선언
}
만약에 객체 "A" 가 인터페이스를 구현한(implement) 객체일 때, 다음과 같이 인터페이스를 구현하고 있음을 선언부에 명시해야 한다.
public class A implements 인터페이스명 {...}
implements 키워드는 해당 클래스가 인터페이스를 통해 사용할 수 있다는 표시이다.
(아래서 쓰일 예시 코드)
public interface RemoteControl {
public void turnOn();
}
public class Television implements RemoteControl {
@override
public void turnOn() {
System.out.println("TV를 켭니다.");
}
}
인터페이스도 하나의 타입이므로 변수의 타입으로 사용할 수 있다.
인터페이스는 참조 타입에 속하므로 인터페이스 변수에는 객체를 참조 X 하고 있다는 뜻으로 null을 대입할 수 있다.
RemoteControl rc; //rc 라는 이름의 인터페이스 타입을 가지는 변수 선언
RemoteControl rc = null;
인터페이스를 통해 구현 객체를 사용하려면, 인터페이스 변수에 구현 객체(의 번지)를 대입해야 한다.
rc = new Television();
만약 Television이 implements RemoteControl로 선언되지 않았다면, RemoteControl 타입의 변수 rc에 대입할 수 없다.
rc.turnOn();
이렇게 인터페이스 변수를 통해 trunOn() 메소드를 호출하면, 실제로 실행되는 것은 Television에서 재정의된 turnOn() 메소드이다.
public class RemoteControlExample {
public static void main(String[] args) {
RemoteControl rc;
rc = new Television()
rc.turnOn();
}
}
//실행결과
//TV를 켭니다.
만약에, Audio 객체가 구현된 객체라면, 다음과 같이 Audio 객체로 교체해 대입할 수도 있다.
rc = new Audio();
rc.turnOn;
Audio 클래스를 생성해 보자.
public class Audio implements RemoteControl {
@Override
public void turnOn() {
System.out.println("Audio를 켭니다.");
}
}
그리고, RemoteControlExample을 수정하면,
public class RemoteControlExample {
public static void main(String[] args) {
RemoteControl rc;
rc = new Television()
rc.turnOn();
rc = new Audio();
rc.turnOn();
}
}
//실행결과
//TV를 켭니다.
//Audio를 켭니다.
어떤 객체를 대입하느냐에 따라 다르게 출력되는 모습.
상수 필드
인터페이스는 public static final 특성을 갖는 불변의 상수 필드를 멤버로 가질 수 있다.
[ public static final ] 타입 상수명 = 값;
(상수명은 대문자로 작성하되, 서로 다른 단어로 구성되어 있을 경우에는 언더바(_)로 연결하는 것이 관례.)
상수는 구현 객체와는 관련 없는 인터페이스 소속 멤버이므로 인터페이스로 바로 접근해서 상수값을 읽을 수 있다.
(간단 당연하니 코드는 생략)
추상 메소드
인터페이스는 구현 클래스가 재정의해야 하는 public 추상 메소드를 멤버로 가질 수 있다.
추상 메소드는 리턴 타입, 메소드명, 매개변수만 기술되고, 중괄호를 붙이지 않는 메소드를 말한다.
public abstract를 생략하더라도 컴파일 과정에서 자동으로 붙게 된다.
public interface RemoteControl {
//상수 필드
int MAX_VOLUME = 10;
int MIN_VOLUME = 0;
//추상 메소드 , 메소드 선언부만 작성한다.
void turnON();
void turnOff();
void setVolume(int volume);
}
이제 추상 메소드들을 구현 객체들에서 재정의해서 실행한다.
public class Television implements RemoteControl {
//필드
private int volume;
//turnOn() 추상 메소드 오버라이딩
@Override
public void turnOn() {
System.out.println("TV를 켭니다.");
}
//turnOff() 추상 메소드 오버라이딩
@Override
public void turnOff() {
System.out.println("TV를 끕니다.");
}
//setVolume() 추상 메소드 오버라이딩
@Override
public void setVolume(int volume) {
if(volume>RemoteControl.MAX_VOLUME) {
this.volume = RemoteControl.MAX_VOLUME;
} else {
this.volume = volume;
}
System.out.println("현재 TV 볼륨: " + this.volume);
}
}
구현 클래스에서 추상 메소드를 재정의할 때 주의할 점은 인터페이스의 추상 메소드는 기본적으로 public 접근 제한을 갖기 때문에,
public보다 더 낮은 접근 제한으로 재정의할 수 없다. (그래서 모든 재정의 메소드에는 public을 붙여둠.)
디폴트 메소드
인터페이스에 완전한 실행 코드를 가진 디폴트 메소드도 선언 가능.
당연하게, 추상 메소드의 실행부엔 중괄호가 없지만, 디폴트 메소드는 실행부(중괄호)가 있다.
선언 방법은 클래스 메소드와 동일한데, 차이점은 default 키워드가 리턴 타입 앞에 붙는다.
default 리턴타입 메소드명(매개변수, ...) {...}
구현 클래스는 디폴트 메소드를 재정의할 수 있다.
재정의 시 주의할 점은 public 접근 제한자를 반드시 붙여야 하고, default 키워드를 생략해야 한다.
정적 메소드
인터페이스에는 정적 메소드(static)도 선언 가능하다.
추상/디폴트 메소드는 구현 객체가 필요하지만, 정적 메소드는 구현 객체가 없어도 인터페이스만으로 호출할 수 있다.
(당연 간단해서 코드는 생략)
private 메소드
인터페이스에, 외부에서 접근할 수 없는 private 메소드 선언도 가능하다.
private 메소드는 디폴트 메소드 안에서만 호출이 가능한 반면, private 정적 메소드는 디폴트 메소드뿐만 아니라 정적 메소드 안에서도 호출이 가능하다.
(당연 간단해서 코드는 생략)
다중 인터페이스 구현
구현 객체는 여러 개의 인터페이스를 implements 할 수 있다.
public class 구현클래스명 implements 인터페이스A, 인터페이스B {
//모든 추상 메소드 재정의
}
인터페이스 상속
인터페이스도 다른 인터페이스를 상속할 수 있으며, 클래스와는 달리 다중 상속을 허용한다.
public interface 자식인터페이스 extends 부모인터페이스1, 부모인터페이스2 {...}
타입 변환
인터페이스의 타입 변환은 인터페이스와 구현 클래스 간에 발생한다.
인터페이스 변수에 구현 객체를 대입하면, 구현 객체는 인터페이스 타입으로 자동 타입 변환된다.
반대로, 인터페이스 타입을 구현 클래스 타입으로 변환시키고 싶으면, 강제 타입 변환이 필요하다.
당연하지만, 구현 객체가 인터페이스 타입으로 자동 변환되면, 인터페이스에 선언된 메소드만 사용 가능하다.
예를 들어, A라는 인터페이스에는 3개의 메소드, A를 implement 하는 B클래스에는 5개의 메소드가 선언되어 있다면,
B클래스에서 A인터페이스로 호출가능한 메소드는 3개뿐이다.
따라서, 자동 타입 변환 후에, B클래스의 나머지 2개 메소드를 호출하고 싶다면, 다음과 같이 캐스팅 기호를 사용해서 원래 B클래스로 강제 타입 변환해야 한다.
B 변수명 = (클래스명) 인터페이스타입으로자동변환된변수명
(결론)
다형성 : 사용 방법은 동일하지만 다양한 결과가 나오는 성질
다형성
상속도 있지만, 인터페이스 또한 다형성을 구현하는 주된 기술로 사용된다.
현업에서는 상속보다는 인터페이스를 통해서 다형성을 구현하는 경우가 더 많다고 한다.
상속의 다형성과 마찬가지로, 인터페이스 역시 다형성을 구현하기 위해 재정의 와 자동 타입 변환 기능을 이용한다.
[메소드 재정의] + [자동 타입 변환] => [다형성]
필드의 다형성
public interface Tire {
// 추상 메소드
void roll();
}
public class HankookTire implements Tire {
//추상 메소드 재정의
@Override
public void roll() {
System.out.println("한국 타이어가 굴러갑니다.");
}
}
public class KumhoTire implements Tire {
//추상 메소드 재정의
@Override
public void roll() {
System.out.println("금호 타이어가 굴러갑니다.");
}
}
public class Car {
//필드
Tire tire1 = new HankookTire();
Tire tire2 = new HankookTire();
//메소드
void run() {
tire1.roll();
tire2.roll();
}
}
public class CarExample {
public static void main(String[] args) {
//자동차 객체 생성
Car myCar = my Car();
//run() 메소드 실행
myCar.run();
//타이어 객체 교체
myCar.tire1 = new KumhoTire();
myCar.tire2 = new KumhoTire();
//run()메소드 실행(다형성: 실행 결과가 다름)
myCar.run();
}
}
//실행결과
//한국 타이어가 굴러갑니다.
//한국 타이어가 굴러갑니다.
//금호 타이어가 굴러갑니다.
//금호 타이어가 굴러갑니다.
간단한 예시지만 코드를 보는 게 이해가 빨라서 다 적어 두었다.
위와 같이, tire1과 tire2에 어떠한 타이어 구현 객체가 대입되어도, Car객체는 타이어 인터페이스에 선언된 메소드만 사용하므로 전혀 문제가 되지 않는다.
매개변수의 다형성
메소드 호출 시 매개값을 다양화하기 위해 상속에서는 매개변수 타입을 부모 타입으로 선언하고 호출할 때에는 다양한 자식 객체를 대입했다.
이것은 자동 타입 변환 때문인데, 비슷한 원리로 매개변수 타입을 인터페이스로 선언하면, 메소드 호출 시 다양한 구현 객체를 대입할 수 있다.
Vehicle 인터페이스가 다음과 같이 선언되었다고 가정하자.
public interface Vehicle {
void.run();
}
운전자 클래스인 Driver는 다양한 Vehicle 구현 객체를 운전하기 위해 Vehicle 인터페이스를 매개변수로 가지는 drive() 메소드를 선언.
public class Driver {
void drive(Vehicle vehicle) {
vehicle.run(); // 인터페이스의 추상 메소드 호출
}
}
여기서 만약, Bus가 Vehicle의 구현 클래스라면, 다음과 같이 Driver의 drive() 메소드를 호출할 때 Bus 객체를 생성해서 매개값으로 줄 수 있다.
Driver driver = new Driver();
Bus bus = new Bus();
driver.drive(bus); // 자동 타입 변환(Vehicle.vehicle = bus;)
public class Bus implements Vehicle {
//추상 메소드 재정의
@Override
public void run() {
System.out.println("버스가 달립니다.");
}
}
public class Taxi implements Vehicle {
//추상 메소드 재정의
@Override
public void run() {
System.out.println("택시가 달립니다.");
}
}
public class DriverExample {
public static void main(String[] args) {
//Driver 객체 생성
Driver driver = new Driver();
//Vehicle 구현 객체 생성
Bus bus = new Bus();
Taxi taxi = new Taxi();
//매개값으로 구현 객체 대입(다형성: 실행 결과가 다름)
driver.drive(bus);
driver.drive(taxi);
}
}
//실행 결과
//버스가 달립니다.
//택시가 달립니다.
drive() 메소드를 호출할 때 인터페이스 Vehicle을 구현하는 어떠한 객체라도 매개값으로 줄 수 있는데, 어떤 객체를 주느냐에 따라,
run() 메소드의 실행 결과는 다르게 나온다.
이유는? 구현 객체에서 재정의된 run() 메소드의 실행 내용이 다르기 때문이다.
=> 이것이 매개변수의 다형성이다.
'Java' 카테고리의 다른 글
[Java] Enum 정의 + 사용법 + 예제 (0) | 2023.09.04 |
---|---|
[Java] 연산자 + 조건문/반복문 (0) | 2023.08.09 |