⚒️ Java 개념 확장

앙지동·2025년 3월 3일
1

JAVA

목록 보기
4/6
post-thumbnail

📚 예외처리

  • 프로그램 실행 중 예상치 못한 문제/오류가 발생 하는것
  • try 블록 → 예외가 발생할 가능성이 있는 코드
  • catch 블록 → 예외 발생 시 실행할 코드

💡예외 발생하면?
→ 프로그램이 멈추고 에러가 뜬다! (사용자 입장에서 불편)
→ 예외 처리를 하면? 프로그램이 멈추지 않고 원하는 메시지를 띄우고 정상 실행됨

🆘 예외처리 요약

구분 체크 예외 (Checked Exception) 런타임 예외 (Unchecked Exception)
예외 처리 강제 여부 ✅ 반드시 처리해야 함 (예외를 잡거나, 다른 메서드로 넘겨야 함) ❌ 처리하지 않아도 컴파일에 문제 없음
예외 발생 시점 컴파일 시점에 발생: 코드 작성 시점에서 경고가 뜸 실행 시점에 발생: 프로그램이 실행될 때 예외가 발생
예외 계층 구조 Exception을 상속하지만, RuntimeException은 제외 RuntimeException을 상속
대표 예외 IOException, SQLException, InterruptedException 등 NullPointerException, ArithmeticException, ArrayIndexOutOfBoundsException 등
사용 사례 파일 읽기, 데이터베이스 연결 등 외부 시스템과 관련된 오류 코드에서 잘못된 로직이나 논리 오류로 발생
예외 처리 방법 반드시 예외를 처리해야 함 (예: try-catch 또는 throws 사용) 예외 처리를 선택적으로 할 수 있음 (처리하지 않아도 실행 가능)

📕언체크 예외

  • 컴파일 단계에서 체크되지않고 실행중에 확인하는 예외
  • try-catch 없이도 코드를 작성할 수 있지만 실행 중 오류 발생 가능하다는걸 기억하기 (예외 처리를 강제하지 않음)
  • 예외 처리를 하지 않아도 컴파일 오류(빨간 줄)이 발생하지 않음
예외 종류 설명 발생 예제
ArithmeticException 0으로 나누기 int a = 10 / 0;
NullPointerException null 객체에 접근 String s = null; s.length();
ArrayIndexOutOfBoundsException 배열 범위 초과 접근 int[] arr = new int[3]; arr[5] = 10;
ClassCastException 잘못된 형 변환 Object obj = 10; String str = (String) obj;

📌 UncheckedException 예시

public class ExceptionExample {
    public static void main(String[] args) {
        try {    // ✅ try 예외처리
            int result = 10 / 0; // ❌ 0으로 나누기 → 오류 발생!
        } catch (ArithmeticException e) {// ✅ 예외 발생 
            System.out.println("오류: 0으로 나눌 수 없습니다.");
        }
        System.out.println("프로그램 정상 실행!");
    }
}

✅ 출력 결과
오류: 0으로 나눌 수 없습니다.
프로그램 정상 실행!

📗 체크 예외

  • 컴파일러가 예외퍼리를 강제하는 예외
  • 예외 처리를 하지 않으면 컴파일 오류(빨간 줄) 발생
  • 반드시 try-catch로 직접 처리하거나 throws 키워드로 예외를 호출한 곳에 위임해야 함
  • Exception 클래스를 상속받는 예외(RuntimeException 제외)
  • 메인 메서드에서 처리하거나 상위 메서드로 throws로 던져주어야 함
예외 종류 설명 발생 예제
IOException 파일이나 네트워크 등의 입출력 작업에서 문제가 발생 FileReader fr = new FileReader("nonexistentfile.txt");
SQLException 데이터베이스와 관련된 오류 (예: 잘못된 SQL 쿼리) Connection con = DriverManager.getConnection("jdbc:mysql://localhost:3306/db", "user", "pass");
InterruptedException 스레드가 일시적으로 중단될 때 발생 Thread.sleep(1000);
FileNotFoundException 파일을 찾을 수 없을 때 발생 FileInputStream fis = new FileInputStream("missingfile.txt");

📌 Checked Exception 예시(파일읽기)

✔️ throws 예외를 호출한 곳에서 처리하도록 위임

       // ✅ 예외가 발생할 가능성이 있는 코드
public class CheckedExceptionExample {
    public static void main(String[] args) {
        try {
            readFile("test.txt"); // ⭐️ 예외 발생 가능
        } catch (IOException e) { 
            System.out.println("파일을 읽는 중 오류 발생: " + e.getMessage());
        }
    }
     // ✅  예외를 호출한 곳에서 처리하도록 위임
    // `throws`를 사용하여 예외를 호출한 곳에서 처리하도록 위임
    public static void readFile(String fileName) 
    throws IOException {  ⭐️ throws 사용
        BufferedReader reader = new BufferedReader(new FileReader(fileName));
        System.out.println(reader.readLine());
        reader.close();
    }
}

        
✅ 출력결과 (파일이 없을 경우)
파일을 읽는 중 오류 발생: test.txt (파일이 존재하지 않습니다.)
        

✔️ try-catch로 직접 예외를 처리할 수도 있음

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_null을 안전하게 처리

  • null 값이 있는 변수를 사용하려 하면 Null에러 발생!
  • null이란? 객체가 참조할 값이 없을 때 사용되는 특별한 값
  • 문제를 해결하기 위해 Optional을 사용하기!
  • null은 프로그래밍에서 값이 없음 또는 참조하지 않음 을 나타내는 키워드
메서드 설명 예제
isPresent() 값이 존재하는지 확인 (true / false) studentOptional.isPresent()
orElse() 값이 없을 때 기본값 제공 orElse(new Student("기본값"))
orElseGet() 값이 없을 때만 기본값 생성 (지연 로딩) orElseGet(() -> new Student("기본값"))

1️⃣ isPresent()예시 - 값 존재 여부 확인

✔️ Optional 내부에 값이 존재하면 true, 없으면 false 반환

import java.util.Optional;

// 캠프 클래스
class Camp {
    private Student student; // 학생 정보 (null 가능)

    // ✅ 학생 정보를 Optional로 반환
    public Optional<Student> getStudent() {
        return Optional.ofNullable(student);
    }

    // 학생 정보 설정(객체생성)
    public void setStudent(Student student) {
        this.student = student;
    }
}

// 학생 클래스
class Student {
    private String name;

    public Student(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

// 실행 클래스 (Main)
public class Main {
    public static void main(String[] args) {
        Camp camp = new Camp(); // ✅ 학생 정보가 없는 캠프 객체 생성

        Optional<Student> studentOptional = camp.getStudent();

        if (studentOptional.isPresent()) { // ✅ 값이 있는 경우
            Student student = studentOptional.get(); // 안전하게 값 가져오기
            System.out.println("학생 이름: " + student.getName());
        } else { // ❌ 값이 없는 경우
            System.out.println("학생이 없습니다.");
        }
    }
}

✅ 출력결과 
학생이 없습니다.

2️⃣ orElse()예시 - 기본값 설정

✔️ 값이 없을 경우 "미등록 학생"이라는 기본값을 제공

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

        // ✅ 값이 없을 경우 기본값 제공
        Student student = camp.getStudent().orElse(new Student("미등록 학생"));

        System.out.println("학생 이름: " + student.getName());
    }
}
✅ 출력결과
학생 이름: 미등록 학생

3️⃣ orElseGet()예시 - 기본값을 필요할 때만 생성

✔️ orElse()와 비슷하지만, 필요할 때만 "미등록 학생" 생성 (성능 최적화)

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

        // ✅ 값이 없을 경우 필요할 때만 기본값 생성
        Student student = camp.getStudent().orElseGet(() -> new Student("미등록 학생"));

        System.out.println("학생 이름: " + student.getName());
    }
}
✅ 출력결과
학생 이름: 미등록 학생

📚 컬렉션

  • 데이터 저장, 조회, 삭제, 정렬 등 다양한 기능을 간편하게 구현할수있음
  • 배열보다 더 유연하게 데이터를 저장하고 관리할 수 있는 자료구조
  • 추가 삭제 시 유연하게 길이가 변경됨

📕 배열의 한계

  • 배열은 크기가 고정되어 있어서 한 번 설정하면 길이를 변경할 수 없음
    → 배열의 길이 초과 시 에러가 발생

✔️ 배열예시

// 배열은 길이가 고정됨
int[] numbers = new int[3];
numbers[0] = 10;
numbers[1] = 20;
numbers[2] = 30;
numbers[3] = 40; // ❌ 요소 추가시 에러발생

✔️ 컬렉션예시

컬렉션객체<자료형> 변수이름 = new 컬렉션객체<자료형>();

// 객체<다룰 데이터: 정수> 변수이름 = new 컬렉션객체생성자<정수>();
ArrayList<Integer> arrayList = new ArrayList<Integer>();
ArrayList<Integer> arrayList = new ArrayList<>();
arrayList.add(10);
arrayList.add(20);
arrayList.add(30);
arrayList.add(40); // ✅ 정상 동작 (길이 제한 없음)

📌 배열 vs 컬렉션

컬렉션 순서 유지 중복 허용 특징
ArrayList ✅ 유지 ✅ 가능 배열처럼 사용, 크기 자동 조절
HashSet ❌ 없음 ❌ 불가 중복 제거, 빠른 검색
HashMap ❌ 없음 ✅ (Key는 중복 불가, Value는 가능) Key-Value 저장, 빠른 검색

1️⃣ List 구현 ArrayList

  • 배열처럼 순서 유지하면서 유연하게 관리
  • 중복된 데이터 저장 가능
  • 크기를 자동으로 조절할 수 있음

💡 대표적인 구현체 ArrayList , LinkedList 있음

  • 요소 추가 → add("값")
  • 요소 조회 → get(인덱스)
  • 요소 제거 → remove("값")

📌 ArrayList 예시

// 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);

2️⃣ Set 구현 HashSet

  • HashSet 은 순서를 유지하지 않고 중복안댕
    → 순서를 보장하지 않기 때문에 get()(요소 조회) 지원안댕
  • 중복 데이터 저장 불가능!
  • 검색 속도가 빠르다는 장점

⚠️ 왜 순서가 랜덤?

👉 HashSet은 내부적으로 빠른 검색을 위해 저장 위치를 자동으로 정리하기 때문임!
💡 대표적인 구현체로는 HashSet , TreeSet

  • 요소 추가 → add("값")
  • 요소 제거 → remove("값")

📌 HashSet 예시

import java.util.HashSet;

public class HashSetExample {
    public static void main(String[] args) {
        // ✅ HashSet 생성
        HashSet<String> set = new HashSet<>();

        // ✅ 데이터 추가
        set.add("사과");
        set.add("바나나");
        set.add("딸기");
        set.add("사과"); // ❌ 중복 저장 불가 (무시됨)

        // ✅ 데이터 출력
        System.out.println("과일 집합: " + set);
    }
}
✅ 출력결과
과일 집합: [바나나, 사과, 딸기]

3️⃣ Map 구현 HashMap

  • HashMap키(Key) - 값(Value) 구조로 데이터를 저장
  • 키(Key) 는 중복될 수 없지만 값(Value) 은 중복 가능
  • 순서를 보장하지않음

⚠️ Key 값이 중복될 경우, 나중에 넣은 값으로 덮어쓰기 됨!
💡 대표적인 구현체로는 HashMap, TreeMap

  • 요소 추가 → put(”키”, 값)
  • 요소 조회 → get(”키”)
  • 요소 제거 → remove("Steve")
  • 키 확인 → keySet()
  • 값 확인 → values()

📌 HashMap 예시

import java.util.HashMap;

public class HashMapExample {
    public static void main(String[] args) {
        // ✅ HashMap 생성 (Key: 학생 이름, Value: 점수)
        HashMap<String, Integer> scores = new HashMap<>();

        // ✅ 데이터 추가
        scores.put("지연", 90);
        scores.put("민수", 85);
        scores.put("철수", 95);
        scores.put("지연", 100); // 🔥 "지연" 키가 중복되므로 100으로 덮어쓰기 됨

        // ✅ 데이터 가져오기
        System.out.println("지연의 점수: " + scores.get("지연"));

        // ✅ 전체 출력
        System.out.println("전체 점수: " + scores);
    }
}
✅ 출력결과
지연의 점수: 100
전체 점수: {철수=95, 지연=100, 민수=85}

📒 제네릭

  • <T> 는 임시 타입 변수로, 어떤 타입이든 사용가능함
  • 타입을 미리 지정하지 않고 사용 시점에 유연하게 결정할 수 있는 문법
  • 코드 재사용성과 타입 안정성을 보장
  • 여러 타입(String, Integer, Double 등)에서 같은 코드를 사용
타입 매개변수 의미 예제
<T> 일반적인 타입 (Type) Box<T>, List<T>
예: List<String> list = new ArrayList<>;
<E> 컬렉션 요소 (Element) List<E>, Set<E>
예: List<String> list = new ArrayList<>;
<K> Key (키) Map<K, V>
예: HashMap<Integer, String> map = new HashMap<>;
<V> Value (값) Map<K, V>
예: HashMap<Integer, String> map = new HashMap<>;
<N> 숫자 타입 제한 (Number) Class<N extends Number>
예: Class<Integer> intClass = Integer.class;
<S> Subtype (하위 타입) Class<S extends T>
예: Class<? extends Number> numberClass = Integer.class;
<X, Y, Z> 두 개 이상의 타입 지정 Pair<X, Y>
예: Pair<String, Integer> pair = new Pair<>;

1️⃣ 제네릭 클래스

✔️ 클래스 선언부에 <T> 가 선언된 클래스
✔️ 타입을 미리 지정하지 않고, 사용할 때 결정가능
✔️ 제네릭을 사용하면 코드 재사용성이 높아짐

 // ✅ <T>를 사용하면 다양한 타입을 저장할 수 있음
public class GenericBox<T> { //  제네릭 클래스 선언
    private T item; // <T> 타입을 저장할 변수

    public GenericBox(T item) { //  생성자
        this.item = item;
    }

    public T getItem() { // 값을 반환하는 메서드
        return this.item;
    }
}

public class Main {
    public static void main(String[] args) {
        // ✅ 제네릭을 활용해 다양한 타입 저장 가능
        GenericBox<String> strBox = new GenericBox<>("Hello");
        GenericBox<Integer> intBox = new GenericBox<>(100);
        GenericBox<Double> doubleBox = new GenericBox<>(3.14);

        // ✅ 값 가져오기
        System.out.println("strBox: " + strBox.getItem());  // 출력: Hello
        System.out.println("intBox: " + intBox.getItem());  // 출력: 100
        System.out.println("doubleBox: " + doubleBox.getItem());  // 출력: 3.14
    }
}


✅ 출력결과
strBox: Hello  
intBox: 100  
doubleBox: 3.14  


2️⃣ 제네릭 메서드

✔️ 클래스뿐만 아니라 메서드에도 적용할수있음
✔️ 제네릭 메서드는 특정 타입(T)에 의존하지 않고 모든 타입을 사용할수있도록함
✔️ <S> 처럼 독립적인 타입 매개변수를 가질 수 있음

public class GenericBox<T> {

    // 속성
    private T item;

    // 생성자
    public GenericBox(T item) {
        this.item = item;
    }

    // 기능
    public T getItem() {
        return this.item;
    }

		// ⚠️ 일반 메서드 T item 는 클래스의 <T> 를 따라갑니다.
    public void printItem(T item) {
        System.out.println(item);
    }
    
    // ✅ 제네릭 메서드 <S><T> 와 별개로 독립적이다.
    public <S> void printBoxItem(S item) { 
        System.out.println(item);
    }
}

public class Main {

    public static void main(String[] args) {
        GenericBox<String> strGBox = new GenericBox<>("ABC");
        GenericBox<Integer> intGBox = new GenericBox<>(100);
        
        // ⚠️ 일반메서드: 클래스 타입 매개변수를 따라갑니다.
        // String 데이터 타입 기반으로 타입소거가 발생.
        // String 타입의 다운캐스팅 코드 삽입!
        strGBox.printItem("ABC"); // ✅ String 만 사용가능
        strGBox.printItem(100);   // ❌ 에러 발생 
        
        // ✅ 제네릭 메서드: 독립적인 타입 매개변수를 가집니다.
        // String 타입 정보가 제네릭 메서드에 아무런 영향을 주지 못함.
        // 다운캐스팅 코드 삽입되지 않음.
        strGBox.printBoxItem("ABC"); //✅ 모든 데이터 타입 활용 가능
        strGBox.printBoxItem(100);   //✅ 모든 데이터 타입 활용 가능
        strGBox.printBoxItem(0.1);   //✅ 모든 데이터 타입 활용 가능
    }
}

✅ 출력결과
Item: Hello  
Another Item: 123  
Another Item: 3.14  
Another Item: Java  

📘 익명 클래스 & 람다식

  • 익명 클래스는 별도 클래스 파일을 만들지 않고 코드 내에서 한 번만 사용할 클래스를 정의할 때 사용함
  • 람다식은 익명 클래스를 더 간결하게 표현하는 문법!

1️⃣ 익명클래스

✔️ 별도의 클래스 파일을 만들지 않고 코드 내에서 일회성으로 정의해 사용하기 때문에 이름이 없다고 부름
✔️ 코드가 길어지지만 복잡한 로직을 구현할 때 유용함

 @FunctionalInterface // 함수형 인터페이스 선언
public interface Calculator {
    int sum(int a, int b); // 오직 하나의 추상 메서드만 선언해야 함
}

public class Main {
    public static void main(String[] args) {
        // 익명 클래스 사용
        Calculator calculator1 = new Calculator() {
            @Override
            public int sum(int a, int b) {
                return a + b;
            }
        };
        int ret1 = calculator1.sum(2, 2);
        System.out.println("익명 클래스에서 계산된 값: " + ret1);
    }
}
✅ 출력결과
익명 클래스에서 계산된 값: 4

2️⃣ 람다(Lambda)

✔️ 람다식은 간결하고 직관적인 문법으로 단순한 로직에 적합
✔️ 함수형 인터페이스 를 통해서 구현하는 것을 권장함
→ 하나의 추상 메서드만 가져야하기 때문
→ 하나의 추상 메서드를 가진 일반 인터페이스를 통해서도 사용 가능

// ✅ 람다 표현식 (간결함)
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);
    }
}
✅ 출력결과
람다식에서 계산된 값: 4

⚠️ 람다 주의사항

1. 함수형 인터페이스 활용

  • 람다식은 함수형 인터페이스에서만 사용 가능
  • 함수형 인터페이스는 단 하나의 추상 메서드만 가지고 있어야 함
    → 강제하는 어노테이션 @FunctionalInterface
@FunctionalInterface
public interface Calculator {
    int sum(int a, int b); // ✅ 하나의 추상 메서드만 선언 가능
}

2. 오버로딩과 람다식의 관계

  • 같은 이름의 메서드를 여러 형태로 정의하는 것
    → 매개변수의 개수나 타입이 다르면 오버로딩이 가능
  • 주의! 람다식은 오버로딩된 메서드를 구현할 때 모호성이 발생할 수 있음
    → ex) 같은 이름의 sum() 메서드를 매개변수 개수가 다르게 정의하면 람다식에서 어떤 메서드를 구현해야 할지 모호해짐
public interface Calculator {
    int sum(int a, int b);          // ✅ 선언 가능
    int sum(int a, int b, int c);   // ⚠️ 오버로딩 시 모호성 발생!
}
  • 함수형 인터페이스는 반드시 하나의 추상 메서드만!!
@FunctionalInterface
public interface Calculator {
    int sum(int a, int b); // ✅ 단 하나의 추상 메서드만 선언 가능
    int sum(int a, int b, int c); // ❌ 선언 불가, 컴파일 오류 발생!
}

텍스트오버로딩 vs 오버라이딩

  • 오버로딩 : 같은 클래스나 인터페이스 내에서 같은 이름의 메서드를 매개변수의 개수나 타입 순서를 달리하여 선언하는 기능
  • 오버라이딩 : 부모 클래스에 정의된 메서드를 자식 클래스에서 재정의하는것

📖 람다식을 매개변수로 전달

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

  • 람다식 없이 직접 객체를 생성해서 전달하는 방식
  • 클래스의 익명 객체를 만든 다음에 매개변수로 전달
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
    }
}

📔 스트림 API

  • 데이터를 효율적으로 처리할 수 있는 흐름
  • 선언형 스타일로 가독성이 굉장히 뛰어남
  • 데이터 준비 → 중간 연산 → 최종 연산 순으로 처리됨
  • 스트림은 컬렉션(List, Set 등)과 함께 자주 활용함
  • 코드가 간단하고 직관적이어서 유지보수가 쉬움
단계 설명 주요 API
1. 데이터 준비 컬렉션을 스트림으로 변환 stream(), parallelStream()
2. 중간 연산 등록 데이터 변환 및 필터링 (즉시 실행되지 않음) map(), filter(), sorted()
3. 최종 연산 최종 처리 및 데이터 변환 collect(), forEach(), count()

💡 ArrayList 를 List 로 받는 이유

  • 다형성을 활용해 List 인터페이스ArrayList 구현체를 받으면 나중에 다른 구현체(LinkedList , Vector) 로 변경할 때 코드 수정을 최소화할 수 있기 때문
  • 실무에서 리스트를 선언할 때 대부분 아래와 같이 List 타입으로 받는 것을 권장
List<Integer> arrayList = new ArrayList<>();
import java.util.*;
import java.util.stream.*;

public class Main {
   public static void main(String[] args) {
       // ✅ ArrayList 선언
       List<Integer> arrayList = new ArrayList<>(List.of(1, 2, 3, 4, 5));

       // ✅ Stream 사용: 각 요소 * 10 처리
       List<Integer> ret2 = arrayList.stream()  // 1. 데이터 준비
           .map(num -> num * 10)                 // 2. 각 요소 * 10 처리
           .collect(Collectors.toList());        // 3. 결과 리스트로 수집

       // 결과 출력
       System.out.println("ret2 = " + ret2); 
   }
}

✅ 출력결과
10, 20, 30, 40, 50


// ✅ 다른예제 (한 줄로 표현 가능)
List<Integer> ret2 = arrayList.stream() // 1. 데이터 준비
   .map(num -> num * 10)               // 2. 중간 연산 등록
   .collect(Collectors.toList());  // 3. 최종 연산

💡 for문 vs Stream문

  • for문특징
    • 반복문을 통해 각 요소를 직접 변환
    • 코드가 길어질 수 있어 간단한 데이터 처리에도 반복문이 추가되면 가독성이 떨어짐
    • 중간 결과를 출력하거나 디버깅하기 쉬움

📕 스트림과 람다식 활용

💡 map() 메서드

  • 함수형 인터페이스를 매개변수
    → 즉, 함수형 인터페이스를 구현한 구현체를 매개변수로 받을 수 있다
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
1️⃣ 익명 클래스 활용
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

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

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

        // ✅ 익명 클래스를 활용한 변환
        Function<Integer, Integer> function = new Function<>() {
            @Override
            public Integer apply(Integer integer) {
                return integer * 10;
            }
        };

        List<Integer> ret1 = arrayList.stream()
                .map(function)
                .collect(Collectors.toList());

        System.out.println("ret1 (익명 클래스) = " + ret1);
    }
}
✅출력값
ret1 (익명 클래스) = [10, 20, 30, 40, 50]


2️⃣ 람다식 활용
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

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

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

        // ✅ 람다식을 변수에 담아 활용
        Function<Integer, Integer> functionLambda = num -> num * 10;

        List<Integer> ret2 = arrayList.stream()
                .map(functionLambda)
                .collect(Collectors.toList());

        System.out.println("ret2 (람다식 변수 활용) = " + ret2);

        // ✅ 람다식을 직접 활용 
        List<Integer> ret3 = arrayList.stream()
                .map(num -> num * 10)
                .collect(Collectors.toList());

        System.out.println("ret3 (람다식 직접 활용) = " + ret3);
    }
}
✅출력값
ret2 (람다식 변수 활용) = [10, 20, 30, 40, 50]
ret3 (람다식 직접 활용) = [10, 20, 30, 40, 50]


 3️⃣ 스트림과 람다식을 함께 사용
public class Main {

    public static void main(String[] args) {

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

        // 스트림 없이: 각 요소 * 10 처리
        ArrayList<Integer> ret1 = new ArrayList<>();
        for (Integer num : arrayList) {
            int multipliedNum = num * 10;
            ret1.add(multipliedNum);
        }
        System.out.println("ret1 = " + ret1);

        // 스트림 활용: 각 요소 * 10 처리
        List<Integer> ret2 = arrayList.stream().map(num -> num * 10).collect(Collectors.toList());
        System.out.println("ret2 = " + ret2);

        // 직접 map() 활용해보기
        // 1. 익명클래스를 변수에 담아 전달
        Function<Integer, Integer> function = new Function<>() {

            @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 = (num -> num * 10);
        List<Integer> ret4 = arrayList.stream()
                .map(functionLambda)
                .collect(Collectors.toList());
        System.out.println("ret4 = " + ret4);
				
				// 람다식 직접 활용
        List<Integer> ret5 = arrayList.stream()
                .map(num -> num * 10)
                .collect(Collectors.toList());
        System.out.println("ret5 = " + ret5);
    }
}
✅출력값
ret5 = [10, 20, 30, 40, 50]

📚 쓰레드

  • 프로그램 내에서 독립적으로 실행되는 작은 실행 단위(한명의일꾼=노비지연이)
  • 기본적으로 자바 프로그램은 main() 메서드를 실행하는 "메인 쓰레드"에서 실행됨
  • 여러 작업을 동시에 실행하려면 멀티 쓰레드를 사용해야 함
    • A, B, C 작업을 각각 다른 일꾼이 동시에 처리하는 방식임
    • 쓰레드 간의 자원 공유와 동기화 문제를 잘 관리해야함
      구분 싱글 쓰레드 (Single Thread) 멀티 쓰레드 (Multi Thread)
      작업 처리 방식 하나의 작업을 순차적으로 실행 여러 개의 작업을 동시에 실행
      장점 구현이 간단하고 디버깅이 쉬움 CPU 활용도가 높고, 빠른 작업 처리 가능
      단점 하나의 작업이 끝나야 다음 작업 실행 (병목 현상) 동기화 문제가 발생할 수 있음
      예제 System.out.println("Hello");
      System.out.println("World");
      Thread t1 = new Thread(() -> System.out.println("Thread 1"));
      Thread t2 = new Thread(() -> System.out.println("Thread 2"));
      t1.start(); t2.start();

📔 싱글 쓰레드

✔ 한 개의 쓰레드(main)에서 모든 작업을 순차적으로 실행
✔ 하나의 작업이 끝나야 다음 작업을 실행 가능
✔ 멀티 쓰레드보다 구현이 간단하지만, 속도가 느릴 수 있음

public class Main {
    public static void main(String[] args) {
        System.out.println("::: main 쓰레드 시작 :::");
        String threadName = Thread.currentThread().getName();
        
        // 숫자 0~9까지 출력하는 작업
        for (int i = 0; i < 10; i++) {
            System.out.println("현재 쓰레드: " + threadName + " - " + i);
            try {
                Thread.sleep(500); // 0.5초 대기
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
        System.out.println("::: 작업 끝 :::");
    }
}
✅ 출력값
::: main 쓰레드 시작 :::
현재 쓰레드: main - 0
현재 쓰레드: main - 1
...
현재 쓰레드: main - 9
::: 작업 끝 :::

✔ 순차 실행됨

📗 멀티 쓰레드

✔ 여러 개의 작업을 동시에 실행 가능 (병렬 처리)
✔ Thread 클래스를 상속받아 구현할 수 있음
✔ start() 메서드를 호출하면 run() 메서드가 새로운 쓰레드에서 실행됨

📌 Thread 클래스 상속 예시

// 1️⃣ 쓰레드 클래스 정의 
public class MyThread extends Thread {
    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        System.out.println("::: " + threadName + " 쓰레드 시작 :::");
        for (int i = 0; i < 10; i++) {
            System.out.println("현재 쓰레드: " + threadName + " - " + i);
            try {
                Thread.sleep(500); // 0.5초 대기
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("::: " + threadName + " 쓰레드 종료 :::");
    }
}

// 2️⃣ 메인 클래스 
public class Main {
    public static void main(String[] args) {
        System.out.println("::: main 쓰레드 시작 :::");

        MyThread thread0 = new MyThread();
        MyThread thread1 = new MyThread();

        thread0.start(); // 새로운 쓰레드 실행
        thread1.start(); // 새로운 쓰레드 실행

        System.out.println("::: main 쓰레드 종료 :::");
    }
}

✅ 출력값
::: main 쓰레드 시작 :::
::: main 쓰레드 종료 :::
::: Thread-0 쓰레드 시작 :::
::: Thread-1 쓰레드 시작 :::
현재 쓰레드: Thread-0 - 0
현재 쓰레드: Thread-1 - 0
현재 쓰레드: Thread-0 - 1
현재 쓰레드: Thread-1 - 1
...
::: Thread-0 쓰레드 종료 :::
::: Thread-1 쓰레드 종료 :::
✔ 쓰레드 2개가 동시에 실행되어 0~9까지 출력이 섞여 나옴

📌 join()예시(특정 쓰레드가 끝날 때까지 기다리기)

✔ join()을 사용하면 특정 쓰레드가 끝날 때까지 기다릴 수 있음
✔ main() 쓰레드가 너무 빨리 끝나지 않도록 동기화 가능

public class Main {
    public static void main(String[] args) {
        System.out.println("::: main 쓰레드 시작 :::");

        MyThread thread0 = new MyThread();
        MyThread thread1 = new MyThread();

        long startTime = System.currentTimeMillis(); // 시작 시간 기록

        thread0.start();
        thread1.start();

        try {
            thread0.join(); // thread0가 끝날 때까지 대기
            thread1.join(); // thread1가 끝날 때까지 대기
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        long endTime = System.currentTimeMillis();
        System.out.println("총 작업 시간: " + (endTime - startTime) + "ms");
        System.out.println("::: main 쓰레드 종료 :::");
    }
}

✅ 출력값
::: main 쓰레드 시작 :::
::: Thread-0 쓰레드 시작 :::
::: Thread-1 쓰레드 시작 :::
현재 쓰레드: Thread-0 - 0
현재 쓰레드: Thread-1 - 0
...
::: Thread-0 쓰레드 종료 :::
::: Thread-1 쓰레드 종료 :::
총 작업 시간: 5052ms
::: main 쓰레드 종료 :::

✔ 모든 작업이 끝난 후 main() 종료됨

📌 Runnable 인터페이스 활용 (권장)

✔ Thread 상속 방식보다 유지보수성이 좋음
✔ Java는 클래스 다중 상속을 지원하지 않기 때문에 Runnable 사용이 유리함
✔ Thread는 실행 로직과 쓰레드 제어를 모두 담당하지만, Runnable은 실행 로직만 담당

// 1️⃣ Runnable 인터페이스 구현
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        for (int i = 0; i < 10; i++) {
            System.out.println("현재 쓰레드: " + threadName + " - " + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

// 2️⃣ 메인 클래스 
public class Main {
    public static void main(String[] args) {
        MyRunnable task = new MyRunnable(); // ✅ 실행 로직 분리

        Thread thread0 = new Thread(task); // ✅ 하나의 작업을 여러 쓰레드에서 공유
        Thread thread1 = new Thread(task); // ✅ 하나의 작업을 여러 쓰레드에서 공유

        thread0.start();
        thread1.start();
    }
}

✅ 출력값
현재 쓰레드: Thread-0 - 0
현재 쓰레드: Thread-1 - 0
현재 쓰레드: Thread-0 - 1
현재 쓰레드: Thread-1 - 1
...
현재 쓰레드: Thread-0 - 9
현재 쓰레드: Thread-1 - 9
✔ 쓰레드 로직을 분리해서 유지보수성을 높일 수 있음
✔ Thread 클래스를 직접 상속받지 않아도 됨 (다른 클래스를 상속받을 수 있음)

⚠️ Runnable을 추천하는 이유

비교 항목 Thread 클래스 상속 Runnable 인터페이스 구현
구현 방식 Thread 클래스를 상속 Runnable 인터페이스 구현
실행 방법 start() 메서드 호출 Thread 객체 생성 후 start() 호출
다중 상속 ❌ 다른 클래스 상속 불가 ✅ 다른 클래스 상속 가능
코드 재사용성 ❌ 낮음 ✅ 높음 (다른 곳에서도 활용 가능)
유지보수성 ❌ 낮음 ✅ 높음 (실행 로직과 쓰레드 제어 분리)
profile
우당탕개발일지

0개의 댓글

관련 채용 정보