728x90
반응형
1. 상속(inheritance)
- 상속
: 기존의 클래스를 재사용하여 새로운 클래스를 작성하는 것이다.- 상속의 장점
1. 보다 적은 양의 코드로 새로운 클래스를 작성할 수 있다.
2. 코드를 공통적으로 관리할 수 있기 때문에 코드의 추가 및 변경이 매우 용이하다.
→ 코드의 재사용성을 높이고 코드의 중복을 제거해 프로그램의 생산성과 유지보수에 크게 기여한다.
- 상속의 장점
- 상속을 구현할 때는 새로 작성하고자 하는 클래스의 이름 뒤에 상속받고자 하는 클래스의 이름을 키워드 extends와 함께 써주면 된다
class Child extends Parent {
}
이 두 클래스를 상속관계에 있다고 하며 상속해주는 클래스(Parent)를 조상/부모/상위/기반 클래스라 하고, 상속 받는 클래스(Child)를 자손/자식/하위/파생된 클래스라고 한다.
(이 글에서는 부모 클래스를 슈퍼 클래스, 자식 클래스를 서브 클래스라고 하겠다)
- 슈퍼 클래스가 변경되면 서브 클래스는 자동적으로 영향을 받게 되지만, 서브 클래스가 변경되면 슈퍼 클래스에 아무런 영향을 주지 못한다.
→ 그래서 상속을 받는다는 것은 슈퍼 클래스를 확장(extends)한다는 의미로 해석할 수 있다.- 주의⚠️ 자손 클래스의 멤버 개수는 조상 클래스보다 항상 같거나 많다.
- 클래스 간 관계에서 형제 관계는 없다.
- Parent와 Child1, 그리고 Parent와 Child2는 서로 상속관계에 있지만 Child1와 Child2간에는 서로 아무런 관계도 성립되지 않는다.
- 만약 두 Child에 공통적으로 추가되어야 하는 멤버가 있다면, 이들의 공통조상인 Parent에 추가하는 것이 좋다.
class Parent { }
class Child1 extends Parent { }
class Child2 extends Parent { }
- 서브 클래스를 상속받는 섭섭클래스(SubSub..)
class Parent { } class Child extends Parent { } class GrandChild extends Child { }
- 서브 클래스는 슈퍼 클래스의 모든 멤버를 물려받으므로 GrandChild는 Child의 모든 멤버와 Child의 슈퍼 클래스인 Parent의 모든 멤버까지 상속받게 된다. 따라서 Child는 GrandChild의 직접 조상이고 Parent는 GrandChild의 간접 조상이 된다. 그래서 GrandChild는 Parent와 간접적인 상속 관계에 있다고 할 수 있다.
- 슈퍼 클래스의 인스턴스를 생성하면 슈퍼 클래스의 멤버도 함께 생성되기 때문에 따로 슈퍼 클래스의 인스턴스를 생성하지 않고도 슈퍼 클래스의 멤버들을 사용할 수 있다. 즉, 서브 클래스의 인스턴스를 생성하면 슈퍼 클래스의 멤버와 서브 클래스의 멤버가 합쳐진 하나의 인스턴스로 생성된다.
1.2 클래스간의 관계 - 포함관계
- 상속 이외에도 클래스를 재사용하는 또 다른 방법은 클래스 간 포함(Composite)관계를 맺어주는 것이다.
포함: 한 클래스의 멤버변수로 다른 클래스 타입의 참조변수를 선언하는 것
1.3 클래스간의 관계 결정하기
class Circle {
int x;
int y;
int r;
}
class Point {
int x;
int y;
}
class Circle { // 포함 관계
Point p = new Point();
int z;
}
class Circle extends Point { // 상속 관계
int r;
}
위 예시로 봤을 때, Circle을 작성하는 데 있어서 Point를 포함시키거나 상속받게 하는 것은 별 차이가 없어 보인다. 그럴 때는 '~은 ~이다(is-a)'와 '~은 ~을 가지고 있다(has-a)'를 넣어 문장을 만들어보면 클래스 간 관계가 명확해진다.
Circle은 Point이다.(Circle is a Point)
vs.
Circle은 Point를 가지고 있다.(Circle has a Point)
두 문장을 비교했을 때 더 적합해 보이는 것은 두번째이다. 즉, 클래스를 가지고 문장을 만들었을 때 '~은 ~이다'가 성립한다면 상속관계, ~은 ~를 가지고 있다가 성립한다면 포함관계를 맺어주면 된다.
1.4 단일 상속(single inheritance)
- C++은 다중 상속을 허용하지만 자바에서는 오직 단일 상속만을 허용한다. 따라서 둘 이상의 클래스로부터 상속을 받을 수 없다.
- 다중상속을 허용하면 여러 클래스로부터 상속받을 수 있기 때문에 복합적인 기능을 가진 클래스를 쉽게 작성할 수 있다는 장점이 있지만, 클래스간의 관계가 복잡해지고 서로 다른 클래스로부터 상속받은 멤버간의 이름이 같은 경우 구별할 수 없다는 단점이 있다.
- 단일 상속이 하나의 슈퍼 클래스만을 가질 수 있기 때문에 다중상속에 비해 불편한 점도 있지만, 클래스 간의 관계가 보다 명확해지고 코드를 더욱 신뢰할 수 있게 만들어 준다는 점에서 다중상속보다 유리하다.
- 만약 동시에 두 클래스를 같이 사용하고 싶다면 하나는 상속을 받고 하나는 포함시켜(서브 클래스 내에 인스턴스를 생성해서) 사용한다.
1.5 Object클래스 - 모든 클래스의 조상
- Object 클래스
: 모든 클래스 상속계층도의 최상위에 있는 슈퍼클래스. 다른 클래스로부터 상속 받지 않는 모든 클래스들은 컴파일러가 자동적으로 Object로부터 상속받게 한다.
그동안 toString()이나 equals(Object o)를 따로 정의하지 않고 사용할 수 있었던 이유는 이 메서드들이 모두 Object에 정의된 것들이기 때문이다.
2. 오버라이딩(overriding)
2.1 오버라이딩이란?
- 오버라이딩(override: ~위에 덮어쓰다)
: 슈퍼 클래스로부터 상속받은 메서드의 내용을 변경하는 것.
2.2 오버라이딩의 조건
- 서브 클래스에서 오버라이딩하는 메서드는 슈퍼 클래스의 메서드와
- 이름이 같아야 한다.
- 매개변수가 같아야 한다.
- 반환타입이 같아야한다.
- ※JDK1.5부터 공변 반환타입이 추가되어 반환타입을 서브 클래스의 타입으로 변경하는 것은 가능하도록 조건이 완화되었다.
- 즉, 선언부가 서로 일치해야한다.
다만 접근제어자(access modifier)와 예외(exception)는 제한된 조건 하에서만 다르게 변경할 수 있다.- 접근제어자는 슈퍼 클래스의 메서드보다 좁은 범위로 변경할 수 없다.
- 슈퍼 클래스의 메서드보다 많은 수의 예외를 선언할 수 없다.
※ 슈퍼 클래스의 메서드에서 IOException, SQLException을 선언했을 때, 서브 클래스의 메서드에서 Exception으로 예외를 선언하면 안된다. 왜냐 하면, Exception은 모든 예외의 최고 조상이므로 가장 많은 개수의 예외를 던질 수 있도록 선언한 것이기 때문이다. - 인스턴스 메서드를 static 메서드로 또는 그 반대로 변경할 수 없다.
※ 슈퍼 클래스에 정의된 static 메서드를 서브 클래스에서 같은 이름의 static 메서드로 정의할 수 있다(오버라이딩X)
2.3 오버로딩 vs. 오버라이딩
- 오버로딩(overloading)
: 기존에 없는 새로운 메서드를 정의하는 것(new)
- 오버라이딩(overriding)
: 상속받은 메서드의 내용을 변경하는 것(change, modify)
2.4 super
- super
: 서브 클래스에서 슈퍼 클래스로부터 상속받은 멤버를 참조하는데 사용되는 참조 변수. 상속받은 멤버와 자신의 멤버의 이름이 같을 때 super를 붙여 구별할 수 있다.(this와 사용법이 비슷하다)- 슈퍼 클래스로부터 상속받은 멤버도 서브 클래스 자신의 멤버이므로 super대신 this를 사용할 수 있다. 그래도 슈퍼 클래스의 멤버와 서브 클래스의 멤버가 중복 정의되어 서로 구별해야 하는 경우에만 super를 사용하는 것이 좋다.
- this와 마찬가지로 super 역시 static메서드에서는 사용할 수 없고 인스턴스 메서드에서만 사용할 수 있다.
- 메서드 역시 super를 써서 호출할 수 있다. 특히 슈퍼 클래스의 메서드를 서브 클래스에서 오버라이딩한 경우 super를 사용한다.
class Point {
int x;
int y;
String getLocation() {
return "x: " + x + ", y: " + y;
}
}
class Point3D extends Point {
int z;
String getLocation(){
return super.getLocation() + ", z: " + z;
}
}
2.5 super() - 슈퍼 클래스의 생성자
- super()
: 슈퍼 클래스의 생성자를 호출하는 생성자.- 서브 클래스에서 인스턴스를 생성하면 서브 클래스의 멤버와 슈퍼 클래스의 멤버가 모두 합쳐진 하나의 인스턴스가 생성된다. 그래서 서브 클래스의 인스턴스가 슈퍼 클래스의 멤버들을 사용할 수 있는 것이다. 이 때 슈퍼 클래스 멤버의 초기화 작업이 수행되어야 하기 때문에 서브 클래스의 생성자에서 슈퍼 클래스의 생성자가 호출되어야 한다.
- 만약 클래스의 생성자가 첫 줄에 자신의 다른 생성자(this()) 또는 슈퍼 클래스의 생성자(super())를 호출하지 않으면 컴파일러가 자동적으로 추가한다.
3. package와 import
3.1 패키지(package)
- 패키지
: 클래스의 묶음. 패키지에는 클래스 또는 인터페이스를 포함시킬 수 있으며, 관련된 클래스를 묶어놓음으로써 클래스를 효율적으로 관리할 수 있다. - 하나의 소스파일에는 첫 번째 문장으로 단 한 번의 패키지 선언만을 허용한다.
- 모든 클래스는 반드시 하나의 패키지에 속해야 한다.
- 패키지는 점(.)을 구분자로 하여 계층구조로 구성할 수 있다.
- 패키지는 물리적으로 클래스 파일(.class)을 포함하는 하나의 디렉토리이다.
3.2 패키지의 선언
- 패키지의 선언
: 클래스나 인터페이스의 소스파일의 맨 위에 다음과 같이 한 줄만 적어주면 된다.package 패키지명;
- 패키지명은 대소문자를 모두 허용하지만, 클래스명과 쉽게 구분하기 위해 소문자로 하는 것을 원칙으로 하고 있다.
- 모든 클래스는 반드시 하나의 패키지에 포함되어야 하는데 여태 소스파일을 작성할 때 패키지를 선언하지 않고도 문제가 없었던 이유는 자바에서 기본적으로 제공하는 unnamed package에 속하게 하기 때문이다.
3.3 import문
- import
: 컴파일러에게 소스파일에 사용된 클래스의 패키지에 대한 정보를 제공한다. 컴파일러는 import문을 통해 소스파일에 사용된 클래스들의 패키지를 알아낸 다음, 모든 클래스이름 앞에 패키지명을 붙여준다.
3.4 import문의 선언
- import문은 package문과 달리 한 소스파일에 여러 번 선언할 수 있다.
- 일반적인 소스파일의 구성은 다음의 순서로 되어 있다.
- package문
- import문
- 클래스 선언
- import문 선언
- import 패키지명.클래스명;
: 해당 패키지의 해당 클래스만 import한다 - import 패키지명.*;
: 해당 패키지에 속하는 모든 클래스를 import한다. 한 패키지에서 여러 클래스를 사용하는 경우 용이하지만 import하는 패키지의 수가 많을 때는 어느 클래스가 어느 패키지에 속하는지 구별하기 어렵다.
import java.util.*; import java.lang.*; // 위를 아래처럼 표현할 수 없다 import java.*;
- import 패키지명.클래스명;
- System과 String 같은 java.lang 패키지들의 클래스들을 패키지명 없이 사용할 수 있었던 이유는 모든 소스파일에 묵시적으로 위 패키지를 import하고 있기 때문이다.
3.5 static import문
- static import
: static 멤버를 호출할 때 클래스 이름을 생략할 수 있다.예시) import static java.lang.Integer.*; import static java.lang.Math.random; import static java.lang.System.out;
이하 생략
4. 제어자(modifier)
4.1 제어자란?
- 제어자: 클래스, 변수 또는 메서드의 선언부에 함께 사용되어 부가적인 의미를 부여한다. 제어자는 클래스, 멤버변수, 메서드에 주로 사용되며 하나의 대상에 대해 여러 제어자를 조합하여 사용하는 것이 가능하다.
접근 제어자는 네가지 중 하나만 선택해서 사용할 수 있다. 제어자간의 순서는 관계없지만 주로 젖ㅂ근 제어자를 제일 왼쪽에 두는 경향이 있다. - 제어자의 종류
1. 접근 제어자: public, protected, (default), private
2. 그 외: static, final, abstract, native, transient, synchronized, volatile, strictfp
4.2 static - 클래스의, 공통적인
- static
: 클래스의, 공통적인의 의미를 가지고 있다.
static이 붙은 멤버변수와 메서드, 초기화 블럭은 인스턴스가 아닌 클래스에 관계된 것이기 때문에 인스턴스를 생성하지 않고도 사용할 수 있다.
4.3 final - 마지막의, 변경될 수 없는
- final
: 마지막의, 변경될 수 없는의 의미를 가지고 있으며 거의 모든 대상에 사용될 수 있다.대상 의미 클래스 변경될 수 없는 클래스. 확장될 수 없는 클래스가 된다.
그래서 final로 지정된 클래스는 다른 클래스의 조상이 될 수 없다.메서드 변경될 수 없는 메서드. final로 지정된 메서드는 오버라이딩을 통해 재정의될 수 없다. 멤버변수
지역변수변수 앞에 final이 붙으면 값을 변경할 수 없는 상수가 된다.
- 생성자를 이용한 final 멤버변수의 초기화
: final이 붙은 변수는 상수이므로 일반적으로 선언과 초기화를 동시에 하지만, 인스턴스 변수의 경우 생성자에서 초기화 되도록 할 수 있다.
클래스 내에 매개변수를 갖는 생성자를 선언하여, 인스턴스를 생성할 때 상수를 초기화하는데 필요한 값을 생성자의 매개변수로부터 제공받는 것이다. 이렇게 하면 각 인스턴스마다 상수가 다른 값을 갖도록 할 수 있다.
4.4 abstract - 추상의 미완성의
- abstract
: 추상의, 미완성의의 의미를 가지고 있다.
메서드의 선언부만 작성하고 실제 수행내용은 구현하지 않은 추상메서드를 선언하는데 사용된다.
클래스에 사용되어 클래스 내 추상메서드가 존재한다는 것을 쉽게 알 수 있게 한다.
- 추상 클래스
: 아직 완성되지 않은 메서드가 존재하는 미완성 클래스이므로 인스턴스를 생성할 수 없다.- 완성된 클래스에 abstract를 붙여 추상 클래스로 만드는 경우가 있는데 이런 클래스는 인스턴스를 생성해봐야 할 수 있는 것이 아무것도 없다. 오버라이딩해도 그래서 인스턴스를 생성하지 못하게 클래스 앞에 제어자 abstract를 붙이는 것이다.
- 추상클래스 자체로는 쓸모가 없지만 다른 클래스가 이 클래스를 상속받아 일부의 원하는 메서드만 오버라이딩해도 된다는 장점이 있다.
4.5 접근제어자(access modifier)
- 접근 제어자
: 멤버 또는 클래스에 사용되어, 해당하는 멤버 또는 클래스를 외부에서 접근하지 못하도록 제한하는 역할을 한다. 접근제어자가 지정되어 있지 않는 경우가 기본값이기 때문에 이름은 default이고 실제로 붙이지는 않는다.대상 클래스 클래스 public, (default) 메서드
멤버변수public, protected, (default), private) - 접근 제어자가 사용될 수 있는 곳 - 클래스, 멤버변수, 메서드, 생성자
private: 같은 클래스 내에서만 접근이 가능하다.
(default): 같은 패키지 내에서만 접근이 가능하다.
protected: 같은 패키지 내에서, 그리고 다른 패키지의 서브클래스에서 접근이 가능하다.
public: 접근 제한이 전혀 없다.
- 접근 제어자를 이용한 캡슐화
: 주로 멤버에 접근 제어자를 사용하는 이유는- 클래스의 내부에 선언된 데이터를 보호하기 위해서이다. 데이터가 유효한 값을 유지하도록, 그리고 외부에서 데이터를 함부로 변경하지 못하도록 하기 위해 외부로부터 접근을 제한하는 것이다.(정보 은닉(data hiding))
- 또 다른 이유는 클래스 내에서만 사용되는 즉, 내부 작업을 위해 임시로 사용되는 멤버변수나 부분작업을 처리하기 위한 메서드 등의 멤버들을 클래스 내부에 감추기 위해서이다. 외부에서 접근할 필요가 없는 멤버들을 private으로 지정해 외부에 노출시키지 않음으로써 복잡성을 줄일 수 있다. 두 경우 모두 캡슐화에 해당한다.
- getter, setter
: get으로 시작하는 메서드(getter)는 멤버변수의 값을 반환하는 일을 하고, set으로 시작하는 메서드(setter)는 매개변수에 지정된 값을 검사하여 조건에 맞는 값일 때만 멤버변수의 값을 변경하도록 작성된다.- 보통 멤버변수의 값을 읽는 메서드의 이름을 'get멤버변수이름'으로 지정하고 멤버변수의 값을 변경하는 메서드의 이름을 'set멤버변수이름'으로 지정한다. 반드시 그러한 것은 아니지만 암묵적인 규칙이니 따르도록 하자.
※ 하나의 소스파일(*.java)에는 public 클래스가 단 하나만 존재할 수 있으며, 소스파일의 이름과 public 클래스의 이름은 같아야 한다.
- 보통 멤버변수의 값을 읽는 메서드의 이름을 'get멤버변수이름'으로 지정하고 멤버변수의 값을 변경하는 메서드의 이름을 'set멤버변수이름'으로 지정한다. 반드시 그러한 것은 아니지만 암묵적인 규칙이니 따르도록 하자.
- 생성자의 접근 제어자
: 생성자에 접근 제어자를 사용함으로써 인스턴스의 생성을 제한할 수 있다. 보통 생성자의 접근제어자는 클래스의 접근 제어자와 같지만, 다르게 지정할 수도 있다.
- 생성자의 접근제어자를 private로 지정하면, 외부에서 생성자에 접근할 수 없으므로 인스턴스를 생성할 수 없게 된다. 그래도 클래스 내부에서는 인스턴스를 생성할 수 있다. 대신 인스턴스를 생성해서 반환해주는 public 메서드를 제공함으로써 외부에서 이 클래스의 인스턴스를 사용하도록 할 수 있다. 이 메서드는 public이면서 static이어야 한다.
// 생성자가 private인 클래스는 상속할 수 없으므로 이를 알리고자 final을 붙인다.
final class Singleton {
...
// getInstance()에서 사용될 수 있도록 인스턴스가 미리 생성되어야 하므로 static
private static Singleton s = new Singleton(); private Singleton() {
}
// 인스턴스를 생성하지 않고도 호출할 수 있어야 하므로 static
public static Singleton getInstance() {
return s;
}
}
4.6 제어자의 조합
- 대상에 따라 사용할 수 있는 제어자
제어자 대상 사용 가능한 제어자 클래스 public, (default), final, abstract 메서드 모든 접근 제어자, final, abstract, static 멤버변수 모든 접근 제어자, final, static 지역변수 final
- 제어자 조합 시 주의사항
- 메서드에 static과 abstract를 함께 사용할 수 없다.
- 클래스에 abstract와 동시에 사용할 수 없다.
- abstract 메서드의 접근제어자가 private일 수 없다.
- 메서드에 private와 final을 같이 사용할 필요는 없다.
5. 다형성(polymorphism)
5.1 다형성이란?
- 다형성: (객체지향개념에서) 여러가지 형태를 가질 수 있는 능력. 슈퍼 클래스의 참조변수로 서브 클래스의 인스턴스를 참조할 수 있도록 한다.
지금까지는 생성된 인스턴스를 다루기 위해 인스턴스의 타입과 일치하는 타입의 참조변수만을 사용했다. 이런 경우가 보통이지만 서로 상속관계에 있을 경우 슈퍼 클래스 타입의 참조변수로 서브 클래스의 인스턴스를 참조하는 것도 가능하다
그럼 위 경우는 인스턴스를 같은 타입의 참조변수로 참조하는 것과 어떤 차이가 있는지 arabozaParent p = new Child();
- 예시
Child 인스턴스를 2개 생성하고 참조변수 p와 c가 생성된 인스턴스를 각각 참조하도록 하였다. 이 경우 실제 인스턴스가 Child타입이라 할지라도, 참조변수 p로 Child의 모든 멤버를 사용할 수 없다.Parent p = new Child(); Child c = new Child();
Parent타입의 참조변수(p)로는 Child인스턴스 중에서 Parent의 멤버들만 사용할 수 있다. 즉, 둘 다 같은 타입의 인스턴스지만 참조변수의 타입에 따라 사용할 수 있는 멤버의 개수가 달라진다.
- 반대로는?
그렇다면 서브타입의 참조변수로 슈퍼타입의 인스턴스를 참조하는 것은 가능할까? 그렇지 않다. 위 코드는 컴파일 에러가 발생한다. 왜냐 하면, 실제 인스턴스인 Parent의 멤버 개수보다 참조변수 c가 사용할 수 있는 멤버 개수가 더 많기 때문이다. 즉, 서브타입의 참조변수로 슈퍼타입의 인스턴스를 참조하는 것은 존재하지 않는 멤버를 사용하고자 할 가능성이 있으므로 허용하지 않는 것이다. 참조변수가 사용할 수 있는 멤버의 개수는 인스턴스의 멤버 개수보다 같거나 적어야 한다. 참조변수의 타입(위 코드에서 Child)이 참조변수(c)가 참조하고 있는 인스턴스(Parent())에서 사용할 수 있는 멤버의 개수를 결정한다.Child c = new Parent();
5.2 참조변수의 형변환
- 참조변수도 서로 상속관계에 있는 클래스 사이에서만 형변환이 가능하다. 서브타입의 참조변수를 슈퍼타입의 참조변수로, 또 반대로의 형변환만 가능하다.
첫번째 코드는 Parent 타입의 참조변수 p가 null이고 이 p를 Child로 형변환해 c2에 대입하는 것이고, 두번째 코드는 Parent 타입의 참조변수 p가 Parent 인스턴스를 참조하고 있는 상태에서 Child로 형변환해 c에 대입하는 것이다. 두번째 코드를 실행하면 에러가 발생하는 이유는 p가 이미 Parent 타입의 인스턴스를 참조하고 있기 때문이다. 슈퍼타입의 인스턴스를 서브타입의 참조변수로 참조하는 것은 허용되지 않는다.Parent p = null; Child c = new Child(); Child c2 = null; p = c; // 업캐스팅 c2 = (Child) p; // 다운 캐스팅 // 아래 코드는 실행 시 에러가 발생한다. Parent p = new Parent(); Child c = null; c = (Child) p;
- 자손타입 → 조상타입(Up-casting, 업캐스팅): 형변환 생략 가능
조상타입 → 자손타입(Down-casting, 다운캐스팅): 형변환 생략 불가
5.3 instanceof 연산자
- instanceof
: 참조변수가 참조하는 인스턴스의 실제 타입을 알아보고 싶을 때 사용한다. 주로 조건문에 사용되고 instanceof의 왼쪽에는 참조변수를, 오른쪽에는 비교하고 싶은 타입(클래스명)을 위치시킨다.
연산 결과로 true와 false중 하나를 반환하는데, true가 반환되면 참조변수가 검사한 타입으로 형변환이 가능하다는 것을 뜻한다.
5.4 참조변수와 인스턴스의 연결
- 슈퍼타입의 참조변수와 서브타입의 참조변수의 차이점은 사용할 수 있는 멤버에 개수에 있다. 그리고 추가적으로, 슈퍼 클래스에 선언된 멤버변수와 같은 이름의 인스턴스 변수를 서브 클래스에 중복으로 정의했을 때, 슈퍼 타입의 참조변수로 서브 인스턴스를 참조하는 경우와 서브 타입의 참조변수로 서브 인스턴스를 참조하는 경우의 결과는 다르다.
메서드의 경우 슈퍼 클래스의 메서드를 서브 클래스에서 오버라이딩한 경우 참조변수의 타입에 관계없이 항상 실제 인스턴스 메서드(오버라이딩된 메서드)가 호출되지만, 멤버변수의 경우 참조변수의 타입에 따라 달라진다.
- 결론적으로, 멤버변수가 슈퍼 클래스와 서브 클래스에 중복으로 정의된 경우,
- 슈퍼타입의 참조변수를 사용했을 때는 슈퍼 클래스에 선언된 멤버변수가 사용되고
- 서브타입의 참조변수를 사용했을 때는 서브 클래스에 선언된 멤버변수가 사용된다.
하지만 중복으로 정의되지 않은 경우, 슈퍼타입의 참조변수를 사용했을 때와 서브타입의 참조변수를 사용했을 때의 차이는 없다.
5.5 매개변수의 다형성
- 참조변수의 다형적인 특징은 메서드의 매개변수에도 적용된다. 매개변수가 슈퍼 타입의 참조변수라는 것은, 메서드의 매개변수로 슈퍼클래스의 자손인 서브 타입의 참조변수면 어느 것이나 매개변수로 받아들일 수 있다.
- Tv와 Computer, Audio는 Product의 서브 클래스이므로 위처럼 buy(Product p) 메서드에 매개변수로 Tv의 인스턴스와 Computer의 인스턴스를 제공하는 것이 가능하다.
class Product {
int price;
int bonusPoint;
}
class Tv extends Product {
...
}
class Computer extends Product {
...
}
class Audio extends Product {
...
}
class Buyer {
int money = 1000;
int bonusPoint = 0;
void buy(Tv t) {
// Tv를 구매했을 때 실행
}
void buy(Computer c) {
// Computer를 구매했을 때 실행
}
void buy(Audio a) {
// Audio를 구매했을 때 실행
}
// 위 함수들을 이 메서드로 간단히 처리할 수 있다.
void buy(Product p) {
// Product(Tv, Computer, Audio)를 구매햇을 때 실행
}
}
5.6 여러 종류의 객체를 배열로 다루기
- 슈퍼타입의 참조변수로 서브타입의 객체를 참조하는 것이 가능하므로, 슈퍼타입의 참조변수 배열로 서브타입의 여러 객체를 다루는 것 역시 가능하다
- 예시
이렇게 슈퍼타입의 참조변수 배열을 사용하면, 공통의 조상을 가진 서로 다른 종류의 객체를 배열로 묶어서 다룰 수 있다.Product[] p = new Product[3]; p[0] = new Tv(); p[1] = new Computer(); p[2] = new Audio();
또는 묶어서 다루고 싶은 객체들의 상속관계를 따져서 가장 가까운 공통 슈퍼타입의 참조변수 배열을 생성해 객체들을 저장하면 된다.
- Vector
: 내부적으로 Object타입의 배열을 가지고 있어서 이 배열에 객체를 추가하거나 제거할 수 있게 작성되어 있다. 그리고 배열의 크기를 알아서 관리해주기 때문에 저장할 인스턴스의 개수에 신경쓰지 않아도 된다. 즉, 동적으로 크기가 관리되는 객체배열이다.- Vector 클래스의 주요 메서드
메서드 / 생성자 설명 Vector() 10개의 객체를 저장할 수 있는 Vector인스턴스를 생성한다.
10개 이상의 인스턴스가 저장되면, 자동적으로 크기가 증가된다.boolean add(Object o) Vector에 객체를 추가한다.
추가에 성공하면 결과값으로 true, 실패하면 false를 반환한다.boolean remove(Object o) Vector에 저장된 객체를 제거한다.
제거에 성공하면 결과값으로 true, 실패하면 false를 반환한다.boolean isEmpty() Vector가 비어있는지 검사한다.
비어있으면 true, 비어있지 않으면 false를 반환한다.Object get(int index) 지정된 위치(index)의 객체를 반환한다.
반환타입이 Object니까 적절한 타입으로 형변환이 필요하다.int size() Vector에 저장된 객체의 개수를 반환한다.
- Vector 클래스의 주요 메서드
6. 추상클래스(abstract class)
6.1 추상 클래스란?
- 추상클래스
: 클래스를 설계도에 비유한다면, 추상클래스는 미완성 설계도에 비유할 수 있다. 클래스가 미완성이라는 것은 멤버의 개수와 별개로 미완성 메서드(추상 메서드)를 포함하고 있다는 의미이다. 추상 클래스로 인스턴스를 생성할 수 없지만 상속을 통해 서브 클래스에 의해 완성될 수 있다.
추상 클래스 자체로는 클래스로서의 역할을 다 못하지만, 새로운 클래스를 작성하는데 있어서 바탕이 되는 슈퍼클래스로서 중요한 의미를 갖는다.
- abstract
: 추상 클래스에 붙이는 키워드.abstract class 클래스 이름 { ... }
6.2 추상 메서드(abstract method)
- 추상 메서드
: 메서드는 선언부와 구현부로 구성되어있는데, 여기서 선언부만 작성하고 구현부는 작성하지 않은 것이 추상메서드이다. 설계만 해놓고 실제 수행될 내용은 작성하지 않았기 때문에 미완성 메서드인 것이다.
메서드를 미완성 상태로 남겨 놓는 이유는 메서드의 내용이 상속받는 클래스에 따라 달라질 수 있기 때문에 슈퍼 클래스에서는 선언부만을 작성하고 주석을 덧붙여 어떤 기능을 수행할 목적으로 작성되었는지 알려주고, 실제 내용은 상속받는 클래스에서 구현하도록 비워두는 것이다. 그래서 추상클래스를 상속받는 서브 클래스는 조상의 추상메서드를 상황에 맞게 적절히 구현해주어야 한다.
- abstract
: 추상 메서드에도 붙일 수 있다. 추상 메서드는 구현부가 없으므로 블록대신 ;을 붙인다. 아무런 내용도 없는 블록만 있어도 추상메서드가 아닌 일반메서드로 간주될 수 있다./* 주석을 통해 어떤 기능을 수행할 목적으로 작성하였는지 설명한다. */ abstract 리턴타입 메서드이름();
- 추상 클래스로부터 상속받는 서브클래스는 오버라이딩을 통해 조상인 추상클래스의 추상메서드를 모두 구현해주어야 한다. 만일 상속받은 추상메서드 중 하나라도 구현하지 않는다면, 서브 클래스 역시 추상 클래스로 지정해야한다.
6.3 추상 클래스의 작성
- 추상(抽象)의 사전적 의미
: 낱낱이 구체적 표상(表象)이나 개념에서 공통된 성질을 뽑아 이를 일반적인 개념으로 파악하는 정신 작용.
- 상속이 서브 클래스르 만드는데 슈퍼 클래스를 사용하는 것이라면, 추상화는 기존 클래스의 공통부분을 뽑아내 슈퍼 클래스를 만드는 것이다.
상속이 발생할수록 서브 클래스는 점점 기능이 추가되어 구체화의 정도가 심해지며, 슈퍼 클래스는 추상화의 정도가 심해진다고 할 수 있다. 상속될수록 세분화되며 상속해주는 클래스에는 공통요소만 남게 된다.
- abstract void method(); vs. void method() { }
왼쪽은 method를 추상메서드로 선언한 문장이고, 오른쪽은 내용이 없는 일반메서드이다. 어차피 서브 클래스에서 오버라이딩으로 구현한다면 둘의 차이는 없어 보인다.
그럼에도 굳이 추상메서드(좌)로 선언하는 이유는 서브 클래스에서 추상 메서드를 반드시 구현하도록 강요하기 위해서이다. 만일 오른쪽 일반 메서드로 정의되어 있다면 상속받는 서브 클래스에서는 이 메서드들이 온전히 구현된 것으로 인식해 오버라이딩을 통해 구현하지 않을 수 있기 때문이다.
7. 인터페이스(interface)
7.1 인터페이스란?
- 인터페이스
: 일종의 추상클래스. 추상메서드를 갖지만 추상클래스보다 추상화 정도가 높아서 몸통을 갖춘 일반 메서드 또는 멤버변수를 구성원으로 가질 수 없다. 오직 추상메서드와 상수만을 멤버로 가질 수 있다.
추상 클래스가 미완성 설계도라고 한다면, 인터페이스는 밑그림만 그려진 기본 설계도라고 할 수 있다. 인터페이스 역시 다른 클래스를 작성하는데 도움을 주는 목적으로 작성된다.
7.2 인터페이스의 작성
- 인터페이스를 작성하는 것은 클래스와 같다. 다만 키워드로 class 대신 interface를 사용한다는 것이 다르다. 그리고 인터페이스의 접근제어자도 클래스와 마찬가지로 public 또는 defualt를 사용할 수 있다.
- 인터페이스 멤버들의 제약사항
- 모든 멤버변수는 public static final이어야 하며 이를 생략할 수 있다.
- 모든 메서드는 public abstract이어야 하며 이를 생략할 수 있다.
(단, JKD1.8부터 static메서드와 디폴드 메서드는 예외)
7.3 인터페이스의 상속
- 인터페이스는 인터페이스로부터만 상속받을 수 있으며, 클래스와는 달리 다중상속, 즉 여러 개의 인터페이스로부터 상속받을 수 있다.
7.4 인터페이스의 구현
- 인터페이스도 추상클래스처럼 그 자체로 인스턴스를 생성할 수 없으며, 인터페이스에 정의된 추상메서드의 몸통을 만들어준는 클래스를 작성해야 하는데, 그 방법은 인터페이스를 상속받는 클래스를 정의하는 것이다. 다만 클래스 상속 키워드인 확장한다는 의미의 extedns가 아닌 구현한다는 의미의 implements를 사용한다.
- 만일 인터페이스의 메서드 중 일부만 구현한다면 그 클래스에는 absract를 붙여 추상 클래스로 선언해야 한다. 인터페이스의 메서드를 구현할 때는 접근제어자를 public으로 지정해야한다. 상속받는 클래스는 상속(extends)과 구현(implements)을 동시에 할 수 있다.
- 인터페이스명
: 인터페이스의 이름에는 주로 ~을 할 수 있는의 의미인 able을 접미사로 사용하는데 그 이유는 1. 어떠한 기능 또는 행위를 하는데 필요한 메서들르 제공한다는 의미와 2. 그 인터페이스를 구현한 클래스는 해당 인터페이스의 기능을 할 수 있는 능력을 갖추었다는 의미를 갖기 때문이다. 반드시 able을 붙여야 하는 것은 아니다.
7.5 인터페이스를 이용한 다중상속
- 인터페이스의 다중상속
: 인터페이스는 static상수만 정의할 수 있으므로 슈퍼클래스의 멤버변수와 충돌하는 경우는 거의 없다. 충돌하더라도 클래스 이름을 붙여서 구분이 가능하다. 추상메서드는 구현내용이 전혀 없으므로 구현클래스의 메서드(선언부)가 일치하는 경우에는 당연히 슈퍼 클래스 쪽의 메서드를 상속받으면 되므로 문제되지 않는다.
이렇게 하면 상속받는 멤버간 충돌은 피할 수 있지만, 다중상속의 장점을 잃게 된다. 만일 두 개 의 클래스로부터 상속을 받아야 할 상황이라면 1. 두 슈퍼클래스 중에서 비중이 높은 쪽을 선택하고 다른 한 쪽은 클래스 내부에 멤버로 포함시키거나 2. 어느 한쪽의 필요한 부분을 뽑아 인터페이스로 만든 다음 구현하도록 한다.
7.6 인터페이스를 이용한 다형성
- 인터페이스 타입의 참조변수로 이를 구현할 클래스의 인스턴스를 참조할 수 있으며, 인터페이스 타입으로의 형변환도 가능하다.
인터페이스 Fightable을 클래스 Fight가 구현했을 때, 다음과 같이 Fighter인스턴스를 Fightable타입의 참조변수로 참조하는 것이 가능하다.
※Fightable의 참조변수로는 Fightable에 정의된 멤버들만 호출이 가능하다// 예시 Fightable f = (Fightable) new Fighter(); //또는 Fightable f = new Fight();
- 그리고 인터페이스는 매개변수의 타입으로도 사용될 수 있다. 인터페이스 타입의 매개변수가 갖는 의미는 메서드 호출 시 해당 인터페이스를 구현한 클래스의 인스턴스를 매개변수로 제공해야 한다는 것이다.
// Fightable을 구현한 Fighter클래스의 attack메서드
public void attack(Fightable f) {
}
// 위를 아래로 표현할 수 있다.
public void attack(new Fighter()) {
}
- 추가로 메서드의 리턴타입으로 인터페이스의 타입을 지정하는 것 역시 가능하다.
리턴타입이 인터페이스라는 것은 메서드가 해당 인터페이스를 구현한 클래스의 인스턴스를 반환한다는 것을 의미한다.
Fightable method() {
Fighter f = new Fighter();
return f;
}
// 위 문장은 아래와 같다
Fighter method() {
return new Fighter();
}
7.7 인터페이스의 장점
- 인터페이스를 사용하는 이유- 개발 시간을 단축시킬 수 있다.
- 표준화가 가능하다
- 서로 관계없는 클래스들에게 관계를 맺어줄 수 있다.
- 독립적인 프로그래밍이 가능하다.
7.8 인터페이스의 이해
- 인터페이스를 이해하기 위한 두 가지- 클래스를 사용하는 쪽(user)과 클래스를 제공하는 쪽(provider)이 있다.
- 메서드를 호출하는 쪽(user)에서는 사용하려는 메서드(provider)의 선언부만 알면 된다.(내용은 몰라도 된다.)
- 예시1
class A { public void methodA(B b) { b.methodB(); } } class B { public void methodB() { System.out.println("methodB()"); } } class InterfaceTest { A a = new A(); a.methodA(new B()); }
- 클래스 A와 클래스 B가 있고, A는 B의 인스턴스를 생성하고 메서드를 호출한다. 이 두 클래스는 직접적인 관계(A-B)에 있다.
- 이 경우 클래스 A를 작성하려면 클래스 B가 이미 작성되어 있어야 한다. 그리고 클래스 B의 methodB()의 선언부가 변경되면, 이를 사용하는 클래스A도 변경되어야 한다.
→ 이와 같이 직접적인 관계의 두 클래스는 제공자(Provider)가 변경되면 사용자(User)도 변경되어야 한다는 단점이 있다.
- A가 B를 직접 호출하지 않고 인터페이스를 매개체로 해서 A가 인터페이스를 통해 B의 메서드에 접근하도록 하면, B에 변경사항이 생기거나 B와 같은 기능의 다른 클래스로 대체되어도 A는 전혀 영향을 받지 않도록 할 수 있다.
interface I { public abstract void methodB(); } class B implements I { public void methodB() { System.out.println("methodB in B class"); } } class A { public void methodA(I i) { i.methodB(); // I를 구현한 B의 methodB()의 결과가 반환된다. } }
- 두 클래스 간의 관계를 간접적으로 변경하기 위해서는 먼저 인터페이스를 이용해서 B의 선언과구현을 분리해야한다.
- B에 정의된 메서드를 추상메서드로 정의하는 인터페이스 I를 정의하고 B가 I를 구현하도록 한다.
- 이제 B대신 I를 사용해 A를 작성할 수 있다.
→ A를 작성하는데 B가 사용되지 않았다. 이제 클래스간의 관계는 A-B에서 A-I-B의 간접적인 관계로 바뀌었다. 결국 A는 여전히 B를 호출하지만 A는 I하고만 직접적인 관계에 있기 때문에 B의 변경에 영향을 받지 않는다.
A는 인터페이스를 통해 실제로 사용되는 클래스의 이름을 몰라도 되고 심지어는 실제로 구현된 클래스가 존재하지 않아도 문제되지 않는다. A는 오직 I의 영향만 받는다.
- 예시2
- 클래스 A가 인터페이스 I를 사용해 작성되긴 하였지만 이처럼 매개변수를 통해 인터페이스 I를 구현한 클래스의 인스턴스를 동적으로 제공받아야 한다.(Thread)
implements I {
public abstract void play();
}
class A {
void autoPlay(I i) {
i.play();
}
}
class B implements I {
public void play() {
System.out.println("play in B class");
}
}
class InterfaceTest2 {
public static void main(String[] args) {
A a = new A(); a.play(new B());
}
}
- 예제3
- 위처럼 매개변수를 통해 동적으로 제공받을 수도 있지만 다음과 같이 제3의 클래스를 통해 제공받을 수도 있다(JDBC DriverManager)
-
interface I { public abstract void methodB(); } class B { public void methodB() { System.out.println("methodB in B class"); } public String toString() { return "class B"; } } class InstanceManager { public static I getInstance() { return new B(); // 다른 인스턴스로 바꾸려면 여기만 변경하면 됨 } } class A { void methodA() { I i = InstanceManager.getInstance(); i.methodB(); System.out.println(i.toString()); } } class InterfaceTest3 { public static void main(String[] args) { A a = new A(); a.methodA(); } }
- 인스턴스를 직접 생성하지 않고 getInstance()라는 메서드를 통해 제공받는다.
- 이렇게 하면 나중에 다른 클래스의 인스턴스로 변경되어도 A의 변경없이 getInstance()만 변경하면 된다는 장점이 생긴다.
- 그리고 인터페이스 I 타입의 참조변수 i로도 Object 클래스에 정의된 메서드들을 호출할 수 있다. i에 toString()이 정의되어 있지 않지만, 모든 객체는 Object 클래스에 정의된 메서드를 가지고 있을 것이기 때문에 허용하는 것이다.
7.9 디폴트 메서드와 static메서드(JDK1.8부터)
- static메서드
: 인스턴스와 관계가 없는 독립적인 메서드이기 때문에 인터페이스에 선언할 수 있다. 접근제어자가 항상 public이어야 한다.(생략가능)
- 디폴트 메서드(default method)
- 인터페이스에 새로운 메서드를 추가하는 것은 굉장히 까다롭다. 인터페이스에 메서드를 추가한다는 것은 추상 메서드를 추가한다는 것이고, 이 인터페이스를 구현한 모든 클래스들이 새로 추가된 메서드를 구현해야 하기 때문이다.
- 디폴트 메서드는 추상 메서드의 기본적인 구현을 제공하는 메서드로, 추상 메서드가 아니기 때문에 디폴트 메서드가 추가되어도 해당 인터페이스를 구현한 클래스는 변경하지 않아도 된다.
- 디폴트 메서드는 키워드 default(접근 제어자 아님)를 붙이며, 일반 메서드처럼 블록{}이 잇어야 한다. 접근제어자는 public이어야 한다.(생략 가능)
- 이 새로 추가된 메서드가 기존 메서드와 이름이 중복되어 충돌하는 경우가 발생할 수 있다. 이 충돌을 해결하는 규칙은 다음과 같다.
- 여러 인터페이스의 디폴트 메서드 간의 충돌
: 인터페이스를 구현한 클래스에서 디폴트 메서드를 오버라이딩 해야 한다. - 디폴트 메서드와 슈퍼 클래스의 메서드 간의 충돌
: 슈퍼 클래스의 메서드가 상속되고, 디폴트 메서드는 무시된다.
- 여러 인터페이스의 디폴트 메서드 간의 충돌
8. 내부 클래스(inner class)
8.1 내부 클래스란?
- 내부 클래스
: 클래스 내에 선언된 클래스. 클래스에 다른 클래스를 선언하는 이유는 두 클래스가 서로 긴밀한 관계에 있기 때문이다.- 내부 클래스의 장점- 내부 클래스에서 외부 클래스의 멤버들에 쉽게 접근할 수 있다.
- 코드의 복잡성을 줄일 수 있다.(캡슐화) - 각각 독립적인 클래스 A와 클래스 B가 있을 때, B를 A의 내부에 구현하면 B는 A의 내부 클래스가 되고, A는 B를 감싸는 외부 클래스가 된다.
이때 B는 A를 제외하고 다른 클래스에서 잘 사용되지 않아야 한다.class A { ... class B { ... } }
- 내부 클래스의 장점- 내부 클래스에서 외부 클래스의 멤버들에 쉽게 접근할 수 있다.
8.2 내부 클래스의 종류와 특징
- 내부 클래스의 종류와 특징내부 클래스특징
내부 클래스 특징 인스턴스 클래스 외부 클래스의 멤버변수 선언 위치에 선언하며, 외부 클래스의 인트섵느 멤버처럼 다루어진다. 주로 외부 클래스의 인스턴스 멤버들과 관련된 작업에 사용될 목적으로 선언된다. 스태틱 클래스 외부 클래스의 멤버변수 선언 위치에 선언하며, 외부 클래스의 static 멤버처럼 다루어진다. 주로 외부 클래스의 static 멤버, 특히 static 메서드에서 사용될 목적으로 선언된다. 지역 클래스 외부 클래스의 메서드나 초기화블럭 안에 선언하며, 선언된 영역 내부에서만 사용될 수 있다. 익명 클래스 클래스의 선언과 객체의 생성을 동시에 하는 이름없는 클래스(일회용)
8.3 내부 클래스의 선언
- 예시
인스턴스 변수, 클래스 변수, 지역변수와 각각 선언 위치가 동일하다. 따라서 각 변수와 유효범위, 접근성 역시 동일하다.class Outer { class InstanceInner { } // 인스턴스 클래스 static class staticInner { } // 스태틱 클래스 void myMethod() { class LocalInner { } // 지역 클래스 } }
8.4 내부 클래스의 제어자와 접근성
- 제어자
- 접근 제어자: 멤버변수는 멤버 클래스와, 지역 변수는 지역 클래스와 동일
- 내부 클래스도 클래스이므로 abstract, final 가능
- 내부 클래스 중 스태틱 클래스만 static 멤버를 가질 수 있다. 내부 클래스에 static 변수를 선언해야한다면 스태틱 클래스로 선언해야 한다.
다만 final과 staic이 동시에 붙은 변수(상수)는 모든 내부 클래스에서 정의가 가능하다
- 인스턴스 멤버는 같은 클래스에 있는 인스턴스 멤버와 static 멤버 모두 호출이 가능한 것처럼, 인스턴스 클래스는 외부 클래스의 인스턴스 멤버를 객체생성 없이 바로 사용할 수 있다. 그리고 스태틱 클래스의 멤버들을 객체생성 없이 사용할 수 있다.
static 멤버는 인스턴스 멤버를 직접 호출할 수 없는 것처럼, 스태틱 클래스는 외부 클래스의 인스턴스 멤버를 객체생성 없이 사용할 수 없다. 그리고 인스턴스 클래스의 멤버들을 객체생성 없이 사용할 수 없다.
- 인스턴스 클래스는 외부 클래스의 인스턴스 멤버이기 때문에 인스턴스 변수와 static 변수 모두 사용할 수 있다. 심지어 외부 클래스의 멤버가 private이어도 가능하다.
스태틱 클래스는 외부 클래스의 static 멤버이기 때문에 외부 클래스의 인스턴스 멤버는 사용할 수 없다.
지역 클래스는 외부 클래스의 인스턴스 멤버와 static 멤버를 모두 사용할 수 있으며, 지역 클래스가 포함된 메서드에 정의된 지역변수도 사용할 수 있다.
<이하 생략>
8.5 익명 클래스(anonymous class)
- 익명 클래스
: 익명 클래스는 다른 내부 클래스와 달리 이름이 없다. 클래스의 선언과 객체으 ㅣ생성을 동시에 하기 때문에 단 한번만 사용될 수 있고 오직 하나의 객체만ㅇ르 생성할 수 있는 일회용 클래스이다
이름이 없기 때문에 1. 생성자도 가질 수 없으며, 2. 하나의 클래스로 상속받는 동시에 인터페이스를 구현하거나 3. 둘 이상의 인터페이스를 구현할 수 없다. 즉, 오직 단 하나의 클래스를 상속받거나 단 하나의 인터페이스만을 구현할 수 있다.new 조상클래스이름() { // 멤버 선언 } 또는 new 구현인터페이스이름() { // 멤버 선언 }
- 익명 클래스의 예시
-
class InnerEx8 { public static void main(String[] args) { Button b = new Button("Start"); b.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { System.out.println("ActionEvent occurred!"); } }); // new ActionListner() { }가 익명 클래스 } }
728x90
반응형