본문 바로가기

2025/05/15

반응형

🕘 오전 수업: 제네릭(Generic)

✅ 제네릭 클래스

public class Box<T> {
    public T content;
}
  • T는 타입 매개변수로, 클래스 정의 시 미정이지만 객체 생성 시 실제 타입 결정
Box<String> box1 = new Box<>();
box1.content = "Hello";

Box<Integer> box2 = new Box<>();
box2.content = 100;
 

✅ 제네릭 클래스 다중 타입

public class Product<K, M> {
    private K kind;
    private M model;
}
  • 예시: Product<Tv, String>, Product<Car, String>

✅ 제네릭 인터페이스와 구현

public interface Rentable<T> {
    T rent();
}
  • HomeAgency implements Rentable<Home>
  • CarAgency implements Rentable<Car>

✅ 제네릭 메서드

public static <T> Box<T> boxing(T t) {
    Box<T> box = new Box<>();
    box.set(t);
    return box;
}

✅ 제한된 타입 파라미터

public static <T extends Number> boolean compare(T t1, T t2) {
    return t1.doubleValue() == t2.doubleValue();
}

✅ 와일드카드 활용 (<?>, <? extends ...>, <? super ...>)

Course.registerCourse1(Applicant<?> applicant);
Course.registerCourse2(Applicant<? extends Student> applicant);
Course.registerCourse3(Applicant<? super Worker> applicant);

🕓 오후 수업: 멀티스레드(Thread)

✅ 단일 스레드에서 순차 작업

Toolkit toolkit = Toolkit.getDefaultToolkit();
toolkit.beep();
System.out.println("띵");
  • 메인 스레드에서 순차적으로 소리 → 텍스트 출력

✅ Runnable을 이용한 멀티스레드

Thread thread = new Thread(new Runnable() {
    public void run() {
        // 비프음 출력
    }
});
thread.start();
  • 병렬 처리: 스레드에서 소리, 메인에서 "띵" 출력

✅ Thread 상속을 이용한 멀티스레드

Thread thread = new Thread() {
    public void run() {
        // 비프음 출력
    }
};
thread.start();

💡 비교 정리

방식 장점 단점
Runnable 구현 다른 클래스 상속 가능 코드 조금 더 김
Thread 상속 코드 간단 단일 상속 제약

✅ 핵심 요약표

구분 개념 키워드
제네릭 클래스 타입을 유연하게 처리 <T>, <K, V>
제네릭 메서드 메서드에서 타입 매개 <T> T method(T t)
와일드카드 제네릭 제한 조건 <?>, <? extends>, <? super>
멀티스레드 동시 실행 흐름 Thread, Runnable
스레드 생성 start()로 실행 run() 정의 필요
댓글
반응형

이번 글에서는 Thread 클래스를 직접 상속받은 익명 객체를 사용해
비프음 출력과 문자 출력을 동시에 실행하는 멀티스레드 구조를 구성합니다.
127강과 달리 Runnable을 구현하지 않고, Thread 클래스를 바로 상속받는 방식입니다.


📌 예제 코드

import java.awt.Toolkit;

public class BeepPrintExample {
    public static void main(String[] args) {
        // Thread 클래스를 직접 상속한 익명 객체 생성
        Thread thread = new Thread() {
            public void run() {
                Toolkit toolkit = Toolkit.getDefaultToolkit();
                for (int i = 0; i < 5; i++) {
                    toolkit.beep();  // 비프음 출력
                    System.out.println("Thread beep");
                    try {
                        Thread.sleep(500);  // 0.5초 대기
                    } catch (Exception e) {
                        // 예외 무시
                    }
                }
            }
        };

        thread.start();  // 스레드 시작

        // 메인 스레드에서 "띵" 출력
        for (int i = 0; i < 5; i++) {
            System.out.println("띵");
            try {
                Thread.sleep(500);  // 0.5초 대기
            } catch (Exception e) {
                // 예외 무시
            }
        }
    }
}

💻 실행 결과 (순서는 OS 스케줄링에 따라 달라짐)

Thread beep
띵
Thread beep
띵
Thread beep
띵
Thread beep
띵
Thread beep
띵

💬 코드 설명

  • Thread 클래스를 익명 객체로 생성하고,
    내부의 run() 메서드를 오버라이딩하여 실행 코드를 작성했습니다.
  • thread.start()를 호출하면 메인 스레드와는 별개의 실행 흐름으로 비프음과 "Thread beep" 출력이 진행됩니다.
  • 메인 스레드는 동시에 "띵"을 출력합니다.
  • 127강과 실행 구조는 동일하지만, 구현 방식이 다릅니다.

🔄 127강과의 비교

항목 127강 (Runnable 구현) 128강 (Thread 상속)
스레드 생성 방식 new Thread(Runnable) new Thread() + run() 오버라이딩
코드 구조 인터페이스 구현 → 더 유연함 클래스 상속 → 단일 상속만 가능
확장성 및 실무 활용도 ✅ 실무에서 더 많이 사용됨 ⚠️ 다른 클래스를 상속 중이면 사용 불가
클래스 재사용성 높음 (다른 클래스와도 결합 쉬움) 낮음 (Thread 자체를 확장해야 함)
람다 표현 가능 여부 가능 (Runnable은 함수형 인터페이스) 불가능 (Thread는 클래스임)

💡 포인트 정리

  • Thread를 직접 상속하는 방식은 구조가 간단하지만,
    이미 다른 클래스를 상속 중이라면 사용이 불가능하므로 확장성은 떨어짐
  • 실무에서는 Runnable 또는 ExecutorService 기반 멀티스레드 방식이 더 자주 사용됨
  • 하지만 테스트, UI 애플리케이션 등 간단한 멀티태스킹에는 여전히 유용하게 사용됨

📌 정리하자면, Thread를 상속하는 방식은 익명 클래스로 간단하게 구현할 수 있어 초보자에게 직관적이지만,
구조적 유연성과 재사용성 면에서는 Runnable 방식이 더 우수합니다.
이번 예제를 통해 두 방식 모두 숙지하고, 상황에 맞게 선택할 수 있도록 하는 게 중요해요.

댓글
반응형

이번 글에서는 Thread 객체를 사용해
비프음 출력과 문자 출력을 동시에 실행하는 멀티스레드 구조를 실습합니다.
Runnable 인터페이스를 익명 구현 객체로 만들어 스레드에 전달하고,
start()를 호출하면 메인 흐름과는 별도의 독립 실행 흐름이 생성됩니다.


📌 예제 코드

import java.awt.Toolkit;

public class BeepPrintExample {
    public static void main(String[] args) {
        // 새로운 스레드 정의
        Thread thread = new Thread(new Runnable() {
            public void run() {
                Toolkit toolkit = Toolkit.getDefaultToolkit();
                for (int i = 0; i < 5; i++) {
                    toolkit.beep();  // 비프음 출력
                    System.out.println("Thread beep");
                    try {
                        Thread.sleep(500);  // 0.5초 대기
                    } catch (Exception e) {
                        // 예외 무시
                    }
                }
            }
        });

        thread.start();  // 스레드 실행

        // 메인 스레드에서 실행되는 반복문
        for (int i = 0; i < 5; i++) {
            System.out.println("띵");
            try {
                Thread.sleep(500);  // 0.5초 대기
            } catch (Exception e) {
                // 예외 무시
            }
        }
    }
}

💻 실행 결과 (실행 환경마다 순서는 달라질 수 있음)

Thread beep
띵
Thread beep
띵
Thread beep
띵
Thread beep
띵
Thread beep
띵

※ 비프음은 시스템 사운드로 출력되며, "Thread beep"는 비프음 직후 함께 출력됨


💬 코드 설명

  • new Thread(Runnable) 구조로 새로운 스레드를 정의하고,
    run() 메서드에 실행할 코드를 작성합니다.
  • thread.start()를 호출하면, 이 스레드가 메인 스레드와 병렬로 실행됩니다.
  • main() 메서드 안의 "띵" 출력 루프는 메인 스레드가 직접 실행하며,
    이로써 두 개의 루프가 동시에 반복 실행되는 구조가 만들어집니다.
  • Thread.sleep(500)으로 두 루프 모두 0.5초마다 동작하므로,
    실행 순서는 비교적 교대로 보이지만 실제 순서는 OS 스케줄링에 따라 달라질 수 있음

💡 포인트 정리

  • Runnable은 run() 메서드 하나만 정의된 스레드 실행용 함수형 인터페이스입니다.
  • new Thread(Runnable).start() 구조는 멀티스레드 기본 작성법
  • 하나의 프로그램 안에서 여러 작업을 동시에 실행하고자 할 때 반드시 필요한 개념
  • UI 동작과 계산, 사운드 처리 등을 분리할 때도 자주 사용됩니다.

📌 정리하자면, 이번 예제는 Thread를 통해 하나의 작업(비프음 + 출력)을 별도의 흐름으로 실행하면서,
메인 스레드에서는 "띵" 텍스트를 병렬로 출력하게 만드는 구조입니다.
이는 자바 멀티스레드 프로그래밍의 기본 형태이며, 실제 애플리케이션 동시 작업 구현의 기초가 되는 구조입니다.

댓글
반응형

이번 글에서는 java.awt.Toolkit의 beep() 메서드를 이용해 소리(비프음) 를 내고,
Thread.sleep()을 사용하여 일정 시간 동안 멈췄다가 다시 실행되는 구조를 실습합니다.
소리와 출력이 순차적으로 실행되는 구조로, 시간 제어 흐름과 반복문의 기본을 함께 연습할 수 있습니다.


📌 예제 코드

import java.awt.Toolkit;

public class BeepPrintExample {
    public static void main(String[] args) {
        Toolkit toolkit = Toolkit.getDefaultToolkit();  // Toolkit 객체 생성

        // 5번 비프음 출력
        for (int i = 0; i < 5; i++) {
            toolkit.beep();  // 소리 발생
            try {
                Thread.sleep(500);  // 0.5초 대기
            } catch (Exception e) {
                // 예외 무시
            }
        }

        // 5번 "띵" 출력
        for (int i = 0; i < 5; i++) {
            System.out.println("띵");
            try {
                Thread.sleep(500);  // 0.5초 대기
            } catch (Exception e) {
                // 예외 무시
            }
        }
    }
}

💻 실행 결과

(비프음 5회 출력, 0.5초 간격)
띵
띵
띵
띵
띵

※ 비프음은 콘솔에 보이지 않지만, PC 스피커에서 "삑" 소리가 들려야 합니다.
OS 및 장치 설정에 따라 소리가 안 날 수도 있어요.


💬 코드 설명

  • Toolkit.getDefaultToolkit()은 Toolkit 객체를 생성합니다.
    • toolkit.beep()은 OS 시스템의 기본 비프음을 출력합니다.
  • Thread.sleep(500)은 현재 스레드를 0.5초간 멈춥니다.
    • 비프음과 "띵" 사이에 간격을 주기 위한 용도입니다.
  • 두 개의 for문이 순차적으로 실행되기 때문에,
    소리가 먼저 5번 발생하고 그 다음 "띵"이 출력됩니다.

💡 포인트 정리

  • Toolkit.beep()은 간단한 시스템 알림음을 출력할 수 있는 기능입니다.
  • Thread.sleep(ms)는 일시정지로 흐름 제어할 수 있는 방법입니다.
  • try-catch로 InterruptedException을 처리해야 하며,
    일반적으로 멀티스레드, 애니메이션, 반복 실행 등에서 자주 활용됩니다.

📌 정리하자면, 이 예제는 비프음 → 정지 → 텍스트 출력 → 정지 구조를 통해
간단한 흐름 제어와 반복문의 응용을 연습할 수 있습니다.
이후 멀티스레드로 소리와 텍스트를 동시에 출력하는 구조로 확장도 가능하며,
타이머 기능, 경고음 구현, 간단한 알림 시스템에서도 유용하게 활용됩니다.

댓글
반응형

이번 강의에서는 제네릭 타입에서 사용하는 와일드카드(?)와 상한/하한 제한(extends, super)
수강 등록 시스템을 예로 들어 실습합니다.
registerCourse1, registerCourse2, registerCourse3 각각은 등록 가능한 대상의 타입을 다르게 제한하고 있으며,
이를 통해 유연하지만 안전한 타입 필터링이 가능해집니다.


📌 예제 코드 요약

📦 클래스 구조

class Person {}
class Worker extends Person {}
class Student extends Person {}
class HighStudent extends Student {}
class MiddleStudent extends Student {}

📦 Applicant 클래스

public class Applicant<T> {
    public T kind;

    public Applicant(T kind) {
        this.kind = kind;
    }
}

📦 Course 클래스 – 제네릭 메서드 정의

public class Course {
    public static void registerCourse1(Applicant<?> applicant) {
        System.out.println(applicant.kind.getClass().getSimpleName() + "이(가) Course1을 등록함.");
    }

    public static void registerCourse2(Applicant<? extends Student> applicant) {
        System.out.println(applicant.kind.getClass().getSimpleName() + "이(가) Course2을 등록함.");
    }

    public static void registerCourse3(Applicant<? super Worker> applicant) {
        System.out.println(applicant.kind.getClass().getSimpleName() + "이(가) Course3을 등록함.");
    }
}

✅ 메인 실행 예제

public class GenericExample {
    public static void main(String[] args) {
        // 모든 사람이 수강 가능한 일반 강좌
        Course.registerCourse1(new Applicant<>(new Person()));
        Course.registerCourse1(new Applicant<>(new Worker()));
        Course.registerCourse1(new Applicant<>(new Student()));
        Course.registerCourse1(new Applicant<>(new HighStudent()));
        Course.registerCourse1(new Applicant<>(new MiddleStudent()));
        System.out.println();

        // 학생만 수강 가능한 강좌
        Course.registerCourse2(new Applicant<>(new Student()));
        Course.registerCourse2(new Applicant<>(new HighStudent()));
        Course.registerCourse2(new Applicant<>(new MiddleStudent()));
        System.out.println();

        // 근로자 또는 그 상위 클래스만 수강 가능한 강좌
        Course.registerCourse3(new Applicant<>(new Person()));
        Course.registerCourse3(new Applicant<>(new Worker()));
    }
}

💻 실행 결과

Person이(가) Course1을 등록함.
Worker이(가) Course1을 등록함.
Student이(가) Course1을 등록함.
HighStudent이(가) Course1을 등록함.
MiddleStudent이(가) Course1을 등록함.

Student이(가) Course2을 등록함.
HighStudent이(가) Course2을 등록함.
MiddleStudent이(가) Course2을 등록함.

Person이(가) Course3을 등록함.
Worker이(가) Course3을 등록함.

💬 코드 설명

🔹 Applicant<?>

  • registerCourse1은 모든 타입의 지원자 허용
  • 와일드카드 <?>는 타입 제한 없이 모든 객체 허용

🔹 Applicant<? extends Student>

  • registerCourse2는 Student와 그 자식 클래스만 허용
  • 즉, Student, HighStudent, MiddleStudent만 수강 가능
  • Person, Worker는 Student의 하위 클래스가 아니므로 불가

🔹 Applicant<? super Worker>

  • registerCourse3는 Worker 또는 그 상위 클래스만 허용
  • 즉, Worker, Person은 가능
  • Student 및 그 하위 클래스는 불가 (상위 타입 아님)

💡 포인트 정리

선언 형태 의미
<?> 모든 타입 허용
<? extends T> T와 그 자식들만 허용 (상한 제한)
<? super T> T와 그 부모들만 허용 (하한 제한)
  • extends는 데이터를 꺼낼 때(read) 적합
  • super는 데이터를 넣을 때(write) 적합
  • 제한을 통해 타입 안전성과 코드 유연성을 동시에 확보할 수 있음

📌 정리하자면, 와일드카드와 extends, super 제한을 활용하면
"누가 수강할 수 있는가?" 같은 조건을 제네릭으로 명확하게 표현할 수 있습니다.
이는 컬렉션, API 설계, 프레임워크 개발 등에서 가장 자주 쓰이는 제네릭 패턴 중 하나입니다.

댓글
반응형

이번 글에서는 제네릭 타입 매개변수에 제한을 두는 방법을 배웁니다.
<T extends Number>처럼 extends 키워드를 사용하면,
해당 제네릭 메서드는 Number를 상속한 타입(예: Integer, Double, Float)만 사용할 수 있게 제한됩니다.


📌 예제 코드

public class GenericExample {
    // Number를 상속한 타입만 허용하는 제네릭 메서드
    public static <T extends Number> boolean compare(T t1, T t2) {
        System.out.println("compare(" + t1.getClass().getSimpleName() + ", " + t2.getClass().getSimpleName() + ")");
        double v1 = t1.doubleValue();
        double v2 = t2.doubleValue();
        return (v1 == v2);  // double 값으로 비교
    }

    public static void main(String[] args) {
        boolean re = compare(10, 20);     // Integer 비교
        System.out.println(re + "\n");

        re = compare(4.5, 4.5);           // Double 비교
        System.out.println(re);
    }
}

💻 실행 결과

compare(Integer, Integer)
false

compare(Double, Double)
true

💬 코드 설명

  • <T extends Number>는 이 메서드가 Number 또는 그 자식 타입만 인자로 받을 수 있도록 제한합니다.
  • t1.doubleValue()는 Number 클래스에 정의된 메서드로, int, float, double 값을 double형으로 변환해줍니다.
  • compare(10, 20)은 10.0 == 20.0 비교 → false
  • compare(4.5, 4.5)는 4.5 == 4.5 → true
  • t1.getClass().getSimpleName()은 콘솔에 전달된 타입 이름을 출력하기 위해 사용되었습니다.

💡 포인트 정리

  • T extends Number는 해당 제네릭이 Number의 자식만 받도록 제한합니다.
  • 이 방식은 특정 기능(예: 숫자 연산)을 보장하고 싶은 경우 유용합니다.
  • 대표적인 Number 하위 클래스: Integer, Double, Float, Long, Short, Byte
  • 타입 제한을 통해 불필요한 타입 사용을 컴파일 단계에서 차단할 수 있습니다.

📌 정리하자면, <T extends Number>처럼 타입을 제한하면,
숫자 타입에만 특화된 기능을 안전하게 제공할 수 있어 코드 신뢰성과 가독성이 높아집니다.
이는 제네릭의 강력한 확장성과 타입 안전성을 동시에 활용하는 대표적인 예입니다.

댓글
반응형

이번 글에서는 제네릭 메서드(Generic Method) 를 정의하고 사용하는 방법을 배웁니다.
제네릭 메서드는 메서드 자체에서 타입 매개변수를 선언하고,
호출 시점에 타입을 유추하거나 명시하여 다양한 타입의 데이터를 처리할 수 있습니다.


📌 예제 코드

package ch13.sec03;

public class Box<T> {
    private T t;

    public T get() {
        return t;
    }

    public void set(T t) {
        this.t = t;
    }
}
package ch13.sec03;

public class GenericExample {
    // 제네릭 메서드 정의
    public static <T> Box<T> Boxing(T t) {
        Box<T> box = new Box<T>();
        box.set(t);
        return box;
    }

    public static void main(String[] args) {
        Box<Integer> box1 = Boxing(100);           // T가 Integer로 결정됨
        int intVal = box1.get();
        System.out.println(intVal);

        Box<String> box2 = Boxing("홍길동");        // T가 String으로 결정됨
        String strVal = box2.get();
        System.out.println(strVal);
    }
}

💻 실행 결과

100
홍길동

💬 코드 설명

  • Box<T>는 제네릭 클래스이며, set()과 get() 메서드를 통해 T 타입 데이터를 설정하고 읽습니다.
  • Boxing() 메서드는 <T>라는 제네릭 선언을 메서드 앞에 명시함으로써,
    메서드 내부에서 제네릭 타입을 사용할 수 있도록 합니다.
  • Boxing(100) 호출 시 T는 Integer, Boxing("홍길동") 호출 시 T는 String으로 자동 유추됩니다.
  • 덕분에 형변환 없이도 타입 안전하게 값을 처리할 수 있습니다.

💡 포인트 정리

  • 제네릭 메서드는 클래스가 제네릭이 아니어도 사용 가능함
public static <T> T methodName(T t) { ... }
  • 메서드 호출 시 타입을 유추하거나 명시할 수 있음 (Boxing(123), Boxing<String>("hello"))
  • 제네릭 메서드는 유틸리티 클래스나 정적 메서드에서 특히 자주 사용
  • 컴파일러가 타입을 체크해주므로 형변환 없이도 안전하게 값 처리 가능

📌 정리하자면, 제네릭 메서드는 타입에 의존하지 않고 범용적인 기능을 메서드 단위에서 제공할 수 있는 매우 유용한 기능입니다.
정적 메서드에서도 유연하게 제네릭을 사용할 수 있어, 코드 재사용성과 타입 안전성을 동시에 확보할 수 있습니다.

댓글
반응형

이번 글에서는 제네릭 클래스를 활용한 타입 안전한 비교 메서드 구현 방법을 배울겁니다.
Box<T> 클래스는 T 타입의 데이터를 담고 있으며,
같은 타입의 Box<T> 객체와 내용이 같은지를 비교하는 compare() 메서드를 제공합니다.


📌 예제 코드

package ch13.sec02;

public class Box<T> {
    public T content;

    public boolean compare(Box<T> other) {
        boolean result = content.equals(other.content);  // 내용 비교
        return result;
    }
}
package ch13.sec02;

public class GenericExample4 {
    public static void main(String[] args) {
        Box<String> box1 = new Box<>();
        Box<String> box2 = new Box<>();
        box1.content = "100";
        box2.content = "100";

        boolean re = box1.compare(box2);
        System.out.println("re1 : " + re);
    }
}

💻 실행 결과

re1 : true

💬 코드 설명

  • Box<T>는 타입 매개변수 T를 갖는 제네릭 클래스이며,
    내부에 T 타입의 content 필드를 가지고 있습니다.
  • compare() 메서드는 Box<T> 타입의 다른 객체를 받아서,
    this.content와 other.content를 equals()로 비교합니다.
  • 제네릭 덕분에 타입이 일치하는 객체끼리만 비교 가능하며,
    컴파일러가 타입을 보장해주므로 형변환 없이도 안전한 비교가 가능합니다.
  • 두 Box<String> 객체 모두 "100"이라는 문자열을 담고 있으므로 결과는 true입니다.

💡 포인트 정리

  • 제네릭 클래스 내부에서도 제네릭 타입끼리의 비교가 가능합니다.
  • compare(Box<T> other) 구조는 타입이 다른 Box끼리의 비교는 컴파일 단계에서 막아주기 때문에 오류 예방이 됩니다.
  • 내부적으로 equals() 메서드를 활용하므로, T 타입은 equals()를 제대로 오버라이딩하고 있어야 정확한 비교가 가능합니다.

📌 정리하자면, 제네릭 클래스를 활용하면 객체 내부의 내용 비교도 타입 안정성을 유지하면서 구현할 수 있습니다.
Box<T> 구조처럼 동일한 타입끼리만 비교하도록 만들면,
코드 재사용성과 안정성을 동시에 확보할 수 있는 깔끔한 설계가 가능해집니다.

댓글