제네릭스는 타입시스템을 유연하게 사용할 수 있는 기능이다.
클래스나 메소드 레벨에서 명세(동작)는 같지만 사용되는 타입만 다른 경우, 객체를 생성할 때 타입을 지정할 수 있도록 하는 기능이다.
public class 클래스명<T> {...} public interface 인터페이스명<T> {...} <T> 리턴타입 함수명(파라미터타입 a){...} // T를 리턴타입, 파라미터 타입, 함수 구현체 등에서 타입으로 쓸 수 있음.
<T>
== Type<E>
== Element<K>
== Key<V>
== Value<N>
== Number<R>
== ResultCollection 클래스를 살펴보면 제네릭스를 어떻게 사용하는지 가장 잘 알 수 있다.
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList();
Collection<String> collection = list;
}
}
Collection.java 중 일부
public interface Collection<E> extends Iterable<E> {
int size();
boolean isEmpty();
Iterator<E> iterator();
boolean add(E e);
<T> T[] toArray(T[] a);
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
}
List.java 중 일부
public interface List<E> extends Collection<E> {
// Collection 에 있는 메소드들 모두 포함
// + List 에만 있는 메소드들
boolean add(E e);
}
ArrayList.java 중 일부
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
public <T> T[] toArray(T[] a) {
if (a.length < size)
// Make a new array of a's runtime type, but my contents:
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
}
람다는 클래스와 함수를 선언하지 않고, 함수형 표현을 할 수 있는 문법이다.
"식별자 없이 실행 가능한 함수" 로, 함수의 이름을 따로 정의하지 않아도 곧바로 함수처럼 사용할 수 있다. 문법이 간결하여 보다 편리한 방식이다 (익명 함수라고도 부른다.)
'→'의 의미는 매개변수를 활용하여 {}안에 있는 코드를 실행한다.
[기존의 메소드 형식]
반환타입 메소드이름(매개변수 선언) {
수행 코드 블록
}
[람다식의 형식]
반환타입 메소드이름(매개변수 선언) -> {
수행 코드 블록
}
public class Main {
public static void main(String[] args) {
ArrayList<String> strList =
new ArrayList<>(Arrays.asList("korea", "japan", "china", "france", "england"));
Stream<String> stream = strList.stream();
stream.map(str -> str.toUpperCase()).forEach(System.out::println);
}
}
위의 예제에서 ::
가 사용됐다. ::(이중 콜론 연산자)
는 호출하고자 하는 함수의 파라미터의 갯수와 각각의 타입이 labmda식에 전달되는 인자와 일치하는 경우, object 변수에 대한 선언과 파라미터 값 입력을 생략하고 호출하고자 하는 클래스의 함수만을 입력해서 코드 양을 줄일 수 있도록 도와준다.
이중 콜론 연산자는 functional(Java 8의 Stream) programming에서 사용할 수 있는 연산자다.
예제 - 이중 콜론 연산자를 사용한 경우
public class Main {
public static void main(String[] args) {
List<String> cities = Arrays.asList("서울", "부산", "속초", "수원", "대구");
cities.forEach(System.out::println);
}
}
예제 - 이중 콜론 연산자를 사용하지 않은 경우
public class Main {
public static void main(String[] args) {
List<String> cities = Arrays.asList("서울", "부산", "속초", "수원", "대구");
cities.forEach(x -> System.out.println(x));
}
}
람다식이 코드를 보다 간결하게 만들어주는 역할을 하지만 그렇다고 무조건 좋다고만 이야기 할 수는 없다.
람다를 사용하면 로그를 보거나 디버깅을 할 때 함수의 이름이 없다.
우리가 함수를 선언한다면 클래스를 선언하고 클래스는 패키지 이름까지 해서 자기의 이름이 있고 함수가 있고 안에 라인이 있으니까 찾아가기 쉽다. 그런데 식제로 익명함수를 쓰면 anonymous라는 이름으로 라인만 뜨는데 그 라인이 또 우리가 보는 코드의 라인과 일치하는 않는 경우도 있다. 그래서 함수의 정확한 이름과 위치가 나타나지 않을 수 있기 때문에 디버깅 시 람다를 많이 쓰면 어렵다.
StreamWithoutFunction.java
public class StreamWithoutFunction {
public static void main(String[] args) {
List<String> stringList = new ArrayList<>(Arrays.
asList("Korea","Japan","China","France","England"));
Stream<String> stream = stringList.stream();
stream.map(s -> {
System.out.println(s);
System.out.println("logic");
return s.toUpperCase();
}).forEach(System.out::println);
Stream<String> stream2 = stringList.stream();
stream2.map(s -> {
System.out.println(s);
System.out.println("logic");
return s.toUpperCase();
}).forEach(System.out::println);
}
}
StreamWithFunction.java
// 람다를 사용하지 않고 함수 사용하면 더 간결하게 코드 작성할 수 있다.
public class StreamWithFunction {
public static void main(String[] args) {
List<String> stringList = new ArrayList<>(Arrays.
asList("Korea", "Japan", "China", "France", "England"));
Stream<String> stream = stringList.stream();
stream.map(StreamWithFunction::logic).forEach(System.out::println);
Stream<String> stream2 = stringList.stream();
stream2.map(StreamWithFunction::logic).forEach(System.out::println);
}
public static String logic(String param){
System.out.println(param);
System.out.println("logic");
return param.toUpperCase();
}
}
public class Main {
public static void main(String[] args) {
List<String> stringList =
new ArrayList<>(Arrays.asList("Korea", "Japan", "China", "France", "England"));
Stream<String> errorStream = stringList.stream();
errorStream.map(Main::logic).map((str) -> new ArrayList<>(Arrays.asList(str)).stream()
.map(String::toLowerCase).map((nextStr) -> {
System.out.println("inner lambda");
if ("korea".equals(nextStr)) {
throw new RuntimeException("error");
}
return nextStr;
}).findFirst()).collect(Collectors.toList());
}
}
.map((nextStr) -> {
// ...
})
nextStr -> { ... }
는 람다 내부에 직접 구현된 익명 함수다.if ("korea".equals(nextStr)) {
throw new RuntimeException("error");
}
RuntimeException
이 발생한 위치가 익명 람다이기 때문에 스택 트레이스가 불친절해 디버깅이 어렵다.