Set

JavaScript 에서 Set 자료 구조는 ES6 에서 추가되었습니다.

Set 은 중복을 허용하지 않고 순서가 없는 리스트입니다.


생성자 Constructor

const a = new Set()
// Set { }

const b = new Set([1, 2, 3])
// Set { 1, 2, 3 }

const c = new Set([1, 1, 1])
// Set { 1 }

add

값을 추가합니다.

값을 추가할 때 Object.is() 메서드를 사용해서 값을 비교합니다.

const a = new Set()
// Set { }

a.add(1)
// Set { 1 }

a.add(2)
// Set { 1, 2 }

a.add(1)
// Set { 1, 2 }

size

크기를 알려줍니다.

size() 가 아니라 size 입니다.

const a = new Set([1, 2, 3])
// Set { 1, 2, 3 }

a.size
// 3

has

값이 이미 있는 지 검사합니다.

const a = new Set([1])
// Set { 1 }

a.has(1)
// true

a.has(2)
// false

delete

값을 지웁니다.

만약 값이 있어서 지우는데 성공하면 true 를 리턴하고 아니면 false 를 리턴합니다.

const a = new Set[1, 2, 3])
// Set { 1, 2, 3 }

a.delete(1)
// true
// Set { 2, 3 }

a.delete(4)
// false
// Set { 2, 3 }

clear

Set 에 있는 모든 값을 지웁니다.

const a = new Set([1, 2, 3])
// Set { 1, 2, 3 }

a.clear()
// Set { }

forEach

forEach 를 사용하여 Set 을 순회할 수 있습니다.

forEach 는 콜백 함수를 파라미터로 받으며 콜백 함수는 세 가지 파라미터를 받습니다.

  1. 키 (index)
  2. 현재 배열 (여기서는 Set)

Set 은 키값이 따로 없기 때문에 1번 2번이 같은 값을 가집니다.

const a = new Set([1, 2, 3, 4, 5])
// Set { 1, 2, 3, 4, 5 }

a.forEach((value) => {
    console.log(value)
})
// 1
// 2
// 3
// 4
// 5

a.forEach((key, value) => {
    console.log(key, value)
})
// 1 1
// 2 2
// 3 3
// 4 4
// 5 5

a.forEach((key, value, currentSet) => {
    console.log(key value, currentSet)
})
// 1 1 Set { 1, 2, 3, 4, 5 }
// 2 2 Set { 1, 2, 3, 4, 5 }
// 3 3 Set { 1, 2, 3, 4, 5 }
// 4 4 Set { 1, 2, 3, 4, 5 }
// 5 5 Set { 1, 2, 3, 4, 5 }

Set ⇒ Array 또는 Array ⇒ Set

전개 연산자 (spread) 를 사용하면 간단하게 Set 을 Array 로, Array 를 Set 으로 변경할 수 있습니다.

const a = new Set([1, 2, 3]) 
// Array => Set { 1, 2, 3}

const b = [...a]
// Set => [1, 2, 3]

문자열 한글 포함 여부 확인

정규식을 사용하면 한글의 포함 여부를 알 수 있습니다.

const koRegex = /[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/;

koRegex.check("hello world"); // false
koRegex.test("안녕"); // true
koRegex.test("반가워 Hi"); // true
koRegex.test("ㅏㅏㅁㄹㄹㄲ"); // true

1. Overview

Java Enum 타입은 일정 개수의 상수 값을 정의하고, 그 외의 값은 허용하지 않습니다.

과거에는 특정 상수값을 사용하기 위해선 모두 상수로 선언해서 사용했습니다.

public static final String MON = "Monday";
public static final String TUE = "Tuesday";
public static final String WED = "Wednesday";

이렇게 사용하면 개발자가 실수하기도 쉽고 한눈에 알아보기도 쉽지 않습니다.

그리고 관련있는 값들끼리 묶으려면 접두어를 사용해서 점점 변수명도 지저분해집니다.

Enum 클래스는 이러한 문제점을 말끔히 해결해주는 굉장히 유용한 클래스입니다.

추가적인 활용법은 Java Enum 2편 : 여러가지 활용법 에서 다루기로 하고 여기서는 기본적인 사용법에 대해서 알아봅니다.


2. 정의

public enum Day {
    MON, TUE, WED, THU, FRI, SAT, SUN
}

위처럼 단순하게 요일을 열거한 Enum 클래스를 만들 수 있습니다.

하지만 각각의 요소들이 특정한 값을 갖게 하고 싶을 수도 있습니다.

예를 들어, 각 요일의 풀네임 (full-name) 이 필요할 때도 있을겁니다.


3. 생성자와 final 필드 추가

public enum Day {
    MON("Monday"),
    TUE("Tuesday"),
    WED("Wednesday"),
    THU("Thursday"),
    FRI("Friday"),
    SAT("Saturday"),
    SUN("Sunday")
    ;

    private final String label;

    Day(String label) {
        this.label = label;
    }

    public String label() {
        return label;
    }
}

Enum 요소에 특정 값을 매핑하고 싶다면 위 코드처럼 필드값을 추가하면 됩니다.

여기서는 label 이라는 String 값을 추가했습니다.

필드값을 추가하면 생성자도 함께 추가해야하는데 Enum 클래스는 생성자가 있다고 하더라도 new 연산으로 생성할 수 없습니다.


System.out.println(Day.MON.name());      // MON
System.out.println(Day.MON.label());     // Monday

이렇게 규칙이 존재하는 특정 요소들을 하나의 Enum 클래스로 묶어두면 가독성도 좋아지고 if 문으로 일일히 검사할 필요도 없어서 편리합니다.

필드값을 추가할 때 이름을 name 으로 정하는건 피하는게 좋습니다.

Enum 클래스 자체에서 name() 이라는 메소드를 제공하기 때문에 헷갈릴 수 있습니다.


4. 필드값으로 Enum 값 찾기

Enum 은 자체적으로 name() 값으로 Enum 값을 찾는 valueOf() 메소드를 제공합니다.

특정 필드값으로 찾는 기능은 제공하지 않기 때문에 직접 만들어야 합니다.


4.1. 직접 Enum values() 순회하며 찾기

public enum Day {
    // ..codes

    public static Day valueOfLabel(String label) {
        return Arrays.stream(values())
                    .filter(value -> value.label.equals(label))
                    .findAny()
                    .orElse(null);
    }
}

다른 필드값으로 찾기 위해선 위 코드와 같이 모든 Enum 값을 순회하면서 일치하는 값이 있는지 찾아야 합니다.


4.1. 캐싱해서 순회 피하기

public enum Day {
    // ..codes

    private static final Map<String, Day> BY_LABEL =
            Stream.of(values()).collect(Collectors.toMap(Day::label, e -> e));

    public static Day valueOfLabel(String label) {
        return BY_LABEL.get(label);
    }
}

HashMap 을 사용해서 값을 미리 캐싱해두면 조회할 때마다 모든 값을 순회할 필요가 없습니다.

처음부터 값을 캐싱해두는게 싫다면 valueOfLabel() 에 처음 접근할 때 Lazy Caching 할 수 있습니다.

다만, 이 때는 동시성 문제 해결을 위해 HashMap 을 동기화 해야 합니다.

그리고 위 코드에서는 valueOfLabel() 메소드를 그냥 리턴했기 때문에 없는 label 값으로 호출하면 null 값이 리턴됩니다.

이런 경우에 사용자에게 nullable 가능성을 알려주기 위해 반환 값을 Optional<Day> 로 넘겨주는 방법도 있습니다.


5. 여러 개의 값 연결하기

public enum Day {
    MON("Monday", 10),
    TUE("Tuesday", 20),
    WED("Wednesday", 30),
    THU("Thursday", 40),
    FRI("Friday", 50),
    SAT("Saturday", 60),
    SUN("Sunday", 70)
    ;

    private final String label;
    private final int number;

    Day(String label, int number) {
        this.label = label;
        this.number = number;
    }

    public String label() {
        return label;
    }

    public int number() {
        return number;
    }

    private static final Map<String, Day> BY_LABEL =
            Stream.of(values()).collect(Collectors.toMap(Day::label, Function.identity()));

    private static final Map<Integer, Day> BY_NUMBER =
            Stream.of(values()).collect(Collectors.toMap(Day::number, Function.identity()));

    public static Day valueOfLabel(String label) {
        return BY_LABEL.get(label);
    }

    public static Day valueOfNumber(int number) {
        return BY_NUMBER.get(number);
    }
}

위 코드처럼 여러 개의 필드값을 세팅할 수 있습니다.

여기서 사용한 예시는 요일이라서 그냥 number 필드를 추가했지만 만약 과일이라면 사과의 이름, 색, 무게, 가격 등등 여러 요소를 한번에 매핑시켜서 정의할 수 있습니다.


Reference

Overview

Java Coding Best Practices And Standards 에 있는 글을 번역한 글입니다.

여러 개의 항목이 있었는데, 그 중에서 제가 개발하면서 공감되었던 부분들만 간단하게 정리했습니다.

굉장히 기본적인 내용도 있는데 초심을 잊지 말자는 의미에서 기록합니다.

  1. NullPointerException 을 고려하자
  2. String 생성할 때 new 키워드를 사용하지 말자
  3. 반복문 내에서 새로운 객체를 생성하지 말자
  4. Collections 을 반복하는 동안 수정하지 말자
  5. Switch-Case 문에서 break 키워드를 뺴먹지 말자
  6. 객체 비교 "==" 와 "eqauls()" 의 차이를 알자
  7. 무작정 StringBuffer 를 사용하지 말자
  8. Java 파일 작성 시 표준

1. NullPointerException 을 고려하자

개발할 때 NullPointerException, 즉 NPE 를 고려하지 않는 경우가 있습니다.

항상 발생가능한 상황을 고려해서 null 체크를 해주어야 합니다.

Java 8 부터는 Optional 을 사용할 수도 있습니다.


가장 흔한 케이스가 Stringequals() 입니다.

// Bad Case : name 으로 null 값이 넘어오면 NPE 발생
public boolean isKim(String name) {
    return name.equals("Kim");
}

// Good Case 1 : name 이 null 값이어도 NPE 발생하지 않음
public boolean isKim(String name) {
    return "Kim".equals(name);
}

// Good Case 2 : org.apache.commons.lang 에서 제공하는 StringUtils 클래스 사용해서 비교
// (내부적으로 null 체크해서 안전함)
public boolean isKim(String name) {
    return StringUtils.equals(name, "Kim");
}

빈 컬렉션에 null 값을 세팅하는 것도 나쁜 습관입니다.

null 값 대신에 Collections.EMPTY_LIST 같은 값을 넣어줍시다.

// Bad Case : null 세팅
List<String> strings = null;

// Good Case : EMPTY_LISTY 세팅
List<String> strings = Collections.EMPTY_LIST

2. String 생성할 때 new 키워드를 사용하지 말자

// Slow
String s = new String("hello");

// Fast
String s = "hello";

new 키워드를 사용하면 항상 새로운 객체를 만들고 힙에 추가합니다.

반면, 사용하지 않으면 String pool 을 먼저 확인하고 없는 경우에만 추가하기 때문에 좀더 효율적입니다.


3. 반복문 내에서 새로운 객체를 생성하지 말자

for (int i = 0; i < 5; i++) {
    Foo f = new Foo();
    f.getMethod();
}

루프 반복횟수 만큼 많은 객체가 생성되기 때문에 지양해야 합니다.


4. Collections 을 반복하는 동안 수정하지 말자

List<String> names = new ArrayList<>();
names.add("Kim");
names.add("Lee");
names.add("Park");

for (String name : names) {
    if ("Kim".equals(name)) {
        names.remove(name);
    }
}

예를 들어 이름 목록에서 Kim 을 삭제하려고 합니다.

위 코드는 names 라는 리스트를 반복하는 동시에 remove() 로 요소를 삭제하고 있습니다.

이러면 ConcurrentModificationException 가 발생할 수 있습니다.


5. Switch-Case 문에서 break 키워드를 빼먹지 말자

int index = 0;

switch (index) {
    case 0:
        System.out.println("Zero");
    case 1:
        System.out.println("One");
        break;
    case 2:
        System.out.println("Two");
    break;
    // ...
    default:
        System.out.println("Default");
}

위 코드를 보면 case 0 에서 break 문을 빼먹었기 때문에 "Zero" 이후에 "One" 까지 출력됩니다.


6. 객체 비교 "==" 와 "eqauls()" 의 차이를 알자

== 연산자는 객체 참조가 같은 지 비교합니다.

equals() 메소드는 객체의 값을 비교합니다.

대부분은 equals() 메소드를 사용해서 비교하지만, 이 둘의 차이점을 잘 모르고 사용하는 경우가 있습니다.


7. 무작정 StringBuffer 를 사용하지 말자

StringBuilderStringBuffer 의 차이를 잘 모르고 무작정 StringBuffer 를 사용하는 경우가 있습니다.

StringBuffer 는 기본적으로 동기화되기 때문에 많은 오버헤드를 생성할 수 있습니다.

따라서 동기화가 우선순위가 아니라면 StringBuilder 를 사용하는 것도 고려해보아야 합니다.


8. Java 파일 작성 시 표준

  • 변수는 public, protected, private 순으로 정의합니다.
  • 생성자는 필드 수가 적은 생성자를 먼저 정의합니다.
  • 메소드는 접근성 (accessibility) 이 아닌 기능별 (functionality) 로 그룹화되어야 합니다. 예를 들어 public 메소드 사이에 private 메소드가 올 수도 있습니다.
  • 코드 설명을 위해 주석을 사용할 수도 있지만, 최대한 주석을 줄이고 가독성 있는 코드를 작성하는 것이 좋습니다.

Reference

Overview

textarea 내부에는 <b> 와 같은 HTML Tag 적용이 되지 않습니다.

구글링 해보니 textarea 내부에 부분적으로 변화를 주는 건 불가능하고 textarea 를 사용하지 않고 <div> 태그를 사용해서 꼼수 부리는 방식들이 나와있었습니다.


1. contenteditable 로 편집 기능 추가

contenteditable 이라는 값을 이용하면 다른 태그에도 편집 기능을 추가할 수 있습니다.


1.1. HTML

<div class="editable" contenteditable="true"></div>

HTML 옵션으로 contenteditable 값을 주면 편집기능이 추가됩니다.


1.2. JavaScript or JQuery 로 편집 기능 추가

$('.editable').each(function(){
    this.contentEditable = true;
});

JavaScript 로도 contentEditable 옵션을 사용해서 내부에 text 를 입력가능하게 할지 말지 설정할 수 있습니다.

만약 값을 false 로 준다면 readonly 처럼 동작합니다.


2. CSS 로 textarea 처럼 변경

div.editable {
    width: 300px;
    height: 200px;
    border: 1px solid #dcdcdc;
    overflow-y: auto;
}

textarea 처럼 보이기 위한 방법입니다.


Reference

1. Overview

Java 의 람다 표현식은 익명 함수처럼 자유 변수 (Free Variable) 를 활용할 수 있습니다.

여기서 자유변수란 파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수 를 의미합니다.


2. 지역 변수 제약

하지만 모든 자유 변수를 사용할 수 있는건 아닙니다.

변수의 종류에는 클래스 변수, 인스턴스 변수, 지역 변수가 있는데 이 중에서 지역 변수는 final 이거나 final 처럼 사용 되어야 합니다.

여기서 final 처럼 사용되어야 한다는건 effectively final 라고 표현하는데 final 선언이 되어 있지 않지만 변수의 재할당이 발생하지 않는 변수 를 의미합니다.


3. Effectively final

람다 표현식에서 사용 불가능한 지역 변수는 인텔리제이에서 다음과 같이 경고해줍니다.

Variable used in lambda expression should be final or effectively final

간단한 예를 통해 effectively final 가 뭔지 쉽게 이해해 봅니다.


3.1. 가능

int number = 1357;
Runnable r = () -> System.out.println(number);

위 코드에서 number 변수는 final 선언이 되어 있지 않지만 에러가 발생하지 않습니다.

변수의 재할당이 이루어지지 않았기 때문입니다.


3.2. 불가능

int number = 1357;
number = 2;
Runnable r = () -> System.out.println(number);

위 코드는 number 가 한번 선언된 후 2 로 재할당 되었기 때문에 람다 표현식 내부에서 사용 불가능합니다.


int number = 1357;
Runnable r = () -> System.out.println(number);
number = 3;

마찬가지로 사용한 이후에 변경되어도 사용 불가능합니다.


4. 이유가 뭘까?

우선 인스턴스 변수와 지역 변수의 차이점부터 알아야 합니다.

인스턴스 변수는 힙에 저장되고 지역 변수는 스택에 저장됩니다.

스택 영역은 힙과 달리 각 쓰레드끼리 공유되지 않는 영역 입니다.

참고로 클래스 변수는 쓰레드끼리 공유 되는 메소드 영역에 저장됩니다.


4.1. Lambda Capturing

람다 표현식은 다른 쓰레드에서도 실행 가능합니다.

만약 A 쓰레드에서 생성한 람다 표현식을 B 쓰레드에서 사용한다고 생각해봅니다.

인스턴스 변수는 쓰레드끼리 공유 가능한 힙 영역에 저장되기 때문에 공유 가능하지만 스택 영역에 있는 지역 변수는 외부 쓰레드에서 접근 불가능합니다.

따라서 자바에서는 스택 영역에 있는 변수를 사용할 수 있도록 지역 변수의 복사본을 제공해서 람다 표현식에서 접근 가능하도록 합니다.

이를 람다 캡쳐링이라고 합니다.

그런데 원본 값이 바뀌면 복사본에 있는 값과의 동기화가 보장되지 않기 때문에 동시성 이슈가 발생합니다.

그래서 지역 변수는 값이 변하지 않는다는 final 임이 보장되어야 합니다.


5. Conclusion

Inner 클래스, 익명 (Anonymous) 클래스, 람다 표현식 등에서 사용되는 외부 지역 변수가 final 또는 effectively final 상태여야 하는 이유를 알아봤습니다.

요약하면 아래와 같습니다.

  1. 람다 표현식은 여러 쓰레드에서 사용할 수 있다.
  2. 힙 영역에 저장되는 인스턴스 변수와 달리 스택 영역에 저장되는 지역 변수는 외부 쓰레드에서 접근 불가능하다.
  3. 외부 쓰레드에서도 지역 변수 값을 사용할 수 있도록 복사본 기능을 제공하는데 이를 람다 캡쳐링이라고 한다.
  4. 복사본은 원본의 값이 바뀌어도 알 수 없기 때문에 쓰레드 동기화를 위해 지역 변수는 final 또는 effectively final 상태여야 한다.

Reference

Overview

Java 8 Stream 에는 count() 라는 종결 함수가 있습니다.

현재 Stream 의 원소 갯수를 카운트 해서 long 타입으로 리턴합니다.


count 의 중간 연산으로 peek 사용

public class NotePad {
    public static void main(String[] args) {
        Stream.of(1, 2, 3, 4, 5)
              .peek(System.out::println)
              .count();
    }
}

위 코드는 어떻게 동작할까요?

아무것도 나오지 않습니다.

원인을 몰라서 구글링 해봤더니 Java 9 Stream API Reference 에 다음과 같은 글이 있었습니다.


The number of elements covered by the stream source, a List, is known and the intermediate operation, peek, does not inject into or remove elements from the stream (as may be the case for flatMap or filter operations). Thus the count is the size of the List and there is no need to execute the pipeline and, as a side-effect, print out the list elements.


요약하자면 count() 라는 종결함수는 Stream 의 갯수를 세기 때문에 효율을 위해 중간 연산을 생략하기도 한다는 뜻입니다.

그래서 중간 연산을 강제로 실행시키고 싶다면 filterflatMap 과 같이 Stream 요소의 갯수를 변화시킬 수 있는 중간 연산을 추가하면 됩니다.


filter 로 강제 출력

public class NotePad {
    public static void main(String[] args) {
        Stream.of(1, 2, 3, 4, 5)
              .filter(e -> e > 0)
              .peek(System.out::println)
              .count();
    }
}

filter 를 추가하면 peek 도 정상적으로 동작합니다.


1
2
3
4
5

Reference

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)

+ Recent posts