슈퍼코딩/주특기(JAVA)

2024.05.14(화) 슈퍼코딩 신입연수원 7주차 Day 2 후기 - 유틸리티 클래스, 내부 클래스, 람다식, stream

곰돌이볼 2024. 5. 14. 08:21
  • 강의
    • 55강(내부 클래스와 유틸클래스) ~ 57강(stream 사용하여 컬렉션 우아하게 사용하기)

유틸리티 클래스 & 내부 클래스


  • Java 클래스의 아쉬움 점
    • 클래스를 이용하려면 인스턴스화 해야함 → static 활용
    • 여러 클래스/메서드와 유사한걸 표현하려면 상속을 해야함 → 내부 클래스 이용해서 개념적 영역으로 그룹화
    • 한 번만 사용할 클래스를 새로 정의 → 내부 클래스 문법 이용

유틸리티 클래스

  • 불필요한 인스턴스화를 할 필요가 없는 클래스
  • 클래스의 메서드는 전부 static으로 선언해서 인스턴스 없이 사용 가능
public class MathUtils {
	public static final double PI = 3.14;
    
    public static int sum(int a, int b);
    	return a - b;
    }
}

 

내부 클래스

  • 내부 클래스 종류
    • 일반 중첩 클래스(inner class) : 클래스 내부에 선언된 클래스
    • 정적 중첩 클래스(static nested class) : static이 붙은 클래스 내부에 선언된 클래스
    • 지역 중첩 클래스(method local inner class) : 메서드 내에 선언된 내부클래스
    • 익명 중첩 클래스(anoymous inner class)
class Outer { 
	class InstanceInner{}{} // 일반 중첩 클래스
    static class StaticInner(){} // 정적 중첩 클래스
    void method() {
    	class LocalInner(){} // 지역 중첩 클래스
    }
}

 

  • 선언
// 내부 클래스 정의
class OuterClass { 
	class InstanceInner{}{} // 일반 중첩 클래스
    static class StaticInner(){} // 정적 중첩 클래스
    void method() {
    	class LocalInner(){} // 지역 중첩 클래스
    }
}

interface Flyable {
	void fly();
}

// 선언
public class Main {
    public static void main(String args[]) {
        OuterClass outer = new OuterClass();
        Outer.InnerClass inner = outer.new InnerClass(); // 내부 중첩 클래스 선언
        Outer.StaticInner staticInner = new OuterClass.InnerClass(); // 정적 중첩 클래스 선언
		
        // 지역 내부 클래스 선언
        class LocalFlyalbe implements Flyable {
        	@Override
            public void fly() { System.out.println("LocalFlyable.fly()"); }
        }
        
        // 지역 내부 클래스 사용
        LocalFlyable localFlyable = new LocalFlyable();
        localFlyable.fly();
        
        // 익명 내부 클래스 선언
        Flyable anonymousFlyable = new Flyable() {
        	@Override
            public void fly() { System.out.println("AnonymousFlyable.fly()"); }
        };
        
        // 익명 내부 클래스 사용
        anonymousFlyable.fly();        
    }
}

 

  • 일반 중첩 클래스(inner class, 인스턴스 클래스)
    • 외부 클래스 인스턴스와 연관되어 있음
    • 외부 클래스의 멤버변수 위치에 선언
    • 인스턴스 멤버처럼 사용
  • 정적 중첩 클래스(static nested class)
    • 외부 클래스 인스턴스와 독립적임
    • 외부 클래스의 멤버변수 위치에 선언
    • static 멤버처럼 사용
  • 지역 중첩 클래스(method local inner class)
    • 보통 인터페이스를 상속받아서 클래스 내부에 선언
    • 여러 번 인스턴스화 가능
    • 외부 클래스의 메서드나 초기화 블록 안에서 선언
    • 선언된 영역 내부에서만사용 가능
  • 익명 중첩 클래스(anoymous inner class)
    • 보통 인터페이스를 상속받아서 구현 후 인스턴스화
    • 한 번만 인스턴스화 가능
    • 클래스의 선언과 객체의 생성을 동시에 하는 클래스(1회성)

함수형 프로그래밍


  • 함수형 프로그래밍(Function programming)
    • 모든 프로그램을 함수의 연속으로 보는 방식
  • 대표적인 함수형 프로그래밍 언어
    • 클로저(Clojure)
    • 스칼라(Scala)
    • 하스켈(Haskell)
  • java에서 함수형 프로그래밍 일부 수용 → 람다식 문법

람다식


  • 람다식 문법
// 원래 문법
int sum(int a, int b) {
	return a + b;
}

// 람다식 : 메서드 이름과 반환타입 제거 & -> 붙이기
(int a, int b) -> {
	return a + b;
}

// 선택사항1 : return문이나 {} 생략 가능
(int a, int b) -> a + b

// 선택사항2 : 매개변수 타입 추론 가능 시 생략 가능
(a, b) -> a + b
  • 람다식 구현
    • 람다 인터페이스에 @FunctionalInterface 어노테이션 사용해서 선언하기
    • @FunctionalInterface는 1개의 메서드만 선언 가능
    • 람다식 구현 시 익명 내부클래스가 자동으로 구현됨
    • 외부에 선언된 변수 변경 X(참조 가능) → 아래의 이유로 인해서 참조하는 외부 변수 final이어야함
      • 동시성 문제 방지 : 람다식이 병렬로 실행되더라도 외부 변수가 안전하게 유지
      • 스택과 힙의 차이로 인한 문제 방지 : 람다식이 로컬 변수의 값을 안전하게 참조할 수 있도록 함
      • 자바의 불변성 원칙 유지 : 람다식의 동작을 예측 가능, 코드의 이해와 유지 보수 용이
// 람다 인터페이스 정의
@FunctionalInterface
interface CalculateNum {
    int add(int str);
}

@FunctionalInterface
interface StringNum {
    void printString(String str, int count);
}

// 람다 구현 및 실행
public class Main {
    public static void main(String[] args) {
        // 매개변수가 1개인 경우
        CalculateNum calculateNum = (x) -> x + 1;
        calculateNum.add(10);
        /*
        CalculateNum calculateNum = new CalculateNum() {
            @Override
            public String add(String str) {
            	return str + 1;
            }
        }
        */

        // 매개변수가 2개인 경우
        StringNum stringNum1 = (str, count) -> System.out.println(str + " " + count);
        StringNum stringNum2 = (str, count) -> {
            for(int i=0; i<count; i++) System.out.println(i+"번째 : " + str);
        };

        stringNum1.printString("hello1", 2);
        stringNum1.printString("hello2", 5);

    }
}
  • 활용
    • 메서드의 매개변수에 람다식 이용
    • Genric을 람다식의 매개변수에 이용
  • 실전
    • 직접적인 사용 X → 다른 실전 API의 매개변수에 사용(orElseGet, stream 등...)

Stream


  • stream : 함수형 프로그래밍을 도입하여 collection, array 등의 처리를 간단하고 효율적으로 처리하는 API
  • 사용 이유
    • 람다식으로 코드 개선
    • 가독성 ↑
    • 병렬 연산 가능

 

stream 특징

  • stream 단계 : 생성 → 중간연산 → 최종연산
    • 중간연산은 여러 번 가능하지만 최종연산은 1번만 가능
  • 주의점 : 재사용 X
  • 기존 자료형으로난 변환 X → Wrapper 클래스 사용하기

 

stream 생성

  • stream 생성
    • Stream.of()
    • Arrays.stream()
    • collection.stream()
public class Main {
    public static void main(String[] args) {
        // stream 선언 : Stream.of, Arrays.Stream, Collection -> Stream

        // Stream.of
        Stream<String> fruits = Stream.of("apple", "pear", "banana");
        Stream<Integer> numbers = Stream.of(1, 2, 3, 4, 5);

        // Arrays.Stream
        Stream<String> fruits1 = Arrays.stream(new String[]{"apple", "pear", "banana"});
        Stream<Integer> numbers3 = Arrays.stream(new Integer[]{1, 2, 3, 4, 5});

        // Collection
        List<String> fruitsList = Arrays.asList("apple", "pear", "banana");
        Stream<String> fruits2 = fruitsList.stream();
    }
}

 

stream 중간 연산

  • 여러 중간 연산 연동 가능
  • 종류
    • filter(요소 -> 조건식) : 조건식이 true인 요소만 남김
    • distinct() : 중복된 요소 제거
    • map(요소 -> 함수) : 특정 함수를 통해서 새로운 요소로 변환
    • limit(max) : max개의 요소 반환(처음부터 시작한 값)
    • skip(n) : 처음 n개를 제외한 요소 반환
    • sorted() : 정렬
      • comparator을 통해서 사용자 정의로 정렬 사용 가능
  • 코드 예시
public class Main {
    public static void main(String[] args) {
        // stream 값
        List<String> fruitsList = Arrays.asList("apple", "pear", "banana", "apple");
        System.out.println("원래값 : " + fruitsList);

        // 1. filter : 과일이름의 길이가 5 이상의 과일 선택
        List<String> result = fruitsList.stream()
                .filter(fruit -> fruit.length() >= 5)
                .collect(Collectors.toList());
        System.out.println("filter : " + result);

        // 2. distinct : 중복된 과일 제거
        result = fruitsList.stream()
                .distinct()
                .collect(Collectors.toList());
        System.out.println("distinct : " + result);

        // 3-1. map : 과일의 첫 글자를 대문자로 변환
        result = fruitsList.stream()
                .map(fruit -> fruit.toUpperCase())
                .collect(Collectors.toList());
        System.out.println("map(대문자) : " + result);

        // 3-2. map : 과일의 길이 변환
        List<Integer> result2 = fruitsList.stream()
                .map(fruit -> fruit.length())
                .collect(Collectors.toList());
        System.out.println("map(길이) : " + result2);

        // 4. limit : 처음 3개의 과일만 선택
        result = fruitsList.stream()
                .limit(3)
                .collect(Collectors.toList());
        System.out.println("limit : " + result);

        // 5. skip : 처음 2개를 제외한 나머지 과일 선택
        result = fruitsList.stream()
                .skip(2)
                .collect(Collectors.toList());
        System.out.println("skip : " + result);

        // 6-1. sorted : 과일을 알파벳 순으로 정렬
        result = fruitsList.stream()
                .sorted()
                .collect(Collectors.toList());
        System.out.println("sorted(알파벳순) : " + result);

        // 6-2. sorted : 길이가 짧은 순으로 정렬
        result = fruitsList.stream()
                .sorted((fruit1, fruit2) -> fruit1.length() - fruit2.length())
                .collect(Collectors.toList());
        System.out.println("sorted(길이순) : " + result);
    }
}

 

원래값 : [apple, pear, banana, apple]
filter : [apple, banana, apple]
distinct : [apple, pear, banana]
map(대문자) : [APPLE, PEAR, BANANA, APPLE]
map(길이) : [5, 4, 6, 5]
limit : [apple, pear, banana]
skip : [banana, apple]
sorted(알파벳순) : [apple, apple, banana, pear]
sorted(길이순) : [pear, apple, apple, banana]

 

stream 최종연산

  • 진행 연산을 닫고 최종값 산출
  • 종류
    • foreach() : 각 요소 출력
    • collect() : collection 반환
    • findFirst() : 첫번째 값 반환(반환값 : Optional<T>)
    • sum(), average() : 총합 및 평균값 계산, 숫자 stream만 사용 가능
    • count(), max(), min() : 개수, 최고값, 최소값 반환, 숫자 stream만 사용 가능
    • reduce() : (초기값 설정 및 연산)을 통해서 규칙 설정 가능
  • stream을 이용한 for-loop 개선
public class Main {
    public static void main(String[] args) {
        // for-loop
        List<String> fruitsList = Arrays.asList("apple", "pear", "banana");
        for(String fruit : fruitsList) {
            System.out.println(fruit);
        }

        // stream을 이용한 for-loop 개선
        Stream<String> stream = fruitsList.stream();
        stream.forEach(System.out::println);
    }
}
  • 예시 코드
public class Main {
    public static void main(String[] args) {
        // stream 값
        List<String> fruitsList = Arrays.asList("apple", "pear", "banana");

        // 1. forEach() : 출력
        fruitsList.stream().forEach(fruit -> System.out.println("for-each : " + fruit));

        // 2. collect : List, Set, Map, Collector 등 반환
        Set<String> set = fruitsList.stream().collect(Collectors.toSet());
        System.out.println("set : " + set);

        // 3. findFirst() : stream의 첫번째 값 산출(return값 : Optional<T>)
        Optional<String> optional = fruitsList.stream().findFirst();
        System.out.println("findFirst : " + optional.orElseGet(() -> "not found"));

        // 숫자 stream 값
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        // 4. sum, average
        System.out.println("sum : " + numbers.stream().mapToInt(i -> i).sum());
        System.out.println("average : " + numbers.stream().mapToInt(i -> i).average().getAsDouble());

        // 5. max, min, count
        System.out.println("max : " + numbers.stream().mapToInt(i -> i).max().getAsInt());
        System.out.println("min : " + numbers.stream().mapToInt(i -> i).min().getAsInt());
        System.out.println("count : " + numbers.stream().count());

        // 6. reduce() 로 소모값 구하기
        // reduce로 규칙 설정 가능
        int result = numbers.stream().reduce(0, (a, b) -> a + b);
        System.out.println("reduce : " + result);
    }
}
for-each : apple
for-each : pear
for-each : banana
set : [banana, apple, pear]
findFirst : apple
sum : 55
average : 5.5
max : 10
min : 1
count : 10
reduce : 55