와일드카드 : 제네릭 타입에서 타입 매개변수의 값을 미리 지정하지 않고 사용할 수 있는 기능.

한정적 와일드카드는 이러한 와일드카드를 특정한 타입의 하위 타입으로 제한하는 방법 중 하나이다. 이를 통해 API의 유연성을 높일 수 있다.

 

import java.util.List;

class Animal {
    // 동물 클래스
}

class Dog extends Animal {
    // 개 클래스
}

class Cat extends Animal {
    // 고양이 클래스
}

public class AnimalProcessor {
    // 모든 동물의 리스트를 처리하는 메서드
    public static void processAnimals(List<? extends Animal> animals) {
        for (Animal animal : animals) {
            // 동물 처리 로직
            System.out.println(animal.getClass().getSimpleName() + " is being processed.");
        }
    }

    public static void main(String[] args) {
        List<Dog> dogs = List.of(new Dog(), new Dog());
        List<Cat> cats = List.of(new Cat(), new Cat());

        // processAnimals 메서드는 List<Dog>와 List<Cat>을 모두 처리할 수 있음
        processAnimals(dogs); // 개 리스트 처리
        processAnimals(cats); // 고양이 리스트 처리
    }
}

 

한정적 와일드카드는 메서드 시그니처에서 사용되고 있습니다. 메서드 processAnimals의 파라미터 타입인

List<? extends Animal>에서 한정적 와일드카드가 사용되었다. 이것은 Animal 클래스를 상속하는 모든 클래스(Animal 클래스를 포함하여)의 리스트를 받을 수 있음을 의미합니다. 따라서 List<Dog>List<Cat> 모두 이 메서드의 인자로 사용될 수 있습니다.

 

 

PECS(Producer Extends, Consumer Super) : 제네릭 타입을 다룰 때 사용되는 원칙 중 하나.

  • Producer Extends: 생산자(Producer) 역할을 하는 메서드에서는 와일드카드의 extends를 사용한다. 이는 해당 제네릭 타입을 "읽기 전용"으로 사용한다는 것을 의미한다. 즉, 데이터를 가져오는 역할을 한다.
  • Consumer Super: 소비자(Consumer) 역할을 하는 메서드에서는 와일드카드의 super를 사용한다. 이는 해당 제네릭 타입을 "쓰기 전용"으로 사용한다는 것을 의미한다. 즉, 데이터를 추가하거나 변경하는 역할을 한다.
import java.util.ArrayList;
import java.util.List;

class Animal {
    public void makeSound() {
        System.out.println("Animal is making a sound.");
    }
}

class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Dog is barking.");
    }
}

class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Cat is meowing.");
    }
}

public class Main {
    public static void main(String[] args) {
        List<Dog> dogs = new ArrayList<>();
        dogs.add(new Dog());
        dogs.add(new Dog());

        List<Cat> cats = new ArrayList<>();
        cats.add(new Cat());
        cats.add(new Cat());

        // Producer - extends
        // List<Dog>를 처리하는 메서드에는 List<? extends Animal>을 사용
        processAnimals(dogs);

        // Consumer - super
        // List<Animal>를 받아서 처리하는 메서드에는 List<? super Dog>를 사용
        addDogToList(cats);
        processAnimals(cats);
    }

    /**
     * 동물 리스트를 처리하는 메서드.
     * 
     * @param animals 처리할 동물 리스트
     */
    public static void processAnimals(List<? extends Animal> animals) {
        for (Animal animal : animals) {
            animal.makeSound();
        }
    }

    /**
     * 개를 리스트에 추가하는 메서드.
     * 
     * @param animals 개를 추가할 리스트
     */
    public static void addDogToList(List<? super Dog> animals) {
        animals.add(new Dog());
    }
}

 

이 코드에서 processAnimals 메서드는 List<? extends Animal>을 인자로 받아서 동물 리스트를 처리한다. 이 경우 List<Dog>와 List<Cat> 모두 processAnimals 메서드의 인자로 사용될 수 있도록 한정적 와일드카드를 사용한다.

addDogToList 메서드는 List<? super Dog>를 인자로 받아서 개 객체를 리스트에 추가합니다. 이 경우 List<Animal>을 포함하여 Animal 클래스의 상위 클래스를 인자로 받을 수 있도록 한정적 와일드카드를 사용한다.

 

결론 : 조금 복잡하더라도 와일드카드 타입을 적용하면 API가 훨씬 유연해진다. 그러니 널리 쓰일 라이브러리를 작성한다면 반드시 와일드카드 타입을 적절히 사용해줘야 한다. PECS공식을 기억하자. 즉, 생산자(producer)는 extends를 소비자(consumer)는 super를 사용한다.

제네릭 메서드 :  메서드 선언 시에도 타입 매개변수를 사용하여 메서드의 매개변수 타입 또는 반환 타입을 일반화하는 방법. 제네릭 메서드를 사용하면 메서드를 호출할 때마다 타입을 명시할 필요가 없으며, 여러 종류의 타입에 대해 일반적으로 사용할 수 있는 메서드를 정의할 수 있다.

 

public <T> 반환타입 메서드이름(매개변수) {
    // 메서드 내용
}

 

여기서 <T>는 타입 매개변수를 나타내며, 반환타입이나 매개변수에서 이를 사용할 수 있다.

예를 들어, 다음은 제네릭 메서드를 사용하여 배열의 요소를 출력하는 메서드의 예시이다:

public class ArrayPrinter {
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.print(element + " ");
        }
        System.out.println();
    }
}

// 사용 예시
Integer[] intArray = {1, 2, 3, 4, 5};
String[] strArray = {"Hello", "World"};
ArrayPrinter.printArray(intArray); // 정수 배열 출력
ArrayPrinter.printArray(strArray); // 문자열 배열 출력

 

클래스와 마찬가지로, 메서드도 제네릭으로 만들 수 있다.

 

결론 : 제네릭 타입과 마찬가지로, 클라이언트에서 입력 매개변수와 반환값을 명시적으로 형변환해야 하는 메서드보다 제네릭 메서드가 더 안전하며 사용하기도 쉽다. 타입과 마찬가지로, 메서드도 형변환 없이 사용할 수 있는 편이 좋으며, 많은 경우 그렇게 하려면 제네릭 메서드가 되어야 한다. 역시 타입과 마찬가지로, 형변환을 해줘야 하는 기존 메서드는 제네릭하게 만들자. 기존 클라이언트는 그대로 둔 채 새로운 사용자의 삶을 훨씬 편하게 만들어줄 것이다.

https://www.acmicpc.net/problem/1189

 

1189번: 컴백홈

첫 줄에 정수 R(1 ≤ R ≤ 5), C(1 ≤ C ≤ 5), K(1 ≤ K ≤ R×C)가 공백으로 구분되어 주어진다. 두 번째부터 R+1번째 줄까지는 R×C 맵의 정보를 나타내는 '.'과 'T'로 구성된 길이가 C인 문자열이 주어진다

www.acmicpc.net

 

 

[정답 코드]

import java.io.*;
import java.util.*;

public class Main {
    static int R, C, K, answer = 0;
    static char[][] graph;
    static boolean[][] visited;
    static int[] dx = {-1, 1, 0, 0};
    static int[] dy = {0, 0, -1, 1};
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        StringTokenizer st = new StringTokenizer(br.readLine(), " ");

        R = Integer.parseInt(st.nextToken());
        C = Integer.parseInt(st.nextToken());
        K = Integer.parseInt(st.nextToken());
        graph = new char[R][C];
        visited = new boolean[R][C];
        visited[R-1][0] = true;

        for (int i = 0; i < R; i++) {
            String input = br.readLine();
            for (int j = 0; j < C; j++) {
                graph[i][j] = input.charAt(j);
            }
        }

        dfs(R-1, 0, 1); // 시작지점은 왼쪽 아래
        System.out.println(answer);

    }

    static void dfs(int x, int y, int k) {
        if (x == 0 && y == C - 1) { // 오른쪽 위에 도착했을 경우
            if (k == K) { // 이동한 거리가 K일 경우
                answer++; // 1씩 증가
            }
            return;
        }

        for (int i = 0; i < 4; i++) {
            int nx = x + dx[i];
            int ny = y + dy[i];
            if (nx >= 0 && nx < R && ny >= 0 && ny < C) { // graph 안에 있는경우
                if (!visited[nx][ny] && graph[nx][ny] != 'T') { // 방문하지 않은곳인 면서 T가 아닌곳
                    visited[nx][ny] = true;
                    dfs(nx, ny, k + 1);
                    visited[nx][ny] = false;
                }
            }
        }
    }
}

 

[설명]

4방탐색을 하며 거리가 K가 될때까지 반복한다. 갔던 자리는 true로 체크하고 탐색이 1회 끝났다면 다시 false로 변경해준다. 

https://www.acmicpc.net/problem/15686

 

15686번: 치킨 배달

크기가 N×N인 도시가 있다. 도시는 1×1크기의 칸으로 나누어져 있다. 도시의 각 칸은 빈 칸, 치킨집, 집 중 하나이다. 도시의 칸은 (r, c)와 같은 형태로 나타내고, r행 c열 또는 위에서부터 r번째 칸

www.acmicpc.net

 

 

[정답 코드]

import java.io.*;
import java.util.*;

public class Main {
    static int N, M;
    static int[][] graph;
    static ArrayList<int[]> house = new ArrayList<>();
    static ArrayList<int[]> chicken = new ArrayList<>();
    static ArrayList<int[]> selected = new ArrayList<>();
    static int result = Integer.MAX_VALUE;
    static boolean[] visited;
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        StringTokenizer st = new StringTokenizer(br.readLine(), " ");

        N = Integer.parseInt(st.nextToken());
        M = Integer.parseInt(st.nextToken());
        graph = new int[N][N];

        for (int i = 0; i < N; i++) {
            st = new StringTokenizer(br.readLine(), " ");
            for (int j = 0; j < N; j++) {
                graph[i][j] = Integer.parseInt(st.nextToken());
                if (graph[i][j] == 1) {
                    house.add(new int[]{i, j}); // 집의 좌표 저장
                } else if (graph[i][j] == 2) {
                    chicken.add(new int[]{i, j}); // 치킨집의 좌표 저장
                }
            }
        }
        visited = new boolean[chicken.size()];

        back(0, 0);
        System.out.println(result); // 출력
    }

    static void back(int depth, int start) {
        if (depth == M) { // M개를 뽑아서 selected 리스트에 M개 저장이 끝났다면
            int sum = 0;
            for (int[] h : house) { // 모든 집들과 치킨집과의 최소거리를 계산
                int min = Integer.MAX_VALUE;
                for (int[] s : selected) { // 선택한 M개의 치킨집과 집의 거리를 계산해 최소거리를 구함
                    int d = Math.abs(h[0] - s[0]) + Math.abs(h[1] - s[1]);
                    min = Math.min(d, min);
                }
                sum += min; // 그렇게 구한 최소거리를 sum에 저장
            }
            result = Math.min(result, sum); // 그렇게 구한 sum들중에 최소값만 저장
            return;
        }

        for (int i = start; i < chicken.size(); i++) { // 모든 치킨집들을 탐색함
            if (!visited[i]) {
                visited[i] = true;
                selected.add(chicken.get(i));
                back(depth + 1, i + 1);
                selected.remove(selected.size() - 1);
                // 탐색이 끝났다면 리스트를 비우기 위한 로직
                // 배열로 했다면 덮어씌울수 있지만 리스트라 제거해줘야함
                visited[i] = false;
            }
        }
    }
}

 

[설명]

백트래킹 알고리즘 : 

1. for문을 돌며 치킨집들중 M개의 치킨집을 골라 selected 리스트에 담는다.

2. selected 리스트에 M개의 치킨집이 저장 되었다면 집과 선택한 치킨집들과의 최소거리를 구한다.

3. 그렇게 구한 각집과 선택한치킨집과의 최소거리를 모두 더했다면

4. 다시 치킨집들 중 아까와 다른 M개의 치킨집을 골라 반복한다.

제네릭 타입 : 제네릭 타입은 자바에서 컬렉션과 같은 클래스나 인터페이스를 정의할 때, 타입 파라미터를 사용하여 타입 안정성을 확보하는 방법이다. 즉, 클래스나 인터페이스를 선언할 때 실제 타입이 아닌 타입 매개변수를 사용하여 일반화된 형태로 선언한다.

 

// 제네릭을 사용하지 않은 ArrayList
List list = new ArrayList();
list.add("Hello");
String item = (String) list.get(0); // 형변환 필요

// 제네릭을 사용한 ArrayList
List<String> list = new ArrayList<>();
list.add("Hello");
String item = list.get(0); // 형변환 필요 없음

 

결론 : 클라이언트에서 직접 형변환해야 하는 타입보다 제네릭 타입이 더 안전하고 쓰기 편하다. 그러니 새로운 타입을 설계할 때는 형변환 없이도 사용할 수 있도록 하라. 그렇게 하려면 제네릭 타입으로 만들어야 할 경우가 많다. 기존 타입 중 제네릭이었어야 하는 게 있다면 제네릭 타입으로 변경하자. 기존 클라이언트에는 아무 영향을 주지 않으면서, 새로운 사용자를 훨씬 편하게 해주는 길이다.

+ Recent posts