완전하지 않은 클래스. 부족하다는 뜻이기도 하지만 가능성이 남아 있다는 뜻이기도 하다. 상속으로 확장할 수 있다.
달라지는 것만 자식에게 넘기기
공통된 것은 부모가 구현하고 달라지는 것만 자식에게 맡기려는 것이다. Computer 클래스에서 turnOn()과 turnOff()는 하위 클래스가 그대로 물려받아 쓰지만, display()와 typing()은 노트북이냐 데스크톱이냐에 따라 달라진다. 달라지는 쪽을 추상 메서드로 남겨 구현 책임을 상속받는 클래스에 넘긴다. 그 구조가 템플릿 메서드 패턴이다.
public abstract class Computer {
public abstract void display();
public abstract void typing();
public void turnOn() { System.out.println("전원을 켭니다."); }
public void turnOff() { System.out.println("전원을 끕니다."); }
}인스턴스 대신 부모 타입으로
인스턴스를 만들 수는 없다. 구현되지 않은 메서드가 있어 호출해도 수행할 내용이 없기 때문이다. new Car2()라고 쓰면 Car2 is abstract; cannot be instantiated 오류가 난다. 상속받아 추상 메서드를 구현해야 쓸 수 있다.
대신 부모 타입으로 자식을 참조해서 여러 가지를 한 종류처럼 다룬다. Car2[] 배열 하나에 버스와 스포츠카를 담아 두고 순회하면서 run()을 부르면 각자 오버라이딩한 것이 실행된다.
선언만 남기는 값어치
로직을 구현하는 것보다 어떻게 구현할지를 결정하는 것이 더 중요하다. int add(int num1, int num2); 한 줄만 봐도 두 정수를 받아 더해서 반환한다는 것을 알 수 있다. 추상 메서드는 그 결정만 남긴 형태다.
몸체 없는 선언
추상 메서드가 하나라도 있으면 그 클래스는 반드시 추상 클래스여야 한다. 거꾸로는 성립하지 않는다. 추상 메서드가 하나도 없어도 abstract를 붙일 수 있고, 인스턴스화만 막고 싶을 때 그렇게 쓴다.
추상 메서드는 구현 코드가 없다.
abstract int add(int num1, int num2); // 추상 메서드, 몸체가 없다
int add(int x, int y) { } // 추상 메서드가 아니다, 몸체가 비어 있을 뿐중괄호가 있느냐 없느냐로 갈린다. {}가 있으면 내용이 비어 있어도 구현한 것이다. 클래스 다이어그램에서는 추상 클래스를 기울임 꼴로 적어 구별한다.
인터페이스와의 차이
인터페이스는 여럿 구현하지만 추상 클래스는 하나만 상속한다. 필드도 추상 클래스만 자유롭게 가지고, 인터페이스에 선언한 변수는 상수가 된다. 구현을 담는 것도 원래는 추상 클래스뿐이었으나 디폴트 메서드가 생기면서 그 경계는 흐려졌다.
의도는 여전히 다르다. 인터페이스는 구현하는 쪽이 같은 동작을 한다는 것을 보장하려는 설계도이고, 추상 클래스는 공통 구현을 물려주고 그 위에서 확장해 나가려는 것이다. 안드로이드 라이브러리처럼 일부만 구현해 두고 나머지를 앱에서 채우게 하는 프레임워크가 뒤쪽을 쓴다.
final
추상 클래스와 반대 방향의 키워드다. 더 이상 바꾸지 못하게 막는다. 변수에 붙으면 재할당할 수 없어 상수가 되고, 메서드에 붙으면 재정의할 수 없고, 클래스에 붙으면 상속할 수 없다.
여러 파일이 함께 쓰는 상수는 한 파일에 모아 public static final로 선언한다. public이라 밖에서 참조할 수 있고 static이라 인스턴스 없이 클래스 이름으로 가져다 쓴다.
final 클래스는 불변 객체를 만드는 조건 중 하나다. 상속을 열어두면 재정의로 기능이 바뀔 수 있어서, 기반 클래스가 변하면 안 되는 String과 Integer가 final로 선언되어 있다.
참고
원본은 추상 클래스가 항상 추상 메서드를 포함한다고 적었다. 자바 언어 명세는 그렇게 요구하지 않는다. 추상 메서드가 없는 추상 클래스도 선언할 수 있다. JLS 8.1.1.1