본문 바로가기

Java 길찾기/이것이 자바다

[Java] 클래스의 구성 멤버 (2)

메소드

메소드는 객체의 동작에 해당하는 중괄호 { } 블록을 말한다. 중괄호 블록은 이름을 가지고 있는데, 이것이 메소드 이름이다. 메소드를 호출하게 되면 중괄호 블록에 있는 모든 코드들이 일괄적으로 실행된다. 메소드는 필드를 읽고 수정하는 역할도 있지만, 다른 객체를 생성해서 다양한 기능을 수행하기도 한다. 메소드는 객체 간의 데이터 전달의 수단으로 사용된다. 외부로부터 매개값을 받을 수도 있고, 실행 후 어떤 값을 리턴할 수도 있다.

 

메소드 선언

메소드 선언은 선언부(리턴타입, 메소드이름, 매개변수선언)와 실행블록으로 구성된다. 메소드 선언부를 메소드 시그너처(signature)라고도 한다.

 

리턴 타입

리턴 타입은 메소드가 실행 후 리턴하는 값의 타입을 말한다. 메소드는 리턴값이 있을 수도 있고 없을 수도 있다. 메소드가 실행 후 결과를 호출한 곳에 넘겨줄 경우에는 리턴값이 있어야 한다. 예를 들어 전자계산기 객체에서 전원을 켜는 powerOn() 메소드와 두 수를 나누는 기능인 divide() 메소드가 있다고 가정해보자. divide() 메소드는 나눗셈의 결과를 리턴해야 하지만 powerOn() 메소드는 전원만 켜면 그만이다. 따라서 powerOn() 메소드는 리턴값이 없고, divide() 메소드는 리턴값이 있다고 봐야 한다. 리턴값이 없는 메소드는 이턴 타입에 void가 와야 하며 리턴값이 있는 메소드는 리턴값의 타입이 와야 한다. divide() 메소드의 결과가 double 값이라면 double을 리턴 타입으로 사용해야 한다.

void powerOn() { ... }
double divide(int x, int y) { ... }

리턴값이 있느냐 없느냐에 따라 메소드를 호출하는 방법이 조금 다르다. 위의 메소드는 다음과 같이 호출할 수 있다.

powerOn();
double result = divide( 10, 20 );

powerOn() 메소드는 리턴값이 없기 때문에 변수에 저장할 내용이 없다. 단순히 메소드만 호출하면 된다. 그러나 divide() 메소드는 10을 20으로 나눈 후 0.5를 리턴하므로 이것을저장할 변수가 있어야 한다. 리턴값을 받기 위해 변수는 메소드의 리턴 타입인 double 타입으로 선언되어야 한다. 만약 result 변수를 int 타입으로 선언하게 되면 double 값을 저장할 수 없기 때문에 컴파일 에러가 발생한다.

int result = divide( 10, 20 ); // 컴파일 에러

리턴 타입이 있다고 해서 반드시 리턴값을 변수에 저장할 필요는 없다. 리턴값이 중요하지 않고, 메소드 실행이 중요할 경우에는 다음과 같이 변수 선언 없이 메소드를 호출할 수도 있다.

divie( 10, 20 );

 

메소드 이름

메소드 이름은 자바 식별자 규칙에 맞게 작성하면 되는데, 다음 사항에 주의하면 된다.

  • 숫자로 시작하면 안 되고, $와 _를 제외한 특수 문자를 사용하지 말아야 한다.
  • 관례적으로 메소드명은 소문자로 작성한다.
  • 서로 다른 단어가 혼합된 이름이라면 뒤이어 오는 단어의 첫머리 글자는 대문자로 작성한다.
void run() { ... }
void startEngine() { ... }
String getName() { ... }
int[] getScores() { ... }

메소드 이름은 이 메소드가 어떤 기능을 수행하는지 쉽게 알 수 있도록 기능 이름으로 지어주는 것이 좋다. 메소드명의 길이는 프로그램 실행과는 무관하니, 너무 짧게 주지 않도록 한다.

 

매개 변수 선언

매개 변수는 메소드가 실행할 때 필요한 데이터를 외부로부터 받기 위해 사용된다. 매개 변수고 필요한 경우가 있고 필요 없는 경우가 있다. 예를 들어 powerOn() 메소드는 그냥 전원만 켜면 그만이지만, divide() 메소드는 나눗셈할 두 수가 필요하다. 따라서 powerOn() 메소드는 매개 변수가 필요없고, divide() 메소드는 매개 변수가 두 개 필요하다. 다음은 매개 변수가 있는 divide() 메소드 선언 예를 보여준다.

double divide(int x, int y) { ... }

이렇게 선언된 divide() 메소드를 호출할 때에는 반드시 두 개의 int 값을 주어야 한다.

double result = divide(10, 20);

호출 시 넘겨준 매개값인 10과 20은 해당 위치의 매개변수인 x와  y에 각각 저장되고, 이 매개 변수들을 이용해서 메소드 블록을 실행하게 된다. 매개값은 반드시 매개 변수의 타입에 부합되는 값이어야 한다. divide() 메소드가 int 타입 매개 변수를 가지고 있다면 호출 시 매개값으로 int 값이나 int 타입으로 변환될 수 있는 값을 넘겨주어야 한다.

double result = divide( 10.5, 20.0);  // 컴파일 에러

10.5와 20.0은 double 값이므로 int 타입으로 변환될 수 없다. 하지만 다음 코드는 컴파일 오류가 생기지 않고 정상적으로 실행된다. 매개값의 타입과 매개 변수의 타입이 달라도 자동 타입 변환되기 때문에 컴파일 오류가 생기지 않는다.

byte b1 = 10;
byte b2 = 20;
double result = divide(b1, b2);

 

다음은 Calculator 클래스에서 여러 메소드를 선언한 것이다.

public class Calculator {
	// 메소드
	void powerOn() {
		System.out.println("전원을 켭니다.");
	}
	
	int plus(int x, int y) {
		int result = x + y;
		return result;
	}
	
	double divide(int x, int y) {
		double result = (double)x / (double)y;
		return result;
	}
	
	void powerOff() {
		System.out.println("전원을 끕니다.");
	}
}

 

외부 클래스에서 Calculator 클래스의 메소드를 호출하기 위해서는 다음 예제와 같이 3라인에서 Caculator 객체를 생성하고 참조 변수인 myCalc를 이용해야 한다. myCalc 변수에 도트 ( . )와 함께 메소드이름(매개값, ...) 형태로 호출하면 메소드 블록이 실행된다.

public class CalculatorExample {
	public static void main(String[] args) {
		Calculator myCalc = new Calculator();
		myCalc.powerOn();
		
		int result1 = myCalc.plus(5, 6);
		System.out.print("result1: " + result1);
		
		byte x = 10;
		byte y = 4;
		double result2 = myCalc.divide(x, y);
		System.out.println("result2 : " + result2);
		myCalc.powerOff();
	}
}

 

매개 변수의 수를 모를 경우

메소드의 매개 변수는 개수가 이미 정해져 있는 것이 일반적이지만, 경우에 따라서는 메소드를 선언할 때 매개 변수의 개수를 알 수 없는 경우가 있다. 예를 들어 여러 개의 수를 모두 합산하는 메소드를 선언해야 한다면, 몇 개의 매개 변수가 입력될지 알 수 없기 때문에 매개 변수의 개수를 결정할 수 없을 것이다. 해결책은 다음과 같이 매개 변수를 배열타입으로 선언하는 것이다.

int sum1(int[] values) { }

sum1() 메소드를 호출할 때 배열을 넘겨줌으로써 배열의 항목 값들을 모두 전달할 수 있다. 배열의 항목 수는 호출할 때 결정된다.

int[] values = { 1, 2, 3 };
int result = sum1(values);
int result = sum1(new int[] { 1, 2, 3, 4, 5 });

 

매개 변수를 배열 타입으로 선언하면, 메소드를 호출하기 전에 배열을 생성해야 하는 불편한 점이 있다. 그래서 배열을 생성하지 않고 값의 리스트만 넘겨주는 방법도 있다. 다음과 같이 sum2() 메소드의 매개 변수를 "···"를 사용해서 선언하게 되면, 메소드 호출 시 넘겨준 값의 수에 따라 자동으로 배열이 생성되고 매개값으로 사용된다.

int sum2(int ··· values) { }

 

"···"로 선언된 매개 변수의 값은 다음과 같이 메소드 호출 시 리스트로 나열해주면 된다.

int result = sum2(1, 2, 3);
int result = sum2(1, 2, 3, 4, 5);

"···"로 선언된 매개 변수는 배열 타입이므로 다음과 같이 배열을 직접 매개값으로 사용해도 좋다.

int[] values = { 1, 2, 3 };
int result = sum2(values);
int result = sum2(new int[] { 1, 2, 3, 4, 5 });

 

다음 예제는 매개 변수를 배열로 선언한 sum1()과 매개 변수를 "···"로 선언한 sum2()의 작성 방법을 보여준다. 둘 다 항목의 값을 모두 더해서 리턴한다.

public class Computer {
	int sum1(int[] values) {
		int sum = 0;
		for(int i = 0; i < values.length; i++) {
			sum += values[i];
		}
		return sum;
	}
	
	int sum2(int ... values) {
		int sum = 0;
		for(int i = 0; i < values.length; i++) {
			sum += values[i];
		}
		return sum;
	}
}

sum1()과 sum2() 메소드의 실행문들이 완전 일치하는 것을 볼 수 있다. 매개 변수의 선언 방법만 다를 뿐이지 매개 변수의 타입이 배열이므로 처리 내용이같으 수밖에 없다.

 

리턴(return)문

리턴값이 있는 메소드

메소드 선언에 리턴 타입이 있는 메소드는 반드시 리턴(return)문을 사용해서 리턴값을 지정해야 한다. 만약 return문이 없다면 컴파일 오류가 발생한다. return문이 실행되면 메소드는 즉시 종료된다.

return 리턴값;

return문의 리턴값은 리턴 타입이거나 리턴 타입으로 변환될 수 있어야 한다. 예를 들어 리턴 타입이 int인 plus() 메소드에서 byte, short, int 타입의 값이 리턴되어도 상관없다. byte와 short는 int로 자동 타입 변환되어 리턴되기 때문이다.

int plus(int x, int y) {
    int result = x + y;
    return result;
}

int plus(int x, int y) {
    byte result = (byte) (x + y);
    return result;
}

 

return문을 사용할 때 주의할 점은 return문 이후에 실행문이 오면 컴파일 오류가 발생한다. 왜냐하면 return문 이후의 실행문은 결코 실행되지 않기 때문이다. 따라서 다음은 잘못된 코드이다.

int plus(int x, int y) {
    int result = x + y;
    return result;
    System.out.println(result); // Unreachable code
}

 

하지만 다음과 같은 경우, 컴파일 에러가 발생하지 않는다. 2번은 return false; 다음에 있지만, if문의 조건식이 false가 될 경우 정상적으로 2번이 실행되기 때문에 2번은 에러를 발생시키지 않는다. if문의 조건식이 true가 되면 1번이 실행되고 return false;가 실행되어 메소드는 즉시 종료한다. 당연히 2번은 실행되지 않는다.

boolean isLeftGas() {
    if(gas ==0 ) {
        System.out.println("gas가 없습니다."); // 1번
        return false;
    }
    System.out.println("gas가 있습니다."); // 2번
    return true;
}

 

리턴값이 없는 메소드(void)

void로 선언된 리턴값이 없는 메소드에서도 return 문을 사용할 수 있다. 다음과 같이 return문을 사용하면 메소드 실행을 강제 종료 시킨다.

return;

다음은 gas 값이 0보다 클 경우 계속해서 while문을 실행하고, 0일 경우 return문을 실행해서 run() 메소드를 즉시 종료한다. while문이 한 번 루핑할 때마다 gas를 1씩 감소시키기 때문에 언젠가는 0이 되어 run() 메소드를 종료한다. 이 예제에서는 return문 대신 break문을 사용할 수도 있다.

void run() {
    while(true) {
        if(gas > 0) {
            System.out.println("달립니다.(gas잔량 : " + gas + ")");
            gas -= 1;
        }
        else {
            System.out.println("멈춥니다.(gas잔량 : " + gas + ")");
            return;
        }
    }
}

 

메소드 호출

메소드는 클래스 내외부의 호출에 의해 실행된다. 클래스 내부의 다른 메소드에서 호출할 경우에는 단순한 메소드 이름으로 호출하면 되지만, 클래스 외부에서 호출할 경우에는 우선 클래스로부터 객체를 생성한 뒤, 참조 변수를 이용해서 메소드를 호출해야 한다. 객체가 존재해야 메소드도 존재하기 때문이다.

 

메소드 호출 - 객체 내부에서 호출

클래스 내부에서 다른 메소드를 호출할 경우에는 다음과 같은 형태로 작성하면 된다. 메소드가 매개 변수를 가지고 있을 때에는 매개 변수의 타입과 수에 맞게 매개값을 제공한다.

메소드( 매개값, ... );

 

예를 들어 method2() 메소드에서 method1() 메소드를 호출하려면 다음과 같이 작성하면 된다. "호출"에서 method1("홍길동", 100)이 호출되면 매개값인 "홍길동"은 p1 매개 변수에 대입되고 100은 p2 매개 변수에 대입된다. 그리고 "실행"에서 p1과 p2 변수를 이용하게 된다.

주의해야 할 점은 변수 타입은 메소드 리턴 타입과 동일하거나, 타입이 변환이 될 수 있어야 한다. 예를 들어 int 타입은 double 타입으로 자동 변환되기 때문에 int 리턴값은 double 변수에 대입할 수 있다.

public class ClassName {
    int method1(int x, int y) {
        int result = x + y;
        return result;
    }
    
    void method2() {
        int result1 = method1(10, 20);  // result1 에는 30이 저장
        double result2 = method1(10, 20);  // result2 에는 30.0이 저장
    }
}
public class Calculator {
	int plus(int x, int y) {  // 1번 
		int result = x + y;
		return result;
	}
	
	double avg(int x, int y) {  // 2번
		double sum = plus(x, y); // 1번 호출
		double result = sum / 2;
		return result;
	}
	
	void excute() {
		double result = avg(7, 10);  // 2번 호출
		println("실행결과 : " + result);  // 3번 호출
	}
	
	void println(String message) {  // 3번 
		System.out.println(message);
	}
}

 

메소드 호출 - 객체 외부에서 호출

외부 클래스에서 메소드를 호출하려면 우선 다음과 같이 클래스로부터 객체를 생성해야 한다. 메소드는 객체에 소속된 멤버이므로 객체가 존재하지 않으면 메소드도 존재하지 않기 때문이다.

클래스 참조변수 = new 클래스(매개값, ...);

객체가 생성되었다면 참조 변수와 함께 도트 ( . ) 연산자를 사용해서 메소드를 호출할 수 있다. 도트( . ) 연산자는 객체 접근 연산자로 객체가 가지고 있는 필드나, 메소드에 접근할 때 사용된다.

참조변수.메소드( 매개값, ...);  // 리턴값이 없거나, 받지 않을 경우
타입 변수 = 참조변수.메소드( 매개값, ...);  // 리턴값이 있고, 받을 경우
public class Car {
    // 필드
	int speed;
	
	// 생성자
	
	// 메소드
	int getSpeed() {
		return speed;
	}
	
	void keyTurnOn() {
		System.out.println("키를 돌립니다.");
	}
	
	void run() {
		for(int i = 10; i <= 50; i+=10) {
			speed = i;
			System.out.println("달립니다. (시속 : " + speed + "km/h)");
		}
	}
}

public class CarExample {
	public static void main(String[] args) {
		Car myCar = new Car();
		myCar.keyTurnOn();
		myCar.run();
		int speed = myCar.getSpeed();
		System.out.println("현재 속도 : " + speed + "km/h");
	}
}

 

메소드 오버로딩

클래스 내에 같은 이름의 메소드를 여러 개 선언하는 것을 메소드 오버로딩이라고 한다. 오버로딩의 사전적 의미는 많이 싣는 것을 의미한다. 하나의 메소드 이름으로 여러 기능을 담는다 하여 붙여진 이름이라 생각할 수 있다. 메소드 오버로딩의 조건은 매개 변수의 타입, 개수, 순서 중 하나가 달라야 한다.

 

메소드 오버로딩이 필요한 이뉴는 매개값을 다양하게 받아 처리할 수 있도록 하기 위해서이다. 예를 들어 다음과 같이 plus() 메소드가 있다고 가정해보자.

int plus(int x, int y) {
    int result = x + y;
    return result;
}

plus() 메소드를 호출하기 위해서는 두 개의 int 매개값이 필요하다. 하지만 int 타입이 아니라 double 타입의 값을 것셈하기 위해서는 plus() 메소드를 호출할 수 없다. 해결방법은 매개 변수가 double 타입으로 선언된 Plus() 메소드를 하나 더 선언하는 것이다.

int plus(double x, double y) {
    double result = x + y;
    return result;
}

오버로딩된 메소드를 호출할 경우 JVM은 매개값의 타입을 보고 메소드를 선택한다. 그렇가면 다음 코드는 어떻게 될까? 컴파일 오류가 날까? 만일 실행이 된다면 어떤 메소드가 실행이될까?

int x = 10;
double y = 20.3;
plus(x, y);

첫 번째 매개 변수가 int 타입이고 두 번째 매개 변수가 double 타입인 plus() 메소드가 없기 때문에 컴파일 오류가 날 것 같지만, 사실은 plus(double x, double y) 메소드가 실행된다. JVM은 일차적으로 매개 변수 타입을 보지만, 매개 변수의 타입이 일치하지 않을 경우, 자동 타입 변환이 가능한지를 검사한다. 첫 번째 매개 변수인 int 타입은 double 타입으로 변환이 가능하므로 최종적으로 plus(double x, double y) 메소드가 선택된다.

 

메소드를 오버로딩할 때 주의할 점은 매개 변수의 타입과 개수, 순서가 똑같을 경우 매개 변수 이름만 바꾸는 것은 메소드 오버로딩이라고 볼 수 없다. 또한 리턴 타입만 다르고 매개 변수가 동일하다면 이것은 오버로딩이 아니다. 왜냐하면 리턴 타입은 JVM이 메소드를 선택할 때 아무런 도움을 주지 못하기 때문이다. 만약 아래와 같이 선언했다면 오버로딩이 아니기 때문에 컴파일 오류가 발생한다.

int divide(int x, int y) { ... }
double divide(int boonja, int boonmo) { ... }

메소드 오버로딩의 가장 대표적인 예는 System.out.println() 메소드이다. println() 메소드는 호출할 때 주어진 매개값의 타입에 따라서 오버로딩된 println() 메소드를 호출한다. 

 

다음 예제는 메소드 오버로딩의 예를 보여준다.

public class Calculator {
    // 정사각형의 넓이
    double areaRectangle(double width) {
        return width * width;
    }
    
    // 직사각형의 넓이
    double areaRectangle(double width, double height) {
        return width * height;
    }
}

'Java 길찾기 > 이것이 자바다' 카테고리의 다른 글

[Java] 정적 멤버와 static  (0) 2022.01.11
[Java] 인스턴스 멤버와 this  (0) 2022.01.10
[Java] 클래스의 구성 멤버 (1)  (0) 2022.01.06
[Java] 클래스  (0) 2022.01.05
[Java] 참조 타입 (3)  (0) 2022.01.04