Study/Java

[Java]인터페이스(Interface)와 중첩 클래스

코챱 2022. 2. 26. 00:41

인터페이스를 설명하기에 앞서 final 키워드를 얘기하자면, final 키워드가 붙은 멤버는 변경이 불가능해진다. 필드는 리터럴 데이터(상수)로, 메서드는 오버라이딩 불가 상태로, 클래스는 상속 불가 상태로 바뀐다. 그리고 final 키워드를 변수 앞에 붙여 상수로 만들 수 있다. 이때 static을 함께 붙여서 상수형 변수라고 부르고, 역시 값을 변경할 수 없다. (static final 변수명) 참고로 상수명은 주로 대문자와 언더스코어(_)로만 작성한다.

 

인터페이스(Interface)

인터페이스는 객체와 개발자를 연결시켜주는 중간다리 역할을 한다. 클래스와 비슷하지만 클래스가 아니며, 멤버로 상수(상수형 변수)와 추상메서드만 가질 수 있다. 추상메서드가 포함되어 있고, 인터페이스 타입으로 인스턴스 생성은 불가능하나 업캐스팅을 통한 다형성은 가능하기 때문에 추상클래스와 비슷하다. 하지만 변수와 일반메서드도 가질 수 있는 추상클래스보다는 범위가 좁다.

또한 인터페이스는 구현(implements)한다고 표현하는데, 인터페이스 - 클래스는 구현, 인터페이스 - 인터페이스는 상속이다. 자식 인터페이스도 물론 상수와 추상메서드 외에는 가질 수 없고, 모든 구현은 구현 클래스에서 해야 한다. 상속과 다른 점이라면 상속은 단일 상속(하나의 슈퍼클래스)만 가능하지만 인터페이스는 다중 구현이 가능하다. 구현 클래스에서 상속도 같이 받는 경우에는 extends -> implements 순으로 작성한다.

interface ExInterface1 {
	public static final int NUM1 = 10;    //상수형 변수

	public abstract void method1();    //추상메서드
}

interface ExInterface2 {
	public int NUM2 = 20;    //상수로 취급됨

	public void method2();    //추상메서드로 취급됨
}

class SubClass implements ExInterface1, ExInterface2 {    //다중 구현
	public void constant() {
		System.out.println("NUM1 : " + NUM1 + ", NUM2 : " + NUM2);
	}
	@Override
	public void method1() {
		System.out.println("method1() 호출");
	}
	@Override
	public void method2() {
		System.out.println("method2() 호출");
	}
}



public class Main {
	public static void main(String[] args) {

		ExInterface1 ex1 = new SubClass();
		ExInterface2 ex2 = new SubClass();    //다형성
		SubClass sc = new SubClass();

		ex1.method1();    //ExInterface1 멤버만 가능
		ex2.method2();    //ExInterface2 멤버만 가능
		sc.constant();    //셋 다 가능
	}
}

 

인터페이스를 쓰는 이유에는 여러 가지가 있다. 먼저 추상메서드를 클래스에서 무조건 구현해야 하기 때문에, 실수로 구현을 빠뜨릴 위험이 없다. 그리고 다형성 덕분에 모듈(객체)을 교체/추가할 경우 별도의 코드 수정이 필요 없으며, 상속시킬 수 없는 클래스끼리 관계를 형성할 수 있다. 더불어 각 모듈(객체)의 독립적인 프로그래밍이 가능하여 개발 시간 역시 단축된다.

 

아래 코드는 일반 클래스와 구현 클래스의 차이를 나타낸 것이다. 일반 클래스들에 이름이 같은 메서드가 존재할 때, 반복문으로 그 메서드들을 모두 호출할 경우 코드가 복잡해진다. 그래서 인터페이스 구현을 통해 다형성을 이용하면 코드가 훨씬 단순해진다.

interface Chargeable {
	public abstract void charge();
}

//일반 클래스
class Phone {
	public void charge() {    //오버라이딩 메서드가 아닌 일반 메서드
		System.out.println("phone 충전");
	}
}
class Camera {
	public void charge() {
		System.out.println("camera 충전");
	}
}

//구현 클래스
class Phone2 implements Chargeable {
	@Override
	public void charge() {
		System.out.println("phone2 충전");
	}
}
class Camera2 implements Chargeable {
	@Override
	public void charge() {
		System.out.println("camera2 충전");
	}
}



public class Main {

	//일반 클래스 요소 출력
	public void badCase() {
		Object[] obj = {new Phone(), new Camera()};
		
		for(int i=0; i<obj.length; i++) {
			Object o = obj[i];
			
			if(o instanceof Phone) {
				Phone p = (Phone)o;
				p.charge();
			} else if(o instanceof Camera) {
				Camera c = (Camera)o;
				c.charge();
			}
		}
	}

	//구현 클래스 요소 출력
	public void goodCase() {
		Chargeable[] obj = {new Phone2(), new Camera2()};
		
		for(int i=0; i<obj.length; i++) {
			obj[i].charge();
		}
	}
}

 

추상클래스와 인터페이스 둘 다 추상메서드가 있고 서브(구현)클래스에서 오버라이딩해야 한다는 점은 똑같다. 다른 점은, 클래스를 보통 설계도로 비유하는데 일부만 구현된 미완성 설계도가 추상클래스, 틀만 잡아놓은 기초 설계도가 인터페이스라고 보면 된다.

예를 들면 전원 기능은 모든 Tv가 공통으로 가지고 있다. 전원을 켜고 끄는 방법이 똑같기 때문에 굳이 추상메서드일 필요가 없으니, 일반메서드도 포함하는 추상클래스가 필요하다. 하지만 프린터의 경우, 레이저 프린터나 잉크 프린터 등 방식만 다른 공통 기능을 사용하는 것이다. 이럴 때 프린트 추상메서드만 가지는 인터페이스를 적용할 수 있다.

 

중첩 클래스(Nested Class)

중첩 클래스는 클래스 내에 정의된 클래스를 의미한다. 멤버 내부클래스와 정적 내부클래스, 로컬 클래스로 나뉘는데, 멤버 내부클래스는 일반 멤버와, static은 static 멤버와 같은 레벨이다. 로컬 클래스는 클래스의 생성자 또는 메서드 내에 만들어지는 클래스라서 그 생성자(메서드)와 같이 로딩된다. 로컬 클래스는 로컬 변수와 같은 레벨이기 때문에 해당 생성자(메서드) 외부에서는 접근할 수 없다.

class Outer {
	private int num = 10;

	public void method() {
		System.out.println(num);

		//로컬 클래스
		class LocalInner {
			int num2 = 20;
		}

		LocalInner local = new LocalInner();
		System.out.println(local.num2);
	}

	//멤버 내부클래스
	class Inner {
		public void innderMethod() {
			System.out.println(num);
			method();
		}
	}

	//정적 내부클래스
	static class StaticInner {
		static int num3 = 30;

		public void staticInnerMethod() {
			System.out.println(num3);
		}
	}
}



public class Main {
	public static void main(String[] args) {

		Outer outer = new Outer();
		outer.method();    //해당 메서드 내 로컬클래스도 같이 호출

		Outer.Inner inner = new Outer().new Inner();    //멤버 내부클래스의 인스턴스 생성
		inner.innerMethod();

		System.out.println(Outer.StaticInner.name);    //static 내부클래스 내의 static 변수

		Outer.StaticInner staticInner = new Outer.StaticInner();
		staticInner.staticInnerMethod();    //static 클래스에 있어도 일반메서드이기 때문에 인스턴스로 호출
	}
}

 

중첩클래스는 파일 형태로 존재하기 때문에, workspace 폴더 - 해당 프로젝트 - bin 에 들어가보면 멤버와 정적 내부클래스는 '외부클래스명$내부클래스명.class' 로, 로컬 클래스는 '외부클래스명$1내부클래스명.class' 로 생성되어 있다. 참고로 class 파일은 코드를 컴파일해야 만들어지는 실행 파일이다.

 

아래는 이름이 똑같은 일반 필드, 멤버 내부클래스의 필드, 내부클래스 내의 메서드 속 인스턴스 변수를 해당 메서드 내에서 호출할 경우의 코드다. 내부에서 외부로 접근하는 것은 상속 개념이 아니기 때문에 super를 사용할 수 없다.

더불어 클래스 내에 정의하는 중첩 인터페이스도 있는데, 단순히 클래스를 인터페이스로 바꾼 것이라고 보면 된다.

class Outer {
	int num = 10;

	class Inner {    //멤버 내부클래스
		int num = 20;

		public void method() {
			int num = 30;

			System.out.println(Outer.this.num);   //10
			System.out.println(this.num);         //20
			System.out.println(num);              //30
		}
	}


	interface InnerInterface { }    //중첩 인터페이스

}