티스토리 뷰

Java

String Class와 equals()

LichKing 2015. 12. 2. 11:15

이런저런사이트에서 이제막 자바를 배우기 시작한 분들이 많이 하는 실수중 하나가 문자열 비교이다.

String 변수와 리터럴 문자열을 비교하는데 동일한 값인데 false가 떨어진다는 것이다. 이런 질문에 대한 답변은 주로 '문자열은 eqlaus()로 비교하세요.' 라는 답변이 주로 달리는데 이러다보니 equals()가 문자열 비교를 위해 존재하는 메서드인줄 아는 분들이 있는것같아 이번 포스팅을 작성하게 됐다.

 

일단 String 클래스의 특수성부터 확인해보자. 문자열은 무조건 equals()로 비교하라고 하지만 ==연산자가 항상 false를 반환하진 않는다.

 

String str1 = "str";

System.out.println(str == "str");

 

String str2 = new String("str");

System.out.println(str == "str");

 

String 클래스는 자바의 무한한 참조형 타입중에 유일하게 리터럴 표현을 지원하는 타입이다. 이때문에 우리는 문자열을 선언할때마다 new String()을 작성할 필요없이 ""로만 작업을 할 수 있는것이다. 다만 위 예제 소스의 두 출력문이 다르다는건 분명 리터럴 선언과 new 연산자를 이용한 객체생성이 무언가 차이점이 있다는 이야기일것이다. 그 차이점이 무엇일까.

 

이클립스로 작업한다는 가정하에 소스를 작성하고 실행 버튼을 누르게되면 컴파일러가 자바의 바이트코드를 생성하고(class파일) jvm이 해당 class파일들을 인터프리터방식으로 실행하게된다. 이때 jvm은 메모리를 몇가지 영역으로 나누게되는데 지금 알아야할건 상수영역과 heap영역이다.

 

리터럴로 선언하게되면 상수영역에 상수로 생성이 되고 new 연산자를 이용해 생성하면 여타 객체들과 동일하게 heap영역에 생성이 되는것이다. 상수영역에 좀 더 얘기를 하자면 상수는 변할수없는 고정된 값을 의미한다. 그리고 상수선언은 final 키워드를 붙여야하는데 상수라니?

 

이것에 대해 좀 더 얘기를 해보면 일단 리터럴로 생성된 문자열은 상수가 맞다. 해당 주소에 생성된 String 객체는 값이 절대 변하지않는다. 예제소스를 봐보자.

 

String str = "hello";

str += "world";

 

System.out.println(str);

 

잘 모르는상태에서 우리가 생각했을때는 str에 "hello"를 저장하고...(얼마전에 call by value, call by reference 포스팅도 작성했고 이미 String 클래스가 참조형 변수인걸 알고있으니 좀 더 고급지게 표현해보자) "hello" 라는 값을 갖고있는 String 객체를 생성하고 str은 그 객체를 가리키게된다. 그리고 += "world" 를 통해 str이 가리키고있는 객체의 내부값에 연산을 하게된다. 라고 생각이 될것이다. 하지만 리터럴로 생성된 String 객체는 상수이기때문에 값이 바뀔수가없다. 어쨋든 출력은 우리가 예상한대로 출력될테지만 내부는 우리의 예상과는 좀 다르게 진행된다.

 

상수영역(constant pool)에 "hello" 가 생성되고 연산을 위해 또 다른 주소에 "world" 가 생성된다. 그리고 "hello" 는 상수이기때문에 변할 수 없다. 연산결과를 저장하기위해 또다른 주소에 "helloworld" 라는 객체를 생성하고 str이 그곳을 가리키게한다.

 

감이 오는가? 사실 위 예제 소스는 그리 낯설지않은 소스이다. 코딩을 하다보면 계속해서 문자열에 추가문자열을 더하는 연산을 많이 하게되기때문인데 사실 저런 연산은 메모리적으로 봤을때 매우 비효율적인 연산인것이다.

 

(범위가 벗어나는것같아 잠깐만 언급하자면 해당 문제를 해결하기위한 클래스를 자바에서 제공하고있으며 StringBuilder와 StringBuffer가 존재한다. 그런데 요즘은 또 컴파일러도 더 좋아져서 String변수에 += 연산을 계속하게되면 자기가 알아서 StringBuffer로 바꿔서 컴파일한다고한다. 그래서 사실...이제는 그냥 String += 계속 써도된다.)

 

자 이제 String 타입의 객체를 리터럴로 생성할때와 new 연산자를 이용해 생성할때의 차이점은 알게되었다.

그럼 이제 도대체 equals() 메서드가 뭐길래 문자열 비교할땐 얘를 쓰라는건지 살펴보자.

 

일단 equals()가 가장 처음 정의되어있는 곳은 String 클래스가 아니다. 모든 클래스들의 부모클래스인 Object 클래스이다. 그리고 그 Object에 정의되어있는 equals()메서드는 내부적으로 == 연산을 한 값을 반환하게 된다.

 

??? 그렇다면 ==로 비교하지 뭣하러 equals()라고 따로 메서드까지 만들고 또 문자열 비교시에 호출 결과가 다른것일까?

모든 클래스가 상속받는 Object 클래스는 당연히 String 클래스도 상속을 받게되고 equals()메서드를 오버라이딩해서 비교를 하는것이다.

 

먼저 == 연산부터 살펴보자. == 연산은 기본형 타입의 경우 값을 비교하고 참조형타입의 경우 참조값을 비교하게된다. 그런데 객체는 내부 속성값이 전부 동일해도 new 연산을 할때마다 다른 주소에 객체가 생성되게 되므로 객체간의 비교에서 ==연산은 무조건 false가 반환되게된다. 그렇다면 객체간 비교에 맞는 연산이 있어야하는데 그것을 수행하는게 equals() 메서드인것이다.

 

equals() 메서드는 그냥 문자열 비교하라고 제공하는 메서드가 아니라 객체간 비교연산을 위해 제공되는 메서드이며 그렇기때문에 항상 오버라이딩을 해줘야하는것이다. 오버라이딩을 해주지않게되면 어차피 equals()의 원형은 == 이므로 항상 false가 반환되게 될것이다.

그리고 equals() 메서드의 제공이유에 걸맞게 String은 equals() 메서드를 완벽하게 오버라이딩해서 제공하고있고 그때문에 서로 다른 주소에 있다 하더라도 동일한 문자열을 가지고있을시에는 true를 반환하게 하는것이다.

 

이제 포스팅 막바지인데 다음의 예제소스를 보자

String str1 = "str";

String str2 = "str";

 

System.out.println(str1 == str2);

 

가장 처음 만났던 예제소스와 비슷한내용인데 리터럴 생성시 상수영역에 생긴다는건 이해했는데 이건 왜 true가 나올까? 상수영역에 "str" 로 2개의 객체가 생성되게되고 메모리 영역은 다르더라도 어쩃든 주소는 서로 다른게 아닌가?

리터럴로 문자열을 생성하게되면 상수영역을 한번 훑게된다. 그리고 찾는 문자열이 있으면 해당 주소를 가리키게되고 없으면 그때 생성하게된다.

즉 str1은 상수영역에 "str"이 없었기때문에 새로 객체를 생성했지만 str2를 생성할때는 이미 존재하는 "str"을 가리키게된다는 것이다. 그렇기때문에 해당 연산은 true가 반환된다.

 

정리하면

1. String 클래스는 자바 참조형 타입중 유일하게 리터럴 표현을 지원함

2. 리터럴로 생성하면 상수영역, new 연산자로 생성하면 힙 영역에 생성됨

3. equals()는 문자열 비교메서드가 아니라 객체비교메서드임. 객체간 비교를 하기위해선 문자열이 아니더라도 equals() 메서드를 오버라이딩 해야함

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함