1. String 객체의 생성(new , "")과 메모리 속 저장
String은 Java에서 가장 많이 사용하는 클래스라고 해도 과언이 아니다. String의 생성자는 매우 많아서 다 외우는 것보다 필요에 의해 찾아서 사용하는 편이 더 좋다. 일반적으로 Java에서 이 String 객체를 생성하는 2가지 방법이 있다.
1. String literal인 큰 따옴표("")를 사용하는 것. 예시 : String name = "symbol"
2. new 연산자를 이용하는 것. 예시 : String name = new String("symbol")
소스 코드를 직접 작성해서 실행해보면 의문점이 생길 수 있다.
과연 1번과 2번을 통해 생성한 객체에 동일한 문자열을 주고 같다, 다르다를 출력해보면 어떻게 나올까?
과연 이 String literal을 통해 생성한 String 객체 text, text2를 ==을 통해 확인하면 어떻게 출력이 될까?
코드를 직접 작성해보면 '같다' 라고 나올 것이다.
코드를 이렇게 바꿔본다면??
결과는?
?? 분명히 같은 값인데? 왜 ? 다르다고 하는거지?
코드를 이렇게 수정해보면?
이것도 다를까?
그러면 이 두 방법에는 어떠한 차이가 있을까??
위 의문점에 대한 코드를 정리해보면 우선 큰 따옴표("")를 통해 생성한 객체의 값을 비교했을 땐 '같다'
new를 통해 새로운 객체를 만들고, 객체끼리 값을 비교했을 때는 '다르다'
1번과 2번을 비교했을때도 '다르다'
해당 원리는 JVM의 구조 속에 답이 있다.
JVM은 JavaVirtualMachine의 약자로 자바를 실행하기 위한 가상의 컴퓨터(기계)로 볼 수 있는데, 자세한 내용은 다음에 학습 후 정리하도록 하겠다.
JVM 속에는 다양한 영역이 존재한다. 코드를 작성할 때 사용하는 데이터, 메소드 등을 영역을 정해서 저장하게 되는데, 상수풀(Contstant pool)도 이러한 영역 중 하나이다. 여기서 상수풀(Constant pool)이라 하믄 클래스와 같은 Heap 영역의 고정영역 (Permanent area)에 생성되어 Java Process의 종료까지 생을 같이 한다.
상수풀의 위치는 heap에 있을 수 있거나, method 영역에 있을 수 있다.
위 그림을 보면 String literal을 통해 생성한 객체는 String constant pool 영역에 저장하고, new를 통해 생성한 객체는 Heap 영역에 저장됨을 확인할 수 있다. String literal을 통해 생성된 상수풀(Constant Pool)에 저장하고 사용하는 것을 String Interning이라고 하는데, 이렇게 String Interning에 저장된 String 값은 불변성(Immutability)를 가지게 된다.
JVM속에는 여러 영역이 존재하는데, 그 중 Heap 영역과 연관이 있다. new 생성은 Heap 메모리 영역에 저장되며, 큰 따옴표(" ")는 상수풀(Runtime Constant pool)에 저장된다. 결국 여기서 만약에 str2로 새로운 객체를 만들어 동일한 문자열 값을 주게 되면, Heap 영역에 새로운 객체인 str2가 만들어 진다. 결국 str1과 str2의 객체는 다른 객체가 되는 것이다.
하지만 str2를 new가 아닌 String literal("")로 만들게 되면 constant pool에 있는 똑같은 주소 값을 갖게 된다. (이미 같은 값이 있기에 새로 만들지 않는다.)
그래서 위와 같은 결과를 내는 것이다.
new 연산자를 통해 String 객체를 생성하지 않는 쪽이 좋겠다. 결국 같은값을 new 연산자로 계속 만들면 heap 영역에 계속 쌓이니까..? 불필요할 것이라 생각된다. (같은 내용의 여러 객체가 heap 영역을 차지함.)
그래서 메모리를 효율적으로 사용하기 위해선 항상 String literal(큰 따옴표)를 사용하는 것이 좋겠다.
추가
기존의 자바에서는 상수풀을 Permanent라는 정적 영역에서 관리 했지만, 정적 영역(static)은 할당 받은 메모리를 늘릴 수 없어 관리 측면에서 어려워 상수가 추가되면 StackOverFlow가 발생할 가능성이 있어서 Java8부터 Heap 영역으로 바뀌었다.
String은 왜 불변성을 갖지?
Java에서 문자열이 불변성을 갖는 이유는 보안, 동기화 및 동시성, 캐싱 및 클래스 로딩 때문이다.
String 객체는 String 풀에 캐시되어 String을 불변으로 만드는데, 캐시된 문자열 리터럴은 여러 클라이언트에서 엑세스한다. 때문에 한 클라이언트가 수행하는 작업이 다른 모든 클라이언트에 영향을 미치는 위험이 있다.
때문에 성능상의 이유로 String 객체의 Caching이 중요하므로 String을 불변으로 만들어야 한다.
참고
https://www.javatpoint.com/why-string-is-immutable-or-final-in-java
기존의 String문자열에 계속 더해서 문자열을 추가하면 새로운 String 객체가 생성되고, 기존에 존재하는 객체는 버려져서 GC(가비지 컬렉터)의 대상이 된다. (버려진다는 말은 쓰레기가 된다는 말)
이러한 단점을 보완하기 위해서 나온 클래스가 StringBuffer와 StringBuilder이다. 이 두 클래스는 기존의 String의 단점을 보완하기 위해 나왔다. 가장 큰 차이점은 불변성과 가변성이다.
위에서 우리가 정리한 내용에 의하면 String 객체는 불변성을 가진다고 했는데, StringBuffer과 StringBuilder는 가변성을 갖는다.
만약에 고정된 문자열을 읽어서 사용하는 경우에는 String을 사용한다면 성능의 이점을 얻을 수 있겠지만, 삭제, 수정 등 자주 변하는 문자열을 처리하는 경우에 String을 사용하면 결국 계속 새로운 객체를 만들고 기존의 객체는 GC의 대상이되어 성능상에서 이슈가 발생할 것이다.
그래서 이 두 클래스(StringBuffer와 Builder)는 객체의 공간이 부족해지면 기존의 버퍼 크기를 늘리면서 유연하게 동작한다.
StringBuffer와 StringBuilder에서 제공하는 메소드는 같지만, 동기화의 차이점이 존재한다.
StringBuffer는 멀티쓰레드 환경에서 동기화를 지원(Thread-safe)하지만, StringBuilder는 단일쓰레드 환경에서만 동기화를 지원해준다. 결국 멀티쓰레드 환경에서 buffer를 사용하는 것이 적절하지만, 단일 쓰레드에서는 동기화를 고려하지 않아도 되기에 builder를 사용하는 것이 성능적인 측면에서는 더 좋다.
문자열을 처리해야 할 때, 해당 문자열을 어떻게 사용할 것인지 잘 생각해서 String과 buffer, builder를 선택적으로 사용해야 성능측면에서 효율적으로 사용할 수 있다.
2. 다양한 String의 내용을 비교하는 메소드
1. 문자열 길이 확인 메소드 length()
2. 문자열의 공백을 확인하는 메소드 isEmpty()
입력받은 혹은 전달받은 문자열이 비어 있어아먄 ture를 출력하고, 나머지는 false
3. 문자열이 같은지 비교하는 메소드
String 클래스에서는 문자열이 같은지 비교하는 메서드들이 많다. 이름을 통해 분류한다면 equals로 시작하는 메소드, compareTo로 시작하는 메소드, contentEquals로 시작하는 메소드 (3가지)로 분류할 수 있는데, 이름은 다 다르지만 결국 매개 변수로 넘어온 값과 String 객체가 같은지를 비교하는 메소드들이다.
- equals(Object anObject), equalsIgnoreCase(String anotherStr)
- compareTo(String anotherStr), compareToIgnoreCase(String str)
- contentEquals(charSequence cs), contentEquals(StringBuffer sb)
여기서 ignoreCase가 붙은 메소드들은 대소문자의 구분 여부만 다르며, ignore 무시한다. Case는 대소문자를 말한다.
1. Equals()
자바에서 객체를 비교하려고 한다면 이 메소드를 사용해야 한다. 자바에서 비교를 위해서 연산자인 ==과 equals() 메소드를 사용하는데, == 연산자는 주소의 값을 비교하는데, 실제 내용의 값이 아닌 자료의 위치 값이다.
equals()는 객체끼리 내용을 비교한다.
==연산자 : Call by Reference (주소 값을 비교함)
equals 메소드 : Call by Value (값 자체를 비교함)
우리가 예상했던대로 출력한 결과가 나왔을 것이다.
2. EqualsIgnoreCase()
1번과 동일한 기능을 갖고 있으며, 대소문자를 구분하지 않고 두 개의 값이 같은지만 확인한다.
3. CompareTo()
해당 메소드는 보통 정렬을 할 때 사용하기에 True, False의 결과가 아닌 비교하려는 매개 변수로 넘겨준 String 객체가 알파벳 순서로 앞에 있으면 양수를, 뒤에 있으면 음수를 리턴하고, 알파벳 순서만큼 그 숫자값은 커진다.
4. CompareToIgnoreCase()
마찬가지로 대소문자 구분하지 않고 CompareTo() 메소드를 수행하는 것과 같음.
5. ContentsEquals
매개변수로 넘어오는 CharSequence와 StrungBuffer 객체가 String 객체와 같은지 비교할 때 사용.
4. 특정 조건에 맞는 문자열을 확인하는 메소드
문자열을 처리할 때, 우리가 원하는 단어나 특정 조건을 걸어서 해당 문자열을 찾을 수 있어야 하는데, 여기에도 마찬가지로 다양한 메소드들이 존재한다. 평소에는 indexOf()라는 메소드를 사용해 확인이 가능하지만 이 메소드의 단점은 모든 내용을 다 확인해야 하기에 만약 데이터가 엄청나게 많은데 일일이 다 확인하기에는 성능 이슈가 발생할 수 있다고 생각한다.
리턴 타입 | 메소드 이름 및 매개 변수 |
boolean | startsWith(String prefix) |
boolean | startsWith(String prefix, int toffset) |
boolean | endsWith(String suffix) |
boolean | contains(CharSequence s) |
boolean | matches(String regex) |
boolean | regionMatches(boolean ignoreCase, int toffset, String other, int ooffset, int len) |
boolean | regionMatchers(int toffset, String other, int ooffset, int len) |
1. startsWith()
이 메소드는 이름 그대로 매개 변수로 넘겨준 값으로 시작하는 지를 확인한다.
2. endsWith()
이 메소드는 매개 변수로 넘어온 값으로 해당 문자열이 끝나는 지를 확인한다.
3. contains()
이 메소드는 매개 변수로 넘어온 값이 문자열에 존재하는 지를 확인한다.
4. matches()
3번 contain()와 비슷하지만, 매개 변수로 넘어오는 값이 '정규 표현식'으로 되어 있어야 한다.
5. regionMatches()
문자열 중에서 특정 영역이 매개 변수로 넘어온 문자열과 동일한지를 확인하는 데 사용한다.
'Language > Java' 카테고리의 다른 글
문자열 나누기 - StringTokenizer (0) | 2022.08.03 |
---|---|
자료형과 BigDecimal 그리고 소수점(부동, 고정) (0) | 2022.05.22 |
예외 처리(Exception) (0) | 2022.05.15 |
인터페이스와 추상클래스 그리고 Enum (0) | 2022.05.14 |
람다식(Lambda)과 함수형 인터페이스(fuctional interface) (0) | 2022.01.23 |