이번에는 제네릭 이라는 중요한 기능을 알아볼 것이다.
제네릭은 다음에 게시할 컬렉션에서 중요하게 다루는 핵심 기능이다.
설명하기에 앞서 제네릭의 장점을 설명하자면.
1.코드의 중복을 줄일수가 있다.
2.타입의 안전성을 확보하고 오류를 방지한다.
이렇게 2가지가 있는데 이해는 안가겠지만 장점을 계속 기억하며 아래 글을 읽어보자.(결국 다 이야기 할꺼임)
1. 코드의 중복을 줄일수가 있다.
제네릭
사전에 검색하면 포괄적인 이라는 뜻을 보여준다.
뭘 포괄 한다는 것일까.
바늘이 있다.
바늘은 한의사가 침을 놓을때 쓸수도 있고 아니면 옷을 수선 할때 사용할수도 있다.
바늘의 사용법을 사용자의 목적에 맞게 사용자가 스스로 정의 할수 있는 것이다.
우리는 어떠한 값을 넣을때 타입이 중요하다는 것을 잘 알고 있다.
숫자를 넣고 싶다면 변수를 선언할때 int를 쓰고, 문자를 넣고 싶으면 String, 논리식은 boolean등.. 값을 넣기 전에 타입을 입력해 주었다.
잘 알겠지만 int a = 5; 는 가능하지만 int a = "치킨"; 은 불가능 하단 소리다.
그런데 만약 a라는 변수에 숫자5도 넣고 싶고 "치킨" 라는 문자열도 넣고 싶으면 어떻게 할까? 예제를 만들어 보았다.
아래 코드는 delivery 클래스에서 인스턴스로 인자를 받아서 order라는 매개변수에 값을 넣는 코드이다.
class delivery{
public int order;
delivery(int order){
this.order = order;
}
@Override
public String toString() {
return "delivery{" +
"order=" + order +
'}';
}
}
public class Main {
public static void main(String[] args) {
delivery d1 = new delivery(5);
System.out.println(d1);
// delivery d2 = new delivery("치킨");
}
}
실행 결과는 다음과 같다.
delivery{order=5}
int형을 선언했기에 당연히 배달이라는 매개변수에 "치킨" 이라는 문자열을 입력할수가 없다. 뭐.. 간단하게는 클래스 하나더 추가하면 당연히 String 입력이 가능할 것이다.(표현 할수 있는 다른 방법도 많겠지만 이걸 예시로 하겠다.)
class delivery{
public int order;
delivery(int order){
this.order = order;
}
}
class deliveryitem{
public String order;
deliveryitem(String order){
this.order = order;
}
}
public class Main {
public static void main(String[] args) {
delivery d1 = new delivery(5);
System.out.println(d1);
deliveryitem d2 = new deliveryitem("치킨");
System.out.println(d2);
}
}
예제를 만들 필요도 없었겠지만 이것이 지금 까지 흔히 보던 방식일 것이다. 하지만 제네릭을 사용한다면
class delivery<T>{
public T order;
delivery(T order){
this.order = order;
}
}
public class Main {
public static void main(String[] args) {
delivery<Integer> d1 = new delivery<Integer>(5);
System.out.println(d1);
delivery<String> d2 = new delivery<String>("치킨");
System.out.println(d2);
delivery<Double> d3 = new delivery<Double>(2.5);
System.out.println(d3);
}
}
코드 중복은 줄었지만 같은 결과를 출력하게 된다.
제네릭은 <> 이라는 다아이몬드 연산자를 사용하게 된다. 여기에 내가 원하는 타입 인자를 입력하면 되는데 다음과 같은 규칙을 따른다.
<T> 타입
S, U, V... - 2번째, 3번째, 4번째 타입...
<E> 요소
<K> 키
<N> 숫자
<V> 값
<R> 결과
해서 일반적으로 클래스 혹은 메서드 옆에<T> 를 입력한뒤 인스턴스에 <> 다이아몬드 연산자를 열어서 내가 입력하고 싶은 타입을 넣게 된다. (컬렉션 부터 나머지도 서서히 보게 될꺼임)
그리고 인스턴스에도 연산자 입력을 해줘야 하는데 문제는 그안에 참조 타입만 입력이 가능하다.
우리가 알고 있는 자바의 기본타입은 int, String, double 등이 있겠지만 값을 끌고오는 참조타입은 클래스와 인터페이스등이 있다. 그런데 기본타입을 참조타입으로 변경할수가 있는데 그걸 래퍼클래스 라고 부른다.

이것도 가능하다.
delivery d1 = new delivery(5);
System.out.println(d1);
delivery d2 = new delivery("치킨");
System.out.println(d2);
delivery d3 = new delivery(2.5);
System.out.println(d3);
}
}
우리가 인스턴스에서 다이아몬드 연산자를 생략을 해도 자바에서는 자동으로 인식을 해주는 기능이 있다. 해서 문제 없이 실행이 가능하다.
하나 말고 타입 인자를 두개를 넣을수도 있다.
class delivery<T,S>{
public T order;
public S item;
delivery(T order,S item){
this.order = order;
this.item = item;
}
@Override
public String toString() {
return "delivery{" +
"order=" + order +
", item=" + item +
'}';
}
}
public class Main {
public static void main(String[] args) {
delivery d1 = new delivery(5,5.44);
System.out.println(d1);
delivery d2 = new delivery("치킨","삼겹살");
System.out.println(d2);
delivery d3 = new delivery(2.5,true);
System.out.println(d3);
}
}
결과는 다음과 같다.(item이 5.44 혹은 true라고 그냥 생각하자 사실 변수명은 그냥 만들었다 다양한 인자가 두개 들어간다는 것과 인자의 타입을 자바가 알아서 인식해 준다는것을 기억하자.)
delivery{order=5, item=5.44}
delivery{order=치킨, item=삼겹살}
delivery{order=2.5, item=true}
메서드도 사용할수 있다. (가독성을 위해 일부 짤라서 올리겠다.)
예시가 살짝 이상할 수도 있지만 5천원 남은걸 제네릭를 사용한 메서드로 표현한다고 하면
public <U> void money(U remain){
System.out.println(remain);
}
d1.money("5000원");
이렇게 사용해도 5000원이 정상적으로 출력이 되는걸 확인할수 있다.
2.타입의 안전성을 확보하고 오류를 방지한다.
타입이 맞아야 오류가 안뜨는건 다들 알 것이다.
타입의 안정성이란 정말 중요한테 실행할때는 물론 실제 동작이 불가능 하니 문제가 보이겠지만
우리가 실행전에 빨간 밑줄뜨는 자바의 컴파일 단계에서는 세밀하게 프로그램을 동작해서 오류를 찾는것이 아닌 문법오류 정도를 확인하는거라 여기서 오류를 거르지 못하는 경우가 종종 있다.
그러면 나중에 프로그램 다 짜고 실행 돌렸을때 찾지도 못하고 눈물나는 것이다.(오류 안떠있는 오류인데 코드가 만줄이라 생각해보자 건투를 빈다.)
오류 안뜨는 예제를 한번 보여주자면...대표적으로 형변환 오류가 있다.
class UbereatsFood{
public String Food;
UbereatsFood(String Food){
this.Food = Food;
}
}
class YogiyoFood{
public String Food;
YogiyoFood(String Food){
this.Food = Food;
}
}
class Ubereats_delivery{
public int order;
Ubereats_delivery(int order){
this.order = order;
}
}
class Yogiyo_delivery{
public int order;
Yogiyo_delivery(int order){
this.order = order;
}
}
public class Main {
public static void main(String[] args) {
Ubereats_delivery D1 = new Ubereats_delivery(10);
UbereatsFood U1 = new UbereatsFood(" 피자");
System.out.println(D1.order+U1.Food);
Yogiyo_delivery D2 = new Yogiyo_delivery(5);
YogiyoFood Y1 = new YogiyoFood(" 햄버거");
System.out.println(D2.order+Y1.Food);
}
}
중요한 코드는 아니여서 대충보자.
우버이츠에서 피자를 팔고 주문은 10개이다. 요기요에서는 햄버거를 팔고 주문은 5개이다.
그런데 중복되는 코드를 합치고 싶다.
추가로 delivery 들을 Food 객체에 넣어 버리고 싶다.
자, 우버이츠랑 요기요를 하나로 묶어보자.

음... 전부 에러 뜬다 당연히 Food클래스가 String을 받고 있는데 형변환을 하려면 기본적으로 상속 관계에 있어야 한다. 그래야 연관이 생기지... 그렇다고 일일이 상속하기도 그렇다. 뭐 결과는 같겠지만...
Object를 써주면 된다 이건 이미 모든 클래스가 상속하고 있는 녀석이니깐.

거짓말 처럼 빨간줄이 사라졌다.
실행해보자.

에러뜬다.
형변환을 해줘도 타입이 안맞는다.. Ubereats_delivery로 맞춘거 아니냐고?
Object는 모든 클래스의 기초이자 타입의 기초이다. 즉. F1 F2 객체에 String 값인 불고기 피자와 불고기 버거를 넣어줘도 형변환으로 타입만 맞춰주면 컴파일 단계에서는 특별한 오류를 검출하지 못하는 것이다.
그런데 실행하면 타입 불일치로 런타임 단계에서 충돌 나는 것이다.
이런걸 타입이 안전하지 않다고 말한다.
class Food{
public String Food;
Food(String Food){
this.Food = Food;
}
}
class Ubereats_delivery <T>{
public T order;
Ubereats_delivery(T order){
this.order = order;
}
}
public class Main {
public static void main(String[] args) {
Food F1 = new Food("불고기버거");
Ubereats_delivery<Food> U1 = new Ubereats_delivery<>(F1);
System.out.println(U1);
}
}
물론 비효율적인걸 알지만 불고기버거를 넣을수 있게 된다.
제네릭을 사용하면 어떤 단점이 있을까?
보다 보면 눈치 챘을수도 있겠지만 별의별 타입이 무작위로 들어올수가 있다는 것이다.
뭐 래퍼클래스들은 말할것도 없고 사용자 정의 객체들도 모조리 들어갈수가 있다. 그렇게 되면 프로그램이 꼬이거나 안정성이 떨어질수가 있게 된다.
상속 혹은 인터페이스를 사용하면 간단하게 해결이 가능하다.
class Food{
public String Food;
public int order;
public Food(){}
Food(String Food){
this.Food = Food;
}
}
class Ubereats_delivery <T extends Food>{
public T order;
Ubereats_delivery(T order){
this.order = order;
}
}
class Yogiyo_delivery extends Food{
Yogiyo_delivery(){}
Yogiyo_delivery(int order){
this.order = order;
}
}
class BAEMIN_delivery {
int order;
BAEMIN_delivery(int order){
this.order = order;
}
}
public class Main {
public static void main(String[] args) {
Ubereats_delivery<Food> U2 = new Ubereats_delivery<Food>(new Yogiyo_delivery(50));
/*여긴 에러*/Ubereats_delivery<BAEMIN_delivery> U3 = new Ubereats_delivery<BAEMIN_delivery>(new Yogiyo_delivery(50));
}
}
제네릭은 타입을 집어넣을때 상속이나 인터페이스가 있으면 그것과 관련된 녀석들만 타입을 넣을수가 있다.
따라서 Ubereats_delivery가 Food가 부모라 관련이 있고 Yogiyo_delivery또한 같은 자식이라 들어갈수 있게 된다.
이렇게 제네릭에 들어오는 타입을 막을수가 있다.
(이 게시글은 추후에 수정될 확률이 있다)
'자바 > 정리' 카테고리의 다른 글
(JAVA) ArrayList, LinkedList (0) | 2022.12.22 |
---|---|
(JAVA)Collection Framework (0) | 2022.12.18 |
(JAVA) Object 클래스 toSting(), hashcode() (0) | 2022.12.08 |
(JAVA) Object 클래스 equals() (0) | 2022.12.08 |
(JAVA) 예외 (1) | 2022.11.30 |