[Java] 객체지향 프로그래밍(4)

Minit88·2023년 2월 28일
0

[Java]

목록 보기
9/18
post-thumbnail

Lab_01 - 다형성

일반적인 의미에서 다형성이란 "여러 개"를 의미하는 poly와 어떤 ‘형태' 또는 ‘실체’를 의미하는 morphism의 결합어로 하나의 객체가 여러 가지 형태를 가질 수 있는 성질을 의미합니다.

자바 프로그래밍에서 다형성은 한 타입의 참조변수를 통해 여러 타입의 객체를 참조할 수 있도록 만든 것을 의미한다. 구체적으로 얘기하면, 상위 클래스 타입의 참조변수를 통해서 하위 클래스의 객체를 참조할 수 있도록 허용한 것

즉, 다형성이란 사용 방법은 동일하지만 다양한 결과가 나오는 성질을 말한다.
아래 코드를 통해 예시를 보자


class Friend {
    public void friendInfo() {
        System.out.println("나는 당신의 친구입니다.");
    }
}

class BoyFriend extends Friend {
   
    public void friendInfo() {
        System.out.println("나는 당신의 남자친구입니다.");
    }
}

class GirlFriend extends Friend {
    
    public void friendInfo() {
        System.out.println("나는 당신의 여자친구입니다.");
    }
}

public class FriendTest {

    public static void main(String[] args) {
        Friend friend = new Friend(); // 객체 타입과 참조변수 타입의 일치
        BoyFriend boyfriend = new BoyFriend();
        Friend girlfriend = new GirlFriend(); // 객체 타입과 참조변수 타입의 불일치

        friend.friendInfo();
        boyfriend.friendInfo();
        girlfriend.friendInfo();
    }
}


[그림] 위 코드 실행결과
friend , boyfriend 객체는 각각 Friend , BoyFriend 클래스를 통해 생성이 되었지만 girlFriend 객체는 GirlFriend의 상위 클래스인 Friend를 통해 생성되었다. 이를 통해 ' 상위 클래스 타입의 참조변수로 하위 클래스의 객체를 참조하는 것 ' 임을 알 수 있다.

상위클래스 타입의 참조변수로 하위 클래스의 객체를 참조는 가능하지만, 이 반대 즉, 하위 클래스 타입의 참조 변수로 상위클래스의 객체를 참조하는 것은 불가능하다
아래 예제를 통해 확인해보면

public class FriendTest {

    public static void main(String[] args) {
        Friend friend = new Friend(); // 객체 타입과 참조변수 타입의 일치 -> 가능
        BoyFriend boyfriend = new BoyFriend();
        Friend girlfriend = new GirlFriend(); // 객체 타입과 참조변수 타입의 불일치 -> 가능
	//  GirlFriend friend1 = new Friend(); -> 하위클래스 타입으로 상위클래스 객체 참조 -> 불가능

        friend.friendInfo();
        boyfriend.friendInfo();
        girlfriend.friendInfo();
    }
}

참조변수의 타입 변환

참조 변수의 타입 변환은 다르게 설명하면 사용할 수 있는 멤버의 개수를 조절하는 것을 의미하는데, 이는 자바의 다형성을 이해하기 위해서 꼭 필요한 개념이다.

타입 변환을 위해서는 다음의 세 가지 조건을 충족해야 한다.

  • 서로 상속관계에 있는 상위 클래스 - 하위 클래스 사이에만 타입 변환이 가능하다
  • 하위 클래스 타입에서 상위 클래스 타입으로의 타입 변환(업캐스팅)은 형변환 연산자(괄호)를 생략할 수 있다.
  • 반대로 상위 클래스에서 하위 클래스 타입으로 변환(다운캐스팅)은 형변환 연산자(괄호)를 반드시 명시해야한다.

아래의 예시에서 좀더 자세히 보면

ublic class VehicleTest {
    public static void main(String[] args) {
        Car car = new Car();
        Vehicle vehicle = (Vehicle) car; // 상위 클래스 Vehicle 타입으로 변환(생략 가능)
        Car car2 = (Car) vehicle; // 하위 클래스 Car타입으로 변환(생략 불가능)
        MotorBike motorBike = (MotorBike) car; // 상속관계가 아니므로 타입 변환 불가 -> 에러발생
    }
}

class Vehicle {
    String model;
    String color;
    int wheels;

    void startEngine() {
        System.out.println("시동 걸기");
    }

    void accelerate() {
        System.out.println("속도 올리기");
    }

    void brake() {
        System.out.println("브레이크!");
    }
}

class Car extends Vehicle {
    void giveRide() {
        System.out.println("다른 사람 태우기");
    }
}

class MotorBike extends Vehicle {
    void performance() {
        System.out.println("묘기 부리기");
    }
}

psvm 에서 Vehicle vehicle = (Vehicle) car;은 Car와 Vehicle이 상속 관계에 있으므로, 캐스팅이 가능하며 이때 업 캐스킹이므로 타입 생략이 가능하다.

Car car2 = (Car) vehicle; 은 Car와 Vehicle이 상속 관계에 있으므로, 캐스팅이 가능하며 이때 다운 캐스팅이므로 타입 생략이 불가능하다.
MotorBike motorBike = (MotorBike) car; MotorBike 와 Car은 상속 관계가 아니므로 캐스팅이 불가능하다.

instanceof 연산자

instanceof 연산자는 앞서 배웠던 참조변수의 타입 변환, 즉 캐스팅이 가능한 지 여부를 boolean 타입으로 확인할 수 있는 자바의 문법요소이다.
캐스팅 가능 여부를 판단하기 위해 두가지를 따져보는데,

  • 객체를 어떤 생성자로 만들었는가
  • 클래스 사이에 상속관계가 존재하는가

프로젝트 규모가 커지고, 클래스가 많아지면 매번 이러한 정보를 확인하는 것이 어려워져 이를 해결하기 위해 instanceof 연산자를 사용한다.

참조_변수 instanceof 타입

아래의 예제를 통해 살펴보면

class Parent{}
class Child extends Parent{}

public class InstanceofTest {

    public static void main(String[] args){

        Parent parent = new Parent();
        Child child = new Child();

        System.out.println( parent instanceof Parent );  // true
        System.out.println( child instanceof Parent );   // true
        System.out.println( parent instanceof Child );   // false
        System.out.println( child instanceof Child );   // true
    }

}
  1. parent instanceof Parent : 부모가 본인 집을 찾았으니 true
  2. child instanceof Parent : 자식이 상속받은 부모 집을 찾았으니 true
  3. parent instanceof Child : 부모가 자식 집을 찾았으니 false (자식 집은 자식 집이지 부모 집은 아니니까)
  4. child instanceof Child : 자식이 본인 집을 찾았으니 true
    즉, instanceof는 해당 클래스가 자기집이 맞는지 확인해주는 것으로 생각해도 된다.

다형성의 활용 예제


public class Main {
    public static void main(String[] args) {
        Customer customer = new Customer();
        Coffee coffee1 = new Americano();
        Coffee coffee2 = new CaffeLatte();
        customer.buyCoffee(coffee1);
        customer.buyCoffee(coffee2);
    }
}

class Coffee{
    int price;
    String name;

    public Coffee(int price){
        this.price = price;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName(){
        return this.name;
    }
}
class Americano extends Coffee{
    public Americano(){
        super(4000);
        super.setName("아메리카노");

    }
}
class CaffeLatte extends Coffee{
    public CaffeLatte(){
        super(5000);
        super.setName("카페라떼");
    }
}

class Customer{
    int money = 50000;
    void buyCoffee(Coffee coffee){
        if (money<coffee.price){
            System.out.println("잔액이 부족합니다");
            return;
        }
        money = money - coffee.price;
        System.out.println(coffee.getName()+"를 구입했습니다.");
        System.out.println(String.format("%s를 구입후 남은 잔액 : %d",coffee.getName(),money));
        System.out.println();
    }
}


[그림] 위 코드를 실행한 결과

클래스 Americano , CaffeLatte는 Coffee를 상위 클래스로 상속 받고있다. psvm 에서 앞서 배운 다형성 내용과 접목해 알아보면

		Coffee coffee1 = new Americano();
        Coffee coffee2 = new CaffeLatte();
        customer.buyCoffee(coffee1);
        customer.buyCoffee(coffee2);
  

coffee1 객체에서 타입 Coffee 으로 하위 클래스 Americano 생성자를 불러와 상위 클래스 Coffee에 대입이 가능해지고 이를 coffee1 인스턴스로 사용함. coffee2도 마찬가지이다.
buyCoffee는 매개변수를 Coffee 타입 형태로 받기 때문에 Coffee 타입인 coffee1 변수가 들어갈 수 있게 된다.

Lab_02 - 추상화

먼저 추상이라는 용어의 사전적 의미를 보면 " 사물이나 표상을 어떤 성질, 고통성,본질에 착안하여 그것을 추출하여 파악하는 것" 이라고 정의한다.
여기서 핵심적인 개념은 공통성과 본질을 모아 추출하는 것이다.

추상화는 기존 클래스들의 공통적인 요소들을 뽑아서 상위 클래스를 만들어 내는 것이라고 할 수 있다.

abstract 제어자

abstract 는 주로 클래스와 메서드를 형용하는 키워드로 사용되는데, 메서드 앞에 붙은 경우를 '추상 메서드' 클래스 앞에 붙은 경우를 '추상 클래스'라 각각 부른다.

아래 예시를 통해 한번 확인해보면

abstract class AbstractExample{ // 추상 메서드가 최소 하나 이상 포함돼있는 추상 클래스
	abstract void start(); // 메서드 바디가 없는 추상 메서드
}

abstract 의 가장 핵심적인 개념은 앞서 언급한 '미완성'에 있다.
추상 메서드는 메서드의 시그니처만 있고 바디가 없는 메서드를 의미하는데, abstract 키워드를 메서드 이름 앞에 붙여주어 해당 메서드가 추상 메서드임을 표시한다.

AbstractExample abstractExample = new AbstractExample(); //에러발생

마지막으로 추상 클래스는 앞서 설명한대로 미완성 설계도이기 때문에 메서드 바디가 완성이 되기 전까지 이를 기반으로 객체 생성이 불가능하다.

추상 클래스

추상 클래스란, 메서드 시그니처만 존재하고 바디가 선언되어있지 않은 추상 메서드를 포함하는 '미완성 설계도'이다. 미완성된 구조를 가지고 있기에 객체를 생성하는 것이 불가능하지만 , 다음의 장점들로 미완성 클래스를 사용한다.

  • 상속 관계에 있어 새로운 클래스를 작성하는데 매우 유용하다
  • 오버라이딩을 통해 추상 클래스로부터 상속받은 추상 메서드의 내용을 구현하여 메서드를 완성시킬 수 있고, 이렇게 완성된 클래스를 기반으로 해당 객체를 생성할 수 있다.

아래의 예제를 통해 확인해보면

abstract class Animal {
	public String kind;
	public abstract void sound();
}

class Dog extends Animal { // Animal 클래스로부터 상속
	public Dog() {
		this.kind = "포유류";
	}

	public void sound() { // 메서드 오버라이딩 -> 구현부 완성
		System.out.println("멍멍");
	}
}

class Cat extends Animal { // Animal 클래스로부터 상속
	public Cat() {
		this.kind = "포유류";
	}

	public void sound() { // 메서드 오버라이딩 -> 구현부 완성
		System.out.println("야옹");
	}
}

class DogExample {       
    public static void main(String[] args) throws Exception {
       Animal dog = new Dog();
       dog.sound();

       Cat cat = new Cat();
       cat.sound();
    }
 }


[그림] 위 코드 실행값

추상 클래스 Animal을 선언해 Dog,Cat은 Animal을 상속하며 동시에 Dog,Cat의 생성자는 kind와 추상 메서드를 정의하고 있다.
이를 통해 추상클래스를 선언하고 그 하위 클래스에서 다시 메서드를 재정의하고 있음을 알 수 있다.

final 키워드

영어로 '최종의' 라는 뜻을 가지고 있는 final 키워드는 필드,지역 변수,클래스 앞에 위치할 수 있으며 그 위치에 따라 그 의미가 조금씩 달라지게 된다.

final class FinalEx{ // 확장,상속이 불가능한 클래스
	final in x =1;  // 변경되지 않는 상수
    
    final int getNUm(){ // 오버라이딩이 불가능한 메서드
    	final int localVar = x;
        return x;
    }
}

인터페이스

인터페이스는 "-간/사이"를 뜻하는 inter와 "얼굴/면"을 의미하는 face의 결합으로 구성된 단어로 두 개의 다른 대상 사이를 연결한다는 의미를 가지고 있다.

인터페이스의 기본 구조

인터페이스를 작성하는 것은 기본적으로 클래스를 작성하는 것과 유사하지만, class 키워드 대신 interface 키워드를 사용

일반 ㅡㄹ래스와 다르게, 내부의 모든 필드가 public static final로 정의된다. 또 static과 default 메서드 이외의 모든 메서드가 public absract 로 정의 된다는 차이가 존재한다.

public interface InterfaceEx {
    public static final int rock =  1; // 인터페이스 인스턴스 변수 정의
    final int scissors = 2; // public static 생략
    static int paper = 3; // public & final 생략

    public abstract String getPlayingNum();
		void call() //public abstract 생략 
}

인터페이스의 구현

class 클래스명 implements 인터페이스명 {
			// 인터페이스에 정의된 모든 추상메서드 구현
}

특정 인터페이스를 구현한 클래스는 해당 인터페이스에 정의된 모든 추상메서드를 구현해야한다.

즉, 어떤 클래스가 특정 인터페이스를 구현한다는 것은 그 클래스에게 인터페이스의 추상 메서드를 반드시 구현하도록 강제하는 것을 의미한다.

다른말로, 어떤 클래스가 어떤 인터페이스를 구현한다는 것은 그 인터페이스가 가진 모든 추상 메서드들을 해당 클래스 내에서 오버라이딩하여 바디를 완성한다라는 의미를 가진다.

인터페이스의 다중 구현

클래스 간의 상속에서 다중 상속은 허용이 되지 않는다. 하지만 인터페이스는 다중적 구현이 가능하다. 즉, 하나의 클래스가 여러 개의 인터페이스를 구현할 수 있다.

class ExampleClass implements ExampleInterface1,ExampleInterface2,ExampleInterface3{
	///
}

다음의 예를 한번 살펴보면

interface Animal { // 인터페이스 선언. public abstract 생략 가능.
	public abstract void cry();
} 

interface Pet {
	void play();
}

class Dog implements Animal, Pet { // Animal과 Pet 인터페이스 다중 구현
    public void cry(){ // 메서드 오버라이딩
        System.out.println("멍멍!"); 
    }

    public void play(){ // 메서드 오버라이딩
        System.out.println("원반 던지기");
    }
}

class Cat implements Animal, Pet { // Animal과 Pet 인터페이스 다중 구현
    public void cry(){
        System.out.println("야옹~!");
    }

    public void play(){
        System.out.println("쥐 잡기");
    }
}

public class MultiInheritance {
    public static void main(String[] args) {
        Dog dog = new Dog();
        Cat cat = new Cat();

        dog.cry();
        dog.play();
        cat.cry();
        cat.play();
    }
}

Dog,Cat 클래스는 각각 Animal과 Pet 인터페이스를 다중으로 구현하여 객체에 맞는 메서드를 오버라이딩하고 그 내용을 출력값으로 돌려주고 있다.

마지막으로, 특정 클래스는 다른 클래스로부터의 상속을 받으면서 동시에 인터페이스를 구현할 수 있다.

abstract class Animal { // 추상 클래스
	public abstract void cry();
} 
interface Pet { // 인터페이스
	public abstract void play();
}

class Dog extends Animal implements Pet { // Animal 클래스 상속 & Pet 인터페이스 구현
    public void cry(){
        System.out.println("멍멍!");
    }

    public void play(){
        System.out.println("원반 던지기");
    }
}

class Cat extends Animal implements Pet { // Animal 클래스 상속 & Pet 인터페이스 구현
    public void cry(){
        System.out.println("야옹~!");
    }

    public void play(){
        System.out.println("쥐 잡기");
    }
}

public class MultiInheritance {
    public static void main(String[] args) {
        Dog dog = new Dog();
        Cat cat = new Cat();

        dog.cry();
        dog.play();
        cat.cry();
        cat.play();
    }
}

인터페이스의 장점

인터페이스의 장점을 설명하기 위해 예시를 들면,

public class InterfaceExample {
    public static void main(String[] args) {
        User user = new User(); // User 클래스 객체 생성
        user.callProvider(new Provider()); // Provider 객체 생성 후에 매개변수로 전달
    }
}

class User { // User 클래스
    public void callProvider(Provider provider) { // Provider 객체를 매개변수로 받는 callProvider 메서드
        provider.call();
    }
}

class Provider { //Provider 클래스
    public void call() {
        System.out.println("무야호~");
    }
}

// 출력값
무야호~

해당 코드를 보면 User 클래스에 정의된 callProvider 메서드의 매개 변수로 Provider 타입이 전달되어 호출되는 것을 볼 수 있다.
여기서 Provider 클래스가 아닌 Provider2 클래스로 교체를 해야하는 상황이 발생하면

public class InterfaceExample {
    public static void main(String[] args) {
        User user = new User(); // User 클래스 객체 생성
        user.callProvider(new Provider2()); // Provider객체 생성 후에 매개변수로 전달
    }
}

class User { // User 클래스
    public void callProvider(Provider2 provider) { // Provider 객체를 매개변수로 받는 callProvider 메서드
        provider.call();
    }
}

class Provider2 { //Provider 클래스
    public void call() {
        System.out.println("야호~");
    }
}

// 출력값
야호~

처럼 번거로운 수정 과정을 거쳐야 한다.
이를 보완하기 위해 인터페이스를 사용한 코드를 통해 예시를 살펴보면,

interface Cover { // 인터페이스 정의
    public abstract void call();
}

public class Interface4 {
    public static void main(String[] args) {
        User user = new User();
//        Provider provider = new Provider();
//        user.callProvider(new Provider());
        user.callProvider(new Provider2());
    }
}

class User {
    public void callProvider(Cover cover) { // 매개변수의 다형성 활용
        cover.call();
    }
}

class Provider implements Cover {
    public void call() {
        System.out.println("무야호~");
    }
}

class Provider2 implements Cover {
    public void call() {
        System.out.println("야호~");
    }
}

🎓Reference

  1. instanceof 연산자
profile
" To be BE "

0개의 댓글