210113 이것이 자바다 8장

송은석·2021년 1월 18일
0
post-thumbnail

신용권 님의 ''이것이 자바다'' 8장 공부 기록

책을 보면서 내용을 정리했다.

소스코드 repo

8. 인터페이스

8.1 인터페이스의 역할

인터페이스(interface)는 객체의 사용방법을 정의한 타입이다. 인터페이스는 객체의 교환성을 높여주기 때문에 다형성을 구현하는 매우 중요한 역할을 한다. 또한 개발 코드와 객체가 서로 통신하는 접점 역할을 한다. 개발 코드가 인터페이스의 메소드를 호출하면 인터페이스는 객체의 메소드를 호출한다. 그래서 개발 코드는 객체의 내부 구조를 알 필요 없이 인터페이스의 메소드만 알면 된다.

  • 개발 코드로 메소드를 호출하지 않고 굳이 인터페이스를 쓰는 이유는 개발 코드를 수정하지 않고, 사용하는 객체를 변경할 수 있도록 하기 위해서이다.

인터페이서는 여러 객체들과 사용이 가능하므로 어떤 객체를 사용하느냐에 따라 실행 내용과 리턴값이 다를 수 있다. 따라서 개발 코드 측면에선 코드 변경 없이 실행 내용과 리턴값을 다양화할 수 있다는 장점을 가지게 된다.


8.2 인터페이스 선언

인터페이스는 ".java" 형태의 소스 파일로 작성되고 컴파일러를 통해 ".class" 형식으로 컴파일 되기 때문에 물리적 형태는 클래스와 동일하다. 차이점은 소스를 작성할 때 선언하는 방법이 다르다.

  • class 키워드 대신 interface 키워드를 사용한다. 이름 작성법은 클래스와 동일하다.
[public] interface 인터페이스명{...}
  • 클래스는 필드, 생성자, 메소드를 구성멤버로 가지나, 인터페이스는 상수와 메소드만을 구성멤버로 가진다. 인터페이스는 객체 생성이 불가하므로 생성자를 가질 수 없기 때문이다.
interface 인터페이스명{
    //상수
    타입 상수명 =;
    //추상 메소드
    타입 메소드명 (매개변수, ..);
    //디폴트 메소드
    default 타입 메소드명(매개변수, ..);
    //정적 메소드
    static 타입 메소드명(매개변수){..};
}

8.2.1 상수 필드 선언

인터페이스는 데이터를 저장할 수 없으므로 데이터를 저장할 인스턴스 또는 정적 필드를 선언할 수 없다. 대신 상수 필드만 선언할 수 있다.

인터페이스에서 선언된 필드는 모두 public static final의 특성을 갖는다. 이를 생략해도 컴파일 과정에서 자동적으로 붙게 된다. 상수명은 모두 대문자나 언더바로 작성한다.

  • 인터페이스 상수는 static { }블록으로 초기화가 불가하므로 선언과 동시에 초기값을 지정해야 한다.
public interface sample{
    public int MAX = 10;
}

8.2.2 추상 메소드 선언

  • 인터페이스를 통해 호출된 메소드는 최종적으로 객체에서 실행되므로, 실행 블록이 필요 없는 추상 메소드로 선언한다. 추상 메소드는 리턴 타입, 메소드명, 매개 변수만 기술되고 중괄호{}를 붙이지 않는 메소드를 말한다. 이는 모두 public abstract의 특성을 가지므로 컴파일시 자동적으로 붙게 된다.

8.2.3 디폴트 메소드 선언

  • 자바 8에서 추가된 인터페이스의 새로운 멤버이다. default 키워드가 리턴 타입 앞에 붙는다. 디폴트 메소드는 public 특성을 가지므로 public을 생략하더라도 자동적으로 붙게 된다.
  • 디폴트 메소드 선언시 실행내용까지 모두 작성해야 한다.
[public] default 리턴타입 메소드명(매개변수, ...){...}

8.2.4 정적 메소드 선언

  • 디폴트 메소드와 마찬가지로 자바8에서 추가된 인터페이스의 멤버이다. 형태는 정적 메소드와 동일하며, public 특성을 가진다.
[public] static 리턴타입 메소드명(매개변수, ..){..}

8.3 인터페이스 구현

개발 코드가 인터페이스 메소드를 호출하면 인터페이스는 객체의 메소드를 호출한다. 객체는 인터페이스에서 정의된 추상 메소드와 동일한 메소드 이름, 매개타입, 리턴타입을 가진 실체 메소드를 가지고 있어야 한다. 이러한 객체를 인터페이스의 구현(implement) 객체라고 하고, 구현 객체를 생성하는 클래스를 구현 클래스라고 한다.

8.3.1 구현 클래스

  • 구현 클래스는 인터페이스 타입으로 사용할 수 있음을 알려주기 위해 클래스 선언부에 implements 키워드를 추가하고 인터페이스명을 명시한다.
public class 구현클래스명 implements 인터페이스명 {
    //인터페이스에 선언된 추상 메소드의 실체 메소드 선언
}
  • 그리고 인터페이스에 선언된 추상 메소드의 실체 메소드를 선언해야 한다. 이 때 주의할 점은 인터페이스의 모든 메소드는 기본적으로 public 접근 제한을 가지므로 이보다 더 낮은 접근 제한으로 작성할 수 없다. public 생략 시 "Cannot reduce the visibility of the inherited method(상속된 메소드의 가시성을 줄일 수 없다)" 컴파일 에러가 발생한다.

  • 추상 메소드에 대응하는 실체 메소드를 작성하지 않으면 해당 클래스는 자동으로 추상 클래스가 되며, 이 때는 클래스 선언부에 abstrac 키워드를 추가해야 한다.

public abstract calss Tv implements Rc{
    public void turnOn(){...}
    //public void turnOff(){..} -> 실체 메소드 작성하지 않음.
}
  • 구현 클래스의 실체 메소드에 @Override 어노테이션을 붙이면 컴파일러가 실체 메소드인지를 체크한다.

  • 구현 클래스가 작성되면 new 연산자로 객체를 생성하여 인터페이스에 대입할 수 있다. 인터페이스 변수는 참조 타입이므로 구현객체의 번지를 저장한다.

인터페이스 변수;
변수 = new 구현객체; //인터페이스 변수 = new 구현객체;

8.3.2 익명 구현 객체

구현 클래스를 만들어 사용하는 것은 일반적이고 클래스를 재사용할 수 있기 때문이 편리하지만 일회성의 구현 객체를 만들기 위해 소스파일을 만들고 클래스를 선언하는 것은 비효율적이다. 이에 소스파일을 만들지 않고도 구현객체를 만들 수 있는 방법을 제공하는데, 이것이 익명구현 객체이다.

  • ui프로그래밍 이벤트 처리, 임시 작업 스레드를 만들 때 많이 활용된다.
인터페이스 변수 = new 인터페이스(){
    //인터페이스에 선언된 추상 메소드의 실체 메소드 선언
}; //반점 있음!
  • 중괄호에는 인터페이스에 선언된 모든 추상 메소드들의 실체 메소드를 작성해야 한다. 그렇지 않으면 컴파일 에러가 발생한다.

  • 추가적으로 필드와 메소드 선언이 가능하나, 익명 객체 안에서만 사용할 수 있고 인터페이스 변수로 접근할 수 없다.

  • 모든 객체는 클래스로부터 생성된다. 익명 구현 객체의 소스코드를 컴파일하면 컴파일러에 의해 "클래스명$1.class"의 이름으로 파일이 생성된다.

8.3.3 다중 인터페이스 구현 클래스

  • 객체는 다수의 인터페이스 타입으로 사용할 수 있다.
public class 구현클래스명 implements 인터페이스A, 인터페이스B {
    //인터페이스A에 선언된 추상 메소드의 실체 메소드 선언
    //인터페이스B에 선언된 추상 메소드의 실체 메소드 선어
}
  • 다중 인터페이스를 구현할 경우, 구현 클래스는 모든 인터페이스의 추상 메소드에 대해 실체 메소드를 작성해야한다. 하나라도 없으면 추상 클래스로 선언해야 한다.

8.4 인터페이스 사용

인터페이스로 구현 객체를 사용하려면 인터페이스 변수를 선언하고 구현 객체를 대입해야 한다. 인터페이스 변수는 참조 타입이므로 구현 객체가 대입될 경우 구현 객체의 번지를 저장한다.

  • 개발 코드에서 인터페이스는 클래스의 필드, 생성자 또는 메소드의 매개변수, 생성자 또는 메소드의 로컬 변수로 선언될 수 있다.

8.4.1 추상 메소드 사용

  • 구현 객체가 인터페이스 타입에 대입되면 인터페이스에 선언된 추상 메소드를 개발 코드에서 변수를 통해 호출할 수 있게 된다.
RC rc = new Tv();
rc.turnOn();//Tv의 실체 메소드 실행

8.4.2 디폴트 메소드 사용

  • 디폴트 메소드는 인터페이스에 선언되나, 인터페이스에서 바로 사용할 수 없다. 디폴트 메소드는 추상 메소드가 아닌 인스턴스 메소드이므로 구현 객체가 있어야 사용이 가능하다.
RC rc = new tv();
rc.turnOff();
  • 디폴트 메소드는 인터페이스의 모든 구현 객체가 가지고 있는 기본 메소드라고 생각할 수 있다.
  • 어떤 객체의 디폴트 메소드 내용의 수정이 필요할 때는, 이를 재정의(오버라이딩)하면 된다. 그러면 디폴트 메소드를 호출할 때 재정의한 메소드가 호출된다.

8.4.3 정적 메소드 사용

  • 인터페이스의 정적 메소드는 인터페이스로 바로 호출이 가능하다.
public class ex {
    public static void main(String[]args){
        RC.go();//인터페이스로 바로 호출
    }
}

8.5 타입 변환과 다형성

요즘은 상속보다도 인터페이스를 통해 다형성을 구현한다. 상속에서 부모 타입에 어떤 자식 객체를 대입하느냐에 따라 실행 결과가 달라지는 것처럼, 인터페이스 타입에 어떤 구현 객체를 대입하느냐에 따라 실행 결과가 달라진다.

상속은 같은 종류의 하위 클래스를 만드는 기술이고, 인터페이스는 사용 방법이 동일한 클래스를 만드는 기술이다.

프로그램 소스 코드는 변함이 없는데, 구현 객체를 교체함으로써 프로그램의 실행 결과가 다양해지는 것을 인터페이스의 다형성이라고 부른다.

  • 클래스에 문제가 있어 다른 클래스를 만들 때는 같은 메소드를 사용한다면 메소드 선언부가 동일해야 한다. 인터페이스를 추상 메소드를 작성하고 구현 클래스로 해당 메소드를 작성할 시에 이러한 문제를 해결할 수 있다. 처음부터 메소드 선언부가 동일하기 때문이다.

  • 인터페이스는 메소드의 매개 변수로 많이 등장하는데, 해당 매개 값으로 여러 종류의 구현 객체를 줄 수 있으므로 메소드 실행 결과가 다양하게 나온다. 이것이 인터페이스 매개 변수의 다형성이다.

8.5.1 자동 타입 변환

구현 객체가 인터페이스 타입으로 변환되는 것은 자동 타입 변환(promotion)에 해당한다. 자동 타입 변환은 프로그램 실행 도중에 자동적으로 타입 변환이 일어나는 것을 말한다.

인터페이스 변수 = 구현객체;//구현객체가 자동 타입 변환됨
  • 구현 클래스를 상속하여 자식 클래스를 만들었다면 자식 객체 역시 인터페이스 타입으로 자동 타입 변환시킬 수 있다. 이를 통해 필드의 다형성과 매개 변수의 다형성을 구현할 수 있다.

8.5.2 필드의 다형성

  • 같은 인터페이스의 구현 객체는 교체될 수 있다. 따라서 개발 코드의 수정 없이도 다양한 결과(개발 코드에서의 인터페이스 메소드 실행 등)를 얻을 수 있다.

8.5.3 인터페이스 배열로 구현 객체 관리

  • 인터페이스 배열을 통해 여러 구현 객체들을 인덱스로 표현하여 관리할 수 있다. 이렇게 하면 제어문에서 가장 큰 이점이 있다.
Tire[] tires ={
    new HTire();
    new HTire();
    new HTire();
    new HTire();
}
void run(){
    for(Tire tire : tires){
        tire.roll();
    }
}

8.5.4 매개 변수의 다형성

  • 자동 타입 변환은 주로 메소드를 호출할 때 많이 발생한다. 매개 변수를 인터페이스 타입으로 선언하고 호출 시 구현 객체를 대입한다.
//Driver 클래스
public class Driver{
    public void drice(Vehicle vehicle){
        vehicle.run();
    }
}
//Vehicle 인터페이스 타입
public interface Vehicle{
    public void run();
}
//구현 객체 Bus
public class Bus implements Vehicle{
    @Override
    public void run(){
        System.out.println("버스가 달립니다");
    }
}
//매개 변수의 다형성 테스트
public class DriverEx{
    public static void main(String[] args){
        Driver driver = new Driver()
            
        Bus bus = new Bus();
        
        driver.drive(bus);//bus ->자동 타입 변환됨
    }
}
  • 인터페이스가 매개변수 타입으로 제공될 경우, 어떠한 구현 객체도 매개값으로 사용할 수 있고, 이를 통해 메소드의 실행결과가 다양해질 수 있게 된다.(매개 변수의 다형성)

8.5.5 강제 타입 변환

  • 구현 객체가 인터페이스 타입으로 자동 변화하면, 인터페이스에 선언된 메소드만 사용이 가능하다. 그러나 경우에 따라 구현 클래스에 선언된 필드와 메소드를 사용해야 할 수 있는데, 이 때 강제 타입 변환을 사용하여 구현 클래스의 필드와 메소드를 사용할 수 있다.
구현 클래스 변수 = (구현 클래스) 인터페이스 변수;//강제 타입 변환

8.5.6 객체 타입 확인

  • 강제 타입변환은 구현 객체가 인터페이스 타입으로 변환되어 있는 상태에서 가능하다. 이 때 상속에서 객체 타입 확인을 한 것처럼, 인터페이스 타입도 객체의 타입 확인이 필요하다. 어떤 구현 객체가 변환되어 있는지 알지 못하고 변환할 경우 ClassCastException이 발생할 수 있기 때문이다.
  • 객체 타입 확인을 위하여 instanceof 연산자를 사용할 수 있다.
if(vehicle instanceof Bus){
    Bus bus = (Bus) vehicle; //안심하고 변환 가능
}

8.6 인터페이스 상속

  • 인터페이스도 다른 인터페이스를 상속할 수 있다. 인터페이스는 클래스와 달리 다중 상속을 허용한다.
public interface 하위인터페이스 extends 상위인터페이스1, 하위인터페이스2 {...}
  • 하위 인터페이스의 구현 클래스는 하위, 상위 인터페이스의 모든 추상 메소드에 대한 실체 메소드를 가지고 있어야 한다. 따라서 해당 구현 클래스로부터 객체를 생성하고 나서 하위 및 상위 인터페이스 타입으로 변환이 가능하다.
하위인터페이스 변수 = new 구현클래스(...);
상위인터페이스1 변수 = new 구현클래스(...);
상위인터페이스2 변수 = new 구현클래스(...);
  • 하위 인터페이스로 타입 변환이 되면 상하위 인터페이스에 선언된 모든 메소드를 사용할 수 있으나, 상위 인터페이스로 타입 변환시 하위 인터페이스에 선언된 메소드는 사용이 불가하다.

8.7 디폴트 메소드와 인터페이스 확장

  • 디폴트 메소드는 인터페이스에서 선언된 인스턴스 메소드이므로 구현 객체가 있어야 사용가능하다. 왜 선언은 인터페이스에서 하고, 사용은 구현 객체를 통해 하는 것일까?

8.7.1 디폴트 메소드의 필요성

인터페이스에서 디폴트 메소드를 허용한 이유는 기존 인터페이스를 확장해서 새로운 기능을 추가하기 위해서이다. 기존 인터페이스의 이름과 추상 메소드의 변경 없이 디폴트 메소드만 추가할 수 있기 때문에 이전에 개발한 구현 클래스를 그대로 사용할 수 있으면서 새롭게 개발하는 클래스는 디폴트 메소드를 활용할 수 있다.

  • 추상 클래스는 구현 클래스에서 실행 내용을 채워야 하지만, 디폴트 메소드는 인터페이스에 정의된 것을 그냥 사용해도 되고, 필요에 따라 재정의해서 사용할 수도 있다.

8.7.2 디폴트 메소드가 있는 인터페이스 상속

  • 부모 인터페이스에 디폴트 메소드가 정의되어 있을 경우, 자식 인터페이스에서 디폴트 메소드를 활용하는 방법은 다음 세 가지가 있다.
  1. 디폴트 메소드를 단순히 상속만 받는다.
//부모 인터페이스
public interface ParentInterface{
    public void method1();
    public default void method2(){/*실행문*/}
}
//자식 인터페이스1
public interface ChildInterface1 extends ParentInterface {
    public void method3;
}
//자식 인터페이스1을 구현하는 클래스는 method1()과 method3()의 실체 메소드를 가지고 있어야 하며 method2()를 호출할 수 있다.
ChildInterface1 ci1 = new ChildInterface1(){ //익명 구현 객체
    @Override
    public void method1() {/*실행문*/}
    @Override
    public void method3() {/*실행문*/}
};

ci1.method1();
ci1.method2();//ParentInterface의 method2() 호출
  1. 디폴트 메소드를 재정의(Override)해서 실행 내용을 변경한다.
//자식 인터페이스2
public interface ChildInterface2 extends ParentInterface {
    @Override
    public default void method2() {/*실행문*/} //재정의
    public void method3();
}
//자식 인터페이스1 구현 클래스와 유사함.
ChildInterface2 ci2 = new ChildInterface2(){ //익명 구현 객체
    @Override
    public void method1() {/*실행문*/}
    @Override
    public void method3() {/*실행문*/}
};

ci2.method1();
ci2.method2();//ParentInterface의 method2() 호출
  1. 디폴트 메소드를 추상 메소드로 재선언한다.
//자식 인터페이스3
public interface ChildInterface3 extends ParentInterface {
    @Override
    public void method2() //추상 메소드로 재선언
    public void method3();
}
//자식 인터페이스3을 구현하는 클래스는 method1(), method2(), method3()의 실체 메소드를 모두 가지고 있어야 한다.
ChildInterface2 ci3 = new ChildInterface3(){ //익명 구현 객체
    @Override
    public void method1() {/*실행문*/}
    @Override
    public void method2() {/*실행문*/}
    @Override
    public void method3() {/*실행문*/}
};

ci3.method1();
ci3.method2();//ChildInterface3 구현 객체의 method2() 호출
profile
Done is better than perfect🔥

0개의 댓글