본문 바로가기

Java/Java Language

[Java] 다형성 (Polymorphism)

다형성이란?

다형성의 의미

다형성(Polymorphism)은 상속과 깊은 관계가 있으며, 하나의 참조변수로 다양한 형태의 타입을 객체를 참조할 수 있도록 한것이다. 정확하게는 상위 클래스 타입의 참조변수로 하위클래스의 인스턴스를 참조할 수 있다는 것이다.

 

참조에 대한 다형성이 가능한 이유

아래의 코드에서 Person 클래스의 자식 클래스 Student이며, PersonStudentTest메서드 구현부를 보면 Person 클래스의 참조 변수 p가 Student의 인스턴스를 참조하는 것이 가능하다. 자식 클래스는 부모 클래스의 멤버를 모두 상속받아 접근이 가능하므로 참조가 가능하다. 즉, 참조변수는 참조하는데 문제가 없으면 참조가 가능하다.

class Person {  
    String name;  
    int age;  

    Person(String name, int age) {  
        this.name = name;  
        this.age = age;  
    }  

    void introduce() {  
        System.out.println("Hello, my name is " + name + " and I am " + age + " years old.");  
    }  
}

class Student extends Person {  
    String major;  
    int studentId;  

    Student(String name, int age, String major, int studentId) {  
        super(name, age); // 부모 클래스의 생성자 호출  
        this.major = major;  
        this.studentId = studentId;  
    }  

    void printStudentInfo() {  
        System.out.println("Student ID: " + studentId + ", Major: " + major);  
    }  
}

 

참조변수의 접근 범위

다음 코드에서 참조변수 p와 s가 가리키는 인스턴스의 타입은 동일하지만, 접근할수 있는 멤버는 다르다. s는 자식 클래스 타입의 참조변수와 다르게,p는 부모 클래스 타입의 참조변수이므로 부모 클래스의 멤버만 접근할 수 있다.

Person p = new Student("Sujin", 21, "CS", 0);  
Student s = new Student("Minsu", 20, "CS", 1);  

System.out.println(p.name + p.major); // p.major: compile error  
p.printStudentInfo() // compile error

 

따라서 부모 클래스 타입의 참조변수 p는 자식 클래스의 멤버 major와 printStudentInfo()에 참조할 수 없다. 이처럼 참조변수의 타입에 따라 참조할 수 있는 객체의 종류와 사용할수 있는 멤버의 수가 결정된다. 그러나 인스턴스의 타입이 참조변수의 타입과 일치하지 않아도, 인스턴스의 실제 타입에 포함된 멤버는 동일하다. 참고로 모든 참조변수는 null 또는 4byte의 주소값이 저장된다. 

 

실제로는 모든 클래스의 최고조상인 Object 클래스로부터 상속받은 멤버도 포함되어야 하지만 간단하게 표시하기위해 생략하였다.

 

반대로, 자식 클래스 타입의 참조변수로 상위 클래스의 인스턴스를 참조하는 것은 컴파일 에러가 발생한다.

Student s = new Person("fd", 21);

 

자식클래스의 멤버는 부모 클래스의 멤버와 동일하거나 많다. 그리하여 하위 클래스의 참조변수는 자식 클래스에는 존재하지만 상위 클래스에는 존재하지 않는 멤버에 접근할 가능성이 있기 때문에 허용하지 않는것이다.

 

참조에 대한 다형성이 필요한 이유

그렇다면 인스턴스의 타입과 일치하는 참조변수를 사용하면 인스턴스의 모든 멤버를 사용할수 있는데, 왜 상위 클래스의 참조변수를 사용하여 일부 멤버만 사용하도록하는 것인가?

 

예를 들면, 마트에서 가전제품 등의 품목이 있고 가전 제품에는 냉장고, 애어콘, 세탁기 등의 제품이 있다. 이렇게 제품을 품목별로 카테고리화 시키면 고객이 마트에 들어가 빠르게 제품을 찾아낼 수 있고 회사에서는 재고 관리가 편해진다.

 

이처럼 쓰임이 같은 여러 객체들을 하나의 종류로 묶어 관리할 수 있다. 다음 코드에서 Product 클래스는 Tv, Refrigerator, Smartphone을 하나로 묶어, 상위 개념인 Product 클래스의 하위 클래스로 정의할 수 있다.

class Product {  
    int price;  
    int bonusPoint;  

    //...
}

class Smartphone extends Refrigerator {}
class Smartphone extends SmartPhone {}
class Smartphone extends Tv {}

class Buyer {
    //...

    public void buyProduct(Product product) {  
        if (money < product.prifce) {   
            return;  
        }  
        money -= product.price;  
        bonusPoint += product.bonusPoint;  
    }  
}

 

클래스를 같이 묶을 수 있는 상위 클래스를 설계해놓고 상속받으면, buyProduct() 메서드에서 상위 클래스 타입의 매개변수 product는 하위 클래스인 Tv, Refrigerator, Smartphone의 인스턴스를 모두 참조할수 있다. 그리하여 다양한 종류의 제품을 구매하는 작업을 공통적으로 수행할 수 있다.

 

따라서 기능을 수행하는 메서드는 매개변수로 상위 클래스 타입의 참조변수를 전달받아, 여러 하위 클래스의 인스턴스도 공통의 로직으로 처리할 수 있게된다.

 

결국, 객체지향 관점은 일생생활에서 쉽게 접할수 있는 것이며, 물체를 잘 분리하고 정리하여 관리하기 쉽도록하기 위해 쓰는 것이다.


참조변수의 타입 캐스팅

참조변수의 타입 캐스팅이 가능한 이유

기본형 변수와 같이 참조변수도 타입 캐스팅이 가능하다. 단, 서로 상속관계에 있는 클래스 사이에서만 가능하여, 하위 클래스 타입의 참조변수를 상위 클래스 타입의 참조변수로 타입 캐스팅이 가능하다. 또한 상위 클래스 타입의 참조변수를 하위 클래스 타입의 참조변수로 타입 캐스팅도 가능하다. 상속 관계에 있는 클래스 타입은 동일한 멤버가 있으므로 타입 캐스팅이 가능한 것이다.

 

하위 클래스 타입에서 상위 클래스 타입으로 타입 캐스팅하는 것을 up-casting이라 한다. 그리고 상위 클래스 타입에서 하위 클래스 타입으로 타입 캐스팅하는 것을 down-casting이라한다.

 

Person 타입의 참조변수는 Student타입의 참조변수 보다 접근할 수 있는 멤버의 범위가 동일하거나 작으므로, p 참조변수가 인스턴스의 멤버를 참조하는데 문제가 발생하지 않아서 컴파일러가 자동 타입캐스팅을 하도록 구현되었다. 반대로 Studuent 타입의 참조변수는 Person 타입의 참조변수 보다 접근할 수 있는 멤버의 범위가 크므로, 접근 불가능한 멤버에 참조할 수 있다. 따라서 개발자가 명시적으로 타입 캐스팅을 해주어야한다.

// 컴파일러 자동 업캐스팅: (Person) new Student("Sujin", 21, "CS", 0);
Person p = new Student("Sujin", 21, "CS", 0);  
Student s = new Student("Minsu", 20, "CS", 1);  

// 자식 타입의 참조변수를 부모 타입으로 참조변수로 업 캐스팅 (참조변수 범위 축소)
p = s; // person = (Person) s; 타입 캐스팅 생략

// 부모 타입의 참조변수 자식 타입의 참조변수로 다운 캐스팅 (참조변수 범위 확장)
s = (Student) p; // 타입 캐스팅 생략 불가

 

참조변수의 타입 캐스팅은 참조 변수의 타입을 변환하는 것이고 인스턴스를 변환하는 것이 아니기 때문에 참조변수의 타입 캐스팅은 인스턴스에 아무 영향을 끼치지 않는다. 참조변수의 타입 캐스팅을 통해서 참조하고 있는 인스턴스에서 접근할 멤버의 범위를 늘리거나 줄이는 것이다.

 

인스턴스의 타입에 따른 참조변수의 참조 허용

아래의 코드는 컴파일은 성공하지만 런타임시에 ClassCastException에러가 발생한다. Person 타입의 참조변수 p는 부모 타입의 인스턴스를 참조하고 있는데, 자식 타입으로 타입 캐스팅 후에 자식 타입의 참조변수 s에 부모 타입의 인스턴스의 주소를 할당하여 에러가 발생하였다. 참조변수 p는 부모 타입인 Person의 인스턴스의 주소를 저장하고 있기 때문이다.

Person p = new Person("Sujin", 21);  
Student s = new Student("Minsu", 20, "CS", 1);

// Exception in thread "main" java.lang.ClassCastException: class objectoriented.polymorphism.Person cannot be cast to class objectoriented.polymorphism.Student
s = (Student) p; // 타입 캐스팅 생략 불가

 

상위 타입의 인스턴스를 하위 타입의 참조변수로 참조하는 것은 허용되지 않는다. 하위 클래스 타입의 참조 변수는 상위 클래스 타입의 인스턴스 멤버보다 더 큰 접근 범위를 갖기 때문이다.

 

컴파일시에는 참조변수간의 타입만 확인하기 때문에 실행 시 생성될 인스턴스의 타입에 대해서는 전혀 알지 못한다. 그래서 컴파일시에는 문제가 없지만 런타임에는 에러가 발생하여 비정삭적으로 종료된것이다.

 

그러므로 참조 변수를 하위 클래스 타입으로의 타입 캐스팅을 수행하기 전에, instanceof 연산자를 사용하여 참조변수가 참조하고 있는 실제 인스턴스의 타입이 상위 클래스가 아닌지 확인하는 것이 안전하다.


instanceof 연산자

실제 인스턴스의 타입 확인

위에서 상위 클래스 타입의 참조변수로 하위 클래스의 인스턴스를 참조할 수 있기 때문에, 참조 변수의 타입과 인스턴스의 타입이 불일치할 수도 있다는 것을 보았다.

 

만일, 매개변수로 상위 클래스의 타입으로 참조변수를 전달받으면, 해당 참조변수가 어떤 인스턴스를 참조하는지 알수가 없다. 그런데 상위 클래스 타입의 참조변수로 인스턴스의 모든 멤버에 접근할 수 없기 때문에, 하위 클래스에 정의된 멤버에 접근 하기 위해서 하위 클래스 타입의 참조변수로 타입 캐스팅해야한다.

 

이럴 경우, instanceof 연산자로 인스턴스의 실제 타입을 확인할 수 있다. 그러므로 하위 클래스의 인스턴스인지 확인하고 해당 클래스 타입의 참조변수로 타입 캐스팅을 하면된다.

 

다음 예시에서 Manager, Developer 클래스는 Employee 클래스의 하위 클래스이다. 매개변수로 인스턴스 참조변수를 전달 받고 instanceof연산자로 실행시 인스턴스의 실제 타입을 확인하고 일치한다면, 해당 타입으로의 타입 캐스팅 후 하위 클래스에 추가적으로 구현된 메서드를 호출하는 코드이다.

public class Main {  
    // 상위 클래스 타입의 참조 변수를 매개변수로 받는 메서드  
    public static void processEmployee(Employee employee) {  

        // 모든 하위 클래스의 인스턴스는 상위 클래스의 인스턴스를 포함하기 때문에, 조건식 true
        if (employee instanceof Employee) {  
            System.out.println(employee.getClass());  
            employee.printInfo(); // 인스턴스 변수 출력  
            employee.work(); // 모든 Employee 객체가 공통적으로 가진 메서드 호출  
        }  

        // 하위 클래스인 Manager의 인스턴스인지 확인 후, 타입 캐스팅  
        if (employee instanceof Manager) {  
            Manager manager = (Manager) employee;  
            manager.conductMeeting(); // Manager 클래스에만 있는 메서드 호출  
        }  

        // 하위 클래스인 Developer의 인스턴스인지 확인 후, 타입 캐스팅  
        if (employee instanceof Developer) {  
            Developer developer = (Developer) employee;  
            developer.debugCode(); // Developer 클래스에만 있는 메서드 호출  
        }  
    }  

    public static void main(String[] args) {  
        Employee employee = new Employee("Jin", 1);  
        Employee manager = new Manager("Alice", 101, 5);  
        Employee developer = new Developer("Bob", 202, "Java");  

        processEmployee(employee);  
        System.out.println();  

        processEmployee(manager);  
        System.out.println();  

        processEmployee(developer);  
    }  
}

 

하위 클래스는 상위 클래스의 모든 멤버를 상속받기 때문에, 모든 하위 클래스의 인스턴스는 상위 클래스의 인스턴스를 포함하는 것과 같다. 그리하여 매개변수로 넘어온 employee 참조변수가 참조하는 인스턴스가 하위 클래스의 타입인 Manager나 Developer이어도 Employee 타입의 instanceof 연산에서 true 값을 얻는다. 이렇게 instanceof 연산 결과값이 true이면, 참조변수를 해당 클래스 타입으로 캐스팅하여도 문제가 없다는 의미이다.


참조변수와 인스턴스의 연결

참조 변수의 인스턴스 변수 접근

부모 클래스에 선언된 멤버 변수와 동일한 인스턴스 변수를 자식 클래스에 중복 정의 했을 때, 참조변수의 타입에 따라서 자식 클래스의 인스턴스 변수 값을 읽어드리는 것에 차이가 있다.

 

부모 클래스 타입의 참조 변수가 자식 클래스의 인스턴스를 참조하면, 참조 변수는 여전히 부모 클래스의 인스턴스 변수에 접근한다. 그러나 자식 클래스 타입의 참조 변수가 자식 클래스의 인스턴스를 참조하는 경우, 해당 클래스의 인스턴스 변수에 접근한다.

 

아래의 코드에서 부모 클래스인 Animal클래스의 참조변수 animal으로 인스턴스 변수 x에 직접 접근하여 값을 읽어드리면, 항상 Animal 클래스의 인스턴스 변수 값이 10이다. 반면에, 인스턴스 메서드는 makeSound()인스턴스의 실제 타입에 대한 메서드를 호출하는 것을 알 수 있다.

class Animal {  
    int x = 10;  

    void makeSound() {  
        System.out.println(x + ", " + "Animal sound\n");  
    }  
}

class Dog extends Animal {  
    int x = 100;  

    void makeSound() {  
        System.out.println(x + ", " + "Woof\n");  
    }  
}

class Cat extends Animal {  
    int x = 1000;  

    void makeSound() {  
        System.out.println(x + ", " + "Meow\n");  
    }  
}

public class AnimalTest {  
    public static void main(String[] args) {  
        Animal animal = new Animal();  
        System.out.println(animal.x); // 10 
        animal.makeSound(); // 10 Animal sound 출력  

        animal = new Dog(); // 부모 클래스 타입의 참조 변수로 자식 클래스 인스턴스를 참조  
        System.out.println(animal.x); // 10 
        animal.makeSound(); // 100 Woof 출력  

        animal = new Cat(); // 부모 클래스 타입의  참조 변수로 자식 클래스 인스턴스를 참조  
        System.out.println(animal.x); // 10 
        animal.makeSound(); // 1000 Meow 출력  

        Dog dog = new Dog();  
        System.out.println(dog.x); // 100  
        dog.makeSound(); // 100, Woof  

        Cat cat = new Cat();  
        System.out.println(cat.x); // 1000  
        cat.makeSound(); // 1000, Meow  
    }  
}

 

자식 클래스의 인스턴스에서 멤버변수는 다음과 같이 저장된다. 하위 클래스인 Dog과 Cat의 인스턴스에는 부모 클래스의 인스턴스 변수 x와 자식 클래스의 인스턴스 변수 x가 저장되어있다. 따라서 참조변수를 통하여 인스턴스 변수에 직접 접근할때, 어떤 인스턴스 변수를 반환할지 선택해야한다.

Heap
------------------------------------
|       Child Instance             |
|----------------------------------|
|  Parent:                         |
|    - int x                       |
|----------------------------------|
|  Child:                          |
|    - int x                       |
------------------------------------

 

참조변수의 타입이 부모 클래스이면 부모 클래스의 인스턴스 변수를 반환하며, 참조변수의 타입이 자식 클래스이면 자식 클래스의 인스턴스 변수를 반환한다. 왜냐하면 하위 클래스의 인스턴스에는 상위 타입과 하위 타입의 인스턴스 변수가 모두 존재하므로, 참조변수의 클래스 타입과 일치하는 멤버 변수를 먼저 읽어드린다.

 

그리고 인스턴스 메서드는 상속받거나 해당 클래스에서 구현한 메서드 중에서, 인스턴스의 실제 타입과 일치하는 메서드를 vtable에서 찾고 참조하여 실행된다. 따라서 인스턴스 메서드의 오버라이딩 여부에 관계없이 실제 클래스 타입의 메서드가 호출된다.

Method Area
------------------------------------
| Child Class Data                 |
------------------------------------
|  vtable:                         |
| +------------------------------+ |
| | method() -> Parent::method   | |
| +------------------------------+ |
| +------------------------------+ |
| | method() -> Child::method    | |
| +------------------------------+ |
------------------------------------

 

 

참조변수의 메서드 접근

인스턴스 메서드의 경우에는 인스턴스의 실제 타입을 확인하여 메서드를 호출하기 때문에, 참조 변수와 무관하게 인스턴스의 실제 타입에 대한 메서드가 실행된다.

 

위의 코드에서 makeSound()을 호출되면, 런타임시에 런타임 상수풀을 참조하여 인스턴스의 실제 타입을 확인하고 해당 타입의 클래스 데이터로 접근하여 vtable에서 일치하는 인스턴스 메서드를 참조하고 실행한다. 인스턴스 메서드가 실행되면서 인스턴스 변수 x에 접근할 때, 현재 인스턴스의 인스턴스 변수 x를 참조한다.

 

이처럼 참조변수의 타입에 따라 사용되는 인스턴스 변수가 달라지는 문제를 예방하기 위해, 자바에서는 여러 가지 방법을 사용할 수 있다. 대표적인 방법으로는 캡슐화(encapsulation)  메서드 호출을 통한 접근이 있다. 이를 통해 인스턴스 변수에 직접 접근하지 않고, 항상 올바른 변수가 사용되도록 보장할 수 있다.

class Animal {
    private int x = 10;

    public int getX() {
        return x;
    }

    public void makeSound() {
        System.out.println(getX() + ", " + "Animal sound\n");
    }
}

class Dog extends Animal {
    private int x = 100;

    public int getX() {
        return x;
    }

    public void makeSound() {
        System.out.println(getX() + ", " + "Woof\n");
    }
}

class Cat extends Animal {
    private int x = 1000;

    public int getX() {
        return x;
    }

    public void makeSound() {
        System.out.println(getX() + ", " + "Meow\n");
    }
}

public class AnimalTest {
    public static void main(String[] args) {
        Animal animal = new Animal();
        System.out.println(animal.getX()); // 10
        animal.makeSound(); // 10, Animal sound

        animal = new Dog();
        System.out.println(animal.getX()); // 100
        animal.makeSound(); // 100, Woof

        animal = new Cat();
        System.out.println(animal.getX()); // 1000
        animal.makeSound(); // 1000, Meow

        Dog dog = new Dog();
        System.out.println(dog.getX()); // 100
        dog.makeSound(); // 100, Woof

        Cat cat = new Cat();
        System.out.println(cat.getX()); // 1000
        cat.makeSound(); // 1000, Meow
    }
}

 

static 메서드는 static 변수와 같이 클래스에 묶인 리소스이므로, 각 클래스 데이터에 저장되어 참조변수의 클래스 타입에 종속된다. 그리하여 static 메서드나 변수를 읽을 때는 참조변수가 아닌 클래스명으로 접근하는 것이 올바르다.


매개변수의 다형성

참조변수의 다형적인 특징은 메서드의 매개변수에서 유용하게 사용된다. 예를 들어, 마트에서 구매자가 제품을 구매하는 시나리오를 가정한다면, 물건을 사는 Buyer와 제품인Product 클래스를 정의할 수 있다. 그리고 Tv, Refrigerator, Smartphone을 하나로 묶어, 상위 개념인 Product클래스의 하위 클래스로 정의할 수 있다.

class Product {  
    private int price;  
    private int bonusPoint;  

    public Product(int price) {  this.setPrice(price); }  

    public int getPrice() {  return price;  }  

    public int getBonusPoint() {  return bonusPoint;  }  

    public void setPrice(int price) {  
        this.price = price;  
        this.bonusPoint = (int) (price / 10.0);  
    }  
}

class Refrigerator extends Product {
    public Refrigerator(int price) { super(price); }
}

class Smartphone extends Product { 
    public Smartphone(int price) { super(price); }
}

class Smartphone extends Product { 
    public Tv(int price) { super(price); } 
}

class Buyer {  
    private int money;  
    private int bonusPoint;  

    public Buyer() {  
        this(0, 0);  
    }  

    public Buyer(int money, int bonusPoint) {  
        this.money = money;  
        this.bonusPoint = bonusPoint;  
    }  

    public int getMoney() { return money; }  
    public void setMoney(int money) { this.money = money; }  
    public int getBonusPoint() { return bonusPoint; }  
    public void setBonusPoint(int bonusPoint) { this.bonusPoint = bonusPoint;}  

    public void buyProduct(Product product) {  
        if (money < product.getPrice()) {  
            System.out.println("You don't have enough money to buy a " + product.getClass().getSimpleName().toLowerCase());  
            return;  
        }  
        money -= product.getPrice();  
        bonusPoint += product.getBonusPoint();  
        System.out.println("You bought a " + product.getClass().getSimpleName().toLowerCase());  
    }  
}

public class Main {  
    public static void main(String[] args) {  
        Tv tv = new Tv(1000000);  
        Buyer buyer = new Buyer(2000000, 0);  
        buyer.buyProduct(tv);  // You bought a tv
    }  
}

 

Product 클래스는 제품의 가격과 보너스포인트 멤버 변수가 존재한다. Buyer 클래스의 buy메서드는 제품을 구매하고 제품의 가격만큼 Buyer 인스턴스의 돈을 차감하는 로직이다.

 

buy메서드의 매개변수인 Product 클래스 타입의 참조변수로 정의하면, 하위 클래스인 Tv, Refrigerator, Smartphone을 공통으로 처리 가능한 메서드를 구현할수 있다. 이와 같이 참조변수의 다형적인 특징을 활용하면, 코드의 유연성과 재사용성이 높아진다. 만일, 참조변수의 다형적 특성이 없다면 매개변수를 하위 클래스로 참조변수를 선언하여 각각의 메서드로 구현해야한다.

 

이러한 매개변수의 다형성은 구체적인 구현에 의존하지 않고 상위 클래스 타입을 사용하여 메서드를 정의할 수 있다. 이를 통해 메서드는 다양한 타입의 객체를 일관되고 코드 중복없이 한번에 처리할 수 있으며, 코드의 재사용성과 확장성을 크게 향상시킬수 있다. 이처럼 다형성은 객체 지향 프로그래밍의 중요한 개념 중 하나이다.


여러 종류의 객체를 배열로 다루기

상위 클래스의 참조변수를 사용하면 여러 하위 클래스의 객체를 공통의 상위 클래스로 묶어서 다를수 있기 때문에,공통 상위 클래스의 참조변수 배열을 생성하여 객체를 저장할 수 있다.

 

이러한 특성을 사용하여 구입한 여러 하위 타입의 객체를 배열로 묶어서 저장할 수 있다 위에서 설명한 Tv, Refrigerator, Smartphone 타입의 객체들을 하나의 Product 배열에 저장이 가능하다. 이렇게 배열에 저장한 제품들의 정보를 한번에 출력하는 기능의 메서드 summaryMyItems등을 구현할 수 있다.

public class Buyer {  
    private int money;  
    private int bonusPoint;  
    private Product[] items = new Product[100];  
    public void summaryMyItems() { 
        //...
    }
}

 

그러나 배열은 생성시에 크기가 고정되어 제품을 저장 개수의 한계가 있다. 그래서 제품을 추가할 때 배열의 크기를 확장하지 못한다. 이로 인해 ArrayIndexOutOfBoundsException이 발생할 수 있기 때문에, 동적 배열인 ArrayList을 사용하면 관리가 더 용이하다.

package objectoriented.polymorphism.mart;  

import java.util.ArrayList; // class  
import java.util.List; // interface  

public class Buyer {  
    private int money;  
    private int bonusPoint;  
    private List<Product> items = new ArrayList<Product>();;  

    public Buyer(int money) {  
        this.money = money;  
    }  

    public int getMoney() { return money; }  
    public void setMoney(int money) { this.money = money; }  
    public int getBonusPoint() { return bonusPoint; }  
    public void setBonusPoint(int bonusPoint) { this.bonusPoint = bonusPoint;}  

    public void buyProduct(Product product) {  
        if (money < product.getPrice()) {  
            System.out.println("You don't have enough money to buy a " + product.getClass().getSimpleName().toLowerCase());  
            return;  
        }  
        money -= product.getPrice();  
        bonusPoint += product.getBonusPoint();  
        items.add(product);  
        System.out.println("You bought a " + product.getClass().getSimpleName().toLowerCase());  
    }  

    public void summaryMyItems() {  
        int sum = 0;  

        if (items.isEmpty()) {  
            System.out.println("You don't have any items.");  
            return;  
        }  

        for (Product product : items) {  
            System.out.println(product.getClass().getSimpleName().toLowerCase() + ": " + product.getPrice());  
            sum += product.getPrice();  
        }  
        System.out.println("The sum of the items is: " + sum);  
    }  
}

 

참조변수의 선언은 ArrayList<Product>기 아닌 List<Product> 타입으로 선언한 이유는 List는 인터페이스이고, ArrayList는 그 인터페이스를 구현한 클래스이기 때문이다.

 

인터페이스 타입인 List를 사용하면, 나중에 구현을 바꿀 필요가 있을 때 코드 변경이 최소화된다. 예를 들어, 배열인 ArrayList 대신 연결리스트LinkedList로 바꾸고 싶다면, 변수 선언부와 초기화 부분만 수정하면 된다. 인터페이스 List을 구현한 여러 클래스들이 많으므로 자료구조 변경시에 효과적으로 사용할수 있다.

 

참고로 Vector는 동기화된 컬렉션이지만, 동기화가 필요하지 않은 경우 ArrayList를 사용하는 것이 좋다. 더 나은 성능과 가독성을 제공하기 때문이다.


챰고 자료

  • 자바의 정석 (남궁성 지음)