프로그래밍 공부

객체지향 프로그래밍(클래스) 본문

Programming/JAVA

객체지향 프로그래밍(클래스)

khj1999 2024. 10. 8. 08:10

클래스를 만들때 중요한 점

  • 상태 ← 맴버 변수
  • 객체의 생성 ← 생성자
  • 어떤 행동을 할지 ← 메서드

생성자

  • 기본 생성자

    • 기본 생성자는 매개변수가 없는 생성자입니다. 만약 클래스에 생성자가 정의되어 있지 않다면
      Java 컴파일러가 자동으로 매개변수가 없는 기본 생성자를 제공
    • 기본 생성자는 클래스의 모든 필드를 기본값으로 초기화
  • 매개변수가 있는 생성자

    • 매개변수가 있는 생성자는 객체를 생성할 때 초기화해야 할 데이터를 인수로 전달받음.
      이를 통해 생성되는 객체의 상태를 다양하게 설정가능
  • 생성자 오버로딩

    • Java에서는 메소드 오버로딩과 마찬가지로 생성자도 오버로딩할 수 있음
    • 즉, 같은 이름의 생성자를 여러 개 정의할 수 있으며, 각각 다른 매개변수 리스트를 가질 수 있음
  • this 키워드를 사용한 생성자 호출

    • 같은 클래스 내에서 다른 생성자를 호출할 때 this 키워드를 사용할 수 있음.
      이를 통해 중복된 코드 작성을 피할 수 있음
    • 기본 개념
      • this 키워드를 사용하면 같은 클래스 내에서 다른 생성자를 호출할 수 있습니다.
      • this를 사용한 생성자 호출은 반드시 생성자의 첫 번째 줄에 위치해야 합니다.
      • 이를 통해 공통된 초기화 작업을 중앙 집중화할 수 있음
  • 예제 코드

      public class MyClass {
          int x;
          String y;
    
          // 기본 생성자
          public MyClass() {
              this(0, "Default");
              // x = 0;
              // y = "Default";
              // 위의 코드 대신 this(0, "Default");를 호출하여 코드 중복을 방지.
          }
    
          // 매개변수가 있는 생성자 1
          public MyClass(int val) {
              this(val, "Default");
              // x = val;
              // y = "Default";
              // 위의 코드 대신 this(val, "Default");를 호출하여 코드 중복을 방지.
          }
    
          // 매개변수가 있는 생성자 2 (모든 초기화 작업을 수행)
          public MyClass(int val, String str) {
              x = val;
              y = str;
          }
    
          public static void main(String[] args) {
              MyClass obj1 = new MyClass();           // x = 0, y = "Default"
              MyClass obj2 = new MyClass(10);         // x = 10, y = "Default"
              MyClass obj3 = new MyClass(20, "Hi");   // x = 20, y = "Hi"
    
              System.out.println(obj1.x + ", " + obj1.y);  // 출력: 0, Default
              System.out.println(obj2.x + ", " + obj2.y);  // 출력: 10, Default
              System.out.println(obj3.x + ", " + obj3.y);  // 출력: 20, Hi
          }
      }

객체 컴포지션

  • 두 개 이상의 객체를 조합하여 새로운 기능을 만드는 기법
  • 복잡한 객체를 구성하고 재사용 가능성을 높이며, 상속보다 유연한 설계 가능
  • 컴포지션(Composition)은 "has-a" 관계로 설명할 수 있음. 한 객체가 다른 객체를 포함(포함 관계)하고 있다는 의미
  • 컴포지션을 사용하면 큰 클래스가 여러 작은 클래스의 인스턴스를 포함하여 더 복잡한 기능을 수행 할 수 있음
  • 장점
    • 유연성: 상속에 비해 컴포지션은 클래스 간 결합도가 낮아 더 유연하게 클래스를 설계할 수 있음
    • 재사용성: 객체 컴포지션을 사용하면 코드 재사용성을 높일 수 있으며, 서로 다른 클래스에서 동일한 기능을 쉽게 사용할 수 있음
    • 캡슐화: 컴포지션을 통해 구현 세부 사항을 숨길 수 있으며, 인터페이스를 통해 객체 간의 상호작용을 정의할 수 있음.

컴포지션 vs 상속

  • 상속(Inheritance)은 "is-a" 관계를 나타냅니다. 예를 들어, Dog 클래스가 Animal 클래스를 상속받는다면, "개는 동물이다"라는 의미가 됨
  • 컴포지션(Composition)은 "has-a" 관계를 나타내며, 상속에 비해 더 유연하고 캡슐화된 설계를 할 수 있다. 객체 간의 결합도가 낮아지고, 변경에 더 강하게 대응할 수 있음.
  • 컴포지션을 사용할 때
    • 상속보다 컴포지션이 더 적합한 경우는 클래스 간의 관계가 명확하게 "is-a" 관계가 아닌 경우입니다. 예를 들어, 자동차가 엔진을 "포함"하고 있지만, 자동차는 엔진이 아닙니다. 이럴 때 컴포지션을 사용하여 객체 간의 관계를 설계하는 것이 좋습니다.

상속

  • 클래스가 다른 클래스의 속성과 메소드를 물려받아 재사용할 수 있도록 하는 기능.
    상속을 통해 코드의 중복을 줄이고, 클래스 간의 관계를 정의할 수 있음

  • 기본 개념

    • 부모 클래스(슈퍼 클래스, Superclass): 상속을 제공하는 클래스.
      상속을 받는 클래스에게 속성과 메소드를 물려줌.
    • 자식 클래스(서브 클래스, Subclass): 부모 클래스로부터 상속을 받는 클래스.
      부모 클래스의 모든 속성과 메소드를 물려받으며, 이를 확장하거나 오버라이드(재정의)할 수 있음.
    • extends 키워드: Java에서 상속을 구현할 때 사용.
      자식 클래스가 부모 클래스를 상속받을 때, 클래스 선언부에서 extends 키워드를 사용.
  • 장점

    • 코드 재사용: 부모 클래스의 코드(필드와 메소드)를 자식 클래스에서 그대로 사용할 수 있으므로,
      중복된 코드를 작성할 필요가 없음.
    • 유지보수성 향상: 공통된 기능은 부모 클래스에 정의하고, 특정 기능은 자식 클래스에 정의함으로써
      코드를 더 잘 조직화할 수 있음. 수정할 부분이 생기면 부모 클래스에서 한 번만 수정하면 됨.
    • 다형성(Polymorphism): 상속을 통해 자식 클래스는 부모 클래스의 형태로 취급될 수 있다.
      즉, 자식 클래스 객체를 부모 클래스 타입으로 참조할 수 있음.
      이를 통해 다양한 객체를 일관되게 처리할 수 있음.
  • 한계와 주의점

    • 다중 상속 불가: Java에서는 클래스가 하나의 부모 클래스만을 상속받을 수 있다.
      이를 단일 상속(Single Inheritance)이라 하며, 다중 상속(Multiple Inheritance)을 지원하지 않음.
      다중 상속은 인터페이스를 통해서만 구현할 수 있음.
    • 부모 클래스와의 강한 결합: 자식 클래스는 부모 클래스와 강하게 결합되어 있기 때문에
      부모 클래스의 변경이 자식 클래스에 영향을 미칠 수 있음
      잘못 설계된 상속 구조는 유지보수에 어려움을 줄 수 있다.
    • 객체의 복잡도 증가: 상속을 남용하면 클래스 계층 구조가 복잡해질 수 있다.
      이는 코드의 가독성과 유지보수성을 떨어뜨릴 수 있다.
  • 요약

    • 상속은 부모 클래스의 속성과 메소드를 자식 클래스가 물려받아 사용하는 기능.
    • 코드 재사용성과 유지보수성을 향상시키며, 다형성을 통해 유연한 코드를 작성할 수 있게 해줌.
    • 그러나 상속의 사용에는 주의가 필요하며, 적절한 설계가 이루어져야 함.
  • 객체(Object)는 상속의 최상의 순위임

    • 아무것도 상속받지 않으면 자바는 기본적으로 Object라는 클래스를 상속받음

    • 클래스의 근본

    • 모든 클래스는 객체 클래스의 하위 클래스

      public class Person {
        ~~~
      }
      
      // 이런 클래스가 있으면 실제로는
      public class Person extends Object {
        ~~ 
      }
      
      // 이런 방식으로 실행
  • 오버라이딩

    • 함수를 덮어씌우는 것

    • ex) toString() 오버라이딩

        public String toString(){
            retrun "data";
        }
  • super()

    • super 키워드는 자바에서 부모 클래스의 멤버(필드, 메소드, 생성자 등)에 접근하기 위해 사용

    • super 키워드를 통해 부모 클래스의 기능을 활용하거나 확장할 수 있음

    • 하위 클래스는 객체를 생성할때 생성자가 자동으로 super()를 호출함

    • 하위 클래스 생성자는 상위 생성자가 무조건 정의 되어야 함

    • 예시 코드

        class Parent {
            Parent() {
                System.out.println("Parent's default constructor");
            }
      
            Parent(String message) {
                System.out.println("Parent's parameterized constructor: " + message);
            }
      
            void show() {
                System.out.println("This is Parent's show method.");
            }
        }
      
        class Child extends Parent {
            Child() {
                super("Hello from Child!");  // 부모 클래스의 매개변수 있는 생성자 호출
                System.out.println("Child's constructor");
            }
      
            @Override
            void show() {
                super.show();  // 부모 클래스의 show() 메소드 호출
                System.out.println("This is Child's show method.");
            }
        }
      
        public class Main {
            public static void main(String[] args) {
                Child child = new Child();
                child.show();
            }
        }
    • thissuper: 생성자에서 this()super()는 동시에 사용할 수 없습니다. 둘 중 하나만 생성자의 첫 번째 줄에서 호출할 수 있다.

      • 생성자에서 this() 또는 super()를 호출할 때, 호출된 생성자가 완전히 실행된 후에만 그 다음의 코드가 실행됩니다. 생성자는 객체가 초기화되는 첫 단계이기 때문에, 객체의 초기화 순서를 보장하기 위해 반드시 생성자의 첫 번째 줄에서만 다른 생성자를 호출해야 함.

      • this(): 현재 클래스의 다른 생성자를 호출하여 현재 클래스의 초기화를 처리.

      • super(): 부모 클래스의 생성자를 호출하여 부모 클래스의 초기화를 처리.

      • 예시

          // 잘못된 사용
          class Parent {
              Parent() {
                  System.out.println("Parent constructor");
              }
          }
        
          class Child extends Parent {
              Child() {
                  // 컴파일 오류: this()와 super()를 동시에 사용할 수 없음
                  super(); // 부모 클래스의 생성자 호출
                  this("Hello"); // 같은 클래스의 다른 생성자 호출
              }
        
              Child(String message) {
                  System.out.println("Child constructor with message: " + message);
              }
          }
        
          // 옳게된 사용
          class Parent {
              Parent() {
                  System.out.println("Parent constructor");
              }
          }
        
          class Child extends Parent {
              Child() {
                  super();  // 부모 클래스의 생성자 호출
                  System.out.println("Child default constructor");
              }
        
              Child(String message) {
                  this();  // 같은 클래스의 기본 생성자 호출
                  System.out.println("Child constructor with message: " + message);
              }
          }
  • 자바는 다중상속을 허용하지 않음, 상속계층은 있을 수 있음

  • 하위 클래스 변수를 담을 수 있는 상위 클래스 참조 변수를 만들 수 있음

    • 가능한 이유

      1. 객체의 생성과정
        • 자바에서 객체를 생성할 때, 하위 클래스 객체가 생성되면 상위 클래스의 생성자가 먼저 호출
      2. 메서드 호출 과정
        • 메소드 호출참조 변수의 타입이 아니라 실제 객체의 타입에 따라 결정
        • 상위 클래스 타입의 참조 변수를 통해 메소드를 호출할 때, 실제로는 참조된 객체의 타입을 기준으로 메소드가 호출됩니다. 이것이 다형성의 핵심
      3. 메서드 디스패치
        • 자바에서 메소드를 호출할 때, 컴파일러는 참조 변수의 타입만을 확인하여
        • 해당 메소드가 존재하는지*를 확인. 그러나 실제로 어떤 메소드가 실행될지는 런타임에 결정
          이 과정이 동적 메소드 디스패치 또는 런타임 다형성이라고 불림.
          • 컴파일 시점: 참조 변수의 타입(Animal)을 보고 해당 메소드(sound())가 존재하는지 확인.
          • 런타임 시점: 실제 객체(Dog)의 타입을 기준으로 메소드(Dog 클래스의 sound())가 호출.
      4. 상위 클래스와 하위 클래스의 관계
        • 상위 클래스하위 클래스의 일반적인 특성을 정의하고, 하위 클래스는 이 특성을 구체화함.
        • 하위 클래스가 상위 클래스를 상속받으면, 하위 클래스는 상위 클래스의 속성과 메소드를
        • 상속*받게 되며, 이를 기반으로 자신만의 고유한 기능을 추가하거나 기존 기능을 오버라이드함.
      5. 결론
        • 객체는 실제로 하위 클래스의 인스턴스이기 때문에, 상위 클래스 타입의 참조 변수를 사용하더라도 하위 클래스의 메소드가 호출.
        • 상위 클래스 생성자와 하위 클래스 생성자 모두 호출되는 이유는 객체가 생성될 때
          상위 클래스 부분도 초기화되어야 하기 때문.
          하지만 메소드 호출 시점에는 실제 객체의 타입을 기준으로 메소드가 선택되기 때문에, 하위 클래스에서 오버라이드된 메소드가 실행.
    • 예시

        class Animal {
            void sound() {
                System.out.println("Some generic animal sound");
            }
        }
      
        class Pet extends Animal {
            @Override
            void sound() {
                System.out.println("Some pet sound");
            }
      
            void play() {
                System.out.println("Pet is playing");
            }
        }
      
        class Cat extends Pet {
            @Override
            void sound() {
                System.out.println("Meow");
            }
      
            void purr() {
                System.out.println("Cat is purring");
            }
        }
      
        public class Main {
            public static void main(String[] args) {
                // Animal 타입의 참조 변수로 Cat 객체 참조
                Animal myCat = new Cat();
      
                // 참조 변수에 저장된 객체에 따라 메소드가 호출됨
                myCat.sound();  // "Meow" 출력 (Cat 클래스의 sound() 호출)
      
                // myCat.play(); // 컴파일 오류 (Animal 타입에는 play() 메소드가 없음)
                // myCat.purr(); // 컴파일 오류 (Animal 타입에는 purr() 메소드가 없음)
      
                // 캐스팅을 통해 하위 클래스의 메소드 호출 가능
                Pet petCat = (Pet) myCat;
                petCat.play();  // "Pet is playing" 출력
      
                Cat realCat = (Cat) myCat;
                realCat.purr(); // "Cat is purring" 출력
            }
        }
  • 자바에서 상위 클래스 타입의 참조 변수하위 클래스 객체를 참조할 때, 다음과 같은 규칙이 적용

    • 상위 클래스에 정의된 메소드는 상위 클래스 타입의 참조 변수를 통해 호출할 수 있음.

    • 하위 클래스에서 오버라이딩된 메소드는 상위 클래스 타입의 참조 변수로 호출하면 하위 클래스의 오버라이딩된 메소드가 실행.

    • 하위 클래스에만 정의된 메소드는 상위 클래스 타입의 참조 변수로는 호출할 수 없음. 해당 메소드를 호출하려면 다운캐스팅이 필요.

    • 예시

        class Animal {
            void sound() {
                System.out.println("Some generic animal sound");
            }
      
            void sleep() {
                System.out.println("Animal is sleeping");
            }
        }
      
        class Dog extends Animal {
            @Override
            void sound() {
                System.out.println("Bark");
            }
      
            void wagTail() {
                System.out.println("Dog is wagging its tail");
            }
        }
      
        public class Main {
            public static void main(String[] args) {
                Animal myDog = new Dog();
      
                // Animal 클래스에 정의된 메소드 호출 가능
                myDog.sound();  // Dog 클래스의 오버라이딩된 메소드가 호출됨 -> "Bark"
                myDog.sleep();  // Animal 클래스의 메소드가 호출됨 -> "Animal is sleeping"
      
                // Dog 클래스에만 있는 메소드는 호출 불가
                // myDog.wagTail();  // 컴파일 오류: Animal 타입에 wagTail() 메소드가 정의되어 있지 않음
      
                // wagTail() 메소드를 호출하려면 다운캐스팅 필요
                Dog realDog = (Dog) myDog;
                realDog.wagTail();  // "Dog is wagging its tail"
            }
        }
Comments