📌제어자
제어자는 클래스, 변수 또는 메서드의 선언부에 함께 사용되어 부가적인 의미를 부여한다. 제어자의 종류는 다음과 같다.
접근 제어자 : public, protected, default, private
그 외 : static, final, abstract, native, transient, synchronized, volatile, strictfp
📌접근 제어자
접근 제어자는 클래스의 멤버(변수와 메소드)들의 접근 권한을 지정한다.
- public : 아무대서나 쓸때
- private : 같은클래스 안에서만 쓸때
- default : 같은 패키지에서 쓸때 (접근 제어 지시자를 명시하지 않았을 때 자동적으로 패키지 멤버가 된다. default)
- protected : 같은 패키지내에서, 그리고 다른 패키지의 자손클래스에서 접근이 가능하다.
제어자 | 같은 클래스 | 같은 패키지 | 자손 클래스 | 전 체 |
public | O | O | O | O |
protected | O | O | O | |
(default) | O | O | ||
private | O |
cf) public class는 클래스명과 java파일명이 같아야하므로 java파일내 하나만 존재해야 한다.
접근 제어자를 이용한 캡슐화
클래스나 멤버, 주로 멤버에 접근 제어자를 사용하는 이유는 클래스의 내부에 선언된 데이터를 외부로 부터 보호하기 위해서이다. 따라서 데이터가 유효한 값을 유지하도록 또는 함부로 변경하지 못하게 하기위해서 외부로부터의 접근을 제한하는 것이 필요하다. 그것이 바로 객체지향개념의 캡슐화에 해당하는 내용이다.
생성자의 접근 제어자
보통 생성자의 접근 제어자는 클래스의 접근 제어자와 같지만, 다르게 지정할 수도 있다. 생성자의 접근 제어자를 private로 지정하면, 외부에서 생성자에 접근할 수 없으므로 인스터스를 생성할 수 없게 된다. 그래도 클래스 내부에서는 인스턴스를 생성할 수 있다. 대신 인스턴스를 생성해서 반환해주는 public메서드를 제공함으로써 외부에서 이 클래스의 인스턴스를 사용하도록 할 수 있다. 이 메서드는 public인 동시에 static이어야 한다. (생성자가 private인 클래스는 인스턴스를 만들지 못함으로 static으로 선언하여 메모리 상으로 올라가있어야 인스턴스 생성 메소드를 호출할 수 있기 때문이다.)
이처럼 생성자를 통해 직접 인스턴스를 생성하지 못하게 하고 public메서드를 통해 인스턴스에 접근하게 함으로써 사용할 수 있는 인스턴스의 개수를 제한할 수 있다.
또한, 생성자가 private인 클래스는 다른 클래스의 조상이 될 수 없다. 왜냐하면 자손 클래스의 인스턴스를 생성할 때 조상클래스의 생성자를 호출해야 하는데, 생성자의 접근 제어자가 private이므로 자손클래스에서 호출하는 것이 불가능하기 때문이다. 그래스 그 앞에 final을 더 추가하여 상속할 수 없는 클래스라는 것을 알리는 것이 좋다.
final에 대한 설명은 바로 전 포스팅을 확인하면 된다.
final class Singleton{
//getInstance()에서 사용될 수 있도록 인스터가 미리 생성되어야 하므로 static이어야 한다.
private static Singleton s = new Singleton();
private Singleton(){
//...
}
//인스터를 생성하지 않고도 호출할 수 있어야 하므로 static이어야 한다.
public static Singleton getInstance(){
if(s==null)
s = new Singleton();
return s;
}
}
class SingletonTest{
public static void main(String[] args){
//Singleton s = new Singleton(); //에러! Singleton() has private access in Singleton
Singleton s = Singleton.getInstance();
}
}
📌abstract (추상)
추상 메소드(Abstract Method)를 간단하게 설명하면 선언부는 있는데 구현부가 없는 메소드를 말한다. 추상 메소드를 하나라도 갖고 있는 클래스는 반드시 추상 클래스(Abstract Class)로 선언해야 한다. 물론 추상 메소드 없이도 추상 클래스를 선언할 수 있는 있다.
- abstract은 미완성의 의미를 갖고있으며 상속을 강제하는 일종의 규제이다. 즉, abstract클라스나 abstract메소드를 사용하기 위해서는 반드시 상속해서 사용하도록 강제하는 것이 abstract이다.
제어자 | 대상 | 의미 |
abstract | 클래스 | 클래스 내에 추상 메서드가 선언되어 있음을 의미한다. |
메서드 | 선언부만 작성하고 구현부는 작성하지 않은 추상 메서드임을 알린다. |
abstract class A{
public abstract int b(); //본체가 있는 메소드는 abstract 키워드를 가질 수 없다.
//이런 형태가 안된다.->public abstract int c(){System.out.println("Hello")}
//추상 클래스 내에는 추상 메소드가 아닌 메소드가 존재 할 수 있다.
public void d(){
System.out.println("world");
}
}
class B extends A{
public int b() {return 1;} //이 처럼 abstract는 상속을 강제하고있다.
}
public class Abstract {
public static void main(String[] args) {
//A obj = new A(); //abstract클래스이기 때문에 상속해서 사용해야한다.
B obj = new B();
System.out.println(obj.b());
}
}
추상 메소드는 본체가 없으므로 항상 세미콜론(;)으로 종료되어야 한다.
abstract 클래스를 먼저 정의했다기 보다는 b라는 추상(abstract)메소드를 갖고 있으면 그 해당 클래스는 자동으로 추상(abstract)클래스가 된다.
추상 클래스를 사용하는 이유
추상 클래스는 위에서 설명한거 처럼 상속을 강제하기 위한 것이다. 즉, 부모 클래스에서는 메소드의 시그니처만 정의해놓고 메소드의 실제 동작 방법은 하위클래스에 위임하는 것이다.
📓제어자 조합 (중간 정리)
- 메서드에 static과 abstract를 함께 사용할 수 없다.
static메서드는 몸통이 있는 메서드에만 사용할 수 있기 때문이다. - 클래스에 abstract와 final을 동시에 사용할 수 없다.
클래스에 사용되는 final은 클래스를 확장할 수 없다는 의미이고 abstract는 상속을 통해서 완성되어야 한다는 의미이므로 서로 모순되기 때문이다. - abstract메서드의 접근 제어자가 private일 수 없다.
abstract메서드는 자손클래스에서 구현해주어야 하는데 접근 제어자가 private이면, 자손 클래스에서 접근할 수 없기 때문이다. - 메서드에 private과 final을 같이 사용할 필요는 없다.
접근 제어자가 private인 메서드는 오버라이딩될 수 없기 때문이다. 이 둘 중 하나만 사용해도 의미가 충분하다.
📌인터페이스(interface)
💡인터페이스란?
인터페이스(interface)는 추상 클래스의 극단적인 경우이다. interface는 추상 메소드들로만 이루어져있다!!
어떤 객체가 특정한 인터페이스를 사용한다면, 그 객체는 반드시 인터페이스의 메소드들을 구현해야한다. 만약 인터페이스에서 강제하고 있는 메소드를 구현하지 않으면 컴파일 조차 되지 않는다. 즉, 인터페이스는 하위 클래스에 인터페이스의 메소드가 반드시 존재하도록 강제한다.
💡인터페이스 조건
인터페이스(interface)는 public 추상 메소드와 public 정적 상수만 가질 수 있다. 인터페이스는 추상 메소드와 정적 상수만 가질 수 있기 때문에 따로 메소드에 public과 abstract, 속성에 public과 static과 final을 붙이지 않아도 자동으로 Java가 알아서 붙여준다.
- 모든 멤버변수는 public static final 이어야 하며, 이를 생략할 수 있다.
- 모든 메서드는 public abstract이어야 하며, 이를 생략할 수 있다.
단.static메서드와 디폴트 메서드는 예외(JDK1.8 부터)
사용 방식
interface I{
public void z(); //interface안에서 선언되는 메소드들은 모두 묵시적으로 public, abstract이므로
//public이나 abstract 수식어는 안 써도 된다.
//하지만 구현할 때는 public을 붙여서 써라!!!
}
class A implements I{ //implements는 A라는 클래스가 인터페이스 I를 구현하고 있다는 의미이다.
public void z(){} //여기서 인터페이스 I의 멤버인 public void z() 메소드를 구현하고
//A는 인터페이스 I의 메소드를 전부 구현해야한다.
}
클래스 선언할 때는 class 사용하지만 인터페이스는 interface를 사용한다.
상속은 extends를 사용하지만 인터페이스는 implements를 사용한다.
💡인터페이스의 특징
1. 하나의 클래스가 여러 개의 인터페이스를 구현할 수 있다.
interface I1{
public void x();
}
interface I2{
public void y();
}
class A implements I1, I2{
public void x(){}
public void y(){}
}
2.인터페이스도 상속이 된다. (인터페이스는 인터페이스로부터만 상속받을 수 있다. + 다중 상속도 가능하다.)
interface I1{
public void x();
}
interface I2 extends I1{
public void y();
}
class A implements I2{
public void x(){}
public void y(){}
}
3. 인터페이스의 멤버는 반드시 public 이다.
- 인터페이스는 그 인터페이스를 구현한 클래스를 어떻게 조작할 것인가를 규정한다. 즉, 외부에서 제어할 수 있는 public만을 허용한다.
- 위에서 설명했듯이 interface는 public만을 허용하고 abstract만을 사용하므로 수식어를 생략하면 자동적으로 public, abstract가 된다.
4. 인터페이스는 클래스가 아니므로 new 연산자를 사용할 수 없다.
💡인터페이스(interface) vs 추상클래스(abstract class)
인터페이스 | 클래스가 아니다 | 미완성된 채로 남겨진 추상 클래스 |
추상 클래스 | 부분적으로 미완성된(추상 메소드) 클래스 | 일반적으로 필드나 메소드를 가질 수 있다. |
💡인터페이스의 필요성
1. 인터페이스를 디자인하면, 클래스들간의 통합에 대하여 걱정할 필요없이 각 클래스들은 별도로 병렬적으로 작성될 수 있다. 이 말 뜻은 따로 구현한다해도 인터페이스라는 규격이 있기때문에 서로 호환이 가능해진다는 말이다.
2. Java는 다중 상속을 지원하지 않기 때문에 interface를 이용하면 사용할 수 있다.
// JAVA는 다중 상속이 안된다 - 컴파일 오류
//예시
public class Sub extends Super1, Super2{
...
}
// interface를 이용한 다중 상속
//예시
public class Sub extends Super implements Interface1, Interface2 {
...
}
3. interface를 이용하여 여러 클래스에서 사용되는 상수들을 정의할 수 있다.
interface Days{
public static final int MONDAY=1, TUESDAY=2,WEDNESDAY=3,
THURSDAY=4,FRIDAY=5,SATURDAY=6,SUNDAY=7;
}
public class Day implements Days{
public static void main(String[] args){
System.out.println("일요일: "+SUNDAY);
}
}
인터페이스 장점
- 개발시간을 단축시킬 수 있다.
- 표준화가 가능하다.
- 서로 관계없는 클래스들에게 관계를 맺어 줄 수 있다.
- 독립적인 프로그래밍이 가능하다.
📌다형성
다형성이란 '여러 가지 형태를 가질 수 있는 능력'을 의미한다. Java에서는 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도록 함으로써 다형성을 프로그램적으로 구현하였다. 이 말은 조상클래스 타입의 참조변수로 자손클래스의 인스턴스를 참조할 수 있도록 하였다는 것이다.
class A{
public String x(){return "x";}
}
class B extends A{
public String y(){return "y";}
}
public class PolymorphismDemo1 {
public static void main(String[] args) {
A obj = new B();
obj.x(); //코드가 실행된다.
obj.y(); //코드가 실행되지 않는다.
}
}
class A{
public String x(){return "A.x";}
}
class B extends A{
public String x(){return "B.x";}
public String y(){return "y";}
}
public class PolymorphismDemo1 {
public static void main(String[] args) {
A obj = new B();
System.out.println(obj.x());
}
}
// 실행결과
// B.x
// 설명
// 클래스 B를 클래스 A의 데이터 타입으로 인스턴스화 했을 때
// 클래스 A에 존재하는 멤버만이 클래스 B의 멤버가 된다.
// => 클래스 B가 클래스 A화 되었으므로 B의 메소드 y는 실행 안된다.
// => 메소드 x를 실행하면 클래스 A에서 정의된 메소드가 아니라 클래스 B에서 정의된 메소드가 실행
//(클래스 B의 기본적인 성질은 그대로 간직하고 있다.->오버라이딩)
//즉, 클래스 A에 존재하는 멤버만이 클래스 B의 멤버가 된다.
지금까지 인스턴스의 타입과 참조변수의 타입이 일치하는 것이 보통이었지만, 위 예시와 같이 A와 B 클래스가 서로 상속관계에 있을 경우, 조상클래스 타입의 참조변수로 자손 클래스의 인스턴스를 참조하도록 하는 것도 가능하다.
B obj = new B( ); 이 두 개는 무슨 차이 일까?
A obj = new B( );
이렇게 되면 실제 인스턴스가 B타입이라 할지라도, 참조 변수 obj로는 B인스턴스의 모든 멤버를 사용할 수 없다.
A타입의 참조변수로는 B인스턴스 중에서 A클래스의 멤버들(상속받은 멤버포함)만 사용할 수 있다. 따라서, 생성된 B인스턴스의 멤버중에서 A클래스에 정의 되지 않은 멤버, y()는 참조변수 obj로 사용이 불가능하다.
즉, 둘 다 같은 타입의 인스턴스지만 참조변수의 타입에 따라 사용할 수 있는 멤버의 개수가 달라진다.
이러한 특성으로
조상타입의 참조변수로 자손타입의 인스턴스를 참조할 수 있지만,
반대로 자손타입의 참조변수로 조상타입의 인스턴스를 참조할 수 없다.
(참조변수가 사용할 수 있는 멤버의 개수가 인스턴스의 멤버 개수보다 같거나 적어야 하기 때문 )
💡참조변수의 형변환
기본형 변수와 같이 참조변수도 형변환이 가능하다. 단. 서로 상속관계에 있는 클래스 사이에서만 가능하기 때문에
자손타입의 참조변수 → 조상타입의 참조변수
조상타입의 참조변수 → 자손타입의 참조변수 로의 형변환만 가능하다.
이 때 기본형 변수의 형변환에서 작은 자료형에서 큰 자료형의 형변환은 생략이 가능하듯이,
자손 → 조상 형변환하는 경우에 형변환을 생략할 수 있다.
class Car{
////
}
class FireEngine extends Car{
////
}
class Ambulance extends Car{
////
}
//ex1)
FireEngine f;
Ambulance a;
a=(Ambulance)f; //에러 - 상속관계가 아닌 클래스간의 형변화 불가
f=(fireEngine)a; //에러 - 상속 관계가 아닌 클래스간의 형변환 불가
//ex2)
Car car = null;
FireEngine fe = new FireEngine();
FireEngine fe2 = null;
car = fe; //car = (Car)fe; 에서 형변환 생략된다. 자손->조상
fe2 = (FireEngine)car; //형변환을 생략불가능하다. 조상->자손
즉, 자손타입으로의 형변환은 생략할 수 없으며, 형변환을 수행하기 전에 instanceof연산자를 이용해 참조변수가 참조하고 있는 실제 인스턴스의 타입을 확인하는 것이 안전하다.
정리
형변환은 참조변수의 타입을 변환하는 것이지 인스턴스를 변환하는 것은 아니기 때문에 참조변수의 형변환은 인스턴스에 아무런 영향을 미치지 않는다. 단지 참조변수의 형변환을 통해서, 참조하고 있는 인스턴스에서 사용할 수 있는 멤버의 범위(개수)를 조절하는 것 뿐이다.
💡instanceof연산자
참조변수가 참조하고 있는 인스턴스의 실제 타입을 알아보기 위해 instanceof연산자를 사용한다.
주로 조건문에 사용되며, instanceof의 왼쪽에는 참조변수를 오른쪽에는 타입(클래스명)이 피연산자로 위치한다. 그리고 연산의 결과로 boolean값인 true와 false 중의 하나를 반환한다.
이 때 true를 얻었다는 것은 참조변수가 검사한 타입으로 형변환이 가능하다는 것을 뜻한다.
class InstanceofTest{
public static void main(String args[]){
FireEngine fe = new FireEngine();
if(fe instanceof FireEngine){
System.out.println("This is a FireEngine instance.");
}
if(fe instanceof Car){
System.out.println("This is a Car instance.");
}
if(fe instanceof Object){
System.out.println("This is an Object instance.");
}
System.out.println(fe.getClass().getName()); //클래스의 이름을 출력
}
} //class
class Car{}
class FireEngine extends Car {}
/*실행결과
This is a FireEngine instance.
This is a Car instance.
This is an Object instance.
FireEngine
*/
매개변수의 다형성
Tv, Computer, Audio 등 제품의 종류가 늘어날 때마다 Buyer클래스에는 새로운 buy 메서드를 추가해 주어야 할 것이다.
그러나 메서드의 매개변수에 다형성을 적용하여 void buy(Product p)로 하면 하나의 메서드로 간단히 처리할 수 있다.
여러 종류의 객체를 배열로 다루기
조상타입의 배열에 자손들의 객체를 담을 수 있다.
※ Vector 클래스
매서드/생성자 | 설명 |
Vector( ) | 10개의 객체를 저장할 수 있는 Vector인스턴스를 생성한다. 10개 이상의 인스턴스가 저장되면, 자동적으로 크기가 증가한다. |
boolean add(Object o) | Vector에 객체를 추가한다. 추가에 성공하면 결과값으로 true, 실패하면 false를 반환한다. |
boolean remove(Object o) | Vector에 저장되어 있는 객체를 제거한다. 제거에 성공하면 true, 실패하면 false를 반환한다. |
boolean isEmpty( ) | Vector가 비어있는지 검사한다. 비어있으면 true, 그렇지 않으면 false를 반환한다. |
Object get(int index) | 지정된 위치(index)의 객체를 반환한다. 반환타입이 Object타입이므로 적절한 타입으로의 형변화이 필요하다. |
int size( ) | Vector에 저장된 객체의 개수를 반환한다. |
public class Vector extends AbstractList implements List, Cloneable, java.io.Serializable{
protected Object elementData[];
...
}
인터페이스의 이해
클래스를 사용하는 쪽(User)과 클래스를 제공하는 쪽(Provider)이 있다.
메서드를 사용(호출)하는 쪽(User)에서는 사용하려는 메서드(Provider)의 선언부만 알면 된다. (내용은 몰라도 된다.)
class A{
public void methodA(B b){
b.methodB();
}
}
class B{
public void methodB(){
System.out.println("methodB()");
}
}
class InterfaceTest{
public static void main(String args[]){
A a = new A();
a.methodA(new B());
}
}
이 두 클래스는 서로 직접적인 관계에 있다. 이것은 간단히 'A-B'라고 표현하자.
이 경우 클래스 A를 작성하려면 클래스 B가 이미 작성되어 있어야 한다. 그리고 클래스 B의 methodB()의 선언부가 변경되면, 이를 사용하는 클래스 A도 변경되어야 한다. 이와 같이 직접적인 관계의 두 클래스는 한 쪽(Provider)이 변경되면 다른 한 쪽(User)도 변경되어야 한다는 단점이 있다.
클래스 A가 클래스 B를 직접호출하지 않고 인터페이스를 매개체로해서 두 클래스간의 관계를 간접적으로 변경 위 같은 단점을 개선 가능하기 떄문에, 인터페이스를 이용해서 클래스 B(Provider)의 선언과 구현을 분리해야한다.
interface I{
public abstract void methodB();
}
class B implements I{
public void methodB(){
System.out.println("methodB in B class");
}
}
class A{
public void methodA(I i){
i.methodB();
}
}
이런 식으로 클래스 A는 클래스 B대신 인터페이스 I를 사용해서 작성할 수 있다.
클래스 A를 작성하는데 있어서 클래스 B가 사용되지 않았고 클래스 A와 클래스 B는 'A-B'의 직접적인 관계에서
'A-I-B'의 간접적인 관계로 바뀌었다.
결국 클래스 A는 여전히 클래스 B의 메서드를 호출하지만, 클래스 A는 인터페이스 하고만 직접적인 관계이 있기 때문에 클래스 B의 변경에 영향을 받지 않는다. 또한 클래스 A는 인터페이스를 통해 실제로 사용하는 클래스의 이름을 몰라도 되고 심지어는 실제로 구현된 클래스가 존재하지 않아도 문제되지 않는다. 클래스 A는 오직 직접적인 관계에 있는 인터페이스 I의 영향만 받는다.
이미지로 생각해보면 인터페이스 I는 실제구현 내용(클래스 B)을 감싸고 있는 껍데기이며, 클래스 A는 껍데기 안에 어떤 알맹이(클래스)가 들어 있는지 몰라도 된다.
////예제////
class InterfaceTest{
public static void main(String[] args){
A a = new A();
a.methodA();
}
}
class A{
void methodA(){
//제 3의 클래스의 메서드를 통해서 인터페이스 I를 구현한 클래스의 인스턴스를 얻어온다.
I i = InstanceManager.getInstance();
i.methodB();
System.out.println(i.toString()); //i로 Object의 메서드 호출가능
}
}
interface I{
public abstract void methodB();
}
class B implments I{
public void methodB(){
System.out.println("methodB in B class");
}
public String toString() { return " class B "; }
}
class InstanceManager{
public static I getInstance(){
return new B(); //다른 인스턴스로 바꾸려면 여기만 변경하면 된다.
}
}
여기서 인스턴스를 직접 생성하지 않고, getInstance()라는 메서드를 통해 제공받는다. 이는 위에서 한 매개변수를 통해 동적으로 제공받은 방법이 아닌 제 3의 클래스를 통해서 제공받은 방식이다.
이렇게 하면 나중에 다른 클래스의 인스턴스로 변경되어도 A클래스의 변경없이 getInstance()만 변경하면 된다는 장점이 있다.
i.toString()이 가능한 이유는 뭘까?
인터페이스 I타입의 참조변수 i로도 Object클래스에 정의된 메서드들을 호출할 수 있다. i에 toString()이 정의되어 있지는 않지만, 모든 객체는 Object클래스에 정의된 메서드를 가지고 있을 것이기 때문에 허용하는 것이다.
디폴트 메서드와 static 메서드
인터페이스에 디폴트 메서드, static메서드를 추가 가능하다.
디폴트 메서드란?
상속에서 조상클래스에 새로운 메서드를 추가하는 것은 별일이 아니다. 하지만 인터페이스의 경우에는 다르다. 인터페이스에 메서드를 추가한다는 것은 추상 메서드를 추가한다는 것이고, 이 인터페이스를 구현한 기존의 모든 클래스들이 새로 추가된 메서드를 구현해야하기 때문이다. JDK 설계자들은 이러한 방식은 개선하려 디폴트 메서드(default method)라는 것을 고안해 내었다.
디폴트 메서드는 추상 메서드의 기본적인 구현을 제공하는 메서드로, 추상 메서드가 아니기 때문에 디폴트 메서드가 새로 추가되어도 해당 인터페이스 구현한 클래스를 변경하지 않아도 된다.
디폴트 메서드는 앞에 키워드 default를 붙이며, 추상 메서드와 달리 일반 메서드처럼 몸통{ }이 있어야 한다. 디폴트 메서드 역시 접근 제어자가 public이며, 생략가능하다.
interface Myinterface{
void method();
default void newMethod() {}
}
newMethod()라는 추상 메서드를 추가하는 대신, 디폴트 메서드를 추가하면, 기존의 Myinterface를 구현한 클래스를 변경하지 않아도 된다. 즉 조상 클래스에 새로운 메서드를 추가한 것과 동일해 지는 것이다. 대신, 새로 추가된 디폴트 메서드가 기존의 메서드와 이름이 중복되어 충돌하는 경우가 발생하는 그 때 해결방안은 다음과 같다.
1. 여러 인터페이스의 디폴트 메서드 간의 충돌
: 인터페이스를 구현한 클래스에서 디폴트 메서드를 오버라이딩해야한다.
2. 디폴트 메서드와 조상 클래스의 메서드 간의 충돌
: 조상 클래스의 메서드가 상속되고, 디폴트 메서드는 무시된다.
사실 이런 방법을 몰라도 되고 그냥 필요한 쪽의 메서드와 같은 내용으로 오버라이딩 해버리면 된다.
📌내부 클래스(inner class)
내부클래스는 주로 AWT나 Swing과 같은 GUI어플리케이션의 이벤트처리 외에는 잘 사용하지 않을 정도로 사용빈도가 높지않으므로 기본원리와 특징을 이해하는 정도까지만 학습해도 충분하다.
내부 클래스란?
내부 클래스는 클래스 내에 선언된 클래스이다.
클래스에 다른 클래스를 선언하는 이유는 간단하다. 두 클래스가 서로 긴밀한 관계에 있기 때문이다.
내부클래스의 장점
-내부 클래스에서 외부 클래스의 멤버들을 쉽게 접근할 수 있다.
-코드의 복잡성을 줄일 수 있다.(캡슐화)
내부 클래스의 종류와 특징
내부 클래스 | 특징 |
인스턴스 클래스 | 외부 클래스의 멤버변수 선언위치에 선언하며, 외부 클래스의 인스턴스멤버처럼 다루어진다. 주로 외부 클래스의 인스턴스 멤버들과 관련된 작업에 사용될 목적으로 선언된다. |
스태틱 클래스 | 외부 클래스의 멤버변수 언언위치에 선언하며, 외부 클래스의 static멤버처럼 다루어진다.주로 외부 클래스의 static멤버, 특히 static메서드에서 사용될 목적으로 선언된다. |
지역 클래스 | 외부 클래스의 메서드나 초기화블럭 안에 선언하며, 선언된 영역 내부에서만 사용될 수 있다. |
익명 클래스 | 클래스의 선언과 객체의 생성을 동시에 하는 이름없는 클래스 (일회용) |
내부 클래스의 제어자와 접근성
인스턴스 클래스와 스태틱 클래스는 외부 클래스의 멤버변수(인스턴스 변수 & 클래스 변수)와 같은 위치에 선언되며, 또한 멤버변수와 같은 성질을 갖는다. 따라서 내부 클래스가 외부 클래스의 멤버와 같이 간주되고, 인스턴스 멤버와 static멤버 간의 규칙이 내부 클래스에도 적용된다.
그리고 내부 클래스도 클래스이기 때문에 abstract나 final과 같은 제어자를 사용할 수 있을 뿐 아니라, 멤버변수들처럼 private, protected과 접근제어자도 사용이 가능하다.
내부 클래스 중에서 스태틱 클래스만 static 멤버를 가질 수 있다.
드문 경우지만 내부 클래스에 static 변수를 선언해야한다면 스태틱 클래스로 선언해야 한다.
다만 final과 static이 동시에 붙은 변수는 상수(constant)이므로 모든 내부 클래스에서 정의가 가능하다.
static을 잘 이해하였으면 내부클래스도 이해하기 쉽다.
ex)
내부클래스 중 인스턴스클래스의 인스턴스를 생성하려면 외부 클래스의 인스턴스를 먼저 생성하여야 한다.
스태틱 내부 클래스의 인스턴스는 외부 클래스를 먼저 생성하지 않아도 된다.
익명 클래스
익명 클래스는 특이하게도 다른 내부 클래스들과는 달리(지역 클래스, 인스턴스 클래스, 스태틱 내부 클래스) 이름이 없다.
클래스의 선언과 객체의 생성을 동시에 하기 때문에 단 한번만 사용할 수 있고 오직 하나의 객체만을 생성할 수 있는 일회용 클래스이다. 이름이 없기 때문에 생성자도 가질 수 없으며, 조상클래스의 이름이나 구현하고자 하는 인터페이스의 이름을 사용해서 정의하기 때문에 하나의 클래스로 상속받는 동시에 인터페이스를 구현하거나 둘 이상의 인터페이스를 구현할 수 없다. 오로지 단 하나의 클래스를 상속받거나 단 하나의 인터페이스만을 구현할 수 있다.
//익명 클래스
new 조상클래스이름(){
//멤버 변수
}
//또는
new 구현인터페이스이름(){
//멤버선언
}
'Java' 카테고리의 다른 글
[Java] 제네릭, 열거형, 애노테이션 (0) | 2021.05.04 |
---|---|
[Java] 예외처리 (0) | 2021.05.04 |
[Java] 상속, overriding, overloading, super, final (0) | 2021.04.08 |
[Java] 생성자 (0) | 2021.04.08 |
[Java] 객체와 클래스(멤버 변수/메소드) (0) | 2021.03.25 |