인터페이스 (Interface)
소프트웨어 엔지니어링에는 서로 다른 프로그래머 그룹이 소프트웨어가 상화작용하는 방식을 설명하는 계약(Contact)에 동의하는 것이 중요한 여러 가지 상황이 있다.
각 그룹은 다른 그룹의 코드가 어떻게 작성되는지는 전혀 몰라도 자신의 코드를 작성할 수 있어야 한다. 즉 추상화이다.
일반적으로 인터페이스는 그러한 계약이다.
게시된 인터페이스를 계속 준사하는 한 언제든지 소프트웨어를 수정할 수 있는 권리는 보유한다
자바에서의 인터페이스
자바 프로그래밍 언어에서 인터페이스를 클래스와 유사한 참조 타입이다. 상수, 메서드 시그니, 기본 메서드, 정적 메서드, 중첩 타입 (인터페이스)만을 포함할 수 있다.
메서드 본문은 기본 메서드나 정적 메서드에서만 존재한다.
클래스에 의해 구현되거나 다른 인터페이스에 의해 확장될 수만 있다.
인터페이스에서는 추상(Abstarct) 메서드가 생략되어 있지만 적용되어 있다. 추상 메서드는 바디가 없다.
인스턴스화 할 수 있는 클래스가 인터페이스를 구현하면, 인터페이스에 선언된 각 메서드에 대한 메서드 본문을 제공한다.
# 인터페이스는 값을 변경할 수 있는 필드를 가질 수 없다.
API에서의 인터페이스
일반적은 소프트웨어 회사는 다른 회사가 자체 소프트웨어 제품에 사용하려는 복잡한 방법이 포함된 소트프웨에 패키지를 판매한다.
예시로 이미지 처리 회사는 고객에게 공개되는 인터페이스를 구현하기 위해 클래스를 작성하고, 그래픽 회사는 인터페이스에 정의된 서명 및 반환 유형을 사용하여 이미지 처리 방법을 호출한다.
이미지 처리 회사의 API는 고객에게 공개되지는 API 구현은 극비로 유지된다.
인터페이스를 계속 구현하는 한 나중에 구현을 수정할 수 있다.
인터페이스 선언
수정자, 인터페이스 키워드, 인터페이스 명, 쉼표로 구분된 상위 인터페이스 목록 및 인터페이스 본문으로 구성된다,
public interface GroupedInterface extends Interface1, Interface2, Interface3 {
double E = 2.718282;
void doSomething (int i, double x);
int doSomethingElse(String s);
}
인터페이스를 public으로 지정하지 않으면 이 인터페이스와 동일한 패키지에 정의된 클래스에만 이 인터페이스에 액세스할 수 있다.
인터페이스는 클래수 하위 클래스처럼 다른 인터페이스를 확장하거나 다른 클래스를 확장활 수 있다. 그러나 클래스는 다른 클래스 하나만 확장할 수 있는 반면 인터페이스는 여러 인터페이스를 확장할 수 있습니다.
인터페이스 바디
인터페이스 본문에는 abstract 메서드, default 메서드 및 정적 메서드가 포함될 수 있습니다. 인터페이스 내의 추상 메서드 뒤에는 세미콜론이 오고 중괄호는 없다. (추상 메서드에는 구현이 포함되지 않음)
기본 메서드는 default 수정자로 정의되고 정적 메서드는 static 키워드로 정의된다. 인터페이스는 모든 추상, 기본 및 정적 메서드는 암시적으로 공개되므로 public 한정자를 생략할 수 있다.
또한 인터페이스에는 상수 선언이 포함될 수 있다. 인터페이스에 정의된 도믄 상수 값은 암시적으로 public, static 및 final이다.
인터페이스 구현 (Implementing an Interface)
인터페이스를 구현하는 클래스를 선언하려먼 클래스 선언에 implements 절을 포함한다. 클래스는 두 개 이상의 인터페이스를 구현할 수 있으므로, implements 키워드 뒤에는 클래스에서 구현하는 인터페이스의 쉼표로 구분된 목록이 온다.
관례적으로 implements 절은 extensions 절 뒤에 온다.
public interface Relatable {
public int isLargerThan(Relatable other);
}
public class RectanglePlus implements Relatable {
public int width = 0;
public int height = 0;
public Point origin;
// four constructors
public RectanglePlus() {
origin = new Point(0, 0);
}
public RectanglePlus(Point p) {
origin = p;
}
public RectanglePlus(int w, int h) {
origin = new Point(0, 0);
width = w;
height = h;
}
public RectanglePlus(Point p, int w, int h) {
origin = p;
width = w;
height = h;
}
// a method for moving the rectangle
public void move(int x, int y) {
origin.x = x;
origin.y = y;
}
// a method for computing
// the area of the rectangle
public int getArea() {
return width * height;
}
// a method required to implement
// the Relatable interface
public int isLargerThan(Relatable other) {
RectanglePlus otherRect
= (RectanglePlus)other;
if (this.getArea() < otherRect.getArea())
return -1;
else if (this.getArea() > otherRect.getArea())
return 1;
else
return 0;
}
}
# RectanglePlus otherRect = (RectanglePlus)other 신택스 타입을 사용하여 타입을 변경하는 캐스팅을 하는 것이다.
Relatable 인터페이스에 정의된 isLargerThen 메서드는 Relatalbe 타입의 객체를 사용한다. other를 RectanglePlus 인스턴스로 캐스팅한다. 타입 캐스팅은 컴파일러에게 객체가 실제로 무엇인지 알려준다. 다른 인스턴스 (other.getArea())에서 getArea를 직접 호출하면 컴파일러가 other가 실제로 RectanglePlus의 인스턴스라는 것을 이해하지 못하기 때문에 컴파일 오류가 발생한다.
인터페이스 타입 사용
새 인터페이스를 정의하면 새 참조 데이터 유형이 정의된다. 다른 데이터 타입 이름을 사용할 수 있는 모든 곳에서 인터페이스 이름을 사용할 수 있습니다. 타입이 인터페이스인 참조 변수를 정의하는 경우, 여기에 할당하는 모든 객체는 인터페이스를 구현하는 클래스의 인스턴스여야 합니다.
public Object findLargest(Object object1, Object object2) {
Relatable obj1 = (Relatable)object1;
Relatable obj2 = (Relatable)object2;
if ((obj1).isLargerThan(obj2) > 0)
return object1;
else
return object2;
}
위의 코드는 object1을 Relatable 타입으로 캐스팅하여 isLargerThen 메서드를 호출할 수 있다.
다양한 클래스를 Relatatble을 구현하는 경우 해당 클래스 중 하나에서 인스턴스화된 객체를 findLargert() 메서드와 비교할 수 있다. 단, 두 객체가 동일한 클래스에 속해야 한다.
# Relatable 타입으로 타입 캐스팅이 되기 위해 각 파라미터의 아규먼트는 Relatable을 구현한 객체 참조를 전달 받았다.
public Object findSmallest(Object object1, Object object2) {
Relatable obj1 = (Relatable)object1;
Relatable obj2 = (Relatable)object2;
if ((obj1).isLargerThan(obj2) < 0)
return object1;
else
return object2;
}
public boolean isEqual(Object object1, Object object2) {
Relatable obj1 = (Relatable)object1;
Relatable obj2 = (Relatable)object2;
if ((obj1).isLargerThan(obj2) == 0)
return true;
else
return false;
}
이러한 메서드는 클래스 상속이 무엇이든 "관련 가능한" 모든 객체에 대해 작동한다.
Relatable을 구현할 때 자체 클래스 또는 슈퍼 클래스 유형과 Relatalble 유형이 모두 될 수 있다.
이는 다중 상속의 장점 중 일부를 제공하며, 슈퍼 클래스와 인터페이스 모두에서 동작할 수 있다.
인터페이스의 확장 (Evolvoing)
public interface DoIt {
void doSomething(int i, double x);
int doSomethingElse(String s);
}
위 코드에서 세 번째 메서드를 추가한다고 가정한다.
public interface DoIt {
void doSomething(int i, double x);
int doSomethingElse(String s);
boolean didItWork(int i, double x, String s);
}
이렇게 변경하면 DoIt 인터페이스를 구현하는 모든 클래스가 더 이상 이전 인터페이스를 구현하지 않기 때문에 중단된다.
인터페이스는 모든 용도를 예상하고 처음부터 완전히 지정한다.
인터페이스에 추가 메서드를 추가하려는 경우 몇가지 옵션이 있다.
아래의 코드는 DoIt을 확장하는 DoItPlus 인터페이스를 마드는 코드이다.
public interface DoItPlus extends DoIt {
boolean didItWork(int i, double x, String s);
}
이제 코드 사용자는 기존 인터페이스를 계속 사용할지 아니면 새 인터페이스로 업그레이드할지 선택할 수 있다.
혹은 새 방법을 default 메서드로 정의할 수 있다.
public interface DoIt {
void doSomething(int i, double x);
int doSomethingElse(String s);
default boolean didItWork(int i, double x, String s) {
}
}
default 메서드에 대한 구현을 제공해야 한다. 기존 인터페이스에 새로운 정적 메서드를 정의할 수도 있다.
새로운 기본 메서드나 정적 메서드로 향상된 인터페이스를 구현하는 클래스가 있는 사용자는 추가 메서드를 수용하기 위해 클래스를 수정하거나 다시 컴파일할 필요가 없다.
디폴트 메서드
원래 인터페이스에 메서드를 추가하면 해당 인터페이스를 구현한 프로그래머는 구현을 다시 작성해야한다. 정적 메서드로 추가하면 프로그래머는 이를 필수 핵심 메서가드 아닌 유틸리티 메서드로 간주하게 된다.
디폴트 메서드를 사용하면 라이브러니 인터페이스에 새 기능을 추가하고 해당 인터페이스의 이전 버전용으로 작성된 코드와 바이너리 호환성을 보장할 수 있다.
import java.time.*;
public interface TimeClient {
void setTime(int hour, int minute, int second);
void setDate(int day, int month, int year);
void setDateAndTime(int day, int month, int year,
int hour, int minute, int second);
LocalDateTime getLocalDateTime();
}
package defaultmethods;
import java.time.*;
import java.lang.*;
import java.util.*;
public class SimpleTimeClient implements TimeClient {
private LocalDateTime dateAndTime;
public SimpleTimeClient() {
dateAndTime = LocalDateTime.now();
}
public void setTime(int hour, int minute, int second) {
LocalDate currentDate = LocalDate.from(dateAndTime);
LocalTime timeToSet = LocalTime.of(hour, minute, second);
dateAndTime = LocalDateTime.of(currentDate, timeToSet);
}
public void setDate(int day, int month, int year) {
LocalDate dateToSet = LocalDate.of(day, month, year);
LocalTime currentTime = LocalTime.from(dateAndTime);
dateAndTime = LocalDateTime.of(dateToSet, currentTime);
}
public void setDateAndTime(int day, int month, int year,
int hour, int minute, int second) {
LocalDate dateToSet = LocalDate.of(day, month, year);
LocalTime timeToSet = LocalTime.of(hour, minute, second);
dateAndTime = LocalDateTime.of(dateToSet, timeToSet);
}
public LocalDateTime getLocalDateTime() {
return dateAndTime;
}
public String toString() {
return dateAndTime.toString();
}
public static void main(String... args) {
TimeClient myTimeClient = new SimpleTimeClient();
System.out.println(myTimeClient.toString());
}
}
위 예제는 현재 시간을 출력하는 코드이다.
여기에 새로운 기능을 메서드로 인터페이스에 추가한다고 가정한다.
public interface TimeClient {
void setTime(int hour, int minute, int second);
void setDate(int day, int month, int year);
void setDateAndTime(int day, int month, int year,
int hour, int minute, int second);
LocalDateTime getLocalDateTime();
ZonedDateTime getZonedDateTime(String zoneString);
}
인터페이스를 수정했기 떄문에 SimpleTimeClient 클래스를 수정하고 getZonedDateTime 메서드를 구현해야한다.
그러나 getZonedDateTime을 abstract로 두는 대신 디폴트로 구현하여 정의할 수 있다.
import java.time.*;
public interface TimeClient {
void setTime(int hour, int minute, int second);
void setDate(int day, int month, int year);
void setDateAndTime(int day, int month, int year,
int hour, int minute, int second);
LocalDateTime getLocalDateTime();
static ZoneId getZoneId (String zoneString) {
try {
return ZoneId.of(zoneString);
} catch (DateTimeException e) {
System.err.println("Invalid time zone: " + zoneString +
"; using default time zone instead.");
return ZoneId.systemDefault();
}
}
default ZonedDateTime getZonedDateTime(String zoneString) {
return ZonedDateTime.of(getLocalDateTime(), getZoneId(zoneString));
}
}
인터페이스의 메서드 정의가 메서드 시그니처 시작부분에 default 키워드를 사용하여 디폴트 메서드임을 지정한다.
디폴트 메서드를 포함하여 인터페이스의 모든 메서드 선언은 암시적으로 공개되므로 public 한정자를 생략할 수 있다.
이 인터페이스를 사용하면 SimpleTimeClient 클래스를 수정할 필요가 없으며 이 클래스와 TimeClient 인터페이스를 구성하는 모든 클래스에는 getZoneedDateTime 메서드가 이미 정의되어 있다.
아래의 코드 예제는 SimpleTimeClient의 인스턴스에서 getZonedDateTime 메서드를 호출한다.
import java.time.*;
import java.lang.*;
import java.util.*;
public class TestSimpleTimeClient {
public static void main(String... args) {
TimeClient myTimeClient = new SimpleTimeClient();
System.out.println("Current time: " + myTimeClient.toString());
System.out.println("Time in California: " +
myTimeClient.getZonedDateTime("Blah blah").toString());
}
}
인터페이스 확장과 디폴트 메서드 (Extending Interfaces That Contain Default Methods)
- 디폴트 메서드를 전혀 언급하지 않아서, 확장된 인터페이스 디폴트 메서드를 상속받도록 한다.
- 디폴트 메서드를 다시 선언하여 abstract 메서드로 만든다.
- 디폴트 메서드를 재정의하여 override 한다.
이와 같은 방식으로 확장된 인터페이스에서 디폴트 메서드를 처리할 수 있다.
아래의 코드 예제는 인터페이스를 구현하는 모든 클래스에는 디폴트 메서드 TimeClient.getZonedDateTime에 의해 지정된 구현이 있다.
public interface AnotherTimeClient extends TimeClient { }
다음과 같이 TimeClient 인터페이스를 확장한다고 가정한다.
public interface AbstractZoneTimeClient extends TimeClient {
public ZonedDateTime getZonedDateTime(String zoneString);
}
AbstractZoneTimeClient 인터페이스를 구현하는 모든 클래스는 getZonedDateTime 메서드를 구현해야 한다. 이 메서드는 인터페이스의 다른 모든 디폴트가 아닌 (비정적) 메서드와 같은 추상 메서드이다.
다음 코드는 인터페이스를 확장한 TimeClient이다.
public interface HandleInvalidTimeZoneClient extends TimeClient {
default public ZonedDateTime getZonedDateTime(String zoneString) {
try {
return ZonedDateTime.of(getLocalDateTime(),ZoneId.of(zoneString));
} catch (DateTimeException e) {
System.err.println("Invalid zone ID: " + zoneString +
"; using the default time zone instead.");
return ZonedDateTime.of(getLocalDateTime(),ZoneId.systemDefault());
}
}
}
HandleInvalidTimeZoneClient 인터페이스가 구현하는 모든 클래스는 TimeClient 인터페이스가 지정한 getZonedDateTime 구현 대신 인터페이스가 지정한 getZonedDateTime 구현을 사용한다.
정적 메서드
인터페이스에서 static 메서드를 정의할 수 있다. 정적 메서드는 객체가 아니라 정의된 클래스에 연결된 메서드이다. 클래스의 모든 인스턴스는 정적 메서드를 공유한다.
이렇게 하면 라이브러리에서 도우미(Helper) 메서드를 더 쉽게 구성할 수 있습니다.
별도의 클래스가 이난 동일한 인터페이스의 인터페이스에 특정한 정적 메서드를 유지할 수 있습니다.
아래의 코드는 시간대 식별자에 해당하는 ZoneId 객체를 검색하는 정적 메서드를 정의한다.
만약 지정된 식별자에 해당하는 ZoneId 객체가 없으면 시스템 기본 시간대를 사용한다.
public interface TimeClient {
// ...
static public ZoneId getZoneId (String zoneString) {
try {
return ZoneId.of(zoneString);
} catch (DateTimeException e) {
System.err.println("Invalid time zone: " + zoneString +
"; using default time zone instead.");
return ZoneId.systemDefault();
}
}
default public ZonedDateTime getZonedDateTime(String zoneString) {
return ZonedDateTime.of(getLocalDateTime(), getZoneId(zoneString));
}
}
클래스의 정적 메서드와 마찬가지로 인터페이스의 메서드 정의가 메서드 시그니처 시작 부분에 static 키워드를 사용하여 정적 메서드임을 지정한다.
정적 메서드를 포함하여 인터페이스의 모든 메서드 선언은 암시적으로 공개되므로 public 한정자를 생략할 수 있다.
Integration Default Methods into Existing Libraries
디폴트 메서드를 사용하면 기존 인터페이스에 새 기능을 추가하고 해당 인터페이스의 이전 버전 전용으로 작성된 코드와의 바이너리 호환성을 보장할 수 있다.
특히 디폴트 메서드를 사용하면 람다 표현식을 파라미터로 허용하는 메서드를 기존 인터페이스에 추가할 수 있다.
public interface Card extends Comparable<Card> {
public enum Suit {
DIAMONDS (1, "Diamonds"),
CLUBS (2, "Clubs" ),
HEARTS (3, "Hearts" ),
SPADES (4, "Spades" );
private final int value;
private final String text;
Suit(int value, String text) {
this.value = value;
this.text = text;
}
public int value() {return value;}
public String text() {return text;}
}
public enum Rank {
DEUCE (2 , "Two" ),
THREE (3 , "Three"),
FOUR (4 , "Four" ),
FIVE (5 , "Five" ),
SIX (6 , "Six" ),
SEVEN (7 , "Seven"),
EIGHT (8 , "Eight"),
NINE (9 , "Nine" ),
TEN (10, "Ten" ),
JACK (11, "Jack" ),
QUEEN (12, "Queen"),
KING (13, "King" ),
ACE (14, "Ace" );
private final int value;
private final String text;
Rank(int value, String text) {
this.value = value;
this.text = text;
}
public int value() {return value;}
public String text() {return text;}
}
public Card.Suit getSuit();
public Card.Rank getRank();
}
import java.util.*;
import java.util.stream.*;
import java.lang.*;
public interface Deck {
List<Card> getCards();
Deck deckFactory();
int size();
void addCard(Card card);
void addCards(List<Card> cards);
void addDeck(Deck deck);
void shuffle();
void sort();
void sort(Comparator<Card> c);
String deckToString();
Map<Integer, Deck> deal(int players, int numberOfCards)
throws IllegalArgumentException;
}
PlayingCard 클래스는 Card 인터페이스를 구현하고 StandardDeck 클래는 Deck 인터페이스를 구현한다.
StandardDeck 클래스는 다음과 같이 추상 메서드 Deck.sort를 구현한다.
public class StandardDeck implements Deck {
private List<Card> entireDeck;
// ...
public void sort() {
Collections.sort(entireDeck);
}
// ...
}
CompareTo 메서드를 사용하면 Standard Deck.sort() 메서드가 먼저 카드 덱을 모양별로 정렬한 다음 순위별로 정렬된다.
새로운 정렬 기준을 지정하려면 Comparator 인터페이스를 구현하고 sort (List<T> Comparator<?super T> c) 메서드를 사용해야한다.
public void sort(Comparator<Card> c) {
Collections.sort(entireDeck, c);
}
PlayingCard 클래스는 Card 인터페이스를 구현하고 StandardDeck 클래스는 Deck 인터페이스를 구현한다.
public class StandardDeck implements Deck {
private List<Card> entireDeck;
// ...
public void sort() {
Collections.sort(entireDeck);
}
// ...
}
Collections.sort 메소드는 엘리먼트 타입이 Comparable 인터페이스를 구현하는 List 인스턴스를 정렬한다. 전체 Deck 멤버는 엘리먼트가 Comparable를 확장하는 Card 유형인 List의 인스턴스이다.
아래 코드는 PlayingCard 클래스가 Comparable.compareTo 메서드를 구현한다.
public int hashCode() {
return ((suit.value()-1)*13)+rank.value();
}
public int compareTo(Card o) {
return this.hashCode() - o.hashCode();
}
CompareTo 메서드를 사용하면 Standard Deck.sort() 메서드가 먼저 덱을 모양별로 정렬하고 순위별로 정렬한다
public void sort(Comparator<Card> c) {
Collections.sort(entireDeck, c);
}
package defaultmethods;
import java.util.*;
import java.util.stream.*;
import java.lang.*;
public class SortByRankThenSuit implements Comparator<Card> {
public int compare(Card firstCard, Card secondCard) {
int compVal =
firstCard.getRank().value() - secondCard.getRank().value();
if (compVal != 0)
return compVal;
else
return firstCard.getSuit().value() - secondCard.getSuit().value();
}
}
위의 코드로 Collecions.sort 메서드가 Card 클래스의 인스턴스를 정렬하는 방법을 지정할 수 있다.
다음 호출은 카드 놀이 덱을 먼저 순위별로 정렬한 다음 모양별로 정렬한다,
StandardDeck myDeck = new StandardDeck();
myDeck.shuffle();
myDeck.sort(new SortByRankThenSuit());
하지만 위 코드는 점근방식이 너무 장황하므로 정렬 기준만 지정하고 여러 정렬을 구현하는 것을 피해야한다.
아래 코드는 모양에 관계없이 순위별로 카드 덱을 정렬한다.
StandardDeck myDeck = new StandardDeck();
myDeck.shuffle();
myDeck.sort(
(firstCard, secondCard) ->
firstCard.getRank().value() - secondCard.getRank().value()
);
Coparator 인터페이스는 함수형 인터페이스이므로 람다 표현식을 sort 메서드의 매게변수로 사용할 수 있다.
myDeck.sort(Comparator.comparing((card) -> card.getRank()));
혹은 method reference를 사용할 수 있다.
myDeck.sort(Comparator.comparing(Card::getRank));
위 방식은 다양한 정렬 기준을 지정하고 여러 정렬 구현을 생성하지 않는 방법을 더 잘 보여준다.
StandardDeck myDeck = new StandardDeck();
myDeck.shuffle();
myDeck.sort(
(firstCard, secondCard) -> {
int compare =
firstCard.getRank().value() - secondCard.getRank().value();
if (compare != 0)
return compare;
else
return firstCard.getSuit().value() - secondCard.getSuit().value();
}
);
위코드는 람다 표현식을 사용하여 카드 덱을 순위별로 정렬한 다음 모양별로 정렬하도록 변경한 것이다.
디폴트 메서드 theComparing을 사용하여 인터페이스를 향상시킬 수 있다.
myDeck.sort(
Comparator
.comparing(Card::getRank)
.thenComparing(Comparator.comparing(Card::getSuit)));
아래 코드는 람다 표현식을 사용하여 순위를 내림차순으로 먼저 카드 덱을 정렬한 것이다.
myDeck.sort(
Comparator.comparing(Card::getRank)
.reversed()
.thenComparing(Comparator.comparing(Card::getSuit)));
디폴트 메서드, 정적 메서드, 람다 표현식 및 메서드 레퍼런드를 사용하여 인터페이스를 강화하고 프로그래머가 호출방법을 보고 기능을 빠르게 추론할 수 있는 표현력이 뛰어난 라이브러리를 만드는 방법을 보여준다,
'자바 튜토리얼' 카테고리의 다른 글
리팩토링 (Refactoring) (0) | 2024.07.11 |
---|---|
인터페이스와 상속 (Interface and Inheritance) [1] (0) | 2024.07.08 |
클래스와 객체 (Classes and Objects) [4] (0) | 2024.07.05 |
클래스와 객체 (Classes and Objects) [3] (0) | 2024.07.04 |
클래스와 객체 (Classes and Objects) [2] (0) | 2024.07.03 |