예외/오류와 제네릭/와일드카드

tech_bae·2025년 3월 16일

Java

목록 보기
6/10
post-thumbnail

예외(Exception)와 오류(Error)

예외(Exception)

  • 프로그램 실행 중 발생하는 비정상적인 상황
  • 예외도 객체다..

Checked Exception

  • 컴파일 타임에서 검사되는 예외 → 예외 처리 강제 → 무시하면? 컴파일 오류

⇒ 고로 try-catchthrows / throw 를 사용해서 예외처리 해야함

  • Exception 을 상속

Unchecked Exception

  • 컴파일러가 체크하지 않는 예외 → 예외처리 하지 않아도 컴파일됨 → 실행중에 예외가 발생 할 수 있다.
  • NullPointerException 과 같은 예외 자주 발생 → null 여부 확인해야 한다. 등
  • RunTimeException 을 상속(RunTimeException은 Exception을 상속)

예외처리

throws

  • 메서드 선언부에 작성하여 메서드에서 예외가 발생할 수 있음을 알림
  • 메소드를 호출하는 시점에서 예외처리가 되야함(예외를 던져서 호출한 놈한테 책임전가 ㄷㄷ;)

throw

  • 예외를 강제로 발생시킴

try-catch-fianlly

  • try : 예외가 발생할 수 있는 코드 작성
  • catch : 예외가 발생할때의 대처 작성
  • finally : 예외 발생여부 상관없이 반드시 실행(생략가능)
public class ExceptionTest {
    public void ex() throws RuntimeException {
        System.out.println("예외 날라간드아~ㅋㅋ");
        throw new RuntimeException("쿠쿠루 삥뽕");
    }
}
public class ExMain {
    public static void main(String[] args) {
        try {
            ExceptionTest ex = new ExceptionTest();
            ex.ex();
        }catch (RuntimeException e) {
            System.out.println("에러 발생!!"+e);
        }finally {
            System.out.println("생략 안해주셔서 감사합니다..");
        }
    }
}

/*
출력
예외 날라간드아~ㅋㅋ
에러 발생!!java.lang.RuntimeException: 쿠쿠루 삥뽕
생략 안해주셔서 감사합니다..
*/

사용자 정의 예외

  • 새로운 예외 클래스를 정의하여 사용가능 ⇒ Exception 이나 RunTimeException클래스를 상속받아 생성
  • 예외의 원인을 좀 더 정확히 특정 할 수 있음
catch(GotWeakerException e) {
	throw new FailLiftException("수행을 실패했습니다.",e); 
public class FailLiftException extends Exception {
    public FailLiftException (String message, Throwable cause) {
        super(message, cause);
    }
}

cause에 이 예외가 발생한 원인 예외를 넣어주면 연쇄적인 예외처리가 가능하다.

오류(Error)

  • 시스템 수준의 심각한 문제 → 프로그램에서 처리 못함
  • Throwable을 상속 받기에 try-catch 사용가능하지만 치명적인 문제이기에 조취를 취해야함 ⇒ 즉, 잡지마라
  • 오류도 객체..

제네릭(Generic)

  • 자바에서 제네릭이란 참조 자료형의 타입을 일반화하는 것
    • 코드의 재사용성 높임
    • 타입 안정성 보장

컴파일 시점에 타입이 결정되는 가변적인 타입 매개변수

한마디로 어떤 참조 자료형(클래스 타입)이 들어올지 모르기에 일단 뭐든 들어올 수 있게 열어 놓고 있다가, 실제 객체가 특정 타입으로 생성될 때 그 타입으로 결정되는 것 (양자역학인가..?)

제네릭 기호 네이밍

기본적으론 제네릭의 이름에 제약사항은 따로 없다. 사실 변수 이름 지을 때처럼 마음대로 정해도 된다. 하지만 다른 사람을 배려하는 마음으로 통상적인 네이밍을 따르는 것도 바람직 할 것 같다. (보통 대문자 알파벳 문자를 사용한다.)

  • <T> : Type
  • <E> : Element
  • <K> : Key (Map)
  • <V> : Value (Map)
  • <N> : Number
  • <R> : 리턴 타입(메서드에서 자주 사용)
  • <U> , <S> : 제네릭 타입 여러개 일대 보조적 으로 사용

제네릭 사용

public class GenericBox<T> {
    public T stuff;

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

    public T whatHave() {
        System.out.println("I have " + stuff);
        return stuff;
    }
}
public class Main {
    public static void main(String[] args) {
        GenericBox<String> strBox = new GenericBox<String>("Tools");
        GenericBox<Integer> intBox = new GenericBox<>(1234);
        GenericBox<Boolean> boolBox = new GenericBox<>(true);

        strBox.whatHave();
        intBox.whatHave();
        boolBox.whatHave();
    }
}

/*
출력
I have Tools
I have 1234
I have true
*/

이런식으로 GenericBox의 필드타입을 제네릭을 사용하여 객체가 생성될때 필드의 타입이 결정된다.

참고로 제네릭 클래스의 인스턴스를 생성할 때

Box<String> myBox = new Box<String>();

이건 당연히 되겠지만

Box<> myBox = new Box<String>();
이러면 오류발생 가능(컴파일러가 좌변의<>를 보고 우변의 <>을 추론하기 때문에)

Box<String> myBox = new Box<>();
고로 이건 오류가 안남.

제네릭과 Static

Static필드에서의 제네릭

그렇다면 필드값이 Static이어도 제네릭이 사용가능 할까?

답은 안된다.

이유는 크게 두 가지다.

  • static변수는 객체 생성 없이 접근이 가능
    • static 변수는 클래스 로드 시점에 생성 된다. 제네릭은 객체가 생성될 때 결정된다 → 충돌발생 ⇒ 한마디로 static은 모든 인스턴스가 공유하지만, 제네릭 타입은 각 객체마다 다를 수 있으므로!!
  • 타입소거
    • 제네릭은 컴파일 시에만 유효, 런타임에서 제네릭 타입 제거 → Object 또는 특정 상위 클래스로 변환 ⇒ 클래스 수준에서 static 변수를 정의할 방법이 없다. (런타임에는 제네릭이 존재 X, ) 즉, static 변수는 클래스 로드 시점에 메로리에 로딩되어 런타임까지 유지되어야한다. 근데, 제네릭은 타입 소거로 인해서 런타임에서 타입 정보가 사라짐(Object)

Static메소드에서의 제네릭

근데 또 static 메소드는 된다. 뭘까 이 일관되지 않은 규칙은..

이유를 알아보자.

한마디로 Static 메서드의 제네릭 타입은 메서드가 호출될 때 결정되기 때문이다.

static변수와 마찬가지로 static메소드의 정의가 클래스 로드 시점에 메서드 영역에 저장된다.

하지만 static변수와 다르게 static메소드는 이 시점에서 제네릭의 타입이 결정 되지 않고 (static변수는 저장될때 타입이 결정되어야 하지만, static메소드는 호출 될때 결정해도 된다.)

메소드가 호출될 때마다 다른 제네릭 타입을 사용할 수 있기때문에 제네릭 사용이 가능하다.

public class GenericBox<T> {

    public static <T> void whatHave(T t) {
        System.out.println("I have " + t);
    }
}
public class Main {
    public static void main(String[] args) {
        GenericBox.whatHave("Hello");
        GenericBox.whatHave(1234);
        GenericBox.whatHave(false);
    }
}

/*
출력
I have Hello
I have 1234
I have false
*/

제네릭 타입 제한

  • 제네릭에서 특정 타입만을 허용하도록 제한하는 기능
  • 상한제한하한제한으로 나눌 수 있다.

앞으로의 설명을 위해 아래와 같은 상속(구현) 관계가 있다고 생각 해보자

interface A
class B implements A
class C extends B
class D

상한제한

extends 키워드를 사용하여 특정 클래스나 인터페이스를 상속하거나 구현한 타입만 허용

예를 들어

class UpperBoundedClass <T extends B>

이렇게 제한한다면 TB를 상속하는 타입만이 올 수 있습니다.(B포함)

⇒ B, C가 올 수 있음.

하한제한

super 키워드를 사용하여 특정 클래스의 상위 타입만 가능

class LowerBoundedClass <T super B>

⇒ B의 상위 타입인 B, A타입이 가능(B도 포함)

다중제약(다중 경계)

&를 사용하여 두개 이상의 제약을 걸 수 있다.(| 은 안됨!!)

class MultiBoundedClass <T extends B & A>

T 에는 B를 상속함과 동시에 A를 구현한 타입만이 올 수 있음.

⇒ 클래스는 하나만 가능하다(자바에선 다중 상속이 안되기에..)

와일드카드 : ?

  • 와일드카드 ? 는 제네릭 타입을 사용할 때, 어떤 타입이 올지 모를 겅우 유연하게 처리하는 기능
  • 메서드에서만 사용 가능
public class WildCard {
    public static void printList(List<?> list){
        for (Object o : list) {
            System.out.println(o);
        }
    }
}
public class WildCardMain {
    public static void main(String[] args) {
        List<String> strList = new ArrayList<>();
        strList.add("Hello");
        strList.add("World");

        List<Integer> intList = new ArrayList<>();
        intList.add(1);
        intList.add(2);

        WildCard.printList(strList);
        WildCard.printList(intList);
    }
}

/*
출력
Hello
World
1
2
*/

위처럼 printList의 매개변수를 List<?> 는 List의 타입이 무엇이든 받을 수 있다.

와일드카드 메서드와 제네릭 메서드의 차이

구분제네릭와일드카
타입 결정메서드 호출 시 타입을 지정해야 함어떤 타입이든 받을 수 있음
사용 목적특정 타입을 강제여러 타입을 유연하게 처리
타입 안정성타입이 정해져 있어서 안전내부적으로 Object로 처리

와일드카드 타입제한

제네릭과 유사하게 와일드카드의 범위를 제한할 수 있다.

  1. ? (Unbounded Wildcard)
    • 어떤 타입의 제네릭이든 허용(모두 허용)
  2. 상한제한(Upper Bounded Wildcard)
    • extends 키워드를 사용하여 제네릭과 동일하게 상속하는 하위 타입만 허용
    • ex) <? extends Number> : Number와 그 하위 타입만 허용(Number를 상속하는)
    • 읽기만 가능, 쓰기 불가 ⇒ 위 예에서 Number와 그 하위 타입중 하나이지만 정확히 어떤 타입인지 모르기에 쓰기가 불가능.(Integer, Double, Float 뭘 넣어야할지 모름..)
  3. 하한제한(Lower Bounded Wildcard)
    • super 키워드를 사용하여 상위 타입만을 허용
    • ex) <? super Integer> : Integer와 그 상위 타입만 허용
    • 쓰기가 가능 ⇒ 위 예에서 Integer 또는 그 상위 타입(Number, Object) 중 하나지만, 최소한 Integer 이상이 보장되기 때문에!

번외: Optional

Optional 객체를 사용하면 null처리를 안전하고 우아하게 가능(정적 팩토리 메서드 사용하여 객체 생성)

  • 정적 팩토리 메서드 : new 키워드로 객체를 만드는 대신 static메서드로 객체를 생성하게 하는 메소드
  • null을 감싸는 객체 제공 → null을 직접 다루지 않고 안전한 연산 가능!

Optional 객체 생성 방법

  1. Optional<String> opt1 = Optional.of("Hello"); : 반드시 값이 있어야 함
  2. Optional<String> opt2 = Optional.ofNullable(null); : 값이 null일 수도 있다.
  3. Optional<String> op3 = Optional.emtpy(); : 빈 객체 생성

Optional 구현 실습

import java.util.Objects;

public class Optional<T> {

    private final T data;

    private Optional(T data) { //private 생성자이므로 외부에서 접근 불가
        this.data = data;
    }

    public static <T> Optional<T> empty() { //빈 객체 반환
        return new Optional<>(null);
    }

    //값이 있는 객체를 반환 (requireNonNull : Null이면 예외 발생)
    public static <T> Optional<T> of(T value) { 
        return new Optional<>(Objects.requireNonNull(value));
    }

		// null이면 빈 객체 반환, 아니면 그 값 담긴 객체 반환
    public static <T> Optional<T> ofNullable(T value) {
        return value == null
                ? new Optional<>(null)
                : new Optional<>(value);
    }

		// 객체에 담긴 값 확인 -> null이면 예외 발생(커스텀 가능!)
    public T get() {
        if ( data == null ){
            throw new NoSuchElementException("No value present");
        }
        return data;
    }
		// 값 있음?
    public boolean isPresent() {
        return data != null;
    }
		// 빔?
    public boolean isEmpty() {
        return data == null;
    }

}
profile
전 아무고토 몰루고 아무고토 못해여

0개의 댓글