모던 자바(자바 5 이후 기술)에서 가장 중요한 개념은 Generics, Lambda, Annotation
타입 파라미터를 사용하면 Generic
class Hello<T> {
}
타입 파라미터를 추가했다.
메소드의 파라미터: 먼저 선언이 되어있고(괄호 안에) input이 됨.
타입에 해당하는 값을 던지면, 그 값이 선언된 파라미터에 바인딩이 되어서 사용할 수 있게 되는 것
에 실제 타입 정보를 주면 T에 해당하는 자리에 딱 맵핑이 되어서
public class Generics {
static class Hello<T> { // 타입 파라미터 추가
T t; // 필드로 사용되기도 하고
void method(T val) {} // 메서드의 value type으로 사용되기도 함.
}
public static void main(String[] args) {
new Hello<String>(); // 타입 인자로 String을 넘겨줌
}
넘기는 정보를 타입 인자 (type argument)라고 하고, T를 타입 파라미터 (type parameter)라고 함
컴파일 시점 컴파일러가 정확하게 타입 체킹, 캐스팅을 해줄 수 있다.
→ 런타임 시점에 밖에 checking이 안 되는 bug들이 숨어들어갈 수 있다.
```java
List list = new ArrayList();
list.add("Str");
Integer i = (Integer) list.get(0);
```
```java
List list = Array.asList(1,2,3);
list.add("abc");
```
Generic한 코드를 만들어 타입만 바꿔 재사용하기 유용하다.
99% 같은 코드를 Integer용, String용으로 여러 벌 만들기는 불필요.
타입과 관련해 심각한 에러를 낼 수 있다.
List<Integer> ints = new ArrayList<Integer>();
List rawInts = ints;
제네릭이면서 타입 파라미터 값 없이 선언한 것. 이렇게 써도 동작은 한다.
반대는 컴파일러가 잡지 못 하지만 런타임 에러가 발생함.
public static class Hello<T> {}
처럼 Generic이 class에 붙는 경우도 있지만, 메소드 혹은 인터페이스에도 붙을 수 있다.
인스턴스 메소드에 제네릭을 사용한 예
public class Generics {
<T> void print(T t) {
System.out.println(t.toString());
}
}
인자로 제네릭하게 T를 받을 거면, return type 앞에 타입 파라미터 를 붙임.
public static void main(String[] args) {
new Generics().print("Hello");
}
static 메소드에 제네릭을 사용한 예
(static 메소드: 인스턴스를 만들지 않아도 쓸 수 있는 메소드)
public class Generics {
<T> static void print(T t) {
System.out.println(t.toString());
}
}
public static void main(String[] args) {
print("Hello");
}
아래처럼 Class 레벨에 타입 파라미터를 정의하고, 메소드에서 갖다 쓰는 건 문제가 없음.
public class Generics<T> {
static void print(T t) {
System.out.println(t.toString());
}
}
이 경우 print 메소드를 static으로 사용할 수 없음.
public class Generics<T> {
static void print(T t) {
System.out.println(t.toString());
}
}
Type variable은 class의 instance가 만들어질 때 인자를 받아오게 되어 있음.
static은 Generics라는 클래스의 오브젝트를 만들지 않고 사용하는 것이므로 T type이 뭐가 될지 모름.
따라서 아래처럼 method 레벨에서 type 파라미터를 정의해줘야함
public class Generics<T> {
static <T> void print(T t) {
System.out.println(t.toString());
}
}
이럴 경우 class레벨의 와 메소드 레벨의 가 혼동이 올 수 있으니
public class Generics<T> {
static <S> void print(S s) {
System.out.println(s.toString());
}
}
알파벳을 바꿨음.
public class Generics<T> {
static <S,T> T print(S s) {
System.out.println(s.toString());
}
}
메소드 레벨에서 정의한 타입 파라미터 S를 인자로 받고, 클래스 레벨에서 정의한 타입 파라미터 T를 리턴 (혼용 가능)
public class Generics<T> {
public <S> Generics(S s) {
}
}
쓸 수 있는 타입에 제약을 걸어주는 것
타입 파라미터 자리엔 어떠한 타입도 올 수 있다. 여기에 이떠한 파라미터만 오도록 제한을 두겠음
public class Generics<T extends List> {
}
T라는 타입 파라미터는 List 타입의 subtype만 가능하다. (upper bound)
타입의 제약조건을 여러 타입으로 정해주는 것. 모두 만족하는 타입만 들어올 수 있음
인터페이스를 여러 개 구현할 수 있는 것과 마찬가지로 인터페이스만 가능.
클래스라면 상속은 하나만 할 수 있으니까 클래스는 하나만 가능.
public class Generics {
static <T extends List> void print(T t) {}
public static void main(String[] args) {
}
}
타입에 Boundary를 줄 때 &
로 분리해 여러 개를 줄 수 있음. <T extends List & Serializable>
public class Generics {
static long countGreaterThanTarget(Integer[] nums, int target) {
return Arrays.stream(arr).filter(num -> num > target).count();
}
public static void main(String[] args) {
Integer[] nums = new Integer[] { 1, 2, 3, 4, 5, 6, 7 };
System.out.println(countGreaterThanTarget(nums, 5));
}
}
String도 정렬할 수 있음. (사전 순에 따라) 만약에 이 함수를 스트링에 대해 쓰고 싶으면?
→ Generic 메소드로 정의하면 되겠구나
public class Generics {
static <T> long countGreaterThanTarget(T[] arr, T target) {
return Arrays.stream(arr).filter(element -> ).count();
}
public static void main(String[] args) {
Integer[] nums = new Integer[] { 1, 2, 3, 4, 5, 6, 7 };
String[] strs = new String[] { "a", "b", "c", "d" };
System.out.println(countGreaterThanTarget(nums, 5));
System.out.println(countGreaterThanTarget(strs, "b"));
}
}
메소드 레벨에서 타입을 정의하고, T type 배열, T type 원소 하나를 받게 함
부등호를 이용한 크기 비교가 문제임. 일반적으로 Object 사이에 크기 비교를 하려면 Comparable 인터페이스를 구현하여 compareTo 메소드를 사용하면 됨.
element.compareTo
를 하면 T type에 compareTo가 있는지 모름.
→ countGreaterThanTarget 메소드가 받는 인자 타입에 제한을 걸 수 있음
comparable이라는 인터페이스를 구현한 클래스에 대해서만, Comparable의 타입도 T임
static <T extends Comparable<T>> long countGreaterThanTarget(T[] arr, T target) {
return Arrays.stream(arr).filter(element -> element.comapreTo(target) > 0).count();
}
이게 가능한 이유는 Integer와 String 모두 Comparable를 구현했기 때문
Java의 숫자값을 표현하는데 쓰는 클래스들은 다 Number class의 서브 클래스
public class Generics {
public static void main(String[] args) {
Integer i = 10;
Number n = i; // 업캐스팅 가능
List<Integer> intList = new ArrayList<>();
List<Number> numberList = intList; // 컴파일 에러
}
왜 컴파일 에러가 발생할까? Integer는 Number의 서브타입이 맞지만, List는 List의 서브타입이 아님.
타입 파라미터(Integer와 Number)의 상속 관계는 적용이된 제네릭 타입(List)의 상속 관계에 영향을 미치지 않는다.
ArrayList<Integer> arrList = new ArrayList<>();
List<Integer> list = arrList; // 가능
이건 가능. ArrayList가 List의 서브타입이라 업캐스팅이 가능.
public class Generics {
static class MyList<E, V> implements List<E> {
}
public static void main(String[] args) {
}
}
타입 파라미터가 두 개인 MyList가 타입 파라미터가 하나인 List를 implements
타입 파라미터가 더 많은 경우 대입이 가능한가?
public static void main(String[] args) {
List<String> s1 = new MyList<String, Integer>();
List<String> s2 = new MyList<String, String>();
}
List type의 subtype으로 선언을 했기 때문에 대입이 됨! 타입 파라미터가 어떻게 붙든 상관이 없음
메서드 호출할 때 호출하는 정보를 보고 type argument가 뭐가 들어가야할지 컴파일러가 추론을 하는 기능
public class Generics {
static <T> void method(T t, List<T> list) {
}
public static void main(String[] args) {
method(1, Arrays.asList(1,2,3);
}
}
이 경우엔 컴파일러가 알아서 타입추론을 하여 잘 넣어주지만, 만약 모를 경우,
public static void main(String[] args) {
method<Integer>(1, Arrays.asList(1,2,3);
}
<Integer>
하고 알려주면 된다.
List<String> list = new ArrayList<>();
<>
안에 들어갈 것이 String이라는 것을 컴파일러가 알 수 있도록 해주는 타입 추론 중 하나.
List<String> c = Collections.emptyList();
T라는 타입 정보는 보통 메서드의 파라미터로 넘기는 인자 값의 파라미터 타입이 뭔가로 체크.
이런 경우는 매개변수가 없지만, 컴파일러가 String 타입의 빈 list구나를 판단.
List<String> c = Collections.<String>emptyList();
컴파일러가 알지 못 한다면 명시적으로 타입을 지정해줄 수 있음
List<T> list;
지금 선언하는 시점엔 T가 무슨 type인지 모르지만, 이 type이 정해지면 뭔지 알고 이후에 사용 하겠다.
List<?> list;
= List<? extends Object> list;
Object의 subtype인데, Object class에 정의된 기능 가져다 사용하겠다고 정의 (equals
, toString
등)
이 안에 type이 뭐가 오든 상관 없음. List interface가 가지고 있는 메서드를 사용하겠다고 정의 (size
등)
static void method(List<?> t) {
}
static <T> void method(List<T> t) {
}
static void method(List<? extends Comparable> t) {
}
static <T extends Comparable> void method(List<T> t) {
}
? == ? extends Object
static void printList(List<Object> list) {
list.forEach(s -> System.out.println(s));
}
명확하게 List의 원소가 Object여야 한다고 정의한 위와 같은 메소드를
List<Integer> list = Arrays.asList(1,2,3);
printList(list); // 컴파일 에러
이렇게 사용한다면 컴파일 에러.
List 를 List에 대입하는 것.
아까 example에서 대입했던 것과 같이 Integer는 Object의 하위 클래스지만,
List는 List의 하위 타입이 아님.
static void printList(List<?> list) {
list.forEach(s -> System.out.println(s));
}
List의 원소는 모르지만 어떤 거든 상관 없다고 정의한 위와 같은 메소드를
List<Integer> list = Arrays.asList(1,2,3);
***printList(list); // 컴파일 가능***
이렇게 사용한다면 컴파일 에러가 나지 않음.
? extends Object
라고 해도 오류나지 않음.