[TIL] 코드스테이츠 SEB BE Day 17

💡 Today I Will Learn

  • Enum
  • Annotation
  • Lambda
  • Stream

✏️ Summary


Enum (열거형)


서로 관련있는 ‘상수들’의 집합이다.

enum Seasons{ SPRING, SUMMER, FALL, WINTER}

public class Test{
  public static void main(String[] args){
    Seasons season = Seasons.SUMMER;
  }
}

관례적으로 대문자로 작성하며, 열거 상수 하나하나도 객체이다.

또한 정적(static)변수를 참조하는 것과 동일하게 Enum명.상수명 으로 참조 가능하다.

특정 개수의 상수로 사용하는 타입이므로 swtich문으로 활용 가능하다.

  • Enum객체 메서드
메서드설명
String name()열거 객체가 가진 문자열 반환.
열거타입을 정의할 때 사용한 상수명
int ordinal()처음 순번부터 반환
int compareTo(비교값)매개변수와 비교하여 순번 차이를 반환
열거타입 valueOf(String name)매개변수의 열거 객체를 반환
열거배열 values()열거 객체를 배열로 반환

public static final의 문제


public static final SPRING=1; // 계절

public static final SPRING=1; // 프레임워크
  • 문제1

연관성이 없는 두 상수가 중복되는 경우가 있으면 컴파일 에러가 발생한다.

interface Season{
  int SPRING=1;
  // public static final
}

interface Framework{
  int SPRING=1;
}
  • 문제2

문제1은 해결할 수 있다. 하지만 해당 값들은 순서를 위해 붙여진 숫자이지만, 순서를 비교하는 의미없는 코드 작성이 가능하다.

class Season{
  public static final Season SPRING = new Season();
}

class Framework{
  public static final Framework SPRING = new Framework();
}
  • 문제3

문제2를 해결할 수 있다. 각 상수를 인스턴스화 했기 때문에 비교할 수 없다. 하지만 switch 문에서는 사용할 수 없다.
switch문은 “char, byte, short, int 그리고 이들의 Wrapper클래스와 String, enum” 타입만 사용이 가능하기 때문이다.

  • enum의 장점
    1. 상수명 중복 방지
    2. 타입 안정성
    3. 코드가 간결해진다.
    4. swtich문에 사용 가능

Annotation


주석과 같이 프로그래밍 언어에 영향을 주지 않으며 정보 제공의 역할을 한다. 다만 주석은 읽는 사람에게, 어노테이션은 프로그램에게 제공한다는 차이가 있다.

  • 용도

    • 컴파일러에게 문법 에러를 체크하도록 정보제공
    • 프로그램을 빌드할 때 코드를 자동으로 생성할 수 있도록 정보제공
    • 런타임에 특정 기능을 실행하도록 정보제공
  • 종류

표준 어노테이션설명
@Override컴파일러에게 오버라이딩을 알림
@Deprecated사용하지 않을 대상을 알림
@FunctionalInterface함수형 인터페이스임을 알림
@SuppressWarning컴파일러 경고를 나타내지 않음

자바에서 기본적으로 제공하는 어노테이션.

메타 어노테이션설명
@Target적용 대상을 지정
@Documentedjavadoc으로 작성된 문서에 포함
@Inherited하위 클래스에 상속되도록 함
@Retention유지되는 기간을 설정
@Repeatable반복해서 적용할 수 있게 함

사용자 정의 어노테이션.

표준 어노테이션


  • @Override

상위클래스의 메서드를 오버라이딩함을 컴파일러에게 알린다.
상위클래스에 해당 메서드가 없다면 에러를 발생시킨다.

@Override를 붙이지 않고 사용된다면, 오타로 인해 새로운 메서드가 생성될 것이며 생각했던 로직이 수행되지 않을 것이다.

  • @Deprecated

새로운 기능이 추가되었을 때, 더 이상 사용하지 않게되는 필드나 메서드에 선언한다.

사용하게 된다면 컴파일 시 특정 메시지가 출력된다.

  • @SuppressWarnings

컴파일 경고를 무시한다. 보통 인지하고 있는 경고를 무시하고 넘어갈 때 사용한다.

@SuppressWarings("무시할 경고메시지")
  • @FunctionalInterface

함수형 인터페이스가 올바르게 선언되었는지 확인한다.

예를 들면 함수형 인터페이스는 하나의 추상메서드만 존재해야하는데, 2개가 존재할때 에러를 발생시킨다.

@FunctionalInterface
public interface FuncInter{
  public abstract void method();
}

메타 어노테이션


어노테이션에 붙이는 어노테이션이다.

  • @Target

어노테이션의 적용범위를 설정한다.

타입적용범위
ANNOTATION_TYPE어노테이션
CONSTRUCTOR생성자
FIELD필드(멤버변수, 열거상수)
LOCAL_VARIABLE지역변수
METHOD메서드
PACKAGE패키지
PARAMETER매개변수
TYPE타입(클래스, 인터페이스, 열거형
TYPE_PARAMETER타입 매개변수
TYPE_USE타입이 사용되는 모든대상

해당 값들은 Enum으로 정의되어 있다.

  • @Documented

어노테이션에 대한 정보를 javadoc 문서에 표기한다.
지금까지 배운 어노테이션 중 Ovveride, SuppressWarings를 제외한 모든 어노테이션에 적용되어 있다.

  • @Inherited

어노테이션을 하위클래스에게도 상속해준다.

  • @Retention

어노테이션이 유지되는 기간을 설정한다.

유지정책설명
SOURCE.java 소스파일까지는 어노테이션이 존재한다. 컴파일되면 사라진다.
CLASS.class 파일까지는 어노테이션이 존재한다. 런타임시점에 사라진다.
RUNTIME런타임 실행시까지 어노테이션이 남아있다.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override(){}
  • @Repeatable

이 어노테이션을 여러번 작성할 수 있다.


@Repeatable(ToDos.class)
@interface ToDo{}

@ToDo()
@ToDo()
class Main{}

@interface ToDos{
  ToDo[] value(); // 묶으려면 반드시 메서드명이 "value"여야 한다.
}

여러번 적용될 수 있기에 어노테이션을 하나로 묶는 별도의 어노테이션이 필요하다.

사용자 정의 어노테이션


인터페이스 정의와 비슷하게 이루어진다.

java.lang.annotaion 인터페이스를 상속받아 다른 인터페이스나 클래스를 상속받을 수 없다.

@interface BackendFramework{
    enum Frameworks{SPRING, DJANGO}

    Frameworks backendFramework() default Frameworks.DJANGO;
}

class Lee{
    @BackendFramework(backendFramework = BackendFramework.Frameworks.SPRING)
    private String frame;

    public String getFrame() {
        return frame;
    }
}

위와 같이 사용되며, 어노테이션을 사용한 위치나 지정한 값을 사용하려면 Reflection API를 사용해야한다.

Lambda


병렬처리와 이벤트 지향 프로그래밍에 적합한 함수형 프로그래밍에 관심이 많아졌으며, OOP와 함수형 프로그래밍을 혼합하여 효율적으로 프로그래밍하는 방향으로 패러다임이 변화하고 있다.

Lambda식은 익명 함수를 생성하기위한 식으로 코드가 간결해지며, 컬렉션 요소를 효율적으로 조작하여 결과를 쉽게 얻을 수 있다.

람다식의 형태는 매개변수와 코드블럭으로 이루어져 있지만, 런타임에서는 익명 구현 객체를 생성한다.


Test test = () -> {...}
// 람다식

Test test = new Test(){
  public void run(){...}
}
//익명 구현 객체

Lambda식 기본문법


// 1.
(매개변수) -> {...}

//2.
매개변수 -> {...}

//! 3. 보편적인 방법
(매개변수1, 매개변수2) -> return  or  
// 리턴문만 존재할 경우 생략 가능

//4.
(매개변수1, 매개변수2) -> {...}

매개변수명은 자유롭게 지정해도 상관없다.

함수형 인터페이스


자바에서 메서드 사용은 클래스에 정의하여 사용해야한다.

이를 개선하고자 인터페이스 문법을 활용해 람다식을 표현한다. 단 하나의 추상메서드만 존재하는 인터페이스를 함수형 인터페이스라 한다.

  • @FunctionalInterface 활용

앞서 함수형 인터페이스는 “단 하나의 추상메서드만 존재하는 인터페이스”라고 하였다.

두개 이상의 추상메서드가 존재하면 함수형 인터페이스가 아니므로 어노테이션을 이용하여 방지한다.

@FunctionalInterface
public interface Test{
  public void method();
  public void method2(); // 컴파일 에러
}
  • Function<T, R>

1개의 인자 T와 1개의 객체 R을 리턴하는 함수형 인터페이스이다.

public interface Function<T, R>{
  R apply(T t);
}

public interface BiFunction<T, U, R>{
  R apply(T t, U u);
}

메서드 레퍼런스


Lambda식에서 불필요한 매개변수 제거를 목적으로 사용한다.

(a, b) -> Math.max(a, b);

(a, b) -> Math::max;

메서드 레퍼런스는 ”::” 이중콜론 연산자를 사용하며, 또한 익명 구현 객체로 생성되므로 인터페이스의 추상 메서드가 어떤 매개변수를 가지고, 리턴 타입이 무엇인가에 따라 달라진다.

  • 정적메서드와 인스턴스 메서드 레퍼런스

// 정적메서드
클래스::메서드

//인스턴스메서드
참조변수::메서드
  • 매개변수의 메서드 레퍼런스

매개변수 (a,b) 중 a 인스턴스의 메서드 매개변수에 b를 넣고 싶다면, 정적메서드와 유사하게 사용하면 된다.

(a, b) -> a.method(b);

a의 클래스명(참조변수명X)::method;
  • 생성자 참조
public static void main(String[] args){

  Function<String, String, Memeber> function1 = Memeber::new;
  
  Member member = function1.apply("Lee", "Choi");
}

Member에 (String, String) 매개변수를 갖는 생성자가 존재하고, Member apply(String, String) 메서드가 Function 클래스내 존재한다.
메서드 레퍼런스로 apply 메서드가 생성자를 리턴하게 하는 과정이다.

Stream


배열, 컬렉션 요소를 Lambda 식으로 처리할 수 있는 반복자이다.

1. 선언형으로 데이터 소스를 처리

무엇“을 수행하는데 포커스를 둔 선언형 프로그래밍으로 수행한다. 즉, 내부원리를 몰라도 이해할 수 있다.

List<Integer> list = List.of(1,2,3,4,5,6);

int sum = list.stream()
              .filter(n -> n>4 && (n%2==0))
              .mapToInt(n->n)
              .sum();       

for문을 사용하는 것보다 가독성이 좋고 단순하다.

2. Lambda식으로 요소 처리 코드를 제공

Stream에서 제공되는 요소 처리 메서드는 “함수형 인터페이스“를 매개타입으로 가진다.


List<Student> list = Arrays.asList(new Student("Lee", 100),
new Student("Choi", 95));

list.stream().forEach(s -> {
  System.out.println(s.getScore());
})

3. 내부 반복자를 사용하므로 병렬 처리가 쉽다.

외부,내부반복자

https://www.irahul.dev/2020/10/iterator-pattern.html

for, iterator와 같은 외부 반복자가 아닌 내부 반복자를 이용하는 형태이다.

즉, 반복시킬 행위에 대한 고민은 벗어두고 요소의 처리에 집중할 수 있다. 내부 반복자는 요소의 반복 순서를 변경 또는 멀티코어 CPU를 최대한 활용하기위해 요소를 병렬 작업 시키게 도와줌으로써 효율적으로 요소를 반복시킨다.

병렬 스트림은 parallel() 메서드 활용
병렬처리는 한 작업을 여러 작업으로 나누고 나눈 작업을 스레드에서 병렬적으로 처리하는 것이다.

4. 중간 연산과 최종 연산

요소에 대한 매핑, 필터링, 정렬 등의 중간연산이 가능하고 반복, 카운팅, 평균, 총합 등의 최종연산을 수행할 수 있다.

파이프라인 구성(.)


데이터를 가공하여 축소하는 것을 Reduction 이라한다.

컬렉션 요소를 리덕션할 수 없을때는 중간연산이 필요하다.

  • 파이프 라인

중간연산과, 최종연산을 파이프라인으로 해결한다. 이는 여러개의 스트림이 연결된 구조를 의미한다. (최종연산을 제외한 모두가 중간연산)

double ageAve = ilst.stream()// Origin 스트림
  .filter(m -> m.getGender() == Member.MALE)// 중간연산
  .mapToInt(Member::getAge) // 중간연산
  .average() // 최종연산
  .getAsDouble();

스트림 생성, 중간 연산, 최종 연산


  • 스트림 생성
// 1.
List<String> list = Arrays.asList("a","b");
Stream<String> listStream = list.stream();

//2. 
Stream<String> stream = Stream.of("a");
Stream<String> stream = Stream.of(new String[]{"a"});
Stream<String> stream = Arrays.stream(new String[]{"a"});
Stream<String> stream = Arrays.stream(new String[]{"a", "b", "c"},0,3);

List를 통해 stream() 메서드로 생성이 가능하며, Stream.of() 또는 Arrays.stream() 메서드로도 생성이 가능하다.

IntStream, LongStream, DoubleStream과 같은 Primitive Type을 이용하기 위한 스트림도 존재한다.

  • 스트림 사용 시 주의할 점

    1. 스트림은 데이터를 읽기만 하는 Read-Only
    2. 스트림은 일회용이므로 닫히면 새로 만들어야한다.
  • 중간 연산

중간 연산은 결과를 스트림으로 반환하기에 여러번 수행할 수 있다.

1. 필터링(filter(), distinct())

distinct(): 중복제거
filter(): 원하는 데이터만 정제, 즉 조건이 필요하다.

List<String> names = Arrays.asList("Lee", "Choi", "Kim", "Lee");

names.stream()
      .distinct() // Lee 중복제거
      .forEach(System.out::println);

names.stream()
      .filter(n -> n.startWith("L")) // Lee만 정제
      .forEach(System.out::println);

2. 매핑(map())

기존의 Stream 요소를 대체하고 새로운 Stream을 구성한다.

map 함수의 인자는 “함수형 인터페이스”를 받는다.

List<String> names = Arrays.asList("Lee", "Choi", "Kim", "Lee");  

names.stream()
      .map(String::toUpperCase)
      .forEach(System.out::println);

mapToInt(), mapToDouble(), mapToLong() 등의 메서드가 있다. 이처럼 일반 Stream 객체를 Primitive Stream 객체로 바꾸거나, 반대로 mapToObject() 메서드로 바꿀 수 있다.

String[][] sample = new String[][]{
{"a", "b"}, {"c", "d"}, {"e", "a"}, {"a", "h"}, {"i", "j"}
};

Stream<String> stream = sample.stream()
  .flatMap(array -> Arrays.stream(array))
  .filter(x-> "a".equals(x));

stream.forEach(System.out::println);

flatMap() 메서드는 모든 원소를 단일 원소 스트림으로 반환한다. 즉, 입력한 원소를 가장 작은 단위의 단일 스트림으로 변환.

3. 정렬(sorted())

Stream 요소의 정렬은 sorted() 이며, Comparator 사용이 가능하다.


list.stream()
    .sorted(Comprator.reverseOrder());
    //  Comparator에 내림차순으로 변경하는 메서드
    forEach(System.out::println)

Comparable이 구현되어있지 않다면, Comparing() 메서드를 사용해 비교할 수 있다.

4. 연산자 결과 확인(peek())

두 동작은 비슷(요소를 돌면서 출력)하지만 peek는 중간, forEach는 최종연산 메서드이다.

ForEach는 스트림 요소를 소모하며 한번만 호출 할 수 있지만, peek는 중간연산이므로 하나의 스트림에 여러 번 사용할 수 있다.

주로 연산 중간 디버깅을 할 때 많이 사용된다.

  • 최종 연산

연산 결과가 스트림이 아니므로, 한 번만 연산이 가능하다.

1. 연산 결과 확인(forEach())

파이프라인 마지막에서 요소 하나씩 뽑아 연산한다.
출력할 때 사용함은 물론, 리턴값이 없는 스케줄링, 이메일 발송 등에도 사용된다.

2. 매칭(match())

Stream 요소들이 특정 조건을 충족하는 지 검사할 때 사용한다.

함수형 인터페이스 Predicate를 받아 조건을 만족하는 지 검사하고, 결과를 boolean으로 반환한다.

  • allMatch(): 모든 요소들이 만족하는 지
  • anyMatch(): 최소 한개의 요소가 만족하는 지
  • nonMatch(): 모든 요소들이 만족하지 않는 지
int[] intArr = {2,4,6};
boolean result = Arrays.stream(intArr).allMatch(a -> a%2==0);

3. 기본 집계(sum(), count(), average(), max(), min())

int[] intArr = {1,2,3,4,5};

int max = Arrays.stream(intArr).max().getAsInt();

위와 같이 전반적으로 활용된다.

4. Reduce()

누적하여 하나로 만드는 방식이다.


int[] intArr = {1,2,3,4,5};

// 초기값이 없는
int sum = Arrays.stream(intArr)
              .reduce((a,b) -> a+b)
              .getAsInt();

// 초기값이 있는
int sum2 = Arrays.stream(intArr)
              .reduce(0, (a,b) -> a+b)
              .getAsInt();

스트림에 요소가 없을 경우, 초기값이 없는 reduce를 사용한다면 요소가 없다는 예외가 발생한다.

count(), sum() 등은 내부적으로 reduce()를 사용한다.

5. Collect()

Stream의 요소를 컬렉션 등의 다른 결과로 수집할 때에 사용된다. 일반적으로 Collectors 클래스에서 제공하는 static 메서드를 이용하거나, Collector 인터페이스를 직접 구현하여 사용한다.

List<Student> male = list.stream()
.filter(s -> s.getGender() == Student.Gender.Male)
.collect(Collectors.toList());

List<Student> female = list.stream()
.filter(s -> s.getGender() == Student.Gender.Female)
.collect(Collectors.toCollection(HashSet::new));

Optional


NPE(Null Pointer Exception) 을 효율적으로 방지하기 위해 도입하였다.

모든 타입의 객체를 담을 수 있는 Wrapper 클래스이다.

public final class Optional<T>{
  private final T value;
}
메서드설명
of()객체 생성
ofNullable()생성시 null일 가능성이 있을 때
empty()기본값으로 초기화
get().orElse(default)객체에 저장된 값 조회
null일 가능성이 있다면 orElse()

Collection vs Stream


CollectionStream
데이터를 저장데이터를 가공 처리
추가/삭제 가능추가/삭제 불가능
요소를 읽어 소비만 한다.
외부 반복자내부 반복자
여러번 탐색가능한번만 탐색가능
Eager 데이터 처리Lazy 데이터 처리 및 short-circuit

Eager방식은 메모리에 전체를 한번에 올려서 처리한다. 반면에 Lazy방식은 필요부분을 그때 올리면서 처리한다.
즉, 최종연산을 호출해야 그때 모든 작업을 수행한다.

short-circuit은 무한 스트림을 유한하게 연산할 수 있도록 한다.

IntStream stream = new Random().ints(); //무한 스트림
stream.limit(10).forEach(System.out:;println);

순서대로 실행된다면 2번째 줄은 실행되지 않을 것이다. 하지만 단락평가인 limit(10) 메서드로 10개만 연산하도록 한다.

📌 정리


사실 제대로된 정의는 생략하고 생각나는대로 사용했던 Enum, Annotation, Lambda, Stream 이었다.

이 개념들이 이렇게 복잡하게 구성되어있는지 깨달았고, 정리하는데 상당히 오랜 시간이 걸렸다. 그 만큼 TIL에만 남겨두는 것 보다는 따로 글을 포스팅하여 나누는 작업을 진행해야겠다😥

🎯 Tomorrow


  • Stream - Pair
  • 파일 입출력
  • Thread
  • JVM

Back to [TIL] 코드스테이츠 SEB BE Day 16