25. 톱레벨 클래스는 한 파일에 하나만 담아라

우리가 사용하는 파일 내에 하나만 존재하는 클래스를 톱레벨 클래스라고 합니다.
(중첩클래스는 top 레벨 클래스가 아닙니다)

하지만, 소스파일 하나에 여러개의 톱레벨 클래스가 있다면 심각한 위험을 감수해야 합니다

문제

Utensil.java

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

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

Dessert.java

class UtenSil {
    static final String NAME = "pot";
}

class Dessert {
    static final String NAME = "pie";
}

여기서 아래의 main 함수를 실행해보자

class Main {
    public static void main(String[] args) {
        System.out.println(Utensil.NAME + Dessert.NAME);
    }
}

일반적으론 중복 클래스가 존재한다고 컴파일 에러가 나지만,
javac Main.java Utensil.java의 명령으로 컴파일 한다면 ‘pancake’가
Javac Main.java Dessert.java의 명령으로 컴파일 한다면 ‘potpie’가 출력될 것입니다

해결방안

정적멤버 클래스 또는 톱레벨 클래스를 서로 다른 파일로 분리

정적멤버클래스

class Main {
    public static void main(String[] args) {
        System.out.println(Utensil.NAME + Dessert.NAME);
    }

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

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

톱레벨 클래스로 분리

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

참조

effective java 3/e

블로그 이미지

yhmane

댓글을 달아 주세요

24. 멤버 클래스는 되도록 static으로 만들라

중첩클래스란

중첩 클래스(nested class) 다른 클래스 안에 정의된 클래스를 말합니다.
중첩 클래스는 자신을 감싼 바깥 클래스에서만 쓰여야 하며, 그 외의 쓰임새가 있다면 톱 레벨 클래스로 만들어야 합니다

  • 정적 멤버 클래스
  • (비정적) 멤버 클래스
  • 익명 클래스
  • 지역클래스

이 중에서 정적 멤버 클래스를 제외한 나머지를 내부 클래스 (inner class)라고 합니다

정적 멤버 클래스

정적 멤버 클래스는 바깥 클래스의 private 멤버에도 접근할 수 있다는 점을 제외하고
일반 클래스와 쓰임새는 동일합니다

public class OuterWithStaticClass {

    private String name;

    static class StaticClass {
        void hello() {
            OuterWithStaticClass outerWithStaticClass = new OuterWithStaticClass();
            outerWithStaticClass.name = "홍길동";
            System.out.println(outerWithStaticClass.name);
        }
    }
}
public class Item24App {

    public static void main(String[] args) {

        OuterWithStaticClass.StaticClass staticClass = new StaticClass();
        staticClass.hello();
    }
}

비정적 멤버 클래스

구문상으로는 정적 클래스와 static이 붙어있고 없고의 차이입니다.
하지만 의미상으로 비정적 멤버 클래스는 바깥 클래스의 인스턴스와 암묵적으로 연결됩니다. 따라서 정규화된 this를 사용해 바깥 인스턴스의 메서드를 호출하거나 참조를 가져올 수 있습니다

public class OuterWithNoneStaticClass {

    private final String name;

    public OuterWithNoneStaticClass(String name) {
        this.name = name;
    }

    public String getName() {
        NonStaticClass nonStaticClass = new NonStaticClass("noneStatic-class : ");
        return nonStaticClass.getNameWithOuter();
    }

    private class NonStaticClass {
        private final String noneStaticName;

        public NonStaticClass(String noneStaticName) {
            this.noneStaticName = noneStaticName;
        }

        public String getNameWithOuter() {
            return noneStaticName + OuterWithNoneStaticClass.this.name;
        }
    }
}
public class Item24App {

    public static void main(String[] args) {
        OuterWithNoneStaticClass noneStaticClass = new OuterWithNoneStaticClass("고길동");
        System.out.println(noneStaticClass.getName());
    }
}

드물게 직접 바깥 인스턴스의 클래스.new Member Class(args)를 호출해 수동으로
만들기도 합니다. 하지만 이 관계 정보는 비정적 멤버 클래스의 인스턴스 안에 만들어져 메모리 공간을 차지하며, 생성시간도 더 걸립니다.

따라서, 중첩 클래스의 인스턴스가 바깥 클래스의 인스턴스와 독립적으로 존재해야 한다면 정적 멤버 클래스로 만드는게 좋습니다.

익명 클래스

  • 이름이 없고 바깥 클래스의 멤버도 아닙니다
  • 쓰이는 시점에 선언과 동시에 인스턴스가 만들어집니다
  • 비정적인 문맥에서 사용될 때만 바깥 클래스의 인스턴스 참조가 가능하다
  • 상수 정적변수 (static final) 외에는 정적 변수를 가질 수 없다.
public class Anonymouss {

    private String name;

    public void hello() {
        HelloBot helloBot = new HelloBot() {
            @Override
            public void hello() {
                System.out.println("안녕하세요. 헬로우 봇입니다");
            }
        };
        helloBot.hello();
    }
}

interface HelloBot {
    void hello();
}
public class Item24App {

    public static void main(String[] args) {
        Anonymouss anonymouss = new Anonymouss();
        anonymouss.hello();
    }
}

익명 클래스의 제약사항

  • 선언한 지점에서만 인스턴스를 만들수 있습니다
  • 여러 인터페이스를 구현할 수 없고, 구현과 동시에 다른클래스 상속도 불가능합니다
  • 익명 클래스 사용 클라이언트는 사용하는 익명 클래스가 상위타입에서 상속한 멤버외에는 호출이 불가능합니다
  • 람다(자바7) 등장 이전에는 작은 함수 객체나 처리 객체 구현에 사용되었지만 java8 부터는 람다를 제공합니다

지역 클래스

네 가지 중첩 클래스 중 가장 드물게 사용됩니다.
지역 클래스는 지역 변수를 선언할 수 있는 곳이면 어디서든 선언할 수 있고, 유효 범위도 지역 변수와 같습니다.

public class LocalClass {

    public void hello() {
        class LocalExample {
            private String name;

            public LocalExample(String name) {
                this.name = name;
            }

            public String getName() {
                return name;
            }
        }

        LocalExample localExample = new LocalExample("윤호호");
        System.out.println(localExample.getName());
    }
}
public class Item24App {

    public static void main(String[] args) {
        LocalClass localClass = new LocalClass();
        localClass.hello();
    }
}

지역 클래스는 다른 중첩 클래스들의 공통점을 하나씩 가지고 있다.

  • 멤버 클래스처럼 이름을 가질수 있고 반복해서 사용할 수 있습니다
  • 비정적 문맥에서 사용될 때만 바깥 인스턴스를 참조할 수 있습니다
  • 정적 멤버는 가질 수 없으며, 가독성을 위해 짧게 작성되어야 합니다
블로그 이미지

yhmane

댓글을 달아 주세요

23. 태그 달린 클래스보다는 클래스 계층구조를 활용하라

태그 달린 클래스

태그 클래스란, 두가지 이상의 의미를 표현할 때 그 중 현재 표현하는 의미를 태그값으로 알려주는 클래스 입니다.

책에 나온 예제로 살펴보도록 하겠습니다.

class Figure {

    enum Shape {
        RECTANGLE, CIRCLE
    }

    // 태그 필드
    final Shape shape;

    // RECTANGLE 용 필드
    double length;
    double width;

    // CIRCLE 용 필드
    double radius;

    // RECTANGLE
    Figure(double length, double width) {
        shape = Shape.RECTANGLE;
        this.length = length;
        this.width = width;
    }

    // CIRCLE
    Figure(double radius) {
        shape = Shape.CIRCLE;
        this.radius = radius;
    }

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

태그 클래스의 단점

  1. Enum switch, field 등 여러 구현이 혼합 되어 있습니다
  2. 이로 인해 가독성이 떨어집니다
  3. 사용하지 않는 필드로 인해 메모리도 추가적으로 사용합니다
  4. 또 다른 타입을 추가하게 되면 switch 문의 수정이 일어납니다
  5. 인스턴스 타입만으로 객체의 의미를 알 수 없습니다

태그가 달린 클래스는 코드가 길어지고 확장성에 취약합니다.
또한, 태그가 추가 될 때마다 사용하지 않는 필드들이 추가 될 수도 있습니다.


계층 클래스

계층 구조의 클래스를 만드는 방법

  1. 계층구조의 최상위(root)가 될 추상클래스를 정의합니다
  2. 태그값에 따라 달라지는 동작(메서드)들을 최상위 클래스의 추상 메서드로 선언합니다
  3. 태그값에 상관없이 동작이 일정한 메서드는 최상위 클래스에 일반 메서드로 정의합니다.
  4. 모든 하위 클래스에 공통으로 사용하는 상태값(필드)들은 루크 클래스에 정의합니다.

계층구조로 변환

abstract class Figure {
    abstract double area();
}

class Rectangle extends Figure {

    final double length;
    final double width;

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

    @Override
    double area() {
        return length * width;
    }
}


class Circle extends Figure {

    final double radius;

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

    @Override
    double area() {
        return Math.PI * (radius * radius);
    }
}

클래스 계층구조의 장점

  1. 독립된 의미를 가지는 상태값(필드)들이 제거 되어 각 구현 클래스는 간결해집니다
  2. 살아남은 field는 모두 final이므로 정의할 수 있습니다
  3. 실수로 빼먹은 case 문으로 인해 runTime 에러를 방지할 수 있습니다
  4. 최상위 클래스를 수정하지 않고 타입을 확장 할 수 있습니다
  5. 타입사이의 자연스러운 계층 관계를 반영할 수 있습니다
블로그 이미지

yhmane

댓글을 달아 주세요

22. 인터페이스는 타입을 재정의하는 용도로만 사용하라

인터페이스는 타입을 정의하는 용도로만 사용해야 합니다.
상수 공개용 수단으로 사용해서는 안됩니다

안티패턴 - 상수 인터페이스

메서드 없이, 상수를 뜻하는 static final 필드로만 가득찬 인터페이스

public interface PhysicalConstants {

    // 아보가드로 수
    static final double AVOGADROS_NUMBER = 6.022_140_857e23;
    // 볼츠만 상수
    static final double BOLTZMANN_CONSTANT = 1.380_648_52e-23;
    // 전자 질량
    static final double ELECTRON_MASS = 9.109_383_56e-31;
}
  • 클래스 내부에서 사용하는 상수는 외부 인터페이스가 아닌 내부 구현입니다
    • 내부 구현을 클래스의 API로 노출하는 행위가 됩니다
    • 또한, 사용자에게 혼란과 오해의 소지를 줄 뿐입니다

대안

클래스의 상수로 선언하여 사용합니다

public class PhysicalConstants {

    // 인스턴스화 방지
    private PhysicalConstants(){}

    static final double AVOGADROS_NUMBER = 6.022_140_857e23;
    static final double BOLTZMANN_CONSTANT = 1.380_648_52e-23;
    static final double ELECTRON_MASS = 9.109_383_56e-31;
}

또는 enum의 값으로 사용합니다

public enum PhysicalConstants {

    AVOGADROS_NUMBER(6.002_140_857e23),
    BOLTZMANN_CONSTANT(1.380_648_52e-23),
    ELECTRON_MASS(9.109_383_56e-31);

    private double value;

    PhysicalConstants(double value) {
        this.value = value;
    }

    public double getValue(){
        return value;
    }
}

참조

effective java 3/e - 조슈아 블로크

블로그 이미지

yhmane

댓글을 달아 주세요

늦은 초기화 lateinit, by lazy

늦은 초기화란 무엇일까요??

늦은 초기화란?
말 그대로 객체 초기화를 늦게 하는 것을 말합니다

아래 코드를 잠시 살펴 보겠습니다. 코틀린에서는 Java와 달리 null을 허용/비허용 해줄수 있는데요

아래 코드를 보면 ‘?’ na라는 변수에 null을 허용해준다는 뜻입니다

var na: String? = null 

위의 코드에서는 왜 null을 허용했습니다. 하지만 우리는 java에서 null로 인해 충분히 스트레스를 받았기 때문에 위의 코드를 아래의 방법으로 null을 제거할 수 있습니다

  1. lateinit 사용
  2. by lazy 사용

lateinit 사용

소스코드를 먼저 보며 파악해보도록 하겠습니다

LateInitTest.kt

class LateInitTest {

    lateinit var lateStr: String

    fun test(): String {
        val hello = "hello"
        lateStr = hello
        println(lateStr)

        val world = "world"
        lateStr = world
        println(lateStr)

        return this.lateStr
    }
}

LateInitTests.kt

class LateInitTests {

    @Test
    fun test() {
        // Given
        val lateInit = LateInitTest()

        // When
        val result = lateInit.test()

        // Then
        assertThat(result).isEqualTo("world")
    }
}
  • lateinit을 붙여주면 nullable로 사용할 수 없습니다
  • lateinit은 var 와 함께 사용됩니다
  • 즉, 지연초기화를 이용하고 이후에도 값을 여러번 바꿀 수 있다는 것을 뜻합니다
  • 주의점으로, primitive type(Int, Double, Boolean, Float 등)은 올 수 없습니다

by lazy 사용

마찬가지로 소스코드를 먼저 보며 파악해보도록 하겠습니다

ByLazyTest.kt

class ByLazyTest {

    lateinit var lateStr: String
    val strLength: Int by lazy {
        lateStr.length
    }

    fun test(): Int {
        lateStr = "hello"
        return this.strLength
    }
}

ByLazyTests.kt

class ByLazyTestTests {

    @Test
    fun test() {
        // Given
        val byLazy = ByLazyTest()

        // When
        val result = byLazy.test()

        // Then
        assertThat(result).isEqualTo(5)
    }
}
  • var 대신 val을 사용합니다. 값이 불변함을 뜻 합니다
  • by lazy 선언 당시에는 초기화할 방법이 없지만, 의존하는 값들이 초기화된 이후 값을 넣어줄 때 사용됩니다

정리

lateinit과 by lazy는 사용 목적은 비슷하지만 쓰임새는 조금의 차이가 있습니다

  • 가변/불변으로 쓰임새의 따라 사용합니다
  • lateinit은 초기화 이후 값이 계속 바뀔 때, by lazy는 읽기 전용으로 사용될 때 사용합니다

참조

'Kotlin' 카테고리의 다른 글

[kotlin] 늦은 초기화 lateinit, by lazy 사용 | 비교  (0) 2021.12.27
kotlin 시작하기  (0) 2020.08.04
블로그 이미지

yhmane

댓글을 달아 주세요