웹(WEB)/자바(Java)

[Java] 자바 객체지향 프로그래밍

마달랭 2024. 12. 29. 23:15
반응형

개요

자바는 기본적으로 객체지향 프로그래밍 언어이다.

객체란 주체가 아닌, 주체가 활용하는 것을 의미한다.

우리 주변에 있는 모든 것으로 사물, 개념, 논리 등 프로그래밍의 대상이 된다.

 

객체지향 프로그래밍은 주변의 많은 것들을 객체화 하여 프로그래밍 하는것을 말한다.

예를 들어 게임을 할 때 자신의 캐릭터는 하나의 객체가 되어 상태를 갖고 동작을 한다.

 

 

장점

블록 형태의 모듈화된 프로그래밍을 통해 신뢰성 높은 프로그래밍이 가능하다.

추가, 수정, 삭제에 용이하여 객체에 대한 관리를 쉽게할 수 있다.

재사용성이 높아 작업의 반복을 줄여 수고로움을 줄여준다.

또한 실제 세계와 유사한 모델링이 가능하다.

 

 

현실과 프로그램의 객체

현실의 객체가 갖는 속성과 기능은 추상화되어 클래스에 정의된다.

클래스는 구체화 되어 프로그램의 객체가 된다.

 

현실의 객체는 실생활에 구체화 되어 있는 내용으로 우리가 만지고 느낄 수 있는 것을 의미한다.

클래스는 객체를 정의해 놓은 객체를 만들기 위한 틀이다.

객체는 클래스를 데이터 타입으로 메모리에 생성되어 실제로 동작하는 것을 의미한다.

 

 

특징

객체 지향 프로그램은 4가지의 특징을 가진다.

  • 추상화 : 현실의 객체를 추상화 해서 클래스를 구성한다.
  • 다형성 : 하나의 객체를 여러가지 타입(형)으로 참조할 수 있다.
  • 상속 : 부모 클래스의 자산을 물려받아 자식을 정의함으로 코드의 재사용이 가능하다.
  • 캡슐화 : 데이터를 외부에 직접 노출시키지 않고 메서드를 이용해 보호할 수 있다.

아래에서 네 가지 특징에 대해 자세히 알아보도록 하자

 

 

캡슐화

public class BankAccount {
    private int balance = 0;

    // 잔액 조회 (Getter)
    public int getBalance() {
        return balance;
    }

    // 입금 (Setter)
    public void deposit(int amount) {
        if (amount > 0) {
            balance += amount;
        }
    }
}

 

  • 데이터를 보호하고 외부에서 직접 접근하지 못하도록 함.
  • getter/setter 메서드를 사용해 접근.

클래스 내부의 private 형식의 변수인 balance는 인스턴스에서 변수값을 임의로 확인할 수 없다.

단, 내부 멤버 함수 getBalance를 통해 balance에 저장된 값을 확인할 수 있다.

 

 

 

상속

class Animal {
    void eat() {
        System.out.println("This animal eats food.");
    }
}

class Dog extends Animal {
    void bark() {
        System.out.println("Dog barks.");
    }
}

public class Test {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.eat();  // 부모 클래스 메서드 사용
        dog.bark(); // 자식 클래스 메서드 사용
    }
}

 

  • 기존 클래스를 기반으로 새로운 클래스를 만듦.
  • 코드의 재사용성 증가.

Animal클래스를 상속한 Dog클래스에서는 부모의 멤버 함수인 eat함수를 사용할 수 있다.

또한 Dog클래스의 멤버 함수인 bark함수 또한 사용할 수 있다.

이를 통해 Dog함수에서는 eat함수를 정의할 필요가 없이 부모의 함수를 재사용한다.

 

 

 

다형성

class Animal {
    void sound() {
        System.out.println("Some sound...");
    }
}

class Cat extends Animal {
    @Override
    void sound() {
        System.out.println("Meow");
    }
}

public class Test {
    public static void main(String[] args) {
        Animal myCat = new Cat();
        myCat.sound();  // "Meow" 출력
    }
}

 

  • 같은 메서드 이름이지만 다른 동작을 수행.
  • 메서드 오버라이딩(Override)와 오버로딩(Overload)으로 구현.

 

Cat클래스는 Animal클래스를 상속한다, 두 클래스 모두 sound라는 동일한 멤버 함수를 포함하고 있다.

하지만 오버라이딩을 통해 동일한 이름을 가진 함수를 Cat클래스 만의 멤버 함수로 표현할 수 있다.

 

 

추상화

abstract class Shape {
    abstract void draw();  // 추상 메서드
}

class Circle extends Shape {
    @Override
    void draw() {
        System.out.println("Drawing a circle.");
    }
}

public class Test {
    public static void main(String[] args) {
        Shape shape = new Circle();
        shape.draw();  // "Drawing a circle."
    }
}

 

  • 불필요한 부분은 숨기고 필요한 부분만 노출.
  • 추상 클래스(Abstract class)나 인터페이스(Interface)로 구현.

 

Circle의 부모 클래스인 Shape 클래스의 draw함수를 Circle 클래스 내에서 오버라이딩 했다.

이를 통해 shape는 Shape 클래스 타입이지만 draw함수를 사용하면 Circle클래스의 shape함수를 사용할 수 있다.

 

 

오버라이딩

오버라이딩은 객체지향의 핵심인 "다형성"을 구현하는 중요한 도구로, 부모 클래스의 메서드를 자식 클래스에서 재정의하는 것이다.

 

  • 메서드의 이름, 매개변수, 리턴 타입이 완전히 동일해야 한다.
  • 자식 클래스는 부모 클래스의 메서드 기능을 다르게 구현할 수 있다.

오버로딩의 경우 메서드의 이름은 같지만 매개변수의 타입이나 리턴 타입이 달라야했다.

하지만 오버라이딩은 모든 내용이 완전히 동일해야 한다.

 

// 부모 클래스
class Animal {
    // 부모 클래스 메서드
    void sound() {
        System.out.println("Some sound...");
    }
}

// 자식 클래스
class Dog extends Animal {
    @Override
    void sound() {
        System.out.println("Woof woof!");
    }
}

// 메인 클래스
public class Test {
    public static void main(String[] args) {
        Animal animal = new Animal();
        animal.sound();  // Some sound...

        Dog dog = new Dog();
        dog.sound();  // Woof woof!

        // 업캐스팅 (부모 타입으로 자식 객체 참조)
        Animal polyDog = new Dog();
        polyDog.sound();  // Woof woof! (다형성)
    }
}
  • 메서드 시그니처(이름, 매개변수, 리턴 타입)가 같아야 함
  • 접근 제어자는 더 넓거나 같아야 함 (부모보다 좁은 범위 X)
  • 부모 메서드에 예외가 선언되어 있다면, 자식은 같은 예외 또는 하위 예외만 던질 수 있음
  • @Override 어노테이션을 사용해 오버라이딩을 명시하는 것이 권장됨

Animal클래스와 Dog클래스의 sound함수는 이름, 매개변수, 리턴 타입이 모두 같은 것을 볼 수 있다.

또한 @Override를 명시해 줌으로서 컴파일러가 오버라이딩을 정확히 수행했는지 검사해준다.

 

 

접근 제어자와 오버라이딩 규칙

부모 메서드 접근 제어자 자식 클래스에서 가능한 접근 제어자
public public
protected protected, public
default (패키지 접근) default, protected, public
private 오버라이딩 불가

 

 

오버라이딩과 오버로딩의 차이점

구분 오버라이딩 (Overriding) 오버로딩 (Overloading)
정의 부모 메서드를 자식에서 재정의 같은 이름의 메서드를 여러 개 정의
관계 상속 관계에서만 발생 같은 클래스 내에서 발생
시그니처 메서드 이름, 매개변수, 리턴 타입 동일 매개변수의 타입, 개수 다름
접근 범위 같거나 더 넓은 범위 상관없음
어노테이션 @Override 사용 (권장) 사용하지 않음

 

 

super

자식 클래스에서 오버라이딩된 메서드 내에서 부모 메서드의 원래 기능을 호출하고 싶다면 super 키워드를 사용한다.

this를 통해 멤버에 접근했듯이 super를 통해 조상 클래스 멤버에 접근할 수 있다.

이를 통해 클래스 내부에 멤버 함수를 중복적으로 정의할 필요 없이 조상의 코드를 재사용 할 수 있다.

class Animal {
    void sound() {
        System.out.println("Some sound...");
    }
}

class Dog extends Animal {
    @Override
    void sound() {
        super.sound();  // 부모 메서드 호출
        System.out.println("Woof woof!");
    }
}

public class Test {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.sound();
    }
}

 

Dog클래스의 인스턴스 dog를 생성한 뒤 sound함수를 출력하였다.

Dog클래스의 sound함수에선 super.sound() 호출 후 이하 로직을 수행한다.

따라서 부모 클래스 Animal의 sound함수를 호출한 뒤 자신의 로직을 실행하게 된다.

 

 

변수의 범위

변수의 범위는 사용된 위치에서 점점 확장해가며 처음 만난 선언부에 연결된다.

메소드 내부 > 해당 클래스 멤버 변수 -> 조상 클래스 멤버 변수 순서이다.

class Par {
    String s = "parent";
}

class Child extends Par {
    String s = "child";

    void func() {
        String s = "local";
        System.out.println(s);
        System.out.println(this.s);
        System.out.println(super.s);
    }
}

public class Main {
    public static void main(String[] args) {
        Child child = new Child();
        child.func();
    }
}

 

void func() {
//        String s = "local";
        System.out.println(s);
        System.out.println(this.s);
        System.out.println(super.s);
    }

 

class Child extends Par {
//    String s = "child";

    void func() {
//        String s = "local";
        System.out.println(s);
        System.out.println(this.s);
        System.out.println(super.s);
    }
}

 

 

종합

객체지향 프로그래밍의 특징을 조합한 예시를 확인해 보자.

// 상위 클래스
class Person {
    String name;

    public Person(String name) {
        this.name = name;
    }

    void introduce() {
        System.out.println("Hi, I'm " + name);
    }
}

// 하위 클래스 (상속)
class Student extends Person {
    String school;

    public Student(String name, String school) {
        super(name);
        this.school = school;
    }

    @Override
    void introduce() {
        System.out.println("Hi, I'm " + name + " and I study at " + school);
    }
}

// 실행 클래스
public class Main {
    public static void main(String[] args) {
        Person p = new Person("John");
        Student s = new Student("Alice", "XYZ High School");

        p.introduce();  // Hi, I'm John
        s.introduce();  // Hi, I'm Alice and I study at XYZ High School
    }
}

 

Person 클래스의 인스턴스 p를 생성자 함수에 "John"이라는 이름을 매개변수로 전달하였다.

따라서 생성자 함수로 인해 p의 name엔 John이 저장되었다.

 

Student 클래스의 인스턴스 s를 생성자 함수에 "Alice", "XYZ High School"을 매개변수로 전달하였다.

따라서 생성자 함수로 인해 상속받은 부모 클래스의 변수 name에 "Alice"를, 멤버 변수 school에 "XYZ High School"을 저장하였다.

 

두 클래스는 모두 introduce라는 멤버 함수를 가지고 있다.

Student 클래스는 부모의 introduce함수를 오버라이딩을 통해 재정의하였다.

따라서 동일한 이름의 함수를 호출하여도 다른 형태의 출력값을 가진다.

728x90
반응형

'웹(WEB) > 자바(Java)' 카테고리의 다른 글

[Java] 자바 제한자  (0) 2024.12.30
[Java] 자바 package, import  (0) 2024.12.29
[Java] 자바 생성자, this  (1) 2024.12.28
[Java] 자바 메서드  (0) 2024.12.28
[Java] 자바 변수의 선언 위치에 따른 분류  (0) 2024.12.28