[자바의 정석] 07. 객체지향 프로그래밍Ⅱ(1)
1. 상속
두 클래스를 부모와 자식으로 관계를 맺어주는 것
1.1 상속의 정의와 장점
상속이란? 기존의 클래스를 재사용하여 새로운 클래스를 작성하는 것이다.(코드의 재사용)
상속을 통해서 클래스를 작성하다 보면 적은 양의 코드로 새로운 클래스를 작성할 수 있고 코드를 공통적으로 관리할 수 있기 때문에 코드의 추가 및 변경이 매우 용이하다.
이러한 특징은 코드의 재사용성을 높이고 코드의 중복을 제거하여 프로그램의 생산성과 유지보수에 크게 기여한다.
상속을 구현하는 방법은 새로 작성하고자 하는 클래스의 이름 뒤에 상속받고자 하는 클래스의 이름을 키워드 ‘extends’와 함께 써주기만 하면 된다.
class Child extends Parent { }
//새로 작성하려는 클래스 Child ( 자손클래스 )
//상속받고자 하는 기존의 클래스 Parent ( 조상클래스 )
//extend : 더 크게만들다, 확장하다
조상클래스 : 부모(parent)클래스,상위(super)클래스,기반(base)클래스
자손클래스 : 자식(child)클래스,하위(sub)클래스,파생(derived)된클래스
프로그램이 커질수록 클래스간의 관계가 복잡해지는데, 이 때 그림으로 표현하면 클래스간의 관계를 보다 쉽게 이해할 수 있다.
자손클래스는 조상클래스의 모든 멤버를 상속받는다.(생성자, 초기화블럭 제외)
자손클래스가 변경되는 것은 조상클래스에 아무런 영향을 주지 못한다.
생성자와 초기화 블럭은 상속되지 않는다. 멤버만 상속된다.
자손 클래스의 멤버 개수는 조상 클래스보다 항상 같거나 많다.
❗접근제어자가 private또는 default인 멤버들은 상속되지 않는다기보다는 상속은 받지만 자손클래스로부터의 접근이 제한되는 것이다.
클래스 간의 관계에서 형제관계와 같은 것은 없다.
각각 두 클래스에 공통적으로 추가되어야 하는 멤버가 있다면 이들의 공통조상 클래스에 추가해주는 것이 좋다.
GrandChild클래스는 Child클래스의 자손이면서 Parent클래스의 자손이기도 하다.
Child : 직접조상
Parent : 간접조상
자손 클래스의 인스턴스를 생성하면 조상 클래스의 멤버도 함께 생성되기 때문에 따로 조상클래스의 인스턴스를 생성하지 않고도 조상 클래스의 멤버들을 사용할 수 있다.
자손 클래스의 인스턴스를 생성하면 조상 클래스의 멤버와 자손 클래스의 멤버가 합쳐진 하나의 인스턴스로 생성된다.
1.2 클래스 간의 포함관계
클래스의 관계
- 상속 = 상속 10%사용 (상속은 제약이 많음, 꼭 필요할 때만)
- 포함 = 포함 90&사용 (대부분 포함으로 하면 됨)
상속이외에도 클래스를 재사용하는 또 다른 방법이 있는데, 그것은 클래스간에 ‘포함(Composite)’관계를 맺어주는 것이다.
클래스 간의 포함관계를 맺어주는 것은 한 클래스의 멤버변수로 다른 클래스 타입의 참조변수를 선언하는 것을 뜻한다.
✏️작은 단위의 클래스들을 만들고, 이들을 조합해서 클래스를 만든다.
class Circle {
int x; //원점의 x좌표
int y; //원점의 y좌표
int r; //반지름(radius)
}
class Circle {
Point c = new Point();
int r;
//Point클래스를 재사용해서 Circle클래스에 작성
}
1.3 클래스간의 관계 결정하기
상속관계 ~은 ~이다.(is - a)
포함관계 ~은 ~을 가지고 있다.(has-a)
스포츠카는 카 이다.
Deck은 Card 를 가지고 있다.(Deck는 카드 한 벌)
❗프로그램은 모든 클래스를 분석하여 가능한 많은 관계를 맺도록 노력해서 코드의 재사용성을 높여야 한다.
1.4 단일상속
자바에서는 오로지 단일 상속만을 허용한다.
✏️비중이 높은 클래스 하나만 상속관계로, 나머지는 포함관계로 한다.
장점 : 클래스 간의 관계가 보다 명확해지고 코드를 더욱 신뢰할 수 있게 만들어 준다는 점에서 다중상속보다 유리하다.
1.5 Object클래스 - 모든 클래스의 조상
Object클래스는 모든 클래스의 상속계층도 최상위에 있는 최고조상클래스이다.
다른 클래스로부터 상속 받지 않는 모든 클래스들은 자동적으로 Object클래스로부터 상속받게 함으로써 이것을 가능하게 한다.
*extends Object를 컴파일러가 자동 추가
모든 클래스들은 Object클래스의 멤버들을 상속 받음 toString(), equals() 등 모든 클래스가 가져야 할 기본적인 11개의 메서드가 정의되어있다.
2. 오버라이딩(overriding)
2.1 오버라이딩이란?
조상 클래스로부터 상속받은 메서드의 내용을 변경하는 것을 오버라이딩이라고 한다.
조상의 메서드를 자신에 맞게 변경하는 것
선언부는 못바꾸고 내용(구현부)만 바꾸는 것
❗(~위에 덮어쓰다)
class Point {
int x;
int y;
Stirng getLocation() {//문자열로 만들어서 반환
return "x: "+x+", y: "+y;
}
}
class Point3D extends Point {
int z;
Stirng getLocation() {//오버라이딩. 선언부는 못바꾸고 구현부만 변경
return "x: "+x+", y: "+y+", z: "+z;
}
}
2.2 오버라이딩 조건
1. 메서드의 선언부는 조상의 것과 완전히 일치해야 한다.(메서드이름,매개변수,반환타입)
2. 접근 제어자는 조상 클래스의 메서드보다 좁은 법위로 변경 할 수 없다. puvlic>protected>(default)>private
3. 조상 클래스의 메서드보다 많은 수의 예외를 선언할 수 없다.
조상 클래스의 메서드를 자손 클래스에서 오버라이딩할 때
- 접근제어자를 조상 클래스의 메서드보다 좁은 법위로 변경할 수 없다.
- 예외는 조상 클래스의 메서드보다 많이 선언할 수 없다.
- 인스턴스메서드를 static메서드로 또는 그 반대로 변경할 수 없다.
2.3 오버로딩 vs 오버라이딩
오버로딩과 오버라이딩은 전혀 관계가 없음
오버로딩 - 기존에 없는 새로운 메서드를 정의하는 것
오버라이딩 - 상속받은 메서드의 내용을 변경하는 것
2.4 super
super는 자손 클래스에서 조상 클래스로부터 상속받은 멤버를 참조하는데 사용되는 참조변수이다.
상속받은 멤버와 자신의 멤버와 이름이 같을 때는 super를 붙여서 구별할 수 있다.
모든 인스턴스메서드에는 자신이 속한 인스턴스의 주소가 지역변수로 저장되는데, 이것이 참조변수인 this와 super의 값이 된다.
객체 자신을 가리키는 참조변수, 인스턴스 메서드(생성자) 내에만 존재(static 메서드 내에서 사용불가)
조상의 멤버를 자신의 멤버와 구별할 때 사용
super.x = 조상
this.x = 자신
자기변수만이 아니라 메서드 역시 super를 이용해서 호출할 수 있다.
class SuperTest {
public static void main(String args[]) {
Child c = new Child();
c.method();
}
}//end of SuperTest
class Parent {
int x = 10;
}//end of Parent
class Child extends Parent {
int x = 20;
void method() {
System.out.println("x="+x); //가까운쪽 int x = 20;
System.out.println("this.x="+this.x);
System.out.println("super.x="+super.x);
}
}//end of Child
▼실행결과
x=20
this.x=20
super.x=10
조상 클래스에 선언된 멤버변수와 같은 이름의 멤버변수를 자손 클래스에서 중복해서 정의하는 것이 가능하며 참조변수 super를 이용해서 서로 구별할 수 있다.
2.5 super() - 조상 클래스의 생성자
this()와 마찬가지로 super() 역시 생성자이다.
super()는 조상 클래스의 생성자를 호출하는데 사용된다.
✏️조상의 멤버는 조상의 생성자를 호출해서 초기화
Object클래스를 제외한 모든 클래스의 생성자 첫 줄에 생성자,this() 또는 super(), 를 호출해야 한다. 그렇지 않으면 컴파일러가 자동적으로 'super();'를 생성자의 첫줄에 삽입한다.
인스턴스를 생성할 때는 클래스를 선택하는 것만큼 생성자를 선택하는 것도 중요하다.
1.클래스 - 어떤 클래스의 인스턴스를 생성할 것인가?
2. 생성자 - 선택한 클래스의 어떤 생성자를 이용해서 인스턴스를 생성할 것인가?
모든 생성자는 첫줄에 다른생성자를 호출해야 한다. 그렇지 않으면 컴파일러가 super(); 호출
class PointTest {
public static void main(String args[]) {
Point3D p3 = new Point3D();
}
}//end of PointTest
class Point {
int x, y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
String getLocation() {
return "x: "+x+", y: "+y;
}
}//end of Point
class Point3D extends Point {
int z;
Point3D(int x, int y, int z){
//생성자 첫 줄에서 다른 생성자를 호출하지 않았기 때문에 컴파일러가 'super();'를 여기에 삽입한다.
//super()는 Point3D의 조상인 Point클래스의 기본 생성자인 Point()를 의미한다.
// this.x = x;
// this.y = y;
// this.z = z;
super(x, y); //조상클래스의 생성자 Point(int x, int y)를 호출한다.
this.z = z; //자신의 멤버를 초기화
}
String getLocation() {
return "x: "+x+", y: "+y+", z: "+z;
}
}//end of Point3D
↓이런식으로 해줘야함
Point(int x, int y, int z) {
super(x, y); //조상클래스의 생성자 Point(int x, int y)를 호출한다.
this.z = z; //자신의 멤버를 초기화
}
조상 클래스의 멤버변수는 이처럼 조상의 생성자에 의해 초기화되도록 해야 하는 것이다.
어떤 클래스의 인스턴스를 생성하면, 클래스 상속관계의 최고조상인 Object클래스까지 거슬러 올라가면서 모든 조상클래스의 생성자가 순서대로 호출된다는 것을 알 수 있다.
3. package와 import
3.1 패키지(package)
패키지란 서로 관련된 클래스의 묶음이다.
패키지에는 클래스 또는 인터페이스를포함시킬 수 있으며, 서로 관련된 클래스들끼리 그룹으로 묶어 놓음으로써 클래스를 효율적으로 관리할 수 있다.
같은 이름의 클래스 일지라도 서로 다른 패키지에 존재하는 것이 가능하다.
사실 클래스의 실제 이름(full name)은 패키지명을 포함한 것이다.(java.lang.String)
String 클래스의 실제이름 : java.lang.String = java.lang패키지에 속한 String 클래스
클래스가 물리적으로 하나의 클래스파일(.class)인 것과 같이 패키지는 물리적으로 하나의 디렉토리이다.
✏️클래스는 클래스파일, 패키지는 폴더, 하위 패키지는 하위폴더
✏️클래스 파일들을 압축한 것이 jar파일(*.jar)이다.
- 하나의 소스파일에는 첫 번째 문장으로 단 한 번의 패키지 선언만을 허용한다.
- 모든 클래스는 반드시 하나의 패키지에 속해야 한다.
- 패키지는 점(.)을 구분자로 하여 계층구조로 구성할 수 있다.
- 패키지는 물리적으로 클래스 파일(.class)을 포함하는 하나의 디렉토리이다.
3.2 패키지의 선언
클래스나 인터페이스의 소스파일(.java)의 맨 위에 다음과 같이 한 줄만 적어주면 된다.
package 패키지명; //대소문자혀용, 소문자로 하는 것이 원칙
위와 같은 패키지 선언문은 반드시 소스파일에서 주석과 공백을 제외한 첫 번째 문장이어야 하며, 하나의 소스파일에 단 한번만 선언될 수 있다. 해당 소스파일에 포함된 모든 클래스나 인터페이스는 선언된 패키지에 속하게 된다.
자바는 기본적으로 ‘이름 없는 패키지(unnamed package)’를 제공한다.
패키지를 지정하지 않는 모든 클래스들은 자동적으로 ‘이름없는 패키지’에 속하게 된다. 결국 패키지를 지정하지 않은 모든 클래스는 같은 패키지에 속하는 셈이다.
큰 프로젝트나 Java API와 같은 클래스 라이브러리를 작성하는 경우에는 미리 패키지를 구성하여 적용해야 한다.
3.3 import문
소스코드를 작성할 때 다른 패키지의 클래스를 사용하려면 패키지명이 포함된 클래스 이름을 사용해야 한다.
하지만, 매번 패키지명을 붙여서 작성하기란 여간 불편한 일이 아니다.
클래스의 코드를 작성하기 전에 import문으로 사용하고자 하는 클래스의 패키지를 미리 명시해주면 소스코드에 사용되는 클래스이름에서 패키지명은 생략할 수 있다. import문의 역할은 컴파일러에게 소스파일에 사용된 클래스의 패키지에 대한 정보를 제공하는 것이다.
3.4 import문의 선언
모든 소스파일(.java)에서 import문은 package문 다음에, 그리고 클래스 선언문 이전에 위치해야 한다.
import문은 packaga문과 달리 한 소스파일에 여러 번 선언할 수 있다.
일반적인 소스파일(.java)의 구성은 다음의 순서로 되어 있다.
- package문
- import문
- 클래스 선언
import문을 선언하는 방법은 다음과 같다.
import 패키지명.클래스명;
또는
import 패키지명.*; //이 패키지에 있는 모든 클래스를 패키지명 없이 사용할 수 있다.
클래스 이름대신 *를 사용하는 것이 하위 패키지의 클래스 까지 포함하는 것은 아니다
모든 소스파일에는 묵시적으로 다음과 같은 import문이 선언되어 있다
import java.lang.*;
java.lang패키지(기본패키지)는 매우 빈번히 사용되는 중요한 클래스들이 속한 패키지이기 때문에 따로 import문으로 지정하지 않아도 되도록 한 것이다.(Stirng, Object, System, Thread .....)
3.5 static import 문
static import 문을 사용하면 static멤버를 호출할 때 클래스 이름을 생략할 수 있다.
출처 : 남궁성. 「자바의 정석」. 도우출판. 2016