1. 상속관계에서의 접근제한자
2. 추상클래스
3. final
1. 상속관계에서의 접근제한자
접근 제한자는 public, protected, default, private 네가지가 있습니다. public은 아무데서나 자유롭게, private은 클래스 내에서만(개인적인) 사용가능하다고 했었죠. default와 protected는 패키지내에서만 사용가능하다는 공통점이 있지만, 그 중 protected가 상속관계와 관련이 있어, 이 부분을 여기서 예제로 살펴보겠습니다.
Aclass.java
package example.pkg1;
public class Aclass {
protected String varA;
String varA2;
protected void methodA() {
System.out.println("methodA");
}
void methodA2() {
System.out.println("methodA2");
}
}
AclassMain.java
package example.pkg1;
public class AclassMain {
public static void main(String[] args) {
Aclass ac = new Aclass();
ac.varA = "varA";
ac.varA2 = "varA2";
ac.methodA();
ac.methodA2();
}
}
실행 결과
methodA
methodA2
Aclass와 AclassMain 클래스는 example.pkg1 이라는 패키지에 같이 존재하기 때문에 protected, default 접근제한자를 가지고 있는 변수와 메서드 모두 사용이 가능합다. 이번엔 다른 패키지에 있는 클래스에서 사용해보겠습니다.
Bclass.java
package example.pkg2;
import chapter08.pkg1.Aclass;
public class Bclass {
public void methodB() {
Aclass ac = new Aclass();
//ac.varA = "varA"; // 사용 불가
//ac.varA2 = "varA2"; // 사용 불가
//ac.methodA(); // 사용 불가
//ac.methodA2(); // 사용 불가
}
}
Cclass.java
package example.pkg2;
import chapter08.pkg1.Aclass;
public class CClass extends Aclass {
CClass() {
this.varA = "varA"; // 사용 가능
//this.varA2 = "varA2"; // 사용 불가
this.methodA(); // 사용 가능
//this.methodA2(); // 사용 불가
}
}
BClass와 CClass는 example.pkg2 패키지에 존재하는 파일입니다. Aclass가 있는 example.pkg1과 다른 패키지이기 때문에 먼저 상단에 import를 해줘야 하고, BClass를 보면 protected 접근제한자 변수/메서드, default 접근제한자 변수/메서드 모두 사용할 수 없습니다. 위 예제는 주석으로 처리해놨지만 주석을 제거해보녀 이클립스에서 에러로 표기해줍니다.
CClass 파일을 보면 생성자안에 this.varA와 this.methodA()는 사용이 가능한것을 알수 있습니다. 이 변수와 메서드는 접근제한자가 protected인데, protected 접근제한자는 같은 패키지에서만 사용이 가능하지만, 예외로 다른 패키지인 경우라도 상속관계에 있으면 생성자에서 this 참조변수를 사용해서 사용이 가능하게 된것입니다.
2. 추상클래스
추상적이다라는 단어의 의미는 대상을 추려서 나타낸 것을 말합니다. 구체적이다라는 말과 반대되는 말이죠. 영어로는 abstract라고 합니다.
일반적인 메서드는 선언부와 구현부(몸통, 중괄호{})를 가지고 있는 메서드를 말합니다. 지금까지 예제들도 전부 일반 메서드들이었고, 이 일반 메서드들로 이루어진 클래스만 다뤘었는데, 구현부가 없고, 선언부만 가지고 있는 메서드(추상 메서드)가 하나라도 있으면 이 클래스는 추상 클래스가 됩니다. 이 추상 클래스는 new 연산자를 사용해서 객체화할 수 없으며, 부모 클래스로만 사용할 수 있습니다. 상속받는 자식 클래스는 부모 클래스의 메서드 중 추상메서드가 있다면 이 추상메서드를 반드시 구현(재정의)해야 합니다. 구현한다는 말은 구현부(몸통)가 빠져 있는 추상메서드의 구현부를 채워준다는 얘기다. 즉 메서드 재정의(오버라이딩) 를 말합니다.
추상 메서드를 선언하는 방법은
접근 제한자 abstract 리턴타입 메서드명(매개변수);
abstract 키워드를 사용하며, 나머지는 메서드의 선언부와 동일합니다. 그리고, 구현부(몸통)이 존재하지 않는데, 중괄호도 없고, 선언부만 표기하고 세미콜론으로 끝냅니다. 아래 예제를 살펴보겠습니다.
ShapeEx.java
package example.absractEx;
abstract class Shape {
String type;
Shape(String type) {
this.type = type;
}
abstract double area();
abstract double length();
}
class Circle extends Shape{
int r;
Circle(int r) {
super("원");
this.r = r;
}
@Override
double area() {
return r * r * Math.PI;
}
@Override
double length() {
return 2 * r * Math.PI;
}
@Override
public String toString() {
return "Shape [type=" + type + ", r=" + r + "]";
}
}
class Rectangle extends Shape {
int width, height;
Rectangle(int width, int height) {
super("사각형");
this.width = width;
this.height = height;
}
@Override
double area() {
return width * height;
}
@Override
double length() {
return 2 * (width + height);
}
@Override
public String toString() {
return "Shape [type=" + type + ", width=" + width + ", height=" + height+"]";
}
}
public class ShapeEx {
public static void main(String[] args) {
Shape[] shapes = new Shape[2];
shapes[0] = new Circle(10);
shapes[1] = new Rectangle(5,5);
for(Shape s : shapes) {
System.out.println(s);
System.out.println("넓이:"+s.area()+" 둘레:"+s.length());
}
}
}
실행 결과
Shape [type=원, r=10]
넓이:314.1592653589793 둘레:62.83185307179586
Shape [type=사각형, width=5, height=5]
넓이:25.0 둘레:20.0
맨위의 abstract 클래스가 Shape라는 추상 클래스입니다. 클래스 선언부에 abstract라는 키워드가 붙어 있네요. 이 추상 클래스는 area(), length() 두 개의 메서드가 추상메서드로 구현부(중괄호 블록)가 없고, 선언부만 정의되어 있습니다. 이렇게 추상메서드는 기능은 필요하지만, 구체화 되어야 기능을 구현할 수 있는 경우에 사용합니다. area()는 넓이, length()는 길이를 구하는 기능임을 추상적으로 정의할 수 있지만, 어떤 도형이냐에 따라 넓이, 길이를 구하는 식이 달라지므로 설계단계에서는 메서드를 구현할 수가 없습니다. 어느 도형인지가 정해져야 구현될 수 있는 것이죠. 그래서 Shape 클래스를 상속받아 원이라는 도형이 정해지려면 Circle 클래스는 반드시 abstract 메서드인 area()와 length() 메서드를 구현해야 합니다. 같은 방식으로 사각형 도형을 정의하기 위해 Rectangle 클래스 역시 area()와 length() 메서드를 모두 구현하였습니다. Circle, Rectangle 클래스 모두 Shape라는 추상 클래스를 상속받아 그 틀에 맞춰 구현된것을 알 수 있습니다.
ShapeEx 클래스의 main() 메서드에서는 Shape 자료형의 길이가 2인 배열을 선언하였는데, 다형성의 개념으로 Shape가 부모클래스이기 때문에 Circle, Rectange 클래스 모두 객체를 생성하여 대입할 수 있습니다. 0번 인덱스엔 Circle이, 1번 인덱스엔 Rectangle 객체를 생성해 각각 대입하였습니다. 마지막으로 배열에 있는 각 객체들을 향상된 for문으로 반복하면서 toString() 메서드를 통해 출력하고, 넓이, 길이를 각각 출력하고 있습니다.
그럼, 추상메서드, 추상클래스를 왜 만드는 것일까요? 위 예제도 마찬가지지만 추상클래스나 추상메서드 없이도 프로그램을 구현할 수 있는데, 굳이 클래스를 하나 더 만들면서까지 추상클래스를 만들필요가 있을까?
첫번째로 클래스를 설계할 때 변수와 메서드의 이름을 공통적으로 적용시키기 위함입니다. 유사한 특성을 가진 클래스들을 모아 공통 변수나 메서드의 이름을 통일 시켜 각 클래스에 맞게 재정의하도록 할 수 있습니다.
두번째는 중복 소스들을 줄일 수 있습니다. 상속관계는 기본적으로 모든 변수, 메서드를 물려받기 때문에 개발시간을 줄일 수 있습니다.
그리고 세번째는 다형성의 개념을 적용시킬 수 있어, 소스의 수정이나 변경사항이 있을 때, 전체를 변경하거나 바꾸는 것이 아니라 부품 교체하듯이 특정 클래스만 새 클래스로 바꾸면 쉽게 수정이 가능합니다.
3. final
변수 앞에 사용되었던 final과 마찬가지로 클래스와 메서드 앞에도 final 이라는 키워드를 사용할 수 있습니다. 여기서도 역시 마지막이라는 의미로 생각하면 됩니다.
먼저 final 클래스는 상속이 불가능한 클래스입니다. 즉 다른 클래스의 부모(상위) 클래스가 될 수 없다는 얘기입니다. 대표적인 final 클래스로는 String, Math 등의 클래스가 있습니다.
public class SubClass extends String { // 사용불가
...
}
final 메서드는 재정의(오버라이딩)가 불가능한 메서드입니다. 부모(상위) 클래스에서 해당 메서드를 상속받는 자식 클래스에서 메서드가 변경되지 못하도록 하기 위해 final 키워드를 사용합니다.
package example;
public class FinalMethod {
// 재정의 가능한 메서드
void method() {
}
// 재정의가 불가능한 메서드
final void finalMethod() {
}
}
class SubFinalMethod extends FinalMethod {
void method() { // 재정의 가능
System.out.println("method() 재정의");
}
/*
void finalMethod() { // 재정의 불가
System.out.println("finalMethod() 재정의");
}
*/
}
final 키워드가 없는 method 라는 메서드는 재정의가 가능한 메서드이므로, SubFinalMethod에서 재정의되었습니다. 하지만 finalMethod 라는 메서드 선언부에는 final 키워드가 들어가 있습니다. 자식 클래스인 SubFinalMethod 클래스에서 재정의(오버라이딩) 하려고 하면 에러가 발생하게 됩니다.