자신의 책임에 해당하지 않는 작업을 필요로 한다면, 이 작업을
담당하는 다른 객체에 접근할 필요가 있다. 절차적인 프로그래밍에서의 함수 호출보다 일반적
인 방법을 이용하여 첫번째 객체가 두 번째 객체에게 이러한 작업을 수행하도록 요청한다.(자
바에서는 이러한 함수 호출을 보통 메소드 호출(method call)이라 한다.) OOP 용어로는
클라이언트가 서버 객체에게 메시지를 보내는 것이다.
다른 객체의 내부 데이터를 직접 조작할 수 없다는 점이다. 모든
통신은 메시지, 다시 말하면 함수 호출을 통하여 이루어진다. 적절한 메시지와 내부 데이터를
조작할 수 있도록 객체를 설계함으로써, 재사용성을 극대화 시키며 디버깅 시간을 최소화할
수 있다.
클래스는 객체가 실제로 만들어지는 템플릿(template) 이나
청사진(blueprint)으로 묘사된다. 쿠키 자르는 기구를 연상하여 클래스에 대하여 생각해 보
자. 객체는 쿠키 그 자체이다. 메모리 형태로 “가루반죽”이 할당되어야 할 것이다. 자바는 이
러한 “가루반죽 준비” 과정을 없애주는데 유리하다. 단순히 new 키워드를 사용하여 메모리를
획득할 수 있으며, 아무도 더 이상 이것을 사용하지 않으면 내장된 쓰레기 수집기가 쿠키를 먹
어치울 것이다.(어떤 비유도 완벽하지 않다.) 클래스로부터 객체를 생성하면, 클래스의 인스
턴스(instance)를 생성했다고 한다.
AudioClip meow = new AudioClip();
new 연산자를 사용하여 AudioClip 클래스의 새로운 인스턴스(instance)를 생성한다.
이미 보아왔듯이 자바로 작성된 모든 것은 클래스 내에 존재한다.
클래스는 다른 클래스를 기반으로 하여 생성될 수 있다.(자바에서는 항상 그렇다.) 실제로 자바
는 “최상위 기본 클래스(Cosmic base class)”, 다시 말하면 다른 모든 클래스가 이것으
로부터 만들어진다. 다른 클래스를 기반으로 만들어진 클래스는 이것을 확장했다고(extend)
말할 수 있다. 자바에서의 모든 클래스는 Object 라는 최상위 기본 클래스를 확장한다.
기본 클래스를 확장하면, 새로운 클래스는 처음에 부모의 모든 특성과 기능을 가지고 있다.
부모가 가지고 있는 기능을 변경하거나 삭제할 수 있고, 자식 클래스만이 가지는 새로운 기능
을 만들 수도 있다. 기본 클래스를 확장하는 일반적인 개념을 상속(inheritance)이라고 부
른다.
객체에 대한 다른 중요한 개념은 캡슐화(encapsulation)이다. 형식적으로 말하면, 캡슐화
는 데이터와 행동을 하나의 패키지에 결합하여, 객체의 사용자로부터 데이터의 구현을 감추는
방식이다. 객체의 데이터는 보통 인스턴스 변수(instance variable) 또는 필드(field)
라 부르며, 자바 클래스의 함수와 프로시저는 메소드(method)라 부른다(그림 4-3 참조).
클래스의 인스턴스인 특정 객체는 현재 상태(state)를 정의하는 필드에 대한 특정 값을 가지
고 있을 것이다.
프로그램이 클래스의 인스턴스 변수(필드)에 직접 접근할 수 없게 하는 갭슐화 작업 때문에
스트레스를 받을 필요는 없다. 프로그램은 오직 객체의 메소드만을 통하여 이 데이터를 상호
접근할 수 있다. 캡슐화는 객체에 재사용성과 안정성을 제공하는 “블랙 박스” 역할을 한다.
이 말은 객체가 데이터를 저장하는 방법을 완전히 제어할 수 있다는 말이다. 그러나 객체가 데
이터를 조작하는 메소드들을 똑같이 사용하는 한 다른 객체들은 전혀 내부 내용에 대해 신경
쓸 필요가 없다.
객체
OOP 를 잘 다루기 위해서는 객체의 세가지 주된 특성을 구별할 수 있어야 한다.(고등학교 시
절을 기억할 수 있는 사람들은 사건을 표현하는 “누가(Who), 무엇을(What), 그리고 어디서
(Where)”을 연상하면 된다.) 세 개의 주된 질문은 다음과 같다.
l 객체의 행동은 무엇인가?
l 객체의 상태는 무엇인가?
l 객체는 무엇으로 식별하는가?
동일한 클래스의 인스턴스인 모든 객체는 비슷한 행동(behavior)을 공유한다. 객체의 행동
은 받아들이는 메시지에 의해 정의된다.
다음으로, 각 객체는 현재 자기가 어떻게 보이는지에 대한 정보를 저장한다. 이것을 소위 객
체의 상태(state)라고 부른다. 객체의 상태는 시간이 흐름에 따라 변화하지만 자발적이지는
않다. 객체의 상태 변화는 객체에 전달되는 메시지의 결과이다.
클래스 간의 관계
클래스 간의 일반적인 관계는 다음과 같다.
l 사용(use)
l 포함(containment, “has-a”)
l 상속(inheritance, “is-a”)
사용(use) 관계는 가장 명확하며 일반적이다. 예를 들면, Order 객체는 신용 상태를 확인하
기 위해 계좌 객체에 접근해야 하기 때문에 Order 클래스는 Account 클래스를 사용한다. 하
지만 Item 객체는 고객의 계좌에 전혀 신경을 쓸 필요가 없기 때문에 Item 클래스는
Account 클래스를 사용하지 않는다. 따라서 하나의 클래스가 다른 클래스의 객체를 다루려면
그 클래스를 사용한다.
일반적으로 다음의 조건을 만족하면, 클래스 A 는 클래스 B 를 사용한다.
l A 의 메소드가 클래스 B 의 객체에 메시지를 보낸다. 또는
l A 의 메소드가 클래스 B 의 객체를 생성하거나 전달 받거나 반환한다.
———————————————————————-
팁 : 상호 사용하는 클래스의 수를 최소화하도록 노력하라. 요점은, 클래스 A 가 클래스 B 의
존재를 모른다면, B 의 변경에 신경을 쓸 필요가 없다는 점이다.(그리고 이것은 B 의 변경이
A 에 버그를 발생시키지 않음을 의미한다.)
———————————————————————-
포함(containment) 관계는 구체적이기 때문에 이해하기 쉽다. 예를 들면, Order 객체는
Item 객체를 포함한다. 포함은 클래스 A 의 객체가 클래스 B 의 객체를 포함하고 있음을 의미
한다. 물론, 포함은 사용의 특별한 경우이다. 객체 A 가 객체 B 를 포함하고 있다면, 클래스
A 의 적어도 하나의 메소드는 클래스 B 의 그 객체를 사용할 것이다.
상속(inheritance) 관계는 특수화(specialization)를 나타낸다. 예를 들면, 그림 4-4
에서처럼 RushOrder 클래스는 Order 클래스로부터 상속한다. 특수화된 RushOrder 클래스
는 우선순위를 다루는 특수한 메소드라든가 선적료를 계산하는 다른 메소드를 가지고 있다.
하지만 항목을 추가하고 청구하는 다른 메소드는 Order 클래스로부터 상속한다. 일반적으로,
클래스 A 가 클래스 B 를 확장한다면, 클래스 A 는 클래스 B 로부터 메소드를 상속(확장)하지
만 더 많은 기능을 가지고 있다.(상속은 다음 장에서 더 보다 자세히 설명할 것이다.)
객체 변수가 아무런 객체도 참조하지 않도록 의도적으로 null 로 설정할 수 있다.
chirp = null;
…
if (chirp != null) chirp.play();
null 변수를 통하여 메소드를 호출하면, 실행시간 오류가 발생한다.
지역 객체 변수는 자동적으로 null 로 초기화되지 않는다. new 를 호출하거나 null 로 설정
하여 이것을 초기화해야 한다.
GregorianCalendar todaysDate = new GregorianCalendar ();
위의 코드는 다음을 행한다.
GregorianCalendar todaysDate = new GregorianCalendar ();
위의 코드는 다음을 행한다.
1. todaysDate 라는 GregorianCalendar 클래스의 인스턴스를 새롭게 생성한다.
2. 동시에 todaysDate 객체의 상태를 현재 날짜(호스트의 운영체제가 유지)가 되도록 초
기화한다.
또한 특정 날짜로 GregorianCalendar 클래스의 인스턴스를 생성할 수도 있다.
GregorianCalendar preMillenium = new GregorianCalendar (1999, 11,
31);
이것은 preMillenium 이라는 GregorianCalendar 인스턴스를 생성하여, 1999 년 12 월
31 일로 초기 상태를 설정한다. 달의 시작을 0 으로부터 하기 때문에 11 이 12 월을 나타내게
된다. 좀 더 분명하게 표현하기 위해서는 Calendar.DECEMBER 와 같은 상수를 사용한다.
GregorianCalendar 클래스는 실제로 날짜/시간 클래스이기 때문에 시간을 설정할 수도 있
다.(설정하지 않으면 기본값은 자정이다.) 예를 들면,
GregorianCalendar preMillenium = new GregorianCalendar (1999,
Calendar.DECEMBER, 11, 31, 23, 59, 59);
이것은 GregorianCalendar 객체를 생성하여 인스턴스 필드를 1999 년 12 월 31 일 자정 1
초 전으로 설정한다.( GregorianCalendar()를 사용하면 운영체제에서 유지하는 시간으로
날짜 인스턴스가 설정된다.)
주어진 날짜에 대한 그레고리안 달력을 생성한다.
매개변수: year 그 날짜의 년도
month 그 날짜의 월. 값은 0 부터 시작한다. 1 월-0
date 그 월의 날
l GregorianCalendar (int year, int month, int date, int hour, int
minutes, int seconds)
주어단 날짜와 시간에 대한 그레고리안 달력을 생성한다.
매개변수: year 그 날짜의 년도
month 그 날짜의 월. 값은 0 부터 시작한다. 1 월-0
date 그 월의 날
hour 시간(0 에서 23 까지)
minutes 분(0 에서 59 까지)
seconds 초(0 에서 59 까지)
l boolean equals (Object when)
이 달력 객체를 when 과 비교하고 객체가 같은 시점을 나타내면 true 를 리턴한다.
l boolean before (Object when)
이 달력 객체를 when 과 비교하고 객체가 when 보다 전이면 true 를 리턴한다.
l boolean after (Object when)
이 달력 객체를 when 과 비교하고 객체가 when 보다 뒤면 true 를 리턴한다.
l int get (int field)
특정 필드의 값을 취한다.
매개변수: field one of
Calendar.ERA
Calendar.YEAR
Calendar.MONTH
Calendar.WEEK_OF_YEAR
Calendar.WEEK_OF_MONTH
Calendar.DATE
Calendar.DAY_OF_MONTH
Calendar.DAY_OF_YEAR
Calendar.DAY_OF_WEEK
Calendar.DAY_OF_WEEK_IN_MONTH
Calendar.AM_PM
Calendar.HOUR
Calendar.HOUR_OF_DAY
Calendar.MINUTE
Calendar.SECOND
Calendar.MILLISECOND
Calendar.ZONE_OFFSET
Calendar.DST_OFFSET
l void set (int field, int value)
특정 필드의 값을 설정한다. 이 메소드는 주의해서 사용해야 한다. 잘못하면 있지 않은
날짜를 만들 수 도 있다.
매개변수: field get 에 의해 받아들여진 상수들 중 하나
value 새로운 값
l void set (int year, int month, int date)
날짜 필드를 새로운 날짜로 설정한다.
매개변수: year 그 날짜의 년도
month 그 날짜의 월. 값은 0 부터 시작한다.
date 그 달의 날
l void set (int year, int month, int date, int hour, int minutes, int
seconds)
날짜와 시간의 필드들을 새로운 값으로 설정한다.
매개변수: year 그 날짜의 년도
month 그 날짜의 월. 값은 0 부터 시작한다. 1 월-0
date 그 월의 날
hour 시간(0 에서 23 까지)
minutes 분(0 에서 59 까지)
seconds 초(0 에서 59 까지)
l void add (int field, int amount)
날짜를 수학적으로 계산하는 함수이다. 주어진 시간 필드에 특정 양의 시간을 추가한다.
예를 들어, 7 일을 추가하기 위해서는 c.add(Calendar.DATE, 7)을 호출한다.
매개변수: field 변경을 위한 필드(gst 메소드에 있는 하나의 상수를 사
용한다.)
amount 그 필드가 변하는 양(음수도 가능하다.)
l void sstGregorianChange (Date date)
날짜를 줄리안 달력의 끝과 그레고리안 시작에 맞추어 설정한다.기본값은 00:00:00,
10 월 15, 1582 이다.
매개변수: date 이상적인 그레고리안 시작 날짜
l date getGregorianChange ()
줄리안 달력에서 그레고리안 달력으로 바뀌는 날짜를 취한다.
GregorianCalendar 클래스는 다소 비정형적인 방법으로 캘랜더 객체에 저장된 정보에 접근
한다. 대개의 클래스들은 개체의 상태에 도달하기 위해 getYear 나 getMonth 등의 메소드
를 가지고 있다. 그러나 GregorianCalendar 클래스는 단지 년, 월 등뿐만 아니라 주중에
대한 정보와 같은 많은 숫자로 이루어진 것들에 대한 질의에 사용하기 위해 다양한 기능의
get 메소드들을 가지고 있다. 원하는 항목을 고르기 위해 Calendar 클래스 내에 정의된 상
수 즉, Calendar.MONTH 나 Calendar.DAY_OF_WEEK 를 고려해야 한다:
GregorianCalendar todaysDdate = new GregorianCalendar ( )
System.out.println (todaysData.get (Calendar.Month) );
System.out.println (todaysData.get (Calendar.DAY_OF_WEEK) );
날짜는 set 메소드나 add 메소드를 통해 바꿀 수도 있다. 예를 들어
dueDate.set (1999, 11, 31);
get 메소드와 set 메소드나 add 메소드와는 개념적인 차이가 있다. get 메소드는 객체의
상태를 살펴보고 그 상태를 보고한다. set 과 add 메소드는 객체의 상태를 바꿔 버린다. 인
스턴스 필드를 변경시키는 메소드를 변경자 메소드(mutator methods)라 부르고, 인스턴스
필드를 접근하는 메소드를 접근자 메소드(accessor methods)라 부른다. 자바에서는 습관
적으로 접근자 메소드에 소문자 접두사 get 을 사용하고, 변경자 메소드에는 set 을 사용한다.
예를 들어 GregorianCalendar 클래스는 getGregorianCalendar 와
setGregorianCalendar 와 같은 메소드를 가진다. 실제로, GregorianCalendar 클래스
는 다소 새로운 방법을 쓴다. 년, 월, 일, 시, 분, 초 등에 접근하기 위해 분리된 get 접근
자를 쓰지 않고 단순 get 메소드를 사용한다. getGregorianCalendar 와
setGregorianCalendar 메소드는 달력을 만드는 프로그래머가 아니면 실질적인 효익은 없
지만 자바 메소드들의 전형적인 특성을 보여준다. 다음 절에서는 GregorianCalendar 클래
스의 이해를 돕기 위해 그 메소드들이 전현적인 자바 클래스와 유사한 Day 클래스를 소개한다.
corejava.Day
l void advance(int n)
지정된 일만큼 현재 설정된 일을 더한다. 예를 들면, d.advance(100)은 d 를 100 일 뒤로
변경시킨다.
l int getDay(), int getMonth(), int getYear()
이 날짜 객체의 년, 월, 일을 리턴한다. 일은 1 에서 31 까지이고, 월은 1 부터 12, 년은 아
무 년이나(1996 년 또는 -333 년) 가능하다. 클래스는 Julian 력과 Gregorian 력을 서로
변환하는 방법을 알고 있다.
l int weekday()
요일에 해당하는 0 부터 6 까지의 정수를 리턴한다.(0=일요일)
l int daysBetween(Day b)
이 메소드는 우리가 Day 클래스를 만든 주된 이유 중의 하나이다. 이것은 Day 클래스의 현재
인스턴스와 Day 클래스의 인스턴스 b 사이의 일의 수를 계산한다.
import corejava.*;
public class DaysAlive
{ public static void main(String[] args)
{ int year;
int month;
int day;
month = Console.readInt
(“Please enter the month, 1 for January and so on”);
day = Console.readInt
(“Please enter the day you were born.”);
year = Console.readInt
(“Please enter the year you were born (starting with 19..)”);
Day birthday = new Day(year, month, day);
Day today = new Day();
System.out.println(“You have been alive “
+ today.daysBetween(birthday) + “ days.”);
}
}
private 키워드는 이 클래스의 메소드를 통해서 만이 인스턴스 필드에 접근이 가능하며, 외
부에서는 접근할 수 없음을 의미한다. 이 책에서의 인스턴스 필드는 항상 private 이다.(서
로 아주 밀접한 클래스들을 구현할 때에만 예외가 적용된다. 예를 들면, 연결 리스트 데이터
구조에서 List 와 Link 클래스와 같은 경우이다.)
———————————————————————-
노트 : 인스턴스 변수를 public 으로 선언할 수도 있지만, 아주 좋지 않은 생각이다. 데이터
필드가 public 인 경우에는 프로그램의 어느 부분에서나 이 인스턴스 변수를 읽거나 변경할
수 있다. 이것은 캡슐화를 완전히 망쳐 놓기 때문에 public 인스턴스 필드를 사용하지 않을
것을 강력히 권고한다.
———————————————————————-
Employee 클래스의 인스턴스를 생성할 경우,
hireDate = new Day(1950, 1, 1);
Employee number007 = new Employee
(“James Bond”, 100000, hireDate);
인스턴스 필드는 다음과 같이 설정된다.
name = “James Bond”;
salary = 100000;
hireDay = January 1, 1950 // actually a Day class with this
// data encapsulated
클래스를 생성하기 위해 생성자와 함께 항상 new 메소드가 사용된다. 생성자는 객체의 초기
상태를 강제로 설정한다. 자바에서는 인스턴스 변수를 초기화(의도적으로 또는 묵시적으로)하
지 않고는 클래스의 인스턴스를 생성할 수 없다. 이러한 설계 결정의 이유는 간단하다. 적절
하게 초기화되지 않고 생성된 객체는 항상 쓸모없고 때로는 위험하다. Delphi 같은 다른 많
은 언어에서는 초기화되지 않은 객체를 생성할 수 있다. 이러한 결과는 대부분 메모리가 더럽
혀졌다는 의미인 GPF(General Protection Fault)나 세그멘테이션 폴트
(segmentation fault)를 발생시킨다.
이 장의 뒷부분에서 생성자 메소드에 대해 더 배우겠지만, 현재로서는 다음 사항을 기억해 두
어라.
1. 생성자의 이름은 클래스의 이름과 동일하다.
2. 생성자는 (이 예제에서와 같이) 하나 또는 그 이상의(또는 아예 없는) 매개변수를 갖는
다.
3. 생성자는 항상 new 키워드와 함께 호출된다.
4. 생성자는 리턴값이 없다.
또한 생성자와 다른 메소드와의 중요한 차이점은 다음과 같다.
l 생성자는 new 를 사용해서만 호출될 수 있다. 인스턴스 필드를 다시 설정하기 위해 이미
존재하는 객체에 생성자를 적용할 수 없다. 예를 들면, d.Date(1950, 1, 1)은 오류
이다.
———————————————————————-
c++ 노트 : 자바에서의 생성자는 C++에서와 동일한 방식으로 작동된다. 하지만 모든 자바 객
체는 힙 상에 만들어지며, 생성자는 항상 new 와 결합되어야 한다는 점을 명심하여라. 다음은
C++ 프로그래머가 저지르기 쉬운 new 연산자를 빼먹어서 발생한 오류이다.
Employee number007(“James Bond”, 100000, hireDate);
// C++, not Java
이것은 C++에서는 작동하지만 자바에서는 그렇지 않다.
———————————————————————-
함정 : 지역 변수들을 인스턴스 필드들과 동일하게 명명하지 않도록 주의하라. 예를 들면, 다
음 생성자는 salary 를 설정하지 않을 것이다.
public Employee(String n, double s, Day d)
{ name – n;
hireDay = d;
double salary = s; // ERROR
}
지역 변수 salary 는 인스턴스 필드 salary 의 그림자이고 생성자 내부에서만 접근이 가능하
다. 이것은 추적하기 아주 어려운 에러이다. 여러분은 단지 여러분의 모든 메소드 내에서 인
스턴스 필드의 이름과 동일한 이름을 사용하지 않도록 주의해야 한다.
마지막으로, 간단한 getName 메소드를 좀더 자세히 살펴보자.
public String getName()
{ return name;
}
이것은 접근자 메소드의 전형적인 예이다. 이것은 클래스의 필드를 직접 다루므로 때로는 필
드 접근자 메소드(field accessor method)라 불리 운다. 이 메소드는 단순히 name 필드
의 현재 상태를 리턴한다.
클래스 구현자에게는 단순히 공개 데이터 필드를 작성하기 보다 은폐 필드와 공용 접근자 메소
드를 작성하는 편이 더 어렵다. 하지만 클래스를 이용하는 프로그래머에게는 불편하지 않다.
number007 이 employee 클래스의 인스턴스 이름이라면, number007.name 이라고 하기 보
다는 오히려 number007.getName()을 쓰는 편이 간단하다.
된다.(그리고 어떤 경우라도 자바에서의 관습은 접근자 메소드가 소문자
“get”으로 시작한다
class Employee
{ . . .
public Day getHireDay() {return (Day) hireDay.clone ( );}
}
경험적으로 변경 가능한 데이터 필드의 복사본을 리턴 할 때 항상 clone 을 사용한다.