사실 나는 이 파트 너무 헷갈린다.
이 와일드 카드가 제한된 지네릭 클래스와 어떤 차이가 있다는 말일까?
계속 찾아봤는데
✔️ 제한된 지네릭 클래스(메서드)는 선언되는 것이어서 대표적이고( 좀 더 무거운 느낌)
✔️ 와일드 카드는 클래스(메서드)가 아닌 참조변수의 타입에 추가적으로 하한,상한 등의 제한을 주는 것 ( 제한된 지네릭 클래보다 가벼운 느낌) 이다.
개념이 명확해지면 추가하도록 하겠다.
복습하면서 개념이 명확해져서 추가한다
+) 일단 제한된 지네릭 클래스는 클래스(메서드) 선언할 때 즉 맨 처음 우리가 이런 역할로 사용하는 클래스나 메서드를 만들 때 그 틀에서 적용한다. T는 누구를 상속받은 객체여야 한다...인터페이스를 구현해야 한다 등 처음에 하는 것
+) 와일드 카드는 매개변수로 들어가는 참조변수가 이 선언된 클래스의 객체를 받는 경우 즉, 활용할 때 와일드 카드를 이용한다. 이미 지정된 타입으로 콕 집지 않고 범위를 넣게끔 만들어 준것!
static Juice makeJuice(FruitBox<Apple> box)->Apple로 콕 찍었다.
static Juice makeJuice(FruitBox<? extends Fruit> box)->Fruite의 자손이므로 콕에서 범위로 바뀜..
아래 예시를 보자
static 메서드의 매개변수인 참조형 변수 box에 <Fruit> 지네릭을 사용하였다.
앞 서 말했지만 static 메서드에는 T(인스턴스 변수 성격)를 매개변수로 사용할 수 없다. 따라서 여기 예시는 아예 특정타입 Fruit를 지정해주었다.
class Juicer {
static Juice makeJuice(FruitBox<Fruit> box) {
String tmp ="";
for(Fruit f:box.getList()) tmp+=f+" ";
return new Juice(tmp);
}
}
그렇다면 만약에 우리가 <Fruit>가 아닌 <Apple>이 필요하다면
저 Juicer 클래스에 static Juice makeJuice(FruitBox<Apple> box) 메서드를 하나 더 정의해야만 한다.
class Juicer {
static Juice makeJuice(FruitBox<Fruit> box) {
String tmp ="";
for(Fruit f:box.getList()) tmp+=f+" ";
return new Juice(tmp);
}
static Juice makeJuice(FruitBox<Apple> box) {
String tmp ="";
for(Fruit f:box.getList()) tmp+=f+" ";
return new Juice(tmp);
}
}
우리는 앞서 이것을 오버로딩이라고 배웠다. 하지만 이는 오버로딩이 아닌 에러이다.
왜냐하면 지네릭타입은 컴파일러가 컴파일할 때마 이용하고 제거해버리기 때문에
중복된 메서드 정의이며 에러이다.
그렇다면 어떻게 해야할까?
참조변수에 무언가 제한을 줄 수 있는 방법이 없을까?
이 때 우리는 와일드 카드를 이용한다.
와일드 카드는 '?' 라는 기호를 이용한다.
< ? extends T > : 와일드 카드의 상한 제한. T와 그 자손들만 가능
< ? super T > : 와일드 카드의 하한 제한. T와 그 조상들만 가능
< ? > : 제한 없음. 모든 타입이 가능. < ? extends Object >와 동일
참고로 &을 사용할 수 없다.
따라서 우리는 makeJuice 메서드의 참조형 매개변수를 아래와 같이 바꾸자.
static Juice makeJuice(FruitBox<? extends Fruit> box) {
String tmp ="";
for(Fruit f:box.getList()) tmp+=f+" ";
return new Juice(tmp);
}
예시를 통해 더 자세히 알아보자
import java.util.*;
class Fruit {
String name;
int weight;
Fruit(String name, int weight) { //생성자
this.name=name;
this.weight = weight;
}
public String toString() {return name+"("+weight+")";}
}
class Apple extends Fruit {
Apple(String name, int weight){
super(name, weight);
}
}
class Grape extends Fruit {
Grape(String name, int weight) {
super (name, weight);
}
}
class AppleComp implements Comparator<Apple>{
public int compare(Apple a1, Apple a2) {
return a2.weight - a1.weight;
}
}
class GrapeComp implements Comparator<Grape>{
public int compare(Grape g1, Grape g2) {
return g2.weight-g1.weight;
}
}
class FruitComp implements Comparator<Fruit>{
public int compare(Fruit f1, Fruit f2) {
return f1.weight-f2.weight;
}
}
class FruitBoxEx2 {
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("RedApple",200));
appleBox.add(new Apple("YellowApple",100));
grapeBox.add(new Grape("GreenGrape",300));
grapeBox.add(new Grape("RedGrape",200));
grapeBox.add(new Grape("YellowGrape",100));
Collections.sort(appleBox.getList(),new AppleComp());
Collections.sort(grapeBox.getList(),new GrapeComp());
System.out.println("appleBox-"+appleBox);
System.out.println("grapeBox-"+grapeBox);
System.out.println();
Collections.sort(appleBox.getList(),new FruitComp());
Collections.sort(grapeBox.getList(),new FruitComp());
System.out.println("appleBox-"+appleBox);
System.out.println("grapeBox-"+grapeBox);
}
}
class FruitBox<T extends Fruit> extends Box<T> {}
class Box<T>{
ArrayList<T> list = new ArrayList<T>();
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();}
}
👆 Collections 클래스의 sort메서드를 java API에 들어가서 캡처했다.
공식적으로 아래와 같다.
static <T> void sort(List list, Comparator<? super T> c)
이렇게 설정되었기 때문에 List에 요소로 Apple이 들어가 있어도 Comparator를 구현한 클래스가 Apple요소를 객체로 쓰거나 혹은 Apple의 조상을 요소로 쓰는 객체까지도 가능하도록 sort 메서드가 구현되어져 있다.
static <T> void sort(List list, Comparator<? super T> c)
메서드 선언부에 지네릭 타입이 선언된 메서드를 지네릭 메서드라고 하며
반환타입 바로 앞(!)에 위치한다.
주의할 것이 있다면
지네릭 메서드의 T는 지역변수의 의미가 있기 때문에
지네릭 클래스에 정의된 타입 매개변수 T(멤버 변수 중 인스턴스 변수의 의미)와 다른 것이다.
따라서 그 둘은 변수 T라는 이름은 같이 사용하지만 서로 다른 박자로, 다른 용도로, 전혀 다르게 사용된다는 것을 알아두자. (지역변수와 인스턴스변수)
class FruitBox <T>{
...
static <T> void sort(List<T> list, Comparator<? super T> c) {
...
}
}
한가지 더 짚고 넘어가자.
static멤버와 사용할 수 없는 타입 매개변수 T
하자민 static 메서드와는 사용가능하다. 그 이유는 앞서 말했던 주의해야할 점과 관련이 있는데 static멤버에 사용되는 T는 지네릭 클래스의 T이기 때문에 인스턴스 변수여서 사용이 불가능하다. 어떻게 인스턴스 변수 T에 static을 붙일 수 있겠는가?
그리고 static 메서드의 T는 메서드의 지역변수 의미가 있기 때문에 가능하다.
공부하다 보면 이 static에 집착하여 뭔가 static과 T가 연관이 깊은 것처럼 보이지만 그냥 T도 변수이기 때문에 변수와의 관계랑 똑같다!
지네릭 메서드로 바꾸는 방법
static Juice makeJuice(FruitBox<? extends Fruit> box) {
String tmp ="";
for(Fruit f : box.getList()) tmp+=f+"";
return new Juice(tmp);
}
⬇️
static <T extends Fruit> Juice makeJuice(FruitBox<T> box) {
String tmp ="";
for(Fruit f : box.getList()) tmp+=f+"";
return new Juice(tmp);
}
자 몇 가지 선언할 때 규칙들이 있다.
①
FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
FruitBox<Aplle> appleBox = new FruitBox<Apple>();
System.out.println(Juicer.<Fruit>makeJuice(fruitBox));
System.out.println(Juicer.<Apple>makeJuice(appleBox));
② 아래와 같이 컴파일러가 타입을 추정할 수 있는 경우 생략 가능
FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
FruitBox<Aplle> appleBox = new FruitBox<Apple>();
System.out.println(Juicer.makeJuice(fruitBox));
System.out.println(Juicer.makeJuice(appleBox));
③ 단, 지네릭 메서드를 호출할 때 대입된 타입을 생략할 수 없는 경우에는
참조변수나 클래스 이름을 생략할 수 없다.
FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
FruitBox<Aplle> appleBox = new FruitBox<Apple>();
System.out.println(<Fruit>makeJuice(fruitBox));//에러
System.out.println(this.<Fruit>makeJuice(appleBox));
System.out.println(Juicer.<Apple>makeJuice(appleBox));
지네릭 메서드는 매개변수의 타입이 복잡할 때도 유용하다.
public static void printAll(ArrayList <? extends Product> list,
ArrayList <? extends Product> list2) {
for(Unit u: list)
System.out.println(u);
}
⬇️
public static void <T extends Product> printAll(ArrayList <T> list,
ArrayList <T> list2) {
for(Unit u: list)
System.out.println(u);
}
① 원시타입과 지네릭 타입 간의 형변환 : 가능 하지만 경고 발생
Box box = null;
Box objBox = null;
box = (Box) objBox; // 가능, 하지만 경고는 발생
objBox = (Box) box ; // 가능, 하지만 경고는 발생
② 대입된 타입이 다른 지네릭 타입 간의 형변환 : 에러 Error!
Box objBox = null;
Box strBox = null;
objBox = (Box)strBox;//Error
strBox = (Box)objBox;//Error
③ 참조변수와 생성자에 대입된 타입이 일치해야 한다.( 앞서 배운 내용 1.3)
Box objBox = new Box();//Error
④ 다형성 제공
Box<? extends Object> objBox = new Box();//다형성으로 인한 형변환 가능
👀 앞 서 배웠던 내용과 같다.
static Juice makeJuice(FruitBox<? extends Fruit> box){...}
FruitBox<? extends Fruit> box = new FruitBox();//OK
FruitBox<? extends Fruit> box = new FruitBox();//OK
FruitBox<? extends Fruit> box = new FruitBox();//OK
모두 다형성으로 인해 가능했다.
⑤ ④와 반대의 경우 : 가능, 하지만 경고 발생
FruitBox<? extends Fruit> box = null;
FruitBox appleBox = (FruitBox)box;//<? extends Fruit>에 대입될 수 있는 타입 여러개
⑥ 실질적인 예를 통해 더 자세히 알아보자
위에 파란 칸은 이렇게 3가지 방법으로 표현될 수 있으나 저렇게 빈 Optional<?>로 Empty를 한 이유는 형변환 때문이다.
Optional<?> wopt = new Optional();
Optional oopt = new Optional();
Optional sopt = (Optional) wopt;//OK
Optional sopt = (Optional) oopt;//Error
즉 정리하자면 Optional를 Optional으로 직접 형변환 하는 것은 불가능하지만 와일드 카드가 포함된 지네릭 타입으로 형변환하면 가능하다.
✔️ 서로 관련된 상수를 편리하게 선언하기 위한 것
✔️ JDK1.5부터 추가된 기능
before | After |
---|---|
class Card{ | class Card{ |
static final int CLOVER =0; | enum Kind {CLOVER, HEART, DIAMOND, SPADE}; |
static final int HEART =1; | enum Value {TWO, THREE, FOUR}; |
static final int DIAMOND =2; | final Kind kind; |
static final int SPADE = 3; | final Value value; } |
static final int TWO =0; | |
static final int THREE =1; | |
static final int FOUR=4; | |
final int kind; | |
final int num; } |
자바의 열거형은 '타입에 안전한 열거형' 즉 실제값이 같아도 타입이 다르면 컴파일 에러가 발생 ==> 값 뿐만 아니라 타입까지 체크하여 타입에 안전하다!
상수의 값이 바뀌면 해당 상수를 참조하는 모든 소스를 다시 컴파일 해야한다는 것
하지만 열거형 상수를 사용하면 기존의 소스를 다시 컴파일 하지 않아도 된다!
열거형에 대한 정의
enum 열거형이름 {상수명1, 상수명2, ...}
정의된 상수 사용법 : '열거형이름.상수명'
① 열거형 상수간의 비교에는 equals()가 아닌 '=='를 사용
② '<','>'와 같은 비교 연산자가 아닌 comporeTo() 사용
같으면0, 왼쪽이 크면 양수, 작으면 음수
③ switch 조건식에도 열거형 사용, 이 때 주의할점은 case문에 열거형의 이름은 적지 않고 상수의 이름만 적는다.
열거형에 정의된 모든 상수를 출력하려면 values()메서드 이용
이 메서드는 열거형의 모든 상수를 배열에 담아 반환
이 메서드는 모든 열거형이 가지고 있는 것으로 컴파일러가 자동으로 추가해준다.
마찬가지로 static E valueOf(String name) 이 메서드도 자동적으로 추가해주는데
열거형 상수의 이름으로 문자열 상수에 대한 참조를 얻을 수 있게 해준다.
메서드 | 설명 |
---|---|
Class getDeclaringClass() | 열거형의 Class 객체를 반환한다. |
String name() | 열거형의 상수의 이름을 문자열로 반환한다. |
int ordinal() | 열거형 상수가 정의된 순서를 반환한다.(0부터 시작) |
T valueOf(Class enumType,String name) | 지정된 열거형에서 name과 일치하는 열거형 상수를 반환한다. |
enum Direction {EAST, SOUTH, WEST, NORTH}
public class EnumEx1 {
public static void main(String[] args) {
Direction d1= Direction.EAST;
Direction d2 =Direction.valueOf("WEST");
Direction d3 = Enum.valueOf(Direction.class,"EAST");
System.out.println("d1="+d1);
System.out.println("d2="+d2);
System.out.println("d3="+d3);
System.out.println("d1==d2 ? "+(d1==d2));
System.out.println("d1==d3 ? "+(d1==d3));
System.out.println("d1.equals(d3) ? "+(d1.equals(d3)));
// System.out.println("d2>d3 ? "+(d2>d3)); 에러
System.out.println("d1.compareTo(d3) ? "+d1.compareTo(d3));//같으면 0, 왼쪽이 크면 양수, 작으면 음수
System.out.println("d1.compareTo(d2) ? "+d1.compareTo(d2));
switch(d1) {
case EAST :
System.out.println("The direction is EAST.");break;
case SOUTH:
System.out.println("The direction is SOUTH.");break;
case WEST :
System.out.println("The direction is WEST.");break;
case NORTH :
System.out.println("The direction is NORTH.");break;
default :
System.out.println("Invalid direction.");break;
}
Direction [] Arr = Direction.values();
for(Direction d : Arr)
System.out.printf("%s=%d%n",d.name(),d.ordinal());
}
}
Enum클래스에서 정의된 ordinal()이 열거형 상수가 정의된 순서를 반환하지만, 이 값을 열거형 상수의 값으로 사용하지 않는 것이 좋다.
열거형 상수의 값이 불연속적인 경우에는
enum Direciton {EAST(1), SOUTH(5), WEST(-1), NORTH(10)}
위와 같이 열거형 상수 이름 옆 괄호에 그 값을 함께 적어준다.
① 그 후에 지정된 값을 저장할 수 있는 인스턴스 변수
② 생성자를 추가 (하지만 외부에서 생성 불가능-> 묵시적으로 private)
③ 또한 열거형 상수 값에는 여러개의 값을 저장할 수도 있다.
enum Direction {
EAST(1,">"), SOUTH(2,"V"), WEST(3,"<"),NORTH(4,"^");
...}
정리하면 아래 예시와 같다.
enum Direction {
EAST(1,">"), SOUTH(2,"V"), WEST(3,"<"),NORTH(4,"^");
private static final Direction[] DIR_ARR = Direction.values();
private final int value;
private final String symbol;
Direction(int value, String symbol) {//생성자
this.value=value;
this.symbol=symbol;
}
public int getValue() {return value;}
public String getSymbol() {return symbol;}
public static Direction of(int dir) {//dir에 있는 값 실제로는 dir-1
if(dir<1||dir>4) {
throw new IllegalArgumentException("Invalid value : "+dir);
}
return DIR_ARR[dir-1];
}
public Direction rotate(int num) {
num=num%4;//4번 돌면 제자리이므로 4로 나눈 나머지만
if(num<0) num+=4;
//시계 반대로 90도씩 n번 도는 것 = 시계 방향으로 90도씩 n+4번 도는 것
return DIR_ARR[(value-1+num)%4];
//DIR_ARR의 index는 0부터 시작하므로
}
public String toString() {
return name()+getSymbol();
}
}
class EnumEx2 {
public static void main(String[] args) {
for(Direction d : Direction.values())
System.out.printf("%s=%d%n",d.name(),d.getValue());
Direction d1 = Direction.EAST;
Direction d2 = Direction.of(1);
System.out.printf("d1=%s, %d%n",d1.name(),d1.getValue());
System.out.printf("d2=%s, %d%n",d2.name(),d2.getValue());
System.out.println(Direction.EAST.rotate(1));
System.out.println(Direction.EAST.rotate(2));
System.out.println(Direction.EAST.rotate(-1));
System.out.println(Direction.EAST.rotate(-2));
}
}
👆 나는 여기서 Direction.EAST.rotate(1);이 표현이 정말 신기한거 같다.
열거형 Direction의 상수 EAST가 메서드 rotate에 접근한다는 표현법을 기억하자.
✔️ 열거형 Transportation은 운송수단의 종류별로 상수를 정의
✔️ 각 운송수단에는 기본 요금이 책정되어 있다.
enum Transportation {
BUS(100), TRAIN(150), SHIP(100), AIRPLANE(300);//종류별 상수
private final int BASIC_FARE;//기본요금
private Transportation(int basicFare){
BASIC_FARE=basicFare;
}
int fare() {
return BASIC_FARE;
}
}
그러나 우리는 여기에 각 운송수단 별로 거리에 따라 요금을 계산하는 방식이 있을것이며 이것이 각각 수단마다 다르다는 것을 안다. 이럴 때 열거형 추상메서드 fare를 선언해보자.
enum Transportation {
BUS(100) { int fare(int distance) {return distance*BASIC_FARE;}},
TRAIN(150){ int fare(int distance) {return distance*BASIC_FARE;}},
SHIP(100){ int fare(int distance) {return distance*BASIC_FARE;}},
AIRPLANE(300){ int fare(int distance) {return distance*BASIC_FARE;}};
abstract int fare(int distance);//거리에 따라 요금을 측정하는 추상 메서드
//private final int BASIC_FARE;
protected final int BASIC_FARE; //각 상수에서 접근하기 위해
Transportation(int basicFare){
BASIC_FARE=basicFare;
}
public int getBasicFare() {return BASIC_FARE;}
}
예시를 통해 자세히 알아보자
enum Transportation {
BUS(100) {int fare(int distance){ return BASIC_FARE*distance;}},
TRAIN(150){int fare(int distance){ return BASIC_FARE*distance;}},
SHIP(100){int fare(int distance){ return BASIC_FARE*distance;}},
AIRPLANE(300){int fare(int distance){ return BASIC_FARE*distance;}};
protected final int BASIC_FARE;
Transportation (int basicFare) {
BASIC_FARE=basicFare;
}
public int getBasicFare() {return BASIC_FARE;}
abstract int fare(int distance);
}
public class EnumEx3 {
public static void main(String[] args) {
System.out.println("bus fare="+Transportation.BUS.fare(100));
System.out.println("train fare="+Transportation.TRAIN.fare(100));
System.out.println("ship fare="+Transportation.SHIP.fare(100));
System.out.println("airplane fare="+Transportation.AIRPLANE.fare(100));
}
}