개요
StringBuilder sb = new StringBuilder(str);
List<Integer> alphabetIndex = new ArrayList<>();
List<Character> alphabet = new ArrayList<>();
String regex = "^[a-zA-Z]$"; // 알파벳 a~z, A~Z로 시작하는 문자 1개
for (int i = 0; i < sb.length(); i++) {
if(Character.toString(sb.charAt(i)).matches(regex)) { // equals(regex)는?
alphabetIndex.add(i);
alphabet.add(sb.charAt(i));
}
}
입력한 문자열을 문자 단위로 순회하면서 정규식과 일치하는지 확인하고 일치하는 부분은 각각의 리스트에 추가하는 과정을 구현한 코드이다.
처음에는 if문에서. equals(regex)를 이용해서 문자열의 참/거짓을 비교하였다. a%b를 입력했을 때 순서대로 true(a), false(%), true(b)를 예상했지만, 정규식과 일치함에도 조건식의 반환값이 모두 false가 나왔다. 찾아보니 정규식은 matches 메서드를 사용해 비교해야 한다는 사실을 깨달아 문제를 해결할 수 있었다. "왜 이런 실수를 하게 되었는지"에 대한 원인 파악과 해당 개념의 차이점을 상세히 알아보자.
Stirng 객체 생성 방식
- 리터럴(literal)
- new 연산자
두 방식의 차이점은 리터럴로 생성하는 경우 "String constant pool"이라는 영역에 객체가 저장되게 되고, new 연산자로 생성하는 경우 Heap 영역에 객체가 저장되게 된다. 여기서 중요한 점은 String을 리터럴로 선언할 경우 내부적으로 String의 intern() 메서드가 호출되게 된다는 것이다. intern() 메서드는 주어진 문자열이 String Constant Pool에 존재하는지 검색한다. 만약 존재한다면 그 주소값을 반환하고 없다면 String Constant Pool에 넣고 새로운 주소값을 반환한다.
String Constant Pool 이란?
Java에서 문자열 리터럴을 저장하는 독립된 영역을 `String Constant Pool` 또는 `String Pool`이라고 부른다.
String은 불변객체이기 때문에 문자열의 생성 시 이 String Constant Pool에 저장된 리터럴을 재사용할 수 있다.
String Constant Pool에 저장된 문자열은 프로그램 전반에 걸쳐 참조되고 사용되기 때문에 일반적으로 GC(Garbage Collection) 대상이 되지는 않는다.
단, 이 문자열들이 더 이상 참조되지 않는 경우(어떤 문자열을 참조하는 변수가 더 이상 해당 문자열을 참조하지 않는 경우) 선택적으로 GC 대상이 되기도 한다.
※ 참고
Java 7 이전
- JVM - PermGen 영역에 존재했다.
- PermGen은 JVM의 Native 영역에 해당하며, 클래스 메타데이터와 함께 상수와 정적 변수 등을 저장하는 데 사용되었다.
Java 7
- String Constant Pool이 Heap 영역으로 이동하였다. 이 변경의 주요 목적 중 하나는 PermGen 영역의 메모리 제한 문제를 완화하고 더 유연한 메모리 관리를 가능하게 하게 위함이였다.
Java 8
- PermGen 영역이 완전히 제거되고, Metaspace가 도입되게 된다.
- Metaspace는 Native 메모리 영역에서 클래스 메타데이터를 저장하는 데 사용된다. 이러한 변경은 주로 클래스 메타데이터의 저장 방식에 영향을 미쳤고, 필요에 따라 운영 체제로부터 메모리를 동적으로 할당받을 수 있게 되었다. 이는 PermGen의 고정된 메모리 한계와 관련된 문제들을 완화시켰다.
지금부터 설명하는 "종이책과 eBook 예시"를 보면 String 객체 생성 방식을 보다 더 쉽고 명확하게 이해할 수 있다.
"팁택톡"이라는 제목을 가진 eBook을 한번 사이트에 등록해 놓았다고 가정하자. 종이책일 경우 "팁택톡"을 지금 읽고 싶은데, 현재 나에게 없으면 다시 서점에 가서 "팁택톡"이라는 책을 사야 한다. 하지만 eBook일 경우 사이트에 등록해 놓았기 때문에, 다시 다운로드를 하면 언제든지 어디서든지 원할 때 책을 볼 수 있다.
이를 String 객체 생성 방식에 도입해 보자. 'new'연산자로 String 객체를 생성하는 것은 종이책을 매번 새로 구매하는 것과 비슷하고, 리터럴을 사용하는 것은 eBook을 사이트에 한번 등록하여 언제 어디서나 사용할 수 있게 하는 것과 비슷하다고 볼 수 있다.
== 와 equals() 의 차이점
String 객체 생성 방식을 이해했으니 본격적으로 비교 방법의 차이점을 살펴보자.
결론부터 말하자면 참조 데이터 타입인 경우 '=='는 물리적 동치성(메모리 주소가 같은지)을 비교하고, 논리적 동치성(갖고 있는 값이 같은지)을 비교한다. 문장만 봤을 때, 물리적...? 논리적...? 이해가 안 갈 수 있지만, 다음 코드와 그림을 보면 쉽게 이해할 수 있다.
★ [필수] 그림을 보기 전, 두 가지 개념에 대해서 이해하고 가자.
1. 참조 타입 변수는 참조하는 객체의 주소 값을 가지며, 기본 타입 변수는 기본 타입의 값을 가진다.
2. '==' 연산자는 기본 데이터 타입인 경우에는 값을 비교하고, 참조 데이터 타입인 경우에는 메모리 주소를 비교한다.
물리적 동치성 ('==') 비교
double a = 5.5;
double b = 5.5;
String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello");
StringBuilder sb1 = new StringBuilder("hello");
StringBuilder sb2 = new StringBuilder("hello");
1. a == b
a와 b는 기본 타입 변수로 Stack 영역에 값과 함께 저장된다. 기본 데이터 타입이기에 '=='은 값을 비교한다. 따라서 두 변수의 값이 5.5 동일하므로 결과는 'true'이다.
2. s1 == s2
s1과 s2는 참조 타입 변수로 Stack 영역에 참조하는 객체의 주소 값과 함께 저장된다. 참조 데이터 타입이기에 '=='은 메모리 주소를 비교한다.
앞서 말했듯이, String을 리터럴로 선언할 경우 내부적으로 String의 intern() 메서드가 호출되게 된다. 그러면 intern() 메서드는 주어진 문자열이 String Constant Pool에 존재하는지 검색한다. 만약 존재한다면 그 주소값을 반환하고 없다면 String Constant Pool에 넣고 새로운 주소값을 반환한다.
s1에 리터럴로 String 객체 "hello"를 넣으면 이때 intern() 메서드가 호출된다. 그러면 intern() 메서드는 "hello" 객체가 String Constant Pool에 존재하는지 검색하게 되고, 없기 때문에 String Constant Pool에 넣고 새로운 주소값(200번지)을 반환한다. 그리고 s2에 리터럴로 String 객체 "hello"를 넣으면 또 한 번 intern() 메서드가 호출되고, 이번에는 "hello" 객체가 String Constant Pool에 있기에 그 주소값(200번지)을 반환하게 된다.
따라서 s1과 s2는 동일한 메모리 주소를 참조하게 되며 결과는 'true'가 된다.
3. s2 == s3
s3도 역시 참조타입 변수로 Stack 영역에 참조하는 객체의 주소 값과 함께 저장된다. 참조 데이터 타입이기에 '=='은 메모리 주소를 비교한다.
단, s2와의 차이점은 new 연산자를 사용하여 String 객체를 생성했다는 점이다. 앞서 말했듯 new 연산자는 String Constant Pool이 아닌 Heap 영역에 메모리를 할당받는다.
s3에 new 연산자로 String 객체 "hello"를 넣으면 intern() 메서드가 호출되지 않고(String Constant Pool에 존재 여부를 확인하지 않음) 새로운 Stirng 객체 "hello"가 Heap 영역에 생성된다. 즉, s2와는 다른 주소(100번지)로 메모리가 할당되어 객체가 생성된다.
따라서 s2와 s3는 다른 메모리 주소를 참조하므로 결과는 'false'가 된다.
4. sb1 == sb2
StringBuilder라 해서 다를 게 없다. 역시 sb1, sb2 둘 다 new 연산자를 사용하였기에 다른 메모리 주소를 참조하므로 결과는 'false'이다.
논리적 동치성(equals()) 비교
String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello");
String s4 = new String("hello");
StringBuilder sb1 = new StringBuilder("hello");
StringBuilder sb2 = new StringBuilder("hello");
이제 우리는 코드를 보면 s1과 s2이 가리키고 있는 String 객체가 String Constant Pool에 있는 같은 객체이고 s3, s4, sb1, sb2가 가리키고 있는 String 객체는 각각 다른 객체라는 것을 알 수 있다.
1. s1.equals(s2)
s1과 s2가 가리키는 객체는 같은 객체이다. 서로 동일(Same)하고 동등(equal)하므로 결과는 'true'이다.
2. s2.equals(s3)
s2와 s3가 가르키는 객체는 다른 객체이지만, 갖고 있는 값("hello")은 같다. 서로 동일하지 않지만(Not Same), 동등(equal)하므로 결과는 'true'이다.
3. s3.equals(s4)
s3와 s4가 가르키는 객체는 다른 객체이지만, 갖고 있는 값("hello")은 같다. 역시 서로 동일하지 않지만(Not Same), 동등(equal)하므로 결과는 'true'이다.
4. sb1.equals(sb2)
sb1과 sb2가 가르키는 객체는 다른 객체이지만, 갖고 있는 값("hello")은 같다. 서로 동일하지 않지만(Not Same), 동등(equal)하므로 결과는 'true'라고 생각하겠지만 결과는 'false'다.
왜 이런 결과가 발생하는 것일까? 그 이유는 바로 String 클래스의 equals() 메서드와, StringBuilder 클래스의 equals() 메서드는 동일한 메서드가 아니기 때문이다. 다음 코드를 살펴보자
public boolean equals(Object anObject) {
if (this == anObject) { // 동일성(Same or Not Same) check
return true;
}
return (anObject instanceof String aString) // 동등성(equal or Not equal) check
&& (!COMPACT_STRINGS || this.coder == aString.coder)
&& StringLatin1.equals(value, aString.value);
}
String 클래스의 equals 메서드이다. 먼저 if문에서 현재 객체('this')와 비교 대상 객체(anObject)가 메모리에서 "동일(Same)"한 지 비교한다. s1.equals(s2)를 예시로 들면 호출할 때, s1이 this로 간주되고 s2가 equals메서드의 매개변수 anObject로 전달된다.
만약 동일하지 않으면 return 문에서 매개변수로 전달된 객체가 String 타입이고(instanceof 체크), 같은 문자 인코딩 방식을 사용하며 (coder 체크), 실제 문자 데이터가 동일하다면(StringLatin1.equals) 두 String 객체는 "동등(equal)"한 것으로 간주된다.
public boolean equals(Object obj) { // 동일성만 check
return (this == obj);
}
Object 클래스의 equals 메서드이다. 클래스를 착각하여 Object 클래스의 equals 메서드를 가져왔다고 생각했다면 그건 오산이다.
StringBuilder 클래스에는 equals 메서드가 정의되어 있지 않다. 보다 자세하게 말하면 StringBuilder 클래스는 Object 클래스의 equals 메서드를 오버라이드하지 않는다.
Java의 모든 클래스는 최상위 클래스인 Object 클래스를 상속한다. 그래서 equals() 메서드를 오버라이딩 하지 않으면 Object의 equals () 메서드를 사용하게 된다. 그래서 StringBuilder는 Object 클래스의 equals() 메서드를 사용하게 되는 것이고, 동일성 비교만 하기 때문에 앞서 봤던 sb1.equals(sb2)는 서로 다른 객체를 가리키기에 'false'가 되는 것이다.
matches() 분석
String 클래스의 matches
public boolean matches(String regex) {
return Pattern.matches(regex, this);
}
String 클래스의 matches() 메서드이다. 현재 문자열(this)이 주어진 정규 표현식 regex와 일치하는지를 검사하는데 Pattern 클래스의 matches 메서드를 호출하여 검사한다. 좀 더 깊숙하게 들어가 보자.
Pattern 클래스의 matches
public static boolean matches(String regex, CharSequence input) {
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(input);
return m.matches();
}
Pattern 클래스의 matches() 메서드이다. 다음의 과정을 거쳐 정규 표현식과 문자열이 일치하는지를 검사한다.
- 정규 표현식 컴파일: Pattern.matches(regex, this) 호출을 통해, 첫 번째 매개변수로 전달된 정규 표현식 regex를 컴파일한다. Pattern 클래스는 Java의 정규 표현식 API를 제공하며, 정규 표현식을 해석하고 처리하는 데 사용된다.
- 매칭 검사: Pattern 클래스의 matches 정적 메서드는 두 번째 매개변수로 전달된 문자열(this)이 컴파일된 정규 표현식과 일치하는지를 검사한다. 이 메서드는 문자열이 정규 표현식에 완전히 일치할 때 true를 반환한다. 여기서 "완전히 일치"란 문자열 전체가 정규 표현식 패턴에 부합해야 함을 의미한다.
- 결과 반환: 문자열이 정규 표현식과 일치하면 true, 그렇지 않으면 false를 반환한다.
Matcher 클래스의 matches
public boolean matches() {
return match(from, ENDANCHOR);
}
boolean match(int from, int anchor) {
this.hitEnd = false;
this.requireEnd = false;
from = from < 0 ? 0 : from;
this.first = from;
this.oldLast = oldLast < 0 ? from : oldLast;
for (int i = 0; i < groups.length; i++)
groups[i] = -1;
for (int i = 0; i < localsPos.length; i++) {
if (localsPos[i] != null)
localsPos[i].clear();
}
acceptMode = anchor;
boolean result = parentPattern.matchRoot.match(this, from, text);
if (!result)
this.first = -1;
this.oldLast = this.last;
this.modCount++;
return result;
}
Matcher 클래스의 matches 메서드이다. 여기서 가장 중요한 줄은 다음과 같다.
boolean result = parentPattern.matchRoot.match(this, from, text);
이 한 줄의 코드는 매칭 로직의 핵심을 담고 있다. 다음의 과정을 거쳐 Matcher 객체가 가진 정규 표현식 패턴(parentPattern)과 대상 문자열(text) 사이의 매칭을 시도하고, 그 결과를 불리언 값으로 반환하여, 매칭의 성공 여부를 판단한다.
- parentPattern.matchRoot: parentPattern은 현재 Matcher 객체와 연결된 Pattern 객체를 나타낸다. 모든 정규 표현식 매칭 연산은 Pattern 객체를 통해 수행된다. Pattern 객체는 정규 표현식 패턴을 컴파일한 결과를 내부적으로 가지고 있으며, 이 패턴은 매칭을 수행하는 데 사용된다. matchRoot는 이 패턴의 루트 노드를 나타내며, 실제 매칭 로직을 구현한 부분이다.
- match(this, from, text): match 메서드는 실제로 정규 표현식과 문자열이 일치하는지를 검사한다. 이 메서드는 세 개의 매개변수를 받는다.
- this: 현재 Macher 객체의 인스턴스로 Macher 객체는 매칭 상태와 결과를 관리한다.
- from: 매칭을 시작할 문자열 내의 시작 위치를 나타낸다.
- text: 매칭 대상이 되는 문자열을 나타낸다.
- 결과 반환: match 메서드는 매칭이 성공적으로 수행되었는지 여부를 나타내는 불리언 값을 반환한다. 이 값(result)은 매칭이 정규 표현식 패턴과 일치하는 경우 true가 되고, 그렇지 않은 경우 false가 된다.
간단히 말하면, Pattern 클래스의 matches()는 간단한 전체 매칭 검사에 사용되며, Matcher 클래스의 matches()는 매칭 과정을 더 세밀하게 제어하고 매칭 결과에 대한 상세 정보가 필요할 때 사용된다.
장점
String 클래스의 matches() 메서드는 Pattern과 Matcher 클래스의 기능을 내부적으로 활용하여, 문자열이 주어진 정규 표현식과 전체적으로 일치하는지 간편하게 확인할 수 있도록 해준다.
사용자가 직접 Pattern 객체를 컴파일하고, Matcher 객체를 생성한 다음, 매칭을 수행하는 과정을 단순화하여, 정규 표현식의 일치 여부를 한 줄의 코드로 간단히 판단할 수 있게 해 주기 때문에 문자열이 특정 패턴에 부합하는지 빠르게 확인하고자 할 때 유용하게 사용가능하다.
matches() 와 equals() 의 차이점
용도
equals는 두 객체(주로 문자열)의 내용 자체가 같은지 확인하는 데 사용되며, matches는 문자열이 정규 표현식에 정의된 특정 패턴을 따르는지 검사하는 데 사용된다.
비교기준
equals는 문자열의 길이와 각 문자의 값이 같은지를 기준으로 하고, matches는 문자열 전체가 정규 표현식에 정의된 패턴과 일치하는지를 기준으로 한다.
사용 시나리오
문자열의 내용이 완전히 같은지 확인할 때는 equals를 사용해야 하고, 문자열이 특정 형식(이메일, 전화번호 등)을 따르는지 확인할 때는 matches를 사용해야 한다.
문제가 발생한 원인
String regex = "^[a-zA-Z]$";
if(Character.toString(sb.charAt(i)).equals(regex))
내가 적었던 코드는 sb의 i번째 문자를 문자열로 변환한 것이 "^[a-zA-Z]$"라는 문자열과 정확히 같은지를 비교하는 것이었다. a%b를 입력했을 때 모두 false가 나왔던 이유는 각 문자열 "a", "%", "b"가 "^[a-zA-Z]$"와 동등한 지를 물었기 때문이다. 당연히 문자열 내용이 다르기 때문에(Not equal) 'false'일 수밖에 없던 것이다.
결론
💡 문자열을 비교할 때, '=='는 객체 참조의 동일성을, equals()는 객체 내용의 동등성을, matches()는 문자열의 정규 표현식 패턴 일치 여부를 검사하는 데 사용하자!
출처
☕ 자바 String 타입 특징 이해하기 (String Pool & 문자열 비교)
여타 대부분의 프로그래밍 언어에서 문자열 이라는 데이터를 저장하기 위해 string 이라는 데이터 타입을 사용한다. 이 string 데이터를 다루는데 있어 특별히 유의해야 할점은 없어보이지만, 자바
inpa.tistory.com
[ JAVA ] '=='와 'Equals' 비교 분석
개요 public class Main{ public static final void main(String[] args){ StringBuilder sb1 = new StringBuilder("apple"); StringBuilder sb2 = new StringBuilder("apple"); if(sb1.equals(sb2)){ System.out.println("같다"); } System.out.println("다르다"); }
javanitto.tistory.com
[Java] 문자열 비교하기 == , equals() 의 차이점
Java에서 int와 boolean과 같은 일반적인 데이터 타입의 비교는 ==이라는 연산자를 사용하여 비교합니다. 하지만 String처럼 Class의 값을 비교할때는 ==이 아닌 equals()라는 메소드를 사용하여 비교를 합
coding-factory.tistory.com
'JAVA' 카테고리의 다른 글
[JAVA] Generic 타입 (0) | 2024.04.10 |
---|---|
[JAVA] this와 this()의 차이점 (0) | 2024.04.09 |