열심히 끝까지
Java2 day14 본문
[14일차 수업내용]
제네릭스(generics)
---------------------------------------------------------------------------------------
1. 제네릭스(generics)
- 제네릭스는 다양한 타입의 객체들을 다루는 메소드나
컬렉션클래스에 컴파일 시 타입체크(Compile-time type check)를 해주는 기능
- 객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고
형변환의 번거로움이 줄어듬
- 타입안정성을 높인다는 것은 의도하지 않은 타입의 객체가 저장되는 것을 막고
저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 잘못 형변환되어
발생할 수 있는 오류를 줄여준다는 것
Ex)
ArrayList list = new ArrayList();
list.add(10);
list.add("abc");
for(int i = 0; i < list.size(); i++){
int num = (Integer)list.get(i);
}
ArrayList<Integer> list = new ArrayList<Integer>(); // 참조형만 올 수 있음(기본형 못옴)
= new ArrayList<>(); // 위와 동일(앞에서 언급해줬다면)
list.add(10);
list.add("abc"); // 빨간줄 그어짐
for(int i = 0; i < list.size(); i++){
int num = list.get(i); // 어차피 Integer만 오기 때문에 굳이 형변환 시킬 필요 없음
}
* 제네릭스의 장점
① 타입안정성을 제공
② 타입체크와 형변환을 생략할 수 있으므로 코드가 간결
==> 다룰 객체의 타입을 미리 명시해줌으로써
번거로운 형변환을 줄여준다는 의미!
2. 제네릭 클래스의 선언
- 제네릭 타입은 클래스와 메소드에 선언할 수 있는데,
먼저 클래스에 선언하는 제네릭 타입에 대해서 알아보자.
class Box{
Object item;
void setItem(Object Item){}
Object getItem(){}
}
- 위 클래스를 제네릭 클래스로 변경하고자 한다면
아래와 같이 '<T>'를 붙이고 Object는 모두 T로 변경
class Box<T>{
T item;
void setItem(T Item){}
T getItem(){}
}
- Box<T>에서 'T'는 타입변수(type variable)라고 하며 'Type'에서 첫글자를 따온 것
( 타입변수는 T가 아닌 다른 것을 사용해도 됨 )
- 타입변수가 여러개인 경우에는 Map(K, V)처럼 콤마를 구분자로 나열
- T, K, V 이들은 기호의 종류만 다를 뿐, '임이의 참조형 타입'을 의미하는 것은 모두 같음
- 기존에는 다양한 종류의 타입을 다루는 메소드의 매개변수나 리턴타입으로
Object타입의 참조변수를 많이 사용했고, 그로인해 형변환이 불가피했지만
이젠 Object타입 대신 원하는 타입을 지정하기만 하면 되는 것
Box<String> b = new Box<String>();
class Box{
String item;
void setItem(String Item){}
String getItem(){}
}
b.setItem(new Object()); // 에러 O String item = new Object();
b.setItem("abc"); // 에러 X String item = "abc";
2-1) 제네릭스 용어
class Box<T>{}
- Box<T> 제네릭 클래스, 'T의 Box' 혹은 T Box라고 읽음
- T 타입변수 또는 타입 매개변수(T는 타입문자)
- Box 원시타입(raw type)
- 타입문자 T는 제네릭 클래스 Box<T>의 타입 변수 또는 타입 매개변수라고 하는데
메소드의 매개변수와 유사한 점이 있기 때문
- 그래서 아래와 같이 타입 매개변수에 타입을 지정하는 것을 '제네릭 타입 호출'이라 하고
지정된 타입을 '매개변수화된 타입(parameterized type)'이라고 함
Box<String> b = new Box<String>();
Box<Integer> b1 = new Box<>();
- 위처럼 Box<String>, Box<Integer>는 서로 다른 타입을 대입하여 호출하는 것일 뿐
이 둘이 별개의 클래스를 의미하는 건 아님
2-2) 제네릭스 제한
- 제네릭 클래스 Box의 객체를 생성할 때는 객체별로 다른 타입을 지정해주는 것이 적절
왜? 제네릭은 인스턴스별로 다르게 동작하도록 만든 것이니까!
- 모든 객체에 대해 동일하게 동작해야 하는 static 멤버에 타입변수 T를 사용할 수 없음
왜? T는 인스턴스 변수로 간주되게 때문
class Box<T>{
static T item; // 에러
static int compare(T t1, T t2){} // 에러
}
- static 멤버는 타입 변수에 지정된 타입,
즉 대입된 타입의 종류에 관계없이 동일한 것이어야 하기 때문
- 또한 제네릭타입의 배열을 생성하는 것도 허용되지 않음
- 제네릭배열 타입의 참조변수 선언하는 것은 가능하지만(T[]),
new T[10];과 같은 배열을 생성하는 것은 안됨
==> new 연산자로 인해 불가능한 것인데,
new 연산자는 컴파일 시점에 타입 T가 무엇인지 정확히 알아야 하기 때문
-----------------------------------------------------------
package example01;
import java.util.ArrayList;
public class GenericsEx {
public static void main(String[] args) {
// 1. 제네릭스를 사용하지 않는 경우
/*
ArrayList list = new ArrayList();
list.add(10);
list.add("abc");
for(int i = 0; i < list.size(); i++) {
int num = (Integer)list.get(i);
System.out.println("num : " + num);
}
*/
// 2. 제네릭스를 사용한 경우
ArrayList<Integer> list = new ArrayList();
// ArrayList<Integer> list = new ArrayList<Integer>(); 위와 동일
list.add(10);
// list.add("abc");
list.add(20);
for(int i = 0; i < list.size(); i++) {
int num = list.get(i);
System.out.println("num : " + num);
}
}
}
-----------------------------------------------
package example01;
import java.util.ArrayList;
class Fruit{ public String toString() { return "Fruit"; }}
class Apple extends Fruit{ public String toString() { return "Apple"; }}
class Grape extends Fruit{ public String toString() { return "Grape"; }}
class Toy{ public String toString() { return "Toy"; }}
class Box<T>{ // 과일을 담는 Box 클래스
ArrayList<T> list = new ArrayList<>();
void add(T item) { // 과일을 박스에 추가하는 메소드
list.add(item);
}
T get(int i) { // 몇번째의 어떤 과일이 있는지 확인하는 메소드
return list.get(i);
}
int size() { // 박스 내의 과일의 개수를 확인하는 메소드
return list.size();
}
public String toString() { return list.toString(); }
}
public class FruitBoxEx01 {
public static void main(String[] args) {
Box<Fruit> fruitBox = new Box<>();
Box<Apple> appleBox = new Box<>();
Box<Toy> toyBox = new Box<>();
// Box<Grape> grapeBox = new Box<Apple>(); compile error ==> 제네릭 타입 불일치
// Box<Fruit> fruitBox2 = new Box<Apple>(); 상속관계여도 제네릭 타입이 일치하지 않는다면 에러
Fruit f = new Fruit();
fruitBox.add(f);
fruitBox.add(new Apple()); // Fruit item = new Apple(); 다형성 및 상속
fruitBox.add(new Grape()); // Fruit item = new Grape(); 다형성 및 상속
// fruitBox.add(new Toy()); // Fruit item = new Toy(); Fruit와 Toy는 상속관계가 아니다.
// appleBox.add(new Fruit()); // Apple item = new Fruit(); 다형성 적용 X
appleBox.add(new Apple());
appleBox.add(new Apple());
toyBox.add(new Toy());
System.out.println("fruitBox : " + fruitBox);
System.out.println("appleBox : " + appleBox);
System.out.println("toyBox : " + toyBox);
System.out.println("fruitBox[0] : " + fruitBox.get(0));
System.out.println("fruitBox.size : " + fruitBox.size());
}
}
-----------------------------------------------
3. 제한된 제네릭 클래스
- 타입문자로 사용할 타입을 명시하면 한 종류의 타입만 지정할 수 있도록 제한할 수 있지만,
그래도 여전히 모든 종류의 타입을 지정할 수 있다는 것에는 변함이 없음
- 타입 매개변수 T에 지정할 수 있는 타입의 종류를 제한할 수 있는 방법은 없을까?
Box<Toy> toyBox = new Box<>();
toyBox.add(new Toy()); // 에러는 안나지만 내 마음에 안듬
==> 과일 상자에 장난감을 담음
- 아래와 같이 제네릭 타입에 extends를 사용하면 특정 타입의 자손들만 대입할 수 있게
제한할 수 있음
class FruitBox<T extends Fruit> { /* 내용 생략 */ }
- 여전히 한 종류의 타입만 담을 수 있지만, Fruit클래스의 자손들만 담을 수 있다는
제한이 더 추가된 것
FruitBox<Toy> toyBox = new FruitBox<>(); // 에러남!!!
FruitBox<Fruit> fruitBox = enw FruitBox<>();
fruitBox.add(new Apple());
fruitBox.add(new Grape());
- 다형성에서 조상타입의 참조변수로 자손타입의 객체를 가리킬 수 있는 것 처럼,
매개변수화된 타입의 자손 타입도 가능한 것
- 만약 클래스가 아니라 인터페이스를 구현해야 한다는 제약이 필요하다면,
이 때도 implements가 아닌 extends를 사용
interface Eatable{}
class FruitBox<T implements Eatable> (X)
class FruitBox<T extends Eatable> (O)
class FruitBox<T extends Eatable & Fruit> (O)
-------------------------------------------
package example02;
import java.util.ArrayList;
interface Eatable{}
class Fruit implements Eatable{ public String toString() { return "Fruit"; }}
class Apple extends Fruit{ public String toString() { return "Apple"; }}
class Grape extends Fruit{ public String toString() { return "Grape"; }}
class Toy{ public String toString() { return "Toy"; }}
class Box<T>{
ArrayList<T> list = new ArrayList<>();
void add(T item) { list.add(item); }
T get(int i) { return list.get(i); }
int size() { return list.size(); }
public String toString() { return list.toString(); }
}
class FruitBox<T extends Fruit & Eatable> extends Box<T>{}
public class FruitBoxEx02 {
public static void main(String[] args) {
FruitBox<Fruit> fruitBox = new FruitBox<>();
FruitBox<Apple> appleBox = new FruitBox<>();
FruitBox<Grape> grapeBox = new FruitBox<>();
// FruitBox<Toy> toyBox = new FruitBox<>();
// FruitBox<Grape> grapeBox2 = new FruitBox<Apple>();
// FruitBox<Fruit> fruitBox2 = new FruitBox<Apple>();
// FruitBox<Apple> fruitBox2 = new FruitBox<Fruit>();
fruitBox.add(new Fruit());
fruitBox.add(new Apple());
fruitBox.add(new Grape());
appleBox.add(new Apple());
// appleBox.add(new Grape());
grapeBox.add(new Grape());
System.out.println("fruitBox : " + fruitBox);
System.out.println("appleBox : " + appleBox);
System.out.println("grapeBox : " + grapeBox);
}
}
-----------------------------------------------------------
4. 와일드카드 ( wild card )
- 매개변수에 과일박스(FruitBox)를 대입하면
주스를 만들어 반환하는 Juicer 클래스가 있고,
이 클래스에는 과일을 주스로 만들어서 반환하는
static 메소드 makeJuice()가 있다고 가정
class Juicer{
static Juice makeJuice(FruitBox<T> box){}
}
- Juicer클래스는 제네릭클래스가 아닌데다가 제네릭클래스라고 해도
static 메소드에서는 타입매개변수 T를 사용할 수 없으므로
아예 제네릭스를 적용하지 않던가 타입매개변수 대신 특정타입을 정해줘야 함
class Jucier{
static Juice makeJuice(FruitBox<Fruit> box){}
}
- FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
- FruitBox<Apple> appleBox = new FruitBox<Apple>();
Juicer.makeJuice(fruitBox); // 에러 안남 FruitBox<Fruit> box = new FruitBox<Fruit>();
Juicer.makeJuice(appleBox); // 에러가 남 FruitBox<Fruit> box = new FruitBo<Apple>();
- 이렇게 제네릭타입을' FruitBox<Fruit>'로 고정해 놓으면 위 코드에서도 알 수 있듯이
FruitBox<Apple>타입의 객체는 메소드의 매개변수가 될 수 없으므로
해당 메소드를 사용하기 위해선 메소드를 오버로딩 해야함
==> "제네릭타입이 다른 것만으로는 오버로딩이 성립하지 않는다"라는 규칙 때문에
우리가 생각한 것처럼 오버로딩을 하면 에러가 발생
- 이럴 때 사용하는 것이 와일드카드이다. 와일드 카드는 기호 ? 를 사용
class Jucier{
static Juice makeJuice(FruitBox<? extends Fruit> box){}
}
- FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
- FruitBox<Apple> appleBox = new FruitBox<Apple>();
Juicer.makeJuice(fruitBox);
FruitBox<? extends Fruit> box ==> FruitBox<Fruit> box;
Juicer.makeJuice(appleBox);
FruitBox<? extends Fruit> box ==> FruitBox<Apple> box;
- 와일드카드는 기호 ?로 표현하는데 와일드카드는 어떠한 타입도 될 수 있음
- ? 만으로는 Object타입과 다를게 없으므로 아래와 같이 제한을 할 수 있음
* < ? extends T > 와일드카드 상한제한, T와 그 자손들만 가능
* < ? super T > 와일드카드 하한제한, T와 그 조상들만 가능
* < ? > 제한없음, 모든 타입 가능
( < ? extends Object > 와 동일한 의미)
---------------------------------------------
package example03;
import java.util.ArrayList;
class Fruit { public String toString() { return "Fruit"; }}
class Apple extends Fruit { public String toString() { return "Apple"; }}
class Grape extends Fruit { public String toString() { return "Grape"; }}
class Box<T>{
ArrayList<T> list = new ArrayList<>();
void add(T item) { list.add(item); }
T get(int i) { return list.get(i); }
ArrayList<T> getList(){ return list; }
int size() { return list.size(); }
public String toString() { return list.toString(); }
}
class FruitBox<T extends Fruit> extends Box<T> {}
class Juice{
String name;
public Juice(String name) { this.name = name + "Juice"; }
public String toString() { return name; }
}
class Juicer{
// // 1. 와일드카드를 사용하지 않은 메소드
// static Juice makeJuice(FruitBox<Fruit> box) {
// String tmp = "";
//
// // 일반적인 for문
// /*
// for(int i = 0; i < box.getList().size(); i++) {
// tmp += box.getList().get(i) + " ";
// }
// return new Juice(tmp);
// */
//
// //향상된 for문
// for(Fruit f : box.getList()) {
// tmp += f + " ";
// }
// return new Juice(tmp);
// }
// 2. 와일드카드를 이용한 메소드
static Juice makeJuice(FruitBox<? extends Fruit> box) {
String tmp = "";
for(Fruit f : box.getList()) {
tmp += f + " ";
}
return new Juice(tmp);
}
}
public class FruitBoxEx03 {
public static void main(String[] args) {
FruitBox<Fruit> fruitBox = new FruitBox<>();
FruitBox<Apple> appleBox = new FruitBox<>();
fruitBox.add(new Apple());
fruitBox.add(new Grape());
appleBox.add(new Apple());
// 1. 와일드카드를 사용하지 않은 메소드
/*
System.out.println("[와일드카드 미사용]");
System.out.println(Juicer.makeJuice(fruitBox));
// System.out.println(Juicer.makeJuice(appleBox));
*/
// 2. 와일드카드를 이용한 메소드
System.out.println("[와일드카드 사용]");
System.out.println(Juicer.makeJuice(fruitBox));
System.out.println(Juicer.makeJuice(appleBox));
}
}
---------------------------------------------
5. 제네릭 메소드
- 메소드의 선언부에 제네릭 타입이 선언된 메소드를 제네릭 메소드라고 함
- 제네릭 타입의 선언 위치는 반환타입 바로 앞
public static <T> void sort(T[] a, Comparator<? super T> c)
- 제네릭 클래스에 정의된 타입 매개변수와 제네릭 메소드에 정의된 타입 매개변수는
별개의 것
- 같은 타입문자 T를 사용해도 같은 것이 아니라는 것에 주의해야 함
class A<T>{
public static <T> void sort(T[] a, Comparator<? super T> c){}
}
- 메소드에 선언된 제네릭타입은 지역변수를 선언한 것과 같다고 생각하면 편한데,
이 타입 매개변수는 메소드 내에서만 지역적으로 사용될 것이므로
메소드가 static이건 아니건 상관 없음
static Juice makeJuice(FruitBox<? extends Fruits> box){}
↓
↓제네릭 메소드로 변환
↓
static <T extends FruitBox> Juice makeJuice(FruitBox<T> box){}
FruitBox<Fruit> fruitBox = new FruitBox<>();
FruitBox<Apple> appleBox = new FruitBox<>();
Juicer.<Fruit>makeJuice(fruitBox);
Juicer.<Apple>makeJuice(appleBox);
↓
↓파라미터를 통해 컴파일러가 타입을 추정할 수 있기 때문에 생략 가능
↓
Juicer.makeJuice(fruitBox);
Juicer.makeJuice(appleBox);
---------------------------------------------------------------------------
package example03;
import java.util.ArrayList;
class Fruit { public String toString() { return "Fruit"; }}
class Apple extends Fruit { public String toString() { return "Apple"; }}
class Grape extends Fruit { public String toString() { return "Grape"; }}
class Box<T>{
ArrayList<T> list = new ArrayList<>();
void add(T item) { list.add(item); }
T get(int i) { return list.get(i); }
ArrayList<T> getList(){ return list; }
int size() { return list.size(); }
public String toString() { return list.toString(); }
}
class FruitBox<T extends Fruit> extends Box<T> {}
class Juice{
String name;
public Juice(String name) { this.name = name + "Juice"; }
public String toString() { return name; }
}
class Juicer{
// // 1. 와일드카드를 사용하지 않은 메소드
// static Juice makeJuice(FruitBox<Fruit> box) {
// String tmp = "";
//
// // 일반적인 for문
// /*
// for(int i = 0; i < box.getList().size(); i++) {
// tmp += box.getList().get(i) + " ";
// }
// return new Juice(tmp);
// */
//
// //향상된 for문
// for(Fruit f : box.getList()) {
// tmp += f + " ";
// }
// return new Juice(tmp);
// }
// 2. 와일드카드를 이용한 메소드
// static Juice makeJuice(FruitBox<? extends Fruit> box) {
// String tmp = "";
//
// for(Fruit f : box.getList()) {
// tmp += f + " ";
// }
// return new Juice(tmp);
// }
// 3. 제네릭 메소드
static <T extends Fruit> Juice makeJuice(FruitBox<T> box) {
String tmp = "";
for(Fruit f : box.getList()) {
tmp += f + " ";
}
return new Juice(tmp);
}
}
public class FruitBoxEx03 {
public static void main(String[] args) {
FruitBox<Fruit> fruitBox = new FruitBox<>();
FruitBox<Apple> appleBox = new FruitBox<>();
fruitBox.add(new Apple());
fruitBox.add(new Grape());
appleBox.add(new Apple());
// 1. 와일드카드를 사용하지 않은 메소드
/*
System.out.println("[와일드카드 미사용]");
System.out.println(Juicer.makeJuice(fruitBox));
// System.out.println(Juicer.makeJuice(appleBox));
*/
// 2. 와일드카드를 이용한 메소드
// System.out.println("[와일드카드 사용]");
// System.out.println(Juicer.makeJuice(fruitBox));
// System.out.println(Juicer.makeJuice(appleBox));
// 3. 제네릭 메소드 사용
System.out.println("[제네릭 메소드 사용]");
System.out.println(Juicer.<Fruit>makeJuice(fruitBox));
System.out.println(Juicer.makeJuice(fruitBox));
System.out.println(Juicer.<Apple>makeJuice(appleBox));
System.out.println(Juicer.makeJuice(appleBox));
}
}
----------------------------------------------------
package example04;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
class Fruit{
String name;
int weight;
public Fruit(String name, int weight) {
this.name = name;
this.weight = weight;
}
public String toString() { return name + "(" + weight + ")"; }
}
class Apple extends Fruit{ public Apple(String name, int weight) { super(name, weight); }}
class Grape extends Fruit{ public Grape(String name, int weight) { super(name, weight); }}
class Box<T>{
ArrayList<T> list = new ArrayList<>();
void add(T item) { list.add(item); }
ArrayList<T> getList() { return list; }
public String toString() { return list.toString(); }
}
class FruitBox<T extends Fruit> extends Box<T>{}
// 사과정렬기준
class AppleComp implements Comparator<Apple>{
@Override
public int compare(Apple o1, Apple o2) {
return o2.weight - o1.weight; // 내림차순
}
}
// 포도정렬기준
class GrapeComp implements Comparator<Grape>{
@Override
public int compare(Grape o1, Grape o2) {
return o1.weight - o2.weight; // 오름차순
}
}
// 과일정렬기준
class FruitComp implements Comparator<Fruit>{
@Override
public int compare(Fruit o1, Fruit o2) {
if(o1 instanceof Apple && o2 instanceof Apple) {
return o2.weight - o1.weight;
}else if(o1 instanceof Grape && o2 instanceof Grape) {
return o1.weight - o2.weight;
}
return -1;
}
}
public class FruitBox04 {
public static void main(String[] args) {
FruitBox<Apple> appleBox = new FruitBox<>();
FruitBox<Grape> grapeBox = new FruitBox<>();
appleBox.add(new Apple("GreenApple", 300));
appleBox.add(new Apple("GreenApple", 100));
appleBox.add(new Apple("GreenApple", 200));
grapeBox.add(new Grape("GreenGrape", 200));
grapeBox.add(new Grape("GreenGrape", 400));
grapeBox.add(new Grape("GreenGrape", 100));
System.out.println("[정렬 전 Apple과 Grape]");
System.out.println("AppleBox : " + appleBox);
System.out.println("GrapeBox : " + grapeBox);
// Collections.sort(appleBox.getList(), new AppleComp());
// Collections.sort(grapeBox.getList(), new GrapeComp());
Collections.sort(appleBox.getList(), new FruitComp());
Collections.sort(grapeBox.getList(), new FruitComp());
System.out.println("[정렬 후 Apple과 Grape]");
System.out.println("AppleBox : " + appleBox);
System.out.println("GrapeBox : " + grapeBox);
}
}
'Java2(주말)' 카테고리의 다른 글
Java2 day16 (0) | 2022.05.29 |
---|---|
Java2 day15 (0) | 2022.05.28 |
Java2 day13 (0) | 2022.05.21 |
Java2 day12 (0) | 2022.05.15 |
Java2 day11 보충 (0) | 2022.05.15 |