클래스로부터 객체를 만드는 과정을 클래스의 인스턴스화라고 하며 그 과정에서 만들어진 객체를 인스턴스라고 한다.
인스턴스는 참조변수를 통해서만 다룰 수 있고, 참조변수와 인스턴스의 타입은 일치해야 한다.
Person p = new Person();
많은 수의 객체를 다뤄야할 때, 객체 배열을 생성하여 편리하게 다룰 수 있다.
Person[] pArr = new Person[3];
# 초기화 방법1
Person[] pArr = { new Person(), new Person(), new Person()};
# 초기화 방법2
Person[0] = new Person();
Person[1] = new Person();
Person[2] = new Person();
# 초기화 방법3
for (int i = 0; i < pArr.length; i++) {
Person[i] = new Person();
}
변수는 클래스 변수, 인스턴스 변수, 지역 변수 세 종류가 있다.
class Variable {
int a = 5;
static int b;
void method() {
int a = 10;
}
}
위 코드에서, 변수 a는 인스턴스 변수, b는 클래스 변수, method안의 a는 지역 변수이다.
인스턴스 변수는 클래스 영역에 선언되고, 인스턴스를 생성할 때 만들어진다. 또한 인스턴스마다 각각 서로 다른 값을 가진다.
클래스 변수는 한 클래스의 인스턴스들이 공통적으로 공유하는 변수다. A 인스턴스에서 a가 5면 B 인스턴스에도 5다. 인스턴스 변수와 달리 인스턴스를 생성하지 않고도 바로 사용할 수 있으며, "클래스이름.변수이름" 같은 형식으로 사용된다. 또, 클래스 변수는 클래스가 메모리에 로딩될때 생성되어 프로그램이 종료될 때 까지 유지되며, public을 앞에 붙이면 같은 프로그램 내에서 어디서나 접근할 수 있는 전역변수의 성격도 갖는다.
지역 변수는 메서드 내에 선언되어 메서드 내에서만 사용 가능하고, 메서드가 종료되면 소멸된다.
메서드를 사용하는 대표적인 세 가지 이유는
첫 번째로, 높은 재사용성을 갖고 있다. java API에서 제공하는 print같은 메서드처럼 한번 만들어놓으면 몇 번이고 호출할 수 있고, 다른 프로그램에서도 사용가능하다.
두 번째로, 중복된 코드의 제거가 가능하다. 프로그램을 작성하다보면 같은 내용의 문장들이 여러 곳에서 반복해서 나타날 수 있는데, 이를 묶어 하나의 메서드로 작성해놓으면 반복되는 문장들 대신 메서드 호출 하나로 해결할 수 있다.
세 번째로, 프로그램의 구조화가 가능하다. main메서드 안에 모든 내용을 넣을 수 도 있지만, 몇 천줄, 몇 만 줄이 넘어가는 프로그램도 그렇게 작성할 수는 없다. 큰 규모의 프로그램에서는 문장들을 작업단위로 나눠서 여러 개의 메서드에 담아 프로그램의 구조를 단순화시키는 것이 필수적이다.
return문은 현재 실행중인 메서드를 종료하고 호출한 메서드로 되돌아간다. 원래는 반환값의 유무에 관계없이 모든 메서드에서는 적어도 하나의 return문이 있어야 한다. 하지만 반환타입이 void인 경우, 컴파일러가 알아서 return문을 자동을 추가해준다.
void print() {
System.out.print("hi");
return; // 생략해도 컴파일러가 자동으로 추가
}
응용 프로그램이 실행되면, JVM은 시스템으로부터 프로그램을 수행하는데 필요한 메모리를 할당받고 JVM은 이 메모리를 용도에 따라 여러 영역으로 나누어 관리한다. 그 중 3가지 주요 영역을 살펴본다.
1. Method Area(메서드 영역)
프로그램 실행 중 어떤 클래스가 사용되면, JVM은 해당 클래스의 클래스 파일을 읽어서 분석하여 클래스에 대한 정보(클래스 데이터)를 이곳에 저장한다. 이 때, 그 클래스의 클래스 변수도 이 영역에 함께 생성된다.
2. Heap(힙)
프로그램 실행 중 생성되는 인스턴스가 전부 이 공간에 생성된다. 즉, 인스턴스 변수들이 생성되는 공간이다.
3. Call Stack(호출 스택)
호출 스택은 메서드의 작업에 필요한 메모리 공간을 제공한다. 메서드가 호출되면 호출스택에 호출된 메서드를 위한 메모리가 할당되며, 이 메모리는 메서드가 작업을 수행하는 동안 지역변수(매개변수 포함)들과 연산의 중간결과 등을 저장하는데 사용된다. 그리고 메서드가 작업을 마치면 할당되었던 메모리 공간은 반환되어 비워진다. 각 메서드를 위한 메모리상의 작업공간은 서로 구별되며, 첫 번째로 호출된 메서드를 위한 작업공간이 스택의 맨 밑에 마련되고, 첫 번째 메서드 수행중에 다른 메서드가 호출되면, 그 위에 바로 공간이 마련된다. (스택 구조) 그리고 두 번째 메서드가 종료되면 그 호출스택의 메모리 공간이 비워지고, 첫 번째 메서드가 다시 수행을 계속한다. 즉, 호출 스택의 제일 상위에 위치하는 메서드가 현재 실행중인 메서드다. 호출 스택의 구조를 간략하게 정리하면 다음과 같다.
메서드가 호출되면 수행에 필요한 만큼의 메모리를 스택에 할당 받는다.
메서드가 수행을 마치고나면 사용했던 메모리를 반환하고 스택에서 제거된다.
호출스택의 제일 위에 있는 메서드가 현재 실행중인 메서드다.
아래에 있는 메서드가 바로 위의 메서드를 호출한 메서드다.
반환 타입이 있는 메서드는 종료되면서 결과값을 자신을 호출한 메서드에게 반환한다. 대기상태에 있던 호출한 메서드는 넘겨받은 반환값으로 수행을 계속한다.
자바는 메서드를 호출할 때 매개변수로 지정한 값을 메서드의 매개변수에 복사하여 넘겨준다. 이 때 매개변수의 타입이 기본형이면 값(value)의 복사가 일어나고, 참조형이면 주소(reference)의 복사가 일어난다.
메서드 앞에 static을 붙이면 클래스 메서드가 된다. 클래스 메서드 안에서는 인스턴스 메서드나 인스턴스 변수를 사용할 수 없다. 그 이유는 클래스 메서드는 인스턴스를 생성하지 않고 호출이 가능하기 때문에, 클래스 메서드 안에서 인스터스를 사용하면 오류가 발생한다. 따라서 클래스 메서드는 메서드들 중에 인스턴스와 관련이 없는 것들로 정의한다.
추가로, 인스턴스 메서드는 실행시 호출되어야할 메서드를 찾는 과정이 추가로 필요하기 때문에 시간이 걸린다. 인스턴스를 필요로 하지 않는다면 클래스 메서드로 만들어 성능을 향상시킬 수 있다.
자바에서는 한 클래스 내에 같은 이름의 메서드가 있어도, 매개변수의 개수나 타입이 다르면 다른 메서드로 정의한다. 이것을 메서드 오버로딩이라고 한다.
말했듯이, 오버로딩에는 조건이 있는데 먼저 메서드 이름이 같아야 하고, 매개변수의 개수또는 타입이 달라야 한다. 주의할 것은 반환 타입은 오버로딩 조건에 들어가지 않는다.
가장 대표적인 메서드 오버로딩의 예는 println이다. println은 매개변수에 따라 호출되는 메서드가 달라진다. PrintStream 클래스에는 어떤 종류의 매개변수를 지정해도 출력할 수 있도록 println에 대해서 10개의 오버로딩된 메서드를 정의해놓고 있다.
만약 오버로딩이 없다면 같은 기능을 하지만, 각기 다른 이름을 가진 메서드가 10개가 존재해야할 것이다. 메서드 오버로딩의 장점은 이런 여러 메서드들이 하나의 이름으로 정의될 수 있고, 어떤 역할을 하는지 쉽게 예측이 가능하다는 것이다. 또한 메서드의 이름을 짓는데 고민을 덜 수 있는 동시에 사용되었어야 할 메서드의 이름을 다른 메서드의 이름으로 사용가능 하다는 점도 있다.
기존에는 메서드의 매개변수 개수가 고정적이었으나, JDK 1.5부터 동적으로 지정해줄 수 있게 되었다. 이것을 가변인자라고 한다. 가변인자는 "타입...변수명" 같은 형식으로 선언한다.
public PrintStream printf(String format, Object ... args){ ... }
가변인자는 선언된 메서드가 호출될 때마다 배열을 새로 생성하여 할당하며, 가변인자말고 다른 매개변수가 더 존재한다면, 가변인자는 제일 마지막에 선언해주어야 한다.
또, 가변인자를 선언한 메서드를 오버로딩하면, 메서드를 호출했을 때 구별되지 못하는 경우가 발생하기 쉽기 때문에 가능한 오버로딩을 하지 않는 것이 좋다.
인스턴스가 생성될 때 실행해야할 작업을 해주는 것이 생성자이다.
class Card {
Card() {
...
}
Card(String k, int num) {
...
}
}
생성자도 메서드이며, 인스턴스 초기화 메서드라고도 한다. 따라서 인스턴스 변수의 초기화 작업에 주로 사용된다.
생성자는 생략이 가능하며, 생략시엔 컴파일러가 기본 생성자를 자동으로 추가한다. 단, 하나의 생성자라도 존재한다면 기본 생성자를 추가하지 않는다.
class Card {
Card() {}
}
참고로, 클래스가 public인 경우엔 기본 생성자 앞에도 public이 추가된다.
생성자에서 다른 생성자를 호출할 수 도 있다.
class Card {
Card() {
this("hi", 5);
}
Card(String k, int num) {
...
}
}
다른 생성자를 호출할 때는 this()를 이용한다.
매개변수와 맴버변수의 이름이 같다면 this를 이용하며 구분한다.
class Card {
int num;
Card(int num) {
this.num = num;
}
}
this는 인스턴스 자신을 가리키는 참조변수이며 모든 인스턴스 메서드에 지역 변수로 숨겨진 채 존재한다. 인스턴스 자신을 가리키므로 당연히 클래스 변수나 클래스 메서드가 사용할 수 없다.
this는 this()와는 구별된다. this()는 같은 클래스의 생성자를 호출할 때 사용한다.
멤버 변수는 초기화 하지 않더라도 자동적으로 변수의 자료형에 맞는 기본값으로 초기화되지만 지역 변수는 사용하기 전에 반드시 초기화해야한다.
또, 변수를 선언과 동시에 초기화하는 것을 명시적 초기화라고 부르며 가장 기본적이고 간단하므로 여러 초기화 방법 중 우선적으로 고려되어야 한다.
초기화 블럭에는 클래스 초기화 블럭과 인스턴스 초기화 블럭 두 가지 종류가 있다.
클래스 초기화 블럭은 클래스 변수의 복잡한 초기화에 사용되고, 인스턴스 초기화 블럭은 인스턴스 변수의 복잡한 초기화에 사용된다.
class Init() {
// 클래스 초기화 블럭
static {
....
}
// 인스턴스 초기화 블럭
{ ... }
}
초기화 블럭 내에서는 메서드처럼 조건문, 반복문 등을 자유롭게 사용할 수 있으므로 초기화 작업이 복잡하여 명시적 초기화만으로는 부족한 경우 사용한다. 또 여러 생성자 내에서 중복적으로 수행하는 작업이 있을 경우 중복된 코드를 줄이기 위해 사용하기도 한다.
클래스 초기화 블럭은 클래스가 메모리에 로딩될 때 처음 한번만 수행되며, 인스턴스 초기화 블럭은 인스턴스가 생성될 때마다 수행된다. 또 생성자보다 인스턴스 초기화 블럭이 먼저 수행된다. 이들의 초기화 순서를 나타내면 다음과 같다.
클래스 변수 : 클래스가 처음 로딩될 때 단 한번 초기화
인스턴스 변수 : 인스턴스가 생성될 때마다 각 인스턴스별로 초기화
클래스 변수의 초기화 순서 : 기본값 -> 명시적 초기화 -> 클래스 초기화 블럭
인스턴스 변수의 초기화 순서 : 기본값 -> 명시적 초기화 -> 인스턴스 초기화 블럭 -> 생성자
클래스는 프로그램 실행도중에 클래스에 대한 정보가 요구될 때, 클래스는 메모리에 로딩된다. 예를 들면 클래스 멤버를 사용했을 때, 인스턴스를 생성할 때 등이 해당한다. 하지만 이미 메모리에 로딩되어 있다면 다시 로딩하지 않는다.
클래스의 로딩 시기는 JVM의 종류마다 다른데, 클래스가 필요할 때 바로 메모리에 로딩되게 설계되어있는 것도 있고, 실행효율을 높이기 위해 사용될 클래스들을 프로그램이 시작될 때 미리 로딩하도록 되어있는 것도 있다.