본문 바로가기
IT & AI/AI 지식

JAVA 객체 지향 프로그래밍: 클래스, 상속

by 빛나는해커 2024. 11. 21.

1.  클래스

 

객체 지향 프로그래밍

  • 객체 지향 프로그래밍(OOP)은 객체 간의 상호작용을 통해 프로그램을 설계하는 방식이다.
  • 객체는 현실 세계의 사물이나 개념을 모델링한 것으로, 속성(필드)과 행동(메소드)을 가진다.
  • 객체는 서로 메시지를 주고받으며 상호작용할 수 있다.
  • 객체는 '연관 관계', '상속 관계', '의존 관계' 등 다양한 관계를 통해 상호작용한다.
  • 이러한 관계는 프로그램의 구조를 이해하고 설계하는 데 중요한 요소이다.
  • 클래스는 객체를 생성하기 위한 청사진이다.
  • 클래스는 객체의 속성과 행동을 정의하며, 이러한 클래스를 기반으로 여러 개의 객체를 생성할 수 있다.
  • 객체는 클래스의 인스턴스라고 부른다.
  • 클래스를 선언하려면 class 키워드를 사용하고 클래스 이름을 지정한다.
  • 클래스 이름은 관례적으로 대문자로 시작한다.
public class Car {
    String color;
    int speed;
}
  • 객체를 생성하려면 new 키워드를 사용한다.
  • 생성된 객체는 클래스의 인스턴스이며, 클래스 변수를 통해 객체의 속성에 접근할 수 있다.
Car myCar = new Car();
myCar.color = "Red";
myCar.speed = 100;
  • 클래스는 필드, 메소드, 생성자 등의 구성 요소를 가진다.
  • 필드는 객체의 상태를 나타내고, 메소드는 객체의 행동을 정의하며, 생성자는 객체를 초기화하는 역할을 한다.

필드

  • 필드는 클래스의 속성을 저장하는 변수이다.
  • 필드는 클래스 내에서 선언되며, 객체가 생성될 때마다 별도로 할당된다.
public class Car {
    String color;
    int speed;
}
  • 필드는 객체를 통해 접근할 수 있다.
  • 필드에 값을 할당하거나 읽어올 수 있다.
Car myCar = new Car();
myCar.color = "Blue";
System.out.println("색상: " + myCar.color);

 

생성자

  • 생성자는 객체를 생성할 때 호출되는 메소드로, 주로 필드를 초기화하는 데 사용된다.
  • 기본 생성자는 명시적으로 정의하지 않으면 컴파일러가 자동으로 추가해준다.
public class Car {
    Car() {
        System.out.println("자동차가 생성되었습니다.");
    }
}
  • 생성자는 클래스 이름과 동일해야 하며, 반환 타입이 없다.
public Car(String color, int speed) {
    this.color = color;
    this.speed = speed;
}
  • 생성자를 통해 필드를 초기화할 수 있다.
  • 이를 통해 객체가 생성될 때 초기 상태를 설정할 수 있다.
Car myCar = new Car("Green", 150);
  • 하나의 클래스에 여러 개의 생성자를 정의하는 것을 생성자 오버로딩이라고 한다.
  • 각 생성자는 서로 다른 매개변수를 가진다.
public Car() {
    this("Black", 0);
}

public Car(String color, int speed) {
    this.color = color;
    this.speed = speed;
}
  • this()는 동일 클래스 내에서 다른 생성자를 호출할 때 사용한다.
  • 생성자 내에서 코드를 중복하지 않도록 도와준다.

메소드

  • 메소드는 객체의 행동을 정의하는 멤버이다.
  • 메소드는 반환 타입, 이름, 매개변수 목록, 그리고 메소드 본체로 구성된다.
public void drive() {
    System.out.println("자동차가 주행 중입니다.");
}
  • return문은 메소드의 실행을 종료하고 결과값을 반환하는 역할을 한다.
public int getSpeed() {
    return speed;
}
  • 메소드는 객체를 통해 호출할 수 있다.
  • 호출된 메소드는 객체의 행동을 실행한다.
myCar.drive();
  • 메소드 오버로딩은 동일한 이름의 메소드를 여러 개 정의하는 것을 의미하며, 각 메소드는 서로 다른 매개변수를 가진다.
public void drive(int distance) {
    System.out.println(distance + "km를 주행합니다.");
}

 

인스턴스 멤버와 정적 멤버

  • 인스턴스 멤버는 객체가 생성될 때마다 별도로 존재하는 멤버이다.
  • this 키워드는 현재 객체의 인스턴스를 참조할 때 사용된다.
public void setColor(String color) {
    this.color = color;
}
  • 정적 멤버는 클래스에 소속된 멤버로, 객체를 생성하지 않고도 사용할 수 있다.
  • static 키워드를 사용하여 정의한다.
public static int wheelCount = 4;
  • 싱글톤 패턴은 클래스의 인스턴스를 하나만 생성하도록 보장하는 디자인 패턴이다.
public class Singleton {
    private static Singleton instance = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    }
}
  • final 키워드를 사용하여 필드를 상수로 정의할 수 있다.
  • 이는 한 번만 값을 할당할 수 있음을 의미한다.
public static final double PI = 3.14159;

 

패키지와 접근 제한자

  • 패키지는 관련 있는 클래스들을 논리적으로 묶어주는 역할을 한다.
  • package 키워드를 사용하여 패키지를 선언한다.
package com.example;
  • 접근 제한자는 클래스, 필드, 메소드의 접근 범위를 제어하는 키워드이다.
  • public, protected, default, private가 있다.
  • 클래스에는 public과 default 접근 제한자를 사용할 수 있다.
  • public 클래스는 모든 곳에서 접근 가능하지만, default 클래스는 같은 패키지 내에서만 접근 가능하다.
  • 생성자에 접근 제한자를 사용하여 객체 생성 범위를 제한할 수 있다.
  • 필드와 메소드에도 접근 제한자를 사용할 수 있다.
  • private 필드는 클래스 외부에서 접근할 수 없으며, 이를 위해 GetterSetter 메소드를 제공한다.
public class Car {
    private String color;

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }
}
  • Getter와 Setter 메소드는 private 필드에 대한 접근을 제공하기 위해 사용된다.
  • 이를 통해 외부에서는 필드를 안전하게 접근하고 수정할 수 있다.

2.  상속

 

상속

  • 상속은 객체 지향 프로그래밍(OOP)의 중요한 특징 중 하나로, 기존 클래스를 확장하여 새로운 클래스를 만드는 방식이다.
  • 이를 통해 코드의 재사용성을 높이고, 유지보수를 쉽게 할 수 있다.
  • 자바에서는 extends 키워드를 사용하여 상속을 구현한다.
public class Car {
    String color;
    int speed;
}

public class SportsCar extends Car {
    boolean turbo;
}
  • 위 예제에서 SportsCar 클래스는 Car 클래스를 상속받아 turbo라는 새로운 필드를 추가하였다.
  • 상속을 통해 Car 클래스의 속성과 기능을 재사용할 수 있다.
  • 자식 클래스는 객체 생성 시 부모 클래스의 생성자를 호출해야 한다.
  • 이를 통해 부모 클래스의 필드를 초기화할 수 있다.
  • 자바에서는 super() 키워드를 사용하여 부모 생성자를 호출한다.
public class Car {
    String color;
    int speed;

    public Car(String color, int speed) {
        this.color = color;
        this.speed = speed;
    }
}

public class SportsCar extends Car {
    boolean turbo;

    public SportsCar(String color, int speed, boolean turbo) {
        super(color, speed);  // 부모 클래스 생성자 호출
        this.turbo = turbo;
    }
}
  • super()는 반드시 자식 클래스 생성자의 첫 번째 줄에서 호출되어야 한다.
  • 이를 통해 부모 클래스의 초기화 작업이 먼저 완료된 후 자식 클래스의 초기화가 이루어진다.
  • 메소드 재정의(오버라이딩)는 부모 클래스의 메소드를 자식 클래스에서 재정의하여 사용하는 것이다.
  • 자바에서는 @Override 애노테이션을 사용하여 메소드가 재정의된 것임을 명시할 수 있다.
  • 오버라이딩된 메소드는 부모 클래스의 메소드를 대신하여 자식 클래스의 로직으로 실행된다.
public class Car {
    public void drive() {
        System.out.println("자동차가 주행 중입니다.");
    }
}

public class SportsCar extends Car {
    @Override
    public void drive() {
        System.out.println("스포츠카가 고속으로 주행 중입니다.");
    }
}
  • 위 코드에서 SportsCar 클래스는 Car 클래스의 drive() 메소드를 재정의하여 고유의 동작을 정의하였다.
  • final 키워드를 사용하여 클래스나 메소드를 선언할 수 있다.
  • final 클래스로 선언된 클래스는 상속될 수 없으며, final 메소드로 선언된 메소드는 자식 클래스에서 재정의할 수 없다.
public final class Utility {
    // 이 클래스는 상속될 수 없음
}

public class Car {
    public final void stop() {
        System.out.println("자동차가 멈췄습니다.");
    }
}
  • 이렇게 final 키워드를 사용하면 상속을 통해 변경되어서는 안 되는 기능을 보호할 수 있다.
  • protected 접근 제한자는 같은 패키지 내에서, 또는 다른 패키지에 있는 상속 관계에 있는 클래스에서 접근할 수 있도록 한다.
  • protected는 상속받은 자식 클래스에서 부모 클래스의 멤버에 접근하는 데 유용하다.
public class Car {
    protected int speed;

    protected void accelerate() {
        speed += 10;
    }
}

public class SportsCar extends Car {
    public void boost() {
        accelerate();  // 부모 클래스의 protected 메소드 호출 가능
        speed += 20;    // 부모 클래스의 protected 필드 접근 가능
    }
}
  • 위 예제에서 SportsCar 클래스는 Car 클래스의 protected 필드와 메소드에 접근할 수 있다.

타입 변환과 다형성

  • 자동 타입 변환(업캐스팅)은 자식 클래스의 객체가 부모 클래스 타입으로 자동 변환되는 것을 의미한다.
  • 자바에서는 부모 타입 변수로 자식 객체를 참조할 수 있으며, 이를 통해 다형성을 구현할 수 있다.
Car myCar = new SportsCar();  // SportsCar 객체가 Car 타입으로 자동 변환
  • 자동 타입 변환을 통해 부모 클래스 타입으로 자식 클래스의 객체를 참조할 수 있지만, 이 경우 부모 클래스에 정의된 멤버만 접근할 수 있다.
  • 부모 타입의 필드는 다양한 자식 타입의 객체를 참조할 수 있다.
  • 이를 통해 여러 객체를 같은 타입으로 처리할 수 있어 코드의 유연성과 재사용성을 높인다.
Car car1 = new SportsCar();
Car car2 = new Truck();
  • 이와 같이 하나의 부모 타입 필드를 통해 다양한 자식 객체를 관리할 수 있다.
  • 메소드의 매개변수를 부모 클래스 타입으로 선언하면 자식 클래스의 객체를 매개변수로 전달할 수 있다.
  • 이를 통해 코드의 일반화가 가능해진다.
public void operateCar(Car car) {
    car.drive();
}

operateCar(new SportsCar());  // SportsCar 객체 전달
operateCar(new Truck());      // Truck 객체 전달
  • operateCar() 메소드는 Car 타입을 매개변수로 받기 때문에, 다양한 자식 객체를 전달받아 처리할 수 있다.
  • 강제 타입 변환(다운캐스팅)은 부모 타입으로 변환된 객체를 다시 자식 타입으로 변환하는 것을 의미한다.
  • 이때 instanceof 연산자를 사용하여 변환이 가능한지 확인한 후 안전하게 캐스팅해야 한다.
Car myCar = new SportsCar();
if (myCar instanceof SportsCar) {
    SportsCar mySportsCar = (SportsCar) myCar;
    mySportsCar.turbo = true;
}
  • instanceof 연산자를 사용하여 객체가 특정 타입인지 확인한 후 캐스팅을 수행하면 런타임 오류를 방지할 수 있다.
  • 객체가 특정 클래스의 인스턴스인지 확인하려면 instanceof 연산자를 사용한다.
  • 이를 통해 타입 변환이 가능한지 확인할 수 있다.
if (myCar instanceof SportsCar) {
    System.out.println("myCar는 SportsCar 타입입니다.");
}

 

추상 클래스

  • 추상 클래스는 공통적인 특성을 가진 객체들의 부모 역할을 하며, 직접 객체를 생성할 수 없다.
  • 주로 다른 클래스들이 상속받아 사용할 수 있도록 공통의 필드와 메소드를 정의한다.
  • 추상 클래스는 abstract 키워드를 사용하여 선언한다.
  • 추상 클래스는 일반 필드와 메소드를 가질 수 있으며, 하나 이상의 추상 메소드를 포함할 수 있다.
public abstract class Animal {
    String name;
    abstract void sound();  // 추상 메소드
}
  • Animal 클래스는 추상 클래스이므로 직접 객체를 생성할 수 없으며, 이를 상속받은 클래스에서 추상 메소드를 구현해야 한다.
  • 추상 메소드는 선언만 하고 구현하지 않으며, 자식 클래스에서 반드시 재정의해야 한다.
  • 이를 통해 자식 클래스마다 서로 다른 동작을 구현할 수 있다.
public class Dog extends Animal {
    @Override
    void sound() {
        System.out.println("멍멍");
    }
}

public class Cat extends Animal {
    @Override
    void sound() {
        System.out.println("야옹");
    }
}
  • 위 코드에서 Dog와 Cat 클래스는 Animal 클래스를 상속받아 각각 sound() 메소드를 재정의하였다.
  • 이를 통해 각 클래스의 특성에 맞는 동작을 구현할 수 있다.
  • 이처럼 추상 클래스를 사용하면 공통적인 기능을 정의하고, 자식 클래스에서 구체적인 구현을 강제할 수 있어 코드의 일관성과 재사용성을 높일 수 있다.

JAVA 기초 클래스, 상속 소개 이미지