[F-Lab 모각코 챌린지 12일차] try-with-resource / String의 hashCode() 뜯어보기
TIL
- try-with-resource
- String의 hashCode() 뜯어보기
- hash란?
- String 클래스의 hashCode()
- hash를 구할 때, 31을 곱하는 이유
try-with-resoure
보통 resource는 외부의 데이터(DB, Network, File)를 일컫는다. 이런 리소스들은 자바 내부에 위치한 요소들이 아니다. 자바 코드에서 외부에 있는 리소스에 접근하려고 할 때, 문제(예외)가 발생할 수 있는 여지가 존재한다. 특히 입출력에 관련된 리소스들에 접근해서 사용하고 나면 close 해주는 것이 굉장히 중요하다. 예를 들어, 파일에 접근해 파일을 열고 내용을 쓴 후에는 꼭 닫아주어야 한다. 왜냐하면 어떤 리소스를 사용하다가 다른 곳에서 같은 리소스에 접근해 사용하다 보면 꼬일 수 있기 때문이다.
public static void main(String[] args) {
FileWiriter file = null;
try{
file = new FileWriter("data.txt");
file.write("F-lab Mentoring");
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
// close()에서 발생하는 예외도 처리해야한다.
try {
file.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
위 코드는 data.txt 파일을 읽고 쓰는 코드이다. 해당 텍스트 파일을 외부에 있는 리소스이기 때문에 예상치 못한 오류가 발생할 수 있다. 그래서 try-catch문을 사용해서 IOException으로 예외처리를 했다. 그리고 외부 리소스를 사용했다면 반드시 닫아줘야 하기 때문에 finally문에서 파일을 close() 해주었다. 그런데 파일을 닫는 메소드에서도 IOException이 발생할 수 있기 때문에 close()에 대해서도 예외처리를 해주어야 한다. 그저 파일을 열고 닫는데 이렇게 많은 코드를 작성해야하고 가독성도 매우 떨어진다.
이렇게 외부 리소스를 다룰 때의 코드 복잡성을 개선하기 위해 JAVA7에서 try-with-resource문이 추가 되었다.
try-with-resource문은 주로 입출력(I/0)과 관련된 클래스를 사용할 때, 유용하다. 입출력에 사용한 객체를 자동으로 반환시켜주기 때문이다. 즉, 개발자가 명시적으로 close()를 해줄 필요가 없다는 것이다. try 블록에 소괄호를 추가하여 그 안에서 파일을 열거나 자원을 할당할 수 있고, try 블록이 끝나자마자 자동으로 파일을 닫거나 할당된 자원을 해제해준다.
public interface Closeable extends AutoCloseable { }
public static void main(String[] args) {
// try 블록 소괄호 내에 예외가 발생할 수 있는 객체
// close()를 이용해 파일을 닫아야할 필요가 있을 때, AutoCloseable을 구현한 객체는 별도로 처리할 필요가 없다.
try (FileWriter file = new FileWriter("data.txt")) {
file.write("Hello World");
} catch (IOException e) {
e.printStackTrace();
}
}
JAVA7에는 AutoCloseable이라는 인터페이스가 추가되었다. 이 인터페이스는 java.lang 패키지에 선언 되어있다. try-with-resource를 사용할 때, 이 인터페이스를구현한 클래스는 close()를 호출할 필요가 없다. 위 예제에서 쓰인 FileWriter 클래스는 Closeable 인터페이스를 구현하고 있는데 이 Closeable 인터페이스는 AutoCloseable 인터페이스를 상속 받고 있다.
try(
FileInputStream fis = new FileInputStream("a.txt");
DataInputStream dis = new DataInputStream(fis)
) {
// 내용생략
} catch (EOFException e){
} catch (IOException ie){
}
try 블록 소괄호 안에 세미콜론 ;을 이용해서 IO 객체 문장을 두개 이상 넣어줄 수도 있다.
String의 hashCode() 뜯어보기
Hash란?
해시(함수, 알고리즘)란 임의의 길이의 데이터를 고정된 길이의 데이터로 매핑하는 함수이다. 해시함수에 의해 얻어지는 값을 해시, 해시 코드, 해시 체크섬라고 한다.
해시 알고리즘을 통해 생성된 데이터는 원본 데이터가 어떤 데이터인지 유추하기 어려워서 암호학에 활용된다. 예를 들어, 컴퓨터는 3 * 7의 결과가 21이라는 것은 빨리 연산할 수 있지만 21이라는 숫자가 어떤 두 소수로 이루어져 있는지는 찾아내기 힘들다.
어떤 길이의 데이터라도 고정된 데이터로 바뀐다는 특징과 같은 값을 넣으면 같은 결과를 갖는다는 성질을 이용해 데이터를 빠르게 검색할 수도 있다. HashMap 자료구조에서는 같은 key 값으로 항상 같은 value를 얻을 수 있다. 이때의 시간복잡도는 O(1)을 갖는다. 그러나 해시 충돌이 일어날수록 O(N)에 가까워진다. 해시 충돌은 다른 데이터를 넣었는데 같은 hash 결과가 나왔을 때를 의미한다.
String 클래스의 hashCode() 메소드는 String 객체가 가지고 있는 문자열을 고정된 길이의 정수값으로 매핑한다.
String 클래스의 hashCode()
// String
private final byte[] value;
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
hash = h = isLatin1() ? StringLatin1.hashCode(value)
: StringUTF16.hashCode(value);
}
return h;
}
// StringLatin1
public static int hashCode(byte[] value) { // flab -> [102, 108, 97, 98]
int h = 0;
for (byte v : value) {
h = 31 * h + (v & 0xff);
}
return h;
}
// StringUTF16
public static int hashCode(byte[] value) {
int h = 0;
int length = value.length >> 1;
for (int i = 0; i < length; i++) {
h = 31 * h + getChar(value, i);
}
return h;
}
String 클래스에서 hash를 만드는 로직이다. 여기서는 StringLatin1 클래스를 기준으로 설명하겠다.
flab이라는 문자열로 hash를 생성한다.- 반복문을 통해 byte 배열에 저장된 character와
0xff를 &연산한다.- &연산: 비트단위로 AND 연산을 한다.

- 저장 된 해시에
31을 곱하고 2단계 결과값을 더한다. - 문자열 길이만큼 반복한다.
hash를 구할 때, 31을 곱하는 이유
그런데 해시코드를 만드는 과정 중, 왜 하필 31을 곱해주는 걸까?
결론만 얘기하면 JVM은 짝수를 곱할 때, shift 연산으로 처리한다. 짝수를 계속 곱하게 되면 비트를 왼쪽으로 옮기면서 오른쪽을 0으로 채운다. 이 방식으로 계속해서 left shift 연산을 하게 되면 결국 모든 비트가 오버플로우 되고 0만 가득 차게 된다. 이렇게 되면 해시 충돌이 일어날 가능성이 높아지고 정보 손실에 우려가 생긴다. 그래서 31이라는 홀수를 곱하는 것이다. 아래서 더 자세히 알아보자.
2진수에 2의 배수를 곱하는 것은 left shift 연산을 하는 것과 같다.
0000 0101(2) = 5
0000 1010(2) = 5 X 2 = 5 << 1
0001 0100(2) = 5 X 2 X 2 = 5 << 2
즉, 어떤 수 a X 2^n 은 a << n 와 같다.
컴퓨터 입장에서는 곱하기 연산을 하는 것보다 비트를 옆으로 한 칸씩 옮기는 것이 더 편하다.
다만 shift 연산 시, 짝수를 계속 곱해서 오버플로우가 발생할 경우엔 비트들이 모두 0으로 채워지고 해시 충돌로 인하여 정보의 손실이 우려된다.
0000 0101(2) = 5
0000 1010(2) = 5 << 1 = 10
0001 0100(2) = 5 << 2 = 20
0010 1000(2) = 5 << 3 = 40
0101 0000(2) = 5 << 4 = 80
1010 0000(2) = 5 << 5 = 160
0100 0000(2) = 5 << 6
1000 0000(2) = 5 << 7
0000 0000(2) = 5 << 8
위의 예제를 보면 left shfit 연산을 할 때, 연산 값이 8이상으로 커지면 해시 충돌이 일어날 가능성이 높아진다.
JVM에서 어떤 수에 홀수를 곱할 때, 다음과 같이 최적화 한다.
a * 31은 JVM에서 a << 5 - a로 계산한다.

31을 곱하게 되면 shfit 연산과 뺄셈 연산을 통해서 오른쪽 비트에 채워지는 데이터가 0이 아닌 다른 값이 올 수 있다.
이제 홀수를 곱하는 이유까진 알겠는데 그 많은 홀수 중에서 굳이 31을 사용하는 이유가 있을까? 이펙티브자바를 참고하면 그냥 예전부터 그렇게 사용했기 때문에 지금까지 사용하는 것이라고 볼 수 있겠다.
롬복이라는 라이브러리에서는 hashCode() 메소드 구현 시, 59라는 숫자를 사용한다.
📚 참고
자바의 신
이펙티브자바
https://velog.io/@indongcha/hashCode%EC%99%80-31
자바 Try With Resource 예외 처리
'JAVA' 카테고리의 다른 글
| [F-Lab 모각코 챌린지 14일차] ArrayList 특징, 배열과의 차이점, 순회하는 방법 (0) | 2023.06.21 |
|---|---|
| [F-Lab 모각코 챌린지 13일차] Java Collection Framework 개요 (0) | 2023.06.20 |
| [F-Lab 모각코 챌린지 11일차] 시간복잡도, 공간복잡도 (0) | 2023.06.18 |
| [F-Lab 모각코 챌린지 10일차] 자바가 실수를 다루는 법-2 (BigDecimal) (0) | 2023.06.17 |
| [F-Lab 모각코 챌린지 9일차] 자바가 실수를 다루는 법-1 (float, double) (0) | 2023.06.16 |
댓글
이 글 공유하기
다른 글
-
[F-Lab 모각코 챌린지 14일차] ArrayList 특징, 배열과의 차이점, 순회하는 방법
[F-Lab 모각코 챌린지 14일차] ArrayList 특징, 배열과의 차이점, 순회하는 방법
2023.06.21 -
[F-Lab 모각코 챌린지 13일차] Java Collection Framework 개요
[F-Lab 모각코 챌린지 13일차] Java Collection Framework 개요
2023.06.20 -
[F-Lab 모각코 챌린지 11일차] 시간복잡도, 공간복잡도
[F-Lab 모각코 챌린지 11일차] 시간복잡도, 공간복잡도
2023.06.18 -
[F-Lab 모각코 챌린지 10일차] 자바가 실수를 다루는 법-2 (BigDecimal)
[F-Lab 모각코 챌린지 10일차] 자바가 실수를 다루는 법-2 (BigDecimal)
2023.06.17