2025-04-03 java

성현규·2025년 4월 3일
post-thumbnail

0. 대화

Java에서의 함수형 인터페이스, Optional 처리 방식, 선언형과 명령형 스타일의 차이를 정리한다.
실무와 이론 모두에서 자주 등장하는 핵심 개념들을 간결하고 명확하게 요약하였다.


0-1. Function<? super Object, ? extends Object> classifier

📌 핵심 요약:

  • Function<T, R>: 입력 T를 받아 출력 R을 반환하는 함수형 인터페이스
  • 와일드카드:
    • <?>: 아무 타입이나 가능
    • <? extends T>: T를 상속한 타입 (T 포함 X)
    • <? super T>: T의 부모 타입 (T 포함 O)
  • classifier:
    • 분류기 역할을 하는 함수
    • 객체를 받아서 특정 기준에 따라 변환하여 반환함

✅ 예시 설명:

Function<? super Object, ? extends Object> classifier;
  • 어떤 Object의 부모 타입도 입력 받을 수 있으며,
  • 반환은 Object를 상속한 타입 중 하나가 될 수 있다.

0-2. Optional

📌 핵심 요약:

Optional은 Java에서 null을 안전하게 다루기 위해 사용하는 래퍼 클래스이다.

✅ 메서드 요약:

  • Optional.of(value)

    • 절대 null이 아님
    • null을 넣으면 예외 발생
  • Optional.ofNullable(value)

    • null일 수도 있는 값 처리 가능
  • Optional.empty()

    • 값이 아예 없음을 명시적으로 표현
  • .orElse(default)

    • 값이 없을 경우 기본값 반환
  • .ifPresent(fn)

    • 값이 존재하면 함수 실행
  • .map(fn)

    • Optional 내부의 값을 변환
  • OptionalDouble, OptionalInt, OptionalLong

    • 기본형 타입의 Optional 표현
⚠️ 주의할 점:
  • Optional.of(null)절대 사용하지 말 것
  • 기본 자료형을 감싸는 경우는 OptionalInt 등으로 명확히 구분

0-3. 선언형 vs 명령형 (프로그램 설계 철학)

프로그래밍 스타일에 따라 문제 해결 방식이 달라진다.

구분선언형 스타일명령형 스타일
말 그대로“무엇을 할지” 선언“어떻게 할지” 직접 지시
특징결과 중심절차 중심
목적무엇을 원하는지만 말함어떻게 처리할지를 자세히 설명함
예시 언어SQL, HTML, Java Stream, ReactC, Java for문, Python 기본 반복문 등

📌 핵심 요약:

  • 선언형은 간결한 표현가독성 향상에 유리
  • 명령형은 세부 제어가 가능하나, 복잡성 증가 위험 있음
    |

1. 자바 기본 API(2)

1-1. 파일 입출력

자바에서는 FileOutputStream, FileInputStream과 같은 바이트 기반 스트림을 사용하여 파일에 데이터를 저장하고 읽어올 수 있다.
입출력 시에는 반드시 예외 처리를 통해 안정적인 프로그램 흐름을 구성하며, 스트림의 닫기 작업은 finally 블록에서 수행하는 것이 일반적이다.


1-1-1. 📌 핵심 요약:

  • 자바에서 파일 입출력은 스트림(Stream) 개념을 기반으로 한다. (파이프, 흐름)
  • FileOutputStream → 파일에 데이터를 바이트 단위로 저장 (바이트 단위로 흘려보낼 수 있게 해주는 통로/흐름)
  • FileInputStream → 파일에서 데이터를 바이트 단위로 읽음 (바이트 단위로 들어올 수 있게 해주는 통로/흐름)
  • 반드시 예외처리스트림 닫기(close) 작업이 포함되어야 한다.

1-1-2. 파일저장:

문자열을 UTF-8 바이트 배열로 변환 후, FileOutputStream을 통해 파일로 저장하는 예제이다.

public class Ex07_파일저장 {
    public static void main(String[] args) {
        // 파일 경로 (새로 만들 파일 포함), 파일에 기록할 내용 지정
        String filePath = "./01-JAVA-Basics/text.txt";
        String content = "안녕하세요 자바";

        // 문자열을 UTF-8 바이트 배열로 인코딩 (파일 저장을 위해)
        // FileOutputStream은 바이트 단위 스트림이라서, 흐를 수 있는 건 "바이트"뿐임. 
        // 따라서 그 안으로 보내려면 무조건 직렬화(객체의 경우)되었거나, 
        // 혹은 문자열을 바이트 배열로 인코딩한 형태여야 한다.
        byte[] buffer = null;
        try {
            buffer = content.getBytes("utf-8"); //enc-Kr도 있다.(엑셀o, vscode x)
        } catch (UnsupportedEncodingException e) {
            e.getStackTrace();
        }

        // 파일쓰기
        OutputStream os = null;
        
        try {
            os = new FileOutputStream(filePath); 
            // 자바 프로그램에서 파일을 "열어서" 데이터를 "써주는" 파이프(관) (쓰기모드로 연다)
            
            // 프로그램에서 파일로 데이터를 보냄 (out - 프로그램 기준), 
            // Stream은 데이터를 한 바이트씩 차례대로 흘려보내는 통로(파이프) 개념
            os.write(buffer); 
            // 파이프를 통해 쓰기 작업 수행
            // 이때 이미 존재하는 파일이면 덮어쓰고, 없으면 자동으로 생성됨.
            
        } catch (FileNotFoundException e) { 
            // 파일을 못찾았을때 발생하는 에러
            e.printStackTrace();
        } catch (IOException e) { 
            // 파일 입출력시 하드가 부족하거나 해서 발생하는 에러
            e.printStackTrace();
        } catch (Exception e) {  
            // 예상치 못한 에러가 발생할 수 있으므로  항상 catch 마지막은 Exception을 써준다.
            e.printStackTrace();
        } finally {
            if (os!= null) { 
                // 스트림이 성공적으로 열렸을 경우에만 닫을 수 있다.
                try {
                    os.close();  
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
✅ 예시 설명:
  • 문자열 → getBytes("utf-8")을 통해 바이트 배열로 변환
  • FileOutputStream으로 흘려서 바이트 배열을 파일에 저장
  • 예외처리 (FileNotFoundException, IOException, Exception)를 통해 안정성 확보
  • finally 블록에서 close() 수행

1-1-3. 파일읽어오기

저장된 파일을 FileInputStream으로 읽고, 바이트 배열을 다시 문자열로 복원하는 예제이다.

public class Ex08_파일읽기 {
    public static void main(String[] args) {
        String filepath = "01-JAVA-Basics/text.txt"; // ./는 생략가능
        byte [] buffer = null; // 읽어올 내용이 저장될 임시공간
        String content = null; // 임시공간의 바이트들을 문자열로 변환하여 저장할 변수

        InputStream is = null; // 꽂을 파이프 정의 

        try {
            is = new FileInputStream(filepath); // 프로그램 기준의 명명이니까 변수로는 파이프가 이어질 대상을 지정 -> 순간 파이프의 크기도 할당
            buffer = new byte[is.available()]; // 파이프(흐름)의 크기를 불러옴.
            is.read(buffer); // 관 꽂았으니까 이제 읽고 buffer에다가 담음.
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (is != null){ // 통로가 잘 열렸으면 닫아야지
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        if (buffer != null ){
            try {
                content = new String(buffer, "utf-8");
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            } 
        }
        
        System.out.println(content);
    }
}
✅ 예시 설명:
  • FileInputStream → 파일 내용을 읽어서 바이트 배열 buffer에 저장
  • buffer를 다시 문자열로 복원 → new String(buffer, "utf-8")
  • available()을 통해 스트림에서 읽을 수 있는 바이트 크기 확보
⚠️ 주의할 점:
  • FileOutputStreamFileInputStream은 모두 바이트 기반이므로, 문자열 처리 시 반드시 인코딩/디코딩 작업이 필요하다.
  • 스트림을 닫지 않으면 리소스 누수(리소스가 공간을 계속 점유하고 있는 것을 의미한다.)가 발생할 수 있으므로 finally 블록에서 닫는 처리를 해야 한다.
  • getBytes() 또는 new String() 시 인코딩명을 명시하지 않으면 시스템 기본값을 사용하므로, 명확하게 "utf-8" 지정해야 한다.

2. 자바 문제 풀이

2-1. 객체 리스트에서 특정 필드 기준 필터링 및 추출

  • Person::name클래스::필드값으론 접근 안됨
  • p -> p.name이 맞음. p는 지금 스트림에 흐르고 있는 하나의 객체를 의미함.
    객체를 변수로 받는 람다식을 세우는 게 맞음.
// 답(오답)
people.stream().filter(Person->Person.age>=20).map(Person::name).collect(Collectors.toList());

// 수정코드
people.stream()
      .filter(p -> p.age >= 20)
      .map(p -> p.name)
      .collect(Collectors.toList());

// 문제
class Person {
    String name;
    int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

List<Person> people = List.of(
    new Person("철수", 19),
    new Person("영희", 22),
    new Person("민수", 25)
);

2-2. 문자열 2차원 리스트 평탄화 후 중복제거

  • 내부를 우선 스트림으로 만들어야 평탄화할 수 있다.
// 답
data.stream().flatMap(list -> list.stream()).distinct().sorted().forEach(s->System.out.println(s));

// 문제
List<List<String>> data = List.of(
    List.of("apple", "banana"),
    List.of("banana", "carrot"),
    List.of("apple", "date")
);

2-3. 문자열 리스트를 길이 기준으로 그룹화하여 Map으로 저장

// 답
words.stream().collect(Collectors.groupingBy(String::length));

// 문제
List<String> words = List.of("a", "bee", "ant", "banana", "dog");

2-4. 모든 아이템의 가격합계 구하기

  • 클래스의 필드값을 가져오는 것은 거르는 게 아니라 전부 가져와야 하므로 filter가 아니라 map으로 해야 한다.
  • reduce는 특정 작동을 하면서 값을 압축하는 것이다.
    합, 곱, 문자열 연결, 최솟값/최댓값(비교식 누적) 등을 구할 때 사용하고 특정 연산을 누적한다. → 객체형 스트림에서 사용하는 것
  • min, max, sum기본형 스트림에만 제공된다.
// 답(오답)
items.stream().filter(Item::price).reduce(0, (a,b) -> a+ b);

// 정답
int sum = items.stream()
    .map(item -> item.price)
    .reduce(0, Integer::sum); // 또는 (a, b) -> a + b

// 문제
class Item {
    String name;
    int price;
    public Item(String name, int price) {
        this.name = name;
        this.price = price;
    }
}

List<Item> items = List.of(
        new Item("노트북", 1200000),
        new Item("마우스", 30000),
        new Item("키보드", 45000)
    );

2-5. 50 이하의 수만 남기고 제곱 후 짝수만 리스트로 수집

// 답
numbers.stream().filter(i -> i <= 50).map(i -> i*i).collect(Collectors.toList());

// 문제
List<Integer> numbers = List.of(5, 8, 60, 7, 9, 100);

2-6. 이름이 김으로 시작하는 사람만 이름을 영어 대문자로 바꿔서 리스트로 수집

// 답
names.stream()
    .filter(s -> s.startsWith("김"))
    .map(s->s.replace("김","KIM"))
    .collect(Collectors.toList());

// 문제
List<String> names = List.of("김철수", "이영희", "김영수", "박민수");

2-7. 사용자 객체 리스트에서 도메인만 추출해서 중복제거하고 정렬

  • map 안에 문장은 이게 스트림이 아닐 때라고 치고 조건문을 작성하는 것
// 답
users.stream()
    .map(s -> s.email.split("@")[1])
    .distinct()
    .sorted()
    .forEach(System.out::println);

// 문제
class User {
    String name;
    String email;
}

List<User> users = List.of(
        new User("A", "a@gmail.com"),
        new User("B", "b@naver.com"),
        new User("C", "c@gmail.com")
    );

2-8. 책 객체 리스트에서 "프로그래밍"이라는 단어가 포함된 제목만 골라 정렬하고 출력

// 답
books.stream()
    .map(s->s.tilte)
    .filter(s->s.contains("프로그래밍"))
    .sorted()
    .forEach(System.out::println);

//문제
List<Book> books = List.of(
    new Book("자바 프로그래밍 기초", 30000),
    new Book("자료구조", 25000),
    new Book("파이썬 프로그래밍", 28000),
    new Book("인공지능", 40000)
)
    class Book {
    String title;
    int price;
}

2-9. 이름 리스트를 Set으로 수집

// 답(약간 틀림)
names.stream()
    .distinct()
    .collect(Collectors.toSet());

// 정답
names.stream().collect(Collectors.toSet()); // 자동으로 중복을 걸러줌.

// 문제
List<String> names = List.of("김철수", "이영희", "김철수", "박민수");

2-10. 배열에서 시작해서 평균구하기

  • average가 OptionalDouble을 리턴하고 그대로 가면 이건 값이 있을 땐 double, 아니면 컴파일 에러가 난다.
  • 실무에서는 orElse로 기본값을 설정하고 뽑는 방식을 많이 사용한다.
  • getAsDouble()은 optional에 값이 비어있을 경우 에러가 난다.
// 답 (애매함.)
Arrays.stream(scores)
    .average()
    .getAsDouble();

// 정답
Arrays.stream(scores)
    .average()
    .orElse(0.0); // 기본값 더블타입으로 설정

// 문제 설명
int[] scores = {80, 90, 75, 100, 95};

2-11. 조건에 따른 분류와 수집 (고난도)

  • Collectors.groupingBy(
    분류함수, // ① keyMapper (필수) → 각 요소가 어떤 키로 묶일지
    하위 수집기 // ② downstream Collector (선택)
    → 그룹핑된 각 그룹 안에서 값을 어떻게 처리할지
    )
  • Collectors.mapping(
    매핑 함수, // ① valueMapper → 각 요소를 어떤 값으로 변환할지 지정
    수집 방식 // ② downstream Collector → 변환된 값을 어떻게 수집할지 지정
    )
Map<String, List<String>> result = students.stream()
    .collect(Collectors.groupingBy(
        s -> s.score >= 60 ? "합격" : "불합격",    // key: 조건에 따른 문자열
        Collectors.mapping(s -> s.name, Collectors.toList()) // value: 이름만 리스트로 수집
    ));
profile
학생

0개의 댓글