우선 객체란 무엇일까?
객체는 실생활에 있는 어떤 사물이나 행동을 소프트웨어에 논리적/물리적으로 실체화한 것이다.
그렇다면 인스턴스란?
이 객체를 사용하게 위해서 실제 메모리를 할당받고 물리적으로 실체화한 것이다.
정확한 설명일지는 모르겠지만, 내가 생각하는 두 개념의 정의에 따르면 논리적/물리적이라는 점에서 차이점이 생기는 것을 알아차렸을 것이다. 하지만 객체는 논리적인 개념과 함께 물리적인 개념도 포함하기 때문에 인스턴스 개념은 객체 개념에 포함된다고 볼 수 있다.
예를 들어보자.
Person jaeseok;
jaeseok = new Person();
위의 코드가 JVM 메모리에 위에서 어떻게 동작하는지 먼저 생각해보자.
우선 jaeseok이라는 이름을 가진 참조변수가 Stack 영역에 생성된다. 이 변수는 가지고 있는 참조 값이 없기 때문에 null 값을 가지고 있을 것이다.
그리고 new Person() 부분을 보면, new 연산자에 의해 Heap 영역에 Person 객체가 생성된다.(메모리 할당받는다) 이 때 Person 클래스에 명시되어 있는 생성자나, 명시되지 않았다면 컴파일러가 기본적으로 생성해주는 기본 생성자를 Class 영역에서 참고하여 할당받은 메모리에 초기화해준다.
이렇게 Heap 영역에 할당받은 메모리의 주소값을 jaeseok 변수에 =로 넣어준다.
여기서 첫 번째 줄은 객체를 생성한 것이다. jaeseok이라는 실세계에 있을 법한 사람을 소프트웨어에 변수를 선언함으로써 실체화했다. 따라서 이 시점에서 jaeseok은 객체라고 할 수 있다.
그리고 두 번째 줄은 이 객체를 인스턴스화한 것이다. 첫 번째 줄에서는 실체화만 했을 뿐 소프트웨어에서 사용할 수 있는 상태가 아니었지만, 물리적으로 메모리를 할당받고 객체 내부의 필드나 메소드가 모두 초기화 되어서 사용가능한 상태가 되었으므로 jaeseok은 인스턴스라고 볼 수 있다. Heap 영역에 할당된 메모리 자체도 인스턴스라고 볼 수 있다. 그리고 jaeseok과 메모리 둘 모두 인스턴스이면서 객체이기도하다.
비유를 들어서 조금 덧붙이자면, 도로 위에 자동차가 다니는 프로그램이 있다고 하자.
여기서 자동차의 설계도는 Class이다. 자동차들은 이 설계도에 따라서 생성된다.
자동차는 객체이다. 도로 위를 지나고 있는 자동차 한 대 보고 자동차라고 하기도 하고, 아직 만들어지지 않은 자동차도 그냥 자동차라고 한다. 설계도에 따라 만들어지는 자동차들을 넓은 의미로 자동차라고 하듯이 객체도 넓은 의미로 이 클래스에 따라 만들어지는 데이터 포맷을 객체라고 한다.
자동차 한 대는 인스턴스이다. 자동차 한 대마다 다른 번호를 가지듯이 인스턴스마다는 다른 주소값을 가진다. 여러 개의 자동차 한 대가 존재하기도 하고 여러 운전자가 이 자동차 한 대를 운전할 수도 있다. 그리고 이 한 대의 자동차를 차 번호로 부르기도 하지만 그냥 넓은 의미로 자동차라고 하기도 한다.
Java에서는 오버로딩과 오버라이딩으로 다형성을 구현한다. 이 두 개념은 다형성을 지원하는 도구라는 공통점이 있지만 (그리고 이름도 비슷하지만), 서로 완전히 다른 개념이다.
오버로딩의 종류로는 생성자 오버로딩, 메소드 오버로딩이 있다. 둘은 거의 비슷한 개념이다. 생성자나 메소드를 설계할 때 같은 기능을 가지는 모듈을 파라미터가 다른 경우에 오버로딩을 사용할 수 있다.
오버로딩은 생성자나 메소드가 같은 기능을 다른 파라미터로 구현할 수 있도록 지원하는 기능을 뜻한다.
public static class Person {
int age = 20;
String language;
public Person(){}
int sumAge(int val) {
return this.age + val;
}
int sumAge(int val1, int val2) {
return this.age + val1 + val2;
}
}
public static main(String[] args){
Person jaeseok = new Person();
System.out.println(jaeseok.sumAge(1)); // 21
System.out.println(jaeseok.sumAge(2,3)); // 25
}
위 코드에서는 Person이라는 정적 내부 클래스를 하나 선언해서 sumAge라는 메소드를 파라미터만 다르게 두 개 선언했다. main 메소드에서 보면 같은 이름의 메소드를 다른 파라미터를 넣어서 출력해보면 Java에서 알아서 두 개의 메소드를 매칭해줘서 적절한 결과값이 나올 수 있음을 확인할 수 있다.
따라서 내가 사용할 기능을 메소드 이름만 알면 그 메소드는 내가 입력한 다양한 형태의 파라미터(다형성)
를 받아서 사용자가 원하는 기능을 수행해준다.
오버라이딩은 슈퍼 클래스와 자식 클래스의 상속관계에서 슈퍼 클래스의 어떤 메소드를 상속받은 자식 클래스가 그 어떤 메소드를 재정의해서 사용하는 것을 의미한다.
// Person 클래스
public class Person {
int age = 20;
String language;
public Person(){}
int sumAge(int val) {
return this.age + val;
}
}
// Developer 클래스
public class Developer extends Person {
super();
@Override
int sumAge(int val) {
return this.age + val + val;
}
}
위의 코드에서는 Person 클래스를 상속받은 Developer 클래스에서는 상속받은 sumAge 메소드를 재정의해서 사용하고 있다. 이러한 경우에 Java에서는 어떤 객체를 생성해서 사용하느냐에 따라 같은 이름의 메소드를 여러가지 기능(다형성)
으로 사용할 수 있다.
일반적으로 객체 내에서 선언되는 변수는 필드라고 하며, 객체 생성시에 같이 생성되어 객체가 사라지면 이 필드도 같이 사라진다.
하지만 static 변수는 객체에 종속적이지 않고 클래스에 종속적이다.
이것이 무슨 의미이냐면 static 변수는 클래스 로더가 클래스 영역에 클래스 정보를 로딩할 때에 같이 로딩되어 객체가 생성되기 전 시점에도 이미 초기화가 되어 있는 상태이다. 따라서 객체 생성없이 [클래스 이름].[static 변수 이름]
으로 가져다가 바로 사용할 수 있다.
보통 static 변수는 이러한 특성 때문에 한 클래스의 모든 객체가 공유할만한 정보를 담는 용도로 사용된다.
final 변수는 말 그대로 최종적인 변수이다. 한번 값이 들어가면 이 값은 변하지 앟는다. 따라서 생성자나 클래스 내에 final 변수를 초기화하는 값이 들어가 있고 이 값을 수정하려고하면 에러가 뜰 것이다.
보통 final 변수는 고정되어 변하지 않는 값을 담는 용도로 사용된다.
그렇다면 static final 변수는 어떻게 사용될까? 이는 상수를 담는 변수이다.
static final double pi = 3.141592;
이와 같이 파이 값은 어떤 시스템이든, 어떤 클래스든, 어떤 메소드든 3.14...으로 정의되어서 사용된다. 따라서 static 개념에 따라 모든 객체가 공유할 수 있고, final 개념에 따라 수정이 절대 불가능하게 선언해 놓는 것이다.
만약 static으로만 선언된다면 이 pi값이 수정되어 큰 혼란을 초래할 수 있다. 그리고 만약 final로만 선언된다면 모든 객체마다 이 변수를 하나씩 다 선언하게 되어 메모리 상 성능 저하를 초래할 수도 있다.
싱글톤 패턴은 어떤 하나의 클래스에서 단 하나의 인스턴스만 생성해서 사용할 경우에 사용하는 디자인 패턴이다.
public class Person {
// 정적 필드
private static Person getPerson = new Person();
// 생성자
private Person(){};
// 메소드
public static Person getInstance() {
return getPerson;
}
}
위 코드와 같이 생성자를 private로 선언하여 외부에서 직접 인스턴스를 생성하지 못하게 하고, 정적 필드에 클래스 로딩 시 instance 한 개를 할당해두어 이 인스턴스만 getInstance() 메소드로 가져다 쓸 수 있도록 한다.