Skip to content

Latest commit

 

History

History
409 lines (337 loc) · 13 KB

java-7.md

File metadata and controls

409 lines (337 loc) · 13 KB

Java - Functional Interface와 Method Reference

Fuction


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

Function 인터페이스는 apply 메서드 하나를 추상 메서드로 제공합니다.

사용 방법 1: 구현 클래스 사용

public class Adder implements Function<Integer, Integer> {

	@Override
	public Integer apply(Integer x) {
		return x + 10;
	}
}
---------------------------------------------------------
public static void main(String[] args) {
		Function<Integer, Integer> myAdder = new Adder();
		int result = myAdder.apply(5);
		System.out.println(result);
	}

인터페이스의 구현체를 선언하고 사용하는 방식입니다.

사용 방법 2: 람다식 사용

람다식을 사용하면 구현체를 선언하지 않고도 간편하게 사용할 수 있습니다.

public static void main(String[] args) {
		Function<Integer, Integer> myAdder = x -> x + 10;
		int result = myAdder.apply(5);
		System.out.println(result);
	}

제네릭으로 인해 타입을 유추할 수 있기 때문에 람다식에는 타입을 명시하고 않고 바로 변수명을 사용해도 됩니다.

BiFunction


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

BiFunction은 파라미터를 T, U 타입의 파라미터를 받아서 R타입을 반환하는 추상메서드를 제공하는 인터페이스입니다.

예시

public static void main(String[] args) {
		BiFunction<Integer, Integer, Integer> add = (x, y) -> x + y;
		int result = add.apply(3, 5);
		System.out.println(result);
	}

x, y를 받아서 덧셈 결과를 반환하는 람다식입니다.

Functional interface


Fuction와 BiFunction를 타고 들어가보면 인터페이스에 @FunctionalInterface 가 붙어있습니다.
이는 단 하나의 abstract method(추상 메서드)만을 갖는 인터페이스 를 의미합니다.
default 메서드와 static 메서드는 이미 구현되어 있으므로 해당 메서드는 존재해도 괜찮습니다.
인자를 3개 받는 함수형 인터페이스는 선언되어 있지 않기 때문에 한 번 예시로 만들어보겠습니다.

@FunctionalInterface
public interface TriFunction<T, U, V, R> {
	R apply(T t, U u, V v);
}
------------------------------------------------------------------------------
public static void main(String[] args) {
	TriFunction<Integer, Integer, Integer, Integer> addThreeNumbers =
			(x, y, z) -> x + y + z;
	int result = addThreeNumbers.apply(3, 2, 5);
	System.out.println(result);
}

위와 같이 간단하게 만들어두고 사용할 수 있습니다.


Supplier


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

Supplier는 공급하는 인터페이스로 input 없이 리턴값만 갖는 추상 메서드를 갖고 있습니다.

예시

public class SupplierTest {
	public static void main(String[] args) {
		Supplier<Double> myRandomDoubleSupplier = () -> Math.random();
		printRandomDoubles(myRandomDoubleSupplier, 5);
	}
	
	public static void printRandomDoubles(Supplier<Double> randomSupplier, int count) {
		for (int i = 0; i < count; i ++) {
			System.out.println(randomSupplier.get());
		}
	}
} 

myStringSupplier의 get 함수가 랜덤값을 반환하도록 람다식으로 정의했습니다.
이 함수형 인터페이스를 함수의 인자로 넘겨서 5번 호출하는 예시입니다.
결과적으로 랜덤값이 5번 찍히게 됩니다.

Consumer


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

Consumer 인터페이스는 값을 받아서 처리하고 아무것도 리턴하지 않는 추상메서드를 갖고 있습니다.

예시

public class ConsumerTest {
	public static void main(String[] args) {
		
		List<Integer> integerInputs = Arrays.asList(4, 2, 3);
		Consumer<Integer> myIntegerProcessor = x -> 
			System.out.println("Processing integer " + x);
		process(integerInputs, myIntegerProcessor);
	}
	
	public static <T> void process(List<T> inputs, Consumer<T> processor) {
		for (T input : inputs) {
			processor.accept(input);
		}
	}
}

process 함수에 인자로 리스트와 Consumer를 넣어주고 Consumer의 accept 함수를 호출해서 콘솔에 찍는 예시입니다.

BiConsumer


@FunctionalInterface
public interface BiConsumer<T, U> {
    void accept(T t, U u);
}

BiConsumer는 2개의 인자를 받아서 처리하고 아무것도 리턴하지 않는 추상메서드를 갖고 있습니다.

예시

public class BiConsumerTest {
	public static void main(String[] args) {
		BiConsumer<Integer, Double> myDoubleProcessor = 
				(index, input) -> 
					System.out.println("Processing " + input + " at index " + index);
		List<Double> inputs = Arrays.asList(1.1, 2.2, 3.3);	
		process(inputs, myDoubleProcessor);
	}
	
	public static <T> void process(List<T> inputs, BiConsumer<Integer, T> processor) {
		for (int i = 0; i< inputs.size(); i++) {
			processor.accept(i, inputs.get(i));
		}
	}
}

process 함수에 인자로 리스트와 BiConsumer를 넣어주고 BiConsumer의 accept 함수를 호출해서 콘솔에 찍는 예시입니다.


Predicate


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

Predicate 인터페이스는 하나의 인자를 받아서 처리하고 boolean을 반환하는 추상 메서드를 하나 갖고 있습니다.

예시

public class Chapter4Section4 {

	public static void main(String[] args) {
		Predicate<Integer> isPositive = x -> x > 0;
		System.out.println(isPositive.test(-10)); // false
		
		List<Integer> inputs = Arrays.asList(10, -5, 4, -2, 0, 3);
		System.out.println("Positive number: " + filter(inputs, isPositive));
		System.out.println("Non-positive number: " + filter(inputs, isPositive.negate()));
		System.out.println("Non-negative number: " 
				+ filter(inputs, isPositive.or(x -> x == 0)));
		System.out.println("Positive even numbers: " 
				+ filter(inputs, isPositive.and(x -> x % 2 == 0)));
	}
	
	public static <T> List<T> filter(List<T> inputs, Predicate<T> condition) {
		List<T> output = new ArrayList<>();
		for (T input : inputs) {
			if (condition.test(input)) {
				output.add(input);
			}
		}
		return output;
	}
}

filter 함수는 인풋 중에서 통과한 것들만 걸러서 반환하는 함수로 만들었습니다.
거르는 것에 대한 판단은 두 번째 인자로 받는 Predicate가 처리합니다.
Predicate 인터페이스에는 default 함수가 여러 가지 있는데 몇가지만 살펴보면 다음과 같습니다.

  • default Predicate<T> or(Predicate<? super T> other)
    • or API는 인자로 다른 Predicate을 받아서 둘 중에 하나라도 만족하면 true를 반환하는 새로운 Predicate을 반환해줍니다.
  • default Predicate<T> and(Predicate<? super T> other)
    • and API는 인자로 다른 Predicate을 받아서 둘다 만족해야만 true를 반환하는 새로운 Predicate을 반환해줍니다.
  • default Predicate<T> negate()
    • negate는 현재 만든 Predicate의 정반대의 Predicate을 반환해줍니다.

위 예시에서도 보았듯이 결국에는 하나의 filter 함수를 만들었지만 여러개의 Predicate을 제공함으로써 다양한 방식으로 활용할 수 있습니다.

Operator


  • UnaryOperator<T>
    • Function인데 인풋과 리턴타입이 같은 경우에 사용합니다.
  • BinaryOperator<T>
    • 인풋 타입 2개와 리턴타입이 같은 경우에 사용합니다.

Primitive Type Functional Interface


java.util.function 패키지에 보면 인터페이스명에 Primitive 타입이 붙은 것들이 존재합니다.
Function같이 제네릭 타입으로 다 처리할 수 있는데 굳이 이렇게 Primitive 타입이 붙은 것들을 제공하는 이유는 제네릭의 경우는 박싱타입을 사용해야하므로 메모리를 더 잡아먹기 때문입니다.
따라서 메모리를 아끼면서 사용할 수 있게끔 Primitive 타입을 붙인 인터페이스를 제공합니다.


Comparator


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

앞서 소개한 함수형 인터페이스는 java.util.function 패키지에 있지만, Comparator는 java.util 패키지 안에 있는 함수형 인터페이스입니다.
인수 2개를 받아서 비교하는 int를 반환하는 compare 추상 메서드를 제공합니다.
보통 비교하여 정렬하는 과정이 필요할 때 사용합니다.

  • 음수면 o1 < o2
  • 0이면 o1 = o2
  • 양수이면 o1 > o2

예시

public class ComparatorTest {

	public static void main(String[] args) {
		List<User> users = new ArrayList<>();
		users.add(new User(3, "Alice"));
		users.add(new User(1, "Charlie"));
		users.add(new User(5, "Bob"));
		System.out.println(users);
		
		Comparator<User> idComparator = (u1, u2) -> Integer.compare(u1.getId(),u2.getId());
		Collections.sort(users, idComparator);
		System.out.println(users);
		
		Collections.sort(users, (u1, u2) -> u1.getName().compareTo(u2.getName()));
		System.out.println(users);
	}

}

Collections.sort의 두 번째 인자 또는 Arrays.sort의 두 번째 인자로 넣어서 사용할 수 있습니다.
박싱타입의 compare를 사용하면 둘을 비교해서 첫 번째 인자가 크면 양수, 작으면 음수, 같으면 0을 반환해줍니다.
오름차순으로 정렬하고 싶다면 순서를 바꿔주면 됩니다.

Method Reference


  • 기존에 이미 선언되어있는 메서드를 지정하고 싶을 때 사용합니다.
  • :: 오퍼레이터를 사용합니다.
  • 생략이 많기 때문에 사용할 메서드의 매개변수의 타입과 리턴 타입을 미리 숙지해야 합니다.

4가지 사용 케이스

  • className::staticMethodName
    • 클래스의 static method를 지정할 때 사용합니다.
  • objectName::instanceMethodName
    • 선언 된 객체의 instance method를 지정할 때 사용합니다.
public class MethodReferenceTest {
	public static int calculate(int x, int y, BiFunction<Integer, Integer, Integer> operator) {
		return operator.apply(x, y);
	}
	
	public static int multiply(int x, int y) {
		return x * y;
	}
	
	public int subtract(int x, int y) {
		return x - y;
	}
	
	public void myMethod() {
		System.out.println(calculate(10, 3, this::subtract));
	}
	
	
	public static void main(String[] args) {
		int a = Integer.parseInt("15");
		Function<String, Integer> str2int = Integer::parseInt; // 첫 번째 방식
		System.out.println(str2int.apply("20"));
		
		String str = "hello";
		boolean b = str.equals("world");
		Predicate<String> equalsToHello = str::equals; // 두 번째 방식
		System.out.println(equalsToHello.test("world"));
		
		System.out.println(calculate(8, 2, (x, y) -> x + y)); // BiFunction 람다식 이용하기
		System.out.println(calculate(8, 2, MethodReferenceTest::multiply)); // BiFunction method Reference 첫 번째 방식 이용하기
		
		MethodReferenceTest instance = new MethodReferenceTest();
		System.out.println(calculate(8, 2, instance::subtract)); // BiFunction method Reference 두 번째 방식 이용하기
		instance.myMethod();
	}

}

  • className::instanceMethodName
    • 객체의 instance method를 지정할 때 사용합니다.
public class MethodReferenceTest {

	public static void main(String[] args) {
		Function<String, Integer> strLength = String::length; // 세 번째 방식 -> 앞선 두 번째 방식은 인스턴스::메서드 지만 이것은 클래스타입::메서드
		int length = strLength.apply("hello world");
		System.out.println(length);
		
		BiPredicate<String, String> strEquals = String::equals; // 세 번째 방식 클래스타입:: 메서드
		boolean helloEqualsWorld = strEquals.test("hello", "world");
		System.out.println(strEquals.test("hello", "hello"));
		
		List<User> users = new ArrayList<>();
		users.add(new User(3, "Alice"));
		users.add(new User(1, "Charlie"));
		users.add(new User(5, "Bob"));
		
		printUserField(users, User::getName);
	}

	public static void printUserField(List<User> users, Function<User, Object> getter) {
		for (User user : users) {
			System.out.println(getter.apply(user));
		}
	}
}

  • className::new
    • 클래스의 constructor를 지정할 때 사용합니다.
public class MethodReferenceTest {

	public static void main(String[] args) {		
		BiFunction<Integer, String, User> userCreator = User::new; // 4번째 방식 사용
		User backtony = userCreator.apply(1, "backtony");
	}
}

----------------------------------------------------------------------------
// 이런식으로도 사용할 수 있습니다.
Map<String, BiFunction<String, String, Car>> carTypeToConstructorMap = new HashMap<>();
carTypeToConstructorMap.put("sedan", Sedan::new);
carTypeToConstructorMap.put("suv", Suv::new);
carTypeToConstructorMap.put("van", Van::new);



참고
Java Function Programming & Stream Lectur