class Cow extends Animal {
...
}
위에서 Cow
가 child class, Animal
이 parent class다.
parent class의 attribute와 method는 child class도 그대로 상속 받는다.
child class가 이를 사용 가능하다랑은 별도의 의미다. 접근 제어자가 뭐냐에 따라 child가 parent로부터의 attribute/method를 사용하진 못할 수 있다. 자세한건 후술. 일단 기본적으로 접근 및 사용이 가능하다는 것만 알자.
parent class의 constructor과 initialization block은 child class가 상속 받지 않는다.
child class의 그 어느것도 parent class가 상속받진 않는다.
child class도 parent class가 될 수 있다. 밑에서 Cow
는 MilkCow
의 parent class다. 또 Animal
이 MilkCow
의 ancestor이라고 볼 수 있다.
class MilkCow extends Cow {
...
}
class Nose {
...
}
class Face {
class Nose = new Nose(); //composite relationship
...
}
역시나 코드 재사용에 도움이 많이 된다.
어떤 class가 다른 class와 composite relationship을 가질지, inheritence relationship을 가질지는 말 그대로 서로 포함되는 관계인지, 상속되는 관계인지 고려하면 된다.
C++과 다르게 Java는 여러 parent class로부터의 inherit을 불허한다.
이유는 inherit하는 서로 다른 두 parent에서 같은 이름의 method를 가질 경우 (static method가 아니면) 구별할 방법이 없기 때문이다.
각 방식이 일장일단이 있어서 뭐가 더 낫다고 하긴 애매한 것 같다.
우회 방법은 여러가지가 있는데, 후술할 interface의 multiple inheritence를 활용하거나 다른 inherit하고 싶은 class들을 composite relationship 형태로 추가하는 것이다.
어떤 class가 다른 class를 inherit하지 않을 경우 자동으로 inherit되는 class.
밑의 첫번째 코드는 실제로는 두번째 코드처럼 취급된다고 생각하면 된다.
class TV {
...
}
class TV extends Object {
...
}
toString()
, equals(Object o)
가 대표적인 method들이다. 추후 더 알아볼 것이다.어떤 class가 본인의 ancestor, 그러니까 parent class들을 따라 올라가다보면 도달 할 수 있는 class들의 method 정의를 변경시키는 것.
상속관계가 있는 class들끼리의 비슷한 역할의 함수들에 대해 따로 이름을 부여할 필요가 없다는 장점이 있다. 코드를 더 직관적으로 만드는데도 도움이 된다. 이름 지을 필요가 없다는건 진짜 큰 장점이다. 진.짜.큰.장.점.이.다.
overriding시, override할 method와 이름, parameter type, parameter 개수, return type이 완전 동일해야 한다.
access modifier, 그러니까 접근 제어자의 경우 변동이 가능하긴 하나 더 넓게만 변동이 가능하다.
overriding하는 method보다 더 많은 exception을 던지도록 정의하면 안되나, 더 적게 던지는건 가능하다.
overriding하는 method보다 더 포괄적인 exception을 던지도록 정의하면 안되나, 더 좁은 exception을 던지는건 가능하다.
class Animal {
void method1() throws IOException, SQLException {
...
}
}
class Cow extends Animal {
void method1() throws Exception { //error
...
}
}
class Parent {
static int method1() {
return 0;
}
}
class Child extends Parent {
int method1() { //error
return 1;
}
}
class Parent {
int method1() {
return 0;
}
}
class Child extends Parent {
static int method1() { //error
return 1;
}
}
parent의 static method와 동일한 꼴의 static method를 정의하면 이건 parent class와 별도인 method를 그냥 정의한것이지 overriding은 아니다. 각 class별로 각각의 static method가 종속되었다고 생각하면 된다. compile time에 뭐가 실행될지 결정되기 때문에 그렇다.
overriding을 overloading으로 헷갈리지 말자. overloading은 child class가 본인 ancestor에 '없는' method를 새로 정의하는 것을 말한다.
child class가 ancestor의 attribute, method를 참조하는데 사용된다.
사실 this
로도 접근이 가능하다. 보통 overriding 등으로 인해 동일 method/attribute에 여러 정의가 있을 때 구별을 하기 위해 super
을 사용한다.
예시로 첫 코드는 둘 다 10을 print하나, 두번째 코드는 처음은 20, 두번째는 10을 print한다.
class Parent {
int x = 10;
}
class Child {
void testPrint() {
System.out.println(this.x);
system.out.println(super.x);
}
}
class Test {
public static void main (String[] args) {
Child c = new Child();
c.testPrint();
}
}
class Parent {
int x = 10;
}
class Child {
int x = 20;
void testPrint() {
System.out.println(this.x);
system.out.println(super.x);
}
}
class Test {
public static void main (String[] args) {
Child c = new Child();
c.testPrint();
}
}
이것도 this
처럼 instance랑 연관된 녀석이라서 static method에서는 사용이 불가능하다.
this()
랑 유사한 super()
도 있다. ancestor의 생성자 접근에 사용이 된다.
child class가 본인 parent class의 생성자를 호출하지 않은 경우 자동으로 super();
이라는 코드를 집어넣는다. (지금까지 그래왔다.)
child class 차원에서 꼭 parent class의 생성자를 호출해가지고 parent에 있는 관련 attribute들이 적절히 initialize되도록 해야 한다. 잘못하면 오류가 나올 수 있다.
제어자. class, variable, method declare시 앞부분에 붙는 keyword들을 전부 말하며 access modifier과 그렇지 않은 modifier로 나뉘어진다.
access modifier은 하나의 대상에 대해 한종류만 사용이 가능하며, 나머지는 여러개를 붙여도 상관이 없다.
static
final
변동 불가를 나타내는 modifier.
class 앞에 final
이 붙으면 해당 class는 다른 class의 ancestor이 될 수 없다.
method 앞에 final
이 붙으면 해당 method를 override할 수 없다.
variable 앞에 final
이 붙으면 값 변동이 불가능하다.
class의 attribute에 final
을 붙이면 꼭 선언시 초기화를 할 필요는 없고 constructor에서 초기화를 해도 된다.
abstract
미완성된 method와 class를 나타낸다.
class 앞에 abstract
가 붙으면 해당 class가 abstract method를 가진다는걸 의미한다. 해당 class의 instance를 만드는건 불가능함.
method 앞에 abstract
가 붙으면 abstract method가 되며, 아무 정의가 존재하질 않는다. overriding을 통해 child가 정의를 해줘야 하는데 용도는 후술.
사실 abstract class에 abstract method가 없는것도 가능하다. 대신 아무것도 정의가 안 된 method들만 존재. 굳이 이러는 이유는 사실 abstract method가 있는 class를 상속받는 child는 parent의 '모든' abstract method가 수행할 코드를 정의해야 하는데, 이렇게 abstract method가 없고 선언들만 있으면서 abstract class를 통해 instantiate는 막아놓으면, 의도치 않은 instantiation은 막으면서 child가 '원하는' method들만 overriding을 하는 것이 가능하다.
단 abstract class가 아닌 class에 abstract method가 존재하는건 불가능하다는 점 유의
(혹시 이미 알고 있다면) interface와 엄연히 하는 역할이 다르다. 후술.
class, attribute, method, constructor의 접근 범위를 지정한다.
private
이 붙으면 같은 클래스 내에서만 해당 요소를 접근할 수 있다.
public
이 붙으면 접근 제한이 없다.
protected
가 붙으면 같은 패키지와 다른 패키지의 child class만 해당 요소 접근이 가능하다.
아무것도 안 붙은 경우 default
취급인데, 이 경우 같은 패키지 내에서만 해당 요소 접근이 가능하다.
encapsulation 보장을 위해 존재한다.
method에 static과 abstract를 같이 사용하는건 불가능하다.
class에 abstract과 final을 동시에 사용할 수 없다. 의미가 없기 때문.
abstract method에 사용되는 access modifier이 private이면 안된다. 의미가 없기 때문.
method에 private과 final을 같이 사용하는게 가능은 하지만 의미가 없다.
ancestor class의 reference가 들어가야 하는 곳에 child class의 reference가 들어가는 것을 허용하는 기능이다. 직관적으로 있을만한 기능인데, hierarchy에 따라 child class를 ancestor class처럼 생각하는게 문제가 없기 때문이다.
예를 들어 Cow
가 Animal
의 child class면 다음이 가능하다.
Animal a = new Cow();
단 cow에 대한 reference를 생성했음에도 불구하고, a로는 Animal과 관련된 method 및 attribute만 참조하는 것이 가능하다. type에 의해 a가 Animal 취급을 받기 때문.
이 기능이 왜 유용한지는 나중에 보도록 하겠다.
polymorphism과 연관된 개념이 바로 type casting이다.
어떤 type을 '연관된' 다른 type으로 취급하는 operation이라고 생각하면 된다. 여기서 연관의 기준이 되는 관계는 hierarchy. 이때 '취급'이 변환되는 것이지 내용물의 변동은 없다는 점에 조심하자.
위의 polymorphism에서 예시로 든 코드는 up-casting의 예시로, child class가 ancestor class로 취급 되는 경우인데 이 경우에는 explicit하게 type을 표기할 필요가 없다.
다음 코드를 보도록 하자. Cow
가 Animal
의 child class라고 해보자.
Cow c = new Animal(); //error
직관적으로 위 코드가 동작해선 안되고, 실제로 컴파일 오류가 나온다. 왜냐하면 Aniaml
에 정의되지 않으면서 Cow
에 정의된 method나 attribute가 존재하는 것이 가능하기 때문.
위와 같이 parent class를 child class type으로 취급하는 것을 down-casting이라고 한다. 그러면 down-casting은 java에서 불가능한걸까? 충분히 가능하며, 이 경우 explicit하게 type을 표기해야 한다.
단, 변환을 하려는 parent class로 취급되는 object가 실제로 처음 initialize 되었을 때의 type이 child class이었던 경우에만 가능하다. 직관적으로 우리가 이미 '소'인 것을 아는 객체가 동물 취급 중일 때 이를 다시 소로 세부적으로 취급하는 것이 괜찮은 것이지, 소인지 모르는 동물을 소로 취급하거나, 고양이었던 동물이 동물로 취급되다가 갑자기 소로 취급을 하는 것은 말이 안 되기 때문이다.
즉 down-casting 기능이 java에서 존재하는 이유는 특정 class로 취급 되는 object가 '실제로' 현재 취급되는 class를 ancestor로 가지는 하위 class에 해당되었던 경우에 그 하위 class로 취급하기 위해서 쓰이는 것이다. 좀 복잡해보이는 이 기능이 나중에 이게 어떻게 활용되는지 볼 것이다.
이 때문에 밑의 첫번째 코드는 가능하지만, 두번째랑 세번째 코드는 불가능하다.
Cow c1 = new Cow();
Animal a = c1;
Cow c2 = (Cow) a; //ok
Cow c = (Cow) new Animal(); //runtime error : ClassCastException
Cat c1 = new Cat();
Animal a = c1;
Cow c2 = (Cow) a; //runtime error : ClassCastException
Animal
을 extend하는 Dog
, Cat
가 존재시 다음은 불가능하다.Cat c1 = new Cat();
Cow c2 = (Cow) c1; //compile error
double
과 int
의 경우 표기에 사용하는 format이 다른데 어떻게 취급만 다르게 할 수 있겠는가? 아예 값들을 그 format에 맞춰서 변환을 해야 한다. 그러면 정수끼리는 취급만 다르게 해도 되지 않냐고? Java는 모든 정수가 부호가 있다고 가정하기 때문에, 다른 정수로 변환을 하면 sign extension을 하거나 상위 bit를 drop해야 하기 때문에 이것도 conversion이라고 볼 수 있다.instanceof
instanceof
를 사용한다.void makeSound(Animal a) {
if (a instanceof Cow) { System.out.println("moo"); }
else if (a instanceof Cat) { System.out.println("meow"); }
else { System.out.println("ow!"); }
}
Animal
과 이를 extend하는 Cow
가 있고 다음과 같이 정의되어 있다 해보자.class Animal {
int x = 1;
int method() { return 1; }
...
}
class Cow extends Animal {
int x = 2;
int method() { return 2; }
...
}
Animal a = new Cow();
Cow c = new Cow();
System.out.println(a.x);
System.out.println(c.x);
System.out.println(a.method());
System.out.println(c.method());
1
2
2
2
void buy(Product p) {
money = money - p.price;
}
buyer
이라는 class에 위와 같은 함수가 존재하고, Product
class에 구체적인 제품들로 나뉘는 여러 class가 존재시, 그 제품들을 그대로 저 buy의 parameter로 집어넣어가지고 활용이 가능하다. Product
class의 하위 class 하나하나에 대해 buy
method를 선언하는것보다 훨신 효율적.
또 array를 특정 class에 대한 배열로 선언한 후, 그것의 child class들을 배열에 다 넣는 것도 polymorphsim 덕분에 가능하다.
Product[] ps = new Product[3];
p[0] = new Tv();
p[1] = new Computer();
p[2] = new Desk();
abstract method를 가진 class...라고 앞에 얘기했었다. abstract method는 선언만 되었고 실제 implementation이 없는 method를 말한다.
abstract class의 child class는 abstract method를 꼭 다 implement 해줘야 한다.
보통 해당 class에 있는 구성원이 다들 가지고 있는 behavior이 있는데, 이게 그 class내에서 어떤 세부 class냐 (child class냐)에 따라 달라지는 경우에 사용된다.
혹자는 위의 이유에 대해 그냥 default behavior을 지정해놓고 각 child class마다 overriding을 하면 되지 않냐고 할 수 있는데, 강제로 implement를 해줘야 한다는 점에서, 프로그래머가 새로운 child class를 만들었을 때 관련 수정사항을 까먹지 않을 수 있도록 notify하는 역할도 준다. 사람은 잘 까먹으니까
특정 class에 대한 array라고 선언된 녀석에 하위 class object들만 집어넣고, array의 type으로 선언된 class의 abstract method를 호출하면, 앞에 말했듯이 무슨 type으로 취급되든 객체가 생성되었을 때 사용된 class가 override한 method가 사용되기 때문에 하위 class들의 각기 override된 method를 호출하는 응용도 가능하다.
abstract class의 하위 개념으로 볼 수 있다.
다만 JDK 1.7이하에서는 abstract method랑 constant만, JDK 1.8이후는 여기에 더불어 static/default method를 가질 수 있다는 것이 특징이다. 이미 instantiate된 method나 변동가능한 attribute를 보유하는 것이 불가능. 한마디로 class의 틀 역할을 가진다고 보면 된다.
틀을 사용한 class에서 정의된 상수를 못쓰는 것은 의미가 없고, 그 상수가 변동될 일도 없으니 모두가 공유해도 상관이 없기에 interface의 상수는 무조건 public static final
형태다. 꼭 저걸 앞에다 다 적을 필요는 없다.
interface를 특정 class가 사용할거면 implements
를 사용하면 되며, 여러개의 class 상속이 불가능한것과 달리 여러개의 interface를 implement하는 것은 가능하다.
어떤 class를 interface로 만들지가 고민이라면, 보통 공통된 '기능'들을 나타낼 때 사용된다고 생각하면 된다. 그것도 그 '기능'이 구체적으로 무엇을 하는지가 각기 class마다 다른 경우에 그렇다.
그러면 그 기능들을 가진 class들의 parent class에다가 기능들을 abstract method 형태로 집어넣으면 되지 않냐고 할 수 있으나, 프로그램 구조에 따라 공통된 기능을 가지고 있는 class들이 동일한 parent class를 가지는 것이 말이 안 될 수가 있기에, hierarchy랑 상관없이 무조건 implement가 가능한 녀석이 필요해 얘가 등장한 것이라고 생각하면 된다.
그리고 여러개의 parent class를 가지는게 안되는 Java에서 우회법으로 사용되는 것도 가능하다.
굳이 이름이 interface인 이유는 특정 method가 어떤 기능을 하는지만 대충 알려주고, 구체적인 구현부는 상황마다 다르게 하면서 사용자에게 숨기는 용도인데 이게 보통 interface의 역할이기 때문이다. abstraction을 한다고 생각하면 된다.
A
라는 class가 만약 B
라는 interface를 implement한 경우, A
를 B
로 up-casting하는 것도 가능하다. 그리고 이를 활용한 polymorphism도 전부 가능하다.
이 polymorphism을 활용해 B
라는 interface를 받는 method를 만일 만들었을 경우, 거기에 들어가는 parameter은 꼭 B
라는 interface를 implement한 class이어야 한다.
또 B
라는 interface를 반환하는 method도 물론 만드는게 가능한데, 이 경우 B
를 구현한 어떤 class type을 반환한다는 것이지 B
라는 interface의 instance를 반환하는 것은 아니라는 점 참고. interface는 애초에 instance를 가질 수가 없다.
사실 interface 안에 method를 추가해야 하는 경우가 생길 수 있다. 어지간하면 그런 경우를 방지하는게 맞긴 하나 어쩔 수 없을 때가 있긴 하다. 뭐 거기까지 생각을 처음 구상할때 못했을 수도 있고
이때 추가하는 경우 abstract method만 허용되던 시절에는 무조건 해당 method를 abstract method로 추가해야 했는데 그러면 기존에 interface를 implement한 애들이 전부 다 그 abstract method를 다시 implement해야 한다.
사실 저렇게 추가하는게 귀찮더라도 맞을 수도 있다고 볼 수는 있다만 결국 JDK 1.8에서 default method를 허용, 새로운 method를 추가할때 이를 default로 추가할 수 있도록 해서 기존에 interface를 implement한 녀석들을 단체로 수정해야 하는 일을 방지할 수 있도록 했다. 만약 바꿔야 한다면 overriding을 하면 된다.
이 때 새로 method를 추가했더니 그게 다른 interface의 method랑 이름이 같거나, implement한 측의 ancestor class에서 implement한 method랑 이름이 같을 수 있는데, 전자의 경우 overriding이 선택이 아닌 필수가 되며, 후자의 경우 ancestor class의 method를 사용하도록 결정된다. 귀찮다면 그냥 사용하고 싶은걸로 맨날 overriding하고 있으면 된다. 나름 양날의 검인 method이기 때문에 (default method랑 다른 method를 implement해야 하는 경우를 까먹을 수도 있음.) 유의하면서 사용하자.
class 안에 선언되는 class를 inner class라고 한다.
굳이 왜 이런 기능을 만든것이냐고 물어볼 수 있는데, 특정 class 안에서'만' 사용되는 object들이 특정 특성을 가지는 경우에, 외부에서 이를 보여주지 않고 사용하기 위해서다. 즉 특정 class에서 사용되는 공통된 특성을 가지는 object들을 abstract한 다음에 그 class 안에서만 사용할 수 있도록 encapsulate한것이라고 보면 된다. C++에서 이미 class 안의 class나 struct같은 것을 많이 만들어 봤을텐데 그거랑 비슷한 기능이다.
이 내부 class가 어느 위치에서 선언되었냐에 따라 instance/static/local/anonymous class로 분류가 된다. 이름 그대로의 유효성 및 접근성을 가진다는 것도 참고.
특히 inner class가 instance/static class일 때 외부의 static/instance attribute/method를 사용할 수 있냐 없냐도 일반적인 class가 instance/static class일 때 본인의 static/instance attribute/method를 사용할 수 있냐 없냐랑 완전히 동일하다는 점 참고.
anoymous class는 class가 선언된 동시에 initialize가 된다. 이 때문에 해당 class의 instance는 하나만 등장하는게 가능하다. 또 본인 정의시에 사용할 class나 interface 이름을 가지고 initialize를 해가지고 여러개의 interface를 동시에 implement하거나 하나의 class를 extend하면서 추가적인 interface를 implement하는 것이 불가능하다.