본 게시글은 대학 전공수업을 들으며 노션에 정리한 내용을 블로그로 옮긴 것으로, 노션 웹을 통해 최적화된 형태로 읽으시길 권장드립니다.(➡️ 노션 링크)

5강 - 추상 클래스, 인터페이스, 다형성, enum, 익명클래스, 중첩클래스

//추상클래스
abstract class Shape { 

    //어떤 도형인지 모르므로, 추상메소드로 선언
  abstract public double getArea(); 
}

추상메소드

  • 메소드 정의에 abstract 키워드를 사용
  • 몸체 구현 없이 형식만 존재
    • ⇒ 상속 될 때 반드시 구현이 필요

✅ 완전히 못박은 final과는 상반된 의미 → 함께 사용 x

✅ 추상메소드는 추상클래스 또는 인터페이스와 함께 사용되어야 한다.

추상 클래스

정의

  • 클래스 정의에 abstract 키워드를 사용
  • 추상 메소드를 포함
  • 당연히, 데이터 필드와 일반 메소드 포함 가능.

✅ 객체 생성x : 구체적이지 못한, 불완전한 클래스.

사용

  • 의미적으로 유사한 자식클래스를 묶고자 할 때
  • 기능적으로 구현하기 어려운 메소드가 있을 때 ⇒ 불완전한 클래스를 만듬
  • 자식클래스로 상속될 때 사용됨
  • 자식클래스에서 추상 메소드를 구현해야 하고, 구현해 사용한다. ⇒ 구현 후 객체 생성 가능

✅ 자식클래스가 추상 메소드를 구현하지 않으면, 자식클래스도 추상클래스여야 함.

class Circle extends Shape {

  private double radius;

  @Override // 구현
  public double getArea() {
    return Math.PI * radius * radius;
  }
}

인터페이스

  • 코드

      package lecture05;
    
      // Comparable 인터페이스 선언: 제네릭 타입 T를 사용하여 다른 객체와 크기를 비교하는 메서드를 정의
      interface Comparable<T> {
        // 다른 객체와 크기를 비교하는 메서드: 구현 클래스에서 정의해야 함
        boolean isLargerThan(T o);
      }
    
      // Box 클래스 선언: Comparable 인터페이스를 구현
      class Box implements Comparable<Box> {
        // 인스턴스 변수: 길이, 너비, 높이
        private int length, width, height;
    
        // 생성자: Box 객체를 초기화
        public Box(int length, int width, int height) {
          this.length = length;
          this.width = width;
          this.height = height;
        }
    
        // Comparable 인터페이스의 메서드 구현: Box 객체의 부피를 비교
        @Override
        public boolean isLargerThan(Box otherBox) {
          // 현재 Box 객체의 부피 계산
          int thisVolume = this.length * this.width * this.height;
          // 비교 대상 Box 객체의 부피 계산
          int otherVolume = otherBox.length * otherBox.width * otherBox.height;
          // 현재 Box 객체의 부피가 더 큰지 여부 반환
          return thisVolume > otherVolume;
        }
      }
    
      public class InterfaceEx {
        public static void main(String[] args) {
          Box box1 = new Box(3, 3, 3);
          Box box2 = new Box(4, 4, 4);
    
          // Box 객체의 크기를 비교하고 결과 출력
          System.out.println("Box1 is larger than Box2: " + box1.isLargerThan(box2));
          System.out.println("Box2 is larger than Box1: " + box2.isLargerThan(box1));
        }
      }
    

정의

  • 100% 추상클래스로, 모든 메소드가 추상메소드.
  • 구현되어야 할 메소드의 형식만 정의해둠.
    • 단, default 인스턴스 메소드와 static 메소드는 몸체를 구현함.
  • 참조형으로, 직접 객체 생성 불가.
  • 이름은 보통 형용사 : Runnable, Serializable, Comparable

사용방법

클래스 정의와 유사, class 대신 interface (abstract 생략)

✅ 접근제어자는 생략해도 public abstract.

✅ 몸체는 없음, 반환형, 이름, 매개변수 목록만 표시

✅ 모든 메소드의 기본 접근제어자는 public.

✅ 데이터 필드는 클래스 상수만 가능(public static final)

언제 사용하는가?

  • 자식클래스에서 상속되어 사용됨.
  • 인터페이스를 상속받아 자식 인터페이스를 정의할 수 있음
interface SubInterface extends SuperInterface {
    ...
}

위의 경우 SubInterface를 상속받는 클래스는 Sub와 Super 인터페이스 메소드를 모두 구현해야함.

  • 다중 상속이 가능하다.

  • 코드

    
      interface Movable {
        void add(double dx, double dy);
        void sub(double dx, double dy);
      }
    
      interface Scalable {
        void mul(double s);
        void div(double s);
      }
    
      public class MultipleInherit implements Movable, Scalable {
        @Override
        public void add(double dx, double dy) { ... }
    
        @Override
        public void sub(double dx, double dy) { ... }
    
        @Override
        public void mul(double s) { ... }
    
        @Override
        public void div(double s) { ... }
      }
    

디폴트 메소드

  • 인터페이스에서 선언하는 메소드에, 기본 구현(=몸체)를 넣을 수 있음.
    • 자식클래스에서 상속 받을 때 그대로 사용 가능.
    • default를 사용해 몸체 구현

왜 사용하는가?

  • 인터페이스에 새 메소드를 추가할 때, 기존 인터페이스를 구현한 클래스들이 오류없이 작동하기 위함.

  • 따라서 그러한 오류해결을 위해 별도로 수정하지 않아도 됨! 아니면 모든 클래스에 추가/수정하던가..

  • 예시 코드

      package lecture05;
    
      // 인터페이스 선언
      interface Vehicle {
    
        void start();
    
        // 디폴트 메소드: 기본 구현을 제공
        default void stop() {
          System.out.println("Vehicle is stopping");
        }
      }
    
      // 클래스가 인터페이스를 구현
      class Car implements Vehicle {
    
        @Override
        public void start() {
          System.out.println("Car is starting");
        }
    
        // stop() 메소드를 재정의하지 않으면 디폴트 메소드가 사용됨
      }
    
      // 또 다른 클래스가 인터페이스를 구현
      class Bike implements Vehicle {
    
        @Override
        public void start() {
          System.out.println("Bike is starting");
        }
    
        // 디폴트 메소드를 재정의하여 다른 동작을 구현
        @Override
        public void stop() {
          System.out.println("Bike is stopping in its own way");
        }
      }
    
      public class DefaultMethod {
    
        public static void main(String[] args) {
          Vehicle car = new Car();
          car.start();
          car.stop(); // Car 클래스는 stop() 메소드를 재정의하지 않았으므로 인터페이스의 디폴트 메소드가 호출됨
    
          Vehicle bike = new Bike();
          bike.start();
          bike.stop(); // Bike 클래스는 stop() 메소드를 재정의했으므로 재정의된 메소드가 호출됨
        }
      }
    

형 변환

  • 인터페이스와 클래스는 모두 사용자 정의형.

  • extends와 implements에 따라 상위/하위 자료형 관계가 설정

  • 상위 유형의 변수는 하위 객체의 참조값을 가질 수 있음.

  • 동적 바인딩 : 상위 유형의 변수가 가리키는 객체의 실제 유형에 따라 메소드가 결정됨

  • 코드

      // 인터페이스 선언
      interface Animal {
          void sound();
      }
    
      // 클래스 정의
      class Dog implements Animal {
          @Override
          public void sound() {
              System.out.println("멍멍!");
          }
    
          public void wagTail() {
              System.out.println("꼬리 흔들!");
          }
      }
    
      public class Main {
          public static void main(String[] args) {
              // Dog 객체 생성
              Dog dog = new Dog();
    
              // 상위 유형의 변수에 하위 객체의 참조값 할당
              Animal animal = dog;
    
              // 인터페이스 메소드 호출
              animal.sound(); // Dog의 sound() 메소드가 호출됨(동적 바인딩)
    
              // 하위 유형의 메소드는 호출 불가능 (Animal 인터페이스에는 없음)
              // animal.wagTail(); // 컴파일 에러 발생
          }
      }

다형성(polymorphism)

  • 유사하지만 다양한 형상 / 기능을 가진다는 뜻.
    • 한 부모에서 나온 두 자식의 객체는 비슷하지만 다르다.

종류

  1. 메소드 오버로딩 - 유사하지만 다르다
  2. 추상메소드 - 자식 클래스에서 정의된 메소드는 부모의 것과 유사하지만 다르다.
  3. 클래스의 다형성(오버라이딩)
    • 부모로부터 상속받은 메소드를 자식에서 오버라이딩 가능 : 같은 이름의 메소드가 다른 기능 수행
  4. 인터페이스의 다형성
    • 여러 자식클래스들에서 상위 인터페이스의 메소드를 다르게 구현

형변환

  • 전혀 다른 클래스간에는 형 변환이 되지 않는다. but! 상속관계의 클래스끼리는 가능하다.
  • 하위클래스(dog) → 상위(Animal)클래스로 변환은 문제 없다. (UpCasting : 자동 형변환 가능)
  • 코드의 Animal animal = (Animal) new Dog; 참고 !

뭐가 좋은가?

  • 코드의 유연성과 재사용성

  • 동적 바인딩

    • 각 객체의 실제 타입에 따라 동적으로 결정
    • 따라서 실제 유형을 명시적으로 다룰 필요가 없음.
  • 좋은 이유 예시

          public static void main(String[] args) {
              // 동물 배열 생성
              Animal[] animals = new Animal[2];
              animals[0] = new Dog();
              animals[1] = new Cat();
    
              // 동물들이 소리를 내도록 호출
              for (Animal animal : animals) {
                  animal.sound(); // 각 동물의 실제 타입에 따라 makeSound() 메소드가 호출됨
              }
          }

열거형(enum)

  • 미리 정의된 상수값의 집합을 만들기 위한 자료형
  • 미리 지정된 값만 대입 가능
enum Day {
    SUNDAY , MONDAY , TUESDAY , WEDNESDAY , THURSDAY , FRIDAY , SATURDAY
}
  • 열거형 변수에는 미리 지정된 값만 대입 가능.
  • 상수값을 배열로 리턴하는 values() 제공
public class EnumEx {
  public static void main(String args[]) {
    Day day = Day.MONDAY;
    for (Day d : Day.values()) {
      System.out.println(d);
    }
  }
}

  • 필드와 메소드 포함 가능하나 상수가 먼저 정의되어야 함. (’;’로 끝나야 함)
  • 상수값은 하나의 객체와 같다. ex) day.Monday
  • 생성자는 열거형과 같은 이름을 가지며, 접근제어자는 생략 or private이여야 함.
  • 열거형 생성자는 모든 객체에 대한 초기화 수행. (상수가 사용될 때 한꺼번에 자동 호출)
  • 코드

      // 열거형 Day 정의
      enum Day {
          // 상수 정의 (하나의 객체와 같음)
          Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday;
    
          // 필드와 메소드 정의
          private String description;
    
          // 열거형 생성자 (생성자 이름은 열거형과 같아야 함)
          Day() {
              this.description = "A day of the week";
          }
    
          // 열거형 생성자를 통한 초기화
          Day(String description) {
              this.description = description;
          }
    
          // Getter
          public String getDescription() {
              return description;
          }
      }
    
      public class Main {
          public static void main(String[] args) {
              // 열거형 상수 사용
              Day today = Day.Monday;
              System.out.println("Today is " + today);
    
                      // 열거형 상수의 초기화 : 모든 객체에 대한 초기화
              System.out.println("Description of Monday: " + Day.MONDAY.getDescription());
          }
      }
    

익명 클래스

  • 일회성으로 1개의 객체를 생성하기 위한 클래스

    • 따라서, 정의와 동시에 객체를 생성함.
  • 부모클래스를 상속받거나 / 인터페이스 또는 추상클래스 메소드를 구현하도록 익명클래스를 정의함.

    • new 슈퍼클래스() {…}
    • new 인터페이스() {…}
    • 이 때, {…}가 익명클래스의 몸체임. new Greeting() {…}
  • 코드

      interface Greeting {
          void greet();
      }
    
      public class AnonymousClass {
          public static void main(String[] args) {
              // 익명 클래스를 이용한 Greeting 인터페이스의 구현
              Greeting greeting = new Greeting() {
                  @Override
                  public void greet() {
                      System.out.println("Hello!");
                  }
              };
    
              // 익명 클래스의 메소드 호출
              greeting.greet();
          }
      }

중첩 클래스(내부클래스, inner class) - 어렵다.

  • 외부 클래스 정의 내부에 정의된 또 다른 클래스
  • 외부 클래스의 멤버가 되며, 외부 클래스의 모든 멤버에 접근 가능.
  • 내부 클래스는 보통의 클래스와 다르게 private, protected클래스가 될 수 있음.
  • 대개 내부클래스는 외부클래스의 필드와 관련된 작업을 처리

non-static 중첩 클래스

  • 외부 클래스 객체가 생성된 후 사용 가능
  • 외부 클래스의 모든 멤버에 접근할 수 있음.
  • 객체 생성 방법 🔽
    Outer outer = new Outer();

    Outer.Inner inner = outer.new Inner();
    //외부클래스객체.new 내부클래스();를 통해 객체 생성
  • 코드

      class Outer {
          class Inner {
              void greet() {
                  System.out.println("Hello from inner class!");
              }
          }
      {
    
      public class Main {
          psvm {
              Outer outer = new Outer();
              Outer.Inner inner = outer.new Inner();
              inner.greet();
          }
      }

static 중첩 클래스

  • 외부클래스 객체 생성과 무관하게 사용 가능

  • 외부클래스 정적 멤버에 접근 가능.

  • 코드

      class Outer {
          static class StaticNested {
              void greet() {
                  System.out.println("Hello from static nested class!");
              }
          }
      }
    
      public class Main {
          public static void main(String[] args) {
              // 외부 클래스의 객체 생성과 무관하게 정적 중첩 클래스의 객체 생성 가능
              //StaticNested 클래스는 Outer의 정적멤버 => Outer 클래스 생성 없이 직접 접근
              Outer.StaticNested staticNested = new Outer.StaticNested();
              staticNested.greet();
          }
      }

왜 사용?

논리적 그룹화 + 캡슐화

중요하진 않지만 어려운 부분임 → 필요할 때 한번 더 보도록