로 타입(raw type) : 제네릭 클래스나 인터페이스에서 타입 매개변수를 명시하지 않고 raw 타입으로 사용하는 것을 의미

 

제네릭 : 자바에서 컬렉션 클래스 및 메서드, 인터페이스 등을 작성할 때 타입을 파라미터화하는 기능

 

컬렉션 클래스 : 자바에서 데이터를 모으고 관리하는 데 사용되는 클래스들의 집합

 

//잘못된 예
List list = new ArrayList(); // 로 타입 사용

list.add("hello");
list.add(123);

String str = (String) list.get(0); // 형변환 필요
int num = (int) list.get(1); // 형변환 필요

//옳은 예
List<String> list = new ArrayList<>(); // 제네릭 사용

list.add("hello");
// list.add(123); // 컴파일 오류: 타입 불일치

String str = list.get(0); // 형변환 불필요

 

결론 : 로 타입을 사용하면 런타임에 예외가 일어날 수 있으니 사용하면 안된다.

톱 레벨 클래스 : 다른 클래스의 내부에 정의되지 않고, 자체 파일에 독립적으로 존재하는 클래스. 즉, 톱 레벨 클래스는 패키지 내에서 최상위에 위치하는 클래스이며, 다른 클래스의 멤버가 아닌 독립된 클래스.

 

아래 예시는 한 파일에 두 개의 클래스가 정의된것이다. 절대 따라하면 안되는 코드이다.

class Utensil {
	static final String NAME = "pan";
}

class Dessert {
	stattic final String NAME = "cake";
}

 

위와 같은 코드로 작성할 경우 오류가 발생 할 수 있다. 이를 해결하려면 단순히 톱 레벨 클래스들을 서로 다른 소스 파일로 분리해주면 된다. 굳이 여러 톱 레벨 클래스를 한 파일에 담고 싶다면 정적 멤버 클래스로 만들 수 있다.

public class Test {
	public static void main(String[] args) {
    	System.out.println(Utensil.NAME + Dessert.NAME);
    }
    
    private static class Utensil {
    	static final String NAME = "pan";
    }
    
    private static class Dessert {
    	static final String NAME = "cake";
    }
}

 

 

결론 : 소스 파일 하나에는 반드시 톱레벨 클래스(혹은 톱레벨 인터페이스)를 하나만 담자. 이 규칙만 따른다면 컴파일러가 한 클래스에 대한 정의를 여러 개 만들어내는 일은 사라진다. 소스 파일을 어떤 순서로 컴파일하든 바이너리 파일이나 프로그램의 동작이 달라지는 일은 결코 일어나지 않을 것이다.

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

 

14225번: 부분수열의 합

수열 S가 주어졌을 때, 수열 S의 부분 수열의 합으로 나올 수 없는 가장 작은 자연수를 구하는 프로그램을 작성하시오. 예를 들어, S = [5, 1, 2]인 경우에 1, 2, 3(=1+2), 5, 6(=1+5), 7(=2+5), 8(=1+2+5)을 만들

www.acmicpc.net

 

 

[정답 코드]

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

public class Main {
    static int[] arr, answer;
    static int N, max = 0;
    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());

        arr = new int[N];
        visited = new boolean[N];

        st = new StringTokenizer(br.readLine(), " ");
        for (int i = 0; i < N; i++) {
            arr[i] = Integer.parseInt(st.nextToken());
            max += arr[i];
        }
        answer = new int[max + 2]; // 최대 가능한 합 + 2 로 설정해야 메모리 낭비를 막을 수 있다

        dfs(0, 0);

        for (int i = 1; i < answer.length; i++) {
            if (answer[i] == 0) { // 0일 경우 해당 인덱스는 나온적 없는 숫자이므로 출력
                System.out.println(i);
                break;
            }
        }

    }

    static void dfs(int depth, int sum) {
        if (depth == N) { // depth 가 N일 경우 종료
            answer[sum] = 1; // 현재까지 저장된 sum 값은 이미 나온 숫자이므로 1로 변경
            return;
        }
        dfs(depth + 1, sum + arr[depth]); // 현재까지 저장된 sum + 현재 인덱스의 arr 값
        dfs(depth + 1, sum); // 현재 인덱스의 arr 값을 추가하지않고 다음 인덱스 탐색
    }
}

 

[설명]

배열의 원소를 입력받을 때마다 값을 max에 추가해 나올 수 있는 가장 큰 자연수를 계산한다. 나올 수 있는 자연수들이 담긴 배열 answer = new int[max+ 2] 해준다. 다른 코드에서는 문제에서 주어진 나올 수 있는 가장 큰 값으로 초기화 해주었지만 max가 적을수록 메모리낭비가 생기기 때문에 이처럼 해준다.

 

dfs로직 : 매개변수로 depth, sum을 받는다. depth는 현재의 인덱스 위치, sum은 현재까지 저장된 값이다. depth 가 N이 될 경우 초기에는 answer배열의 모든값이 0이기 때문에 answer[sum]을 임의의 값 1로 변경해준후 return 한다.

        dfs(depth + 1, sum + arr[depth]); // 현재까지 저장된 sum + 현재 인덱스의 arr 값
        dfs(depth + 1, sum); // 현재 인덱스의 arr 값을 추가하지않고 다음 인덱스 탐색

이 부분은 배열의 완전탐색을 하기위한 알고리즘이다. 인덱스를 증가시키면 배열을 탐색하다 현재 인덱스의 arr[depth] 더하고 다음 인덱스를 탐색할 경우에는 첫 번째를, 아니라면 두 번째를 실행해야한다. 모든 배열을 탐색해야하기 때문에 두 가지 모두 수행한다.

'코딩테스트' 카테고리의 다른 글

백준 1189번: 컴백홈[JAVA]  (0) 2024.03.28
백준 15686번: 치킨 배달[JAVA]  (0) 2024.03.28
백준 10819번: 차이를 최대로[JAVA]  (0) 2024.03.21
백준 1260번: DFS와 BFS[JAVA]  (0) 2024.03.20
백준 1068번: 트리[JAVA]  (1) 2024.03.15

중첩 클래스(nested class) : 다른 클래스 내부에 선언된 클래스.

 

중첩 클래스의 종류 4가지 : 

정적 멤버 클래스

정적 멤버 클래스는 바깥 클래스의 인스턴스와 독립적으로 존재할 수 있는 클래스다. 즉, 바깥 클래스의 인스턴스 없이도 생성하고 사용할 수 있다. 정적 멤버 클래스는 바깥 클래스의 정적 멤버에만 접근할 수 있으며, 바깥 클래스의 인스턴스 멤버에는 접근할 수 없다.

 

비정적 멤버 클래스

비정적 멤버 클래스는 바깥 클래스의 인스턴스와 연관되어 있는 클래스다. 이러한 클래스의 객체는 바깥 클래스의 객체와 연결되어 있으며, 바깥 클래스의 인스턴스 멤버와 메서드에 접근할 수 있다. 비정적 내부 클래스는 바깥 클래스의 인스턴스를 통해서만 생성할 수 있다. 정적 클래스와 의 구문적 차이는 단지 static이 붙어 있고 없고의 차이뿐이지만, 의미상 차이는 꽤 크다.

 

지역 클래스 (Local Class)

메서드 내부에 선언된 클래스로, 선언된 메서드 내에서만 사용할 수 있다.

 

익명 클래스 (Anonymous Class)

이름이 없는 클래스로, 주로 단일 인스턴스 생성에 사용된다.

 

public class OuterClass {
    private static int staticVar = 100;
    private int instanceVar = 200;

    // 정적 멤버 클래스
    static class StaticMemberClass {
        void display() {
            System.out.println(staticVar); // 정적 변수에 접근 가능
            // System.out.println(instanceVar); // 컴파일 에러: 인스턴스 변수에 접근 불가
        }
    }

    // 비정적 멤버 클래스
    class NonStaticMemberClass {
        void display() {
            System.out.println(staticVar); // 정적 변수에 접근 가능
            System.out.println(instanceVar); // 인스턴스 변수에 접근 가능
        }
    }

    // 메서드 내에서 지역 클래스 사용
    void methodWithLocalClass() {
        // 지역 클래스 정의
        class LocalClass {
            void display() {
                System.out.println("Inside Local Class. staticVar: " + staticVar + ", instanceVar: " + instanceVar);
            }
        }

        // 지역 클래스 인스턴스 생성 및 사용
        LocalClass localInstance = new LocalClass();
        localInstance.display();
    }

    // 메서드 내에서 익명 클래스 사용
    void methodWithAnonymousClass() {
        // 익명 클래스를 통한 Runnable 인터페이스 구현
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("Inside Anonymous Class. staticVar: " + staticVar);
                // instanceVar 접근 가능 (익명 클래스는 비정적 컨텍스트 내에 정의되었기 때문)
                System.out.println("instanceVar: " + instanceVar);
            }
        };
        
        // 익명 클래스 인스턴스 사용
        new Thread(runnable).start();
    }

    public static void main(String[] args) {
        OuterClass outerInstance = new OuterClass();

        // 정적 멤버 클래스 인스턴스 생성 및 사용
        OuterClass.StaticMemberClass staticInstance = new OuterClass.StaticMemberClass();
        staticInstance.display();

        // 비정적 멤버 클래스 인스턴스 생성 및 사용
        OuterClass.NonStaticMemberClass nonStaticInstance = outerInstance.new NonStaticMemberClass();
        nonStaticInstance.display();

        // 지역 클래스 사용
        outerInstance.methodWithLocalClass();

        // 익명 클래스 사용
        outerInstance.methodWithAnonymousClass();
    }
}

 

결론 : 중첩 클래스에는 네 가지가 있으며, 각각의 쓰임이 다르다.

메서드 밖에서도 사용해야하거나 메서드 안에 정의하기엔 너무 길다면 멤버 클래스로 만든다.

멤버 클래스의 인스턴스 각각이 바깥 인스턴스를 참조한다면 비정적으로, 그렇지 않으면 정적으로 만들자.

중첩 클래스가 한 메서드 안에서만 쓰이면서 그 지점이 단 한곳이고 해당 타입으로 쓰기에 적합한 클래스나 인터페이스가 이미 있다면 익명 클래스로 만들고, 그렇지 않으면 지역 클래스로 만들자.

태그 달린 클래스(tagged class):  클래스 내부에 특정 "태그" 필드를 두어 객체의 유형을 나타내는 방식. 이러한 클래스는 보통 여러 유형의 객체를 하나의 클래스로 표현하고자 할 때 사용되며, 태그 값에 따라 다른 동작을 수행하도록 메서드들이 조건문(예: if-else, switch)을 사용해 구현된다.

// 태그 달린 클래스
public class Shape {
    enum ShapeType { CIRCLE, RECTANGLE }
    final ShapeType shapeType;

    double radius; // 원일 경우 사용
    double width; // 직사각형일 경우 사용
    double height; // 직사각형일 경우 사용

    // 원을 위한 생성자
    Shape(double radius) {
        shapeType = ShapeType.CIRCLE;
        this.radius = radius;
    }

    // 직사각형을 위한 생성자
    Shape(double width, double height) {
        shapeType = ShapeType.RECTANGLE;
        this.width = width;
        this.height = height;
    }

    double area() {
        switch (shapeType) {
            case CIRCLE:
                return Math.PI * radius * radius;
            case RECTANGLE:
                return width * height;
            default:
                throw new AssertionError(shapeType);
        }
    }
}

 

태그 달린 클래스는 단점이 한가득이다. 열거 타입 선언, 태그 필드, switch 문 등 쓸데없는 코드가 많다. 여러 구현이 한 클래스에 혼합돼 있어서 가독성도 나쁘다. 태그 달린 클래스는 장황하고, 오류를 내기 쉽고, 비효율적이다. 태그 달린 클래스는 클래스 계층구조를 어설프게 흉내낸 아류이다.

 

클래스 계층구조(Class Hierarchy) : 객체 지향 프로그래밍에서 클래스들 사이의 상속 관계를 조직화하는 방법. 이 구조는 계층적이며, 상위 클래스(superclass)로부터 하위 클래스(subclass)가 파생되는 방식으로 구성된다. 클래스 계층구조는 실세계의 개념을 모델링하는 데 있어 상속과 다형성을 활용하여 코드의 재사용성, 확장성 및 유지보수성을 향상시키는 핵심적인 요소이다.

 

클래스 계층구조의 구성 요소 :

  • 상위 클래스 (Superclass) / 부모 클래스 (Parent Class): 다른 클래스에 공통된 속성과 메서드를 제공하는 클래스.  하위 클래스는 이 상위 클래스의 속성과 메서드를 상속받는다.
  • 하위 클래스 (Subclass) / 자식 클래스 (Child Class): 상위 클래스의 속성과 메서드를 상속받아, 추가적인 속성과 메서드를 가지며 더 구체적인 개념을 모델링하는 클래스.
  • 추상 클래스 (Abstract Class): 인스턴스화할 수 없으며, 하나 이상의 추상 메서드(구현되지 않은 메서드)를 포함할 수 있는 클래스. 하위 클래스는 추상 클래스의 모든 추상 메서드를 구현해야 한다.
  • 인터페이스 (Interface): 모든 메서드가 추상 메서드인 특별한 유형의 클래스로, 클래스가 특정 행동을 할 수 있음을 선언하는 데 사용된다.

 

// 클래스 계층구조
// 추상 클래스 Shape는 도형의 공통적인 특성을 정의하는 상위 클래스입니다. 
// 모든 도형은 면적을 가지므로, 면적을 계산하는 추상 메서드 area()를 선언합니다.
// 이는 클래스 계층구조에서 공통 인터페이스 역할을 합니다.
abstract class Shape {
    // 추상 메서드 area()는 하위 클래스에서 구체적인 면적 계산 로직을 구현해야 합니다.
    // 이 메서드는 다형성을 가능하게 하는 중요한 부분입니다.
    abstract double area();
}

// Circle 클래스는 Shape 클래스를 상속받아 원의 구체적인 특성을 모델링하는 하위 클래스입니다.
// Circle은 Shape의 구체적인 구현체로, 원의 면적을 계산하는 로직을 포함합니다.
class Circle extends Shape {
    final double radius; 

    Circle(double radius) {
        this.radius = radius;
    }

    @Override
    double area() {
        // 이 메서드는 Shape 클래스에서 선언된 추상 메서드 area()의 구체적인 구현입니다.
        return Math.PI * radius * radius;
    }
}

// Rectangle 클래스는 Shape 클래스를 상속받아 직사각형의 구체적인 특성을 모델링하는 하위 클래스입니다.
// Rectangle 역시 Shape의 구체적인 구현체로, 직사각형의 면적을 계산하는 로직을 포함합니다.
class Rectangle extends Shape {
    final double width;
    final double height;

    Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    @Override
    double area() {
        // 이 메서드는 Shape 클래스에서 선언된 추상 메서드 area()의 구체적인 구현입니다.
        return width * height;
    }
}

 

결론 : 태그 달린 클래스를 써야 하는 상황은 거의 없다. 새로운 클래스르 작성하는 데 태그 필드가 등장한다면 태그를 없애고 계층구조로 대치하는 방법을 생각해보자. 기존 클래스가 태그 필드를 사용하고 있다면 계층구조로 리팩토링하는 걸 고민해보자.

인터페이스 : 자신을 구현한 클래스의 인스턴스를 참조할 수 있는 타입 역할.

즉, 클래스가 어떤 인터페이스를 구현한다는 것은 자신의 인스턴스로 무엇을 할 수 있는지를 클라이언트에 얘기해주는 것이다.

 

인터페이스는 위의 지침에 맞지 않는 예로 상수 인터페이스라는 것이 있다. 상수 인터페이스란 메서드 없이, 상수를 뜻하는 static final 필드로만 가득 찬 인터페이스를 말한다.

 

상수 인터페이스 안티패턴의 예시

// 상수 인터페이스 예시
public interface CarConstants {
    int MAX_SPEED = 240;
    String ERROR_MESSAGE = "Operation not allowed";
}

// 위의 상수 인터페이스는 이런식으로 사용될 수 있다.
public class SportsCar implements CarConstants {
    public void drive() {
        System.out.println("Driving at speed: " + MAX_SPEED);
    }
}

 

이러한 패턴은 상수 인터페이스 안티패턴이다 - 절대 사용하면 안된다.

 

안티패턴을 사용하지 않고 상수를 공유하기 위한 더 좋은 방법은 열거형(enum)을 사용하거나, 클래스 내부에 상수를 정의하는 것이다.

public final class CarUtils {
    private CarUtils() {} // 인스턴스화 방지

    public static final int MAX_SPEED = 240;
    public static final String ERROR_MESSAGE = "Operation not allowed";
}

 

결론 : 인터페이스는 타입을 정의하는 용도로만 사용해야 한다. 상수 공개용 수단으로 사용하지 말자.

 

디폴트 메서드 : 인터페이스 내에서 구현 코드를 가진 메서드

추상 메서드 : 선언만 있고 구현이 없는 메서드

예시 : 

interface MyInterface {
    // 추상 메서드
    void abstractMethod();

    // 디폴트 메서드
    default void defaultMethod() {
        System.out.println("This is a default method.");
    }
}

// MyInterface를 구현하는 클래스
class MyClass implements MyInterface {
    @Override
    public void abstractMethod() {
        System.out.println("Abstract method implementation.");
    }
}

public class Main {
    public static void main(String[] args) {
        MyClass myClass = new MyClass();
        myClass.abstractMethod(); // "Abstract method implementation." 출력
        myClass.defaultMethod(); // "This is a default method." 출력
    }
}

 

과거에는 모든 인터페이스의 메서드가 추상 메서드 이어야 했지만 이제는 디폴트 메서드의 도입으로 인해 인터페이스에 메서드의 기본 구현을 제공할 수 있게 되었다. 기존 인터페이스에 메서드를 추가하는 길이 열렸지만 모든 기존 구현체들과 매끄럽게 연동되리라는 보장은 없다. 디폴트 메서드는 범용적이라 대부분 상황에서 잘 작동한다.

하지만 생각할 수 있는 모든 상황에서 불변식을 해치지 않는 디폴트 메서드를 작성하기란 어렵다.

즉 디폴트 메서드라는 도구가 생겼더라도 인터페이스를 설계할 때는 세심한 주의를 기울여야 한다. 디폴트 메서드는 꼭 필요한 경우가 아니면 기존 인터페이스에 디폴트 메서드로 새 메서드를 추가하는일을 피해야 한다. 추가할경우 디폴트 메서드가 기준 구현체들과 충돌하는지도 체크해야 한다. 디폴트 메서드는 인터페이스로부터 메서드를 제거하거나 기존 메서드의 시그니처를 수정하는 용도가 아니다. 새로운 인터페이스를 만들 경우 표준적인 메서드 구현을 제공할때는 아주 유용한 수단이다.

새 인터페이스라면 릴리스 전 반드시 테스트를 거쳐야 한다. 최소 세가지는 구현해봐야 한다. 각 인터페이스의 인스턴스를 다양한 작업에 활용하는 클라이언트도 여러 개 만들어봐야 한다. 릴리스하기 전, 즉 바로잡을 기회가 아직 남아있을 때 결함을 찾아내야 한다. 인터페이스를 릴리스한 후라도 결함을 수정하는게 가능한 경우도 있지만 어려운 방법이니 피하자.

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

 

10819번: 차이를 최대로

첫째 줄에 N (3 ≤ N ≤ 8)이 주어진다. 둘째 줄에는 배열 A에 들어있는 정수가 주어진다. 배열에 들어있는 정수는 -100보다 크거나 같고, 100보다 작거나 같다.

www.acmicpc.net

 

 

[정답 코드]

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

public class Main {
    static int result = Integer.MIN_VALUE;
    static int N;
    static int[] arr, selected;
    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());
        arr = new int[N];
        visited = new boolean[N];
        selected = new int[N];

        st = new StringTokenizer(br.readLine(), " ");
        for (int i = 0; i < N; i++) {
            arr[i] = Integer.parseInt(st.nextToken());
        }

        dfs(0);

        System.out.println(result);
    }

    public static void dfs(int count) {
        if (count == N) {
            result = Math.max(result, getResult());
            return;
        }

        for (int i = 0; i < N; i++) { //
            if (!visited[i]) {
                visited[i] = true;
                selected[count] = arr[i];
                dfs(count + 1);
                visited[i] = false;
            }
        }
    }

    public static int getResult() {
        int sum = 0;
        for (int i = 0; i < N - 1; i++) {
            sum += Math.abs(selected[i] - selected[i + 1]);
        }
        return sum;
    }
}

 

[설명]

부르트 포스 알고리즘과 dfs를 활용해 풀었다. for문을 돌며 깊이 우선 탐색으로 count가 N이 될 때 까지 탐색하다 count 가 N이 될 경우 getResult() 메서드를 실행해 지금까지 저장했던 result 값과 비교해 최댓값을 출력한다. getResult() 메서드는 dfs 알고리즘을 돌며 count가 N이 될 때 까지 선택해왔던 숫자들을 담은 배열을 계산해주는 메서드이다.

'코딩테스트' 카테고리의 다른 글

백준 15686번: 치킨 배달[JAVA]  (0) 2024.03.28
백준 14225번: 부분수열의 합[JAVA]  (0) 2024.03.25
백준 1260번: DFS와 BFS[JAVA]  (0) 2024.03.20
백준 1068번: 트리[JAVA]  (1) 2024.03.15
백준 1991번: 트리 순회[JAVA]  (1) 2024.03.15

+ Recent posts