가변인수(varargs) : 자바에서 메서드에 임의의 개수의 인수를 전달할 수 있도록 하는 기능. 이를 통해 메서드를 호출할 때 원하는 개수의 인수를 전달할 수 있다. 가변인수는 배열로 처리되며, 메서드 선언 시에 타입 뒤에 세 개의 점(...)을 사용하여 정의한다.

 

제네릭 : 자바 프로그래밍 언어의 한 기능으로, 타입 매개변수를 사용하여 클래스, 인터페이스, 메서드를 정의하는 방법. 이를 통해 컬렉션 클래스나 제네릭 메서드 등을 작성할 때 특정 타입에 의존하지 않고 일반화된 형태로 구현할 수 있다.

 

@SafeVarargs : 자바의 애너테이션 중 하나로, 가변인수(varargs) 매개변수를 사용하는 메서드에 붙인다. 이 애너테이션은 컴파일러에게 해당 메서드가 안전하게 사용될 수 있다는 것을 알려준다.

 

제네릭과 가변인수를 함께 사용할 때 주의점 : 

  1. 가변인수의 타입이 제네릭일 경우에는 @SafeVarargs 애너테이션을 사용해야 한다.
  2. 제네릭 배열을 생성하는 것은 안전하지 않으므로, 컴파일러 경고가 발생할 수 있다.
  3. 가변인수의 타입이 제네릭이거나, 가변인수를 포함하는 제네릭 메서드를 오버로드할 경우 혼동을 줄 수 있다.
import java.util.Arrays;
import java.util.List;

public class GenericVarargsExample {

    // 주의사항 1: 가변인수의 타입이 제네릭일 경우에는 @SafeVarargs 애너테이션을 사용해야 함
    @SafeVarargs
    public static <T> List<T> createList(T... elements) {
        return Arrays.asList(elements);
    }

    // 주의사항 2: 제네릭 배열 생성은 안전하지 않음
    public static <T> T[] createArray(T... elements) {
        // 컴파일러 경고 발생 가능
        return elements;
    }

    // 주의사항 3: 제네릭과 가변인수를 함께 사용한 오버로딩은 혼동을 줄 수 있음
    public static void processElements(String... elements) {
        System.out.println("Processing Strings...");
        for (String element : elements) {
            System.out.println(element);
        }
    }

    public static void processElements(Integer... elements) {
        System.out.println("Processing Integers...");
        for (Integer element : elements) {
            System.out.println(element);
        }
    }

    public static void main(String[] args) {
        // 예시 1: @SafeVarargs 애너테이션 사용한 제네릭 가변인수 메서드 호출
        List<Integer> intList = createList(1, 2, 3, 4, 5);
        System.out.println("Integer List: " + intList);

        // 예시 2: 제네릭 배열 생성
        String[] stringArray = createArray("Hello", "World");
        System.out.println("String Array: " + Arrays.toString(stringArray));

        // 예시 3: 제네릭과 가변인수를 함께 사용한 오버로딩
        processElements("One", "Two", "Three"); // 어떤 메서드가 호출될까?
        processElements(1, 2, 3); // 어떤 메서드가 호출될까?
    }
}
  1. createList 메서드에서는 @SafeVarargs 애너테이션을 사용하여 제네릭 가변인수를 안전하게 처리한다.
  2. createArray 메서드에서는 제네릭 배열 생성으로 인한 컴파일러 경고를 확인할 수 있다.
  3. processElements 메서드에서는 제네릭과 가변인수를 함께 사용한 오버로딩이 혼동을 줄 수 있음을 보여준다. 호출할 때 어떤 메서드가 호출될지 주의해야 한다.

 

결론 : 가변인수와 제네릭은 궁합이 좋지 않다. 가변인수 기능은 배열을 노출하여 추상화가 완벽하지 못하고, 배열과 제네릭의 타입 규칙이 서로 다르기 때문이다. 제네릭 varargs 매개변수는 타입 안전하지는 않지만, 허용된다. 메서드에 제네릭 (혹은 매개변수화된)varargs 매개변수를 사용하고자 한다면, 먼저 그 메서드가 타입 안전한지 확인한 다음 @SafeVarargs 애너테이션을 달아 사용하는 데 불편함이 없게끔 하자.

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

한정적 와일드카드는 이러한 와일드카드를 특정한 타입의 하위 타입으로 제한하는 방법 중 하나이다. 이를 통해 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); // 형변환 필요 없음

 

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

  1. 공변성 (Covariance):
    • 공변성은 서브타입 관계에서 타입이 변할 때 같은 방향으로 변하는 특성을 말합니다. 즉, A가 B의 서브타입인 경우, A 타입의 객체를 B 타입으로 변환할 수 있습니다.
    • 예를 들어, List<String>이 List<Object>의 서브타입이라면, List<String> 타입의 객체를 List<Object> 타입으로 변환할 수 있습니다. 이것은 배열의 공변성과 유사합니다.
  2. 불변성 (Invariance):
    • 불변성은 서브타입 관계에서 타입이 변하지 않는 특성을 말합니다. 즉, A가 B의 서브타입이더라도 A 타입의 객체를 B 타입으로 변환할 수 없습니다.
    • 예를 들어, List<String>이 List<Object>의 서브타입이 아니라면, List<String> 타입의 객체를 List<Object> 타입으로 변환할 수 없습니다.

 

 

 

결론 : 배열과 제네릭에는 매우 다른 타입 규칙이 적용된다. 배열은 공변이고 실체화되는 반면, 제네릭은 불공변이고 타입 정보가 소거된다. 그 결과 배열은 런타임에는 타입 안전하지만 컴파일타임에는 그렇지 않다. 제네릭은 반대다. 그래서 둘을 섞어 쓰기란 쉽지 않다. 둘을 섞어 쓰다가 컴파일 오류나 경고를 만나면, 가장 먼저 배열을 리스트로 대체하는 방법을 적용해보자.

비검사 경고(Unchecked Warning) : 컴파일러가 코드에서 타입 안전성을 검사하지 못하는 상황을 감지하여 발생하는 경고 메시지이다. 이 경고는 제네릭을 사용하는 코드에서 발생할 수 있는데, 일반적으로 제네릭을 사용하면서 발생하는 타입 캐스팅이나 원시 타입 사용 등의 상황에서 발생한다. 할 수 있는 한 모든 비검사 경고를 제거해야 한다.

 

비검사 경고를 제거할 수는 없지만 타입 안전하다고 확실할 수 있다면 @SuppressWarning("unchecked") 애너테이션을 달아 경고를 숨기자. 단 타입 안전함을 반드시 검증해야만 한다. 이 에너테이션을 사용했다면 그 경고를 무시해도 안전한 이유를 항상 주석으로 남겨야만 한다. 이 애너테이션은 보통은 변수 선언, 아주 짧은 메서드, 혹은 생성자 등 가능한 한 좁은 범위에다가만 적용하자. 자칫 심각한 경고를 놓칠 수 있으니 절대로 클래스 전체에 적용해서는 안 된다.

 

결론 : 비검사 경고는 중요하니 무시하지 말자. 모든 비검사 경고는 런타임에 ClassCastException을 일으킬 수 있는 잠재적 가능성을 뜻하니 최선을 다해 제거하라. 경고를 없앨 방법을 찾이 못하겠다면, 그 코드가 타입 안전함을 증명하고 가능한 한 범위를 좁혀 @SuppressWarnings("unchecked") 애너테이션으로 경고를 숨겨라. 그런 다음 경고를 숨기기로 한 근거를 주석으로 남겨라.

 

 

+ Recent posts