코딩/sparta TIL

TIL 8 : Java 문법 종합반 3주차

americanoallday 2025. 2. 26. 11:55

Timeline

09:00 ~ 09:30 : 데일리 스크럼

09:30 ~ 12:00 : 개인 학습

12:00 ~ 13:00 : 점심 시간

13:00 ~ 18:00 : 개인 학습

18:00 ~ 19:00 : 저녁 시간

19:00 ~ 21:00 : 개인 학습

 

예외(Exception)란?

  • 예외는 프로그램 실행 중 예상하지 못한 상황이 발생하는 것을 의미합니다.
    • → 대표적인 산술 예외: 10 / 0 (0 으로 나누기)
  • 의도적으로 예외를 발생시킬 때는 throw 키워드를 통해 발생시킵니다.
  • 예외를 처리하지 않으면 프로그램이 중단될 수 있습니다.
  • 그래서 예외처리(try-catch)를 통해 프로그램이 안정적으로 실행되게 할 수 있습니다.

의도하지 않은 예외

public class Main {
    public static void main(String[] args) {
        System.out.println("프로그램 시작");
        int result = 10 / 0; // ❌ 예외 발생 (ArithmeticException)
        System.out.println("이 문장은 실행되지 않음");
    }
}
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at chapter3.exception.Main.main(Main.java:8)

Process finished with exit code 1

 

의도적인 예외 - throw

public class Main {
    public static void main(String[] args) {
        int age = 10;
        if (age < 18) {
	        // ✅ 의도적으로 예외를 발생시키는 부분
        	throw new IllegalArgumentException("미성년자는 접근할 수 없습니다!");
        }
        System.out.println("프로그램 종료");
    }
}

IllegalArgumentException

 

예외 구조와 종류

Checked Exception컴파일 시점에 체크됨 (반드시 try-catch로 처리해야 함)

Unchecked Exception실행(Runtime) 중에 발생 (강제 처리 X, 개발자가 신경 써야 함)

📌 즉, Checked Exception은 “필수 처리”, Unchecked Exception은 “개발자가 처리 선택 가능” 🚀

구분 Checked Exception Unchecked Exception
발생 시점 컴파일 시 체크됨 실행(Runtime) 중 발생
처리 강제 여부  try-catch 또는 throws 필수 선택 사항(예외 발생 시 런타임 오류)
예제 IOException, SQLException, InterruptedException NullPointerException, ArrayIndexOutOfBoundsException, ArithmeticException
주로 발생하는 상황 파일, 네트워크, DB 오류 코드 논리 오류 (잘못된 배열 인덱스, null 참조 등)

📌 즉, Checked Exception은 예상 가능한 외부 오류(파일, 네트워크), Unchecked Exception은 논리적 버그! 🚀

 

🔹 Checked Exception 예제 (IOException)

import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;

public class CheckedExceptionExample {
    public static void main(String[] args) {
        try {
            File file = new File("nonexistent.txt");
            Scanner scanner = new Scanner(file); // ✅ 파일이 없으면 FileNotFoundException 발생
        } catch (FileNotFoundException e) {
            System.out.println("⚠️ 파일을 찾을 수 없습니다: " + e.getMessage());
        }
    }
}

📌 Checked Exception은 try-catch 없이 사용할 수 없음!

 

🔹 Unchecked Exception 예제 (NullPointerException)

public class UncheckedExceptionExample {
    public static void main(String[] args) {
        String str = null;
        System.out.println(str.length()); // ❌ NullPointerException 발생
    }
}
Exception in thread "main" java.lang.NullPointerException

📌 Unchecked Exception은 try-catch 없이도 실행되지만, 실행 중 오류가 발생할 수 있음! 

 

throws를 사용하여 Checked Exception 처리

Checked Exception은 try-catch 외에도 throws 키워드로 예외를 넘길 수 있음

즉, 예외 처리를 “이 메서드를 호출하는 쪽”에서 하도록 미룰 수 있음

 

🔹 throws를 사용한 예제

import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;

public class ThrowsExample {
    public static void main(String[] args) throws FileNotFoundException {
        File file = new File("nonexistent.txt");
        Scanner scanner = new Scanner(file); // ✅ `throws` 덕분에 여기서 예외 처리를 강제하지 않음
    }
}

하지만 main()에서 예외 처리를 안 하면 실행 중 오류 발생 가능

📌 즉, throws는 “예외를 넘기는 역할”을 하지만, 결국 어디선가 try-catch 해야 함!

 

Checked Exception을 Unchecked Exception으로 변환 가능

Checked Exception이지만, RuntimeException으로 감싸서 Unchecked Exception처럼 만들 수도 있음!

 

🔹 Checked → Unchecked 변환 예제

import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;

public class ConvertToUnchecked {
    public static void main(String[] args) {
        try {
            readFile();
        } catch (RuntimeException e) {
            System.out.println("⚠️ 처리되지 않은 예외 발생: " + e.getMessage());
        }
    }

    public static void readFile() {
        try {
            File file = new File("nonexistent.txt");
            Scanner scanner = new Scanner(file);
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e); // ✅ Checked Exception을 Unchecked Exception으로 변환
        }
    }
}

즉, Checked Exception을 RuntimeException으로 감싸면 Unchecked Exception처럼 동작함! 🚀

 

✅ Checked Exception vs Unchecked Exception을 코드에서 어떻게 알 수 있을까? 🚀

더보기

처음에는 구분하기 어려울 수 있지만, 익숙해지면 어떤 예외가 필수 처리(Checked)인지, 개발자가 신경 써야 하는 예외(Unchecked)인지 쉽게 알 수 있음

하지만, 코드 작성 중에도 “이 예외가 Checked인지 Unchecked인지” 바로 확인하는 방법이 있음

 

 

📌 1. 예외가 Checked인지 Unchecked인지 확인하는 방법

 

예외가 Exception을 상속하지만 RuntimeException을 상속하지 않으면 Checked Exception!

예외가 RuntimeException을 상속하면 Unchecked Exception!

 

 

📌 2. 예외의 부모 클래스를 확인하는 방법

 

자바 공식 문서(Java API)에서 예외 클래스의 부모 클래스를 보면 바로 구분할 수 있음.

 

🔹 Checked Exception 예제 (FileNotFoundException)

// Checked Exception (반드시 처리해야 함)
public class FileNotFoundException extends IOException { }

IOException은 RuntimeException을 상속하지 않음 → Checked Exception (필수 처리 필요)

 

🔹 Unchecked Exception 예제 (NullPointerException)

// Unchecked Exception (개발자가 신경 써야 함)
public class NullPointerException extends RuntimeException { }

RuntimeException을 상속 → Unchecked Exception (필수 처리 X, 런타임 오류 발생 가능)

 

 

📌 3. 코딩 중 필수 처리 여부를 확인하는 방법

 

실제 코드를 작성할 때 Checked Exception은 “컴파일 오류”가 발생하므로 바로 알 수 있음!

 

🔹 Checked Exception 예제 (컴파일 오류 발생!)

import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;

public class CheckedExceptionExample {
    public static void main(String[] args) {
        File file = new File("test.txt");
        Scanner scanner = new Scanner(file); // ❌ 컴파일 오류 발생 (Unhandled exception: FileNotFoundException)
    }
}

📌 즉, try-catch를 쓰지 않으면 컴파일 오류가 나서 “반드시 처리해야 하는 예외”라는 걸 바로 알 수 있음.

 

🔹 Unchecked Exception 예제 (컴파일 오류 없음)

public class UncheckedExceptionExample {
    public static void main(String[] args) {
        String str = null;
        System.out.println(str.length()); // ❌ 실행 중 NullPointerException 발생
    }
}

📌 Unchecked Exception은 컴파일 오류가 발생하지 않아서, 실행(Runtime) 중에 오류가 터질 수 있음! 

 

 

📌 4. 결국 예외를 필수로 처리해야 하는지 직접 알아야 할까?

 

✅ YES → Checked Exception (try-catch 또는 throws 필수)

컴파일 오류가 발생하면 Checked Exception (필수 처리)

대표 예외: IOException, SQLException, InterruptedException

파일, 네트워크, 데이터베이스 작업을 할 때 발생

 

❌ NO → Unchecked Exception (개발자가 논리적으로 신경 써야 함)

컴파일 오류가 발생하지 않음 (예외 처리는 개발자의 선택)

대표 예외: NullPointerException, IndexOutOfBoundsException

• null을 참조하거나 배열 범위를 벗어날 때 발생

 

📌 즉, 코드 작성 중에 컴파일러가 “예외 처리를 하라고 하면” Checked Exception이고, 그냥 실행하다가 오류가 발생하면 Unchecked Exception! 

  • RuntimeException - UncheckedException 
    • RuntimeException 을 상속받는 모든 예외를 UncheckedException 이라고 합니다.
    • 예외처리를 컴파일러가 확인하지 않습니다.
  • Exception - CheckedException
    • Exception 클래스를 직접 상속받는 모든 예외를 CheckedException 이라고합니다. RuntimeException과 RuntimeException 을 상속받은 예외는 제외합니다.
    • 예외처리를 컴파일러가 확인해 줍니다.

 

예외 전파

  • 예외 전파는 메서드에서 발생한 예외가 해당 메서드 내에서 처리되지 않았을 때 메서드를 호출한 상위 메서드로 전달되는 과정을 말합니다.
  • 예외가 프로그램 시작 지점(main()) 까지 전파되고 끝내 처리되지 않으면 프로그램이 비정상 종료 됩니다.
더보기

RuntimeException - UncheckedException

  • 컴파일러가 예외 처리를 강제하지 않는 예외입니다.
  • 예외 처리를 하지 않아도 컴파일 오류(빨간 줄) 가 발생하지 않습니다.
  • 처리되지 않은 예외는 계속 프로그램 시작 지점까지 전파됩니다.
  • 끝내 예외가 처리되지 않으면 프로그램이 비정상적으로 종료됩니다.
  • RuntimeException 을 상속받는 모든 예외를 UncheckedException 이라고 합니다.

try-catch 활용

public class ExceptionPractice {
    public void callUncheckedException() {
        if (true) {
            System.out.println("언체크 예외 발생");
            throw new RuntimeException(); // ✅ 예외발생 
        }
    }
}
public class Main {

    public static void main(String[] args) {
        ExceptionPractice exceptionPractice = new ExceptionPractice();
        
        // ✅ 상위로 전파된 예외처리
        try {
            exceptionPractice.callUncheckedException();

        } catch (RuntimeException e) { // ✅ 예외처리
            System.out.println("언체크 예외 처리");   
            
        } catch (Exception e) {
            System.out.println("체크 예외 처리");
        }

        System.out.println("프로그램 종료");
    }
}

 


Exception - CheckedException

  • Exception 클래스를 직접 상속받는 모든 예외를 CheckedException 이라고 합니다.
    • RuntimeException 을 상속받는 예외는 제외.
  • 컴파일러가 예외 처리를 강제하는 예외입니다.
  • 예외 처리를 하지 않으면 “컴파일 오류가 발생한다(코드에 빨간줄)” 라고 이해하시면 쉽습니다.
  • 반드시 try-catch로 예외를 처리하거나 throws 키워드를 사용해야 합니다. → throws 로 예외 처리의 책임을 호출자에게 전가할 수 있습니다.

try-catch 활용

public class ExceptionPractice {

    public void callCheckedException() {
		    // ✅ try-catch 로 예외 처리
        try { 
            if (true) {
                System.out.println("체크예외 발생");
                throw new Exception();
            }
        } catch (Exception e) {
            System.out.println("예외 처리");
        }
    }
}
public class Main {
    public static void main(String[] args) {
        
        // 예외 실습 객체 인스턴스화
        ExceptionPractice exceptionPractice = new ExceptionPractice();

        // ✅ 체크예외 호출
        exceptionPractice.callCheckedException();
    }
}

 

throws 활용

  • throws 키워드를 사용하여 예외를 호출한 곳에서 처리하도록 강제하는 방식입니다. → (책임 전가) 
public class ExceptionPractice {

    public void callCheckedException() throws Exception { // ✅ throws 예외를 상위로 전파
        if (true) {
            System.out.println("체크예외 발생");
            throw new Exception();
        }
    }
}
package chapter3.exception;

public class Main {
    public static void main(String[] args) {

        // 예외 실습 객체 인스턴스화
        ExceptionPractice exceptionPractice = new ExceptionPractice();

        // 체크 예외 사용
        // ✅ 반드시 상위 메서드에서 try-catch 를 활용해 주어야합니다.
        try {
            exceptionPractice.callCheckedException();
        } catch (Exception e) {
            System.out.println("예외처리");
        }
    }
}

 

Optional 이란?

Optional 객체는 null 을 안전하게 다루게 해주는 객체입니다.


null 이란?

  • null은 프로그래밍에서 값이 없음 또는 참조하지 않음 을 나타내는 키워드

null 을 직접 다루는 대신 Optional 을 사용하면 NullPoinerException 을 방지할 수 있습니다.

 

Optional 이 왜 필요한가?

✔ Optional<T>는 “null 가능성”이 있는 값을 감싸는 래퍼 클래스.

✔ 즉, null 반환을 방지하고, NullPointerException(NPE)을 예방하는 데 사용.

✔ 자바 8부터 도입되었으며, 주로 “값이 없을 수도 있는 경우” 사용.

NullPointerException 을 방지해야 하는 이유

  • NPE 예외는 런타임 예외이고 컴파일러가 잡아주지 못합니다.
  • 예외가 발생했을 때 처리해 주지 않으면 프로그램이 종료됩니다.
더보기
  • camp.getStudent() 는 null 을 반환할 수 있는 메서드입니다.
  • 학생이 없는 경우 null을 반환하면 NPE(NullPointerException)가 발생합니다.
  • null인 객체에서 student.getName()을 호출하는 것은 존재하지 않는 객체의 메서드를 실행하려는 것입니다.
public class Student {

    // 속성
    private String name;
    
    // 생성자
    
    // 기능
    public String getName() {
        return this.name;
    }
}
public class Camp {

    // 속성
    private Student student;

    // 생성자
    
    // 기능: ⚠️ null 을 반환할 수 있는 메서드
    public Student getStudent() {
        return student;
    }
    
    public void setStudent(Student student) {
    	this.student = student;    
    }
}
public class Main {

    public static void main(String[] args) {

        Camp camp = new Camp();
        Student student = camp.getStudent(); // ⚠️ student 에는 null 이 담김
        // ⚠️ 아래 코드에서 NPE 발생! 컴파일러가 잡아주지 않음
        String studentName = student.getName(); // 🔥 NPE 발생 -> 프로그램 종료
        System.out.println("studentName = " + studentName);
    }
}

 

 

NULL 직접 처리의 한계

if문을 활용해서 null 처리를 할 수 있지만 모든 코드에서 null 이 발생할 가능성을 미리 예측하고 처리하는 것은 현실적으로 어렵습니다.

더보기
public class Main {
    public static void main(String[] args) {
        Camp camp = new Camp();
        Student student = camp.getStudent();

        String studentName;
        if (student != null) { // ⚠️ 가능은하지만 현실적으로 어려움
            studentName = student.getName();
        } else {
            studentName = "등록된 학생 없음"; // 기본값 제공
        }

        System.out.println("studentName = " + studentName);
    }
}

 

Optional 활용하기

https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html

  • Optional 객체는 값이 있을 수도 있고 없을 수도 있는 컨테이너라고 생각하시면 됩니다.
  • Optional 객체를 메서드 반환 자료형에 선언해서 해당 메서드가 null 이 반환될 가능성을 명확하게 전달할 수 있습니다.
  • Optional.ofNullable() 을 사용하여 null 이 반환될 수 있는 객체를 감쌉니다.
  • 활용할 때는 isPresent() 와 같은 Optional API 를 통해 안전하게 null 처리를 할 수 있습니다.
메서드 설명
Optional.of(value) null이 아닌 값을 감싸는 Optional 객체 생성 (null이면 NullPointerException 발생)
Optional.ofNullable(value) null 여부와 상관없이 안전하게 감쌈 (null이면 빈 Optional 반환)
Optional.empty() 비어 있는 Optional 객체 반환
Optional.isPresent() 내부 값이 null 일 경우 false 를 반환
public class OptionalExample {
    public static void main(String[] args) {
        // ✅ 값이 있는 경우
        Optional<String> optionalValue = Optional.of("Hello");
        System.out.println(optionalValue.get()); // Hello

        // ✅ 값이 null일 가능성이 있는 경우
        Optional<String> nullableValue = Optional.ofNullable(null);
        System.out.println(nullableValue.isPresent()); // false (null이면 비어있는 Optional 반환)

        // ✅ 비어있는 Optional
        Optional<String> emptyValue = Optional.empty();
        System.out.println(emptyValue.isPresent()); // false
    }
}
더보기

isPresent() 활용 방법

  • Optional 내부의 값이 존재할 경우에 true 반환합니다.
  • 내부 값이 null 일 경우 false 를 반환합니다.
import java.util.Optional;

public class Camp {

    // 속성
    private Student student;

    // 생성자

    // 기능
    // ✅ null 이 반환될 수 있음을 명확하게 표시
    public Optional<Student> getStudent() {
        return Optional.ofNullable(student); //null 가능성 대비
    }
    
    public void setStudent(Student student) {
    	this.student = student;    
    }
}
public class Main {

    public static void main(String[] args) {

        Camp camp = new Camp();
        
        // isPresent() 활용시 true 를 반환하고 싶을때 활용
        // Student newStudent = new Student();
        // camp.setStudent(newStudent);

        //  Optional 객체 반환받음
        Optional<Student> studentOptional = camp.getStudent();

        // Optional 객체의 기능 활용
        boolean flag = studentOptional.isPresent(); // false 반환
        if (flag) {
            // 존재할 경우
            Student student = studentOptional.get(); // ✅ 안전하게 Student 객체 가져오기
            String studentName = student.getName();
            System.out.println("studentName = " + studentName);

        } else {
            // null 일 경우
            System.out.println("학생이 없습니다.");
        }
    }
}

 

Optional 값 가져오기 (orElse, orElseGet, orElseThrow)

메서드 설명
get() 값이 없으면 NoSuchElementException 발생 (주의 필요)
orElse(defaultValue) 값이 있으면 반환, 없으면 defaultValue 반환
orElseGet(supplier) 값이 있으면 반환, 없으면 람다 실행 (defaultValue 대신 동적 처리 가능)
orElseThrow() 값이 없으면 NoSuchElementException 발생
orElseThrow(Supplier) 값이 없으면 커스텀 예외 발생

 

🔹 orElse와 orElseGet 차이

public class OptionalExample {
    public static void main(String[] args) {
        Optional<String> optionalValue = Optional.ofNullable(null);

        // ✅ 기본값 반환
        String value1 = optionalValue.orElse("기본값");
        System.out.println(value1); // 기본값

        // ✅ 동적 기본값 처리
        String value2 = optionalValue.orElseGet(() -> "동적 기본값");
        System.out.println(value2); // 동적 기본값

        // ✅ 예외 발생
        try {
            String value3 = optionalValue.orElseThrow(() -> new RuntimeException("값이 없습니다!"));
        } catch (RuntimeException e) {
            System.out.println(e.getMessage()); // 값이 없습니다!
        }
    }
}

📌 즉, orElse()는 기본값을 직접 제공하고, orElseGet()은 람다로 동적 처리가 가능

더보기

orElseGet() 활용 방법

  • orElseGet()은 값이 없을 때만 기본값을 제공하는 로직을 실행하는 메서드입니다.
  • orElseGet()을 제대로 활용하려면 람다 표현식을 이해해야 합니다.
  • 하지만 이 부분은 이후 수업에서 다룰 예정이므로 지금은 메서드를 매개변수로 전달한다 정도로 이해하셔도 충분합니다
import java.util.Optional;

public class Camp {

    // 속성
    private Student student;

    // 생성자

    // 기능
    // ✅ null 이 반환될 수 있음을 명확하게 표시
    public Optional<Student> getStudent() {
        return Optional.ofNullable(student);
    }
}
import java.util.Optional;

public class Main {
    public static void main(String[] args) {
        Camp camp = new Camp();

        // ✅ Optional 객체의 기능 활용 (orElseGet 사용)
        Student student = camp.getStudent()
                              .orElseGet(() -> new Student("미등록 학생"));

        System.out.println("studentName = " + student.getName());
    }
}

 

 

 Optional을 사용하면 모든 객체에 붙여야 할까?

❌ No! 모든 객체에 Optional을 사용할 필요는 없음.

Optional은 “값이 없을 수도 있는 경우”에만 사용하는 게 좋음.

“항상 값이 존재하는 필드”에는 Optional을 쓰면 오히려 불필요한 객체 할당이 늘어나 성능이 나빠질 수 있음

 

언제 Optional을 사용해야 할까?

1. null을 반환할 가능성이 있는 메서드 결과값

public Optional<String> getUsername(User user) {
    return Optional.ofNullable(user.getName());
}

 

2. 데이터베이스 조회 결과 (findById() 같은 경우)

Optional<User> user = userRepository.findById(1L);

 

3. API 응답 값 (null이 올 수도 있는 경우)

Optional<Response> response = apiClient.getResponse();

 

🚨 하지만, 필드 변수에는 Optional을 사용하지 않는 게 좋음 🚨

// ❌ Optional 필드를 사용하면 메모리 낭비가 발생할 수 있음!
public class User {
    private Optional<String> name;  // ❌ 필드에 Optional 사용 X
}

 

 

✅ Unchecked Exception vs Checked Exception vs Optional 비교 🚀

개념 Checked Exception ✅ Unchecked Exception ❌ Optional 🤔
발생 시점 컴파일 시 체크됨 (필수 처리) 런타임(Runtime) 중 발생 컴파일 시 체크 X (개발자가 선택적으로 사용)
처리 강제 여부 try-catch 또는 throws 필수 예외 처리 강제 X, 실행 중 오류 발생 가능 예외 처리 강제 X, orElse(), ifPresent() 등으로 안전하게 다룰 수 있음
사용 목적 외부적인 오류 (파일, 네트워크, DB 등) 개발자의 논리적 오류 (null 참조, 배열 범위 초과 등) null을 안전하게 다루기 위한 대체 수단
대표 예외 IOException, SQLException, InterruptedException NullPointerException, ArrayIndexOutOfBoundsException, ArithmeticException Optional<User> findById(Long id)
컴파일 시 오류 여부 예외 처리를 안 하면 컴파일 오류 발생 예외 처리를 안 해도 컴파일 오류 없음 (하지만 실행 중 오류 발생 가능) 컴파일 오류 없음 (null 대신 안전하게 값 관리 가능)
예외 처리 방식 try-catch 또는 throws try-catch (필수 아님) orElse(), orElseThrow(), ifPresent()

📌 즉, Checked Exception은 예외 처리를 강제하지만, Unchecked Exception과 Optional은 개발자가 알아서 처리해야 함.

 

📌 언제 각각을 사용해야 할까?

사용 상황 Checked Exception ✅ Unchecked Exception ❌ Optional 🤔
파일 입출력 (파일이 없을 수도 있음) IOException (파일이 없을 가능성이 있음)
네트워크 오류 (서버 다운 가능성) SQLException (DB 접속 오류)
배열의 잘못된 인덱스 접근 ArrayIndexOutOfBoundsException
null 체크 없이 안전한 코드 작성 Optional<User>
DB 조회 (findById() 결과가 없을 수도 있음) Optional<User>

 

예시

더보기

1️⃣ Checked Exception 예제 (IOException - 파일 읽기 오류)

파일이 없을 경우 예외가 발생할 가능성이 있으므로 try-catch 필수!

import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;

public class CheckedExceptionExample {
    public static void main(String[] args) {
        try {
            File file = new File("nonexistent.txt");
            Scanner scanner = new Scanner(file); // ❌ FileNotFoundException 발생 가능 → `try-catch` 필수
        } catch (FileNotFoundException e) {
            System.out.println("⚠️ 파일을 찾을 수 없습니다: " + e.getMessage());
        }
    }
}

출력 (파일이 없을 경우)

⚠️ 파일을 찾을 수 없습니다: nonexistent.txt (지정된 파일을 찾을 수 없습니다)

 

2️⃣ Unchecked Exception 예제 (NullPointerException)

개발자의 논리적 실수로 인해 실행 중 예외 발생 (컴파일 시에는 오류 없음)

public class UncheckedExceptionExample {
    public static void main(String[] args) {
        String str = null;
        System.out.println(str.length()); // ❌ NullPointerException 발생
    }
}

출력 (실행 중 오류 발생)

Exception in thread "main" java.lang.NullPointerException

 

3️⃣ Optional을 활용한 안전한 null 처리 (Optional<String>)

기존에는 null을 반환했지만, Optional을 사용하면 null 체크가 필요 없음!

import java.util.Optional;

public class OptionalExample {
    public static void main(String[] args) {
        Optional<String> name = getUserName(false); // ✅ null 대신 Optional 사용 → 안전하게 처리 가능
        System.out.println(name.orElse("기본 이름")); // 값이 없으면 "기본 이름" 반환
    }

    public static Optional<String> getUserName(boolean exists) {
        if (exists) {
            return Optional.of("홍길동");
        } else {
            return Optional.empty(); // ✅ null 대신 빈 Optional 반환
        }
    }
}

출력 결과 (exists = false일 경우)

기본 이름

 

컬렉션(Collection)이란?

컬렉션(Collection)은 데이터를 효율적으로 저장하고 관리하는 자료 구조(데이터 컨테이너)

배열(Array)처럼 여러 개의 데이터를 저장할 수 있지만, 크기가 동적으로 변경되고 다양한 기능을 제공함.

자바의 Collection Framework를 사용하면, 데이터를 쉽고 효율적으로 추가/삭제/검색할 수 있음.

더보기

https://docs.oracle.com/javase/8/docs/api/java/util/Collections.html

  • 자바 컬렉션 프레임워크는 이러한 자료구조들을 쉽게 사용할 수 있도록 인터페이스와 구현체(ArrayList, HashSet, HashMap 등)를 제공하는 집합입니다.
  • 컬렉션을 통해 데이터 저장, 조회, 삭제, 정렬 등 다양한 기능을 간편하게 구현할 수 있습니다.
  • 배열과 다르게 컬렉션은 길이를 동적으로 변경할 수 있습니다.(추가 삭제 시 유연하게 길이가 변경됩니다.)

 

배열의 한계

  • 배열은 크기가 고정되어 있어서 한 번 설정하면 길이를 변경할 수 없습니다. → 배열의 길이 초과 시 에러가 발생합니다.
  • 자바에서는 다양한 컬렉션 클래스(ArrayList, HashSet, HashMap 등)를 제공합니다.
  • 컬렉션 객체를 활용해 데이터들을 저장하고 관리할 수 있습니다.
  • 배열과 다르게 컬렉션은 길이를 동적으로 변경할 수 있습니다.(추가 삭제 시 유연하게 길이가 변경됩니다.)

 

컬렉션 종류와 특징

인터페이스 특징 구현체
List(리스트) 순서 유지, 중복 허용 ArrayList, LinkedList, Vector, Stack
Set(집합) 순서 없음, 중복 불가 HashSet, LinkedHashSet, TreeSet
Map(맵) 키-값 구조, 키 중복 불가 HashMap, LinkedHashMap, TreeMap, Hashtable

 

2차 보강

더보기

자바에서 Collection은 여러 개의 요소(데이터)를 담을 수 있는 인터페이스(설계도)를 의미

 Class처럼 객체를 만들 수는 없지만, 메서드의 구조(형태)를 정의 할 수 있음.

 인터페이스를 implements(구현)하면 해당 인터페이스에 정의된 메서드를 반드시 구현해야 함.

 

 

Collection

  • List
    • ArrayList
    • LinkedList
    • Vector
  • Set
    • HashSet
    • LinkedHashSet
    • TreeSet
  • Queue
    • PriorityQueue
    • LinkedList

Map은 Collecion의 하위 인터페이스가 아님

  •  Map
    • HashMap
    • LinkedHashMap
    • TreeMap
    • Hashtable

 

📌 Collection 인터페이스의 주요 메서드

메서드 설명 사용 가능한 컬렉션
add(E e) 요소 추가 ✅ List, Set, Queue
remove(Object o) 요소 삭제 ✅ List, Set, Queue
contains(Object o) 해당 요소가 있는지 확인 ✅ List, Set, Queue
size() 요소 개수 반환 ✅ List, Set, Queue
isEmpty() 비어있는지 확인 ✅ List, Set, Queue
clear() 모든 요소 제거 ✅ List, Set, Queue
iterator() 요소를 순회할 Iterator 반환 ✅ List, Set, Queue
toArray() 컬렉션을 배열로 반환 ✅ List, Set, Queue

 

🍒 List 인터페이스

  • List는 “순서 보장, 중복 허용”을 지원하는 컬렉션
  • 배열과 비슷하지만 크기가 자동으로 조절됨
구현체 특징 차이점
ArrayList 배열 기반, 검색 속도 빠름 삽입/삭제 느림
LinkedList 노드 기반, 삽입/삭제 빠름 검색 느림
Vector ArrayList와 유사, 동기화 지원 멀티스레드 환경에서 사용

🔹 List 인터페이스의 주요 메서드

메서드 설명
add(int index, E element) 특정 위치에 요소 추가
get(int index) 특정 위치에 요소 가져오기
set(int index, E element) 특정 위치의 요소 변경
remove(int index) 특정 위치의 요소 삭제
indexOf(Object o) 요소의 인덱스 반환
subList(int from, int to) 특정 범위의 요소 리스트 반환

 

🍒 Set 인터페이스

  • Set은 중복 없는 데이터를 저장할 때 사용
구현체 특징 차이점
HashSet 순서 없음, 빠른 검색 순서 보장 안 됨
LinkedHashSet 입력 순서 유지 정렬 없음
TreeSet 자동 정렬(오름차순) 속도 느림

🔹 Set 인터페이스의 주요 메서드

메서드 설명
add(E e) 요소 추가 (중복 허용 X)
remove(Object o) 요소 삭제
contains(Object o) 요소 포함 여부 확인
size() 요소 개수 반환
iterator() Iterator 반환

 

🍒 Queue 인터페이스

  • FIFO(선입선출)
구현체 특징 차이점
PriorityQueue 우선순위 큐(기본 오름차순) 정렬된 순서로 요소 반환
LinkedList FIFO 방식 큐 지원 일반 리스트 기능도 포함

🔹 Queue 인터페이스의 주요 메서드

메서드 설명
offer(E e) 요소 추가(add()와 유사)
poll() 첫 번째 요소 제거 및 반환
peek() 첫 번째 요소 반환(삭제 X)

 

🍒 Map 인터페이스

  • “Key-Value” 형태
구현체 특징 차이점
HashMap 순서 없음, 빠른 검색 키 순서 보장 안됨
LinkedHashMap 입력 순서 유지 느림
TreeMap 키 정렬(오름차순) 속도 느림

🔹 Map 인터페이스의 주요 메서드

메서드 설명
put(K ket, V value) 키-값 추가
get(Object key) 키에 해당하는 값 반환
remove(Object Key) 키-값 삭제
containsKey(Object key) 특정 키 존재 여부 확인
containsValue(Object value) 특정 값 존재 여부 확인

 

 

List 인터페이스를 구현한 ArrayList

  • ArrayList 는 요소의 순서를 유지하고 중복된 값을 저장할 수 있는 자료구조입니다.
  • 요소 추가 → add("값")
  • 요소 조회 → get(인덱스)
  • 요소 제거 → remove("값")
  • 대표적인 구현체로는 ArrayList , LinkedList가 있습니다.
더보기
// List 를 구현한 ArrayList
ArrayList<String> names = new ArrayList<>();
names.add("Spartan");      // 1 번째 요소 추가
names.add("Steve");        // 2 번째 요소 추가
names.add("Isac");         // 3 번째 요소 추가
names.add("1");
names.add("2");

 // ✅ 순서 보장
System.out.println("names = " + names);

// ✅ 중복 데이터 허용
names.add("Spartan");
System.out.println("names = " + names);

// ✅ 단건 조회
System.out.println("1 번째 요소 조회: " + names.get(0)); // 조회 Spartan

// ✅ 데이터 삭제
names.remove("Steve"); 
System.out.println("names = " + names);

 

 

Set 인터페이스를 구현한 HashSet

  • HashSet순서를 유지하지 않고 중복을 허용하지 않습니다.
    • → 순서를 보장하지 않기 때문에 get() 지원을 하지 않습니다.
  • 요소 추가 → add("값")
  • 요소 제거 → remove("값")
  • 대표적인 구현체로는 HashSet , TreeSet 이 있습니다.
더보기
// Set 을 구현한 HashSet
HashSet<String> uniqueNames = new HashSet<>();

// ✅ 추가
uniqueNames.add("Spartan");
uniqueNames.add("Steve");
uniqueNames.add("Isac");
uniqueNames.add("1");
uniqueNames.add("2");

// ⚠️ 순서를 보장 안함
System.out.println("uniqueNames = " + uniqueNames); 
uniqueNames.get(0); // ❌ get 사용 불가

// ⚠️ 중복 불가
uniqueNames.add("Spartan");
System.out.println("uniqueNames = " + uniqueNames); 

// ✅ 제거
uniqueNames.remove("Spartan");
System.out.println("uniqueNames = " + uniqueNames);

 

 

Map 인터페이스를 구현한 HashMap

  • HashMap 은 키(Key) - 값(Value) 구조로 데이터를 저장합니다.(키: 값)
  • 키(Key) 는 중복될 수 없지만 값(Value) 은 중복 가능합니다.
  • 순서를 보장하지 않습니다.
  • 요소 추가 → put(”키”, 값)
  • 요소 조회 → get(”키”)
  • 요소 제거 → remove("Steve")
  • 키 확인 → keySet()
  • 값 확인 → values()
  • 대표적인 구현체로는 HashMap, TreeMap 이 있습니다.
더보기
// Map 을 구현한 HashMap
HashMap<String, Integer> memberMap = new HashMap<>();

// ✅ 추가
memberMap.put("Spartan", 15);
memberMap.put("Steve", 15); // ✅ 값은 중복 가능
memberMap.put("Isac", 1);
memberMap.put("John", 2);
memberMap.put("Alice", 3);

// ⚠️ 순서 보장 안함 
System.out.println("memberMap = " + memberMap);

// ⚠️ 키 중복 불가: 값 덮어쓰기 발생
memberMap.put("Alice", 5);
System.out.println("memberMap = " + memberMap);

// ✅ 조회: 15
System.out.println(memberMap.get("Steve"));

// ✅ 삭제 가능
memberMap.remove("Spartan"); 
System.out.println("memberMap = " + memberMap);

// ✅ 키 확인
Set<String> keys = memberMap.keySet();
System.out.println("keys = " + keys);

// ✅ 값 확인
Collection<Integer> values = memberMap.values();
System.out.println("values = " + values);

 

전체 코드

더보기
package chapter2.collection;

import java.util.*;

public class Main {
    public static void main(String[] args) {

        // 배열의 한계
        // 선언과 동시에 길이를 설정 필요
        int[] numbers = new int[3];
        numbers[0] = 10;
        numbers[1] = 20;
        numbers[2] = 30;
        // 배열의 정적인 특징의 한계
//        numbers[3] = 40;

        //컬렉션
        ArrayList<Integer> arrayList = new ArrayList<Integer>();
        // <> : 제너릭 : 어떤 객체를 다룰건지
        // 두번째 <> 내용 생략가능
        arrayList.add(10);
        arrayList.add(20);
        arrayList.add(30);
        arrayList.add(40);

        // ArrayList 활용
        ArrayList<String> names = new ArrayList<>();

        // 데이터 추가
        names.add("sun");
        names.add("steve");
        names.add("Isac");
        names.add("1");
        System.out.println("names = " + names); // names = [sun, steve, Isac, 1]

        // 중복 데이터 허용
        names.add("sun");
        System.out.println("names = " + names); // names = [sun, steve, Isac, 1, sun]

        // 데이터 단건 조회
        String name1 = names.get(0);
        System.out.println("name1 = " + name1); // name1 = sun

        // 데이터 삭제
        names.remove("steve");
        System.out.println("name1 = " + names); // name1 = [sun, Isac, 1, sun]

        names.remove("sun");
        System.out.println("name1 = " + names); // name1 = [Isac, 1, sun]

        // HashSet 활용
        HashSet<String> uniqueNames = new HashSet<>();

        // 데이터 추가
        uniqueNames.add("sunn");
        uniqueNames.add("steve");
        uniqueNames.add("joo");
        uniqueNames.add("1");

        // 순서 보장 안됨 -> get() 활용 불가
        System.out.println("uniqueNames = " + uniqueNames); // uniqueNames = [1, sunn, steve, joo]

        // 중복 데이터 불가
        uniqueNames.add("sunn");
        System.out.println("uniqueNames = " + uniqueNames); // uniqueNames = [1, sunn, steve, joo]

        // 데이터 제거
        uniqueNames.remove("sunn");
        System.out.println("uniqueNames = " + uniqueNames); // uniqueNames = [1, steve, joo]


        // HashMap 활용
        HashMap<String, Integer> memberMap = new HashMap<>();
        // <키, 값> 구조로 저장

        // 데이터 저장
        memberMap.put("sun", 15);
        memberMap.put("joo", 13);
        memberMap.put("steve", 20);
        memberMap.put("alice", 3);

        // 순서 보장이 안됨
        System.out.println("memberMap = " + memberMap); // memberMap = {steve=20, alice=3, joo=13, sun=15}

        // 키 중복 불가
        memberMap.put("sun", 15);
        System.out.println("memberMap = " + memberMap); // memberMap = {steve=20, alice=3, joo=13, sun=15}

        // 단건 조회
        Integer Num = memberMap.get("sun");
        System.out.println("Num = " + Num); // Num = 15

        // 데이터 삭제
        memberMap.remove("alice");
        System.out.println("memberMap = " + memberMap); // memberMap = {steve=20, joo=13, sun=15}

        // 키 확인
        Set<String> keySet = memberMap.keySet();
        System.out.println("keySet = " + keySet); // keySet = [steve, joo, sun]

        // 값 확인
        Collection<Integer> values = memberMap.values();
        System.out.println("values = " + values); // values = [20, 13, 15]

    }
}

 

 

제네릭(Generic)이란?

  • 제네릭은 클래스, 메서드 등에 사용되는 <T>**타입 매개변수**를 의미합니다.
  • “데이터 타입을 미리 지정하지 않고, 나중에 사용할 때 타입을 결정하는 기능”
  • 제네릭을 활용하면 코드 재사용성과 타입 안정성을 보장받을 수 있습니다.
  • 하지만 과도하게 사용하면 오히려 복잡해질 수 있으므로 주의해야 합니다.

제네릭 <T>(타입매개변수)

  • <T>(타입매개변수) 는 제네릭에서 타입을 의미하는 자리입니다.
  • 실제 데이터 타입으로 대체되어 활용 됩니다.

 

타입소거(**Erasure**)

  • 타입 소거는 컴파일 시점에 제네릭 타입 정보를 제거하는 과정입니다.
    • <T> 타입 매개변수 부분은 Object 로 대체됩니다.
    • 필요한 경우 컴파일러가 자동으로 강제 다운 캐스팅(cast) 코드를 삽입하여 타입 안전성을 보장합니다.
더보기

제너릭 미사용 (형 변환 필요, 오류 발생 가능)

import java.util.ArrayList;

public class NoGenericsExample {
    public static void main(String[] args) {
        ArrayList list = new ArrayList(); // ❌ 제너릭 미사용 → Object 타입으로 저장됨

        list.add("Hello"); // 문자열 저장
        list.add(123);     // 정수 저장 (다른 타입 가능)

        String str = (String) list.get(0); // ✅ 형 변환 필요
        System.out.println(str);

        // ❌ 형 변환 오류 (Integer → String)
        String num = (String) list.get(1); // ClassCastException 발생
    }
}

 

제너릭 사용 

import java.util.ArrayList;

public class GenericsExample {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>(); // ✅ 제너릭 사용 → String 타입만 저장 가능

        list.add("Hello"); // ✅ 정상
        // list.add(123);  // ❌ 컴파일 오류 발생! (Integer는 저장 불가능)

        String str = list.get(0); // ✅ 형 변환 없이 안전하게 가져오기 가능
        System.out.println(str);
    }
}

 

 

제너릭 클래스

  • 제네릭 클래스는 클래스 선언부에 <T> 가 선언된 클래스입니다.
  • 제네릭 클래스는 클래스 선언 시 타입 매개변수를 사용해 다양한 데이터 타입을 안전하게 처리할 수 있는 구조입니다.
  • GenericBox<T> 를 활용해서 String, Integer, Double 등 다양한 타입 저장 가능 합니다.
더보기
// ✅ 제너릭 클래스 정의 (T는 타입 변수)
class Box<T> {
    private T value;

    public void set(T value) { this.value = value; }
    public T get() { return value; }
}

public class GenericsClassExample {
    public static void main(String[] args) {
        Box<String> stringBox = new Box<>(); // ✅ String 타입 지정
        stringBox.set("Hello");
        System.out.println(stringBox.get()); // Hello

        Box<Integer> intBox = new Box<>(); // ✅ Integer 타입 지정
        intBox.set(123);
        System.out.println(intBox.get()); // 123
    }
}

 

 

제네릭 메서드(Generic Method)

  • 제네릭 메서드는 메서드 선언부에 <T> 가 선언된 메서드입니다.
  • 제네릭 메서드는 클래스 제네릭 타입과 별개로 독립적인 타입 매개변수를 가집니다.
더보기
public class GenericMethodExample {
    // ✅ 제너릭 메서드 (T는 메서드에서만 사용)
    public static <T> void print(T value) {
        System.out.println(value);
    }

    public static void main(String[] args) {
        print("Hello");  // ✅ String 사용
        print(123);      // ✅ Integer 사용
        print(3.14);     // ✅ Double 사용
    }
}

 

 

자바에서 <T> 제네릭이 활용된 곳

  • Optional<T>ArrayList<T> 컬렉션 클래스 등은 제네릭 클래스입니다.
  • 여기서 <T>는 실제 데이터 타입으로 대체되어 활용됩니다.

 

전체 코드

더보기
package chapter2.generic;

public class GenericBox<T> {
    private T item;

    public GenericBox(T item){
        this.item = item;
    }

    public T getItem() {
        return item;
    }
    
    // 일반 메서드
    public void printItem(T item){
        System.out.println("item = " + item);
    }
    
    // 제너릭 메서드
    // 클래스의 타입 매개변수와, 메서드의 타입 매개변수는 별도로 작동 함
    public <S> void printBoxItem(S item){
        System.out.println("item = " + item);
    }
}
package chapter2.generic;

public class Main {
    public static void main(String[] args) {

        // 1. 재사용 불가
        Box box1 = new Box(100);
        //다른 데이터 타입 생성 불가
        // Box box2 = new Box("String");

        // 2. 낮은 타입 안정성
        ObjectBox strBox = new ObjectBox("ABC");
        ObjectBox intBox = new ObjectBox(100);

        // item을 활용하기 위해서는 다운캐스팅 필요
        String item = (String) strBox.getItem();
        // Object형을 반환하는데 해당 데이터 타입을 바로 출력할 수 없어서 변환해줌
        System.out.println("item = " + item); // item = ABC

        // 다운캐스팅은 잘못 사용하면 컴파일러가 미리 오류를 잡아주지 못함
//        String item2 = (String) intBox.getItem();

        // Generic 활용
        // 1. 재사용성 보장(타입소거 T -> Object)
        GenericBox<String> strGBox = new GenericBox<>("ABC");
        GenericBox<Integer> intGBox = new GenericBox<>(100);
        GenericBox<Double> doubleGBox = new GenericBox<>(1.1);

        // 2. 타입 안정성 보장(타입소거 : 자동으로 다운캐스팅 삽입)
        // 컴파일러가 타입소거 과정에서 알아서 형변환 해줌
        String strGBoxItem = strGBox.getItem();
        System.out.println("strGBoxItem = " + strGBoxItem); // strGBoxItem = ABC

        Integer intGBoxItem = intGBox.getItem();
        System.out.println("intGBoxItem = " + intGBoxItem); // intGBoxItem = 100

        Double doubleGBoxItem = doubleGBox.getItem();
        System.out.println("doubleGBoxItem = " + doubleGBoxItem); // doubleGBoxItem = 1.1

        // 일반 메서드
        strGBox.printItem("ABC");
        // strGBox.printItem(100); // 인스턴스 생성 시 타입 매개변수가 선언됐기 때문에 사용 불가

        // 제너릭 메서드
        strGBox.printBoxItem(100);
        strGBox.printBoxItem("ABC");
        strGBox.printBoxItem(5.5);
        // 클래스의 타입 매개변수와 별도로 작동함

    }
}

 

익명 클래스란(Anonymous Class)?

익명 클래스: 이름이 없는 “일회성 클래스” (한 번만 사용할 클래스)

 

람다(Lambda)

람다(Lambda): 익명 클래스를 더 간결하게 표현한 것 (주로 함수형 인터페이스(메서드가 1개만 있는 인터페이스)에서 사용)

더보기

익명 클래스

  • 별도의 클래스 파일을 만들지 않고 코드 내에서 일회성으로 정의해 사용하기 때문에 이름이 없다고 부릅니다.
  • 인터페이스, 클래스(일반, 추상)의 구현과 상속을 활용해 익명 클래스를 구현할 수 있습니다.
    • → 람다에서는 인터페이스를 사용한 익명 클래스가 활용됩니다.

람다 

  • 함수형 인터페이스 를 통해서 구현하는 것을 권장합니다.
    •  하나의 추상 메서드만 가져야하기 때문입니다.
    • → 하지만, 하나의 추상 메서드를 가진 일반 인터페이스를 통해서도 사용 가능합니다.

 

람다식을 활용한 익명 클래스 변환 방법

  • 컴파일 시점에 컴파일러가 (a, b) -> a + b 람다 표현식을 보고 sum() 메서드를 가진 익명 클래스를 구현합니다.
  • Calculator 인터페이스에 추상 메서드가 하나뿐이기 때문에 컴파일러는 (a, b) -> a + b 람다 표현식이sum() 메서드라고 추론 가능하기 때문입니다.
// 람다 표현식
Calculator calculator1 = (a, b) -> a + b;

// 익명클래스
Calculator calculator1 = new Calculator() {
		@Override
		public int sum(int a, int b) {
				return a + b;
		}
};
@FunctionalInterface // ✅ 함수형 인터페이스 선언
public interface Calculator {

    int sum(int a, int b); // ✅ 오직 하나의 추상 메서드만 선언해야합니다.
}
public class Main {

    public static void main(String[] args) {
    
		    ...

        // ✅ 람다식 활용
        Calculator calculator2 = (a, b) -> a + b;
        int ret2 = calculator2.sum(2, 2);
        System.out.println("ret2 = " + ret2);
    }
}

 

람다 사용시 주의사항

람다식을 활용할때는 꼭 함수형 인터페이스를 활용합시다.

  • 함수형 인터페이스 단 하나의 추상 메서드만 가지도록 강제하는 어노테이션입니다.
  • 람다식에서는 함수형 인터페이스가 활용됩니다.
  • 인터페이스에 두 개 이상의 추상 메서드가 존재하면 컴파일러가 어떤 메서드를 구현하는지 모호해지기 때문입니다.
  • 예를 들어 오버로딩(Overloading) 기능을 통해 같은 이름의 sum() 메서드를 여러 형태로 정의한다면 람다 표현식이 어떤 메서드를 구현하는 것인지 명확하지 않아 모호성이 발생할 수 있습니다.

 

1️⃣ 익명 클래스로 작성한 코드

interface MyInterface {
    void show();
}

public class AnonymousClassExample {
    public static void main(String[] args) {
        MyInterface obj = new MyInterface() {
            @Override
            public void show() {
                System.out.println("익명 클래스 실행!");
            }
        };
        obj.show();
    }
}

 

2️⃣ 람다 표현식으로 변환 (()-> {} 사용)

interface MyInterface {
    void show();
}

public class LambdaExample {
    public static void main(String[] args) {
        // ✅ 익명 클래스를 람다로 변환
        MyInterface obj = () -> System.out.println("람다 실행!");
        obj.show();
    }
}

 

📌 함수형 인터페이스(@FunctionalInterface)와 람다

 

람다는 “함수형 인터페이스”에서만 사용 가능.

“함수형 인터페이스”는 “메서드가 1개만 있는 인터페이스”를 의미.

@FunctionalInterface를 붙여서 “람다에서 사용할 인터페이스”라고 표시

 

🔹 @FunctionalInterface 예제

@FunctionalInterface
interface Calculator {
    int add(int a, int b);
}

public class LambdaExample {
    public static void main(String[] args) {
        // ✅ 람다 표현식으로 함수형 인터페이스 구현
        Calculator calc = (a, b) -> a + b;

        System.out.println(calc.add(5, 3)); // 8
    }
}

 

 

오버로딩(Overloading) vs 오버라이딩(Overriding)

  • **오버로딩**은 같은 클래스나 인터페이스 내에서 동일한 메서드 이름을 사용해서 선언하는 기능입니다. → 매개변수의 개수나 타입, 순서는 다르게 선언해야 합니다.
  • **오버라이딩**은 부모 클래스에 정의된 메서드를 자식 클래스에서 재정의하는 것을 의미합니다.

 

람다식을 매개변수로 전달하는 방법

익명 클래스를 변수에 담아 전달

  • 람다식 없이 직접 객체를 생성해서 전달하는 방식입니다.
  • 클래스의 익명 객체를 만든 다음에 매개변수로 전달합니다.
더보기
public class Main {
    public static int calculate(int a, int b, Calculator calculator) {
        return calculator.sum(a, b);
    }

    public static void main(String[] args) {

        Calculator cal1 = new Calculator() {
            @Override
            public int sum(int a, int b) {
                return a + b;
            }
        };

        // ✅ 익명 클래스를 변수에 담아 전달
        int ret3 = calculate(3, 3, cal1);
        System.out.println("ret3 = " + ret3); // 출력: ret3 = 6
    }
}

람다식을 변수에 담아 전달

  • 람다식을 변수에 담아 매개변수로 전달하는 방식입니다.
  • 람다식을 전달하면 calculate() 메서드의 매개변수의 타입으로 Calculator 인터페이스를 구현했는지 추론되기 때문에 람다식을 전달 가능합니다.
더보기
public class Main {

    public static int calculate(int a, int b, Calculator calculator) {
        return calculator.sum(a, b);
    }

    public static void main(String[] args) {
        Calculator cal2 = (a, b) -> a + b;
        
        // ✅ 람다식을 변수에 담아 전달
        int ret4 = calculate(4, 4, cal2);
        System.out.println("ret4 = " + ret4); // 출력: ret4 = 8
    }
}

람다식을 직접 전달

  • 람다식을 직접 전달합니다.
  • 마찬가지로 calculate() 메서드의 매개변수의 타입으로 Calculator 인터페이스를 구현했는지 추론됩니다.
더보기
public class Main {

    public static int calculate(int a, int b, Calculator calculator) {
        return calculator.sum(a, b);
    }

    public static void main(String[] args) {
        // ✅ 람다식을 직접 매개변수로 전달
        int ret5 = calculate(5, 5, (a, b) -> a + b);
        System.out.println("ret5 = " + ret5); // 출력: ret5 = 10
    }
}

 

 

 

스트림(stream) 이란?

스트림(Stream)은 “데이터 처리 기능”을 제공하는 자바의 기능.

배열이나 컬렉션(List, Set, Map)의 요소를 쉽게 “필터링, 변환, 정렬, 집계”할 수 있음.

반복문(for)을 사용하지 않고, 더 간결하고 효율적인 코드로 데이터를 다룰 수 있음.

filter(), map(), sorted(), collect() 같은 “체이닝 방식”으로 가독성이 좋아짐.

  • 스트림은 데이터를 효율적으로 처리할 수 있는 흐름입니다.
  • 선언형 스타일로 가독성이 굉장히 뛰어납니다.
  • 데이터 준비 → 중간 연산 → 최종 연산 순으로 처리됩니다.
  • 스트림은 컬렉션(List, Set 등)과 함께 자주 활용됩니다.

스트림 처리 단계

단계 설명 주요 API
1. 데이터 준비 컬렉션을 스트림으로 변환 stream(), parallelStream()
2. 중간 연산 등록
(즉시 실행되지 않음)
데이터 변환 및 필터링 map(), filter(), sorted()
3. 최종 연산 최종 처리 및 데이터 변환 collect(), forEach(), coumt()

 

스트림 예제 전체 코드

더보기
  • stream() → map() → collect() 순으로 데이터 흐름을 처리합니다.
  • stream(): 데이터 준비 - 데이터를 스트림으로 변환하여 연산 흐름을 만들 준비합니다.
  • map(): 중간 연산 등록 - 각 요소를 주어진 함수에 적용해서 변환합니다.
  • collect(): 최종 연산 - 결과를 원하는 형태(List, Set)로 수집합니다.
  • 반복문 없이 간결하게 데이터 변환이 가능합니다.
package chapter2.stream;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

public class Main {
    public static void main(String[] args) {

        List<Integer> arrayList = new ArrayList<Integer>(List.of(1,2,3,4,5));

        // for 명령형 스타일 : 각 요소 * 10 처리
        List<Integer> ret1 = new ArrayList<>();
        for(Integer num : arrayList){
            Integer multipliedNum = num * 10;
            ret1.add(multipliedNum);
        }
        System.out.println("ret1 = " + ret1);

        // stream 선언형 스타일: 각 요소 * 10 처리
        List<Integer> ret2 = arrayList.stream().map(num -> num *10).collect(Collectors.toList());
        System.out.println("ret2 = " + ret2);
        // steam > map > collect
        // steam : 데이터를 스트림으로 변환해서 데이터 준비
        // map : 중간 연산 등록 단계, 데이터를 어떻게 처리 할 것인지 명시
        // collect : 최종 연산, 결과를 어떤 형태로 받을지 명시 해주는 것

        // ArrayList를 List로 받는 이유
        // 코드 수정을 최소화 하기 위해
        // ArrayList는 List 중 한 종류다. 다른 타입으로 변경하기 위해


        //스트림과 람다식을 활용
        // 1. 익명 클래스를 만들어서 변수에 담아 매개변수로 전달

        Function<Integer, Integer> function = new Function<Integer, Integer>(){
            @Override
            public Integer apply(Integer integer){
                return integer * 10;
            }
        };

        List<Integer> ret3 = arrayList.stream().map(function).collect(Collectors.toList());
        System.out.println("ret3 = " + ret3);

        // 2. 람다식을 만들어서 변수에 담아 매개변수로 전달
        Function<Integer, Integer> functionLambda = (integer -> integer*10);
        List<Integer> ret4 = arrayList.stream()
                .map(functionLambda)
                .collect(Collectors.toList());
        System.out.println("ret4 = " + ret4);

        // 3. 람다식을 직접 매개변수로 전달
        List<Integer> ret5 = arrayList.stream()
                .map(num -> num*10)
                .collect(Collectors.toList());
        System.out.println("ret5 = " + ret5);

        // 4. 중간 연산 함께 사용 방법
        // filter(), map()
        // 리스트에서 짝수를 찾아서 * 10
        List<Integer> ret6 = arrayList.stream()
                .filter(num -> num % 2 == 0)
                .map(num -> num * 10)
                .collect(Collectors.toList());
        System.out.println("ret6 = " + ret6);

    }
}