들어가기 전에
이번 포스팅은 스프링 입문을 위한 자바 객체 지향의 원리(김종민 저)를 읽으면서 자바가 제공하는 객체 지향 키워드 및 연산자에 대해 정리한 글입니다. 이해한 내용을 토대로 정리한 글이기 때문에, 책으로 읽어보는 것을 권장합니다.
abstract 키워드 - 추상 메소드와 추상 클래스
추상 메소드(Abstract Method)란, 선언부는 있는데 구현부가 없는 메소드를 의미합니다. 추상 메소드를 하나라도 가지고 있다면 해당 클래스는 추상 클래스(Abstract Class)로 선언되어야 합니다.
구현 내용 없이 선언만 되어 있는 메소드가 필요한 이유에 대해 알아보도록 하겠습니다. 만약 아래와 같이 Animal 클래스 배열이 존재하고 각 배열의 원소는 Animal 클래스를 상속 받은 클래스의 객체를 가리킨다고 가정해보겠습니다.
public class Driver {
public static void main(String[] args) {
Animal[] AnimalArray = new Animal[3];
AnimalArray[0] = new Whale();
AnimalArray[1] = new Cat();
AnimalArray[2] = new Dog();
for(Animal animal: AnimalArray) {
animal.crying(); // crying()는 Whale, Cat, Dog 클래스에서 각각 오버라이딩된 메소드
}
}
}
public class Animal {
void crying() {
System.out.println("나는 동물! 어떻게 울어야 하나요?");
}
}
public class Whale extends Animal {
void crying() {
System.out.println("Whale이 울어요!");
}
}
public class Cat extends Animal {
void crying() {
System.out.println("Cat이 울어요!");
}
}
public class Dog extends Animal {
void crying() {
System.out.println("Dog이 울어요!");
}
}
위 코드를 실행하게 되면 AnimalArray의 각 원소가 가리키는 객체가 속한 클래스의 crying 메소드가 수행됨을 확인할 수 있습니다. 만약, AnimalArray의 원소 중에 Animal을 상속받은 클래스가 아닌 Animal 클래스 객체를 가리키면, Animal 클래스의 crying() 메소드가 수행될 것입니다. 하지만, Animal은 분류일 뿐 실체가 아니기 때문에 crying한다는 것 자체가 논리에 맞지 않습니다. 물론, Animal 클래스의 crying() 메소드를 {}와 같이 비워둘 수도 있겠지만, 실수로 Animal 객체의 crying() 메소드를 호출해버리고, 하위 클래스의 crying() 메소드를 구현하지 않게 되면 난감해집니다.
이럴 때 사용하는 것이 추상 메소드(메소드 선언만 있고 몸체가 없는 형태의 메소드)입니다. 추상 메소드를 사용하게 되면, 아래와 같이 Animal 클래스의 crying() 메소드의 구현부를 작성하지 않고 선언부만 작성하게 됩니다.
public abstract class Animal { // 추상 메소드가 존재하기 때문에 추상 메소드가 됩니다.
abstract void crying(); // 추상 메소드
}
이렇게 추상 메소드를 사용하게되면, 위에서 걱정했던 두 가지 문제가 해결됩니다.
- Animal 객체를 직접 생성해 crying()을 호출하면 어떻게할까? → 왼쪽 사진처럼 추상 클래스의 객체 생성 불가(해당 클래스 상속한 클래스의 객체 생성 가능)
- Animal 클래스를 상속한 클래스의 crying() 메소드를 오버라이딩하지 않으면 어떻게할까? → 오른쪽 사진처럼 추상 클래스를 상속할 때 추상 메소드는 구현이 강제됨
추상 클래스는 인스턴스, 즉 객체를 만들 수 없습니다(new 사용 불가).
추상 메소드는 하위 클래스에서 메소드의 구현을 강제합니다(오버라이딩 강제).
추상 메소드를 포함하는 클래스는 반드시 추상 클래스여야 합니다.
static 블록
static 블록은 클래스가 스태틱 영역에 배치될 때 실행되는 코드 블록을 의미합니다. static 블록이 실행되는 시점에 대해 아래 예시 코드를 통해 알아보도록 하겠습니다.
public class Driver04 {
public static void main(String[] args) {
System.out.println("main 메소드 시작");
Animal pingu = new Animal();
Animal mickey = new Animal();
}
}
public class Animal {
static {
System.out.println("Animal 클래스 시작");
}
}
자바가 스태틱 영역에 클래스를 로딩하는 시점은 해당 패키지 또는 클래스가 처음으로 사용되는 시점입니다.
- 위 예시의 경우 main() 메소드 내에서 println 메소드를 실행된 후에야 Animal 클래스가 처음 사용되었기 때문에, Animal 클래스의 static 영역은 println 작업을 마친 후에 진행됩니다.
- Animal 클래스가 두 번 사용되었지만, static 블록은 클래스가 처음 사용될 때만 로딩되므로, 한번만 Animal 클래스 시작이라는 문구가 출력됩니다.
클래스가 처음으로 사용되는 시점이란?
- 클래스의 정적 속성을 사용할 때
- 클래스의 정적 메소드를 사용할 때
- 클래스의 인스턴스를 최초로 만들 때
왜 프로그램이 실행될 떄 모든 클래스들의 정보를 T 메모리의 static 영역에 로딩하지 않을까?
메모리는 최대한 늦게 사용을 시작되고 최대한 빨리 반환하는 것이 정석입니다. static 영역 역시 메모리에 속하기 때문에 최대한 늦게 로딩을 하는 것입니다. 특히 static 영역은 한 번 올라가게 되면 프로그램 종료시까지 메모리를 반환할 수 없기 때문에 최대한 늦게 로딩합니다.
instance 블록도 있나요?
instance 블록 역시 static 영역처럼 존재합니다. instance 블록은 {} 블록을 의미하며 인스턴스가 생성될 때마다 실행됩니다. {} 블록은 객체 생성자가 실행되기 전에 먼저 실행됩니다.
final 키워드
final은 마지막, 최종이라는 의미를 가진 단어로 변경 불가능한 상수를 선언하고자 할 때 사용합니다.
final이 붙은 클래스
클래스 앞에 final이 붙으면 상속이 불가능한 클래스가 됩니다. 아래의 예시처럼 하위 클래스를 만들 수 없습니다.
final이 붙은 변수
변수에 final이 붙으면 마지막이라는 뜻을 갖게 되어 변수 값을 한 번 초기화하면 변경할 수 없게 됩니다.
- 정적(static) 상수: 선언 시 또는 static 블록에서 최초 한 번 초기화 가능
- 객체 상수: 선언 시 또는 객체 생성자 또는 인스턴스 블록에서 최초 한 번 초기화 가능
- 지역 상수: 선언 시 또는 최초 한 번만 초기화 가능
final이 붙은 메소드
메소드 앞에 final이 붙으면, 최종이라는 뜻을 갖게 되어 재정의가 불가능합니다. 따라서, 오버라이딩을 금지합니다.
package 키워드
package 키워드는 네임스페이스(이름공간)를 만들어주는 역할을 합니다. 쉽게 예를 들자면, 같은 Customer 클래스를 마케팅 담당에서도 만들어 사용할 수 있고, 인사 담당에서도 만들어 각각 사용할 수 있습니다. 이를 네임스페이스라고 합니다.
- 마케팅.Customer 클래스
- 인사.Customer 클래스
interface 키워드와 implements 키워드
interface는 public 추상 메소드와 public 정적 상수만 가질 수 있습니다. 따라서, 아래와 같이 메소드에는 public과 abstract를 변수에는 public과 static, final을 붙이지 않아도 자바가 알아서 해당 키워드를 붙여줍니다.
- 자바가 알아서 해당 키워드들을 붙여주지만, 명확성을 위해 public, static, final, abstract 키워드를 붙여주는게 좋습니다.
this 키워드
this는 객체가 자기 자신을 지칭할 때 사용하는 키워드입니다. 아래와 같은 예시처럼 지역 변수에 저장되어 있는 값이 아닌 객체 변수에 저장된 값을 사용하고자할 때 사용합니다.
class Penguin {
int num = 10;
void numCheck() {
int num = 20;
System.out.println(num);
System.out.println(this.num);
}
}
public class Driver {
public static void main(String[] args) {
Penguin pororo = new Penguin();
pororo.numCheck();
}
}
- 지역 변수와 속성(객체 변수, 정적 변수)의 이름이 같은 경우 지역 변수가 우선합니다.
- 객체 변수와 이름이 같은 지역 변수가 있는 경우 객체 변수를 사용하려면 this를 접두사로 사용합니다.
- 정적 변수와 이름이 같은 지역 변수가 있는 경우 정적 변수를 사용하려면 클래스명을 접두사로 사용합니다.
객체.객체메소드() 호출시, 실제로는 클래스명.객체메소드() 호출
debugTest() 메소드는 static 메소드가 아니기 때문에 객체 메소드입니다. 객체 메소드라면, T 메모리 구조에서 힙 영역의 Penguin 객체 내에 존재할 것으로 생각이 되지만 위 예시에서 볼 수 있듯이 스태틱 영역의 Penguin 클래스 내 존재하는 것을 확인할 수 있습니다.
만약에 힙 영역에 객체 메소드가 존재한다면, Penguin 객체를 여러개 호출할 경우 힙 영역의 각 Penguin 객체마다 객체 메소드가 할당되어야 합니다. 객체 메소드는 각 객체별로 다르지 않으며, 객체 멤버 메소드에서 사용하는 객체 멤버 속성의 값만 다르기 때문에 객체 메소드를 힙 영역에 두면 메모리 낭비입니다. 이에 JVM은 지능적으로 객체 멤버 메소드를 스태틱 영역에 단 하나만 보유합니다.
사실상 위와 같이 코드를 작성해 실행하면 JVM은 아래와 같이 this 객체 참조 변수를 이용해 메소드를 호출하게 됩니다.
class Penguin {
// void debugTest() {
// System.out.println("Test");
// }
static void debugTest(Penguin this) {
System.out.println("Test");
}
}
public class Driver {
public static void main(String[] args) {
Penguin pororo = new Penguin();
//pororo.debugTest();
Penguin.debugTest(pororo);
}
}
참고 자료
'PROGRAMMING LANGUAGE > JAVA' 카테고리의 다른 글
[effective JAVA] 아이템6: 불필요한 객체 생성을 피하라 (0) | 2022.10.28 |
---|---|
[effective JAVA] 아이템1: 생성자 대신 정적 팩토리 메소드를 고려하라 (0) | 2022.10.28 |
[JAVA] 객체 지향 4대 특성(캡! 상추다: 캡슐화/상속/추상화/다형성) (1) | 2022.06.03 |
[JAVA] 자바 프로그램 개발 및 구동 (0) | 2022.06.01 |
[JAVA 개념] ArrayList 초기화 (0) | 2022.02.20 |