2021 7.27
1.상속
1.1상속의 정의와 장점
상속: 기존의 클래스를 재사용해서 새로운 클래스를 작성하는 것이다.
특징: 적은 양의 코드로 새로운 클래스를 작성할 수 있고,
코드를 공통으로 관리할 수 있어서 코드의 추가 및 변경이 매우 용이하다.
장점: 재사용성을 높임. 코드 중복 제거로 프로그램의 생산성 및 유지보수에 크게 기여한다.
구현방법: 새로 작성하려는 클래스 뒤에 extend 상속받을 클래스이름을 적어주면 된다.
class child extends Parent{
}
조상 클래스: 상속해주는 클래스 (부모 , 상위 , 기반 )
자손 클래스: 상속받는 클래스 (자식 , 하위 , 파생된)
자손 클래스는 조상 클래스의 모든 멤버를 상속받는다.
그래서 자손 클래스가 조상 클래스의 멤버들을 포함한다고 볼 수도 있다.
자손 클래스인 Child 클래스에 Play()메서드가 추가되어도 조상 클래스에는 영향이 없다.
-조상 클래스의 변경만 자손 클래스에 영향을 줄 수 있다.( age를 추가하니 자손 클래스에도 자동으로 멤버 추가된다.)
- 생성자와 초기화 블럭은 상속이 안되고 멤버만 상속된다.
-자손 클래스의 멤버수 >=조상 클래스의 멤버수
하나의 조상 클래스를 2개이상의 자손 클래스가 각각 상속받고 있을 때,
자손 클래스에 공통적으로 추가해야 될 멤버가 있다면 공통 조상 클래스에 추가하는 것이
코드도 적어지고, 유지보수, 중복제거에 도움이 된다.
같은 내용의 코드를 1개 이상 클래스에 중복적으로 추가해야 할 때는
상속을 이용해서 최소화 해야한다.
조상 클래스만 변경해줘도 모든 자손 클래스에 , 자손의 자손 클래스까지 영향을 끼치므로,
클래스 간의 상속관계를 맺어줘서 공통적인 부분은 부모 클래스에서 관리하고, 자손 클래스에서는 자신에 정의된
멤버들만 관리하도록 함으로써 코드를 줄이고 중복을 제거, 재사용성을 높여 관리를 쉽게 한다.
클래스간의 상속관계를 적절히 맺어 주는것은 OOP의 가장 중요한 부분이다.
1.2 클래스간의 관계 - 포함관계
포함관계: 한 클래스의 멤버변수로 다른 클래스 타입의 참조변수를 선언하는 것이다.
클래스를 재사용하는 또다른 방법.
상속과 같은 효과를 얻을 수 있다.
코드가 간결해지고, 재사용성을 높일 수 있다.(작성된 단위 클래스들)
클래스를 재사용하는 방법에는 상속과 포함이 있다.
상속은 새로운 클래스에 extends를 붙여 상속할 클래스를 적는 것이고,
포함은 새로운 클래스에 기존의 클래스타입의 참조변수를 선언하는 것이다.
1.3 클래스간의 관계 설정하기
클래스를 작성할 때 상속을 할지, 포함을 할지 헷갈릴 수 있다.
~은 ~이다 와 ~은 ~을 가지고 있다.를 넣어서 문장을 만들어 보면 클래스 간의 관계가 명확해진다.
원은 점이다. -> 이게 성립하면 상속관계를 맺어주면 된다.
하지만 원은 점이 아니다.
원은 점을 가지고 있다. -> 이게 성립하면 포함관계를 맺어주면 된다.
원은 점을 가지고 있으니 포함관계가 맞겠다.
ex) 원은 도형이다. -> Circle 클래스와 Shape 클래스는 상속으로
원은 점을 가지고 있다. -> Circle클래스와 Point클래스는 포함관계로
1.4 단일 상속
자바는 단일 상속만을 허용한다.
다중상속의 장점 : 여러 클래스로부터 상속받을 수 있어서 복합적인 기능을 가진 클래스를 쉽게 작성 가능
단점: 서로 다른 클래스로부터 상속받은 멤버간의 이름이 같으면 구별할 수 없음
단점을 해결하려면 조상 클래스의 메서드 이름이나 매개변수를 바꾸는 방법밖에 없는데 이러면 그 클래스에
의존적인 다른 클래스들도 영향을 받아서 함부로 할 수는 없다.
그래서 단일 상속을 하는데, 불편한 점도 있지만 클래스간의 관계가 더 명확해지고, 신뢰성이 높다는 장점이 있다.
하나는 상속으로, 하나는 포함으로 받아도 된다.
1.5 Object클래스 - 모든 클래스의 조상
모든 클래스 상속계층도 최상위에 있는 조상 클래스.
아무것도 상속을 받지 않아도 컴파일러가 자동으로 extends Object를 달아준다.
다른 클래스로 상속을 받아도 계속 조상 클래스를 찾아 올라가다 보면 Object클래스가 나온다.
그래서 Object에 정의된 멤버 toString()이나 equals(Object o)등을 따로 메서드를 정의 안해도 사용가능하다.
2.오버라이딩
2.1 오버라이딩이란?
오버라이딩 : 조상클래스에서 상속받은 메서드의 내용을 변경해서 재정의 하는것
상속받은 메서드를 그대로 사용할 수도 있지만 자식 클래스에 맞게 변경해야 할 때 사용한다.
2.2 오버라이딩의 조건
자손 클래스에서 오버라이딩하는 메서드는 조상 클래스의 메서드와
이름, 매개변수 , 반환타입이 같아야 한다.
정확히 구현부만 달라지는 것이다.
선언부가 서로 일치해야 한다.
접근제어자와 예외는 제한된 조건 하에서만 다르게 변경가능하다.
1.접근 제어자는 조상 클래스의 메서드보다 좁은 범위로 변경 할 수 없다.
넓음 -> 좁음
public -> protected,(default),private
즉 조상 클래스 메서드 접근 제어자가 protected라면
자식 클래스에서는 protected와 public 밖에 못쓴다.
2. 조상 클래스의 메서드보다 많은 수의 예외를 선언할 수 없다.
Child의 예외 개수가 더 적으므로 올바르게 오버라이딩 되었다.
class Parent{
void parentMethod() throws IOException, SQLexception{}
}
class Child extends Parent{
void parentMethod() throws IOException{}
}
하지만 단순히 개수의 문제가 아니다.
class Child extends Parent{
void parentMethod() throws Exception{}
}
이렇게 오버라이딩하면 Exception은 모든 예외의 최고 조상이라 가장 많은 개수의
예외를 던질 수 있도록 선언한 거라 실패다.
+인스턴스 메서드를 static메서드 또는 그 반대로 변경할 수 없다.
+ 조상 클래스에서 정의된 static메서드를 똑같은 이름의 static메서드로 정의해도
오버라이딩이 아니라 별개의 메서드를 정의한것이다.
2.3 오버로딩과 오버라이딩
오버로딩: 기존에 있는 메서드와 이름만 같고 매개변수의 타입과 개수가 다른 메서드를 정의하는것
오버라이딩: 조상 클래스에서 상속받은 메서드의 내용(구현부)를 변경해서 정의하는것. (재정의)
class Parent{
void parentMethod(){}
}
class Child extends Parent{
void parentMethod(){} -> 이게 오버라이딩이다. 조상 클래스의 parentMethod()메서드를 새로 변경해서 정의
vdoi parentMethod(int i){} -> 이건 오버로딩이다. parentMethod()와 이름은 같지만 매개변수가 다른 새 메서드를 정의
}
2021 0728 16:34~
2.4 super
super : 자손 클래스에서 조상 클래스로부터 상속받은 멤버를 참조하는데 사용되는 참조 변수이다.
멤버변수와 지역변수 이름이 같을 때 this를 사용한 것처럼
상속받은 멤버와 자신의 클래스에 정의된 멤버이름이 같으면 super로 구별가능하다.
조상클래스의 멤버와 자손 클래스의 멤버가 중복 정의되어 구별이 필요할 때만 사용하는 것이 좋다.
조상클래스와 자손클래스의 멤버구분을 제외하면 this와 super는 근본적으로 같다.
모든인스턴스메서드에서 자신이 속한 인스턴스의 주소가 지역변수로 저장됨 (this, super)
super역시 static메서드에서는 사용할 수 없다. static메서드는 인스턴스와 관련이 없어서 안됨.
현재 Child 클래스는 Parent를 상속받아서 멤버변수 x를 가지고 있는데, this와 super 모두 같은 변수를 가리키고 있어서
10이 나온다.
그러나 Child클래스에 int x가 지역변수로 존재하면 조상 클래스, 자식 클래스에도 같은 이름의 멤버변수가 존재하게 된다.
이때는 this는 현재 메서드의 지역변수 (20) , super는 조상 클래스(Parent)의 멤버변수(10)를 가리킨다.
메서드 역시 super.(메서드명) 으로 호출해서 할 수 있다.
특히 자손 클래스에서 오버라이딩 했을 때 super를 사용한다고 한다
super는 자식클래스에서 조상클래스의 멤버를 호출할 때 사용하며 자식 클래스와 조상 클래스의 멤버변수가 이름이 같아서 구분해야 할 경우 사용한다.
2.5 super() - 조상 클래스의 생성자
조상 클래스의 생성자를 호출하는 명령어.
this()가 같은 클래스의 다른 생성자를 호출하는데 사용된다면
super()는 조상 클래스의 생성자를 호출하는데 사용된다.
Object클래스를 제외한 모든 클래스 생성자 첫줄에 생성자 , this() 또는 super()를 호출해야 한다.
안그러면 알아서 컴파일러가 super();를 생성자의 첫줄에 삽입한다.
왜?
자식 클래스의 인스턴스가 생성되면 자손, 조상의 멤버가 모두 합쳐진 인스턴스가 생성된다.
이 때 자손 클래스의 멤버가 조상 클래스의 멤버를 사용할 수도 있으니 조상의 멤버들이 먼저
초기화되기 위해서 super()의 호출이 필요하다.
코드를 보면 Point3D의 생성자에서 에러가 뜬다. 왜냐면 super()가 없으니 컴파일러에서 자동으로 넣어주게 되는데,
여기서 super();는 상속받은 Point클래스의 생성자 Point();를 의미한다.
그러나 Point();는 Point클래스에 선언되지 않았다. 이미 다른 생성자가 있어서 컴파일러도 자동으로 안끼워 넣어준다.
이 때 해결방법은 2가지다. Point클래스에 Point()생성자도 만들어주던가, 아니면 Point3D(int x, ...)클래스의 맨 위에
Point(x, y);를 넣어주면 된다.
이 에러는 Point();를 못찾아서 만들어진 에러이기 때문이다.
이처럼 조상 클래스의 멤버변수는 조상의 생성자에 의해 초기화 되어야 한다.
생성자의 이동 흐름
this(100,200,300);-> Point3D(int x, int y, int z)->super(x,y)-> Point(int x, int y ) -> super()-> Object()
3. package와 import
3.1 패키지
패키지는 클래스의 묶음이다.
같은 이름의 클래스도 다른 패키지에 존재하는 것이 가능하다.
클래스의 실제 이름은 패키지명을 포함한 것이다.
ex: java.lang.String
-하나의 소스파일에는 첫번째 문장으로 단 한번의 패키지 선언만 허용함.
-모든 클래스는 반드시 하나의 패키지에 속해야 한다.
-패키지는 .을 구분자로 해서 계층구조 구성이 가능하다.
-패키지는 물리적으로 클래스파일(.class)를 포함하는 하나의 디렉토리이다.
3.2 패키지의 선언
package 패키지명;
소문자로 하는걸 원칙으로 한다.
패키지를 지정안하고 클래스를 만들면 이름없는 패키지로 자동으로 제공해준다.
3.3 import문
소스코드 작성시 다른 패키지의 클래스를 사용하려면 패키지명이 포함된 클래스 명을 사용해야 한다.
이때 import로 사용하려는 클래스의 패키지를 미리 명시하면 생략이 가능해져서 편리하다.
import문의 역할은 컴파일러에게 소스파일에서 사용된 클래스의 패키지에 대한 정보를 제공하는 것이다.
3.4 import문의 선언
import 패키지명.클래스명;
or
import 패키지명.*; -> 한패키지에서 여러 클래스를 사용할 때 사용. 이 패키지의 모든 클래스를 패키지 명없이 사용가능
으로 활용한다.
*활용의 단점: 패키지수가 많으면 어느클래스가 어떤 패키지에 속하는지 알기 어려움
*를 사용하는게 하위 패키지의 클래스까지 포함하는 것은 아니다.
java.lang = System이나 String같은 클래스를 담고 있는 중요한 패키지라 자동으로 선언됨.
3.5 static import문
static 멤버를 호출할 때 클래스 이름을 생략할 수 있다.
특정 클래스의 static멤버를 자주 사용할 때 편리하다.
ex:
import static java.lang.Integer.*;
import static java.lang.Math.random;
import static java.lang.System.out;
System.out.println(Math.random()); - > out.println(random() ) ;
4. 제어자
4.1 제어자란?
클래스, 변수 또는 메서드 선언부에 함께 사용되어 부가적인 의미를 부여하는것.
접근 제어자 : public ,protected ,default ,private
그 외 : static, final , abstract , native , transient , synchronized , volatile ,strictfp
제어자는 클래스나 멤버변수, 메서드에 주로 사용되고 하나의 대상에 대해 여러 제어자를 조합해서 사용하는 것이
가능하다.
단, 접근 제어자는 4개중 1개만 선택해서 사용할 수 있다.
접근 제어자를 제일 왼쪽에 두는 경향이 있다.
4.2 static - 클래스의, 공통적인
클래스 변수 (static멤버변수)는 인스턴스 변수와는 다르게 같은 값을 가진다.
왜?
하나의 변수를 모든 인스턴스가 공유하므로.
static이 붙은 멤버변수, 메서드, 클래스 초기화 블럭은 인스턴스를 생성안해도 사용가능하다.
인스턴스 메서드와 static메서드의 가장 근본적인 차이는 메서드 내에서 인스턴스 멤버를 사용하냐, 안하냐의 여부이다.
static 메서드 내에서 왜 인스턴스 멤버를 못쓰냐면, static 메서드는 인스턴스를 생성 안해도 사용가능한데, 인스턴스 멤버는 반드시 생성시 사용해야 되기 때문에 static메서드 사용시점에서 인스턴스가 존재하지 않을 수도 있어서이다.
멤버변수, 메서드, 초기화 블럭에서 static이 사용될 수 있다.
제어자 | 대상 | 의미 |
static | 멤버변수 | -모든 인스턴스에 공통적으로 사용되는 클래스변수가 된다. -인스턴스를 생성하지 않고도 사용가능 -클래스가 메모리에 로드될 때 생성됨 -static여부로 인스턴스 변수와 구분된다. |
메서드 | -인스턴스를 생성안해도 사용가능 -static메서드 내에서는 인스턴스 멤버를 직접적으로 사용불가능. 사용하려면 내부에서 따로 인스턴스를 생성한 후 사용해야 됨. 클래스 멤버는 사용가능. 인스턴스 멤버(변수,메서드)들은 인스턴스멤버, 클래스 멤버 전부 사용가능. |
인스턴스 멤버를 사용안하는 메서드는 static메서드를 고려하자.
4.3 final -마지막의, 변경될 수 없는
제어자 | 대상 | 의미 |
final | 클래스 | -변경,확장이 불가능한 클래스가 된다. -다른 클래스의 조상이 될 수 없다.(상속을 할수 없다.) |
메서드 | -오버라이딩을 할 수 없는 메서드가 된다. |
|
멤버변수 | -변수 앞에 fiinal이 붙으면 값을 변경할 수 없는 상수가 된다. | |
지역변수 |
final은 클래스,메서드, 멤버변수, 지역변수 거의 모든것에서 사용 가능하다.
생성자를 이용한 final멤버 변수 초기화
final이 붙은 변수는 상수라 일반적으로 선언과 초기화를 동시에 하지만,
인스턴스 변수의 경우 생성자에서 초기화 되도록 할 수 있다. (보통 생성자에서 많이 초기화하고 공통적인건 인스턴스 초기화 블럭을 쓴다.)
매개변수를 갖는 생성자를 클래스 내에 선언해서 인스턴스 생성시 final이 붙은 멤버변수를 초기화하는데 필요한 값을
생성자의 매개변수에서 제공받는다.
이기능을 사용하면 인스턴스마다 final이 붙은 다른 값을 가지는 멤버변수를 갖도록 할 수 있다.
ex:)
final int Number;
final String kind;
Card(String kind, int Number)
{
this.Number=Number;
this.kind=kind;
}
4.4 abstract - 추상의 미완성의
'미완성'의 의미를 갖고 있다.
메서드의 선언부만 작성하고 구현부가 없는 추상 메서드 선언시 사용한다.
abstract가 사용될 수 있는 곳 - 클래스, 메서드
제어자 | 대상 | 의미 |
abstract | 클래스 | -클래스 내에 추상메서드가 선언되어 있음을 의미한다. |
메서드 | -선언부만 있고 구현부는 없는 추상 메서드임을 알린다. |
추상 클래스는 미완성 메서드가 존재하는 클래스라 인스턴스를 생성할 수 없다.
ex:
abstract class AbstractTest{
abstract void move();
}
완성된 클래스도 abstract를 붙여서 추상 클래스로 만드는 경우도 있다.
왜?
인스턴스를 생성해봐야 할 수 있는 게 없을 때 인스턴스 생성 못하도록 막으려고
이렇게 되면 다른 클래스가 이 클래스를 상속받으면 원하는 메서드만 오버라이딩하려고
이 클래스가 없다면 아무 내용 없는 메서드를 잔뜩 오버라이딩 해야되서 ( 잘 와닿지는 않음)
4.5 접근 제어자
멤버 또는 클래스에 사용되어 해당하는 멤버 및 클래스를 외부에서 접근하지 못하게 제한한다.
접근 제어자가 지정되어 있지 않으면 default임을 뜻한다.
클래스, 멤버변수,메서드, 생성자 전부에서 사용됨.
private- 같은 클래스 내에서만 접근가능
default- 같은 패키지 내에서만 접근가능
protected- 같은 패키지 내에서, 다른 패키지의 자손클래스에서 접근가능
public- 접근제한 없음
제어자 | 같은 클래스 | 같은 패키지 | 자손클래스 | 전체 |
public | O | O | O | O |
protected | O | O | O | |
(default) | O | O | ||
private | O |
public > protected > default > private 순으로 접근 범위가 넓다.
사용 가능한 접근 제어자
클래스 : public ,default
메서드,멤버변수: public,protected,default,private
지역변수: 없음
접근 제어자를 이용한 캡슐화
클래스, 주로 멤버에 접근제어자를 사용하는 이유는
1.클래스 내부에 선언된 데이터 보호를 위해서다.
2.내부에서만 사용되는 멤버변수나 부분작업처리를 위한 메서드등을 클래스 내부에서 감추려고.
-> 데이터 감추기 . 캡슐화에 해당되는 내용.
접근제어자에 따라 접근 범위가 달라지므로, 좁으면 좁을 수록 테스트할 때 편하다.
보통 멤버변수를 private나 protected로 제한하고
setter(멤버변수의 값을 변경하는 메서드이름)&getter(멤버변수의 값을 읽는 메서드이름)
public메서드를 제공해서 직접적으로 변경 못하고 간접적으로 변경할 수 있게 한다.
20210729 16:23~
생성자의 접근 제어자
생성자에 접근 제어자를 사용해서 인스턴스 생성을 제한할 수 있다.
만약 생성자의 접근제어자가 private면, 클래스 내부를 제외하고는 인스턴스생성이 불가능하다.
대신 인스턴스를 생성해서 반환해주는 public메서드를 제공해서 외부에서 이 클래스의 인스턴스를 사용하도록
할 수 있다. 이메서드는 public +static이어야 한다.
왜?인스턴스를 생성하지 않고도 이 public메서드를 반환해주어야 되는데, 그럴려면 미리 인스턴스가 클래스 내에서
생성되어 있어야 한다. 그러기 위해서 static을 인스턴스 객체, 메서드 둘다에 붙여준다.
그림을 보면 현재 private Singleton()으로 생성자가 접근제한되어있어 인스턴스 생성이 안된다. 그래서 에러.
getInstance()로 static으로 인스턴스가 생성안되있어도 가능하게 s를 미리 만들어놓고 간접으로 생성했따.
4.6 제어자의 조합
한번더 제어자 정리. ( 사용될 수 있는 대상 중심으로)
대상 | 사용가능한 제어자 |
클래스 | static,final,abstract,public,default |
메서드 | 모든 접근제어자, static,final,abstract |
멤버변수 | 모든 접근 제어자, final,static |
지역변수 | final |
조합 시 주의사항
1. 메서드에 static과 abstract를 함께 사용 불가능
static메서드는 구현부가 있는 메서드에만 사용가능
2. 클래스에 abstract와 final 동시에 사용 불가능
클래스에 사용되는 final은 변경, 확장 불가능의 의미
abstract는 상속을 통해서 완성되어야 한다는 의미로 모순.
3.abstract메서드의 접근 제어자가 private 불가능
abstract메서드는 자식 클래스에서 구현해주어야 하는데
4.메서드에 private와 final을 동시에 사용할 필요는 없음
final을 메서드에 쓰는 의미도 오버라이딩을 막으려고 쓰는건데
private가 접근 제어자면 어차피 오버라이딩이 안되므로.
5.다형성
5.1 다형성이란?
다형성: 여러형태를 가질 수 있는 능력. 조상클래스 타입의 참조변수로 자손 클래스의 인스턴스를 참조할 수 있도록
했다.
조상 클래스 타입의 참조변수로 자손클래스의 인스턴스를 참조할 수 있도록 한다.
class Tv{
boolean power;
int channel;
void power(){ }
void channelUp(){ }
void channelDown(){ }
}
class CaptionTv extends Tv{
String text;
void caption(){}
}
이렇게 2개의 클래스가 있을 때 Tv는 조상 클래스, CaptionTv는 자식 클래스가 된다.
Tv t=new CaptionTv();
이런식으로 조상 타입의 참조변수로 자식 타입의 인스턴스를 참조할 수 있다.
다만 이렇게 될 경우 실제 인스턴스타입이 CaptionTv()여도 t.text, t.caption()은 사용할 수 없다.
CaptionTv c=new CaptionTv();
Tv t=new CaptionTv();
아무리 인스턴스타입이 같아도 참조변수의 타입에 따라서 사용할 수 있는 멤버 개수가 달라진다.
조상 타입의 참조변수로 자손 타입의 인스턴스를 참조할 수는 있어도
자손타입의 참조변수로 조상 타입의 인스턴스를 참조할 수는 없다.
CaptionTv c= new Tv();
왜냐면 실제 인스턴스인 Tv타입보다 참조변수 c가 사용할 수 있는 멤버 개수가 더 많으면
실제 인스턴스에서 존재하지 않는 멤버(ex: text, caption은 Tv타입에서는 존재하지 않는다.)
를 사용할 가능성이 있기 때문에 불가능하다.
참조변수가 사용할 수 있는 멤버 개수는 인스턴스의 멤버 개수보다 작거나 같아야 한다.
5.2 참조변수의 형변환
서로 상속관계에 있는 클래스 사이에서만 자손타입의 참조변수를 조상타입의 참조변수로,
조상타입의 참조변수를 자식타입의 참조변수로 형변환이 가능하다.
형변환과 참조는 다르다.
자손타입 -> 조상타입 : 업캐스팅. 생략할 수 있다.
ex: Tv t= new (Tv)CaptionTv();
생략해서 Tv t= new CaptionTv)(); 이런식으로 된다.
조상타입 -> 자손타입 : 다운 캐스팅. 생략할 수 없다.
이 그림에서 상속 관계를 보면
조상타입인 Car 타입과 자손타입인 FireEngine, Ambulance와는 각각 서로 형변환이 가능하지만
FireEngine 타입의 참조변수와 Ambulance타입의 참조변수는 서로 형변환이 불가능하다.
FireEngine f;
Ambulance a;
f= (FireEngine)a; -> 불가능
Car car = null;
FireEngine fe=new FireEngine();
FireEngine fe2=null;
car= fe; // fe가 참조하고 있는 인스턴스주소를 car로 넘김. 서로 형을 맞추기 위해서 자식타입을 조상타입으로 업캐스팅 ( 생략가능) car=(car)fe;
fe2=(FireEngine)car;// 조상 타입을 자손 타입으로 다운 캐스팅. 생략 불가. 왜냐하면 생략하면 자손 타입에 조상 타입의 참조변수( 조상 타입의 인스턴스)주소를 넣는 꼴이므로
Car형 참조변수 c가 있다고 가정할 때 이 c가 참조하고 있는 인스턴스는 Car인스턴스나 자손인 FireEngine인스턴스일 것이다. 조상 타입 참조변수에는 조상, 자손 인스턴스 둘다 올 수 있으므로 (다형성)
참조 변수 c를 자손인 FireEngine타입으로 변환하는 것은 (다운캐스팅) 참조변수가 다룰 수 있는 멤버의 개수를 늘리는 것이기 때문에 실제 인스턴스 멤버 개수보다 참조변수가 사용할 수 있는 멤버의 개수가 더 많아지므로 문제가 생길 수도 있다.
쉽게 말해서, 만약 c가 Car인스턴스를 참조하고 있다면 , 다운캐스팅을 했을 때 인스턴스에 없는 멤버를 호출할 경우 문제가 생길 수도 있다는 뜻이다. 확실하지 않고 복불복으로 에러가 생길 수 있다는 의미다.
그래서 자손타입으로의 형변환(다운캐스팅)은 절대 생략 불가능하고,
형변환 전에 instanceof 연산자로 참조변수가 실제 참조하고 있는 인스턴스 타입을 확인하는 것이 좋다.
형변환은 참조변수의 타입을 변환하는 것이라 인스턴스에 영향은 없다.
이걸 쓰는 이유는
참조변수의 형변환을 통해서 참조하고 있는 인스턴스에서 사용할 수 있는 멤버의 개수(범위)를 조절하기 위해서다.
왜냐면 어차피 사용할 수 있는 멤버의 범위,개수를 정하는 것은 인스턴스가 아무리 멤버 개수가 많아도
참조변수가 어떤 타입을 가지고 있냐이기 때문이다.
1.Car 형 참조변수를 만들었다. null로 초기화
2. FireEngine형 참조변수 fe를 FireEngine형 인스턴스로 초기화 한 상태.
인스턴스의 주소 0x100을 저장하고 있다.
3. car = fe ; ( 업캐스팅이라 형변환 생략)
car형 참조변수가 참조변수 fe가 가리키고 있는 인스턴스의 주소를 참조하게 한다.
두 참조변수의 타입이 달라서 업캐스팅.
참조변수 car를 통해서도 FireEngine인스턴스를 사용할 수는 있지만 ,Car클래스가 아닌 water()메서드는 사용할 수 없다.
멤버의 사용 범위가 줄여진 것이라고 볼 수도 있겠다.
4. fe2= (FireEngine)car; (다운 캐스팅이라 생략 불가)
참조변수 car가 참조하고 있는 인스턴스( FireEngine)를 참조변수 fe2가 참조하도록 한다.
이경우가 가능한 이유는 참조변수형은 Car형이지만 그 변수가 참조하고 있는 인스턴스가 FireEngine타입이기 때문에
fe2에서 참조가 가능하고, 또 모든 멤버들을 사용할 수 있다. 왜냐하면 본질적으로 참조변수 타입과 인스턴스타입이
FireEngine타입으로 같다고 볼 수 있기 때문이다.
이 코드가 컴파일은 되도 에러가 뜨는 이유는 현재 참조변수car가 Car타입의 인스턴스를 가지고 있는데,
fe=(FireEngine)car; 이코드에서 FireEngine타입으로 다운캐스팅되었기 때문이다.
이것까진 좋은데, 참조변수car가 실제로 참조하고 있는 인스턴스는 Car타입이기 때문에
fe는 FireEngine타입의 멤버인 water()를 사용은 가능하지만, 실제 인스턴스(Car)타입에는 저 멤버가 없기 때문에 에러가 난다.
해결방법은 간단하게 car가 참조하는 인스턴스 타입을 FireEngine 타입으로 바꿔주면 된다.
5.3 instanceof연산자
참조변수가 참조하고 있는 인스턴스 실제 타입을 알아보기 위해 instanceof연산자를 활용한다.
주로 조건문에 사용된다.
instanceof 왼쪽에는 참조변수를, 오른쪽에는 타입(클래스명)이 위치한다.
연산 결과는 boolean값으로 나온다.
True가 나오면 참조변수가 검사한 타입으로 형변환 가능하다는 의미이다.
아까 예제에서 이어가 보면 car참조변수는 FireEngine형으로 형변환이 불가능하므로 false가 나왔다. ( 확인 완료!)
정리해보면, 조상타입의 변수로 자손 타입의 인스턴스를 참조할 수 있기 때문에, 참조변수의 타입과 인스턴스의 타입은 항상 일치하지는 않는다.
참조변수의 타입과 실제로 참조하고 있는 인스턴스의 타입이 일치해야만 인스턴스 내의 모든 멤버를 사용할 수 있다.
그 방법으로 형변환을 사용하는 것이다.
다운 캐스팅 : 조상 타입 -> 자손 타입으로 형변환 하는것. 실제 인스턴스 범위에 따라서 에러가 뜰 수도 있으니
instanceof연산자로 확인하고 변환해야 한다.
업캐스팅 : 자손타입-> 조상타입으로 형변환 하는것. 생략 가능
참조변수의 멤버 사용 범위가 인스턴스 타입의 멤버 사용범위보다 작거나 같아야 한다.
(그래서 자손 타입의 참조변수가 조상 타입의 인스턴스를 참조하지 못하는 것이다.)
5.4 참조변수와 인스턴스의 연결
조상 타입과 자손 타입 참조변수의 차이 = 사용할 수 있는 멤버의 개수
메서드의 경우 조상클래스의 메서드를 자손 클래스에서 오버라이딩을 했을 때도
참조변수의 종류( 조상 타입 참조변수, 자손 타입 참조 변수)에 상관없이
실제 인스턴스의 메서드(오버라이딩된 메서드)가 호출되지만 , 멤버변수는 참조변수의 타입에 따라서 달라진다.
참조 변수 타입 | 메서드 | 멤버변수(자손클래스에 중복 정의된 경우) | 멤버변수(조상 클래스에만 있는 경우) |
조상 | 실제 인스턴스의 메서드 | 조상 클래스의 멤버변수 사용 | 조상 멤버변수 |
자손 | 실제 인스턴스의 메서드 | 자식 클래스의 멤버변수 사용 | 자식 멤버변수 |
1. 중복 정의되어 있는 경우
현재 Parent(조상 ) 과 Child(자손) 클래스에 x가 중복으로 정의되어 있다.
메서드(method()) 역시 오버라이딩 되어 있는 상태이다.
c와 p는 같은 인스턴스를 참조하고 있으나, 참조 타입이 다르고, 멤버변수가 중복 정의되어 있어서
p는 조상 클래스의 멤버변수 100이 사용되고, c는 자손 클래스의 멤버변수 200이 사용된다.
메서드는 오버라이딩이 되어있어도 똑같이 실제 인스턴스의 메서드를 가리키는 모습이다.
멤버변수 중복 정의시 참조변수에 따라서 중복정의 된 x는 값이 달라진다.
2. 중복 정의가 안되어 있을 때
중복 정의가 Child에 안되어 있기 때문에 c.x , p.x 모두 조상클래스 x의 값 100을 가리킨다.
조상클래스의 멤버변수 이름과 같은 멤버변수가 자식 클래스에서 중복 정의된 경우가 아니면
멤버변수 값은 참조변수 타입에 따른 변화는 없다.
중요한건 인스턴수 변수에 직접 접근하면 참조변수의 타입에 따라 사용되는 인스턴스 변수가 달라질 수 있기 때문에
조심해야 되고 보통 멤버변수를 private로 하고, setter&getter로 멤버변수에 접근하게 한다.
5.5 매개변수의 다형성
참조변수의 다형적인 특징은 매개변수에서도 적용 가능하다.
즉 매개변수가 조상 클래스의 참조변수이면 매개변수로 받을 변수들은 매개변수의 자손 클래스인
참조변수들로도 받을 수 있다는 뜻이다.
예제
현재 코드를 보면 Tv클래스와 Computer클래스는 Product를 상속받고 있다.
Product의 생성자는 Product(int price)로, 내부 인스턴스 변수들을 생성자로 초기화 하는 코드이다.
다만 생성자가 매개변수이기 때문에,
상속받은 자손 클래스에서 생성자를 호출할 때 반드시 모든 조상 클래스들의 멤버들을 초기화하기 위해서
생성자 호출이 필요하다. 근데 매개변수가 있는 생성자만 있으니까 그걸 호출한다.
따라서 super(100);은 Tv클래스에 상속받은 price, bonusPoint 멤버변수를 100이라는 값으로 초기화하는 코드이다.
toString()은 Object클래스의 메서드를 오버라이딩한 코드이다.
현재 코드의 경우 Buyer 클래스를 볼 수 있는데,
buy 메서드를 보면 매개변수로 Tv와 Computer의 조상 클래스인 Product를 사용하고 있다.
다형성을 적용한 케이스인데, 만약 이걸 안쓰면 Tv를 매개변수로 받는, Computer를 매개변수로 받는 메서드들을
다 따로 만들어야 한다. 다형성으로 불필요한 코드를 줄여준 케이스다.
System.out.println에서 p를 호출하고 있는데, print 내부에 변수를 넣으면
print메서드 내부에서 toString()을 호출하는데 그걸 이용한 코드이다.
따라서 main에서 b.buy(new Tv());를 하면 초기화된 Tv의 가격 100을 처음 가격 1000에서 빼게 되는 것이다.
5.6 여러 종류의 객체를 배열로 다루기
위 코드에서 Product타입의 참조변수 배열로 처리하면
Product p[]=new Product[3];
p[0]=new Tv();
p[1]=new Computer();
p[2]=new Audio();
이렇게 조상타입의 참조변수 배열을 사용하면 공통의 조상을 가진 서로다른 종류의 객체들을
묶어서 관리하고 다룰 수 있다.
아니면 묶어서 다루고 싶은 객체들의 상속관계를 따져서
가장 가까운 공통조상 클래스 타입의 참조변수 배열을 생성해서 객체들을 저장하면 된다.
'자바의 정석' 카테고리의 다른 글
vol6 1.객체지향 언어 (0) | 2021.07.19 |
---|---|
자바 특징 (0) | 2021.07.19 |