[JAVA] 동일성과 동등성, String/StringBuffer/StringBuilder

mingsso·2023년 11월 19일
0

Java-Kotlin

목록 보기
12/17
post-thumbnail

1️⃣ 동일성(Identity)

두 객체와 완전히 같은 경우를 의미함

  • 두 객체의 메모리 주소가 같기 때문에 두 참조 변수가 같은 객체를 가리키고 있음
  • 즉, 내용과 주소값이 모두 같음
  • 자바에서 동일성은 비교 연산자 ‘==’로 확인할 수 있음

Number number1 = new Number(1);
Number number2 = number1;

System.out.println(number1 == number2);  // true



2️⃣ 동등성(Equality)

두 객체가 같은 정보(내용)를 가지고 있음을 의미함

  • 즉, 동일함은 동등함을 보장하지만, 반대로 동등함은 동일함을 보장하지 않음
  • 자바에서 동등성을 비교하기 위해서는 equals()와 hashCode()를 오버라이딩해서 사용해야 함

equals 메소드

모든 객체의 조상인 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;
}

hashCode 메소드

객체의 주소 값을 이용해서 해싱 기법을 통해 해시코드를 만든 뒤 반환하는 메소드 → 해시코드는 주소값으로 만든 고유한 숫자값임
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
   }
}

3️⃣ String 클래스

String 클래스에는 문자열을 저장하기 위해서는 문자열 배열 변수(char[]) value를 인스턴스 변수로 정의해놓고 있음

public final class String implements java.io.Serializable, Comparable {
    private char[] value;
    ...
}

즉, 인스턴스 생성 시 생성자의 매개변수로 입력받는 문자열은 이 인스턴스 변수(value)에 문자형 배열(char[])로 저장되는 것

→ 한 번 생성된 String 인스턴스가 갖고 있는 문자열은 읽어 올 수만 있고, 변경할 수는 없음

  • replace() 메소드가 리턴하는 문자열은 원래 문자열의 수정본이 아니라 완전히 새로운 문자열!
  String oldStr = "자바 프로그래밍";
  String newStr = oldStr.replace("자바", "JAVA");

  • 따라서 문자열 간의 결합이나 추출 등 문자열을 다루는 작업이 많이 필요한 경우에는 메모리 공간을 절약하기 위해 String 클래스 대신 StringBuffer 클래스를 사용하는 것이 좋음


문자열을 만드는 2가지 방법

문자열을 만들 때는 문자열 리터럴을 지정하는 방법과, 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 영역은 완전히 사라짐)



4️⃣ StringBuffer 클래스

내부적으로 문자열 편집을 위한 버퍼를 가지고 있으며 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");

  • String 클래스에서는 equals 메소드를 오버라이딩해서 문자열의 내용을 비교하도록 구현되어 있지만, StringBuffer 클래스는 equals 메소드를 오버라이딩하지 않아서 StringBuffer 클래스의 equals 메소드를 사용해도 ‘==’로 비교한 것과 같은 결과를 얻음
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
*/



5️⃣ StringBuilder 클래스

StringBuffer 클래스와 거의 유사함!

  • 생성자를 포함한 메소드 수, 메소드의 기능, 메소드의 이름과 매개변수의 선언이 일치함
    StringBuilder 클래스는 Java 5에서 등장한 클래스이고, 이전에는 StringBuffer 클래스가 사용된 것
StringBuilder sb = new StringBuilder(64);
sb.append("abcd");

StringBuilder sb = new StringBuilder();

StringBuilder sb = new StringBuilder("abc");

단, StringBuffer 클래스는 스레드에 안전하지만(동기화 보장), StringBuilder 클래스는 스레드에 안전하지 않다(동기화 보장하지 않음)는 것이 차이점임

  • 멀티 스레드에 안전하게 설계된 StringBuffer 클래스는 속도가 느림
  • 멀티 스레드와 상관없는 상황에서 사용할 목적으로 StringBuilder 클래스를 만들게 된 것



6️⃣ String vs StringBuffer vs StringBuilder

  • String과 StringBuffer, StringBuilder의 차이점은 String은 immutable(불변), StringBuffer/StringBuilder는 mutable(변함)이라는 것
    • String 클래스는 문자열 연산이 많은 경우 성능이 좋지 않음
    • But, 멀티 스레드 환경에서 동기화를 신경쓰지 않아도 되고 내부 데이터를 자유롭게 공유 가능하다는 장점이 있음
  • 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

profile
🐥👩‍💻💰

0개의 댓글