상속이란 기존 클래스를 재사용하여 새로운 클래스를 작성하는 것이다. 공통적인 코드를 중복해서 사용하게 되면 코드를 변경할 일이 생길 경우 유지보수가 어려워지고 일관성을 유지하기 어렵기 때문에 상속이라는 개념을 사용하게 되었다.
자식 클래스는 부모 클래스의 멤버를 상속받게 되며 자식 클래스의 멤버가 같거나 더 많다. 그래서 상속을 다른 의미로 하면 확장(extends)한다고도 볼 수 있다. 멤버의 개수를 늘리는 행위이기 때문이다.
부모 클래스의 생성자와 초기화 블럭은 상속되지 않는다. 하지만 자식 클래스의 생성자를 만들 때 부모 클래스의 생성자를 호출해야만 자식 클래스의 인스턴스를 생성할 수 있다. 부모 클래스의 생성자를 자식 클래스의 생성자에서 호출해야만 하는 이유는 자손 클래스의 멤버가 부모 클래스의 멤버를 사용할 수도 있으므로 부모 클래스의 멤버들의 초기화가 되어있어야 하기 때문이다. 부모 클래스의 생성자를 호출하는 방법은 super()를 사용하며 의도적으로 부모 클래스의 생성자를 호출하지 않는 경우 컴파일러가 자동적으로 super();를 추가하기 때문에 문법에 맞지 않는 경우도 생기므로 문법에 맞게 부모 클래스의 생성자를 작성해야 한다.
상속 이외에도 클래스를 재사용하는 방법이 있다. 클래스 내에 재사용 하고자 하는 클래스의 인스턴스를 생성하는 것이다. 즉, 확장하고자 하는 클래스 내에 필요로 하는 클래스의 인스턴스를 가르키는 참조변수를 멤버변수로 선언하는 것이다. 아래의 코드는 좌표를 나타내는 Point 클래스와 원을 나타내는 Circle 클래스이다. 상속을 통해 Point 클래스의 멤버변수를 사용할 수도 있지만 Circle 클래스 내에 멤버변수로 Point 클래스의 인스턴스 주소값을 담은 참조변수를 선언하면 상속 없이도 Point 클래스의 멤버변수를 재사용할 수 있다.
class Point {
int x;
int y;
}
class Circle {
Point p = new Point;
int r;
}
package sample;
public class Card {
static final int KIND = 4;
static final int NUM = 13;
static final int SPADE = 4;
static final int DIAMOND = 3;
static final int HEART = 2;
static final int CLOVER = 1;
int kind;
int number;
Card() {
this(SPADE, 1);
}
Card(int kind, int number) {
this.kind = kind;
this.number = number;
}
public String toString() {
String[] kind = {"","CLOVER","HEART","DIAMOND","SPADE"};
String numbers = "0123456789XJQK";
return "kind : " + kind[this.kind] + ", number : " + numbers.charAt(this.number);
}
}
위 코드는 Card 클래스이고 카드를 카드 낱장을 정의하기 위해 만든 클래스이다. 생성자를 통해 모양과 숫자를 입력 받을 수 있고 인스턴스를 출력하면 toString() 메소드를 통해 종류와 숫자를 출력해준다.
멤버 변수인 상수들을 static으로 선언한 이유는 Card 클래스와 Deck 클래스 간에 HAS-A 관계를 맺어 주어 Card 클래스의 멤버변수들을 Deck 클래스에서 객체 생성없이 사용하기 위해서 이다.
kind와 number는 생성자의 매개변수로 초기화될 멤버변수들이라 Deck 클래스에서 사용될 필요는 없으므로 static으로 선언하지 않아도 된다.
package sample;
public class Deck {
final int CARD_NUM = 52;
Card[] cardArr = new Card[CARD_NUM];
Deck() { // 카드 초기화
int i = 0;
for (int k = Card.KIND; k > 0; k--) {
for (int n = 0; n < Card.NUM; n++) {
cardArr[i++] = new Card(k, n + 1);
}
}
}
Card pick(int index) {
return cardArr[index];
}
Card pick() {
int index = (int) (Math.random() * CARD_NUM);
return cardArr[index];
}
void shuffle() {
for (int i = 0; i < cardArr.length; i++) {
int index = (int) (Math.random() * CARD_NUM);
Card temp = cardArr[i];
cardArr[i] = cardArr[index];
cardArr[index] = temp;
}
}
}
다음의 코드는 Deck(카드 한 벌이라는 뜻) 클래스이다. 이 클래스를 통해 52장의 카드를 생성자를 통해 초기화하고 맨 위의 카드를 뽑는 메소드와 아무 카드를 뽑는 메소드를 만들었고 shuffle 메소드로 카드를 섞는 메소드도 만들었다.
각 카드의 객체 생성을 위해 우선 Card 자료형의 배열을 선언한다. 생성자를 통해 각 카드들을 순서대로 초기화한다.
class Test extends Sample{
static void test() {}
}
class Sample {
void test() {} // 컴파일 오류!
}
위에 언급했듯이 super를 이용해서 자식 클래스의 생성자에서 부모 클래스의 생성자를 호출하는데 사용하기도 하고, 부모 클래스의 멤버변수와 자식 클래스의 멤버변수의 이름이 같은 경우 둘을 구붓짓기 위해 super.을 사용하기도 한다. 자식 클래스의 메소드에서 부모 클래스의 내용과 일치하는 부분이 있다면 super.부모클래스메소드를 호출하여 사용하기도 한다.
class Point {
int x;
int y;
String getLocation() {
return "x : " + x + ", y : " + y;
}
}
class Point3D extends Point {
int z;
String getLocation() {
return super.getLocation() + ", z : " + z;
}
}
패키지는 클래스의 묶음이다. 패키지를 이용하면 클래스를 그룹 단위로 묶어 효율적으로 관리할 수 있다. 모든 클래스는 패키지에 포함되어 있다. 지금까지 패키지를 만들지 않고도 클래스를 사용할 수 있었던 이유는 패키지를 따로 설정해주지 않으면 자바에서 자동적으로 패키지를 생성하기 때문이다. 하나의 클래스는 두개 이상의 패키지에 포함될 수 없고 단 하나의 패키지에 포함되어 있어야 한다. 패키지의 이름은 소문자로 짓는 것을 원칙으로 하고 있다.
import 문을 사용하면 다른 패키지 내의 클래스를 사용할 때 패키지명.클래스명에서 패키지명을 생략하고도 코드를 작성할 수 있다. 자바를 처음 배우기 시작했을 무렵에는 import를 어떻게 생각했냐면 '수입하다' 라는 의미 때문에 import 문을 작성하면 다른 패키지의 클래스를 다운(?)받아와서 사용하는 줄 알고 있었다. 그래서 방대한 양의 클래스가 다 내장되어 있으면 비효율적이라서 이렇게 다운받는 형식으로 import문을 사용하는줄 알고 있었다. 근데 그런 의미가 아니라 import문을 사용함으로써 패키지명을 생략해서 코드를 간결하게 해주다는걸 알게 되었다. import문은 package문 다음에, 클래스 선언문 이전에 위치한다.
import java.text.SimpleDateFormat;
public class Sample {
public static void main(String[] args) {
SimpleDateFormat date = new SimpleDateFormat("yyyy/MM/dd");
}
}
public class Sample {
public static void main(String[] args) {
java.text.SimpleDateFormat date = new java.text.SimpleDateFormat("yyyy/MM/dd");
}
}
final은 변경될 수 없는 이라는 뜻을 가지고 있다. 클래스에 사용되면 상속할 수 없다는 의미가 된다. 즉, 다른 클래스의 조상이 될 수 없다. 메서드에 final이 붙으면 오버라이딩을 통해 재정의 될 수 없다. 멤버변수나 지역변수에 붙으면 값을 변경할 수 없는 상수를 의미한다.
final이 붙은 변수는 상수이므로 일반적으로 선언과 동시에 초기화를 하지만 인스턴스 변수의 경우 생성자를 통해 초기화할 수도 있다. 생성자를 통한 상수의 초기화는 인스턴스마다 다른 상수값을 가지게 할 수 있다는 특징이 있다.
public class Sample {
final int NUM;
Sample(int num) {
this.NUM = num;
System.out.println("" + NUM);
}
public static void main(String[] args) {
Sample sample1 = new Sample(1); // 1출력
Sample sample2 = new Sample(2); // 2출력
Sample sample3 = new Sample(3); // 3출력
}
}
abstract는 추상의, 미완성의 라는 뜻을 가지고 있다. 클래스에 붙으면 미완성된 메소드를 포함하는 클래스라는 의미이고, 메서드에 붙으면 바디가 미완성되어 있으니 abstract class를 상속하는 클래스에서 바디를 완성해야만 한다. abstract 메서드는 선언부만 작성되어있고 바디{}없이 세미콜론;으로 메서드 선언부를 마친다.
abstract class는 미완성된 메소드가 존재하므로 인스턴스를 생성할 수 없다.
abstract class는 미완성된 메소드가 존재할 뿐 일반적인 클래스와 기능이 유사하므로 일반 메서드를 작성할 수 있고 일반 메서드에서 미완성된 abstract메서드를 호출하는 것도 가능하다.
package sample;
public class Sample {
protected static void test() {
System.out.println("hi");
}
}
package test;
import sample.Sample;
public class Test extends Sample {
public static void main(String[] args) {
Test.test(); // hi 출력
}
}
접근제어자를 사용하는 이유는 클래스 내부에 선언된 데이터를 보호하기 위함이다. 데이터가 유지되도록, 함부로 변경되지 않도록 데이터(멤버변수, 클래스 등)에 접근제어자를 붙여 데이터 감추기(data hiding)를 하는 것이다. 이는 객체지향개념의 캡슐화에 해당하는 내용이다. 접근제어자를 사용하는 또다른 이유는 복잡성을 제거하기 위함이다. 클래스 내에서만 사용되거나 내부작업을 위해 임시로 사용되는 변수 또는 메서드 등을 감춰서 외부에 노출시키지 않음으로써 복잡성을 없앨 수 있다. 이 또한 캡슐화에 해당하는 내용이다.
만약 메서드 하나를 변경해야 한다고 가정할 때 메서드의 접근 제어자가 public이라면 메서드를 변경한 후에 오류가 없는지 확인해야할 범위가 매우 넓지만 default라면 패키지 내부만 private라면 클래스 내부만 확인하면 된다.
Time이라는 클래스를 생성해서 hour,minute,second 멤버변수를 private으로 생성한 후 외부에서 멤버변수를 변경하지 못하도록 하고 그대신 메서드를 사용해서 변경하도록 코드를 작성해 보자.
package sample;
public class Time {
private int hour, minute, second;
Time(int hour, int minute, int second) {
this.hour = hour;
this.minute = minute;
this.second = second;
}
Time() {
this(0,0,0);
}
public void setHour(int hour) {
if (hour<0 || hour>23) return;
this.hour = hour;
}
public void setMinute(int minute) {
if (minute<0 || minute>59) return;
this.minute = minute;
}
public void setSecond(int second) {
if (second<0 || second>59) return;
this.second = second;
}
public int getHour() {return hour;}
public int getMinute() {return minute;}
public float getSecond() {return second;}
public String toString() {
return "( " + hour + " : " + minute + " : " + second + " )";
}
}
package sample;
final class SingleTon {
private static SingleTon s = new SingleTon();
private SingleTon() {
}
public static SingleTon getInstance() {
if (s == null)
s = new SingleTon();
System.out.println(s + " 생성 완료");
return s;
}
}
public class Sample {
public static void main(String[] args) {
SingleTon s1 = SingleTon.getInstance();
// sample.SingleTon1@1b6d3586 생성 완료 출력
SingleTon s2 = SingleTon.getInstance();
// sample.SingleTon1@1b6d3586 생성 완료 출력
}
}
보통 생성자의 접근제어자는 클래스의 접근제어자와 같지만 다르게 이용해서 접근제어자로 객체 생성을 제한할 수도 있다.
생성자에 private이 붙었다는 의미는 곧 객체를 생성할 수 없다는 의미가 된다. private는 클래스 내에서만 접근할 수 있기 때문에 클래스 내에서 객체를 생성하는건 가능하지만 다른 클래스에서는 객체를 생성할 수 없다.
클래스 내부에 멤버변수로 참조변수를 선언한다. 선언시 private을 붙여서 외부에서 객체를 가르키는 참조변수를 호출할 수 없도록 하고, static을 붙여 getInstance 메소드에서 객체 생성없이 멤버변수를 사용할 수 있도록 한다.
getInstance 메소드를 생성할 땐 static을 붙여서 다른 클래스에서 메서드를 통해 객체를 생성할 때 객체를 생성하지 않고도 메서드를 호출할 수 있도록 한다. 메서드의 접근제어자는 public으로 하여 어디서든 메서드를 이용할 수 있도록 한다.
멤버변수이자 참조변수값이 null인 경우에만 객체를 생성하도록 하고 참조변수를 리턴한다. 이렇게 하면 여러 객체를 생성해도 참조변수가 똑같은 주소값을 갖게 된다. 결국 객체는 하나만 생성되었다는 의미이다.
생성자가 private이라는 것은 객체를 생성할 수 없는 클래스라는 의미라고 앞서 언급했다. 그 의미는 또 부모 클래스가 될 수 없다는 의미이기도 하다. 왜냐하면, 자식클래스가 객체를 생성할 때 자식클래스의 생성자에서 부모클래스의 생성자를 호출해야 하는데 자식 클래스의 생성자가 private이기 때문에 부모클래스의 생성자에서 호출이 불가능하기 때문이다. 이렇게 부모클래스가 될 수 없는 클래스들에 final을 명시하여 확장할 수 없는 클래스라는걸 알리는 것이 좋다.
다형성이란 객체지향개념의 중요한 특징 중 하나로 여러가지 형태를 가질 수 있는 능력을 의미한다. 즉, 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있다는 의미이다. 더 구체적으로 말하자면 부모클래스 타입의 참조변수로 자식클래스의 인스턴스를 참조할 수 있는 특징을 다형성이라고 한다.
부모클래스 타입의 참조변수로 자식클래스의 인스턴스를 참조한다는 것의 가장 큰 특징은 멤버의 개수의 변화이다. 자식클래스 타입의 참조변수로 자신의 인스턴스를 참조한 경우보다 멤버의 개수가 같거나 더 적다. 자식클래스의 인스턴스를 참조하고 있다고 하더라도 부모 클래스 내의 멤버만 호출이 가능하다.
반대로 자식클래스 타입의 참조변수로 부모클래스의 인스턴스를 참조하는 것은 부모클래스에는 존재하지 않은 멤버를 사용하고자 할 가능성이 있으므로 허용하지 않는다.
package sample;
class Product {
int price;
int bonusPoint;
Product(int price) {
this.price = price;
bonusPoint = (int) price / 10;
}
}
class Tv extends Product {
Tv() {
super(100);
}
@Override
public String toString() {
return "Tv";
}
}
class Computer extends Product {
Computer() {
super(200);
}
@Override
public String toString() {
return "Computer";
}
}
class Buyer {
int money = 1000;
int bonusPoint = 0;
void buy(Product p) {
if (money < p.price) {
System.out.println("돈 부족");
return;
}
money -= p.price;
bonusPoint += p.bonusPoint;
System.out.println(p + " 을/를 구입");
}
}
public class Sample {
public static void main(String[] args) {
Buyer buyer = new Buyer();
Tv tv = new Tv();
Computer computer = new Computer();
buyer.buy(tv);
buyer.buy(computer);
}
}
Product 클래스를 부모클래스로 두고 멤버변수로 제품가격과 포인트를 선언하고 생성자를 만들어서 가격을 초기화하도록 했다.
각각의 가전제품 클래스를 만들고 Product 클래스를 확장했다. 각각의 생성자는 부모 생성자를 호출해야 하기 때문에 super()로 호출하고 괄호안에 각각의 가격을 적는다. Object 클래스의 메서드인 toString메서드를 오버라이딩하여 각 클래스의 이름을 리턴한다.
Buyer 클래스에는 구매자의 현재 가지고 있는 돈과 포인트를 초기화 시켰고, buy메서드를 만들어 구매하는 기능을 추가하였다. buy 메서드의 매개변수로는 각각의 자식 타입의 참조변수를 사용해도 좋지만 모든 인스턴스를 참조할 수 있는 부모 타입의 참조변수를 매개변수로 두어서 코드를 간결하게 했다.
class Buyer {
int money = 1000;
int bonusPoint = 0;
int index = 0; // itemList 의 인덱스로 사용될 변수
Product[] itemList = new Product[10];
void buy(Product p) {
if (money < p.price) {
System.out.println("돈 부족");
return;
}
money -= p.price;
bonusPoint += p.bonusPoint;
itemList[index++] = p;
System.out.println(p + " 을/를 구입");
}
void summery() {
System.out.println("남은 돈 : " + money);
System.out.println("포인트 : " + bonusPoint);
for (int i=0; i<itemList.length; i++) {
if (itemList[i] == null) break;
System.out.printf("%s\t",itemList[i]);
}
}
}
Product 타입의 배열을 적당한 크기로 선언하고, buy 메서드에서 매개변수로 들어오는 제품을 Product 타입의 배열에 index 변수를 사용해서 순서대로 넣는다.
Summery 메서드를 추가해서 현재 정보와 itemList에 쌓여있는 제품들을 출력하기 위해 반복문을 사용한다. element값이 null이면 반복문을 탈출하도록 한다.
제품을 얼마나 구입할지 몰라 충분한 크기의 배열을 사용했지만 예상과는 다르게 배열의 크기를 넘어서도록 제품을 구입할 수도 있다. 이럴 경우 일반적인 배열을 사용하기 보다는 Vector 클래스를 이용하면 배열의 크기를 알아서 관리해주기 때문에 저장할 인스턴스 개수에 신경쓰지 않아도 된다.
class Buyer {
int money = 1000;
int bonusPoint = 0;
Vector itemList = new Vector();
void buy(Product p) {
if (money < p.price) {
System.out.println("돈 부족");
return;
}
money -= p.price;
bonusPoint += p.bonusPoint;
itemList.add(p);
System.out.println(p + " 을/를 구입");
}
void refund(Product p) {
if (itemList.remove(p)) {
money += p.price;
bonusPoint -= p.bonusPoint;
System.out.println(p + " 을/를 환불");
} else {
System.out.println(p + " 없음");
}
}
void summery() {
String items = "";
System.out.println("남은 돈 : " + money);
System.out.println("포인트 : " + bonusPoint);
for (int i = 0; i < itemList.size(); i++) {
Product p = (Product) itemList.get(i);
items += (i == 0) ? "" + p : ", " + p;
}
System.out.println(items);
}
}
클래스가 설계도라면 추상클래스는 미완성 설계도이다. 새로운 클래스를 작성하는데 있어서 바탕이 되는 부모클래스로서의 중요한 의미를 갖는다. 새로운 클래스를 작성할 때 아무것도 없는 상태에서 시작하기 보다는 완전하지는 않더라도 틀을 어느정도 갖춘 상태에서 시작하는 것이 더 나을 것이다. 이러한 역할을 해주는게 추상클래스이다.
추상클래스는 추상메서드를 포함하고 있는 클래스이고, 일반클래스처럼 추상메서드 이외에도 일반메서드나 멤버변수를 가질 수 있다. 또한 일반 메서드에서 추상메서드를 호출할 수도 있다.
추상메서드는 선언부만 작성하고 바디는 작성하지 않는다. 추상클래스를 확장할 여러 클래스에서 오버라이딩하여 각 클래스의 조건에 따라 다른 내용의 메서드를 작성하면 된다.
일반 메서드도 바디를 작성하지 않을 수 있는데 굳이 abstract를 붙여 추상메서드를 사용하는 이유는 뭘까? 일반 메서드와 달리 자식 클래스에서 추상메서드를 오버라이딩하도록 강제하기 때문이다.
프로그램을 작성하는 방법에는 각 클래스의 공통적인 부분을 미리 파악하여 추상클래스를 먼저 작성하는 방법이 있고, 먼저 클래스들을 작성한 후 공통적인 부분을 뽑아 추상클래스를 나중에 작성하는 방법이 있다. 상속계층도를 따라 내려갈수록 구체화된다고 표현하고, 올라갈수록 추상화된다고 표현한다.
추상메서드와 인터페이스는 거의 유사한 기능을 갖지만 인터페이스가 보다 추상화의 정도가 높다. 인터페이스는 오로지 추상메서드(abstract)나 상수(final)만을 가질 수 있다. 일반메서드나 멤버변수는 가질 수 없다. 인터페이스 역시 추상클래스와 마찬가지로 불완전하기 때문에 그 자체로 사용되기 보다는 클래스를 작성하는데 도움을 줄 목적으로 작성된다.
interface name {
public static final 상수 = 값;
public abstract 메서드;
}
package sample;
interface Movable {
void move(int x, int y); // public abstract 생략되어 있음
}
class Marine implements Movable {
@Override
public void move(int x, int y) {}
}
추상클래스와 마찬가지로 인터페이스의 여러 추상메서드 중 일부만을 자식클래스가 구현한다면 자식클래스 역시 abstract를 붙여 일부가 구현이 안되었다는걸 명시해야만 한다.
단일상속 특징을 지닌 클래스와 달리 인터페이스는 다중상속이 가능하다는 특징이 초점이 맞춰지는 바람에 다중상속이 장점으로 느껴질 수 있는데 인터페이스로 다중상속을 구현하는 경우는 거의 없다고 한다.
리턴타입이 인터페이스라는 것은/ 메서드가/ 해당 인터페이스를 구현한 클래스의 인스턴스를/ 반환한다는 것을 의미한다./(중요) 다음은 이를 설명하기 위해 파일의 확장자에 따라 파일 분석을 달리 하는 프로그램을 짜는 예를 들어 보겠다.
package sample;
interface Parseable {
void parse(String fileName); // public abstract 생략
}
class ParserManager {
public static Parseable getParser(String type) {
// 리턴 타입이 Parseable 인터페이스
if (type.equals("XML")) return new XMLParser();
else {
Parseable p = new HTMLParser();
return p; // new HTMLParser() 리턴
}
}
}
class XMLParser implements Parseable {
@Override
public void parse(String fileName) {
System.out.println(fileName + "- XML parsing complete.");
}
}
class HTMLParser implements Parseable {
@Override
public void parse(String fileName) {
System.out.println(fileName + "- HTML parsing complete.");
}
}
public class Sample {
public static void main(String[] args) {
Parseable parser = ParserManager.getParser("XML");
parser.parse("document.xml");
parser = ParserManager.getParser("HTML");
parser.parse("document2.html");
}
}
일단 분석이 가능한 파일들을 그룹화하기 위해 Parseable 인터페이스를 생성한 후 파일 이름을 매개변수로 하는 parse 추상 메서드를 선언한다.
각 확장자 별로 파일을 분석하는 클래스를 생성한 후 Parseable 인터페이스를 구현하여 parse 메서드의 바디를 작성한다.
ParserManager라는 클래스를 생성하여 getParer 메서드로 파일 이름을 매개변수로 받으면 확장자에 따라 파일 분석을 달리 한 후, 해당 확장자 파일의 분석을 하는 클래스의 인스턴스를 리턴하도록 한다. 여기서 리턴타입은 각각의 클래스의 인스턴스를 모두 참조할 수 있는 Parseable 인터페이스 타입으로 한다.
getParser 메서드의 내용은 매개변수로 어떤 확장자를 매개변수로 받느냐에 따라 해당 클래스의 인스턴스를 반환하도록 한다. 해당 인스턴스를 반환함으로써 인스턴스가 파일을 분석 해준다.
package sample;
class A { // User
public void methodA(B b) {
b.methodB();
}
}
class B { // Provider
public void methodB() {
System.out.println("methodB");
}
}
public class Sample {
public static void main(String[] args) {
A a = new A();
a.methodA(new B());
}
}
package sample;
interface I {
void methodB(); // public abstract 생략
}
class A {
public void methodA(I i) {
i.methodB();
}
}
class B implements I {
@Override
public void methodB() {
System.out.println("methodB in B class");
}
}
class C implements I {
@Override
public void methodB() {
System.out.println("methodB in C class");
}
}
public class Sample {
public static void main(String[] args) {
A a = new A();
a.methodA(new B());
a.methodA(new C());
}
}
인터페이스를 통해 클래스 B의 메서드를 호출하게 되면 클래스 B의 변경에 영향을 받지 않는다. 심지어 클래스의 이름을 몰라도 되고 클래스 자체가 존재하지 않아도 문제가 안된다. User는 인터페이스와 직접적인 관계가 있기 때문이다.
인터페이스의 두번째 장점은 서로 관계없는 클래스들에게 인터페이스를 공통적으로 구현해 관계를 맺어줄 수 있다.
클래스와 클래스 간의 직접적인 관계(상속)를 인터페이스를 이용해서 간접적인 관계로 변경하면, 한 클래스의 내용을 변경해야 하는 경우, 다른 클래스에 영향을 미치지 않도록 독립적인 프로그래밍이 가능하다.
조상 클래스에 새로운 메서드를 추가하는 것은 별일 아니지만, 인터페이스의 경우엔 추상 메서드를 하나 추가함으로써 인터페이스를 구현한 모든 클래스 내에 새로운 메서드가 추가되는 것이고 그 메서드를 클래스마다 구현해 내야하기 때문에 매우 큰 일이 된다.
인터페이스가 변경이 되지 않는 경우가 최상이겠지만 아무리 설계를 잘해도 변경해야 할 경우가 생긴다. 이럴 경우 디폴트 메서드를 사용하면 된다. 인터페이스에 추상메서드만 추가가 가능하다고 했지만 업데이트가 되어 디폴트 메서드가 추가되었다. 디폴트메서드는 추상메서드와 달리 바디가 있어야하고, 접근 제어자가 public이며 생략 가능하다. 추상클래스에서의 일반메서드와 비슷한 기능을 하게 되는 것이다. 인터페이스를 구현한 클래스들은 디폴트메소드를 상속 받게 된다.
class A {
class B {
}
}
내부 클래스 역시 인스턴스 클래스, 스태틱 클래스, 지역 클래스, 익명 클래스가 있으며 각 종류들은 멤버의 종류와 유사한 특징을 지니고 있다.
인스턴스클래스와 스태틱 클래스는 외부 클래스의 멤버변수와 같은 위치에 선언되며 멤버변수와 같은 성질을 갖는다.
내부클래스는 클래스의 의미를 갖기도 하고 외부클래스의 입장에서는 멤버변수의 의미도 갖고 있기 때문에 모든 접근제어자를 사용할 수 있다.