본문 바로가기

언어/자바

자바 클래스 구성 시 주의할 점.

정적 변수는 위에서 아래로 초기화가 진행된다.

static class ClassVarTest {
	static ClassVarTest me = new ClassVarTest();
	static int i1;
	static int i2 = 10;

	private ClassVarTest() {
		i1 = i2 -10;
	}
}
    
@Test
public void test10() {
	System.out.println(ClassVarTest.i1);
}

어떤 값이 출력될까? 얼핏보면 i1는 0이고 i2는 10이니 생성자의 연산을 통해서 i1에는 0이 저장되는 것으로 보인다.

0이 출력될까?

 

결과는 -10이 출력된다.

이유는 클래스의 정적 변수를 초기화 하는 순서는 아래와 같다.

1. 모든 정적 변수는 기본값으로 초기화 된다.

2. 위에서 아래로 차례대로 초기화 된다.

 

따라서 생성자 호출이 일어날 때, i2는 순서1에 의해 0으로 초기화된 상태에서 아직 10으로 초기화가 이뤄지지 않은 상태이다.

이 때 생성자를 호출하면 i2 - 10은 0이 아닌 -10이 된다.

 

클래스에 정적변수를 사용할 때는 항상 초기화 순서에 주의하는게 좋습니다.

 

인스턴스 초기화는 생성자보다 빠르다.

class Reluctant {
    private Reluctant internalInstance = new Reluctant();

    public Reluctant() throws Exception {
        throw new Exception("hello");
    }

    public static void main(String[] args) {
        try {
            Reluctant reluctant = new Reluctant();
            System.out.println("end of try");
        } catch (Exception ex) {
            System.out.println("catch");
        }
    }
}

위 코드는 어떤 값을 출력할까?

생성자 호출에서 예외가 발생해서 캐치문에서 잡으면 "catch"만 출력될 것으로 보인다.

 

하지만 실제로는 아무것도 출력이 되지않고 스택 오버플로우가 일어날 것이다.

 

이유는 생성자 호출이 시작되면 인스턴스 초기화가 먼저 이뤄지기 때문이다.

인스턴스 초기화는 멤버 변수 초기화라고 생각하면 된다.

 

즉, 생성자 호출보다 internalInstance변수가 먼저초기화 되는데 다시 생성자를 호출해서 무한 반복에 빠져든다.

다행히 스택의 깊이를 초과하기 때문에 무한 반복은 스택 오버플로우 에러로 인해 끝이난다.

 

메소드 오버로딩을 주의하라

@Test
public void test14() {
    MyTest myTest = new MyTest();
    myTest.hello(null);
}

class MyTest {
    void hello(Object object) {
        System.out.println("hello object");
    }

    void hello(String string) {
        System.out.println("hello string");
    }
}

무엇이 출력될까?

null 이건 아니건 object는 항상 수용할 수 있으니 hello object가 출력 될 것 같기도 하다.

 

실제로는 hello string이 출력된다.

이유는 자바는 오버로딩 메소드를 호출할 때 아래 과정을 거치기 때문이다.

1. 호출할 수 있는 메소드들을 추려낸다.

2. 가장 구체적인 형태의 메서드를 선택한다.

 

1번 과정을 통해 두 메소드 모두 추려진다.

2번 과정을 통해 더 구체적인 메소드를 선택하는데, 이 때 String이 Object보다 구체적이기 때문에 최종적으로는 String 매개변수를 갖는 메소드를 호출한다.

 

메소드를 오버로딩하면 코드는 복잡해진다. 가급적 오버로딩은 꼭 필요하지 않은 이상 사용하지 말고, 사용한다면 이런 오해가 없도록 메소드를 구성하자.

 

 

정적 메소드는 오버라이딩이 안된다.

@Test
public void test11() {
    Animal a = new Animal();
    Animal b = new Dog();

    a.test();
    b.test();
}

class Animal {
    public static void test() {System.out.println("animal");}
}

class Dog extends Animal{
    public static void test() {System.out.println("dog");}
}

무엇이 출력될까?

모두 animal이 출력된다.

이유는 정적 메소드 호출은 컴파일 타임에 결정되기 때문이다.

 

a, b 인스턴스 모두 형태는 Animal이고 이의 정적메소드를 호출할 것으로 컴파일 된다.

정적 메소드는 오버라이딩 되지 않는다. 다만 하이딩이 일어난다.

 

따라서 이러한 혼동을 피하기 위해서는 정적 메소드를 하이딩하지 않길..

 

또한 정적 메소드 호출은 a.test()와 같이 표현식(a를 표현식이라고 말한다)으로 호출하지 말고

Animal.test()와 같이 클래스 이름으로 호출하는 것이 좋다. 오해를 살 수도 있기에..

 

정적메소드는 인스턴스와는 관련이 없다.

@Test
public void test15() {
    ((Animal)null).test();
}

class Animal {
    public static void test() {System.out.println("animal");}
}

null에서 함수를 호출하기 때문에 NullPointerException 이 발생할 거라고 기대할 수 있다.

하지만 test는 정적 메소드이고, 정적메소드는 인스턴스와는 무관하다.

 

따라서 호출이 되고 animal이 호출된다.

 

앞서 말했듯이 표현식 (null)로 호출하면 이러한 혼동이 생긴다.

정적 메소드는 가급적 클래스 이름으로 호출하는 것이 좋다.

 

 

순환 인스턴스 초기화를 피해라

class Point {
    protected final int x, y;
    private final String name;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
        name = makeName();
    }

    protected String makeName() {
        return "["+x+","+y+"]";
    }

    @Override
    public String toString() {
        return name;
    }
}

class ColorPoint extends Point {
    private final String color;

    public ColorPoint(int x, int y, String color) {
        super(x, y);
        this.color = color;
    }

    @Override
    protected String makeName() {
        return super.makeName() + ", color=" + color;
    }

    public static void main(String[] args) {
        System.out.println(new ColorPoint(1, 2, "green"));
    }
}

어떤 값이 출력될까? [1, 2], color=green 이 출력될 것으로 기대된다.

하지만 실제로는 [1, 2], color=null이 출력된다.

 

이유는 순환 인스턴스 초기화 때문이다.

ColorPoint의 생성자가 호출되면 Point의 생성자를 호출한다.

Point에서는 자신의 필드를 초기화하는데, 이 때 makeName 메소드를 호출한다.

실제로 이 객체는 ColorPoint 이기 때문에 오버라이딩된 ColorPoint의 makeName을 호출한다.

 

이 때 color는 부모 클래스 생성자 호출 이후에 초기화 되기 때문에 아직 초기화 되지 않은 상태라서 null이다.

따라서 결과적으로는 null로 출력이되는 것이다.

 

이러한 점 때문에 순환 인스턴스 초기화는 절대 사용해서는 안된다.

또한 오버라이딩 가능성이 있는 메소드를 생성자에서 절대 호출해서는 안된다. 순환 인스턴스 초기화를 나도 모르게 구성할 수도 있기 때문이다.

 

'언어 > 자바' 카테고리의 다른 글

Logback  (0) 2019.12.07
jenv  (0) 2019.09.21
자바 자료형 사용 시 주의  (0) 2019.08.15
자바 표현식 몰랐던 사실 정리  (0) 2019.08.12
[Effective java] equals를 재정의하려거든 hashCode도 재정의하라  (0) 2019.07.24