2024. 6. 3. 11:04ㆍHigh Level Programming Language/Learning the Java Language
Interface
이전 강의에서 인터페이스 구현의 예를 보았습니다. 여기에서 인터페이스에 대한 자세한 내용을 읽을 수 있습니다. 인터페이스의 용도, 인터페이스를 작성하려는 이유, 작성 방법 등이 있습니다.
Inheritance
이 섹션에서는 특정 클래스를 다른 클래스에서 파생시키는 방법을 설명합니다. 즉, 하위 클래스가 상위 클래스의 필드와 메서드를 상속할 수 있는 방법입니다. 모든 클래스가 Object 클래스에서 파생된다는 사실과 하위 클래스가 슈퍼클래스에서 상속하는 메서드를 수정하는 방법을 배우게 됩니다. 이 섹션에서는 인터페이스와 유사한 추상 클래스도 다룹니다.
Interface
소프트웨어 엔지니어링에는 서로 다른 프로그래머 그룹이 소프트웨어가 상호 작용하는 방식을 설명하는 "계약[contact]"에 동의하는 것이 중요한 여러 가지 상황이 있습니다. 각 그룹은 다른 그룹의 코드가 어떻게 작성되는지 전혀 알지 못해도 자신의 코드를 작성할 수 있어야 합니다. 일반적으로 인터페이스는 그러한 계약입니다.
예를 들어, 컴퓨터로 제어되는 로봇 자동차가 인간 운전자 없이 도시 거리를 통해 승객을 수송하는 미래 사회를 상상해 보십시오. 자동차 제조업체는 정지, 출발, 가속, 좌회전 등 자동차를 작동하는 소프트웨어(물론 Java)를 작성합니다. 또 다른 산업 그룹인 전자 유도 장치 제조업체는 GPS(Global Positioning System) 위치 데이터와 교통 상황의 무선 전송을 수신하고 해당 정보를 사용하여 자동차를 운전하는 컴퓨터 시스템을 만듭니다.
자동차 제조업체는 자동차(제조업체의 모든 자동차)를 움직이기 위해 호출할 수 있는 메서드를 자세히 설명하는 업계 표준 인터페이스를 게시해야 합니다. 그런 다음 안내 제조업체는 인터페이스에 설명된 메서드를 호출하여 자동차에 명령을 내리는 소프트웨어를 작성할 수 있습니다. 두 산업 그룹 모두 다른 그룹의 소프트웨어가 어떻게 구현되는지 알 필요가 없습니다. 실제로 각 그룹은 자사의 소프트웨어가 매우 독점적이라고 생각하며 게시된 인터페이스를 계속 준수하는 한 언제든지 소프트웨어를 수정할 수 있는 권리를 보유합니다.
Interfaces in Java
자바 프로그래밍 언어에서 인터페이스는 클래스와 유사한 참조 타입으로, 상수, 메서드 시그니, 디폴트 메서드, 정적 메서드, 중첩 타입[인터페이스]만을 포함할 수 있습니다. 메서드 본문은 디폴트 메서드와 정적 메서드에만 존재합니다. 인터페이스는 인스턴스화할 수 없으며, 클래스에 의해 구현되거나 다른 인터페이스에 의해 확장될 수만 있습니다. 확장에 대해서는 이 레슨의 나중에 논의됩니다.
인터페이스 정의는 새로운 클래스를 만드는 것과 유사합니다:
public interface OperateCar {
// 상수 선언, 있을 경우
// 메서드 서명
// RIGHT, LEFT 값을 가진 enum
int turn(Direction direction,
double radius,
double startSpeed,
double endSpeed);
int changeLanes(Direction direction,
double startSpeed,
double endSpeed);
int signalTurn(Direction direction,
boolean signalOn);
int getRadarFront(double distanceToCar,
double speedOfCar);
int getRadarRear(double distanceToCar,
double speedOfCar);
......
// 더 많은 메서드 서명
}
메서드 시그니처에 중괄호가 없으며 세미콜론으로 종료된다는 점에 유의하세요.
인터페이스를 사용하려면, 해당 인터페이스를 구현하는 클래스를 작성합니다. 인스턴스화할 수 있는 클래스가 인터페이스를 구현하면, 이 클래스는 인터페이스에 선언된 각 메서드에 대한 메서드 본문을 제공합니다. 예를 들어,
public class OperateBMW760i implements OperateCar {
// OperateCar 메서드 서명과 구현 --
// 예를 들어:
public int signalTurn(Direction direction, boolean signalOn) {
// BMW의 왼쪽 방향 지시등을 켜는 코드
// BMW의 왼쪽 방향 지시등을 끄는 코드
// BMW의 오른쪽 방향 지시등을 켜는 코드
// BMW의 오른쪽 방향 지시등을 끄는 코드
}
// 필요에 따라 다른 멤버 -- 예를 들어, 인터페이스의 클라이언트에게 보이지 않는 헬퍼 클래스
}
위의 로봇 자동차 예제에서, 인터페이스를 구현하는 것은 자동차 제조업체입니다. 쉐보레의 구현은 토요타의 구현과 크게 다르겠지만, 두 제조업체 모두 동일한 인터페이스를 준수할 것입니다. 인터페이스의 클라이언트인 가이드 시스템 제조업체는 GPS 데이터, 디지털 도로 지도, 교통 데이터를 사용하여 자동차를 운전하는 시스템을 구축할 것입니다. 그렇게 할 때, 가이드 시스템은 회전, 차선 변경, 브레이크, 가속 등의 인터페이스 메서드를 호출할 것입니다.
Interfaces as APIs
로봇 자동차의 예는 업계 표준 API(응용 프로그래밍 인터페이스)로 사용되는 인터페이스를 보여줍니다. API는 상용 소프트웨어 제품에서도 흔히 사용됩니다. 일반적으로 회사는 다른 회사가 자체 소프트웨어 제품에 사용하려는 복잡한 방법이 포함된 소프트웨어 패키지를 판매합니다. 최종 사용자 그래픽 프로그램을 만드는 회사에 판매되는 디지털 이미지 처리 방법 패키지를 예로 들 수 있습니다. 이미지 처리 회사는 고객에게 공개되는 인터페이스를 구현하기 위해 클래스를 작성합니다. 그런 다음 그래픽 회사는 인터페이스에 정의된 서명 및 반환 유형을 사용하여 이미지 처리 방법을 호출합니다. 이미지 처리 회사의 API는 (고객에게) 공개되지만 API 구현은 극비로 유지됩니다. 실제로 원래 인터페이스를 계속 구현하는 한 나중에 구현을 수정할 수도 있습니다. 고객이 의존해 온 것입니다.
Defining an Interface
인터페이스 선언은 액세스 수정자(public, package-private), interface 키워드, 인터페이스 이름, 쉼표로 구분된 상위 인터페이스 목록(extends 키워드 뒤에 있는 경우) 및 인터페이스 본문[{ }]으로 구성됩니다. 예를 들어:
public interface GroupedInterface extends Interface1, Interface2, Interface3 {
// constant declarations
// base of natural logarithms
double E = 2.718282;
// method signatures
void doSomething (int i, double x);
int doSomethingElse(String s);
}
public 액세스 지정자는 모든 패키지의 모든 클래스에서 인터페이스를 사용할 수 있음을 나타냅니다. 인터페이스를 public으로 지정하지 않으면 이 인터페이스와 동일한 패키지에 정의[package-private]된 클래스에만 이 인터페이스에 액세스할 수 있습니다.
interface PackagePrivateInterface {
void someMethod();
}
인터페이스는 클래스 하위 클래스처럼 다른 인터페이스를 확장하거나 다른 클래스를 확장할 수 있습니다. 그러나 클래스는 다른 클래스 하나만 확장할 수 있는 반면 인터페이스는 여러 인터페이스를 확장할 수 있습니다. 인터페이스 선언에는 확장되는 모든 인터페이스의 쉼표로 구분된 목록이 포함됩니다.
The Interface Body
인터페이스 본문에는 abstract 메서드, default 메서드 및 static 메서드가 포함될 수 있습니다. 인터페이스 내의 추상 메서드 뒤에는 세미콜론이 오고 중괄호는 없습니다(추상 메서드에는 구현이 포함되지 않음). default 메소드는 default 수정자로 정의되고 static 메소드는 static 키워드로 정의됩니다. 인터페이스의 모든 abstract, default 및 static 메서드는 암시적으로 공개되므로 public 수정자를 생략할 수 있습니다.
또한 인터페이스에는 상수 선언이 포함될 수 있습니다. 인터페이스에 정의된 모든 상수 값은 암시적으로 public, static 및 final입니다. 다시 한 번 이러한 수정자를 생략할 수 있습니다.
Implementing an Interface
인터페이스를 구현하는 클래스를 선언하려면 클래스 선언에 implements 절을 포함합니다. 클래스는 두 개 이상의 인터페이스를 구현할 수 있으므로, implements 키워드 뒤에는 클래스에서 구현하는 인터페이스의 쉼표로 구분된 목록이 옵니다. 관례적으로, implements 절은 extensions 절(있는 경우) 뒤에 옵니다.
A Sample Interface, Relatable
객체의 크기를 비교하는 방법을 정의하는 인터페이스를 고려해보세요.
public interface Relatable {
// this (object calling isLargerThan)
// and other must be instances of
// the same class returns 1, 0, -1
// if this is greater than,
// equal to, or less than other
public int isLargerThan(Relatable other);
}
유사한 객체의 크기를 비교할 수 있으려면 객체가 무엇이든 관계없이 해당 객체를 인스턴스화하는 클래스에서 Relatable을 구현해야 합니다.
클래스에서 인스턴스화된 객체의 상대적인 "크기"를 비교할 수 있는 방법이 있는 경우 모든 클래스는 Relatable을 구현할 수 있습니다. 문자열의 경우 문자 수가 될 수 있습니다. 책의 경우 페이지 수가 될 수 있습니다. 학생들에게는 무게가 될 수 있습니다. 기타 등등. 평면형 기하학적 개체의 경우 면적이 좋은 선택이고(다음 RectanglePlus 클래스 참조) 볼륨은 3차원 기하학적 개체에 적합합니다. 이러한 모든 클래스는 isLargerThan() 메서드를 구현할 수 있습니다.
임의의 클래스가 Relatable을 구현한다는 것을 알고 있다면 해당 클래스에서 인스턴스화된 객체의 크기를 비교할 수 있다는 것을 알고 있습니다.
Implementing the Relatable Interface
다음은 객체 생성 섹션에 제시된 Rectangle 클래스이며, Relatable을 구현하도록 다시 작성[RectanglePlus]되었습니다.
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는 Relatable을 구현하므로 두 RectanglePlus 객체의 크기를 비교할 수 있습니다.
참고: Relatable 인터페이스에 정의된 isLargerThan 메소드는 Relatable 타입의 객체를 사용합니다. 이전 예제에서 굵게 표시된 코드 라인은 other를 RectanglePlus 인스턴스로 캐스팅합니다. 타입 캐스팅은 컴파일러에게 객체가 실제로 무엇인지 알려줍니다. 다른 인스턴스(other.getArea())에서 getArea를 직접 호출하면 컴파일러가 other가 실제로 RectanglePlus의 인스턴스라는 것을 이해하지 못하기 때문에 컴파일에 실패합니다.
Using an Interface as a Type
새 인터페이스를 정의하면 새 참조 데이터 타입이 정의됩니다. 다른 데이터 타입 이름을 사용할 수 있는 모든 곳에서 인터페이스 이름을 사용할 수 있습니다. 타입이 인터페이스인 참조 변수를 정의하는 경우 여기에 할당하는 모든 객체는 이 인터페이스를 구현하는 클래스의 인스턴스여야 합니다.
예를 들어, Relatable을 구현하는 클래스에서 인스턴스화되는 모든 객체에 대해 두 개의 객체에서 가장 큰 객체를 찾는 방법은 다음과 같습니다.
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 타입으로 캐스팅하면 isLargerThan 메서드를 호출할 수 있습니다.
다양한 클래스에서 Relatable을 구현하려는 경우 해당 클래스 중 하나에서 인스턴스화된 객체를 findLargest() 메서드와 비교할 수 있습니다. 단, 두 객체가 모두 동일한 클래스에 속해야 합니다. 마찬가지로 다음 방법으로 모두 비교할 수 있습니다.
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을 구현할 때 자체 클래스(또는 슈퍼클래스) 타입과 Relatable 타입이 모두 될 수 있습니다. 이는 다중 상속의 장점 중 일부를 제공하며, 슈퍼클래스와 인터페이스 모두에서 동작할 수 있습니다.
위 설명에 대한 샘플 코드는 다음과 같습니다.
Relatable 인터페이스를 다음과 같이 변경합니다
package com.intheeast.interfaces;
public interface Relatable {
public int isLargerThan(Relatable other);
double getArea();
public static Object findLargest(Object object1, Object object2) {
Relatable obj1 = (Relatable)object1;
Relatable obj2 = (Relatable)object2;
if (obj1.isLargerThan(obj2) > 0)
return object1;
else
return object2;
}
public static Object findSmallest(Object object1, Object object2) {
Relatable obj1 = (Relatable)object1;
Relatable obj2 = (Relatable)object2;
if ((obj1).isLargerThan(obj2) < 0)
return object1;
else
return object2;
}
}
Relatable 인터페이스를 구현한 CirclePlus 클래스.
package com.intheeast.interfaces;
import java.awt.Point;
public class CirclePlus implements Relatable{
public Point origin;
private double radius;
public CirclePlus(Point origin, double radius) {
this.origin = origin;
this.radius = radius;
}
// Relatable의 abstract method를 구현함
public double getArea() {
double area = Math.PI * this.radius * this.radius;
return area;
}
public int isLargerThan(Relatable other) {
if (this.getArea() < other.getArea())
return -1;
else if (this.getArea() > other.getArea())
return 1;
else
return 0;
}
}
RectanglePlus 클래스가 구현한 isLargerThan 메서드를 다음과 같이 수정합니다.
package com.intheeast.interfaces;
import java.awt.Point;
public class RectanglePlus implements Relatable {
// 생략
// ...
// a method required to implement
// the Relatable interface
public int isLargerThan(Relatable other) {
if (this.getArea() < other.getArea())
return -1;
else if (this.getArea() > other.getArea())
return 1;
else
return 0;
}
}
Relatable 인터페이스를 구현한 두 클래스 객체가 크기를 서로 비교합니다.
public static void compareArea() {
Point pi = new Point(40, 40);
Point pi2 = new Point(40, 40);
Relatable rectangleRel = new RectanglePlus(
pi,
40, 40);
Relatable circleRel = new CirclePlus(pi, 20);
Relatable.findSmallest(rectangleRel, circleRel);
RectanglePlus rectanglePlus = new RectanglePlus(
pi,
40, 40);
CirclePlus circlePlus = new CirclePlus(pi, 20);
Relatable.findSmallest(rectanglePlus, circlePlus);
/////////////
Relatable.findSmallest((Object)rectanglePlus, (Object)circlePlus);
}
Evolving Interfaces
여러분이 개발한 DoIt이라는 인터페이스를 생각해 보세요.
public interface DoIt {
void doSomething(int i, double x);
int doSomethingElse(String s);
}
나중에 인터페이스가 다음과 같이 DoIt에 세 번째 메서드를 추가한다고 가정해 보겠습니다.
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 메서드로 정의할 수 있습니다. 다음 예제에서는 didItWork라는 default 메서드를 정의합니다.
public interface DoIt {
void doSomething(int i, double x);
int doSomethingElse(String s);
default boolean didItWork(int i, double x, String s) {
// Method body
}
}
기존 인터페이스에 새로운 static 메서드를 정의할 수도 있습니다. 새로운 default 메서드나 static 메서드로 향상된 인터페이스를 구현하는 클래스가 있는 사용자는 추가 메서드를 수용하기 위해 클래스를 수정하거나 다시 컴파일할 필요가 없습니다.
Default Methods
인터페이스 섹션에서는 자동차를 작동하기 위해 호출될 수 있는 메서드를 설명하는 업계 표준 인터페이스를 게시하는 컴퓨터 제어 자동차 제조업체와 관련된 예를 설명합니다. 컴퓨터로 제어되는 자동차를 생산하는 제조업체가 자동차에 비행과 같은 새로운 기능을 추가하면 어떻게 될까요? 이러한 제조업체는 다른 회사(예: 전자 유도 장치 제조업체)가 자사의 소프트웨어를 비행 자동차에 적용할 수 있도록 새로운 방법을 지정해야 합니다. 이러한 자동차 제조업체는 이러한 새로운 비행 관련 방법을 어디에 선언합니까? 원래 인터페이스에 추가하면 해당 인터페이스를 구현한 프로그래머는 구현을 다시 작성해야 합니다. 정적 메서드로 추가하면 프로그래머는 이를 필수 핵심 메서드가 아닌 유틸리티 메서드로 간주하게 됩니다.
디폴트 메서드를 사용하면 라이브러리 인터페이스에 새 기능을 추가하고 해당 인터페이스의 이전 버전용으로 작성된 코드와의 바이너리 호환성을 보장할 수 있습니다.
질문에 대한 답변 및 연습: 인터페이스에 설명된 대로 TimeClient 인터페이스를 고려하십시오.
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();
}
다음 클래스 SimpleTimeClient는 TimeClient를 구현합니다.
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());
}
}
ZonedDateTime 객체(표준 시간대 정보를 저장한다는 점을 제외하면 LocalDateTime 객체와 유사)를 통해 시간대를 지정하는 기능과 같은 새로운 기능을 TimeClient 인터페이스에 추가한다고 가정해 보겠습니다.
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);
}
TimeClient 인터페이스를 수정한 후에는 SimpleTimeClient 클래스를 수정하고 getZonedDateTime 메소드를 구현해야 합니다. 그러나 이전 예제에서와 같이 getZonedDateTime을 abstract로 두는 대신 디폴트 구현을 정의할 수 있습니다. (abstract 메서드는 구현 없이 선언된 메서드라는 점을 기억하세요.)
package defaultmethods;
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 인터페이스를 구현하는 모든 클래스)에는 getZonedDateTime 메소드가 이미 정의되어 있습니다. 다음 예제 TestSimpleTimeClient는 SimpleTimeClient의 인스턴스에서 getZonedDateTime 메서드를 호출합니다.
package defaultmethods;
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합니다.
이와 같은 방식으로 확장된 인터페이스에서 디폴트 메서드를 처리할 수 있습니다.
public interface AnotherTimeClient extends TimeClient { }
AnotherTimeClient 인터페이스를 구현하는 모든 클래스에는 디폴트 메소드 TimeClient.getZonedDateTime에 의해 지정된 구현이 있습니다.
다음과 같이 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 Methods
default 메서드 외에도 인터페이스에서 static 메서드를 정의할 수 있습니다. (클래스 정적 메서드는 객체가 아니라 정적 메서드를 정의한 클래스와 연결된 메서드입니다. 클래스의 모든 인스턴스는 정적 메서드를 공유합니다.) 이렇게 하면 라이브러리에서 static 메서드를 활용하여 도우미[helper] 용도의 메서드를 더 쉽게 구성할 수 있습니다. 별도의 클래스가 아닌 동일한 인터페이스에 특정한 정적 메서드를 유지할 수 있습니다. 다음 예제에서는 시간대 식별자에 해당하는 ZoneId 개체를 검색하는 정적 메서드를 정의합니다. 지정된 식별자에 해당하는 ZoneId 개체가 없으면 시스템 기본 시간대를 사용합니다. (결과적으로 getZonedDateTime 메소드를 단순화할 수 있습니다):
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 한정자를 생략할 수 있습니다.
Integrating Default Methods into Existing Libraries
디폴트 메서드를 사용하면 기존 인터페이스에 새 기능을 추가하고 해당 인터페이스의 이전 버전용으로 작성된 코드와의 바이너리 호환성을 보장할 수 있습니다. 특히 디폴트 메서드를 사용하면 람다 expression을 파라미터로 허용하는 메서드를 기존 인터페이스에 추가할 수 있습니다.
예시
@FunctionalInterface
interface Processor {
void process(String input);
// 디폴트 메서드 추가
default void processWithLambda(String input, Processor processor) {
System.out.println("Before processing: " + input);
processor.process(input);
System.out.println("After processing: " + input);
}
}
위 인터페이스를 구현하고 사용하는 샘플 코드입니다.
이 섹션에서는 디폴트 및 정적 메서드를 사용하여 Comparator 인터페이스가 어떻게 향상되었는지 보여줍니다.
질문 및 연습: 클래스에 설명된 대로 카드 및 데크 클래스를 고려하세요. 이 예제에서는 Card 및 Deck 클래스를 인터페이스로 다시 작성합니다. Card 인터페이스에는 두 개의 열거형(Suit 및 Rank)과 두 개의 추상 메소드(getSuit 및 getRank)가 포함되어 있습니다.
package defaultmethods;
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();
}
Deck 인터페이스에는 덱의 카드를 조작하는 다양한 메서드가 포함되어 있습니다.
package defaultmethods;
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);
}
// ...
}
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() 메서드가 먼저 카드 덱을 모양별로 정렬한 다음 순위별로 정렬합니다.
덱을 먼저 순위별로 정렬한 다음 슈트별로 정렬하려면 어떻게 해야 합니까? 새로운 정렬 기준을 지정하려면 Comparator 인터페이스를 구현하고 sort(List<T> list, Comparator<? super T> c) 메서드(Comparator 파라미터를 포함하는 정렬 메서드 버전)를 사용해야 합니다. StandardDeck 클래스에서 다음 메서드를 정의할 수 있습니다.
public void sort(Comparator<Card> c) {
Collections.sort(entireDeck, c);
}
이 방법을 사용하면 Collections.sort 메서드가 Card 클래스의 인스턴스를 정렬하는 방법을 지정할 수 있습니다. 이를 수행하는 한 가지 방법은 Comparator 인터페이스를 구현하여 카드 정렬 방법을 지정하는 것입니다. SortByRankThenSuit 예제에서는 다음을 수행합니다.
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();
}
}
다음 호출은 카드 놀이 덱을 먼저 순위별로 정렬한 다음 모양별로 정렬합니다.
StandardDeck myDeck = new StandardDeck();
myDeck.shuffle();
myDeck.sort(new SortByRankThenSuit());
그러나 이 접근 방식은 너무 장황합니다. 정렬 기준만 지정하고 여러 정렬 구현을 만드는 것을 피할 수 있다면 더 좋을 것입니다. 당신이 Comparator 인터페이스를 작성한 개발자라고 가정해보자. 다른 개발자가 정렬 기준을 더 쉽게 지정할 수 있도록 Comparator 인터페이스에 어떤 디폴트 또는 정적 메서드를 추가할 수 있습니까?
시작하려면 모양에 관계없이 순위별로 카드 덱을 정렬한다고 가정해 보겠습니다. 다음과 같이 StandardDeck.sort 메서드를 호출할 수 있습니다.
StandardDeck myDeck = new StandardDeck();
myDeck.shuffle();
myDeck.sort(
(firstCard, secondCard) ->
firstCard.getRank().value() - secondCard.getRank().value()
);
Comparator 인터페이스는 함수형 인터페이스이므로 람다 expression을 sort 메서드의 아규먼트로 사용할 수 있습니다. 이 예에서 람다 expression은 두 정수 값을 비교합니다.
개발자가 Card.getRank 메소드만 호출하여 Comparator 인스턴스를 생성할 수 있다면 더 간단할 것입니다. 특히 개발자가 getValue 또는 hashCode와 같은 메서드에서 숫자 값을 반환할 수 있는 객체를 비교하는 Comparator 인스턴스를 만들 수 있다면 도움이 될 것입니다. Comparator 인터페이스는 다음을 비교하는 정적 메서드를 통해 이 기능을 통해 향상되었습니다.
myDeck.sort(Comparator.comparing((card) -> card.getRank()));
이 예에서는 대신 method reference를 사용할 수 있습니다.
myDeck.sort(Comparator.comparing(Card::getRank));
이 호출은 다양한 정렬 기준을 지정하고 여러 정렬 구현을 생성하지 않는 방법을 더 잘 보여줍니다.
Comparator 인터페이스는 다른 데이터 유형을 비교하는 Comparator 인스턴스를 생성할 수 있도록 해주는 CompareDouble 및 ComparingLong과 같은 정적 메서드 비교의 다른 버전으로 향상되었습니다.
개발자가 둘 이상의 기준으로 객체를 비교할 수 있는 Comparator 인스턴스를 생성하려고 한다고 가정해 보겠습니다. 예를 들어, 먼저 카드 덱을 순위별로 정렬한 다음 모양별로 정렬하려면 어떻게 해야 할까요? 이전과 마찬가지로 람다 expression을 사용하여 다음 정렬 기준을 지정할 수 있습니다.
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();
}
);
개발자가 일련의 Comparator 인스턴스에서 Comparator 인스턴스를 구축할 수 있다면 더 간단해질 것입니다. 디폴트 메소드 thenComparing을 사용하여 이 기능을 통해 Comparator 인터페이스가 향상되었습니다.
myDeck.sort(
Comparator
.comparing(Card::getRank)
.thenComparing(Comparator.comparing(Card::getSuit)));
Comparator 인터페이스는 다른 데이터 타입을 비교하는 Comparator 인스턴스를 구축할 수 있는 디폴트 메서드 thenComparing(예: thenComparingDouble 및 thenComparingLong)의 다른 버전으로 향상되었습니다.
개발자가 객체 컬렉션을 역순으로 정렬할 수 있는 Comparator 인스턴스를 생성하려고 한다고 가정해 보겠습니다. 예를 들어, 에이스에서 2(2에서 에이스가 아닌)로 순위를 내림차순으로 먼저 카드 덱을 정렬하려면 어떻게 해야 할까요? 이전과 마찬가지로 다른 람다 expression을 지정할 수 있습니다. 그러나 개발자가 메서드를 호출하여 기존 비교기를 되돌릴 수 있다면 더 간단할 것입니다. 비교기 인터페이스는 기본 방법을 거꾸로 한 이 기능으로 향상되었습니다.
myDeck.sort(
Comparator.comparing(Card::getRank)
.reversed()
.thenComparing(Comparator.comparing(Card::getSuit)));
이 예에서는 디폴트 메서드, 정적 메서드, 람다 expression 및 method reference를 사용하여 Comparator 인터페이스를 강화하여 프로그래머가 호출 방법을 보고 기능을 빠르게 추론할 수 있는 보다 표현력이 뛰어난 라이브러리 메서드를 만드는 방법을 보여줍니다. 라이브러리의 인터페이스를 향상하려면 이러한 구성을 사용하세요.
'High Level Programming Language > Learning the Java Language' 카테고리의 다른 글
Lesson: Interfaces and Inheritance[Inheritance 2] (0) | 2024.06.04 |
---|---|
Lesson: Interfaces and Inheritance[Inheritance 1] (0) | 2024.06.03 |
Using Java Reflection (0) | 2024.05.27 |
Lesson: Classes and Objects 2 (0) | 2024.05.27 |
Lesson: Classes and Objects 1 (0) | 2024.05.27 |