📝익명 클래스
프로그램에서 일시적으로 한번만 사용되고 버려지는 객체입니다.
일시적으로 사용된다는 것은 재사용이 되지 않거나 그럴 필요가 없다는 것인데 이건 확장성이 좋지 않다는 걸 의미합니다.
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");
}
}
함수형 인터페이스 장점
- 같은 Input에 같은 Ouput을 제공하고 변경 개념이 아니라 복사되고 복사 된 것을 함수를 거쳐 결과를 내기 때문에 멀티스레드 공유자원에 안전하다
- 코드를 간결하고 가독성 있게 작성할 수 있다.
함수형 인터페이스 단점
- 익명 함수의 재사용이 불가능
- 재귀함수의 구현이 어렵다.
- 재사용이 불가능하므로 비슷한 함수 구현이 잦을 수 있다.
- 디버깅이 어렵다.
Java에서 제공하는 많이 쓰는 함수형 인터페이스
- Runnable → 파라미터 없고 리턴값도 없는 경우 / 추상메소드 = run()
- Consumer<T> → 파라미터 있고 리턴값 없는 경우 / 추상메소드 = accept(T t)
- Supplier<T> → 파라미터 없고 리턴값 있는 경우 / 추상메소드 = get()
- Function<T,R> → 파라미터 있고 리턴값 있는 경우 / 추상메소드 = apply(T t) // T파라미터, R 반환값
- Operator<T> → 파라미터 있고 리턴값 있는 경우 / 연산용도, 임의의 타입 전달하고 임의의 타입 반환
- 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);
}
- 함수반환 타입을 생략할 수 있다.
- 함수명을 생략할 수 있다.
- 인자 타입을 생략할 수 있다.
- 함수내용인 중괄호 내용이 한줄이면 중괄호는 생략이 가능하다
- 함수형 인터페이스 + 람다식 이용해보기
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
'[Java] > [Java]' 카테고리의 다른 글
[Java] Optional (of, ofNullable, empty(), orElse, orElseGet, orElseThrow, ifPresent) [orElse와 orElseGet의 차이] (0) | 2023.08.08 |
---|---|
[Java] Double vs BigDecimal (정확한 소수점 계산 방법) (0) | 2023.08.08 |
[Java] 자바 멀티스레드 [Multi-Thread] 스레드풀 (Thread Pool) (0) | 2022.09.23 |
[Java] 자바 멀티스레드 [Multi-Thread] 스레드 그룹 (0) | 2022.09.23 |
[Java] 자바 멀티스레드 [Multi-Thread] Runnable vs Thread (0) | 2022.09.22 |