반응형

📝익명 클래스

프로그램에서 일시적으로 한번만 사용되고 버려지는 객체입니다.

일시적으로 사용된다는 것은 재사용이 되지 않거나 그럴 필요가 없다는 것인데 이건 확장성이 좋지 않다는 걸 의미합니다.

OOP 측면에서는 확장성 있게 만드는 걸 지향하지만 오히려 설계를 해치는 경우가 있습니다.

 

이러한 경우에 익명 클래스를 많이 씁니다.

 

    1. 프로그램 한번만 사용되어야하는 객체의 경우 [ UI 이벤트 처리, 스레드 객체 등... ]

 

밑에는 UI 이벤트 처리에 대한 예제이다 사실상 어거지로 만든 거고 클래스로도 처리가 가능할 거 같은데 설계 부분에 솔직히 이해는 가지만 크게 와닿진 않는다

 

Button를 생성해서 인터페이스를 즉각 구현해서 익명 객체(매개변수)를 구현한 예제이다.

 

RingListener

package org.example;

public interface RingListener {

    public void ring ();
}

 

Button

package org.example;

public class Button {
    public void ring(RingListener listener){
        listener.ring();
    }
}

 

Main

package org.example;

public class Main {

    public static void main(String[] args) {

    Button button = new Button();
    button.ring(new RingListener() {
        @Override
        public void ring() {
            System.out.println("1회용 ring ring");
        }
    }); // 1회용 ring ring
  }
}

 


    2. 비즈니스 로직이 제각각이며 재사용성이 없어서 매번 클래스를 생성해야하는 경우 [ 아래와 예제와 같이 버튼이 여러개인데 각각 다른 역할의 응답을 하는 경우 버튼이 많아질수록 기하급수적으로 늘어난다]

 

Button

package org.example;

public class Button {

    void turnOn () {
        System.out.println("버튼을 킵니다.");
    }
}

 

Anonymous

package org.example;

public class Anonymous {


    /** 객체 생성해 재정의하기 **/
    Button button = new Button(){
        String name = "방 스위치";

        // 익명 클래스 안에서만 살아있기 때문에 사용 불가
        void turnOff(){
            System.out.println("Down 버튼을 눌러 " + name + "를 끕니다");
        }

        // 재정의를 했기 때문에 사용 가능
        @Override
        void turnOn(){
            System.out.println("Up 버튼을 눌러 " + name + "를 킵니다");
        }
    };


    /** 클래스안 메소드에서 객체 생성해 재정의하기 [1번과 동일하다] **/
    void method1(){

        Button button1 = new Button(){

            String name = "정답 스위치";

            // 익명 클래스 안에서만 살아있기 때문에 사용 불가
            void turnOff(){
                System.out.println("X 버튼을 눌러 " + name + "를 끕니다");
            }

            // 재정의를 했기 때문에 사용 가능
            @Override
            void turnOn(){
                System.out.println("O 버튼을 눌러 " + name + "를 킵니다");
            }

        };

        button1.turnOn();
    }

    /** 매개변수에 익명함수 사용해보기 **/
    void method2(Button button){
        button.turnOn();
    }
    
}

 

Main Thread

public class Main {
    public static void main(String[] args) {
    
        Anonymous button = new Anonymous();

        // 1번 방법으로 호출
        button.button.turnOn(); // 위 버튼을 눌러 방 스위치를 킵니다

        // 2번 방법으로 호출
        button.method1();       // O 버튼을 눌러 정답 스위치를 킵니다

        // 3번 방법으로 호출
        // 2번은 Anonymous에서 상세 내용을 작성한 거고 3번 방법은 그걸 밖에서 내가 정의해서 사용하는 차이가 있다.
        // 즉, 어디서 상세 내용을 재정의하냐에 따라 다름
        button.method2(new Button(){

            String name = "엘레베이터";

            // 익명 클래스 안에서만 살아있기 때문에 사용 불가
            void turnOff(){
                System.out.println("엑스 버튼을 눌러 " + name + "를 닫습니다.");
            }

            // 재정의를 했기 때문에 사용 가능
            @Override
            void turnOn(){
                System.out.println("동그라미 버튼을 눌러 " + name + "를 엽니다.");
            }

        }); // 동그라미 버튼을 눌러 엘레베이터를 엽니다.
    }
}

 

 

📝함수형 인터페이스

함수형 인터페이스란 1 개의 추상 메소드를 갖는 인터페이스로 여러 개의 디폴트 메서드가 있더라도 추상 메서드가 오직 하나면 함수형 인터페이스라고 한다 (default method 또는 static method 는 여러 개 존재해도 상관 없다)

매번 함수형 인터페이스를 직접 만들어서 사용하는 건 번거로운 일로서 그래서 Java 에서는 기본적으로 많이 사용되는 함수형 인터페이스를 제공한다 기본적으로 제공되는 것만 사용해도 충분하기 때문에 직접 만드는 경우는 거의 없다.

 

예제 코드

@FunctionalInterface // 함수형 인터페이스 조건에 맞는지 검사
interface CustomInterface<T> {
    
    T myCall(); // abstract method 오직 하나!!

    // default method 는 존재해도 상관없음
    default void printDefault() {
     	System.out.println("Hello Default");
    }

    // static method 는 존재해도 상관없음
    static void printStatic() {
     	System.out.println("Hello Static");
    }
}

 

함수형 인터페이스 장점

  1. 같은 Input에 같은 Ouput을 제공하고 변경 개념이 아니라 복사되고 복사 된 것을 함수를 거쳐 결과를 내기 때문에 멀티스레드 공유자원에 안전하다
  2. 코드를 간결하고 가독성 있게 작성할 수 있다.

 

함수형 인터페이스 단점

  1. 익명 함수의 재사용이 불가능
  2. 재귀함수의 구현이 어렵다.
  3. 재사용이 불가능하므로 비슷한 함수 구현이 잦을 수 있다.
  4. 디버깅이 어렵다.

 

Java에서 제공하는 많이 쓰는 함수형 인터페이스

  1. Runnable           → 파라미터 없고 리턴값도 없는 경우                / 추상메소드 = run()
  2. Consumer<T>  → 파라미터 있고 리턴값 없는 경우                   / 추상메소드 = accept(T t)
  3. Supplier<T>      → 파라미터 없고 리턴값 있는 경우                   / 추상메소드 = get()
  4. Function<T,R>  → 파라미터 있고 리턴값 있는 경우                   / 추상메소드 = apply(T t) // T파라미터, R 반환값
  5. Operator<T>     → 파라미터 있고 리턴값 있는 경우                   / 연산용도, 임의의 타입 전달하고 임의의 타입 반환
  6. Predicate<T>    → 파라미터 있고 리턴값(boolean) 있는 경우  / 추상 메소드 = test(T t)

 

  • Runnable
public class Main {
    public static void main(String[] args) {
    	Runnable runnable = () -> System.out.println("Runnable run");
        runnable.run(); // Runnable run
    }
}

 

  • Consumer
public class Main {
    public static void main(String[] args) {
        Consumer<String> con = text -> System.out.println("parameter : " + text);
        con.accept("Consumer Testing!!");

        /** Consumer Chaining **/
        Consumer<String> cutFruit = fruit -> System.out.println(fruit + "를 깍습니다." ); // Apple를 깍습니다.
        Consumer<String> eatFruit = fruit -> System.out.println(fruit + "를 먹습니다.");  // Apple를 먹습니다.
        cutFruit.andThen(eatFruit).accept("Apple");
    }
}

 

Consumer 파생형

BiConsumer =>  파라미터를 2개 받아서 처리
DoubleConsumer ==> double타입으로 파라미터를 받아서 처리
IntConsumer ==> int타입으로 파라미터를 받아서 처리
LongConsumer ==> long타입으로 파라미터를 받아서 처리
ObjDoubleConsumer ==> 두개의 파라미터중에 하나는 obj 다른하나는 double
ObjIntConsumer ==> 두개의 파라미터중에 하나는 obj 다른하나는 int
ObjLongConsumer ==> 두개의 파라미터중에 하나는 obj 다른하나는 long

 

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

        BiConsumer<String, Integer> bi = (text, num) -> System.out.println("파리미터 2개 -> " + text + " + " + num);
        bi.accept("프로그래밍!!", 1234); // 파리미터 2개 -> 프로그래밍!! + 1234
    }
}

 

  • Supplier
public class Main {
    public static void main(String[] args) {
        Supplier<String> supplier = () -> "supplier has return but no params";
        System.out.println(supplier.get()); // supplier has return but no params
    }
}

 

Supplier 파생형

BooleanSupplier :  boolean 타입으로 반환
DoubleSupplier  :  double 타입으로 반환
IntSupplier     :  int 타입으로 반환
LongSupplier    :  long 타입으로 반환

 

  • Operator
public class Main {
    public static void main(String[] args) {
        BinaryOperator<Integer> binaryOperator = (num1, num2) -> num1 + num2;
        int result = binaryOperator.apply(5, 10);
        System.out.println(result); // 15
    }
}

Operator라는 인터페이스는 없다

 

BinaryOperator<T> : BiFunction<T,U,R>  상속 →  두개의 파라미터 전달하고 반환 ( 모두 데이터타입 동일 )

UnaryOperator<T> : Function<T,R> 상속          →  하나의 파라미터 전달하고 반환 ( 모두 데이터타입 동일 )

 

 

Operator 파생형

DoubleBinaryOperator : 두개의 파라미터 double 전달하고  double 반환 ( 모두 데이터타입 동일 )
DoubleUnaryOperator  : 하나의 파라미터 double 전달하고 double반환
IntBinaryOperator    : 두개의 파라미터 int 전달하고  int 반환 ( 모두 데이터타입 동일 )
IntUnaryOperator     : 하나의 파라미터 int 전달하고 int반환
LongBinaryOperator   : 두개의 파라미터 long 전달하고  long 반환 ( 모두 데이터타입 동일 )
LongUnaryOperator    : 하나의 파라미터 long 전달하고 long반환

 

 

  • Predicate
package org.example;

import java.util.function.*;

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

        Predicate<String> isNameString = str -> str.equals("name");
        System.out.println("is String Name? test() -> " + isNameString.test("age"));
        // is String Name? test() -> false
        System.out.println("is String Name? test() -> " + isNameString.test("name"));
        // is String Name? test() -> true
    }
}

 

📝 람다식 [익명 객체]

람다는 익명 객체이다 (익명 함수는 함수가 독자적으로 존재해야하지만 Java는 클래스에 종속적이기 때문에 익명 객체라는 말이 맞다)

 

  • 람다식 축약 예제 따라해보기
 class Calculator {

    int sum(int x, int y){
        return x + y;
    } // 람다식 : (x,y) -> x+y;

    void print(String name, int i){
        System.out.println(name + " = " + i);
    } // 람다식 : (name,i) -> System.out.println(name + " = " + i);

    int square(int x){
        return x * x;
    } // 람다식 : x -> x * x

    int roll() {
        return (int) (Math.random() * 6);
    } // 람다식 : () -> (int) (Math.random() * 6);
}
  1. 함수반환 타입을 생략할 수 있다.
  2. 함수명을 생략할 수 있다.
  3. 인자 타입을 생략할 수 있다.
  4. 함수내용인 중괄호 내용이 한줄이면 중괄호는 생략이 가능하다

 

  • 함수형 인터페이스 + 람다식 이용해보기
package org.example;

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

        /** 익명 객체 잘못된 사용 방법 **/
        Object object = new Object() {
            int max(int a, int b){
                return a > b ? a : b;
            }
        }; // Object에 max라는게 없기 때문에 object.max(3,5); 사용 불가

        /** 함수형 인터페이스 **/
//        MyFunction myFunction = new MyFunction() {
//            @Override
//            public int max(int a, int b) {
//                return a > b ? a: b;
//            }
//        };

        /** 함수형 인터페이스 + 람다식 **/
        MyFunction myFunction = (a, b) -> a > b ? a : b;
        // 함수형 인터페이스 자체가 추상 메서드가 하나밖에 없기 때문에 
        // 매개변수 및 리턴값에 뭐가 나올지 알고 있어서 생략이 가능하다

        System.out.println(myFunction.max(3,5)); // 5


    }

    interface MyFunction {
        public abstract int max(int a, int b);
    }

}

 

람다식하고 함수형 인터페이스는 짝궁이라고 생각하면 된다.

 

 

 

 

 

 

🔗 참고 및 출처

https://youngest-programming.tistory.com/476

https://ittrue.tistory.com/161

https://limkydev.tistory.com/226

반응형