객체 지향의 4대 특성을 넘어, 자바가 객체 지향을 확장하기 위해 사용하는 키워드와 개념 살펴보기
abstract 키워드 - 추상 메서드와 추상 클래스
추상 메서드(Abstract Method)
: 선언부는 있는데, 구현부가 없는 메서드
: 추상 메서드를 하나라도 갖고 있는 클래스는 반드시 추상 클래스(Abstact Class)로 선언해야함(추상 메서드 없이도 추상 클래스 선언 가능)
몸체가 없이 선언만 있는 메서드는 왜 필요?
package abstractMethod01;
public class 동물 {
void 울어보세요() {
System.out.println("나는 동물! 어떻게 울어야 하나요?");
}
}
1. 동물 클래스 인스턴스는 어떻게 울어야함? 소리 내어 울게 하는 것 자체가 논리에 맞지 않음. 그렇다고 객체 멤버 메서드의 몸체를 {} 로 비워 두는 것도 이상함
2. 동물 타입의 참조 변수를 통해 하위 클래스의 인스턴스가 가진 울어보세요() 메서드를 호출하고 있음
-> 상위 클래스인 동물의 울어보세요() 메서드는 반드시 존재해야함!
-> 실수로, 동물 클래스의 인스턴스를 만들고 울어보세요()를 오버라이딩 하지 않고 호출하면 난감해짐
이런 경우, 추상 메서드 사용!!
package abstractMethod02;
public abstract class 동물 {
abstract void 울어보세요();
}
하위 클래스가 추상 메서드 구현(오버라이딩) 안하면 컴파일 시점에 "Cannot instantiate the type" 동물 오류 뜸
-> 추상 클래스는 인스턴스, 즉 객체를 만들 수 없는 클래스가 됨
- 추상 클래스는 인스턴스, 즉 객체를 만들 수 없다. 즉, new를 사용할 수 없다.
- 힙 메모리에 생성되는 객체는 내부 요소가 미완성이 상태로 들어갈 수 없음!
- 추상 메서드는 하위 클래스에게 메서드 구현을 강제한다. 오버라이딩 강제
- 추상 메서드를 포함하는 클래스는 반드시 추상 클래스여야한다.
추상 클래스의 객체 생성 방법 2가지(자식 클래스 생성 여부에 따라)
1. 일반 클래스로 상속해 객체 생성
abstract class Animal{
public abstract void cry(); //추상 메서드 {}가 없다.
}
class Cat extends Animal{
@Override
public void cry() {
System.out.println("야옹스");
}
}
class Dog extends Animal{
@Override
public void cry() {
System.out.println("멍멍");
}
}
public class Main {
public static void main(String[] args) {
Animal cat = new Cat();
Animal dog = new Dog();
cat.cry();
dog.cry();
}
}
장점: 여러 개의 객체를 생성해야할 경우 좋다.
단점: 자식 클래스를 정의해야 한다.
2. 익명 이너 클래스 사용
public class Main {
public static void main(String[] args) {
//익명 이너 클래스
Animal cat = new Animal() {
@Override
public void cry() {
System.out.println("야옹스");
}
};
cat.cry(); //야옹스 출력
장점: 자식 클래스를 정의하지 않아도 됨
단점: 일회성이기 때문에 여러 객체를 만들 경우에는 불편 -> 이벤트, 안드로이드 등에서 자주 쓰이는 방법
생성자
클래스의 인스턴스, 즉 객체를 만들 때마다 new 키워드 사용
new 클래스명()
클래스 명에 붙는 소괄호 -> 클래스명()도 메서드!
반환값이 업고, 클래스 명과 같은 이름을 가진 메서드
= 객체를 생성하는 메서드
= 객체 생성자 메서드
= 생성자
컴파일 과정에서 자바 컴파일러가 알아서 인자가 없는 기본 생성자를 자동으로 만들어줌!
동물 클래스에는 아무런 메서드가 없는 것처럼 보임. 그러나 하나의 메서드가 존재!
-> 아무런 인자도 갖지 않는 기본 생성자 메서드!
필요하다면 인자를 갖는 생성자를 더 만들 수 있음
package constructor03;
public class 동물 {
public 동물(String name) {
System.out.println(name);
}
}
그러나 이런 생성자가 만들어지면, 아래 코드에서 new 동물() 밑에 빨간 밑줄 표시, 컴파일 거부!
package constructor03;
public class Driver02 {
public static void main(String[] args) {
동물 뽀로로 = new 동물("뽀로로");
//동물 무명 = new 동물(); // 주석을 제거하면 컴파일 에러 발생
}
}
기억해야할 자바의 특징
- 개발자가 아무런 생성자도 만들지 않으면, 자바는 인자가 없는 기본 생성자를 자동으로 만들어줌
- 인자가 있는 생성자를 하나라도 만든다면, 자바는 기본 생성자를 만들어주지 않음
생성자는 개발자가 필요한 만큼 오버로딩해서 만들 수 있음.
생성자=객체 생성자 메서드
클래스 생성 시의 실행 블록, static 블록
클래스 생성자는 존재하지 않음.
But, 클래스가 스태틱 영역에 배치될 때 실행되는 코드 블록이 있음: static 블록
동물 클래스를 사용하는 코드가 없으면, 동물 클래스의 static 블록을 실행하지 않음.
동물 클래스가 T 메모리 스태틱 영역에 자리 잡지도 않음
static 블록에서 사용할 수 있는 속성과 메서드는 당연히 static 멤버 뿐
T 메모리를 그려보면, 객체 멤버에는 접근할 방법이 없다는걸 알 수 있음!
-> 객체 멤버는 클래스가 static 영역에 자리 잡은 후, 객체 생성자를 통해 힙에 생성됨
-> 클래스의 static 블록이 실행되고 있을 때는 해당 클래스의 객체는 하나도 존재하지 않기 때문에 static 블록에서 객체 멤버에 접근할 수 없음
앞에서는 설명을 쉽게 하기 위해 프로그램이 시작될 때 모든 패키지와 클래스가 T 메모리의 스태틱 영역에 로딩 된다고 함
-> 실제로는, 해당 패키지 또는 클래스가 처음으로 사용될 때 로딩됨
package staticBlock;
public class Driver03 {
public static void main(String[] args) {
System.out.println("main 메서드 시작!");
동물 뽀로로 = new 동물();
}
}
결과는?
main 메서드 시작!
동물 클래스 레디 온!
즉, 동물 클래스의 static 블록보다 main() 메서드의 실행문이 먼저 실행됨
동물 클래스의 인스턴스를 여러개 만들어도 동물 클래스의 static 블록은 단 한번만 실행됨
package staticBlock;
public class Driver05 {
public static void main(String[] args) {
System.out.println("main 메서드 시작!");
System.out.println(Animal.age);
}
}
class Animal {
static int age = 0;
static {
System.out.println("Animal class ready on!");
}
}
실행결과:
main 메서드 시작!
Animal class ready on!
0
정리해보자.
- 클래스 정보는 해당 클래스가 코드에서 맨 처음 사용될 때 T 메모리의 스태틱 영역에 로딩
- 단 한번 해당 클래스의 static 블록이 실행됨
- 클래스의 정적 속성 사용할 때
- 클래스의 정적 메소드 사용할 때
- 클래스의 인스턴스를 최초로 만들 때
왜 실행될 때 바로 클래스의 정보들을 T 메모리의 static 영역에 로딩하지 않고, 해당 클래스가 처음 사용될 때 로딩????
-> 스태틱 영역도 메모리!
-> 메모리는 최대한 늦게 사용 시작, 최대한 빨리 반환이 정석
자바는 스태틱 영역에 한번 올라가면 프로그램이 종료되기 전까지는 해당 메모리를 반환할 수 없지만,
그럼에도 최대한 늦게 로딩함으로써 메모리 사용을 최대한 늦춤
실무에서는 static 블록을 사용할 일이 거의 없지만, 그래도 static 블록의 특성을 기억해두면 언젠가 한번은 긴요하게 써먹을 것!
더 깊은 관심이 있다면 JUnit의 @BeforeClass 어노테이션 살펴보기
참고)
static 블록과 유사하게 클래스의 인스턴스를 위한 인스턴스 블록도 존재
아무런 표시 없이 {} 블록 사용하게 되면, 인스턴스가 생성될 때 마다 {} 블록 실행됨
{} 블록은 객체 생성자가 실행되기 전에 먼저 실행됨
객체는 주로 생성자를 통해 초기화하기 때문에 {} 블록 사용할 일은 없지만,
필자가 최근 Spring Batch 프로젝트를 살펴보다 {} 블록 사용되는 코드를 보고 언급ㅎ봄
다음 URL에서 initializing instance members 부분 참고
https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html
final 키워드
마지막, 최종
final 키워드가 나타날 수 있는 곳 = 객체 지향 언어의 구성 요소 = 클래스, 변수, 메서드
final과 클래스
package finalClass;
public final class 고양이 {}
상속을 허락하지 않겠다는 의미
public class 길고양이 extends 고양이{} 와 같이 하위 클래스를 만들 수 없음
컴파일러가 다음과 같은 에러 표시
The type 길고양이 cannot subclass the final class 고양이
final과 변수
package finalVariable;
public class 고양이 {
final static int 정적상수1 = 1;
final static int 정적상수2;
final int 객체상수1 = 1;
final int 객체상수2;
static {
정적상수2 = 2;
// 상수는 한번 초기화 되면 값을 변경할 수 없다.
// 정적상수2 = 4;
}
고양이() {
객체상수2 = 2;
// 상수는 한번 초기화 되면 값을 변경할 수 없다.
// 객체상수2 = 4;
final int 지역상수1 = 1;
final int 지역상수2;
지역상수2 = 2;
}
}
변경 불가능한 상수가 됨
정적 상수는 선언 시 or 정적 생성자에 해당하는 static 블록 내부에서 초기화 가능
객체 상수는 선언 시 or 객체 생성자 or 인스턴스 블록에서 초기화 가능
지역 상수는 선언 시 or 최초 한번만 초기화 가능
다른 언어에서는 읽기 전용 상수에 대해 const 키워드 사용 -> 자바에서 이런 혼동 피하기 위해 const를 키워드 등록, not used 하게 함
final과 메서드
package finalMethod;
public class 동물 {
final void 숨쉬다() {
System.out.println("호흡 중");
}
}
class 포유류 extends 동물 {
// 에러 발생: Cannot override the final method from 동물
/*
* void 숨쉬다() { System.out.println("호흡 중"); }
*/
}
재정의, 즉 오버라이딩 금지
instanceof 연산자
인스턴스 = 클래스를 통해 만들어진 객체
instanceof 연산자: 만들어진 객체가 특정 클래스의 인스턴스인지 물어보는 객체 -> true/false 반환
객체_참조_변수 instanceof 클래스명
package instanceOf01;
class 동물 {
}
class 조류 extends 동물 {
}
class 펭귄 extends 조류 {
}
public class Driver {
public static void main(String[] args) {
동물 동물객체 = new 동물();
조류 조류객체 = new 조류();
펭귄 펭귄객체 = new 펭귄();
System.out.println(동물객체 instanceof 동물);
System.out.println(조류객체 instanceof 동물);
System.out.println(조류객체 instanceof 조류);
System.out.println(펭귄객체 instanceof 동물);
System.out.println(펭귄객체 instanceof 조류);
System.out.println(펭귄객체 instanceof 펭귄);
System.out.println(펭귄객체 instanceof Object);
}
}
모두 true
package instanceOf02;
class 동물 {
}
class 조류 extends 동물 {
}
class 펭귄 extends 조류 {
}
public class Driver {
public static void main(String[] args) {
동물 동물객체 = new 동물();
동물 조류객체 = new 조류();
동물 펭귄객체 = new 펭귄();
System.out.println(동물객체 instanceof 동물);
System.out.println(조류객체 instanceof 동물);
System.out.println(조류객체 instanceof 조류);
System.out.println(펭귄객체 instanceof 동물);
System.out.println(펭귄객체 instanceof 조류);
System.out.println(펭귄객체 instanceof 펭귄);
System.out.println(펭귄객체 instanceof Object);
}
}
모두 true
객체 참조 변수의 타입이 아닌 실제 객체의 타입에 의해 처리하기 때문
instanceof 연산자는 강력하지만, 객체 지향 설계 5원칙 가운데 LSP(리스코프 치환 원칙)를 어기는 코드에서 주로 나타나는 연산자
-> 이 연산자가 보인다면, 냄새나는 코드인지, 리팩터링의 대상이 아닌지 점검해봐야함!
클래스 상속 관계 뿐 아니라 인터페이스의 구현관계에서도 동일하게 적용됨
package instanceOf03;
interface 날수있는 {
}
class 박쥐 implements 날수있는 {
}
class 참새 implements 날수있는 {
}
public class Driver {
public static void main(String[] args) {
날수있는 박쥐객체 = new 박쥐();
날수있는 참새객체 = new 참새();
System.out.println(박쥐객체 instanceof 날수있는);
System.out.println(박쥐객체 instanceof 박쥐);
System.out.println(참새객체 instanceof 날수있는);
System.out.println(참새객체 instanceof 참새);
}
}
모두 true 반환
package 키워드
네임스페이스(이름 공간)를 만들어주는 역할을 함
네임스페이스의 필요성
같은 이름의 클래스끼리 이름 충돌이 발생
-> 이름 공간을 나누어, 고객사업부.Customer, 마케팅사업부.Customer 라고 클래스의 전체 이름 지정
-> 스마트폰도 '누구의 스마트폰' 이러면 구분이 됨
interface 키워드와 inmplements 키워드
인터페이스는 public 추상 메서드, public 정적 상수만 가질 수 있음
근데 아래 예제를 보자
package Interface;
interface Speakable {
double PI = 3.14159;
final double absoluteZeroPoint = -275.15;
void sayYes();
}
/*
* interface Speakable { public static double PI = 3.14159; public static final
* double absoluteZeroPoint = -275.15;
*
* public abstract void sayYes(); }
*/
class Specker implements Speakable {
public void sayYes() {
System.out.println("I say NO!!!");
}
}
public class Driver {
public static void main(String[] args) {
System.out.println(Speakable.absoluteZeroPoint);
System.out.println(Speakable.PI);
Specker reporter1 = new Specker();
reporter1.sayYes();
}
public static void test() {
// 에러: The final field Speakable.PI cannot be assigned
// Speakable.PI = 3.14;
// 에러: The final field Speakable.absoluteZeroPoint cannot be assigned
// Speakable.absoluteZeroPoint = -275.0;
}
}
PI와 absoluteZeroPoint 변수에 정적 멤버를 나타내는 static 키워드가 안보임 -> 객체 속성인가?!
sayYes() 메서드는 추상을 의미하는 abstract 키워드 안보임
=> 제임스 고슬링과 자바 언어 설계팀의 배려!
인터페이스는 추상 메서드와 정적 상수만 가질 수 있기에 따로 메서드에 public, abstract, 속성에 public, static, final을 붙이지 않아도 자동으로 자바가 알아서 붙여줌
결국 아래와 동일
interface Speakable {
public static final double PI = 3.14159;
public static final double absoluteZeroPoint = -275.15;
public abstract void sayYes();
}
어느 것이 더 좋냐면,, 정답은 없고 모범 답안은 있다. public, static, final, abstact이 붙은 코드! -> 명확하니까!
인터페이스의 메서드가 추상 메서드인 증거
추상 메서드: 몸체가 없는 메서드
-> 그래서 public, abstract 키워드가 없는 예제를 실행해도 오류가 안남
Speakable.PI: 이렇게 접근하면 빨간 밑줄! cannot be assigned
-> 변수에 값을 할당할 수 없음 = 상수, 즉 final 변수라는 의미!
-> 클래스명으로 접근할 수 있는 속성은 정적 속성!
고로 PI와 absoluteZeroPoint는 final static 멤버임!
2014 오라클에서 빅데이터와 병렬성 지원을 강화한 자바8 출시
이를 위해 자바는 컬렉션을 강화, 람다(Lambda)라고 하는 기능을 언어적으로 추가
이 책은 자바6 기준이라 람다와 기타 자바7, 8의 변화를 설명하고 있지 않음
람다는 별도로 학습!
람다: 함수를 의미, 변수에 할당할 수 있음. 함수는 로직!
즉, 람다는 "변수에 저장할 수 있는 로직"
변수는 값을 저장할 수 있고, 메서드의 인자로 쓰일 수 있고, 메서드의 반환값으로 사용할 수 있음.
결국 람다로 인해 변수에 로직을 저장할 수 있고, 로직을 메서드로 쓸 수 있고, 로직을 메서드의 반환값으로 사용할 수 있음
-> 함수형 언어가 지닌 특성을 자바도 수용함!
람다는 인터페이스를 기초로 하고 있음
자바8 이전까지 인터페이스는 정적 상수, 객체 추상 메서드만 가질 수 있었음
자바8부터는 디폴트 메서드라고 하는 객체 구상 메서드, 정적 추상 메서드를 지원할 수 있게 되어 언어 스펙이 바뀜!
-> 하위 호환성 유지하면서 인터페이스 보완 진행
this 키워드
객체가 자기 자신을 지칭할 때 쓰는 키워드
"나"라고 하는 대명사와 같음
package 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();
}
}
class 선언 끝난 후의 T 메모리 스냅샷
var 변수는 지역 변수도 있고, 객체 변수도 존재
System.out.println(var): test() 메서드 내부의 지역 변수 var에 우선권 있음 // 20 출력
System.out.println(this.var): 10 출력
즉, 지역 변수에 저장돼 있는 값이 아닌 객체 변수에 저장돼 있는 값을 사용하고 싶은 경우, this.var
- 지역 변수와 속성(객체 변수, 정적변수)의 이름이 같은 경우 지역 변수가 우선한다.
- 객체 변수와 이름이 같은 지역 변수가 있는 경우, 객체 변수를 사용하려면 this를 접두사로 사용
- 정적(static) 변수와 이름이 같은 지역 변수가 있는 경우, 정적 변수를 사용하려면 클래스명을 접두사로 사용
super 키워드
this는 객체 멤버 메서드 내부에서 객체 자신을 지칭하는 키워드
super는 단일 상속만을 지원하는 자바에서, 바로 위 상위 클래스의 인스턴스 지칭
package Super;
class 동물 {
void method() {
System.out.println("동물");
}
}
class 조류 extends 동물 {
void method() {
super.method();
System.out.println("조류");
}
}
class 펭귄 extends 조류 {
void method() {
super.method();
System.out.println("펭귄");
// Syntax error on token "super", Identifier expected
// super.super.method();
}
}
public class Driver {
public static void main(String[] args) {
펭귄 뽀로로 = new 펭귄();
뽀로로.method();
}
}
super 키워드로 바로 위의 상위 클래스 인스턴스에는 접근 가능하지만,
super.super 형태로 상위의 상위 클래스의 인스턴스에는 접근 불가능ㅋㅋㅋㅋㅋ
예비 고수를 위한 한마디
객체 메서드를 호출할 때, STS 스택 정보를 보면
객체명.객체메서드명()이 아닌 클래스명.객체메서드명()임을 확인할 수 있음
스택에 쌓여진 프레임은 위처럼
뽀로로.test()
Driver.main(...)
와 같은 형태여야 할 것 같은데, STS 에서 나타나는 모습은
펭귄.test()
Driver.main(...)
생각해보자.
만약 펭귄 객체가 뽀로로 하나가 아니라 펭귄[100]과 같이 요소가 100개인 배열이라면
힙 영역에 생기는 펭귄 객체는 100개, test() 메서드도 각 펭귄 객체에 따라 힙에 100개가 만들어져야함.
-> 심각한 메모리 낭비
근데, 객체 멤버 메서드는 각 객체별로 달라지는 것이 아님
객체 멤버 메서드에서 사용하는 객체 멤버 속성의 값이 다를 뿐
=> JVM은 지능적으로 객체 멤버 메서드 test()를 스태틱 영역에 단 하나 보유!
그리고 눈에 보이지는 않지만 test() 메서드를 호출할 때, 객체 자신을 나타내는 this 객체 참조 변수 넘긴다.
즉, 위의 코드는 JVM에 의해 아래와 같이 변경됨
정리 - 자바 키워드와 OOP 확장
기타에는 예외 처리 지원, 멀티 스레드 지원, 직렬화 지원 키워드가 보임
-> 추후 공부!
추가 참고
'SW Books > 스프링 입문을 위한 자바 객체지향의 원리와 이해' 카테고리의 다른 글
[자바 객체지향 원리] 6장 - 스프링이 사랑한 디자인 패턴 (1) | 2023.08.31 |
---|---|
[자바 객체지향 원리] 5장 - 객체 지향 설계 원칙 - SOLID (0) | 2023.08.28 |
[자바 객체지향 원리] 3장 - 자바와 객체 지향 (0) | 2023.08.10 |
[자바 객체지향 원리] 2장 - 자바와 절차적/구조적 프로그래밍 (1) | 2023.08.07 |
[자바 객체지향 원리] 1장 - 사람을 사랑한 기술 (1) | 2023.08.07 |