TIL - 2022.03.07 ~

YulHee Kim·2022년 3월 7일
0

참고 강의 : 백기선님의 더 자바, Java8

[ 자바 8 소개 ]

✏️ 자바 8

  • LTS qjwjs
  • 출시일 2014년 3월

LTS와 비-LTS의 차이

  • 비-LTS는 업데이트 제공 기간이 짧다
  • 비-LTS 배포 주기 6개월
  • 비-LTS 지원 기간은 배포 이후 6개월
  • LTS 배포 주기 3년 (매 6번째 배포판이 LTS가 된다)
  • LTS 지원 기간은 5년 이상으로 JDK를 제공하는 벤더와 이용하는 서비스에 따라 다르다
  • 다음 LTS : 자바 17

✏️ 주요 기능

  • 람다 표현식
  • 메소드 레퍼런스
  • 스트림 API
  • Optional<T>

[ 함수형 인터페이스와 람다 표현식 ]

✏️ 함수형 인터페이스와 람다 표현식 소개

함수형 인터페이스(Functional Interface)

  • 추상 메소드를 딱 하나만 가지고 있는 인터페이스
  • SAM(Single Abstract Method)인터페이스
  • @FuncationInterface 애노테이션을 가지고 있는 인터페이스

예시)

람다 표현식

  • 함수형 인터페이스의 인스턴스를 만드는 방법으로 쓰일 수 있다
  • 코드를 간결하게 쓸 수 있다
  • 메소드 매개변수, 리턴 타입, 변수로 만들어 사용할 수도 있다

예시

@FunctionalInterface
public interface RunSomething {

    void doIt();
}

-> 추상 메서드를 꼭 한가지만 가지고 있어야함


public class Foo {

    public static void main(String[] args) {
        // 익명 내부 클래스 anonymous inner class
        RunSomething runSomething = () -> System.out.println("hello");
    }
}

자바에서 함수형 프로그래밍

  • 함수를 First class object로 사용 가능
  • 순수 함수
    • 사이드 이팩트가 없다(함수 밖에 있는 값을 변경하지 않는다)
    • 상태가 없다 (함수 밖에 있는 값을 사용하지 않는다)
  • 고차 함수
    • 함수가 함수를 매개변수로 받을 수 있고 함수를 리턴할 수도 있다
  • 불변성

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

Java가 기본으로 제공하는 함수형 인터페이스

  • java.util.function 패키지
  • 자바에서 미리 정의해둔 자주 사용할만한 함수 인터페이스
  • Function<T, R>
  • BiFunction<T, U, R>
  • Consumer<T>
  • Supplier<T>
  • Predicate<T>
  • UnaryOperator<T>
  • BinaryOperator<T>

예제

import java.util.function.Function;

public class Plus10 implements Function<Integer, Integer> {
	@Override
	public Integer apply(Integer integer) {
		return integer + 10;
	}
	
}

Function<T, R>첫번째 파라미터 입력값의 타입, 두번째 파라미터가 리턴값의 타입


public class Foo {

    public static void main(String[] args) {
        Plus10 plus10 = new Plus10();
        System.out.println(plus10.apply(1));
    }
}

결과 : 11

-> 클래스를 만들지 않고 람다를 활용해서 바로 구현 가능


public class Foo {

    public static void main(String[] args) {
        Function<Integer, Integer> plus10 = (i) -> i + 10;
        //UnaryOperator<Integer> plus10 = (i) -> i + 10;
        Function<Integer, Integer> multiply2 = (i) -> i * 2;
        System.out.println(plus10.apply(1));

        //plus10.compose(multiply2);

        System.out.println(plus10.andThen(multiply2).apply(2)); //24
}
  • compose는 파라미터에 있는 함수를 먼저 계산하고 그 결과 반환값을 또 다른 함수의 입력값으로 사용함
  • 위 예시는 곱하기 2를 먼저하고 그 결과 값에 10을 더함
  • andThen같은 경우는 plus10함수를 실행한 결과 값에 2를 곱해줌
  • 입력값과 리턴값이 같은경우 UnaryOperator<Integer>로 대체 가능

Function<T,R>

  • T 타입을 받아서 R 타입을 리턴하는 함수 인터페이스
    • R apply(T t)
  • 함수 조합용 메서드
    • andThen
    • compose

BiFunction<T, U, R>

  • 두개의 값(T, U)를 받아서 R 타입을 리턴하는 함수 인터페이스
    • R apply(T t, U u)

Consumer< T >

  • T 타입을 받아서 아무값도 리턴하지 않는 함수 인터페이스
    • void Accept(T t)
  • 함수 조합용 메소드
    • andThen

Supplier< T >

  • T 타입의 값을 제공하는 함수 인터페이스
    • T get()

Predicate< T >

  • T 타입을 받아서 boolean을 리턴하는 함수 인터페이스
    • boolean test(T t)
  • 함수 조합용 메소드
    • And
    • Or
    • Negate

UnaryOperator< T >

  • Function<T, R>의 특수한 형태로, 입력 값 하나를 받아서 동일한 타입을 리턴하는 함수 인터페이스

BinaryOperator< T >

  • BiFunction<T, U, R>의 특수한 형태로, 동일한 타입의 입력값 두개를 받아 리턴하는 함수 인터페이스

✏️ 람다 표현식

람다

(인자 리스트) -> {바디}

public class Foo {
	
	public static void main(String[] args) {

		BiFunction<Integer> sum = (a, b) -> a + b;
	}
}

인자 리스트

  • 인자가 없을 때: ()
  • 인자가 한개일 때: (one) 또는 one
  • 인자가 여러개일 때: (one, two)
  • 인자의 타입은 생략 가능, 컴파일러가 추론하지만 명시할 수도 있다.(Integer one, Integer two)

바디

  • 화살표 오른쪽에 함수 본문을 정의한다
  • 여러 줄인 경우에 {}를 사용
  • 한 줄인 경우에 생략 가능, return도 생략 가능

변수 캡처

public class Foo {
	
	public static void main(String[] args) {
		Foo foo = new Foo();
		foo.run();

	}
	
	private void run() {
		int baseNumber = 10;


		//로컬 클래스
		class LocalClass {
			void printBaseNumber() {
				int baseNumber = 11;
				System.out.println(baseNumber); //11
			}
		}

		//익명 클래스에서 로컬 변수 참조하기
		Consumer<Integer> integerConsumer = new Consumer<Integer>() {
			@Override
			public void accept(Integer baseNumber) {
				System.out.println(baseNumber); //파라미터
			}
		}

		//람다에서 로컬 변수 참조
		InConsumer printInt = (i) -> {
			System.out.println(i + baseNumber);
		};

		printInt.accept(10);
	}
}

baseNumber 로컬 variable이 캡쳐가 됨
익명 클래스와 로컬 클래스는 클래스 안에서 각각의 scope을 가짐
람다는 다름.

InConsumer printInt = (baseNumber) -> {
	System.out.println(i + baseNumber);
};

baseNumber라는 변수명 새로 정의 불가함. 같은 스코프이기 때문에

  • 로컬 변수 캡처

    • final이거나 effective final인 경우에만 참조할 수 있다
    • 그렇지 않을 경우 concurrency 문제가 생길 수 있어서 컴파일이 방지
  • effective final

    • 자바 8부터 지원하는 기능으로 사실상 final인 변수
      • final 키워드 사용하지 않은 변수를 익명 클래스 구현체 또는 람다에서 참조할 수 있다
  • 익명 클래스 구현체와 달리 쉐도윙하지 않는다

    • 익명 클래스는 새로 스콥을 만들지만, 람다는 람다를 감싸고 있는 스콥과 같다

✏️ 메소드 레퍼런스

람다가 하는 일이 기존 메소드 또는 생성자를 호출하는 거라면, 메소드 레퍼런스를 사용해서 간결하게 표현할 수 있다.

public class Greeting {
	
	private String name;

	public Greeting() {
	}

	public Greeting(String name) {
		this.name = name;
	}

	public String hello(String name) {
		return "hello " + name;
	}

	public static String hi(String name) {
		return "hi " + name;
	}
}
public class App {
	
	public static void main(String[] args) {

		//Greeting 클래스의 static 메서드 hi메서드 참조
		UnaryOperator<String> hi = Greeting::hi;

		//특정한 인스턴스의 메서드 사용할 땐
		Greeting greeting = new Greeting();
		UnaryOperator<String> hello = greeting::hello;
		System.out.println(hello.apply("hi"));

		//생성자 참조 -> 입력값 X
		Supplier<Greeting> newGreeting = Greeting::new;
		Greeting greeting = newGreeting.get();

		//생성자 참조 -> 입력값 O
		Function<String, Greeting> neewGreeting = Greeting::new;

		Greeting test = neewGreeting.apply("test");
		System.out.println(test.getName());

		//임의 객체의 인스턴스 메소드 참조
		String[] names = {"Btest", "Atest", "Ctest"};
		Arrays.sort(names, String::compareToIgnoreCase);
		System.out.println(Arrays.toString(names)); // A,  B, C

	}
}

메소드 참조하는 방법

스태틱 메소드 참조타입::스태틱 메소드
특정 객체의 인스턴스 메서드 참조객체 레퍼런스::인스턴스 메소드
임의 객체의 인스턴스 메소드 참조타입::인스턴스 메소드
생성자 참조타입::new
  • 메소드 또는 생성자의 매개변수로 람다의 입력값을 받는다

  • 리턴값 또는 생성한 객체는 람다의 리턴값이다


[ 인터페이스의 변화 ]

✏️ 인터페이스 기본 메소드와 스태틱 메소드

기본 메소드

  • 인터페이스에 메소드 선언이 아니라 구현체를 제공하느 ㄴ방법

  • 해당 인터페이스를 구현한 클래스를 깨트리지 않고 새 기능을 추가할 수 있다.

  • 기본 메소드는 구현체가 모르게 추가된 기능으로 그만큼 리스크가 있다.

    • 컴파일 에러는 아니지만 구현체에 따라 런타임 에러가 발생할 수 있다.
    • 반드시 문서화 할 것. (@implSpec 자바독 태그 사용)
  • Object가 제공하는 기능 (equals, hasCode)는 기본 메소드로 제공할 수 없다.

    • 구현체가 재정의해야 한다
  • 본인이 수정할 수 있는 인터페이스에만 기본 메소드를 제공할 수 있다.

  • 인터페이스를 상속받는 인터페이스에서 다시 추상 메소드로 변경할 수 있다.

  • 인터페이스 구현체가 재정의 할 수도 있다.

스태틱 메소드

  • 해당 타입 관련 헬퍼 또는 유틸리티 메소드를 제공할 때 인터페이스에 스태틱 메소드를 제공할 수 있다.

예제

public interface Foo {
	
	void printName();

	//void printNameUpperCase(); // 이걸 추가하면 모든 클래스가 컴파일 에러가 남. 추가한 추상화메서드를 구현하지 않았기 때문에 -> default로 해결

	/**
	 * @implSpec 
	 * 이 구현체는 getName()으로 가져온 문자열을 대문자로 바꿔 출력한다.
	 */
	default void printNameUpperCase() {
		System.out.println(getName().toUpperCase());
	}

	static void printAnything() {
		System.out.println("Foo");
	}

	String getName();

}
public class DefaultFoo implements Foo {

	String name;

	public DefaultFoo(String name) {
		this.name = name;
	}

	@Override
	public void printName() {
		System.out.println(this.name);
	}

	@Override
	public String getName() {
		return this.name;
	}
}
import java.util.Arrays;

public class App {

	public static void main(String[] args) {
		Foo foo = new DefaultFoo("yulhee");
		foo.println(); // yulhee
		foo.printNameUpperCase(); // YULHEE

		Foo.printAnything();
	}
	
}
public interface Bar extends Foo {

	void printNameUpperCase();
	
}

-> Bar에서 Foo가 제공하는 기본 구현체를 제공하고 싶지 않을 때
추상 메서드로 선언하여 다시 재정의 해주면 됨

✏️ java 8 API의 기본 메소드와 스태틱 메소드

Iterable의 기본 메소드

  • forEach()
  • spliterator()

Collection의 기본 메소드

  • stream()/parallelStream()
  • removeIf(Predicate)
  • spliterator()

Comparator의 기본 메소드 및 스태틱 메소드

  • reversed()
  • thenComparing()
  • static reverseOrder()/naturalOrder()
  • static nullsFirst()/nullsLast()
  • static comparing()

예제

import java.util.*;
import java.util.stream.Collectors;

public class App {
	
	public static void main(String[] args) {
		List<String> name = new ArrayList<>();
		name.add("yulhee");
		name.add("duboo");
		name.add("gab");
		name.add("foo");

		name.forEach(System.out::println);

		for (String n: name) {
			System.out.println(n);
		}

		Spliterator<String> spliterator = name.spliterator();
		Spliterator<String> spliterator2 = spliterator.trySplit(); //절반으로 쪼개줌
		while(spliterator.tryAdvance(System.out::println));
		System.out.println("============");
		while(spliterator2.tryAdvance(System.out::println));

		name.stream().map(String::toUpperCase)
				.filter(s -> s.startsWith("y"))
				.collect(Collectors.toSet());

		name.removeIf(s -> s.startsWith("y"));

		Comparator<String> compareToIgnoreCase = String::compareToIgnoreCase;
		name.sort(compareToIgnoreCase.reversed()); //문자열 역순으로 정렬
		name.sort()

	}
}

-> spliterator 출력 결과

yulhee
duboo
============
gab
foo

[ Stream ]

✏️ Stream 소개

Stream

  • sequence of elements supporting sequential and parallel aggregate operations
  • 데이터를 담고 있는 저장소 (컬렉션)이 아니다.
  • Funtional in nature, 스트림이 처리하는 데이터 소스를 변경하지 않는다.
  • 스트림으로 처리하는 데이터는 오직 한번만 처리한다.
  • 무제한일 수도 있다. (Short Circuit 메소드를 사용해서 제한할 수 있다.)
  • 중개 오퍼레이션은 근본적으로 lazy 하다.
  • 손쉽게 병렬 처리할 수 있다.

스트림 파이프라인

  • 0 또는 다수의 중개 오퍼레이션 (intermediate operation)과 한개의 종료 오퍼레이션 (terminal operation)으로 구성한다.
  • 스트림의 데이터 소스는 오직 터미널 오퍼네이션을 실행할 때에만 처리한다.

중개 오퍼레이션

  • Stream을 리턴한다.
  • Stateless / Stateful 오퍼레이션으로 더 상세하게 구분할 수도 있다. (대부분은 Stateless지만 distinct나 sorted 처럼 이전 이전 소스 데이터를 참조해야 하는 오퍼레이션은 Stateful 오퍼레이션이다.)
  • filter, map, limit, skip, sorted, .

종료 오퍼레이션

  • Stream을 리턴하지 않는다.
  • collect, allMatch, count, forEach, min, max, ...
public class App {
	
	public static void main(String[] args) {
		List<String> names = new ArrayList<>();
		name.add("yulhee");
		name.add("duboo");
		name.add("gab");
		name.add("foo");

		Stream<String> stringStream = names.stream().map(String::toUpperCase);


		//종료형 오퍼레이터 선언 -> collect
		//collect 사용해야만 println문 실행됨
		List<String> collect = names.stream().map((s) -> {
			System.out.println(s); //소문자 출력
			return s.toUpperCase();
		}).collect(Collectors.toList());
		collect.forEach(System.out::println); //대문자 출력

		System.out.println("============");

		names.forEach(System.out::println); //소문자로 출력됨
	}

✏️ Stream API

걸러내기

  • Filter(Predicate)

변경하기

  • Map(Function) 또는 FlatMap(Function)
  • 예) 각각의 Post 인스턴스에서 String title만 새로운 스트림으로

생성하기

  • generate(Supplier) 또는 Iterate(T seed, UnaryOperator)
  • 예) 10부터 1씩 증가하는 무제한 숫자 스트림

제한하기

  • limit(long) 또는 skip(long)
  • 예) 최대 5개의 요소가 담긴 스트림을 리턴한다.

스트림에 있는 데이터가 특정 조건을 만족하는지 확인

  • anyMatch(), allMatch(), nonMatch()
  • 예) k로 시작하는 문자열이 있는지 확인한다. (true 또는 false를 리턴한다.)

개수 세기

  • count()

스트림 데이터 하나로 뭉치기

  • reduce(identity, BiFunction), collect(), sum(), max()
public class OnlineClass {

    private Integer id;

    private String title;

    private boolean closed;

    public OnlineClass(Integer id, String title, boolean closed) {
        this.id = id;
        this.title = title;
        this.closed = closed;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public boolean isClosed() {
        return closed;
    }

    public void setClosed(boolean closed) {
        this.closed = closed;
    }
}
import java.util.ArrayList;
import java.util.List;

public class App {

    public static void main(String[] args) {
    	List<OnlineClass> springClasses = new ArrayList<>();
        springClasses.add(new OnlineClass(1, "spring boot", true));
        springClasses.add(new OnlineClass(2, "spring data jpa", true));
        springClasses.add(new OnlineClass(3, "spring mvc", false));
        springClasses.add(new OnlineClass(4, "spring core", false));
        springClasses.add(new OnlineClass(5, "rest api development", false));

        System.out.println("spring 으로 시작하는 수업");
        // TODO
        springClasses.stream()
        	.filter(oc -> oc.getTitle().startsWith("spring"))
        	.forEach(oc -> System.out.println(oc.getId()));

        System.out.println("close 되지 않은 수업");
        // TODO
        springClasses.stream()
        	//.filter(oc -> !oc.isClosed())
        	.filter(Predicate.not(OnlineClass::isClosed))
        	.forEach(oc -> System.out.println(oc.getId()));

        System.out.println("수업 이름만 모아서 스트림 만들기");
        // TODO
        springClasses.stream()
        	.map(oc -> oc.getTitle())
        	.forEach(s -> System.out.println(s));


        List<OnlineClass> javaClasses = new ArrayList<>();
        javaClasses.add(new OnlineClass(6, "The Java, Test", true));
        javaClasses.add(new OnlineClass(7, "The Java, Code manipulation", true));
        javaClasses.add(new OnlineClass(8, "The Java, 8 to 11", false));

        List<List<OnlineClass>> keesunEvents = new ArrayList<>();
        keesunEvents.add(springClasses);
        keesunEvents.add(javaClasses);


        System.out.println("두 수업 목록에 들어있는 모든 수업 아이디 출력");
        // TODO
        //keesunEvents.stream().flatMap(list -> list.stream())
        keesunEvents.stream()
        		.flatMap(Collection::stream)
        		.forEach(oc -> System.out.println(oc.getId()));

        System.out.println("10부터 1씩 증가하는 무제한 스트림 중에서 앞에 10개 빼고 최대 10개 까지만");
        // TODO
        Stream.iterate(10, i -> i + 1)
        		.skip(10)
        		.limit(10)
        		.forEach(System.out::println);

        System.out.println("자바 수업 중에 Test가 들어있는 수업이 있는지 확인");
        // TODO
        boolean test = javaClasses.stream().anyMatch(oc -> oc.getTitle().contains("Test"));
        System.out.println(test);

        System.out.println("스프링 수업 중에 제목에 spring이 들어간 것만 모아서 List로 만들기");
        // TODO
        List<String> spring = springClasses.stream()
        		.filter(oc -> oc.getTitle().contains("spring"))
        		.map(OnlineClass::getTitle)
        		.collect(Collectors.toList());
        spring.forEach(System.out::println);
    }

}

profile
백엔드 개발자

0개의 댓글