LinkedHashMap의 내부 구현 코드를 통해서 작동 방식부터 살펴보자.
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
...
// HashMap의 Put 메서드에서 마지막에 afterNodeInsertion을 호출한다.
afterNodeInsertion(evict);
}
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
// removeEldestEntry을 통해서 오래된 데이터를 지울 것인지에 대한 여부를 체크한다.
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
// LinkedHashMap 현재 내부 구현 : 항상 false를 리턴하고 있다.
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
이렇게 구현되어 있는 LinkedHashMap을 removeEldestEntry를 통해서 캐시로 사용할 수 있다. 아래에 책에 소개 되어 있는 방법을 소개한다.
class OverrideLinkedHashMap extends LinkedHashMap<String, Integer> {
private final int maxSize;
OverrideLinkedHashMap(int maxSize) {
this.maxSize = maxSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry<String, Integer> eldest) {
return size() > maxSize;
}
}
@Test
void Eldest_Entry_Removal_Override_Test() {
OverrideLinkedHashMap overrideLinkedHashMap = new OverrideLinkedHashMap(1);
overrideLinkedHashMap.put("1번", 1);
overrideLinkedHashMap.put("2번", 2);
assertThat(overrideLinkedHashMap.size()).isEqualTo(1);
assertThat(overrideLinkedHashMap.get("1번")).isEqualTo(null);
}
@FunctionalInterface
public interface EldestEntryRemovalFunction<K, V> {
boolean remove(Map<K, V> map, Map.Entry<K, V> eldest);
}
class InterFaceLinkedHashMap extends LinkedHashMap<String, Integer> {
private final EldestEntryRemovalFunction<String, Integer> removalFunction;
InterFaceLinkedHashMap(EldestEntryRemovalFunction<String, Integer> removalFunction) {
this.removalFunction = removalFunction;
}
@Override
protected boolean removeEldestEntry(Map.Entry<String, Integer> eldest) {
return removalFunction.remove(this, eldest);
}
}
@Test
void Eldest_Entry_Removal_Interface_Test() {
InterFaceLinkedHashMap overrideLinkedHashMap = new InterFaceLinkedHashMap(
(map, eldest) -> map.size() > 1
);
overrideLinkedHashMap.put("1번", 1);
overrideLinkedHashMap.put("2번", 2);
assertThat(overrideLinkedHashMap.size()).isEqualTo(1);
assertThat(overrideLinkedHashMap.get("1번")).isEqualTo(null);
}
class BiPredicateLinkedHashMap extends LinkedHashMap<String, Integer> {
private final BiPredicate<Map<String, Integer>, Map.Entry<String, Integer>> removalBiPredicate;
BiPredicateLinkedHashMap(BiPredicate<Map<String, Integer>, Map.Entry<String, Integer>> removalBiPredicate) {
this.removalBiPredicate = removalBiPredicate;
}
@Override
protected boolean removeEldestEntry(Map.Entry<String, Integer> eldest) {
return removalBiPredicate.test(this, eldest);
}
}
@Test
void Eldest_Entry_Removal_BiPredicate_Test() {
BiPredicateLinkedHashMap biPredicateLinkedHashMap = new BiPredicateLinkedHashMap(
(map, eldest) -> map.size() > 1
);
biPredicateLinkedHashMap.put("1번", 1);
biPredicateLinkedHashMap.put("2번", 2);
assertThat(biPredicateLinkedHashMap.size()).isEqualTo(1);
assertThat(biPredicateLinkedHashMap.get("1번")).isEqualTo(null);
}
표준 함수형 인터페이스는 총 43개이며, 기본 인터페이스 6개를 통해서 나머지 부분을 유추할 수 있는 시스템이다.
- 인수 타입, 반환 타입이 같다
- 인수 값이 1개이다.
인터페이스 | 메서드 |
---|---|
UnaryOperator | T apply(T t) |
IntUnaryOperator | int applyAsInt(int value) |
LongUnaryOperator | long applyAsLong(long value) |
DoubleUnaryOperator | double applyAsDouble(double value) |
// 예시: String::toLowerCase
@Test
void UnaryOperator_Test() {
UnaryOperator<String> unaryOperator = String::toLowerCase;
assertThat(unaryOperator.apply("ABC")).isEqualTo("abc");
}
- 인수 타입, 반환 타입이 같다
- 인수 값이 2개이다.
인터페이스 | 메서드 |
---|---|
BinaryOperator | T apply(T t1, T t2) |
IntBinaryOperator | int applyAsInt(int value1, int value2) |
LongBinaryOperator | long applyAsLong(long value, long value2) |
DoubleBinaryOperator | double applyAsDouble(double value, double value2) |
// 예시: BigInteger::add
@Test
void BinaryOperator_Test() {
BinaryOperator<BigInteger> binaryOperator = BigInteger::add;
assertThat(binaryOperator.apply(BigInteger.ONE, BigInteger.TWO)).isEqualTo(BigInteger.valueOf(3L));
}
- 인수 값이 1개이며, boolean 반환한다.
인터페이스 | 메서드 |
---|---|
Predicate | boolean test(T t) |
BiPredicate<T, U> | boolean test(T t, U u) |
IntPredicate | boolean test(int value) |
LongPredicate | boolean test(long value) |
DoublePredicate | boolean test(double value) |
// 예시: Collection::isEmpty
@Test
void Predicate_Test() {
Predicate<Collection> predicate = Collection::isEmpty;
assertThat(predicate.test(new ArrayList<>())).isTrue();
}
- 인수 타입과 반환 타입이 다르다.
인터페이스 | 메서드 |
---|---|
Function<T, R> | R apply(T t) |
BiConsumer<T, U, R> | R apply(T t, U u) |
PFunction | R apply(p value) |
PtoQFunction | q applyAsQ(p value) |
ToPFunction | p applyAsP(T t) |
ToPBiFunction<T, U> | p applyAsP(T t, U u) |
(P, Q 는 기본 자료형(Primitive Type) : Int, Long, Double)
// 예시: Arrays::asList
@Test
void Function_Test() {
Function<int[], List> function = Arrays::asList;
int[] array = {1, 2};
List<Integer> list = function.apply(array);
assertThat(function.apply(array).size()).isEqualTo(2);
}
- 예시: Instant::now
- 인수가 필요하지 않고, 반환이 있다.
인터페이스 | 메서드 |
---|---|
Supplier | T get() |
BooleanSupplier | boolean getAsBoolean() |
IntSupplier | int getAsInt() |
LongSupplier | long getAsLong() |
DoubleSupplier | double getAsDouble() |
// 예시: Instant::now
@Test
void Supplier_Test() {
Instant past = Instant.MIN;
Supplier<Instant> supplier = Instant::now;
assertThat(supplier.get().isAfter(past)).isTrue();
}
- 예시: System.out::println
- 인수가 필요하고, 반환이 없다.
인터페이스 | 메서드 |
---|---|
Consumer | void accept(T t) |
BiConsumer<T, U> | void accept(T t, U u) |
IntConsumer | void accept(int value) |
LongConsumer | void accept(long value) |
DoubleConsumer | void accept(double value) |
ObjIntConsumer | void accept(T t, int value) |
ObjLongConsumer | void accept(T t, long value) |
ObjDoubleConsumer | void accept(T t, double value) |
// 예시: System.out::println
@Test
void Consumer_Test() {
OutputStream output = new ByteArrayOutputStream();
System.setOut(new PrintStream(output));
Consumer consumer = System.out::print;
consumer.accept("안녕");
assertThat(output.toString()).isEqualTo("안녕");
}
- 표준 인터페이스 중 필요한 용도에 맞는 게 없을 때
@FunctionalInterface
public interface TriPredicate<A, B, C> {
boolean test(A a, B b, C c);
}
- 구조적으로 똑같은 표준 함수형 인터페이스가 있지만 다음 조건을 따를 경우
- 자주 쓰이며, 이름 자체가 용도를 명확히 설명해준다.
- 반드시 따라야 하는 규약이 있다.
- 유용한 디폴트 메서드를 제공할 수 있다.
@FunctionalInterface
public interface ToIntBiFunction<T, U> {
int applyAsInt(T t, U u);
}
@FunctionalInterface
public interface Comparator<T> {
// 반드시 따라야 하는 규약
// Compares its two arguments for order.
// Returns a negative integer, zero, or a positive integer as
// the first argument is less than, equal to, or greater than the second.
int compare(T o1, T o2);
// 유용한 디폴트 메서드
default Comparator<T> reversed() {
return Collections.reverseOrder(this);
}
}
항상 @FunctionalInterface 애너테이션을 사용해야한다.
- 해당 클래스의 코드나 설명 문서를 읽을 이에게 그 인터페이스가 람다용으로 설계된 것임을 알려준다.
- 해당 인터페이스가 추상 메서드를 오직 하나만 가지고 있어야 컴파일되게 해준다.
- 유지보수 과정에서 누군가 실수로 메서드를 추가하지 못하게 막아준다.
자바도 람다를 지원한다. 이 의미는 객체의 행동을 항상 Override해서 원하는 동작을 구현하는 것이 아닌, 같은 동작을 할 수 있는 메서드를 파라미터로 넣어 주는 것으로 원하는 동작을 구현할 수 있다는 뜻이다.
이때 함수 객체를 파라미터로 건내어 줄 때 보통의 경우에 java.util.function
패키지의
표준 함수형 인터페이스를 사용하는 것이 가장 좋은 선택이다.
하지만 특수한 경우에 새로운 함수형 인터페이스를 만들어 쓰는 것이 더 좋은 경우도 있지만, 신중해서
사용하도록 하자.