'abstract'의 사전적 의미는 '추상적인'의 뜻이다. 추상이라는 말은 구체적이지 않다 또는 실체가 없다라고 해석할 수 있다. 이번 장에서는 객체 지향 프로그래밍의 특징 중의 하나인 추상화(abstraction)를 살펴보자.

 

 

 

abstract class

아래의 noAction 메소드를 유심히 보자. 이 메소드는 몸체(body)가 없고 대신에 abstract라는 키워드가 붙어있다.

 

publicabstractvoid noAction();     // body({...})가 없고 세미콜론(;)이 있다.

 

noAction 메소드는 몸체가 없으니 구체적인 동작이 없다. 이런 메소드를 추상 메소드라고 하고 메소드 앞에 abstract가 붙어있다.

 

추상 메소드를 포함하는 클래스를 추상 클래스(abstract class)라고 하고 클래스를 기술할 때 abstract를 삽입해야 한다.

 

abstract class Myclass{               // Myclass는 abstract class이다.

  int myInt;

  public abstract void noAction();   // x1, 추상 메소드

  public getMyint(){                    // 비(非) 추상 메소드

    return myInt;

  }

}

 

추상 메소드를 포함하는 클래스, 즉 추상 클래스는 객체가 될 수 없다. 구체적인 동작이 정의되지 않았기 때문이다.

 

Myclass a = new Myclass();   // 에러, 추상 클래스는 객체가 될 수 없다.

 

그렇다면 프로그램을 짤 때 어떤 클래스를 추상 클래스로 만들어야 할까? 또 추상 클래스는 객체가 될 수 없는데 무슨 소용이 있을까?

 

예를 들어, 다음 그림과 같은 상속 관계를 가진 클래스들을 정의한다고 가정하자.

 

사용자 삽입 이미지

[그림 9-1] 추상 클래스

 

도형은 도화지에 그릴 수 있으므로 '그리다' 메소드를 가지고 있고, 도형 클래스를 상속하는 삼각형과 사각형 클래스가 '그리다' 메소드를 오버라이드했다고 치자.

 

그리고 아래와 같이 각 클래스로부터 객체를 생성하였다고 가정하면

 

도형 do = new 도형();

도형 sam = new 삼각형();

도형 sa = new 사각형();

 

삼각형 객체(sam)는 그릴 때 삼각형 모양으로 그리면 될 것이고 사각형 객체(sa)는 사각형 모양으로 그리면 될 것이다. 그러면 도형 객체(do)는 어떻게 그릴까?

 

sam.그리다();      // △ 모양으로 그린다.

sa.그리다();       // □ 모양으로 그린다.

do.그리다();       // 무슨 모양이지???

 

그렇다. 도형은 그릴 수 없다. 즉 메소드의 body를 정의할 수 없다. 도형 클래스의 '그리다'가 바로 추상 메소드인 것이다. 또 도형은 추상 메소드를 포함하고 있으므로 추상 클래스이다. 도형의 모양을 머리 속에 떠올려 보자. 삼각형, 사각형은 구체적 모양이 있지만 도형은 구체적 모양이 머리 속에 떠오르지 않을 것이다. 이와 같은 클래스를 추상 클래스로 정의하면 딱 좋다.

 

도형 클래스로부터 객체는 만들 수 없지만, 도형을 상속하는 클래스에서 추상 메소드인 '그리다()'를 오버라이드(body 정의)하면 삼각형 객체는 삼각형 모양으로 그리고 사각형 객체는 사각형 모양으로 그리게 되는 것이다.

 

도형 클래스의 멤버, '그리다()'를 추상 메소드로 선언하는 궁극적 이유는 일종의 약속을 만드는 것이다. 추상 메소드인 '그리다()'가 없다면 삼각형은 그릴 때 'draw()'를, 사각형은 그릴 때 'paint()'를 사용할 수도 있을 것이다. 또는 원(Circle) 클래스는 'drawCicle()' 메소드를 사용하도록 정의할 수도 있을 것이다. 이런 현상은 혼란스럽고 바람직하지 않다. 따라서, 도형 클래스에서 추상 메소드인 '그리다()'를 선언함으로써 도형의 자식은 '그리다()'를 상속받아서 오버라이드하게 만든다, 따라서 그릴 때는 무조건 '그리다()'를 사용하게 하여 일관성을 가질 수 있다.

 

그리고 중요한 것은 추상 클래스를 상속받은 자식 클래스는 추상 메소드를 오버라이드해야 할 것이다. 그렇지 않으면 그 자식도 추상 클래스가 되기 때문이다.

 

Abstract1.java

 

abstractclass도형{                // 추상 클래스

  double 면적;

 

  public abstract void 그리다();   // 추상 메소드

 

  public double 면적구하기(){

    return 면적;

  }

}

 

class삼각형 extends 도형{           // 상속

  double 밑변, 높이;

  public 삼각형(int a, int b){           // 생성자

    밑변=a;

    높이=b;

    면적=밑변*높이/2;

  }

 

  public void 그리다(){              // 상속받은 추상 메소드를 오버라이드 한다.

    System.out.println("삼각형을 그린다");

  }

}

 

class사각형 extends 도형{                // 상속

  double 가로,세로;

  public 사각형(int a, int b){               // 생성자

    가로=a;

    세로=b;

    면적=가로*세로;

  }

  public void 그리다(){                     // 추상 메소드 오버라이드

    System.out.println("사각형을 그린다");

  }

}

 

public classAbstract1{

  public static void main(String[] args){

    도형 tr=new 삼각형(10,20);

    도형 re=new 사각형(10,20);

    System.out.println(tr.면적구하기());

    tr.그리다();

    System.out.println(re.면적구하기());

    re.그리다();  // tr과 re는 모두 그리다()를 사용하여 그린다.

  }

}


 

출력 결과

 

100.0

삼각형을 그린다

200.0

사각형을 그린다


 

 

주의하기

 

abstract 메소드는 private 또는 static이 될 수 없다.

abstract 메소드는 자식이 오버라이드하여 클래스 외부에서 부모의 것에 접근할 수 있어야 한다. 하지만 추상 메소드를 private로 지정하면 클래스 외부에서 접근할 수 없게 된다. 따라서 abstract 메소드는 private이 될 수 없다.

 

static 메소드는 객체없이도 호출되는 메소드이므로 body가 정의되어야 한다. body가 없다면 '클래스.메소드()' 형식으로 호출할 수 없기 때문이다.

 

 

 

final

final은 '최후, 최종, 끝'을 의미한다. final 키워드는 변수, 메소드, 클래스에 사용될 수 있다.

 

publicfinalint a = 10;

 

위와 같이 변수 앞에 final이 붙으면 a가상수(Constant)임을 의미한다. 즉 a에 처음으로 어떤 값이 대입되고 나면 나중에 다른 값이 대입될 수 없다. a는 말 그대로 변하지 않는 수이다. 추상 클래스의 추상 메소드가 일종의 약속인 것처럼, 상수는 그 값이 불변함을 객체들에게 보장하는 일종의 약속이라고 볼 수 있다.

 

보통, 상수는 모든 객체가 공유하기 때문에 상수를 static으로 선언하는 경우가 많다.

 

publicstatic finaldouble PI = 3.14159265358979323846;

 

PI(π)값은 변경될 수 없으며 모든 객체가 공유할 수 있다. 참고로 Math 클래스에 PI가 정의되어 있다.

 

짚어두기

 

static 변수

static 멤버 변수는 객체를 만들지 않고도 사용할 수 있는 변수이자 모든 객체가 공유하는 변수이다. 일반 멤버 변수(non-static)는 객체를 만들어야만 사용할 수 있는데 반해 static 변수는 객체를 만들지 않고도 사용할 수 있다.

 

아래와 같이 final 키워드가 메소드 앞에 올 때 도 있다.

 

publicfinalvoid f(){...}

 

메소드 앞에 final이 붙는 경우에는, 이 메소드를 가지는 클래스를 상속하는 자식 클래스는 이메소드를 오버라이드할 수 없다.자식이 오버로드하여 메소드의 기능을 변질하면 치명적 오류가 생길 수 있거나, 또는 메소드의 기능을 보호하고자 할 때 사용한다.

 

final 키워드는 클래스 앞에도 올 수 있다.

 

finalclass Myclass{...}

 

클래스 앞에 final이 붙는 경우는다른 클래스가 이 클래스를 상속할 수 없다.대표적으로 우리가 알고 있는 System, String 클래스가 final 클래스이다. 따라서 절대로 System이나 String 클래스를 상속할 수 없다. 만일 System 클래스를 상속할 수 있다면 자식이 그 기능을 변질시켜 시스템에게 치명적 오류를 발생시킬 수도 있을 것이다. 그래서 이런 클래스들은 final로 지정해서 상속 못하게 하는 것이다. String 클래스는 다른 클래스와 많이 연관되어 있다. 따라서 상속하여 변질하면 다른 클래스에게도 영향을 주기 때문에 final로 지정되어 있다.

 

아래 예제는 컴파일 되지 않는다. 에러의 원인을 찾아보자.

 

Final1.java

 

finalclassFinal1_1{

  public staticfinaldouble PI=3.14;

  publicfinalvoid fmethod(){

    System.out.println("이 메소드는 오버라이드할 수 없다.");

  }

}

public class Final1extends Final1_1{     // final 클래스는 상속할 수 없다.

  public void fmethod(){                 // final 메소드는 오버라이드할 수 없다.

    System.out.println("오류");

  }

  public static void main(String[] args){

    PI=3.15;                              // 상수의 값은 바뀔 수 없다.

  }

}


 

 

 

interface

interface는 '경계면'이라는 뜻으로 두 객체가 상호 작용할 때 interface를 통한다. 예를 들어 춘향전에서 이도령이 춘향에게 메시지를 전달할 때 방자를 통해서 했다. 이 때 방자를 인터페이스로 볼 수 있다. 흔히 말하는 API(Application ProgramInterface)도 응용프로그램과 운영체제가 서로 교신할 때 인수로써 정의되는 API을 통해서 하기 때문에 인터페이스라는 용어를 사용한다. 따라서 인터페이스란 두 객체가 통신하는 규칙 또는 약속이라고 할 수 있다. 추상 메소드를 선언하는 이유도 자식들이 그 메소드를 오버라이드하여 사용하도록 만드는 일종의 약속을 정한 것이다.

 

자바의 인터페이스는 모든 메소드가 추상이고 모든 변수가 static 상수이다. 즉, 인터페이스는 그 자체가 약속인 것이다.

 

publicinterfaceMyInter{

  public static final int MAX=100;        // x1

  public int MIN=1;                      // x2

  public abstract void method1();        // x3

  public void method2(int a);            // x4

}

 

interface의 모든 변수는 static이고 final이다. 그런데 x2행의 변수 MIN에 static final이 붙지 않았다. 하지만 컴파일 할 때 컴파일러가 static final을 붙이므로 x2행의 MIN도 static final이다. 그리고interface의 모든 메소드는 추상 메소드이다. method1()과 method2()가 추상 메소드임을 확인할 수 있다. abstract 키워드도 생략할 수 있다. 그러나 변수나 메소드를 선언할 때 x1행처럼 'static final'을 삽입하여 변수의 형을 명백히 하는 것이 프로그래밍 습관상 좋다. 명백하게 작성하지 않고 바르게 동작하기를 바라는 것은 나쁜 습관이다.

 

인터페이스의 모든 메소드가 추상이므로 인터페이스를 상속하는 클래스는 모든 추상 메소드를 오버라이드 해야만 객체가 될 수 있다. 여기서 인터페이스는 '상속하다(extends)'라고 표현하지 않고 대신에 '구현하다(implements)'라고 표현한다. 인터페이스는 추상 클래스보다 더 추상적이므로 인터페이스를 상속하는 클래스는 인터페이스의 body를 구현해야 한다는 뜻이다. 하지만 구현도 상속과 비슷한 개념으로 인터페이스의 모든 멤버를 물려받는다.

 

다음 예제를 보자.

 

Interface1.java

 

interface MyInter{                                   // 인터페이스

  public static final int MAX=100;

  public static final int MIN=1;

  public void method1();

  public void method2(int a);

}

 

public classInterface1 implements MyInter{       // MyInter를 구현하는 클래스

  public void method1(){                            // x1, 오버라이드

    System.out.println("method1 override");

  }

  public void method2(int a){                       // x2, 오버라이드

    System.out.println(a+": method2 override");

  }

  public static void main(String[] args){

    Interface1 ob=new Interface1();

    ob.method1();                          // x1행의 오버라이드된 method1 호출

    ob.method2(123);                       // x2행의 오버라이드된 method2 호출

    System.out.println(Interface1.MAX);      // Interface1은 Max을 물려받았다.

    System.out.println(MyInter.MIN);         // static 변수는 객체 없이도 사용 가능

  }

}


 

출력 결과

 

method1 override

123: method2 override

100

1


 

인터페이스는 상속하는 것이 아니라 구현한다고 표현하고 인터페이스를 구현하는 클래스는 모든 추상 메소드를 오버라이드 해야 한다.

 

클래스의 상속은 단일 상속만 가능한데 반해 인터페이스는 다중 구현이 가능하다.

 

interface2.java

 

interface MyInter1{               // 인터페이스

  public void method1();

}

interface MyInter2{               // 인터페이스

  public void method2();

}

public class Interface2implements MyInter1, MyInter2{    // x1

  public void method1(){                     // x2, MyInter1의 메소드 오버라이드

   System.out.println("method1 override");

  }

  public void method2(){                     // x3, MyInter2의 메소드 오버라이드

   System.out.println("method2 override");

  }

  public static void main(String[] args){

    Interface2 ob=new Interface2();

    ob.method1();

    ob.method2();

  }

}


 

출력 결과

 

method1 override

method2 override


 

x1행의 Interface2 클래스는 MyInter1과 MyInter2를 동시에 구현하고 있다. 따라서 두 interface의 모든 메소드를 오버라이드 해야 한다(x1, x2).

 

3개 이상의 interface도 다음과 같이 콤마(,)를 사용하여 구현할 수 있다.

 

class Interface2implements MyInter1, MyInter2, MyInter3{...}

 

일반적인 클래스는 다른 클래스를 상속할 수 있을 뿐만 아니라 동시에 interface의 구현도 가능하다. 다음 예제를 보자.

 

Interface3.java

 

interface MyInter1{

  public void method1();

}

interface MyInter2{

  public void method2();

}

class MyClass{

  public void hi(){

    System.out.println("안녕");

  }

}

 

// Interface3는 MyClass를 상속하고 MyInter1과 MyInter2를 구현한다.

public class Interface3extends MyClass implements MyInter1, MyInter2{

  public void method1(){                        // 구현

    System.out.println("method1 override");

  }

  public void method2(){                        // 구현

    System.out.println("method2 override");

  }

  public static void main(String[] args){

    Interface3 ob=new Interface3();

    ob.method1();

    ob.method2();

    ob.hi();

  }

}


 

출력 결과

 

method1 override

method2 override

안녕


 

 

interface는 interface를 상속할 수 있다. 다음 예제를 보자.

 

Interface4.java

 

interface MyInter1{

  public void method1();

}

interface MyInter2 extends MyInter1{             // x1

  public void method2();

}

publicclass Interface4 implements MyInter2{    // x2

  public void method1(){                          // 오버라이드

    System.out.println("method1 override");

  }

  public void method2(){                          // 오버라이드

    System.out.println("method2 override");

  }

  public static void main(String[] args){

    Interface4 ob=new Interface4();

    ob.method1();

    ob.method2();

  }

}


 

출력 결과

 

method1 override

method2 override


 

x1행을 보면 interface가 interface를 상속할 때 extends를 사용하고 있다. 자식도 interface이므로 부모를 구현하는 것이 아니라 상속하는 것이다. 상속하였으므로 MyInter2가 가지는 추상 메소드는 method1()과 method2()이다. x2행의 Interface4 클래스는 자식 interface인 MyInter2를 구현하고 있다. MyInter2 인터페이스의 모든 추상 메소드를 구현하고 있는지 확인하자.

 

interface의 메소드는 private 또는 protected가 될 수 없다. private이 될 수 없는 이유는 abstract class와 같은 이유이다. protected 접근 지정자는 자식에게 접근을 허용하는 것이다. 하지만 interface는 자식 클래스가 없으므로(상속하지 않고 구현한다) protected를 사용할 수 없다.

 

다음 예제의 에러를 분석하고 수정해 보자.

 

Interface5.java

 

interfaceMyInter{

  privatevoid method1();

  protectedvoid method2();

}

public classInterface5 implements MyInter{

  public void method1(){

    System.out.println("method1 override");

  }

  public void method2(){

    System.out.println("method2 override");

  }

  public static void main(String[] args){

    Interface5 ob=new Interface5();

    ob.method1();

    ob.method2();

  }

}


 

 

 

Cloneable 인터페이스

7장에서 '레퍼런스에 의한 호출(pass by Reference)'을 살펴보았었다. 매소드의 매개 변수가 레퍼런스이면 인수로 넘어오는 객체를 참조한다는 내용이었다. 그러나 때에 따라서는 객체를 복제하여 메소드에게 넘겨줄 필요가 있다. 복제된 객체를 클론(Clone)이라고 한다.

 

void f(SomeObject ob){      // 어떤 메소드

  ...

}

 

SomeObject ob=new SomeObject();   // 어떤 객체

f(ob의 클론);       // ob의 복제 객체를 인수로 넘긴다.

 

자바는 객체의 클론을 만들 때, 다음의 약속을 지키도록 권장하고 있다.

 

어떤 객체가 자신의 클론을 만들 수 있으려면

첫째, 클래스는 Cloneable 인터페이스를 구현해야 한다.

둘째, Object 클래스의 clone 메소드를 오버라이드해야 한다.

 

clone 메소드는 객체의 클론을 반환하는 메소드이다.

 

protected native Objectclone() throws CloneNotSupportedException;

 

다음 예제를 통해 객체의 클론을 생성하는 방법을 익히자.

 

Cloneable1.java

 

public classCloneable1 implements Cloneable{    // Cloneable 구현

  String s;

  int a;

  public Cloneable1(String s, int a){

    this.s=s;

    this.a=a;

  }

  public Objectclone(){          // 자신의 클론을 반환하는 메소드, 오버라이드

    try{

      returnsuper.clone();                      // Object 클래스의 clone 호출

    }catch(CloneNotSupportedExceptioncse){}

    return null;

  }

  public static void main(String[] args){

    Cloneable1 ob1 =new Cloneable1("abcd", 10);

    Cloneable1 ob2 =(Cloneable1)ob1.clone();       // ob1의 클론을 생성

 

    // 해시코드 출력

    System.out.println("원본: "+ob1.hashCode());

    System.out.println("복제: "+ob2.hashCode());

 

    System.out.println("원본: "+ob1.s+"  "+ob1.a);

    System.out.println("복제: "+ob2.s+"  "+ob2.a);

  }

}


 

출력 결과

 

원본: 2536009

복제: 8581339

원본: abcd  10

복제: abcd  10


 

ob2는 ob1의 클론(복제 객체)이므로 해시코드(hash code)가 서로 다르다.

 

 

 

Enumeration 인터페이스

Enumeration 인터페이스를 구현하는 객체는 내부에 주어진 요소(elements)들을 가지고 있고, 필요할 때 요소를 차례대로 하나씩 꺼낼 수 있다.

 

Enumeration 인터페이스는 java.util 패키지에 있고, 다음과 같이 두 개의 메소드를 가지고 있다.

 

 

Enumeration인터페이스의 메소드

 

booleanhasMoreElements();

남아있는 요소가 있으면 true를, 아니면 false를 반환한다.

ObjectnextElement();

다음 요소를 반환한다.

 

다음 예제를 해보자. 클래스 Enumeration1은 Enumeration을 구현한 것이다.

 

Enumeration1.java

 

import java.util.*;

public classEnumeration1 implements Enumeration{   // Enumeration 구현

  Object[] data;             // 요소 배열

  int count=0;               // 다음으로 꺼낼 요소의 위치

  public Enumeration1(Object[] data){

    this.data=data;

  }  

  public booleanhasMoreElements(){        // 오버라이드

    return count < data.length;

  }

  public ObjectnextElement(){              // 오버라이드

    if (count < data.length) {                // 남아있는 요소가 있으면

      return data[count++];                 // 다음 요소를 반환한다.

    }

    return null;                      // 남아있는 요소가 없으면 null을 반환한다.

                                        // ※ 예외를 던질 수도 있다.

  }

  public static void main(String[] args){

    String[] arr = new String[]{ "abc", "def", "ghi", "jkl"};

    Enumeration enum = new Enumeration1(arr);

 

    // enum의 요소를 차례대로 모두 꺼낸다.

    while(enum.hasMoreElements()){           // 남아 있는 요소가 있으면

      System.out.println(enum.nextElement());   // 다음 요소를 꺼낸다.

    }

  }

}


 

출력 결과

 

abc

def

ghi

jkl


 

 

나중에 다룰 Collection 관련 API중에서, 몇몇의 클래스가 Enumeration 인터페이스를 구현하고 있다.

 

 

 

 

연습 문제

 

 

1. 다음 코드에서 잘못된 부분을 수정해보자.

 

  class Abstract{

    int a=1;

    private void f();

    public void g();

    public void h(){

      a=5;

    }

  }

 

 

2. 객체 지향 프로그래밍에서 상수(final)를 사용하는 이유는 무엇일까? 상수를 사용하지 않았을 때의 문제점을 생각해보자.

 

 

3. '파생 종단'은 무엇을 뜻하는가?

 

 

4. 추상 클래스와 인터페이스의 역할 면에서의 차이점은 무엇인 지 생각해보자.

 

 

5. 도형(Shape)은 추상 클래스로 정의해야 하는가? 아니면 인터페이스로 정의해야 하는가? 엄격히 따져보자.

 

-생각하는자바-


Posted by 영웅기삼
,