7.2.1 접근 범위
Java에서는 public, protected, private 같은 수식어를 자주 볼 수 있습니다. 이 수식어들은 접근 범위를 제한하는 데 사용됩니다.
public
public으로 선언된 class, interface는 다른 모든 클래스에서 접근할 수 있습니다:
//package abc;
public class Hello {
public void hi() {
}
}
위의 Hello 클래스는 public이므로 다른 패키지의 클래스에서 접근할 수 있습니다:
//package xyz;
class Main {
void foo() {
// Main 클래스에서 Hello 접근 가능
Hello h = new Hello();
}
}
public으로 선언된 field, method는 다른 클래스에서 접근할 수 있지만, 먼저 해당 클래스에 접근할 권한이 있어야 합니다:
//package abc;
public class Hello {
public void hi() {
}
}
위의 hi() 메소드는 public이므로 다른 클래스에서 호출할 수 있지만, 먼저 Hello 클래스에 접근할 수 있어야 합니다:
//package xyz;
class Main {
void foo() {
Hello h = new Hello();
h.hi();
}
}
private
private으로 선언된 field, method는 다른 클래스에서 접근할 수 없습니다:
//package abc;
public class Hello {
// 다른 클래스에서 호출 불가:
private void hi() {
}
public void hello() {
this.hi();
}
}
정확히 말하면, private 접근 권한은 클래스 내부로 제한되며, 메소드 선언 순서와 무관합니다. private 메소드는 뒤에 배치하는 것이 좋습니다. public 메소드는 클래스가 외부에 제공하는 기능을 정의하므로, 코드를 읽을 때는 public 메소드에 먼저 주의를 기울여야 합니다:
//package abc;
public class Hello {
public void hello() {
this.hi();
}
private void hi() {
}
}
Java는 중첩 클래스를 지원하므로, 클래스 내부에 중첩 클래스가 정의된 경우, 중첩 클래스는 private에 접근할 권한을 가집니다:
// private
public class Main {
public static void main(String[] args) {
Inner i = new Inner();
i.hi();
}
// private 메소드:
private static void hello() {
System.out.println("private hello!");
}
// 정적 내부 클래스:
static class Inner {
public void hi() {
Main.hello();
}
}
}
클래스 내부에 정의된 클래스를 중첩 클래스(nested class)라고 하며, Java는 여러 종류의 중첩 클래스를 지원합니다.
protected
protected는 상속 관계에 적용됩니다(다음 강의에서 상속을 다룹니다). protected로 선언된 필드와 메소드는 하위 클래스에서 접근할 수 있으며, 하위 클래스의 하위 클래스에서도 접근할 수 있습니다:
//package abc;
public class Hello {
// protected 메소드:
protected void hi() {
}
}
위의 protected 메소드는 상속받은 클래스에서 접근할 수 있습니다:
//package xyz;
class Main extends Hello { //extends 상속 키워드
void foo() {
// protected 메소드 접근 가능:
hi();
}
}
package
마지막으로, 패키지 범위는 동일한 package에 있는 public, private 수식어가 없는 클래스에 접근을 허용하고, public, protected, private 수식어가 없는 필드와 메소드에 접근을 허용합니다:
//package abc;
// 패키지 권한의 클래스:
class Hello {
// 패키지 권한의 메소드:
void hi() {
}
}
동일한 패키지에 있다면, 패키지 권한의 클래스, 필드와 메소드에 접근할 수 있습니다:
//package abc;
class Main {
void foo() {
// 패키지 권한의 클래스 접근 가능:
Hello h = new Hello();
// 패키지 권한의 메소드 호출 가능:
h.hi();
}
}
패키지 이름이 완전히 일치해야 하며, 패키지는 부자 관계가 없습니다. com.apache와 com.apache.abc는 다른 패키지입니다.
지역 변수
메소드 내부에 정의된 변수를 지역 변수라고 하며, 지역 변수의 범위는 변수 선언 부분부터 해당 블록의 끝까지입니다. 메소드 매개변수도 지역 변수입니다:
//package abc;
public class Hello {
void hi(String name) { // 1
String s = name.toLowerCase(); // 2
int len = s.length(); // 3
if (len < 10) { // 4
int p = 10 - len; // 5
for (int i=0; i<10; i++) { // 6
System.out.println(); // 7
} // 8
} // 9
} // 10
}
위의 hi() 메소드 코드를 살펴보겠습니다:
- 메소드 매개변수
name은 지역 변수이며, 그 범위는 전체 메소드(1 ~ 10)입니다. - 변수
s의 범위는 정의 부분부터 메소드 끝까지(2 ~ 10)입니다. - 변수
len의 범위는 정의 부분부터 메소드 끝까지(3 ~ 10)입니다. - 변수
p의 범위는 정의 부분부터 if 블록 끝까지(5 ~ 9)입니다. - 변수
i의 범위는 for 루프(6 ~ 8)입니다.
지역 변수를 사용할 때는 가능한 한 지역 변수의 범위를 최소화하고, 선언을 최대한 늦추어야 합니다.
final
Java는 final 수식어도 제공합니다. final은 접근 권한과 충돌하지 않으며, 여러 가지 기능을 가집니다.
final로 클래스를 수정하면 상속을 막을 수 있습니다:
//package abc;
// 상속 불가:
public final class Hello {
private int n = 0;
protected void hi(int t) {
long i = t;
}
}
final로 메소드를 수정하면 하위 클래스에서 재정의(override)를 막을 수 있습니다:
//package abc;
public class Hello {
// 재정의 불가:
protected final void hi() {
}
}
final로 필드를 수정하면 재할당을 막을 수 있습니다:
//package abc;
public class Hello {
private final int n = 0;
protected void hi() {
this.n = 1; // error!
}
}
| this.n = 1; // error!
최종 변수 n에 값을 할당할 수 없음
final로 지역 변수를 수정하면 재할당을 막을 수 있습니다:
//package abc;
public class Hello {
protected void hi(final int t) {
t = 1; // error!
}
}
| t = 1; // error!
최종 매개변수 t에 값을 할당할 수 없음
모범 사례
public이 필요한지 확실하지 않다면public으로 선언하지 않는 것이 좋습니다. 즉, 외부에 노출되는 필드와 메소드를 최소화해야 합니다.- 메소드를
package권한으로 정의하면 테스트에 도움이 됩니다. 테스트 클래스와 테스트 대상 클래스가 동일한 패키지에 있기만 하면, 테스트 코드에서 테스트 대상 클래스의package권한 메소드에 접근할 수 있습니다. - 하나의
.java파일에는 하나의public클래스만 포함할 수 있지만, 여러 개의 비public클래스를 포함할 수 있습니다.public클래스가 있다면 파일 이름은public클래스의 이름과 동일해야 합니다.
요약
Java 내장 접근 권한에는 public, protected, private, 패키지 권한이 포함됩니다.
Java에서 메소드 내부에 정의된 변수는 지역 변수이며, 지역 변수의 범위는 변수 선언부터 블록 끝까지입니다.
final 수식어는 접근 권한이 아니며, 클래스, 필드, 메소드를 수정할 수 있습니다.
하나의 .java 파일에는 하나의 public 클래스만 포함할 수 있지만, 여러 개의 비public 클래스를 포함할 수 있습니다.
7.2.2 내부 클래스
Java 프로그램에서 일반적으로는 다른 클래스를 다른 패키지 아래에 구성합니다. 하나의 패키지 아래에 있는 클래스들은 동일한 계층에 있으며, 부자 관계가 없습니다:
java.lang ├── Math ├── Runnable ├── String └── ...
또 다른 종류의 클래스는 다른 클래스 내부에 정의되어 있으므로 내부 클래스(Nested Class)라고 합니다. Java의 내부 클래스는 여러 종류로 나뉘며, 일반적으로는 많이 사용되지 않지만 사용 방법을 이해해야 합니다.
내부 클래스 (Inner Class)
하나의 클래스가 다른 클래스 내부에 정의된 경우, 이 클래스는 내부 클래스(Inner Class)입니다:
class Outer {
class Inner {
// 내부 클래스 정의
}
}
위에서 정의된 Outer는 일반 클래스이며, Inner는 내부 클래스입니다. 내부 클래스는 일반 클래스와 가장 큰 차이점은 내부 클래스의 인스턴스는 단독으로 존재할 수 없고, 반드시 Outer 클래스의 인스턴에 의존해야 한다는 점입니다. 예제 코드는 다음과 같습니다:
// 내부 클래스
public class Main {
public static void main(String[] args) {
Outer outer = new Outer("Nested"); // Outer 인스턴스화
Outer.Inner inner = outer.new Inner(); // Inner 인스턴스화
inner.hello();
}
}
class Outer {
private String name;
Outer(String name) {
this.name = name;
}
class Inner {
void hello() {
System.out.println("Hello, " + Outer.this.name);
}
}
}
Hello, Nested
위 코드를 관찰하면, Inner를 인스턴스화하려면 먼저 Outer의 인스턴스를 생성한 다음, Outer 인스턴스의 new를 사용하여 Inner 인스턴스를 생성해야 합니다:
Outer.Inner inner = outer.new Inner();
이것은 Inner 클래스가 자신을 가리키는 this 외에도 암시적으로 Outer 클래스 인스턴스를 가지고 있으며, Outer.this를 사용하여 이 인스턴스에 접근할 수 있기 때문입니다. 따라서 Inner 클래스의 인스턴스는 Outer 인스턴스 없이는 존재할 수 없습니다.
내부 클래스는 일반 클래스에 비해 Outer 인스턴스를 참조할 수 있다는 점 외에도 Outer 클래스의 private 필드를 수정할 수 있는 추가적인 "권한"이 있습니다. 내부 클래스의 범위가 Outer 클래스 내부이므로 Outer 클래스의 private 필드와 메소드에 접근할 수 있습니다.
Java 컴파일러가 컴파일한 후의 .class 파일을 관찰하면 Outer 클래스는 Outer.class로 컴파일되고, Inner 클래스는 Outer$Inner.class로 컴파일됩니다.
익명 클래스 (Anonymous Class)
내부 클래스를 정의하는 또 다른 방법은 Outer 클래스에서 이 클래스를 명시적으로 정의하지 않고, 메소드 내부에서 익명 클래스(Anonymous Class)를 정의하는 것입니다. 예제 코드는 다음과 같습니다:
// 익명 클래스
public class Main {
public static void main(String[] args) {
Outer outer = new Outer("Nested");
outer.asyncHello();
}
}
class Outer {
private String name;
Outer(String name) {
this.name = name;
}
void asyncHello() {
Runnable r = new Runnable() { // 익명 클래스
@Override
public void run() {
System.out.println("Hello, " + Outer.this.name);
}
}; // 익명 클래스 객체, 문장의 일부이므로 세미콜론으로 끝나야 함
new Thread(r).start();
}
}
Hello, Nested
asyncHello() 메소드를 관찰하면, 메소드 내부에서 Runnable을 인스턴스화했습니다. Runnable 자체는 인터페이스이며, 인터페이스는 인스턴스화할 수 없으므로 여기서는 사실적으로 Runnable 인터페이스를 구현하는 익명 클래스를 정의하고, new를 사용하여 이 익명 클래스를 인스턴스화한 다음 Runnable으로 형변환했습니다. 익명 클래스를 정의할 때는 반드시 인스턴스화해야 하며, 익명 클래스를 정의하는 방법은 다음과 같습니다:
Runnable r = new Runnable() {
// 필요한 추상 메소드 구현...
};
익명 클래스는 내부 클래스와 마찬가지로 Outer 클래스의 private 필드와 메소드에 접근할 수 있습니다. 익명 클래스를 정의하는 이유는 일반적으로 클래스 이름에 신경 쓰지 않고, 내부 클래스를 직접 정의하는 것보다 훨씬 적은 코드로 작성할 수 있기 때문입니다.
Java 컴파일러가 컴파일한 후의 .class 파일을 관찰하면 Outer 클래스는 Outer.class로 컴파일되고, 익명 클래스는 Outer$1.class로 컴파일됩니다. 여러 개의 익명 클래스가 있다면 Java 컴파일러는 각 익명 클래스를 순서대로 Outer$1, Outer$2, Outer$3...로 명명합니다.
인터페이스 외에도 익명 클래스는 일반 클래스에서 상속받을 수 있습니다. 다음 코드를 관찰해 보세요:
// 익명 클래스
import java.util.HashMap;
public class Main {
public static void main(String[] args) {
HashMap<String, String> map1 = new HashMap<>();
HashMap<String, String> map2 = new HashMap<>() {}; // 익명 클래스!
HashMap<String, String> map3 = new HashMap<>() {
{
put("A", "1");
put("B", "2");
}
};
System.out.println(map3.get("A"));
}
}
1
map1은 일반 HashMap 인스턴스이지만, map2는 익명 클래스 인스턴스이며, 해당 익명 클래스는 HashMap에서 상속받습니다. map3도 HashMap에서 상속받는 익명 클래스 인스턴스이며, static 블록을 추가하여 데이터를 초기화합니다. 컴파일 출력을 관찰하면 Main$1.class와 Main$2.class 두 개의 익명 클래스 파일이 있습니다.
정적 내부 클래스 (Static Nested Class)
마지막 내부 클래스는 내부 클래스와 유사하지만 static으로 수정되며, 정적 내부 클래스(Static Nested Class)라고 합니다:
// 정적 내부 클래스
public class Main {
public static void main(String[] args) {
Outer.StaticNested sn = new Outer.StaticNested();
sn.hello();
}
}
class Outer {
private static String NAME = "OUTER";
private String name;
Outer(String name) {
this.name = name;
}
static class StaticNested {
void hello() {
System.out.println("Hello, " + Outer.NAME);
}
}
}
Hello, OUTER
static으로 수정된 내부 클래스는 내부 클래스와 크게 다릅니다. 더 이상 Outer의 인스턴스에 의존하지 않으며 완전히 독립된 클래스입니다. 따라서 Outer.this를 참조할 수 없지만, Outer의 private static 필드와 static 메소드에는 접근할 수 있습니다. StaticNested를 Outer 바깥으로 이동하면 private에 접근할 권한이 없어집니다.
요약 Java의 내부 클래스는 내부 클래스(Inner Class), 익명 클래스(Anonymous Class), 정적 내부 클래스(Static Nested Class) 세 가지로 나뉩니다. 내부 클래스와 익명 클래스는 본질적으로 동일하며, 반드시 Outer 클래스의 인스턴스에 의존합니다. 즉, 암시적으로 Outer.this 인스턴스를 가지며, Outer 클래스의 private 접근 권한을 가집니다. 정적 내부 클래스는 독립된 클래스이지만, Outer 클래스의 private 접근 권한을 가집니다.
7.2.3 래퍼 클래스
기본 데이터 타입과 래퍼 클래스
Java는 완전한 객체 지향 언어이며, 거의 모든 메소드는 객체.메소드() 형태로 직접 호출할 수 있습니다. 그러나 8가지 기본 데이터 타입의 존재는 다소 불편합니다. int, char, double 등을 직접 사용하여 원하는 기능을 호출할 수 없으며, 상속, 캡슐화 등 객체 지향 사상도 가지고 있지 않습니다. 따라서 래퍼 클래스(Wrapper Class)가 도입되었습니다:
| 기본 데이터 타입 | 래퍼 타입 |
|---|---|
| byte | java.lang.Byte (상위 클래스 Number) |
| short | java.lang.Short (상위 클래스 Number) |
| int | java.lang.Integer (상위 클래스 Number) |
| long | java.lang.Long (상위 클래스 Number) |
| float | java.lang.Float (상위 클래스 Number) |
| double | java.lang.Double (상위 클래스 Number) |
| boolean | java.lang.Boolean (상위 클래스 Object) |
| char | java.lang.Character (상위 클래스 Object) |
int와 char를 제외하고, 나머지는 첫 글자를 대문자로 바꾸기만 하면 됩니다. 처음 6개 숫자형 래퍼 클래스는 공통 상위 클래스 Number를 가지며, Number는 Object를 상속받습니다.
또한 BigInteger와 BigDecimal 두 개의 래퍼 클래스가 있으며, 해당하는 기본 타입은 없습니다. 이들은 주로 큰 숫자와 부동 소수점 고정 연산을 처리하는 데 사용됩니다. BigInteger는 임의 정밀도 정수를 지원하며, BigDecimal은 임의 정밀도 부동 소수점을 지원합니다.
래퍼 클래스는 java.lang 패키지에 있으므로 import java.lang.*를 사용하여 모든 래퍼 클래스를 가져올 수 있습니다. 래퍼 클래스는 참조 타입이며, 메소드와 속성을 가집니다. 래퍼 클래스는 new로 인스턴스화할 수 있으며, static 메소드 valueOf를 사용하여 기본 타입을 래퍼 타입으로 변환할 수도 있습니다:
래퍼 클래스는 주로 다음과 같은 기능을 제공합니다:
- 기본 데이터 타입을 캡슐화하여 클래스 객체의 특성을 부여합니다. 예를 들어, 래퍼 클래스 객체는 매개변수로 전달할 수 있으며, 반환값으로 사용할 수 있으며, 멤버 변수로 사용할 수 있습니다.
- 기본 데이터 타입에 대한 다양한 변환 기능을 제공합니다. 예를 들어, 문자열을 기본 데이터 타입으로 변환하거나, 기본 데이터 타입을 문자열로 변환합니다.
일부 데이터 구조(예: ArrayList, HashMap)는 원시 값을 저장할 수 없으므로 래퍼 클래스를 사용해야 합니다.
기본 데이터 타입과 래퍼 클래스 간의 변환(Interger를 예시로):
- 기본 데이터 타입을 래퍼 클래스로 변환:
Integer i = Integer.valueOf(10);
Integer k = new Integer(10);
Integer j = 10;
i
10
- 래퍼 클래스를 기본 데이터 타입으로 변환:
int x = i.intValue();
int y = j;
j
10
기본 데이터 타입과 래퍼 클래스의 차이점:
- Java에서는 모든 것이 객체이지만, 8가지 기본 타입은 객체가 아닙니다.
- 선언 방식이 다릅니다. 기본 타입은 직접 선언하며, 래퍼 클래스는 먼저 객체를 생성해야 합니다.
- 저장 방식 및 위치가 다릅니다. 기본 타입은 스택에 직접 저장되며, 래퍼 클래스는 힙에 저장됩니다.
- 초기값이 다릅니다. 기본 타입은 해당하는 기본값을 가집니다(예: int는 0, boolean은 false). 래퍼 클래스의 기본값은 null입니다.
- 사용 방식이 다릅니다. 예를 들어, 컬렉션 클래스와 함께 사용할 때는 래퍼 클래스만 사용할 수 있습니다.
박싱과 언박싱
- 박싱(Boxing)은 기본 타입을 해당 래퍼 객체로 변환하는 것을 의미합니다. 예를 들어, Int 타입을 Integer 객체로 캡슐화합니다.
- 언박싱(Unboxing)은 래퍼 객체를 해당 기본 타입으로 변환하는 것을 의미합니다. 예를 들어, Integer 객체를 Int 타입으로 언박싱합니다.
다음 예를 살펴보겠습니다:
// 수동 박싱
int x=100; // 기본 타입 변수 x 정의
Integer in=new Integer(x); // 기본 데이터 타입 x를 Integer 객체로 캡슐화
System.out.println("래퍼 클래스 Integer 타입 in="+in); // Integer 객체 in 출력
// 수동 언박싱
Float f=new Float(3.14F); /// 기본 데이터 타입 3.14F를 Float 객체로 캡슐화
float y=f.floatValue(); // Float 객체 f를 기본 데이터 타입 float로 변환
System.out.println("기본 타입 float y"+y);
래퍼 클래스 Integer 타입 in=100
기본 타입 float y3.14
// 자동 박싱
int x=100; // 기본 타입 변수 x 정의
Integer in=x; // 기본 데이터 타입 x를 Integer 객체로 캡슐화
System.out.println("래퍼 클래스 Integer 타입 in="+in); // Integer 객체 in 출력
// 자동 언박싱
Float f=new Float(3.14F); /// 기본 데이터 타입 3.14F를 Float 객체로 캡슐화
float y=f; // Float 객체 f를 기본 데이터 타입 float로 변환
System.out.println("기본 타입 float y"+y);
자동 박싱과 언박싱을 사용할 때는 NullPointerException을 유발할 수 있다는 점에 주의해야 합니다. 향후 프로그램 개발 시에는 수동 박싱과 언박싱을 사용하는 것이 좋습니다.
주요 메소드
래퍼 클래스는 많은 메소드를 제공하며, 여기서 일부 주요 메소드를 소개합니다:
- toString(): 기본 데이터 타입을 문자열로 변환합니다.
- valueOf(): 문자열을 기본 데이터 타입으로 변환합니다.
- parseXXX(): 문자열을 기본 데이터 타입으로 변환합니다.
- xxxValue(): 래퍼 클래스를 기본 데이터 타입으로 변환합니다.
- compareTo(): 두 래퍼 클래스의 크기를 비교합니다.
- equals(): 두 래퍼 클래스가 동일한지 비교합니다.
- hashCode(): 객체의 해시 코드 값을 반환합니다.
- clone(): 이 객체의 복사본을 생성하고 반환합니다.
- getClass(): 이 객체의 런타임 클래스를 반환합니다.
7.2.4 Java 일반 클래스 라이브러리
JDK는 개발자가 Java 소프트웨어 개발의 기초로 사용할 수 있는 풍부한 기본 클래스 라이브러리를 제공하여 개발 효율성을 높이고 개발 난이도를 낮춥니다.
날짜 클래스 Date
java.util.Date 클래스는 밀리초 단위로 정확한 특정 순간을 나타내며, GMT(Greenwich Mean Time) 시간으로 표현됩니다. 이는 UTC(Coordinated Universal Time) 시간이라고도 합니다.
java.time.LocalDate.now()
날짜 형식화 클래스 SimpleDateFormat
java.text.SimpleDateFormat
난수 클래스 Random
java.util.Random