1. 익명 구현 객체
2. 인터페이스의 다형성
3. default 메서드와 static 메서드
4. 어노테이션
1. 익명 구현 객체
보통은 구현 클래스를 따로 만들어서 사용하는 경우가 많지만, 한번만 사용하는 경우에는 굳이 파일을 새로 생성하는 것이 더 불편할 수 있습니다. 그래서 실행 클래스에서 이름이 없는 익명 구현 객체로 객체를 생성할 수 있는 방법이 있습니다. 보통 안드로이드 UI 개발을 하거나 이벤트 처리 시, 또는 스레드를 사용하는 프로그래밍을 할 때 자주 사용합니다. 익명 구현 객체로 객체를 생성할 때는, 이름이 없기 때문에 인터페이스명으로 객체를 생성해야 합니다.
익명 구현 객체를 생성하는 방법
인터페이스 객체명 = new 인터페이스() {
// 인터페이스의 모든 추상 메서드 구현
};
익명 구현 객체를 생성하는 코드에서는 맨 마지막 끝에는 항상 세미클론을 붙여줘야 합니다. 객체를 생성하는 하나의 실행문이기 때문이죠. 실수하는 경우가 많으니 주의하세요. 물론 이클립스 같은 툴에서 바로바로 표시해주니 걱정하실 필요는 없습니다.
이번엔 익명 구현 객체로 앞에서 만들었던 Fax 인터페이스를 구현해보도록 하겠습니다.
package example;
public class ComplexerMain2 {
public static void main(String[] args) {
Fax fax = new Fax() {
@Override
public void send(String tel) {
System.out.println("여기는 익명 구현 객체의 send()");
}
@Override
public void receive(String tel) {
System.out.println("여기는 익명 구현 객체의 receive()");
}
};
fax.send("1234");
fax.receive("5678");
}
}
실행 결과
여기는 익명 구현 객체의 send()
여기는 익명 구현 객체의 receive()
main() 메서드에서 Fax 타입으로 익명 구현 객체를 생성했습니다. new 연산자 뒤에는 인터페이스명인 Fax()가 있고, 중괄호 블록 안에서 실제 추상 메서드를 구현하고 있습니다. 이 구현이 끝나는 부분에는 세미콜론을 찍어야 한다는 것도 잊지 않도록 하세요.
이 예제는 앞 예제처럼 Printer, Scanner, Fax 세 개의 인터페이스를 다중 구현한 Complexer 구현 클래스 처럼 여러개를 구현한 예제가 아니라 Fax 인터페이스 하나만 구현한 구현 객체입니다. 익명 구현 객체는 여러 개의 인터페이스를 한번에 다중 구현을 하지 못합니다. 그럼 Complexer처럼 여러 개의 인터페이스를 익명 구현 객체로 구현하려면 어떻게 해야 할까요?
답은 3개의 인터페이스를 상속받는 인터페이스를 만드는 것입니다. ComplexerInterface 라는 인터페이스를 만들어서 Printer, Scanner, Fax를 상속 받게 되면 이제 ComplexerInterface는 세 개의 모든 인터페이스의 상수와 추상 메서드들을 모두 가지고 있는 새로운 인터페이스가 됩니다. 이제 이 인터페이스를 익명 구현 객체로 생성하면 되겠죠?
package example;
public interface ComplexcerInterface extends Printer, Scanner, Fax {
}
package example;
public class ComplexerMain3 {
public static void main(String[] args) {
ComplexcerInterface ci = new ComplexcerInterface() {
@Override
public void send(String tel) {
System.out.println("여기는 익명 구현 객체의 send()");
}
@Override
public void receive(String tel) {
System.out.println("여기는 익명 구현 객체의 receive()");
}
@Override
public void print() {
System.out.println("여기는 익명 구현 객체의 print()");
}
@Override
public void scan() {
System.out.println("여기는 익명 구현 객체의 scan()");
}
};
ci.send("1234");
ci.receive("5678");
ci.print();
ci.scan();
}
}
실행 결과
여기는 익명 구현 객체의 send()
여기는 익명 구현 객체의 receive()
여기는 익명 구현 객체의 print()
여기는 익명 구현 객체의 scan()
ComplexcerInterface 에서 세 개의 인터페이스를 implements 하고 ComplexerMain3 에서는 이 ComplexcerInterface를 익명 구현 객체로 구현하면 3개의 모든 인터페이스의 상수와 추상 메서드들을 구현하여 사용할 수 있게 됩니다.
2. 인터페이스의 다형성
앞 상속관계에서의 자료형 변환과 다형성에 대해 알아보았고, 만들어도 봤는데, 인터페이스에서도 상속과 마찬가지로 형변환과 다형성에 대한 개념이 그대로 사용됩니다. 최근엔 상속보다 인터페이스에서 다형성 개념을 이용해 구현하는 경우가 더 많은데요, 객체 지향 프로그래밍에서 다형성을 구현하는 기술로 상속과 인터페이스 모두 가능합니다.
상속에서와 마찬가지로 상위 클래스를 타입으로 지정해서 여러 하위 클래스를 객체로 생성해 같은 메서드를 실행해도 결과가 다르게 나오도록 구현하는 개념은 동일합니다.
인터페이스를 타입으로 지정한 객체를 구현 클래스로 객체를 생성하면 아주 쉽고, 편하게 구현 객체를 교체할 수 있게 됩니다. 이번에는 GraphicCard라는 인터페이스를 만들어, Amd와 Nvidia 라는 구현 클래스를 생성해보도록 하겠습니다.
GraphicCard.java (인터페이스)
package example;
public interface GraphicCard {
String MEMORY = "2G";
public void process();
}
Amd.java (구현 클래스)
package example;
public class Amd implements GraphicCard {
public void process() {
System.out.println("AMD 그래픽 처리");
}
}
Nvidia.java (구현 클래스)
package example;
public class Nvidia implements GraphicCard {
public void process() {
System.out.println("Nvidia 그래픽 처리");
}
}
Computer.java (실행 클래스)
package example;
public class Computer {
public static void main(String[] args) {
GraphicCard gc = new Amd();
System.out.println("메모리 : "+gc.MEMORY);
// Amd로 생성
gc = new Amd(); // 자동 형변환
gc.process();
// Nvidia로 교체
gc = new Nvidia(); // 자동 형변환
gc.process();
}
}
실행 결과
메모리 : 2G
AMD 그래픽 처리
Nvidia 그래픽 처리
GraphicCard 인터페이스에는 MEMORY라는 상수와 process() 라는 추상 메서드가 정의되어 있습니다. Amd와 Nvidia라는 구현 클래스가 GraphicCard 인터페이스를 구현하고 있는데, 인터페이스의 추상 메서드를 반드시 구현해야 하기 때문에 process() 메서드를 각각 구현하였고, Computer 클래스에서는 인터페이스인 GraphicCard 타입인 gc 변수에 Amd 객체를 생성했다. gc.MEMORY 로 상수를 출력하고, 아래 구현 클래스의 객체를 부품 교체하듯 다양한 자료형으로 바꿀 수 있다는 것을 보여주는 소스입니다.
상속에서의 예제와 다른점은 GraphicCard가 클래스가 아니라 인터페이스라는 점입ㄴ이다. 이 인터페이스에서 그래픽카드의 특징만 추출해서 추상적으로만 구현해 놓은 것 이라고 할 수 있습니다. 실제 구현은 Amd 클래스와 Nvidia 클래스에서 구현하도록 말이죠.
상속보다 인터페이스를 이용해서 다형성을 적용하는 경우가 많은 이유는 선임 개발자나 클래스 설계를 담당하는 개발자가 클래스를 먼저 인터페이스로 구현해 놓으면, 이 인터페이스의 메서드를 반드시 구현하도록 강제할 수 있기 때문에, 클래스 설계용도로도 많이 사용됩니다.
3. default 메서드와 static 메서드
자바 7버전까지는 상수와 추상 메서드로만 인터페이스를 정의할 수 있었는데, 자바 8버전 이후 부터는 default 메서드와 static 메서드도 같이 정의할 수 있도록 개선되었습니다.
MyInterface.java (인터페이스)
package example;
public interface MyInterface {
default void defaultMethod() {
System.out.println("MyInterface의 default 메서드");
}
static void staticMethod() {
System.out.println("MyInterface의 static 메서드");
}
}
DefaultStaticEx.java (실행 클래스)
package example;
public class DefaultStaticEx {
public static void main(String[] args) {
MyInterface obj = new MyInterface() {
};
obj.defaultMethod();
MyInterface.staticMethod();
}
}
실행 결과
MyInterface 의 default 메서드
MyInterface의 static 메서드
MyInterface라는 인터페이스에는 default 메서드와 static 메서드가 하나씩 있습니다. 실행 클래스인 DefaultStaticEx 클래스에서 익명구현객체로 재정의 없이 객체를 생성하고, defaultMethod()를 실행했습니다.
그 아래는 객체 없이 MyInterface라는 인터페이스를 통해 직접 static 메서드를 호출할 수 있는것을 알 수 있습니다.
4. 어노테이션
어노테이션(Annotation)은 프로그램에게 추가적인 정보를 제공해 주는 메타데이터(metadata) 라고 할 수 있습니다. 컴파일러에게 코드를 작성할 때 문법 에러를 체크하거나 정보를 제공하거나 빌드, 배치 시 코드를 자동으로 생성해주는 정보를 제공합니다. 아래 표는 자바에서 제공하는 표준 어노테이션입니다.
어노테이션 명 | 특징 |
@Override | 오버라이딩 검사, 오버라이딩 되지 않으면 에러 |
@Deprecated | Deprecated 된 메서드 사용하지 않도록 검사, 사용하면 에러 |
@SupressWarnings | 경고 메시지 표시 안되도록 설정 |
@SafeVarargs | 제네릭타입 가변인자 사용 시 경고 무시(jdk 7 이상) |
@FunctionalInterface | 함수형 인터페이스 (추상메서드가 한개만 존재하는 인터페이스, jdk 7 이상) |
@Native | Native 메서드에서 참조되는 상수 |
앞에서 배웠던 메서드 재정의(오버라이딩)에서 이클립스를 통해 자동 생성시키면 @Override 어노테이션이 자동으로 생성된 것을 보았는데, 이 어노테이션이 붙어 있으면 정확히 오버라이딩하지 않으면 컴파일러가 에러를 발생하게 됩니다. 사용하지 않아도 코딩 시 제약 사항은 없지만, 좀 더 편하게 코딩할 수 있습니다. 나중에 서블릿이나 스프링 프레임워크를 배우게 되면 어노테이션을 사용해서 개발하는 경우가 많으니, 개념적으로 익혀두세요.
메타 어노테이션이란 사용자 어노테이션을 만들 수 있는 어노테이션을 말합니다. 메타 어노테이션의 종류는 아래와 같습니다.
메타 어노테이션 | 특징 |
@Retention | 어노테이션 범위 설정 |
@Documetned | 문서 어노테이션의 정보가 표현되도록 설정 |
@Target | 어노테이션 적용 위치 설정 |
@Inherited | 어노테이션 상속 |
@Repeatable | 반복적으로 선언 가능 |
어노테이션을 적용할 수 있는 대상은 java.lang.annotation.ElementType 열거 상수로 정의되어 있습니다.
ElementType 열거 상수 | 적용 대상 |
TYPE | 클래스, 인터페이스 등 타입 선언 시 |
ANNOTATION_TYPE | 어노테이션 타입 선언 시 |
FIELD | 멤버변수 선언 시 |
CONSTRUCTOR | 생성자 선언 시 |
METHOD | 메서드 선언 시 |
LOCAL_VARIABLE | 지역변수 선언 시 |
PACKAGE | 패키지 선언 시 |
PARAMETER | 매개변수 선언 시 |
어노테이션을 정의하는 방법은 인터페이스와 비슷한데요, 아래와 같이 @interface를 사용해서 정의합니다.
public @interface 어노테이션명 {
자료형 요소명() [default 기본값];
}
어노테이션을 구성하는 요소는 변수처럼 자료형과 변수명으로 정의합니다. 이렇게 정의한 어노테이션은 @어노테이션명 형태로 사용합니다. 그리고 사용할 때 기본 요소인 value를 사용하려면 String value(); 기본 요소를 정의해줘야 합니다.
그럼 간단한 어노테이션을 직접 만들어 보겠습니다. 어노테이션 역시 이클립스에 New > Annotation 으로도 생성 가능합니다.
UserAnnot.java (어노테이션 정의)
package example;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface UserAnnot {
String value();
int number() default 5;
}
UserClass.java (어노테이션 사용)
package example;
public class UserClass {
@UserAnnot(value="A")
public void methodA() {
System.out.println("methodA() 실행");
}
@UserAnnot(value="B", number=10)
public void methodB() {
System.out.println("methodB() 실행");
}
}
AnnotationEx.java (실행 클래스)
package example;
import java.lang.reflect.Method;
public class AnnotationEx {
public static void main(String[] args) throws Exception {
Method method[] = UserClass.class.getDeclaredMethods();
for (int i=0; i<method.length; i++) {
String methodName = method[i].getName();
UserAnnot annot = method[i].getAnnotation(UserAnnot.class);
System.out.print(methodName + "의 어노테이션 ");
System.out.print("value : "+annot.value() + " ");
System.out.print("number : "+annot.number() + " ");
System.out.println();
method[i].invoke(new UserClass(), null); // 메서드 실행
}
}
}
실행 결과
methodB의 어노테이션 value : B number : 10
methodB() 실행
methodA의 어노테이션 value : A number : 5
methodA() 실행
UserAnno.java 에서 어노테이션을 생성합니다. @Retention은 RetentionPolicy.RUNTIME으로 실행하는 동안 어노테이션 정보를 유지하도록 했고, value와 number라는 요소를 정의했습니다.
그리고 UserClass.java 에서는 두 개의 메서드에 앞에서 생성한 어노테이션을 적용했습니다. 괄호 안에 있는 속성값들이 어노테이션의 속성으로 전달됩니다.
자, 이제 AnnotationEx를 자세히 들여다 보겠습니다. 먼저 아래 코드는 UserClass 라는 클래스의 정의된 메서드들을 메서드 타입으로 가져온 것입니다.
Method method[] = UserClass.class.getDeclaredMethods();
그리고 for문을 통해 이 메서드의 갯수만큼 반복합니다.
반복문 안을 확인해보면, 변수 i를 배열의 인덱스로 활용해 반복하고 있고, 메서드 객체의 이름을 가져와서 해당 메서드의 어노테이션 정보를 가져오는 부분으로 각 메서드의 이름과 해당 어노테이션의 value, number 요소의 값을 출력하고 있습니다.
마지막으로 invoke 메서드는 method 변수의 메서드 객체를 실행하는 메서드입니다. 따라서 methodB() 실행, methodA() 실행 이라는 문자열도 출력이 된 것을 알 수 있습니다.
코드를 글과 말로 이해하려니 쉽지 않죠?
새로운 클래스가 나오고 코드가 지금까지의 예제보다 조금 복잡해서 다소 어렵게 느껴질 수 있습니다. 어노테이션은 여러분이 직접 생성하는 경우보다 외부 라이브러리에서 정해 놓은 어노테이션을 사용하는 경우가 많으니, 조금 어렵게 느껴지더라도, 지금은 어노테이션을 컴파일러나 다른 프로그램에게 지시하기 위해 표시하는 용도라고 기억해 두세요.
당장은 복잡해 보이겠지만, 나중에 다시 보면 훨씬 쉽게 느껴질 것입니다.