docs.oracle.com/javase/tutorial/java/generics/index.html
을 번역 및 이해하기 위해 만든 포스팅.
Lesson: Generics (Updated)
In any nontrivial software project, bugs are simply a fact of life. Careful planning, programming, and testing can help reduce their pervasiveness, but somehow, somewhere, they'll always find a way to creep into your code.
This becomes especially apparent as new features are introduced and your code base grows in size and complexity.
Fortunately, some bugs are easier to detect than others.
Compile-time bugs, for example, can be detected early on; you can use the compiler's error messages to figure out what the problem is and fix it, right then and there.
Runtime bugs, however, can be much more problematic; they don't always surface immediately, and when they do, it may be at a point in the program that is far removed from the actual cause of the problem.
Generics add stability to your code by making more of your bugs detectable at compile time. After completing this lesson, you may want to follow up with the Generics tutorial by Gilad Bracha.
제네릭의 이란, 코드 설계시 발생할 수 있는 문제들을 컴파일 시점에서 찾아내기 위한 방법이다.
Why Use Generics?
In a nutshell, generics enable types (classes and interfaces) to be parameters when defining classes, interfaces and methods. Much like the more familiar formal parameters used in method declarations, type parameters provide a way for you to re-use the same code with different inputs. The difference is that the inputs to formal parameters are values, while the inputs to type parameters are types.
Code that uses generics has many benefits over non-generic code:
- Stronger type checks at compile time.
A Java compiler applies strong type checking to generic code and issues errors if the code violates type safety. Fixing compile-time errors is easier than fixing runtime errors, which can be difficult to find. - Elimination of casts.
The following code snippet without generics requires casting:List list = new ArrayList(); list.add("hello"); String s = (String) list.get(0);
When re-written to use generics, the code does not require casting:List<String> list = new ArrayList<String>(); list.add("hello"); String s = list.get(0); // no cast
- Enabling programmers to implement generic algorithms.
By using generics, programmers can implement generic algorithms that work on collections of different types, can be customized, and are type safe and easier to read.
gerneric이 아닌 코드에 비해 갖는 장점은 위 3개로 요약할 수 있다.
1>컴파일 시점에서 타입 관련 안전 관점에서의 타입체크 기능이 강화된 타입을 사용할 수 있다.
2> Cast를 굳이 쓰지 않아도 된다.
3> 프로그래머가 제네릭알고리즘을 상속하여 사용할 수 있도록한다(제네릭을 사용하여 다양한 타입에서 작동하는 컬렉션을 커스텀하여 사용할 수 있으며, 타입 안전하며 읽기 쉬운 알고리즘을 개발자가 implement하여 설계할 수 있다, By using generics, programmers can implement generic algorithms that work on collections of different types, can be customized, and are type safe and easier to read)
Generic Types
A generic type is a generic class or interface that is parameterized over types.
예제 : 아래와 같은 코드를 생각하자.
[예제1]
public class Box{
private Object object;
public void set(Object object) { this.object = object; }
public Object get() { return object; }
}
위는 Object에 어떤 타입을 입력하더라도 컴파일이 되며, 이것에 대한 런타임에서의 동작에서의 에러는 설계자의 책임이 되는 코드이다.
(Since its methods accept or return an Object, you are free to pass in whatever you want, provided that it is not one of the primitive types. There is no way to verify, at compile time, how the class is used. One part of the code may place an Integer in the box and expect to get Integers out of it, while another part of the code may mistakenly pass in a String, resulting in a runtime error)
A Generic Version of the Box Class
제너릭 클래스는 아래 형식 처럼 정의해서 쓴다.
A generic class is defined with the following format:
class name<T1, T2, ..., Tn> {
/* ...
*/
}
[예제2] 예제 1번을 제너릭한 함수로 고치면 아래와 같다
/** * Generic version of the Box class.
* @param <T> the type of the value being boxed
*/
public class Box<T> {
// T stands for "Type"
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}
원시 타입을 제외한 모든 타입을 T에 쓸 수 있다. 이 방법을 사용하여 제너릭 interface를 사용할 수 있는 것이다.
(As you can see, all occurrences of Object are replaced by T. A type variable can be any non-primitive type you specify: any class type, any interface type, any array type, or even another type variable)
Type Parameter Naming Conventions
타입 파라미터는 variable/class,interface name들과의 혼돈을 피하기 위해 단일 대문자를 사용한다.
(By convention, type parameter names are single, uppercase letters. This stands in sharp contrast to the variable naming conventions that you already know about, and with good reason: Without this convention, it would be difficult to tell the difference between a type variable and an ordinary class or interface name)
잘 사용하는 타입 파라미터의 이름은,
- E - Element (used extensively by the Java Collections Framework)
- K - Key
- N - Number
- T - Type
- V - Value
- S,U,V etc. - 2nd, 3rd, 4th types
Invoking and Instantiating a Generic Type
T에다가 원하는 타입을 넣어 아래와 같이 invoking할 수 있다. 이는 메소드에 argument를 넣어 동작시키는 것처럼, 타입 을 argument처럼 사용하여 class 자체를 만들 수 있는 것이다.
(
To reference the generic Box class from within your code, you must perform a generic type invocation, which replaces T with some concrete value, such as Integer:
Box<Integer> integerBox;
You can think of a generic type invocation as being similar to an ordinary method invocation, but instead of passing an argument to a method, you are passing a type argument — Integer in this case — to the Box class itself.
)
type parameter는 제네릭에서의 T, type argument는 method에서의 Argument를 의미하므로, 둘을 구분하도록하자.
(
Type Parameter and Type Argument Terminology: Many developers use the terms "type parameter" and "type argument" interchangeably, but these terms are not the same. When coding, one provides type arguments in order to create a parameterized type. Therefore, the T in Foo<T> is a type parameter and the String in Foo<String> f is a type argument. This lesson observes this definition when using these terms.
)
선언은 객체를 만들지 않는다. 마찬가지로 타입을 정의한 invoking 또한 실제로 객체를 만들지 않는다.
제네릭 타입을 invoking하는 것은 매개화된 타입 이라 알려져있다(parameterized type)
이를 인스턴스화 하기 위해서는 객체화하는 과정이 따로 필요하다. 아래를 예시를 참고 하라.
Box<Integer> integerBox = new Box<Integer>();
(
Like any other variable declaration, this code does not actually create a new Box object. It simply declares that integerBox will hold a reference to a "Box of Integer", which is how Box<Integer> is read.
An invocation of a generic type is generally known as a parameterized type.
To instantiate this class, use the new keyword, as usual, but place <Integer> between the class name and the parenthesis:
Box<Integer> integerBox = new Box<Integer>();
)
The Diamond(자바)
자바7 이후로, 타입 추론 기능이 강화되어 아래와 같이 <> 만 쓰더라도 알아서 추론할 수 있게 되었다.
Box<Integer> integerBox = new Box<>();
(
In Java SE 7 and later, you can replace the type arguments required to invoke the constructor of a generic class with an empty set of type arguments (<>) as long as the compiler can determine, or infer, the type arguments from the context. This pair of angle brackets, <>, is informally called the diamond. For example, you can create an instance of Box<Integer> with the following statement:
Box<Integer> integerBox = new Box<>();
For more information on diamond notation and type inference, see Type Inference.
)
Multiple Type Parameters
다수의 타입 파라미터를 가지는 것이 가능하다.
위 코드는 아래와 같이 타입 정의하여 인스턴스화 할 수 있다.
타입 추론으로 아래와 같이 쓸수도있다.
Parameterized Types
매개화된 타입으로 타입 파라미터를 대체할 수 있다.
You can also substitute a type parameter (i.e., K or V) with a parameterized type (i.e., List<String>). For example, using the OrderedPair<K, V> example:
OrderedPair<String, Box<Integer>> p = new OrderedPair<>("primes", new Box<Integer>(...));
Raw Types
Raw Type은 type argument 없이 제네릭 클래스나 인터페이스를 사용할 때를 말한다.
아래에 정의해둔 Box에 대해 <Interger> 와 같은 정의 없이, Box rawBox와 같이 사용하는 경우를 말한다.
자바 5.0 이전에 사용되던 lagacy한 방법이며, 타입체크에 대해 취약하므로 사용하지 않도록하자.
(
A raw type is the name of a generic class or interface without any type arguments. For example, given the generic Box class:
public class Box<T> {
public void set(T t) {
/* ... */
} // ...
}
To create a parameterized type of Box<T>, you supply an actual type argument for the formal type parameter T:
Box<Integer> intBox = new Box<>();
If the actual type argument is omitted, you create a raw type of Box<T>:
Box rawBox = new Box();
Therefore, Box is the raw type of the generic type Box<T>. However, a non-generic class or interface type is not a raw type.
Raw types show up in legacy code because lots of API classes (such as the Collections classes) were not generic prior to JDK 5.0. When using raw types, you essentially get pre-generics behavior — a Box gives you Objects. For backward compatibility, assigning a parameterized type to its raw type is allowed:
Box<String> stringBox = new Box<>(); Box rawBox = stringBox; // OK
But if you assign a raw type to a parameterized type, you get a warning:
Box rawBox = new Box(); // rawBox is a raw type of Box<T> Box<Integer> intBox = rawBox; // warning: unchecked conversion
You also get a warning if you use a raw type to invoke generic methods defined in the corresponding generic type:
Box<String> stringBox = new Box<>(); Box rawBox = stringBox; rawBox.set(8); // warning: unchecked invocation to set(T)
The warning shows that raw types bypass generic type checks, deferring the catch of unsafe code to runtime. Therefore, you should avoid using raw types.
The Type Erasure section has more information on how the Java compiler uses raw types.
)
Generic Methods
제네릭 타입 선언과 마찬가지 방법으로 제네릭 메소드도 아래의 예제와 같이 사용할 수 있다.
타입 파라미터의 scope는 메소드가 선언된 부분 내에서 동작하며, 제네릭 클래스 생성자 뿐만 아니라 static, non-static 제네릭 메소드로 사용할 수 있다. 스태틱 메소드는 리턴 타입이 나오기전 타입 파라미터 section이 나와야 한다.
(아래 예제에서 static과 boolean 사이에 <K, V>를 넣어주어야함)
Generic methods are methods that introduce their own type parameters. This is similar to declaring a generic type, but the type parameter's scope is limited to the method where it is declared. Static and non-static generic methods are allowed, as well as generic class constructors.
The syntax for a generic method includes a list of type parameters, inside angle brackets, which appears before the method's return type. For static generic methods, the type parameter section must appear before the method's return type.
위 코드는 아래와 같이 인스턴스화 할 수 있다.
The complete syntax for invoking this method would be:
Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.<Integer, String>compare(p1, p2);
The type has been explicitly provided, as shown in bold.
Generally, this can be left out and the compiler will infer the type that is needed:
Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.compare(p1, p2);
Bounded Type Parameters
매개 변수화 된 타입에서 type arguments로 사용할 수있는 타입을 제한하려는 경우가 이를 사용한다.
예를들어서 Number instance로만 사용하고자할 때, 위의 방법을 사용한다.
extends 키워드를 사용하여 제한하고자 하는 타입의 upper bound를 설정할 수 있다.
(
There may be times when you want to restrict the types that can be used as type arguments in a parameterized type. For example, a method that operates on numbers might only want to accept instances of Number or its subclasses. This is what bounded type parameters are for.
To declare a bounded type parameter, list the type parameter's name, followed by the extends keyword, followed by its upper bound, which in this example is Number. Note that, in this context, extends is used in a general sense to mean either "extends" (as in classes) or "implements" (as in interfaces).
)
아래와 같이 에러가 나는 것을 볼 수 있다.
아래와 같이 제한을 건 제네릭타입이 제공하는 메소드를 invoke할 수 있다.
intValue는 integer에 정의 된 메소드이다.
Multiple Bounds
다수의 타입에 대한 제한도 가능하다.
제한할 타입에 클래스가 존재한다면 아래와 같이 제일 먼저 선언해주어야한다.
아래와 같이 순서가 바뀌면, 컴파일 에러가 난다.
Generic Methods and Bounded Type Parameters
바인딩 된 타입 매개 변수는 제네릭 알고리즘 구현의 핵심이다.
아래 예제는, 지정된 요소 elem보다 큰 배열 T []의 요소 수를 계산하는 방법인데 이를 참고하라.
위와 같이 쓰면 컴파일 에러가 난다.
비교 연산자 ( > )는 short,int, double과 같은 원시타입 에서만 사용할 수 있기때문이다.
이를 해결하기위해서는 Comparable<T>인터페이스로 타입파라미터를 제한하면된다.
(
The implementation of the method is straightforward, but it does not compile because the greater than operator (>) applies only to primitive types such as short, int, double, long, float, byte, and char. You cannot use the > operator to compare objects. To fix the problem, use a type parameter bounded by the Comparable<T> interface:
)
아래와 같이 타입을 제한함으로서 해결할 수 있다( T extends Comparable<T> 부분을 참조)
Generics, Inheritance, and Subtypes
객체지향 용어에서 "is a" 관계가 존재한다. 예를 들어 Object는 Number의 상위 타입이며, Number는 Integer의 상위 타입이다. 이럴 때 Integer is a kind of Object. 그리고, Integer also is a kind of Number.
(
As you already know, it is possible to assign an object of one type to an object of another type provided that the types are compatible. For example, you can assign an Integer to an Object, since Object is one of Integer's supertypes:
Object someObject = new Object();
Integer someInteger = new Integer(10);
someObject = someInteger; // OK
In object-oriented terminology, this is called an "is a" relationship. Since an Integer is a kind of Object, the assignment is allowed. But Integer is also a kind of Number, so the following code is valid as well:
public void someMethod(Number n) {
/* ... */
}
someMethod(new Integer(10)); // OK
someMethod(new Double(10.1)); // OK
)
위와 같은 관계는 제네릭에서도 동일하게 적용된다.
Box<Number> box = new Box<Number>();
box.add(new Integer(10)); // OK
box.add(new Double(10.1)); // OK
------------------------------------------------
아래 코드를 유심히 보라
------------------------------------------------
public void boxTest(Box<Number> n) {
/* ... */
}
위의 경우 무엇을 의미하는지를 생각해보라. Box<Number>의 타입은 정의한 그대로의 의미를 지닌다. 즉, Box<Double>, Box<Integer>는Box<Number>의
Subtype이 되지 않음을 분명히 기억하라.
(
What type of argument does it accept? By looking at its signature, you can see that it accepts a single argument whose type is Box<Number>. But what does that mean? Are you allowed to pass in Box<Integer> or Box<Double>, as you might expect? The answer is "no", because Box<Integer> and Box<Double> are not subtypes of Box<Number>.
This is a common misunderstanding when it comes to programming with generics, but it is an important concept to learn.
)
(
Note: Given two concrete types A and B (for example, Number and Integer), MyClass<A> has no relationship to MyClass<B>, regardless of whether or not A and B are related. The common parent of MyClass<A> and MyClass<B> is Object.
For information on how to create a subtype-like relationship between two generic classes when the type parameters are related, see Wildcards and Subtyping.
)
Generic Classes and Subtyping
type argument가 바뀌지않는다면 Class에서의 Subtype 관계는 성립된다. 아래 예제를 참고하라.
(
You can subtype a generic class or interface by extending or implementing it. The relationship between the type parameters of one class or interface and the type parameters of another are determined by the extends and implements clauses.
Using the Collections classes as an example, ArrayList<E> implements List<E>, and List<E> extends Collection<E>. So ArrayList<String> is a subtype of List<String>, which is a subtype of Collection<String>. So long as you do not vary the type argument, the subtyping relationship is preserved between the types.
)
Optional Value General Type P를 가지는 List interface를 아래와 같이 정의 할 수 있다.
interface PayloadList<E,P> extends List<E> {
void setPayload(int index, P val); ...
}
PayloadList의 매개화된 아래 선언들은 List<String>의 subtypes가 된다.
- PayloadList<String,String>
- PayloadList<String,Integer>
- PayloadList<String,Exception>
Type Inference
자바컴파일러는 type arguments의 적절한 타입을 가장 specific 한 type으로 추론해내려는 시도를 하게끔 설계되어있다.
(Type inference is a Java compiler's ability to look at each method invocation and corresponding declaration to determine the type argument (or arguments) that make the invocation applicable. The inference algorithm determines the types of the arguments and, if available, the type that the result is being assigned, or returned. Finally, the inference algorithm tries to find the most specific type that works with all of the arguments)
static <T> T pick(T a1, T a2) { return a2; }
Serializable s = pick("d", new ArrayList<String>());
위의 예에서, 컴파일러는 pick 메소드에 전달되는 두번째 인수가 Serializable이라고 결정한다.
(
부연 :
1> pick 메소드는 T를 return한다.
2> 그렇다면, ArrayList<String>()를 인수로 넣었는데 왜 Serializable인가?
3> pick 메소드는 두번째 인수 a2를 리턴하며 이것은 T 타입이다.
4> 그런데 s를 Serializable 타입으로 정의하였다. 따라서 리턴 타입 T는 Serializable이 되어야한다.
5> 컴파일러는 따라서 두번째에 들어가는 인수 ArrayList<String>()를 받고서 이를 Serializable로 추론한다.
6> Serializable는 ArrayList<String>의 SuperType일 것이다.
)
Type Inference and Generic Methods
이전 예제에서 제네릭 메소드의 타입추론을 잠시 보았다. 아래 코드를 보라.
public class Box<T> { private T t; // T stands for "Type"
public void set(T t) { this.t = t; }
public T get() { return t; } } |
public static <U> void addBox(U u, java.util.List<Box<U>> boxes) {
public class BoxDemo {
Box<U> box = new Box<>();
box.set(u);
boxes.add(box);
}
public static <U> void outputBoxes(java.util.List<Box<U>> boxes) {
int counter = 0;
for (Box<U> box: boxes) {
U boxContents = box.get();
System.out.println("Box #" + counter + " contains [" + boxContents.toString() + "]");
counter++;
}
}
public static void main(String[] args) {
java.util.ArrayList<Box<Integer>> listOfIntegerBoxes = new java.util.ArrayList<>();
BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);
BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);
BoxDemo.addBox(Integer.valueOf(30), listOfIntegerBoxes); BoxDemo.outputBoxes(listOfIntegerBoxes);
}
}
Box #0 contains [10]
Box #1 contains [20]
Box #2 contains [30]
1> main문에서 listOfIntegerBoxes를 정의할때 Box에 <Integer>가 들어가면서 Box의 parameter Type이 결정된다.
2> addBox에서 타입 파라미터 U를 정의하며, 1>에서 정의해둔 리스트를 넣어 사용할 수 있다
3> 그 다음부터 나오는 addBox들은 앞에 타입을 명시하지않아도 추론한다.
(
The generic method addBox defines one type parameter named U. Generally, a Java compiler can infer the type parameters of a generic method call. Consequently, in most cases, you do not have to specify them. For example, to invoke the generic method addBox, you can specify the type parameter with a type witness as follows:
BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);
Alternatively, if you omit the type witness,a Java compiler automatically infers (from the method's arguments) that
the type parameter is Integer:
BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);
)
Type Inference and Instantiation of Generic Classes
타입을 유추할 수 있는 곳은 다이아몬드(<>)를 사용하여 표현할 수 있다. 아래 예제를 보라.
Map<String, List<String>> myMap = new HashMap<String, List<String>>();
를 아래 처럼 바꿀 수 있다.
Map<String, List<String>> myMap = new HashMap<>();
아래와 같이 <>를 쓰지 않으면 아래와 같이 rawType 선언이 되면서 warning이 뜨게 된다.
Map<String, List<String>> myMap = new HashMap(); // unchecked conversion warning
Type Inference and Generic Constructors of Generic and Non-Generic Classes
아래와 같이 생성자에도 제네릭을 사용한 부분에 있어서 타입 추론을 적용할 수 있다.
class MyClass<X> {
<T> MyClass(T t) { // ... }
}
위를 정의 하고 아래와 같이 사용한다면,
new MyClass<Integer>("")
컴파일러가 Type 파라미터 T는 String으로 X는 Integer로 추론한다.
(
This statement creates an instance of the parameterized type MyClass<Integer>; the statement explicitly specifies the type Integer for the formal type parameter, X, of the generic class MyClass<X>. Note that the constructor for this generic class contains a formal type parameter, T. The compiler infers the type String for the formal type parameter, T, of the constructor of this generic class (because the actual parameter of this constructor is a String object).
In this example, the compiler infers the type Integer for the formal type parameter, X, of the generic class MyClass<X>. It infers the type String for the formal type parameter, T, of the constructor of this generic class.
)
Target Types
타겟 타입은 자바 컴파일러가 표현에 맞추어 예상하는 타입을 의미함.
The target type of an expression is the data type that the Java compiler expects depending on where the expression appears
static <T> List<T> emptyList();
List<String> listOne = Collections.emptyList();
라고 썼을 경우, listOne의 타겟 타입은 List<String>이다. 즉 data type List<String>이
listOne의 타겟 타입이 된다.
'Programming Theory > Generics' 카테고리의 다른 글
Generics -scala(1) (0) | 2020.12.05 |
---|---|
Generics - java(2) (0) | 2020.12.04 |