Live Study 5주차 - 클래스

클래스와 객체

클래스에 앞서 객체 를 먼저 이해할 필요가 있다.

객체를 중심으로 프로그래밍을 하는 것을 객체지향 프로그래밍(Object Oriented Programming)이라 한다. 객체의 역할과 책임이 객체지향 프로그래밍의 중요한 부분이 된다.

세탁기를 객체로 비유하면, 세탁기는 빨래감을 받고, 실행명령이 들어오면 세탁을 시작하면 된다. 어떤 옷이 들어올지는 알필요가 없고, 빨래가 들어왔고, 실행버튼이 눌려졌다면 세탁을하고 끝나면 알림만 전달하면 된다. 건조기는 세탁기에서 나온 빨랫감을 받아서 실행명령이 들어오면 건조를 시작하면 된다. 그리고 건조가 끝나면 끝났음을 알려주기만 하면 된다. 건조기와 세탁기는 서로가 하는 일을 알 필요가 없다. 그저 자신에게 주어진 빨랫감을 세탁 또는 건조를 하고 끝나면 알림을 하기만 하면된다. 이런 세탁 과정에서 세탁기와 건조기는 객체라 할 수 있다.

1
2
3
4
5
6
public class StudyHalle {
public static void main(String[] args){
WashingMachine wash = new WashingMachine();
wash.turnOn(clothes);
}
}

클래스란 이런 객체를 생성하는 템플릿같은 개념이다. 클래스 WashingMachine을 사용하기위해서 인스턴스 wash를 생성했다.

객체를 정의하는 방법은 아래에서 정리했다.

클래스 정의하는 방법

1
public class 클래스명 { }

클래스는 위의 구조로 생성되는데, 이 때 클래스에서 사용할 수 있는 접근제한자는 publicdefault로만 사용가능하다.

접근제한자

접근 제한자 적용가능 대상 접근 가능 대상
public 클래스, 생성자, 메서드, 필드 모든 대상
protected 생성자, 메서드, 필드 같은 패키지 또는 상속받은 자식 클래스
default 클래스, 생성자, 메서드, 필드 같은 패키지에 소속된 클래스
private 생성자, 메서드, 필드 클래스 내부

출처 : Kephi Javatory - Java 접근제한자 public, protected, default, private


클래스명은 .java 파일명과 동일해야 컴파일 이후에 생성된 바이트코드를 JVM이 로딩할때 클래스를 찾을 수 있다.

ide를 사용하지 않고 터미널에서 Vim 에디터를 이용하여 클래스를 직접 생성해보았다.

1
vim JavaTest.java
1
2
3
4
5
public class JavaTest {
public static void main(String[] args){
System.out.println("Hello World!");
}
}
1
javac JavaTest.java

javac 는 자바 컴파일러이다. java 소스파일을 자바 컴파일러로 컴파일하라는 명령이기 때문에 이 명령어가 실행되면 디렉토리에 .class 파일이 생성된다.

클래스 이름은 파일이름과 반드시 같아야 한다. 그래야 JVM이 실행해야할 클래스를 찾을 수 있다. 그리고 클래스의 main()을 호출하게 된다.

1
2
java JavaTest
Hello World!

java 는 바이트코드를 JVM으로 실행하는 명령어이다. 이 명령어의 결과로 JavaTest가 실행되어 main()가 호출된 것이다. 실제로 JVM이 어떤 순서로 클래스를 실행하는지 아래의 코드로 살펴보겠다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class JavaTest {
public static void main(String[] args){
int num1 = 10;
int num2 = 20;

JavaTest test = new JavaTest();

System.out.println(test.add(num1, num2));
}

public int add(int num1, int num2){
return num1+num2;
}
}

1
2
javac JavaTest.java && java JavaTest
30

&&은 복수의 쉘 명령을 순차적으로 할 수 있도록 하는 키워드이다.

위의 바이트코드를 javap로 뜯어보면, JVM에서 클래스와 메서드를 어떤 순서로 실행하는지 알 수 있다.

1
2
3
4
5
6
7
javap JavaTest
compiled from "JavaTest.java"
public class JavaTest {
public JavaTest();
public static void main(java.lang.String[]);
public int add(int, int);
}

디폴트 생성자를 먼저 실행하고, 이후 main()을 실행, int형 인자 2개를 파라미터로 받는 add()가 호출된걸 알 수 있다.

생성자

생성자는 파라미터가 없는 기본 생성자도 있을 수 있고, 파라미터가 있는 생성자도 만들수 있다. 다만 개발자가 직접 생성하지 않는다면 자바 컴파일러에서 파라미터가 없는 기본생성자를 자동으로 생성해준다.

개발자가 임의로 생성자를 생성한다면, 자바 컴파일러는 기본 생성자를 생성하지 않는다.


객체 만드는 방법(new 키워드 이해하기)

클래스는 속성과 기능을 갖는다. 클래스를 바탕으로 객체를 생성하면 객체도 클래스의 속성과 기능을 갖게되는데, 이 때 객체가 갖는 속성과 기능을 멤버(member)라고 일컫는다.

1
2
// 클래스  인스턴스명 = new 생성자()
StudyHalle study = new StudyHalle();

여기서 StudyHalle는 클래스이고, study가 이 클래스를 바탕으로 생성한 인스턴스 객체에 해당한다.

클래스는 템플릿이고, 이 템플릿을 바탕으로 실제 사용할 목적으로 생성한 객체가 인스턴스라고 한다. 따라서 하나의 클래스로 여러개의 독립된 인스턴스를 만드는것도 가능하다. 인스턴스를 통해서 해당 객체의 메서드나 필드에 접근이 가능해진다.

클래스는 참조형 타입이므로 인스턴스 생성시 new 키워드로 생성한다. new 키워드로 생성한다는건 곧 JVM의 Heap 영역에 메모리를 할당한다는 이야기이다.

객체는 null을 참조할수도 있는데, Heap에 있는 객체가 null을 참조할경우, 가비지 컬렉터(GC)가 이 객체를 Heap에서 제거함으로써 메모리 누수를 방지해준다.

정리하면, 위의 코드는 StudyHalle 라는 클래스로부터 study 라는 이름의 인스턴스 객체를 생성하며, 생성자 StudyHalle()로 인스턴스를 초기화하는 작업을 거친 후 객체를 생성하는 코드이다.

아래 코드를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Super {
public void printMyName() {
System.out.println("Super.");
}
public void hello() {
System.out.println("Hello world.");
}
}
class Sub extends Super {
public void printMyName(){
System.out.println("Sub.");
}
}
public class StudyHalle {
public static void main(String[] args){
Super a1 = new Super();
Super a2 = new Sub();

Sub b1 = new Super(); // compile error
Sub b2 = new Sub();

a1.printMyName();
a2.printMyName();
}
}
1
2
Super.
Sub.

SuperSub는 Sub -> Super의 상속관계이다. class Super로부터 인스턴스 객체 a1, a2를 생성해서 print()를 호출했는데 다른 결과가 출력되었다.

이유는 Class Super로부터 인스턴스 객체를 생성하면서 서로 다른 생성자를 사용했기 때문이다. a1은 생성자 Super()를 초기화하면서 생성한 객체이고, a2는 생성자 Sub()를 초기화하면서 생성한 객체이다.

그렇다면 아래의 코드는 어떨까?

1
2
3
4
5
6
public class StudyHalle {
public static void main(String[] args){
Sub b1 = new Super(); // 컴파일 에러 발생
Sub b2 = new Sub();
}
}

Class Sub에서 Super() 생성자를 호출하는건 불가능했다. 왜 그럴까?

상속관계(Sub->Super)를 다시 생각해볼 필요가 있다.

1
Sub b1 = new Super();

Class Sub 타입의 변수를 선언하면서 JVM의 heap에 Class Super 생성자로 초기화하여 대입하는 코드이다.

컴파일 에러가 출력되는 이유는 부모클래스의 생성자를 자식 클래스 타입에 대입하는 것이 불가능하기 때문이다.

반대로 자식 클래스의 생성자는 부모/자식 클래스 타입에 모두 대입가능한데, 이유는 자식 클래스의 경우 super() 를 이용해서 부모 클래스에도 접근이 가능하기 때문일 것으로 추정된다(?) 혹시 잘못된 부분이 있다면, 댓글로 지적해주시면 감사하겠습니다.!!


메서드 정의하는 방법

1
[접근제한자] [반환타입] [메서드명]([파라미터]) { } 

메서드는 클래스 내부에서 선언할 수 있으며, 위의 문법으로 선언한다.

같은 클래스 또는 상속받은 객체에서만 접근가능하며, 파라미터를 받지않고 문자열 타입을 반환하는 메서드는 다음과 같이 생성할 수 있다.

1
2
3
protected String studyHalle(){
return "Hello World";
}

생성자 정의하는 방법

클래스를 작성할때, 특별히 생성자를 생성하지 않더라도 디폴트로 생성자가 생성되지만, 직접 생성자를 정의할 수도 있다.

위에서 ide를 사용하지 않고 java를 생성해서 컴파일하면서 JVM이 클래스를 읽어들이면서 가장 먼저 생성자를 읽어들인걸 확인했다. 생성자는 JVM이 클래스를 읽어들일때 먼저 읽어들이므로 생성자를 통해서 하나의 클래스를 바탕으로 여러 인스턴스를 생성할 수 있다. 아래 예제 코드를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class DevAndy {
private static final int birthYear = 1991;
public int age;

public DevAndy() {
LocalDate date = LocalDate.now();
this.age = date.getYear()-birthYear;
}

public DevAndy(String nation){
if(nation.equals("Korea")){
LocalDate date = LocalDate.now();
this.age = date.getYear()-birthYear+1;
} else {
LocalDate date = LocalDate.now();
this.age = date.getYear()-birthYear;
}
}
}

생성자는 클래스와 동일한 이름을 가져야 하며, 파라미터 갯수와 인자 타입 등만 다르게 둘 수 있다. 파라미터 갯수와 인자 타입을 다르게 하면, 생성자를 여러개로 생성 할 수도 있다. this에 대한 설명은 아래에서 정리했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
package basic02;

import basic01.DevAndy;

public class JavaBasic02 {
public static void main(String[] args){
DevAndy andy01 = new DevAndy();
DevAndy andy02 = new DevAndy("Korea");

System.out.println(andy01.getName()+"'s age is "+andy01.age);
System.out.println(andy02.getName()+"'s age is "+andy02.age);
}
}
1
2
DevAndy's age is 30
DevAndy's age is 31

인스턴스를 선언할때 파라미터를 넣으면 같은 클래스임에 필드값(age)이 다르게 가공되어 출력되는걸 알 수 있다.


this 키워드 이해하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class JavaBasic03 {
public int num = 20;

public int setNum(int num){
return this.num += num;
}
}

class JavaBasic04 {
public static void main(String[] args) {
JavaBasic03 basic03 = new JavaBasic03();
System.out.println(basic03.setNum(10));
}
}
1
30

JavaBasic03 클래스에 대한 인스턴스를 생성해서 setNum()을 호출하고 반환된 값을 출력하는 코드이다. 여기서 주의깊게 봐야할 부분은 JavaBasic03의 setNum()이다.

setNum()에서 다루는 변수는 this.num과 파라미터로 받는 num이다. 여기서 this.num은 클래스의 필드로 선언된 num이다. 리터럴로 20이 할당된 변수이다. 그냥 num은 파라미터로 들어온 변수로써 아직 리터럴을 알 수 없다.

this 키워드를 사용하여 접근하는 변수는 로컬변수가 아닌 클래스 변수 또는 인스턴스 변수이다.