7. 메서드와 생성자(Method and Constructor)
1. 키워드
- 메서드(Method)
- 생성자(Constructor)
this
와this()
- 메서드 시그니처(Method Signature)와 메서드 오버로딩(Method Overloading)
- 재귀 호출(Recursive Call)
2. 메서드(Method)
- 자바에서 클래스는 멤버로 속성을 표현하는 필드와 기능을 표현하는 메서드를 가진다.
- 그중에서 메서드란 어떠한 특정 작업을 수행하기 위한 명령문의 집합이라고 할 수 있다.
1) 메서드의 사용 목적
- 클래스에서 메서드를 작성하여 사용하는 이유는 중복되는 코드의 반복적인 프로그래밍을 피할 수 있기 때문이다.
- 또한, 모듈화로 인해 코드의 가독성도 좋아진다.
- 그리고 프로그램에 문제가 발생하거나 기능의 변경이 필요할 때도 손쉽게 유지보수를 할 수 있게 된다.
Note
- 메서드를 작성할 때는 되도록 하나의 메서드가 하나의 기능만을 수행하도록 작성하는 것이 좋다.
2) 메서드 정의
- 클래스에서 메서드를 정의하는 방법은 일반 함수를 정의하는 방법과 크게 다르지 않다.
- 자바에서 메서드를 정의하는 방법은 다음과 같다.
1] 접근 제어자: 해당 메서드에 접근할 수 있는 범위를 명시한다.
2] 반환 타입: 메서드가 모든 작업을 마치고 반환하는 데이터의 타입을 명시한다.
3] 메서드 이름: 메서드를 호출하기 위한 이름을 명시한다.
4] 매개변수 목록: 메서드 호출 시에 전달되는 인수의 값을 저장할 변수들을 명시한다.
5] 구현부: 메서드의 고유 기능을 수행하는 명령문의 집합이다.
Note
- 메서드 시그니처(Method Signature)란 메서드의 선언부에 명시되는 매개변수 목록을 가리킨다.
- 만약 두 메서드가 매개변수의 개수와 타입, 그 순서까지 모두 같다면, 이 두 메서드의 시그니처는 같다고 할 수 있다.
- 다음 예제는
Car
클래스의accelerate()
메서드를 정의하는 예제이다.
class Car {
private int currentSpeed;
private int accelerationTime;
① public void accelerate(int speed, int second) {
System.out.println(
second + "초간 속도를 시속 " + speed + "(으)로 가속함!!"
);
}
}
- 위 예제에서 ①번 라인에서는
accelerate()
메서드를 정의하고 있다. - 이 메서드는
public
접근 제어자를 사용하여 선언되어 해당 객체를 사용하는 프로그램 어디에서나 직접 접근할 수 있다. - 반환 타입에는 어떠한 값도 반환하지 않는다는 의미를 가진
void
를 명시한다. - 그 다음으로 메서드의 이름을 명시하고, 매개변수로
int
형 변수인speed
와second
를 전달받는다. - 이렇게 전달받은 매개변수를 가지고 메서드 구현부에서 고유한 작업을 수행할 수 있는 것이다.
3) 메서드 호출
- 자바에서 위와 같은 방법으로 정의한 메서드는 멤버 참조 연산자(
.
)를 사용하여 호출할 수 있다.
- 자바에서 메서드를 호출하는 방법은 다음과 같다.
- 다음 예제는 앞서 정의한
accelerate()
메서드를 호출하는 예제이다.
- 다음 예제는 실제로
accelerate()
메서드를 정의하고 호출하는 예제이다.
class Car {
private int currentSpeed;
private int accelerationTime;
public void accelerate(int speed, int second) {
System.out.println(
second + "초간 속도를 시속 " + speed + "(으)로 가속함!!"
);
}
}
class Test {
public static void main(String[] args) {
Car myCar = new Car();
myCar.accelerate(60, 3);
}
}
// 3초간 속도를 시속 60(으)로 가속함!!
3. 생성자(Constructor)
1) 인스턴스 변수의 초기화
- 클래스를 가지고 객체를 생성하면, 해당 객체는 메모리에 즉시 생성된다.
- 하지만 이렇게 생성된 객체는 모든 인스턴스 변수가 아직 초기화되지 않은 상태이다.
- 자바에서 클래스 변수와 인스턴스 변수는 별도로 초기화하지 않으면, 다음 값으로 자동 초기화된다.
변수의 타입 | 초깃값 |
---|---|
char |
'\u0000' |
byte , short , int |
0 |
long |
0L |
float |
0.0F |
double |
0.0 또는 0.0D |
boolean |
false |
배열, 인스턴스 | null |
- 하지만 사용자가 원하는 값으로 인스턴스 변수를 초기화하려면, 일반적인 초기화 방식으로는 초기화할 수 없다.
- 인스턴스 변수 중에는
private
변수도 있으며, 이러한private
변수에는 사용자나 프로그램이 직접 접근할 수 없기 때문이다. - 따라서
private
인스턴스 변수에도 접근할 수 있는 초기화만을 위한public
메서드가 필요해진다. - 이러한 초기화만을 위한 메서드는 객체가 생성된 후부터 사용되기 전까지 반드시 인스턴스 변수의 초기화를 위해 호출되어야 한다.
2) 생성자
- 자바에서는 객체의 생성과 동시에 인스턴스 변수를 원하는 값으로 초기화할 수 있는 생성자라는 메서드를 제공한다.
- 자바에서 생성자의 이름은 해당 클래스의 이름과 같아야 한다.
- 즉,
Car
클래스의 생성자의 이름은Car
가 된다. - 이러한 생성자는 다음과 같은 특징을 가진다.
1] 생성자는 반환값이 없지만, 반환 타입을 void
형으로 선언하지 않는다.
2] 생성자는 초기화를 위한 데이터를 인수로 전달받을 수 있다.
3] 객체를 초기화하는 방법이 여러 개 존재할 경우에는 하나의 클래스가 여러 개의 생성자를 가질 수 있다. 즉, 생성자도 하나의 메서드이므로 메서드 오버로딩이 가능하다는 의미이다.
- 다음 예제는
Car
클래스를 선언하면서 여러 개의 생성자를 선언하는 예제이다.
Car(String modelName) {}
Car(String modelName, int modelYear) {}
Car(String modelName, int modelYear, String color) {}
Car(String modelName, int modelYear, String color, int maxSpeed) {}
3) 생성자의 선언
- 자바에서 클래스 생성자를 선언하는 문법은 다음과 같다.
- 위와 같이 생성자 중에는 매개변수를 전달받아 인스턴스 변수를 초기화하는 생성자도 선언할 수 있다.
- 다음 예제는 앞서 살펴본
Car
클래스의 생성자를 선언하는 예제이다.
Car(String modelName, int modelYear, String color, int maxSpeed) {
this.modelName = modelName;
this.modelYear = modelYear;
this.color = color;
this.maxSpeed = maxSpeed;
this.currentSpeed = 0;
}
Note
- 위의 예제처럼 클래스의 생성자는 어떠한 반환값도 명시하지 않아야 한다.
4) 생성자의 호출
- 자바에서는
new
키워드를 사용하여 객체를 생성할 때 자동으로 생성자가 호출된다.
class Car {
private String modelName;
private int modelYear;
private String color;
private int maxSpeed;
private int currentSpeed;
Car(String modelName, int modelYear, String color, int maxSpeed) {
this.modelName = modelName;
this.modelYear = modelYear;
this.color = color;
this.maxSpeed = maxSpeed;
this.currentSpeed = 0;
}
public String getModel() {
return this.modelYear + "년식 " + this.modelName + " " + this.color;
}
}
class Test {
public static void main(String[] args) {
Car myCar = new Car("아반떼", 2016, "흰색", 200);
System.out.println(myCar.getModel());
}
}
// 2016년식 아반떼 흰색
5) 기본 생성자(Default Constructor)
- 자바의 모든 클래스에는 하나 이상의 생성자가 정의되어 있어야 한다.
- 하지만 특별히 생성자를 정의하지 않고도 인스턴스를 생성할 수 있다.
- 이것은 자바 컴파일러가 기본 생성자라는 것을 기본적으로 제공해 주기 때문이다.
- 기본 생성자는 매개변수를 하나도 가지지 않으며, 아무런 명령어도 포함하고 있지 않다.
- 자바 컴파일러는 컴파일 시 클래스에 생성자가 하나도 정의되어 있지 않으며, 자동으로 다음과 같은 기본 생성자를 추가한다.
- 다음 예제는 자바 컴파일러가
Car
클래스에 자동으로 추가해 주는 기본 생성자의 예제이다.
- 위와 같이 기본 생성자는 어떠한 매개변수도 전달받지 않으며, 기본적으로 아무런 동작도 하지 않는다.
- 다음 예제는
Car
클래스에 생성자를 정의하지 않고, 기본 생성자를 호출하는 예제이다.
class Car {
private String modelName = "소나타";
private int modelYear = 2016;
private String color = "파란색";
public String getModel() {
return this.modelYear + "년식 " + this.color + " " + this.modelName;
}
}
class Test {
public static void main(String[] args) {
Car myCar = new Car();
System.out.println(myCar.getModel());
}
}
// 2016년식 파란색 소나타
- 위의 예제에서
Car
클래스의 인스턴스myCar
는 기본 생성자를 사용하여 생성된다. - 하지만 기본 생성자는 아무런 동작도 하지 않으므로, 인스턴스 변수를 클래스 필드에서 바로 초기화하고 있다.
- 이처럼 인스턴스 변수의 초기화는 생성자를 사용하여 수행할 수도 있지만, 클래스 필드에서 바로 수행할 수도 있다.
- 하지만 만약 매개변수를 가지는 생성자를 하나라도 정의했다면, 기본 생성자는 자동으로 추가되지 않는다.
- 따라서 매개변수를 가지는 생성자를 하나 이상 정의한 후 기본 생성자를 호출하면, 오류가 발생할 것이다.
class Car {
private String modelName;
private int modelYear;
private String color;
private int maxSpeed;
private int currentSpeed;
① Car(String modelName, int modelYear, String color, int maxSpeed) {
this.modelName = modelName;
this.modelYear = modelYear;
this.color = color;
this.maxSpeed = maxSpeed;
this.currentSpeed = 0;
}
public String getModel() {
return this.modelYear + "년식 " + this.modelName + " " + this.color;
}
}
class Test {
public static void main(String[] args) {
② Car myCar = new Car(); // 오류 발생
③ Car myCar = new Car("아반떼", 2016, "흰색", 200);
System.out.println(myCar.getModel());
}
}
// 2016년식 아반떼 흰색
- 위의 예제는 ①번 라인에서 4개의 매개변수를 갖는 생성자를 정의하고 있다.
- 따라서 자바 컴파일러는
Car
클래스에 별도의 기본 생성자를 추가하지 않을 것이다. - 이때 ②번 라인에서는 기본 생성자
Car()
를 호출하여 인스턴스를 생성하려고 하면, 자바 컴파일러는 오류를 발생시킬 것이다. - ③번 라인과 같이 4개의 매개변수를 전달해야만 인스턴스가 생성될 것이다.
4. this
와 this()
1) this
참조 변수
this
참조 변수는 인스턴스가 바로 자기 자신을 참조하는 데 사용하는 변수이다.- 이러한
this
참조 변수는 해당 인스턴스의 주소를 가리키고 있다.
- 다음 예제는
Car
클래스의 생성자를 나타낸 예제이다.
class Car {
private String modelName;
private int modelYear;
private String color;
private int maxSpeed;
private int currentSpeed;
Car(String modelName, int modelYear, String color, int maxSpeed) {
this.modelName = modelName;
this.modelYear = modelYear;
this.color = color;
this.maxSpeed = maxSpeed;
this.currentSpeed = 0;
}
...
}
- 위의 예제처럼 생성자의 매개변수 이름과 인스턴스 변수의 이름이 같을 경우에는 인스턴스 변수 앞에
this
키워드를 붙여 구분해야만 한다. - 이렇게 자바에서는
this
참조 변수를 사용하여 인스턴스 변수에 접근할 수 있다. - 이러한
this
참조 변수를 사용할 수 있는 영역은 인스턴스 메서드뿐이며, 클래스 메서드에서는 사용할 수 없다. - 모든 인스턴스 메서드에는
this
참조 변수가 숨겨진 지역 변수로 존재하고 있다.
2) this()
메서드
this()
메서드는 생성자 내부에서만 사용할 수 있으며, 같은 클래스의 다른 생성자를 호출할 때 사용한다.this()
메서드에 인수를 전달하면, 생성자 중에서 메서드 시그니처가 일치하는 다른 생성자를 찾아 호출해 준다.
- 다음 예제는
this
참조 변수와this()
메서드를 사용한 예제이다.
class Car {
private String modelName;
private int modelYear;
private String color;
private int maxSpeed;
private int currentSpeed;
Car(String modelName, int modelYear, String color, int maxSpeed) {
this.modelName = modelName;
this.modelYear = modelYear;
this.color = color;
this.maxSpeed = maxSpeed;
this.currentSpeed = 0;
} // 주석 처리하면 this() 메서드에 해당하는 생성자가 존재하지 않게 되어 오류 발생
Car() {
// 첫 번째 라인에서만 다른 생성자를 호출할 수 있다.
this("소나타", 2012, "검정색", 160);
}
public String getModel() {
return this.modelYear + "년식 " + this.modelName + " " + this.color;
}
}
class Test {
public static void main(String[] args) {
Car myCar = new Car();
System.out.println(myCar.getModel());
}
}
// 2012년식 소나타 검정색
- 위의 예제에서 매개변수를 가지는 첫 번째 생성자는
this
참조 변수를 사용하여 인스턴스 변수에 접근하고 있다. - 또한, 매개변수를 가지지 않는 두 번째 생성자는 내부에서
this()
메서드를 이용하여 첫 번째 생성자를 호출한다. - 이렇게 내부적으로 다른 생성자를 호출하여 인스턴스 변수를 초기화할 수 있다.
Note
- 단, 한 생성자
B
에서 다른 생성자A
를 호출할 때에는 반드시B
생성자의 첫 번째 라인에서만 호출할 수 있다. - 만약
B
생성자의 첫 번째 라인에 다른 코드가 존재한다면 오류가 발생할 것이다.
5. 메서드 오버로딩(Method Overloading)
1) 메서드 시그니처(Method Signature)
- 메서드 오버로딩의 핵심은 바로 메서드 시그니처에 있다.
- 메서드 시그니처란 메서드의 선언부에 명시되는 매개변수의 목록을 가리킨다.
- 만약 두 메서드가 매개변수의 개수와 타입, 그 순서까지 모두 같다면, 이 두 메서드의 시그니처는 같다고 할 수 있다.
2) 메서드 오버로딩
- 메서드 오버로딩이란 같은 이름의 메서드를 중복하여 정의하는 것을 의미한다.
- 자바에서는 원래 한 클래스 내에 같은 이름의 메서드를 둘 이상 가질 수 없다.
- 하지만 매개변수의 개수나 타입을 다르게 하면, 하나의 이름으로 메서드를 작성할 수 있다.
- 즉, 메서드 오버로딩은 서로 다른 시그니처를 갖는 여러 메서드를 같은 이름을 정의하는 것이라고 할 수 있다.
- 이러한 메서드 오버로딩을 사용함으로써 메서드에 사용되는 이름을 절약할 수 있다.
- 또한, 메서드를 호출할 때 전달해야 할 매개변수의 타입이나 개수에 대해 크게 신경을 쓰지 않고 호출할 수 있게 된다.
- 메서드 오버로딩은 OOP의 특징 중 하나인 다형성(Polymorphism)을 구현하는 방법 중 하나이다.
- 메서드 오버로딩의 대표적인 예로는
println()
메서드를 들 수 있다.
println()
메서드는 전달받는 매개변수의 타입에 따라 다음과 같이 다양한 원형 중에서 적절한 원형을 호출하게 된다.
1. println()
2. println(boolean x)
3. println(char x)
4. println(char[] x)
5. println(double x)
6. println(float x)
7. println(int x)
8. println(long x)
9. println(Object x)
10. println(String x)
3) 메서드 오버로딩의 조건
- 자바에서 메서드 오버로딩이 성립하기 위해서는 다음과 같은 조건을 만족해야 한다.
1] 메서드의 이름이 같아야 한다.
2] 메서드의 시그니처, 즉 매개변수의 개수 또는 타입이 달라야 한다.
- 메서드 오버로딩은 반환 타입과는 관계가 없다.
- 만약 메서드의 시그니처는 같은데 반환 타입만이 다른 경우에는 오버로딩이 성립하지 않는다.
4) 메서드 오버로딩의 예제
- 자바 컴파일러는 사용자가 오버로딩된 함수를 호출하면, 전달된 매개변수의 개수와 타입과 같은 시그니처를 가지는 메서드를 찾아 호출한다.
- 다음 예제는 함수의 오버로딩을 이용하여 정의한
display()
메서드의 원형 예제이다.
1. void display(int num1)
2. void display(int num1, int num2)
3. void display(int num1, double num2)
- 이제 사용자가
display()
메서드를 호출하면, 컴파일러는 자동으로 같은 시그니처를 가지는 메서드를 찾아 호출한다.
1. display(10); // 1번 display() 메서드 호출 -> 10
2. display(10, 20 // 2번 display() 메서드 호출 -> 200
3. display(10, 3.14 // 3번 display() 메서드 호출 -> 13.14
4. display(10, 'a'); // 2번과 3번 모두 호출 가능
- 문제는 4번과 같이 첫 번째 인수로는 정수를, 두 번째 인수로는 실수를 전달받는 호출이다.
- 자바에서는
char
형 데이터는int
형뿐만 아니라double
형으로도 타입 변환될 수 있기 때문이다. - 따라서 이와 같은 호출은 자바 컴파일러가 어느 시그니처의
display()
메서드를 호출해야 할지 불명확하다. - 자바에서는 오버로딩한 메서드의 이러한 모호한 호출을 허용하지 않으며, 위와 같은 경우에는 더 작은 표현 범위를 가지는
int
형으로 자동 타입 변환하게 된다.
- 다음 예제는 위에서 살펴본
display()
메서드를 다양한 시그니처로 오버로딩하는 예제이다.
class OverloadingTest {
public void display(int num1) {
System.out.println(num1);
}
① public void display(int num1, int num2) {
System.out.println(num1 * num2);
}
public void display(int num1, double num2) {
System.out.println(num1 + num2);
}
}
class Test {
public static void main(String[] args) {
OverloadingTest myfunc = new OverloadingTest();
myfunc.display(10);
myfunc.display(10, 20);
myfunc.display(10, 3.14);
② myfunc.display(10, 'a');
}
}
// 10
// 200
// 13.14
// 970
- 위의 예제처럼 메서드의 오버로딩은 매개변수의 타입뿐만 아니라 매개변수의 개수를 달리해도 작성할 수 있다.
- 위 예제의 ②번 라인의
display()
메서드 호출은 영문 소문자'a'
의 아스키 코드값이97
이므로,int
형으로 자동 타입 변환되어 ①번 라인의display()
메서드가 호출된 것이다.
6. 재귀 호출(Recursive Call)
- 재귀 호출이란 메서드 내부에서 해당 메서드가 또다시 호출되는 것을 의미한다.
- 이러한 재귀 호출은 자기가 자신을 계속해서 호출하므로, 끝없이 반복될 것이다.
- 따라서 메서드 내에 재귀 호출을 중단하도록 조건이 변경될 명령문을 반드시 포함해야 한다.
1) 재귀 호출의 개념
- 재귀 호출의 개념을 파악하기 위해서 우선 재귀 호출을 사용하지 않고
1
부터n
까지의 합을 구하는 메서드를 만들어보자.
class Test {
static int sum(int n) {
int result = 0;
for (int i = 1; i <= n; i++) {
result += i;
}
return result;
}
public static void main(String[] args) {
System.out.println(sum(4));
}
}
// 10
- 위의 예제에서
sum()
메서드는 재귀 호출을 사용하지 않고 만든 메서드이다. - 이러한 메서드는 그냥 봐서는 그 목적을 바로 알 수 없으며, 코드를 해석해야 무슨 목적으로 만든 메서드인지 알 수 있다.
- 즉, 변수
i
와result
는 왜 정의되었으며,for
문은 왜 사용되었는지 바로 알 수가 없다. - 이제 재귀 호출을 사용하여
1
부터n
까지의 합을 구하는recursiveSum()
메서드를 만들어보자. - 우선
1
부터4
까지의 합을 구하는 알고리즘을 생각해 보자.
1] 1
부터 4
까지의 합은 1
부터 3
까지의 합에 4
를 더하면 된다.
2] 1
부터 3
까지의 합은 1
부터 2
까지의 합에 3
을 더하면 된다.
3] 1
부터 2
까지의 합은 1
부터 1
까지의 합에 2
를 더하면 된다.
4] 1
부터 1
까지의 합은 그냥 1
이다.
- 위의 알고리즘을 의사 코드(Pseudo Code)로 작성하면 다음과 같다.
- 위와 같이 논리적인 재귀 알고리즘을 구상하고 의사 코드를 작성하면, 재귀 호출을 이용해 바로 코드로 옮길 수 있다.
class Test {
static int recursiveSum(int n) {
if (n == 1) {
return 1;
}
return n + recursiveSum(n - 1);
}
public static void main(String[] args) {
System.out.println(recursiveSum(4));
}
}
// 10
- 위의 예제에서
if
문이 존재하지 않으면, 이 프로그램은 실행 직후 스택 오버플로우(Stack Overflow)에 의해 종료될 것이다. - 따라서
if
문처럼 재귀 호출을 중단하기 위한 조건문을 반드시 포함해야 한다. - 이처럼 재귀 호출은 다양한 알고리즘을 표현한 의사 코드를 그대로 코드로 옮길 수 있게 해주므로, 직관적인 프로그래밍을 하는 데 도움이 된다.
Note
- 스택 오버플로우는 메모리 구조 중 스택 영역에서 해당 프로그램이 사용할 수 있는 메모리 공간 이상을 사용하려고 할 때 발생한다.