코드 스테이츠 12일차
다형성
자바 프로그래밍에서 다형성은 한 타입의 참조 변수를 통해 여러 타입의 객체를 참조할 수 있도록 만든 것을 의미한다.
상위 클래스 타입의 참조 변수를 통해서 하위 클래스의 객체를 참조할 수 있도록 허용한 것이라고 할 수 있다.
아래 코드 예시를 통해 살펴보자
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 클래스의 인스턴스를 생성하고 그것을 Friend 타입의 참조변수 girlfriend에 할당하고 있다.
원래라면 타입을 일치 시키기 위해 GirlFriend를 참조 변수의 타입으로 지정해주어야 하지만, 그러지 않고 상위 클래스 Friend를 타입으로 지정해주고 있다.
이 경우, 상위 클래스를 참조 변수의 타입으로 지정했기 때문에 자연스럽게 참조 변수가 사용할 수 있는 멤버의 개수는 상위 클래스의 멤버의 수가 된다.
이것이 앞서 설명했던 '상위 클래스 타입의 참조 변수로 하위 클래스의 객체를 참조하는 것' 다양성의 핵심적인 부분이라고 할 수 있다.
여기서 한 가지 기억해야 할 다형성의 핵심 중 하나는 상위 클래스의 타입으로 하위 클래스 타입의 객체를 참조하는 것은 가능하나, 그 반대로 하위 클래스 타입으로 상위 클래스 타입의 객체를 참조하는 것은 불가능하다
참조 변수의 타입 변환
참조 변수의 타입 변환은 다르게 설명하면 사용할 수 있는 멤버의 개수를 조절하는 것을 의미한다.
이는 자바의 다형성을 이해하기 위해 꼭 필요한 개념이다.
타입 변환을 위해서는 다음의 3가지 조건을 충족해야 한다.
- 서로 상속 관계에 있는 상위 클래스 - 하위 클래스 사이에만 타입 변환이 가능합니다.
- 하위 클래스 타입에서 상위 클래스 타입으로의 타입 변환(업캐스팅)은 형 변환 연산자(괄호)를 생략할 수 있습니다.
- 반대로 상위 클래스에서 하위 클래스 타입으로 변환(다운캐스팅)은 형 변환 연산자(괄호)를 반드시 명시해야 합니다.
- 또한, 다운 캐스팅은 업 캐스팅이 되어 있는 참조 변수에 한해서만 가능합니다.
아래 코드 예시를 통해 살펴보자.
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("묘기 부리기");
}
}
코드 예시를 보면 먼저 Vehicle 클래스가 있고, 이로부터 각각 상속을 받아 만들어진 Car와 MotorBike 클래스가 있다.
먼저 Car 클래스의 인스턴스 객체 car를 생성하고, 그 객체를 가리키는 참조 변수 vehicle의 타입을 Vehicle로 지정하여 참조 변수의 타입 변환을 실행했다. 그 후 반대로 vehicle을 하위 클래스 타입인 Car로 타입 변환하여 참조 변수 car2에 할당하였다.
이처럼 상속 관계에 있는 클래스 간에는 상호 타입 변환이 수행될 수 있다.
다만 하위 클래스를 상위 클래스 타입으로 변환하는 경우 타입 변환 연산자(괄호)를 생략할 수 있는 반면, 그 반대의 경우는 업 캐스팅이 되어있다는 전제하에 다운 캐스팅을 할 수있으며, 타입 변환 연산자를 생략할 수 없다는 점에만 차이가 있다고 할 수 있다.
위의 Car 클래스 와 MotorBike 클래스는 상속 관계가 아니므로 타입 변환이 불가하여 에러가 발생하는 것을 확인할 수 있다.
정리하자면, 참조 변수의 타입 변환은 서로 상속 관계에 있는 관계에서는 양방향으로 수행될 수 있으나, 상위 클래스로의 타입 변환이냐(괄호 생략 가능) 아니면 하위 클래스로의 타입 변환이냐(괄호 생략 불가)에 따라서 약간의 차이가 있다.
instanceof 연산자
instanceof 연산자란?
참조 변수의 타입 변환, 즉 캐스팅이 가능한지 여부를 boolean 타입으로 확인할 수 있는 자바의 문법 요소이다.
캐스팅 가능 여부를 판단하기 위해서는 두 가지, 즉 ‘객체를 어떤 생성자로 만들었는가’와 ‘클래스 사이에 상속관계가 존재하는가’를 판단해야 한다.
프로젝트 규모가 커지고, 클래스가 많아지면 매번 이러한 정보를 확인하는 것은 매우 어려워진다.
이를 해결하기 위해 자바는 instanceof 라는 연산자를 제공한다.
참조_변수 instanceof 타입
만약 참조_변수 instanceof 타입을 입력했을 때 리턴 값이 true가 나오면 참조 변수가 검사한 타입으로 타입 변환이 가능하며, 반대로 false가 나오는 경우에는 타입 변환이 불가능하다. 참조 변수가 null인 경우에는 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{};
Animal 클래스가 있고, Bat과 Cat 클래스가 각각 Animal 클래스를 상속받고 있습니다. 그리고 각각 객체를 생성하여 Animal 타입의 참조 변수에 넣고 instanceof 키워드를 사용하여 형 변환 여부를 확인하고 있다.
Cat 객체를 예로 들어보면, 생성된 객체는 Animal 타입으로 선언되어 있지만 다형적 표현 방법에 따라 Object와 Animal 타입으로도 선언될 수 있다는 점을 확인할 수 있다.
이렇듯 소스 코드가 길어지는 등 일일이 생성 객체의 타입을 확인하기가 어려운 상황에서 instanceof 연산자는 형 변환 여부를 확인하여 에러를 최소화하는 매우 유용한 수단이 될 수 있다.
추상화
추상화란?
자바에서 추상화는 객체의 공통적인 속성과 기능을 추출하여 정의하는 것을 의미한다.
앞서 배웠던 상속이 하위 클래스를 정의하는데 상위 클래스를 사용하는 것이라고 한다면 추상화는 반대로 기존 클래스들의 공통적인 요소를 뽑아서 상위 클래스를 만들어 내는 것이라고 할 수 있다.
방법에 있어서는 상향식과 하향식 설계 모두 크게 상관이 없습니다.
공통적인 속성과 기능을 정의하고 하위 클래스들을 생성할 수도 있고, 반대로 하위 클래스들의 공통성을 모아 상위 클래스를 정의할 수 있다.
자바에서는 주로 추상 클래스와 인터페이스라는 문법 요소를 사용해서 추상화를 구현한다.
abstract 제어자
abstract제어자란?
주로 클래스와 메서드를 형용하는 키워드로, 메서드 앞에 붙은 경우를 ‘추상 메서드(abstract method)’, 클래스 앞에 붙은 경우를 ‘추상 클래스(abstract class)’라 각각 부릅니다.
어떤 클래스에 추상 메서드가 포함되어 있는 경우 해당 클래스는 자동으로 추상 클래스가 됩니다.
영단어 abstract의 사전적 의미는 ‘추상적인'이라는 뜻을 가지고 있는데, 자바의 맥락에서 abstract라는 단어가 내포하는 의미는 ‘미완성'이라 정리할 수 있다.
아래 예시를 통해 한번 확인해 보자.
abstract class AbstractExample { // 추상 메서드가 최소 하나 이상 포함돼있는 추상 클래스
abstract void start(); // 메서드 바디가 없는 추상메서드
}
abstract의 가장 핵심적인 개념은 앞서 언급한 ‘미완성'에 있다.
추상 메서드는 메서드의 시그니처만 있고 바디가 없는 메서드를 의미하는데, abstract 키워드를 메서드 이름 앞에 붙여주어 해당 메서드가 추상 메서드임을 표시한다.
추상 메서드는 메서드의 시그니처만 있고 바디가 없는 메서드를 의미하는데, abstract 키워드를 메서드 이름 앞에 붙여주어 해당 메서드가 추상 메서드임을 표시한다.
AbstractExample abstractExample = new AbstractExample(); // 에러발생.
마지막으로 추상 클래스는 앞서 설명한 대로 미완성 설계도이기 때문에 메서드 바디가 완성되기 전까지 이를 기반으로 객체 생성이 불가하다.
추상클래스
abstract 제어자를 학습하면서 추상 클래스란, 메서드 시그니처만 존재하고 바디가 선언되어있지 않은 추상 메서드를 포함하는 '미완성 설계도'임을 학습했습니다.
또한 미완성된 구조를 가지고 있기에 이를 기반으로 객체를 생성한는 것이 불가능 하다고 배웠다.
그렇다면 객체 생성도 못하는 미완성 클래스를 왜 만드는 걸까?
크게 2가지를 생각할 수 있다.
1. 추상 클래스는 상속 관계에 있어 새로운 클래스를 작성하는데 매우 유용하다.
메서드의 내용이 상속을 받는 클래스에 따라서 종종 달라지기 때문에 상위 클래스에서는 선언부만 작성하고, 실제 구체적인 내용은 상속 받는 하위 클래스에서 구현하도록 비워둔다면 설계하는 상황이 변하더라도 보다 유연하게 변화에 대응할 수 있다.
이때 사용하게 되는 것이 앞서 상속파트에서 학습한 '오버라이딩'이다.
오버라이딩을 통해 추상 클래스로부터 상속받은 추상 메서드의 내용을 구현하여 메서드를 완성시킬 수 있고, 이렇게 완성된 클래스를 기반으로 해당 객체를 생성할 수 있습니다.
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 클래스 안에 abstract 키워드를 사용한 sound() 메서드가 추상 메서드로 선언되었고, 따라서 이를 포함하는 Animal 클래스 또한 abstract 키워드를 사용하여 추상 클래스로 만들어주었다.
그 이후 추상 클래스 Animal을 상속받은 Dog 클래스와 Cat 클래스 안에 추상 메서드 sound()를 각각 오버라이딩하여 각 객체에 맞는 구현부를 완성해 주었고, 마지막으로 이렇게 완성된 클래스를 기반으로 dog 인스턴스와 cat 인스턴스를 생성하여 sound() 메서드를 호출했다.
이처럼 추상 클래스를 사용하면 상속받는 하위 클래스에서 오버라이딩을 통해 각각 상황에 맞는 메서드 구현이 가능하다는 장점이 있다.
2. 추상 클래스는 자바 객체지향 프로그래밍의 마지막 기둥인 추상화를 구현하는데 핵심적인 역할을 수행한다.
추상화를 한마디로 정리하면 "객체의 공통적인 속성과 기능을 추출하여 정의하는 것"으로 정리할 수 있다.
위의 코드 예시를 다시보면, 동물이 가지는 공통적인 특성을 모아 먼저 추상 클래스로 선언해 주었고, 이를 기반으로 각각의 상속된 하위 클래스에서 오버라이딩을 통해 클래스의 구체적인 내용을 결정해 주었다.
만약 여러 사람이 함께 개발하는 경우, 공통된 속성과 기능임에도 불구하고 각각 다른 변수와 메서드로 정의되는 경우 발생할 수 있는 오류를 미연에 방지할 수 있다.
결론적으로 구체화에 반대되는 개념으로 추상화를 생각해 보면, 상속계층도의 상층부에 위치할수록 추상화의 정도가 높고 그 아래로 내려갈수록 구체화된다고 정리해 볼 수 있다.
즉, 상층부에 가까울수록 더 공통적인 속성과 기능들이 정의되어 있다고 생각할 수 있다.
final 키워드
final 키워드란?
영어로 '최종의', '마지막의' 라는 뜻을 가지고 있으며, 자바에서는 필드, 지역 변수, 클래스 앞에 위치할 때 그 위치에 따라 의미가 조금씩 달라지는 키워드이다.
위치 | 의미 |
클래스 | 변경 또는 확장 불가능한 클래스, 상속 불가 |
메서드 | 오버라이딩 불가 |
변수 | 값 변경이 불가한 상수 |
final 키워드는 위치에 따라 각각 조금의 차이점이 있지만 결국 공통적으로 변경이 불가능하고 확장할 수 없다는 점에서 유사하다고 할 수 있다.
final class FinalEx { // 확장/상속 불가능한 클래스
final int x = 1; // 변경되지 않는 상수
final int getNum() { // 오버라이딩 불가한 메서드
final int localVar = x; // 상수
return x;
}
}
인터페이스
자바에서의 인터페이스는 추상 클래스처럼 자바에서 추상화를 구현하는데 활용된다는 점은 동일하지만, 추상 클래스에 비해 더 높은 추상성을 가진다.
추상 클래스를 설계가 모두 끝나지 않은 “미완성 설계도"에 비유할 수 있다면, 인터페이스는 그보다 더 높은 추상성을 가지는 가장 기초적인 “밑그림"에 빗대어 표현할 수 있다.
추상 클래스는 메서드 바디가 없는 추상 메서드를 하나 이상 포함한다는 점 외에는 기본적으로 일반 클래스와 동일하다고 할 수 있다.
반면 인터페이스는 기본적으로 추상 메서드와 상수만을 멤버로 가질 수 있다는 점에서 추상 클래스에 비해 추상화 정도가 더 높다고 할 수 있다.
인터페이스의 기본 구조
인터페이스를 작성하는 것은 기본적으로 클래스를 작성하는 것과 유사하지만, class 키워드 대신 interface 키워드를 사용한다는 점에서 차이가 있다.
또한, 일반 클래스와 다르게 내부의 모든 필드가 public static final 로 정의되고, static 과 default 메서드 이외의 모든 메서드가 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 생략
}
인터페이스는 interface 키워드를 사용하여 만들어지고 구현부가 완성되지 않은 추상 메서드와 상수만으로 구성되어 있다.
인터페이스 안에서 상수를 정의하는 경우에는 반드시 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();
}
}
// 출력값
멍멍!
원반 던지기
야옹~!
쥐 잡기
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();
}
}
// 출력값
멍멍!
원반 던지기
야옹~!
쥐 잡기
위의 코드 예제에서는 기존의 Animal 인터페이스를 추상 클래스로 바꾸고 Animal 상위 클래스로부터 Dog과 Cat 클래스로 확장되는 것과 동시에 Pet 인터페이스를 구현하도록 하여 같은 결과물이 출력되게 만들었다.
코드 스테이츠 12일차 후기
다형성, 추상화에 대해 학습했다. 앞서 배운 상속과 캡슐화가 생가보다 많은 도움이 되었다. 이론상으로는 이해가 되나 프로그래밍에 실사용을 한다고 생각하니 머리가 아파왔다. 어디서 어떻게 활용해야 좋은 코드가 될까? 앞전에 배운 것 들을 어떻게 활용할 수 있을까? 라는 고민들이 생겼다.
'코드 스테이츠' 카테고리의 다른 글
코드 스테이츠 - Java 심화(Effective) 1 (0) | 2023.05.03 |
---|---|
코드 스테이츠 - Java 컬렉션(Collection) (0) | 2023.05.01 |
코드 스테이츠 04/25 - 객체 지향 프로그래밍 심화1 (1) | 2023.04.25 |
코드 스테이츠 4/24 - 객체지향 프로그래밍 기초 2 (0) | 2023.04.24 |
코드 스테이츠 4/21 - 객체지향 프로그래밍 기초 1 (0) | 2023.04.21 |