💡예외 발생하면?
→ 프로그램이 멈추고 에러가 뜬다! (사용자 입장에서 불편)
→ 예외 처리를 하면? 프로그램이 멈추지 않고 원하는 메시지를 띄우고 정상 실행됨
구분 | 체크 예외 (Checked Exception) | 런타임 예외 (Unchecked Exception) |
---|---|---|
예외 처리 강제 여부 | ✅ 반드시 처리해야 함 (예외를 잡거나, 다른 메서드로 넘겨야 함) | ❌ 처리하지 않아도 컴파일에 문제 없음 |
예외 발생 시점 | 컴파일 시점에 발생: 코드 작성 시점에서 경고가 뜸 | 실행 시점에 발생: 프로그램이 실행될 때 예외가 발생 |
예외 계층 구조 | Exception을 상속하지만, RuntimeException은 제외 | RuntimeException을 상속 |
대표 예외 | IOException, SQLException, InterruptedException 등 | NullPointerException, ArithmeticException, ArrayIndexOutOfBoundsException 등 |
사용 사례 | 파일 읽기, 데이터베이스 연결 등 외부 시스템과 관련된 오류 | 코드에서 잘못된 로직이나 논리 오류로 발생 |
예외 처리 방법 | 반드시 예외를 처리해야 함 (예: try-catch 또는 throws 사용) | 예외 처리를 선택적으로 할 수 있음 (처리하지 않아도 실행 가능) |
예외 종류 | 설명 | 발생 예제 |
---|---|---|
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; |
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으로 나눌 수 없습니다.
프로그램 정상 실행!
예외 종류 | 설명 | 발생 예제 |
---|---|---|
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"); |
✔️ 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("예외처리");
}
}
}
메서드 | 설명 | 예제 |
---|---|---|
isPresent() |
값이 존재하는지 확인 (true / false ) |
studentOptional.isPresent() |
orElse() |
값이 없을 때 기본값 제공 | orElse(new Student("기본값")) |
orElseGet() |
값이 없을 때만 기본값 생성 (지연 로딩) | orElseGet(() -> new Student("기본값")) |
✔️ 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("학생이 없습니다.");
}
}
}
✅ 출력결과
학생이 없습니다.
✔️ 값이 없을 경우 "미등록 학생"이라는 기본값을 제공
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());
}
}
✅ 출력결과
학생 이름: 미등록 학생
✔️ 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); // ✅ 정상 동작 (길이 제한 없음)
컬렉션 | 순서 유지 | 중복 허용 | 특징 |
---|---|---|---|
ArrayList | ✅ 유지 | ✅ 가능 | 배열처럼 사용, 크기 자동 조절 |
HashSet | ❌ 없음 | ❌ 불가 | 중복 제거, 빠른 검색 |
HashMap | ❌ 없음 | ✅ (Key는 중복 불가, Value는 가능) | Key-Value 저장, 빠른 검색 |
💡 대표적인 구현체 ArrayList , LinkedList 있음
- 요소 추가 →
add("값")
- 요소 조회 →
get(인덱스)
- 요소 제거 →
remove("값")
// 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);
HashSet
은 순서를 유지하지 않고 중복안댕get()
(요소 조회) 지원안댕⚠️ 왜 순서가 랜덤?
👉 HashSet은 내부적으로 빠른 검색을 위해 저장 위치를 자동으로 정리하기 때문임!
💡 대표적인 구현체로는HashSet
,TreeSet
- 요소 추가 →
add("값")
- 요소 제거 →
remove("값")
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()
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>
는 임시 타입 변수로, 어떤 타입이든 사용가능함타입 매개변수 | 의미 | 예제 |
---|---|---|
<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<>; |
✔️ 클래스 선언부에 <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
✔️ 클래스뿐만 아니라 메서드에도 적용할수있음
✔️ 제네릭 메서드는 특정 타입(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
✔️ 별도의 클래스 파일을 만들지 않고 코드 내에서 일회성으로 정의해 사용하기 때문에 이름이 없다고 부름
✔️ 코드가 길어지지만 복잡한 로직을 구현할 때 유용함
@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
✔️ 람다식은 간결하고 직관적인 문법으로 단순한 로직에 적합
✔️ 함수형 인터페이스 를 통해서 구현하는 것을 권장함
→ 하나의 추상 메서드만 가져야하기 때문
→ 하나의 추상 메서드를 가진 일반 인터페이스를 통해서도 사용 가능
// ✅ 람다 표현식 (간결함)
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
@FunctionalInterface
public interface Calculator {
int sum(int a, int b); // ✅ 하나의 추상 메서드만 선언 가능
}
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
}
}
❷ 람다식을 변수에 담아 전달
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
}
}
❸ 람다식을 직접 전달
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 |
---|---|---|
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]
구분 | 싱글 쓰레드 (Single Thread) | 멀티 쓰레드 (Multi Thread) |
---|---|---|
작업 처리 방식 | 하나의 작업을 순차적으로 실행 | 여러 개의 작업을 동시에 실행 |
장점 | 구현이 간단하고 디버깅이 쉬움 | CPU 활용도가 높고, 빠른 작업 처리 가능 |
단점 | 하나의 작업이 끝나야 다음 작업 실행 (병목 현상) | 동기화 문제가 발생할 수 있음 |
예제 |
System.out.println("Hello");
|
Thread t1 = new Thread(() -> System.out.println("Thread 1"));
|
✔ 한 개의 쓰레드(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() 메서드가 새로운 쓰레드에서 실행됨
// 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()을 사용하면 특정 쓰레드가 끝날 때까지 기다릴 수 있음
✔ 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() 종료됨
✔ 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 클래스를 직접 상속받지 않아도 됨 (다른 클래스를 상속받을 수 있음)
비교 항목 | Thread 클래스 상속 | Runnable 인터페이스 구현 |
---|---|---|
구현 방식 | Thread 클래스를 상속 | Runnable 인터페이스 구현 |
실행 방법 | start() 메서드 호출 | Thread 객체 생성 후 start() 호출 |
다중 상속 | ❌ 다른 클래스 상속 불가 | ✅ 다른 클래스 상속 가능 |
코드 재사용성 | ❌ 낮음 | ✅ 높음 (다른 곳에서도 활용 가능) |
유지보수성 | ❌ 낮음 | ✅ 높음 (실행 로직과 쓰레드 제어 분리) |