공부했던 자료 정리하는 용도입니다.

재배포, 수정하지 마세요.

 

 

 

 

추상 클래스(Abstract Class)

 추상 클래스는 미완성 설계도와 비슷하다. 추상 클래스만으로는 인스턴스를 생성할 수 없고 자식 클래스에서 상속받아야만 완성시킬 수 있다. 추상 클래스는 클래스로서의 역할을 다 못하지만, 새로운 클래스를 작성하는 데 있어서 바탕이 되는 부모 클래스로서 중요한 의미를 갖는다.

 

abstract class 클래스이름{
	...
}

추상 클래스는  abstract 키워드를 붙여서 만든다. 클래스의 선언부에 이 키워드가 있다면 추상 메서드가 있으니 상속을 통해서 구현해주어야 한다. 추상 클래스는 추상 메서드를 포함하고 있다는 것을 제외하고는 일반 클래스와 전혀 다르지 않다. 추상 클래스에도 생성자가 있으며, 멤버 변수와 메서드도 가질 수 있다.

추상 메서드를 포함하고 있지 않은 클래스에도  abstract 를 붙여서 추상 메서드로 지정할 수도 있다. 추상 메서드가 없는 완성된 클래스여도 추상 메서드로 지정되면 클래스의 인스턴스를 생성할 수 없다.

 

 

 

 

추상 메서드(Abstract Method)

 메서드의 선언부만 작성하고 구현부는 미완성인 채로 남겨둔 것을 추상 메서드라고 한다. 미완성으로 남겨놓는 이유는 상속받는 클래스에 따라서 메서드의 내용이 달라질 수 있기 때문이다. 그래서 부모 클래스에서는 선언부만을 작성하고 주석을 덧붙여서 어떤 기능을 수행할 목적으로 작성되었는지 알려주고, 실제 내용은 상속받는 클래스에서 구현하도록 비워두는 것이다. 그래서 추상 클래스를 상속받는 자식 클래스는 부모의 추상 메서드를 상황에 맞게 적절히 구현해 주어야 한다. 

 

/* 주석으로 어떤 기능을 구현해야 하는지 설명 */
abstract 리턴타입 메서드이름();

추상 메서드 역시  abstract 키워드를 앞에 붙여주고, 구현부가 없으므로 괄호 { } 대신 문장의 끝을 알리는  ; 을 적어준다.

추상 클래스로부터 상속받는 자식 클래스는 오버라이딩을 통해 부모인 추상 클래스의 추상 메서드를 모두 구현해주어야 한다. 만일 부모로부터 상속받은 추상 메서드 중 하나라도 구현하지 않는다면 자식 클래스 또한 추상 클래스로 지정해주어야 한다.

 

추상 메서드를 이용하면, 메서드를 사용하는 쪽에서는 메서드가 실제로 어떻게 구현되어있는지 몰라도 선언부(메서드의 이름과 매개변수, 리턴 타입)만 알고 있으면 되므로 내용이 없을지라도 추상 메서드를 사용하는 코드를 작성하는 것이 가능하며, 실제로는 자식 클래스에 구현된 완성된 메서드가 호출되도록 할 수도 있다.

 

 

 

 

추상 클래스의 작성

 여러 클래스에 공통적으로 사용될 수 있는 클래스를 바로 작성하기도 하고, 기존 클래스의 공통적인 부분을 뽑아서 추상 클래스로 만든 뒤 상속하도록 하는 경우도 있다. 상속이 자식 클래스를 만드는데 부모 클래스를 사용하는 것이라면, 추상화는 기존 클래스들의 공통부분을 뽑아내서 부모 클래스를 만드는 것이라고 할 수 있다.

 

추상화 : 클래스간의 공통점을 찾아내서 공통의 부모를 만드는 작업
구체화 : 상속을 통해서 클래스를 구현, 확장하는 작업

 추상화와 구체화는 서로 반대되는 개념이다. 상속계층도를 내려갈수록 클래스는 점점 기능이 추가되어 구체화의 정도가 심해지고 세분화된다. 반대로 상속계층도를 올라갈수록 클래스는 추상화의 정도가 심해지고 공통 요소만 남게 된다.

 

어차피 자식 클래스에서 오버라이딩 해야하니까 함수의 구현부를 빈 괄호 { } 로 표현해도 추상 메서드와 차이점이 없을 것 같지만 굳이  abstract 를 붙여서 추상 메서드를 만드는 이유는 자식 클래스에서 추상 메서드를 반드시 구현하도록 강요하기 위해서이다.

 

 

 

 

 


인터페이스(Interface)

인터페이스는 일종의 추상 클래스이다. 인터페이스는 추상 클래스처럼 추상 메서드를 갖지만 추상 클래스보다 추상화 정도가 높아서 추상 클래스와 달리 몸통을 갖춘 일반 메서드나 멤버 변수를 가질 수 없다. 오직 추상 메서드와 상수만을 멤버로 가질 수 있다. 추상 클래스가 부분적으로만 완성된 것이라면 인터페이스는 구현된 것이 아무것도 없고 틀만 잡혀있는 것이라고 할 수 있다. 인터페이스도 추상 클래스처럼 완성되지 않은 불완전한 것이기 때문에 그 자체만으로 사용되기보다는 다른 클래스를 작성하는데 도움을 줄 목적으로 작성된다. 

 

 

 

인터페이스의 작성

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

인터페이스를 작성하는 것은 클래스를 작성하는 것과 같다. 키워드로  class 대신  interface 를 사용하는 것만 다르다. 그리고  interface 에도 클래스와 같이 접근제어자로  public 또는  default 를 사용할 수 있다.

 

 

일반적인 클래스의 멤버들과 달리 인터페이스의 멤버들은 다음과 같은 제약사항이 있다.


인터페이스 멤버의 제약사항

모든 멤버 변수는  public static final 이어야 하며, 이를 생략할 수 있다.
모든 메서드는  public abstract 이어야 하며 생략할 수 있다. 단,  static 메서드와 디폴트 메서드는 예외(JDK 1.8부터)

인터페이스에 정의된 모든 멤버에 예외 없이 적용되는 사항이기 때문에 제어자를 생략할 수 있는 것이며, 편의상 생략하는 경우가 많다. 생략된 제어자는 컴파일 시에 컴파일러가 자동으로 추가해준다.

 

 

 

 

인터페이스의 상속

interface Changeable{
    /* 채널을 바꾸는 기능의 메서드 */
    void change(Channel c);
}

interface Powerable{
    /*전원을 껐다 켰다 하는 메서드*/
    void power(boolean b);
}

interface Controlable extends Changeable, Powerable {}	//다중상속 가능

인터페이스는 인터페이스로부터만 상속받을 수 있으며, 클래스와는 달리 다중 상속(여러 개의 인터페이스로부터 상속받는 것)이 가능하다. 클래스의 상속과 마찬가지로 자식 인터페이스( Controlable )는 부모 인터페이스( Changeable ,  Powerable )에 정의된 멤버들을 모두 상속받는다. 그래서  Controlable 에는 정의된 멤버가 하나도 없지만 부모 인터페이스로부터 상속받은 2개의 추상 메서드  change(Channel c)  power(boolean b) 를 멤버로 갖게 된다.

 

 

 

 

인터페이스의 구현

 class 클래스이름 implements 인터페이스이름{
 	// 인터페이스에 정의된 추상메서드를 구현해야한다.
 }
 
 class Control implements Controlable{
    public void change(Channel c) { /* 내용생략 */ }
    public void power(boolean b) { /* 내용생략 */}
 }

인터페이스도 추상 클래스처럼 그 자체로는 인스턴스를 생성할 수 없으며, 추상 클래스가 상속을 통해 완성되는 것처럼 인터페이스도 자신에게 정의된 추상 메서드의 구현부를 만들어주는 클래스를 만들어야 한다. 인터페이는 구현한다는 의미의 키워드인  implements 를 사용한다. 만일 구현하는 인터페이스의 메서드 중 일부만 구현한다면  abstract 를 붙여서 추상 클래스로 선언해야 한다. 

 

 class Control extends Channel implements Controlable{
    public void change(Channel c) { /* 내용생략 */ }
    public void power(boolean b) { /* 내용생략 */}
 }

위의 코드와 같이 상속과 구현을 동시에 할 수도 있다.

인터페이스의 이름은 어떠한 기능 또는 행위를 하는데 필요한 메서드를 제공한다는 의미를 강조하기 위해서 '~을 할 수 있는'이라는 의미인 able로 끝나는 것들이 많다. 또한 그 인터페이스를 구현한 클래스는 '~를 할 수 있는 능력'을 갖추었다는 의미이기도 하다. 이름이 able로 끝나는 것은 인터페이스라고 추측할 수 있지만, 모든 인터페이스의 이름이 반드시 able로 끝나야 하는 것은 아니다.

 

 

  예시로 든 코드의 관계도를 그리면 위와 같다. 실제로  Control 클래스는  Channel 클래스로부터 상속받고  Controlable 인터페이스만을 구현했지만  Channel 클래스는  Object 클래스의 자식이고,  Controlable 인터페이스는  Changeable  Powerable인터페이스의 자식이므로  Control 클래스는 이 모든 클래스와 인터페이스의 자식이 되는 셈이다. 인터페이스는 상속 대신 구현이라는 용어를 사용하지만 인터페이스로부터 상속받은 추상 메서드를 구현하는 것이기 때문에 인터페이스도 조금은 다른 의미의 부모라고 할 수 있다.

 

접근제어자를 주의해야 하는데  Changeable 이나  Powerable 인터페이스에 정의된 함수들을  Control 에서 구현할 때  public 을 사용해야 한다는 것이다. 오버라이딩때는 부모의 메서드보다 넓은 범위의 접근제어자를 지정해야 하는데  Changeable  Powerable 인터페이스에 정의된 함수들은  void 라고만 써져있지만 실제로는  public abstract 가 생략된 것이기 때문에 public abstract void 함수이름(매개변수) 가 된다. 그래서 이를 구현하는 클래스에서는 접근제어자를 반드시  public 으로 해야 한다.

 

 

 

 

인터페이스를 이용한 다중 상속

  자바에서는 다중 상속을 허용하지 않는다. 인터페이스를 이용하면 다중 상속이 가능하지만 자바에서 인터페이스로 다중 상속을 구현하는 경우는 거의 없다. 인터페이스는  static 상수만 정의할 수 있으므로 부모 클래스의 멤버 변수와 충돌하는 경우는 거의 없고 충돌된다 하더라도 클래스 이름을 붙여서 구분이 가능하다. 그리고 추상 메서드는 구현 내용이 전혀 없으므로 부모 클래스의 메서드와 선언부가 일치하는 경우에는 당연히 부모 클래스쪽의 메서드를 상속받으면 되므로 문제되지 않는다. 그러나 이렇게하면 상속받는 멤버의 충돌은 피할 수 있지만, 다중상속의 장점을 잃게 된다. 그래서 2개의 클래스로부터 상속을 받아야 하는 상황이라면, 두 부모클래스 중에서 비중이 높은 쪽을 선택하고 다른 한쪽은 클래스 내부의 멤버로 포함시키는 방식으로 처리하거나 어느 한쪽의 필요한 부분을 뽑아서 인터페이스로 만든 다음 구현하도록 한다.

 

 

 

 

 

인터페이스를 이용한 다형성

  인터페이스도 해당 인터페이스 타입의 참조 변수로 이를 구현한 클래스의 인스턴스를 참조할 수 있으며, 인터페이스 타입으로 형변환도 가능하다.

 

Controlable c = (Controlable)new Control();

//이렇게도 가능
Controlable c = new Control();

인터페이스  Controlable 을 클래스  Control 이 구현했을 때 다음과 같이  Control 인스턴스를  Controlable 타입의 참조 변수로 참조하는 것이 가능하다.

 

 

void power(Controlable c){	//매개변수로 Controlable인터페이스를 구현한 클래스의 인스턴스를 넘겨주어야한다.
	//...
}

따라서 인터페이스는 위의 코드와 같이 메서드의 매개변수 타입으로 사용될 수 있다. 인터페이스 타입의 매개변수가 갖는 의미는 메서드 호출 시 해당 인터페이스를 구현한 클래스의 인스턴스를 매개변수로 제공해야 한다는 것이다. 그래서  power 메서드를 호출할 때는 매개변수로  Controlable 인터페이스를 구현한 클래스의 인스턴스를 넘겨주어야 한다.

 

 

Controlable method(){
	...
    Control c = new Control();	// 1
    return c;	    // 2, 1과 2를 한줄로 바꾸면 return new Control();이 된다.
}

또한 위의 코드처럼 메서드의 리턴 타입으로 인터페이스의 타입을 지정하는 것도 가능하다. 리턴 타입이 인터페이스라는 것은 메서드가 해당 인터페이스를 구현한 클래스의 인스턴스를 반환한다는 것을 의미한다. 위의 코드에서는  method( ) 의 리턴 타입이  Controlable 인터페이스이기 때문에 메서드의  return 으로  Controlable 인터페이스를 구현한  Control 클래스의 인스턴스를 반환한다.

 

 

 

 

인터페이스의 장점

  1. 개발 시간을 단축시킬 수 있다.
    : 메서드를 호출하는 쪽에서는 선언 부만 알면 되기 때문에 인터페이스만 가지고도 프로그램을 작성할 수 있다. 동시에 다른 한쪽에서는 인터페이스를 구현하는 클래스를 작성하면 인터페이스의 구현을 기다리지 않고 동시에 작업이 가능하다
  2. 표준화가 가능하다.
    : 프로젝트에 사용되는 기본 틀을 인터페이스로 작성한 다음 개발자들에게 인터페이스를 구현하여 프로그램을 짜도록 하면 보다 일관되고 정형화된 프로그램의 개발이 가능하다.
  3. 서로 관계없는 클래스들에게 관계를 맺어줄 수 있다.
    : 아무 관계도 없는 클래스들에게 하나의 인터페이스를 공통적으로 구현하도록 함으로써 관계를 맺어줄 수 있다.
  4. 독립적인 프로그래밍이 가능하다.
    : 인터페이스를 사용하면 클래스의 선언과 구현을 분리시킬 수 있기 때문에 실제 구현에 독립적인 프로그램을 작성하는 것이 가능하다. 클래스와 클래스 간의 직접적인 관계를 인터페이스를 이용해서 간접적인 관계로 변경하면, 한 클래스의 변경이 관련된 다른 클래스에 영향을 미치지 않는 독립적인 프로그래밍이 가능하다.

 

 

 

 

인터페이스의 이해

package test;

class A{	//class A 선언
	public void methodA(B b) {	//클래스 B타입인 b를 매개변수로 받는 메서드 methodA 선언
		b.methodB();	//인스턴스 b의 methodB()호출
	}
}

class B{	//class B 선언
	public void methodB() {	//methoB 선언
		System.out.println("methodB()");
	}
}

class test{
	public static void main(String[] args) {
		A a = new A();	//A타입의 인스턴스를 생성해서 A타입의 참조변수 a에 저장
		a.methodA(new B());	//B타입의 새로운 인스턴스를 생성하고, 이를 매개변수로 하는 methodA()호출
	}
}

 

실행결과

위의 코드와 같이 클래스  A  B 가 있다고 가정하자. 클래스  A 는 클래스  B 의 인스턴스를 생성하고 메서드를 호출한다. 

 

 

이 두 클래스는 서로 직접적인 관계에 있어서 클래스  A 를 작성하려면 클래스  B 가 이미 작성되어 있어야 하고, 클래스  B 의  methodB( ) 의 선언부가 변경되면 이를 사용하는 클래스  A 도 변경되어야 한다는 단점이 있다.

 

이때 인터페이스를 매개체로 해서 클래스  A 가 인터페이스를 통해서 클래스  B 의 메서드에 접근하도록 만들면, 클래스  B 가 변경되거나 다른 클래스로 대체되어도 클래스  A 는 전혀 영향을 받지 않는다. 두 클래스 간의 관계를 간접적으로 변경하려면 인터페이스를 이용해서 클래스 B 의 선언과 구현을 분리해야 한다.

 

 

package test;

class A{
	public void methodA(I i) {	//매개변수를 인터페이스타입인 I로 바꿈
		i.methodB();	//인터페이스 i의 메소드 methodB()호출 (I를 구현한 B클래스의 메소드 호출)
	}
}

interface I{	//인터페이스 I선언
	public abstract void methodB();
}

class B implements I{	//인터페이스 I를 구현한 클래스B선언
	public void methodB() {	//인터페이스 I의 추상메서드 methodB()를 구현
		System.out.println("methodB() in Bclass");
	}
}

class test{
	public static void main(String[] args) {
		A a = new A();	
		a.methodA(new B());
	}
}

 

실행결과

위의 있던 예시를 인터페이스를 사용한 코드로 변경했다. 변경된  A 의 코드를 보면 클래스 A 에는 이제 클래스 B 가 사용되지 않은 것을 확인할 수 있다. 이전 예시에서 클래스  A 와 클래스  B 는 직접적인 관계였지만 이제는 인터페이스를 통해서만 연결되는 간접적인 관계로 바뀌었다.

 

 

클래스  A 는 여전히 클래스  B 의 메서드를 호출하지만 클래스  A 는 인터페이스  I 하고만 직접적인 관계기 때문에 클래스  B 의 변경에 영향을 받지 않는다. 클래스  A 는 인터페이스를 통해 실제로 사용하는 클래스의 이름을 몰라도 되고 심지어는 실제로 구현된 클래스가 존재하지 않아도 문제 되지 않는다.

 

 

인터페이스  I 는 실제 구현 내용(클래스  B )를 감싸고 있는 껍데기이며, 클래스  A 는 껍데기 안에 어떤 알맹이(클래스)가 들어있는지 몰라도 된다.

 

 


+ 참고 내용

 

클래스  A 가 인터페이스  I 를 사용해서 작성되긴 했지만 매개변수를 통해서 인터페이스  I 를 구현한 클래스의 인스턴스를 동적으로 제공받아야 한다.(클래스  Thread 의 생성자인  Thread(Runnable target) 이 이런 방식으로 되어있음 /  Runnable 은 인터페이스)

 

 

또 다른 방법으로는 새로운 클래스를 통해서 매개변수를 제공받는 방법이다.(JDBC의  DriverManager 클래스가 이와 같은 방식)

package test;

class A{
	public void methodA() {	
		I i = InstanceManager.getInstance();	//getInstance() 메서드를 이용해서 인스턴스 생성
		i.methodB();	//인스턴스 i의 methodB()호출(I를 구현한 B클래스의 getmethodB()가 호출됨)
		System.out.println(i.toString());	//인터페이스 I에는 toString()이 정의되어있지 않지만 모든 객체는 Object클래스에 정의된 메서드를 가지고 있으므로 호출가능
		
	}
}

interface I{	
	public abstract void methodB();
}

class B implements I{	
	public void methodB() {	
		System.out.println("methodB() in Bclass");
	}
	
	public String toString() { return "class B"; }
}

class InstanceManager{	
	public static I getInstance() {	//getInstance() 메서드로만 인스턴스 생성
		return new B();
	}
}

class test{
	public static void main(String[] args) {
		A a = new A();	
		a.methodA();
	}
}

위의 코드에서는 인스턴스를 직접 생성하지 않고,  getInstance( ) 라는 메서드를 통해 제공받는다. 이렇게 하면 나중에 다른 클래스의 인스턴스로 변경되어도  A 클래스의 변경 없이  getInstance( ) 만 변경하면 된다는 장점이 있다.

 

그리고 인터페이스  I 타입의 참조 변수  i 로도  Object 클래스에 정의된 메서드들을 호출할 수 있다.  i   toString( ) 이 정의되어있지 않지만 모든 객체는  Object 클래스에 정의된 메서드를 가지고 있기 때문에 허용된다.

 


 

 

 

디폴트 메서드와 static메서드, private 메서드

원래는 인터페이스에 추상 메서드만 선언할 수 있는데 JDK1.8부터 디폴트 메서드와  static 메서드도 추가할 수 있게 되었다.  static 메서드는 인스턴스와 관계없는 독립적인 메서드기 때문에 추가해도 상관없었지만 규칙을 단순히 할 필요가 있어서 허용되지 않았고, 이 때문에 인터페이스와 관련된  static 메서드는 별도의 클래스에 따로 두어야 했다. 대표적으로  java.util.Collection 인터페이스가 있는데, 이 인터페이스와 관련된  static 메서드들이 인터페이스에는 추상 메서드만 선언할 수 있다는 원칙 때문에 별도의 클래스인  Collections 라는 클래스에 들어가게 되었다. 인터페이스의  static 메서드 역시 접근제어자가 항상  public 이며 생략할 수 있다.

  • 디폴트 메서드(Default Method) (Java 8 이후)
    디폴트 메서드는 추상 메서드의 기본적인 구현을 제공하는 메서드로 추상 메서드가 아니기 때문에 디폴트 메서드가 새로 추가되어도 해당 인터페이스를 구현한 클래스를 변경하지 않아도 된다. 디폴트 메서드는 앞에 키워드  default 를 붙이며, 추상 메서드와 달리 일반 메서드처럼 구현부 { } 가 있어야 한다. 디폴트 메서드 역시 접근제어자가  public 이며생략 가능하다.
  • 정적 메서드(Static Method) (Java 8 이후)
    인스턴스 생성과 상관없이 인터페이스 타입으로 사용할 수 있는 메서드
  • private 메서드 (Java 9 이후)
    - 인터페이스를 구현한 클래스에서 사용하거나 재정의 할 수 X
    - 인터페이스 내부에서만 사용하기 위해 구현하는 메서드
    - default 메서드나 static 메서드에서 사용함

새로 추가된 디폴트 메서드가 기존 메서드와 이름이 중복되어 충돌하는 경우

1. 여러 인터페이스의 디폴트 메서드 간의 충돌
  : 인터페이스를 구현한 클래스에서 디폴트 메서드를 오버라이딩 해야 한다.


2. 디폴트 메서드와 부모 클래스 메서드 간의 충돌
  : 부모 클래스의 메서드가 상속되고, 디폴트 메서드는 무시된다.



(규칙이 귀찮으면 필요한 쪽의 메서드와 같은 내용으로 오버라이딩 해버리면 된다!)

 

 

 

+ Recent posts