자바스터디 - 8주차

megaseunghan·2022년 3월 31일
0

자바스터디

목록 보기
4/15
post-thumbnail

목표

자바의 인터페이스에 대해 학습하세요.

학습할 것 (필수)

  • 인터페이스 정의하는 방법
  • 인터페이스 구현하는 방법
  • 인터페이스 레퍼런스를 통해 구현체를 사용하는 방법
  • 인터페이스 상속
  • 인터페이스의 기본 메소드 (Default Method), 자바 8
  • 인터페이스의 static 메소드, 자바 8
  • 인터페이스의 private 메소드, 자바 9

인터페이스 정의하는 방법

인터페이스의 정의, 특징, 장점을 배운다.

인터페이스란 ?

객체의 사용 방법을 정의한 타입으로, 모든 기능을 추상화하여 정의한 상태만 선언한다.

프로그래밍 관점에서 인터페이스는 추상 메소드의 집합, 상수도 존재하고 jdk1.8부터는 static 메소드, default 메소드도 추가 됐지만 핵심은 추상 메소드이다.

객체의 사용 설명서라고 이해하면 편하다.

인터페이스의 특징 개인 공부

  • 구현된 것이 전혀 없다.(설계도의 개념)
  • 모든 멤버가 public

인터페이스의 장점 개인 공부

  • 두 대상 간의 '연결, 대화, 소통'을 돕는 중간 역할을 한다.

    : 예를 들어 컴퓨터라는 하드웨어(대상 1 )를 사람(대상 2)이 직접 제어하는 것은 큰 어려움이 있다(기계와 사람은 사용하는 언어가 서로 다르기 때문에). 이를 해결하기 위해 우리는 인터페이스(Graphic Users Interface)를 사용하는데 이를 통해 얻는 이점은 컴퓨터의 내부 하드웨어가 바뀌어도 사용법은 똑같기 때문에 변경에 유리해진다는 것이다.

  • 선언과 구현을 분리시킬 수 있게 한다.

    • 클래스만 있을 경우
    class B {
        public void method() {
            System.out.println("MethodInB");
        }
    }

    유연하지 않고 변경에 불리하다.

    • 인터페이스를 통해 분리하였을 경우
    // 인터페이스에 추상메소드 선언
    interface I {
        public void method();
    }
    
    // 인터페이스를 구현한 클래스가 추상메소드를 구현
    class B implements I {
        public void method() {
            System.out.println("MethodInB");
        }
    }

    선언부와 구현부가 나누어져 있기 때문에 유연하고 변경에 유리하다.

  • 인터페이스를 사용하여 B가 변경되어도 A는 안바꿀 수 있게 된다(느슨한 결합)

    • 직접적인 관계의 두 클래스 (A-B)
    class A {
        public void methodA(B b) {
            b.methodB();
        }
    }
    
    class B {
        public void methodB() {
            System.out.println("method B()");
        }
    }
    
    class InterfaceTest {
        public static void main(String[] args) {
            A a = new A();
            a.methodA(new B());
        }
    }
    • 간접적인 관계의 두 클래스(A-I-B)
    class A {
        public void methodA(I i) {
            i.methodB();
        }
    }
    
    interface I { void methodB(); }
    
    // 1
    class B implements I {
        public void methodB() {
            System.out.println("method B()");
        }
    }
    // 2
    class C implements I {
        public void methodB() {
            System.out.println("methodB() in C");
        }
    }
    
    class InterfaceTest {
        public static void main(String[] args) {
            A a = new A();
            a.methodA(new B());
        }
    }

    직접 클래스를 참조하지 않고 인터페이스인 I를 사용하기 때문에 구현 객체가 변경되어도 A 클래스는 변경할 필요가 없다.

  • 개발 시간을 단축할 수 있다.

    : A라는 클래스가 B라는 클래스에 의존하여 개발이 필요하다고 가정을 하자. 하지만 B도 구현이 지연된 상황이라 A의 개발이 늦춰진다. 이때 인터페이스 I를 생성후 추상 메소드를 선언한뒤, B가 I를 구현하게 만든다면 A는 I의 추상 메소드를 호출할 수 있으므로 개발의 시간을 단축시킬 수 있다.

  • 변경에 유리한 유연한 설계가 가능하다.
    : 상단의 느슨한 결합을 보게되면 A 클래스는 method()를 호출할 때의 매개변수로 인터페이스인 I를 사용하는데, 이는 I를 구현한 클래스의 인스턴스만 매개변수로 올 수 있다는 것이다. 따라서 A의 인스턴스가 생성되고 method()의 매개변수로는 I를 구현한 B, C가 A의 직접적인 변경 없이 매개변수로 사용 가능하기 때문에 변경에 유리한 유연할 설계가 가능하다는 것이다.

  • 표준화가 가능하다.

    : JDBC를 예로 들어보자. 사용자는 데이터베이스로 현재 Oracle을 사용하고 있는데 Oracle의 가격 정책이 변경되면서 큰 부담이 느껴져 MySQL로 변경하게 되었다. 이 때 JDBC를 사용하지 않은 상태에서는 Oracle만의 표준을 따라 Application을 개발했기 때문에 MySQL로써의 변경 중 Application에 필요한 수정사항이 많을 것이다. Java는 JDBC라는 표준 인터페이스 집합을 제공하고 있다. 각 데이터베이스 회사는 JDBC의 표준에 따라 개발하고, 사용자는 이 JDBC를 사용해서 DB를 바꿔주면 된다.

  • 서로 관계없는 클래스들의 관계를 맺어줄 수 있다.

정의 방법

interface 인터페이스이름 {
  	public static final 타입 상수이름 =;
    public abstract 메소드이름(매개변수목록);
}

모든 인터페이스의 멤버가 public인 것과 추상 메소드(abstract)로 이루어진 것을 확인할 수 있다.

interface PlayingCard {
    public static final int SPADE = 4; // 생략 없음
    final int DIAMOND = 3; // public, static 생략
    static int HEARD = 2; // public, final 생략
    int CLOVER = 1; // public static final 생략
    
    public abstract String getCardNumber(); // 생략 없음
    String getCardKind(); // public, abstract 생략
}

인터페이스의 멤버(메소드 || 상수)는 항상 public, abstract, static, final이기 때문에 생략이 가능하다.

인터페이스 구현하는 방법

인터페이스의 구현이란 인터페이스에 정의된 추상 메소드를 완성하는 것이다. implements 키워드를 사용하여 구현한다. 구현 방법은 총 2가지로 구현 클래스를 통한 구현과, 익명 객체를 통한 구현이 있다.

  • 구현 클래스를 통한 구현
class 클래스이름 implements 인터페이스이름 {
    // 인터페이스에 정의된 추상메소드를 모두 구현해야 한다.
}
  • 익명 객체를 통한 구현
public class A {
  public static void main(String[] args) {
    인터페이스 변수명 = new 인터페이스() {
      @Override
      public 메소드명() {
        /* 구현부 */
      }
      @Override
      public 메소드명2() {
        /* 구현부 */
      }
    }
  }
}
  • 예시
class Fighter implemments Fightable {
    public void move(int x, int y) {
        System.out.print(x + "만큼 좌로 이동, ");
       	System.out.print(y + "만큼 앞으로 이동");
    }
    
    public void Attack(Unit unit) {
        System.out.print(unit + "에게 공격을 합니다");
	} 
}

Fighter 클래스는 FIghtable 인터페이스를 구현 했다.

  • 일부만 구현할 경우에는 클래스 앞에 abstact 키워드를 붙혀야 한다.
abstract class Fighter implemments Fightable {
    public void move(int x, int y) {
        System.out.print(x + "만큼 좌로 이동, ");
       	System.out.print(y + "만큼 앞으로 이동");
    }
}

attack 메소드는 구현하지 않았다. 따라서 추상 메소드가 존재하기 때문에 구현 클래스는 abstract 키워드를 가진 추상 클래스가 된다.

인터페이스를 이용한 다형성 개인 공부

인터페이스 또한 다형성을 구현하는 기술이 사용된다.

  • 다형성?
    : 하나의 타입에 대입되는 객체에 따라서 실행 결과가 다양한 상태로 나오는 성질을 말함

클래스에서 부모 타입에 어떤 자식 객체를 대입하냐에 따라서 실행 결과가 달라지듯, 인터페이스 타입에 따라 어떤 구현 객체를 대입하느냐에 따라 결과가 달라진다. 상속은 같은 종류의 하위 클래스를 만드는 기술이고, 인터페이스는 사용 방법이 동일한 클래스를 만드는 기술이지만 다형성을 구현하는 점은 틀림없다.

class Fighter extends Unit implements Fightable {
    public void move(int x ,int y) { /* 내용 생략 */ };
    public void attack(Fightable f) { /* 내용 생략 */};
}

다중 상속의 문제는 각각의 다른 클래스가 둘 다 attack()이라는 메소드를 가지고 있을 때 어떤 메소드를 사용할지에 대한 충돌이 일어난다는 것이다.

인터페이스는 추상 메소드로 선언되어 구현부가 없기 때문에 선언부가 충돌해도 상관이 없다. 따라서 인터페이스를 통해 다중 상속의 문제를 해결하면서 다형성을 구현할 수 있게 된다.

  • 인터페이스 타입의 객체 생성
// 부모 클래스 상속
Unit u = new Fighter();
// 인터페이스 구현
Fightable f = new Fighter();

f.move(100, 200);
f.attack(new Fighter());

Fightable의 타입의 참조변수로 Fighter 클래스를 참조하는게 가능하다. 다만 구현된 객체는 Fightable에 정의된 멤버만 사용가능하다. Fightable은 move(), attack() 멤버만 가지고 있기 때문에 그 외의 멤버는 사용하지 못한다.

  • 인터페이스 타입 매개변수는 인터페이스를 구현한 클래스의 객체만 가능하다.
interface Fightable {
    void move(int x, int y);
    void attack(Fightable f);
}
  • 인터페이스를 메소드의 리턴타입으로 지정할 수 있다.
Fightable method() {
	//Fighter f = new Fighter();
    //return f;
    return new Fighter();
}

Fightable f = method();
// ==
Fightable f = new Fighter();

Fightable을 구현한 클래스 Fighter가 리턴 타입으로 대입 가능한 것을 확인할 수 있다.

인터페이스 레퍼런스를 통해 구현체를 사용하는 방법

// 인터페이스 선언
interface Fightable {
  void move(int x, int y);
  void attack(Unit unit);
}

class Fighter implements Fightable {
  @Override
  public void move(int x, int y) {
    System.out.println(x + "만큼 이동, " + y + "만큼 이동");
  }
 	public void attack(Unit unit) {
   	System.out.println(unit + "에게 공격을 합니다.") 
  }
}

class Main {
  public static void main(String[] args) {
		Fighter fighter = new Fighter(); // Fighter 객체 생성 
    fighter.move(100, 200);
    
    /*
    * 인터페이스 타입의 객체를 선언하면서 인터페이스를 구현한 구현체를 참조하여 객체 초기화
    */
    Fightable fighter_2 = fighter;
    fighter_2.move(200, 400);
  }
}

/* 출력결과 : 
* 100만큼 이동, 200만큼 이동
*
* 200만큼 이동, 400만큼 이동
*/

새로운 인터페이스를 정의했다면 참조 타입으로써 인터페이스를 사용할 수 있다. 해당 인터페이스 타입을 구현한 클래스가 인터페이스를 데이터 타입으로써 인스턴스가 될 수 있다.

인터페이스 상속

인터페이스의 조상은 인터페이스만 가능(Object가 최고 조상이 아니다.)

인터페이스는 다중 상속이 가능하다. 추상 메소드는 선언부만 있기 때문에 구현부에서 충돌이 나지 않기 떄문이다.

interface Fightable extends Movable, Attackable { }

interface Movable {
    void move(int x, int y);
}

interface Attackable {
    void attack(Unit unit);
}

인터페이스의 기본 메소드 (Default Method), JAVA 8

위에서 적었듯이, 인터페이스는 8버전이 나오면서부터 default 메소드와 static 메소드가 추가 되었다. default 메소드는 인터페이스에 선언되지만 사실은 객체가 가지고 있는 인스턴스 메소드라고 생각해야 한다.

  • 디폴트 메소드의 필요성

    • 자바 8에서 default 메소드를 허용하는 이유는 기존 인터페이스의 확장하여 새로운 기능을 추가하기 위함이다.

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

/*
* 기존 인터페이스
*/
interface Fightable {
  void move(int x, int y);
  void attack(Unit unit);
}

/*
* 기존 구현 클래스
*/
class Fighter implements Fightable {
  @Override
  public void move(int x, int y) {
    System.out.println(x + ", " + y + "만큼 이동");
  }
  @Override
  // 생략
}

/*
* 수정 인터페이스
*/
interface Fightable {
  void move(int x, int y);
  void attack(Unit unit);
  // 디폴트 메소드 추가
  default void breath(int sec) {
    System.out.println(sec + "초 만큼 숨을 고릅니다.");
  }
}

/*
* 새로운 구현 클래스
*/
class newFighter implements Fightable {
  @Override
  public void move(int x, int y) {
    System.out.println(x + ", " + y + "만큼 이동");
  }
  @Override
  // 생략
  
  // 디폴트 메소드 재정의, 
  @Override
  public void breath(int sec) {
    System.out.println(sec + "초 만큼 숨을 고릅니다.");
    System.out.println("스으으으으읍");
    System.out.println("후우우우우우");
  }
}
  • 디폴트 메소드 선언
[public] default 리턴타입 메소드명(매개변수 목록) { 구현부 }

형태는 클래스의 인스턴스 메소드와 동일하지만 default 키워드가 리턴 타입 앞에 붙는다. public 특성을 가지고 있기 때문에 public을 생략해도 컴파일 과정에서 자동적으로 추가 된다. 인터페이스에 디폴트 메소드를 추가해도 구현 객체에는 영향이 없으며 구현 객체에서 재정의가 가능하다는 특징이 있다.

  • 디폴트 메소드 사용

디폴트 메소드는 객체의 구현 없이 사용할 수 없다. 따라서 모든 구현 객체가 가지고 있는 기본 메소드라고 생각하면 된다. 그러나 어떤 구현 객체는 디폴트 메소드의 내용이 맞지 않아 수정이 필요할 수도 있다. 구현 클래스를 작성할 때 디폴트 메소드를 재정의해서 자신에게 맞게 수정하면 디폴트 메소드가 호출될 때 재정의한 메소드가 호출된다.

인터페이스의 static 메소드, JAVA 8

디폴트 메소드와 마찬가지로 8버전 부터 추가 되었으며, 객체가 없어도 인터페이스만으로 호출이 가능한 메소드이다.

  • 정적 메소드 선언
[public] static 리턴타입 메소드명(매개변수 목록) { 구현부 }

형태는 클래스의 정적 메소드와 완전히 동일하다. 정적 메소드 또한 public의 특성을 가지고 있어 생략하더라도 컴파일 과정에서 자동으로 public이 붙는다. 구현 객체에서는 재정의가 불가능하다.

인터페이스의 private 메소드, JAVA 9

자바 9 버전에서는 private 메소드와 private static 메소드가 추가 되었는데, 캡슐화의 유지를 할 수 있게 되고, 인터페이스 내부에서 재사용성이 높아졌다.

  • private 메소드는 abstract, 즉 추상화 될 수 없고 인터페이스 내부에서만 사용할 수 있다.
  • private static 메소드는 다른 static과 non-static 인터페이스 메소드 내에서 사용할 수 있다.
  • private non-static 메소드는 private static 메소드 내에서 사용할 수 없다.

0개의 댓글