[Java] Object 클래스

박세진·2021년 1월 31일
1
post-thumbnail

오늘은 자바의Object클래스에 대해 포스팅을 하려고 한다.
Object 클래스의 정적 메소드들 (toString(), equals()) 에 대해 알아보자.


Object

Object는 모든 클래스의 가장 최상위 클래스이다.
자바에서 제공하는 모든 클래스들은 계층 구조로 되어있고, 그 최상위로 올라가면 Object라는 클래스가 존재한다.

우리가 일반적으로 생성하는 클래스들도 extends로 직접 상속하지는 않았지만, 자바 컴파일러는 일반 클래스를 Object의 하위 클래스로 자동으로 설정하게 되어있다.

결론?

자바 라이브러리 or 유저가 만든 모든 클래스는 Object 클래스를 상속받아서 사용하게 된다.
따라서 자바의 모든 클래스는 Object 클래스의 모든 메소드를 바로 사용할 수 있다.

그러면 이러한 Object 클래스에는 어떠한 메소드가 있을까?
아래부터 하나씩 살펴보도록 하자.


toString() 메소드

: 객체가 가지고 있는 정보 or 값들을 문자열로 만들어 return 하는 메소드.

아래의 예시를 보자.

package study.test.java;
class Human {
	private int age;

	public void setAge(int age) {
		this.age = age;
	}

public class ToStringExample {
	public static void main(String[] args) {
		Human human = new Human();
		
        	System.out.println(human.toString()); 
        	// 출력결과 : study.test.java.Human@15db9742
	}
}    

필자가 만든 Human 클래스로 객체를 만들어 toString()메소드를 사용해 출력한 결과이다.
결과에는 이상한 정보가 담겨있다. 이 값은 순수 Object 클래스의 toString 결과값이다. 차례대로 패키지명, 클래스명, 그리고 구분자@와 함께 16진수 해시코드가 출력된 것이다.

이것만 가지고는 Human 클래스의 객체 human에 대한 유의미한 정보를 뽑아 낼 수 없다. 이것이 toString 메소드의 재정의가 권장되는 이유이다.

아래처럼 String 클래스나, File 클래스 등 자바 API 클래스에서는 toString 메소드를재정의 하여 의미있는 값을 리턴해준다.
아래의 예시를 보자.

package study.test.java;

public class ToStringExample {
	public static void main(String[] args) {
		String str = "ABCDEFG";
		System.out.println(str.toString()); // 출력결과 : ABCDEFG
	}
}

String 클래스는 내부에서 toString을 재정의하기 때문에 자신이 가진 값을 그대로 리턴해주는 것을 알 수 있다.

그러면 앞에서 했던 Human 클래스의 toString 메소드도 재정의해서 사용해보자.

	@Override
	public String toString() {
		return "나이는 " + age;
	}

Human 클래스에서 Override/Implements method 항목을 이용해서 toString 메소드를 재정의하였다.

public class ToStringExample {
	public static void main(String[] args) {
		Human human = new Human();
		human.setAge(25);
		
		System.out.println(human.toString()); // 출력결과 : 나이는 25
		
	}
}

필자가 재정의했던 toString 메소드가 잘 호출되었음을 출력값을 통해 알 수 있다.

toString() 메소드는 자동으로 호출이 된다? 😐

우리가 자주 사용하는 String 클래스를 보자.
String 클래스는 객체를 선언하여 toString 메소드 없이 그냥 객체 자체로 변수에 담긴 내용을 가져와서 사용했다.
아래의 코드를 보자.

public class ToStringExample {
	public static void main(String[] args) {
		String str = "세진세진";
		System.out.println(str); // 출력결과 : 세진세진
	}
}

참조변수 str은 String클래스의 객체의 주소를 가리킨다. 근데 객체임에도 불구하고, str 만 가지고 독단적으로 사용되어지고 있다.
바로 이 지점에서 toString 메소드가 자동으로 호출되는 것이다.

위에서 필자가 만들었던 Human 클래스를 가지고 똑같이 시도해보자.

public class ToStringExample {
	public static void main(String[] args) {
		Human human = new Human();
		human.setAge(25);
		
		System.out.println(human); // 아까와 다른점 : 여기서 toString 메소드만 제거
        	// 출력결과 : 나이는 25
	}
}

마찬가지로, toString 메소드 없이 그냥 객체 주소가 담긴 참조변수만 print 했을 때 toString으로 재정의된 내용이 출력됨을 알 수 있다.

Tip !

toString 메소드가 자동으로 호출되는 경우? 😶

  • 객체를 println, printf, 문자열 연결 연산자 ( + ) 구문에 넘길 때
  • 디버거가 객체를 출력할 때

equals() 메소드

: 두 객체를 비교하여 같으면 true, 다르면 false 를 리턴하는 메소드이다.
(기본적으로는 값 비교. 재정의해서 사용할 수 있음.)

equals() 와 == 의 차이점 ? 😐

  1. 메소드 vs 연산자
    • equals() : Object 클래스에 정의된 메소드.
      (String, Integer 등의 하위 클래스에서 오버라이딩 되어있음.)
    • == : 자바 언어 자체의 연산자.
다시 말해서, equals() 메소드는 오버라이딩 가능하지만, == 는 오버라이딩이 불가능하다. 특정 언어들은 연산자도 오버라이딩이 가능하다고 하지만, 자바는 불가능하다.
  1. 값 비교 vs 주소값 비교
    • equals() : 기본적으로 값 비교를 한다. 기본 타입에는 적용 불가.
    • == : 기본적으로 주소값 비교.(기본 타입에 대해서는 값 비교.)
      -> 참조 타입/기본 타입 전부 사용가능 !
아래의 예제를 참고하자.
// == 연산자 예제.
package study.test.java;

public class TestExample {
	public static void main(String[] args) {
		int a = 10;
		int b = 10;
		System.out.println(a == b); // true
		
		Person person1 = new Person();
		Person person2 = new Person();
		Person person3 = person2;
		
		System.out.println(person1 == person2); // false
		System.out.println(person2 == person3); // true
	}
}

처음으로 생성한 Person 객체가 0x100 주소에 생성되었다고 가정하자.
그러면 참조형 변수 person1의 값은 해당 주소값인 0x100이 된다.
그 다음으로 생성한 Person 객체의 참조형변수 person2는 0x200 의 값을 가진다고 가정하자. 그림으로 나타내면 다음과 같다.

int 타입인 변수 a,b 가 값을 비교하듯이 참조형 변수인 person1, person2, person3 가 주소값을 비교한다는 의미가 결국 해당 변수의 값을 비교하는 것이다.


다시 본문으로 돌아와서 equals() 메소드에 대해 알아보자.
equals() 역시 Object 클래스에 포함된 메소드이다. 하지만 기본 메소드를 그대로 사용할 경우 올바른 비교가 되지 않기 때문에, 실제 사용시에는 해당 클래스에 맞도록 오버라이딩 하여 사용한다.

먼저 Object 에 포함된 기본 equals() 메소드의 코드를 보면, 객체 자체를 관계 연산자 (==) 으로 비교한다. 따라서 내부적으로 같은 값을 가진 객체 2개라도, 다른 메모리 주소를 가지기 때문에 서로 다른 값으로 인식한다.

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

즉, 자기 자신을 비교하지 않는 이상 무조건 다른 값이 되는 것이다.
사용할 일은 그다지 없을 것으로 보인다.

이번에는 String 클래스의 equals()메소드에 대해 알아보자.
보통 String 타입을 비교할 때는, 내용이 같은지 비교한다고 설명한다.
그 이유는 String 클래스에서 equals() 메소드를 문자열 내용이 같은 경우 true를 리턴하도록 재정의했기 때문이다.

문자열 비교시 == 말고 equals()를 사용하는 이유? 😧

기본 타입들 (int, char)은 Call by Value 형태로 기본적으로 대상에 주소값을 가지지 않는 형태로 사용된다. 하지만 String 은 기본타입이 아닌 클래스이다. 클래스는 기본적으로 Call by Reference 의 형태로 생성 시 주소값이 부여된다.
그렇기 때문에 String 타입을 선언했을 때는, 같은 값을 부여하더라도 서로간의 주소값이 다를 수 있다.

// == 연산자를 이용한 문자열 비교.
public class compare {
    public static void main(String[] args) {
        String s1 = "abcd";
        String s2 = new String("abcd");
        if(s1 == s2) {
            System.out.println("두개의 값이 같습니다.");
        }else {
            System.out.println("두개의 값이 같지 않습니다."); // 이게 출력.
        }
    }
}

== 연산자의 경우 객체의 주소값을 비교하기 때문에
일반 객체처럼 Heap 영역에 생성된 String 객체와, 리터럴을 이용해 String Constant Pool 에 저장된 String 객체의 주소값은 다를 수 밖에 없다. 그러므로 두개의 값은 서로 다르다는 결론이 나오게 된다.
이러한 경우가 발생할 수 있기 때문에, 자바에서 문자열을 비교하기 위해서는 equals() 메소드를 이용해 두개의 값을 비교해 주어야 한다.

[참고] String 변수 생성시 주소할당

String 변수를 생성하는 2가지 방법

  • 리터럴을 이용한 방식
  • new 연산자를 이용한 방식
위의 2가지 방식에는 큰 차이점이 있다.

리터럴을 사용하게 되면, String Constant Pool 이라는 영역에 존재하게 되고,
new 를 통해 생성하게 되면 Heap 영역에 존재하게 된다.
String을 리터럴로 선언할 경우 내부적으로 String의 intern() 메소드가 호출된다. 이 메소드는 주어진 문자열이 String Constant Pool에 존재하는지 검색하고, 있다면 그 주소값을 반환. 없다면 String Constant Pool에 넣고 새로운 주소값을 반환한다.

이번에는 equals() 메소드를 이용해 이전의 코드를 써보자.

// equals 메소드를 이용한 문자열 비교.
public class compare {
    public static void main(String[] args) {
        String s1 = "abcd";
        String s2 = new String("abcd");
		
        if(s1.equals(s2)) {
            System.out.println("두개의 값이 같습니다.");
        }else {
            System.out.println("두개의 값이 같지 않습니다.");
        }
    }
}

위의 코드처럼 String 클래스 안의 equals() 메소드를 사용하면, 두 비교대상의 주소값이 아닌 데이터값을 비교하기 때문에 어떻게 String을 생성하느냐에 따라 결과가 달라지지 않고 정확한 비교를 할 수 있다.

좀더 심화된 예제를 보자.

class Person{
	String name;
	
	public Person(String name) {
		this.name = name;
	}
    // equals() 메소드의 재정의.
	@Override
	public boolean equals(Object obj) {
		Person anotherObj = (Person) obj;
		return(this.name.equals(anotherObj.name));
	}
}
package study.test.java;

public class EqualsExample {
	public static void main(String[] args) {
		String str1 = "hello";
		String str2 = "hello";
		System.out.println(str1 == str2); // true
		System.out.println(str1.equals(str2)); // true
		
		String str3 = new String("abc");
		String str4 = new String("abc");
		System.out.println(str3 == str4); // false
		System.out.println(str3.equals(str4)); // true
		
		Person person1 = new Person("kim");
		Person person2 = new Person(new String("kim"));
		Person person3 = person2;
		System.out.println(person1 == person2); // false
		System.out.println(person1.equals(person2)); // true
		System.out.println(person3 == person2); // true
		System.out.println(person3.equals(person2)); // true
	}
}
  • str1 == str2는, 동일한 문자열을 String Constant Pool에서 곧바로 참조한 것이다. 동일한 String 문자열을 바로 참조할 경우 같은 주소값을 가질 수 있다. 그래서 결과는 true 이다.
  • str1.equals(str2)는, String 객체의 equals() 메소드가 각 문자열의 글자를 하나씩 비교해서 동일했다고 판단하기 때문에 true가 나온다.
  • str3, str4는 객체 2개를 만들고 각 객체의 멤버변수인 name에 문자열을 할당한다.
  • str3 == str4는, 서로 다른 주소값을 가지고 있기 때문에 false이다.
  • str3.equals(str4)는, abc라는 동일한 문자열을 가진 String 객체이므로 재정의된 equals()의 결과로 true가 된다.

그림으로 표현하면 아래와 같다.

그 다음은 Person 객체에 대한 내용이다.

첫번째 코드에서 Person 클래스의 equals() 메소드를 재정의했다. 내용을 보면, 멤버변수인 name의 문자열이 동일한지를 비교하고 있다.

  • person1 == person2 는, 주소값 비교이기 때문에 false 이다.
  • person1.equals(person2)는, Person 클래스에서 재정의된 equals()를 사용한다. 따라서 두 객체의 name의 문자열이 kim으로 동일하기 때문에 true이다.
    person2는 name에 값을 할당할 때, new String("kim")으로 했기 때문에 새로운 String 객체를 생성한 것이다. 만약 equals()를 재정의할 때
    return (this.name == anotherObj.name);
    과 같이 했다면 결과는 false가 될 것이다. name이 가리키는 주소값은 다르기 때문이다.
  • person2와 person3는 주소값이나 name에 할당된 문자열이 모두 같기 때문에 ==, equals() 의 결과가 모두 true가 된다.

여기까지 String 클래스에서 재정의된 equals() 메소드에 대해 알아보았다.

[참고] equals의 재정의 메소드 만들기
만드는 방법은 간단하다. 이클립스에서 자동완성 해주는 equals() 메소드를 사용하는 것이다.
(기본적으로 hashCode, equals 2가지를 동시에 쓰는 것을 권장하지만 단순 비교만 하는 경우라면 equals()만 사용해도 무방하다.)

package study.first;

public class Main {

	public static void main(String[] args) {

		Sub s1 = new Sub();
		Sub s2 = new Sub();
		System.out.println(s1.equals(s2));   // True

	}
}

/* Sub 클래스 */
class Sub {

	int x;
	String a;

	@Override
	public boolean equals(Object obj) {
	
		/* 객체 단위 비교 */
		if (this == obj)  // 자기 자신이 비교 대상으로 들어왔으면 true
			return true;
		if (obj == null)  // 메모리 주소가 없는 객체라면 false
			return false;
		if (getClass() != obj.getClass())   // 비교 대상 둘이 다른 클래스라면 false
			return false;  // 필드와 값이 완전히 같더라도 다른 클래스 소속이면 false 

		/* 객체의 내용 비교 */
		Sub other = (Sub) obj;  // Object 타입으로 들어왔으므로 비교를 위해 형변환
		if (a == null) {  // 문자열 비교해서 다르면 false
			if (other.a != null)
				return false;
		} else if (!a.equals(other.a))
			return false;
		if (x != other.x)  // int(원시 타입) 비교해서 다르면 false
			return false;
		return true;  // 모두 같으면 true 리턴
	}
}

내용을 간단히 살펴보면, 먼저 큰 단위로 객체 자체를 비교한다.
그 후 비교대상이 맞다면 필드를 비교한다. 3번째 if문을 보면, 형식과 값이 같더라도 클래스가 다르다면 다른 것으로 간주한다.
문자열은 문자열 비교 메소드를 사용해서 다시 비교하고, 원시타입은 그냥 비교해준다.

이정도의 원리만 알고 이클립스에서 만들어주는 메소드를 사용하면 될 것이라 생각한다.


글을 마치면서

toString 과 equals에 대해 공부를 했지만 까먹은 부분이 많았던 것 같다.
특히 == 와 equals의 차이는 알고있으면서도 막상 떠올리면 까먹어서 헷갈렸던 순간이 많았다. 이번에 정리하면서 다시한번 머릿속에 넣을 수 있어서 좋았다.

profile
계속해서 기록하는 개발자. 조금씩 성장하기!

0개의 댓글