6 minute read

가장 많이 사랑 받는 Java 8의 모든것 1강.

2014년 3월에 출시 되어 6년이 넘게 지난 지금도 자바 개발자들에게 가장 사랑 받고 있는 자바 버전인 자바 8에 대해 쭉 알아보고자 한다.

이 강의는 인프런 백기선 강사님의 강의를 정리하고, 추가로 내용을 붙이는 방법으로 포스팅 하려 한다.

5시간짜리 강의밖에 되지 않으므로, 하루 한시간강의 분의 내용을 다룰 예정이다.

그나저나,, 회사서 ml팀으로 발령이 되어 작년 하반기부터 공부하던 자바를 잠시 다시 내려놓게 될 수도 있는 상황이다…. ㅠ

0. 목차

  1. 함수형 인터페이스와 람다 표현식
  2. 자방서 제공하는 함수형 인터페이스
  3. 람다 표현식
  4. 메소드 레퍼런스

1. 함수형 인터페이스와 람다 표현식

1-1 함수형 인터페이스 이해하기

함수형 인터페이스 (Functional interface) 는 1개의 추상 메소드를 가지고 있는 인터페이스를 말한다. (= Single Abstract Method(SAM)) @FuncationInterface Annotaion을 가지고 있는 인터페이스

함수형 인터페이스 예

public interface FunctionalInterface {
    public abstract void doSomthing(String doIt);
}

그런데 이렇게 함수형 인터페이스를 만들었을때 누군가가 이 인터페이스에 메서드를 추가하면 어떻게 될까?. 그런경우를 방지하기 위해 @FunctionalIterface 어노테이션을 추가한다.

@FunctionalInterface
public interface FunctionalInterface {
    public abstract void doSomthing(String doIt);
}

@FunctionalInterface를 사용하면 해당 인터페이스가 함수형 인터페이스라는걸 알려주고, 추상메서드가 1개가 아닌경우엔 컴파일 에러를 낸다. @Override 어노테이션 처럼 꼭 붙여야 하는것은 아니지만 유지보수를 위해 붙이는걸 권장한다.

1-2 함수형 인터페이스 사용 이유

함수형 인터페이스에는 어떠한 강점이 있기 때문에 사용될까? 그 이유는 1의 제목 처럼 자바의 람다식은 함수형 인터페이스로만 접근이 가능하기 때문이다.

아래 코드의 func는 람다식으로 생성한 객체를 가르킨다, doSomthing()에 인자로 문자열을 전달하면 람다식에 정의된 것처럼 로그로 출력을 한다.

public static void start (){
        FunctionalInterface func = text -> System.out.println(text);
        func.doSomething;()    
}

즉 함수형 인터페이스를 사용하는 것은 람다식으로 만든 객체에 접근하기 위함이다. 위처럼 람다식을 사용할 때마다 함수형 인터페이스를 매번 재 정의해야 하는데 이가 너무 불편하기 대문에 자바에서 라이브 러리로 제공도 한다.

2. 자바에서 제공하는 함수형 인터페이스

  • Function
    • Function<T, R> 형태로 T는 파라미터 타입, R은 리턴 타입을 뜻한다, 추상메서드 apply() 를 가진다
      • R apply(T t) : T 타입의 t를 함수의 입력값으로 받아 함수를 실행
    • 함수 조합용 메서드
      • andThen : input을 받아 함수를 수행한 후 그 결과를 after 함수의 input으로 넣어 수행하는 합성함수를 return 한다.
      • compose : input으로 받은 값으로 before 함수를 수행한 후 그 결과값 input으로 받아 수행하는 합성함수를 return 한다.
      • identity : 자기 자신을 리턴 한다.

메서드 활용 예제

public class Ex01 {

    private static void functionExamples() {
        final Function<String, Integer> toInt = value -> Integer.parseInt(value);
        final Integer number = toInt.apply("100");

        System.out.println(number);

        final Function<Integer, Integer> identity = Function.identity();
        System.out.println(identity.apply(100));

        final Function<Integer, Integer> identity2 = i -> i;
        System.out.println(identity2.apply(100));

        final Function<Integer, Integer> square = i -> i*i;
        System.out.println(square.apply(100));

        final Function<Integer, Integer> plus10 = i -> i+10;
        final Function<Integer, Integer> multiplyBy2 = i -> i*2;

        // compose : input으로 들어온 10을 multiply 후에 plus10을 진행
        Integer composeVal = plus10.compose(multiplyBy2).apply(10);
        System.out.println(composeVal);
        // andThen : input으로 들어온 10을 10을 더한 후네 mulitply을 진행
        Integer andThenVal = plus10.andThen(multiplyBy2).apply(10);
        System.out.println(andThenVal);
    }

    public static void main(String[] args) {
        functionExamples();
    }
}
  • BiFunction<T, U, R>
    • 두개의 값(T, U)를 받아서 R 타입을 리턴하는 함수 인터페이스
      • R apply(T t, U u) ```java class Ex02 { public static void biFunctionExample() { BiFunction<Integer, Integer, Integer> biFunction = (num1, num2) -> num1 + num2; int result = biFunction.apply(1, 2);

      System.out.println(result); // 결과 값 = 3 } } ```

  • Consumer
    • T 타입을 받아서 아무 값도 리턴하지 않는 함수 인터페이스
      • void Accept(T t)
    • 함수 조합용 메서드
      • andThdn
    • 추상 메서드 accept()를 가진다.
public static void consumerExample() {
        final Consumer<String> print = value -> System.out.println(value);
        print.accept("hi hi");

        final Consumer<String> greetings = value -> System.out.println("Hello " + value);
        greetings.accept("hoho");
        greetings.accept("haha");
}
  • Predicate<T, Boolean>
    • T 타입을 받아서 boolean을 리턴한다.
    • 추상메서드 test() 를 가진다.
public static void predicateExample() {
        Predicate<Integer> ispostive = i -> i > 0;
        System.out.println(ispostive.test(1));
        System.out.println(ispostive.test(-1));
    }
  • UnaryOperator
    • Function<T, R>의 특수한 형태로, 입력값 하나를 받아서 동일한 타입을 리턴 하는 함수 인터페이스이다
public static void unaryOperatorExample() {
        //UnaryOperator<Integer> unaryOperator = onlynum -> onlynum + "s"; // 불가
        UnaryOperator<Integer> unaryOperator = onlynum -> onlynum + 1;
        int result = unaryOperator.apply(10);
        System.out.println(result);
}
  • BinarOperator
    • BiFunction<T, U, R>의 특수한 형태로,수 동일한 타입의 입력값 두개를 받아 리턴하는 함
      public static void binaryOperatorExample() {
        BinaryOperator<Integer> binaryOperator = (num1, num2) -> num1 + num2;
        int result = binaryOperator.apply(2,3);
        System.out.println(result);
      }
      

      선배에게 내용 전달하였습니다.

  • Supplier
    • Supplier는 형태로 파라미터가 없는 형태이다.
    • 추상메서드 get()을 갖는다.
    • 위의 get()메서드를 통해 Lazy Evaluation이 가능하다.

3. 람다 표현식

3-1 람다?

일단 람다는 메서드로 전달할 수 있는 익명 함수르 단순화 한것이다. 람다를 사용하면 더 쉽게 동작 파라미터 형식이 코드를 구현할 수 있으며, 그에 따라 코드가 더욱 간결해 질수 있다.

3-2 람다의 특징

  1. 익명 : 람다는 익명 메서드와 같이 이름이 없다
  2. 함수 : 람다는 메서드처럼 특정 클래스에 종속되지 않기에 메서드라할 수 있다. 하지만 메서드 처럼 파라미터리스트, 바디, 반환형식, 가능한 예외 리스트 등을 포함한다.
    • 화살표는 람다의 파라미터 리스트와 바디를 구분한다.
    • 바디는 반환값에 해당하는 표현식이다. 람다는 return이 함축되어 있기 때문에 명시적으로 사용하지 않아도 된다.
  3. 전달 : 람다 표현식을 메서드의 인수로 저장하거나 변수로 지정할 수 있다.

3-3 람다의 기본문법.

(parameters) -> expression 또는 (parameters) –> { statements; }

3-4 람다의 사용법

람다는 기본적으로 우리가 위에서 배웠던 함수형 인터페이스 라는 문맥에서 사용이 가능하다.

람다 표현식은 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있다. 따라서 전체 표현식을 함수형 인터페이스로 취급하는것이 가능하다.

interface BarkBark {
    void bark(String str);
}

class Dog implements BarkBark {
    @Override
    public void bark(String str) {
        System.out.println("how to bark?" + str);
    }
}

람다로 표현

BarkBark bark = str -> System.out.println("how to bar?" + str);

3-5 람다와 stateless object

메서드와 함수의 차이는 무엇일까?
클래스를 구현할때 람다와같이 행위만 존재하는건 아니다. 인스턴스의 필드가 존재하는 경우도 있다.

하지만 람다는 바로 메서드를 구현해버리는 구조라 인스턴스 필드가 들어갈 공간이 없다.
메서드와 함수의 차이가 여기서 나온다.

메서드는 객체에 종속된 존재로 인풋의 값이 같더라도 필드의 상태에 따라 값이 다르다.
하지만 함수는 인풋에 값이 같으면 그 리턴의 결과가 항상 같아야 한다.

그렇기 떄문에 함수형 인터페이스를 이용하는 람다 표현식은 객체의 상태를 가질수 없다.

3-6 행위 파라미터화

행위를 파라미터화 한다는건 어떤것일까,,?

SoccerPlayer.java

public class SoccerPlayer {

    private String name;
    private String team;
    private String position;

    public SoccerPlayer(String name, String team, String position) {
        this.name = name;
        this.team = team;
        this.position = position;
    }

    public String getName() {
        return name;
    }

    public String getTeam() {
        return team;
    }

    public String getPosition() {
        return position;
    }

    @Override
    public String toString() {
        return "SoccerPlayer{" +
                "name='" + name + '\'' +
                ", team='" + team + '\'' +
                ", position='" + position + '\'' +
                '}';
    }
}

Extract.java

public class Extract {

    /*public List<SoccerPlayer> extractTeam(List<SoccerPlayer> SoccerPlayers){
        List<SoccerPlayer> resultList = new ArrayList<>();
        for(SoccerPlayer SoccerPlayer : SoccerPlayers){
            if("맨시티".equals(SoccerPlayer.getTeam())){
                resultList.add(SoccerPlayer);
            }
        }

        return resultList;
    }

    public List<SoccerPlayer> extractPosition(List<SoccerPlayer> SoccerPlayers){
        List<SoccerPlayer> resultList = new ArrayList<>();
        for(SoccerPlayer SoccerPlayer : SoccerPlayers){
            if("공격".equals(SoccerPlayer.getPosition())){
                resultList.add(SoccerPlayer);
            }
        }

        return resultList;
    }*/

    public static List<SoccerPlayer> extractPlayerList(List<SoccerPlayer> soccerPlayers, Predicate<SoccerPlayer> predicate) {
        List<SoccerPlayer> resultList = new ArrayList<>();

        for(SoccerPlayer player : soccerPlayers) {
            if(predicate.test(player)) {
                resultList.add(player);
            }
        }
        return resultList;
    }
}

Main.java

public class Main {

    public static void main(String[] args) {
        List<SoccerPlayer> soccerPlayers = Arrays.asList(
                new SoccerPlayer("포그바","맨유","미드필더"),
                new SoccerPlayer("덕배","맨시티","미드필더"),
                new SoccerPlayer("아구에로","맨시티","공격수"),
                new SoccerPlayer("홀란드","도르트문","공격수"));

        // Predicate<T> 인터페이스를 통해 추출 예제 만들어보자.
        List<SoccerPlayer> teamList = Extract.extractPlayerList(soccerPlayers, soccerPlayer -> "맨시티".equals(soccerPlayer.getTeam()));
        List<SoccerPlayer> positionList = Extract.extractPlayerList(soccerPlayers, soccerPlayer -> "공격수".equals(soccerPlayer.getPosition()));

        System.out.println(teamList);
        System.out.println(positionList);
    }

}

4. 메소드 레퍼런스 (Method Reference)

메소드 레퍼런스는 Lambda 표현식을 좀더 간단하게 만들어주는 방법이다.

예로 아래는 람다식을 통해 Hello World를 출력하는 코드를 보겠다. (Consumer는 객체의 입력을 받아 void를 출력시키는 함수형 인터페이스이다.)

Consumer<String> func = text -> System.out.println(text);
func.accept("Hello World")

위의 람다식은 아래의 형태로 System.out::prinln 라는 메소드 레퍼런스로 표현이 가능하다. 여기엔 String 인자 1개를 받아 void를 출력시키는 함수라는 의미가 생략되어 있다.

Consumer<String> func = System.out::println;
func.accept("Hello World")

메소드 레퍼런스는 위와 같이 ClassName::MethodName 형식으로 입력이 된다. 메소드를 호출하는 것이지만 괄호()는 쓰지 않는다.

메서드 레퍼런스는 사용하는 패턴에 따라 3가지로 나뉜다.

  • Static 메서드 레퍼런스
  • Instance 메서드 레퍼런스
  • Constructor 메서드 레퍼런스

메서드 레퍼런스 참고 링크

Categories:

Updated: