Ref_제네릭 / 오토박싱 / 캐스팅 / static / @Nullable

Dev.Hammy·2023년 12월 27일
0

Spring Guides

목록 보기
37/46

키워드 사용 범위

키워드사용 가능한 범위
public클래스, 인터페이스, 메서드, 필드
private메서드, 필드, 내부 클래스
protected메서드, 필드, 내부 클래스 (동일 패키지 내에서 상속받은 클래스에서도 사용 가능)
static메서드, 필드, 내부 클래스 (탑레벨에서는 사용 불가능)
final클래스, 메서드, 필드, 매개변수 (변경 불가능한 변수, 메서드 오버라이딩 방지, 클래스 확장 방지)
abstract클래스, 메서드, 내부 클래스 (추상 클래스 및 추상 메서드 선언)
synchronized메서드, 블록
volatile필드
transient필드
default인터페이스 메서드 (인터페이스의 기본 구현 제공)
native메서드 (네이티브 메서드, C/C++로 작성된 코드를 자바에서 호출할 때 사용)
strictfp클래스, 메서드 (실수 연산 시 동일한 결과를 보장하기 위한 키워드)
assert메서드 (프로그램의 상태를 검증하기 위한 키워드)
interface탑레벨에서만 사용되는 인터페이스 선언 키워드
enum탑레벨에서만 사용되는 열거형 선언 키워드

필드 : 로컬 변수, 클래스 변수, 인스턴스 변수

값이 결정되는 시점

컴파일 타임에 값이 정해짐런타임에 값이 정해짐
리터럴(상수, 직접 입력한 값)사용자 입력 (실행 중 사용자가 입력한 값)
상수 (final 변수)외부 데이터 소스(파일, 데이터베이스 등)로부터 가져온 값
컴파일 시점에 결정된 값 (조건문, 반복문 등)네트워크에서 받은 데이터
Enum 상수실행 중에 동적으로 생성된 값 (동적 할당, Reflection 등으로 생성된 값)
정적(final) 필드 초기화 (클래스 로딩 시)런타임에 결정되는 특정 시점의 환경 변수 및 외부 설정 파일 등
제네릭 (컴파일 시에 제한된 타입 사용)와일드카드 (실행 중에 특정 타입에 제한 없이 동작할 수 있는 타입)
어노테이션 (컴파일 타임에 소스 코드에 메타데이터 추가)리플렉션 (런타임에 클래스나 메서드 등의 정보를 분석 및 조작)
빈 (스프링 프레임워크에서 컨테이너에 의해 관리되는 객체)동적 생성된 빈 (런타임에 DI 컨테이너에 의해 동적으로 생성 및 관리)

팩토리 메서드

  1. 정적 팩토리 메서드(Static Factory Method):

    • 정적(static) 메서드로, 해당 클래스의 인스턴스를 반환하는 메서드입니다.
    • 보통 클래스의 생성자 대신에 사용되며, 객체 생성 시에 생성자 대신에 이러한 정적 메서드를 호출합니다.
    • valueOf(), of(), getInstance() 등과 같은 널리 사용되는 명명 규칙이 있습니다.
    • 예시:
      public class MyClass {
          private MyClass() {
              // private 생성자
          }
          
          public static MyClass createInstance() {
              return new MyClass();
          }
      }
  2. 동적 팩토리 메서드(Instance Factory Method):

    • 인스턴스 메서드로, 이미 생성된 객체의 인스턴스를 반환하는 메서드입니다.
    • 주로 인스턴스화된 객체가 다른 객체의 인스턴스를 생성하는 데 사용됩니다.
    • 예시:
      public class MyClass {
          public AnotherClass createAnotherInstance() {
              return new AnotherClass();
          }
      }

이러한 팩토리 메서드들은 객체 생성을 조율하고 제어하기 위해 사용됩니다. 정적 팩토리 메서드는 객체 생성에 유연성을 제공하고, 생성자와 달리 호출될 때마다 항상 새로운 객체를 생성할 필요가 없습니다. 반면, 동적 팩토리 메서드는 이미 생성된 객체를 이용하여 새로운 객체를 생성하거나 다른 타입의 객체를 반환할 수 있습니다.

SuperType / SubType

형변환 / 캐스팅

Primitive Type / Reference Type

Autoboxing / Unboxing

Issue

제네릭의 특성

  1. 타입 제한(Bounded Type): 특정 타입 또는 특정 타입의 하위 타입만을 허용할 수 있도록 제한할 수 있습니다.

  2. 와일드카드(Wildcards): <?>, <? extends T>, <? super T>와 같은 표현을 통해 유연한 타입 매개변수 사용이 가능합니다.

  3. 타입 소거(Type Erasure): 컴파일 후 제네릭 타입의 정보는 소거되고, 필요한 경우 컴파일러는 타입 캐스팅을 추가하여 타입 안정성을 유지합니다.

  4. 제네릭 메서드(Generic Methods): 단일 메서드에 대해 제네릭 타입을 선언할 수 있습니다.

와일드 카드

제네릭 타입을 선언할 때 ?를 사용하여 특정한 타입을 제한하는 데에 활용됩니다.

  • Unbounded Wildcard (<?>): 어떤 타입이든지 사용 가능한 와일드카드입니다. 예를 들어, List<?>List<String>, List<Integer>, List<Object> 등의 모든 종류의 리스트를 포함할 수 있습니다.

  • Upper Bounded Wildcard (<? extends 타입>): 특정한 상한을 가지는 와일드카드입니다. 예를 들어, List<? extends Number>는 Number 클래스를 확장한 클래스들 (Integer, Double 등)을 포함하는 리스트를 의미합니다.

  • Lower Bounded Wildcard (<? super 타입>): 특정한 하한을 가지는 와일드카드입니다. 예를 들어, List<? super Integer>는 Integer 클래스나 Integer가 상속한 클래스 (Number)의 리스트를 포함합니다.

다이아몬드 연산자

꺾쇠 괄호(<>)를 자바에서는 "다이아몬드 연산자"라고 부르기도 합니다. 다이아몬드 연산자는 주로 제네릭 타입을 사용할 때 타입 추론을 돕는데 사용됩니다.

List<String> myList = new ArrayList<>();

생성자 호출에서는 타입을 직접 명시할 수 없으며, 이 연산자를 사용하여 타입 추론을 유도하는 것이 일반적입니다.

타입 소거

제네릭의 타입 소거는 컴파일 시에 제네릭 타입에 대한 정보가 삭제되는 것을 의미합니다. 컴파일러는 타입 소거를 통해 제네릭 타입에 대한 정보를 유지하지 않고, 기존 코드를 변환하여 호환성을 유지합니다.

예를 들어, 다음과 같은 제네릭 클래스가 있다고 가정해봅시다.

public class Example<T> {
    private T data;

    public Example(T data) {
        this.data = data;
    }

    public T getData() {
        return data;
    }
}

이 클래스를 컴파일하면 컴파일러는 타입 소거를 수행합니다. 실제로 컴파일된 코드에서는 제네릭 타입 정보가 사라지고, Object 타입으로 변환됩니다.

컴파일된 코드는 대략적으로 다음과 같을 것입니다.

public class Example {
    private Object data;

    public Example(Object data) {
        this.data = data;
    }

    public Object getData() {
        return data;
    }
}

이 때문에 컴파일 시점에서는 제네릭 타입 정보가 없으므로, 런타임에 타입 캐스팅을 추가하여 타입 안정성을 유지합니다. 사용자가 getData()를 호출할 때 실제로는 캐스팅이 발생합니다.

Example<String> example = new Example<>("Hello");
String value = example.getData(); // 컴파일러는 getData()에서 Object를 String으로 캐스팅합니다.

이처럼 컴파일러는 타입 소거로 실제 제네릭 타입 정보를 삭제하지만, 코드를 변환하여 타입 안정성을 유지하고 호환성을 보장합니다.

제네릭 메서드

제네릭 메서드는 특정 메서드 내에서만 사용되는 제네릭 타입을 선언할 수 있게 해줍니다. 이를 통해 해당 메서드에서 다양한 타입의 데이터를 다룰 수 있습니다.

예를 들어, 다음은 배열에서 특정 위치의 두 요소를 교환하는 제네릭 메서드의 예시입니다.

public static <T> void swap(T[][] array, int i, int j) {
    if (i >= 0 && i < array.length && j >= 0 && j < array.length && array[i].length == array[j].length) {
        T[] temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    } else {
        System.out.println("Invalid indices or mismatched array sizes");
    }
}

이 메서드는 T라는 제네릭 타입을 선언하고, 이 메서드 안에서만 사용됩니다. 호출할 때마다 전달되는 실제 타입에 따라 동작하며, 배열의 원소를 교환하는 일반적인 메서드로 사용될 수 있습니다.

Integer[] intArray = {1, 2, 3, 4};
SwapUtil.swap(intArray, 0, 2); // 배열의 첫 번째와 세 번째 요소를 교환

String[] strArray = {"Hello", "World"};
SwapUtil.swap(strArray, 0, 1); // 배열의 첫 번째와 두 번째 요소를 교환

// 이렇게 다양한 타입의 배열에서도 동작합니다.

이와 같이 제네릭 메서드를 사용하면 같은 로직을 여러 타입에 대해 재사용할 수 있으며, 타입 안정성을 유지하면서 유연한 코드를 작성할 수 있습니다.

제네릭 사용 위치와 컴파일러의 심볼 해석

cannot resolve symbol T

cannot resolve symbol T 오류는 컴파일러가 T라는 심볼(symbol)을 찾을 수 없다는 것을 나타냅니다. 이는 컴파일러가 해당 심볼 T에 대한 정보를 알지 못해 발생하는 오류입니다.

Java에서는 제네릭 타입을 사용할 때, 해당 타입을 선언해야 합니다. 위의 코드에서 T는 메서드 레벨에서 선언되지 않았으며, 클래스 레벨에서도 선언되지 않았기 때문에 컴파일러가 T에 대한 정보를 알지 못합니다.

ClassName.this cannot be referenced from a static context

이 코드에서 문제가 발생하는 이유는 createList 메서드가 static으로 선언되어 있지만, 해당 메서드가 T를 사용하려고 하기 때문입니다.

static 메서드는 클래스 레벨에 속하며, 인스턴스화되지 않은 상태에서도 호출될 수 있습니다. 하지만 T는 제네릭 타입으로, 해당 타입은 클래스가 실제로 인스턴스화될 때까지 결정되지 않습니다. 그래서 static 메서드에서는 T에 접근할 수 없는데, 이 때문에 컴파일러가 오류를 발생시키는 것입니다.

해결 방법 중 하나는 static 키워드를 제거하여 해당 메서드를 인스턴스 메서드로 변경하는 것입니다.

메서드 레벨에서 제네릭 타입 선언

static <T> List<T> createList(T element)에서의 메서드 레벨에서 선언된 <T>는 해당 메서드 내에서만 유효한 새로운 제네릭 타입으로 처리되기 때문에 클래스 레벨에서 선언된 OurClassT와는 관련이 없습니다. 이것이 컴파일러가 오류를 발생시키지 않고 문제 없이 코드를 실행하는 이유 중 하나입니다.

결론적으로, <T>가 메서드 레벨에서 새로운 제네릭 타입으로 선언되었기 때문에 이 메서드 내에서는 클래스 레벨의 T와는 독립적으로 동작하게 됩니다.

위 코드처럼 클래스 레벨의 제네릭 타입과 메서드 레벨의 제네릭 타입은 각각 독립적인 유효 범위를 가지므로 타입이 불일치하고, 캐스팅이나 박싱이 가능한 관계가 아니더라도 IDE의 컴파일러는 오류를 표시하지 않는다.

와일드 카드 사용 위치 제한

제네릭과 와일드카드는 Java에서 타입 유연성과 안정성을 제공하는 데 사용됩니다. 각각의 사용이 결정되는 위치는 일반적으로 다음과 같습니다:

  1. 타입 지정이 필요한 부분:

    • 클래스나 메서드의 선언 부분: 클래스나 메서드를 정의할 때, 제네릭 타입 파라미터를 명시적으로 지정해야 합니다. 이 위치에서는 와일드카드를 사용할 수 없습니다.
    • 변수 선언: 변수를 선언할 때는 주로 제네릭 타입을 명시하여 사용합니다. 이 역시 와일드카드를 사용할 수 없는 위치입니다.
  2. 유연성이 필요한 부분:

    • 인스턴스 생성 시점: 컬렉션 또는 제네릭 클래스의 인스턴스를 생성할 때, 다이아몬드 연산자 내에서 와일드카드를 사용하여 타입을 추론하거나, 타입을 명시적으로 지정하는 부분입니다.
    • 제네릭한 데이터 타입을 다루는 코드 내부: 메서드의 반환 타입, 메서드의 인자로 받는 매개변수, 또는 제네릭 컬렉션의 요소로 사용될 때, 와일드카드를 사용하여 유연성을 제공하고 다양한 타입을 다룰 수 있습니다.

컬렉션 프레임워크 / 제네릭 컬렉션

제네릭 컬렉션과 컬렉션 프레임워크는 서로 다른 개념입니다.

컬렉션 프레임워크(Collection Framework):
컬렉션 프레임워크는 자바에서 데이터 그룹을 저장하고 관리하기 위한 클래스들을 모아놓은 API입니다. 이는 여러 종류의 데이터를 저장하고 조작하는데 필요한 인터페이스와 클래스들을 제공합니다. List, Set, Queue, Map 등의 인터페이스와 이를 구현한 ArrayList, LinkedList, HashSet, HashMap 등의 클래스들이 이에 속합니다. 이 컬렉션 프레임워크는 컬렉션들을 다루는데 필요한 여러 유용한 기능들을 제공하여 데이터를 쉽게 저장, 검색, 정렬, 조작할 수 있도록 도와줍니다.

제네릭 컬렉션(Generic Collections):
제네릭 컬렉션은 컬렉션 프레임워크의 일부분으로, 자바 5부터 제네릭 타입을 도입하여 제공됩니다. 이는 제네릭을 이용하여 컬렉션에 저장되는 요소의 타입을 컴파일 시에 체크하여 안정성을 높이는 데 사용됩니다. 이전에는 컬렉션에 Object를 저장하는 방식이었기 때문에 타입 안전성을 보장받지 못했지만, 제네릭 컬렉션을 사용하면 컴파일러가 타입 불일치에 대한 경고를 더욱 명확하게 제공하고, 런타임 시에 ClassCastException과 같은 오류를 방지하는 데 도움이 됩니다.

ArrayList, LinkedList, HashSet, HashMap 등 구현체와 제네릭 타입의 도입은 서로 다른 것들입니다.

제네릭 컬렉션의 내부 자료형

컬렉션의 제네릭 타입 변경 불가

Inconvertible types; cannot cast

제네릭 타입은 컴파일 타임에 결정되며, 한 번 생성된 컬렉션의 제네릭 타입은 런타임 동안 변경될 수 없습니다.

제네릭 컬렉션이 가질 수 있는 요소 타입

제네릭의 하위 타입

List<Number>는 제네릭의 특성 상Number의 하위 타입인 Integer를 포함할 수 있다. 그러나 List<Integer>Integer의 상위 타입인 Number를 포함할 수 없다.

상위 타입인 Number 타입인 요소 element(Integer)로 다운 캐스팅하여 List<Integer>에 추가할 수 있다.

NumberInteger가 아닌 하위 클래스의 객체를 참조하고 있을 때, 해당 객체를 Integer로 강제 형변환하여 캐스팅하려고 하면 ClassCastException이 발생합니다.

NumberDouble로 초기화한 후에 Integer로 명시적으로 캐스팅하는 부분이 문제가 되어 노란 밑줄이 그어지며 ClassCastException이 발생할 것이라고 경고하고 있습니다. DoubleInteger의 상위 클래스가 아니기 때문입니다.

타입이 결정되지 않은 리스트와 타입 소거

"타입이 결정되지 않은" 리스트를 생성한 후, 이 리스트에 특정 타입의 요소를 추가하는 경우에는 원하는 타입의 요소를 담을 수 있습니다.

List<Object> list = new ArrayList<>(); // 타입이 결정되지 않은 리스트

list.add(10); // Integer 추가
list.add("Hello"); // String 추가

// 런타임 시점에 타입이 결정되기 때문에, 다양한 타입의 요소를 담을 수 있음

위의 코드에서 List<Object> list = new ArrayList<>();는 컴파일 시에는 Object 타입의 리스트로 처리되지만, 리스트에 추가되는 요소들은 런타임 시점에서 실제 객체의 타입에 맞게 동적으로 결정됩니다.

이러한 유연성은 제네릭 타입의 타입 소거로 인해 가능해집니다. 제네릭은 컴파일 시에만 사용되고 런타임에서는 타입 정보가 소거됩니다. 따라서 컴파일러는 제네릭 타입의 실제 유형을 알 수 없지만, 컴파일 시에는 타입 안전성을 유지하기 위해 캐스팅이나 다른 방법을 사용하여 경고를 발생시키거나 적절한 타입 체크를 수행합니다.

오토 박싱

List<Number>Number의 서브 타입을 포함할 수 있습니다. 그러나 intNumber의 서브 타입이 아니라 기본 데이터 타입입니다. 그래서 List<Number>int를 바로 추가할 수 없습니다.

하지만, Java에서 intInteger로 자동으로 박싱될 수 있습니다. 따라서 intInteger로 변환한 후에 List<Number>에 추가할 수 있습니다.

제네릭 컬렉션에서는 Reference Type(참조형)만 제네릭으로 이용할 수 있기 때문에 제네릭 컬렉션을 Primitive Type(기본형)과 같이 사용하면 Autoboxing은 필연적으로 발생하게 된다.

Null의 형변환과 @Nullable

  • null은 아무 것도 없음을 나타내는 것으로, 아무런 값도 갖지 않는 것입니다. 메모리상에서 아무런 주소도 참조하지 않는 상태를 말합니다.

  • (형변환할 타입) null은 기존의 null 값을 다른 형식으로 형변환하는 것을 의미합니다. 이 경우, null 값은 여전히 null이지만, 코드상에서는 특정 타입으로 형변환하여 사용할 수 있게 됩니다.
    이것은 실제로 메모리에 있는 값이나 객체를 변경하는 것이 아니라, 컴파일러에게 이 null을 특정 타입으로 취급하라고 알려주는 것에 불과합니다. 실행 시점에서는 여전히 실제 null 값을 가지고 있습니다.

  • @Nullable은 주석(annotation)으로, 코드에서 해당 변수나 매개변수가 null일 수 있다는 것을 명시적으로 나타내는 데 사용됩니다. 이것은 해당 요소가 null 또는 null이 아닌 값 둘 다 가질 수 있다는 의미입니다. 이 주석은 코드의 문서화 및 정적 분석을 위해 사용되며, 개발자에게 해당 요소에 대한 정보를 전달합니다.

0개의 댓글