Effective Java Review

1편이랑 이어지는 2편!

<br>

8. 계승이야기

  • 메소드 호출과 달리, 계승은 캡슐화의 원칙을 위반한다.
  • 하위 클래스의 정상작동이 상위클래스에 의존적이기 때문이다.
  • 따라서, 상위클래스가 수정했을 때, 하위 클래스도 망가질 가능성이 높다.
  • 사용하는 라이브러리의 클래스를 계승받아 커스텀 클래스를 구현한다면, 그 라이브러리의 버전이 바뀐다해도, 커스텀클래스가 온전히 동작한다고 확신할 수 있을까?
  • 상위클래스의 메소드를 재정의 했을 때, 온전히 동작한다고 할 수 있을까?, 예를들어 HashSet.addAll()HashSet.add()를 호출한다. HashSet을 상속받아 add()를 재정의 했을 때, 상위클래스의 HashSet.addAll()은 온전히 동작할 수 있다 확신 할 수 있을까?
  • 계승받은 자식클래스에서 정의한 메소드가, 상위클래스에 추후에 정의 안된다고 할 수 있을까? int a()라는 메소드를 정의했을때, 상위클래스에서 정말운이없게 String a()를 정의한다면? 안전하다고 할 수 있을까?
  • 계승은 이런 많은 문제를 끌어 안고 있다, 문제가 될 가능성이 다분하다..
  • 따라서, 계승은 상당히 많은 제약사항을 두고 있으며, 주의해야할 개념이다.
  • 이 문제를 해결할 수 있는 방법은 위임이라는 개념을 사용하는 것이다!
  • 클래스를 계승받지 않고, 클래스를 사용하는 것이다.
  • 상위클래스에 의존적인 하위클래스가 아닌, 참조를 하는 클래스를 정의하는것이다.
  • HashSet계승 받지 않고, HashSet참조하는 MyHashSet을 정의한다, MyHashSet.add() 호출은, 참조하는 hashSet.add()를 호출하도록하자.
  • 하지만 위임이 아닌 계승을 필요로 하는 상황은 주어질 것이다..
  • 따라서 계승을 할 수 있는 메소드를 정의 할 때는, 이 메소드가 어떻게 지금 현재 사용하고 있는지를 문서화해서 명확히 명시해야한다. 추후 어떠한 클라이언트 코드가 계승을 하려한다면, 충분히 고려 할 수 있도록 말이다!
  • 또한, 계승을 할 수 있는 필드, 메소드를 정의할때는 신중해야하며, 충분히 하위클래스를 구현해보고 테스트 해봐야한다.
  • 계승 가능한 클래스를 정의함에있어서 다음을 기억하자.
  • 생성자에서는 재정의 가능 메소드를 호출하면 안된다, 하위클래스에서 메소드를 재정의하면, 생성자에 문제가 생길 가능성이 높다.
  • clone, readObject에도 재정의 가능한 메소드를 사용하면 안된다.

<br>

9. 인터페이스 이야기

  • 추상클래스보다는 인터페이스를 사용하자
  • 인터페이스는 추상클래스보다 많은 장점을 가지고 있다.
  • 구현체는 여러 인터페이스를 혼합해 구현 할 수 있다.
  • 또 인터페이스는 클래스와 달리 비 계층적 요소 이고, 계층이라는 개념을 고려하지 않고, 유연하게 사용 할 수 있다.
  • 추상클래스는 계승의 방법만을 제공하지만,
  • 인터페이스는 포장클래스 숙어를 제공한다.
  • 포장클래스 숙어는 계승이 아닌, 앞서말한 위임의 개념을 말한다.
  • 인터페이스의 공통적으로 사용하는 메소드를 정의하는 방법이 없는 것인데, 이는 추상 골격 구현 클래스 를 사용함으로써 추상클래스와 인터페이스의 장점을 활용 할 수 있다.
  • 추상 골격 구현 클래스Abastractprefix로 두어 보통 정의하는데, 다음과 같은 방법을 사용하자. ex) AbstractList
  • 다른 메소드를 구현할 때 정의할 기본 메소드를 추상메소드로 정의한다.
  • 다른 메소드들은 추상 골격 클래스에서 구현하자.
  • 예를 들어, Map 인터페이스를 추상 골격 구현 클래스로 구현한다면
  • 다른메소드들이 사용하는 getKey(), getValue()와 같은 기본메소드는 추상메소드로,
  • blabla()와 같이 기본메소드를 사용하는 메소드는 구현한다.
  • 인터페이스를 사용함에 있어 주의해야할 점이 있다!
  • 인터페이스는 한번 공개되고 널리 퍼지면, 수정이 거의 불가능함을 명심하자. 인터페이스의 메소드를 정의하고, 메소드 네임을 바꾼다면, 인터페이스의 구현체의 메소드는 모두 수정되어야 할 것이다.
  • 인터페이스를 항시 자료형으로 정의하는데 사용하자.
  • 인터페이스로 값을 참조하자. 추상화된 참조는 옳다.
  • 상수 필드 가지는 인터페이스를 정의해서는 안된다!
  • 클래스가 어떤 상수를 가지는것은 구현이며, 인터페이스의 역할이 아니다.

<br>

10. 클래스 이야기

  • 클래스의 특정 필드값으로, 클래스를 태그를 달아주지 말자.
  • 예를 들어, Figure 클래스에 모양을 정의하는 Shape 필드를 태그로서 정의해서, 이 태그값을 분기해서 사용하는 로직을 구현하지 말자.
  • 태그가 아닌 계층을 만들어 Rectangle extends Figure, Circle extends Figure를 정의하자.
  • 태그 기반 클래스는 난잡하면, 문제를 야기할 확률이 높다.
  • 람다식을 사용하자.
  • 내부 클래스는 가능하면 static class로 정의하자.
  • 내부 클래스를 non-static class로 정의한다면, 항상 바깥 객체의 참조를 유지하게되고, 시간과 공간의 요구량이 늘어난다.

<br>

11.제네릭이야기

  • 인자가 없는 제네릭을 쓰지말자. 형안전성을 잃게된다. List, Set 쓰지마!
  • 다만, 클래스 리터럴에는 사용가능하다 List.class
  • instance of에도 사용가능하다. if(a instance of Set) ~~
  • @SuppressWarning 어노테이션을 사용해, 경고를 제거하자
  • @SuppressWarning는 가장 최소한의 범위에 적용해야한다.
  • 제네릭은 배열대신 리스트를 써야됨을 명심하자
  • String[]Long을 넣는다면, 예외가 발생한다.
  • 다음 경우는 어떨까?
  • 제네릭은 배열을 쓸 수 없지만, 하지만 배열을 쓸수 있다고 한번, 가정해보자
  • List<String>[] stringLists = new List<>();
  • List<integer> intList = new List<>()
  • Object[] objects = stringLists
  • objects[0] = intList
  • List<String>[]List<Integer>를 삽입 할 수 있다. 문제가 발생한다!!!!
  • 왜 그럼 제네릭배열과 궁합이 맞지 않을까?
  • 우선 배열과 제네릭의 차이점을 이해해야 한다.
  • 배열은 변하는 자료형이다. 즉, Integer[]Objecrt[] 가 될 수 있다.
  • 반대로 제네릭은 불변 자료형이다. List<Integer>List<Object>가 될 수 없다.
  • List<String>[]의 상위클래스인 Object[]에, 실행시점에는 List<Integer>, List<String>도 삽입될 수 있는 것이다!
  • 또 배열은 실체화 자료형이다. 말이 어렵지만, 실행시점에도 Integer[] 라는 자료형을 그대로 가지고 있다는 것이다.
  • 반대로, 제네릭은 삭제과정을 가진다, 말이 좀 어렵지만 List<Integer>는 실행 시점에 List<E>로 변경되고, 자료형의 정보가 삭제된다는 것이다. ( 삭제라는 과정이 있기때문에, 제네릭이 유연함을 가질 수 있는 것이다.)
  • 번역하면, 리스트가 아니더라도 제네릭 타입에 Integer를 주입하더라도, 실행시점엔 E로 변경된다.
  • 다시 한번 번역하면, 제네릭 타입 배열에 Integer[]를 주입하면, 실행시점에는 E[]로 변경된다, 아니 안된다!
  • 앞서 말했듯이, 배열은 실체화 자료형이기 때문에, 이는 허용되지 않는다.
  • 따라서 제네릭은 배열을 쓰면 안된다. 궁합이 안맞는다. 리스트를 사용해야한다, 애초에 컴파일도 안된다..
  • 하지만 이 지침은 항상 옳은것은 아니다.
  • 제네릭 Stack<E> 클래스를 보면 내부적으로 (Object[]) 배열을 사용하고 있다.
  • 아니 왜 제네릭으로 받아서, Object[]로 치환해서 사용하지?
  • 왜냐하면 리스트는 자바의 기본자료형이 아니기 때문이다.
  • 예를들어 ArrayList 같은 제네릭 타입은 배열을 이용해 구현해야 한다.
  • 예를들어, HashMap은 성능 이슈 때문에 배열을 이용해 구현해야 한다.
  • 가능하면 제네릭 클래스, 제네릭 메소드를 사용해 유연함을 늘리는 것이 좋다.
  • 한정적 와일드 카드를 써서 유연성을 높이자.
  • 다음과 같이 말이다. List<? extend AClass>
  • 한정적 와일드 카드에는 규칙이 있다.
  • 생산자에는, 즉 어떤 값을 추가하는 예를 들어 push()라면, <? extends E>를 사용하자.
  • ex) push(List<? extends E>)
  • // 이해가 좀 어려운데, 삽입하는 것이니, 하위클래스도 넣을수 있는 것이 좋다!
  • 소비자에는, 즉 어떤 값을 사용하는, 예를들어 pop()이라면<? super E>를 사용하자.
  • ex) popAll(List<? super E>)
  • // 이해가 좀 어려운데, 꺼내는 것이니 상위캐스팅해서 꺼내는 것도 좋다!
  • 하지만, 리턴값은 와일드 카드 자료형을 사용하면안된다, E를 사용해야한다.
0
이전 댓글 보기
등록
TOP