| 백기선님의 라이브 스터디를 참고하여 작성한 게시물입니다.
참고: Inheritance
상속과 관련된 자바의 여러 특징을 알아보자
여러 클래스를 상속했을 때 여러 문제들이 발생해서 자바는 다중 상속을 지원하지 않는다.
보통 클래스를 선언할 때 extends Object
를 적지 않는다. 그럴때마다 자바 컴파일러가 자동으로 extends Object
를 대신 적어준다.
Object
클래스는 유일하게 superclass가 없는 class이다.
상위 클래스에 대한 정보가 기록되어 있다.
상위 클래스의 이름, 생성자, 데이터 타입등이 기록되어 있다.
상위 클래스의 생성자를 호출(super())하고, 변수를 초기화하는 것 같다.
아래와 같은 구조가 있다.
package test;
public class Parent {
private int a;
int c;
protected int b;
public int d;
public static void main(String[] args) {
Parent parent = new Parent();
System.out.println(parent.a);
System.out.println(parent.b);
System.out.println(parent.c);
System.out.println(parent.d);
}
}
자기 자신에선 당연히 전부 접근 가능
package test;
public class Child1 extends Parent {
public static void main(String[] args) {
Child1 child1 = new Child1();
System.out.println(child1.b); // protected
System.out.println(child1.c); // default
System.out.println(child1.d); // public
}
}
같은 패키지에서 부모를 상속한 자식에서는 private
은 상속 불가능
package test2;
import test.Parent;
public class Child2 extends Parent {
public static void main(String[] args) {
Child2 child2 = new Child2();
System.out.println(child2.d); // public
System.out.println(child2.b); // protected
}
}
다른 패키지에서 부모를 상속한 자식에서는 private
, default
는 상속 불가능
참고: super와 super()
기본적으로 super
는 상속한 클래스에 정의된 인스턴스, 메소드에 접근한다.
그런데 다음과 같은 특이사항이 있다.
class Parent {
int p;
}
class Child extends Parent {
void print() {
System.out.println(super.p); // Parent에 정의된 p에 접근
System.out.println(this.p); // Child에 정의된 p가 없으므로, Parent에 정의된 p에 접근
}
}
class Parent {
int p;
}
public class Child extends Parent {
int p;
public Child(int c) {
super.p = c - 1;
this.p = c - 2;
}
void print() {
System.out.println(p); // 기본적으로 this에 먼저 접근
System.out.println(super.p);
}
public static void main(String[] args) {
Child child = new Child(10);
child.print();
}
}
public class Parent {
public Parent() {
System.out.println("parent constructor");
}
}
public class Child extends Parent {
int a;
public Child() {
System.out.println("child constructor");
}
public Child(int a) {
}
public static void main(String[] args) {
Child child = new Child();
Child child2 = new Child(10);
}
}
지난 주에 공부했던 클래스의 생성자 법칙을 떠올려보면,
1. 생성자는 this() 또는 super()를 호출해야 한다.
2. this() 또는 super()를 호출하지 않으면 자바 컴파일러를 통해 자동으로 super()를 호출한다.
child는 다음 과정을 통해 초기화된다.
1. 생성자 Child()를 호출
2. this() 또는 super()를 호출하지 않음 -> 자동으로 super()호출
3. 생성자 Parent()를 호출 -> 내부적으로 Object의 생성자를 호출
child2는 다음 과정을 통해 초기화된다.
1. 생성자 Child(int a)를 호출
2. this() 또는 super()를 호출하지 않음 -> 자동으로 super()호출
3. 생성자 Parent()를 호출 -> 내부적으로 Object의 생성자를 호출
하나 이상의 생성자가 있으면, 컴파일러는 default constructor를 생성해 주지 않는다. 그러므로 다음과 같은 상황에 유의해야 한다.
class Parent {
int a;
Parent(int n) { a = n; }
}
class Child extends Parent {
int b;
Child() {
super(); // Error: 실행할 생성자가 없음
b = 20;
}
}
다음 코드의 바이트 코드를 살펴보자
package inheritance;
public class Parent {
void overrideMe() {
}
}
public class Child extends Parent {
@Override
void overrideMe() {
super.overrideMe();
}
public static void main(String[] args) {
new Child().overrideMe();
}
}
크게 특이한 내용 없이, 부모 클래스의 메소드를 호출한다.
보통 IDE의 도움을 받기 때문에 오버라이딩을 할 때 실수할 일이 많지는 않다.
그렇다고 프로그래밍 언어 측면에서 실수 가능성이 사라진건 아니다.
자바5 부터 Annotation을 지원하기 시작했는데, Annotation 중 @Override
를 심심치 않게 발견할 수 있다.
@Override
자바 컴파일러에게 해당 메소드는 Override된 것이야
라고 알려주는 역할이다. 이를 통해 컴파일 타임에 잘못된 override를 검출할 수 있다.
Annotation에 대해선 추후에 자세히 다루도록하겠다.
특정 메소드를 하위 클래스에서 오버라이딩 하지 못하게 하고싶다.
그런데 상속 계층이 복잡해지다 보면 뭐가 뭔지 구분이 안 되는 경우가 종종 있다.
메소드에 final
키워드를 붙이면 하위 클래스에서 override하지 못하는 메소드임을 자바 컴파일러에게 알려줄 수 있다.
final class MyFinal {} // 해당 클래스를 상속할 수 없음
class Parent {
final void myMethod() {} // 해당 메소드를 하위 클래스에서 override할 수 없음
}
class Child extends Parent {
@Override
void myMethod() {} // Error
}
그 외에도 variable에 사용하여 최초의 초기화 이후에는 값을 변경할 수 없는 변수임을 나타낸다.
class MyClass {
final int a = 10; // 변경 불가능
final boolean b; // 생성자 또는 초기화 블록등에서 반드시 초기화를 해줘야 한다 (blank final)
MyClass(boolean b) {
this.b = b;
}
void myMethod(fianl int a) { // 파라미터에도 final 키워드를 사용할 수 있다.
final int a = 10;
a = 20; //Error
final MyClass myClass = new MyClass(false);
myClass.setB(true); // not Error
myClass = new MyClass(true); // Error
}
}
고정 불변하는 상수값을 표현하기 위해 static
을 같이 사용하면 메모리 측면에서 이득이 있다고 한다.
class Parent {
static void print() {
System.out.println("parent");
}
}
// case1: 자식에서 override 시도
class Child extends Parent {
static void print() {
System.out.println("child");
}
public static void main(String[] agrs) {
Child.print(); // child
}
}
// case2: static method가 상속이 안되는건 아니다. 단순히 override할 수 없을 뿐
class Child extends Parent {
public static void main(String[] agrs) {
Child.print(); // parent
}
}
부모의 static method와 자식의 static method는 별개로 인식한다.
@Override를 붙이면 컴파일 에러가 작동한다.
즉, static method는 override할 수 없다.
오버로딩: 메소드 이름은 같으나 파라미터 개수, 타입이 다른 상황 / 리턴 타입은 영향 X
메소드 오버라이딩은 자바가 Runtime Polymorphism
을 지원하는 방법 중 하나이다.
Dynamic method dispatch
란 오버라이딩 된 메소드를 컴파일 타임이 아닌, 런타임에 결정(resolve)하는 메커니즘을 의미한다.
오버라이딩된 메소드가 superclass reference을 통해 호출될 때, 자바는 메소드를 호출한 객체의 타입에 따라, '호출 시점'에 상위 클래스, 하위 클래의 메소드중 어떤 메소드를 실행할지 결정한다.
이때 중요한 점은, 변수의 타입이 아닌, 인스턴스의 타입에 따라 실행할 메소드가 결정된다는 사실이다.
public class Parent {
void print() {
System.out.println("parent");
}
}
class Child extends Parent {
@Override
void print() {
System.out.println("child");
}
public static void main(String[] args) {
Parent p1 = new Child(); // upcasting
Parent p2 = new Parent();
p1.print(); // child
p2.print(); // parent
}
}
상위클래스의 변수로 하위 클래스 객체를 가리킬(refer)수 있는데, 이를 upcasting
이라고 한다.
자바는 이 특성을 이용해 override된 method를 resolve한다.
Static binding은 컴파일 타임에 바인딩이 이뤄지는 방식을 의미한다.
자바의 private
, final
, static
메소드와 varaible
은 static binding 방식을 통해 컴파일 타임에 binding된다.
이처럼 동적으로 실행할 method가 resolve되는 특징은 일반(?) 메소드에만 적용된다.
public class Parent {
int a = 10;
void print() {
System.out.println("parent");
}
}
class Child extends Parent {
int a = 20;
int b = 30;
@Override
void print() {
System.out.println("child");
}
public static void main(String[] args) {
Parent p1 = new Child(); // upcasting
Parent p2 = new Parent();
p1.print(); // child
p2.print(); // parent
System.out.println(p1.a); // 10
System.out.println(p2.a); // 10
}
}
Parent p = new Child();
System.out.println(p.a); // 10
System.out.println(p.b); // Error : Parent타입 변수에는 b라는 varaible이 없다
// Down Casting 실시
Child c = (Child) p;
System.out.println(c.b); // 30
추상 메소드
를 하나라도 가지고 있는 클래스를 의미한다.
추상 메소드란 자식 클래스에서 반드시 오버라이딩해야만 사용할 수 있는 메소드를 의미한다.
public abstract class MyAbstract {
abstract void myAbstractMethod(); // method body없이 header만 정의했다.
void myNotAbstractMethod() { // abstract 클래스는 abstract method만 가지는건 아니다.
...
}
}
class Child extends MyAbstract {
@Override
void myAbstractMethod() { // MyAbstract의 추상 메소드를 전부 override하지 않으면 컴파일 에러
...
}
}
인터페이스와의 차이점은 인터페이스 파트에서 다루도록 하겠다.
일반적인 메소드와 달리 Code
부분이 없다.
아마 인터페이스의 메소드도 비슷한 형태를 띠지 않을까 싶다. 인터페이스 파트에서 살펴보도록 하겠다.
참고: Object 클래스
모든 클래스의 부모 클래스(최상위 클래스)
연산자 파트 공부를 대충해서 지금 벌 받는다.
비교 연산자(==)는 primitive type data에 대해서는 값 비교, reference type data에 대해선 주소값 비교를 수행한다.
primitive data의 값 비교도 사실 주소값 비교에 기반한다.
constant pool에 값이 저장되어 있고, primitive data을 할당받은 변수들은 결국 동일한 constant pool주소를 가리키고 있으므로, ==연산을 수행해도 의도대로 작동한다.
반면 reference type data는 각 데이터가 heap 영역에 별도로 생성, 할당되므로 == 비교를 사용할 수 없다. 해도 되는데 의도대로 작동하진 않을 것이다.
Object 클래스의 equals는 내부적으로 == 연산자를 사용한다. 즉, Object의 equals는 비교 연산자와 동일하게 작동한다.
public class Object {
public boolean equals(Object obj) {
return (this == obj);
}
}
따로 equals를 override하지 않으면 Object의 equals를 사용한다.
스트링은 ==으로 비교하지 마라. 대신 equals로 비교하라
String 클래스는 equals를 override해서 주소값 비교가 아닌, 진짜 문자열 비교(?)를 수행한다.
System.out.println("123".equals("123")); // String의 equals는 진짜 문자열 비교(?)를 수행 -> true
System.out.println("123" == "123"); // constant pool의 동일한 주소를 가리킨다 -> true
String s = new String("123"); // 변수 s는 heap 영역에 저장
String s1 = new String("123"); // 변수 s1는 heap 영역에 저장
System.out.println(s == s1); // s의 주소 != s1의 주소 -> false
System.out.println(s.equals(s1)); // String의 equals는 진짜 문자열 비교(?)를 수행 -> true
참고
[java] hashcode()와 equals() 메서드는 언제 사용하고 왜 사용할까?
Guide to hashCode() in Java
Object 클래스의 hashcode
는 주소값을 hash한 결과인 int값을 반환하는 native로 작성된 메소드이다.
이걸 어디에 쓰나 할 수 있는데, 생각보다 많은 곳에 쓰이더라.
대표적으로 hashXXX(hashMap, hashSet, hashTable, ...)에서 쓰인다.