java, 클래스와 인터페이스의 기본 - 접근제어와 안전한 작성

들어가며

  • 이펙티브 자바에서 클래스/메서드 파트를 공부를 공부할 때 자바 기본 지식에 대한 부족함을 느꼈다.
  • 신용권 개발자님의 이것이 자바다를 참고하였다.

클래스의 선언과 class 파일의 생성

  • 소스파일의 이름과 동일하게 코드에서의 클래스 이름을 정해야 한다. 접근 제어자는 public으로 부여해야 한다.
  • 소스파일 내부에 public class 이외에 클래스를 작성할 수 있다. 소스코드로는 한 파일에 저장하더라도 컴파일 시점에서 다른 파일(.class)로 분리 된다. class PackagePrivateClass의 경우 package-private 상태이기 때문에 같은 패키지 내부에서 참조 가능하다.

  • PublicClass.java
public class PublicClass {

}

// 컴파일 시점에서 분리된다.
class PackagePrivateClass{

}
  • 아래의 이미지처럼 실제로 분리된다! 신기하다!

정적 초기화 블록

  • 정적 필드의 경우 필드 선언과 동시에 초기화 하거나 생성자에서 초기화 한다.
  • 추가적으로 아래와 같이 정적 초기화 블록을 사용할 수 있다.
public class StaticInitBlock {
    public static final String RANDOM_UUID;

    static {
        System.out.println("정적 클래스에 대한 초기화 블록이 실행됩니다!");
        RANDOM_UUID = UUID.randomUUID().toString();
    }

    public static void main(String[] args) {
        System.out.println("StaticInitBlock.RANDOM_UUID = " + StaticInitBlock.RANDOM_UUID);
    }
}

패키지와 import

  • 자기 자신을 제외한 클래스에 접근하는 방법은 세 가지이다.
    • 메서드 블럭에서 경로와 클래스 이름을 다 명시한다.
    • 소스코드 최상단에 import로 경로를 적는다. 메서드 블럭에서 클래스 이름만 작성한다.
    • 소스코드 최상단에 import static로 경로를 적는다. 매서드 명만 적을 수 있다.
import java.time.LocalDate;

import static java.time.LocalDateTime.*;

public class ImportTest {
    public static void main(String[] args) {
        System.out.println("java.time.Instant.now() = " + java.time.Instant.now());
        System.out.println("LocalDate.now() = " + LocalDate.now());
        System.out.println("now() = " + now());
    }
}

접근 제한자와 상속

  • 클래스와 그것의 구성요소인 필드, 메서드는 외부 클래스에서의 접근 제한을 할 수 있다.
  • 접근 제한이 강한 순서는 다음과 같다 : private - package-private(default) - protected - public
  • private은 해당 클래스 내부에서만 사용 가능하다. 상속할 수 없다.
  • package-private(default)은 같은 패키지에서만 접근 가능하다.
  • protected는 상속한 서브타입만 접근 가능하다.
    • protected은 대체로 공개된 api로 이해한다. 상속을 하면 접근 가능하기 때문이다.
    • protected는 상속을 통해 변경 및 튜닝이 필요한 부분에 대해서만 제한적으로 사용한다.
  • 가능하면 접근 제한자는 private로 사용하며 불가피할 경우 package-private로 사용한다.
  • 클래스는 public 혹은 package-private(default) 만 사용 가능하다.

같은 패키지 내의 접근 예시

public class AccessTest {
    public static void main(String[] args) {
        String publicField = PackagePrivateClass.PUBLIC_FIELD;
        String protectedField = PackagePrivateClass.PROTECTED_FIELD;
        String packagePrivateField = PackagePrivateClass.PACKAGE_PRIVATE_FIELD;
        String privateField = PackagePrivateClass.PRIVATE_FIELD; // 컴파일 에러
    }
}

class PackagePrivateClass{
    public static final String PUBLIC_FIELD = "hi!";
    protected static final String PROTECTED_FIELD = "hi!";
    static final String PACKAGE_PRIVATE_FIELD = "hi!";
    private static final String PRIVATE_FIELD = "hi!";
}

다른 패키지에서의 접근

  • access.a 패키지에 있는 클래스를 접근할 때 다음과 같은 컴파일 결과가 발생한다.
package access.a;

public class AccessTest {
    public static final String PUBLIC_FIELD = "hi!";
    protected static final String PROTECTED_FIELD = "hi!";
    static final String PACKAGE_PRIVATE_FIELD = "hi!";
    private static final String PRIVATE_FIELD = "hi!";
}
package access.b;

import access.a.AccessTest; 

public class AccessTestTest {
    public static void main(String[] args) {
        final String protectedField = AccessTest.PROTECTED_FIELD; // 컴파일 오류
        final String publicField = AccessTest.PUBLIC_FIELD;
    }
}

class AccessExtends extends AccessTest{
    public static void main(String[] args) {
        final String protectedField = AccessTest.PROTECTED_FIELD; // 상속을 하니 protected 까지 접근 가능하다. 
    }
}

인터페이스의 구현

  • 자바에는 암묵적인 코드가 존재한다. 예를 들면 생성자가 없는 클래스는 컴파일 시 자동적으로 기본 생성자가 작성된다.
  • 인터페이스의 모든 메서드는 누락하더라도 public abstract이 암묵적으로 작성된다.
  • 필드의 경우 public static final이 암묵적으로 작성된다.
  • 리스코프의 원칙에 따라 하위 객체는 상위 객체로 치환될 수 있어야 한다. 그러므로 인터페이스를 오버라이딩한 모든 메서드는 반드시 public 메서드이다.
public interface InterfaceJava8 {

    String abc =  "abc";

    LocalDateTime getDate();

}
public class Main {
    public static void main(String[] args) throws NoSuchFieldException, NoSuchMethodException {
        // public static final java.lang.String interfaces.a.InterfaceJava8.abc
        final Field abc = InterfaceJava8.class.getDeclaredField("abc"); 
        
        // public abstract java.time.LocalDateTime interfaces.a.InterfaceJava8.getDate()
        final Method getDate = InterfaceJava8.class.getDeclaredMethod("getDate"); 
    }
}

자바 8과 인터페이스 구현

  • 자바 8 이전의 모든 메서드는 반드시 abstract 메서드였다.
  • 자바 8 이후 인터페이스에서 구현 메서드를 작성할 수 있다.
  • default와 static가 그것이며 전자는 인스턴스 메서드이고 후자는 정적 메서드이다.
public interface InterfaceJava8 {

    String abc =  "abc";

    LocalDateTime getDate();

    default String defaultMethod(){
        return "defaultMethod";
    }

    static String staticMethod(){
        return "staticMethod";
    }

}