Java가 확장한 객체 지향
java가 객체 지향을 확장하기 위해 사용하는 키워드와 개념을 알아보자.
abstract 키워드 - 추상 메서드와 추상 클래스
추상 메서드(Abstract Method)
선언부는 있는데 구현부가 없는 메서드
추상 클래스(Abstract Class)
추상 메서드를 하나라도 갖고 있는 클래스
추상 메서드가 필요한 이유
package abstractMethod01;
public class Driver{
public static void main(String[] args){
동물[] 동물들 = new 동물[3];
동물들[0] = new 쥐();
동물들[1] = new 고양이();
동물들[2] = new 강아지();
for (int i=0;i<동물들.length;i++){
동물들[i].cry(); // 다형성 이용
}
}
}
package abstractMethod01;
public class 동물{
void cry(){
System.out.println("어떻게 울어야 할까? ");
}
}
package abstractMethod01;
public class 쥐 extends 동물{
void cry(){
System.out.println("찍찍");
}
}
package abstractMethod01;
public class 고양이 extends 동물{
void cry(){
System.out.println("야옹");
}
}
package abstractMethod01;
public class 강아지 extends 동물{
void cry(){
System.out.println("월월");
}
}
- 동물 타입의 참조 변수를 통해 하위 클래스의 인스턴스가 가진 cry() 메서드를 호출하므로 상위 클래스의 cry()는 존재해야한다. (다형성 이용)
- 하지만 동물은 너무 큰 분류라서 cry()에 대한 구현을 하기 어렵다.
=> 메서드 선언은 있되 몸체는 없는 abstract 메서드(추상 메서드)를 사용하자!
추상 메서드가 있는 추상 클래스 사용하기
package abstractMethod02;
public abstract class 동물{
abstract void cry(); // 메서드 선언만 존재
}
package abstractMethod02;
public class 고양이 extends 동물{
@Override
void cry() {
System.out.println("야옹");
}
}
package abstractMethod02;
public class Driver{
public static void main(String[] args) {
// 동물 animal = new 동물();
고양이 cat = new 고양이();
cat.cry(); // 야옹
}
}
- 추상 클래스는 인스턴스, 즉 객체를 만들 수 없다. (new 사용 불가)
- 추상 클래스를 상속한 하위 클래스에게 추상 메서드의 구현을 강제한다. (오버라이딩 강제)
- 추상 메서드를 포함한 클래스는 반드시 추상 클래스여야한다.
인터페이스와 추상 클래스
- 인터페이스와 추상 클래스 모두 추상 메서드를 통해 메서드 구현을 강제한다.
- 추상 클래스와 인터페이스는 존재 목적이 다르다.
- 추상 클래스 : 공통적인 기능을 추상 클래스에 정의함으로써 코드의 재사용과 확장성을 높이고, 하위 클래스에서 추상메서드를 구현함으로써 다형성을 이용한 유연한 설계를 할 수 있다. => 상속(재사용과 확장)
- 인터페이스 : 클래스들이 특정 메서드를 구현하도록 강제함으로써, 해당 기능의 구현을 보장한다. 다양한 구현체를 같은 방식으로 처리할 수 있다. => 일관된 기능 보장
- 인터페이스는 Java8부터 default 메서드와 static 메서드도 선언할 수 있다.
interface Calculator {
default void showOpening() {
System.out.println("계산기입니다.");
}
static int calculate(int a, int b) { return a + b; }
void speak();
}
- default 메서드 : 구현된 메서드, 인터페이스에 default 메서드 추가해도 기존 구현한 클래스들에게 영향을 주지 않고 호환성 유지 가능. 오버라이딩도 가능하다.
- static 메서드 : 유틸리티 메서드를 제공할 수 있다.
추상 클래스의 장점
Java8부터 인터페이스가 추상 클래스의 역할을 일부 대체할 수 있게 되었지만, 추상 클래스만이 가지는 장점이 있다.
- 상태(인스턴스 변수)를 가질 수 있다. (인터페이스는 정적 변수만 가진다)
- 생성자를 정의할 수 있다.
- 접근 범위를 제한할 수 있다. (인터페이스는 public 접근범위)
인터페이스는 계약(메서드의 시그니처)를 정의하는데 중점을 두고,
추상 클래스는 공통의 기반 기능과 상태를 제공하는데 중점을 둔다.
인터페이스에서 같은 이름의 default 함수가 존재할때
두 개 이상의 인터페이스를 클래스에서 구현할 수 있다.
이때 각각의 default 메서드의 이름이 같다면 다중 상속의 문제(다이아몬드 문제)가 발생하지 않을까?
다행히 그러한 문제를 방지하기위해 default 메서드명이 같다면 명시적으로 어떤 인터페이스의 default 메서드를 사용하는지 결정해야한다.
interface Animal {
default void move() {
System.out.println("동물이 움직입니다.");
}
}
interface Movable {
default void move() {
System.out.println("움직일 수 있는 객체가 움직입니다.");
}
}
class Dog implements Animal, Movable {
// 두 인터페이스의 move() 메서드 충돌 발생
@Override
public void move() {
// Animal 인터페이스의 move() 메서드 선택
Animal.super.move();
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.move(); // "동물이 움직입니다." 출력
}
}
인터페이스 기반의 계산기 구현 vs 추상 클래스 기반의 계산기 구현
- 추상 클래스 기반 계산기 : 계산기의 상태 관리가 필요하거나 여러 계산기에서 공통적으로 사용할 수 있는 기능(공통 코드)가 많은 경우 적합하다. 하나의 추상 클래스만 상속이 가능하다.
- 인터페이스 기반 계산기 : 한 클래스에서 여러 인터페이스를 구현할 수 있으므로 기능을 여러 인터페이스로 나누어 모듈화하거나 클래스 하나에서 여러 기능을 구현해야하는 경우 적합하다.
생성자(=객체 생성자 메서드)
객체를 생성하는 메서드, 반환값이 없고 클래스명과 같은 이름을 가진다.
- 생성자를 따로 작성하지 않으면 java가 알아서 인자가 없는 기본 생성자를 생성해준다.
package constructor01;
public class 동물{
// public 동물(){} // java가 알아서 기본 생성자를 만들어준다.
}
- 인자가 있는 생성자를 하나라도 만든다면 java는 기본 생성자를 만들어주지 않는다.
package constructor01;
public class 동물{
// 기본 생성자 안만들어준다.
public 동물(String name){
System.out.println(name);
}
}
package constructor01;
public class Driver{
public static void main(String[] args){
동물 뽀로로 = new 동물("뽀로로);
// 동물 무명 = new 동물(); // 컴파일 에러
}
}
static 블록
클래스가 static 영역에 배치될 때 실행되는 코드 블록
예제 코드
package prac;
public class 동물{
static{
System.out.println("동물 Ready");
}
}
package prac;
public class Driver{
public static void main(String[] args){
동물 뽀로로 = new 동물();
}
}
결과
동물 Ready
package prac;
public class Driver{
public static void main(String[] args){
System.out.println("메서드 시작!);
}
}
결과
메서드 시작!
- 여기서 동물 클래스의 static 블록이 실행되지 않은 이유는 동물 클래스를 사용하는 코드가 없기 때문이다.
- 앞서 설명할때는 프로그램이 시작될 때 모든 패키지와 모든 클래스가 메모리의 static 영역에 로딩된다고 했지만, 실제로는 해당 패키지 또는 클래스가 처음으로 사용될 때 로딩된다.
사용하는 패키지 또는 클래스만 로딩된다.
package prac;
public class Driver{
public static void main(String[] args){
System.out.println("메서드 시작!");
동물 뽀로로 = new 동물();
}
}
결과
메서드 시작!
동물 Ready
클래스의 인스턴스를 여러개 만들어도 해당 클래스의 static 블록은 단 한번 실행된다.
package prac;
public class Driver{
public static void main(String[] args){
System.out.println("메서드 시작!");
동물 뽀로로 = new 동물();
동물 모카 = new 동물();
}
}
결과
메서드 시작!
동물 Ready
static 블록이 실행되는 기준
- 클래스의 정적 속성을 사용할 때 => 클래스 로딩 후 static 블록 실행
- 클래스의 정적 메서드를 사용할 때 => 클래스 로딩 후 static 블록 실행
- 클래스의 인스턴스를 최초로 만들 때 => 클래스 로딩 후 static 블록 실행
package prac;
public class Driver{
public static void main(String[] args){
System.out.println("main() 메서드 실행");
System.out.println(Animal.age);
}
}
class Animal{
static int age = 5;
static {
System.out.println("Animal Ready");
}
}
Driver 클래스 로딩 => main() 실행 => "main() 메서드 실행 " 출력
Animal 클래스 로딩 => 정적 변수 및 static 블록 실행 => "Animal Ready" 출력
Animal.age 출력
결과
main() 메서드 실행
Animal Ready
5
왜 모든 클래스를 메모리에 미리 올려두지 않고 사용시에 로딩할까?
=> static 영역도 메모리이기 때문에 최대한 늦게 로딩함으로써 메모리 사용량을 최대로 늦추기 위해서이다.
실무에서 static 블록을 사용할 일은 거의 없지만 JUnit의 @BeforeClass 어노테이션을 살펴봐도 좋다
- 아래 글에 정리해 보았다.
https://shout-to-my-mae.tistory.com/413
final 키워드
final = 최종
객체 지향의 구성 요소인 클래스, 변수, 메서드에서 사용할 수 있다.
final 클래스
package finalClass;
public final class 고양이{}
final + 클래스 : 상속을 허락하지 않는다.
final과 변수
package finalClass;
public class 고양이{
final static int 정적상수1 = 1;
final static int 정적상수2;
final int 객체상수1 = 1;
final int 객체상수2;
static{
정적상수2 = 2;
}
고양이(){
객체상수2 = 2;
final int 지역상수1 = 1;
final int 지역상수2;
지역상수2 = 2;
}
}
final + 변수 : 변경 불가능한 상수이다.
정적 상수, 객체 상수, 지역 상수 모두 선언시 초기화하거나 선언 후 초기화할 수 있다.
final과 메서드
public class 동물 {
final void 숨쉬다(){
System.out.print("호흡 중");
}
}
class 포유류 extends 동물{
// void 숨쉬다(){} // 컴파일러 에러 발생
}
final 메서드 = 최종 메서드
오버라이딩(재정의)가 금지된다.
instanceof 연산자
만들어진 객체가 특정 클래스의 인스턴스인지 물어보는 연산자이다.
객체_참조_변수 instanceof 클래스명
클래스 상속 관계
class 동물{
}
class 조류 extends 동물{
}
class 펭귄 extends 조류{
}
pubic class Driver{
public static void main(String[] args){
동물 동물객체 = new 동물();
조류 조류객체 = new 조류();
동물 펭귄객체 = new 펭귄();
System.out.println(동물객체 instanceof 동물); // true
System.out.println(조류객체 instanceof 동물); // true
System.out.println(조류객체 instanceof 조류); // true
System.out.println(펭귄객체 instanceof 동물); // true
System.out.println(펭귄객체 instanceof 조류); // true
System.out.println(펭귄객체 instanceof 펭귄); // true
}
}
- 객체 참조 변수의 타입이 아닌 실제 객체의 타입에 의해 처리된다.
instanceof 연산자는 LSP(리스코프 치환 원칙)을 어기는 코드에서 주로 나타난다.
리팩터링의 대상이 아닌지 점검해보아야한다.
인터페이스 구현 관계
interface 날수있는{
}
class 박쥐 implements 날수있는{
}
class 참새 implements 날수있는{
}
public class Driver{
public static void main(String[] args){
날수있는 박쥐객체 = new 박쥐();
날수있는 참새객체 = new 참새();
System.out.println(박쥐객체 instanceof 날수있는); // true
System.out.println(박쥐객체 instanceof 박쥐); // true
System.out.println(참새객체 instanceof 날수있는); // true
System.out.println(참새객체 instanceof 참새); // true
}
}
인터페이스 구현관계에서도 동일하게 적용이 가능하다.
package 키워드
네임스페이스(이름 공간)을 만들어주는 역할을 한다.
같은 클래스명을 여러 개발팀에서 사용할 때, 이름 충돌을 피하고 각각을 소유할 수 있다.
ex) 고객사업부.Customer, 마케팅사업부.Customer
interface 키워드와 implements 키워드
인터페이스는 public 추상 메서드와 public 정적 상수만 가질 수 있다.
interface Speakable{
double PI = 3.14159;
final double absoluteZeroPoint = -275.15;
void sayYes();
}
class Sepcker 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);
Speakable reporter1 = new Speakable();
reporter1.sayYes(); // I say NO!!
}
}
- 메서드에 따로 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이나 public abstract 붙여주는게 좋다.
람다(Lambda)
Java8에서 람다(Lambda) 기능을 추가했다.
람다란 함수를 의미하고, 변수에 할당할 수 있다. 함수는 로직을 나타낸다.
즉, 람다는 변수에 저장할 수 있는 로직이다.
=> 변수에 로직을 저장할 수 있고, 로직을 메서드의 인자로 쓸 수 있고, 로직을 메서드의 반환값으로 사용할 수 있다.
=> Java에서 람다는 인터페이스를 기초로 하고 있기 때문에, 정적 상수(public static final)와 객체 추상 메서드(public abstract) 뿐만 아니라 default 메서드와 정적 메서드(static)를 지원할 수 있도록 언어 스펙이 바뀌었다.
this 키워드
this : 객체가 자기 자신을 지칭할 때 쓰는 키워드
package this;
class 펭귄{
int var = 10;
void test(){
int var = 20;
System.out.println(var); // 20 (지역 변수)
System.out.println(this.var); // 10 (객체 변수)
}
}
public class Driver{
public static void main(String[] args){
펭귄 뽀로로 = new 펭귄();
뽀로로.test();
}
}
- 지역변수와 속성(객체 변수, 정적 변수)의 이름이 같은 경우 지역 변수가 우선한다.
- 객체 변수와 이름이 같은 지역 변수가 있는 경우 객체 변수를 사용하려면 this를 접두사로 사용한다.
- 정적 변수와 이름이 같은 지역 변수가 있는 경우 정적 변수를 사용하려면 클래스명을 접두사로 사용한다.
super 키워드
- 상위 클래스의 인스턴스를 지칭하는 키워드
class 동물 {
void test(){
System.out.println("동물");
}
}
class 조류 extends 동물{
void test(){
super.test();
System.out.println("조류");
}
}
class 펭귄 extends 조류{
void test(){
super.test();
System.out.println("펭귄");
}
}
public class Driver{
public static void main(String[] args){
펭귄 뽀로로 = new 펭귄();
뽀로로.test();
}
}
결과
동물
조류
펭귄
뽀로로.method()를 호출할 때 생기는 일
뽀로로.method() 를 호출하면 펭귄.method() 가 호출된다.
객체 멤버 메서드는 각 객체별로 로직이 달라지는 것이 아니고, 객체 멤버 속성의 값만 다를 뿐이다.
그래서 JVM은 지능적으로 객체 멤버 메서드 test()를 static 영역에 단 하나만 보유한다.
그리고 test() 메서드를 호출할 때 객체 자신을 나타내는 this 객체 참조 변수를 넘긴다.
실제 호출시 코드
class 펭귄 extends 조류{
static void test(펭귄 this){
super.test();
System.out.println("펭귄");
}
}
public class Driver{
public static void main(String[] args){
펭귄 뽀로로 = new 펭귄();
펭귄.test(뽀로로);
}
}
- 호출한 객체를 넘긴다.
실제 호출시 메모리
- heap에 배치된 객체가 각각 메서드들을 가지고 있지않고, static에 하나씩만 위치한다.
- 메서드 호출시 호출한 객체의 정보를 this로 넘긴다.
Reference
https://m.yes24.com/Goods/Detail/17350624
+ 책 내용 기반으로 추가적으로 내용을 작성했는데 오개념이 있다면 지적 부탁드립니다~!!
'Spring > 객체지향' 카테고리의 다른 글
코드에 SRP 원칙 적용 후 Mock 테스트 작성하기 (0) | 2024.04.06 |
---|---|
객체 지향과 디자인 패턴 (0) | 2024.04.02 |
객체 지향 설계 5원칙: SOLID (0) | 2024.04.01 |
Java와 객체 지향 : OOP 4대 특성 (0) | 2024.03.25 |
프로그래밍의 발전 및 Java와 절차적/구조적 프로그래밍 (2) | 2024.03.23 |