객체지향 프로그래밍은 점프투자바에서도 다루었지만 중요한 내용이기 때문에 이미 알고 있는 지식도 다시 정리하기 위해 반복적으로 포스팅 하려고 한다.
절차적 언어가 주류를 이루던 시절 프로그램의 규모가 점점 커짐에 따라 사용자들의 요구가 빠르게 변화해가는 상황을 절차적 언어로는 극복하기 어렵다는 한계를 느끼게 되었다. 이러한 배경으로 객체지향언어가 탄생하게 되었다. 객체지향언어란란 실제 세계가 사물(객체)로 이루어져 있는 것처럼, 발생하는 모든 사건들의 속성, 기능 등을 파악하여 객체화하여 코드 간에 관계를 맺어줌으로써 보다 유기적으로 프로그램을 구성하게 할 수 있는 언어이다. 객체지향언어의 특징은 코드의 재사용성이 높고, 코드의 관리가 용이하고, 신뢰성 높은 프로그래밍을 가능하게 한다.
클래스는 붕어빵을 만드는 붕어틀과 같다. 만들고자 하는 객체의 특징들을 잘 정의해 놓고, 객체가 필요할 때마다 클래스를 통해 객체를 생성해서 손쉽게 사용할 수 있다. 객체를 생성하는 방법은 다음과 같고, 읽는 방법은 'Data 클래스의 인스턴스를 생성한 후, Data의 주소값을 참조변수 d1에 저장한다.'
Data d1 = new Data();
Data d2 = new Data();
d2 = d1; // d2이 가리키고 있던 인스턴스는 제거된다.
두 개의 객체를 생성한 후 한 객체에 저장되어있는 주소값을 다른 객체에 저장하게 되면 자신을 참조하고 있는 참조 변수가 하나도 없는 인스턴스는 더 이상 사용되어질 수 없으므로 JVM의 '가비지컬렉터(Garbage Collector)에 의해 메모리에서 제거된다.
각각의 인스턴스 d1과 d2는 서로 독립적으로 작용한다. d1 객체의 인스턴스 변수에 변화를 준다고 해서 d2의 인스턴수 변수가 변화하지 않는다.
객체 역시 배열로 다루는 것이 가능하다. 배열을 선언한다고 객체가 생성되는 것은 아니고, new 키워드를 이용해 배열의 각 element를 반복문 등을 이용해 객체를 생성해 줘야한다.
Data[] dataArr = new Data[3]; // Data 자료형 배열 생성
for (int i=0; i<dataArr.length; i++) {
dataArr[i] = new Data(); // 객체 생성
}
타입의 종류에는 기본형(primitive) 타입과 참조형(reference) 타입이 있다. primitive 타입 이외에 새로 추가하는 것을 reference 타입 혹은 사용자 정의(user-defined) 타입이라고 하는데 클래스에서 객체를 생성함에 따라 사용자가 직접 타입을 정의하여서 사용할 수 있기 때문이다.
클래스는 변수(데이터)뿐만 아니라 함수를 함께 정의하기 때문에 수많은 변수에 대한 조건들을 쉽게 반영할 수 있어서 편리하다.
변수는 선언 위치에 따라 두종류로 나뉜다. 클래스 영역에 선언하면 멤버변수, 그 이외의 변수를 지역변수라고 한다.
멤버 변수는 또 두가지로 나뉘는데 클래스가 메모리에 올라갈 때 생성되는 변수를 클래스 변수, 인스턴스가 생성될 때 생성되는 변수는 인스턴스 변수이고 차이는 static이 붙었느냐 안붙었느냐의 차이이다.
메서드는 함수와 거의 유사하지만 입력값, 출력값이 없어도 된다는 점이 다르다. 메서드를 잘 만들어놓으면 그 후로는 고민 없이 메서드를 호출만 하면 되므로 재사용성이 높다는 점이 장점이다. 그리고 코드를 중복해서 작성하지 않아도 된다.
메서드의 기본 원리는 메서드를 호출한 곳에서 인자를 매개변수에 복사해서 넘겨주고, 일련의 과정을 거쳐 리턴을 하면 리턴값이 호출된 곳으로 넘어간다.
메인 메서드는 프로그램이 실행될때 OS에 의해 자동으로 호출된다. main 메서드는 프로그램이 전체 흐름이 한눈에 들어올 정도로 단순화, 구조화 시키는 것이 좋으므로 메소드를 잘 이용해서 메인 메서드를 작성하는 것이 좋다. 처음에 프로그램을 설계할 때 빈 메서드를 먼저 생성해 놓고 하나하나 채워 나가는 것도 프로그램을 구조화하는 좋은 방법이다.
메서드의 구조는 헤더와 바디로 나뉘고, 헤더에는 반환 타입, 메서드 이름, 매개변수가 들어가고 바디에는 실행할 내용이 들어간다.
메서드를 생성할 때의 변수는 매개변수라고 하고, 메서드를 호출할 때 괄호 안에 지정해준 값은 인자(argument)라고 한다. 매개변수와 인자는 타입이 일치하거나 자동형변환이 가능해야 에러가 나지 않는다.
같은 클래스 내의 메서드끼리는 참조변수를 사용하지 않고도 호출이 가능하지만 static 메서드는 같은 클래스 내의 인스턴스 메서드를 호출할 수 없다.
public class Sample {
int add(int a, int b) {
return a+b;
}
int add2(int a, int b) {
return add(a,b) * 2;
}
static int add3(int a, int b) {
return add(a,b) *3;
}
}
인스턴스 메서드(add, add2)끼리는 호출이 가능하다. 왜냐하면 인스턴스 메서드는 객체가 생성되고 난 이후에 사용이 가능하기 때문에 인스턴스 메서드 하나를 사용할 수 있다는 의미는 즉, 객체가 생성되었다는 의미이기 때문에 모든 인스턴스 메서드를 사용할 수 있다는 의미가 된다. 반면 클래스 메서드에서는 인스턴스 메서드를 호출할 수가 없다. 그 이유는 클래스 메서드는 클래스가 메모리에 올라가자마자 사용할 수 있는 반면에 인스턴스 메서드는 객체를 생성해야 비로소 사용가능하기 때문에 클래스 메서드를 사용할 수가 없어진다.
메서드를 작성할 때 중요하게 생각해야 하는 것 중 하나가 유효성 검사이다. 유효성 검사란 메서드의 가능한 모든 경우의 수에 대해 고민하고 그에 대비한 코드를 작성하는 것을 뜻한다. 예를 들어 나누기 메서드를 만든다고 했을 때 분모가 0이 되면 안되므로 분모로 0을 입력받는 경우에 대비한 코드를 작성해야 좋은 코드라고 할 수 있다.
여기서도 항상 궁금했던 부분을 해결할 수 있었는데 어떤 부분이냐면 메서드를 생성할 때 if문 안에 return문을 생성하면 에러가 나는 부분이였다. 그 이유를 알게 되었는데 return문은 언제든지 반환이 되어야만 하는데 if문에 return문을 작성하면 if문이 거짓인 경우에는 return문을 반환하지 못하기 때문이다.
return은 하나의 값만 반환할 수 있다. 하지만 여러 값을 반환하는 효과를 볼 수도 있는데 반환타입을 배열로 하면 리턴값은 주소값을 가르키는 참조 변수하나지만 그 주소에는 여러 값들이 저장되어 있으므로 결과적으로는 여러 값을 리턴할 수도 있다.
응용 프로그램이 실행되면 JVM은 시스템으로부터 프로그램을 수행하는데 필요한 메모리를 할당받고 JVM은 이 메모리를 용도에 따라 여러 영역으로 나누어 관리한다.
메서드 영역(method area) - 프로그램 실행 중 어떤 클래스가 사용되면, JVM은 해당 클래스의 클래스파일(*.class)을 읽어서 분석하여 클래스에 대한 정보를 이곳에 저장한다. 클래스 변수 역시 메서드 영역에 저장된다.
힙(Heap) - 인스턴스, 인스턴스 변수가 생성되는 공간.
호출스택(call stack, execution stack) - 메서드가 작업을 수행하는 동안 지역변수(매개변수 포함)들과 연산의 중간결과 등을 저장하는데 사용된다. 메서드가 작업을 마치면 할당되었던 모든 공간은 반환되어 비워진다. 호출 스택에 제일 위에 있는 메소드가 현재 실행중인 메소드이다.
메서드는 기본형 타입의 변수 뿐만 아니라 참조형 변수도 매개변수로 받을 수 있다. 메서드의 매개변수가 기본형일 때는 호출된 곳에서 기본형 값이 매개변수로 복사되겠지만, 참조형일 때는 주소값이 복사된다. 이 차이를 두가지 예제로 비교해 보면,
class Data {int x;}
public class Sample {
public static void main(String[] args) {
Data d = new Data();
d.x = 10;
System.out.println(d.x); // 10
changed(d.x);
System.out.println(d.x); // 10
}
static void changed(int x) {
x = 1000;
System.out.println(x); // 1000
}
}
class Data {int x;}
public class Sample {
public static void main(String[] args) {
Data d = new Data();
d.x = 10;
System.out.println(d.x); //10
changed(d);
System.out.println(d.x); // 1000
}
static void changed(Data d) {
d.x = 1000;
System.out.println(d.x); // 1000
}
}
재귀호출에 대해서는 전에 피보나치 수열을 메서드로 만들면서도 잠깐 배웠다. 재귀호출은 메서드 내부에서 자기 자신을 호출하는 것을 뜻한다. 호출된 메서드는 '값에 의한 호출(call by value)'을 통해 원래 값이 아닌 복사된 값으로 작업하기 때문에 독립적인 작업수행이 가능하다. 아무 조건없이 자기 자신을 호출하면 반복문의 무한루프와 다를 바가 없기 때문에 재귀호출을 사용할 땐 조건문을 사용하는 것이 필수이다. 이번엔 피보나치 수열말고 팩토리얼을 메서드로 구현해 보자.
public class Sample {
static int factorial(int n) {
if (n == 1) return 1;
return n * factorial(n - 1);
}
public static void main(String[] args) {
System.out.println(factorial(4));
}
}
public class Sample {
static int factorial(int n) {
if (n <= 0 || n > 12) {
System.out.println("유효하지 않는 값!!");
return -1;
}
else if (n == 1) return 1;
return n * factorial(n - 1);
}
public static void main(String[] args) {
for (int i=0; i<13; i++)
System.out.printf("%2d! = %10d\n",i,factorial(i));
}
}
이 두가지 메서드를 어떤 경우에 써야할까? 인스턴스 메서드는 인스턴스 변수를 필요로 하는 작업을 해야 하는 경우 사용한다. 반면 인스턴스와 관계없는 작업을 할 때는 클래스 메서드를 사용한다. 인스턴스 변수를 사용하지 않는다고 해서 반드시 클래스 메서드를 사용해야 하는 것은 아니지만 특별한 이유가 없는 한 그렇게 하는 것이 일반적이다.
public class Sample {
int iv1 = 2;
static int cv1 = 1; // 클래스 변수에 인스턴스 변수 불러오면 에러!
int iv2 = cv1; // 인스턴스 변수에 클래스 변수 사용 가능!
static int cv2 = new Sample().iv1;
// 클래스 변수가 인스턴스 변수 불러오려면 객체 생성 후 가능
static void staticMethod() { // 클래스 메서드
System.out.println(cv1);
Sample sample = new Sample();
// 클래스 메서드에서는 객체를 생성해야만 인스턴스 변수 사용가능!
System.out.println(sample.iv1);
}
void instanceMethod() { // 인스턴스 메서드
System.out.println(cv1);
// 인스턴스 메서드에서는 클래스 변수, 인스턴수 변수 둘 다 사용가능!
System.out.println(iv1);
}
static void staticMethod2() { // 클래스 메서드
staticMethod();
// 클래스 메서드에서 클래스 메서드를 호출하는 것 가능
new Sample().instanceMethod();
// 인스턴스 메서드를 호출하려면 객체 생성해야함
}
void instanceMethod2() { // 인스턴스 메서드
staticMethod2();
instanceMethod();
System.out.println(iv1);
System.out.println(cv1);
// 인스턴스 메서드 내에서는 객체 생성없이 인스턴스 메서드, 변수 사용 가능!
}
}
기존의 메서드와 이름이 같지만 매개변수의 개수 또는 타입이 다른 메서드를 생성하는 것을 오버로딩이라고 한다. 오버로딩을 하는 이유는 println 메서드를 예로 들어 설명하면 print메서드 내에 매개변수의 타입으로는 int, String, char, long 등의 여러 타입들이 존재하는데 타입마다 이름을 달리하면 사용자입장에서도 하나하나 암기하기 힘들고 개발자 입장에서도 이름을 하나하나 붙여주기 힘들 것이다. 하나의 메서드 이름으로 다양한 타입을 모두 사용할 수 있다면 매우 편리할 것이다.
public class Sample {
public static void main(String[] args) {
String[] strArr = {"100","200","300"};
System.out.println(concatenate(",","1","2","3","4","5"));
// 1,2,3,4,5, 출력
System.out.println(concatenate("-",strArr));
// 100-200-300- 출력
}
static String concatenate(String delim, String... args) {
String result = "";
for (String str : args) {
result += str + delim;
}
return result;
}
}
생성자는 인스턴스가 생성될 때 호출되는 인스턴스 초기화 메서드이다. 생성자의 형태는 생성자의 이름은 클래스명과 똑같이 하고, 생성자는 리턴값이 없지만 void를 사용하지는 않는다. 생성자를 통해 인스턴스 변수의 값을 초기화하고 싶은 경우 초기화할 인스턴스 변수를 매개변수로 받는다.
class명() {}
class명(변수1,변수2) {
...
}
class Car {
String color;
String gearType;
int door;
Car() {
this("white","auto",4);
}
Car(Car c) {
color = c.color;
gearType = c.gearType;
door = c.door;
}
Car(String color, String gearType, int door) {
this.color = color;
this.gearType = gearType;
this.door = door;
}
}
public class Sample {
public static void main(String[] args) {
Car c1 = new Car();
Car c2 = new Car(c1);
System.out.println(c1.color + c1.gearType + c1.door);
System.out.println(c2.color + c2.gearType + c2.door);
c2.door = 100;
System.out.println(c1.color + c1.gearType + c1.door);
System.out.println(c2.color + c2.gearType + c2.door);
}
}
this는 참조변수로 인스턴스 자신을 가르키는 키워드이다. 이 키워드는 생성자에서 다른 생성자를 호출할 때 사용한다. 생성자의 이름으로 클래스이름 대신 this를 사용한다. 생성자를 호출할 때는 반드시 맨 앞줄에서 호출해야만 한다. 이 키워드를 사용하는 이유는 생성자의 매개변수로 인스턴수변수들의 초기값을 제공받는 경우가 많기 때문에 매개변수 명과 인스턴스변수 명이 일치하는 경우가 많다. 각각의 변수들의 이름을 달리하여 차별화하는 것보다는 this 키워드를 붙여 구별되도록 하는것이 더욱 의미가 있다.
가능하면 선언과 동시에 변수를 적절한 값으로 초기화 하는 것이 바람직하다. 멤버변수는 초기화를 하지 않아도 변수의 자료형에 맞는 기본값으로 자동적으로 초기화되기 때문에 선언만 하고 사용할 수 있지만, 지역변수는 사용전에 반드시 초기화를 해야만 한다.
class Sample{
int i; // 0으로 초기화
String j; // null로 초기화
void method() {
int i;
int j = i // 컴파일 에러!
}
}
초기화 방법에는 명시적 초기화, 초기화 블럭, 생성자를 사용하는 방법이 있으며 명시적 초기화는 말그대로 선언과 동시에 초기화하는 것이다. 초기화 과정이 복잡하다면 초기화 블럭이나 생성자를 사용해야 할 것이다.
초기화 블럭은 중괄호{}안에 초기화를 위한 일련의 과정을 적기만 하면 된다. 초기화 블럭도 클래스와 인스턴스로 나누어지는데 static의 유무로 구별한다. 클래스 초기화 블럭은 클래스가 메모리에 처음 로딩될 때 한번만 수행되며, 인스턴스 초기화 블럭은 객체가 생성될 때마다 수행된다.
인스턴스 변수의 초기화는 주로 생성자를 이용하고, 모든 생성자에서 공통적으로 수행되어야 하는 코드를 인스턴스 초기화 블럭에 넣는다. 생성자보다 인스턴스 초기화 블럭이 먼저 수행된다.
public class Sample {
static { // 클래스 초기화 블럭
System.out.println("static { }");
}
{ // 인스턴스 초기화 블럭
System.out.println("{ }");
}
public Sample() { // 생성자
System.out.println("생성자");
}
public static void main(String[] args) {
Sample sample = new Sample();
Sample sample2 = new Sample();
}
}
static { }
{ }
생성자
{ }
생성자
class Document {
static int count = 0;
String name;
Document() {
this("제목없음" + ++count);
}
Document(String name) {
this.name = name;
System.out.println("문서 " + this.name + " 가 생성되었습니다.");
}
}
public class Sample {
public static void main(String[] args) {
Document d1 = new Document();
Document d2 = new Document("java.txt");
Document d3 = new Document("python.txt");
Document d4 = new Document();
}
}
문서 제목없음1 가 생성되었습니다.
문서 java.txt 가 생성되었습니다.
문서 python.txt 가 생성되었습니다.
문서 제목없음2 가 생성되었습니다.