Overview

함수형 인터페이스란 1 개의 추상 메소드를 갖는 인터페이스를 말합니다.

Java8 부터 인터페이스는 기본 구현체를 포함한 디폴트 메서드 (default method) 를 포함할 수 있습니다.

여러 개의 디폴트 메서드가 있더라도 추상 메서드가 오직 하나면 함수형 인터페이스입니다.

자바의 람다 표현식은 함수형 인터페이스로만 사용 가능합니다.


1. Functional Interface

함수형 인터페이스는 위에서도 설명했듯이 추상 메서드가 오직 하나인 인터페이스를 의미합니다.

추상 메서드가 하나라는 뜻은 default method 또는 static method 는 여러 개 존재해도 상관 없다는 뜻입니다.

그리고 @FunctionalInterface 어노테이션을 사용하는데, 이 어노테이션은 해당 인터페이스가 함수형 인터페이스 조건에 맞는지 검사해줍니다.

@FunctionalInterface 어노테이션이 없어도 함수형 인터페이스로 동작하고 사용하는 데 문제는 없지만, 인터페이스 검증과 유지보수를 위해 붙여주는 게 좋습니다.


1.1. Functional Interface 만들기

@FunctionalInterface
interface CustomInterface<T> {
    // abstract method 오직 하나
    T myCall();

    // default method 는 존재해도 상관없음
    default void printDefault() {
        System.out.println("Hello Default");
    }

    // static method 는 존재해도 상관없음
    static void printStatic() {
        System.out.println("Hello Static");
    }
}

위 인터페이스는 함수형 인터페이스입니다.

default method, static method 를 넣어도 문제 없습니다.

어차피 함수형 인터페이스 형식에 맞지 않는다면 @FunctionalInterface 이 다음 에러를 띄워줍니다.

Multiple non-overriding abstract methods found in interface com.practice.notepad.CustomFunctionalInterface


1.2. 실제 사용

CustomInterface<String> customInterface = () -> "Hello Custom";

// abstract method
String s = customInterface.myCall();
System.out.println(s);

// default method
customInterface.printDefault();

// static method
CustomFunctionalInterface.printStatic();

함수형 인터페이스라서 람다식으로 표현할 수 있습니다.

String 타입을 래핑했기 때문에 myCall()String 타입을 리턴합니다.

마찬가지로 default method, static method 도 그대로 사용할 수 있습니다.

위 코드를 실행한 결과값은 다음과 같습니다.

Hello Custom
Hello Default
Hello Static

2. Java 에서 기본적으로 제공하는 Functional Interfaces

매번 함수형 인터페이스를 직접 만들어서 사용하는 건 번거로운 일입니다.

그래서 Java 에서는 기본적으로 많이 사용되는 함수형 인터페이스를 제공합니다.

기본적으로 제공되는 것만 사용해도 웬만한 람다식은 다 만들 수 있기 때문에 개발자가 직접 함수형 인터페이스를 만드는 경우는 거의 없습니다.


함수형 인터페이스 Descripter Method
Predicate T -> boolean boolean test(T t)
Consumer T -> void void accept(T t)
Supplier () -> T T get()
Function<T, R> T -> R R apply(T t)
Comparator (T, T) -> int int compare(T o1, T o2)
Runnable () -> void void run()
Callable () -> T V call()

2.1. Predicate

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

Predicate 는 인자 하나를 받아서 boolean 타입을 리턴합니다.

람다식으로는 T -> boolean 로 표현합니다.


2.2. Consumer

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

Consumer 는 인자 하나를 받고 아무것도 리턴하지 않습니다.

람다식으로는 T -> void 로 표현합니다.

소비자라는 이름에 걸맞게 무언가 (인자) 를 받아서 소비만 하고 끝낸다고 생각하면 됩니다.


2.3. Supplier

@FunctionalInterface
public interface Supplier<T> {
    T get();
}

Supplier 는 아무런 인자를 받지 않고 T 타입의 객체를 리턴합니다.

람다식으로는 () -> T 로 표현합니다.

공급자라는 이름처럼 아무것도 받지 않고 특정 객체를 리턴합니다.


2.4. Function

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

Function 은 T 타입 인자를 받아서 R 타입을 리턴합니다.

람다식으로는 T -> R 로 표현합니다.

수학식에서의 함수처럼 특정 값을 받아서 다른 값으로 반환해줍니다.

T 와 R 은 같은 타입을 사용할 수도 있습니다.


2.5. Comparator

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
}

Comparator 은 T 타입 인자 두개를 받아서 int 타입을 리턴합니다.

람다식으로는 (T, T) -> int 로 표현합니다.


2.6. Runnable

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

Runnable 은 아무런 객체를 받지 않고 리턴도 하지 않습니다.

람다식으로는 () -> void 로 표현합니다.

Runnable 이라는 이름에 맞게 "실행 가능한" 이라는 뜻을 나타내며 이름 그대로 실행만 할 수 있다고 생각하면 됩니다.


2.7. Callable

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

Callable 은 아무런 인자를 받지 않고 T 타입 객체를 리턴합니다.

람다식으로는 () -> T 로 표현합니다.

Runnable 과 비슷하게 Callable 은 "호출 가능한" 이라고 생각하면 좀 더 와닿습니다.


Supplier vs Callable

SupplierCallable 은 완전히 동일합니다.

아무런 인자도 받지 않고 특정 타입을 리턴해줍니다.

둘이 무슨 차이가 있을까..?

사실 그냥 차이가 없다고 생각하시면 됩니다.

단지 CallableRunnable 과 함께 병렬 처리를 위해 등장했던 개념으로서 ExecutorService.submit 같은 함수는 인자로 Callable 을 받습니다.


3. 두 개의 인자를 받는 Bi 인터페이스

특정 인자를 받는 Predicate, Consumer, Function 등은 두 개 이상의 타입을 받을 수 있는 인터페이스가 존재합니다.


함수형 인터페이스 Descripter Method
BiPredicate (T, U) -> boolean boolean test(T t, U u)
BiConsumer (T, U) -> void void accept(T t, U u)
BiFunction (T, U) -> R R apply(T t, U u)

4. 기본형 특화 인터페이스

지금까지 확인한 함수형 인터페이스를 제네릭 함수형 인터페이스라고 합니다.

자바의 모든 형식은 참조형 또는 기본형입니다.

  • 참조형 (Reference Type) : Byte, Integer, Object, List
  • 기본형 (Primitive Type) : int, double, byte, char

Consumer<T> 에서 T 는 참조형만 사용 가능합니다.

Java 에서는 기본형과 참조형을 서로 변환해주는 박싱, 언박싱 기능을 제공합니다.

  • 박싱 (Boxing) : 기본형 -> 참조형 (int -> Integer)
  • 언박싱 (Unboxing) : 참조형 -> 기본형 (Integer -> int)

게다가 개발자가 박싱, 언박싱을 신경쓰지 않고 개발할 수 있게 자동으로 변환해주는 오토박싱 (Autoboxing) 이라는 기능도 제공합니다.

예를 들어 List<Integer> list 에서 list.add(3) 처럼 기본형을 바로 넣어도 사용 가능한 것도 오토박싱 덕분입니다.

하지만 이런 변환 과정은 비용이 소모되기 때문에, 함수형 인터페이스에서는 이런 오토박싱 동작을 피할 수 있도록 기본형 특화 함수형 인터페이스 를 제공합니다.

IntPredicate, LongPredicate 등등 특정 타입만 받는 것이 확실하다면 기본형 특화 인터페이스를 사용하는 것이 더 좋습니다.

아래에서 소개하는 인터페이스 외에 UnaryOperatorBi 인터페이스에도 기본형 특화를 제공합니다.


4.1 Predicate (T -> boolean)

기본형을 받은 후 boolean 리턴

  • IntPredicate
  • LongPredicate
  • DoublePredicate

4.2. Consumer (T -> void)

기본형을 받은 후 소비

  • IntConsumer
  • LongConsumer
  • DoubleConsumer

4.3. Function (T -> R)

기본형을 받아서 기본형 리턴

  • IntToDoubleFunction
  • IntToLongFunction
  • LongToDoubleFunction
  • LongToIntFunction
  • DoubleToIntFunction
  • DoubleToLongFunction

기본형을 받아서 R 타입 리턴

  • IntFunction<R>
  • LongFunction<R>
  • DoubleFunction<R>

T 타입 받아서 기본형 리턴

  • ToIntFunction<T>
  • ToDoubleFunction<T>
  • ToLongFunction<T>

4.4. Supplier (() -> T)

아무것도 받지 않고 기본형 리턴

  • BooleanSupplier
  • IntSupplier
  • LongSupplier
  • DoubleSupplier

Conclusion

Java 8 에서 람다에 활용 가능한 함수형 인터페이스를 제공하고 있습니다.

직접 만들어서 쓸 수도 있지만 이미 제공하는 인터페이스로도 대부분 처리 가능하므로 어떤 게 있는지 잘 파악해서 활용해야 합니다.


Reference

Overview

Java 설치 방법과 여러 개의 버전을 사용할 때 어떤 식으로 변경하는지 알아봅시다.

여러 Java (JDK) 버전을 사용하는 경우 원하는 버전을 기본으로 설정할 수 있습니다.


1. Java (OpenJDK) 설치

1.1. adoptopenjdk/openjdk 저장소 추가

$ brew tap adoptopenjdk/openjdk

1.2. cask 가 없다면 설치

$ brew install cask

1.3. OpenJDK 8 과 11 을 설치

$ brew install --cask adoptopenjdk8
$ brew install --cask adoptopenjdk11
  • 하나의 버전만 사용한다면 하나만 설치하면 됩니다.

1.4. 설치 여부 확인

$ java -version
openjdk version "1.8.0_292"
OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_292-b10)
OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.292-b10, mixed mode)
  • Java 버전 확인을 확인해서 잘 나오면 설치가 완료된 겁니다.

2. Java (JDK) 버전 변경

2.1. 설치된 JDK 버전 확인

$ /usr/libexec/java_home -V

Matching Java Virtual Machines (2):
    11.0.11 (x86_64) "AdoptOpenJDK" - "AdoptOpenJDK 11" /Library/Java/JavaVirtualMachines/adoptopenjdk-11.jdk/Contents/Home
    1.8.0_292 (x86_64) "AdoptOpenJDK" - "AdoptOpenJDK 8" /Library/Java/JavaVirtualMachines/adoptopenjdk-8.jdk/Contents/Home
/Library/Java/JavaVirtualMachines/adoptopenjdk-11.jdk/Contents/Home
  • 저는 현재 JDK 11 과 JDK 8 버전이 설치되어 있습니다.

2.2. JDK 버전 변경

# 1.8 버전으로 변경
$ export JAVA_HOME=$(/usr/libexec/java_home -v 1.8)

# 11 버전으로 변경
$ export JAVA_HOME=$(/usr/libexec/java_home -v 11)

2.3. JDK 변경 확인

$ java -version
openjdk version "1.8.0_292"
OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_292-b10)
OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.292-b10, mixed mode)

$ echo $JAVA_HOME
/Library/Java/JavaVirtualMachines/adoptopenjdk-8.jdk/Contents/Home
  • $JAVA_HOME 도 변경된 것을 확인 할 수 있습니다.

3. 기본 Java 버전 적용

프로젝트에 따라서 여러 개의 Java 버전을 사용해야 하는 경우가 있습니다.

자주 사용하는 Java 버전을 기본으로 세팅하고 싶다면 bash 를 사용하는 경우 ~/.bash_profile, zsh 를 사용하는 경우 ~/.zshrc 파일 가장 하단에 아래 코드를 한줄 추가해주면 됩니다.

JDK 버전 변경 때 사용했던 명령어와 동일합니다.

# 1.8 버전을 기본으로
$ export JAVA_HOME=$(/usr/libexec/java_home -v 1.8)

# 11 버전을 기본으로
$ export JAVA_HOME=$(/usr/libexec/java_home -v 11)

Collection 의 합을 구하는 방법

Collection 의 합을 구하는 방법은 reducesum 두 가지가 존재합니다.

단, Stream 에서 sum() 을 사용하려면 IntStream, LongStream, DoubleStream 와 같은 기본형 (Primitive Type) 특화 스트림을 사용해야 합니다.

그래서 보통 mapToInt, mapToLong, mapToDouble 같은 메소드로 스트림을 변환시키고 사용합니다.

 

reduce

reduce(초기값, 연산) 형식으로 사용합니다.

초기값부터 시작하여 각 원소를 차례대로 순회하며 연산을 수행합니다.

이전 연산의 결과를 다음 초기값으로 넘기면서 연산의 결과를 누적해서 총 결과값을 구하는 메서드입니다.

int sum = Stream.of(1, 2, 3).reduce(0, Integer::sum);

 

sum

Stream 의 총합을 구하는 메서드입니다.

기본형 특화 스트림에서만 사용 가능합니다.

int sum = Stream.of(1, 2, 3).mapToInt(e -> e).sum();

 

그렇다면 합을 구할 때 어떤걸 사용할까?

처음 생각 할 때는 reduce 로 한번에 하는게 Stream 처리가 적어서 더 빠를거라고 생각했습니다.

가독성도 크게 차이 안납니다.

그런데 실제로 테스트 해보니 결과는 달랐습니다.

 

Test Code

public static void main(String[] args) {
    Map<String, Integer> hashmap = new HashMap<>();

    for (int i = 0; i < 10000000; i++) {
        hashmap.put(i + "", i);
    }

    // for-loop 시간측정
    long start1 = System.currentTimeMillis();
    int sum1 = 0;
    for (String key : hashmap.keySet()) {
        sum1 += hashmap.get(key);
    }
    long end1 = System.currentTimeMillis();

    // stream mapToInt 시간측정
    long start2 = System.currentTimeMillis();
    int sum2 = hashmap.values().stream().reduce(0, Integer::sum);
    long end2 = System.currentTimeMillis();

    // stream reduce 시간측정
    long start3 = System.currentTimeMillis();
    int sum3 = hashmap.values().stream().mapToInt(i -> i).sum();
    long end3 = System.currentTimeMillis();

    System.out.println("for-loop: " + (end1 - start1));
    System.out.println("int reduce: " + (end2 - start2));
    System.out.println("mapToInt: " + (end3 - start3));
}

Result

for-loop: 303
reduce: 368
mapToInt: 310

 

원인 & 결론

reduce 에는 박싱, 언박싱 비용이 들어갑니다.

내부적으로 합계를 계산하기 위해 Integerint 형으로 언박싱 하고 다시 int -> Integer 로 박싱하는 과정이 숨겨져 있어서 시간이 더 오래 걸립니다.

그래서 reduce 의 성능이 더 느리니 IntStream 에서 제공하는 메서드가 있는 경우에는 해당 메서드를 사용하는 게 유용합니다.

 

Reference

'Language > Java' 카테고리의 다른 글

Java Stream count() 의 비밀  (0) 2021.04.18
Java 8 함수형 인터페이스 (Functional Interface)  (1) 2021.04.10
Mac OS Java (OpenJDK) 설치 및 버전 변경  (2) 2021.03.08
[Java] Static  (0) 2020.09.19
[Java] Map, HashMap, LinkedHashMap  (0) 2020.04.25

Java Static 이란?

변수나 메소드 앞에 static 키워드를 붙여 사용한다.

static 은 같은 클래스에 있는 경우 같은 메모리 주소를 바라본다는 점을 이용해서 메모리 효율 최적화, 데이터 공유 등을 위해 사용한다.

한글로는 "정적인" 이라는 뜻인데 한번에 이해하기가 쉽지 않다.

동적인 다른 자원들과 달리 프로그램 상의 변하지 않는 자원이라고 생각하면 된다.


생성 시기와 소멸 시기

static 키워드를 사용하면 객체가 생성되는 시점이 아닌 Class Loader 가 클래스를 Load 할 때 Data 영역에 메모리가 할당되게 된다.

이 영역은 같은 유형의 클래스마다 공유되며 Process 가 종료되는 시점에서 해제되므로 static 키워드의 생명주기 역시 Class Load 시 생성되고 Process 종료 시 해제되게 된다.


static 변수

클래스 내에서 static 으로 선언된 변수는 모든 클래스 객체에서 같은 메모리 주소를 바라본다.

예를 들어 Student 라는 클래스가 있다. 이 클래스에는 학생들이 살고 있는 도시인 teacher 변수를 갖는다.

public class Student {
  String name;
  String teacher;
}

학생들은 모두 같은 반이라서 선생님이 같다고 가정하자.

만약 반의 선생님이 바뀌어도 모든 학생들이 동시에 바뀌어야 한다.

보통 한 반에 학생이 30 명이라고 가정하면, 선생님이 바뀌었을 때 30 명의 학생들을 전부 바꿔줘야 한다.

하지만 이럴 때 teacher 변수를 static 으로 선언한다면 Student 객체의 모든 teacher 가 같은 메모리를 공유해서 데이터를 바꾸면 동시에 바뀐다.


static 메소드

static 메소드는 객체 생성 없이 사용할 수 있으며 보통 유틸리티 클래스에서 주로 사용한다.

static 메소드 내에서는 static 변수만 사용 가능하고 this 호출이 불가능하다.

"이메일 형식 검사", "현재 시간 구하기" 등은 새로운 객체를 할당할 필요가 없기 때문에 static 으로 작성하는 게 좋다.


static 을 응용항 싱글톤 패턴 (Singleton pattern)

디자인 패턴 중 하나인 싱글톤 패턴은 static 의 개념을 이용한 패턴이다.

싱글톤은 단 하나의 객체만 생성하는 패턴이다.

싱글톤 클래스를 만드는 규칙은 다음과 같다.

  1. 싱글톤 클래스의 생성자를 private 로 하여 외부에서 생성하지 못하게 한다.
  2. 클래스 내부에 static 변수를 선언한다.
  3. getInstance() 메소드를 static 으로 만들고 내부에서 싱글턴 객체를 선언하여 리턴한다.
class Singleton {
  private static Singleton instance;

  // 생성자를 private 로 하여 외부에서 생성 불가능하게 만듬
  private Singleton() { }

  public static Singleton getInstacne() {
    if (instance == null) {
      instance = new Singleton();
    }

    return instance;
  }
}


Map

KeyValue 형태로 데이터를 저장하는 자료구조



HashMap

선언

일반적으로 변수 부분은 인터페이스로 선언하는게 확장에 유리하다.


Map<String, String> map = new HashMap<>();



데이터 삽입

V put(K key, V value) 로 값을 넣을 수 있다.


put(k, v) 을 했을 때 이미 키값이 존재한다면 데이터를 덮어쓴다.


putIfAbsent 을 이용하면 Map 에 Key 값이 없을 때에만 데이터를 넣을 수 있다.


map.put("animal", "cat");           // {animal=cat}
map.put("food", "pizza");           // {animal=cat, food=pizza}


// 이미 "animal" 키값이 있기 때문에 "dog" 로 갱신되지 않음
map.putIfAbsent("animal", "dog");   // {animal=cat, food=pizza}
map.putIfAbsent("animal2", "dog");  // {animal=cat, animal2=dog, food=pizza}



데이터 가져오기

V get(Object key) 로 value 값을 가져올 수 있다.


V getOrDefault(Object key, V defaultValue) 을 사용하면 key 값이 없을 때 null 대신 설정된 값을 리턴한다.


map.get("animal");  // "cat"

map.getOrDefault("food", "chicken");     // "pizza"
map.getOrDefault("food2", "chicken");    // "chicken"



데이터 삭제

V remove(Object key) 로 데이터를 삭제 할 수 있다.


map.remove("animal2");



데이터 확인

boolean containsKey(Object key) 또는 boolean containsValue(Object value) 로 key 나 value 값이 존재하는 지 확인할 수 있다.


map.containsKey("food");    // true
map.containsValue("dog");   // false



크기(길이) 확인

map.size();



Key, Value 묶음 가져오기

Set<K> keySet() 은 Key들로 이루어진 Set 자료구조를 리턴한다.


Collection<V> values() 은 Value 들로 이루어진 Collection 을 리턴한다.


// map: {animal=cat, food=pizza}

map.keySet();   // [animal, food]
map.values();   // [cat, pizza]



Map 순회

forEach 를 사용해서 Map 을 순회할 수 있다.


람다식을 이용하면 좀더 간단하게 나타낼 수 있으며 코드가 한줄이라면 중괄호 { } 를 생략할 수 있다.


// 출력
// animal: cat
// food: pizza
map.forEach((k, v) -> {
    System.out.println(k + ": " + v);
});

// 한 줄이면 중괄호 { } 생략 가능
map.forEach((k, v) -> System.out.println(k + ": " + v));

// 람다식 안쓰고 for 문으로 구현. forEach 도 내부적으로는 이렇게 구현되어있음
for (Map.Entry entry : map.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}



Compute

compute 를 사용해 원하는 로직을 실행하고 데이터를 넣을 수 있다.


만약 Key 가 없으면 새로운 데이터를 넣어주고 Key 값이 있으면 데이터를 갱신해준다.


만약 기존에 없는 데이터로 compute 연산을 하게 될 때 value 값은 null 이 된다.


map.compute("animal", (k, v) -> {
    System.out.println(k + "'s value is " + v  + " -> lion");      // animal's value is cat -> lion
return "lion"; }); // map: {animal=lion, food=pizza}



computeIfAbsent 와 computeIfPresent 는 조건을 걸어서 compute 연산을 실행한다.


computeIfAbsent 는 Key 가 없을 때만 실행되기 때문에 람다식으로도 key 값 하나 밖에 받지 않는다.


computeIfPresent 는 Key 값이 존재할 때만 실행된다.


만약 Key 가 없거나 있어서 조건이 일치하지 않으면 로직이 아예 실행되지 않는다.


map.computeIfAbsent("fruit", (k) -> {
    System.out.println("New value of " + k + " is apple");      // New value of fruit is apple
    return "apple";
});
// map: {fruit=apple, animal=lion, food=pizza}


map.computeIfPresent("animal", (k, v) -> {
    System.out.println(k + "'s value is " + v +  " -> tiger");      // animal's value is lion -> tiger
    return "tiger";
});
// map: {fruit=apple, animal=tiger, food=pizza}




LinkedHashMap

HashMap 은 hashcode 를 사용하기 때문에 순서가 일정하지 않다.


LinkedHashMap 은 내부를 Double-Linked List 로 구성하여 HashMap 의 순서를 유지한다.


HashMap 에서 상속받기 때문에 HashMap 의 모든 메소드를 사용할 수 있다.



순서 유지

데이터는 먼저 들어간 데이터가 무조건 앞에 위치하게 된다.


forEach 문에서도 동일하다.


Map<String, String> map = new LinkedHashMap<>();

map.put("animal", "cat");
map.put("fruit", "apple");

System.out.println(map);        // {animal=cat, fruit=apple}

map.put("animal", "dog");       
System.out.println(map);        // {animal=dog, fruit=apple}

map.forEach((k, v) -> System.out.print(k + ": " + v + ", "));       // animal: dog, fruit: apple, 



접근 빈도에 따른 순서 변경

LinkedHashMap 은 생성자 파라미터로 accessOrder 라는 값을 받는다.


accessOrder 는 기본값이 false 인데, 만약 true 로 설정한다면 LinkedHashMap 의 접근 빈도에 따라서 순서가 바뀌게 된다.


public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}



아래 코드는 위와 완전히 동일하지만 accessOrder 만 true 로 생성한 LinkedHashMap 이다.


"animal" 키를 먼저 넣었지만 중간에 put 메소드로 "animal" 키에 접근을 했기 때문에 순서가 바뀌었다.


put 뿐만 아니라 get 이나 compute 에도 동일하게 동작한다 (containsKey 는 바뀌지 않음)


Map<String, String> map = new LinkedHashMap<>(16, 0.75f, true); map.put("animal", "cat"); map.put("fruit", "apple"); System.out.println(map); // {animal=cat, fruit=apple} map.put("animal", "dog"); System.out.println(map); // {fruit=apple, animal=dog} map.forEach((k, v) -> System.out.print(k + ": " + v + ", ")); // fruit: apple, animal: dog,



accessOrder 를 이용하면 가장 첫번째에 존재하는, 즉 가장 사용되지 않은 Entry 를 알 수 있다.


Map.Entry leastUsedEntry = map.entrySet().iterator().next();
int leastUsedKey = map.keySet().iterator().next();
int leastUsedValue = map.values().iterator().next();

// 아래처럼 구할 수도 있다.
leastUsedKey = leastUsedEntry.getKey();
leastUsedValue = leastUsedEntry.getValue();


+ Recent posts