-
TIL #18 - 중첩클래스, inner class , 익명클래스프로그래밍/TIL(국비과정) 2020. 4. 20. 19:41
# 중첩클래스 (Nested class)
중첩클래스는 클래스 안에 또 클래스가 들어 있는 경우이다.
class 클래스1 { private int b; class 클래스2 { private int b; } }
클래스 2는 클래스 1의 필드 int b를 자유롭게 사용할 수 있다.
int b가 private 필드여도 Setter/getter를 만들지 않아도 접근할 수 있다.
하지만 클래스 1은 클래스 2의 int b에 접근할 수 없다.
접근하려면 객체를 생성해야 가능하다.
객체지향프로그램에서 클래스들은 서로 상호작용하기 때문에
어느 특정 클래스와 관계를 맺는 경우에는 관계 클래스를 클래스 내부에 선언해서 관리해주는 것이 좋다.
중첩 클래스를 사용하게 되면 두 클래스의 멤버에 서로 쉽게 접근할 수 있고,
외부에 불필요한 관계 클래스를 감춰 코드의 복잡성을 줄일 수 있다.
+ 파일명을 저장할 때는 main이 없더라도 가장 바깥의 class명으로 저장한다.
또 컴파일 후 저장시 바깥클래스$안쪽클래스.class의 파일이 별도로 생성된다.
아래의 코드에는 바깥 클래스 Outer와 내부 클래스 Inner가 있다.
먼저 바깥 클래스 Outer 이다
class Outer{ private String name; public void output(){ Inner in = new Inner(); //Inner 클래스 호출 System.out.println("이름 = " + name + "\t 나이=" + in.age); }
필드로 name이 하나 있고, 메소드로 output()이 하나 있다.
그리고 Outer 클래스 안에 Inner 클래스가 들어 있다.
class Inner { private int age; public void disp(){ System.out.println("이름 = " + name + "\t 나이=" + age); } } // class Inner
Innter 클래스에는 age 필드가 있고
disp() 라는 메소드가 하나 있다.
만약 Outer에서 Inner 클래스의 필드값을 사용하고 싶다면 위와 같이 Inner 클래스를 선언해서 호출해주면 된다.
Inner in = new Inner(); //Inner 클래스 호출 System.out.println("이름 = " + name + "\t 나이=" + in.age);
하지만 Inner 클래스의 경우 Outer 클래스에 있는 name을 사용하는데에 setter/getter 메소드와 같은 작업이 없어도
자유롭게 사용할 수 있다.
이제 메인메소드 안에서 Outer 클래스와 Inner 클래스를 불러와본다.
public static void main(String[] args) { Outer outer = new Outer(); outer.name = "양아무개";
그런데 클래스는 Outer, Inner 두개인데 Outer 만 선언해줄까?
이것이 중첩클래스와 상속의 다른 점이다.
Inner 클래스는 중첩이 일어났을 뿐 Outer와 같은 클래스가 아니다.
같은 공간에 묶어놨기 때문에 서로 관계가 있는 것이지 상속의 관계가 아니라는 것이다.
그래서 아래와 같이 Inner 클래스에 대한 단독 선언은 불가능하다.
독립된 자신만의 class가 아니므로 Outer 안으로 들어가야하는 것이다.
Inner in2 = new Inner();
만약 Inner를 선언해주고 싶다면 반드시 소속을 명시해야한다.
Outer.Inner in2 = outer.new Inner();
위와 같이 선언해주고 중첩클래스와 상관없이 데이터를 넣어줄 수 있다.
outer.name = "양아무개";
in2.age = 25; in2.disp();
이너클래스는 여러번 생성해줄 수 있다.
Outer.Inner in3 = outer.new Inner(); in3.age = 30; in3.disp();
Outer도 새로 만들 수 있다.
Outer.Inner in4 = new Outer().new Inner(); // in4.name = "김아무개"; in4.age = 50; in4.disp();
하지만 in4.name = "김아무개"; 는 불가능하다.
이렇게 데이터를 주는 것은 클래스 안에서 가능하다.
new를 해준 Outer는 같은 영역이 아니기 때문에 접근이 불가능하다.
in4는 현재 Inner 클래스를 가리키고 있고, new Outer를 해주었기 때문에
기존에 name "양아무개" 데이터가 있는 Outer에서 나와 새로운 주소값의 Outer가 생겼다.
이 Outer는 원래 클래스와 다른 영역이기 때문에 새롭게 값을 줄 수 없고 null 값으로 존재하게 된다.
단, age의 경우 Inner클래스 본인이 가지고 있는 메소드이기 때문에 데이터를 넣어줄 수 있다.
#익명 클래스
어떤 한 추상 클래스가 있다.
추상 클래스는 추상 메소드를 가질 수도 가지지 않을 수도 있다.
하지만 역으로는 성립하지 않는다.
추상 메소드를 가진 클래스는 반드시 추상 클래스여야한다.
abstract class AbstractTest { String name; public abstract void setName(String name); // 추상메소드 public String getName(){ // 구현 return name; } }
위의 추상 클래스를 상속받는 AbstractMain 클래스를 만들어 본다.
보통은 아래와 같이 상속을 받을 것이다.
class AbstractMain extends AbstractTest @Override public void setName(String name){ this.name = name; }
메인 메소드에서 추상클래스를 생성할 때,
추상클래스는 자체적인 new가 안되므로 new를 사용해서 생성해줄 수 없다.
따라서 상속을 시켜준다
단, 상속을 시켜주면 자식클래스는 반드시 추상클래스 안의 추상메소드에 Override 해주어야한다.
Override 후 자식클래스로 생성한다.
AbstractTest at = new AbstractMain();
그런데 위의 과정을 extends와 상속 없이 할 수도 있다.
class AbstractMain { AbstractTest at = new AbstractTest(){ @Override public void setName(String name){ this.name = name; } }; }
extends를 없애고 상속을 한 클래스를 생성해 클래스의 메소드를 Override 해주었다.
그런데 클래스 생성과 함께 중괄호를 써준다.
이 안에서 Override를 하는 것이다.
여기서의 Override는 AbstractMain에서 일어나야할 Override를 AbstractTest가 대신해준다는 의미이다.
이 작업이 상속 대신에 하되 상속과 같은 효과를 내는 것이다.
메소드의 구현부를 가질 수 있는 것은 클래스 뿐이다.
즉, 중괄호 내부는 메소드 구현부라는 것이다.
at는 클래스이기 때문에 그것이 가능하다.
그런데 이 클래스는 class 클래스명 과 같은 형태가 아니다.
이 클래스는 클래스 안의 클래스로 익명클래스라고 부른다.
이 파일을 컴파일해보면 아래와 같은 파일 형태가 나온다.
AbstractMain.java AbstractMain.class AbstractMain$1.class
익명클래스는 이름이 없기 때문에 1로 표현이 된다.
AbstractMain 클래스 안에 InterA 라는 인터페이스도 불러와본다.
InterA.java
interface InterA { public abstract void aa(); public void bb(); }
InterA bb = new InterA(){ public void aa(){} public void bb(){} };
위의 코드는 Interface를 new한 것이 아니고 위와 같이 익명을 부여해준 것이다.
중괄호 내부를 메소드 구현부로서 aa와 bb 메소드를 오버라이드 해준 것이다.
한번 쓰고 마는 용이다.
이번엔 추상 클래스를 불러와본다.
AbstractExam.java
abstract class AbstractExam { public void cc(){} public void dd(){} }
위의 추상클래스 안에 추상메소드는 없다.
세미콜론을 찍어주지 않았고 AbstractMain 클래스로 가 Override 해서 써야하므로 빈 body로 만들어준다.
위의 코드는 오버라이드 항목을 지정해놓고 원하는 것만 골라쓰는 꼴이다.
만약 메소드에 직접 abstract를 붙이면 무조건 오버라이드를 해야하지만,
위와 같이 abstract를 붙이지 않고 빈 body로 지정해주면 반드시 추상화하지 않아도 되기 때문에
필요에 따라 골라 사용할 수 있다.
이제 AbstractMain 클래스로 돌아와서 AbstractExam을 불러와본다.
하지만 역시 AbstractExam은 추상클래스이기 때문에 아래와 같은 선언은 불가능하다.
AbstractExam cc = new AbstractExam();
따라서 익명을 붙여준다.
AbstractExam cc = new AbstractExam(){};
그럼 아까와 같이 중괄호 안에 AbstractExam의 메소드들을 오버라이드 해줘도 되고 안해줘도 된다.
상속을 시켜주면 자식클래스는 반드시 추상클래스 안의 추상메소드를 Override 해주어야하는데
위와 같이 익명클래스로 만들어주면 Override를 하지않아도 오류가 나지 않는다.
이제 이 익명객체들을 어떻게 사용하는지 보자.
아래의 프로그램은 버튼 클릭에 따라 색깔이 바뀌는 프로그램이다.
이 프로그램에서는 버튼에 대한 이벤트와, 프레임 창에 대한 이벤트를 발생시켜야하므로
ActionEvent, ActionListener
WindowEvent, WindowListener
가 필요하다.
그래서 각 이벤트의 Listener를 클래스 옆에 implements 해주는 작업을 한다.
class RGB extends Frame implements ActionListener, WindowListener{
하지만 인터페이스를 implements를 해주면 해당 인터페이스가 가지고 있는 모든 메소드를 구현해주어야한다.
그렇지 않으면 오류가 난다.
만약 내가 WindowListener에서 7개의 메소드 중 창을 닫아주는 windowClosing이라는 메소드만 사용하고 싶더라도
implements를 한 이상 사용하지 않는 모든 메소드를 구현해 주어야한다.
public void windowActivated(WindowEvent e){} public void windowClosed(WindowEvent e){} public void windowClosing(WindowEvent e){ System.exit(0); // 창을 꺼버린다. } // x를 누르는 시점 public void windowDeactivated(WindowEvent e){} public void windowDeiconified(WindowEvent e){} public void windowIconified(WindowEvent e){} public void windowOpened(WindowEvent e){}
이렇게 되면 코드도 길어지고 보기에 좋지 않다.
이런 경우 우리는 익명객체를 사용할 수 있다.
이제 버튼 이벤트에 따라 색을 바꿔주는 RGB 클래스를 보자
import java.awt.Button; import java.awt.Canvas; import java.awt.Color; import java.awt.Frame; import java.awt.Panel; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; class RGB extends Frame { private Button redBtn, greenBtn, blueBtn; private DrCanvas canvas;
색을 의미하는 각 버튼 3개와 프레임 안에서 색을 바꿀 컨버스를 필드로 선언했다.
그리고 생성자 안에 버튼과 컨버스를 생성한다.
public RGB(){ redBtn = new Button("빨강"); greenBtn = new Button("초록"); blueBtn = new Button("파랑"); canvas = new DrCanvas();
버튼들(Component)을 묶어줄 panel을 생성한다.
panel은 FlowLayout으로 가운데 정렬이 default이다.
그리고 그 panel 안에 앞서 만든 버튼들을 차례로 넣는다.
p.add(redBtn); p.add(greenBtn); p.add(blueBtn);
그리고 만든 panel과 컨버스를 각각 북쪽과 가운데에 위치한다.
add("North", p); // 패널 붙이기 add("Center", canvas); // 캔버스 붙이기
마지막으로 프레임에 대한 정의를 해주면 이벤트를 제외한 모든 것이 준비된 것이다.
setBounds(700,100,300,400); setVisible(true);
그럼 이제 이벤트를 발생시킨다.
우리가 발생시킬 이벤트는 버튼 클릭과 클릭에 따른 convas 색상 변경이다.
먼저 프레임창을 닫아주는 이벤트를 실행시켜본다.
원래라면 implements를 통해 각 Listener들을 데려왔겠지만, 이번에는 WindowAdapter를 가져온다.
this.addWindowListener(new WindowAdapter(){ //Override public void windowClosing(WindowEvent e){ System.exit(0); } });
앞에서 본 익명메소드이다.
WindowAdapter 객체를 가져와 중괄호 안에 오버라이드를 해주었다.
WindowAdapter는 추상 클래스이면서 추상메소드가 없다.
따라서 원해는 것을 골라서 구현한다.
다음은 버튼 이벤트이다.
버튼 이벤트의 경우엔 ActionListener를 가져온다.
그런데 ActionListener는 앞의 WindowAdapter처럼 Adapter가 없다.
이처럼 대신할 메소드가 없다면 일회적으로 interface를 직접 new를 해준다.
redBtn.addActionListener(new ActionListener(){ // Override public void actionPerformed(ActionEvent e){ canvas.setBackground(Color.RED); // 캔버스를 빨강으로 } }); greenBtn.addActionListener(new ActionListener(){ // Override public void actionPerformed(ActionEvent e){ canvas.setBackground(Color.GREEN); // 캔버스를 빨강으로 } }); blueBtn.addActionListener(new ActionListener(){ // Override public void actionPerformed(ActionEvent e){ canvas.setBackground(Color.BLUE); // 캔버스를 빨강으로 } });
마지막으로 컨버스를 상속받고 컨버스 설정을 하는 메소드를 만들고 main 메소드에서 RGB 생성자를 호출해주면 끝이다.
class DrCanvas extends Canvas{ public DrCanvas(){ this.setBackground(new Color(200, 191, 231)); } } public static void main(String[] args) { new RGB(); } }
* 정리
상속과 인터페이스의 차이로는 강제성에 있다.
인터페이스를 implements 할 시 해당 인터페이스에 있는 메소드를 모두 구현해야하만 하는데
이를 보완해준 것이 익명객체이다.
'프로그래밍 > TIL(국비과정)' 카테고리의 다른 글
TIL #19 - 제네릭, Collection, ArrayList, Set, Map (0) 2020.04.21 TIL #18 - 계산기 프로그램 (0) 2020.04.20 TIL #17 - 추상, enum, interface (0) 2020.04.18 TIL #16 - Final, instanceof, 추상 (0) 2020.04.16 TIL #15 -String, 상속, 오버로드/ 오버라이딩 (0) 2020.04.15