티스토리 뷰
웹개발에서 문자열을 다루는 방법은 매우 중요합니다. 그렇다 보니 많은 웹 서비스 업체의 코딩 테스트에는 문자열을 다루는 문제가 꼭 하나씩은 포함되는데요. 때로는 이미 언어의 API에 구현되어 있는 메소드를 통해 손쉽게 해결 가능할 때도 있습니다. 그런 경우에 그 메소드를 미리 몰랐다면 괜한 고생을 하게 되겠죠.
자바의 경우 String 클래스 안에 이미 유용한 메소드들을 많이 제공하고 있습니다. 코딩테스트나 실무에서 문자열을 다룰 때 이 메소드들을 잘 알고 있다면 시간과 비용을 절감할 수 있겠죠? 실제 자바의 String 클래스를 확인해 보면서 자바에서 문자열을 어떻게 다루는지 또, 어떤 메소드를 언제 사용하면 좋을 지 알아 보도록 하겠습니다.
String
자바에서 문자열은 String이라는 객체로 표현되지만 실제로는 character의 배열입니다. 배열이라는 것은 결국 고정된 주소값을 가진다는 것이고 그러다 보니 자바에서 String은 객체이지만 상수로 취급이 됩니다. 다시 말해 최초에 문자열이 생성되고 나면 그 값을 바꿀 수 없다는 뜻입니다. (이것을 보충하기위해 String buffers를 사용할 수 있습니다.) 그러나 String 객체가 불변성을 가지기 때문에 서로 쉽게 공유될 수 있다는 장점도 가지고 있습니다.
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
private int hash;
private static final long serialVersionUID;
public String() {
this.value = new char[0];
}
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
...
}
String 클래스의 생성자를 살펴보면 이런 특성을 쉽게 파악할 수 있습니다. 기본적으로 String 의 값은 value라는 char형 배열에 담기는데 final로 선언이 되어서 한 번 생성되면 바뀔 수 없다는 것을 알 수 있습니다.
int length()
public int length() {
return value.length;
}
String의 value가 문자 배열로 이루어 져 있다는 것은 결국 그 문자열의 길이를 구하는 것은 value라는 문자 배열의 길이를 구하는 것과 같다는 뜻입니다. 실제로 String 클래스에서 제공하는 length 메소드는 value의 배열 길이를 리턴합니다.
boolean isEmpty()
public boolean isEmpty() {
return value.length == 0;
}
배열의 길이가 0이라면 문자열이 없다는 뜻이겠지요? 그래서 isEmpty 메소드는 배열의 길이가 0인지를 판단해 boolean 값을 리턴합니다.
char charAt(int index)
public char charAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
charAt 메소드는 해당하는 인덱스의 문자를 반환합니다. value는 문자 배열이기 때문에 O(1)이라는 짧은 시간 안에 문자를 리턴하게 됩니다. 그러니까 n번째 문자를 찾아야 하는 경우에는 무조건 charAt 메소드를 사용하는 것이 좋습니다.
boolean equals()
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;
}
두 문자열이 같은지를 비교하는 것이 equals 메소드입니다. 자바에서 객체간의 비교는 먼저 가리키는 메모리 위치가 같은 지를 비교하고, 그 다음에 저장되어있는 값이 같은지를 비교하는 식으로 진행됩니다. String은 문자 배열로 이루어져 있기 때문에 문자 배열을 하나씩 비교해 가며 같은 값을 가지는지를 파악하는 것을 볼 수 있습니다. 그렇기 때문에 최악의 경우 O(n)의 시간이 걸리겠습니다.
boolean startWith(), endWith()
public boolean startsWith(String prefix, int toffset) {
char ta[] = value;
int to = toffset;
char pa[] = prefix.value;
int po = 0;
int pc = prefix.value.length;
// Note: toffset might be near -1>>>1.
if ((toffset < 0) || (toffset > value.length - pc)) {
return false;
}
while (--pc >= 0) {
if (ta[to++] != pa[po++]) {
return false;
}
}
return true;
}
이제 문자열 비교에 관련된 메소드의 원리는 다 비슷합니다. 결국 문자 배열에서 하나씩 값을 비교하는 것이죠. startWith 메소드는 어떤 특정한 문자열로 시작하는지, endWith 메소드는 어떤 특정한 문자열로 끝나는지를 검사합니다. 재밌는 것은 endWith 메소드 역시 내부에서 startWith 메소드를 호출해서 검사한다는 것이죠. 결국 같은 로직이기 때문입니다.
int indexOf()
public int indexOf(int ch, int fromIndex) {
final int max = value.length;
if (fromIndex < 0) {
fromIndex = 0;
} else if (fromIndex >= max) {
// Note: fromIndex might be near -1>>>1.
return -1;
}
if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
// handle most cases here (ch is a BMP code point or a
// negative value (invalid code point))
final char[] value = this.value;
for (int i = fromIndex; i < max; i++) {
if (value[i] == ch) {
return i;
}
}
return -1;
} else {
return indexOfSupplementary(ch, fromIndex);
}
}
이제 특정한 문자의 index를 찾을 수 있습니다. 원리는 결국 value 라는 문자 배열을 하나씩 돌면서 비교 하는 것이죠 이 메소드는 가장 처음 나타다는 해당 문자의 위치를 반환합니다. 반대로 마지막 인덱스를 반환하는 lastIndex 메소드도 존재합니다.
String substring()
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
substring은 문자열의 일부를 복사하는 메소드입니다.
String concat(String str)
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
concat 메소들을 이용해 문자열을 이어 붙일 수 있습니다. 다시한번 말씀 드리지만 value 값은 final 이기 때문에 수정되지 않습니다. 그래서 새로운 문자열 객체를 만들어야 합니다.
String replace()
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(buf, true);
}
}
return this;
}
replace 메소드는 문자 하나를 교체합니다. 코드를 살펴보면 가장 처음 해당하는 문자를 교체하고 새로운 String 객체를 만들어 반환하는 것을 알 수 있습니다.
String replaceAll()
public String replaceAll(String regex, String replacement) {
return Pattern.compile(regex).matcher(this).replaceAll(replacement);
}
해당하는 모든 문자를 교체하는 메소드는 replaceAll 입니다. 그러나 그 로직은 String 클래스 안에 공개되어있지 않습니다.
boolean matches()
public boolean matches(String regex) {
return Pattern.matches(regex, this);
}
matches 메소드는 정규표현식으로 나타낸 문자열 패턴을 검사하는 메소드입니다.
boolean contains()
public boolean contains(CharSequence s) {
return indexOf(s.toString()) > -1;
}
contains 메소드는 해당 문자열을 포함하고 있는지를 검사합니다. 그런데 해당하는 로직을 직접 구성하지 않고 indexOf 함수를 이용하고 있는 것이 인상적입니다. indexOf 함수의 반환값이 있다는 것은 해당하는 문자열을 포함한다는 뜻이겠지요.
String[] split()
public String[] split(String regex, int limit) {
/* fastpath if the regex is a
(1)one-char String and this character is not one of the
RegEx's meta characters ".$|()[{^?*+\\", or
(2)two-char String and the first char is the backslash and
the second is not the ascii digit or ascii letter.
*/
char ch = 0;
if (((regex.value.length == 1 &&
".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) ||
(regex.length() == 2 &&
regex.charAt(0) == '\\' &&
(((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 &&
((ch-'a')|('z'-ch)) < 0 &&
((ch-'A')|('Z'-ch)) < 0)) &&
(ch < Character.MIN_HIGH_SURROGATE ||
ch > Character.MAX_LOW_SURROGATE))
{
int off = 0;
int next = 0;
boolean limited = limit > 0;
ArrayList<String> list = new ArrayList<>();
while ((next = indexOf(ch, off)) != -1) {
if (!limited || list.size() < limit - 1) {
list.add(substring(off, next));
off = next + 1;
} else { // last one
//assert (list.size() == limit - 1);
list.add(substring(off, value.length));
off = value.length;
break;
}
}
// If no match was found, return this
if (off == 0)
return new String[]{this};
// Add remaining segment
if (!limited || list.size() < limit)
list.add(substring(off, value.length));
// Construct result
int resultSize = list.size();
if (limit == 0) {
while (resultSize > 0 && list.get(resultSize - 1).length() == 0) {
resultSize--;
}
}
String[] result = new String[resultSize];
return list.subList(0, resultSize).toArray(result);
}
return Pattern.compile(regex).split(this, limit);
}
split 메소드는 특정한 문자열을 기준으로 문자열을 분리해서 배열로 만드는 메소드 입니다. 역시 내부 로직에서 indexOf 와 substring 메소드를 활용하는 것을 볼 수 있습니다.
String join()
public static String join(CharSequence delimiter, CharSequence... elements) {
Objects.requireNonNull(delimiter);
Objects.requireNonNull(elements);
// Number of elements not likely worth Arrays.stream overhead.
StringJoiner joiner = new StringJoiner(delimiter);
for (CharSequence cs: elements) {
joiner.add(cs);
}
return joiner.toString();
}
join 메소드는 내가 이 포스트를 작성하게 만든 원인이 된 메소드 입니다. 이 메소드는 구분자와 문자열 배열을 전달 받으면 문자열들 사이에 구분자를 넣어서 하나로 합쳐주는 메소드 입니다.
String trim()
public String trim() {
int len = value.length;
int st = 0;
char[] val = value; /* avoid getfield opcode */
while ((st < len) && (val[st] <= ' ')) {
st++;
}
while ((st < len) && (val[len - 1] <= ' ')) {
len--;
}
return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}
trim 메소드는 문자열의 양 끝에 포함된 공백을 제거합니다.
char[] toCharrArray()
public char[] toCharArray() {
// Cannot use Arrays.copyOf because of class initialization order issues
char result[] = new char[value.length];
System.arraycopy(value, 0, result, 0, value.length);
return result;
}
toCharArray 메소드는 문자열을 문자 배열로 반환합니다. 애초에 문자열 자체가 문자배열을 value 값으로 가지기 때문에 value를 반환하면 될 것 같지만 value는 final 값이라 변경이 되지 않기 때문에 다른 문자 배열에 복사해서 리턴하게 됩니다.
toLowerCase(), toUpperCase()
자주 사용하는 메소드로 toLowerCase와 toUpperCase가 있습니다. 모든 문자열을 소문자 혹은 대문자로 바꿔주는 메소드 입니다.
이상으로 String 클래스에서 자주 사용하는 메소드들에 대해 알아 보았습니다. 메소드들을 잘 파악하고 있으면 코딩테스트나 실무에서 당황하지 않고 효율을 높일 수 있을 것입니다.
감사합니다.
'language > JAVA' 카테고리의 다른 글
추상 클래스와 인터페이스의 차이 (3) | 2019.10.14 |
---|---|
final 키워드 (0) | 2019.10.12 |
BufferedInputStream과 BufferedOutputStream (0) | 2019.05.06 |
우아한 프리코스 피드백 내용 정리 (0) | 2019.04.25 |
logger.error("Exception :: {}" , e.getMessage()); (0) | 2019.04.23 |
- Total
- Today
- Yesterday
- 몰라서망신
- 야근
- Warning
- 크롬
- was
- java
- 문장 생성기
- 클린코드
- DP
- 전략패턴
- 자바스크립트개론
- REST API
- 코딩의 기술
- RESTful
- Count
- 유지보수
- 마르코프 연쇄
- 로그
- 마르코프
- markov chain
- restful api
- 동적계획법
- html
- Markov
- 경고
- 디자인패턴
- 자바스크립트 개론
- Spring in Action
- CONVENTIONS
- GROUP BY
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 31 |