TIL
TIL_20220907
번잔중
2022. 9. 7. 22:42
오늘 할 일
🐳 코드스테이츠 14일차
- 다형성
- 추상화
- 1일 1커밋
오늘 배운 것
🫂 다형성(Polymorphism)과 추상화(Abstraction)
다형성(Polymorphism)
상위 클래스 타입의 참조변수를 통해서 하위 클래스의 객체를 참조할 수 있도록 허용한 것이다.
핵심 point
- 다형성이란 상위 클래스 타입의 참조 변수로 하위 클래스의 객체를 참조하는 것이다.
- 하위 클래스의 타입으로 상위 클래스 타입의 객체를 참조할 수 없다. 즉, 반대는 성립하지 않는다는 것이다. 그 이유는 하위 클래스에 포함된 멤버가 상위 클래스보다 더 많기 때문이다. 있는 기능을 없애고 안쓸 수는 있지만 없는 기능을 사용할 수는 없다!
- 메서드 오버로딩과 오버라이딩 또한 하나의 객체가 여러 가지 형태를 가질 수 있는 성질이라 정의할 수 있는 점에서 결이 같다고 볼 수 있다.
예시)
class Soldier {
public void soldierInfo() {
System.out.println("나는 대한민국 군인입니다.");
}
}
class Army extends Soldier{
public void soldierInfo() {
System.out.println("나는 대한민국 육군입니다.");
}
}
class Navy extends Soldier {
public void soldierInfo() {
System.out.println("나는 대한민국 해군입니다.");
}
}
public class SoldierTest {
public static void main(String[] args) {
Soldier soldier = new Soldier(); // 객체 타입과 참조변수 타입의 일치 -> 가능
Army army = new Army();
Soldier navy = new Navy(); // 객체 타입과 참조변수 타입의 불일치 -> 가능
// Navy soldier1 = new Soldier(); -> 하위 클래스 타입으로 상위 클래스 객체 참조 -> 불가능
soldier.soldierInfo();
army.soldierInfo();
navy.soldierInfo();
}
}
navy는 상위 클래스를 참조변수의 타입으로 지정했기 때문에 자연스럽게 참조변수가 사용할 수 있는 멤버의 개수는 상위 클래스의 멤버의 수가 된다.
이것이 앞서 설명했던 ‘상위 클래스 타입의 참조변수로 하위 클래스의 객체를 참조하는 것’이자 다형성의 핵심적인 부분이라 할 수 있다.
참조 변수의 타입 변환
사용할 수 있는 멤버의 개수를 조절하는 것을 참조 변수의 타입 변환이라고 한다.
참조 변수 타입 변환의 조건
- 서로 상속 관계에 있는 상위 클래스 - 하위 클래스 사이에만 타입 변환이 가능하다.
- 하위 클래스 타입에서 상위 클래스 타입으로의 타입 변환(업캐스팅)은 형변환 연산자(괄호)를 생략할 수 있다.
- 반대로 상위 클래스에서 하위 클래스 타입으로 변환(다운캐스팅)은 형변환 연산자(괄호)를 반드시 명시해야합니다.
즉, 상속 관계에 있는 두 클래스의 형변환은 업캐스팅이든 다운캐스팅이든 가능하다. 그리고 당연히 상속 관계의 클래스여야 가능하다.
❗️ 상속 관계에 있다고 하더라도 다운캐스팅의 경우는 업캐스팅이 선행된 이후에만 가능하다. 객체 생성을 한 이후 바로 다운캐스팅을 진행하면 오류가 발생한다!
업캐스팅과 다운캐스팅 | 겁나 헷갈려
- 업캐스팅: 하위 클래스 타입에서 상위 클래스 타입으로의 타입 변환은 문제가 생기지 않는다.
- 다운캐스팅: 문제는 여기서 발생한다. 상위 클래스 타입에서 하위 클래스 타입으로 가려고 할 때, 전제 조건이 크게 붙는다. 그것은 바로 “업캐스팅을 수행한 참조 변수에 대해서만 다운캐스팅이 가능하다.” 그렇기 때문에 업캐스팅을 하지 않고 다운캐스팅을 시도하면 컴파일 에러는 생기지 않지만 런타임 에러가 발생한다.
public class VehicleTest {
public static void main(String[] args) {
Car car = new Car();
Vehicle vehicle = (Vehicle) car; // 상위 클래스 Vehicle 타입으로 변환(생략 가능)
Car vehicle1 = (Car) vehicle; // 이전에 업캐스팅을 했던 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 타입으로 확인할 수 있는 자바의 문법요소이다.
// 참조 변수의 타입 변환이 가능한지 확인
// true가 나오면 참조 변수가 검사한 타입으로 변환 가능
// false라면 불가능
참조_변수 instanceof 타입
public class InstanceOfExample {
public static void main(String[] args) {
Vehicle vehicle3 = new Vehicle();
System.out.println(vehicle3 instanceof Object); // true
System.out.println(vehicle3 instanceof Vehicle); // true
System.out.println(vehicle3 instanceof MotorBike); // false, 업캐스팅 과정이 없기 때문이다.
Vehicle vehicle4 = new MotorBike();
System.out.println(vehicle4 instanceof MotorBike); // true, 업캐스팅 이후 다운캐스팅이 가능한지 여부이기 때문
Vehicle benz = new Car();
System.out.println(benz instanceof Object); // true
System.out.println(benz instanceof Vehicle); // true
System.out.println(benz instanceof Car); // true
System.out.println(benz instanceof MotorBike); // false, 상속 관계가 아니기 때문
}
}
class Vehicle {};
class Car extends Vehicle{};
class MotorBike extends Vehicle{};
소스 코드가 길어지는 등 일일이 생성 객체의 타입을 확인하기가 어려운 상황에서 instanceof 연산자는 형변환 여부를 확인하여 에러를 최소화하는 매우 유용한 수단이 될 수 있다.
추상화(Abstraction)
기존 클래스들의 공통적인 요소들을 뽑아서 상위 클래스를 만들어 내는 것이다. 공통적인 속성과 기능을 정의하고 하위 클래스들을 생성할 수도 있고, 반대로 하위 클래스들의 공통성을 모아 상위 클래스를 정의할 수 있다.
자바에서는 추상 클래스와 인터페이스라는 문법 요소를 사용해서 추상화를 구현한다.
추상화의 장점
공통적인 속성과 기능을 모아서 정의하면
- 코드의 중복을 줄일 수 있다.
- 효과적으로 클래스 간의 관계를 설정할 수 있다.
- 유지 및 보수가 용이해진다.
abstract 제어자
자바에서 abstract라는 단어의 의미는 ‘미완성'이라고 정리할 수 있다. 주로 클래스와 메서드를 형용하는 키워드로 사용되는데, 메서드 앞에 붙으면 ‘추상 메서드’, 클래스 앞에 붙으면 ‘추상 클래스’라고 부른다.
어떤 클래스에 추상 메서드가 포함되어 있는 경우 해당 클래스는 자동으로 추상 클래스가 된다.
abstract class AbstractExample { // 추상 메서드가 최소 하나 이상 포함돼있는 추상 클래스
abstract void start(); // 메서드 바디가 없는 추상메서드
}
- 추상 메서드: 메서드의 시그니처만 있고, 바디가 없는 메서드이다. 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();
}
}
// 출력값
멍멍
야옹
final 키워드
클래스, 메서드, 그리고 변수 앞에 final 제어자가 추가되면 이제 해당 대상은 더이상 변경이 불가하거나 확장되지 않는 성질을 지닌다. 필드, 지역 변수, 클래스 앞에 위치할 수 있으며 그 위치에 따라 그 의미가 조금씩 달라진다.
위치 | 의미 |
클래스 | 변경 또는 확장 불가능한 클래스, 상속 불가 |
메서드 | 오버라이딩 불가 |
변수 | 값 변경이 불가한 상수 |
의미에 차이가 있어보이지만 결론적으로는 변경이 불가능하고 확장할 수 없다는 점에서 유사하다.
final class FinalEx { // 확장/상속 불가능한 클래스
final int x = 1; // 변경되지 않는 상수
final int getNum() { // 오버라이딩 불가한 메서드
final int localVar = x; // 상수
return x;
}
}
인터페이스
‘미완성 설계도'라고 부르는 추상 클래스보다 더 높은 차원의 추상성을 가지고 있다. ‘밑그림'이라고 표현할 수 있다. 추상 메서드와 상수만을 멤버로 가질 수 있기 때문에 추상화 정도가 추상 클래스보다 더 높다! (추상 클래스는 추상 메서드를 하나 이상만 포함하면 된다.)
인터페이스의 기본 구조
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 생략
}
생략된 부분은 컴파일러가 자동으로 추가해준다.
인터페이스의 구현
메서드 바디를 정의하는 클래스를 따로 정의해야 인스턴스를 생성할 수 있다. “구현하다"라는 뜻을 가진 implements 키워드를 사용한다.
class 클래스명 implements 인터페이스명 {
... // 인터페이스에 정의된 모든 추상메서드 구현
}
❗️ 어떤 클래스가 특정 인터페이스를 구현한다는 것은 그 클래스에게 인터페이스의 추상 메서드를 반드시 구현하도록 강제하는 것이다. 추상 메서드들은 해당 클래스 내에서 오버라이딩하여 바디를 완성하는 것으로 이해할 수 있다.
인터페이스의 다중 구현
- 클래스 간의 상속에서는 다중 상속이 불가능하지만 인터페이스는 다중적 구현이 가능하다. 그 말은 하나의 클래스가 여러 개의 인터페이스를 구현할 수 있다는 뜻이다.
- 인터페이스는 인터페이스로부터만 상속이 가능하다.
- 클래스와 달리 Object 클래스와 같은 최고 조상이 존재하지 않는다.
- 미완성된 멤버를 가지고 있기 때문에 클래스에서 발생하는 상위 클래스와 하위 클래스 간의 동일한 이름의 필드 또는 메서드로 인한 충돌 발생이 발생할 여지가 없다. 그래서 안전하게 다중 구현이 가능하다.
- 특정 클래스는 다른 클래스로부터의 상속을 받으면서 동시에 인터페이스를 구현할 수 있다.
class ExampleClass implements ExampleInterface1, ExampleInterface2, ExampleInterface3 {
... 생략 ...
}
// 다중 구현(Only interface)
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("원반 던지기");
}
}
// 특정 클래스와 인터페이스 동시에 상속 및 구현
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("원반 던지기");
}
}
인터페이스의 장점
- User 클래스는 Provider 클래스에 의존하고 있다. 의존한다는 의미는 Provider에 정의된 특정 속성이나 기능을 가져와서 사용하고 있다는 의미이다.
- 만약 Provider 클래스에 변경 사항이 발생하면 User 클래스에도 변경 사항을 적용해야 한다. (User 클래스의 코드도 수정해야 한다.)
- 한 두개의 변경 사항이 아니라 수백, 수천개의 변경 사항이 존재한다면 번거로워진다.
- Provider 클래스에 변경 사항이 발생해서 Provider 클래스가 아닌 Provider2 클래스로 교체해야하는 상황
- 이를 해결할 수 있는 것이 인터페이스인데, 역할과 구현을 분리시켜서 사용자의 입장에서는 복잡한 구현의 내용 또는 변경과 관계없이 해당 기능을 사용할 수 있다는 점이다. 그래서 코드를 하나하나 변경해줄 필요도 없다.
- User 클래스가 interface를 매개변수로 받도록 정의하고, Provider 클래스와 Provider의 변경 사항이 적용된 클래스가 interface의 기능을 각각 구현하게 만든다면 User 클래스는 Provider 클래스의 내용 변경이나 교체가 발생하더라도 코드를 변경하지 않고 같은 결과를 출력할 수 있다.
💡 결론적으로 정리하면, 인터페이스는 기능이 가지는 역할과 구현을 분리시켜 사용자로 복잡한 기능의 구현이나 교체/변경을 신경쓰지 않고도 코드 변경의 번거로움을 최소화하고 손쉽게 해당 기능을 사용할 수 있도록 한다.
반대로 기능을 구현하는 개발자의 입장에서도 선언과 구현을 분리시켜 개발시간을 단축할 수 있고, 독립적인 프로그래밍을 통해 한 클래스의 변경이 다른 클래스에 미치는 영향을 최소화할 수 있다는 큰 장점이 있다.
느낀점
- 객체지향프로그래밍의 4요소인 상속, 캡슐화, 다형성, 추상화를 순차적으로 배워보니 모든 내용이 연결되어 있다는 생각이 들었다.
- 다형성과 추상화는 헷갈리는 부분이 많아서 주말까지 복습을 진행해야 할 것 같다.
- 하지만 다형성과 추상화를 잘 이용하면 반복 작업을 줄이는 데에 큰 역할을 할 것이라는 생각이 들었다.
- 코드업 100제가 얼마 남지 않았으니 빨리 마무리 해야겠다. 언어를 배울 때마다 많이 써보는 것이 중요하다는 느낌을 받게 된다.
내일 할 일
🙉 코드스테이츠 15일차
- 객체지향프로그래밍 심화 복습
- 블로깅
- 1일 1커밋