영권's
JAVA - 과제 (Object 클래스, String, StringBuilder, StringBuffer) 본문
StringBuilder와 StringBuffer는 무슨 차이가 있는가?
먼저 String 과 StringBuilder,StringBuffer 와의 큰 차이점은 String은 불변(immutable)의 속성을 갖는다는 점입니다.
String 객체를 생성하는 방법은 new 연산자를 이용하는 것과 리터럴을 이용한 방식이 있다.
리터럴을 이용하여 사용하면 String 값은 JVM 메모리 내의 Heap영역에 "String Constant Pool"에 저장되어 사용되지만,
new 연산자로 생성하면 같은 내용이라도 여러개의 객체가 각 Heap 영역을 차지한다.
같은 "cat" 이라는 문자열이지만 리터럴로 생성한 cat1 과 cat2는 같은 객체를 참조하고 cat3은 다른 객체를 참조한다.
하지만 StringBuilder와 StringBuffer는 가변성을 가지기 때문에 append()와 delete() 등의 API를 이용해서 동일 객체내에서 문자열을 변경하는 것이 가능합니다.
String은 값을 변경하면 hashCode 값이 변경되는데 StringBuilder는 값이 변경되어도 hashCode 값은 변경되지 않는다.
그럼 StringBuilder와 StringBuffer의 차이점은 무엇일까?
바로 동기화의 차이이다.
StringBuffer의 append에는 synchronized 키워드가 보이는데 바로 해당 메서드 전체를 임계영역으로 지정해서 동기화를 보장해준다는 것이다.
때문에 멀티스레드 환경에서 안전하다.
(synchronized 키워드란 https://cyk0825.tistory.com/28 )
반대로 StringBuilder에서는 동기화를 보장하지 않기 때문에 싱글스레드나 동기화를 고려하지 않아도 되는경우 사용 할 수 있다.
Object
- 모든 객체의 최상위 객체 입니다.
- 모든 객체에는 Object의 메소드를 호출할 수가 있다는 말.
- Object에 어떤 메소드가 있는지, 어떤 기능을 수행하는지.
- toString()
- equals()
- hashCode()
Object 객체는 모든 객체의 최상의 객체입니다.
Test라는 클래스를 만들어서 바이트코드를 보았더니
INVOKESPECIAL java/lang/Object.<init> ()V 라는 코드가 보인다.
4: invokespecial #3 // Method "<init>":()V
invokespecial은 다음과 같이 생성자, 현재 클래스, 수퍼클래스의 메서드를 호출한다고 나와 있다.
invokespecial
Operation
Invoke instance method; direct invocation of instance initialization methods and methods of the current class and its supertypes
그렇기 때문에 상속 받은적이 없는 Object 클래스의 메서드들을 사용하거나 오버라이딩할 수 있는것이다.
참고로 인텔리제이에서 show bytecode 를 검색하면 해당 클래스의 바이트 코드를 볼 수 있다.
toString()
객체의 문자열 표현을 반환합니다.
일반적으로 toString메서드는 객체를 "텍스트적으로 표현"합니다.
- toString의 일반 규약에 따르면 '간결하면서 사람이 읽기 쉬운 형태의 유익한 정보'를 반환해야 한다.
- 하지만 재정의 하지 않는다면 보통 이 메서드는 다음 값과 같은 문자열을 반환합니다.
getClass().getName() + '@' + Integer.toHexString(hashcode()) - 따라서 모든 하위 클래스에서 이 메서드를 재정의해야한다. toString을 잘 구현한 클래스는 사용하기에 훨씬 좋고, 그 클래스를 사용한 시스템은 디버깅하기 쉽다.
toString 메서드는 객체를 println,printf, 문자열 연결 연산자(+), assert 구문에 넘길 때, 혹은 디버거가 객체를 출력할 때 자동으로 불린다.
객체를 참조하는 컴포넌트가 오류 메시지를 로깅할 때 자동으로 호출할 수 있다.
하지만 toString을 제대로 정의하지 않는다면 쓸모없는 메시지만 로그에 남을 것이다.
PhoneNumber용 toString을 제대로 재정의했다면 다음 코드만으로 문제를 진단하기에 충분한 메세지를 남길수 있다.
System.out.println(phoneNumber + "에 연결할 수 없습니다.");
toString의 재정의와 상관없이 보통 진단 메시지는 이런 형태로 만들것이다. 하지만 재정의 하지 않았다면 쓸모없는 메시지가 출력된다.
좋은 toString 이 인스턴스를 포함하는 객체에서 유용하게 쓰인다.
map 객체를 출력했을 때 {name=PhoneNumber@adbdb} 보다는 {name=010-0000-0000}라는 메시지가 나오는것이 훨씬 유익할 것이다.
- 실전에서 toString은 그 객체가 가진 주요 정보 모두를 반환하는 게 좋다.
- 하지만 객체가 거대하거나, 객체의 상태가 문자열로 표현하기에 적합하지 않다면 무리가 있다. 이런 상황이라면 "맨하튼 거주자 전화번호부(총 1000개)" 나 "Thread[main,5,main]" 같은 요약 정보를 담아야 한다.
- 이상적으로는 스스로를 완벽히 설명하는 문자열이어야 한다.
정리
- 상위 클래스에서 이미 알맞게 재정의한 경우는 제외하고 모든 구체 클래스에서 toString을 재정의하는 것이 좋다.
- toString을 재정의한 클래스는 사용하기도 편하고 그 클래스를 사용한 시스템을 디버깅하기 쉽게 해준다.
- toString은 해당 객체에 관해 명확하고 유용한 정보를 읽기 좋은 형태로 반환해야한다.
eqauls()
Object 클래스에서 equals 메서드는 두 객체가 동일한지 검사하기 위해 사용된다.
즉, 2개의 객체가 가리키는 곳이 동일한 메모리 주소일 경우에만 동일한 객체가 된다.
equals는 재정의하기 쉬워보이지만 곳곳에 함정이 있다. 문제를 회피하는 가장 쉬운 길은 아예 재정의하지 않는 것이다.
equals를 재정의하지 않아야 할 상황
- 각 인스턴스가 본질적으로 고유할 때
- 인스턴스의 논리적 동치성(logical equality)를 검사할 일이 없을 때
- 상위 클래스에서 재정의한 equals가 하위 클래스에도 들어맞을 때
- 클래스가 private이거나 package-private이고 equals 메서드를 호출할 일이 없을 때
그렇다면 equals를 재정의해야 할 때는 언제일까?
객체 식별성(object identity: 두 객체가 물리적으로 같은가)이 아니라 논리적 동치성을 비교하도록 확인해야 하는데, 상위 클래스의 equals가 논리적 동치성을 비교하도록 재정의되지 않았을 때다(주로 값 클래스 ex)String ).
equals 메서드를 재정의할 때 반드시 따라야 할 Object 명세에 적힌 equals 메서드의 일반 규약
- 반사성(reflexive) : null이 아닌 모든 참조 값x에 대해 x.equals(x)는 true다.
- 대칭성(symmetric) : null이 아닌 모든 참조 값 x,y에 대해, x.equals(y)가 true면 y.equals(x)도 true다.
- 추이성(transitive) : null이 아닌 모든 참조 값 x,y,z에 대해 x.equals(y)와 y.equals(z)가 true면 x.equals(z)도 true다.
- 일관성(consistent) : null이 아닌 모든 참조 값 x,y에 대해, x.equals(y)를 반복해서 호출하면 항상 true 이거나 항상 false 이다
- null-아님 : null이 아닌 모든 참조 값 x에 대해, x.equals(null)은 false다.
정리
- 꼭 필요한 경우가 아니면 equals를 재정의하지 말자.
- 많은 경우에 Object의 equals가 프로그래머가 원하는 비교를 정확히 수행해준다.
- 재정의해야 할 때는 그 클래스의 핵심 필드 모두를 빠짐없이, 다섯 가지 규약을 확실히 지켜가며 비교해야 한다.
HashCode
hashCode 메소드는 객체의 해시코드 값을 리턴합니다.
이 메소드는 해시 테이블(java.util.HashMap 같은)을 사용할 때의 이점을 위해 제공됩니다.
hashCode 메소드의 일반 규약은 다음과 같습니다.
- 변경되지 않은 한 객체의 hashCode 메소드를 호출한 결과는 항상 똑같은 integer 값이어야 합니다.
- 객체가 변경됐더라도 equals 메소드가 참고하는 정보가 변경되지 않았다면 hashCode 값은 달라지지 않습니다.
- equals 메소드가 같다고 판별한 두 객체의 hashCode 호출 결과는 똑같은 integer 값이어야 합니다.
- 그러나 java.lang.Object.equals 메소드가 다르다고 판별한 두 객체의 hashCode 값이 반드시 달라야 하는 것은 아닙니다.
- 그러나 프로그래머는 동일하지 않은 객체에 대해 구별되는 정수 결과를 생성하면 해시 테이블의 성능이 향상될 수 있음을 알아야 합니다.
equals를 재정의한 클래스는 hashCode도 재정의 해야한다.
그렇지 않으면 hashcode 일반 규약을 어기게 되어 해당 클래스의 인스턴스를 hashMap이나 HashSet 같은 컬렉션의 원소로 사용할 때 문제를 일으킬 것이다.
equals는 논리적 동치성 비교를 위해 물리적으로 다른 두 객체를 논리적으로는 같다고 할 수도 있다.
하지만 Object의 기본 hashcode 메서드는 이 둘이 전혀 다르다고 판단하여, 규약과 달리 서로 다른 값을 반환한다.
Map<User, String> map1 = new HashMap();
map1.put(new User(20, "choi"), "1");
System.out.println(map1.get(new User(20, "choi")));
위 코드는 map1.get 에서 결과로 "1"이 나와야 할 것 같지만 실제로는 null을 반환한다.
여기서는 2개의 서로 다른 객체가 사용되었다.
User 클래스는 hashcode를 재정의하지 않았기 때문에 논리적 동치인 두 객체가 서로 다른 해시코드를 반환하여 두 번째 규약을 지키지 못한다.
그 결과 get메서드는 엉뚱한 해시 버킷에 가서 객체를 찾으려 한 것이다.
두 인스턴스를 같은 버킷에 담았더라도 get 메서드는 여전히 null을 반환하는데, HashMap은 해시코드가 다른 엔트리끼리는 동치성 비교를 시도조차 하지 않도록 최적화되어 있기 때문이다.
정리
- equals를 재정의할 때는 hashcode도 반드시 재정의해야 한다. 그렇지 않으면 프로그램이 제대로 동작하지 않을 것이다.
- 재정의한 hashcode는 Object의 API문서에 기술된 일반 규약을 따라야하며 서로 다른 인스턴스면 되도록 해시코드도 다르게 구현해야 한다.
Object 메서드들
참고 :
도서 - 이펙티브 자바 3/E 아이템 10, 11, 12
https://docs.oracle.com/javase/8/docs/api/index.html
'데브코스 웹 백엔드' 카테고리의 다른 글
디자인 패턴(Behavioral) - Mediator (0) | 2021.08.17 |
---|---|
20210810 - TIL (데이터베이스) (0) | 2021.08.11 |
백엔드 데브코스 10일 동안의 회고 및 다짐 (0) | 2021.08.10 |
객체지향과 디자인 패턴 (0) | 2021.08.03 |