// Object.java
public boolean equals(Object obj) {
return (this == obj);
}
String 의 equals 재정의 예
// String.java
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String aString = (String)anObject;
if (!COMPACT_STRINGS || this.coder == aString.coder) {
return StringLatin1.equals(value, aString.value);
}
}
return false;
}
String 은 먼저 == 연산자로 같은 레퍼런스 값이면 true를 반환
아닐 경우에는 value 를 비교하기 위해 if 문 안에서 bytecode를 비교하여 동일여부를 판단
이처럼 class에 따라 equals 를 재정의 할 필요성이 있을 수 있음
String과 같이 이미 재정의된 equals 를 사용하는 객체가 있을 수 있음
이건 개발자의 맘인데 String과 같이 논리적 동시성 즉, value가 같은지 확인할 필요가 없다면 재정의 할 필요는 없다.
public class Student {
// 학생 번호
private int studentId;
public Student(int studentId) {
this.studentId = studentId;
}
// equals 재정의 : 학생 번호가 같으면 true
@Override
public boolean equals(Object o) {
if(o instanceof Student){
Student student = (Student) o;
return studentId == student.studentId;
}
return false;
}
}
혹여라도 equals가 실행될 수 있으니 아래처럼 아싸리 막아버리는 수도 있겠다.
@Override
public boolean equals(Object obj) {
throw new AssertionError();
}
null이 아닌 모든 참조 값 x에 대해, x.equals(x)는 true이다.
null이 아닌 모든 참조 값 x,y에 대해, x.equals(y)가 true면 y.equals(x)도 true다.
public class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s) {
this.s = Objects.requireNonNull(s);
}
//대소문자에 관계없이 같은 값을 가지는지 여부를 돌려주는 함수
@Override
public boolean equals(Object o ) {
if (o instanceof CaseInsensitiveString)
return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
if (o instanceof String)
return s.equalsIgnoreCase((String) o);
return false;
}
}
CaseInsensitiveString 인스턴스의 equals는 String을 알기에 의도한대로 작동하지만 String의 equals는 CaseInsensitiveString 를 알지 못하기에 무조건 false를 반환할 것.
CaseInsensitiveString cis = new CaseInsensitiveString("SungKyum");
String s = "sungkyum";
System.out.println(cis.equals(s));
System.out.println(s.equals(cis));
실행결과
//ArrayList 의 contains 안, indexOf 함수가 부르는 indexOfRange 함수의 로직
int indexOfRange(Object o, int start, int end) {
Object[] es = elementData;
if (o == null) {
for (int i = start; i < end; i++) {
if (es[i] == null) {
return i;
}
}
} else {
for (int i = start; i < end; i++) {
if (o.equals(es[i])) { // 객체의 equals 로직을 사용
return i;
}
}
}
return -1;
}
@Override
public boolean equals(Object o ) {
return o instanceof CaseInsensitiveString && ((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
}
null이 아닌 모든 참조 값 x,y,z에 대해 x.equals(y)가 true이고 y.equals(z)도 true이면 x.equals(z)도 true이다.
@Override
public boolean equals(Object o) {
if (!(o instanceof ColorPoint))
return false;
return super.equals(o) && ((ColorPoint) o).color == color;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Point))
return false;
// point는 color를 무시하게 한다?
if (!(o instanceof ColorPoint) )
return o.equals(this);
return super.equals(o) && ((ColorPoint) o).color == color;
}
ColorPoint cp = new ColorPoint(1, 2, Color.white);
Point p = new Point(1,2);
ColorPoint cp2 = new ColorPoint(1,2, Color.black);
System.out.println(cp.equals(p));
System.out.println(p.equals(cp));
System.out.println(p.equals(cp2));
System.out.println(cp.equals(cp2));
@Override
public boolean equals(Object o) {
if (o == null || o.getClass() != getClass())
return false;
Point p = (Point) o;
return p.x == x && p.y == y;
}
“리스코프 치환 원칙”
하위 타입의 객체를 상위 타입의 객체로 치환해도 상위 타입을 사용하는 프로그램은 정상적으로 동작해야 한다.
- 상위 클래스의 구현 로직을 하위 클래스가 사용하지 못하게 되는 경우가 생기기 때문
@Getter
class Rectangle {
private int height;
private int width;
public Rectangle(int height, int width) {
this.height = height;
this.width = width;
}
public void setSize(int height, int width) {
this.height = height;
this.width = width;
}
}
@Getter
class Square extends Rectangle {
public Square(int height, int width) {
super(height, width);
if(height != width) {
throw new AssertionException('cannot create!');
}
}
public void setSize(int height, int width) {
if(height != width) {
throw new AssertionException('cannot create!');
}
this.super(height, width);
}
}
// 직사각형 문맥 로직 수행
public void changeWideSize(Rectangle rec) {
dim.setSize(dim.getHeight(), dim.getWidth() * 2); // throw Exception.
}
다형성에 의해 정사각형이 들어와도 작동하기 때문에 가로/세로 변의 길이가 달라지는 문제 생김
이 방식은 상속의 메서드 재정의에 의한 문제를 해소할 수 있는 유용한 방법상속이 아닌 private으로 생성된 필드에 인스턴스로 참조하는 방식
null이 아닌 모든 참조 값 x,y에 대해, x.equals(y)를 반복해서 호출하면 항상 true를 반환하거나 항상 false를 반환한다.
public class UrlTest {
public static void main(String[] args) throws MalformedURLException, UnknownHostException {
URL googleUrlByDomainName = new URL("https://www.google.co.kr/");
URL googleUrlByIpAddress = new URL("https://142.250.204.35/"); // 구글의 접속 IP는 다양하므로 테스트때마다 다름
InetAddress address = InetAddress.getByName(googleUrlByDomainName.getHost());
System.out.println(address.getHostAddress()); //현재 ip주소는 172.217.31.3
InetAddress address2 = InetAddress.getByName(googleUrlByIpAddress.getHost());
System.out.println(address2.getHostAddress()); // 10분전 ip 주소 142.250.204.35
System.out.println(googleUrlByDomainName.equals(googleUrlByIpAddress)); // false
}
}
시간이 흘러서 DNS서버가 다른 IP주소를 넘겨준다면 false가 나올 수 있음null이 아닌 모든 참조 값 x에 대해, x.equals(null)은 false다.
@Override
public boolean equals(Object o) {
if (o == null)
return false;
...
}
이거보단 필수 필드를 필연적으로 확인하게 되니
@Override
public boolean equals(Object o ) {
if (!(o instanceof NullCheck))
return false;
NullCheck nc = (NullCheck) o;
...
}
이렇게 첫번째 피연산자가 null이면 자동으로 false를 반환하도록 하면 좋다.
너무 필요한 상황이 아니면 굳이굳이 재정의 하지 말자!