JDK1.8(8) 버전 이후 바뀐 java 언어의 발전과 적용 방안에 대해 공부한 내용을 기록한다.
Java의 객체 지향 프로그래밍에서 진화하여, 코드의 재사용성/유지보수/신뢰성을 더욱 높일 수 있는 방안으로 나타난 프로그래밍 방법이다.
순수함수처럼, 오로지 입력값으로 결과값을 산출하는 일방향적인 기능을 만들어내는 방법을 함수형 프로그래밍이라 한다.
내부적으로 다른(예측할 수 없는) 데이터 변경 등의 작업없이 프로그래밍이 이루어진다.
import java.util.ArrayList;
import java.util.List;
public class LambdaAndStream {
public static void main(String[] args) {
ArrayList<Car> carsWantToPark = new ArrayList<>();
ArrayList<Car> parkingLot = new ArrayList<>();
Car car1 = new Car("Benz", "Class E", true, 0);
Car car2 = new Car("BMW", "Series 7", false, 100);
Car car3 = new Car("BMW", "X9", false, 0);
Car car4 = new Car("Audi", "A7", true, 0);
Car car5 = new Car("Hyundai", "Ionic 6", false, 10000);
carsWantToPark.add(car1);
carsWantToPark.add(car2);
carsWantToPark.add(car3);
carsWantToPark.add(car4);
carsWantToPark.add(car5);
parkingLot.addAll(parkingCarWithTicket(carsWantToPark));
parkingLot.addAll(parkingCarWithMoney(carsWantToPark));
for (Car car : parkingLot) {
System.out.println("Parked Car : " + car.getCompany() + "-" + car.getModel());
}
}
public static List<Car> parkingCarWithTicket(List<Car> carsWantToPark) {
ArrayList<Car> cars = new ArrayList<>();
for (Car car : carsWantToPark) {
if (car.hasParkingTicket()) {
cars.add(car);
}
}
return cars;
}
public static List<Car> parkingCarWithMoney(List<Car> carsWantToPark) {
ArrayList<Car> cars = new ArrayList<>();
for (Car car : carsWantToPark) {
if (!car.hasParkingTicket() && car.getParkingMoney() > 1000) {
cars.add(car);
}
}
return cars;
}
}
class Car {
private final String company; // 자동차 회사
private final String model; // 자동차 모델
private final boolean hasParkingTicket;
private final int parkingMoney;
public Car(String company, String model, boolean hasParkingTicket, int parkingMoney) {
this.company = company;
this.model = model;
this.hasParkingTicket = hasParkingTicket;
this.parkingMoney = parkingMoney;
}
public String getCompany() {
return company;
}
public String getModel() {
return model;
}
public boolean hasParkingTicket() {
return hasParkingTicket;
}
public int getParkingMoney() {
return parkingMoney;
}
}
위 로직에서 parkingLot에 add하는 부분은 Ticket이 있는지(parkingCarWithTicket)와 주차요금이 있는지(parkingCarWithMoney) 검증하는 로직을 먼저 진행한다.
하지만 위 두 검증로직은 인수, 검증과정, 반환값이 매우 비슷하고 중복되는 부분이 많기에 이를 하나로 통합할 수 없을까라는 생각을 할 수 있다.
이때 함수형 인터페이스(하나의 추상 메서드)를 생성하여 위 검증 로직을 좀 더 간결하게 작성할 수 있다(아이디어는 검증 로직을 하나의 함수로 전달받아 처리한다).
public static List<Car> vaildateCar(ArrayList<Car> carList, Predicate<Car> validateFunc){
//내부 주요 검증 로직을 함수로 전달받는다.
ArrayList<Car> resultList = new ArrayList<>();
for(Car car : carList){
//함수를 전달받아 검증 로직을 간결하게 구성
//if(validateFunc.test(car))
if(validateFunc.validate(car)){
resultList.add(car);
}
}
return resultList;
}
interface Predicate<T>{
//Type : Car
//해당 함수를 인자로 전달하기 위한 인터페이스, 함수형 인터페이스는 구현 메서드가 하나이므로 명확한 목적으로 인터페이스를 활용할 수 있게 된다.
boolean validate(T t);
}
관심사분리에 따라, 해당 검증로직을 Car 객체 안에서 구성하도록 설정한다.
public Car(String company, String model, boolean hasParkingTicket, int parkingMoney) {
this.company = company;
this.model = model;
this.hasParkingTicket = hasParkingTicket;
this.parkingMoney = parkingMoney;
}
public String getCompany() {
return company;
}
public String getModel() {
return model;
}
public boolean hasParkingTicket() {
return hasParkingTicket;
}
public int getParkingMoney() {
return parkingMoney;
}
public static boolean hasTicket(Car car){
return car.hasParkingTicket();
}
public static boolean noTicketButMoney(Car car){
return !car.hasParkingTicket() && car.getParkingMoney() > 1000;
}
위 함수형 프로그래밍을 적용한 이후에는 메인 로직에서 검증로직을 아래와 같이 간결하게 바꿀 수 있다.
public static void main(String[] args) {
ArrayList<Car> carsWantToPark = new ArrayList<>();
ArrayList<Car> parkingLot = new ArrayList<>();
Car car1 = new Car("Benz", "Class E", true, 0);
Car car2 = new Car("BMW", "Series 7", false, 100);
Car car3 = new Car("BMW", "X9", false, 0);
Car car4 = new Car("Audi", "A7", true, 0);
Car car5 = new Car("Hyundai", "Ionic 6", false, 10000);
carsWantToPark.add(car1);
carsWantToPark.add(car2);
carsWantToPark.add(car3);
carsWantToPark.add(car4);
carsWantToPark.add(car5);
//Predicate : 해당 객체의 메소드를 추출
parkingLot.addAll(carsWantToPark, Car::hasTicket());
parkingLot.addAll(carsWantToPark, Car::noTicketButMoney());
....
이름이 없는 함수이기에, 오로지 목적 달성을 위해 사용하는 임시적인 객체이다. 그 자체로 인수 및 하나의 값으로 활용할 수 있기에 일급 객체로 취급할 수 있다.
위 검증 로직에서 익명함수를 사용하여, 별도의 Car 메서드를 만들지 않고 즉각적으로 검증 로직을 구현할 수 있다.
별도의 로직없이 return만 있는 함수의 경우, return 문도 생략하고 바로 검증로직을 작성할 수 있다.
parkingLot.addAll(carsWantToPark, (Car car) -> return car.hasParkingTicket() && car.getParkingMoney > 1000);
데이터를 기억하지 않고(원본 데이터를 저장하지 않고, 각 단계마다 부분적인 데이터를 전달받고 새로운 흐름(리스트)를 추출한다는 개념), 단계단계마다 내부적인 연산을 통해 필요한 부분을 추출하고 병렬적인 처리를 지원하는 등 컬렉션에 비해 더 효율적인 처리를 할 수 있도록 기능을 제공해주는 인터페이스이다.
자료구조를 추상화하였기에 자료구조에 상관없이 적용할 수 있고, 또한 스트림은 데이터의 한 시점에서 처리하는데 집중하기에, 최종적인 결과를 저장하는 것은 연산 종료 시점에 진행한다(소비(consume)).
NullPointerException을 방지할 수 있는 방안으로, isEmpty()라는 하나의 객체를 전달하여 Null을 전달하여 발생하는 오류를 방지할 수 있는 인터페이스이자 하나의 형태이다.
쉽게 말하면 Null을 참조하여 발생하는 예외를 미연에 방지하고자 Null이 아닌 하나의 객체를 전달하기 위해 만든 도구이다.
if(userId != null){
logic with userId
}
이를 아래와 같이 발전할 수 있고
SomeObjectNullableReturn isSuccess = findUserIdByUserName(userId);
if(isSuccess)
logic with userId
...
public SomeObjectNullableReturn findUserIdByUserName(String userName){
if(data != null) return new SomeObjectNullableReturn(data, true);
esle return new SomeObjectNullableReturn(null, false);
}
이를 일반화한 개념이 바로 Optional이다.
//carName이 null일 경우 NoCar를 반환받는다.
Optinal<String> carName = carName.orElse("NoCar");