[Java] 객체지향 심화

최우형·2023년 2월 27일
1

Java

목록 보기
10/24
post-thumbnail

📌상속

상속이란 기존의 클래스를 재활용하여 새로운 클래스를 작성하는 자바의 문법 요소

두 클래스를 상위 클래스와 하위 클래스를 나누며, 서로 상속 관계에 있다고 하고, 하위 클래스는 상위 클래스가 가진 모든 멤버(필드, 메서드, 이너클래스)를 상속받는다.

하위 클래스의 멤버 개수는 언제나 상위 클래스의 그것과 비교했을 때 같거나 많다.

~클래스로부터 상속받았다 보다는 "~클래스로부터 확장되었다."가 적절하다.


예시

사람의 클래스가 상위 클래스, 그리고 프로그래머, 댄서, 가수 클래스가 상위클래스로부터 특정한 속성과 기능을 내려받는 하위 클래스이다.


상속을 사용하는 이유?

코드를 재사용하여 보다 적은 양의 코드로 새로운 클래스를 작성할 수 있어 코드의 중복을 제거할 수 있다.


코드 예제

extends 키워드를 사용하여, 클래스명 다음에 extends 상위 클래스명을 사용하여 정의한다.

class Person {
    String name;
    int age;

    void learn(){
        System.out.println("공부를 합니다.");
    };
    void walk(){
        System.out.println("걷습니다.");
    };
    void eat(){
        System.out.println("밥을 먹습니다.");
    };
}

class Programmer extends Person { // Person 클래스로부터 상속. extends 키워드 사용 
    String companyName;

    void coding(){
        System.out.println("코딩을 합니다.");
    };
}

class Dancer extends Person { // Person 클래스로부터 상속
    String groupName;

    void dancing(){
		    System.out.println("춤을 춥니다.");
		};
}

class Singer extends Person { // Person 클래스로부터 상속
    String bandName;

    void singing(){
		    System.out.println("노래합니다.");
		};
    void playGuitar(){
		    System.out.println("기타를 칩니다.");
		};
}

public class HelloJava {
    public static void main(String[] args){

        //Person 객체 생성
        Person p = new Person();
        p.name = "김코딩";
        p.age = 24;
        p.learn();
        p.eat();
        p.walk();
        System.out.println(p.name);

        //Programmer 객체 생성
        Programmer pg = new Programmer();
        pg.name = "박해커";
        pg.age = 26;
        pg.learn(); // Persons 클래스에서 상속받아 사용 가능
        pg.coding(); // Programmer의 개별 기능
        System.out.println(pg.name);

    }
}

//출력값
공부를 합니다.
밥을 먹습니다.
걷습니다.
김코딩
공부를 합니다.
코딩을 합니다.
박해커

자바의 객체 지향 프로그래밍에서는 단일 상속(single inheritance)만을 허용한다. - 다중 상속 X


포함관계

포함(composite)은 상속처럼 클래스를 재사용할 수 있는 방법으로, 클래스의 멤버로 다른 클래스 타입의 참조변수를 선언하는 것을 의미한다.

public class Employee {
    int id;
    String name;
    Address address;

    public Employee(int id, String name, Address address) {
        this.id = id;
        this.name = name;
        this.address = address;
    }

    void showInfo() {
        System.out.println(id + " " + name);
        System.out.println(address.city+ " " + address.country);
    }

    public static void main(String[] args) {
        Address address1 = new Address("서울", "한국");
        Address address2 = new Address("도쿄", "일본");

        Employee e = new Employee(1, "김코딩", address1);
        Employee e2 = new Employee(2, "박해커", address2);

        e.showInfo();
        e2.showInfo();
    }
}

class Address {
    String city, country;

    public Address(String city, String country) {
        this.city = city;
        this.country = country;
    }
}

// 출력값
1 김코딩
서울 한국
2 박해커
도쿄 일본

원래라면 Address 클래스에 포함되어 있는 인트선스 변수 citycountry를 각각 Employee 클래스의 변수로 정의해주어야 하지만, Address 클래스로 해당 변수들을 묶어준다음 Employee 클래스 안에 참조변수를 선언하는 방법으로 코드의 중복을 없애고 포함관계로 재사용하고 있다.

클래스 간의 관계를 상속관계를 맺어 줄 것인지 포함 관계를 맺어 줄 것인지 어떤 기준으로 판별할까?
-> 가장 쉬운 방법은 클래스 간의 관계가 '~은 ~이다(IS-A)' 관계인지 `~은 ~을 가지고 있다 (HAS-S) 관계인지 문장을 만들어 생각해보기

ex) Employee는 Address이다. 라는 문장은 성립하지 않는 반면, Employee는 Address를 가지고 있다.는 어색하지 않은 올바른 문장이다. 따라서 이 경우에는 상속보다는 포함관계가 적합하다.


메서드 오버라이딩

상위 클래스로부터 상속받은 메서드와 동일한 이름의 메서드를 재정의하는 것

public class Main {
    public static void main(String[] args) {
        Bike bike = new Bike();
        Car car = new Car();
        MotorBike motorBike = new MotorBike();
        
        bike.run();
        car.run();
        motorBike.run();
    }
}

class Vehicle {
    void run() {
        System.out.println("Vehicle is running");
    }
}

class Bike extends Vehicle {
    void run() {
        System.out.println("Bike is running");
    }
}

class Car extends Vehicle {
    void run() {
        System.out.println("Car is running");
    }
}

class MotorBike extends Vehicle {
    void run() {
        System.out.println("MotorBike is running");
    }
}

// 출력값
Bike is running
Car is running
MotorBike is running

메서드 오버라이딩은 상위 클래스에 정의된 메서드를 하위 클래스에서 메서드의 동작을 하위 클래스에 맞게 변경하고자 할 때 사용

메서드를 오버라이딩 하려면 세 가지 조건을 반드시 만족해야한다.

  • 메서드의 선언부(메서드 이름, 매개변수, 반환타입)이 상위클래스의 그것과 완전히 일치해야한다.

  • 접근 제어자의 범위가 상위 클래스의 메서드보다 같거나 넓어야한다.

  • 예외는 상위 클래스의 메서드보다 많이 선언할 수 없다.


super 키워드와 super()

this, this()와 비슷한 의미
super 키워드는 상위클래스의 객체, super() 키워드는 상위 클래스의 생성자 호출하는 것을 의미한다.

super 예제

public class Example {
    public static void main(String[] args) {
        SubClass subClassInstance = new SubClass();
        subClassInstance.callNum();
    }
}

class SuperClass {
    int count = 20; // super.count
}

class SubClass extends SuperClass {
    int count = 15; // this.count

    void callNum() {
        System.out.println("count = " + count);
        System.out.println("this.count = " + this.count);
        System.out.println("super.count = " + super.count);
    }
}


// 출력값
count = 15
count = 15
count = 20

super() 예제

public class Test {
    public static void main(String[] args) {
        Student s = new Student();
    }
}

class Human {
    Human() {
        System.out.println("휴먼 클래스 생성자");
    }
}

class Student extends Human { // Human 클래스로부터 상속
    Student() {    
        super(); // Human 클래스의 생성자 호출
        System.out.println("학생 클래스 생성자");
    }
}

// 출력값
휴먼 클래스 생성자
학생 클래스 생성자

모든 생성자의 첫 줄에는 반드시 this() 또는 super()가 선언되어야 함


Object 클래스

자바의 상속계층도에서 최상위에 위치한 상위클래스이다. 따라서
자바의 모든 클래스는 Object 클래스로부터 확장된다는 명제는 항상 참이다.

컴파일 과정에서 아무런 상속받지 않는 클래스에 자동적으로 extends Object를 추가한다.

class ParentEx {  //  컴파일러가 "extends Object" 자동 추가 

}

class ChildEx extends ParentEx {

}

📌캡슐화

특정 객체 안에 관련된 속성과 기능을 하나의 캡슐(capsule)로 만들어 데이터를 외부로부터 보호하는 것을 말한다.

캡슐화를 해야하는 이유

  • 데이터 보호의 목적

  • 내부적으로만 사용되는 데이터에 대한 불필요한 외부 노출 방지

정리하자면 가장 큰 장점은 정보 은닉(data hiding)에 있다.


패키지(pakage)

특정한 목적을 공유하는 클래스와 인터페이스의 묶음을 의미한다.

클래스들을 그룹 단위로 묶어 효과적인 관리하기 위한 목적

자바에서 패키지는 물리적인 하나의 디렉토리(directory)이고, 하나의 패키지에 속한 클래스나 인터페이스 파일은 모두 해당 패키지에 속해있다. 이 디렉토리는 하나의 계층구조를 가지고 있는데, 계층 구조 간 구분은 점(.)으로 표현된다.

패키지가 있는 경우 첫 번째 줄에 반드시 package 패키지명이 표시되어야 한다.
없을 경우 이름없는 패키지에 속하게 된다.

// 패키지를 생성했을 때
package practicepack.test; // 패키지 구문 포함. 패키지가 없다면 구문 필요없음

public class PackageEx {

}

ex) String 클래스의 실제 이름은 java.lang.String인데, 여기서 java.lang은 패키지명을 나타낸다.

패키지로 클래스를 묶는 또 하나의 장점은 클래스의 충돌을 방지해주는 기능이 있다.


import문

다른 패키지 내의 클래스를 사용하기 위해 사용한다.

package practicepack.test;

public class ExampleImport {
		public int a = 10;
		public void print() {
			System.out.println("Import문 테스트")
		}
}
package practicepack.test2; // import문을 사용하지 않는 경우, 다른 패키지 클래스 사용방법

public class PackageImp {
		public static void main(String[] args) {
			practicepack.test.ExampleImport example = new practicepack.test.ExampleImport();
		}
}

import문을 사용하지 않고 다른 패키지의 클래스를 사용하기 위해서는 practicepack.test.ExampleImport 처럼 사용해야한다.

import 문 작성법

import 패키지명.클래스명; // 해당 패키지의 특정 클래스를 불러옴
import 패키지명.*; // 해당 패키지의 모든 클래스를 불러옴

예시

package practicepack.test;

public class ExampleImp {
		public int a = 10;
		public void print() {
			System.out.println("Import문 테스트")
}

package practicepack.test2; // import문을 사용하는 경우

import practicepack.test.ExampleImp // import문 작성

public class PackageImp {
		public static void main(String[] args) {
			ExampleImp x = new ExampleImp(); // 이제 패키지명을 생략 가능
		}
}

제어자

제어자는 클래스, 필드, 메서드, 생성자 등에 부가적인 의미를 부여하는 키워드를 의미한다.
ex) 파란 하늘, 붉은 노을 - 파란과 붉은 처럼 명사를 꾸며주는 형용사의 역할과 같다고 할 수 있음

제어자는 크게 접근 제어자기타 제어자로 구분할 수 있다.

접근 제어자public, protected, (default), private
기타 제어자static, final, abstract, native, transient, synchronized 등

제어자는 하나의 대상에 대해서 여러 제어자를 사용할 수 있지만 접근 제어자는 단 한 번만 사용할 수 있다.

접근 제어자(Access Modifier)

접근 제어자를 사용하면 클래스 외부로의 불필요한 데이터 노출을 방지(data hiding)을 할 수 있고, 외부로부터 데이터가 임의로 변경되지 않도록 막을 수 있다.

접근 제어자접근 제한 범위
private동일 클래스에서만 접근 가능
default동일 패키지 내에서만 접근 가능
protected동일 패키지 + 다른 패키지의 하위 클래스에서 접근 가능
public접근 제한 없음

public(접근 제한 없음) > protected(동일 패키지 + 하위클래스) > default (동일 패키지) > private (동일 클래스) 순으로 정리

접근 제어자클래스 내패키지 내다른 패키지의 하위 클래스패키지 외
PrivateOXXX
DefaultOOXX
ProtectedOOOX
PublicOOOO

예시

package package1; // 패키지명 package1 

//파일명: Parent.java
class Test { // Test 클래스의 접근 제어자는 default
    public static void main(String[] args) {
        Parent p = new Parent();

//        System.out.println(p.a); // 동일 클래스가 아니기 때문에 에러발생!
        System.out.println(p.b);
        System.out.println(p.c);
        System.out.println(p.d);
    }
}

public class Parent { // Parent 클래스의 접근 제어자는 public
    private int a = 1; // a,b,c,d에 각각 private, default, protected, public 접근제어자 지정
    int b = 2;
    protected int c = 3;
    public int d = 4;

    public void printEach() { // 동일 클래스이기 때문에 에러발생하지 않음
        System.out.println(a);
        System.out.println(b);
        System.out.println(c);
        System.out.println(d);
    }
}

// 출력값
2
3
4

private접근 제어자가 있는 a에는 접근이 불가하여 에러가 발생

package package2; // package2 

//파일명 Test2.java
import package1.Parent;

class Child extends package1.Parent {  // package1으로부터 Parent 클래스를 상속
    public void printEach() {
        // System.out.println(a); // 에러 발생!
        // System.out.println(b);
        System.out.println(c); // 다른 패키지의 하위 클래스
        System.out.println(d);
    }
}

public class Test2 {
    public static void main(String[] args) {
        Parent p = new Parent();

//        System.out.println(p.a); // public을 제외한 모든 호출 에러!
//        System.out.println(p.b);
//        System.out.println(p.c);
        System.out.println(p.d);
    }
}

결론 - 접근 제어자를 통해 외부로부터 데이터를 보호하고, 불필요하게 데이터가 노출되는 것을 방지 할 수 있다.


getter와 setter 메서드

public class GetterSetterTest {
    public static void main(String[] args) {
        Worker w = new Worker();
        w.setName("김코딩");
        w.setAge(30);
        w.setId(5);

        String name = w.getName();
        System.out.println("근로자의 이름은 " + name);
        int age = w.getAge();
        System.out.println("근로자의 나이는 " + age);
        int id = w.getId();
        System.out.println("근로자의 ID는 " + id);
    }
}

class Worker {
    private String name; // 변수의 은닉화. 외부로부터 접근 불가
    private int age;
    private int id;

    public String getName() { // 멤버변수의 값 
        return name;
    }

    public void setName(String name) { // 멤버변수의 값 변경
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        if(age < 1) return;
        this.age = age;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }
}

// 출력값
근로자의 이름은 김코딩
근로자의 나이는 30
근로자의 ID는 5

예제의 setAge(), setId()와 같이 앞에 set-을 붙인 메서드는 setter 메서드로 외부에서 메서드에 접근하여 조건이 맞을 경우 데이터 값을 변경 가능하게 해준다.

예제의 getAge(), getId()와 같이 앞에 get-을 붙인 메서드는 getter 메서드로 setter로 설정한 변수 값을 읽어오는 데 사용한다.

setter와 getter 메서드는 활용하면 데이터를 효과적으로 보호하면서도 의도하는 값으로 값을 변경하여 캡슐화를 보다 효과적으로 달성할 수 있다.


📌다형성

자바에서 다형성이란 한 타입의 참조변수를 통해 여러 타입의 객체를 참조할 수 있도록 만든 것을 의미한다.

상위 클래스 타입의 참조변수를 통해서 하위 클래스의 객체를 참조할 수 있도록 허용한 것

//참조변수의 다형성 예시

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();
    }
}

// 출력값
나는 당신의 친구입니다.
나는 당신의 남자친구입니다.
나는 당신의 여자친구입니다.

원래라면 타입을 일치시키시 위해 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();
    }
}

반대로 하위 클래스가 상위클래스를 참조하는 것은 안된다.
왜냐하면 Friend의 멤버 개수보다 참조변수 friend1이 사용할 수 있는 멤버 개수가 더 많기 때문이다.


참조변수의 타입 변환

참조변수의 타입 변환은 다르게 설명하면 사용할 수 있는 멤버의 개수를 조절하는 것을 의미한다.

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

  • 서로 상속관계에 있는 상위 클래스 - 하위 클래스 사이에만 타입 변환이 가능하다.

  • 하위 클래스 타입에서 상위 클래스 타입으로의 타입 변환(업캐스팅)은 형변환 연산자(괄호)를 생략할 수 있다.

  • 반대로 상위 클래스에서 하위 클래스 타입으로 변환(다운캐스팅)은 형변환 연산자(괄호)를 반드시 명시해야한다.

    • 또한, 다운캐스팅은 업캐스팅이 되어 있는 참조변수에 한해서만 가능하다.
public 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("묘기 부리기");
    }
}

다운캐스팅( 상위 -> 하위 )은 업 캐스팅이 되어있다는 전제 하에 가능


instanceof 연산자

캐스팅이 가능한 지 여부를 boolean 타입으로 확인 할 수 있는 문법이다.

캐스팅 가능 여부는 두가지를 판단한다

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

instanceof 연산자

참조_변수 instanceof 타입

참조_변수 instaceof 타입을 입력했을 때 리턴 값 true가 나오면 타입 변환이 가능
false가 나오면 타입 변환 불가능하다.

//예시
public class InstanceOfExample {
    public static void main(String[] args) {
        Animal animal = new Animal();
        System.out.println(animal instanceof Object); //true
        System.out.println(animal instanceof Animal); //true
        System.out.println(animal instanceof Bat); //false

        Animal cat = new Cat();
        System.out.println(cat instanceof Object); //true
        System.out.println(cat instanceof Animal); //true
        System.out.println(cat instanceof Cat); //true
        System.out.println(cat instanceof Bat); //false
    }
}

class Animal {};
class Bat extends Animal{};
class Cat extends Animal{};

다형성의 활용 예제

package package2;

public class PolymorphismEx {
  public static void main(String[] args) {
    Customer customer = new Customer();
    customer.buyCoffee(new Americano());
    customer.buyCoffee(new CaffeLatte());

    System.out.println("현재 잔액은 " + customer.money + "원 입니다.");
  }
}

class Coffee {
  int price;

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

class Americano extends Coffee {
  public Americano() {
    super(4000); // 상위 클래스 Coffee의 생성자를 호출
  }

  public String toString() {return "아메리카노";}; //Object클래스 toString()메서드 오버라이딩
};

class CaffeLatte extends Coffee {
  public CaffeLatte() {
    super(5000);
  }

  public String toString() {return "카페라떼";};
};

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 + "를 구입했습니다.");
  }
}

// 출력값
아메리카노를 구입했습니다.
카페라떼를 구입했습니다.
현재 잔액은 41000원 입니다.

📌추상화

자바에서 추상화란 간단히 말해 공통성과 본질을 모아 추출하는 것이다.

기존 클래스들의 공통적인 요소들을 뽑아서 상위 클래스를 만들어 내는 것


abstract 제어자

abstract의 사전적 의미는 추상적인, 미완성이라고 정리할 수 있다.

다른 말로 충분히 구체적이지 않다는 의미 '미완성 메서드', '미완성 클래스'

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

어떤 클래스에 추상 메서드가 포함되어있는 경우 해당 클래스는 자동으로 추상 클래스가 된다.

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

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

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();
    }
 }

// 출력값
멍멍
야옹

이처럼 상속을 받는 하위클래스에서 오버라이딩을 통해 각각 상황에 맞는 메서드 구현이 가능하다.

상속계층도의 상층부에 위치할 수록 추상화의 정도가 높고 그 아래로 내려갈수록 구체화된다.

상층부에 가까울수록 더 공통적인 속성과 기능들이 정의


final 키워드

final키워드는 필드, 지역 변수, 클래스 앞에 위치할 수 있으며 그 위치에 따라 그 의미가 조금씩 달라진다.

위치의미
클래스변경 또는 확장 불가능한 클래스, 상속 불가
메서드오버라이딩 불가
변수값 변경이 불가한 상수

공통적으로 변경이 불가능하고 확장할 수 없다는 점에서 유사하다.

final class FinalEx { // 확장/상속 불가능한 클래스
	final int x = 1; // 변경되지 않는 상수

	final int getNum() { // 오버라이딩 불가한 메서드
		final int localVar = x; // 상수
		return x;
	}
}

final 제어자가 추가되면 해당 대상은 더이상 변경이 불가능하거나 확장되지 않는 성질을 지니게 된다.


인터페이스

추상 메서드의 집합

기본적으로 추상 메서드와 상수만을 멤버로 가질 수 있다.

인터페이스의 기본 구조

class대신 interface키워드를 시용한다.

일반 클래스와 다르게, 내부의 모든 필드가 public static final로 정의되고, staticdefault 메서드 이외의 모든 메서드가 public abstract로 정의된다는 차이가 존재한다.

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 생략 
}

인터페이스 안에서 상수를 정의하는 경우 반드시 public static final로, 메서드를 정의하는 경우에는 public abstract로 정의되어야 하지만 이부분 또는 전부 생략이 가능하다.

생략된 부분은 컴파일러가 자동으로 추가해준다.


인터페이스의 구현

extends를 사용하는 클래스의 상속과 기본적으로 동일하지만, "구현하다"라는 의미를 가진 implements를 사용한다.

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

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

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

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


인터페이스의 다중 구현

상속과 달리 인터페이스는 다중적 구현이 가능하다.

다만 인터페이스는 인터페이스로부터만 상속이 가능하고, 클래스와 달리 Object 클래스와 같은 최고 조상이 존재하지 않는다.

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();
    }
}

// 출력값
멍멍!
원반 던지기
야옹~!
쥐 잡기

인터페이스는 클래스와 달리 다중 구현이 왜 가능할까?

클래스에서 다중 상속이 불가능했던 이유는 부모 클래스에 동일한 이름의 필드 또는 메서드가 존재하는 경우 충돌이 발생하기 때문

반면 인터페이스는 애초에 미완성된 멤버를 가지고 있기에 충돌이 발생할 여지가 없음.

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();
    }
}

// 출력값
멍멍!
원반 던지기
야옹~!
쥐 잡기

기존의 Animal 인터페이스를 추상 클래스로 바꾸고 Animal 상위 클래스로부터 DogCat 클래스로 확장되는 것과 동시에 Pet인터페이스를 구현하도록 하였다.


인터페이스의 장점

Provider클래스의 의존하고 있는 User 클래스가 있다.

여기서 의존한다라는 말을 쉽게 말해 User 클래스에서 Provider에 정의된 특정 속성 또는 기능을 가져와 사용하고있다는 의미이다.

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("무야호~");
    }
}

// 출력값
무야호~

이런 예제 코드가 있는데 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("야호~");
    }
}

// 출력값
야호~

변경된 내용을 Provider2객체를 새로 생성해서 User 클래스의 callProvider메서드를 Provider2로 변경해주었다.

Provider 클래스에 의존하고 있는 User 클래스의 코드 변경이 불가피하다.

여기서 인터페이스의 가장 큰 장점 중 하나는 역할과 구현을 분리시켜 사용자 입장에서는 복잡한 구현의 내용 또는 변경과 상관없이 해당 기능을 사용할 수 있다는 점이다.

인터페이스를 도식화해보면 위 그림처럼 나타낼 수 있다.

기존의 Provider 클래스에 인터페이스라는 껍데기를 씌운 형태이다.

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("야호~");
    }
}

//출력값
야호~

Provider 클래스의 내용 변경 또는 교체가 발생하더라도 User 클래스는 더이상 코드를 변경해주지 않아도 같은 결과를 출력할 수 있다.

결론적으로 인터페이스는 코드 변경의 번거로움을 최소화하고 손쉽게 해당 기능을 사용할 수 있도록 한다.

반대로 기능을 구현하는 개발자 입장에서도 선언과 구현을 분리시켜 개발시간을 단축하고, 독립적인 프로그래밍을 통해 한 클래스의 변경이 다른 클래스에 미치는 영향을 최소화할 수 있다는 큰 장점이 있다.


인터페이스 활용 예제

//시나리오
카페를 운영하는 사람이 있습니다. 
단골손님들은 매일 마시는 음료가 정해져 있습니다.
단골손님A는 항상 아이스 아메리카노를 주문합니다. 
단골손님B는 매일 아침 딸기라떼를 구매합니다.

//예제
//카페 손님
public class CafeCustomer {
  public String CafeCustomerName;

  public void setCafeCustomerName(String cafeCustomerName) {
    this.CafeCustomerName = cafeCustomerName;
  }
}

//CafeCustomer 클래스로부터 단골 손님A와 단골 손님B 상속
public class CafeCustomerA extends CafeCustomer {
  
}

public class CafeCustomerB extends CafeCustomer {
  
}

//카페 사장님 
public class CafeOwner {
  public void giveItem(CafeCustomerB cafeCustomerB) {
    System.out.println("give a glass of strawberry latte to CafeCustomer B");
  }

  public void giveItem(CafeCustomerA cafeCustomerA) {
    System.out.println("give a glass of iced americano to CafeCustomer A");
  }
}

//메뉴 주문
public class OrderExample {
    public static void main(String[] args) throws Exception {
        CafeOwner cafeowner = new CafeOwner();
        CafeCustomerA a = new CafeCustomerA();
        CafeCustomerB b = new CafeCustomerB();

        cafeowner.giveItem(a);
        cafeowner.giveItem(b);
    }
}

// 출력값
give a glass of iced americano to CafeCustomer A
give a glass of strawberry latte to CafeCustomer B

이 코드의 경우 단골 손님이 두 명이 아니라 계속 늘어나면 CafeOwner는 오버로딩한 메서드를 매번 만들어야하기에 매우 번거롭다.

이런 경우 인터페이스를 활용하자.

public interface Customer {
	//상수
    //추상 메서드
}

다음 implements 키워드를 사용해 Customer 인터페이스를 각각 구현한 CafeCustomerACafeCustomerB를 정의한다.

public class CafeCustomerA implements Customer {
  
}

public class CafeCustomerB implements Customer {
  
}

// 기존 코드 
public class CafeOwner {
  public void giveItem(CafeCustomerB cafeCustomerB) {
    System.out.println("give a glass of strawberry latte to CafeCustomer B");
  }

  public void giveItem(CafeCustomerA cafeCustomerA) {
    System.out.println("give a glass of iced americano to CafeCustomer A");
  }
}

// 인터페이스를 활용하여 작성한 코드
public class CafeOwner {
  public void giveItem(Customer customer) {
    System.out.println("??????????");
  }
}

여기서 문제는 현재 작성된 코드로는 각 단골 소님이 주문한 내용을 개별적으로 주문하기 어렵다.

따라서 기존 인터페이스에 getOrder 라는 추상 메서드를 인터페이스 Customer에 추가하자

Customer 인터페이스에 불완전하게 정의되어있는 getOrder() 메서드가 각각 객체에 맞게 구현부에서 정의되고 있는 모습이다.

public interface Customer {
  public abstract String getOrder();
}

public class CafeCustomerA implements Customer {
  public String getOrder(){
		return "a glass of iced americano";
	}
}

public class CafeCustomerB implements Customer {
  public String getOrder(){
		return "a glass of strawberry latte";
	}
}

위와 같이 CafeOwner 클래스를 재정의하여 매개변수로 Customer 타입이 입력될 수 있게끔 만들어주면, 매개변수의 다형성에 의해 Customer를 통해 구현된 객체 모두가 들어올 수 있다.

public class CafeOwner {
  public void giveItem(Customer customer) {
    System.out.println("Item : " + customer.getOrder());
  }
}

public class OrderExample {
    public static void main(String[] args) throws Exception {
        CafeOwner cafeowner = new CafeOwner();
        Customer cafeCustomerA = new CafeCustomerA();
        Customer cafeCustomerB = new CafeCustomerB();

        cafeowner.giveItem(cafeCustomerA);
        cafeowner.giveItem(cafeCustomerB);
    }
}

// 출력값
Item : a glass of iced americano
Item : a glass of strawberry latte

모든 작업을 하나로 이어 붙이자.

interface Customer {
  String getOrder();
}

class CafeCustomerA implements Customer {
  public String getOrder(){
		return "a glass of iced americano";
	}
}

class CafeCustomerB implements Customer {
  public String getOrder(){
		return "a glass of strawberry latte";
	}
}

class CafeOwner {
  public void giveItem(Customer customer) {
    System.out.println("Item : " + customer.getOrder());
  }
}

public class OrderExample {
    public static void main(String[] args) throws Exception {
        CafeOwner cafeowner = new CafeOwner();
        Customer cafeCustomerA = new CafeCustomerA();
        Customer cafeCustomerB = new CafeCustomerB();

        cafeowner.giveItem(cafeCustomerA);
        cafeowner.giveItem(cafeCustomerB);
    }
}

// 출력값
Item : a glass of iced americano
Item : a glass of strawberry latte
profile
프로젝트, 오류, CS 공부, 코테 등을 꾸준히 기록하는 저만의 기술 블로그입니다!

0개의 댓글