클래스 내에 있는 또 다른 클래스를 내부 클래스라 한다.
사용빈도는 높지 않지만, 코드의 복잡성을 줄일 수 있고 내부 클래스에서 외부 클래스의 멤버에 쉽게 접근할 수 있다는 장점이 있다.
class A{
class B{} //내부 클래스인 B
} //외부 클래스인 A
내부 클래스의 종류는 세 가지로 나뉘는데, 유효범위와 성질이 변수와 비슷하다.
- 인스턴스 클래스 : 주로 외부 클래스의 인스턴스 멤버들과 관련된 작업에 사용될 목적으로 선언
- static 클래스 : 주로 외부 클래스의 static 멤버들과 관련된 작업에 사용될 목적으로 선언
- 지역 클래스 : 외부 클래스의 메서드나 초기화 블럭 내에 선언, 선언된 영역 내부에서만 사용 가능
- 익명 클래스 : 이름 없는 클래스로 일회성
예시를 통해 이해해보자.
class Outer{
class InstanceClass{} //인스턴스 클래스
static class StaticClass{} //static 클래스
void methodExample() {
class LocalClass{} //지역 클래스
}
}// 외부 클래스
내부 클래스도 클래스이기 때문에 abstract이나 final 같은 제어자 사용이 가능하다.
일반적인 클래스는 public, default 접근 제어자만 사용 가능하지만, 내부 클래스의 경우 public, protected,default,private 모두 사용 가능하다.
내부 클래스는 외부 클래스의 멤버와 같이 간주되고 인스턴스 멤버와 static 멤버 간의 규칙이 동일하게 적용된다.
- 인스턴스 멤버는 static 멤버 사용이 가능하다
- static 멤버는 인스턴스 멤버 사용이 불가하다
- 지역 멤버는 외부에서 접근이 불가하다
이 규칙이 내부 클래스에도 동일하게 적용되므로 인스턴스 클래스는 static 클래스 사용이 가능하지만, static 클래스는 인스턴스 클래스 사용이 불가하다.
static 클래스에서 인스턴스 클래스를 사용하려면 외부 클래스의 인스턴스를 생성하여 호출해야만 한다.
지역 클래스는 외부에서 접근이 불가능하므로 당연히 인스턴스 클래스와 static 클래스에서 사용이 불가하며, 지역 클래스를 선언한 메서드 내에서만 사용이 가능하다.
class Outer{
class InstanceClass{}
static class StaticClass{}
InstanceClass ic = new InstanceClass();
static StaticClass sc = new StaticClass();
void InstanceMethod() {
InstanceClass ic2 = new InstanceClass();
StaticClass sc2 = new StaticClass();
// LocalClass lc1 = new LocalClass(); // 외부에서 지역 클래스 호출 불가
}
static void StaticMethod() {
// InstanceClass ic3 = new InstanceClass(); //static 멤버는 인스턴스 클래스 호출 불가
StaticClass sc3 = new StaticClass(); //static 멤버 간 호출 가능
// LocalClass lc2 = new LocalClass(); // 외부에서 지역 클래스 호출 불가
}
void ForLocalMethod() {
class LocalClass{}
LocalClass lc = new LocalClass(); //선언된 영역에서는 지역 클래스 호출 가능
}
void methodExample() {
class LocalClass{} // 지역 클래스는 이름 중복 허용 // 단, 저장 시 파일명에 숫자가 추가됨
}
}
static 멤버에서 인스턴스 클래스를 사용하고 싶다면 외부 클래스의 인스턴스를 생성해야 한다.
코드를 다음과 같이 수정해보자.
static void StaticMethod(){
Outer o = new Outer();
InstanceClass ic = o.new InstanceClass();
}
- 인스턴스 클래스는 외부 클래스의 인스턴스 멤버, static 멤버를 객체 생성 없이 사용 가능하다
- static 클래스는 객채를 생성해야 외부 클래스의 인스턴스 멤버 사용이 가능하다
(static 멤버는 객체생성 없이 사용 가능)
여기서 인스턴스 멤버라고 했지만 인스턴스 클래스의 멤버도 포함된다.
class Outer{
private int outeriv = 3;
static int outercv = 4;
class InstanceClass{
int iic = outeriv;
int icv = outercv;
//static int icv2 = cv; //static 변수 선언 불가
}
static class StaticClass{
// int sic = outeriv;
int scv = outercv;
static int scv2 = outercv;
}
void methodExample(){
int lv1 = 8;
final int lv2 = 9;
class InnerClass{
int liv = outeriv;
int liv2 = outercv;
int liv3 = lv1; //JDK 1.8부터 가능
int liv4 = lv2;
}
}
}
멤버 간 사용 규칙을 코드로 정리해보았다.
여기서 두 가지가 눈에 띄는데 첫 번째로 static 변수 선언 불가, 두 번째로 지역변수 호출이다.
static 변수는 내부 클래스 중 static 클래스만 선언이 가능하며(호출과 다르다) 인스턴스 클래스와 지역 클래스에서는 static 변수를 선언할 수 없다.
메서드 내 지역변수의 경우 원래는 final이 붙은 지역변수만 지역 클래스가 사용이 가능했다. 하지만, JDK 1.8 이후부터는 메서드 내의 지역변수에 final이 자동으로 추가되기 때문에 final이 붙지 않아도 상수이므로 지역 클래스에서 호출이 가능하다.
void method(){
int a = 3;
class Local{
//a = 4;
}
}
하지만, 이처럼 지역변수를 초기화 할 경우 컴파일 에러가 발생한다. final이 자동으로 추가되어 상수이기 때문이다.
만약, 외부 클래스와 내부 클래스에서 선언한 변수 명이 같을 경우 어떻게 구분할까?
class Outer{
int iv = 3;
class InstanceClass{
int iv = 4;
}
void methodExample() {
int iv = 5;
System.out.println("iv = " + iv);
System.out.println("this.iv = " + this.iv);
System.out.println("Outer.this.iv = " + Outer.this.iv);
}
}
변수 iv는 외부 클래스, 인스턴스 클래스, 메서드 내에서 변수 명은 같지만 값이 서로 다르다.
class Outer{
int iv = 3;
class InstanceClass{
int iv = 4;
void methodExample() {
int iv = 5;
System.out.println("iv = " + iv); //지역변수
System.out.println("this.iv = " + this.iv); //인스턴스 클래스의 변수
System.out.println("Outer.this.iv = " + Outer.this.iv); //외부 클래스의 인스턴스 변수
}
}
static class StaticClass{
static int iv = 6;
}
}
public class InnerClassExample {
public static void main(String[] args) {
Outer o = new Outer();
Outer.InstanceClass oi = o.new InstanceClass();
oi.methodExample();
System.out.println();
System.out.println("InstanceClassiv = " + oi.iv);
System.out.println("StaticClassic = " +Outer.StaticClass.iv);
}
}
처음 iv를 호출할 경우 methodExample 내에서 선언된 지역변수 iv가 호출되고 this를 붙이면 해당 메서드가 소속된 클래스의 iv가 호출된다.
외부 클래스의 iv를 호출할 경우 외부클래스명.this.변수명으로 호출할 수 있다.
추가로 인스턴스 클래스와 static 클래스를 외부 클래스가 아닌 다른 클래스에서 호출하는 방법이 서로 다르다.
인스턴스 클래스의 경우 외부 클래스의 인스턴스를 생성해야만 생성가능하므로
변수타입도 외부클래스명.인스턴스클래스명으로, new 연산자도 앞에 외부클래스 인스턴스의 참조변수명을 추가해야 한다.
하지만, static 클래스는 외부 클래스의 인스턴스 생성이 불필요하므로 외부클래스명.static클래스명으로 호출/사용이 가능하다.
내부 클래스의 종류를 설명할 때 일회성으로 사용되는 익명 클래스를 언급했었다.
왜 일회성으로 사용할까?
만약, 재사용을 더 할 필요가 없고 한 번만 쓰고 버릴 클래스가 있다면 굳이 클래스를 만들고 인스턴스를 생성해야 할까?
차라리, 일회용으로 사용하고 재사용하지 않는 것이 효율적일 것이다.
그래서 익명 클래스를 사용하는 것이다.
익명 클래스는 클래스의 선언과 객체 생성을 동시에 하기에 한 번만 사용될 수 있고 오직 하나의 객체만을 생성 가능하다.
다음과 같은 형식으로 클래스를 선언한다.
new 조상클래스이름(){
// 내용
}
또는
new 구현인터페이스이름(){
//내용
}
이게 무슨말인지 이해가 잘 가지 않는데 단계별로 설명하겠다.
Myclass extends Parentclass{
//내용
} //클래스 선언
Myclass mc = new Myclass(); // 객체 생성
일반적인 클래스 선언과 객체 생성하는 방법이다. 하지만, 익명 클래스는? 이름이 없다. 여기서 클래스의 이름을 다 지워보자.
_____ extends Parentclass{
//내용
} //클래스 선언
_____ mc = new _____(); // 객체 생성
이름을 전부 없앴더니 선언할 때 참고할 이름이 필요하다. 그래서 상속한 조상 클래스 또는 구현한 인터페이스의 이름을 가져오겠다.
new Parentclass{
//내용
}
예시에서는 Parentclass 클래스를 상속했기 때문에 저 이름을 사용하지만 인터페이스를 구현했을 경우 그 인터페이스의 이름을 사용하면 된다.
익명 클래스는 이름이 없기 때문에 생성자도 가질 수 없으며, 조상클래스 또는 구현한 인터페이스의 이름을 사용해서 정의하기 때문에 이름이 여러 개이면 안된다. 그래서 다음 규칙을 따라야 한다.
- 하나의 클래스를 상속하는 동시에 다른 인터페이스를 동시에 구현할 수 없다
- 둘 이상의 인터페이스를 구현할 수 없다
즉, 단 하나의 클래스 또는 인터페이스를 상속하거나 구현할 수 있다.
예시를 통해 익명 클래스 사용법을 알아보자.
interface borrowable{
public abstract void methodA();
}
class instant implements borrowable{
public void methodA() {
System.out.println("This is anonymous class");
}
}
borrowable 인터페이스를 구현하는 instant 클래스가 있다. 이 클래스를 익명 클래스로 만들어보겠다.
public class AnonymousClassExample {
public static void main(String[] args) {
borrowable ba = new borrowable() {
public void methodA() {
System.out.println("This is anonymous class");
}
};
ba.methodA();
}
}
여기서 왜 인터페이스로 인스턴스를 생성할 수 있지?라는 의문이 들 수 있다. 하지만, 참조변수의 타입만 borrowable 인터페이스 타입으로 선언하고 익명 클래스를 참조하기 때문에 가능하다.
보통 이런 익명 클래스는 복잡한 내용이 들어갈 경우 코드의 복잡성으로 인해 효율성이 떨어지기 때문에 간결하게 한 번만 사용할 기능으로서 사용하는 것이 좋다.