https://sundaland.tistory.com/276
다양성 (Polymorphism)
다형성의 사전적 정의는 유기체나 종의 다양한 형태나 단계를 가질 수 있는 생물학의 원리를 의미한다.
이 원칙은 객체 지향 프로그래밍(OAP) 및 자바 언어와 같은 언어에도 적용이 가능하다.
클래스의 하위 클래스는 고유한 동작을 정의하면서 상위 클래스와 동일한 기능 중 일부를 공유할 수 있다.
다음 코드는 Bicycle 클래스를 약간 수정하여 다형성을 시연한 것이다. 현재 인스턴스에 저장된 모든 데이터를 표시하는 클래스에 printDescription 메서드를 추가했다.
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 + ". ");
}
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 + ". ");
}
}
자바 언어의 다형성 기능을 보여주니 위해 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.");
}
}
RoadBike 클래스에는 타이어 폭을 추적하는 속성(Propert)을 추가한다.
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.");
}
}
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(자바 가성 머신)은 각 변수에서 참조되는 객체에 대한 적절한 메소드를 호출한다.
변수 타입에 의해 정의된 메서드를 호출하지 않는다.!
main 메서드에서 bike02.printDescriptuon()과 bike03.printDescription()을 호출하는 코드는 변수 타입에 의해 정의된 메서드를 호출하지 않는다.
이 동작을 가상 메서드 호출이라고 하며 자바 언어의 중요한 다형성 기능의 측면으 보여준다.
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();
}
}
# Shadow와 Hide는 완전히 다른 개념이다.
슈퍼 키워드 사용
Accessing Superclass Members
하위 클래스의 메서드가 슈퍼 클래스의 메서드 중 하나를 오버라이드하는 경우 super 키워드를 사용하여 오버라이드된 메서드를 호출할 수 있다. super를 사용하여 숨겨진 필드를 참조할 수 있다.
public class Superclass {
public void printMethod() {
System.out.println("Printed in Superclass.");
}
}
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 생성자
아래의 코드는 super 키워드를 사용하여 슈퍼 클래스의 생성자를 호출하는 방법을 보여준다.
public MountainBike(int startHeight,
int startCadence,
int startSpeed,
int startGear) {
super(startCadence, startSpeed, startGear);
seatHeight = startHeight;
}
슈퍼 클래스 생성자의 호출 코드는 서브 클래스 생성자의 첫 번째 라인 코드이어야 한다.
슈퍼 클래스 디폴트 생성자를 호출하는 구문을 다음과 같다.
super();
또는
super(parameter list);
super()를 사용하면 매게변수가 없는 슈퍼 클래스 생성자가 호출된다. super(파라미터 리스트)를 사용하면 파라미터 리스트가 일치하는 슈퍼 클래스 생성자가 호출된다.
# 생성자가 슈퍼 클래스 생성자를 명시적으로 호출하지 않는 경우 자바 컴파일러는 자동으로 슈퍼 클래스의 디폴트 생성자에 대한 호출을 중간 코드(xxx.class)에서 삽입한다. 슈퍼 클래스 디폴트 생성자가 없으면 컴파일 시간 오류가 발생한다. Object에는 그러한 생성자가 있으므로 Object가 유일한 슈퍼 클래스인 경우엔 해당 문제가 없다.
또한 컴파일러는 디폴트 생성자만 생성하는 것 뿐만이 아니라 Dieret 슈퍼 클래스를 호출하는 코드도 생성한다.
하위 클래스 생성자가 명시적으로든 암시적으로든 상위 클래스의 생성자를 호출하는 경우 Object의 생성자까지 호출되는 전체 생성자 체인이 있다고 생각할 수 있다. 이를 생성자 체이닝(Construcotr Chaining)이라고 하는데, 클래스 상속 계층이 깊은 경우, 주의가 필요하다.
Object as a Superclass
- protected Obhect clone() throws CloneNotSupportedException : 해당 객체의 복사복을 만들고 리턴
- public boolean equals(Object obj) : 다른 객체가 해당 객체와 동일한지 여부를 나타낸다.
- protected void finalize() throws Throwable : 가비지 컬랙션에서 해당 객체에 참조가 없다고 판단될땨 객체에 대한 가비지 컬랙터에 의해 호출된다.
- public final Class getClass() : 해당 객체의 런타임 클래스를 리턴한다.
- public int hashCode() : 해당 객체의 해쉬 코드를 리턴한다.
- public Stirng toString() : 해방 객체의 string representation을 리턴한다.
Object의 informm, informAll 및 wait 메서드는 모두 프로그램에서 독립적으로 실행되는 스레드의 활동을 동기화하는 역할을 한다.
- 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() 메서드
클래스 또는 슈퍼 클래스 중 하나가 Cloneable 인터페이스를 구현하는 경우 clone() 메서드를 사용하여 기존 객체에서 복사본을 만들 수 있다.
aCloneableObject.clone();
이 clone 메서드에 대한 객체의 구현은 clone()이 호출된 객체가 Clonealbe 인터페이스를 구현하는지 여부를 확인한다.
객체가 그렇지 않은 경우 메서드는 CloneNotSupprotedException 예외를 발생시킨다.
protected Object clone() throws CloneNotSupportedException
public Object clone() throws CloneNotSupportedException
Object의 clone() 메서드를 오버라이드하기 위해 clone() 메서드를 작성하려는 경우
clone()이 호출된 객체가 클래스의 객체를 만들고 새 객체의 구성원 변수가 원래 객체의 해당 구성원 변수와 동일한 값을 갖도록 초기화한다,
클래스를 복제 가능하게 만드는 가장 간단한 방법은 클래스 선언에 Clonealbe 구현을 추가하는 것으로, 그러면 객체가
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();
}
}
}
# implements Cloneable는 마커 인터페이스로 colne이 사용된다는 것을 알려주는 역할을 한다.
객체에 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();
}
}
}
Deep Copy와 Shallow Copy 두 종류로 나뉘어진다. Shallow Copy(얇은 카피)는 참조하고 있는 주소 값만 복사하기 때문에 값이 같습니다. Deep Copy(깊은 복사)는 실제 값을 메모리 공간에 복사한다.
equals() Method
두 객채가 같은지 비교하고 같으면 true를 반환한다. Object 클래스에 제동되는 equals() 메서드는 항등 연산자 (==)를 사용하여 두 객체가 동일한지 여부를 확인한다. 기본 데이터 유형의 경우 이는 올바른 결과를 제공한다.
그러나 객체에선 Object가 제공하는 equals() 메서드는 객체 참조가 동일한지, 즉 비교돠는 객체가 정확이 동일한 객체인지 테스트한다.
두 객체가 동등성(Equivalency) (동일한 정보 포함) 측면에서 동일한지 테스트하려면 equals() 메서드를 재정의해야 한다.
다음은 equals()를 재정의하는 코드의 예시다.
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");
}
두 개의 서로 다른 객체를 참조하더라도 객체가 동일하다는 것을 표시한다. 비교된 객체에는 동일한 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()도 오버라이드해야 한다.
Finalize() 메서드
오브젝트 클래스는 객체가 가비지 상테가 될 때 호출될 수 있는 콜백 메서드인 finalize()를 제공한다.
객체의 finalize() 구현은 아무 작업도 수행하지 않으며 오버라이드하여 리소스 해제와 같은 정리 작업을 수행할 수 있다.
finalize() 메서드는 시스템에 의해 자동으로 호출될 수 있지만 호출 시기나 호출 여부는 불확실하다. 따라서 정리 작업을 수행하기 위해서 이 방법을 사용해서는 안된다.
I/O를 수행한 후 코드에서 파일 디스크립터를 닫지 않고 finalize()가 해당 디스크립터를 닫을 것으로 예상하는 경우, 파일 디스크립터가 부족할 수 있다. 대신 try-with 리소스 명령문을 사용하여 애플리케이션의 리소스를 자동으로 닫아야 한다.
# 자바 플랫폼, 스탠다드 에디션 핫스팟 가상 머신 가비지 컬랙션 터닝 가이드의 try-wirh 리소스 명령문과 마무리 및 weak하고, soft, pantom 참조를 참조하라.
# https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html
# https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/considerations.html#sthref63
getClass() 메서드
getClass 메서드는 오버라이드할 수 없다!!
클래스 이름(getSimpleName), 슈퍼 클래스 (getSuperClass) 및 클래스가 구현하는 인터페이스(getInterface)와 같은 클래스에 대한 정보를 가져오는데 사용할 수 있는 메서드가 있는 Class 객체를 반환한다.
void printClassName(Object obj) {
System.out.println("The object's" + " class is " +
obj.getClass().getSimpleName());
}
위 코드는 객체의 클래스 이름을 가져오고 표시하는 코드이다.
java.lang 패키지의 Class 클래스에는 50개 이상의 메서드가 있다. 예를 들어 클래스가 어노테이션(isAnnotation), 인터페이스(isInterface) 또는 열거형(inEnum) 인지 확인하기 위해 사용할 수 있다. 객체의 필드(getFields) 또는 해당 메서드(getMethods) 등을 확인할 수 있다.
hashCode() 메서드
hashCode()에 의해 반환되는 값은 객체의 해시 코드이며, 이는 해싱 알고리즘에 의해 생성된 정수 값이다.
정의에 의하면 두 객체가 동일하면 해당 해시 코드도 동일해야 한다. equals() 메서드를 오버라이드하면 두 객체가 동일시되는 방식이 변경되고 Object의 hashCode() 구현이 더 이상 유효하지 않게 된다.
그렇기에 equals() 메서드를 오버라이드하는 경우 hashCode() 메서드도 오버라이드 해야한다.
toString() 메서드
항상 클래스에서 toString() 메서드를 오버라이드 하는 것을 고려해야 한다.
Object의 toString() 메서드는 디버깅에 매우 유용한 객체의 문자열 표현을 리턴한다. 객체의 문자열 표현은 전적으로 객체에 따라 달라지므로 클래스에서 toString()을 오버라이드해야 한다.
System.out.println()과 함께 사용하여 Book 인스턴스와 같은 객체의 텍스트 표현을 표시할 수 있다.
System.out.println(firstBook.toString());
ISBN: 0201914670; The Swing Tutorial; A Guide to Constructing GUIs, 2nd Edition
적절하게 오버라이드된 toString() 메서드의 경우 다음과 같이 유용한 내용을 출력한다.
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 + '}';
}
}
Person 클래스에 toString 메서드를 오버라이드하여 객체의 문자열 표현을 정의했다.
main 메서드에서 toString 메서드를 호출하여 각 객체의 문자열 표현을 출력했다.
personl 객체는 Person { name = 'John' age = 25 }로 출력된다.
Final Classes and Methods
클래스의 메서드 중 일부 또는 전부를 final로 선언할 수 있다. 메서드 선언에서 final 키워드를 사용하여 메서드가 하위 클래스를 오버라이드될 수 없음을 나타낸다.
Object 클래스가 이 작업을 수행하며 해당 메서드 중 상당수는 최종 메서드이다.
변경해서는 안되눈 구현이 있고 객체의 일관된 상태에 중요한 경우 메서드를 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을 출력한다.
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 Class
추상 클래스는 abstract로 선언된 클래스로 abstaract 메서드 포함될 수도 있고 포함되지 않을 수도 있다. 추상 클래스는 인스턴화 할 수 없지만 하위 클래스화 할 수는 있다. (익명 클래스로 인스턴스화할 수 있다.)
abstract void moveTo(double deltaX, double deltaY);
abstarct 메서드는 위의 예제처럼 구현없이 선언되는 메서드이다.
클래스에 추상 메서드가 포함된 경우 다음과 같이 클래스 자체를 추상으로 선언해야 한다.
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();
}
추상 메서드가 없는 추상 클래스를 다음과 같은 역할이 가능하다.
- 기본 구형 제공: 추상 클래스는 하위 클래스들이 공통으로 사용할 수 있는 기본 구현을 제공할 수 있다. 이는 코드의 중복을 줄이고, 일괄된 동작을 보장하는데 도움이 된다.
- 공통 인터페이스 정의: 추상 클래스는 하위 클래스들이 반드시 가져야 하는 속성이나 메서드를 정의할 수 있다.
- 템플릿 역할: 추상 클래스는 하위 클래스들이 상속받아 확장할 수 있는 템플릿 역할을 할수 있다. 하위 클래스는 추상 클래스에서 제공하는 기본동작을 확장하거나 재정의하여 사용할 수 있다.
- 객체 타입 제한: 추상 클래스는 특정 타입의 객체만을 허용하도록 제한할 수 있다. 예를 들어 추상 클래스의 하위 클래스만이 특정 메서드의 매게 변수나 반환 타입으로 사용되도록 할 수 있다.
아래의 코드들은 예시이다.
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 클래스에는 추상 메서드가 없지만, 공통 속성과 메서드를 정의한다. Car와 Bike 클래스는 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의 추상 클래스 예로는 컬렉션 프레임워크의 일부인 AbstarctMap이 있다. HashMap, TreeMap 및 ConcurrentHashMap을 포함하는 하위 클래스는 AbstarctMap이 정의하는 많은 메서드를 공유한다.
# get, put, isEmpty, containKey 및 containValue 포함
여러 인터페이스를 구현하는 JDK 클래스의 예로는 Serialized, Cloneable 및 Map<K, V> 인터스페이스를 구현하는 HashMap이 있다. 이 인터페이스 목록을 읽으면 HashMap의 인스턴스(클래스를 구현한 개발자나 회사에 관계없이)가 복제될 수 있고 직렬화 가능하다는 것을 추론할 수 있다. (바이트 스트림으로 변환될 수 있다.)
또한 Map<K, V> ㅣ인터페이스는 이 인터페이스를 구현한 이전 클래스에서 정의할 필요가 없는 merge 및 forEach와 같은 많은 디폴트 메서드로 향상되었다.
많은 소프트웨어 라이브러리는 추상 크랠스와 인터페이스 모두 사용한다. HashMap 클래스는 여러 인터페이스를 구현하고 추상 클래스 AbstarctMap도 확장한다.
추상 클래스 예제
객체 지향 그리그 응용 프로그램에서 여러 그래픽 객체를 그릴수 있다. 이러한 객체에는 모두 특정 상태와 동장이 있다. 이러한 상태 및 동작 중 일부는 모든 그래픽 객체에 대해 동일하다.
다른 경우에는 다른 구현이 필요하다, 모든 GrapicObject는 스스로 그리거나 크기를 조정할 수 있어야 한다.
abstract class GraphicObject {
int x, y;
...
void moveTo(int newX, int newY) {
...
}
abstract void draw();
abstract void resize();
}
추상 클래스인 GraphicObject를 선언하여 현재 위치 및 MoveTo 메서드와 같이 모든 하위 클래스에서 완전히 공유하는 멤버 변수 및 메서드를 제공한다. GrapicObject는 또한 모든 하위 클래스에서 구현해야 하지만 다른 방식으로 구현해야하는 그리기 또는 킈기 조정과 같은 메서드에 대한 추상 메서드를 선언한다.
class Circle extends GraphicObject {
void draw() {
...
}
void resize() {
...
}
}
class Rectangle extends GraphicObject {
void draw() {
...
}
void resize() {
...
}
}
각 비추상 하위 클래스는 그리기 및 크기 조정 메서드에 대한 구현을 제공해야 한다.
추상 클래스가 인터페이스를 언제 구현하는가?
인터페이스를 구현하는 클래스가 인터페이스의 모든 메서드를 구현해야 하지만, 클래스가 추상 클래스로 선언된 경우 인터페이스의 모든 메서드를 구현하지 않는 크래스를 정의하는 것이 가능하다.
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 구현됨" 출력
}
}
클래스 멤버
추상 클래스네은 정적 필드와 정적 메서드가 있을 수 있다. 다른 클래스와 마찬가지로 클래스 참조와 함께 이러한 정적 멤버를 사용할 수 있다.
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!" 출력
}
}
'자바 튜토리얼' 카테고리의 다른 글
제너릭 (Generics) [2] (0) | 2024.07.16 |
---|---|
제너릭 (Generics) [1] (0) | 2024.07.16 |
리팩토링 (Refactoring) (0) | 2024.07.11 |
인터페이스와 상속 (Interface and Inheritance) [1] (0) | 2024.07.08 |
인터페이스 (Interface) (0) | 2024.07.08 |