이번 장에서는 자바가 객체 지향을 확장하기 위해 사용하는 키워드와 개념을 살펴보자.
abstract 키워드 - 추상 메서드와 추상 클래스
추상 메서드(Abstract Method)를 간단하게 설명하면 선언부는 있는데 구현부가 없는 메서드를 의미한다.
추상 메서드를 하나라도 갖고 있는 클래스는 반드시 추상 클래스(Abstract Class)로 선언해야 한다.
물론 추상 메서드 없이도 추상 클래스를 선언할 수는 있다.
그럼 추상 메서드는 어떤 것이고, 왜 필요할까?
public class 동물 {
void 울어보세요() {
System.out.println("나는 동물! 어떻게 울어야 하나요?");
}
}
public class 쥐 extends 동물 {
void 울어보세요() {
System.out.println("나는 쥐! 찍! 찍!");
}
}
public class 고양이 extends 동물 {
void 울어보세요() {
System.out.println("나는 고양이! 야옹! 야옹!");
}
}
public class Driver {
public static void main(String[] args) {
동물[] 동물들 = new 동물[2];
동물들[0] = new 쥐();
동물들[1] = new 고양이();
동물들[0].울어보세요();
동물들[1].울어보세요();
}
}
여기서 만약 동물 클래스의 인스턴스를 생성한다면 어떻게 울어야 할까?
그렇다고 메서드의 몸체를 비워두는 것도 이상하고 하위 클래스의 인스턴스가 가진 울어보세요() 메서드를 호출하고 있으니 상위 클래스인 동물의 울어보세요() 메서드는 반드시 존재해야 한다.
그런데 실수로 동물 클래스의 인스턴스를 만들고 그렇게 만들어진 동물 객체의 울어보세요() 메서드를 호출하면 난감해진다.
바로 이런 경우 추상 메서드를 사용하게 된다.
public abstract class 동물 {
abstract void 울어보세요();
}
추상 클래스는 인스턴스, 즉 객체를 만들 수 없는 클래스이기 때문에 동물 타입의 객체를 생성할 수 없다.
또한 동물을 상속한 하위 클래스에서 울어보세요() 메서드를 오버라이딩하지 않으면 컴파일 에러가 발생한다.
생성자
클래스의 인스턴스, 즉 객체를 만들 때마다 new 키워드를 사용한다.
new 클래스명()을 자세히 보면 열고 닫는 소괄호가 보인다.
이전에 사용했던 열고 닫는 소괄호는 메서드를 의미하는 것이었다.
즉, 클래스명() 도 메서드이다.
반환값이 없고 클래스명과 같은 이름을 가진 메서드를 객체를 생성하는 메서드라고 해서 객체 생성자 메서드라고 한다.
또한 개발자가 아무런 생성자도 만들지 않으면 자바는 인자가 없는 기본 생성자를 자동으로 만들어주며, 인자가 있는 생성자를 하나라도 만든다면 자바는 기본 생성자를 만들어주지 않는다.
클래스 생성 시의 샐행 블록, static 블록
객체 생성자가 있다면 클래스 생성자도 있을 거라고 기대해 볼 만 하지만 클래스 생성자는 존재하지 않는다.
그러나 클래스가 스태틱 영역에 배치될 때 실행되는 코드 블록이 있다. 바로 static 블록이다.
public class 동물 {
static {
System.out.println("동물 클래스 레디 온!");
}
}
public class Driver01 {
public static void main(String[] args) {
동물 뽀로로 = new 동물();
}
}
//실행
동물 클래스 레디 온!
이번에는 다른 테스트 코드를 만들어보자.
public class Driver02 {
public static void main(String[] args) {
System.out.println("main 메서드 시작!");
}
}
//실행
main 메서드 시작!
동물 클래스를 사용하는 코드가 없기에 동물 클래스의 static 블록을 실행하지 않는다.
심지어 동물 클래스는 T 메모리 스태틱 영역에 자리 잡지도 않는다.
static 블록에서 사용할 수 있는 속성과 메서드는 당연히 static 멤버 뿐이다.
클래스의 static 블록이 실행되고 있을 때는 해당 클래스의 객체는 하나도 존재하지 않기 때문에 static 블록에서는 객체 멤버에 접근할 수 없기 때문이다.
→ 클래스가 로드되는 시점에 static 블록이 실행되기 때문
또한 같은 클래스의 인스턴스를 여러 개 만들어도 static 블록은 단 한 번만 실행된다.
public class Driver04 {
public static void main(String[] args) {
System.out.println("main 메서드 시작!");
동물 뽀로로 = new 동물();
동물 피카츄 = new 동물();
}
}
//실행
main 메서드 시작!
동물 클래스 레디 온!
정리해보자. 클래스 정보는 해당 클래스가 코드에서 맨 처음 사용될 때 T 메모리의 스태틱 영역에 로딩되며, 이 때 단 한 번 해당 클래스의 static 블록이 실행된다.
여기서 클래스가 제일 처음 사용될 때는 다음 세 가지 경우 중 하나다.
- 클래스의 정적 속성을 사용할 때
- 클래스의 정적 메서드를 사용할 때
- 클래스의 인스턴스를 최초로 만들 때
왜 프로그램이 실행될 때 바로 클래스들의 정보를 static 영역에 로딩하지 않고 해당 클래스가 처음 사용될 때 로딩할까?
스태틱 영역도 메모리이기 때문이다.
메모리는 최대한 늦게 사용을 시작하고 최대한 빨리 반환하는 것이 정석이다.
물론 스태틱 영역에 한 번 올라가면 프로그램이 종료되기 전까지는 해당 메모리를 반환할 수 없지만 그럼에도 최대한 늦게 로딩함으로써 메모리 사용을 최대한 늦추기 위해서다.
final 키워드
final은 마지막, 최종이라는 의미를 가진 단어다.
final 클래스 : 상속을 허락하지 않는다.
final 변수 : 변경 불가능한 상수가 된다.
final 메서드 : 오버라이딩을 금지한다.
정적 상수는 선언 시 또는 static 블록 내부에서 초기화가 가능하다.
객체 상수 역시 선언 시 또는 객체 생성자 또는 인스턴스 블록에서 초기화가 가능하다.
지역 상수 역시 선언 시에 또는 최초 한 번만 초기화가 가능하다.
instanceof 연산자
객체가 특정 클래스의 인스턴스인지 물어보는 연산자로 true 또는 false를 반환한다.
객체 참조 변수의 타입이 아닌 실제 객체의 타입에 의해 처리한다.
class 펭귄 extends 조류 {
...
}
class 조류 extends 동물 {
...
}
public class Driver {
public static void main(String[] args) {
동물 조류객체 = new 조류();
동물 펭귄객체 = new 펭귄();
System.out.println(펭귄객체 instanceof 조류);
System.out.println(펭귄객체 instanceof 동물);
}
}
//결과
true
true
instanceof 연산자가 강력하기는 하지만 객체 지향 설계 5원칙 가운데 LSP(리스코프 치환 원칙)을 어기는 코드에서 주로 나타나는 연산자이기에 코드에 instanceof 연산자가 보인다면 리팩터링 대싱이 아닌지 점검해 봐야 한다.
instanceof 연산자는 인터페이스의 구현 관계에서도 동일하게 적용된다.
package 키워드
package 키워드는 네임스페이스(이름공간)를 만들어주는 역할을 한다.
interface 키워드와 implements 키워드
인터페이스는 public 추상 메서드와 public 정적 상수만 가질 수 있다.
interface Speakable {
double PI = 3.14159;
final double absoluteZeroPoint = -275.15;
void sayYes();
}
그런데 위 코드를 보면 이상한 점이 보이지 않는가?
인터페이스는 추상 메서드와 정적 상수만 가질 수 있기에 따로 메서드에 public과 abstract, 속성에 public과 static, final을 붙이지 않아도 자동으로 자바가 붙여준다.
interface Speakable {
public static final double PI = 3.14159;
public static final double absouluteZeroPoint = =275.15;
public abstract void sayYes();
}
위 코드를 정석대로 작성하면 아래 코드가 되는 것이다.
여기서 한 가지 짚고 넘어갈 것이 있다.
2014년 자바 8이 출시되고 람다(Lambda)라고 하는 기능이 추가되었다.
람다란 함수를 의미하고, 변수에 할당할 수 있다. 함수는 로직이다. 이를 삼단 논법으로 전개해보면 결국 “람다는 변수에 저장할 수 있는 로직이다.”라고 할 수 있다.
변수는 값을 저장할 수 있고, 메서드의 인자로 쓰일 수 있고, 메서드의 반환값으로 사용할 수 있다. 결국 람다로 인해 변수에 로직을 저장할 수 있고, 로직을 메서드의 인자로 쓸 수 있고, 로직을 메서드의 반환값으로 사용할 수 있다는 결론에 도달한다.
이것은 함수형 언어가 지닌 특성을 자바도 수용했다는 것을 의미한다.
자바에서 람다는 인터페이스를 기초로 하고 있으며 이에 따라 인터페이스에도 큰 변화가 생겼다.
자바 8부터는 디폴트 메서드라고 하는 객체 구상 메서드와 정적 추상 메서드를 지원할 수 있게 언어 스펙이 바뀌었다.
this 키워드
this는 객체가 자기 자신을 지칭할 때 쓰는 키워드다. 마치 일상생활에서 “나”라고 하는 대명사와 같은 것이라고 보면 된다.
class 펭귄 {
int var = 10;
void test() {
int var = 20;
System.out.println(var);
System.out.println(this.var);
}
}
public class Driver {
public static void main(String[] args) {
펭귄 뽀로로 = new 펭귄();
뽀로로.test();
}
}
7번째 줄은 var 변수에 있는 값을 출력하라는 명령이다.
그런데 var 변수는 지역 변수도 있고 객체 변수도 존재한다.
당연히 test() 메서드 내부의 지역 변수 var에 우선권이 있다.
만약 여기서 지역 변수 var이 아닌 객체 변수 var의 값을 가져오고 싶다면 this 키워드를 사용하면 된다.
- 지역 변수와 속성의 이름이 같은 경우 지역 변수가 우선한다.
- 객체 변수와 이름이 같은 지역 변수가 있는 경우 객체 변수를 사용하려면 this를 접두사로 사용한다.
- 정적 변수와 이름이 같은 지역 변수가 있는 경우 정적 변수를 사용하려면 클래스명을 접두사로 사용한다.
class 펭귄 {
static int var2 = 15;
int var1 = 10;
void test() {
int var1 = 20;
int var2 = 25;
System.out.println(var1);
System.out.println(this.var1);
System.out.println(var2);
System.out.println(펭귄.var2);
}
}
public class Driver {
public static void main(String[] args) {
펭귄 뽀로로 = new 펭귄();
뽀로로.test();
}
}
//결과
20
10
25
15
super 키워드
super는 상속 관계에서 바로 위 상위 클래스의 인스턴스를 지칭하는 키워드이다.
super 키워드로 바로 위의 상위 클래스 인스턴스에는 접근할 수 있지만 super.spuer 형태로 상위의 상위 클래스의 인스턴스에는 접근이 불가능하다.
'Reading > 스프링 입문을 위한 자바 객체지향의 원리와 이해' 카테고리의 다른 글
7. 스프링 삼각형과 설정 정보_IoC/DI (0) | 2023.04.01 |
---|---|
5. 객체 지향 설계 5원칙 - SOLID (0) | 2023.03.29 |
3. 자바와 객체 지향 (0) | 2023.03.23 |
2. 자바와 절차적/구조적 프로그래밍 (0) | 2023.03.21 |
1. 사람을 사랑한 기술 (0) | 2023.03.21 |