그묘일 조아
추상클래스의 정의를 정리하기 전에 한 가지 예시를 들어보자
public class Vehicle {
private int curX, curY;
public void reportPosition() {
System.out.printf("차종: %s: 현재 위치: (%d, %d)%n", this.getClass().getSimpleName(), curX, curY);
}
public void addFuel() {
System.out.printf("연료가 필요해");
}
}
public class ElectricCar extends Vehicle {
@Override
public void addFuel() {
System.out.printf("차종: %s: 연료 주입: %s%n", this.getClass().getSimpleName(), "충전");
}
}
public class DieselSUV extends Vehicle{
@Override
public void addFuel() {
System.out.printf("차종: %s: 연료 주입: %s%n", this.getClass().getSimpleName(), "경유");
}
}
이러한 상속 관계에 있는 클래스가 있다고 가정해보자.
그리고 이를 실행할 실행 함수를 작성해보자.
public class VehicleTest {
public static void main(String[] args) {
// TODO: DieselSUV와 ElectricCar를 각각 한대씩 관리하는배열을 만들고 사용해보자.
Vehicle[] vehicles = {
new ElectricCar(),
new DieselSUV()
};
for(Vehicle v : vehicles) {
v.addFuel();
v.reportPosition();
}
// END
}
}
우리는 상속을 배웠고 동적 바인딩도 배웠기에 v.addFuel()
가 동작하면 각 재정의된 메서드를 호출한다.
근데 한 가지 불편한 점이 있다.
바로, 부모 클래스에 구현한 메서드를 사용하지 않는 다는 점.
약간 인력 낭비라고 할 수 있다.
그럼 극한의 효율을 뽑고 싶은 우리가 할 수 있는 건 무엇일까?
그게 오늘 정리할 내용인 추상 클래스다.
추상 클래스란 말 그대로 한 개 이상의 상태나 행위를 추상화하는 것이다.
부모 클래스의 addFuel()
메서드는 동작하지 않으니 이를 추상화 해보자.
public abstract class Vehicle {
private int curX, curY;
public void reportPosition() {
System.out.printf("차종: %s: 현재 위치: (%d, %d)%n", this.getClass().getSimpleName(), curX, curY);
}
public abstract void addFuel();
}
이렇게 추상 클래스를 사용하려면 class이름 앞에 abstract
키워드를 붙이고 추상화할 메서드 앞에도 해당 키워드를 붙여주면 된다.
끗
아, 추가적으로 추상 클래스는 객체를 생성할 수 없다.
즉, new 연산자를 통해 메모리에 적재할 수 없다는 뜻이다.
대신 상위 클래스 타입으로써 자식을 참조하는 것은 가능하다.
Vehicle v = new DisselSUV();
그럼 왜 추상 클래스를 사용할까?
방금 예시를 보면 알 수 있듯, 상위 추상 클래스의 추상 메서드는 구현이 안되어 있는 상태다.
그럼 추상 클래스를 상속받는 자식 클래스는 추상 메서드를 사용하기 위해 반드시 재정의 해야한다.
즉, 구현의 강제성을 부여하면서 프로그램의 안정성을 향상 시킨다.
간단하게 코드로 예시를 들어보자.
public class HorseCart extends Vehicle{
}
이와 같이 기존의 Vehicle 추상 클래스를 상속 받는 HorseCart 클래스를 선언하자.
코드는 비어 있고 추상 메서드 역시 재정의 하지 않았다.
실제로 실습하면 알 수 있듯,
이와 같은 컴파일 에러가 나온다.
이를 해결하기 위해서는 추상 메서드를 재정의 해주면 된다.
✨인터페이스가 무엇이고 장/단점이 무엇인지 특징은 무엇인지, 인터페이스와 추상 클래스의 차이✨
인터페이스는 추상 클래스와 다르지만 형태는 유사한 점이 있다.
차이점이 있다면 인터페이스의 모든 멤버 변수는 public static final
하다는 점이다.
또한, 모든 메서드는 public abstract
하다.
그래서
인터페이는 멤버 변수와 메서드에 항상 들어가는 키워드를 생략하고 작성한다.
Java8부터 인터페이스에 는 상수(Private static final)만 가능. 변수는 불가능.
추상 클래스는 변수도 가능.
✨인터페이스는 Object클래스를 상속받을까?
Nope : 인터페이스는 클래스가 아님
위에서 말한 특징을 제외한 다른 특징을 더 정리하자.
그럼 인터페이스의 메서드를 구현하기 위해서는 어떻게 할까?
클래스에 인터페이스를 implements 키워드로 구현하면 된다.
implements
한 클래스는
모든 abstract
메서드를 override 해서 구현하거나,
구현하지 않을 경우에는 abstract
클래스로 생성해야 한다.
그럼 구현한 클래스에서 재정의해서 사용할 수 있는 메서드의 접근 제한자는 무엇이 있을까?
정답은 public
하나 뿐이다.
왜?
인터페이스의 모든 메서드가 public
하기 때문에 그 하위 클래스는 같거나 더 큰 접근 제한자를 사용해야 하기 때문이다.
public class IronMan implements Heroable{
int weaponDamage = 100;
@Override
public int fire() {
// TODO Auto-generated method stub
System.out.printf("빔 발사 : %d만큼의 데미지를 가함\n");
return this.weaponDamage;
}
@Override
public void changeShape(boolean isHeroMode) {
// TODO Auto-generated method stub
String status = isHeroMode? "장착" : "제거";
System.out.printf("장갑 %s", status);
}
@Override
public void upgrade() {
// TODO Auto-generated method stub
System.out.println("무기 성능 개선");
}
}
이처럼 Fireable
과 Transformable
인터페이스를 다중 상속 받는 Heroable
인터페이스가 있고
이를 구현하는 클래스는 인터페이스의 모든 메서드를 구현해야한다.
abstract
메서드손쉬운 모듈 교체 지원
public interface Printer {
void print(String fileName);
}
public class InkjetPrinter implements Printer {
@Override
public void print(String fileName) {
System.out.println("Inkjet Printer로 프린트 한다.");
}
}
public class LazerPrinter implements Printer {
@Override
public void print(String fileName) {
// TODO Auto-generated method stub
System.out.println("Lazer Printer로 프린트 한다.");
}
}
public class PrintClient {
private Printer printer;
public void setPrinter(Printer printer) {
this.printer = printer;
}
public void printThis(String fileName) {
printer.print(fileName);
}
}
public class PrinterTest {
public static void main(String[] args) {
PrintClient client = new PrintClient();
client.setPrinter(new InkjetPrinter());
// TODO: client가 LazerPrinter를 사용하도록 하고 클래스의 변화를 확인하시오.
client.setPrinter(new LazerPrinter());
// END
client.printThis("myfile");
}
}
Printer
라는 인터페이스가 있고 이를 구현한 InkjetPrinter
와 LazerPrinter
가 있다. 각 구현체는 인터페이스의 메서드를 구현했다.
PrintClient
클래스에서 Printer
인터페이스 타입의 printer를 선언하고 setPrinter
메서드를 통해 메모리에 적재한다.
setPrinter
메서드의 파라미터로 인터페이스를 받고 있지만, 인터페이스는 new 연산자로 생성이 불가하기 때문에 Printer
를 implements
한 클래스가 파라미터로 들어갈 것이다.
(PrinterTest
를 보면 알 수 있음)
이제 PrinterTest
코드를 보면, 다형성을 통해 객체가 바뀌어도 문제없이 해당 객체가 재정의한 메서드를 호출하는 것을 볼 수 있다.
즉, 인터페이스도 다형성이 적용된다.
서로 상속의 관계가 없는 클래스들에게 인터페이스를 통한 관계 부여로 다형성 확장
이러한 구조를 가진 관계에서 충전이 필요한 상황이 있다고 가정해보자.
그럼 우리는 이렇게 코드를 짤 수 있다.
void badCase() {
Object[] objs = {
new HandPhone(),
new DigitalCamera()
};
for (Object obj : objs) {
if (obj instanceof HandPhone phone) {
phone.charge();
} else if (obj instanceof DigitalCamera camera) {
camera.charge();
}
}
}
이 코드에 컴파일 에러같은 문제는 없지만 좀 불편함 점이 있다.
매 반복문마다 instanceof
연산자로 클래스 정보가 맞는지 확인해 줘야 하기 때문이다.
이는 곧 성능의 저하로 이어진다.
그럼 어떻게 개선하면 좋을까?
그림으로 간단하게 알아보자.
위 관계와의 차이점이 있다면, 충전이 가능한 HandPhone
클래스와 DigitalCamera
클래스가 Chargeable
이라는 인터페이스를 구현하고 있다는 것이다.
단 한 개의 인터페이스만 추가해 주었을 뿐인데, 성능에서 유의미한 개선을 가져올 수 있다.
void goodCase() {
Chargeable[] objs = {
new HandPhone(),
new DigitalCamera()
};
for (Chargeable obj : objs) {
obj.charge();
}
}
매 루프마다 instanceof
연산자로 클래스 정보를 확인하지 않아도 원활하게 수행이 가능해졌다.
Java8부터 생긴 기능이다.
인터페이스에 구현된 메서드를 넣을 수 있을까?
원래는 아니지만 Java8 이후부터는 가능하게 되었다.
즉, 인터페이스를 구현하는 클래스 들 중에 공통 메서드를 인터페이스에서 먼저 구현할 수 있다.
이렇게 되면 코드의 중복을 줄일 수 있게 된다.
그럼, super클래스를 상속받고 하나의 인터페이스를 구현하는 클래스가 있다고 생각해보자.
super클래스의 메서드와 인터페이스의 메서드의 이름이 겹친다고 할 때, 어떤 결과가 나오게 될까?
처음에는 컴파일 에러가 나올 것이라고 생각했지만,
정답은 super클래스의 메서드가 실행되고 인터페이스의 메서드는 무시된다.
또한, 두 개의 인터페이스를 다중 구현한 클래스가 있다고 가정하고 위와 같은 상황이 발생한다면?
정답은 sub클래스에 반드시 재정의해서 사용해야 한다.
Java9부터 생긴 기능이다.
인터페이스에 static method를 생성할 수 있게 된다.
심지어 Body를 가지는 메서드를 생성할 수 있게 되면서 공통적으로 처리할 모듈을 만들 수 있게 되었다.
그냥 인터페이스는 신임
오늘 Java의 기본을 모두(?) 배웠다.
그러는 동시에 나는 Java가 아닌 그냥 Java비슷한 언어로 프로젝트를 해왔던 것을 다시금 알게 되었다.