[Java/자바] 2. 객체지향 Object-Oriented (2)

메소드 오버로딩 Overloading

  • 변수는 중복 정의는 불가하나 클래스 내에 메소드는 중복 정의가 가능하다.
  • 한 클래스 내에서 동일한 이름의 메소드라도 메개변수의 개수와 타입만 다르다면 다른 메소드로 인식하는 것을 의미. 즉, 동일한 이름의 메소드라도 매개변수의 형태에 따라 다른 일을 수행할 수도 있다.

오버로딩이 필요한 이유

다양한 데이터 타입을 출력하려는 함수를 만들 때, 데이터 타입마다 메소드 이름을 달리하면 코드의 중복이 많아지고 재사용성도 저하된다.

이때 매개변수만 달리하여 매개변수에 따라 올바른 함수가 작동하도록 하는 Overloading을 사용한다면, 데이터 타입이 바뀌더라도 메소드를 호출하는 코드를 수정할 필요가 없다.

주의

  • 매개변수의 개수와 타입이 모두 다른 경우 - 오버로딩 인식 O
  • 리턴타입이 다른 경우 - 오버로딩 X
  • 매개변수의 이름이 다른 경우 - 오버로딩 X
  • 매개변수의 개수와 타입이 같지만 순서가 다른 경우 - 오버로딩 O
  • 매개변수가 형변환된 다른 타입인 경우 - 오버로딩 O

생성자 오버로딩 Overloading

  • 생성자는 클래스로부터 객체를 생성할 때 객체의 변수들을 초기화하는 역할을 담당하는 메서드로, 이 또한 메소드 오버로딩이 지원된다.

∴ 하나의 클래스는 매개변수의 유형과 개수를 달리해서 여러 개의 생성자를 갖도록 한다.

이유

클래스로부터 객체를 생성할 때, 필요한 변수들만 적절히 초기화하기 위해서

this()의미 & 사용법

this예약어 this()생성자
생성자나 메서드의 매개변수 이름이 객체 변수의 이름과 같은 경우 같은 클래스 내의 오버로딩 된 다른 생성자 메소드를 호출할 때 사용

this()를 사용하여 중복되는 초기화 코드를 최소화한 경우

int age;
int salary;

public Employee(){
    this(0, "Anonymity", 0, 0);
}
public Employee(int employeeNo, String name){
    this.employeeNo = employeeNo;
    this.name = name;
}
//중복
public Employee(int employeeNo, String name, int age){
    this.employeeNo = employeeNo;
    this.name = name;
    this.age = age;
}
//this()사용하여 중복을 없앤 예
public Employee(int employeeNo, String name, int age){
    this(employeeNo, name);
    this.age = age;
}

매개변수

메소드 호출 시, 객체 간 메세지가 전달될 때 부가 정보가 필요한 경우 매개변수로 전달된다.

기본형 변수 - 값 복사

  • Call by value
  • 특정 변수의 값을 수정해도 다른 변수에는 영향을 미치지 않는다.

참조형 변수 - 주소 복사

  • Call by address
  • 데이터를 공유하기 때문에, 특정 변수로 배열의 값을 수정하면 동일한 객체를 참조하는 다른 변수도 변경된 값을 인식한다.
  • 매개변수를 통해 넘겨진 값/변경된 값을 계속해서 유지하고 싶은 경우 매개변수를 참조형 변수로 선언한다.

가변적 매개변수

  • 매개변수의 숫자를 컴파일이나 실행 시에 미리 지정하지 않는 방식
  • 하나의 메소드만 정의하여 매개변수의 개수를 가변적으로 사용하는 방식
  • 유지보수의 용이성을 위해, 매개변수 개수가 지정되어야만 하는 제한을 극복하기 위해 제공된다.

    public static int intSum(int... num){
        
    }
    
    //... : 매개변수가 가변적이라는 것을 의미한다.
  • 가변적 매개변수는 여러 개의 매개변수와 함께 사용가능하나 함께 사용할 때에는 마지막에서 한 번만 사용가능하다.

    //오류
    int add(int... i, String s){}
    //오류
    int add(String... s, int... i){}
    //적합
    int add(String s, int... i){}

상속 Inheritance

  • 부모 클래스의 모든 변수와 메소드를 자식 클래스가 모두 물려받는 것을 말한다.
  • 새로운 클래스가 필요할 때, 이미 필요한 속성을 일부로 가진 클래스가 있다면 상속을 이용하면 된다.
  • 논리적으로 “is a ~“관계가 성립할 때 상속을 이용할 수 있으며, 부적절한 상속은 소스코드에 대한 분석과 개발을 어렵게 만드는 요인이 될 수 있다.
  • 예약어 extends를 사용한다.
  • 상속 = 부모로부터 물려받은 공통적인 특징 + 자식 클래스에서만 가지는 추가적인 특징
  • 개발의 편의성과 가독성을 위해 문법적으로 단일 상속만을 허용한다. 즉, 하나의 클래스는 오직 하나의 부모 클래스만 상속할 수 있다. 다중 상속을 허용하면 중복되는 변수와 메소드가 상속되는 문제가 발생하기 때문이다.

생성자 자동 호출

상속된 부모 클래스 객체가 생성될 때 부모 클래스의 생성자도 자동으로 호출되어 수행되면서 객체 초기화가 이뤄지도록 한다.

super()생성자

this()생성자 호출 super()생성자 호출
클래스 안에서 오버라이딩 된 또 다른 생성자를 호출하기 위해 사용 부모 클래스의 생성자를 명시적으로 호출할 때 사용
부모 클래스의 생성자가 오버로딩되어 여러 개 존재하는 경우 특정 생성자를 호출하기 위해 사용

부모 클래스의 생성자를 호출하는 super()는 반드시 자식 클래스 생성자의 첫 번째 라인에 위치해야한다. ∵ 부모 클래스의 생성자가 항상 자식 클래스의 생성자보다 먼저 수행되어야 하기 때문이다.

상속과 변수

  • 부모 클래스에서 private로 정의된 변수는 상속 불가
  • 부모 클래스의 변수와 동일한 이름으로 자식 클래스에서 정의된 변수는 부모 클래스로부터 상속되지 않는다.
this예약어 super예약어
생성된 객체 자신에 대한 참조를 의미
멤버 변수와 메서드 매개변수의 이름이 같을 경우, 두 변수를 구분하기 위해 사용
부모 객체에 접근할 수 있는 참조변수로 사용

오버라이딩 Overriding

  • 부모 클래스의 메소드를 재사용하지 않고 새롭게 정의하여 사용하는 것
  • 메소드 재정의
  • 자식 클래스에서 재정의된 메소드를 부모 클래스의 메소드와 메소드 이름, 매개변수의 유형과 개수가 동일해야 한다.

if 부모 클래스의 메소드도 재사용하면서 재정의도 하고 싶다면?

👉 super예약어 사용

class Camera{
    String name;
    int sheets;
    
    public void takePicture(){
        System.out.println("print 1");
    }
}

class PolaroidCamera extends Camera{
    int batteryGage;
    
    public void takePicture(){
        super.takePicture();
        
        System.out.println("print 2");
        System.out.println("print 3");
    }
}

final 예약어와 오버라이딩

final 예약어 + 메소드는 오버라이딩 금지를 의미한다.

❗️오버라이딩을 금지가 필요한 이유는 때때로 부모 클래스 메소드를 자식 클래스 메소드가 잘못 정의하는 일을 막기 위해서이다.

추상 메소드

abstract예약어를 사용하여 정의된 메소드로, 메소드의 시그니처만 정의된 메소드이다.

필요한 이유

추상 메소드는 의미가 없지만, 자식 클래스에서 오버라이딩을 했을 때 의미가 있다. 즉, 상속과 관련이 깊은 예약어이다.

추상 클래스

  • abstract예약어를 사용하여 정의된 클래스로, 일반적으로 하나 이상의 추상 메소드를 포함하지만 추상 메소드가 없는 클래스도 추상 클래스로 선언 가능. 하지만 추상 메소드를 포함하고 있다면 무조건 추상 클래스로 선언되어야 한다.
  • 추상 클래스는 객체 생성을 할 수 없다.

추상 클래스 활용 및 필요한 이유

  • 추상 클래스로 객체를 생성하기 위해서는 자식 클래스를 생성하고 추상 메소드들을 자식 클래스에서 오버라이딩해야 한다.
  • 추상 클래스를 적용하면, 자식 클래스들의 메소드를 통일할 수 있다.
  • 따라서 최소한의 수정으로 원하는 객체를 사용할 수 있게 되어, 유지보수성이 좋아진다.

👉 추상 클래스를 사용하면 유지보수의 편의성을 높일 수 있다.

내부 클래스

  • 클래스가 다른 클래스를 포함하는 경우, 포함되는 클래스를 내부 클래스라고 칭한다.
  • 내부 클래스는 정의되는 위치에 따라 멤버 클래스와 지역 클래스로 나뉜다.

    • 멤버 클래스

      • 멤버 변수와 동일한 위치에 선언된 내부 클래스
      • static 멤버와 instance 멤버로 나뉜다.
      • 동일한 클래스 뿐만 아니라 다른 클래스에서도 활용 가능
      • 클래스의 멤버 변수와 성격이 비슷하다.
    • 지역 클래스

      • 메소드 내에 클래스가 정의되어 있는 경우
      • 지역 클래스(이름 있음), 무명 클래스(이름 없음)로 나뉜다.
      • 활용 범위가 메소드 블록 내부로 제한되는 특징을 갖는 등 지역 변수와 성격이 비슷하다.
  • 내부 클래스를 통해 자바의 클래스 구조를 더 조직화하고 소스 코드 구현 시 효율을 높일 수 있다.
  • 내부 클래스가 생성되기 위해서는 외부 클래스의 객체가 반드시 필요하다.

Instance 멤버 내부 클래스

  • 클래스 멤버와 동일한 위치에서 선언되는 내부 클래스
  • 멤버 변수나 메소드와 동일한 위치에서 선언되었기 때문에 다른 외부의 클래스에서도 사용 가능

    class Outside{
        public class Inside{
            
        }
    }
    
    public class InnerClassTest{
        public static void main(String args[]){
            Outside outer = new Outside();
            Ouside.Inside inner = outer.new Inside();
        }
    }

Static 멤버 내부 클래스

  • static예약어를 통해 내부 클래스를 생성하면 외부 클래스 객체를 생성하지 않아도 내부 클래스 객체를 생성할 수 있다.

    class Outside{
        public static class StaticInner{
            
        }
    }
    
    public class InnerClassTest{
        public static void main(String args[]){
            Ouside.StaticInner sinner = Ouside.StaticInner();
        }
    }

이름이 있는 지역 내부 클래스

  • 메소드 내부에서 정의된 클래스로, 지역변수와 동일한 범위를 가진다.
  • 클래스의 이름이 명시되는 클래스이다.

    class Animal{
        void eatFood(){
            //지역변수와 동일한 범위를 가지기 때문에
            //선언된 메소드 블록 내에서만 사용 가능
            class snack{
                
            }
        }
    }
    
    public class InnerClassTest{
        public static void main(String args[]){
            Animal outer = new Animal();
        }
    }

이름이 없는 지역 내부 클래스

  • 이름을 갖지 않는 무명의 내부 클래스
  • new예약어 뒤에 명시된 클래스가 기존 클래스인 경우 자동으로 이 클래스의 자식 클래스가 된다.
  • 이름이 없는 지역 내부 클래스는 추상 클래스의 객체를 내부 클래스 형태로 생성할 때 자주 사용된다.
  • 추상 클래스는 추상 메소드를 포함하고 있기 때문에 객체를 생성할 수 없다. 따라서 간단하게 이름이 없는 지역 내부 클래스로 만들어 사용하는 것이 편리할 수있다.

    abstract class TV{
        public abstract void powerOn();
        public abstract void powerOff();
    }
    
    public class AnonymousTest{
    public static void watchTV(TV tv){
        tv.powerOn();
        tv.powerOff();
    }
        public static void main(String args[]){
            // 이름 없는 지역 내부 클래스 객체 생성
            // TV라는 추상 클래스의 객체를 내부 클래스 형태로 생성했기 때문에, 실제로는 TV클래스를 상속한 내부 클래스가 만들어진다.
            watchTV(new TV(){
                public void powerOn(){
                    System.out.println("ON");
                }
                public void powerOff(){
                    System.out.println("OFF");
                }
            });
        }
    }

객체 형변환 Object Casting

  • 객체 참조 변수 또한 Casting이 이뤄진다.
  • 예) leftObjRef = rightObjRef

    • leftObjRef: 부모 클래스
    • rightObjRef: 자식 클래스
    • 왼쪽 항과 오른쪽 항의 객체 유형이 서로 다른 경우, 두 유형이 서로 상속관계에 있다.
    • 왼쪽 객체가 오른쪽 객체의 상위 클래스인 경우에만 묵시적 형변환이 일어난다.
    • 자식 클래스에서 부모 클래스로의 할당/형변환은 가능하지만 반대의 경우에는 명시적 형변환을 해야한다.

      • ❗️부모 클래스를 자식 클래스로 강제 변환하는 경우 할당되는 인스턴스 값에 따라 실행오류가 발생할 수 있다.
      • 따라서 내부 특정 클래스 형이 다른 클래스 형으로 변환될 수 있는지의 여부를 수시로 판단해야하는데, 이를 위해 instanceof연산자를 사용한다.

        • 생성된 객체가 class와 관계있는 type으로 만들어졌는지 확인하고
        • true 또는 false 값을 반환한다.
        • <생성된 객체 참조 변수> instanceof <class 또는 interface 명>
  • 클래스의 형변환은 기본적으로 상속관계가 아닌 클래스 사이에서는 발생하지 않는다.
  • 묵시적 형변환을 통해 자식 클래스의 객체는 부모 타임의 참조 변수에 할당될 수 있다.
  • 객체의 형변환을 이용하면 프로그램을 유지보수가 편한 구조로 변경할 수 있다.

❗️주의

형변환에 참여한, 서로 상속관계에 있는 두 클래스 간에는 동일한 이름의 변수가 존재하거나 메소드가 오버라이딩 되어있을 수 있다. 이때 생성된 객체 변수를 통해 멤버에 접근할 때 주의해야 한다.

예시

class Parent{
    int num = 10;
    void printNum(){
        System.out.println(num);
    }
}
class Child extends Parent{
    int num = 20;
    void printNum(){
        System.out.println(num);
    }
}
public class ObjectCastTest{
    public static void main(String args[]){
        //case 1
        //Output is
        // 10
        // 10
        Parent p = new Parent();
        p.printNum();
        System.out.println(p.num);
        
        //case 2
        //Output is
        // 20
        // 20
        Child p = new Child();
        p.printNum();
        System.out.println(p.num);
        
        //case 3
        //Output is
        // 20
        // 10
        Parent p = new Child();
        p.printNum();
        System.out.println(p.num);
    }
}

case3의 결과가 다른 이유

  • output 20: 변수에 대한 접근은 객체의 유형에 의해 결정
  • output 10: 메서드 호출은 할당되는 인스턴스에 의해 결정

👉 객체 참조 변수가 변수나 메소드를 참조하는 경우, 참조 관계를 결정하는 시간이 다르기 때문에 나타나는 차이


Hi! I'm @Yeseul Lee
Passionate for what I love

GitHubLinkedIn