Lesson: Interfaces and Inheritance[Inheritance 2]

2024. 6. 4. 16:28High Level Programming Language/Learning the Java Language

Polymorphism

[공식 튜토리얼]

 

다형성의 사전적 정의는 유기체나 종[species]이 다양한 형태나 단계를 가질 수 있는 생물학의 원리를 의미합니다. 이 원칙은 객체 지향 프로그래밍 및 Java 언어와 같은 언어에도 적용될 수 있습니다. 자바 클래스의 하위 클래스는 고유한 동작을 정의하면서도 상위 클래스와 동일한 기능 중 일부를 공유할 수 있습니다.

 

다형성은 Bicycle 클래스를 약간 수정하여 시연할 수 있습니다. 예를 들어, 현재 인스턴스에 저장된 모든 데이터를 표시하는 클래스에 printDescription 메소드를 추가할 수 있습니다.

public class Bicycle {
        
    // the Bicycle class has three fields
    public int cadence;
    public int gear;
    public int speed;
        
    // the Bicycle class has one constructor
    public Bicycle(int startCadence, int startSpeed, int startGear) {
        gear = startGear;
        cadence = startCadence;
        speed = startSpeed;
    }
        
    // the Bicycle class has four methods
    public void setCadence(int newValue) {
        cadence = newValue;
    }
        
    public void setGear(int newValue) {
        gear = newValue;
    }
        
    public void applyBrake(int decrement) {
        speed -= decrement;
    }
        
    public void speedUp(int increment) {
        speed += increment;
    }
    
    public void printDescription(){
    	System.out.println("\nBike is " + "in gear " + this.gear
        + " with a cadence of " + this.cadence +
        " and travelling at a speed of " + this.speed + ". ");
	}
        
}

 

Java 언어의 다형성 기능을 보여주기 위해 MountainBike 및 RoadBike 클래스를 사용하여 Bicycle 클래스를 확장합니다. MountainBike의 경우 자전거에 전면 충격 흡수 장치인 Front가 있는지 나타내는 문자열 값인 서스펜션 필드를 추가합니다. 또는 자전거에 앞뒤 충격 흡수 장치인 듀얼이 있습니다.

 

업데이트된 클래스는 다음과 같습니다.

public class MountainBike extends Bicycle {
    private String suspension;

    public MountainBike(
               int startCadence,
               int startSpeed,
               int startGear,
               String suspensionType){
        super(startCadence,
              startSpeed,
              startGear);
        this.setSuspension(suspensionType);
    }

    public String getSuspension(){
      return this.suspension;
    }

    public void setSuspension(String suspensionType) {
        this.suspension = suspensionType;
    }

    public void printDescription() {
        super.printDescription();
        System.out.println("The " + "MountainBike has a" +
            getSuspension() + " suspension.");
    }
}

 

오버라이드된 printDescription 메소드를 참고하세요. 이전에 제공된 정보 외에도 서스펜션에 대한 추가 데이터가 출력에 포함됩니다.

 

다음으로 RoadBike 클래스를 만듭니다. 도로 또는 경주용 자전거에는 얇은 타이어가 있으므로 타이어 폭을 추적하는 속성[property:JavaBeans 용어]을 추가하세요. RoadBike 클래스는 다음과 같습니다.

public class RoadBike extends Bicycle{
    // In millimeters (mm)
    private int tireWidth;

    public RoadBike(int startCadence,
                    int startSpeed,
                    int startGear,
                    int newTireWidth){
        super(startCadence,
              startSpeed,
              startGear);
        this.setTireWidth(newTireWidth);
    }

    public int getTireWidth(){
      return this.tireWidth;
    }

    public void setTireWidth(int newTireWidth){
        this.tireWidth = newTireWidth;
    }

    public void printDescription(){
        super.printDescription();
        System.out.println("The RoadBike" + " has " + getTireWidth() +
            " MM tires.");
    }
}

 

다시 한 번 printDescription 메소드가 오버라이드 되었습니다. 이번에는 타이어 폭에 대한 정보가 표시됩니다.

요약하자면 Bicycle, MountainBike, RoadBike의 세 가지 클래스가 있습니다. 두 하위 클래스는 printDescription 메서드를 오버라이드하고 고유 정보를 인쇄합니다.

 

다음은 세 개의 Bicycle 변수를 생성하는 테스트 프로그램입니다. 각 변수는 세 가지 자전거 클래스 중 하나에 할당됩니다. 그런 다음 각 변수가 인쇄됩니다.

public class TestBikes {
  public static void main(String[] args){
    Bicycle bike01, bike02, bike03;

    bike01 = new Bicycle(20, 10, 1);
    bike02 = new MountainBike(20, 10, 5, "Dual");
    bike03 = new RoadBike(40, 20, 8, 23);

    bike01.printDescription();
    bike02.printDescription();
    bike03.printDescription();
  }
}

 

다음은 테스트 프로그램의 출력입니다.

Bike is in gear 1 with a cadence of 20 and travelling at a speed of 10. 

Bike is in gear 5 with a cadence of 20 and travelling at a speed of 10. 
The MountainBike has a Dual suspension.

Bike is in gear 8 with a cadence of 40 and travelling at a speed of 20. 
The RoadBike has 23 MM tires.

 

JVM(Java Virtual Machine)은 각 변수에서 참조되는 객체에 대해 적절한 메소드를 호출합니다. 변수 타입에 의해 정의된 메소드를 호출하지 않습니다.

위 main 메서드에서 
bike02.printDescription() 와
bike03.printDescription()
를 호출하는 코드가 변수 타입에 의해 정의된 메서드를 호출하지 않습니다에 해당됩니다.

 

이 동작을 가상 메소드 호출이라고 하며 Java 언어의 중요한 다형성 기능의 측면을 보여줍니다.

 

Hiding Fields

서브클래스[슈퍼클래스를 상속한]내에서 슈퍼클래스의 필드와 이름이 같은 필드는 타입이 다른 경우[이 슈퍼클래스를 상속한 서브클래스의 객체에서 이 필드를 사용할 경우]에도 슈퍼클래스의 필드를 숨깁니다. 서브클래스 내에서 슈퍼클래스의 필드는 간단한 이름으로 참조될 수 없습니다. 대신 해당 필드는 다음 섹션에서 다루는 super를 통해 액세스해야 합니다. 일반적으로 필드를 숨기면 코드를 읽기 어려워지므로 숨기지 않는 것이 좋습니다.

// 슈퍼클래스를 정의합니다.
class SuperClass {
    int field = 10;
    

    void printField() {
        System.out.println("SuperClass field: " + field);
    }
}

// 서브클래스를 정의합니다.
class SubClass extends SuperClass {
    // 슈퍼클래스의 필드와 이름이 같은 필드를 선언합니다.
    String field = "Hello";

    void printFields() {
        // 서브클래스의 필드를 참조합니다.
        System.out.println("SubClass field: " + field);

        // 슈퍼클래스의 필드를 참조합니다.
        System.out.println("SuperClass field using super: " + super.field);
    }
}

public class FieldHidingDemo {
    public static void main(String[] args) {
        SubClass sub = new SubClass();
        sub.printFields();

        // 슈퍼클래스 메서드를 통해 슈퍼클래스의 필드에 접근합니다.
        sub.printField();
    }
}

 

Using the Keyword super

Accessing Superclass Members

서브클래스의 메서드가 슈퍼클래스의 메서드 중 하나를 오버라이드하는 경우 super 키워드를 사용하여 오버라이드된 메서드를 호출할 수 있습니다. super를 사용하여 숨겨진 필드를 참조할 수도 있습니다(필드를 숨기는 것은 권장되지 않음). 다음 Superclass를 고려해보세요.

public class Superclass {

    public void printMethod() {
        System.out.println("Printed in Superclass.");
    }
}

 

다음은 printMethod()를 오버라이드하는 Subclass라는 하위 클래스입니다.

public class Subclass extends Superclass {

    // overrides printMethod in Superclass
    public void printMethod() {
        super.printMethod();
        System.out.println("Printed in Subclass");
    }
    public static void main(String[] args) {
        Subclass s = new Subclass();
        s.printMethod();    
    }
}

 

Subclass의 main 메소드에서 호출하는 메소드 printMethod()는 Subclass에 선언된 것을 참조하며 Superclass의 printMethod을 오버라이드합니다. 따라서 Superclass에서 상속된 printMethod()를 참조하려면 Subclass는 super를 사용하여 정규화된 이름을 사용해야 합니다. Subclass를 컴파일하고 실행하면 다음이 출력됩니다.

Printed in Superclass.
Printed in Subclass

 

Subclass Constructors

다음 예제에서는 super 키워드를 사용하여 슈퍼클래스의 생성자를 호출하는 방법을 보여줍니다. MountainBike가 Bicycle의 하위 클래스라는 Bicycle 예제를 떠올려 보세요. 다음은 슈퍼클래스 생성자를 호출한 다음 자체 초기화 코드를 추가하는 MountainBike(하위 클래스) 생성자입니다.

public MountainBike(int startHeight, 
                    int startCadence,
                    int startSpeed,
                    int startGear) {
    super(startCadence, startSpeed, startGear);
    seatHeight = startHeight;
}

 

슈퍼클래스 생성자의 호출 코드는 서브클래스 생성자의 첫 번째 라인 코드이어야 합니다.

슈퍼클래스 생성자를 호출하는 구문은 다음과 같습니다.

super();

 

 

또는

super(parameter list);

 

super()를 사용하면 아규먼트가 없는 슈퍼클래스 생성자가 호출됩니다. super(파라미터 리스트)을 사용하면 파라미터 리스트가 일치하는 슈퍼클래스 생성자가 호출됩니다.


참고: 생성자가 슈퍼클래스 생성자를 명시적으로 호출하지 않는 경우 Java 컴파일러는 자동으로 슈퍼클래스의 디폴트 생성자에 대한 호출을 삽입합니다. 슈퍼 클래스에 디폴트 생성자가 없으면 컴파일 시간 오류가 발생합니다. Object에는 그러한 생성자가 있으므로 Object가 유일한 슈퍼클래스인 경우에는 해당 문제가 발생하지 않습니다.


하위 클래스 생성자가 명시적으로든 암시적으로든 상위 클래스의 생성자를 호출하는 경우 Object의 생성자까지 호출되는 전체 생성자 체인이 있다고 생각할 수 있습니다. 실제로 이것이 사실입니다. 이를 생성자 체이닝(Constructor Chaining)이라고 하는데, 클래스 상속 계층이 깊은 경우, 주의가 필요합니다.

 

Object as a Superclass

java.lang 패키지의 Object 클래스는 클래스 계층 구조 트리의 맨 위에 위치합니다. 모든 클래스는 Object 클래스의 직접적이든 간접적이든 자손입니다. 여러분이 사용하거나 작성하는 모든 클래스는 Object의 인스턴스 메서드를 상속합니다. 이러한 메서드를 사용할 필요는 없지만 사용하기로 선택한 경우, 해당 클래스에 특정한 코드로 Object 메서드를 오버라이드해야 할 수도 있습니다. 이 섹션에서 설명하는 Object에서 상속된 메서드는 다음과 같습니다.

 

  • protected Object clone() throws CloneNotSupportedException
          해당 객체의 복사본을 만들고 리턴합니다.
  • public boolean equals(Object obj)
          다른 객체가 해당 객체와 "동일"한지 여부를 나타냅니다.
  • protected void finalize() throws Throwable
          가비지 컬렉션에서 해당 객체에 대한 참조가 더 이상 없다고 판단할 때 객체에 대한 가비지 컬렉터에 의해 호출됩니다.
  • public final Class getClass()
          해당 객체의 런타임 클래스를 리턴합니다.
  • public int hashCode()
          해당 객체의 해시 코드를 리턴합니다.
  • public String toString()
          해당 객체의 string representation를 리턴합니다.

Object의 inform, informAll 및 wait 메소드는 모두 프로그램에서 독립적으로 실행되는 스레드의 활동을 동기화하는 역할을 합니다. 이에 대해서는 이후 단원에서 설명하고 여기서는 다루지 않습니다. 다음과 같은 5가지 방법이 있습니다.

  • public final void notify()
  • public final void notifyAll()
  • public final void wait()
  • public final void wait(long timeout)
  • public final void wait(long timeout, int nanos)

참고: 이러한 여러 방법, 특히 clone 방법에는 몇 가지 미묘한 측면이 있습니다.


 

The clone() Method

클래스 또는 슈퍼클래스 중 하나가 Cloneable 인터페이스를 구현하는 경우 clone() 메서드를 사용하여 기존 객체에서 복사본을 만들 수 있습니다. 복제본을 생성하려면 다음을 작성합니다.

aCloneableObject.clone();

 

이 clone 메소드에 대한 객체의 구현은 clone()이 호출된 객체가 Cloneable 인터페이스를 구현하는지 여부를 확인합니다. 객체가 그렇지 않은 경우 메서드는 CloneNotSupportedException 예외를 발생시킵니다. 예외 처리는 이후 강의에서 다루겠습니다. 지금은 clone()이 다음과 같이 선언되어야 한다는 점을 알아야 합니다.

protected Object clone() throws CloneNotSupportedException

 

또는

public Object clone() throws CloneNotSupportedException

 

Object의 clone() 메소드를 오버라이드하기 위해 clone() 메소드를 작성하려는 경우.

 

clone()이(가) 호출된 객체가 Cloneable 인터페이스를 구현하는 경우 객체의 clone() 메서드 구현은 원래 객체와 동일한 클래스의 객체를 만들고 새 객체의 구성원 변수가 원래 객체의 해당 구성원 변수와 동일한 값을 갖도록 초기화합니다.

 

클래스를 복제 가능하게 만드는 가장 간단한 방법은 클래스 선언에 Cloneable 구현을 추가하는 것입니다. 그러면 객체가 clone() 메서드를 호출할 수 있습니다.

class Person implements Cloneable {
    String name;
    int age;

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

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + "}";
    }
}

public class CloneDemo {
    public static void main(String[] args) {
        try {
            Person original = new Person("John", 30);
            Person clone = (Person) original.clone();

            System.out.println("Original: " + original);
            System.out.println("Clone: " + clone);
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }
}

 

일부 클래스의 경우 Object의 clone() 메서드의 디폴트 동작이 제대로 작동합니다. 그러나 객체에 ObjExternal과 같은 외부 객체에 대한 참조가 포함되어 있는 경우 올바른 동작을 얻으려면 clone()을 오버라이드해야 할 수도 있습니다. 그렇지 않으면 한 객체에 의해 발생한 ObjExternal의 변경 사항이 해당 복제본에도 표시됩니다. 즉, 원본 객체와 해당 복제본은 독립적이지 않습니다. 이를 분리하려면 객체와 ObjExternal을 복제하도록 clone()을 오버라이드해야 합니다. 그런 다음 원본 객체는 ObjExternal을 참조하고 복제본은 ObjExternal의 복제본을 참조하므로 객체와 해당 복제본은 실제로 독립적입니다.

class Address implements Cloneable {
    String city;
    String street;

    Address(String city, String street) {
        this.city = city;
        this.street = street;
    }    

    public String getCity() {
		return city;
	}

	public void setCity(String city) {
		this.city = city;
	}

	public String getStreet() {
		return street;
	}

	public void setStreet(String street) {
		this.street = street;
	}

	@Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    @Override
    public String toString() {
        return "Address{city='" + city + "', street='" + street + "'}";
    }
}

class Person implements Cloneable {
    String name;
    int age;
    Address address;

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

    public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getAge() {
		return age;
	}


	public void setAge(int age) {
		this.age = age;
	}

	public Address getAddress() {
		return address;
	}

	public void setAddress(Address address) {
		this.address = address;
	}
	
	public void setAddress(String city, String street) {
		address.setCity(city);
		address.setStreet(street);
	}

	@Override
    protected Object clone() throws CloneNotSupportedException {
		// return super.clone(); // shallow copy
		Person cloned = (Person) super.clone();
		// deep copy를 위해 Address 객체도 복제
        cloned.address = (Address) address.clone(); 
        return cloned;
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + "}";
    }
}

public class DeepCloneDemo {
    public static void main(String[] args) {
        try {
            String city = "Deagu";
			String street = "Dongu";
			Address address = new Address(city, street);
		
			Person original = new Person("John", 30,address);
		
			Person clone = (Person) original.clone(); 
		
			city = "Busan";
			street = "Haeundaegu";
			original.setAddress(city, street);      
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }
}

 

The equals() Method

equals() 메소드는 두 객체가 같은지 비교하고 같으면 true를 반환합니다. Object 클래스에 제공되는 equals() 메서드는 항등 연산자(==)를 사용하여 두 객체가 동일한지 여부를 확인합니다. 기본 데이터 유형의 경우 이는 올바른 결과를 제공합니다. 그러나 객체의 경우에는 그렇지 않습니다. Object에서 제공하는 equals() 메서드는 객체 참조가 동일한지, 즉 비교되는 객체가 정확히 동일한 객체인지 테스트합니다.두 객체가 동등성[equivalency](동일한 정보 포함) 측면에서 동일한지 테스트하려면 equals() 메서드를 재정의해야 합니다. 다음은 equals()를 재정의하는 Book 클래스의 예입니다.

public class Book {
    String ISBN;
    
    public Book() {}
    
    public Book(String isbn) {
    	ISBN = isbn;
    }
    
    public String getISBN() { 
        return ISBN;
    }
    
    public boolean equals(Object obj) {
        if (obj instanceof Book)
            return ISBN.equals(((Book)obj).getISBN()); 
        else
            return false;
    }
}

 

Book 클래스의 두 인스턴스가 동일한지 테스트하는 다음 코드를 고려해 보세요.

// Swing Tutorial, 2nd edition
Book firstBook  = new Book("0201914670");
Book secondBook = new Book("0201914670");
if (firstBook.equals(secondBook)) {
    System.out.println("objects are equal");
} else {
    System.out.println("objects are not equal");
}

 

이 프로그램은 firstBook과 secondBook이 두 개의 서로 다른 객체를 참조하더라도 객체가 동일하다는 것을 표시합니다. 비교된 객체에는 동일한 ISBN 번호가 포함되어 있으므로 동일한 것으로 간주됩니다.항등 연산자(==)가 클래스에 적합하지 않은 경우 항상 equals() 메서드를 오버라이드해야 합니다.

 

또 다른 예시입니다.

public class Main {
    public static void main(String[] args) {
        // Person 객체 생성
        Person person1 = new Person("John", 25);
        Person person2 = new Person("John", 25);
        Person person3 = new Person("Jane", 30);

        // hashCode 메서드 사용 예제
        System.out.println("HashCode of person1: " + person1.hashCode());
        System.out.println("HashCode of person2: " + person2.hashCode());
        System.out.println("HashCode of person3: " + person3.hashCode());

        // equals 메서드 사용 예제
        System.out.println("person1 equals person2: " + person1.equals(person2)); // true
        System.out.println("person1 equals person3: " + person1.equals(person3)); // false

        // Person 객체를 HashSet에 저장하여 중복 제거 예제
        java.util.HashSet<Person> personSet = new java.util.HashSet<>();
        personSet.add(person1);
        personSet.add(person2);
        personSet.add(person3);

        System.out.println("Number of unique persons: " + personSet.size()); // 2 (person1과 person2는 동일한 객체로 간주)
    }
}

class Person {
    private String name;
    private int age;

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

    // Getters
    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    // Setters
    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + (name != null ? name.hashCode() : 0);
        result = 31 * result + age;
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;

        Person person = (Person) obj;

        if (age != person.age) return false;
        return name != null ? name.equals(person.name) : person.name == null;
    }
}

참고: equals()를 오버라이드하는 경우 hashCode()도 오버라이드해야 합니다.


The finalize() Method

Object 클래스는 객체가 가비지 상태가 될 때 호출될 수 있는 콜백 메서드인 finalize()를 제공합니다. 객체의 finalize() 구현은 아무 작업도 수행하지 않습니다. 즉, finalize()를 오버라이드하여 리소스 해제와 같은 정리 작업을 수행할 수 있습니다.

 

finalize() 메서드는 시스템에 의해 자동으로 호출될 수 있지만 호출 시기나 호출 여부는 불확실합니다. 따라서 정리 작업을 수행하기 위해 이 방법을 사용하지 마십시오. 예를 들어, I/O를 수행한 후 코드에서 파일 디스크립터를 닫지 않고 finalize()가 해당 파일 디스크립터를 닫을 것으로 예상하는 경우, 파일 디스크립터가 부족할 수 있습니다. 대신 try-with resources statement을 사용하여 애플리케이션의 리소스를 자동으로 닫으십시오. Java 플랫폼, Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide의 try-with-resources statement과 마무리 및 weak하고, soft하고, pantom 참조를 참조하세요.

 

The getClass() Method

여러분은 getClass 메소드를 오버라이드할 수 없습니다.

getClass() 메서드는 클래스 이름(getSimpleName()), 슈퍼클래스(getSuperclass()) 및 클래스가 구현하는 인터페이스(getInterfaces())와 같은 클래스에 대한 정보를 가져오는 데 사용할 수 있는 메서드가 있는 Class 객체를 반환합니다. 예를 들어, 다음 메소드는 객체의 클래스 이름을 가져오고 표시합니다.

void printClassName(Object obj) {
    System.out.println("The object's" + " class is " +
        obj.getClass().getSimpleName());
}

 

java.lang 패키지의 Class 클래스에는 수많은 메소드(50개 이상)가 있습니다. 예를 들어 클래스가 어노테이션(isAnnotation()), 인터페이스(isInterface()) 또는 열거형(isEnum())인지 확인하기 위해 테스트할 수 있습니다. 객체의 필드(getFields()) 또는 해당 메소드(getMethods()) 등을 확인할 수 있습니다.

 

The hashCode() Method

hashCode()에 의해 반환되는 값은 객체의 해시 코드이며, 이는 해싱 알고리즘에 의해 생성된 정수 값입니다.

정의에 따르면 두 객체가 동일하면 해당 해시 코드도 동일해야 합니다. equals() 메서드를 오버라이드하면 두 객체가 동일시되는 방식이 변경되고 Object의 hashCode() 구현이 더 이상 유효하지 않게 됩니다. 따라서 equals() 메서드를 오버라이드하는 경우 hashCode() 메서드도 오버라이드해야 합니다.

 

The toString() Method

항상 클래스에서 toString() 메서드를 오버라이드는 것을 고려해야 합니다.

Object의 toString() 메소드는 디버깅에 매우 유용한 객체의 문자열 표현을 리턴합니다. 객체의 문자열 표현은 전적으로 객체에 따라 달라지므로 클래스에서 toString()을 오버라이드해야 합니다.

System.out.println()과 함께 toString()을 사용하여 Book 인스턴스와 같은 객체의 텍스트 표현을 표시할 수 있습니다.

System.out.println(firstBook.toString());

 

적절하게 오버라이드된 toString() 메서드의 경우 다음과 같이 유용한 내용을 출력합니다.

ISBN: 0201914670; The Swing Tutorial; A Guide to Constructing GUIs, 2nd Edition

 

또 다른 예시입니다

toString 메서드를 오버라이드하는 코드를 추가하여 Person 클래스의 객체를 문자열로 표현할 수 있도록 하겠습니다. 이를 통해 객체의 상태를 쉽게 확인할 수 있습니다.

public class Main {
    public static void main(String[] args) {
        // Person 객체 생성
        Person person1 = new Person("John", 25);
        Person person2 = new Person("John", 25);
        Person person3 = new Person("Jane", 30);

        // hashCode 메서드 사용 예제
        System.out.println("HashCode of person1: " + person1.hashCode());
        System.out.println("HashCode of person2: " + person2.hashCode());
        System.out.println("HashCode of person3: " + person3.hashCode());

        // equals 메서드 사용 예제
        System.out.println("person1 equals person2: " + person1.equals(person2)); // true
        System.out.println("person1 equals person3: " + person1.equals(person3)); // false

        // toString 메서드 사용 예제
        System.out.println("person1: " + person1.toString());
        System.out.println("person2: " + person2.toString());
        System.out.println("person3: " + person3.toString());

        // Person 객체를 HashSet에 저장하여 중복 제거 예제
        java.util.HashSet<Person> personSet = new java.util.HashSet<>();
        personSet.add(person1);
        personSet.add(person2);
        personSet.add(person3);

        System.out.println("Number of unique persons: " + personSet.size()); // 2 (person1과 person2는 동일한 객체로 간주)
    }
}

class Person {
    private String name;
    private int age;

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

    // Getters
    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    // Setters
    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + (name != null ? name.hashCode() : 0);
        result = 31 * result + age;
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;

        Person person = (Person) obj;

        if (age != person.age) return false;
        return name != null ? name.equals(person.name) : person.name == null;
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + '}';
    }
}

 

이 코드는 다음을 추가 및 수정했습니다:

1. Person 클래스에 toString 메서드를 오버라이드하여 객체의 문자열 표현을 정의했습니다.
2. main 메서드에서 toString 메서드를 호출하여 각 객체의 문자열 표현을 출력했습니다.

이제 Person 객체를 System.out.println으로 출력하면 객체의 상태를 쉽게 확인할 수 있습니다. 예를 들어, person1 객체는 Person{name='John', age=25}로 출력됩니다.

 

Writing Final Classes and Methods

클래스의 메소드 중 일부 또는 전부를 final로 선언할 수 있습니다. 메서드 선언에서 final 키워드를 사용하여 메서드가 하위 클래스로 오버라이드될 수 없음을 나타냅니다. Object 클래스가 이 작업을 수행합니다. 해당 메서드 중 상당수는 최종 메서드입니다.

변경해서는 안 되는 구현이 있고 객체의 일관된 상태에 중요한 경우 메소드를 final으로 만들 수 있습니다. 예를 들어, 이 ChessAlgorithm 클래스의 getFirstPlayer 메소드를 final로 만들 수 있습니다.

class ChessAlgorithm {
    enum ChessPlayer { WHITE, BLACK }
    ...
    final ChessPlayer getFirstPlayer() {
        return ChessPlayer.WHITE;
    }
    ...
}

 

생성자에서 호출된 메서드는 일반적으로 final로 선언되어야 합니다. 생성자가 final이 아닌 메서드를 호출하는 경우 하위 클래스는 놀랍거나 바람직하지 않은 결과로 해당 메서드를 오버라이드할 수 있습니다.

 

자바에서 생성자에서 호출된 메서드를 final로 선언해야 하는 이유는, 하위 클래스에서 해당 메서드를 오버라이드할 경우 예상치 못한 결과가 발생할 수 있기 때문입니다. 다음은 이러한 개념을 설명하는 샘플 코드입니다.

 

잘못된 예제 (final 메서드가 아닌 경우)

class BaseClass {
    public BaseClass() {
        method();  // 생성자에서 메서드 호출
    }

    public void method() {
        System.out.println("BaseClass method");
    }
}

class SubClass extends BaseClass {
    private String message;

    public SubClass() {
        this.message = "Hello from SubClass";
    }

    @Override
    public void method() {
        System.out.println(message);  // message가 초기화되지 않음
    }
}

public class Main {
    public static void main(String[] args) {
        SubClass subClass = new SubClass();
    }
}

 

위 코드에서 SubClass의 인스턴스를 생성할 때 BaseClass의 생성자가 먼저 호출되면서 SubClass의 method()가 호출됩니다. 하지만 이 시점에는 SubClass의 생성자가 완료되지 않아 message가 초기화되지 않았기 때문에 method()null을 출력합니다.

 

수정된 예제 (final 메서드로 선언한 경우)

class BaseClass {
    public BaseClass() {
        method();  // 생성자에서 메서드 호출
    }

    public final void method() {
        actualMethod();
    }

    protected void actualMethod() {
        System.out.println("BaseClass method");
    }
}

class SubClass extends BaseClass {
    private String message;

    public SubClass() {
        this.message = "Hello from SubClass";
    }

    @Override
    protected void actualMethod() {
        System.out.println(message);  // message가 초기화됨
    }
}

public class Main {
    public static void main(String[] args) {
        SubClass subClass = new SubClass();
    }
}

 

이 예제에서 BaseClass의 method()는 final로 선언되어 있으며, actualMethod()라는 새로운 메서드를 추가하여 이를 SubClass에서 오버라이드합니다. 이제 BaseClass의 생성자에서 호출되는 method()는 오버라이드될 수 없으며, 대신 actualMethod()가 호출되므로 하위 클래스의 예상치 못한 동작을 방지할 수 있습니다.

이 방식은 생성자에서 호출되는 메서드가 final로 선언되어 안전하게 사용할 수 있게 합니다.

 

참고: 전체 클래스를 final로 선언할 수도 있습니다. final로 선언된 클래스는 하위 클래스로 확장할 수 없습니다. 이는 예를 들어 String 클래스와 같은 불변[immutable] 클래스를 생성할 때 특히 유용합니다.

 

Abstract Methods and Classes

추상 클래스는 abstract로 선언된 클래스입니다. abstract 메서드가 포함될 수도 있고 포함되지 않을 수도 있습니다. 추상 클래스는 인스턴스화할 수 없지-익명 클래스로 인스턴스화할 수 있음-만 하위 클래스화할 수는 있습니다.

abstract 메서드는 다음과 같이 구현 없이(중괄호 없이 세미콜론 뒤에) 선언되는 메서드입니다.

abstract void moveTo(double deltaX, double deltaY);

 

클래스에 추상 메서드가 포함된 경우 다음과 같이 클래스 자체를 추상으로 선언해야 합니다.

public abstract class GraphicObject {
   // declare fields
   // declare nonabstract methods
   abstract void draw();
}

 

추상 클래스가 하위 클래스로 확장되면 하위 클래스는 일반적으로 상위 클래스의 모든 추상 메서드에 대한 구현을 제공합니다. 그러나 그렇지 않은 경우 하위 클래스도 추상으로 선언해야 합니다.


참고: default 또는 static으로 선언되지 않은 인터페이스(인터페이스 섹션 참조)의 메서드는 암시적으로 추상이므로 abstract 수정자는 인터페이스 메서드와 함께 사용되지 않습니다. (사용 가능하지만 불필요합니다.)


public class Main {
    public static void main(String[] args) {
        
            GraphicObject aObject = new GraphicObject() {
                @Override
                void draw() {
                    System.out.println("Implement Abstract Class");
                }
            };

            aObject.draw();        

    }
}

 

추상 메서드가 없는 추상 클래스는 다음과 같은 역할을 할 수 있습니다:

1. 기본 구현 제공: 추상 클래스는 하위 클래스들이 공통으로 사용할 수 있는 기본 구현을 제공할 수 있습니다. 이는 코드의 중복을 줄이고, 일관된 동작을 보장하는 데 도움이 됩니다.

2. 공통 인터페이스 정의: 추상 클래스는 하위 클래스들이 반드시 가져야 하는 속성이나 메서드를 정의할 수 있습니다. 이는 하위 클래스들이 일관된 인터페이스를 가지도록 강제할 수 있습니다.

3. 템플릿 역할: 추상 클래스는 하위 클래스들이 상속받아 확장할 수 있는 템플릿 역할을 할 수 있습니다. 하위 클래스는 추상 클래스에서 제공하는 기본 동작을 확장하거나 재정의하여 사용할 수 있습니다.

4. 객체 타입 제한: 추상 클래스는 특정 타입의 객체만을 허용하도록 제한할 수 있습니다. 예를 들어, 추상 클래스의 하위 클래스만이 특정 메서드의 매개변수나 반환 타입으로 사용되도록 할 수 있습니다.

예시

abstract class Vehicle {
    protected String brand;

    public Vehicle(String brand) {
        this.brand = brand;
    }

    public void displayBrand() {
        System.out.println("Brand: " + brand);
    }
}
class Car extends Vehicle {
    public Car(String brand) {
        super(brand);
    }

    public void drive() {
        System.out.println(brand + " car is driving.");
    }
}
class Bike extends Vehicle {
    public Bike(String brand) {
        super(brand);
    }

    public void ride() {
        System.out.println(brand + " bike is riding.");
    }
}
public class Main {
    public static void main(String[] args) {
        Car car = new Car("Toyota");
        Bike bike = new Bike("Harley-Davidson");

        car.displayBrand();  // "Brand: Toyota" 출력
        car.drive();  // "Toyota car is driving." 출력

        bike.displayBrand();  // "Brand: Harley-Davidson" 출력
        bike.ride();  // "Harley-Davidson bike is riding." 출력
    }
}

위 코드에서 Vehicle 클래스는 추상 메서드가 없지만, 공통 속성과 메서드를 정의합니다. CarBike 클래스는 Vehicle 클래스를 상속받아 공통 기능인 displayBrand를 사용하고, 각자의 추가 기능을 정의합니다.

이렇게 하면 코드의 중복을 줄이고, 모든 차량 클래스(Car와 Bike)가 Vehicle의 공통 기능을 가지도록 보장할 수 있습니다. 이를 통해 코드의 유지 보수성과 확장성이 향상됩니다.

 

Abstract Classes Compared to Interfaces

추상 클래스는 인터페이스와 유사합니다. 인스턴스화할 수 없으며 구현 여부에 관계없이 선언된 메서드가 혼합되어 포함될 수 있습니다. 그러나 추상 클래스를 사용하면 static 및 final이 아닌 필드를 선언하고 public, protected 및 private 구체 메소드를 정의할 수 있습니다. 인터페이스를 사용하면 모든 필드가 자동으로 public, static 및 final이 되며 선언하거나 정의하는(디폴트 메서드로) 모든 메서드는 public이 됩니다. 또한 추상 클래스인지 여부에 관계없이 하나의 클래스만 확장할 수 있지만 인터페이스는 원하는 수만큼 구현할 수 있습니다.

추상 클래스와 인터페이스 중 어느 것을 사용해야 합니까?
⦁ 다음 진술 중 하나라도 귀하의 상황에 적용된다면 추상 클래스 사용을 고려하십시오:
   • 밀접하게 관련된 여러 클래스 간에 코드를 공유하려고 합니다.
   • 추상 클래스를 확장하는 클래스에는 많은 공통 메서드나 필드가 있거나 public 이외의 액세스 한정자가 필요할 것으로

     예상합니다(예: protected 및 private).
   • non-static 또는 non-final 필드를 선언하려고 합니다. 이를 통해 자신이 속한 개체의 상태에 액세스하고 수정할 수 있는

     메서드를  정의할 수 있습니다.


⦁ 다음 사항 중 하나라도 귀하의 상황에 해당된다면 인터페이스 사용을 고려하십시오.
   • 관련되지 않은 클래스가 인터페이스를 구현할 것으로 예상합니다. 예를 들어, Comparable 및 Cloneable 인터페이스는

     관련되지 않은 많은 클래스에 의해 구현됩니다.
   • 특정 데이터 유형의 동작을 지정하고 싶지만 해당 동작을 구현하는 사람이 누구인지는 고려하지 않습니다.
   • 타입의 다중 상속을 활용하고 싶습니다.

 

JDK의 추상 클래스 예로는 컬렉션 프레임워크의 일부인 AbstractMap이 있습니다. HashMap, TreeMap 및 ConcurrentHashMap을 포함하는 하위 클래스는 AbstractMap이 정의하는 많은 메서드(get, put, isEmpty, containKey 및 containValue 포함)를 공유합니다.

여러 인터페이스를 구현하는 JDK 클래스의 예로는 Serialized, Cloneable 및 Map<K, V> 인터페이스를 구현하는 HashMap이 있습니다. 이 인터페이스 목록을 읽으면 HashMap의 인스턴스(클래스를 구현한 개발자나 회사에 관계없이)가 복제될 수 있고 직렬화 가능하다는 것을 추론할 수 있습니다(즉, 바이트 스트림으로 변환될 수 있음을 의미합니다. 직렬화 가능 섹션을 참조하세요). 또한 Map<K, V> 인터페이스는 이 인터페이스를 구현한 이전 클래스에서 정의할 필요가 없는 merge 및 forEach와 같은 많은 디폴트 메서드로 향상되었습니다.

많은 소프트웨어 라이브러리는 추상 클래스와 인터페이스를 모두 사용합니다. HashMap 클래스는 여러 인터페이스를 구현하고 추상 클래스 AbstractMap도 확장합니다.

 

An Abstract Class Example

객체 지향 그리기 응용 프로그램에서는 원, 직사각형, 선, 베지어 곡선 및 기타 여러 그래픽 객체를 그릴 수 있습니다. 이러한 객체에는 모두 특정 상태(예: 위치, 방향, 선 색상, 채우기 색상)와 동작(예: moveTo, 회전, 크기 조정, 그리기)이 있습니다. 이러한 상태 및 동작 중 일부는 모든 그래픽 객체에 대해 동일합니다(예: 위치, 채우기 색상 및 moveTo). 다른 경우에는 다른 구현(예: 크기 조정 또는 그리기)이 필요합니다. 모든 GraphicObject는 스스로 그리거나 크기를 조정할 수 있어야 합니다. 그들은 단지 그것을 하는 방법이 다를 뿐입니다. 이는 추상 슈퍼클래스에 대한 완벽한 상황입니다. 다음 그림에 표시된 것처럼 유사점을 활용하고 모든 그래픽 객체가 동일한 추상 상위 객체(예: GraphicObject)에서 상속되도록 선언할 수 있습니다.

 

Classes Rectangle, Line, Bezier, and Circle Inherit from GraphicObject

 

먼저 추상 클래스인 GraphicObject를 선언하여 현재 위치 및 moveTo 메서드와 같이 모든 하위 클래스에서 완전히 공유하는 멤버 변수 및 메서드를 제공합니다. GraphicObject는 또한 모든 하위 클래스에서 구현해야 하지만 다른 방식으로 구현해야 하는 그리기 또는 크기 조정과 같은 메서드에 대한 추상 메서드를 선언합니다. GraphicObject 클래스는 다음과 같습니다.

abstract class GraphicObject {
    int x, y;
    ...
    void moveTo(int newX, int newY) {
        ...
    }
    abstract void draw();
    abstract void resize();
}

 

Circle 및 Rectangle과 같은 GraphicObject의 각 비추상 하위 클래스는 그리기 및 크기 조정 메서드에 대한 구현을 제공해야 합니다.

class Circle extends GraphicObject {
    void draw() {
        ...
    }
    void resize() {
        ...
    }
}
class Rectangle extends GraphicObject {
    void draw() {
        ...
    }
    void resize() {
        ...
    }
}

 

When an Abstract Class Implements an Interface

인터페이스 섹션에서는 인터페이스를 구현하는 클래스가 인터페이스의 모든 메서드를 구현해야 한다는 점을 언급했습니다. 그러나 클래스가 추상 클래스로 선언된 경우 인터페이스의 모든 메서드를 구현하지 않는 클래스를 정의하는 것이 가능합니다. 예를 들어,

abstract class X implements Y {
  // implements all but one method of Y
}

class XX extends X {
  // implements the remaining method in Y
}

 

이 경우 클래스 X는 Y를 완전히 구현하지 않으므로 추상 클래스여야 하지만 실제로 클래스 XX는 Y를 구현합니다.

 

인터페이스와 추상 클래스의 샘플 코드

interface MyInterface {
    void method1();
    void method2();
}

abstract class AbstractClass implements MyInterface {
    // method1만 구현
    @Override
    public void method1() {
        System.out.println("AbstractClass: method1 구현됨");
    }

    // method2는 구현하지 않음
}

class ConcreteClass extends AbstractClass {
    // method2를 구현
    @Override
    public void method2() {
        System.out.println("ConcreteClass: method2 구현됨");
    }
}

public class Main {
    public static void main(String[] args) {
        ConcreteClass concrete = new ConcreteClass();
        concrete.method1(); // "AbstractClass: method1 구현됨" 출력
        concrete.method2(); // "ConcreteClass: method2 구현됨" 출력
    }
}

 

Class Members

추상 클래스에는 정적 필드와 정적 메서드가 있을 수 있습니다. 다른 클래스와 마찬가지로 클래스 참조(예: AbstractClass.staticMethod())와 함께 이러한 정적 멤버를 사용할 수 있습니다.

 

예시코드

1. 수정된 추상 클래스 Animal 정의

abstract class Animal {
    // 인스턴스 필드
    private String name;

    // 스태틱 필드
    private static int animalCount = 0;

    // 생성자
    public Animal(String name) {
        this.name = name;
        animalCount++;
    }

    // Getter for name
    public String getName() {
        return name;
    }

    // Setter for name
    public void setName(String name) {
        this.name = name;
    }

    // 인스턴스 메서드
    public void eat() {
        System.out.println(getName() + " is eating.");
    }

    // 스태틱 메서드
    public static int getAnimalCount() {
        return animalCount;
    }

    // 추상 메서드
    public abstract void makeSound();
}

 

2. 구체 클래스 Dog 정의

class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }

    // 추상 메서드 구현
    @Override
    public void makeSound() {
        System.out.println(getName() + " says Woof!");
    }
}

 

3. 구체 클래스 사용 예제

public class Main {
    public static void main(String[] args) {
        Dog myDog = new Dog("Buddy");
        myDog.eat(); // 인스턴스 메서드 호출
        myDog.makeSound(); // 추상 메서드 호출
        System.out.println("Total animals: " + Animal.getAnimalCount()); // 스태틱 메서드 호출

        // Name 변경 후 테스트
        myDog.setName("Max");
        myDog.eat(); // "Max is eating." 출력
        myDog.makeSound(); // "Max says Woof!" 출력
    }
}