Java Genrics은 Java 5에서 소개된 가장 중요한 기능 중 하나입니다. Java Collections에서 작업하고 5 버전 이상을 사용하고 있다면, 사용한 적이 있을 것입니다. 컬렉션 클래스에서의 Java의 제네릭스는 매우 쉽지만 컬렉션의 유형을 생성하는 것 이상의 기능을 제공합니다. 이 기사에서는 제네릭스의 기능을 배워보겠습니다. 때때로 용어로 가득한 제네릭스를 이해하기는 혼란스러울 수 있습니다. 그래서 저는 그것을 간단하고 이해하기 쉽게 유지하려고 노력하겠습니다.
우리는 Java의 제네릭스에 관한 아래의 주제들을 살펴볼 것입니다.
1. 자바의 제네릭
제네릭은 자바 5에 추가되어 컴파일 시간에 타입 확인 및 컬렉션 클래스 작업 중 흔한 ClassCastException 위험을 제거하기 위해 도입되었습니다. 전체 컬렉션 프레임워크는 타입 안전성을 위해 제네릭을 사용하도록 다시 작성되었습니다. 제네릭이 컬렉션 클래스를 안전하게 사용하는 데 어떻게 도움이 되는지 살펴보겠습니다.
List list = new ArrayList();
list.add("abc");
list.add(new Integer(5)); //OK
for(Object obj : list){
// 타입 캐스팅으로 인한 런타임에서의 ClassCastException
String str=(String) obj;
}
위의 코드는 컴파일은 잘되지만 런타임에서 ClassCastException을 throw합니다. 왜냐하면 우리는 리스트 안의 Object를 String으로 캐스트하려고 시도하는데, 그 중 하나의 요소는 Integer 유형입니다. 자바 5 이후로는 아래와 같이 컬렉션 클래스를 사용합니다.
List list1 = new ArrayList(); // java 7 ? List list1 = new ArrayList<>();
list1.add("abc");
//list1.add(new Integer(5)); // 컴파일 오류
for(String str : list1){
//타입 캐스팅이 필요 없으며, ClassCastException을 피합니다.
}
리스트 생성 시 요소의 유형이 문자열임을 지정했음을 주목하세요. 따라서 목록에 다른 유형의 객체를 추가하려고하면 프로그램이 컴파일 시간 오류를 throw합니다. 또한 for 루프에서 목록의 요소를 타입 캐스팅할 필요가 없으므로 런타임에서 ClassCastException을 제거합니다.
2. 자바 제네릭 클래스
우리는 제네릭 유형으로 우리만의 클래스를 정의할 수 있습니다. 제네릭 유형은 유형에 대한 매개변수화된 클래스 또는 인터페이스입니다. 유형 매개변수를 지정하기 위해 꺽쇠 괄호(<>)를 사용합니다. 이점을 이해하기 위해 다음과 같은 간단한 클래스가 있다고 가정해 봅시다:
package com.journaldev.generics;
public class GenericsTypeOld {
private Object t;
public Object get() {
return t;
}
public void set(Object t) {
this.t = t;
}
public static void main(String args[]){
GenericsTypeOld type = new GenericsTypeOld();
type.set("Pankaj");
String str = (String) type.get(); //type casting, error prone and can cause ClassCastException
}
}
이 클래스를 사용할 때 형변환을 해야 하며, 이는 런타임에 ClassCastException을 발생시킬 수 있습니다. 이제 동일한 클래스를 다시 작성하기 위해 제네릭 클래스를 사용하겠습니다.
package com.journaldev.generics;
public class GenericsType<T> {
private T t;
public T get(){
return this.t;
}
public void set(T t1){
this.t=t1;
}
public static void main(String args[]){
GenericsType<String> type = new GenericsType<>();
type.set("Pankaj"); //valid
GenericsType type1 = new GenericsType(); //raw type
type1.set("Pankaj"); //valid
type1.set(10); //valid and autoboxing support
}
}
주 메서드에서 GenericsType 클래스의 사용에 주목하세요. 우리는 형변환을 하지 않아도 되며 런타임에 ClassCastException을 제거할 수 있습니다. 생성 시 유형을 제공하지 않으면 컴파일러가 “GenericsType는 원시 유형입니다. 제네릭 유형인 GenericsType<T>에 대한 참조를 매개변수화해야 합니다”라는 경고를 발생시킵니다. 유형을 제공하지 않으면 유형은 Object
가 되며 따라서 String 및 Integer 객체를 허용합니다. 그러나 이렇게하는 것은 항상 피해야 합니다. 왜냐하면 원시 유형에서 작업할 때 형변환을 사용해야 하며 이는 런타임 오류를 발생시킬 수 있기 때문입니다.
팁: 컴파일러 경고를 억제하기 위해 @SuppressWarnings("rawtypes")
주석을 사용할 수 있습니다. 자바 어노테이션 튜토리얼을 확인하세요.
3. Java 일반 제네릭 인터페이스
Comparable 인터페이스는 인터페이스에서 제네릭의 훌륭한 예이며 다음과 같이 작성됩니다:
package java.lang;
import java.util.*;
public interface Comparable<T> {
public int compareTo(T o);
}
비슷한 방식으로 우리는 자바에서 일반적인 인터페이스를 만들 수 있습니다. Map 인터페이스와 같이 여러 개의 유형 매개변수를 가질 수도 있습니다. 다시 말해서 매개변수화된 유형에 매개변수화된 값을 제공할 수도 있습니다. 예를 들어 new HashMap<String, List<String>>();
은(는) 유효합니다.
4. Java 제네릭 유형
Java 제네릭 유형 명명 규칙은 코드를 쉽게 이해하고 명명 규칙을 가지는 것이 Java 프로그래밍 언어의 최상의 관행 중 하나입니다. 그래서 일반적으로 유형 매개변수 이름은 자바 변수와 쉽게 구별할 수 있도록 단일 대문자로 작성됩니다. 가장 일반적으로 사용되는 유형 매개변수 이름은 다음과 같습니다:
- E – Element (used extensively by the Java Collections Framework, for example ArrayList, Set etc.)
- K – Key (Used in Map)
- N – Number
- T – Type
- V – Value (Used in Map)
- S,U,V etc. – 2nd, 3rd, 4th types
5. Java 제네릭 메서드
가끔 우리는 전체 클래스를 매개변수화하고 싶지 않을 때가 있습니다. 그런 경우에는 자바 제네릭 메서드를 만들 수 있습니다. 생성자는 특별한 종류의 메서드이므로 생성자에서도 제네릭 유형을 사용할 수 있습니다. 여기 자바 제네릭 메서드 예제를 보여주는 클래스가 있습니다.
package com.journaldev.generics;
public class GenericsMethods {
//자바 제네릭 메서드
public static boolean isEqual(GenericsType g1, GenericsType g2){
return g1.get().equals(g2.get());
}
public static void main(String args[]){
GenericsType g1 = new GenericsType<>();
g1.set("Pankaj");
GenericsType g2 = new GenericsType<>();
g2.set("Pankaj");
boolean isEqual = GenericsMethods.isEqual(g1, g2);
//위의 문장은 다음과 같이 간단하게 작성할 수 있습니다.
isEqual = GenericsMethods.isEqual(g1, g2);
//이 기능은 타입 추론이라고 불리며, 각진 괄호 사이에 타입을 지정하지 않고도 제네릭 메서드를 일반 메서드로 호출할 수 있습니다.
//컴파일러는 필요한 타입을 추론할 것입니다
}
}
제네릭 타입을 메서드에서 사용하는 구문을 보여주는 isEqual 메서드 시그니처를 주목하세요. 또한, 이러한 메서드를 우리의 자바 프로그램에서 사용하는 방법을 주목하세요. 이러한 메서드를 호출할 때 타입을 지정할 수도 있고, 일반 메서드처럼 호출할 수도 있습니다. 자바 컴파일러는 사용할 변수의 타입을 결정하는 데 충분히 똑똑하며, 이 기능을 타입 추론이라고 합니다.
6. 자바 제네릭 바운드 타입 매개변수
우리는 매개변수화된 타입에서 사용할 수 있는 객체 유형을 제한하려고 한다고 가정해보십시오. 예를 들어 두 개의 객체를 비교하는 메서드에서 허용된 객체가 Comparables인지 확인하고 싶을 수 있습니다. 바운드된 타입 매개변수를 선언하려면 타입 매개변수의 이름을 나열한 다음 extends 키워드를 사용하여 상위 경계를 지정합니다. 아래 메서드와 유사하게
public static <T extends Comparable<T>> int compare(T t1, T t2){
return t1.compareTo(t2);
}
이러한 메서드의 호출은 비바운드 메서드와 유사하지만 Comparable이 아닌 클래스를 사용하려고 하면 컴파일 시간 오류가 발생합니다. 바운드된 타입 매개변수는 클래스 및 인터페이스뿐만 아니라 메서드와 함께 사용할 수 있습니다. Java 제네릭은 여러 경계도 지원합니다. 즉 <T extends A & B & C>. 이 경우 A는 인터페이스 또는 클래스일 수 있습니다. A가 클래스이면 B와 C는 인터페이스여야 합니다. 여러 경계에는 한 번에 하나의 클래스만 가질 수 없습니다.
7. Java Generics and Inheritance
우리는 Java 상속이 A가 B의 하위 클래스인 경우 변수 A를 변수 B에 할당할 수 있도록 허용한다는 것을 알고 있습니다. 따라서 A의 제네릭 타입이 B의 제네릭 타입에 할당될 수 있다고 생각할 수 있지만, 실제로 그렇지 않습니다. 간단한 프로그램을 통해 이를 확인해 봅시다.
package com.journaldev.generics;
public class GenericsInheritance {
public static void main(String[] args) {
String str = "abc";
Object obj = new Object();
obj=str; // works because String is-a Object, inheritance in java
MyClass myClass1 = new MyClass();
MyClass
우리는 MyClass
8. Java 제네릭 클래스와 하위 유형
우리는 제네릭 클래스나 인터페이스를 확장하거나 구현함으로써 하위 유형을 생성할 수 있습니다. 한 클래스나 인터페이스의 타입 매개변수와 다른 클래스나 인터페이스의 타입 매개변수 간의 관계는 확장 및 구현 절에서 결정됩니다. 예를 들어, ArrayList
interface MyList<E,T> extends List<E>{
}
List
9. Java 제네릭 와일드카드
물음표(?)는 일반적으로 와일드카드이며 알려지지 않은 타입을 나타냅니다. 와일드카드는 매개변수, 필드 또는 로컬 변수의 타입으로 사용될 수 있으며 때로는 반환 타입으로도 사용될 수 있습니다. 제네릭 메소드를 호출하거나 제네릭 클래스를 인스턴스화하는 동안 와일드카드를 사용할 수 없습니다. 다음 섹션에서는 상한 와일드카드, 하한 와일드카드 및 와일드카드 캡처에 대해 배워 보겠습니다.
9.1) 자바 제네릭 상한 와일드카드
상한 와일드카드는 메소드 내 변수의 타입에 대한 제한을 완화하는 데 사용됩니다. 리스트의 숫자 합계를 반환하는 메소드를 작성하려고 가정해보겠습니다. 이러한 경우 우리의 구현은 다음과 같습니다.
public static double sum(List<Number> list){
double sum = 0;
for(Number n : list){
sum += n.doubleValue();
}
return sum;
}
위의 구현의 문제점은 List<Integer>나 List<Double>과 같은 정수나 실수 리스트와 작동하지 않는다는 것입니다. 왜냐하면 List<Integer>와 List<Double>이 관련되지 않기 때문입니다. 이럴 때 상한 와일드카드가 유용합니다. 제네릭 와일드카드를 사용하여 extends 키워드와 상한 클래스 또는 인터페이스를 함께 사용하여 상한 또는 해당 서브클래스 유형의 인수를 전달할 수 있습니다. 위의 구현은 아래 프로그램과 같이 수정될 수 있습니다.
package com.journaldev.generics;
import java.util.ArrayList;
import java.util.List;
public class GenericsWildcards {
public static void main(String[] args) {
List<Integer> ints = new ArrayList<>();
ints.add(3); ints.add(5); ints.add(10);
double sum = sum(ints);
System.out.println("Sum of ints="+sum);
}
public static double sum(List<? extends Number> list){
double sum = 0;
for(Number n : list){
sum += n.doubleValue();
}
return sum;
}
}
인터페이스 관점에서 코드를 작성하는 것과 유사합니다. 위의 방법에서는 상위 바운드 클래스 Number의 모든 메서드를 사용할 수 있습니다. 상위 바운드 리스트에서는 null을 제외한 다른 객체를 목록에 추가할 수 없습니다. sum 메서드 내에서 목록에 요소를 추가하려고하면 프로그램이 컴파일되지 않습니다.
9.2) Java 제네릭 Unbounded Wildcard
가끔은 제네릭 메서드가 모든 유형과 작동하도록 원하는 상황이 있습니다. 이 경우, 제한되지 않은 와일드카드를 사용할 수 있습니다. 이는 <? extends Object>를 사용하는 것과 동일합니다.
public static void printData(List<?> list){
for(Object obj : list){
System.out.print(obj + "::");
}
}
printData 메서드에 List<String> 또는 List<Integer> 또는 다른 유형의 Object 목록 인수를 제공할 수 있습니다. 상위 바운드 목록과 유사하게 목록에 아무것도 추가할 수 없습니다.
9.3) Java 제네릭 Lower bounded Wildcard
가정하에, 우리가 메소드에 정수를 포함하려고 한다면, List<Integer>의 인수 유형을 유지할 수 있지만 이 경우 Integers와 묶일 것이다. 반면에 List<Number>와 List<Object>는 또한 정수를 보유할 수 있으므로 이를 달성하기 위해 하한 와일드카드를 사용할 수 있습니다. 이를 달성하기 위해 제네릭 와일드카드 (?)를 사용하고 하한 클래스와 super 키워드를 사용합니다. 이 경우 하한 또는 하한의 상위 유형을 인수로 전달할 수 있으며, 이 경우 Java 컴파일러는 하한 객체 유형을 목록에 추가할 수 있도록 허용합니다.
public static void addIntegers(List<? super Integer> list){
list.add(new Integer(50));
}
10. 제네릭 와일드카드를 사용한 하위 유형화
List<? extends Integer> intList = new ArrayList<>();
List<? extends Number> numList = intList; // OK. List<? extends Integer> is a subtype of List<? extends Number>
11. Java 제네릭 타입 소거
Java에서 제네릭은 컴파일 시간에 유형 확인을 제공하기 위해 추가되었으며 실행 중에는 사용되지 않습니다. 따라서 Java 컴파일러는 바이트 코드에서 모든 제네릭 유형 확인 코드를 제거하고 필요한 경우 형 변환을 삽입하기 위해 타입 소거 기능을 사용합니다. 타입 소거는 매개변수화된 유형에 대해 새 클래스가 생성되지 않도록 보장합니다. 따라서 제네릭은 실행 중 오버헤드가 없습니다. 예를 들어, 아래와 같은 제네릭 클래스가 있다고 가정해보겠습니다;
public class Test<T extends Comparable<T>> {
private T data;
private Test<T> next;
public Test(T d, Test<T> n) {
this.data = d;
this.next = n;
}
public T getData() { return this.data; }
}
Java 컴파일러는 바운드된 유형 매개변수 T를 첫 번째 바운드 인터페이스 Comparable로 대체합니다.
public class Test {
private Comparable data;
private Test next;
public Node(Comparable d, Test n) {
this.data = d;
this.next = n;
}
public Comparable getData() { return data; }
}
12. 일반적인 질문
12.1) 자바에서 일반적으로 왜 사용하나요?
일반화는 강력한 컴파일 시간 유형 확인을 제공하고 ClassCastException 및 객체의 명시적 캐스팅의 위험을 줄입니다.
12.2) 일반적으로 T는 무엇인가요?
우리는 제네릭 클래스, 인터페이스 및 메서드를 만들기 위해 <T>를 사용합니다. 사용할 때 T는 실제 유형으로 대체됩니다.
12.3) 자바에서 일반화는 어떻게 작동하나요?
일반화 코드는 유형 안전성을 보장합니다. 컴파일러는 런타임에서의 과부하를 줄이기 위해 컴파일 시간에 모든 유형 매개변수를 제거하기 위해 유형 소거를 사용합니다.
13. Java에서의 제네릭 – 추가 자료
- 제네릭은 하위 유형을 지원하지 않으므로
List<Number> numbers = new ArrayList<Integer>();
는 컴파일되지 않습니다. 제네릭이 왜 하위 유형을 지원하지 않는지를 배우세요. - 제네릭 배열을 생성할 수 없으므로
List<Integer>[] array = new ArrayList<Integer>[10]
은(는) 컴파일되지 않습니다. 왜 우리는 제네릭 배열을 생성할 수 없는지?를 읽어보세요.
이것으로 Java에서의 제네릭에 관한 내용은 모두입니다. Java 제네릭은 정말 방대한 주제이며 이를 이해하고 효과적으로 사용하기 위해서는 많은 시간이 필요합니다. 여기에 게시된 내용은 제네릭의 기본 세부 정보를 제공하고 타입 안전성을 확보하여 프로그램을 확장하는 방법에 대한 시도입니다.
Source:
https://www.digitalocean.com/community/tutorials/java-generics-example-method-class-interface