이번에 배울 내용은 객체 지향 프로그래밍에서 가장 핵심이 되는 내용입니다. 객체 지향 프로그래밍은 보고 이해하는 것보다는 많은 경험을 쌓으면서 직접 필요성을 느끼는 게 중요합니다. 이해가 안되는 부분은 댓글로 달아주시면 그 부분을 게시글에 보충 설명하도록 하겠습니다.

객체(object)

클래스를 배우기 전에 객체(object)가 대충 무엇인지는 알아둘 필요가 있습니다. 여기서 object는 사전적 의미 그대로 '물건, 물체'를 의미합니다. 실생활에서 예를 들면, 나 또한 객체가 될 수 있고 그 주위에 있는 키보드, 마우스, 모니터, 책, 지갑, 달력 등 모든 것이 객체가 될 수 있습니다. 이해를 돕기 위해서 이 중 자동차를 골라 자세히 살펴보도록 하겠습니다. 먼저 자동차의 속성들을 살펴보면 색상, 크기, 모델, 연식 등을 뽑을 수 있습니다. 그리고 자동차로 할 수 있는 행동으로는 속도를 올리거나, 속도를 줄이거나 등이 있습니다. 이를 정리해보면 이렇습니다.

여기서 자동차는 하나의 객체(object)라고 할 수 있으며, 색상, 크기, 모델, 속도 등과 같이 객체가 지니고 있는 특성 또는 속성은 상태(state)를 의미하고, 속도를 올린다, 속도를 줄인다, 경적을 울린다 등은 객체가 할 수 있는 행동(behavior)을 말합니다. 정리하자면, 객체는 상태와 행동으로 이루어져 있다고 할 수 있습니다. 아래와 같이 또다른 예들을 생각해볼 수 있습니다.

우리가 이처럼 객체의 수많은 특성 중에서 덜 중요한 부분은 뒤로 빼고 핵심이 되는 특성을 간추려 내는 작업을 추상화(abstraction)라고 말합니다. 이 추상화는 객체 지향 프로그래밍의 4대 특성 중 하나로서 객체 모델링이라고 부르기도 합니다. 추상화로 얻게 되는 장점에 대해서는 직접 추상화를 여러 번 해보면서 차차 알아나가도록 하겠습니다.

여기서 설명한 객체라는 개념은 컴퓨터 프로그래밍에도 적용이 되는데, 객체의 상태는 프로그래밍에선 필드(field) 또는 속성(property)이라고 부르고 행동은 메서드(method)라고 부릅니다. 우리는 메서드를 '어떤 작업을 수행하는 문장들을 묶어놓은 것'이라고 배웠지만 큰 틀에서 살펴보면 메서드는 객체의 행동이라고 할 수 있습니다.

클래스(class)

그렇다면 클래스는 무엇일까요? 클래스란 바로 어떤 객체를 만들기 위해서 필드나 메서드를 정의하는 일종의 틀 또는 설계도라고 할 수 있습니다. 클래스는 실체가 있는 게 아니라 추상적인 개념에 불과하므로 처음에 받아들이기가 다소 힘들 수 있습니다. 객체와 클래스의 이해를 돕기 위해서 단골로 등장하는 비유가 하나 있는데 한번 살펴보도록 하겠습니다.

클래스를 인스턴스화한다 = 객체를 생성한다

위 그림처럼 붕어빵틀을 설계도인 클래스에 빗대고, 붕어빵틀로 만들어진 결과물인 붕어빵을 객체에 빗댈 수 있습니다. 붕어빵틀은 저마다 다른 크기, 모양의 붕어빵을 만들며, 하나의 붕어빵틀을 가지고 들어가는 재료(속성) 등에 따라서 각기 다른 고유한 붕어빵이 만들어집니다. 그리고 우리는 붕어빵틀로 붕어빵을 찍어내기 전까지는 붕어빵을 먹을 수 없습니다. 클래스도 이와 비슷하게 클래스 내부에 선언 또는 정의되어 있는 필드나 메서드를 이용하려면 먼저 클래스를 통해 객체를 만들어야 합니다.

객체와 인스턴스

대부분의 자바 개발자들은 일상적인 대화에서 객체(object)와 인스턴스(instance)를 혼용해서 사용합니다. 그렇게 해도 의사소통에 문제가 없으며 서로 무엇을 가리키는지 알기 때문입니다. 혼란을 피하기 위해서 앞으로 두 용어를 혼용하지 않고 객체로 통일할 것입니다. 하지만 자바같은 클래스 기반 언어에서 객체와 인스턴스의 사이에는 미세한 차이가 있습니다.

이해를 돕기 위해서 예를 들어봅시다. 우리가 집을 만든다고 가정할 때, 집을 만드려면 각 실의 위치나 넓이 등을 결정하기 위해서 먼저 설계도가 필요합니다. 설계도가 완성된 후, 해당 설계도를 기반으로 만들어진 똑같은 집들은 모두 객체라고 부를 수 있습니다. 하지만 이렇게 같은 설계도로부터 만들어진 집은 모두 똑같기에 어떤 집 하나를 가리키려 할 때 어려움이 생깁니다. 따라서, 번호와 같은 고유한 표시를 부여하여 각 집을 구분했습니다. 이때 각각의 집을 인스턴스라 부를 수 있습니다. 동일하게, 철수와 영희는 모두 사람 객체이지만 철수와 영희를 따로 봤을 때는 고유한 인스턴스라 할 수 있습니다. 느낌이 좀 오시나요?

클래스를 선언하기

클래스의 기본 구성을 살펴보면 다음과 같습니다. 클래스를 선언할 때는 여는 중괄호({)로 시작하여 닫는 중괄호(})로 닫을 수 있습니다. 클래스 내부에는 보통 필드, 생성자, 메서드가 들어갑니다. 생성자에 대해서는 나중에 자세히 알아보도록 하고, 메서드는 이곳에서 다시 복습할 수 있습니다. 여기에서는 필드를 집중적으로 살펴보도록 하겠습니다.

class 클래스명 {
	// 필드
    int 필드명;
    
    // 생성자
    클래스명() {
    	// ...
    }
    
    // 메서드
    void 메서드명() {
    	// ...
    }
}

클래스명 작성 규칙

클래스명 작성 규칙은 변수명 작성 규칙과 동일합니다. 클래스명을 지을 때는 작성 규칙에 벗어나지 않는 틀에서 마음대로 지어도 되지만, 되도록이면 개발자들 사이에서 일반적으로 통용되는 작명 관습을 따르시기 바랍니다.

클래스 작명 관습

클래스 이름은 명사여야 하며, 각 단어의 첫글자는 대문자여야 합니다. 클래스명은 단순하고 그 클래스를 잘 설명할 수 있어야 합니다. URL이나 HTML와 같이 축약형이 훨씬 더 널리 쓰이는 경우가 아니라면, 두문자어나 약어를 피하고 완전한 단어를 사용하세요. 예를 들면 Customer, WikiPage, AddressParser 등이 있습니다.

필드(field)

필드는 위에서 설명했듯이 객체의 상태를 나타냅니다. 필드는 데이터 멤버, 멤버 변수, 인스턴스 변수, 속성으로 부르기도 합니다. 앞으로는 용어를 필드로 통일하여 설명합니다. 덧붙여서, 멤버는 클래스를 구성하는 필드와 메서드를 말합니다. 클래스에 필드를 선언하려면 클래스 안에 변수를 선언하듯이 아래와 같이 작성하시면 됩니다.

타입 필드명;

그리고 필드에 초기값을 주고 싶을 때는 아래와 같이 입력할 수 있습니다.

타입 필드명 = 초기값;

개발자가 따로 초기값을 지정하지 않았을 때는 필드의 값이 기본값으로 설정됩니다. 기본값은 필드의 타입에 따라 달라집니다.

그리고 클래스 내부에서 필드에 접근할 때는 필드명으로 접근하시면 됩니다.

class Car {
    private int speed;
    private int maxSpeed;

	// 생성자
    public Car()
    {
    	speed = 0;
        maxSpeed = 100;
    }
    
    // 메서드
    public int getSpeed()
    {
    	return speed;
    }   
}

반대로 클래스 외부에서 필드에 접근할 때는 아래와 같이 멤버 접근 연산자(.)를 사용해야 합니다. 이것은 필드 뿐만 아니라 메서드도 마찬가지입니다. 물론 클래스 자체는 설계도에 불과하므로, 객체를 생성하고 난 후에야 멤버 접근 연산자로 멤버에 접근할 수 있습니다.

...
Car car = new Car();
...
System.out.println(car.speed);
car.speedUp(10);
...

예제 살펴보기

그럼 이제 클래스를 한번 만들어보도록 하겠습니다. 아래의 Car 클래스에는 자동차의 현재 속도와 최대 속도를 나타내는 필드가 있고, 속도를 올리고 내리는 행동이 있습니다.

class Car {
    private int speed = 0; // 현재 속도를 나타내는 필드
    private int maxSpeed = 100; // 최대 속도를 나타내는 필드

    // 차량의 현재 속도를 가져온다.
    public int getSpeed()
    {
    	return speed;
    }

    // 차량의 최대 속도를 가져온다.
    public int getMaxSpeed()
    {
        return maxSpeed;
    }

    // 속도를 올린다.
    public void speedUp(int increment)
    {
        if (speed + increment > maxSpeed){
            System.out.println("최대 속도 " + maxSpeed + "km/h를 넘어설 수 없습니다.");
        } else {
            speed += increment;
        }
    }

    // 속도를 내린다.
    public void speedDown(int decrement)
    {
        if(speed - decrement < 0) {
            System.out.println("속도는 0 아래로 떨어질 수 없습니다.");
        } else {
            speed -= decrement;
        }
    }
}

하지만 이대로 컴파일 후 실행을 한다면 '메인 클래스를 찾거나 로드할 수 없습니다.'나 'Car 클래스에서 메인 메서드를 찾을 수 없습니다. 다음 형식으로 메인 메서드를 정의하십시오.'와 같은 에러를 볼 수 있습니다. 전에도 말했듯이, 메인 메서드는 프로그램의 진입점으로 메인 메서드가 없다면 프로그램을 시작할 수 없습니다.

따라서 메인 메서드를 정의해야 하는데, 메인 메서드를 Car 클래스 내부에 정의할 수도 있지만 메인 메서드는 Car 클래스와는 아무런 상관이 없으므로 아래와 같이 따로 분리하여 작성하겠습니다.

메인 메서드 정의 후에도 찾을 수 없다는 에러가 계속 발생합니다.

예를 들어서 A라는 클래스에 메인 메서드를 정의했다면 파일 이름이 A.java가 맞는지 확인해보시기 바랍니다. 아니면 이클립스에서는 'Run -> Run Configurations'에서 'Main class:'를 A로 지정 후 Run을 눌러주세요.

class Car {
    private int speed = 0; // 현재 속도를 나타내는 필드
    private int maxSpeed = 100; // 최대 속도를 나타내는 필드

    // 차량의 현재 속도를 가져온다.
    public int getSpeed()
    {
    	return speed;
    }

    // 차량의 최대 속도를 가져온다.
    public int getMaxSpeed()
    {
        return maxSpeed;
    }

    // 속도를 올린다.
    public void speedUp(int increment)
    {
        if (speed + increment > maxSpeed){
            System.out.println("최대 속도 " + maxSpeed + "km/h를 넘어설 수 없습니다.");
        } else {
            speed += increment;
        }
    }

    // 속도를 내린다.
    public void speedDown(int decrement)
    {
        if(speed - decrement < 0) {
            System.out.println("속도는 0 아래로 떨어질 수 없습니다.");
        } else {
            speed -= decrement;
        }
    }
}

class ClassExamples {
	public static void main(String[] args) {
		Car car = new Car();
		
		car.speedUp(10); // car.speed = 10
		System.out.println("현재 자동차의 속도: " + car.getSpeed() + "km/h");
		
		car.speedUp(50); // car.speed = 60
		System.out.println("현재 자동차의 속도: " + car.getSpeed() + "km/h");
		
		car.speedUp(60); // car.speed = 60
		System.out.println("현재 자동차의 속도: " + car.getSpeed() + "km/h");
		
		car.speedDown(40); // car.speed = 20
		System.out.println("현재 자동차의 속도: " + car.getSpeed() + "km/h");
	}
}

메인 메서드를 살펴보도록 하겠습니다. 먼저 40행은 넘어가고, 42~52행의 코드를 살펴보면 아래와 같습니다. 객체 car의 speedUp() 메서드를 호출하여 car의 speed를 계속해서 증가시킵니다.

...
	// 속도를 올린다.
    public void speedUp(int increment)
    {
    	// 속도를 올렸을 때 최대 속도를 넘어선다면 속도를 올리지 않고 메시지를 하나 출력합니다.
        if (speed + increment > maxSpeed){
            System.out.println("최대 속도 " + maxSpeed + "km/h를 넘어설 수 없습니다.");
        } else {
            speed += increment;
        }
    }
...

이 speedUp() 메서드는 위를 보시면 아시겠지만 넘겨받은 increment만큼 speed를 올리는 기능을 합니다. 주석을 확인하면 48행에서 speedUp() 메서드를 호출했음에도 speed가 그대로인 이유를 알 수 있습니다. 다시 40행으로 돌아와서 아래의 코드를 보도록 합시다. 이게 대체 무엇을 의미하는 걸까요?

Car car = new Car();

객체를 생성하기

객체를 생성하려면 아래와 같이 new 키워드를 사용합니다. new 키워드는 새 객체를 생성하기 위해서 힙이라고 불리는 메모리 영역에 공간을 할당하는 역할을 합니다. 그리고 이렇게 할당된 메모리 공간에 접근하기 위해서 그 공간의 주소값을 참조형 변수에 저장하게 됩니다.

클래스명 참조변수명 = new 클래스명();

참조형 변수(Reference variable)

참조형 변수는 무엇일까요? 우리가 변수와 타입 편에서 살펴봤던 기본형 변수와는 다르게 참조형 변수에는 정수나 실수 같은 값이 아니라 객체가 저장된 곳의 주소(정확히는 객체의 실제 위치를 식별하는 데 사용될 수 있는 모종의 값), 즉 참조(reference, 혹은 레퍼런스)가 저장됩니다. 자바에서는 기본 타입이 아닌 타입을 참조 타입이라고 하는데, 참조 타입에는 클래스, 배열, 인터페이스, 열거형, 애노테이션을 예로 들 수 있습니다.

참조형 변수와 기본형 변수의 차이를 살펴보기 전에 먼저 스택 영역과 힙 영역은 무엇인지 살펴보고 가도록 하겠습니다.

스택 영역(Stack)

우리가 메서드를 호출할 때마다 스택이라 불리는 영역에 새로운 스택 프레임(stack frame)이 만들어집니다. 이 하나의 스택 프레임은 또다시 자체 피연산자 스택(operand stack), 자체 지역 변수 배열, 프레임 데이터 영역으로 나눌 수 있습니다. 간단하게 스택 프레임은 하나의 메서드를 위한 공간이며, 이 공간에는 지역 변수, 매개변수 등이 저장된다고 생각해주세요. 이해를 돕기 위해서 아래와 같은 예시를 살펴봅시다.

지역 변수(local variable)

지역 변수는 선언된 블록 내에서만 접근할 수 있는 변수를 말합니다. 전에도 말했지만, 블록은 여는 중괄호(})로 시작하여 닫는 중괄호(})로 끝납니다. 지역 변수의 기본값은 없으므로 지역 변수를 선언한 다음 지역 변수를 사용하기 전에 반드시 초기화해야 합니다.

class Example {
	public Example()
    {
		int a; // 지역 변수 a
	}
	
	public void func()
    {
		int a; // 지역 변수 a
		int b; // 지역 변수 b
	}
}
public class ReferenceExamples {
    public static void main(String[] args) {
        printStackTrace();
        methodA(); // (1)
    }

    public static void methodA() {
        printStackTrace();
        methodB(); // (2)
    }

    public static void methodB() {
        printStackTrace();
        methodC(); // (3)
    }

    public static void methodC() {
        printStackTrace();
    }

	// 이 메서드는 신경쓰지 말자.
    private static void printStackTrace() {
        final List<StackTraceElement> frames = Arrays.stream(Thread.currentThread().getStackTrace())
                .skip(2) // getStackTrace()와 printStackTrace()는 제외함
                .collect(Collectors.toList());
        System.out.println("현재: " + frames.get(0).getMethodName() + " 메서드");
        for (int i = 0; i < frames.size(); i++) {
            System.out.printf("\t[%d] %s%n", frames.size() - i, frames.get(i));
        }
    }
}

예시를 실행하여 결과를 살펴보면 다음과 같습니다.

여기서 스택을 그림으로 살펴보면 다음과 같을 것입니다. 새로운 메서드가 호출될 때마다 그 메서드의 스택 프레임이 스택 영역에 차곡차곡 쌓이는 것을 볼 수 있습니다.

메서드 본문에 있는 문장을 모두 실행하여 블록({...}) 밖으로 벗어날 때, 즉 메서드가 종료될 때 스택에 올라간 해당 메서드의 스택 프레임은 제거됩니다. 이처럼 스택은 먼저 들어온 것이 먼저 나가는(Last-In First-Out, LIFO) 구조를 띄고 있습니다. 당연하지만 여기서 스택 프레임이 제거될 때 그 안에 있던 변수, 즉 메서드 안에서 선언된 지역 변수의 메모리 공간도 같이 회수됩니다. 따라서 메서드 내부에서 선언된 변수는 그 메서드 안에서만 접근이 가능합니다.

스택은 메서드가 호출되고 종료됨에 따라 확장되고 축소될 수 있지만, 스택이 계속해서 확장될 수 있는 건 아닙니다. 당연하게도 메모리 공간은 유한하기 때문입니다. 스택의 최대 크기를 넘어서면 StackOverflowError라는 에러를 마주칠 수 있습니다. 이를 재현하는 방법은 간단합니다. 조건 없이 무한정 자기 자신을 호출하는 재귀 함수를 만들면 스택이 계속해서 불어나게 되어 더 이상 할당할 수 있는 공간이 사라져버리게 됩니다.

public class ReferenceExamples {
    public static void main(String[] args) {
        methodA();
    }

    public static void methodA() {
	    // Exception in thread "main" java.lang.StackOverflowError
        methodA();
    }
}

이어서 이번에는 하나의 스택 프레임을 집중적으로 살펴봅시다. 아래와 같이 메서드 methodA()에 지역 변수 a, b, c가 차례대로 선언되어 있다고 해봅시다.

public class ReferenceExamples {
    public static void main(String[] args) {
        methodA();
    }
    
	public static void methodA() {
		int a = 10;
		int b = 20;
		int c = 30;
		// ...
	}
}

위에서 스택 프레임 안에 자체 지역 변수 배열과 자체 피연산자 스택을 가지고 있다고 했었습니다. 여기서 피연산자 스택은 연산을 수행하기 위해서 필요한 피연산자가 저장되는 스택을 말합니다. 예를 들어서 대입 연산자 =을 통해 지역 변수 a에 10이란 값을 할당하려고 할 때 피연산자 10이 피연산자 스택에 저장됩니다. 위의 코드를 기계어와 좀 더 가까운 바이트코드로 변환해봅시다. 

public static void methodA() { //()V
	bipush 10	// 피연산자 스택에 10이란 값을 저장한다(push).
	istore 0	// 피연산자 스택에서 10을 꺼내 지역 변수 0(a)에 저장한다.
	bipush 20	// 피연산자 스택에 20이란 값을 저장한다(push).
	istore 1	// 피연산자 스택에서 20을 꺼내 지역 변수 1(b)에 저장한다.
	bipush 30	// 피연산자 스택에 30이란 값을 저장한다(push).
	istore 2	// 피연산자 스택에서 30을 꺼내 지역 변수 2(c)에 저장한다.
	return
}

스택 프레임의 중간 상태를 그림으로 나타내면 아래와 같을 것입니다. 피연산자 스택에 값을 집어넣고 이를 꺼내서 지역 변수에 저장하는 일들이 반복됩니다. 

여기서 설명한 과정을 지금 당장은 이해하지 못해도 괜찮습니다. 단순히 스택 영역에 저장된다고 하면 설명이 너무 막연하게 다가올 것 같아서 추가적으로 설명한 것입니다. 여기서 중요한 점은 메서드 안에 선언된 지역 변수나 매개변수가 스택이라 불리는 영역에 저장된다는 것입니다. 그리고 이런 변수들은 메서드가 호출되면 메모리 공간에 할당되고, 종료되면 메모리 공간에서 회수됩니다. 따라서 이 변수들은 메서드 내에서만 접근할 수 있습니다.

힙 영역(Heap)

힙 영역은 스택 영역보다 더 복잡합니다. 이 영역에서 구체적으로 무슨 일이 일어나는지는 제껴두고, 지금은 이 영역이 대충 어떤 영역인지만 알아둡시다. 이 영역은 추후에 다시 자세하게 살펴볼 것입니다. 이 힙 영역은 우리가 객체를 메모리 공간에 할당하려고 할 때 사용됩니다. 다시 말해서 생성된 객체가 저장되는 공간을 말합니다.

public static void methodA() {
	Person person = new Person();
	// ...
}

프로그램 실행 중에 위와 같은 문장을 마주치면 Person 객체를 힙 영역에 할당하고, 이 할당된 영역을 가리키는 일종의 주소, 즉 참조(reference)가 반환되어 참조형 변수 person에 저장됩니다. 이때 참조형 변수 person은 지역 변수이므로 스택 영역에 저장된다는 점에 주의합시다.

그리고 메서드가 종료되면 스택 영역에 할당된 참조형 변수 person도 같이 사라지지만, 힙 영역에 생성된 Person 객체는 회수되지 않습니다. 아래의 경우에는 어떻게 될까요?

public static void methodA() {
	Person person = new Person(); // (1)
	person = new Student(); // (2)
	// ...
}

아래와 같이 (1)에서는 Person 객체를 가리켰다가, (2)에서는 Student 객체를 가리키게 됩니다. 더는 참조되지 않는 Person 객체는 바로 사라지진 않습니다. 그럼 언제 사라질까요?

힙 영역에 있는 객체들은 더 이상 사용되지 않으면(즉, 해당 객체를 참조하고 있는 곳이 없다면) 가비지 컬렉터가 가비지 컬렉션(garbage collection, 이른바 쓰레기 수거)을 수행하여 메모리 영역에서 사용되지 않는 객체를 제거합니다. 이 가비지 컬렉션이 일어나는 시점은 우리가 직접 제어할 수는 없으며 순수하게 JVM이 결정합니다. 가비지 컬렉터 또한 추후에 힙 영역을 살펴보면서 같이 자세하게 알아볼 것입니다.

객체 참조를 메서드로 전달하기

기본형 변수를 메서드에 전달하면 아래와 같이 매개변수에는 기본형 변수가 담고있는 값이 복사됩니다.

class ReferenceExamples {
	public static void main(String[] args) {
		int a = 4;
		
		System.out.println("a: " + a); // a: 4
		change(a);
		System.out.println("a: " + a); // a: 4
	}
	
	static void change(int num) {
		num++;
	}
}

결과에서 확인할 수 있듯이 그저 값만 복사될 뿐, 원래 변수에는 영향을 미치지 않습니다. 이를 메모리에서 봤을 때는 다음과 같을 것입니다. 물론 매개변수 num과 지역 변수 a는 모두 스택 영역에 저장됩니다.

참조형 변수도 마찬가지로 값이 복사되는데, 참조형 변수가 담고 있는 값은 객체가 저장된 곳의 주소, 즉 참조라고 불리는 특별한 값입니다. 따라서 메서드로 참조형 변수를 넘길 때 매개변수에는 참조형 변수가 담고 있는 참조가 복사됩니다.

class Counter { 
	int myCount;
	
	public void increment() {
		myCount++;
	}
	
	public void reset() {
		myCount = 0;
	}
	
	public int getValue() {
		return myCount;
	}
}

class ReferenceExamples {
	public static void main(String[] args) {
		Counter a = new Counter();

		System.out.println("a.getValue(): " + a.getValue()); // a.getValue(): 0
		change(a);
		System.out.println("a.getValue(): " + a.getValue()); // a.getValue(): 1
	}
	
	static void change(Counter p) {
		p.increment();
	}
}

이를 메모리 공간에서 살펴보면 아래와 같을 것입니다. 매개변수 p에 참조형 변수 a가 담고 있는 참조가 복사되고, 이 참조를 통해서 p가 가리키고 있는 객체의 increment() 메서드를 호출하게 됩니다. 이해가 되시나요?

아무것도 가리키지 않는 참조형 변수

그러면 참조형 변수로 아무것도 가리키지 않게 하고 싶을 때는 어떻게 해야 할까요? 현재 참조형 변수가 어느 객체도 가리키지 않음을 나타내기 위해 아래와 같이 null을 대입합니다. 이때 대소문자를 구분하므로 NULL로 쓰시면 안됩니다.

참조타입 참조변수명 = null;

만약, 참조형 변수가 아무것도 가리키지 않는 상태에서 참조형 변수를 사용해 필드나 메서드에 접근하려고 하면 어떻게 될까요? 한번 아래의 코드를 컴파일 후 실행해보시기 바랍니다.

class Counter { 
	int myCount;
	
	public void increment() {
		myCount++;
	}
}

class ReferenceExamples {
	public static void main(String[] args) {
		Counter a = null;
		
		a.increment();
	}
}

실행하면 아래와 같이 NullPointerException 예외가 발생한 것을 볼 수 있습니다. 참조형 변수 a가 가리키는 객체가 없는데 객체의 메서드를 호출하려고 했기 때문입니다. 여기까지 모두 이해가 되셨나요?

'프로그래밍 관련 > 자바' 카테고리의 다른 글

16편. 배열(Array)  (19) 2012.08.12
15편. 생성자(Constructor)  (14) 2012.08.11
12편. 메서드(Method)  (27) 2012.08.09
11편. 반복문 (2)  (11) 2012.08.08
10편. 반복문 (1)  (6) 2012.07.31