JAVA

함수형 인터페이스(Functional Interface) & 기초 람다

함수형 인터페이스

  • 한 개의 추상 메소드를 가지고 있는 인터페이스
  • 자바의 람다식은 함수형 인터페이스로만 접근이 되기에 필요하다.
  • 선언하여 변수처럼 할당시키거나, 다른 메소드들에 변수를 이용해 값을 전달하는 형태의 식을 가진다.
	[선언 에]
	@FunctionalInterface --> 어노테이션을 선언
	private interface user_name{
		public void [자료형] user_name(매개인자,,,);
		
	}
  • 표현식 사용 (구현하는 메소드의 인수)-> {처리} 형식
  • 표현식 특징: 람다 표현식은 익명으로 처리되어 이름이 없다.
    • 람다 표현식은 compiler가 context에 맞는 타입을 유추하기 때문에 return type이 없다.  
    • throws가 없다.
    • 표현식 자체가 generic이 될 수 없어 type 파라미터를 선언할 수 없다.
    • Example
      ex01) 매개 인자로 su를 받아 출력하는 구문
      (int su) ->{ System.out.println(su) }

      ex02) 자료형이 생략 가능하다.
      su -> {System.out.println(su)}

      ex03) 매개인자가 하나일 경우 (), {} 가 생략 가능하다.
      su -> System.out.println(su)

      ex04) 매개인자가 없는 경우 () 빈괄호만 사용하여 명령 실행할 수 있다.
      () -> {System.out.println('A')}

      ex05) 매개인자 두개 이상이면 자료형 생략 하여 실행할 수 있다.
      (a,b) -> {return a+b}

      ex06) 매개인자 두 개 이상이면 {} 와 return 키워드 생략할  수 있다.
      (a,b) -> {a+b}
	@FunctionalInterface // My_function에 있는 메소드를 람다식으로 사용하겠다.
	private interface My_Function{
		public void say(String name);//추상 메소드 하나만 선언(functionalInterface에서는 하나만)
		
	}
	public static void main(String[] args) {
		My_Function func= (name)-> System.out.println("Hello," +name);
		func.say("홍길동");
	}

위의 경우 name 안에 Hello name이라는 것을 담아 준 후 선언한 func로 say 함수에 값을 넣어주면 "Hello, 홍길동" 이라는 값을 받을 수 있다.

	@FunctionalInterface
	interface Func{
		public int calc(int a,int b);
	}
	public static void main(String[] args) {
		
		Func add=(int a, int b) ->{return a+b;};
		Func sub=(int a, int b)->{return a-b;};
		Func mul=(a,b)->{return a*b;};
		Func div=(a,b)->{System.out.println(a+ "/"+ b);return a/b;};
		System.out.println(add.calc(100,50));
		System.out.println(sub.calc(100, 50));
		System.out.println(mul.calc(100, 50));
		System.out.println(div.calc(100, 50));
		
	}

functionalInterface를 활용한 사칙 연산이다. Func라는 인터페이스를 기반으로 사용될 함수들을 정의해 두고, 람다 형식으로 값을 계산하여 출력해 보았다.

 

기본 함수형 인터페이스  java.util.function  참조

  • Runnable
    • 인자를 받지 않고 리턴 값도 없는 인터페이스이다.
public interface Runnable {
  public abstract void run();
}

Runnable runnable = () -> System.out.println("run anything!");
runnable.run();
// 결과
// run anything!

함수형 인터페이스 별로 run()과 같은 실행 메소드 이름이 다르므로 확인하고 사용하여야 한다.

  • Supplier
    • 추상 메소드 get()이 정의되어 있으며, 각 메소드는 정의된 자료형을 리턴하도록 하여 컬렉션 객체를 사용시 적용하면 코드를 간결하게 활용할 수 있다.
    • 인자를 받지 않고 T 타입의 객체를 반환한다.
public interface Supplier<T> {
    T get();
}

Supplier<String> getString = () -> "코딩코딩!";
String str = getString.get();
System.out.println(str);
// 결과
// 코딩코딩!

해당 경우 String 형태를 T로 잡았기에 get을 하게 될 경우 String 형태의 객체를 반환한다.

  • Consumer
    • Consumer<T> 는 T타입의 객체를 인자로 받고 리턴 값은 존재하지 않는다.
    • 값을 받아 처리하지만 반환 값을 리턴하지 않는 void형 function과 흡사하다.
    • BiConsumer<T,U> : void accept(T t, U u)의 경우 T와 U를 받아서 side effect만 발생시키고 결과 값은 똑같이 존재하지 않는다.
		Consumer<String> c = s-> System.out.println(s);
		c.accept("abc");
		
		Consumer<String> c1 = s-> System.out.println("c1="+s);
		Consumer<String> c2 = s-> System.out.println("c2="+s);
		Consumer<String> c_res = c1.andThen(c2);
		
		c_res.accept("abc");
        	//결과
        	//abc
		//=============
		//c1=abc
		//c2=abc

accept() 메소드를 사용하며, andThen을 이용하면 두 개 이상의 Consumer를 연속적으로 실행할 수 있다.

 

  • Function
    • apply() 메소드를 활용하여 람다로 사용시 자주 활용되나.
    • apply() 메소드의 리턴 값이 R이기 떄문에 중괄호 {} 의 리턴 값이 R의 객체가 되도록 한다.
    • andThen(), compose() 메소드는 function을 받아서 function을 리턴한다.
    • Function<T,R>의 경우 R apply(T t)의 형식을 가지며 매개인자 T를 받아서 R로 리턴한다.
    • BiFunction<T,U,R>의 경우 인자로 T와 U를 받아서 R로 리턴한다.
    • UnaryOperator<T> : T apply(T t)의 경우 Function<T,T>을 상속 받아서 동일한 인수로 리턴한다.
    • BinaryOperator<T> : T apply(T t1, T t2)의 경우 BiFunction<T,U,R>을 상속 받아서 동일한 타입으로 리턴한다.
    • compose()는 두개의 function을 조합하여 새로운 function 객체를 만들어 주는 메소드이다. but AndThen()과 순서가 반대임을 명시해야 한다.
	[타입선언]
	package java.util.function
	
	@FunctionalInterface
	public interface Function<T,R>{
		R apply(T t);
		default<V> Function<T,V> andThen(Function<? super R, ? extend V> after);
		default<V> Function<V,R> compose(Function<? super V, ? extend T> before);
		static<T> Function<T,T> identity();
	}
	
	ex) Function<Integer, Integer> a1= e-> e*2;
		Function<Integer, Integer> a1= e-> e*e;
		
		a1.compose(a2).apply(2);//숫자 2를 e로 전달한 후에 a2, a1 순으로 수행 -> 8
		a1.andThen(a2).apply(2);// 숫자 2를 e로 전달 후에 a1, a2 순으로 수행 -> 16
		Function<Integer, String> my_fun=(i)->Integer.toString(i);
		//인티저 값을 스트링으로 바꾼다.
		System.out.println(my_fun.apply(100).length());
		System.out.println(my_fun.apply(10000).length());
		//결과
		//3
		//5
		BiFunction<String, String, String> bi=(x,y)->{
			return x+y;
		};
		
		Function <String,String> f= x->x+"!";
		
		System.out.println(bi.andThen(f).andThen(f).apply("Getting Start", " java"));
		//결과
		//Getting Start java!!
  • Predicate
    • 조건 집합을 정의하고 지정된 객체의 조건 충족 유무에 따라 true/ false로 리턴하는 메소드를 가진다.
    • Predicate<T>의 경우 T에 대해서 true/ false를 리턴한다.
    • test()메소드를 사용한며 and() , or() 메소드와 함께 사용할 수 있다.
    • isEqual()은 static 메소드로 인자로 전달되는 객체와 같은지 확인하는 객체를 만들 수 있다.
	@FunctionalInterface
	public interface Predicate<T>{
		boolean test(T, t);
		default Predicate<T> and(Predicate<? super T> other);
		static <T> Predicate<T> isEqual(Object targetRef);
		default Predicate<T> negate();
		default Predicate<T> or(Predicate<? super T> other);
	}
Predicate<Integer> isBiggerThanFive = num -> num > 5;
System.out.println("10 is bigger than 5? -> " + isBiggerThanFive.test(10));
// 결과
// 10 is bigger than 5? -> true
Predicate<String> isEquals = Predicate.isEqual("Google");
isEquals.test("Google");
// 결과
// true

람다 기초 활용

  • jdk 8부터는 메소드와 생성자를 직접 참조할 수 있다.
  • C++의 :: 연산자를 활용한다.
  • 생성자 참조의 경우 class_name::new
  • 메소드 참조의 경우 
    • static 일떄, class_name::method_name
    • non-static일때, instance_name::method_name
// 이름과 나이를 관리하는 MyConstruct 라는 클래스를 선언해서 생성자 참조를 통해
// 값을 출력하는 소스 코드를 작성해보자.

class MyConstruct{
	String name;
	int age;
	
	public MyConstruct() {
		name="길동";
		age=21;
	}
	public MyConstruct(String name) {
		this.name=name;
		age=25;
	}
	public MyConstruct(String name, int age) {
		this.name=name;
		this.age=age;
	}
	@Override
	public String toString() {
		return "MyConstruct [name=" + name + ", age=" + age + "]";
	}
	
}
//functional Interface 이용해서 생성자
public class ConstRefTest {

	public static void main(String[] args) {
		Supplier<MyConstruct> func=MyConstruct::new; //생성하는 방법
		//매개인자 없는 기본 생성자가 생성된다.
		MyConstruct res=func.get();
		System.out.println(res);
		
		//매개 인자 하나만 받아서 생성하는 경우 이걸 사용한다. MyConstruct로 리턴하기 위한 Function
		Function<String,MyConstruct> func1=MyConstruct::new;
		System.out.println(func1.apply("Dominica_kim"));
		
		BiFunction<String,Integer,MyConstruct> func2=MyConstruct::new;
		System.out.println(func2.apply("pearl", 26));
		//결과
		//MyConstruct [name=길동, age=21]
		//MyConstruct [name=Dominica_kim, age=25]
		//MyConstruct [name=pearl, age=26]

	}
}

default/ String / String, int 형태의 생성자를 만들어 두었기에

  • default의 경우 supplier를 이용하여 get으로 출력
  • String의 경우 String 형태의 값을 넣어 MyConstruct 형태의 값을 뽑아야 하므로 Function<T,R>를 활용
  • String, int의 경우 Function<T,U,R> 을 활용하여 값을 출력하였다.

해당 반환 값은 객체를 반환하나 Class에서 toString을 override 해놓았기에 원하는 결과값을 얻을 수 있었다.

//ConstRefTest의 생성자 참조 형식을 순수하게 람다형식으로 변환
public class ConstRefTest02 {

	public static void main(String[] args) {
		Supplier<MyConstruct> func=()->new MyConstruct(); //생성하는 방법
		//매개인자 없는 기본 생성자가 생성된다.
		MyConstruct res=func.get();
		System.out.println(res);
		
		//매개 인자 하나만 받아서 생성하는 경우 이걸 사용한다. MyConstruct로 리턴하기 위한 Function
		Function<String,MyConstruct> func1=(name)->new MyConstruct(name);
		System.out.println(func1.apply("Dominica_kim"));
		
		BiFunction<String,Integer,MyConstruct> func2=(name,age)-> new MyConstruct(name,age);
		System.out.println(func2.apply("장재성", 26));
	}
}

위 코드의 경우 앞선 코드를 순수한 람다 형식으로 바꿔준 것이다.