자바(3), 인터페이스

2022. 8. 31. 01:28자바

728x90
반응형

1. 인터페이스

1) 인터페이스 선언

인터페이스는 서비스 공급자와 자신의 객체를 이 서비스에 사용하고 싶은 클래스 간의 계약을 기술하는 매커니즘이다. 이는 C++의 추상클래스와 비슷하게 구현을 제공하지 않고 수행할 일을 제시하는 역할을 한다. 기본 구현을 작성하지 않고 선언만 한 메서드는 추상(abstract) 메서드라고 한다. 인터페이스의 모든 메서드는 자동으로 public이 된다.

 

// 정수형 시퀀스의 인터페이스 선언
public interface IntSequence {
	boolean hasNext(); // 다음 요소가 있는지 검사하는 메서드
    int next(); // 다음 요소를 얻는 메서드
}

// 인터페이스 메서드를 사용해 average 메서드 구현
public static double average(IntSequence seq, int n ){
	int count = 0;
    double sum = 0;
    while (seq.hasNext() && count < n) {
    	count++;
        sum += seq.next();
    }
    return count == 0 ? 0 : sum / count;
}

위의 코드에서는 아직 인터페이스를 구현하지 않았고 선언만 한 상황이다.

 

2) 인터페이스 구현

public class SquareSequence implements IntSequence {
	public int i;
    
    public boolean hasNext() {
    	return true;
    }
	
    public int next() {
    	i++;
        return i*i;
    }
}

implements 키워드는 SquareSequence 클래스가 IntSequence 인터페이스를 따른다는 의미이다. 인터페이스를 구현하는 클래스는 인터페이스 메서드를 반드시 public으로 선언해야한다. 그렇지 않으면 클래스의 메서드는 기본적으로 패키지만 접근이 가능하게 된다. 하지만 인터페이스는 public으로 접근이 가능한 것으로 요구되므로 컴파일 오류가 발생한다.

 

인터페이스를 따르는 클래스가 인터페이스의 메서드 중 일부만 구현한다면 해당 클래스는 반드시 abstract 제어자로 선언해야한다. 

 

3) 인스턴스 타입으로의 변환

// SquareSequence 클래스의 객체 만들기
SquareSequence squares = new SquareSequence();
double avg = average(squares, 100); // 변의 길이가 1씩 증가하는 정사각형의 너비 평균 구하기

// Interface 타입으로의 변환
IntSequence interfaceSquare = new SquareSequence();
double avg = average(interfaceSquare, 100);

interfaceSquare의 타입은 intSequence이다. 인터페이스를 구현한 어떤 클래스의 객체도 참조할 수 있다. 또한 이 객체를 해당 인터페이스를 파라미터로 전달받는 메서드에도 전달할 수 있다. 이때 T 타입의 모든 값을 S 타입의 변수에 할당할 수 있다면 T타입은 S 타입의 서브타입, S 타입은 T 타입의 슈퍼타입이라 한다. 위의 경우에는 IntSequence 인터페이스는 SquareSequence 클래스의 슈퍼타입이다. 

 

인터페이스 타입으로 변수를 선언할 순 있지만 타입이 인터페이스 자체인 객체는 만들 수 없다. 모든 객체는 클래스의 인스턴스라는 사실을 기억하자.

 

4) 캐스트와 instanceof 연산자

슈퍼타입에서 서브타입으로 변환을 시킬 때에는 값의 변화가 생길 수 있다. 이는 캐스트를 사용해 변환해야한다. 

IntSequence sequence = new SquareSequence();
SquareSequence squares = (SquareSequence) sequence; // 캐스팅

객체는 실제 클래스나 그 슈퍼타입으로만 캐스트할 수 있다. 잘못 캐스트하면 컴파일 시간 오류나 캐스트 예외가 발생한다. 예외가 발생하지 않게 하기 위해선 먼저 객체가 원하는 타입인지 instanceof 연산자로 검사할 수 있다.

// sequence가 SquareSequence를 슈퍼타입으로 둔 클래스의 인스턴스일 때 true 반환
if (sequence instanceof SquareSequence) { 
	SquareSequence squares = (SquareSequence) sequence);
    ...
}

 

5) 인터페이스 확장

인터페이스는 extends 키워드를 사용하면 또 다른 인터페이스를 확장해서 원래 있던 메서드 외의 추가 메서드를 요구하거나 제공할 수 있다. 

// 예외가 발생할 때 리소스를 닫는 인터페이스
public interface Closeable {
	void close();
}

// Channel 인터페이스는 Closeable 인터페이스를 확장한다.
public interface Channel extends Closeable { 
	boolean isOpen();
}

두 인터페이스 타입 중 어느 것으로든 해당 클래스의 객체를 변환할 수 있다.

 

6) 여러 인터페이스 구현

implements 뒤에 인터페이스 간 ,를 통해 구분해 여러 인터페이스를 구현할 수 있다. 

public class FileSequence implements IntSequence, Closeable {
	...
}

위의 예시에서는 FileSequence 클래스는 IntSequence와 Closeable을 슈퍼타입으로 둔다.

 

7) 상수

인터페이스에서 정의한 변수는 자동으로 public static final이 된다. 해당 상수는 '인터페이스.변수이름'으로 참조할 수 있다. 클래스가 인터페이스를 구현하는 경우에는 인터페이스의 이름인 한정어를 생략하고 간단하게 변수의 이름만 사용할 수 있다. 상수 집합에는 열거를 사용하는 것이 훨씬 좋다. 

 

인터페이스에서는 인스턴스 변수를 두지 않는다. 인터페이스는 객체의 상태가 아니라 동작을 명시한다.

 


2. 인터페이스의 정적 메서드, 기본메서드, 비공개 메서드

자바 8 이전에는 인터페이스의 모든 메서드가 추상 메서드여야했다. 즉, 메서드의 바디가 없어야했다. 자바 9에서는 실제 구현이 있는 메서드 세 종류(정적 메서드, 기본메서드, 비공개 메서드)를 인터페이스에 추가할 수 있다.

 

1) 정적 메서드

기술적으로는 가능하지만 인터페이스를 추상 명세로 보는 관점에는 정적 메서드를 넣는 것이 맞지 않았다. 하지만 팩토리 메서드는 인터페이스에 아주 잘 맞는다. 예를 들어 IntSequence 인터페이스에는 주어진 정수의 숫자 시퀀스를 만들어내는 정적 메서드인 digitsOf를 만들 수 있다. 이렇게 구현하는 경우에는 인스턴스가 어느 클래스의 인스턴스인지 신경쓸 필요가 없다.

 

public interface IntSequence {
	...
    static IntSequence digitsOf(int n) {
    	return new DigitSequence(n);
    }
}

 

2) 기본 메서드

default를 붙이면 기본 메서드를 구현할 수 있다.

// 정수형 시퀀스의 인터페이스 선언
public interface IntSequence {
	default boolean hasNext(); // 시퀀스는 무한으로 가정한다.
    int next(); 
}

이 인터페이스를 구현하는 클래스는 hasNext 메서드를 오버라이드하거나 기본 구현을 상속하는 방법 중 하나를 선택할 수 있다. 기본 메서드의 주요 용도는 인터페이스를 진화하는 것이다. 만약 인터페이스에 메서드를 추가하는 경우 이를 구현하는 클래스는 새로운 메서드를 구현하지 않으면 컴파일되지 않는 문제가 발생한다. 인터페이스에 기본 메서드가 아닌 메서드를 추가하면 소스 수준에서 호환되지 않는다. 

 

그런데 클래스를 다시 컴파일하지 않고 구현 클래스에 포함된 기존 JAR 파일을 그대로 사용한다면 여전히 클래스를 제대로 로드한다. 여전히 그 클래스의 인스턴스를 생성할 수 있고, 문제도 없다. (인터페이스에 메서드를 추가하는 것은 바이너리 수준에서 호환된다.) 하지만 프로그램에서 인스턴스를 통해 새로운 메서드를 호출하면 에러가 발생한다. 

 

인스턴스에서 메서드를 기본 메서드로 선언하면 이 문제를 해결할 수 있다. 즉, 구현 클래스가 제대로 컴파일된다. 또한 그 클래스를 다시 컴파일하지 않고 로드한 후 인스턴스를 통해 메서드를 호출하는 경우에도 잘 동작한다.

 

 

3) 기본 메서드의 충돌 해결

클래스가 두 개의 인터페이스를 구현할 때 한 인터페이스에는 기본 메서드가 있고, 다른 인터페이스에는 이 메서드의 이름, 매개변수 타입이 같은 메서드가 있다면 충돌이 발생한다. 이러한 경우에는 super 키워드를 통해 어떤 인스턴스의 메서드를 호출할 것인지 정해야한다. 

ex) AInterface.super.Afunc();

 

만약 두 인터페이스 모두 공유 메서드의 기본 구현을 제공하지 않으면 충돌이 발생하지 않는다. 이 경우 구현 클래스에 메서드를 구현하거나 메서드를 구현하지 않고 클래스를 abstract로 선언하면 된다.

 

 

4) 비공개 메서드(private methods)

자바 9부터 비공개 메서드를 만들 수 있다. 비공개 메서드는 static이나 인스턴스 메서드는 될 수 있지만 default 메서드는 될 수 없다. default 메서드는 오버라이드가 가능한 메서드이기 때문에 논리상 비공개 메서드와 맞지 않다. 비공개 메서드는 인터페이스 자체에 있는 메서드에서만 쓸 수 있다. 따라서 헬퍼 메서드로서 사용될 수 있다.


3. 인터페이스 예

인터페이스는 그저 클래스가 구현하기로 약속한 메서드 집합이기에 많은 일을 하는 것 같지는 않다. 이에 따라 4가지 예시를 통해 인터페이스의 역할을 알아보고자 한다.

 

1) Comparable 인터페이스

객체의 배열을 정렬할 때 정렬하고자하는 규칙을 알아야하고, 클래스가 제공하는 메서드를 호출할 수 있어야한다. 따라서 Comparable 인터페이스는 제너릭 타입으로 이를 구현한다.

public interface Comparable<T> { // 제너릭타입으로 인터페이스 구현
	int compareTo(T other);
}

 

public class Employee implements Comparable<Employee> {
	...
    
    
    // 음수일 경우 this의 값보다 other의 값이 우선
    // 양수일 경우 this의 값이 other의 값보다 우선
    // 0일 경우 this의 값과 other의 순서가 동일
    public int compareTo(Employee other) {
    	return getId() - other.getId();
    }
	
}

위의 코드는 오버플로우를 발생시키지 않는 이상 올바른 순서를 표현할 수 있을 것이다. 모든 정수에 올바르게 작동하는 Integer.compare 메서드를 사용하면 오버플로우 걱정없이 이를 구현할 수 있다. 또한 부동소수점에 대한 값을 비교할 때는 Double.compare을 사용해야 NaN이나 inf의 값들에 대해서도 올바르게 작동한다.

 

두 객체의 순서는 임금을 기준으로 한다.

public class Employee implements Comparable<Employee> {
	...
    
    public int compareTo(Employee other) {
    
    	// 부동소수점은 Double.compare 메서드 사용
    	return Double.compare(salary, other.salary); 
    }
	
}

 

 

2) Comparator 인터페이스

문자열을 사전 순서가 아닌 길이가 증가하는 순서로 비교한다면 compareTo 메서드를 두 가지 방법으로 구현하지 못한다. 또한 String 클래스는 수정할 수 없다. 이러한 상황에서 Arrays.sort 메서드는 배열과 비교자를 매개변수로 받아 이를 기준으로 정렬할 수 있다.

 

Comparator 인터페이스 

public interface Comparator<T> {
	int compare(T first, T second);
}

 

Comparator 구현

class LengthComparator implements Comparator<String> {
	public int compare(String first, String second) {
    	return first.length() - second.length();
    }
}

 

비교를 실제 수행하기 위해선 이 클래스의 인스턴스를 만들어야한다.

Compartor<String> comp = new LengthComparator();
if (comp.compare(words[i], words[j]) > 0 ) {
	... 
}

 

3) Runnable 인터페이스

- 모든 프로세서가 멀티 코어를 장착하고 있다면 모든 코어를 작업 중인 상태로 유지하고 싶을 것이다. 특정 태스크를 별도의 스레드에서 수행하거나 실행용 스레드 풀에 넣으려고 할 것이다. 이때 테스트를 정의하려면 Runnable 인터페이스를 구현해야한다. 이는 메서드가 하나만 있다.

 

Runnable 구현

class HelloTask implements Runnable {
	public void run() {
    	for (int i = 0; i < 1000; i++) {
        	System.out.println("Hello, World!");
        }
    }
}

 

태스트를 새 스레드에서 실행하려면 Runnable로 스레드를 생성하고 시작해야한다.

Runnable task = new HelloTask();
Thread thread = new Thread(task);
thread.start();

run 메서드는 별도의 스레드에서 실행되므로 현재 스레드는 다른 작업을 계속할 수 있다. 더 자세한 내용은 병행 프로그래밍 파트에서 공부할 예정이다.

 

 

4) 사용자 인터페이스 콜백

자바 기반 GUI 라이브러리에서는 콜백에 인터페이스를 사용한다. 예를 들어 JavaFX에서는 이벤트를 보고할 때 다음 인터페이스를 사용한다.

 

EventHandler 인터페이스

public interface EventHandler<T> {
	void handle(T event);
}

 

EventHandler 구현

class CancleAction implements EventHandler<ActionEvent> {
	public void handle(ActionEvent event) {
    	System.out.println("Oh noes!");
    }
}

 

버튼 객체 생성 후 콜백 함수로 캔슬 액션을 지정할 수 있다.

Button cancelButton = new Button("Cancel");
cancelButton.setOnAction(new CancelAction());
728x90
반응형