HashMap을 사용할 때, put(key, value)으로 데이터를 저장할 수 있는데 HashMap의 경우 key 값이 존재할 경우 value 값을 대체하고 기존의 value 값을 반환해준다. 하지만 key 값이 존재하지 않는 경우에는 새로 데이터를 저장하고 null을 반환한다. (기존 value 값이 없기 때문)
put()으로 데이터를 저장하고 반환된 value 객체를 참조할려고 하니 NullPointerException이 발생한다는 경고를 보고 어떻게 구현이 되어있나 확인해보았다.
HashMap에서 해싱이란 해시 함수를 이용해서 데이터를 해시테이블에 저장하고 검색하는 방식을 말하는데 해시함수가 데이터가 저장되어 있는 곳을 알려주기 때문에 다량의 데이터 중에서 원하는 데이터를 빠르게 찾을 수 있다.
해싱에서 사용하는 자료구조는 배열과 링크드 리스트의 조합으로 되어 있는데 key 값을 해시함수에 넣으면 배열의 한 요소를 얻게 되고, 다시 그 곳에 연결되어 있는 링크드 리스트에 저장이 된다.
해시함수의 계산 결과인 해시코드로 해당 값이 저장되어 있는 링크드 리스트를 찾고, 링크드 리스트에서 검색한 키와 일치하는 데이터를 찾는다. (링크드 리스트는 검색에 불리하기 때문에 해시코드가 서로 중복되지 않도록 하는 전략이 좋다.)
HashMap의 경우 Object 클래스에 정의된 hashCode()를 해시 함수로 사용하는데 이는 객체의 주소를 이용하는 알고리즘으로 모든 객체에 대해 hashCode() 결과가 서로 유일하게 된다.
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
그래서 실제 HashMap의 put() 메서드를 보면 putVal() 메서드를 호출하면서 해시함수 hash(key)를 실행하고 해시 코드를 넘겨준다.
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
...
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
putVal() 메서드의 일부를 보면 key가 존재하는지 확인을 하고 value를 교체한 뒤 oldValue를 반환하거나 그렇지 않을 경우 null을 반환한다.
Stack 자료구조의 특정 객체를 파라미터로 받아서 해당 요소가 있으면 위치를 반환해주는 search() 메서드를 구현해보던 중 equals()로 비교를 할 때, 전달 받은 파라미터가 null인 경우를 따로 체크하는 부분이 있었다.
public synchronized int lastIndexOf(Object o, int index) {
if (index >= elementCount)
throw new IndexOutOfBoundsException(index + " >= "+ elementCount);
// Obect o 객체가 null 인 경우
if (o == null) {
for (int i = index; i >= 0; i--)
if (elementData[i]==null)
return i;
} else {
for (int i = index; i >= 0; i--)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
equals()를 사용할 때는 주의해야 될 부분이 있는데 값을 비교할 때 앞에 null인 객체가 들어올 경우 NullPointerException 예외가 발생하는 것이다.
public int lastIndexOf(Object value) {
int index = array.length - 1;
for (int i = index; i >= 0; i--) {
// value가 Null인 경우
if (value.equals(array[i])) {
return i;
}
}
return -1;
}
객체 참조를 하기 때문에 당연한 부분이지만 전에도 이런 실수를 많이 했던 것 같다. 그래서 객체와 문자열의 값을 비교하는 경우에는 문자열을 왼쪽에 입력하고, 객체끼리 비교하는 경우에는 null 값을 고려해서 코드를 짜도록 해야겠다.
Stack 자료구조를 공부하던 중 System.arraycopy()를 발견하고 Arrays.copy()랑 무슨 차이가 있나 알아보았다.
사실 Arrays.copyOf() 메서드가 어떻게 되어있나 한번 봤으면 내부적으로 System.arraycopy() 를 호출한다는 것을 바로 알았을텐데 멍청하게도 둘이 다른 것인줄 알았다.
System.arraycopy()의 경우 src의 지정된 위치 srcPos에서부터 dest의 지정된 위치 destPos로 length만큼 복사를 하는데 JVM은 복사하기 전 소스 배열과 대상 배열의 타입 비교를 한다.
public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
Arrays.copyOf()는 내부에서 System.arraycopy()를 호출하는만큼 별도로 추가적인 기능을 제공하는데 새로운 배열을 만들고 내용을 자르거나 채울 수 있다. Object 타입 체크를 하는 arraycopy()와 달리 두 Object가 다른 타입일 경우에도 Arrays.newInstance()로 생성을 한다. (primitive 타입일 경우는 바로 배열을 생성하고 복사)
Enum 클래스로 상태 코드를 조회하고 각각의 코드에 따른 로직을 별도의 메소드를 통해 수행을 하도록 만들면 상태 코드의 조회와 코드에 따른 계산이 분리되어서 서로 관계가 있음을 표현할 수가 없다. 그래서 다음과 같이 Enum 클래스를 활용하여 계산 기능을 추가할 수 있다.
public enum CalculatorType {
CALC_A(value -> value),
CALC_B(value -> value * 10),
CALC_C(value -> value * 3),
CALC_ETC(value -> 0L);
private Function<Long, Long>. expression;
CalculatorType(Function<Long, Long> expression) {
this.expression = expression;
}
public long calculate(long value) {
return expresiion.apply(value);
}
}
Function은 value1을 받아서 value2로 반환해주는 역할을 하며 apply()를 통해 인자값을 받으면 계산식을 거쳐서 결과를 반환 받을 수 있다.
핵심은 객체가 상태(값)와 행위(로직)을 갖고 있는 것으로 직접 활용을 안 해봐서 100% 이해는 못 했지만 이번 프로젝트에서 적절하게 적용해보고 직접 적용해본 내용을 정리해보면 좋을 것 같다.