| |
3.1 선언위치에 따른 변수의 종류 변수는 클래스변수, 인스턴스변수, 지역변수 모두 세 종류가 있다. 변수의 종류를 결정짓는 중요한 요소는 '변수의 선언된 위치'이므로 변수의 종류를 파악하기 위해서는 변수가 어느 영역에 선언되었는지를 확인하는 것이 중요하다. 멤버변수를 제외한 나머지 변수들은 모두 지역변수이며, 멤버변수 중 static이 붙은 것은 클래스변수, 붙지 않은 것은 인스턴스변수이다.
아래의 그림에는 모두 3개의 int형 변수가 선언되어 있는데, iv와 cv는 클래스 영역에 선언되어있으므로 멤버변수이다. 그 중 cv는 키워드 static과 함께 선언되어 있으므로 클래스 변수이며, iv는 인스턴스변수이다. 그리고, lv는 메서드인 method() 내부에 선언되어 있으므로 지역변수이다.
1. 인스턴스변수(instance variable) 클래스 영역에 선언되며, 클래스의 인스턴스를 생성할 때 만들어진다. 그렇기 때문에 인스턴스 변수의 값을 읽어 오거나 저장하기 위해서는 먼저 인스턴스를 생성해야한다. 인스턴스는 독립적인 저장공간을 가지므로 서로 다른 값을 가질 수 있다. 인스턴스마다 고유한 상태를 유지해야하는 속성의 경우, 인스턴스변수로 선언한다.
2. 클래스변수(class variable) 클래스 변수를 선언하는 방법은 인스턴스변수 앞에 static을 덧붙이기만 하면 된다. 인스턴스마다 독립적인 저장공간을 갖는 인스턴스변수와는 달리, 클래스변수는 모든 인스턴스가 공통된 저장공간(변수)을 공유하게 된다. 그래서 클래스 변수를 공유 변수(shared variable)라고도 한다. 한 클래스의 모든 인스턴스들이 공통적인 값을 유지해야하는 속성의 경우, 클래스변수로 선언해야 한다. 인스턴스변수는 인스턴스를 생성한 후에야 사용가능하지만, 클래스 변수는 인스턴스를 생성하지 않고도 언제라도 바로 사용할 수 있다는 특징이 있으며, '클래스이름.클래스변수'와 같은 형식으로 사용한다. [참고]위의 예제에서 Variables클래스의 클래스변수 cv를 사용하려면 Variables.cv와 같이 하면 된다.
3. 지역변수(local variable) 메서드 내에 선언되어 메서드 내에서만 사용 가능하며, 메서드가 종료되면 소멸되어 사용할 수 없게 된다. for문 또는 while문의 블럭 내에 선언된 지역변수는, 지역변수가 선언된 블럭{} 내에서만 사용 가능하며, 블럭{}을 벗어나면 소멸되어 사용할 수 없게 된다. [참고]여기서의 메서드는 생성자와 초기화 블럭을 포함한 개념이다. 앞으로 배우게 될 생성자와 초기화 블럭은 내부적으로 메서드로 취급된다.
3.2 클래스변수와 인스턴스변수 클래스변수와 인스턴스변수의 차이를 이해하기 위한 예로 카드 게임에 사용되는 카드를 클래스로 정의해보자.
카드 클래스를 작성하기 위해서는 먼저 카드를 분석해서 속성과 기능을 알아 내야한다. 속성으로는 카드의 무늬, 숫자, 폭, 높이 정도를 생각할 수 있을 것이다. 이 중에서 어떤 속성을 클래스 변수로 선언할 것이며, 또 어떤 속성들을 인스턴스 변수로 선언할 것인지 생각해보자.
| class Card { String kind ; // 카드의 무늬 - 인스턴스 변수 int number; // 카드의 숫자 - 인스턴스 변수 static int width = 100 ; // 카드의 폭 - 클래스 변수 static int height = 250 ; // 카드의 높이 - 클래스 변수 }
|
각 Card인스턴스는 자신만의 무늬(kind)와 숫자(number)를 유지하고 있어야 하므로 이들을 인스턴스변수로 선언하였고, 각 카드들의 폭(width)과 높이(height)는 모든 인스턴스가 공통적으로 같은 값을 유지해야하므로 클래스변수로 선언하였다.
만일 카드의 폭을 변경해야할 필요가 있을 때는 모든 카드의 width값을 변경하지 않고, 한 카드의 width값만 변경해도 모든 카드의 width값이 변경되는 셈이다.
[예제6-4] CardTest.java | class CardTest{ public static void main(String args[]) { // 클래스변수(static 변수)는 객체생성없이 '클래스이름.클래스변수'로 직접 사용 가능하다. System.out.println("Card.width = " + Card.width); System.out.println("Card.height = " + Card.height);
Card c1 = new Card(); c1.kind = "Heart"; c1.number = 7;
Card c2 = new Card(); c2.kind = "Spade"; c2.number = 4;
System.out.println("c1은 " + c1.kind + ", " + c1.number + "이며, 크기는 (" + c1.width + ", " + c1.height + ")" ); System.out.println("c2는 " + c2.kind + ", " + c2.number + "이며, 크기는 (" + c2.width + ", " + c2.height + ")" ); System.out.println("이제 c1의 width와 height를 각각 50, 80으로 변경합니다."); c1.width = 50; c1.height = 80;
System.out.println("c1은 " + c1.kind + ", " + c1.number + "이며, 크기는 (" + c1.width + ", " + c1.height + ")" ); System.out.println("c2는 " + c2.kind + ", " + c2.number + "이며, 크기는 (" + c2.width + ", " + c2.height + ")" ); } }
class Card { String kind ; // 카드의 무늬 - 인스턴스 변수 int number; // 카드의 숫자 - 인스턴스 변수 static int width = 100; // 카드의 폭 - 클래스 변수 static int height = 250; // 카드의 높이 - 클래스 변수 }
|
[실행결과] | Card.width = 100 Card.height = 250 c1은 Heart, 7이며, 크기는 (100, 250) c2는 Spade, 4이며, 크기는 (100, 250) 이제 c1의 width와 height를 각각 50, 80으로 변경합니다. c1은 Heart, 7이며, 크기는 (50, 80) c2는 Spade, 4이며, 크기는 (50, 80)
|
Card클래스의 클래스변수(static변수)인 width, height는 Card클래스의 인스턴스를 생성하지 않고도 '클래스이름.클래스변수'와 같은 방식으로 사용할 수 있다. Card인스턴스인 c1과 c2는 클래스 변수인 width와 height를 공유하기 때문에, c1의 width와 height를 변경하면 c2의 width와 height값도 바뀐 것과 같은 결과를 얻는다. Card.width, c1.width, c2.width는 모두 같은 저장공간을 참조하므로 항상 같은 값을 갖게 된다. 인스턴스 변수는 인스턴스가 생성될 때 마다 생성되므로 인스턴스마다 각기 다른 값을 유지할 수 있지만, 클래스 변수는 모든 인스턴스가 하나의 저장공간을 공유하므로, 항상 공통된 값을 갖는다. [플래시동영상]자료실의 플래시동영상 MemberVar.swf을 꼭 보도록하자.
3.3 메서드 메서드는 어떤 작업을 수행하기 위한 명령문의 집합이다. 주로 어떤 값을 입력받아서 처리하고 그 결과를 되돌려 준다. 경우에 따라서는 입력받는 값이 없을 수도 있고 결과를 반환하지 않을 수도 있다. 메서드를 작성하는 가장 큰 이유는 반복적으로 사용되는 코드를 줄이기 위해서이다. 자주 사용되는 내용의 코드를 메서드로 작성해 놓고 필요한 곳에서 호출만 하면 된다. 코드의 양도 줄일 수 있고 코드를 한 곳에서만 관리하면 되므로 유지보수가 편리하다.
| - 하나의 메서드는 한 가지 기능만 수행하도록 작성하는 것이 좋다. - 반복적으로 수행되어야 하는 여러 문장을 하나의 메서드로 정의해 놓으면 좋다. - 관련된 여러 문장을 하나의 메서드로 만들어 놓는 것이 좋다.
|
메서드의 내부 코드를 몰라도 메서드를 호출할 때 어떤 값을 필요로 하고 어떤 결과를 반환한다는 것만 알아도 프로그램을 작성할 수 있다. 실제 프로젝트에서는 고급개발자들이 프로젝트에 사용될 주요 메서드를 미리 작성해 놓고, 초중급개발자들이 이 들을 사용해서 개발하는 방식으로 프로젝트를 진행한다.
이제 메서드를 작성하는 방법에 대해서 알아보도록 하자.
메서드는 크게 선언부와 구현부(몸통, body), 두 부분으로 나누어져 있다. 메서드의 선언부에는 리턴타입, 메서드이름, 그리고 괄호()에 매개변수를 선언하고, 구현부에는 메서드가 호출되었을 때 수행되어야 할 코드를 넣어 주면 된다. 메서드는 호출될 때 이 매개변수를 통해서 호출하는 메서드로부터 작업수행에 필요한 값들을 제공 받는다. 매개변수의 수는 메서드에 따라 없을 수도 있고, 여러 개일 수도 있다. 매개변수가 여러 개인 경우 쉼표(,)로 구분하여 나열한다.
메서드의 괄호()에 선언된 매개변수는 지역변수로 간주되어 메서드 내에서만 사용될 수 있으며, 메서드가 종료되는 순간 메모리에서 제거되어 더 이상 사용할 수 없게 된다.
리턴타입(return type)은 메서드의 수행결과를 어떤 타입(자료형)으로 반환할 것인지를 알려 주는 것이다. 메서드가 결과값을 반환하지 않는 경우에는 리턴타입 대신 void를 사용하며, 메서드가 결과값을 반환하는 경우에는 메서드 내에 반드시 return문을 사용해서 리턴타입에 맞는 결과값을 호출한 메서드에게 반환하도록 해야 한다.
위의 add메서드는 두 개의 정수형(int)값을 입력 받아서, 덧셈 연산을 한 후 그 결과를 호출한 메서드에게 정수형(int) 결과값을 돌려주는 일을 한다. 이렇게 정의된 add메서드를 호출할 때는 메서드의 괄호()에 선언된 자료형 또는 타입에 맞는 값을 입력해주어야 한다. 그리고, 메서드의 결과를 저장하기 위해서는 메서드에 선언된 리턴타입과 같은 타입의 변수를 준비해야한다.
3.4 return문 메서드가 정상적으로 종료되는 경우는 다음과 같이 두 가지가 있다.
| - 메서드의 블럭{}내에 있는 모든 문장들을 수행했을 때 - 메서드의 블럭{}내에 있는 문장을 수행중 return문을 만났을 때
|
return문은 현재 실행 중인 메서드를 종료하고 호출한 메서드로 되돌아가게 한다.
| 1. 반환값이 없는 경우 - return문만 써주면 된다. return; 2. 반환값이 있는 경우 - return문 뒤에 반환값을 지정해 주어야 한다. return 반환값;
|
반환값이 있는 경우에는 메서드의 선언부에 정의된 반환타입과 반환값의 타입이 일치하거나 반환값의 타입이 반환타입으로 자동형변환이 가능한 것이어야 한다. [참고] 원칙적으로는 모든 메서드의 마지막에는 return문이 있어야 하지만, 반환값이 없는 메서드의 경우 return문을 생략한다고 볼 수 있다.
위의 그림에서 알 수 있는 것처럼, add메서드의 리턴타입이 int로 선언되었으므로, return문에서 반환해주는 값의 타입이 int 또는 int로 자동형변환이 가능한 byte, short, char 중의 하나이어야 한다.
3.5 메서드의 호출 메서드를 작성하는 방법에 대해서 알아봤으니 이제는 작성한 메서드를 사용하는 방법에 대해서 알아보도록 하자. 메서드를 호출하기 위해서는 다음과 같이 한다.
| 참조변수.메서드이름(); // 메서드에 선언된 매개변수가 없는 경우 참조변수.메서드이름(값1, 값2, ...); // 메서드에 선언된 매개변수가 있는 경우
|
같은 클래스 내의 메서드끼리는 참조변수를 사용하지 않고도 서로 호출이 가능하지만 static메서드는 같은 클래스 내의 인스턴스 메서드를 호출할 수 없다. 다음은 두 개의 값을 매개변수로 받아서 사칙연산을 수행하는 4개의 메서드를 가진 MyMath클래스를 정의한 것이다.
| class MyMath { long add(long a, long b) { long result = a+b; return result; // return a + b; // 위의 두 줄을 이와 같이 한 줄로 간단히 할 수 있다. } long subtract(long a, long b) { return a - b; } long multiply(long a, long b) { return a * b; } double divide(double a, double b) { return a / b; } }
|
MyMath클래스의 add(long a, long b)를 호출하기 위해서는 먼저 MyMath mm = new MyMath();와 같이 해서, MyMath클래스의 인스턴스를 생성한 다음 참조변수 mm을 통해야한다.
add메서드의 매개변수의 타입이 long이므로 long 또는 long으로 자동형변환이 가능한 값을 지정해야한다. 호출시 매개변수로 지정된 값은 메서드의 매개변수로 복사된다. 위의 코드에서는 1L과 2L의 값이 long타입의 매개변수 a와 b에 각각 복사된다. 메서드는 호출시 넘겨받은 값으로 연산을 수행하고 그 결과를 반환하면서 종료된다. 반환된 값은 대입연산자에 의해서 변수 value에 저장된다. 메서드의 결과를 저장하기 위한 변수 value역시 반환값과 같은 타입이거나 반환값이 자동형변환되어 저장될 수 있는 타입이어야 한다.
[예제6-5] MyMathTest.java | class MyMathTest { public static void main(String args[]) { MyMath mm = new MyMath(); long result1 = mm.add(5L, 3L); long result2 = mm.subtract(5L, 3L); long result3 = mm.multiply(5L, 3L); double result4 = mm.divide(5L, 3L); // double대신 long값을 입력했다. System.out.println("add(5L, 3L) = " + result1); System.out.println("subtract(5L, 3L) = " + result2); System.out.println("multiply(5L, 3L) = " + result3); System.out.println("divide(5L, 3L) = " + result4); } }
class MyMath { long add(long a, long b) { long result = a+b; return result; // return a + b; // 위의 두 줄을 이와 같이 한 줄로 간단히 할 수 있다. }
long subtract(long a, long b) { return a - b; }
long multiply(long a, long b) { return a * b; }
double divide(double a, double b) { return a / b; } }
|
[실행결과] | add(5L, 3L) = 8 subtract(5L, 3L) = 2 multiply(5L, 3L) = 15 divide(5L, 3L) = 1.6666666666666667
|
사칙연산을 위한 4개의 메서드가 정의되어 있는 MyMath클래스를 이용한 예제이다. 이 예제를 통해서 클래스에 선언된 메서드를 어떻게 호출하는지 알 수 있을 것이다. 여기서 눈여겨봐야 할 곳은 divide(double a, double b)를 호출하는 부분이다. divide메서드에 선언된 매개변수 타입은 double형인데, 이와 다른 long형의 값인 5L과 3L을 사용해서 호출하는 것이 가능하였다.
| double result4 = mm.divide( 5L , 3L );
double divide(double a, double b) { return a / b; }
|
호출 시에 입력된 값은 메서드의 매개변수에 대입되는 값이므로, long형의 값을 double형 변수에 저장하는 것과 같아서 double a = 5L;을 수행 했을 때와 같이 long형의 값인 5L은 double형 값인 5.0으로 자동형변환되어 divide의 매개변수 a에 저장된다. 그래서, divide메서드에 두 개의 정수값(5L, 3L)을 입력하여 호출하였음에도 불구하고 연산결과가 double형의 값이 된다.
이와 마찬가지로 add(long a, long b)메서드에도 매개변수 a, b에 int형의 값을 넣어 add(5,3)과 같이 호출하는 것이 가능하다.
3.6 JVM의 메모리구조 응용프로그램이 실행되면, JVM은 시스템으로부터 프로그램을 수행하는데 필요한 메모리를 할당받고 JVM은 이 메모리를 용도에 따라 여러 영역으로 나누어 관리한다. 그 중 3가지 주요영역(Method Area, 호출스택, Heap)에 대해서 알아보도록 하자.
[참고] cv는 클래스변수, lv는 지역변수, iv는 인스턴스변수를 뜻한다.
1. 메소드영역(Method Area) - 프로그램 실행 중 어떤 클래스가 사용되면, JVM은 해당 클래스의 클래스파일(*.class)을 읽어서 분석하여 클래스에 대한 정보(클래스 데이타)를 Method Area에 저장한다. 이 때, 그 클래스의 클래스변수(class variable)도 이 영역에 함께 생성된다.
2. 힙(Heap) - 인스턴스가 생성되는 공간. 프로그램 실행 중 생성되는 인스턴스는 모두 이 곳에 생성된다. 즉, 인스턴스변수(instance variable)들이 생성되는 공간이다.
3. 호출스택(Call Stack 또는 Execution Stack) 호출스택은 메서드의 작업에 필요한 메모리 공간을 제공한다. 메서드가 호출되면, 호출스택에 호출된 메서드를 위한 메모리가 할당되며, 이 메모리는 메서드가 작업을 수행하는 동안 지역변수(매개변수 포함)들과 연산의 중간결과 등을 저장하는데 사용된다. 그리고, 메서드가 작업을 마치게 되면, 할당되었던 메모리공간은 반환되어 비워진다. 각 메서드를 위한 메모리상의 작업공간은 서로 구별되며, 첫 번째로 호출된 메서드를 위한 작업공간이 호출스택의 맨 밑에 마련되고, 첫 번째 메서드 수행중에 다른 메서드를 호출하게 되면, 첫 번째 메서드의 바로 위에 두 번째로 호출된 메서드를 위한 공간이 마련된다. 이 때 첫 번째 메서드는 수행을 멈추고, 두 번째 메서드가 수행되기 시작한다. 두 번째로 호출된 메서드가 수행을 마치게 되면, 두 번째 메서드를 위해 제공되었던 호출스택의 메모리공간이 반환되며, 첫 번째 메서드는 다시 수행을 계속하게 된다. 첫 번째 메서드가 수행을 마치면, 역시 제공되었던 메모리 공간이 호출스택에서 제거되며 호출스택은 완전히 비워지게 된다. 호출스택의 제일 상위에 위치하는 메서드가 현재 실행 중인 메서드이며, 나머지는 대기상태에 있게 된다. 따라서, 호출스택을 조사해 보면 메서드 간의 호출관계와 현재 수행중인 메서드가 어느 것인지 알 수 있다. 호출스택의 특징을 요약해보면 다음과 같다.
| - 언제나 호출스택의 제일 위에 있는 메서드가 현재 실행 중인 메서드이다. - 아래에 있는 메서드가 바로 위의 메서드를 호출한 메서드이다.
|
반환타입(return type)이 있는 메서드는 종료되면서 결과값을 자신을 호출한 메서드(caller)에게 반환한다. 대기상태에 있던 호출한 메서드(caller)는 넘겨받은 반환값으로 수행을 계속 진행하게 된다.
[예제6-6] CallStackTest.java | class CallStackTest { public static void main(String[] args) { firstMethod(); }
static void firstMethod() { secondMethod(); }
static void secondMethod() { System.out.println("secondMethod()"); } }
|
위의 예제를 실행시켰을 때, 프로그램이 수행되는 동안 호출스택의 변화를 그림과 함께 살펴보도록 하자
(1)~(2) 위의 예제를 컴파일한 후 실행시키면, JVM에 의해서 main메서드가 호출됨으로써 프로그램이 시작된다. 이때, 호출스택에는 main메서드를 위한 메모리공간이 할당되고 main메서드의 코드가 수행되기 시작한다. (3) main메서드에서 firstMethod()를 호출한 상태이다. 아직 main메서드가 끝난 것은 아니므로 main메서드는 호출스택에 대기상태로 남아있고 firstMethod()의 수행이 시작된다. (4) firstMethod()에서 다시 secondMethod()를 호출했다. firstMethod()는 secondMethod()가 수행을 마칠 때까지 대기상태에 있게 된다. secondMethod()가 수행을 마쳐야 firstMethod()의 나머지 문장들을 수행할 수 있기 때문이다. (5) secondMethod()에서 println메서드를 호출했다. 이때, println메서드에 의해서 화면에"secondMethod()"가 출력된다. (6) println메서드의 수행이 완료되어 호출스택에서 사라지고 자신을 호출한 secondMethod()로 되돌아간다. 대기 중이던 secondMethod()는 println메서드를 호출한 이후부터 수행을 재개한다. (7) secondMethod()에 더 이상 수행할 코드가 없으므로 종료되고, 자신을 호출한 firstMethod()로 돌아간다. (8) firstMethod()에도 더 이상 수행할 코드가 없으므로 종료되고, 자신을 호출한 main메서드로 돌아간다. (9) main메서드에도 더 이상 수행할 코드가 없으므로 종료되어, 호출스택은 완전히 비워지게 되고 프로그램은 종료된다.
[예제6-7] CallStackTest2.java | class CallStackTest2 { public static void main(String[] args) { System.out.println("main(String[] args)이 시작되었음."); firstMethod(); System.out.println("main(String[] args)이 끝났음."); } static void firstMethod() { System.out.println("firstMethod()이 시작되었음."); secondMethod(); System.out.println("firstMethod()이 끝났음."); }
static void secondMethod() { System.out.println("secondMethod()이 시작되었음."); System.out.println("secondMethod()이 끝났음."); }
}
|
[실행결과] | main(String[] args)이 시작되었음. firstMethod()이 시작되었음. secondMethod()이 시작되었음. secondMethod()이 끝났음. firstMethod()이 끝났음. main(String[] args)이 끝났음.
|
3.7 기본형 매개변수와 참조형 매개변수 자바에서는 메서드를 호출할 때 매개변수로 지정한 값을 메서드의 매개변수에 복사해서 넘겨준다. 매개변수의 타입이 기본형(Primitive type)일 때는 기본형 값이 복사되겠지만, 참조형(Reference type)이면 인스턴스의 주소가 복사된다. 메서드의 매개변수를 기본형으로 선언하면 단순히 저장된 값만 얻지만, 참조형으로 선언하면 값이 저장된 곳의 주소를 알 수 있기 때문에 값을 읽어 오는 것은 물론 값을 변경하는 것도 가능하다.
| 기본형 매개변수 - 변수의 값을 읽기만 할 수 있다.(read only) 참조형 매개변수 - 변수의 값을 읽고 변경할 수 있다.(read & write)
|
[예제6-8] ParameterTest.java | class Data { int x; } class ParameterTest { public static void main(String[] args) {
Data d = new Data(); d.x = 10; System.out.println("main() : x = " + d.x);
change(d.x); System.out.println("After change(d.x)"); System.out.println("main() : x = " + d.x); }
static void change(int x) { x = 1000; System.out.println("change() : x = " + x); } }
|
[실행결과] | main() : x = 10 change() : x = 1000 After change(d.x) main() : x = 10
|
[플래시동영상]플래시동영상 자료실에서 기본형 매개변수 vs. 참조형매개변수(PrimitiveParam.swfvs. ReferenceParam.swf)를 보면 실행과정과 함께 자세히 설명하고 있으니 여기서는 자세한 설명은 생략하겠다.
[예제6-9] ParameterTest2.java | class Data { int x; } class ParameterTest2 { public static void main(String[] args) {
Data d = new Data(); d.x = 10; System.out.println("main() : x = " + d.x);
change(d); System.out.println("After change(d)"); System.out.println("main() : x = " + d.x);
} static void change(Data d) { d.x = 1000; System.out.println("change() : x = " + d.x); } }
|
[실행결과] | main() : x = 10 change() : x = 1000 After change(d) main() : x = 1000
|
3.8 재귀호출(Recursive Call) 메서드의 내부에서 메서드 자기자신을 다시 호출하는 것을 재귀호출(Recursive Call)이라 한다. 반복적인 작업을 해야 하는 메서드에 재귀호출을 이용하면, 메서드를 훨씬 간단하게 할 수 있는 경우가 있다. 하지만, 재귀호출은 다소 효율이 떨어진다는 단점이 있다.
대표적인 재귀호출의 예는 팩토리얼(factorial)을 구하는 것이다. 팩토리얼은 한 숫자를 1이 될 때까지 1씩 감소시켜가면서 계속해서 곱해 나가는 것인데, n!(n은 양의 정수)와 같이 표현한다. 예를 들면, 5! = 5 * 4 * 3 * 2 * 1 = 120이다. 그리고, 팩토리얼을 수학적 함수로 표현하면 아래와 같이 표현할 수 있다.
| f(n) = n * f(n-1), 단 f(1) = 1.
|
다음 예제는 위의 함수를 자바로 구현한 것이다.
[예제6-10] FactorialTest.java | class FactorialTest { public static void main(String args[]) { System.out.println(factorial(4)); }
static long factorial(int n) { long result=0; if ( n == 1) { result = 1; } else { result = n * factorial(n-1); // 다시 메서드 자신을 호출한다. }
return result; } }
|
[참고] 삼항연산자 ? : 를 이용하면 factorial메서드를 아래와 같이 더욱 간략하게 표현할 수 있다.
| static long factorial(int n) { return (n == 1) ? 1 : n * factorial(n-1); }
|
위 예제는 팩토리얼을 계산하는 메서드를 구현하고 테스트하는 것이다. factorial메서드가 main메서드와 같은 클래스내의 static메서드이므로 인스턴스를 생성하지 않고 직접 호출할 수 있다. 메서드가 자기자신을 다시 호출을 함으로써 반복문을 사용하지 않고도 반복적인 작업을 수행하도록 할 수 있다. 재귀호출은 반복문이 무한반복에 빠지는 것 처럼 무한호출이 되기 쉬우므로 주의해야한다.
[예제6-11] MainTest.java | class MainTest { public static void main(String args[]) { main(null); // 자기자신을 다시 호출한다. } }
|
[실행결과] | java.lang.StackOverflowError at MainTest.main(MainTest.java:3) at MainTest.main(MainTest.java:3) ... at MainTest.main(MainTest.java:3) at MainTest.main(MainTest.java:3)
|
main메서드 역시 자기자신을 호출하는 것이 가능한데, 아무런 조건도 없이 계속해서 자기자신을 다시 호출하기 때문에 무한호출에 빠지게 된다. main메서드가 종료되지 않고 호출스택에 계속해서 쌓이게 되므로 결국 호출스택의 메모리 한계를 넘게 되고 StackOverflowError가 발생하여 프로그램은 비정상적으로 종료된다.
[예제6-12] PowerTest.java | class PowerTest { public static void main(String[] args) { int x = 2; int n = 5; long result = 0;
for(int i=1; i<=n; i++) { result += power(x, i); }
System.out.println(result); }
static long power(int x, int n) { if(n==1) return x; return x * power(x, n-1); } }
|
x의 1제곱부터 x의 n제곱까지의 합을 구하는 예제이다. 재귀호출을 사용하여 x의 n제곱을 구하는 power메서드를 작성하였다. x는 2, n은 5로 계산했기 때문에 21 + 22 + 23 + 24 + 25의 결과인 62가 출력되었다.
3.9 클래스메서드(static메서드)와 인스턴스메서드 변수에서 그랬던 것과 같이, 메서드 앞에 static이 붙어 있으면 클래스메서드이고 붙어 있지 않으면 인스턴스메서드이다. 클래스 메서드는 호출방법 역시 클래스변수처럼, 객체를 생성하지 않고도 '클래스이름.메서드이름(매개변수)'와 같은 식으로 호출이 가능하다. 그렇다면 어느 경우에 static을 사용해서 클래스메서드로 정의해야하는 것일까?
클래스는 '데이터(변수)와 데이터에 관련된 메서드의 집합'이라고 할 수 있다. 같은 클래스 내에 있는 메서드와 멤버변수는 아주 밀접한 관계가 있다. 인스턴스메서드는 인스턴스변수와 관련된 작업을 하는, 즉 메서드의 작업을 수행하는데 인스턴스변수를 필요로 하는 메서드이다. 그래서 인스턴스변수와 관계없거나(메서드 내에서 인스턴스변수를 사용하지 않거나), 클래스변수만을 사용하는 메서드들은 클래스메서드로 정의한다. 물론 인스턴스변수를 사용하지 않는다고 해서 반드시 클래스 메서드로 정의해야하는 것은 아니지만, 그렇게 하는 것이 일반적이다. 참고로 Math클래스의 모든 메서드는 클래스메서드임을 알 수 있다. Math클래스에는 인스턴스변수가 하나도 없거니와 Math클래스의 함수들은 작업을 수행하는데 필요한 값들을 모두 매개변수로 받아서 처리 하기 때문이다. 이처럼, 단순히 함수들만의 집합인 경우에는 클래스메서드로 선언한다.
[참고]인스턴스 변수 뿐만 아니라 인스턴스 메서드를 호출하는 경우에도 인스턴스 메서드로 선언되어야 한다. 인스턴스 메서드를 호출하는 것 역시 인스턴스 변수를 간접적으로 사용하는 것이기 때문이다.
[예제6-12] MyMathTest2.java | class MyMath2 { long a, b; // 인스턴스변수 a, b를 이용한 작업을 하므로 매개변수가 필요없다. long add() { return a + b; } long subtract() { return a - b; } long multiply() { return a * b; } double divide() { return a / b; }
// 인스턴스변수와 관계없이 매개변수만으로 작업이 가능하다. static long add(long a, long b) { return a + b; } static long subtract(long a, long b) { return a - b; } static long multiply(long a, long b) { return a * b; } static double divide(double a, double b) { return a / b; } }
class MyMathTest2 { public static void main(String args[]) { // 클래스메서드 호출 System.out.println(MyMath2.add(200L, 100L)); System.out.println(MyMath2.subtract(200L, 100L)); System.out.println(MyMath2.multiply(200L, 100L)); System.out.println(MyMath2.divide(200.0, 100.0));
MyMath2 mm = new MyMath2(); mm.a = 200L; mm.b = 100L; // 인스턴스메서드는 객체생성 후에만 호출이 가능함. System.out.println(mm.add()); System.out.println(mm.subtract()); System.out.println(mm.multiply()); System.out.println(mm.divide());
}
|
[실행결과] | 300 100 20000 2.0 300 100 20000 2.0
|
인스턴스메서드인 add(), subtract(), multiply(), divide()는 인스턴스변수인 a와 b만으로도 충분히 원하는 작업이 가능하기 때문에, 매개변수를 필요로 하지 않으므로 괄호()에 매개변수를 선언하지 않았다. 반면에 add(long a, long b), subtract(long a, long b) 등은 인스턴스변수 없이 매개변수만으로 작업을 수행하기 때문에 static을 붙여서 클래스메서드로 선언하였다. MyMathTest2의 main메서드에서 보면, 클래스메서드는 객체생성없이 바로 호출이 가능했고, 인스턴스메서드는 MyMath2클래스의 인스턴스를 생성한 후에야 호출이 가능했다. 이 예제를 통해서 어떤 경우 인스턴스메서드로, 또는 클래스메서드로 선언해야하는지, 그리고 그 차이를 이해하는 것은 매우 중요하다.
3.10 클래스멤버와 인스턴스멤버간의 참조와 호출. 같은 클래스에 속한 멤버들간에는 별도의 인스턴스를 생성하지 않고도 서로 참조 또는 호출이 가능하다. 단, 클래스멤버가 인스턴스멤버를 참조 또는 호출하고자 하는 경우에는 인스턴스를 생성해야 한다. 그 이유는 인스턴스멤버가 존재하는 시점에 클래스멤버는 항상 존재하지만, 클래스멤버가 존재하는 시점에 인스턴스멤버가 항상 존재한다는 것을 보장할 수 없기 때문이다.
[예제6-13] MemberCall.java | class MemberCall { int iv = 10; static int cv = 20;
int iv2 = cv; // static int cv2 = iv; 에러. 클래스변수는 인스턴스 변수를 사용할 수 없음. static int cv2 = new MemberCall().iv; // 굳이 사용하려면 이처럼 객체를 생성해야함.
static void classMethod1() { System.out.println(cv); // System.out.println(iv); 에러. 클래스메서드에서 인스턴스변수를 바로 사용할 수 없음. MemberCall c = new MemberCall(); System.out.println(c.iv); // 객체를 생성한 후에야 인스턴스변수의 참조가 가능함. }
void instanceMethod1() { System.out.println(cv); System.out.println(iv); // 인스턴스메서드에서는 인스턴스변수를 바로 사용가능. }
static void classMethod2() { classMethod1(); // instanceMethod1(); 에러. 클래스메서드에서는 인스턴스메서드를 바로 호출할 수 없음. MemberCall c = new MemberCall(); c.instanceMethod1(); // 인스턴스를 생성한 후에야 인스턴스메서드를 호출할 수 있음. } void instanceMethod2() { // 인스턴스메서드에서는 인스턴스메서드와 클래스메서드 classMethod1(); // 모두 인스턴스생성없이 바로 호출이 가능하다. instanceMethod1(); } } |
클래스멤버(클래스변수와 클래스메서드)는 언제나 참조 또는 호출이 가능하다. 그렇기 때문에 인스턴스멤버가 클래스멤버를 참조, 호출하는 것은 아무런 문제가 안 된다. 클래스멤버간의 참조 또는 호출 역시 아무런 문제가 없다.
그러나, 인스턴스멤버(인스턴스변수와 인스턴스메서드)는 반드시 객체를 생성한 후에만 참조 또는 호출이 가능하기 때문에 클래스멤버가 인스턴스멤버를 참조, 호출하기 위해서는 객체를 생성하여야 한다.
하지만, 인스턴스멤버간의 호출에는 아무런 문제가 없다. 하나의 인스턴스멤버가 존재한다는 것은 인스턴스가 이미 생성되어있다는 것을 의미하며, 즉 다른 인스턴스멤버들도 모두 존재하기 때문이다.
실제로는 같은 클래스 내에서 클래스멤버가 인스턴스멤버를 참조 또는 호출해야하는 경우는 드물다. 만일 그런 경우가 발생한다면, 인스턴스메서드로 작성해야할 메서드를 클래스메서드로 한 것은 아닌지 한번 더 생각해봐야 한다.
[알아두면 좋아요] | 수학에서의 대입법처럼, c = new MemberCall()이므로 c.instanceMethod1();에서 c대신 newMemberCall()을 대입하여 사용할 수 있다.
MemberCall c = new MemberCall(); int result = c.instanceMethod1();
위의 두 줄을 다음과 같이 한 줄로 할 수 있다.
int result = new MemberCall().instanceMethod1();
대신 참조변수를 사용하지 않았기 때문에 생성된 MemeberCall인스턴스는 더 이상 사용할 수 없다. |
|