내부클래스, 익명클래스

김운채·2023년 5월 9일
0

TIL

목록 보기
3/22

내부 클래스(Inner class)

내부 클래스란, 클래스안의 또 다른 클래스를 말한다.
내부 클래스는 보통 두 클래스가 서로 긴밀한 관계가 있거나, 하나의 클래스또는 메소드에서만 사용되는 클래스일 때 이용되는 기법이라고 보면 된다.

class A { //외부클래스
  class B{ //내부클래스
  }
}

그럼 내부 클래스는 왜 쓸까?

내부 클래스의 장점


1. 클래스를 논리적으로 그룹화

클래스가 여러 클래스와 관계를 맺지 않고 하나의 특정 클래스와만 관계를 맺는다면, 외부에 클래스를 새로 작성하는 것이 아닌 내부 클래스로 작성할 수 있다.

이런 경우 유지보수 면에서나 코드 이해성 면에서 편리해진다.
또한 내부 클래스로 인해 새로운 클래스를 생성하지 않아도 되므로 패키지를 간소화할 수 있다.

2. 코드의 복잡성을 줄일 수 있다.(캡슐화)

내부 클래스에 private 제어자를 적용해줌으로써, 캡슐화를 통해 클래스를 내부로 숨길 수 있다.

내부클래스의 제어자는 변수에 사용 가능한 제어자와 동일하다.

class Outer{
	private class InstanceInner{}
	protected static class StaticInner{}
	
	void method(){
		class LocalInner{}
	}
}

즉, 외부에서의 접근을 차단하면서도, 내부 클래스에서 외부 클래스의 멤버들을 제약 없이 쉽게 접근할 수 있어 구조적인 프로그래밍이 가능해 진다.(객체 생성 없이 쓸수 있다는 것) 그리고 클래스 구조를 숨김으로써 코드의 복잡성도 줄일 수 있다.

3. 가독성이 좋고 유지관리가 쉽다.

내부 클래스를 작성하는 경우 클래스를 따로 외부에 작성하는 경우보다, 물리적으로 논리적으로 외부 클래스에 더 가깝게 위치하게 된다. 따라서 시각적으로 읽기가 편해질 뿐 아니라 유지보수에 있어 이점을 가지게 된다.

내부 클래스 종류

내부클래스에는 다음과같이 4가지가 있다.

출처

class Outer{
	class InstanceInner { ... } // 인스턴스 클래스
	static class StaticInner { ... } // 스태틱 클래스
    
    void method1(){
    	class LocalInner { ... } // 지역 클래스
    }
}

그럼 만약 이름이 같은 메서드를 호출할때에는 어떻게 해야할까?

정규화된 this를 사용하면 바깥 인스턴스의 메서드를 호출하거나 바깥 인스턴스의 참조를 가져올 수 있다.

정규화된 this란 클래스명.this 형태로 바깥 클래스의 이름을 명시하는 용법을 말한다.

public class Main {

    public void print(String txt) {
        System.out.println(txt);
    }

    class Sub {
        public void print() {
            Main.this.print("외부 클래스 메소드 호출");
            System.out.println("내부 클래스 메소드 호출");
        }
    }
}

static 내부 클래스에서는 정규화된 this 문법을 사용할 수 없다.

Static class

내부클래스를 작성하는데 static 멤버가 필요하다면 내부클래스도 static 이어야한다.
static 멤버는 객체생성없이 사용가능해야 하는 것인데, class 가 static 이 아니면 객체를 생성하고 써야 하기 때문이다.

static이 아닌 내부 인스턴스 클래스는 외부와 연결이 되어 있어 '외부 참조'를 갖게되어 메모리를 더 먹고, 느리며, 또한 GC 대상에서 제외되는 여러 문제점을 일으키기 때문이기도 하다.

static 내부 클래스에서는 외부 클래스의 인스턴스 멤버에 접근할 수 없다. 하지만, 상수인 경우에는 가능하다.

public class InnerClassEx1 {

  public static void main(String[] args) {
    System.out.println(InstanceInner.CONST);
    System.out.println(StaticInner.cv);
  }

  class InstanceInner {
    int iv = 100;
    //static int cv = 100; // static 변수를 선언 불가
    final static int CONST = 100;   // 상수이기에 선언 가능
  }

  static class StaticInner {
    int iv = 100;
    static int cv = 200; // static클래스만 static변수 선언이 가능
  }

  void myMethod() {
    class LocalInner {
      int iv = 300;
      // static int cv = 300; // static 변수선언 불가
      final static int CONST = 300; // 상수이기에 가능
    }
  }

}

익명 클래스

익명 클래스는 클래스 이름이 존재하지 않는 내부 클래스다. (자바스크립트의 익명 함수로 생각해도 된다)
익명 클래스는 이름이 없기 때문에 생성자를 가질 수 없으며, 가질 필요도 없다.
단 하나의 객체만을 생성하는 일회용 클래스이며, 클래스의 선언과 동시에 객체를 생성한다.

만일 어느 메소드에서 부모 클래스의 자원을 상속받아 재정의하여 사용할 자식 클래스가 한번만 사용되고 버려질 자료형이면, 굳이 상단에 클래스를 정의하기보다는, 지역 변수처럼 익명 클래스로 정의하고 스택이 끝나면 삭제되도록 하는 것이 유지보수면에서나 프로그램 메모리면에서나 이점을 얻을 수 있다.

익명 클래스는 전혀 새로운 클래스를 익명으로 사용하는 것이 아니라, 이미 정의되어 있는 클래스의 멤버들을 재정의 하여 사용할 필요가 있을때 그리고 그것이 일회성으로 이용될때 사용하는 기법이다.

즉, 익명 클래스는 부모 클래스의 자원을 일회성으로 재정의하여 사용하기 위한 용도 인 것이다.
일회성 오버라이딩 용!

// 부모 클래스
class Animal {
    public String bark() {
        return "동물이 웁니다";
    }
}

public class Main {
    public static void main(String[] args) {
        // 익명 클래스 : 클래스 정의와 객체화를 동시에. 일회성으로 사용
        Animal dog = new Animal() {
        	@Override
            public String bark() {
                return "개가 짖습니다";
            }
        }; // 단 익명 클래스는 끝에 세미콜론을 반드시 붙여 주어야 한다.
        	
        // 익명 클래스 객체 사용
        dog.bark();
    }
}

다만 주의해야 할 점이 있다.

기존의 부모 클래스를 상속한 자식 클래스에서는 부모 클래스의 메서드를 재정의 할뿐만 아니라 새로운 메소드를 만들어 사용할수 도 있다는 점은 다들 알고 있을 것이다.

하지만 익명 클래스 방식으로 선언한다면 오버라이딩 한 메소드 사용만 가능하고, 새로 정의한 메소드는 외부에서 사용이 불가능 하다.

// 부모 클래스
class Animal {
    public String bark() {
        return "동물이 웁니다";
    }
}

public class Main {
    public static void main(String[] args) {
        Animal dog = new Animal() {
            // @Override 메소드
            public String bark() {
                return "개가 짖습니다";
            }
            
            // 새로 정의한 메소드
            public String run() {
                return "달리기 ㄱㄱ싱";
            }
        };
        
        dog.bark();
        dog.run(); // ! Error - 외부에서 호출 불가능
    }
}

그 이유는 new Animal() {} 를 통해서 생성하는 인스턴스는 별도의 클래스가 아닌 Animal 클래스를 상속받는 익명 클래스이기 때문에, 부모인 Animal 클래스 자체에는 run() 메서드가 선언되어 있지 않기 때문에 사용하지 못하는 것이다. (다형성의 법칙을 따른다)

그러므로 새로 정의한 메소드는 외부 스코프에서 호출할 수 없고, 익명 클래스 내에서만 호출이 가능하다.

익명 클래스도 내부 클래스의 일종이기 때문에, 외부의 지역 변수를 이용하려고 할때 똑같이 내부 클래스의 제약을 받게 된다. 따라서 내부 클래스에서 가져올 수 있는 외부 변수는 final 상수인 것만 가져와 사용할 수 있다.

익명클래스 선언 위치

다음과 같이 사용할 수 있다.

  1. 클래스 필드로 이용

특정 클래스 내부에서 여러 메소드에서 이용될때 고려해볼 만 하다.

class Animal { ... }

class Creature {
    // 필드에 익명자식 객체를 생성 하여 이용
    Animal dog = new Animal() {
        public String bark() {
            return "멍멍";
        }
    };

    public void method() {
        dog.bark();
    }
    
    public void method2() {
        dog.bark();
    }
}
  1. 지역 변수로서 이용

메소드에서 일회용으로 사용하고 버려질 클래스라면 적당하다

class Animal { ... }

class Creature {
	// ...
    
    public void method() {
    	// 지역 변수같이 클래스를 선언하여 일회용으로 사용
        Animal dog = new Animal() {
            public String bark() {
                return "멍멍";
            }
        };
        dog.bark();
    }
}
  1. 메소드 아규먼트로 이용

만일 메소드 매개변수로서 클래스 자료형이 이용된다고 할때 일회성으로만 사용한다면 아규먼트로 익명 객체를 넘겨주면 된다.

class Animal { ... }

class Creature {
	// ...
    
    public void method(Animal dog) { // 익명 객체 매개변수로 받아 사용
        dog.bark();
    }
}

public class Main {
    public static void main(String[] args) {
        Creature monster = new Creature();
        
        // 메소드 아규먼트에 익명 클래스 자체를 입력값으로 할당
        monster.method(new Animal() {
            public String bark() {
                return "멍멍";
            }
        });
    }
}

익명클래스 컴파일

java 파일은 javac를 통해 .class 파일을 만든다.

.java 파일이 .class 파일과 1:1관계를 맺지만 익명객체(익명클래스)가 사용된 .java 파일을 컴파일 하게되면 .class 파일 뿐만 아니라 $.class를 떨구게 된다.

➡ 만약 A.java라는 파일에서 익명객체를 정의했다면 A.class, A$1.class 이렇게 2개 클래스 파일이 생긴다.
➡ 만약 익명객체를 2개 A.java 파일에 정의했다면 A.class, A$1.class, A$2.class 이렇게 3개 클래스 파일이 생기게 된다.

익명 객체 끼리는 아무리 내용이 똑같다고 하더라도 전혀 서로 다른 객체이기 때문에 별개로 취급되기 때문이다.

인터페이스 익명 구현 객체

지금까지 익명 클래스 사용 방법을 배웠지만, 사실 실무에서 멀쩡한 클래스 놔두고 익명 클래스로 사용하는 일은 거의 없다.

익명 클래스는 일회성 오버라이딩 용 이라고 했었다.

이러한 특징과 잘 맞물려 추상화 구조인 인터페이스를 일회용으로 구현하여 사용할 필요가 있을때, 익명 구현 객체로 선언해서 사용하면 매우 시너지가 잘 맞게 된다.

// 인터페이스
interface IAnimal {
    public String bark(); // 추상 메소드
    public String run();
}

public class Main {
    public static void main(String[] args) {
        // 인터페이스 익명 구현 객체 생성
        IAnimal dog = new IAnimal() {
            @Override
            public String bark() {
                return "개가 짖습니다";
            }
            
            @Override
            public String run() {
                return "개가 달립니다";
            }
        };
        
        // 인터페이스 구현 객체 사용
        dog.bark();
        dog.run();
    }
}

위의 코드 모습을 보면, 마치 인터페이스를 클래스 생성자 처럼 초기화하여 인스턴스화 한 것 같아 보인다.
하지만 알다시피 인터페이스 자체로는 객체를 만들수는 없다.
위의 코드에서 new 인터페이스명() 은 그렇게 보일 뿐이지, 사실 자식 클래스를 생성해서 implements 하고 클래스 초기화 한 것과 다름이 없다.
그냥 익명클래스를 작성함과 동시에 객체를 생성하도록하는 Java의 문법으로 보면 된다.

원래는 클래스가 인터페이스를 구현한 후 인터페이스를 구현한 클래스로 객체를 만들어야하는데, 위의 코드는 인터페이스를 바로구현해서 구현한 클래스명이 없이 객체를 만들기 때문에 이를 익명 구현 객체라고 부른다.

일반 상속 익명 객체와 다른 점은 상속과 다르게 인터페이스는 강제적으로 메소드 정의를 통해 사용해야하는 규약이 있기 때문에 규격화에 도움이 된다.

익명 구현 객체 예제

import java.util.Arrays;
import java.util.Comparator; // Comparator 인터페이스를 불러온다
 
public class Main {
    public static void main(String[] args) {
 
        class User {
            String name;
            int age;
 
            User(String name, int age) {
                this.name = name;
                this.age = age;
            }
        }
 
        User[] users = {
            new User("홍길동", 32),
            new User("김춘추", 64),
            new User("임꺽정", 48),
            new User("박혁거세", 14),
        };
 
		// Arrays.sort(배열, Comparator 익명 구현 객체);
        Arrays.sort(users, new Comparator<User>() {
            @Override
            public int compare(User u1, User u2) {
                return Integer.compare(u1.age, u2.age); // Integer 클래스에 정의된 compare 함수로 두 가격 정수 원시값을 비교
            }
        });
 
        // 출력
        for (User u : users) { 
            System.out.println(u.name + " " + u.age + "세");
        }
 
    }
}
박혁거세 14세
홍길동 32세
임꺽정 48세
김춘추 64세

Comparator 인터페이스로 익명 구현 객체를 만들어 Arrays.sort() 메서드의 아규먼트로 보내어, 객체 배열 users 를 나이순으로 정렬하는 코드 이다.


하지만 이런 익명 구현 객체는 오로지 하나의 인터페이스만 구현하여 객체를 생성할 수 있다는 한계가 있다.
둘 이상의 인터페이스를 갖거나, 하나의 클래스를 상속 받고 동시에 인터페이스를 구현하는 형태가 불가능하다는 것이다.

따라서 어쩔수 없이 일회용 용도일지라도 다중 구현한 클래스는 따로 정의하여 사용해야 한다.

interface IAnimal {
}

interface ICreature {
}

abstract class myClass {
}

public class Main {
    public static void main(String[] args) {
    
    	// 인터페이스 두개를 구현한 일회용 클래스 (일회용 이라도 어쩔수 없이 따로 선언)
        class useClass1 implements IAnimal, ICreature {
        }

        // 클래스와 인터페이스를 상속, 구현한 일회용 클래스 (일회용 이라도 어쩔수 없이 따로 선언)
        class useClass2 extends myClass implements IAnimal {
        }

        useClass1 u1 = new useClass1() {
        };

        useClass2 u2 = new useClass2() {
        };
    }
}

익명 객체와 람다 표현식

익명 클래스 기법은 보다 길다랗고 복잡한 자바 문법을 간결하게 하는 것에 초점을 둔다. 그래서 java8의 람다식 문법과 매우 잘 어울리며, 실제로 이 둘은 같이 정말 많이 쓰인다.

Operate operate = new Operate() {
    public int operate(int a, int b) {
        return a + b;
    }
};

// 람다식으로 줄이기
Operate operate = (a, b) -> {
    return a + b;
};

// 더 짧게 줄이기 (리턴 코드만 있다면 생략이 가능)
Operate operate = (a, b) -> a + b;

이런 람다식 표현의 익명 구현 객체도 제약이 있는데,

  • 인터페이스로만 만들 수 있다.
  • 하나의 추상 메소드만 선언되어 있는 인터페이스만 가능하다. (단, default 메소드는 추상메소드가 아니기 땜에 제외)

참고자료: java의 정석<남궁성>, https://inpa.tistory.com/entry//JAVA-☕-내부-클래스Inner-Class

0개의 댓글