『자바의 신 3판』 을 읽고 내용 정리 및 공부한 내용을 정리한 글입니다.
서적: 자바의 신 3판 구입처
상속은 부모 클래스에 선언되어 있는 public 및 protected로 선언되어 있는 모든 변수와 메소드를 내가 갖고 있는 것처럼 사용할 수 있는 것이다.
다른 패키지에 선언된 부모 클래스의 접근 제어자가 없거나(package-private) private으로 선언된 것들은 자식 클래스에서 사용할 수 없다.
extends라는 자바의 예약어를 사용해서 그 뒤에 지정한 클래스를 상속받는다.
public class Parent {}
위와 같은 클래스를 Child 클래스가 상속받는다면, 아래처럼 사용한다.
public class Child extends Parent{}
확장을 한 클래스(자식 클래스)가 생성자를 호출하면,
1. 자동으로 부모 클래스의 기본 생성자가 호출된다.
2. 그 다음 자식 클래스의 생성자에 있는 내용들이 수행된다.
부모 클래스가 갖고 있는 변수와 메소드를 상속받음으로써, 개발할 때 이중, 삼중의 일을 안 해도 된다. 하나의 클래스를 잘 만들어 놓은 게 있으면, 그 클래스를 상속받아 추가적인 기능을 넣을 수 있다.
확장한 클래스는 추가적인 메소드를 만들어도 전혀 문제가 없다. 만약 여러 클래스에 공통된 기능 Print()라는 메소드를 만들었는데, 해당 기능에 문제가 있어 수정할 시 선언한 모든 클래스를 수정해야 하는 상황이 일어난다.
이럴 경우 상속을 사용하면 부모 클래스에 Print()를 선언하고, 해당 메소드만 수정하면 된다.
자식 클래스에서는 모든 생성자가 실행될 때 부모의 기본 생성자를 찾는다.
만약 부모 클래스에 기본 생성자가 없다면, 컴파일 시 자동으로 만들어주므로 문제는 없다.
하지만 부모 클래스에 매개 변수가 있는 생성자를 만들 경우에는 기본 생성자가 없다면 오류가 발생한다.
Implicit super constructor 상위클래스() is undefined. Must explicitly invoke another constructor 오류가 발생한다.
이를 방지하기 위해선 두 가지 방법이 있다.
super()
를 사용한다.super
는 부모 클래스를 가리키는 말로, 메소드처럼 super()
로 사용하면 부모 클래스의 생성자를 호출한다는 것을 의미한다.
자바는 부모의 매개 변수가 없는 기본 생성자를 찾는 것이 기본이다. 그래서, 부모 클래스에 매개 변수가 있는 생성자만 있을 경우에는 super()
를 이용해서 부모 생성자를 꼭 호출해야만 한다.
자식 클래스에서 super()
를 명시적으로 지정하지 않으면 컴파일할 때 자동으로 super()
라는 문장이 들어간다.
이를 이용해 부모 클래스를 고치지 않고, 자식 클래스만 고쳐서 컴파일 및 실행이 정상적으로 수행되도록 하려면 다음과 같이 자식 클래스의 생성자를 변경하면 된다.
super()
는 반드시 자식 클래스의 생성자에서 가장 첫줄에 선언되어야만 한다.
// 기본 생성자가 없는 부모 클래스
public class ParentArg {
public ParentArg(String name) {
System.out.println("ParentArg("+name+") Constructor");
}
public void printName() {
System.out.println("printName() - ParentArg");
}
}
// super()로 부모 생성자를 명시적으로 호출하는 자식 클래스
public class ChildArg extends ParentArg{
public ChildArg() {
super("ChildArg");
System.out.println("Child Constructor");
}
}
여기에서 super("ChildArg")
라고 지정하면, 부모 클래스의 생성자 중 String 타입을 매개 변수로 받을 수 있는 생성자를 찾는다. 그리고 부모 클래스에 String을 매개 변수로 있는 생성자가 있기 때문에 이 생성자가 호출된다.
그런데, 만약 이 생성자처럼 참조 자료형을 매개 변수로 받는 생성자가 하나 더 있을 때 super(null)
이라고 호출하면 컴파일 시 오류가 발생한다.
public class ParentArg {
public ParentArg(String name) {
System.out.println("ParentArg("+name+") Constructor");
}
public ParentArg(InheritancePrint obj) {
System.out.println("ParentArg(InheritancePrint) Constructor");
}
public void printName() {
System.out.println("printName() - ParentArg");
}
}
public class ChildArg extends ParentArg{
public ChildArg() {
super(null);//compile error
System.out.println("Child Constructor");
}
}
error: reference to ParentArg is ambiguous > super(null)
부모 클래스에 참조 자료형인 String 과 InheritancePrint를 매개 변수로 잡는 생성자가 있으므로, null을 넘겨주면 어떤 생성자를 찾아가야 하는지를 자바 컴파일러가 마음대로 정할 수가 없다.
따라서 super()
를 사용할 때에는 null을 넘기는 것보다는 호출하고자 하는 생성자의 기본 타입을 넘겨주는 것이 좋다.
상속에서는 부모의 기능을 자식이 모두 포함한다고 볼 수 있다. (일부 private로 선언된 기능은 제대로 활용할 수 없지만)
상속관계를 보다 유연하기 활용하기 위한 것이 메소드 오버라이딩으로, 부모 클래스에 선언되어 있는 메소드와 동일한 메소드를 자식 클래스에서 선언해서 사용하는 것을 말한다.
자식 클래스에서는 접근 제어자, 리턴 타입, 메소드 이름, 매개 변수 타입 및 개수가 모두 동일해야만 메소드 오버라이딩이라고 부른다.
부모 클래스에 선언되어 있는 메소드와 동일하게 선언되어 있는 메소드를 자식 클래스에 선언하면 자식 클래스의 메소드만 실행된다.
💡 시그니처
시그니처는 메소드 이름과 매개 변수의 타입 및 개수를 의미한다. “동일하게 선언되어 있다”는 말은 “동일한 시그니처 Signature를 가진다”라고 표현할 수 있다.
오버라이딩 시 접근 제어자는 더 큰 접근 권한으로 변경할 수 있다. 다만, 접근 제어자가 더 확대되는 것은 문제가 안되지만 축소되는 것은 문제가 된다.
부모에서 public으로 선언한 것을 자식이 private으로 선언하면 안 된다는 말이다. 부모가 만약 private으로 선언했으면 자식은 어떤 것으로 선언하든지 상관 없다.
public > protected > package-private > private 순으로 접근 권한이 좁아짐.
지금까지 객체를 생성할 때에는 다음과 같이 만들었다.
ParentCasting parent=new ParentCasting();
ChildCasting child=new ChildCasting();
상속 관계가 정립되면 다음과 같이 객체를 생성할 수도 있다.
ParentCasting parent2=child;
그런데, 다음과 같은 객체 생성은 안된다.
ChildCasting child2=parent;//compile error
형 변환이 안되는 이유는 long과 int의 형 변환과 비슷한 이유다.
참조 자료형은 자식 클래스의 타입을 부모 클래스의 타입으로 형 변환하면 부모 클래스에서 호출할 수 있는 메소드들은 자식 클래스에서도 호출할 수 있으므로 전혀 문제가 안된다. 따라서 우리가 명시적으로 해줄 필요가 없다.
하지만 부모 클래스는 자식 클래스에 선언되어 있는 메소드나 변수를 완전히 사용할 수 없다.
따라서, 컴파일 오류를 피하려면 다음과 같이 형 변환을 명시적으로 선언해주어야 한다.
ChildCasting child=new ChildCasting();
ParentCasting parent=child;
ChildCasting child2=(ChildCasting)parent;
parent
는 child
를 대입한 것이고, child
는 ChildCasting
의 객체이다. 그러므로 실제로는 parent
는 ChildCasting
의 객체이기 때문에 ChildCasting
로 형 변환해도 전혀 문제가 없다.
타입을 구분하는 자바의 예약어이다.
instanceof
의 앞에는 객체를, 뒤에는 클래스(타입)를 지정해 주면 된다. 그러면 이 문장은 boolean 형태의 결과를 제공한다. 클래스의 객체이면 true, 아니면 false를 반환한다.
일반적으로 여러 개의 값을 처리하거나, 매개 변수로 값을 전달할 때에는 보통 부모 클래스의 타입으로 보낸다. 이렇게 하지 않으면 배열과 같이 여러 값을 한 번에 보낼 때 각 타입 별로 구분해서 메소드를 만들어야 하는 문제가 생길 수도 있기 때문이다.
이때, instanceof
로 타입을 확인한 후에 명시적으로 형 변환을 하면, 정확하게 타입을 확인할 수 있고 원하는 메소드도 호출할 수 있다.
단, 가장 하위에 있는 자식 타입부터 확인을 해야 제대로 타입 점검이 가능하다. 상위 타입을 넣었을 경우에도 true로 반환하기 때문이다.
public class InheritanceCasting {
public static void main(String args[]) {
InheritanceCasting inheritance = new InheritanceCasting();
inheritance.objectCastArray();
}
public void objectCastArray() {
ParentCasting[] parentArray=new ParentCasting[3];
parentArray[0]=new ChildCasting();
parentArray[1]=new ParentCasting();
parentArray[2]=new ChildCasting();
objectTypeCheck(parentArray);
}
private void objectTypeCheck(ParentCasting[] parentArray) {
for(ParentCasting tempParent:parentArray) {
if(tempParent instanceof ChildCasting) {
System.out.println("ChildCasting");
} else if(tempParent instanceof ParentCasting) {
System.out.println("ParentCasting");
}
}
}
}
어떤 객체의 속성이나 기능이 상황에 따라 여러 가지 형태를 가질 수 있는 성질을 의미한다. 대표적인 예로는 같은 이름의 메서드가 상황에 따라 다른 역할을 수행하는 오버로딩과 오버라이딩이 있다.
책에서는 “형 변환을 하더라도, 실제 호출되는 것은 원래 객체에 있는 메소드가 호출된다”는 것이 바로 다형성이라고 말하고 있다.
책의 예제를 함께 보자. Parent
를 상속받은 Child
와 ChildOther
클래스가 있다.
public class Parent {
public Parent() {
System.out.println("Parent Constructor");
}
public void printName() {
System.out.println("Parent printName()");
}
}
public class Child extends Parent{
public Child() {
System.out.println("Child Constructor");
}
}
public class ChildOther extends Parent{
public ChildOther() {
System.out.println("ChildOther Constructor");
}
public void printName() {
System.out.println("ChildOther printName()");
}
}
public class InheritancePoly {
public static void main(String args[]) {
InheritancePoly inheritance=new InheritancePoly();
inheritance.callPrintNames();
}
public void callPrintNames() {
Parent parent1 = new Parent();
Parent parent2 = new Child();
Parent parent3 = new ChildOther();
parent1.printName();
parent2.printName();
parent3.printName();
}
}
// 1번. Parent parent1 = new Parent();
// 부모의 생성자 호출
Parent Constructor
// 2번. Parent parent2 = new Child();
// 부모 생성자를 호출 후 자식 생성자 호출
Parent Constructor
Child Constructor
// 3번. Parent parent3 = new ChildOther()
// 부모 생성자를 호출 후 자식 생성자 호출
Parent Constructor
ChildOther Constructor
// 4번.parent1.printName();
// 부모의 메소드 호출
Parent printName()
// 5번. parent2.printName();
// 자식에게 printName()이 없으므로 부모의 것을 호출
Parent printName()
// 6번. parent3.printName();
// 자식이 오버라이딩한 printName()를 호출
ChildOther printName()
각 객체의 타입은 모두 Parent
타입으로 선언되어 있는데도 불구하고 printName()
메소드 결과는 상이하다.
다시 말해, 선언 시에는 모두 Parent
타입으로 선언했지만, 실제로 호출된 메소드는 생성자를 사용한 클래스에 있는 것이 호출되었다.
왜냐하면 각 객체의 실제 타입은 다르기 때문이다. 이와 같이 “형 변환을 하더라도, 실제 호출되는 것은 원래 객체에 있는 메소드가 호출된다”는 것이 바로 다형성이다.
super()
를 사용하여 명시적으로 호출할 수 있다.💡 이 요약 내용은 Oracle에서 제공하는 Java tutorial을 참조했습니다.
Me: extends
Me: O
Me: super
Me: 오버로딩은 동일한 시그니처를 가진 메소드를 매개 변수를 다르게 하여 만드는 것이고, 오버라이딩은 상속받은 메소드를 엎어 쓰는 것이라고 할 수 있다.
Me: O
Me: instanceof
Me: 좌측에는 변수 혹은 객체가, 우측에는 클래스(타입) 이름이 들어간다.
Me: boolean
Me: 객체의 속성이나 기능이 상황에 따라 여러가지 형태를 가질 수 있는 성질을 말한다.
💡 책에 있는 내용이 아닙니다.
책을 읽으며 설명이 더 필요하거나, 추가로 궁금한 점에 대해 질문 형식으로 작성 후, 답을 구해보고 있습니다.
참고한 사이트나 영상은 [출처]로 달아두었으며, 오류 지적은 언제나 환영합니다.
책의 설명에 따르면 package-private로 선언된 것은 상속 불가하다고 한다. 실제 접근 제어자의 범위를 보면 맞는 것 같다.
하지만, 실제로 테스트해보면 그렇지 않다.
```java
public class Parent {
private void prvMethod() {
System.out.println("call prvMethod()");
}
void defMethod() {
System.out.println("call defMethod()");
}
public void pubMethod() {
System.out.println("call pubMethod()");
}
}
```
```java
public class Child extends Parent {
public static void main(String[] args) {
Child c = new Child();
}
Child() {
// super.prvMethod();
super.defMethod();
super.pubMethod();
}
}
```
참고 사이트에 따르면
package-private 클래스의 상속은,
1. 같은 패키지 내의 자식 클래스라면, 같은 패키지 내에 있으므로 접근이 가능
2. 다른 패키지 내에 있다면 애초에 자식 클래스로 만들 수조차 없다.
같은 이치로, 상속받을 때 package-private
와 private
으로 선언한 것은 “상속받을 수 없다”라는 말은, “접근 권한이 없다”라고 이해해도 좋을 거 같다.
```java
public class ParentOverriding {
private int intValue = 10;
public ParentOverriding() {
System.out.println("ParentOverriding Constructor");
}
private void privateTest() {
System.out.println("privateTest() - ParentOverriding");
}
public int getInt() {
return intValue;
}
}
```
```java
public class ChildOverriding extends ParentOverriding{
public ChildOverriding() {
System.out.println("ChildOverriding Constructor");
}
public int getIntChild() {
return super.getInt();
}
}
```
```java
public class InheritanceOverriding {
public static void main(String[] args) {
ChildOverriding child=new ChildOverriding();
System.out.println(child.getIntChild());
ParentOverriding par = new ChildOverriding();
System.out.println(par.getInt());
}
}
```
위 결과를 보면, private 메소드는 애초에 접근이 안되므로 확인이 불가능하다. 다만, private 변수는 상속이 된 것인지 확인이 가능했다.
즉, 직접 접근은 안 되지만 자식 객체에 private 변수가 포함되기는 한다. "상속이 안 된다"가 자식 클래스에 포하되지 않는다는 의미와 헷갈렸는데, 포함되기는 한다. 접근이 안될 뿐.
다이아몬드 문제가 생길 수 있기 때문이다.
public class Parent {
public Parent() {
// 최상위 부모 클래스
}
public void printName() {
System.out.println("내가 원조다!");
}
}
그 후, 부모 클래스를 상속받는 자식-딸, 자식-아들 클래스를 만든다.public class Daughter extends Parent {}
public class Son extends Parent {}
그리고 이 둘을 모두 상속받는 조카 클래스를 만든 후 부모의 printName()
를 호출하도록 하자.public class Nephew extends Daughter, Son {
public Nephew() {}
@Override
void printName() {
super.printName();
}
}
printName()
는 부모에게서 상속받았으므로 딸에게도 아들에게도 있다. 그렇다면, 조카가 호출한 super.printName()
의 super는 누구를 말하게 되는 걸까?위와 같이 충돌이 날 수 있으므로, 다중 상속은 금지되고 있다.
테스트해보고 싶은데, 자바에서는 다중상속을 금지시켜서 못하겠다. 따라서, 인터페이스로 다중상속 충돌 구현을 해보겠다.
public interface Mother {
default void print() {
System.out.println("I'm Mohter");
}
}
public interface Father {
default void print() {
System.out.println("I'm Father");
}
}
public class Baby implements Mother, Father {
public static void main(String[] args) {
Baby c = new Baby();
c.print();
}
}
1.접근 제어자, 리턴 타입, 동일한 메소드 시그니처일 경우 충돌 발생
```java
public class Baby implements Mother, Father {
public static void main(String[] args) {
Baby c = new Baby();
}
}
```
2.접근 제어자가 다를 경우 충돌 없음 - 제대로 된 테스트 불가
1) Mother, Father 인터페이스의 print()
메소드를 private으로 변경
오류 없음. 사진 속 오류는 print() 메소드가 private이어서 접근 권한이 없어 발생한 오류이고, 해당 코드를 주석 처리하고 컴파일하면 오류 없이 된다.
2) Father 인터페이스의 print()
메소드를 static으로 변경해보았다.
이 경우엔 오류가 나지 않았다. 그런데, 인터페이스의 Static 메소드는 재정의가 불가능하고 인터페이스에서 직접 호출해야하기 때문에 오류가 나지 않은 것 같다.
```java
public interface Father {
static void print() {
System.out.println("I'm Father");
}
}
```
3.리턴 타입이 다를 경우 충돌 발생
```java
public interface Father {
default String print() {
System.out.println("I'm Father");
return "Father";
}
}
```
이 경우엔, 상속받는 인터페이스의 두 메소드 리턴 타입이 다르다고 오류 표시를 해준다.
```java
public interface Father {
default void print(String name) {
System.out.println("I'm Father" + " " + name);
}
}
```
```java
public class Baby implements Mother, Father {
public static void main(String[] args) {
Baby c = new Baby();
c.print();
c.print("Jim");
}
}
```
메소드 충돌을 피하려면 아래 규칙을 지키면 된다.
자바의 상속 관계에 있는 부모와 자식 클래스 간에는 서로 간의 형 변환이 가능하다. 클래스는 reference 타입으로 분류되니 이를 참조형 캐스팅이라고 부른다. 그리고 참조형 캐스팅에는 업 캐스팅과 다운 캐스팅이 있다.
부모 객체는 자식 객체에 상속을 받고 있으니 더 상위 요소로 판별할 수 있다. 그래서 Up 캐스팅이라고 한다.
반대로 하위 요소인 자식 객체로 형 변환하는 것은 Down 캐스팅이라고 한다.
이처럼 업 캐스팅을 하는 이유는 공통적으로 할 수 있는 부분을 만들어 간단하게 다루기 위해서다.
💡 멤버 개수 감소로 인한 멤버 접근 제한
자식 클래스의 객체는 부모 클래스를 상속하고 있기 때문에 부모의 멤버를 모두 가지고 있지만, 부모 클래스의 객체는 자식 클래스의 멤버를 모두 가지고 있지는 않는다.
따라서 업 캐스팅을 하면 부모 클래스 멤버로 멤버 개수가 한정되기 때문에, 자식 클래스 내에 있는 모든 멤버(메소드 및 멤버 필드)에 접근할 수 없게 된다.
객체를 업 캐스팅하게 되면 자식과 부모의 공통된 것만 사용할 수 있고 자식 클래스에서 새로 만들어진 건 사용할 수 없음을 주의하자.
다운 캐스팅의 진정한 의미는 부모 클래스로 업 캐스팅된 자식 클래스를 복구하여, 본인의 필드와 기능을 회복하기 위해 있는 것이다.
즉, 원래 있던 기능을 회복하기 위해 다운 캐스팅을 하는 것이다.
다운 캐스팅은 곧 사용할 수 있는 객체 멤버 증가를 의미하는데, 실제 참조 변수가 가리키는 객체가 무엇인지 모르기 때문에 어떠한 멤버가 추가 되는지 알 수가 없다.
그래서 반드시 형 변환 괄호를 기재함으로써 증가된 클래스의 멤버가 무엇인지 알게 하도록 개발자한테 알려줘야 한다.
💡 다운캐스팅(downcasting) 예외
다운 캐스팅은 업 캐스팅한 객체를 되돌릴 때 적용 되는 것이지, 오리지널 부모 객체를 자식 객체로 강제 형 변환은 불가능하다.
이는 컴파일 단계에서 잡아주지 않기 때문에 조심해서 사용해야 한다.
조합, 합성. 인터넷 사이트에서는 포함관계라는 이름으로 설명하는 것도 봤다.
한 클래스에서 다른 클래스를 멤버변수로 선언하여 포함시키는 것을 말한다. 단위별로 여러 개의 클래스를 작성하는 것이다.
ex) Circle 클래스 안의 Point 타입 변수
상속은 클래스가 다른 클래스의 필드와 메소드를 물려받아 사용하는 것이다.
장점은 아래와 같다.
단점은 아래와 같다.
조합은 클래스가 다른 클래스의 인스턴스를 포함하거나 참조하여 기능을 확장하는 개념이다.
장점은 아래와 같다.
단점은 아래와 같다.
만약 모호하다면 어떤 것을 우선적으로 고민해야할까?
요약하자면 간단한 관계와 확장이 중요한 경우에는 상속을 사용하고, 낮은 결합도와 유연성이 중요한 경우에는 조합을 사용한다.
보통 상속과 조합을 이야기할 때는 is-a 관계이냐 has-a 관계이냐를 보고, 사용할 경우를 정하기도 한다.
'ia-a'관계는 상속을 통해 표현되며, 'has-a' 관계는 조합을 통해 표현된다. 즉, 명확한 계층을 가졌고 일반화 관계라면 상속을 사용하고 소유의 개념을 가졌다면 조합을 사용한다.
여기서 일반화란 공통된 특성을 추상화하여 하나의 부모 클래스로 표현하는 것이다.
상속이 자손 클래스를 만드는데 조상 클래스를 사용하는 것이라면, 이와 반대로 추상화는 기존의 클래스의 공통부분을 뽑아내서 조상 클래스를 만드는 것이다.
ChatCPT
객체 지향 프로그래밍의 4가지 특징ㅣ추상화, 상속, 다형성, 캡슐화 -