클래스는 다른 클래스를 상속 받을 수 있다. 이를 부모-자식 관계라고 표현하는데 자식 클래스는 부모 클래스의 멤버를 가진다. 주의해야할 것은 멤버만 상속받고, 생성자와 초기화 블럭은 상속되지 않는다는 것이다.
class Parent {
int age;
}
class Child extends Parent {
void play() {
System.out.println("놀자~");
}
}
Parent 클래스는 age 멤버를 가지고 있고 Child 클래스는 age 멤버와 play() 메서드를 갖게 된다.
또한, 클래스들은 오직 하나의 클래스만 상속받을 수 있으며 2개 이상의 클래스를 상속받을 수 없다. (단일상속)
Object 클래스는 모든 클래스 상속계층도의 최상위에 있는 조상클래스이다. 다른 클래스로부터 상속 받지 않는 모든 클래스들은 자동적으로 Object 클래스를 상속 받는다.
class Tv {
...
}
class Tv extends Object {
...
}
위 코드처럼, 아무것도 상속하지 않는 클래스는 컴파일러가 자동적으로 Object 클래스를 상속하게 한다. 그 동안 toString()이나 equlas()같은 메서드를 따로 정의하지 않고 사용할 수 있었던 이유가 바로 이것때문이다.
Object 클래스에 언급한 메서드들이 정의되어 있기 때문이다.
조상 클래스로부터 상속받은 메서드의 내용을 변경하는 것을 오버라이딩이라고 한다. 오버라이딩은 메서드의 내용을 새로 작성하는 것이므로 메서드의 선언부는 조상의 것과 일치해야 한다. 따라서 오버라이딩을 위한 조건은 다음과 같다.
조상 클래스의 메서드와 오버라이딩하고자 하는 메서드는
이름이 같아야 한다.
매개변수가 같아야 한다.
반환타입이 같아야 한다.
또한 변경할 때 다음 제약이 있다.
접근 제어자를 조상 클래스의 메서드보다 좁은 범위로 변경할 수 없다.
예외는 조상 클래스의 메서드보다 많이 선언할 수 없다.
인스턴스 메서드를 static 메서드로 또는 그 반대로 변경할 수 없다.
super는 자손 클래스에서 조상 클래스로부터 상속받은 멤버를 참조하는데 사용하는 참조변수다. this처럼 상속받은 멤버와 자신의 멤버가 이름이 같을 때는 super를 붙여 구별할 수 있다.
class Parent{
int x = 10;
}
class Child extends Parent{
int x = 20;
void method() {
// 결과 : 20 10
System.out.println(this.x);
System.out.println(super.x);
}
}
this.x는 Child의 멤버 x를 뜻하고, super.x는 Parent의 멤버 x를 뜻한다.
class Parent{
void method() {
System.out.println("hi");
}
}
class Child extends Parent{
void method() {
super.method();
super.method();
}
}
또한 메서드도 사용가능하다. 위 코드에서 Child 클래스의 method()를 사용하면 hi가 두번 출력된다. Parent의 메서드 method()를 두 번 사용한 것이다.
super()는 this()와 마찬가지로 생성자이다. this()는 같은 클래스 내의 다른 생성자를 호출하지만 super()는 조상 클래스의 생성자를 호출하는데 사용된다.
자손 클래스의 인스턴스를 생성하면 자손의 멤버와 조상의 멤버가 모두 합쳐진 하나의 인스턴스를 생성한다. 이 때 조상 클래스 멤버의 초기화 작업이 먼저 진행되어야 하기 때문에 자손 클래스의 생성자에서 조상 클래스의 생성자가 호출되어야 한다.
이와 같은 조상 클래스의 생성자 호출은 Object클래스의 생성자인 Object()까지 가서 끝난다. 따라서 Object를 제외한 모든 클래스의 생성자는 첫 줄에 반드시 자신의 다른 생성자 또는 조상의 생성자를 호출해야 한다. 그렇지 않으면 컴파일러가 첫 줄에 super()를 자동으로 추가한다.
class PointTest {
public static void main(String args[]) {
Point3D p3 = new Point3D(1,2,3);
}
}
class Point {
int x;
int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
String getLocation() {
return "x :" + x + ", y :"+ y;
}
}
class Point3D extends Point {
int z;
Point3D(int x, int y, int z) {
this.x = x;
this.y = y;
this.z = z;
}
String getLocation() {
return "x :" + x + ", y :"+ y + ", z :" + z;
}
}
위 코드는 컴파일 에러가 발생한다.
먼저 Point3D의 생성자 첫 줄이 조상의 것이나 자신의 것을 호출하는 문장이 아니기 때문에 컴파일러는 자동적으로 다음과 같이 생성자에 super()를 넣는다.
Point3D(int x, int y, int z) {
super();
this.x = x;
this.y = y;
this.z = z;
}
그래서 Point3D의 인스턴스를 생성하면, super()가 실행되는데 이는 Point 클래스의 기본 생성자와 같다. 그런데 Point 클래스에는 Point() 기본 생성자가 존재하지 않는다. 따라서 오류가 발생한다. 이걸 해결하기 위해서는 다음과 같이 코드를 변경한다.
class PointTest2 {
public static void main(String argsp[]) {
Point3D p3 = new Point3D();
System.out.println("p3.x=" + p3.x);
System.out.println("p3.y=" + p3.y);
System.out.println("p3.z=" + p3.z);
}
}
class Point {
int x=10;
int y=20;
Point(int x, int y) {
this.x = x;
this.y = y;
}
}
class Point3D extends Point {
int z=30;
Point3D() {
this(100, 200, 300);
}
Point3D(int x, int y, int z) {
super(x, y);
this.z = z;
}
}
이처럼 조상 클래스의 멤버변수는 조상의 생성자( super() )에 의해 초기화되도록 해야 한다.
추가적으로, Point 클래스의 생성자 Point(int x, int y)에서 생성자 첫 줄에 다른 생성자를 호출하지 않기 때문에 컴파일러가 자동으로 super()를 삽입한다.
이 때 Point 클래스의 조상은 Object 클래스이므로 여기서 super()는 Object의 생성자인 Object()를 의미한다.
패키지는 클래스의 묶음이다. 패키지에는 클래스 또는 인터페이스를 포함시킬 수 있으며 물리적으로 하나의 디렉토리이다. 패키지는 다음과 같은 내용을 따른다.
하나의 소스파일에는 첫 번째 문장으로 단 한 번의 패키지 선언만을 허용한다.
모든 클래스는 반드시 하나의 패키지에 속해야 한다.
패키지는 점(.)을 구분자로 하여 계층구조로 구성할 수 있다.
패키지는 물리적으로 클래스 파일(.class)을 포함하는 하나의 디렉토리이다.
import문을 통해 다른 패키지에는 클래스들을 가져올 수 있다.
import java.util.Calendar;
import java.util.Date;
import java.util.ArryList;
// 또는
import java.util.*
위 코드처럼 원하는 클래스를 하나하나 가져올 수 있다. 또는 아래처럼 와일드카드를 사용해 java/util에 속하는 모든 클래스들을 가져올 수 있다. '
와일드 카드를 사용하면 컴파일러는 해당 패키지에서 일치하는 클래스 이름을 찾는 수고를 좀 더 하게 된다. 하지만 실행 시 성능상의 차이는 전혀 없다. 하지만 import하는 패키지의 수가 많을 때는 어느 클래스가 어느 패키지에 속하는지 구별하기 어렵다는 단점이 있다.
또, import문에서 클래스의 이름 대신 와일드카드를 사용하는 것은 하위 패키지의 클래스까지 포함시키지 않는다.
import java.util.*;
import java.text.*;
//
import java.*;
따라서 위의 import문을 아래 import문으로 대신할 수 없다.
static import를 사용하면 static 멤버를 호출할 때 클래스 이름을 생략할 수 있다.
import static java.lang.System.out;
import static java.lang.Math.random;
위와 같이 선언했다면 아래 두 문장은 같은 뜻이 된다.
System.out.println(Math.random());
out.println(random());
제어자에는 static, final, abstract 등.. 여러 가지가 존재한다. 그 중 final은 변수에 사용하면 변경할 수 없는 상수가 되며, 메서드에 사용되면 오버라이딩을 할 수 없게 되고 클래스에 사용되면 자신을 확장하는 자손 클래스를 정의하지 못하게 한다. 또한 클래스에서 final 멤버 변수는 생성자에서 값을 받아 초기화할 수 있다.
접근 제어자는 멤버 또는 클래스에 사용하여, 해당하는 멤버 또는 클래스를 외부에서 접근하지 못하도록 제한하는 역할을 한다. 기본 접근 제어자는 default이다.
사용할 수 있는 곳 - 클래스, 멤버변수, 메서드, 생성자
private - 같은 클래스 내에서만 접근이 가능하다.
default - 같은 패키지 내에서만 접근이 가능하다.
protected - 같은 패키지 내에서, 그리고 다른 패키지의 자손 클래스에서 접근이 가능하다.
public - 접근 제한이 없다.
접근 제어자를 생성자에 사용하여 인스턴스의 생성을 제한할 수 있다.
class Person {
private Person() {
...
}
}
위 코드와 같이 생성자에 private을 붙이면 외부에서 Person의 인스턴스를 생성하지 못하게된다. 다만, 클래스 내부에서는 인스턴스를 생성할 수 있다.
따라서, 내부에서 인스턴스를 생성하고, 그 인스턴스를 static 메서드를 이용하여 반환하는 형식으로 인스턴스를 생성할 수 있다.
class Person {
private static Person person = new Person();
private Person() {
...
}
public static Person getInstance() {
return person;
}
}
객체지향개념에서 다형성이란 '여러 가지 형태를 가질 수 있는 능력'을 의미하며, 자바에서는 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도록 함으로써 다형성을 구현했다.
구체적으로 말하면, 조상클래스 타입의 참조변수로 자손클래스의 인스턴스를 참조할 수 있도록 하였다는 것이다.
class Tv {
boolean power;
int channel;
void power() { power = !power; }
void channelUp() { ++channel; }
void channelDown() { --channel; }
}
class CaptionTv extends Tv {
String text;
void caption;
}
위와 같이 클래스를 선언하면 다음과 같은 참조가 가능하다.
Tv t = new CaptionTv();
// 자식 -> 조상 참조는 안됨
CaptionTv c = new Tv();
조상 클래스의 참조 변수로 자손 클래스를 참조할 수는 있지만 그 반대는 불가능하다.
위 참조에서 t 변수는 CaptionTv 객체를 참조하고 있지만, Tv 클래스에 속해있는 멤버들에만 접근이 가능하다.
예를 들어, CaptionTv의 text나 caption() 멤버의 사용이 불가능하다.
기본 변수와 같이 참조 변수도 형변환이 가능하다. 아래의 조건을 따른다.
자손타입 -> 조상타입 (업캐스팅) : 형변환 생략가능
자손타입 <- 조상타입 (다운캐스팅) : 형변환 생략불가
위와 같은 구도의 클래스가 있을 때, 다음과 같은 형변환이 가능하다.
Car car = null;
FireEngine fe = new FireEngine();
FireEngine fe2 = null;
car = fe; // 업캐스팅, 형변환 생략가능
fe2 = (FireEngine)car // 다운 캐스팅, 형변환 생략불가
참조변수 car와 fe의 타입이 서로 다르기 때문에, 형변환을 수행하여 두 변수간의 타입을 맞춰주어야하는데,
자손타입의 참조 변수를 조상타입의 참조 변수에 할당할 경우, car = fe 처럼 형변환을 생략할 수 있지만,
조상타입의 참조 변수를 자손타입의 참조 변수에 할당할 경우, fe2 = (FireEngine)car 처럼 형변환을 사용해야한다.
그 이유는, 자손타입을 조상타입으로 형변환하는 경우 다룰 수 있는 멤버의 개수가 실제 인스턴스가 갖고 있는 멤버의 개수보다 적으므로 문제가 되지 않는다.
하지만, 그 반대의 경우 다룰 수 있는 멤버의 개수를 늘리는 것이므로 실제 인스턴스의 멤버 개수보다 참조변수가 사용할 수 있는 멤버의 개수가 더 많아질 수 있는 문제가 생길 수 있다.
따라서 조상->자손의 형변환은 생략할 수 없다.
참고로, 형변환은 참조변수의 타입을 변환하는 것이지 인스턴스를 변환하는 것이 아니다. 참조변수의 형변환은 인스턴스에 아무런 영향을 미치지 않는다. 단지 참조변수의 형변환을 통해서 참조하고 있는 인스턴스에서 사용할 수 있는 멤버의 개수를 조절하는 것이다.
서로 상속관계에 있는 타입간의 형변환은 양방향으로 자유롭게 수행될 수 있으나, 자손 클래스의 참조 변수로 조상 클래스를 참조하는 것은 불가능하다. 그래서 참조변수가 가리키는 인스턴스의 타입이 무엇인지 확인하는 것이 중요한데, 이를 instanceof로 확인할 수 있다.
instanceof를 이용한 연산결과로 true를 얻었다는 것은 참조 변수가 검사한 타입으로 형변환이 가능하다는 것을 뜻한다.
void doWork(Car c) {
if (c instanceof FireEngine){
FireEngine fe = (FireEngine)c;
fe.water();
}
}
위 메서드에서 doWork 입장에서는 참조변수 c가 정확이 어떤 인스턴스인지 모른다. 따라서 instanceof로 c가 가르키고 있는 인스턴스의 타입을 체크하고, 적절히 형변환을 해야한다. 위 코드는 c가 FireEngine 타입으로 형변환될 수 있는지 검사하는 코드다.
class InstanceofTest {
public static void main(String args[]) {
FireEngine fe = new FireEngine();
if(fe instanceof FireEngine) {
System.out.println("This is a FireEngine instance.");
}
if(fe instanceof Car) {
System.out.println("This is a Car instance.");
}
if(fe instanceof Object) {
System.out.println("This is an Object instance.");
}
System.out.println(fe.getClass().getName());
}
} // class
class Car {}
class FireEngine extends Car {}
추가로, 위 코드의 실행결과는 다음과 같다.
This is a FireEngine instance.
This is a Car instance.
This is an Object instance.
fe의 조상은 Car와 Object이므로 fe는 Car와 Object의 멤버를 전부 포함하고있다. 따라서 타입검사에서 전부 true를 반환한다. 이에 fe는 Car나 Object 타입으로 형변환할 수 있다.
class Car() {
int x = 5;
}
class Cccar() extends Car{
int x = 10;
}
Car c1 = new Cccar();
Cccar c2 = new Cccar();
System.out.println(c1.x);
System.out.println(c2.x);
위 코드에서 c1과 c2는 Car를 상속한 Cccar 객체를 가르킨다. 하지만 이때 c1.x의 값은 5고, c2.x의 값은 10이 된다.
정리하면, 조상클래스와 자손클래스에 같은 이름의 인스턴스 변수가 존재할 때, 참조변수의 타입에 따라 다른 결과를 얻는다.
조상클래스로 자손 인스턴스를 참조하는 경우, 조상클래스의 멤버변수가 사용되고, 자손클래스로 자손 인스턴스를 참조하는 경우, 자손클래스의 멤버변수가 사용된다.
메서드의 경우는 참조변수의 타입에 관계없이 항상 실제 인스턴스의 메서드를 호출한다.
다음과 같은 클래스가 정의되어있다 가정하자.
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) {
money -= t.price;
bonusPoint += t.bonusPoint;
}
}
Buyer 클래스의 buy메서드는 Tv 객체를 받아서 구입한 사람의 돈에서 제품의 가격을 빼고, 보너스점수를 추가하는 작업을 하도록 구성되어있다. 근데 buy(Tv t)로는 Tv밖에 살 수 없기 때문에 Computer와 Audio에 관한 메서드도 필요하다.
void buy(Computer t) {
money -= t.price;
bonusPoint += t.bonusPoint;
}
void buy(Audio t) {
money -= t.price;
bonusPoint += t.bonusPoint;
}
이렇게 되면, 제품의 종류가 늘어날 때마다 새로운 buy 메서드를 추가해주어야 할 것이다.
그러나 메서드의 매개변수에 다형성을 적용하면 아래와 같이 하나의 메서드로 간단히 처리할 수 있다.
void buy(Product t) {
money -= t.price;
bonusPoint += t.bonusPoint;
}
매개변수가 Product 타입의 참조변수라는 것은, 메서드의 매개변수로 Product클래스의 자손타입의 참조변수면 어느 것이나 매개변수로 받아들일 수 있다는 것이다.
그리고 Product 클래스에 price와 bonusPoint가 선언되어 있기 때문에 참조변수 t하나로 Product를 상속받은 모든 인스턴스에서 price와 bonusPoint를 사용할 수 있다.
앞에서 봤듯이 Tv와 Audio, Computer는 Product의 자손이므로 buy메서드에 그 셋의 인스턴스를 제공하면 된다.
위에서 이어서, 조상타입의 참조 변수로 자손타입의 객체를 참조하는 것이 가능하므로, 다음과 같은 선언이 가능하다.
Product p1 = new Tv();
Product p2 = new Computer();
Product p3 = new Audio();
이는 다음과 같이 배열로도 쓸 수 있다.
Product[] p = new Product[3];
p[0] = new Tv();
p[1] = new Computer();
p[2] = new Audio();
이처럼 조상타입의 참조 배열 변수를 이용하면, 공통의 조상을 가진 서로 다른 종류의 객체를 배열로 묶어서 다룰 수 있다.