중첩 클래스(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