[JAVA] Java8, Lamda, Stream

Nam_JU·2022년 8월 20일
0

Java

목록 보기
4/4

Java8

  • 2014년 3월에 정식으로 릴리즈 되었다
  • 인터페이스 디폴트 메서드
  • 람다식 Lamda 을 통한 함수형 프로그래밍
  • 스트림 Stream API 추가

  • 인터페이스의 주요 사용 목적은 구현체를 통일화된 명세로 정의하는것
  • 다수의 기능을 정의한 특정 인터페이스를 구현한 구현 클래스들은 해당 인터페이스를 통해 구현 방법에 상관없이 사용이 가능하다.
  • 이런 인터페이스의 단점은 배포된 이후 수정이 어렵다

함수형 프로그래밍의 이해

함수형 프로그래밍에는 명령형과 선언형 프로그래밍으로 나눠진다

  • 명령형 프로그래밍 Imperative
    명령형 프로그래밍은 특정 기능을 수행하기 위해 어떻게 집중하는 방식을 의미
  • 선언형 프로그래밍 Declarative
    선언형 프로그래밍은 특정 기능을 수행하기 위해 무엇에 집중하는 방식을 의미

함수형 프로그래밍은 선언형 프로그래밍을 따르는 대표적인 프로그래밍 패러다임이다

  • 함수형 프로그래밍은 함수들의 집합으로 프로그램을 구성하는 것을 의미한다
  • 자바에 함수형 프로그래밍의 함수는 순수함수, 일급객체, 불변의 자료구조 혹은 영속 자료구조등과 같은 특성을 갖는다.

    first-class citizen(object) 일급객체

    1. 익명의 literal로 생성가능하다 = 동적으로 생성이 가능하다
    2. 객체가 변수나 자료구조에 저장이 가능
    3. 일급객체를 다른 함수의 인자로 전달이 가능하다
    4. 함수의 리턴값으로 객체를 사용할 수 있다
    • 자바스크립트 함수는 일급객체
    • callback function → 고차함수

실습코드

지역, 성별을 검색해서 유저를 조회할수 있는 프로그래밍

Step1. 각각의 검색 메소드를 생성

  • Customer
public class Customer {
    private String id;
    private String location;
    private Gender gender;
    private int age;

    public Customer(String id, String location, Gender gender, int age) {
        this.id = id;
        this.location = location;
        this.gender = gender;
        this.age = age;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getLocation() {
        return location;
    }

    public void setLocation(String location) {
        this.location = location;
    }

    public Gender getGender() {
        return gender;
    }

    public void setGender(Gender gender) {
        this.gender = gender;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Customer{" +
                "id='" + id + '\'' +
                ", location='" + location + '\'' +
                ", gender=" + gender +
                ", age=" + age +
                '}';
    }
}
  • CusomerService
import java.util.ArrayList;
import java.util.List;

public class CustomerService {

    private List<Customer> customers;

    public CustomerService() {
        this.customers = new ArrayList<>();
    }
    // 손님 추가
    public void addCustomer(Customer newCustomer){
        this.customers.add(newCustomer);
    }

    // 지역으로 검색
    public List<Customer> searchCustomerByLocation(String location){
        List<Customer> foundCustomers = new ArrayList<>();
        //손님들을 꺼내서 같은지 확인 후 같으면 추가
        for (Customer customer : customers){
            if (customer.getLocation().equals(location)){
                foundCustomers.add(customer);
            }
        }
        return foundCustomers;
    }

    //성별로 검색
    public List<Customer> searchCustomerByGender(Gender gender){
        List<Customer> foundCustomers = new ArrayList<>();
        for (Customer customer : customers){
            if (customer.getGender().equals(gender)){
                foundCustomers.add(customer);
            }
        }
        return foundCustomers;
    }

}
  • FunctionalAssist
import java.util.List;

public class FunctionalAssist {

    public static void main(String[] args) {
        CustomerService service = new CustomerService();
        initData(service);

        //지역으로 검색
        List<Customer> result = service.searchCustomerByLocation("Seoul");
        for (Customer customer: result){
            System.out.println(customer);
        }

        //성별로 검색

    }

    private static void initData(CustomerService service){
        service.addCustomer(new Customer("CUS01", "Seoul", Gender.Male, 30));
        service.addCustomer(new Customer("CUS02", "Busan", Gender.Male, 30));
        service.addCustomer(new Customer("CUS03", "Busan", Gender.Male, 30));
        service.addCustomer(new Customer("CUS04", "Seoul", Gender.Male, 30));
        service.addCustomer(new Customer("CUS05", "Seoul", Gender.Male, 30));
        service.addCustomer(new Customer("CUS06", "Gwangju", Gender.Male, 30));
    }
}

검색조건에 따라 서비스에서 메소드를 계속 만드는것은 비효율 적이다. 모든 조건을 충족 시킬 수 있는 효율적인 코드는 무엇일까?

Step2. 하나의 메소드로 검색 조건을 생성

EnumClass를 사용하여 검색조건과 검색 값을 받아 처리한다

                                                         location        seoul
    //하나의 메소드로 처리하는 방법                                  검색조건 ,       검색값
    public List<Customer> searchCustomersBy(SearchOption searchOption, String searchValue){
        List<Customer> foundCustomers = new ArrayList<>();

        for (Customer customer : customers){
            //지역검색
           if (searchOption.equals(SearchOption.Location)) {
               if (customer.getLocation().equals(searchValue)) {
                   foundCustomers.add(customer);
               }
               //성별 검색
           }else if (searchOption.equals(SearchOption.Gender)) {
               if (customer.getGender().name().equals(searchValue)) {
                   foundCustomers.add(customer);
               }
           }
        }
        return foundCustomers;
    }

Step3. 인터페이스를 사용하여 익명 클래스를 생성

  • SearchFilter
public interface SearchFilter {
    boolean isMatched(Customer customer);
}
  • CustomerService
package step2;

import java.util.ArrayList;
import java.util.List;

public class CustomerService {

    private List<Customer> customers;

    public CustomerService() {
        this.customers = new ArrayList<>();
    }
    // 손님 추가
    public void addCustomer(Customer newCustomer){
        this.customers.add(newCustomer);
    }
    

    //인터페이스를 통한 검색 메소드
    public List<Customer> searchCustomers(SearchFilter filter){
        List<Customer> foundCustomers = new ArrayList<>();
        for (Customer customer: customers){
            if (filter.isMatched(customer)){
                foundCustomers.add(customer);
            }
        }
        return foundCustomers;
    }
}
  • FunctionalAssist
package step2;

import java.util.ArrayList;
import java.util.List;

public class FunctionalAssist {

    public static void main(String[] args) {
        CustomerService service = new CustomerService();
        initData(service);
        List<Customer> result = new ArrayList<>();

        result = service.searchCustomers(new SearchFilter() {
            @Override
            public boolean isMatched(Customer customer) {
                if (customer.getLocation().equals("Seoul")){
                    return true;
                }else{
                    return false;
                }
            }
        });

        for (Customer customer : result){
            System.out.println(customer);
        }

    }

    private static void initData(CustomerService service){
        service.addCustomer(new Customer("CUS01", "Seoul", Gender.Male, 30));
        service.addCustomer(new Customer("CUS02", "Busan", Gender.Male, 30));
        service.addCustomer(new Customer("CUS03", "Busan", Gender.Male, 30));
        service.addCustomer(new Customer("CUS04", "Seoul", Gender.Male, 30));
        service.addCustomer(new Customer("CUS05", "Seoul", Gender.Male, 30));
        service.addCustomer(new Customer("CUS06", "Gwangju", Gender.Male, 30));
    }
}

중첩 클래스 Nested Class

  • Static Class
    static이 붙었을 경우 중첩 클래스라고 한다
  • Non-Static Class (InnerClasses)
    static이 붙지 않았을 경우 멤버 클래스, 로컬 클래스, 익명 클래스 3가지로 나뉘어진다


중첩 클래스나 중첩 인터페이스의 특징

  • 중첩 클래스(nested class)나 중첩 인터페이스(nested interface)는 외부로부터 스스로를 감추며, 외부 클래스와 밀접한 관계를 가짐.
  • 중첩 클래스는 외부 클래스(outer class)의 private 멤버를 비롯해 모든 멤버에게 자유롭게 접근 가능
  • 따라서 일반적으로 프로그램을 유지, 보수하기가 쉽고 프로그램이 간단해지지만 중첩 클래스가 복잡할 경우 가독성 저하

내부 클래스 InnerCalss

내부 클래스는 클래스 내에 선언된 클래스이다. 클래스에 다른 클래스를 선언하는 이유는 두 클래스가 서로 긴밀한 관계가 있기 때문
이다. 한 클래스를 다른 클래스의 내부 클래스로 선언하면 두 클래스의 멤버들 간에 서로 쉽게 접근할 수 있다는 장점이 있다.

  • 내부 클래스에서 외부 클래스의 멤버들을 쉽게 접근 할 수 있다.
  • 코드의 복잡성을 줄일 수 있다. (캡슐화)

익명 중첩 클래스

  • 클래스의 선언과 객체의 생성이 동시에 이루어진다
  • 한번만 사용되고 하나의 객체만 생성하는 일회용 클래스
  • java8에서는 람다Lamda식으로 대체 가능하다

Lambda 람다

  • Java8에서 가장 중요한 변화는 람다 표현식이다
  • 람다 표현식은 메소드로 전달 할 수 있는 익명 함수를 단순화한 코드의 블록이다
  • 람다 표현식은 특정 클래스에 종속되지 않으며 함수 이름으로 명명한다
  • 람다 표현식은 함수 자체를 전달인자로 보내거나 변수에 저장하는 것이 가능하다

람다 표현식의 활용

  • 람다 표현식은 익명 구현 클래스를 생성하고 객체화 한다
  • 익명 구현 클래스로 생성된 람다 표현식은 인터페이스로 대입 가능하며 이 인터페이스를 함수형 인터페이스라고 한다
  • 하나의 추상 메소드를 갖는 인터페이스는 모두 함수형 인터페이스가 될 수 있다
  • 다수의 디폴트 메소드를 갖는 인터페이스라도 추상 메소드가 하나라면 함수형 인터페이스이다
  • 함수형 인터페이스를 정의할 때 @FunctionalInterface 어노테이션을 이용해 컴파일 검사를 진행할 수 있다.
  • 함수형 인터페이스의 추상메소드 시그니처를 함수 디스크립터 function descriptor라고 한다

Stream API

  • Java8에 추가된 streamAPI를 활용하면 다양한 데이터 소스를 표준화된 방법으로 다룰 수 있다
  • 따라서 Collection F/W를 통해 관리하는 데이터를 처리하기 위해 주로 사용한다
  • Stream API 활용을 통해 수집된 다양한 데이터를 활용하는데 있어서 간결하고 가독성 있는 처리가 가능하다
  • Stream API의 다양한 기능들은 대부분 람다를 필요로 하기 때문에 람다를 이해하고 사용할 수 있어야 한다

  • 사용 전
  • 사용 후

Stream 객체 생성

  • Stream 객체를 생성하는 방법은 Collection 객체를 통한 방법과 스트림 빌더를 통한 방식 두가지가 있다.
  • Collection 인터페이스는 stream()메소드를 default 메소드로 정의하고 있다
  • 이 메소드는 해당 컬렉션이 가지고 있는 항목들에 대해 스트림 처리가 가능한 Stream 객체를 반환한다
  • 한번 생성한 스트림은 사용 후 다시 사용할 수 없으며 전체 데이터에 대한 처리가 이루어지면 종료된다



Collection 객체를 통한 생성

  • 처리할 데이터가 존재하고 이를 처리하기 위한 일반적인 생성방식

Stream.Builder를 이용한 Stream 객체 생성

  • 직접 데이터를 추가 삭제 해야할때
  • 스트림 자체적으로 데이터를 생성하고 처리할 수 있다.
  • Stream.Builder 인터페이스는 Consumer 인터페이스를 상속하고 있으며 데이터를 추가하는 accept(), add() 메소드와 데이터의 추가 작업을 완료하고 Stream을 반환하는 build()메소드를 정으하고 있다

Stream 연산

  • 스트림을 이용한 연산은 각 연산의 연결을 통해 파이프라인을 구성할 수 있다.
  • 파이프라인을 구성할 수 있다는 것은 스트림 대상 데이터에대한 다양한 연산을 조합할 수 있다.
  • 스트림을 이용한 연산 처리는 스트림 객체의 생성부터 중간 연산, 그리고 최종 연산 단계로 구분 할 수 있다.
  • 스트림 객체가 제공하는 다양한 연산을 이해하고 연산에 필요한 람다 표현식을 이해하고 적용하는게 중요하다

중간 연산 intermediate operation

  • filter, map과 같은 연산으로 Stream을 반환
  • 중간 연산은 연속해서 호출하는 메소드 체이닝 Method Chanining으로 구현 가능하다
  • 최종연산이 실행되어야 중간 연산이 처리되므로 중간 연산들로만 구성된 메소드 체인은 실행되지 않는다.

최종 연산 terminal operation

  • forEach, collect와 같은 연산으로 void를 반환하거나 컬렉션 타입을 반환한다
  • 중간 연산을 통해 가공된 스트림은 마지막으로 최종연산을 통해 각 요소를 소모하여 결과를 출력한다
  • 지연Lazy 되었던 모든 중간 연산들이 최종연산시 모두 수행되는 것이다
  • 최종 연산 후에는 한번 생성해서 소모한 스트림은 닫히게 되고 재사용이 불가능하다


Filter 필터링

  • 필터링은 전체 데이터에서 불필요한 데이터를 없애고 원하는 데이터를 정확하게 추출하기 위한 과정이다
  • Stream API의 filter(), distinct()와 같은 메소드를 이용해 데이터 추출이나 중복제거를 구현한다
  • 필터링 연산은 스트림의 중간연산으로 필터링 결과는 stream 객체로 반환하며 연산 완료를 위한 최종연산이 필요하다

Sorted 정렬

  • StreamAPI의 sorted()메소드는 특정 조건에 따라 데이터를 정렬하고 다시 stream으로 반환한다
  • sorted()를 이용한 정렬을 위해서는 반드시 대상 객체들이 Comparable 인터페이스를 구현한 클래스, 비교 가능한 객체여야 한다.
  • 조건을 추가할경우 sorted(Comparator.comparing(Customer::getName)) 사용

Stream Map

  • 스트림의 매핑(map) 연산은 스트림이 관리하는 데이터를 다른 형태의 데이터로 변환할 수 있도록 한다
  • 매핑 연산의 메소드는 map(), mapToInt(), mapToDouble(), mapToLong()이 있다.
  • 주로 사용하는 메소드는 map() 메소드이며 파라미터는 Function 함수형 인터페이스이다
  • duble, int, long 기본형 데이터 타입의 데이터를 처리하기 위한 메소드들은 매핑된 값의 결과가 기본형 데이터 타입일 경우 적용하여 사용한다
    • (중간 연산)filter, sort는 Stream객체로 반환됨
      이때의 타입은 List의 <> 타입
    • lsit<>의 다른 타입으로 반환하고 싶을때 사용하는게 map


참고자료
https://butter-shower.tistory.com/86
https://blog.itthis.kr/56
https://youtu.be/7cVPlgyx3d8

profile
개발기록

0개의 댓글