item10 - equals는 일반 규약을 지켜 재정의하라
일반적으로 object 에서 override 해서 재정의할 수 있는 대표적인 함수는 이와 같다 equals, toString, hashcode, clone, finalize
equals 는 대부분 인텔리제이에게 해줘 해서 만드는 경우가 많은데, 너무 생각 없이 만드는 것 보다는 다음과 같은 이유가 아닌 경우에 생성해서 사용하자 우선적으로 인스턴스가 싱글톤으로 고유할 때이다 -> 인스턴스가 고유하면 굳이 equals 을 사용할 이유가 전혀 없지요?
다음으로는 인스턴스의 '논리적 동치성'을 검사할 필요가 없을 때 논리적 동치성이란 어떠한 객체의 내부가 같으냐를 비교하는 의미이다 내용이 같다면 똑같은 객체로 보겠다는 의미 = 논리적 동치성 예시를 들면, String 이 있다 - String이 "hello" 이면 또 다른 String이 "hello" 이면 이것을 같다고 보는 것과 같은 논리이다
다음으로는 상위 클래스에 이미 정의 되어 있는 경우 에는 따로 equals을 정의할 필요가 없다 -> 예를 들면 hashset이나 list이 있다
마지막으로는 클래스가 private 이거나 package-private 이고 equals 메소드를 호출할 일이 없을 때 이다 public 한 클래스는 equals 가 호출되지 않을 것이라는 보장을 하는 것이 어렵다... public은 아무나 참조해서 사용하는 것이 가능하기 때문에 만약 참고해서 사용하면서 자연스럽게 List 나 Set에 넣어버리면 equals가 호출되어 버린다 그 의미는 이렇게 어떻게 쓰일지 모르기 때문에 private 에서 우리가 직접 equals을 호출할 일이 없는 클래스라면 재정의하지 말자는 의미이다
그러면 어떻게 equals를 재정의해야하는지 한 번 보자
equals을 재정의하는데 있어서 고려해야하는 점은 몇 가지 있다 순서대로 반사성, 대칭성, 추이성, 일관성, Null이 아님이다 반사성은 거울이라고 생각하면 편하다 -> 객체가 자기 자신과 비교했을 때 같냐? 이것을 보고 반사성이라고 한다
대칭성부터는 두 객체를 비교하게 되는 것이다 -> A와 B가 있을 때 A.equals(B)의 결과와 B.equals(A)의 결과가 같아야 한다는 것이다 이게 실제로 구현하는 것에 따라서 다른게 나올 수도 있기 때문에 항상 주의해야 한다는 점이다
추이성은 3단 논법이라고 생각하면 편하다 -> A, B, C 가 있을 때 A.equals(B) 가 true이고 B.equals(C) 가 true 이면 C.equals(A) 가 true 이여야 한다는 것이다 강의에서 예시로는 Point 와 Point을 상속받아서 만든 ColorPoint 을 비교하는데 ColorPoint(1, 2, RED) , Point(1, 2), ColorPoint(1, 2, BLUE) 이렇게 3개를 예시로 들어주었다 -> 만약에 equals의 구현이 Point에도 되어있으며, ColorPoint 에서는 super.equals와 colorType비교가 같이 들어가 있는 상태에서 Point.equals(ColorPoint-RED), Point.equals(ColorPoint-BLUE) 는 super의 Point 비교를 사용하고, 이는 좌표만 확인하기 때문에 true가 나올 것이다 근데 ColorPoint-RED 와 ColorPoint-BLUE 이 같냐? 는 보기만 해도 아닌 것을 알고 이건 이상한 것이라고 볼 수 있다 실제로 ColorPoint.equals을 통해서 확인해보면 ColorType을 비교하기 때문에 두 개는 다르다고 나올 것이고 이것은 추이성을 지키지 못한 케이스라고 볼 수 있다 여기서 잠깐 보면 Point.equals 에서는 Point 이라는 타입을 가지고 비교하기 때문에 1, 2으로만 비교할 수 있었던 건데 하위 타입은 하위 타입도 비교해주는 것이 필요하지 않을까? 이런 생각을 하면서 super에 있는 equals 에 Object.getClass()을 통해서 타입을 확인해준다면, 이건 라스코프 치환 원칙을 위배한 것이다 라스코프 치환 원칙은 상위(부모) 타입으로 동작하는 코드가 있을 때, 상위 코드를 상속받는 자식 클래스의 타입을 가지고 넣어줬을 때 동일하게 동작해야한다 즉, 부모클래스 대신 자식클래스가 들어가도 코드는 동일하게 동작해야 한다는 의미이다 만약에 상속받는 객체에서 equals을 사용하고 싶다면 상속하지 말고 composition 을 사용하라 그러니까 상속하는 것이 아니라 새로운 클래스를 생성하고 해당 클래스의 필드에 상속하려는 객체를 private final 으로 집어넣어주라는 의미이다 대신 사용하는 방법으로는 해당 필드를 리턴해주는 함수를 통해서 꺼낼 수 있도록 구현하는 것이다 이렇게 해당 필드를 리턴해주는 함수를 통하게 되면 상속하지 않고 신규로 만든 객체를 마치 상속하려던 그 객체처럼 사용해서 비교하거나 사용하는 것이 가능하다
다음으로는 일관성과 not null 이다 일관성은 처음에 A.equals(B) 가 true 일때, 다시 한 번 A.equals(B) 를 했을 때 같은 값이 나와야 한다는 것이다 일관성은 객체의 특성에 따라 다르다 -> 이게 객체가 자주 변화되는 객체이면 일관성을 유지하는 것이 쉽지 않으며 불변객체나 일관성을 중요시 파악해야 한다 not null은 equals 함수를 사용하는데 null 을 넘겨서 확인할 수 없다는 것이다 -> 당연하다고 하는데 난 이거 못 챙겨서 운영에서 이슈도내고... 데였으니까 잘 기억하자!
그래서 어떻게 equals을 구현하는 것이 좋냐?
4가지의 단계를 거쳐서 구현하면 좋다 우선은 자기자신을 비교한다(반사성) 그 다음은 instanceof 을 통해서 클래스가 맞는지 비교하고 비교한 클래스로 타입 캐스팅을 해준다 마지막으로는 비교해야하는 필드만 비교해준다 -> 비교하는데 있어서 부동 소수점이 있으면 Double 이나 Float 에서 제공해주는 .compare() 을 사용해서 비교하면 좋고 알려준다
추가로 null을 허용하고 싶다면 Objects.equals() 을 사용하라. Objects 에서 제공해주는 equals 는 @nullable이기 때문에 원하면 사용하면 된다
이렇게 구현하면 좋아요~~ 근데 굳이 이렇게까지 힘들게 직접 한땀한땀 구현하면 나중에도 필드가 추가되거나 할때마다 정--말 귀찮을 것이다 결국 필드가 추가되거나 삭제되면 그에 따라서 equals 도 수정되야하는 그러한 단점이 있는 것이다 그래서 그렇게 사용하는 방법으로는 롬복에서의 애노테이션을 통하거나 @AutoValue 를 사용하는 것이다 근데 이러한 것들의 단점은 애노테이션 프로세서를 사용했기 떄문에 컴파일 시점에 코드가 생성되게 되는 그러한 플로우로 진행된다 이것이 현재 사용하기 편한 방법이고 자바8을 주로 사용하는 본인으로써는 이정도가 충분한데
사실 위의 애노테이션 프로세서보다는 불편하지만 그래도 굳이 equals을 사용할 필요가 없는 것이 intellij 에서 제공해주는 자동 구현방식이다 클래스에서 자동으로 만들어주면 어렵지 않게 사용할 수 있다. 장점으로는 내부코드를 직접 볼 수 있지만, 단점으로는 위에서 언급한 것 처럼 귀찮게 변화사항이 생기면 수정해야한다 주의해야할 것은 equals는 hashCode와 함께 같이 사용하자
Value Based Class -> 클래스이지만 해당 클래스 내부의 데이터를 기준으로 식별하는 그러한 클래스이다 가장 최신방법은 자바 17에서 제공해주는 record을 사용하는 방법이지만, 수동으로 구현하는 방법은 클래스 내부의 필드를 final로 구현하는 것이다 자바 17를 사용한다면 records 이라는 타입이 있다 records 는 class, enum, interface 와 같은 클래스 타입으로 보인다 이건 그냥 클래스처럼 되, 생성자처럼 클래스의 필드들을 파라미터로 넣어주면, 따로 구현하지 않아도 toString(), equals(), hashcode()를 사용하는 것이 가능하다 또한 getter도 구현해주는데, 롬복에서 사용하는 것 처럼 get필드()가 아니라 단순하게 .필드() 이렇게 해서 getter도 사용하는 것이 가능하다 대신 필드가 변경되지 않는다는 점이다 -> 그래서 위에서 구현한다고 할 때 클래스 내부의 필드를 final로 구현한다고 한 것이다
StackOverFlow Error JVM에서 스택이란 하나의 스레드가 사용하는 메모리 공간이다. 그리고 그 스택에 쌓이는 것을 보고 스택 프레임이라고 하고 스택프레임이 쌓이게 되는데 스택의 특성상 Last In First Out(LIFO)을 특징으로 가지고 있다 프로그램에서 메소드가 호출될 때마다 스택 프레임이 쌓이게 된다. 그리고 그 스택프레임 안에는 메소드를 호출할 때 넣어줬던 매개변수, 그리고 메소드를 참조하는 객체를 가리키는 래퍼런스가 들어가 있으며 리턴 값에 대한 정보가 들어가 있다 이러한 스택에 스택프레임이 쌓이다가 저장하는 스택의 크기를 넘어가는 순간이 StackOverFlow 에러가 발생하는 순간이다
이렇게 객체의 주소나 메소드에 대한 정보를 가지고 있는 곳이면, 힙은 객체를 저장하고 나중에 GC가 도는 공간이다 스택은 메소드를 많이 호출하는 순간에 부하가 오기 시작하는 구조이다. 따라서 로직에 재귀함수가 존재하거나 무한루프가 도는 곳에서 stackoverflow 를 의심해보아야 한다 일단 JVM에서의 기본 스택자체는 운영체제마다 다르지만 기본적으로는 1MB 정도로 잡고 있는데 만약 사이즈를 어느 정도 조정하고 싶다면, -Xss 이라는 옵션을 넣어주자
리스코프 치환 원칙 객체지향의 5대 원칙 SOLID 의 L을 담당 Single Responsibility Open-closed Principle L스코프 치환원칙 Interface segregation Dependency Inversion
S -> 하나의 클래스는 하나의 책임을 가지고 있어야 한다 O -> 클래스는 수정은 닫혀있어야하고, 확장에는 열려있어야 한다 L -> 하위 클래스의 객체가 상위 클래스의 객체를 대체해도 기능이 동일하게 돌아야 한다 I -> 특정 클라이언트를 위한 인터페이스 여러 개가 다 모아둔 하나의 인터페이스보다 좋다 D -> 하위객체가 상위객체에 의존적이지 않고 독립적인 관계를 유지해야 한다
Last updated
Was this helpful?