[기본 개념] 1 | (2.5) 참조형 반환타입, 재귀호출

728x90

[기본 개념] 1 | (2.5) 참조형 반환타입, 재귀호출

1 선언 위치에 따른 변수의 종류

2 클래스변수와 인스턴스변수

3 메서드

4 메서드의 선언과 구현

5 메서드의 호출

6 return문

7 JVM의 메모리구조

8 기본형 매개변수와 참조형 매개변수

9> 참조형 반환타입

10> 재귀호출

11 클래스 메서드와 인스턴스 메서드

12 클래스 멤버와 인스턴스 멤버간의 호출

9. 참조형 반환타입

 매개변수뿐만 아니라 참조형도 반환타입이 될 수 있다. 모든 참조형 타입의 값은 '객체의 주소'이므로 정수값이 반환되는 것일 뿐 특별한 것은 없다.

 

예제/ReferenceReturnEx.java

class Data { int x; }

class ReferenceReturnEx {
    public static void main(String[] args) {
        Data d = new Data();
        d.x = 10;

        Data d2 = copy(d);
        System.out.println("d.x = " + d.x);
        System.out.println("d2.x = " + d2.x);
    }

    static Data copy(Data d) {
        Data tmp = new Data();
        tmp.x = d.x;

        return tmp;
    }
}
실행결과
  
d.x = 10
d2.x = 10

 

 copy메서드는 새로운 객체를 생성한 다음에, 매개변수로 넘겨받은 객체에 저장된 값을 복사해서 반환한다. 반환하는 값이 Data객체의 주소이므로 반환 타입이 'Data'인 것이다.

 

        static Data copy(Data d) {

            Data tmp = new Data( ) ;    // 새로운 객체 tmp를 생성한다.

            tmp.x = d.x ;                    // d.x의 값을 tmp.x에 복사한다.

            

            return tmp ;   // 복사한 객체의 주소를 반환한다.

        }

 

 이 메서드의 반환타입이 'Data'이므로, 호출결과를 저장하는 변수의 타입 역시 'Data'타입의 참조변수여야 한다. copy메서드가 호출된 직후부터 종료까지의 과정을 단계별로 살펴보면 다음과 같다.

 

1. copy메서드를 호출하면서 참조변수 d의 값이 매개변수 d에 복사된다.

2. 새로운 객체를 생성한 다음, d.x에 저장된 값을 tmp.x에 복사한다.

3. copy메서드가 종료되면서 반환한 tmp의 값은 참조변수 d2에 저장된다.

4. copy메서드가 종료되어 tmp가 사라졌지만, d2로 새로운 객체를 다룰 수 있다.

 

반환타입이 '참조형'이라는 것은 메서드가 '객체의 주소'를 반환한다는 것을 의미한다.

 

10. 재귀호출(recursive call)

 메서드의 내부에서 메서드 자신을 다시 호출하는 것을 '재귀호출(recursive call)'이라 하고, 재귀호출을 하는 메서드를 '재귀 메서드'라 한다.

 

        void method( ) {

            method( ) ;     // 재귀호출. 메서드 자신을 호출한다.

        }

 

 호출된 메서드는 '값에 의한 호출'을 통해, 원래의 값이 아닌 복사된 값으로 작업하기 때문에 호출한 메서드와 관계없이 독립적인 작업수행이 가능하다.

 그런데 오로지 재귀호출뿐이면, 무한히 자기 자신을 호출하기 때문에 무한반복에 빠지게 된다. 따라서 재귀호출은 조건문이 필수적으로 따라다닌다.

 

        void method(int n) {

            if(n == 0)

                return;         //  n의 값이 0일 때, 메서드를 종료한다.

            System.out.println(n) ;

 

            method(--n) ;    // 재귀호출. method(int n)을 호출    

 

 이 코드는 매개변수 n을 1씩 감소시켜가면서 재귀호출을 하다가 n의 값이 0이 되면 재취호출을 중단하게 된다.

 

 재귀호출은 반복문과 유사한 점이 많으며, 대부분 재귀호출을 반복문으로 작성하는 것이 가능하다. 반복문은 같은 문장을 반복수행 하는 것이지만, 메서드를 호출하는것은 매개변수 복사와 종료 후 복귀할 주소저장 등 몇가지 과정이 더 필요하기 때문에 재귀호출의 수행시간이 더 길다.

 

 이럼에도 재귀호출을 쓰는 이유는 논리적 간결함 때문이다. 몇 겹의 반복문과 조건문을 단순한 구조로 바꿀 수 있다. 그러나 재귀호출은 비효율적이므로 재귀호출에 드는 비용보다 재귀호출의 간결함이 충분히 큰 경우에만 사용해야 한다.

 

 대표적인 재귀호출의 예는 팩토리얼(factorial)을 구하는 것이다. 팩토리얼이란 한 숫자가 1씩 감소하여 1이 될 때까지 계속 곱해 나가는 것이다. 예를 들면, '5! = 5 * 4 * 3 * 2 * 1 = 120'이다.

 

예제/FactorialTest.java

public class FactorialTest {
    public static void main(String[] args) {
        int result = factorial(4);

        System.out.println(result);
    }

    static int factorial(int n) {
        int result = 0;

        if (n == 1)
            result = 1;
        else
            result = n * factorial(n - 1);   // 다시 메서드 자신을 호출한다.
        return result;
    }
}

 

 위 예제는 팩토리얼을 계산하는 메서드를 재귀호출로 구현한 것이다. factorial메서드가 static메서드이므로 인스턴스를 생성하지 않고 직접 호출할 수 있다. 그리고 main메서드와 같은 클래스에 있기 때문에 static메서드를 호출할 때 클래스이름을 생략하는 것이 가능하다. 그래서 'FactorialTest.factorial(4)'대신 'factorial(4)'와 같이 했다.

 

간단히 하면 다음과 같이 쓸 수 있다.

            

            static int factorial(int n) {

                if (n == 1) return 1 ;

                return n * factorial(n - 1) ;

            }

 

 factorial(2)를 호출했을 때의 실행 과정을 살펴보자. 매개변수 값이 1이 아니므로 조건식이 거짓이 되어 다음 문장인 'return 2 * factorial(1) ;'이 수행되고, 이 식을 계산하는 과정에서 다시 factorial(1)이 호출된다.

 

1. factorial(2)를 호출하면서 매개변수 n에 2가 복사된다.

2. 'return 2 * factorial(1) ;'을  계산하려면 factorial(1)을 호출한 결과가 필요하다. 그래서 factorial(1)이 호출되고 매개변수 n에 1이 복사된다.

3. if문의 조건식이 참이므로 1을 반환하면서 메서드는 종료된다. 그리고 factorial(1)을 호출한 곳으로 되돌아간다.

4. 이제 factorial(1)의 결과값인 1을 얻었으므로, return문이 다음의 과정으로 계산된다.

        return 2 * factorial(1) ;

    → return 2 * 1 ;

    → return 2 ;

 

 하지만 n의 값이 0이거나, 100,000과 같이 큰 수이면 문제가 생긴다. 매개변수 n의 값이 0이면 if문의 조건식이 절대 참이 될 수 없기 때문에 계속해서 재귀호출만 일어나고 메서드가 종료되지 않으므로 '스택오버플로우 에러(Stack Overflow Error)'가 발생한다. n의 값이 100,00과 같이 큰 경우에도 마찬가지이다.

 

 이처럼 어떤 값이 들어와도 에러 없이 처리되는 견고한 코드를 작성해야 한다. 그래서 '매개변수의 유효성 검사'가 중요한 것이다.

 

 n의 최댓값을 반환타입인 int의 최댓값에 맞게 12로 정해 코드를 다시 작성하면 다음과 같다.

 


   int factorial (int n) {

       if (n == 1) return 1 ;

       return n * factorial (n - 1) ;
   }

----->
   int factorial (int n) {

       int result = 1 ;
       while (n != 0)
           result *= n-- ;
       return result ;
   }

 

예제/FactorialTest2.java

public class FactorialTest2 {
    static long factorial (int n) {
        if (n <= 0 || n > 20) return  -1;   // 매개변수 유효성 검사.
        if (n <= 1) return  1;
            return n * factorial (n - 1);
    }

    public static void main(String[] args) {
        int n = 21;
        long result = 0;

        for (int i = 1; i <= n; i++) {
            result = factorial(i);

            if (result == -1) {
                System.out.printf("유효하지 않은 값입니다. (0<n<=20) : %d%n", n);
                break;
            }

            System.out.printf("%2d!=%20d%n", i, result);
        }
    }  // main의 끝
}
실행결과

1!=                    1
2!=                    2
3!=                    6
4!=                   24
5!=                  120
6!=                  720
7!=                 5040
8!=                40320
9!=               362880
10!=             3628800
11!=            39916800
12!=           479001600
13!=          6227020800
14!=         87178291200
15!=       1307674368000
16!=      20922789888000
17!=     355687428096000
18!=    6402373705728000
19!=  121645100408832000
20!= 2432902008176640000

 

 

 

출처 | Java의 정석 (남궁 성)

728x90