두 객체와 완전히 같은 경우를 의미함
Number number1 = new Number(1);
Number number2 = number1;
System.out.println(number1 == number2); // true
두 객체가 같은 정보(내용)를 가지고 있음을 의미함
모든 객체의 조상인 Object 객체에서 정의하고 있는 equals()는 단순히 동일성 비교를 하고 있음
public boolean equals(Object obj) {
return (this == obj);
}
따라서 String 클래스는 아래와 같이 equals()를 재정의하여 인자로 전달된 String의 문자열을 비교하고 있음
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
객체의 주소 값을 이용해서 해싱 기법을 통해 해시코드를 만든 뒤 반환하는 메소드 → 해시코드는 주소값으로 만든 고유한 숫자값임
equals()를 오버라이딩 하고 실행했을 때, hashCode()를 오버라이딩 안하면 다음과 같은 경고문이 뜸
자바는 “equals()의 결과가 true인 두 객체의 해시코드는 반드시 같아야 한다”는 규칙을 가지고 있기 때문에, equals()를 객체의 주소가 아닌 객체의 필드 값을 비교하기 위해 오버라이딩 했다면 hashCode()도 오버라이딩 해줘야 함
❓왜 equals()의 결과가 true인 두 객체의 해시코드는 반드시 같아야 할까?
- hash 값을 사용하는 Collection Framework(HashSet, HashMap, HashTable)을 사용할 때 문제가 되기 때문
class Person { public String name; public Person(String name) { this.name = name; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Person p = (Person) o; return Objects.equals(name, p.name); } } public class ClassTest { public static void main(String[] args) throws >Exception { Person p1 = new Person("홍길동"); Person p2 = new Person("홍길동"); // 두 객체의 해시 코드 System.out.println(p1.hashCode()); // >460141958 System.out.println(p2.hashCode()); // >1163157884 // 해시코드가 달라도, equals를 재정의 했기 때문에 동등함 System.out.println(p1.equals(p2)); // true Set<Person> people = new HashSet<>(); people.add(p1); people.add(p2); // ⁉️논리적으로 equals 결과가 true이므로 1이 나와야 하는데 2가 출력됨 System.out.println(people.size()); } }
따라서 hashCode()를 재정의해야 한다
class Person { public String name; public Person(String name) { this.name = name; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Person p = (Person) o; return Objects.equals(name, p.name); } @Override public int hashCode() { return Objects.hash(name); // name 필드의 해시코드를 반환한다 } } public class ClassTest { public static void main(String[] args) throws Exception { Person p1 = new Person("홍길동"); Person p2 = new Person("홍길동"); // 두 객체의 해시 코드 System.out.println(p1.hashCode()); // 54150093 System.out.println(p2.hashCode()); // 54150093 // 해시코드가 달라도, equals를 재정의 했기 때문에 동등함 System.out.println(p1.equals(p2)); // true // SET를 생성하고 두 객체 데이터를 추가한다 Set<Person> people = new HashSet<>(); people.add(p1); people.add(p2); // 그리고 SET의 길이를 출력한다 System.out.println(people.size()); // 1 } }
String 클래스에는 문자열을 저장하기 위해서는 문자열 배열 변수(char[]) value를 인스턴스 변수로 정의해놓고 있음
public final class String implements java.io.Serializable, Comparable {
private char[] value;
...
}
즉, 인스턴스 생성 시 생성자의 매개변수로 입력받는 문자열은 이 인스턴스 변수(value)에 문자형 배열(char[])로 저장되는 것
→ 한 번 생성된 String 인스턴스가 갖고 있는 문자열은 읽어 올 수만 있고, 변경할 수는 없음
String oldStr = "자바 프로그래밍";
String newStr = oldStr.replace("자바", "JAVA");
문자열을 만들 때는 문자열 리터럴을 지정하는 방법과, String 클래스의 생성자를 사용해서 만드는 방법이 있음
자바는 문자열 리터럴이 동일하다면 동일한 String 객체를 참조하도록 되어 있음
String str1 = "abc";
String str2 = "abc";
String str3 = new String("abc");
String str4 = new String("abc");
/*
str1 == str2 ? true
str1.equals(str2) ? true
str3 == str4 ? false
str3.equals(str4) ? true
*/
new 연산자를 통해 문자열 객체를 생성하는 경우 메모리의 Heap 영역에 할당되고, 리터럴을 이용한 경우에는 String Constant Pool이라는 영역에 할당됨
참고로 문자열이 담기는 상수풀의 위치는 자바 7부터 Heap 영역으로 옮겨졌음 (이전에는 Perm 영역에 저장되었음 -> 자바 8 버전부터 Perm 영역은 완전히 사라짐)
내부적으로 문자열 편집을 위한 버퍼를 가지고 있으며 StringBuffer 인스턴스를 생성할 때 그 크기를 지정할 수 있기 때문에, String 클래스와 달리 변경이 가능함
// 자바 내부 클래스
public StringBuffer(int length) {
value = new char[length];
shared = false;
}
public StringBuffer() {
this(16); // 버퍼의 크기를 지정하지 않으면 버퍼의 크기는 16이 됨
}
public StringBuffer(String str) {
this(str.length() + 16); // 지정한 문자열의 길이보다 16이 더 크게 버퍼를 생성함
append(str);
}
// 사용자 코드
StringBuffer sb = new StringBuffer(100);
sb.append("abcd");
StringBuffer sb = new StringBuffer();
StringBuffer sb = new StringBuffer("Hi");
StringBuffer sb = new StringBuffer("abc");
StringBuffer sb2 = new StringBuffer("abc");
// StringBuffer의 내용을 String으로 변환해서 내용을 비교해야 함
String s = sb.toString();
String s2 = sb2.toString();
/*
sb == sb ? false
sb.equals(sb2) ? false
s.equals(s2) ? true
*/
StringBuffer 클래스와 거의 유사함!
StringBuilder sb = new StringBuilder(64);
sb.append("abcd");
StringBuilder sb = new StringBuilder();
StringBuilder sb = new StringBuilder("abc");
단, StringBuffer 클래스는 스레드에 안전하지만(동기화 보장), StringBuilder 클래스는 스레드에 안전하지 않다(동기화 보장하지 않음)는 것이 차이점임
StringBuffer와 StringBuilder의 차이점은 동기화 여부임
StringBuffer 클래스의 내부를 살펴보면, synchronized 키워드를 사용하여 메소드를 선언함
StringBuilder 클래스에서 제공하는 메소드는 synchronized 키워드가 존재하지 않음
도서 ‘Java의 정석’
도서 ‘윤성우의 열혈 Java 프로그래밍’
https://hudi.blog/identity-vs-equality/
https://creampuffy.tistory.com/140
https://inpa.tistory.com/entry/JAVA-☕-equals-hashCode-메서드-개념-활용-파헤치기
https://12bme.tistory.com/42
https://developer-talk.tistory.com/776