키워드 | 사용 가능한 범위 |
---|---|
public | 클래스, 인터페이스, 메서드, 필드 |
private | 메서드, 필드, 내부 클래스 |
protected | 메서드, 필드, 내부 클래스 (동일 패키지 내에서 상속받은 클래스에서도 사용 가능) |
static | 메서드, 필드, 내부 클래스 (탑레벨에서는 사용 불가능) |
final | 클래스, 메서드, 필드, 매개변수 (변경 불가능한 변수, 메서드 오버라이딩 방지, 클래스 확장 방지) |
abstract | 클래스, 메서드, 내부 클래스 (추상 클래스 및 추상 메서드 선언) |
synchronized | 메서드, 블록 |
volatile | 필드 |
transient | 필드 |
default | 인터페이스 메서드 (인터페이스의 기본 구현 제공) |
native | 메서드 (네이티브 메서드, C/C++로 작성된 코드를 자바에서 호출할 때 사용) |
strictfp | 클래스, 메서드 (실수 연산 시 동일한 결과를 보장하기 위한 키워드) |
assert | 메서드 (프로그램의 상태를 검증하기 위한 키워드) |
interface | 탑레벨에서만 사용되는 인터페이스 선언 키워드 |
enum | 탑레벨에서만 사용되는 열거형 선언 키워드 |
필드 : 로컬 변수, 클래스 변수, 인스턴스 변수
컴파일 타임에 값이 정해짐 | 런타임에 값이 정해짐 |
---|---|
리터럴(상수, 직접 입력한 값) | 사용자 입력 (실행 중 사용자가 입력한 값) |
상수 (final 변수) | 외부 데이터 소스(파일, 데이터베이스 등)로부터 가져온 값 |
컴파일 시점에 결정된 값 (조건문, 반복문 등) | 네트워크에서 받은 데이터 |
Enum 상수 | 실행 중에 동적으로 생성된 값 (동적 할당, Reflection 등으로 생성된 값) |
정적(final) 필드 초기화 (클래스 로딩 시) | 런타임에 결정되는 특정 시점의 환경 변수 및 외부 설정 파일 등 |
제네릭 (컴파일 시에 제한된 타입 사용) | 와일드카드 (실행 중에 특정 타입에 제한 없이 동작할 수 있는 타입) |
어노테이션 (컴파일 타임에 소스 코드에 메타데이터 추가) | 리플렉션 (런타임에 클래스나 메서드 등의 정보를 분석 및 조작) |
빈 (스프링 프레임워크에서 컨테이너에 의해 관리되는 객체) | 동적 생성된 빈 (런타임에 DI 컨테이너에 의해 동적으로 생성 및 관리) |
정적 팩토리 메서드(Static Factory Method):
valueOf()
, of()
, getInstance()
등과 같은 널리 사용되는 명명 규칙이 있습니다.public class MyClass {
private MyClass() {
// private 생성자
}
public static MyClass createInstance() {
return new MyClass();
}
}
동적 팩토리 메서드(Instance Factory Method):
public class MyClass {
public AnotherClass createAnotherInstance() {
return new AnotherClass();
}
}
이러한 팩토리 메서드들은 객체 생성을 조율하고 제어하기 위해 사용됩니다. 정적 팩토리 메서드는 객체 생성에 유연성을 제공하고, 생성자와 달리 호출될 때마다 항상 새로운 객체를 생성할 필요가 없습니다. 반면, 동적 팩토리 메서드는 이미 생성된 객체를 이용하여 새로운 객체를 생성하거나 다른 타입의 객체를 반환할 수 있습니다.
타입 제한(Bounded Type): 특정 타입 또는 특정 타입의 하위 타입만을 허용할 수 있도록 제한할 수 있습니다.
와일드카드(Wildcards): <?>
, <? extends T>
, <? super T>
와 같은 표현을 통해 유연한 타입 매개변수 사용이 가능합니다.
타입 소거(Type Erasure): 컴파일 후 제네릭 타입의 정보는 소거되고, 필요한 경우 컴파일러는 타입 캐스팅을 추가하여 타입 안정성을 유지합니다.
제네릭 메서드(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
오류는 컴파일러가 T
라는 심볼(symbol)을 찾을 수 없다는 것을 나타냅니다. 이는 컴파일러가 해당 심볼 T
에 대한 정보를 알지 못해 발생하는 오류입니다.
Java에서는 제네릭 타입을 사용할 때, 해당 타입을 선언해야 합니다. 위의 코드에서 T
는 메서드 레벨에서 선언되지 않았으며, 클래스 레벨에서도 선언되지 않았기 때문에 컴파일러가 T
에 대한 정보를 알지 못합니다.
이 코드에서 문제가 발생하는 이유는 createList
메서드가 static
으로 선언되어 있지만, 해당 메서드가 T
를 사용하려고 하기 때문입니다.
static
메서드는 클래스 레벨에 속하며, 인스턴스화되지 않은 상태에서도 호출될 수 있습니다. 하지만 T
는 제네릭 타입으로, 해당 타입은 클래스가 실제로 인스턴스화될 때까지 결정되지 않습니다. 그래서 static
메서드에서는 T
에 접근할 수 없는데, 이 때문에 컴파일러가 오류를 발생시키는 것입니다.
해결 방법 중 하나는 static
키워드를 제거하여 해당 메서드를 인스턴스 메서드로 변경하는 것입니다.
static <T> List<T> createList(T element)
에서의 메서드 레벨에서 선언된 <T>
는 해당 메서드 내에서만 유효한 새로운 제네릭 타입으로 처리되기 때문에 클래스 레벨에서 선언된 OurClass
의 T
와는 관련이 없습니다. 이것이 컴파일러가 오류를 발생시키지 않고 문제 없이 코드를 실행하는 이유 중 하나입니다.
결론적으로, <T>
가 메서드 레벨에서 새로운 제네릭 타입으로 선언되었기 때문에 이 메서드 내에서는 클래스 레벨의 T
와는 독립적으로 동작하게 됩니다.
위 코드처럼 클래스 레벨의 제네릭 타입과 메서드 레벨의 제네릭 타입은 각각 독립적인 유효 범위를 가지므로 타입이 불일치하고, 캐스팅이나 박싱이 가능한 관계가 아니더라도 IDE의 컴파일러는 오류를 표시하지 않는다.
제네릭과 와일드카드는 Java에서 타입 유연성과 안정성을 제공하는 데 사용됩니다. 각각의 사용이 결정되는 위치는 일반적으로 다음과 같습니다:
타입 지정이 필요한 부분:
유연성이 필요한 부분:
제네릭 컬렉션과 컬렉션 프레임워크는 서로 다른 개념입니다.
컬렉션 프레임워크(Collection Framework):
컬렉션 프레임워크는 자바에서 데이터 그룹을 저장하고 관리하기 위한 클래스들을 모아놓은 API입니다. 이는 여러 종류의 데이터를 저장하고 조작하는데 필요한 인터페이스와 클래스들을 제공합니다. List
, Set
, Queue
, Map
등의 인터페이스와 이를 구현한 ArrayList
, LinkedList
, HashSet
, HashMap
등의 클래스들이 이에 속합니다. 이 컬렉션 프레임워크는 컬렉션들을 다루는데 필요한 여러 유용한 기능들을 제공하여 데이터를 쉽게 저장, 검색, 정렬, 조작할 수 있도록 도와줍니다.
제네릭 컬렉션(Generic Collections):
제네릭 컬렉션은 컬렉션 프레임워크의 일부분으로, 자바 5부터 제네릭 타입을 도입하여 제공됩니다. 이는 제네릭을 이용하여 컬렉션에 저장되는 요소의 타입을 컴파일 시에 체크하여 안정성을 높이는 데 사용됩니다. 이전에는 컬렉션에 Object
를 저장하는 방식이었기 때문에 타입 안전성을 보장받지 못했지만, 제네릭 컬렉션을 사용하면 컴파일러가 타입 불일치에 대한 경고를 더욱 명확하게 제공하고, 런타임 시에 ClassCastException
과 같은 오류를 방지하는 데 도움이 됩니다.
ArrayList
, LinkedList
, HashSet
, HashMap
등 구현체와 제네릭 타입의 도입은 서로 다른 것들입니다.
제네릭 타입은 컴파일 타임에 결정되며, 한 번 생성된 컬렉션의 제네릭 타입은 런타임 동안 변경될 수 없습니다.
List<Number>
는 제네릭의 특성 상Number
의 하위 타입인 Integer
를 포함할 수 있다. 그러나 List<Integer>
는 Integer
의 상위 타입인 Number
를 포함할 수 없다.
상위 타입인 Number
타입인 요소 element
를 (Integer)
로 다운 캐스팅하여 List<Integer>
에 추가할 수 있다.
단 Number
가 Integer
가 아닌 하위 클래스의 객체를 참조하고 있을 때, 해당 객체를 Integer
로 강제 형변환하여 캐스팅하려고 하면 ClassCastException
이 발생합니다.
Number
를 Double
로 초기화한 후에 Integer
로 명시적으로 캐스팅하는 부분이 문제가 되어 노란 밑줄이 그어지며 ClassCastException
이 발생할 것이라고 경고하고 있습니다. Double
은 Integer
의 상위 클래스가 아니기 때문입니다.
"타입이 결정되지 않은" 리스트를 생성한 후, 이 리스트에 특정 타입의 요소를 추가하는 경우에는 원하는 타입의 요소를 담을 수 있습니다.
List<Object> list = new ArrayList<>(); // 타입이 결정되지 않은 리스트
list.add(10); // Integer 추가
list.add("Hello"); // String 추가
// 런타임 시점에 타입이 결정되기 때문에, 다양한 타입의 요소를 담을 수 있음
위의 코드에서 List<Object> list = new ArrayList<>();
는 컴파일 시에는 Object
타입의 리스트로 처리되지만, 리스트에 추가되는 요소들은 런타임 시점에서 실제 객체의 타입에 맞게 동적으로 결정됩니다.
이러한 유연성은 제네릭 타입의 타입 소거로 인해 가능해집니다. 제네릭은 컴파일 시에만 사용되고 런타임에서는 타입 정보가 소거됩니다. 따라서 컴파일러는 제네릭 타입의 실제 유형을 알 수 없지만, 컴파일 시에는 타입 안전성을 유지하기 위해 캐스팅이나 다른 방법을 사용하여 경고를 발생시키거나 적절한 타입 체크를 수행합니다.
List<Number>
는 Number
의 서브 타입을 포함할 수 있습니다. 그러나 int
는 Number
의 서브 타입이 아니라 기본 데이터 타입입니다. 그래서 List<Number>
에 int
를 바로 추가할 수 없습니다.
하지만, Java에서 int
는 Integer
로 자동으로 박싱될 수 있습니다. 따라서 int
를 Integer
로 변환한 후에 List<Number>
에 추가할 수 있습니다.
제네릭 컬렉션에서는 Reference Type(참조형)만 제네릭으로 이용할 수 있기 때문에 제네릭 컬렉션을 Primitive Type(기본형)과 같이 사용하면 Autoboxing은 필연적으로 발생하게 된다.
null
은 아무 것도 없음을 나타내는 것으로, 아무런 값도 갖지 않는 것입니다. 메모리상에서 아무런 주소도 참조하지 않는 상태를 말합니다.
(형변환할 타입) null
은 기존의 null
값을 다른 형식으로 형변환하는 것을 의미합니다. 이 경우, null
값은 여전히 null
이지만, 코드상에서는 특정 타입으로 형변환하여 사용할 수 있게 됩니다.
이것은 실제로 메모리에 있는 값이나 객체를 변경하는 것이 아니라, 컴파일러에게 이 null
을 특정 타입으로 취급하라고 알려주는 것에 불과합니다. 실행 시점에서는 여전히 실제 null
값을 가지고 있습니다.
@Nullable
은 주석(annotation)으로, 코드에서 해당 변수나 매개변수가 null
일 수 있다는 것을 명시적으로 나타내는 데 사용됩니다. 이것은 해당 요소가 null
또는 null
이 아닌 값 둘 다 가질 수 있다는 의미입니다. 이 주석은 코드의 문서화 및 정적 분석을 위해 사용되며, 개발자에게 해당 요소에 대한 정보를 전달합니다.