제네릭의 선언 및 생성
package me.whiteship.livestudy.week14;
public class GenericSample<T> {
T element;
public void setElement(T element) {
this.element = element;
}
public T getElement() {
return element;
}
}
타입 변수
제네릭 타입의 이름 정하기
예제
package me.whiteship.livestudy.week14;
public class GenericSample<T> {
T element;
public void setElement(T element) {
this.element = element;
}
public T getElement() {
return element;
}
public static void main(String[] args) {
GenericSample<Integer> integerGenericSample = new GenericSample<>();
integerGenericSample.setElement(3);
GenericSample<String> stringGenericSample = new GenericSample<>();
stringGenericSample.setElement("thewing");
System.out.println(integerGenericSample.getElement());
System.out.println(stringGenericSample.getElement());
}
}
output:
3
thewing
제네릭 타입에는 여러가지가 있다.
예제
package me.whiteship.livestudy.week14;
public class BoundTypeSample <T extends Number>{
public void set(T value){}
public static void main(String[] args) {
BoundTypeSample<Integer> boundTypeSample = new BoundTypeSample<>();
boundTypeSample.set("Hi"); // 오류
}
}
<T extends Number>
로 선언한다. BoundTypeSample의 타입으로 Number의 서브 타입만 허용한다는 것이다.Unbounded Wildcard는 List<?>
와 같은형태로 물음표만 가지고 정의되어지게 된다. 내부적으로 Object로 정의되어서 사용되고 모든 타입의 인자를 받을 수 있다. 타입 파라미터에 의존하지 않는 메서드만을 사용하거나 Object 메서드에서 제공하는 기능으로 충분한 경우에 사용한다.
Object 클래스에서 제공되는 기능을 사용하여 구현할 수 있는 메서드를 작성하는 경우
타입 파리미터에 의존적이지 않은 일반 클래스의 메서드를 사용하는 경우 Wild Card를 사용한다.
EX) List.clear,List.size 등등
Upper Bounded WildCard
List<? extends Foo>
와 같은 형태로 사용하고 특정 클래스의 자식 클래스만을 인자로 받는다는 것이다. 임의의 Foo 클래스를 상속받는 어느 클래스가 와도 되지만 사용할 수 있는 기능은 Foo 클래스에 정의된 기능만 사용이 가능하다.Lower Bounded WildCard
List<? super Foo>
와 같은 형태로 사용하고, Upper Bounded WildCard와 다르게 특정 클래스의 부모 클래스만을 인자로 받는다는 것이다.기타
매개변수화 타입(parameterized type)
List<String> list = new ArrayList<>();
public interface List<E> extends Collection<E> {
}
List<String> list = new ArrayList();
예제코드:
public static void main(String[] args) {
List<String> list = new ArrayList<>();
}
컴파일 파일
public static void main(String[] args) {
new ArrayList();
}
바이트코드를 확인해보자
ArrayList()를 생성할때 어떠한 타입 정보도 들고있지않다. new ArrayList()로 생성한 것과 동일하게 바이트코드가 생성된다.
컴파일러는 컴파일 단계에서 List 컬렉션에 String 인스턴스만 저장되어야 한다는 것을 알게되었고 또 그것을 보장해주기 때문에 ArrayList로 변경하여도 런타임에 동일한 동작을 보장한다. E,List와 같은 타입들을 비구체화 타입(타입 소거자에 의해 컴파일 타임에 타입 정보가 사라지는것(런타임에 구체화 하지 않는것))이라하며, 그 반대로 구체화 타입(자신의 타입 정보를 런타임 시에 알고 지키게하는것(런타임에 구체화 하는것))이 있으며 primitives,non-generic type, raw types 또는 List<?> Map
과 같이 Unbounded wildcard Type이 있다.
제네릭 선언에 사용하는 타입의 범위도 지정할수있다.
<>
에 어떤 타입도 상관 없다고 했지만, wildcard로 사용하는 타입을 제한할 수는 있다package me.whiteship.livestudy.week14;
public class WildcardGeneric <E>{
E wildcard;
public E getWildcard() {
return wildcard;
}
public void setWildcard(E wildcard) {
this.wildcard = wildcard;
}
}
package me.whiteship.livestudy.week14;
public class Car {
protected String name;
public Car(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String toString(){
return "Car name = " + name;
}
}
package me.whiteship.livestudy.week14;
public class Bus extends Car{
public Bus(String name) {
super(name);
}
@Override
public String toString(){
return "Bus name= " + name;
}
}
package me.whiteship.livestudy.week14;
public class CarWildcardSample {
public void callBoundedWildcardMethod(){
WildcardGeneric<Car> wildcard = new WildcardGeneric<Car>();
wildcard.setWildcard(new Car("Mustang"));
boundedWildcardMethod(wildcard);
}
public void boundedWildcardMethod(WildcardGeneric<? extends Car> c){
Car value = c.getWildcard();
System.out.println(value);
}
public static void main(String[] args) {
CarWildcardSample carWildcardSample = new CarWildcardSample();
carWildcardSample.callBoundedWildcardMethod();
}
}
<? extends Car> c
이 들어있다. 이렇게 정의한 것은 제네릭 타입으로 Car를 상속받은 모든 클래스를 사용할 수 있다는 의미이다. 따라서 boundedWildcardMethod() 의 매개변수에는 다른 타입을 제네릭 타입으로 선언한 객체가 넘어올 수없다. output:
Car name = Mustang
Process finished with exit code 0
package me.whiteship.livestudy.week14;
public class GenericWildcardSample {
public <T> void genericMethod(WildcardGeneric<T> c, T addValue){
c.setWildcard(addValue);
T value = c.getWildcard();
System.out.println(value);
}
public void callGenericMethod(){
WildcardGeneric<String> wildcard = new WildcardGeneric<String>();
genericMethod(wildcard,"Data");
}
public static void main(String[] args) {
GenericWildcardSample genericWildcardSample = new GenericWildcardSample();
genericWildcardSample.callGenericMethod();
}
}
output:
Data
public static<T> void sort(,,,){
}
아래 예제의 제네릭 클래스에서 정의된 타입 변수 T와 제네릭 메서드에서 사용된 타입 변수 T는 별개이라는 것을 알아야한다.
public class Collections{
public static<T> void sort(List<T> list, Comparator<? super T> c){
list.sort(c);
}
}
List<String> list = new ArrayList<Integer>(); // 컴파일 에러
list/add("thewing"); // typle이 일치하지 않아 addd가 안된다.
이와 같은 상황에서 컴파일 오류를 확인할수있다. java 컴파일러는 타입소거를 아래와 같이 적용한다.
제네릭 타입(Example) 에서는 해당 타입 파라미터(T) 나 Object로 변경해준다. Object로 변경하는 겨우 unbounded 된 경우를 뜻하며, 이는 <E extends Comparable>
와 같이 bound를 해주지 않는 경우를 의미한다. 이 소거 규칙에 대한 바이트 코드는 제네릭을 적용할 수 있는 일반 클래스, 인터페이스, 메서드에 적용이 가능하다.
타입 안전성 보존을 위해 필요시 type casting을 넣어준다.
확장된 제네릭 타입에서 다형성을 보존하기 위해 bridge method를 생성한다.
public static <E> boolean containsElement(E [] elements, E element){
for (E e : elements){
if(e.equals(element)){
return true;
}
}
return false;
}
실제로 이렇게 선언되어 있는 제네릭 메서드의 경우 선언 방식에 따라 컴파일러가 타입 파라미터 E를 실제 유형의 Object로 변경한다.
public static boolean containsElement(Object [] elements, Object element){
for (Object e : elements){
if(e.equals(element)){
return true;
}
}
return false;
}
만든 이유는?
package me.whiteship.livestudy.week14;
import java.util.Arrays;
public class Example1<T>{
private T[] myArray;
public Example1(int size) {
// myArray = new T[size]; X(컴파일 오류)
// Type parameter 'T' cannot be instantiated directly
myArray = (T[]) new Object[size];
}
public void addElem(int index,T t){
myArray[index] = t;
}
public void printElem(){
System.out.println(Arrays.toString(myArray));
}
public static void main(String[] args) {
Example1<String> e = new Example1<>(3);
e.addElem(0,"java");
e.addElem(1,"jeneric");
e.printElem();
}
}
9번라인처럼 제네릭타입을 사용해서 배열을 생성하면 편할텐데, 10라인처럼 생성해야하는 이유는?
**static 변수에도 제네릭 타입을 사용할 수 없다.**
private T[] myArray;
private static T[] arr;
static 키워드를 사용해서 멤버 필드를 선언하게 되면, 특정 객체에 종속되지 않고 클래스 이름으로 접근해서 사용할수있다.
제네릭 타입을 사용하면, 위의 예제의 경우 Example<String>
과Example<Integer>
등으로 객체를 생성해서 인스턴스마다 사용하는 타입을 다르게 사용할수있어야하는데 static 으로 선언한 변수는 사용할수 없다.
하지만 재밌게도 **static 메서드**에는 제네릭을 사용할 수 있다.
Static 키워드를 사용하면 클래스 이름으로 접근하여 객체를 생성하지 않고 여러 인스턴스에서 공유해서 사용할 수 있다.
변수같은 경우 해당 값을 사용하려면 값의 타입을 알아야하지만,
메서드의 경우 해당 기능을 공유해서 사용하는 것이기 때문에 제네릭 타입 변수 T를 매개변수로 사용한다고 하면 해당 값은 메서드안에서 지역 변수로 사용되기 때문에 변수와 달리 메서드는 static 으로 선언 되어 있어도 제네릭을 사용할수있다.
따라서 컴파일러는 코드의 형식 안정성을 보장하고 런타임 오류를 방지한다.
클래스 수준에서 컴파일러는 클래스의 Type Parameter를 버리고 첫번째 바인딩으로 대체하거나 Type Parameter가 바인딩 되지 않은 경우 Object로 변환한다.
배열을 사용하여 Stack 구현의 예시를 보자.
package me.whiteship.livestudy.week14;
public class Stack<E> {
private E[] stackContent;
public Stack(int capacity) {
this.stackContent = (E[]) new Object[capacity];
}
public void push(E data){
}
public E pop(){
}
}
package me.whiteship.livestudy.week14;
public class Stack< {
private Object[] stackContent;
public Stack(int capacity) {
this.stackContent = (Object[]) new Object[capacity];
}
public void push(Object data){
}
public Object pop(){
}
}
package me.whiteship.livestudy.week14;
public class BoundStack <E extends Comparable<E>>{
private E[] stackContent;
public BoundStack(int capacity) {
this.stackContent = (E[]) new Object[capacity];
}
public void push(E data){
}
public E pop(){
}
}
컴파일러는 바인됭 된 형식 매개변수 E를 첫번쨰 바인딩 된 클래스인 Comparable로 대체한다.
package me.whiteship.livestudy.week14;
public class BoundStack{
private Comparable로[] stackContent;
public BoundStack(int capacity) {
this.stackContent = (Comparable로[]) new Object[capacity];
}
public void push(Comparable로 data){
}
public Comparable로 pop(){
}
}
[주어진 배열의 내용을 표시하는 예제]
public static <E> void printArray(E[] array){
for (E element: array){
System.out.printf("%s " , element);
}
}
public static void printArray(Object[] array){
for (Object element: array){
System.out.printf("%s " , element);
}
}
[바인딩된 method type parameter의 경우]
public static <E extends Comparable<e>> void printArray(E[] array){
for (E element: array){
System.out.printf("%s " , element);
}
}
public static void printArray(Comparable로[] array){
for (Comparable로 element: array){
System.out.printf("%s " , element);
}
}
package me.whiteship.livestudy.week14.ex;
public class Entity<K> {
protected K id;
public K getId() {
return id;
}
}
package me.whiteship.livestudy.week14.ex;
public class Apple extends Entity<Integer> {
public static Apple of(Integer id){
Apple apple = new Apple();
apple.id = id;
return apple;
}
}
package me.whiteship.livestudy.week14.ex;
public class Banana extends Entity<Integer> {
public static Banana of(Integer id){
Banana banana = new Banana();
banana.id = id;
return banana;
}
}
package me.whiteship.livestudy.week14.ex;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class GenericDao<E extends Entity<K>,K> {
private Map<K,E> dataSource =new HashMap<>();
public void save(E e){
dataSource.put(e.getId(),e);
}
public void delete(E e){
dataSource.remove(e.getId());
}
public void delete(K k){
dataSource.remove((k));
}
public List<E> findAll(){
return new ArrayList<>(dataSource.values());
}
public E findById(K k){
return dataSource.get(k);
}
}
package me.whiteship.livestudy.week14.ex;
import java.util.ArrayList;
import java.util.List;
public class App {
public static void main(String[] args) {
GenericDao<Apple,Integer> genericDao = new GenericDao<>();
genericDao.save(Apple.of(5));
genericDao.save(Apple.of(7));
List<Apple> appleList = genericDao.findAll();
for (Apple apple : appleList) {
System.out.println(apple.getId());
}
}
}