클래스와 객체 — 설계도와 실물
Article
책 세 권의 제목과 가격을 다뤄야 한다. 배열 두 개면 될 것 같다.
String[] titles = { "자바", "스프링", "코틀린" };
int[] prices = { 20000, 30000, 25000 };
System.out.println(titles[1] + " / " + prices[1]); // 스프링 / 30000
돌아간다. 그런데 가운데 책을 빼야 한다면?
titles 에서 하나 지우고, prices 에서도 같은 번호를 지워야 한다. 하나만 지우면 이때부터 제목과 가격이 어긋난다. 아무 에러도 없이, 조용히.
책 한 권이 두 배열에 흩어져 있는 게 문제다. 제목과 가격은 원래 한 덩어리였다.
클래스는 설계도다
한 덩어리로 묶는다. 그 덩어리의 모양을 적어둔 것이 클래스다.
class Book {
String title;
int price;
}
클래스 안에 선언한 변수를 필드라고 부른다. Book 이라는 것에는 제목과 가격이 있다는 선언이다.
이건 설계도일 뿐이다. 아직 책 한 권도 없다. 실물을 만들려면 new 를 쓴다.
Book b = new Book();
b.title = "자바";
b.price = 20000;
System.out.println(b.title + " / " + b.price);
- 자바 / 20000
new Book() 으로 만들어진 실물을 객체라고 한다. 설계도 하나로 객체는 얼마든지 찍어낼 수 있다.
- Bookb 상자에 담을 타입
- new Book()실물을 만든다. 저 멀리 자리를 잡는다
- =만들어진 곳의 주소를 b 에 넣는다
Book 은 대문자로 시작한다. 4강에서 대문자로 시작하면 참조형이라고 했다. 우리가 만든 클래스도 예외가 아니다.
객체를 출력하면 주소가 나온다
9강에서 배열을 출력했더니 이상한 글자가 나왔다. 객체도 마찬가지다.
Book b = new Book();
System.out.println(b);
- Book@681a9515
Book@681a9515. 9강의 [I@3af49f1c 와 같은 생김새다. b 라는 상자에는 주소가 들어 있다.
상자에는 주소가, 실물은 저쪽에
- b
- 0x681a9515
- → Book { title: null, price: 0 }
title 이 null 이고 price 가 0 인 것도 눈여겨보자. 값을 넣은 적이 없는데 들어 있다.
필드는 배열의 칸처럼 기본값으로 채워진다. 참조형은 null, 기본형은 0. 3강의 지역 변수와 다르다. 지역 변수는 값을 안 넣으면 컴파일러가 막았다.
생성자 — 만들 때 값을 넣는다
new Book() 한 뒤에 한 줄씩 채우는 건 번거롭다. 빠뜨리기도 쉽다.
만드는 순간에 값을 받자. 그 일을 하는 것이 생성자다.
class Book {
String title;
int price;
Book(String title, int price) {
this.title = title;
this.price = price;
}
}
Book b = new Book("자바", 20000);
생성자는 메서드처럼 생겼지만 두 가지가 다르다.
| 메서드 | 생성자 | |
|---|---|---|
| 이름 | 마음대로 | 클래스 이름과 똑같이 |
| 반환 타입 | int, void 등 반드시 적는다 | 아예 적지 않는다 |
| 부르는 법 | b.print() | new Book(...) |
| 언제 도나 | 부를 때마다 | 객체를 만들 때 한 번 |
void 도 적지 않는다. 적으면 생성자가 아니라 그냥 메서드가 되어버린다. 자바 입문자가 한 번씩 겪는 함정이다.
1강의 그 수상한 줄
1강에서 javap -c 로 바이트코드를 들여다봤을 때 이런 게 있었다.
- public class Hello {
- public Hello();
- Code:
- 0: aload_0
- 1: invokespecial #1
- 4: return
- ...
우리는 Hello() 라는 걸 쓴 적이 없었다. 그때 "javac 가 몰래 하나 만들어 넣었다"고만 하고 넘어갔다.
그게 생성자다. 클래스에 생성자를 하나도 안 적으면, 컴파일러가 매개변수 없는 빈 생성자를 대신 넣어준다. 이걸 기본 생성자라고 한다.
new Book() 이 그냥 됐던 이유가 이것이다. 우리가 안 만들었는데 있었다.
그런데 생성자를 하나라도 직접 만들면 기본 생성자는 사라진다.
class Book {
String title;
int price;
Book(String title, int price) { ... }
}
Book b = new Book(); // 이제 안 된다
- error: constructor Book in class Book cannot be applied to given types;
- required: String,int
- found: no arguments
"String, int 가 필요한데 아무것도 안 줬다"고 한다.
컴파일러는 우리가 아무 생각이 없을 때만 도와준다. 직접 만들기 시작하면 손을 뗀다. 둘 다 쓰고 싶으면 둘 다 적으면 된다. 10강의 오버로딩이다.
Book() { }
Book(String title, int price) { ... }
this — 지금 이 객체
생성자 안의 this.title = title; 이 이상해 보인다. 왜 this 를 붙였을까.
빼보면 안다.
Book(String title, int price) {
title = title;
price = price;
}
Book b = new Book("자바", 20000);
System.out.println(b.title);
- null
컴파일도 되고 실행도 되는데 값이 안 들어갔다.
매개변수 이름과 필드 이름이 똑같다. 이럴 때 자바는 가까운 쪽을 고른다. 매개변수다. 그러니 title = title 은 매개변수를 자기 자신에게 넣는, 아무 의미 없는 문장이었다.
this 는 지금 만들어지고 있는 이 객체를 가리킨다. this.title 이라고 적으면 "매개변수 말고 필드"라는 뜻이 된다.
메서드가 필드를 쓴다
이제 클래스 안에 메서드를 넣어보자. 10강과 달리 static 이 없다.
class Book {
String title;
int price;
Book(String title, int price) {
this.title = title;
this.price = price;
}
void print() {
System.out.println(title + " / " + price);
}
int discounted(int percent) {
return price * (100 - percent) / 100;
}
}
Book b = new Book("자바", 20000);
b.print();
System.out.println(b.discounted(10));
- 자바 / 20000
- 18000
print() 안에서 title 을 그냥 썼다. this.title 을 줄여 쓴 것이다. 겹치는 이름이 없으면 this 를 생략해도 된다.
이게 객체지향의 핵심이다. 값과 그 값을 다루는 코드가 한 덩어리에 들어 있다. b.print() 는 "b 라는 책아, 네 정보를 찍어라"로 읽힌다.
discounted 에서 price * (100 - percent) / 100 이 정수 나눗셈이라는 것도 눈여겨보자. 5강에서 본 그대로다. 20000 * 90 / 100 은 딱 떨어져서 다행이지만, 순서를 price / 100 * (100 - percent) 로 바꾸면 값이 어긋날 수 있다.
객체를 비교하면
책 두 권을 똑같이 만들어 비교해보자.
Book c = new Book("자바", 20000);
Book d = new Book("자바", 20000);
System.out.println(c == d);
System.out.println(c.equals(d));
- false
- false
== 가 false 인 건 이제 놀랍지 않다. new 를 두 번 했으니 자리가 둘이고 주소가 다르다.
그런데 .equals() 도 false 다. 9강에서 배열도 그랬다.
우리는 Book 에 .equals() 를 만든 적이 없다. 그런데 부를 수 있다. 어디서 왔을까. 그리고 왜 String 은 내용을 비교하는데 우리 Book 은 주소를 비교할까.
14강에서 답한다. 6강과 9강에서 두 번 미뤄둔 그 질문이다. 이제 세 번째다.
null 인 객체를 건드리면
Book b = null;
b.print();
- Exception in thread "main" java.lang.NullPointerException
4강에서 본 그 예외다. b 상자에는 주소가 없는데 따라가라고 시켰다.
객체를 다루기 시작하면 null 을 만날 자리가 훨씬 많아진다. 필드의 기본값도 null 이니, 생성자에서 채우지 않은 참조형 필드는 전부 지뢰다.
정리하면
클래스는 설계도, 객체는 실물. 값과 그 값을 다루는 코드를 한 덩어리로 묶는다.
- 01
클래스에 필드를 모은다
흩어진 배열 대신 한 덩어리로. 제목과 가격이 어긋날 수 없다.
class Book { String title; int price; } - 02
new로 객체를 만들면 주소를 돌려준다출력하면 Book@681a9515 처럼 주소가 나온다. 필드는 기본값으로 채워진다.
- 03
생성자는 이름이 클래스와 같고 반환 타입이 없다
안 만들면 컴파일러가 기본 생성자를 넣어준다. 하나라도 만들면 넣어주지 않는다.
Book(String title, int price) { this.title = title; this.price = price; } - 04
this는 지금 이 객체다이름이 겹치면 가까운 쪽(매개변수)이 이긴다. this 를 빼면 조용히 null 이 남는다.
- 05
메서드는 필드를 그냥 쓴다
static 이 없다. 객체 하나하나가 자기 값을 갖고 있기 때문이다.
그런데 지금 Book 에는 구멍이 있다. 누구나 이렇게 할 수 있다.
Book b = new Book("자바", 20000);
b.price = -50000;
가격이 마이너스인 책이 생겼다. 생성자를 아무리 잘 만들어놔도, 만든 뒤에 밖에서 마음대로 바꿀 수 있다면 소용이 없다.
다음 강의에서 필드에 자물쇠를 채운다. 2강에서 미뤄둔 public 이 드디어 나온다.