동일성(identity)

객체의 동일성이란 두 객체가 주소값까지 동일해 객체의 정보들까지 동일할 수 밖에 없는 완전히 같은 객체임을 의미한다.

동등성(equality)

객체의 동등성은 두 객체가 가지고 있는 정보가 같을 때를 의미한다.

즉 동일성은 객체의 주소값과 정보 모두 같으니.

동일하다는 것은 동등한 것이지만. 동등하다는 것은 동일하다는 것은 아니다.

 == 

"==" 는 피연산자 둘의 주소값을 비교하여 주소값이 동일할 시 true, 아니라면 false를 반환한다.

즉 동일성을 즉각적으로 비교해주는 연산자이다.

 

오 reference type 끼리의 "==" 연산은 Heap에서의 주소값으로 비교를 하는군!

그러나 우리는 종종 Objects에 포함되지 않는 primitive type(int, byte, short, long, float, double, boolean, char 자료형)들에도 "==" 연산을 사용하는데 이럴 땐 어떻게 작동할까?

이들은 반복적인 변수 선언에서의 메모리 중복을 피하기 위해 Constant pool에 담겨지는데, 이 constant pool에서 가져오는 것이니 주소값을 비교한다고 보아도 무방하다.

 

eqauls

equals 메소드가 동등성을 판별해준다. 라고 말하면 엄밀히는 틀린 말이다.

왜냐하면 우리 모든 객체들의 조상 Object class에 정의된 equals를 한번 살펴보자

//Object.java
public boolean equals(Object obj) {
    return (this == obj);
}

 

놀랍게도 "==" 연산을 반환해준다.

최상위 부모 클래스에선 동일성 비교를 한다는 사실은 변함없지만 자식 클래스들은 다양한 방식으로 equals를 재정의하여 사용하고 있다.

 

가장 먼저 Object 유틸 클래스인 Objects에서는

//Objects.java
public static boolean equals(Object a, Object b) {
    return (a == b) || (a != null && a.equals(b));
}

기본적으로 "==" 연산으로 동일성 비교를 먼저 한 후 인자 a의 reference type만의 equals(만약 재정의되지 않았다면 Object.java의 동일성 비교를 함)를 통해 동등성 비교를 한다.

 

그럼 이번엔 String 클래스에 정의된 equals를 한번 보자.

//String.java
public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    return (anObject instanceof String aString)
            && (!COMPACT_STRINGS || this.coder == aString.coder)
            && StringLatin1.equals(value, aString.value);
}

 

대부분의 동등성 비교 메소드가 비슷한데. 일단 "==" 연산으로 동일성 비교를 먼저 진행해준다.

두 객체가 동일하다면 동등성은 보나마나 보장되는 것이기 때문이다.

그 다음 instanceof 로 두 객체가 같은 reference type인지 비교 후 가지고 있는 데이터가 동일한 지 각자의 개성에 따라 판단해주는 모양새다.

참고로 String은 String을 위한 contstant pool도 제공되고 있어 

String a = "haha";
String b = "haha";
//sout(a.eqauls(b)) 하면 true

이렇게 된다고 한다.

 

//List.java
boolean equals(Object o);

/**
 * Returns the hash code value for this list.  The hash code of a list
 * is defined to be the result of the following calculation:
 * <pre>{@code
 *     int hashCode = 1;
 *     for (E e : list)
 *         hashCode = 31*hashCode + (e==null ? 0 : e.hashCode());
 * }</pre>
 * This ensures that {@code list1.equals(list2)} implies that
 * {@code list1.hashCode()==list2.hashCode()} for any two lists,
 * {@code list1} and {@code list2}, as required by the general
 * contract of {@link Object#hashCode}.
 *
 * @return the hash code value for this list
 * @see Object#equals(Object)
 * @see #equals(Object)
 */

 

다음은 List interface의 equals 메소드이다. 이 친구는 List 내의 모든 원소들의 hashcode들을 이용하여 동등성을 판단해준다고 한다.

여기서 hashcode() 라는 새로운 메소드가 나오는데. 이 친구는 동일성을 판별하는데 도움을 주는 메소드이다.

 

hashcode()

일단 간략하게 설명하면 hashcode는 객체의 주소값으로 만든 하나의 고유번호이다.

//Object.java
@IntrinsicCandidate
public native int hashCode();

여기서 native 키워드는 'Java가 아닌 언어로 구현된 부분'을 JNI(java native interface)를 통해 java에서 쓰인다는 의미라고 한다.

JVM이 내부동작을 통해 객체 생성시 int 값을 반환해주는 것이다. 따라서 따로 재정의하지 않아도 모든 객체에서 사용이 가능하다.

hashcode는 주소값을 통해 얻은 고유값이라는 의미이기 때문에 hashcode가 같다면 동일한 객체라도 봐도 된다는 것이다.

그러나 여기 예외가 있는데.....

 

String.java에 재정의된 hashcode 메소드를 살펴보자.

public int hashCode() {
    int h = hash;
    if (h == 0 && !hashIsZero) {
        h = isLatin1() ? StringLatin1.hashCode(value)
                       : StringUTF16.hashCode(value);
        if (h == 0) {
            hashIsZero = true;
        } else {
            hash = h;
        }
    }
    return h;
}

이를 이해하기엔 내 식견이 너무 짧다... 주석을 보아하니

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

이런식으로 char 하나하나 ascii code로 받아와서 해시코드를 구해준다고 한다.

이 말은 주소값이 다른 String이라도 담긴 데이터가 같다면 hashcode가 동일하다는 것이다!

 

그런데 이뿐만이 아니다. String에 담길 수 있는 값은 무궁무진하다.

그러나 hashcode는 int형이기 때문에(0부터 0xFFFFFFFF(4,294,967,295)) 데이터가 다르더라도 hashcode는 동일한 경우도 나온다.

이로 인해 hashcode collision이란 현상도 생기게 되는데  글이 너무 길어졌으니 다음에 알아보도록 하자..

 

틀린 부분이 있다면 지적 부탁드립니다!

출처

Java hashcode()  https://brunch.co.kr/@mystoryg/133

equals와 hashcode https://mangkyu.tistory.com/101

String의 hashcode가 유일하지 않은 이유 https://blog.ggaman.com/916

'JAVA' 카테고리의 다른 글

JAVA 제어자(modifier)  (0) 2023.10.22

출처 및 아주 좋은 링크 : https://www.tcpschool.com/java/java_modifier_accessModifier

https://docs.oracle.com/javase/tutorial/java/javaOO/accesscontrol.html

제어자

제어자는 클래스 내부의 데이터를 보호하기 위해 클래스와 클래스 멤버에 사용하는 키워드로 접근 제어자와 기타 제어자가 있다.

접근 제어자(access modifier) : (다른 클래스에서의) 접근 레벨을 제어.

기타 제어자(non-access modifier): 기타 다른 좋은 역할을 수행

 

접근 제어자

출처: 자바 공식 문서

접근 제어자는 public, protected, default(따로 접근 제어자를 선언하지 않았을 시), private 4가지가 있다.

위 표로 말끔하게 정리된다.

public

해당 클래스를 사용하는 어디에서나 접근이 가능

 

protected 

같은 클래스 + 같은 패키지 + 상속받은 자식 클래스(같은 패키지가 아니더라도) 에서 접근이 가능

 

default

같은 클래스 + 같은 패키지에서만 접근이 가능. 위 표에서는 no modifier로 된 부분으로 따로 접근 제어자를 사용하지 않으면 자동으로 적용이 된다.

 

private

같은 클래스에서만 접근이 가능.

 

알고 나면 간단하다. 이모저모를 써보자면

어떤 클래스의 private 멤버는 public 을 통해서만 접근이 가능하기 때문에 private + public 조합이 많이 쓰인다(ex. getter, setter)

접근 제어자는 한번의 선언에서 하나만 사용할 수 있다. public protected int name;는 않뒈요.

 

 

기타 제어자

기타 제어자는 이것저것 하는 친구들이다.

final, static, abstract, transient, synchronized, volatile 이렇게 있는데 앞 세가지 친구만 다루겠다. 초보라 죄송하다.

 

final

final 제어자는 말 그대로 클래스, 멤버, 메소드를 최종 버전, 즉 변경 불가능하게 만들어버린다. 

필드와 전역 변수에 사용하면 constant 상수로 만들어 버리고

메소드에 사용하면 overriding이 불가능하게 되고

클래스에 사용하면 상속을 불가능하게 만든다

static

static은 특정 멤버를 모든 객체(클래스)에서 공유하고 싶을 때 사용한다.

즉 static 제어자를 가지는 멤버는 해당 클래스의 모든 인스턴스에서 접근이 가능하며.

따로 객체를 생성(new)하지 않아도 클래스명.멤버 로 접근이 가능하다.

static 멤버는 프로그램 시작 시 단 한번만 생성되며. 메모리 영역에서(code, data, heap, stack 구조로 봤을 때) data영역에 들어간다.

또한 garbage collector의 관리도 받지 않는다.

abstract

이 친구는 추상클래스, 추상메소드를 만들 때 사용된다.

abstract 제어자를 사용한 메소드에는 실제 로직이 존재하지 않고 선언만 해준다. abstract void eat(); 이런 식으로

자식 클래스에게 어떤 메소드를 강제적으로 구현하게끔 할 때 사용하면 좋다.

 

 

보너스  부록  private + static + final

절대 변하징 않을 상수를 선언할 때 private static final ~~ 조합을 많이 볼 수 있다.

생각해보면 private final만 해도 변경 불가능 하고 든든한 접근 제어자 private도 함께하니 보안적인 측면에서 봤을 때 문제가 없어 보인다.

실제로도 보안은 탄탄하지만 static을 이용하면 프로그램 시작 시 단 한번만 사용되기 때문에 메모리적인 이득이 있다.

그냥 변하지 않을 값인데 굳이 많이 생성될 수도 있는 인스턴스의 위험부담을 안을 필요가 없다.

+ Recent posts