반응형
public final class Optional<T> {

    private final T value;
    public static<T> Optional<T> empty() {
        @SuppressWarnings("unchecked")
        Optional<T> t = (Optional<T>) EMPTY;
        return t;
    }
    private Optional(T value) {
        this.value = value;
    }
    public static <T> Optional<T> of(T value) {
        return new Optional<>(Objects.requireNonNull(value));
    }
    @SuppressWarnings("unchecked")
    public static <T> Optional<T> ofNullable(T value) {
        return value == null ? (Optional<T>) EMPTY
                             : new Optional<>(value);
    }
    ......

Optional Class는 모든 개발자의 친구이며 원수인 NULL을 상대하기 위해 자바8 이후에 개발자들에게 주어진 무기쯤으로 생각하면 편하다.

 

기본적으로 Optional은 Wrapper Class로써, 모든 형태의 변수를 감싸고, 주어진 매소드들을 통해 Null 처리를 할 수 있도록 도와준다. 주어진 기능은 Stream과 어느정도 유사하여, 리스트가 1개인 Stream정도로 생각해도 좋다.

 

뭔지 모르겠을땐 일단 사용해보면서 알아보자.

 

예제 클래스

public class Department { //부서 클래스
    private String departmentName; // 부서명
    private int departmentNumber; // 부서번호
    private String responsibilities; // 담당업무
}

public class Employee { // 사원 클래스
    private Department department; // 부서
    private String employeeName; // 사원명
    private int employeeNumber; // 사원번호
}

public class Stock { // 물품 클래스
    private Employee usingEmployee; // 사용사원
    private int stockNumber; // 물품번호
    private String stockName; // 물품명
}

 

생성 (1) <T> Optional<T> of

public static <T> Optional<T> of(T value) {
        return new Optional<>(Objects.requireNonNull(value));
}

가장 기본적인 Optional 객체 생성 방법인 of이다.

사용시 주의해야 할 점은 바로 of는 NULL을 인자로 허용하지 않는 다는 점이다. 만약 of를 통해 optional 객체 생성시 null 일경우, NullPointException이 발생한다.

Department itDepartment = new Department("IT Department", "System maintenance", 101);

Optional<String> dptName = Optional.of(itDepartment.getDepartmentName());

System.out.println(dptName.get()); // RESULT String "IT Department"

/****************************************************************/

Department itDepartment = new Department(null, "System maintenance", 101);

Optional<String> dptName = Optional.of(itDepartment.getDepartmentName());

System.out.println(dptName.get()); // NullPointException

of를 통해 객체 생성을 할 경우는 해당 값이 무조건 Null이 아닐 경우에만 사용해야 한다. 만약 해당 값이 Null 가능성이 있다면 아래의 ofNullable을 사용하여 Optional객체를 생성해야 한다. 

 

생성 (2) <T> Optional<T> ofNullable

public static <T> Optional<T> ofNullable(T value) {
    return value == null ? (Optional<T>) EMPTY
                         : new Optional<>(value);
}

of 와 달리 만약 참조할 객체 또는 값이 null 가능성이 있을 경우, ofNullable을 사용하여 Optional 객체를 생성한다.

Department itDepartment = new Department("IT Department", "System maintenance", 101);
Optional<String> dptName = Optional.ofNullable(itDepartment.getDepartmentName());
System.out.println(dptName); //RESULT : Optional[IT Department]
System.out.println(dptName.get()); // RESULT : "IT Department"

뭐... 별차이 없어 보인다. 하지만 Intelij를 사용하면....

of를 사용할 때와는 다르게 경고줄이 표시되며 "Optional.get() without "isPresent" check" 라는 경고문이 나온다.

isPresent는 Optional에서 제공해 주는 메소드로 값이 존재할 경우 True null일경우 False를 반환해 주는 메소드이다.

if(dptName != null) { // 이 코드와 동일하다고 보면 된다.
    return true;
} else {
    return false;
}

 

즉 Null 가능성이 있으니 isPresent 등을 사용하여 Null Check를 하라는 말이다.(컴파일 에러는 아니다.)

 

반응형

이거 그래서 왜씀? 뭐임?

답은 하나다. 앞에 힘들게 정리해 왔던 funtion / consumer / stream ...등등... 처럼 lamda 식으로 간결하게 처리 하기 위함이다.

 

자 그럼 Optional에서 제공하는 여러가지 메소드 몇몇을 사용해서 기존 Null처리 방식과 어떤 차이가 있는지 예제를 통해 확인해보자

 

1. ifPresent(Consumer<? super T> action)

public void ifPresent(Consumer<? super T> action) {
    if (value != null) {
        action.accept(value);
    }
}

ifPresent는 Optional에서 제공하는 메소드 중 하나로, 만약 참조하는 인자 값이 null이 아닐 경우 주어진 Consumer Action을 실행하는 메소드 이다.

단순하게 부서 클래스로 부터 부서명을 받아 출력하는 코드를 보면

// 전통적인 방식
String dptName = itDepartment.getDepartmentName();
if(dptName != null)
    System.out.println(dptName);

이걸 Optional의 ifPresent를 사용하여 코드를 작성한다면...

Optional<String> dptName = Optional.ofNullable(itDepartment.getDepartmentName());
dptName.ifPresent(s -> System.out.println("test1 data exist"));

이렇게 lamda식으로 간결하게 표현이 가능하다.(그냥 이게 좋은거라고 가스라이팅 합시다.)

 

2. Optional<T> or(Supplier<? extends Optional<? extends T>> supplier)

or 메소드는 of나 ofNullable을 통해 객체를 생성하여 사용시, null 일경우 Default 객체 또는 Default 값을 넣어주기 위해 사용하는 메소드이다.

public Optional<T> or(Supplier<? extends Optional<? extends T>> supplier) {
    Objects.requireNonNull(supplier);
    if (isPresent()) {
        return this;
    } else {
        @SuppressWarnings("unchecked")
        Optional<T> r = (Optional<T>) supplier.get();
        return Objects.requireNonNull(r);
    }
}

구현된 내용을 보면 현제 Optional 객체의 값이 isPresent() 즉 존재하면 자기 자신을 return하고, 만약 값이 존재하지 않으면(Null일 경우) 인자로 받은 Supplier 인터페이스에 존재하는 Optional 객체를 반환한다.

 

3. T orElse(T other)

orElse또한 or과 동일하게 of나 ofNullable을 통해 객체를 생성하여 사용시, null 일경우 Default 객체 또는 Default 값을 넣어주기 위해 사용하는 메소드이다.

큰  차이점은 orElse는 Optional 객체를 반환하는게 아닌 값 그 자체를 반환한다는 것이다.

public T orElse(T other) {
    return value != null ? value : other;
}

or 메소드와 구현된 내용이 많이 다른걸 볼 수 있다. 단순하게 Optional의 Value값이 존재하면 해당 값을 return하고, null일경우 인자로 받은 값을 return 한다.

 

Optional<String> dptName = Optional.ofNullable(itDepartment.getDepartmentName());

Optional<String> hrDepartment = dptName.or(() -> Optional.of("HR Department"));
String hrDepartment = dptName.orElse("HR Department");

해당 코드를 통해 or과 orElse 반환값 차이를 확인 할 수 있다.

 

그 밖에도

2024.01.30 - [개발/Java] - Java 기초부터 다시 - interface Stream 에서 설명한 Stream의 Filter , Map 등의 기능도 사용 가능하다.

 

Optional 실 사용

앞서 예제로 설명한 부서(Department) / 사원(Employee)  / 물품(Stock) 클래스에서 물품을 입력하면 해당 물품을 사용하는 사원의 부서를 반환하는 함수를 만들어보자.

전통적인 방식대로 만든다면....

public String getDepartmentOfEmployeeFromStock(Stock stock) {
    if (stock != null) {
        Employee emp = stock.getUsingEmployee();
        if (emp != null) {
            Department department = emp.getDepartment();
            if (department != null) {
                String departmentName = department.getDepartmentName();
                if (departmentName != null) {
                    return departmentName;
                }
            }
        }
    }
    return "not exist department";
}

이렇게 하나하나 Null검사를 하면 Null에 안전하게 개발을 진행할것이다. PTSD 오지쥬?

Optional을 사용해서 조금 간편하게 구현을 다시하면...

public String getDepartmentOfEmployeeFromStock(Stock stock) {
    return Optional.ofNullable(stock)
            .map(s -> s.getUsingEmployee())
            .map(e -> e.getDepartment())
            .map(d -> d.getDepartmentName())
            .orElse("not exist department");
}

이렇게 표현이 가능하다. != null 로 부터 자유를 찾았다...

반응형
반응형

이전에 정리한 모든 자바 기본 함수형 인터페이스들(Function<T,R> , Consumer<T> ... 등등)은 사실 해당 Stream, Optional  인터페이스를 사용하기 위함이다.(개인적인 생각...)

 

그럼 먼저 자바8버전 이후의 핵심 Stream에 대해서 먼저 정리해 보자.

public interface Stream<T> extends BaseStream<T, Stream<T>> {

    Stream<T> filter(Predicate<? super T> predicate);

    <R> Stream<R> map(Function<? super T, ? extends R> mapper);
    
    
    ....

<stream에서 가장 많이 쓰이는 filter, map 추상 메소드>

 

Stream은 자바8 이전 for, foreach, sort 등을 사용하여 리스트 관리를 해온것을 좀더 간결하고 직관적(쉽게 말해 람다로 표현 하도록) 도와주는 인터페이스 이다.

 

예제

예제로 알아 보기전 예제로 사용할 기본적인 클래스를 먼저 정의 해보겠다.

public class Vehicle {
    public enum Type {
        SUV,
        SEDAN,
        TRUCK
    }

    private Type type;
    private String vendor;
    private String modelName;
    private String color;
    private int price;
}

Vehicle vehicle1 = new Vehicle(Vehicle.Type.SUV, "BMW", "L3", "white", 250000);
Vehicle vehicle2 = new Vehicle(Vehicle.Type.TRUCK, "HYUNDAI", "Q3", "black", 150000);
Vehicle vehicle3 = new Vehicle(Vehicle.Type.SEDAN, "HONDA", "W3", "white", 550000);
Vehicle vehicle4 = new Vehicle(Vehicle.Type.SUV, "BMW", "R3", "black", 23000);
Vehicle vehicle5 = new Vehicle(Vehicle.Type.SUV, "HYUNDAI", "N3", "blue", 125000);
Vehicle vehicle6 = new Vehicle(Vehicle.Type.TRUCK, "HYUNDAI", "V3", "blue", 951000);
Vehicle vehicle7 = new Vehicle(Vehicle.Type.SEDAN, "KIA", "Z3", "red", 123000);

vehicleList = Arrays.asList(vehicle1,vehicle2,vehicle3,vehicle4,vehicle5,vehicle6,vehicle7);

Collection Type(Collection, List , Set 등)을 Stream으로 사용하기 위해서는 자바8 이후 추가된 Default Method인 stream()을 사용하면 된다.

    default Stream<E> stream() {
        return StreamSupport.stream(spliterator(), false);
    }

 

 

 

1. <R> Stream<R> map(Function<? super T, ? extends R> mapper)

[Vehicle 리스트의 stream에서 map 매소드 호출 시 매개변수 및 반환값]

매개변수 : Function<Vehicle, R>

반환값 : Stream<R>

말로 풀어 보면 Vehicle객체받아 R을 반환하는 Function 인터페이스를 사용하여 반환된 R타입의 Stream을 반환 한다.

즉 Stream의 MAP은 기존의 Stream을 변경 및 새롭게 생성을 하는 역할을 한다.

List<String> vehicleColors_V1 = vehicleList.stream().map(new Function<Vehicle, String>() {
            @Override
            public String apply(Vehicle vehicle) {
                return vehicle.getColor();
            }
}).toList();
// Vehicle들의 Color 리스트

[vehicle stream을 각각의 vehicle들의 색상 stream(String type)으로 변경]

 

 

해당 표현방식은 Lamda로 표현이 가능하다.

List<String> vehicleColors = vehicleList.stream().map(vehicle1 -> vehicle1.getColor()).toList();
//또는
List<String> vehicleColors = vehicleList.stream().map(Vehicle::getColor).toList();

 

 

2.  Stream<T> filter(Predicate<? super T> predicate)

[Vehicle 리스트의 stream에서 filter매소드 호출 시 매개변수 및 반환값]

매개변수 : Predicate<Vehicle>

반환값 : Stream<Vehicle>

말로 풀어 보면 Vehicle객체를 받는 Predicate 인터페스를 통해 True/False를 판단하고, True인 Vehicle들의 Stream을 반환한다.

즉 Stream의 Filter는 기존의 Stream에서 Predicate를 사용하여 알맞는 객체들만 필터링 후, 자신과 동일한 형태의 Stream을 반환하는 역할을 한다.

List<Vehicle> blackVehicles = vehicleList.stream().filter(new Predicate<Vehicle>() {
    @Override
    public boolean test(Vehicle vehicle) {
        if (vehicle.getColor().equals("black")) return true;
        else return false;
    }
}).toList();

Map과 동일하게 해당 Lamda로 표현이 가능하다.

List<Vehicle> blackVehicles = vehicleList.stream().filter(v -> v.getColor().equals("black")).toList();

 

 

3. 마무리

기본적으로 Stream은 Map과 Filter만으로도 리스트를 다양하게 원하는 형태로 바꾸는 것이 가능하다.

//EXAMPLE
List<String> stringList2 = vehicleList2.stream().filter(v -> v.getColor().equals("blue")).map(v -> v.getVendor()).toList();

 

이 외에도 Stream은 flatMap, foreach, sorted등 다양한 기능을 제공한다. 필요에 따라서 구글링을 통해 자기가 필요한 기능을 찾아가면서 사용한다면 확실히 기존보다 편하게 리스트를 관리할 수 있다.

반응형
반응형

1. Consumer<T>

@FunctionalInterface
public interface Consumer<T> {

    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);
    .....

자바8 이후 부터 제공되는 기본 함수형 인터페이스 Consumer<T>은 T 타입의 인자를 받아 이름 그대로 특정 작업(Accept)을 수행 하는 함수형 인터페이스 이다.

 

예제

//Integer 값을 받아, 출력한는 인터페이스
Consumer<Integer> printT = new Consumer<Integer>() {
    @Override
    public void accept(Integer integer) {
        System.out.println("Consumer 는 리턴값이 없어요.>>>" + integer);
    }
};

//람다 표현
Consumer<Integer> printT = integer -> 
	System.out.println("Consumer 는 리턴값이 없어요.>>>" + integer);

 

특정 값을 인자로 받아 자유롭게 처리가 가능 하다.

 

사용 예제 (기타 Default 메소드)

public class SomethingConsumer implements Consumer<Integer> {
    @Override
    public void accept(Integer integer) {
        System.out.println("SomeThingConSumer Plus 10 >>>>" + integer + 10);
    }
    @Override
    public Consumer<Integer> andThen(Consumer<? super Integer> after) {
        return Consumer.super.andThen(after);
    }
}

andThen: 본체의 Apply를 적용 후, 인자로 받은 Funtion의 Apply 적용.

 

Function의 compose와 addThen처럼 실제 사용을 해보자.

SomethingConsumer plus10 = new SomethingConsumer();
Consumer<Integer> minus10 = (i) -> System.out.println("SomethingConsumer minus10 >>>> " + (i - 10));
Consumer<Integer> multiply2 = (i) -> System.out.println("SomethingConsumer Multiply *2  >>> " + (i * 2));
        
plus10.andThen(minus10.andThen(multiply2)).accept(30);

/**
RESULT
SomeThingConSumer Plus 10 >>>>40
SomethingConsumer minus10 >>>> 20
SomethingConsumer Multiply *2  >>> 60
**/

순서 대로 plus10의 accept > minus10의 accept > multiply2 의 accept가 실행되는 것을 확인할 수 있다.

 

 

2. Supplier<T>

@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

자바8 이후 부터 제공되는 기본 함수형 인터페이스 Suppplier<T>은 매개변수를 받지 않고 정의된 T 타입의 변수를 받환하는 추상 메소드 get()을 제공한다.

 

예제

Supplier<Integer> get10 = new Supplier<Integer>() {
            @Override
            public Integer get() {
                System.out.println("Supplier는 파라미터 없이 바로 넘겨요>>>>" + 10);
                return 10;
            }
};

Supplier<String> getString = () -> "Hello World";

System.out.println(get10.get()); // RESULT Integer 10
System.out.println(getString.get()); // RESULT "Hello World"

 

 

2. Predicate<T>

@FunctionalInterface
public interface Predicate<T> {

    /**
     * Evaluates this predicate on the given argument.
     *
     * @param t the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);
    ....

자바8 이후 부터 제공되는 기본 함수형 인터페이스 Predicate<T>은 매개변수 T를 받은 후, 전달된 인자를 사용하여 boolean(true, false)를 반환 하는 인터페이스를 제공 한다.

 

예제

Predicate<Integer> isEven = i -> i%2 == 0;
System.out.println(isEven.test(10)); // RESULT true
System.out.println(isEven.test(7)); // RESULT false
System.out.println(isEven.test(1)); // RESULT false
반응형
반응형
@FunctionalInterface
public interface Function<T, R> {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);
    ......

자바8 이후 부터 제공되는 기본 함수형 인터페이스 Funtion<T, R>은 T 타입의 인자를 받아 R타입의 인자를 반환하는 apply라는 추상 메소드를 제공한다.

 

사용 예제 (기본)

// Integer 타입의 수를 인자로 받아 +10 후, Integer 타입 반환
Function<Integer, Integer> plus10_v1 = new Function<Integer, Integer>() {
    @Override
    public Integer apply(Integer integer) {
        return integer + 10;
    }
};
//람다 표현1
Function<Integer, Integer> plus10_v2 = (i) -> i + 10;
//람다 표현2
Function<Integer, Integer> plus10_v3 = (i) -> {
	i = i + 10;
    return i;
};
plus10_v1.apply(10); // result Integer 20
plus10_v2.apply(10); // result Integer 20
plus10_v3.apply(10); // result Integer 20

이렇게 IN, OUT 인자 타입만 잘 맞춰 주면 자유롭게 사용이 가능하다.

반응형

사용 예제 (기타 Default 메소드)

Default Method란 자바8 이후 추가된 기능으로 기본 인터페이스에 추상 메소드가 추가 될 경우, 이를 상속받는 모든 Class에 해당 메소드를 구현해야 하는 이슈가 있었지만, Default 메소드는 메소드의 구현체를 제공 함으로써 기존의 추상메소드가 구현된 Class가 깨지지 않고 사용 가능하게 해주는 기능이다.

 

Default 메소드각 몇개가 있던 추상 메소드가 1개일 경우, FuntionalInterface로 간주 된다.

// 위에 Funtion 기능을 상속받아 구현한 클래스
public class Plus10 implements Function<Integer, Integer> {
    @Override
    public Integer apply(Integer integer) {
        return integer + 10;
    }

    @Override
    public <V> Function<V, Integer> compose(Function<? super V, ? extends Integer> before) {
        return Function.super.compose(before);
    }

    @Override
    public <V> Function<Integer, V> andThen(Function<? super Integer, ? extends V> after) {
        return Function.super.andThen(after);
    }
}

Funtion<T, R>에서 제공되는 Default 메소드는 compose , andThen 2가지의 Default 메소드를 제공한다.

 

compose : 인자로 받은 Funtion 의 Apply를 먼저 적용 후, 본체의 Apply 적용.

andThen: 본체의 Apply를 적용 후, 인자로 받은 Funtion의 Apply 적용.

 

이렇게만 보면... 뭔말인지 모르겠으니 예제 코드로 확인해 보자.

Plus10 plus10 = new Plus10();
// 받은 인자의 x2를 하는 Funtion
Function<Integer, Integer> multiply = integer -> integer * 2;

System.out.println(plus10.apply(10)); // result : 20
System.out.println(plus10.compose(multiply).apply(20)); // result (20 * 2) + 10 = 50
System.out.println(plus10.andThen(multiply).apply(2)); // result (2 + 10) * 2 = 24

 

첫번째 출력 값 : 인자로 받은 10에 +10을 한 값을 반환.

두번째 출력 값 : 인자로 받은 20을 Compose의 인자인 multiply의 apply(x2)를 먼저 적용 후, 본체의 apply(+10)을 적용.

세번째 출력 값 : 인자로 받은 2를 본체의 apply(+10) 을 먼저 적용 후, addThen의 인자인 multiply의 apply(x2)를 적용.

반응형
반응형

FunctionalInterface란?

함수형인터페이스(Functional Interface)는 단 하나의 추상 메소드만 가지는 인터페이스를 말한다. 다른 말로 SAM 인터페이스(Single Abstract Method Interface)라고도 한다.

 

예시

@FunctionalInterface
public interface RunSomething {
    void doIt();
    //이렇게 추상 메소드가 2개 있으면 안됨. 오류 발생
    //void doItAgain();
}

 

 

 

그래서... 이게 다인가? 왜쓰는거임?

여러 가지 이유가 있겠지만... 이유는 단하나 Lamda(함수형 프로그래밍)을 위해서 라고 생각한다.

예제를 보며 한번 확인해보자.

 

        //기존에 쓰던 방식
RunSomething runSomething = new RunSomething() {
    @Override
    public void doIt() {
        System.out.println("ki hy");
    }
};
// 람다 방식1 한줄일 경우 이렇게 쓸수 있음.
RunSomething runSomething2 = () -> System.out.println("ki hy");

// 람다 방식2 한줄 넘을 경우 저렇게 중괄호로 묶어 줌.
RunSomething runSomething3 = () -> {
     System.out.println("ki hy");
     System.out.println("ki hy2");
};
runSomething.doIt();
runSomething2.doIt();
runSomething3.doIt();

위 코드 처럼 정의된 함수형 인터페이스는 이렇게 람다식으로 표현이 가능 하다.

 

만약 인자(Arguments)가 있는 경우라면?

@FunctionalInterface
public interface RunSomeThingArguments<T,R> {
    R doSomething(T a, T b);
}


RunSomeThingArguments<Integer,String> runSomeThingArguments = new RunSomeThingArguments<Integer, String>() {
    @Override
    public String doSomething(Integer a, Integer b) {
        return Integer.toString(a+b);
    }
} ;
        
RunSomeThingArguments<Integer,String> runSomeThingArguments2 = (a, b) -> Integer.toString(a+b);
RunSomeThingArguments<Integer,String> runSomeThingArguments3 = (a, b) -> {
    //각 인자에 2배를 곱해서 더하는 경우.
    a = a * 2;
    b = b * 2;
    return Integer.toString(a + b);
};
runSomeThingArguments.doSomething(1,3); // result : String type "4"
runSomeThingArguments2.doSomething(1,3); // result : String type "4"
runSomeThingArguments3.doSomething(1,3); // result : String type "12"

 

이런식으로 사용할 수 있겠다.

 

사실 개념 자체는 간단하지만... 글쓴이도 처음 배웠을때 왜 쓰는지 언제 써야할지에 대해서 감이 안잡혔다.

결국 이러한 개념은 Java8 이후 도입된 기본 함수형 인터페이스를 사용하기 위함이라는 것을 깨달았다.

 

기본 함수형 인터페이스

public interface Function<T, R> 

public interface Consumer<T> 

public interface Supplier<T>

public interface Predicate<T>

해당 기본 함수형 인터페이스에 대해서 하나씩 알아보자.

반응형

+ Recent posts