공대생의 개발 일기장

ch07 객체지향프로그래밍2 본문

Java 공부

ch07 객체지향프로그래밍2

SeoKyung 2023. 3. 23. 17:49

https://github.com/SeoKyung0221/Java-study

 

GitHub - SeoKyung0221/Java-study

Contribute to SeoKyung0221/Java-study development by creating an account on GitHub.

github.com

chapter 07 객체지향 프로그래밍은 이전 ch06과 달리 많은 내용이 포함되어있다. 6장까지는 객체지향프로그래밍의 '클래스'에 대한 것을 설명했다면 7장에서는 이 클래스들이 서로 어떤 상호작용을 하는지 그럼으로써 객체지향프로그래밍이란 것이 무엇인지 알 수 있는 파트이다.

 

상속(inheritance) : ~은 ~이다. (is-a)

- 기존의 클래스를 재사용하여 새로운 클래스를 작성하는 것. 

- 키워드 extends를 사용해 상속관계를 붙여준다. ex) class Child extends Parent - Parent를 조상 클래스로 가지는 Child 클래스.

- 상속해주는 클래스를 '조상 클래스', 상속 받는 클래스를 '자손 클래스' // Parent ⊂ Child 이다.

- Child는 Parent의 '멤버만' 상속 받는다. 생성자, 초기화 블럭은 받지 않음.

- 상속 계층도에 따라 조상 클래스가 변하면 자손 클래스도 영향을 받지만, 자손 클래스의 변화는 조상 클래스에 아무런 영향도 주지 못한다.

Child와 Parent 클래스의 상속 계층도

포함(composite) : ~은 ~을 가지고 있다. (has-a)

- 포함관계를 맺어 주는 것은 한 클래스의 멤버변수로 다른 클래스 타입의 참조변수를 선언하는 것. (인스턴스 생성)

 

class A {
 B c = new B();
}

 

단일 상속

- C++에서는 다중상속 허용하지만 Java에서는 '오직 단일 상속만 허용'한다.

- 다중상속을 받으면 당연히 복합적인 기능을 가질 수 있지만 너무 관계가 복잡해지기 때문에 각 멤버의 이름이 같아지기라도 하면 쉽지 않다. 이런 문제점을 해결하기 위해 약간의 장점을 포기하고 단일 상속만을 허용하며 조금 불편해지더라도 클래스 간의 관계가 명확해지고 코드를 더욱 신뢰할 수 있다는 점에서는 다중상속보다 유리하다.

- Java에서는 두 개의 클래스에 대해 하나는 '상속' 다른 하나는 '포함' 시켜서 이용하는 방법을 쓴다.

 

Object class

- 모든 클래스의 조상이다.(최상위 조상)

- 그렇기 때문에 자바의 모든 클래스는 Object클래스에 정의된 멤버들을 자유자재로 사용 가능하다.

 

오버라이딩

- 조상 클래스로부터 상속받은 메서드의 내용을 변경하는 것 (자손 클래스 자신에 맞게 변경할 때 사용)

- 내용만을 변경하는 것이기 때문에 메서드의 선언부는 같다. (이름, 매개변수, 반환타입이 같다)

- 다만 접근 제어자와 예외(exception)은 제한된 조건 하에서만 다르게 변경 가능하다.

 

1. 접근 제어자 - 조상 클래스의 메서드보다 좁은 범위로 변경할 수 없다. 대부분의 경우 같은 범위의 접근 제어자를 사용한다.

2. 예외 - 조상클래스의 메서드보다 많은 수의 예외를 선언할 수 없다. (throw를 사용하는 것들)

3. 인스턴스메서드를 static메서드로 또는 그 반대로 변경할 수 없다. (static멤버들은 자신들이 정의된 클래스에 묶여있다)

 

- 오버로딩 (기존에 없는 새로운 메서드를 정의하는 것, 매개변수 변화)과 햇갈리지 말자.

 

super

- 자손 클래스에서 조상 클래스로부터 상속받은 멤버를 참조하는데 사용되는 참조 변수. 일전 this를 이용해 지역변수와 멤버변수를 구별했듯이 상속받은 멤버와 자신의 멤버의 이름이 같으면 구분하는데 사용된다.

- 물론 상속받은 멤버도 자신의 멤버이기 때문에 this를 사용가능하나 그래도 혹시나의 멤버 중복 정의를 대비해 super를 사용한다.

- 근본적으로 this와 super는 같다. 모든 인스턴스메서드는 자신이 속한 인스턴스의 주소가 지역변수로 저장되는데 그 값이 이 둘의 값이 된다.

- this와 마찬가지로 static메서드에서는 사용할 수 없다. 오직 인스턴스 메서드에서만 사용 가능하다.

- super.x는 조상클래스의 멤버변수 x, this.x는 자손 클래스(지금 클래스)의 멤버변수 x이다.

- 특히 오버라이딩된 메서드의 경우 super를 사용하는 편이다.

 

super( )

- super( )는 생성자이다. this( )는 같은 클래스의 다른 생성자를 호출하지만 super( )는 조상 클래스의 생성자를 호출한다.

- 자손 클래스에서 인스턴스를 생성하면 당연히 '자손 + 조상' 멤버의 하나의 인스턴스가 생성된다. 이때 조상 멤버를 초기화 해야하므로 당연히 조상 클래스의 생성자가 필요하다. 즉, 이때 super( )를 통해 조상 클래스의 생성자를 호출하는 것이다.

- 자손 클래스의 생성자의 첫 줄에서 호출되어야 한다. 자손 클래스의 멤버가 조상 클래스의 초기화되지 않은 멤버를 사용하기 전에 먼저 초기화를 시켜야 하기 때문이다.

- Object클래스를 제외한 모든 클래스의 생성자는 첫 줄에 반드시 자신의 다른 생성자(this( )) 혹은 조상의 생성자를 호출(super( )) 해야한다. 없다면 자동적으로 컴파일러가 super( )를 추가한다. 예를 들어 보자

class Parent{
	Parent(int a, int b){};
}

class Child extends Parent{
	Child(){
    // 조상 클래스 생성자 미호출 -> error!
    }
}

child 클래스의 생성자 Child( )에서 조상 클래스의 생성자를 호출하지 않았다. 이에 컴파일러가 super( );를 자동으로 추가하는데 이는 즉 Parent( )의 기본 생성자임을 알 수 있다. 하지만 조상 클래스인 Parent에는 이런 형태의 생성자가 없으므로 호출 되는 것이 없다. 그러므로 error!가 발생한다.- 정리하자면 조상 클래스의 멤버는 조상 클래스의 생성자로 초기화 하는 것이다.

 

패키지(package)

- 클래스의 묶음이다. 패키지에는 클래스 또는 인터페이스를 포함시킬 수 있고, 서로 관련된 클래스끼리 그룹 단위로 묶는 등의 행위로 클래스를 효율적으로 관리할 수 있다. 참고로 같은 이름의 클래스라도 다른 패키지에 존재하는 것이 가능하다.- 사실 클래스의 실제 이름(full name)은 패키지명이 포함되어 있다.- 클래스가 .class와 같은 클래스파일인 것처럼 패키지도 하나의 물리적인 directory다. 실제로 컴퓨터에 폴더로 존재한다.- java.lang.String클래스는 물리적으로 디렉토리 java안에 서브디렉토리 lang 안에 String.class 파일인 것이다. ' . '으로 계층 구조가 구분된다.- 패키지의 선언은 간단하다.    "package 패키지명;" 이다. 반드시 소스파일에서 주석과 공백을 제외한 첫 줄에 있어야 한다. 패키지명은 대소문자를 모두 허용하지만 소문자로 하는 것을 원칙으로 한다.이렇게 선언하게 되면 해당 소스파일에 포함된 모든 클래스나 인터페이스는 선언된 패키지에 속하게 된다. 참고로 지금까지 패키지를 추가하지 않아도 아무런 문제가 없었던 이유는 자바에서 기본적으로 이름없는 패키지를 제공하기 때문이다.- cmd로 실행하거나  클래스패스(classpath)를 포함 변경하는 것은 유튜브의 강의를 참고하자.

 

import문

- 사실 package에서 java.lang.String을 보면서 뭔가 쓱 감이 오는 문장이 하나 있을 것이다. 그렇다! 바로 우리가 입력을 받기 위해 사용했던 import java.util.*이 생각나지 않는가?- import문은 매번 패키지명을 붙여서 작성하는 불편한 일을 해결하기 위해 사용하는 것이다. import문으로 사용하고자 하는 클래스의 패키지를 미리 명시해주면 소스코드에 사용되는 클래스이름에서 패키지명은 생략된다. 즉, import문의 역할은 컴파일러에게 소스파일에 사용된 클래스의 패키지에 대한 정보를 제공하는 것.- ctrl + shift + o를 누르면 자동으로 import문을 추가해준다.- 뭔가 import는 그러면 성능에 영향을 줄 것 같지만 영향은 주지 않는다.- "import 패키지명.클래스명;" 혹은 " import 패키지명.*;"으로 선언한다. *은 모든 클래스를 의미한다. 그렇기 때문에 지정된 패키지에 속한 모든 클래스를 패키지명 없이 사용할 수 있다. 이쯤이면 import java.util.*이 무슨 의미인지 알 것이다.

import java.util.Calendar;
import java.util.Date;
import java.util.ArrayList;

// 이렇게 여러번 쓰지말고 한번에

import java.util.*;

다만 여기서 중요한 것은 *을 사용하는 것은 하위 패키지의 클래스까지 모두 포함하는 것은 아니라는 것이다. 즉 서브디렉토리 안에 있는 모든 클래스까지 싹다 포함하는 것은 아니라는 것이다. ex) import java.*은 모든 것을 포함하는 것처럼 보이지만 실제로 import java.util.*, import java.text.*대신 사용할 수는 없다.

- 모든 소스파일에는 묵시적으로 import java.lang.*;가 선언되어 있다. 즉, lang 패키지의 모든 클래스들을 패키지명 없이 사용할 수 있었던 것이다. 지금까지.

 

static import문

- static import문은 별다를 것은 없고 import문이 사용하면 클래스의 패키지명을 생략할 수 있었던 것과 같이 static멤버를 호출할 때 클래스 이름을 생략할 수 있다.

 

import static java.lang.Integer.*;
import static java.lang.Math.random;
import static java.lang.System.out;

// static import문들이 이렇게 선언되면

System.out.println(Math.random());
// 를 대신하여
out.println(random());
// 이 가능해진다.

 

제어자(modifier)

- 클래스, 변수, 메서드의 선언부에 함께 사용되어 부가적인 의미를 부여

- 접근 제어자: public, protected, default, private / 그 외: static, final, abstract, native,transient, synchronized, volatile, strictfp

- 하나의 대상에 대해서 여러 제어자를 조합하는 것이 가능하다. 이제부터 각 제어자들의 역할들을 한 번 살펴보자.

 

1. static - 클래스의, 공통적인

- '클래스의', '공통적인'의 의미를 가지고 있다. 사실 이미 실사용을 해보았기에 무슨 느낌인지는 알 것이다.

- 클래스 변수는 인스턴스에 관계없이 같은 값을 갖는다. 인스턴스를 생성하지 않아도 사용 가능하다. 즉, static 메서드는 메서드 내에서 인스턴스 멤버스를 사용하지 않는다.

- 사용될 수 있는 곳: 메서드, 멤버변수, 초기화 블럭

 

2. final - 마지막의 변경될 수 없는

- 이미 알고있듯이 상수에 사용했었다. 그렇기 때문에 무슨 느낌의 제어자인지 알 것이다.

- 변수에 사용하면 상수, 메서드에 사용하면 오버라이딩 불가(재설정불가), 클래스에 사용하면 자신을 상속하는 자손클래스를 정의할 수 없다.(상속불가)

- 일반적으로 선언과 동시에 초기화 하지만, 인스턴스 변수의 경우 생성자에서 초기화 할 수 있다. 그 값을 생성자의 매개변수로부터 제공받는다. 즉, 각 인스턴스마다 상수값이 달라지게 할 수 있다!

- 사용될 수 있는 곳: 클래스, 메서드, 멤버변수, 지역변수

 

3. abstract - 추상의, 미완성의 (모든 분야에서 도대체 '추상적'인게 뭐야?? 하고 날 괴롭혔던 그것!)

- 추상적이란 단어를 생각해보자. 두루뭉실하다는 것이다. 한마디로 "미완성"의 무언가라고 생각하자!

- '메서드의 선언부만 작성하고 실제 수행내용은 구현하지 않은 추상 메서드'를 선언하는데 사용한다. 무슨 말이냐 한마디로 그냥 미완성의 메서드에 사용한다는 것이다.

- 클래스에도 사용가능하다. 당연히 추상 클래스는 미완성 클래스이므로 인스턴스를 생성할 수 없다.

- (왜 이런 귀찮은 짓을) 드물지만 완성된 클래스에도 abstract를 붙여서 추상 클래스화 시켜버리는 경우도 있다. 완성은 되있으니 인스턴스를 만들 수 있지만 굳이 만들 필요가 없는 것들은 인스턴스를 못만들게 잠금해버리는 느낌이다. (왜 이런 귀찮은 짓을 하는지는 모르겠지만) 이를 상속받아서 일부의 원하는 메서드만 쏙쏙 뽑아내 오버라이딩이 가능하다는 장점이 있다.

- 사용될 수 있는 곳: 클래스, 메서드

 

접근 제어자(access modifier)

- 멤버(멤버변수, 메서드, 생성자), 클래스에 사용되어 해당 멤버 혹은 클래스를 외부에서 접근하지 못하도록 제한하는 역할. 접근 제어자는 모두 있는데 default의 경우에는 생략되어 붙이지 않는다. 즉, 접근 제어자가 따로 없다면 모두 default이다.

- 사용될 수 있는 곳: 클래스, 멤버변수, 메서드, 생성자

 

1. private - '같은 클래스 내'에서만 접근 가능

2. default - '같은 패키지 내'에서만 접근 가능

3. protected - '같은 패키지 내에서 + 패키지의 관계없이 상속관계의 자손클래스'에서 접근 가능 (default+상속관계 자손클래스)

4. public - 접근 제한 없음

 

즉, 접근 범위가 넓은 순으로 public > protected > default(굉장히 의외다) > private 이다.

- 클래스: public, default

- 메서드, 멤버변수: public, protected, default, private

- 지역변수는 접근 제어자 붙을 수 없음.

 

- 접근제어자를 이용해 "캡슐화"가 가능하다. 접근 제어자를 사용하는 이유는 결국 클래스의 내부에 데이터를 보호하기 위해서이다. 데이터가 유효한 값을 유지하도록 외부로부터의 접근을 제한하는 것을 '데이터 감추기' 이는 객체지향개념의 "캡슐화"이다.

- 접근제어자를 사용하는 또다른 이유는 굳이 외부에서 접근할 필요없이 내부에서만 쓰이는 데이터들을 감추기 위해서 숨기기 위해서이다. 이것도 역시 "캡슐화"에 해당한다.

- ex) 상속을 통해 확장될 것이 예상되는 클래스 -> 멤버에 protected 사용

 

*보통 멤버변수의 값을 읽는 메서드의 이름은 get멤버변수이름, 값을 변경하는 메서드는 set멤버변수이름으로 한다. 명심!

 

- 생성자의 접근 제어자를 사용할 수 있다고 하였는데 이를 통해 인스턴스의 생성을 제한할 수 있다. 보통의 경우 생성자의 접근 제어자는 클래스의 접근 제어자와 같다. 하지만 다르게 지정할 수도 있고 생성자의 접근 제어자를 private로 하면 외부에서는 생성자에 접근을 당연히 못하게 되고 그렇게 되면 인스턴스를 생성하지 못한다. 물론, 클래스 내부에서는 인스턴스를 생성할 수 있다. 그렇다면 인스턴스 생성을 어떻게 해야할까?

- 인스턴스를 생성해서 반환해주는 public 메서드를 사용한다.(public이여야 접근이 가능하니까) 앞에서 말하는 get 메서드의 일종이라고 보면 된다.  이 메서드는 public인 동시에 static이다. 참고로 이 메서드가 static이기 때문에 인스턴스 변수는 사용하지 못한다. 즉, 인스턴스를 생산하는 A a = new A( ); 도 static을 붙여 static A a = new A( ); 이어야 한다.

- 이걸 왜 하느냐? 이를 통해 인스턴스를 직접 생성하지 못하게 하고 사용할 수 있는 인스턴스의 개수를 제한한다.

- 추가적으로 생성자가 private인 경우 이 클래스는 다른 클래스의 조상이 될 수 없다. 앞서 말했듯 자손 클래스의 생성자의 첫 줄에서는 조상 클래스의 생성자를 호출해야하는데 private라면 호출할 수 없다. 그래서 상속할 수 없다임을 표기해주기 위해 앞에서 배운대로 final 제어자를 추가해 확실하게 표기해준다.

 

- 제어자는 조합될 수 있다. 다시 한번 모든 제어자들이 사용될 수 있는 부분을 살펴보자.

- 클래스: public, default, final, abstract

- 메서드: 모든 접근 제어자(public, default, protected, private), final, abstract, static

- 멤버변수: 모든 접근 제어자, final, static

- 지역변수: final

 

- 이들을 조합할 때 조심해야하는 부분이 있다.

1. 메서드에서 static과 abstract는 같이 사용할 수 없다. -> 그냥 직관적으로 생각해도 그렇긴한데 조금 더 자세히 설명하면 static은 몸통이 확실한 메서드에만 사용이 가능하다.

2. 클래스에 abstract와 final을 동시에 사용할 수 없다. -> 이것도 직관적으로 생각하면 알 수 있다. abstract은 구현을 위해 상속을 해주는데 final은 상속을 못하게 막으니까 서로 모순이므로 사용할 수 없다.

3. abstract메서드의 접근 제어자가 private일 수 없다. -> 2번과 비슷한 맥락이다. abstract는 상속을 통해 구현해야하는데 private를 접근 제어자로 설정할 경우 상속받은 자식 클래스가 멤버를 참조할 수 없기 때문에 모순이다.

4. 메서드에 private와 final을 같이 사용할 필요는 없다. -> 둘이 비슷한 역할을 하기 때문이다. private도 접근이 안되기 때문에 오버라이딩 할 수 없고, final도 본래 역할인 오버라이딩을 막을 수 있으므로 둘이나 필요없는 것이다.

 

다형성(polymorphism)

- 여러 가지 형태를 가질 수 있는 능력, 구체적으로 말하면 조상클래스 타입의 참조변수로 자손클래스의 인스턴스를 참조할 수 있도록 하였다는 것

class A{}

class B extends A{}

A a = new A();
B b = new B(); // 본래 인스턴스 생성방식 - 인스턴트 타입과 일치하는 타입의 참조변수

A a = new B(); // 조상 타입의 참조변수로 자손 인스턴스를 참조

그런데, 이와 같이 실제 인스턴스가 자손인 B의 것이라고 하더라도 변수 a로는 B인스턴스의 모든 멤버를 사용할 수 없다. a로는 B가 상속받은 A의 것만 사용가능하다! (그럼 도대체 이걸 왜 한거지?) 또, 반대로 자손 타입의 참조변수가 조상타입의 인스턴스를 참조할 수는 없다. (B b = new A( ); XXXXX 불가!) 왜 그럴까? 당연히 참조변수 b가 사용할 수 있는 멤버의 개수가 A인스턴스보다 많기 때문이다. 결론 (왜 하는지는 모르겠지만) 참조변수가 사용할 수 있는 멤버의 개수는 인스턴스의 멤버 개수보다 작거나 같아야 한다.

 

참조변수의 형변환

- 참조변수도 놀랍게도 형변환이 가능하다. 단, 서로 상속관계에 있는 클래스 사이에서만 가능하다. 즉, 조상과 자손끼리만!

- 기본형 변수때의 형변환을 떠올려보자 작은 자료형에서 큰 자료형의 형변환은 생략이 가능했는데 (int -> double) 참조변수도 마찬가지이다. 그렇다면 더 큰 경우는 조상일까 자손일까? 이 경우 상속을 받았기도 했고 앞에서 상속 계층도로 그린 집합 상에서 자손 클래스가 더 크므로 자손일거라 생각하지만 아니다. '조상타입'이 더 크다. (왜 그런지는 나도 모른다.) 그러므로 자손타입에서 조상타입으로 형변환(Up-casting) 될 때는 생략이 가능하며 그 반대로 조상에서 자손타입으로 형변환(Down-casting) 할 경우 생략이 불가능하다.

- 마찬가지로 형변환엔 캐스트 연산자가 사용되는데 괄호( )안에 변환하고자 하는 타입의 이름을 적어주면된다. (int)A 이런식으로 말이다.

- 형변환 없이 서로 다른 타입의 참조변수를 대입연산(=)으로 수행하면 안된다. 예전에도 char 관련해서 글을 올린적이 있었지만 형변환을 수행해 반드시 두 변수간의 타입을 맞춰주어야 한다.

- 참조변수의 형변환은 기본형의 형변환때와 달리 크게 신경써야 할 부분은 없다. 인스턴스가 변환되는 것도 아닌 그저 참조변수의 타입을 변환하는 것 뿐. 다만, 형변환을 통해 참조하고 있는 인스턴스에서 사용할 수 있는 멤버의 개수가 조절된다.

-다만, 여기서 아주 햇갈려선 안되는 중요한 것은

A a = new A();
B b = null; // B extends A, A는 B의 조상이다.

b = (B)a; // Error! -> 서로 참조변수 형은 똑같은데 어째서 에러일까?

그 이유는 바로 참조변수 a가 형변환 되었다고 해도 a가 참조하고 있는 인스턴스는 A라는 것이다. B는 A의 자손인데 이미 말했듯 서로 상속관계의 클래스에서 자손클래스의 참조변수는 조상클래스의 인스턴스를 참조할 수 없다. 이를 꼭 주의하고 햇갈리지 말자!

 

instanceof연산자

- 말그대로 연산자다. 굉장히 쉬운 형태이고 "왼쪽 instanceof 오른쪽" 이렇게 사용된다. 왼쪽에는 참조변수를 오른쪽에는 타입(클래스명)이 피연산자로 위치한다. 연산의 결과 return값은 boolean값인 true 혹은 false이다. 예를 들어보자.

a instanceof A = true, b instance B = true 그렇다면 b instanceof A는 어떨까? true이다. 왜냐하면 B클래스는 A클래스의 자손 클래스이기 때문에 조상의 멤버들을 모두 상속받아서 b 인스턴스는 A 인스턴스를 모두 가지고 있고 이를 A로 형변환해도 아무런 문제가 없다는 것이다. (연산 결과가 true라는 것은 검사한 타입으로 형변환을 해도 아무런 문제가 없다.)

- 참고로 구현(implement)또한 true가 된다. B implement C이면 b instanceof C도 true이다.

 

참조변수와 인스턴스의 연결

- 조상 클래스에 선언된 멤버변수와 같은 이름의 인스턴스 변수를 자손 클래스에 중복된 이름으로 정의했을 때, 조상타입의 참조변수로 자손 인스턴스를 참조하는 경우(a.B( ))와 자손타입의 참조변수로 자손 인스턴스를 참조하는 경우(b.B( ))는 서로 다른 결과를 얻는다.

- 메서드의 경우에는 '참조변수의 타입에 관계없이' 항상 '실제 인스턴스의 메서드'가 호출된다.

- 멤버변수의 경우 '참조변수의 타입에 따라 달라진다' - 조상타입의 참조변수면 조상의 멤버변수가, 자손타입의 참조변수면 자손의 멤버변수가 호출된다.

- 참고로 static도 참조변수에 영향을 받는다. 다만 static은 클래스이름.메서드( )로 호출한다.(햇갈리지 않기 위해서)

- 다만, 햇갈려선 안된다. 참조변수가 자손 타입이더라도 참조하려는 멤버 변수가 조상에만 정의되어 있다면 조상의 멤버변수가 호출된다. 어디까지나 이름이 '중복' 되었을 때의 이야기이다.

 

매개변수의 다형성

- 말이 어렵지만 매개변수로 다른 클래스 타입의 참조변수를 받는 것이다.

void internet(Computer c){}

Computer 타입의 참조변수 c를 메서드의 매개변수로 받는 모습이다. 이때 Computer타입을 매개변수로 받을 수 있다는 것은 Computer 타입의 하위 즉, 자손 클래스들의 타입 참조변수들도 모두 받을 수 있다는 것을 의미한다.

 

객체를 배열로 다루기

- 객체의 타입을 참조변수로 할 수는 없는걸까? 당연히 된다. 하지만 특별한 것이 하나 있다. 배열의 타입이 조상 클래스의 타입이라면 배열로 자손 클래스들의 객체를 배열로 묶을 수 있다. 즉, 공통된 조상이기만 하다면 서로 형제관계이더라도 묶어서 관리할 수 있는 것이다.

- Vector클래스 같은 경우 내부적으로 최고 조상은 Object타입의 배열을 가지고 있기 때문에 이 배열에 객체를 추가하거나 제거할 수 있다. 또 동적 배열이기 때문에 배열의 크기를 알아서 관리해 저장 공간을 신경쓰지 않아도 된다. 그냥 객체도 담을 수 있는 동적 배열이라고 보면 된다.

- vector클래스의 주요 기능은 372p에 자세히 정리되어 있다. 꼭 보길 바란다.

 

추상 클래스 - abstract class 클래스이름

- 미완성 설계도. 그런데 클래스로서 부족하다는 개념보다는 미완성 메서드를 포함하고 있다는 의미다. 한마디로 말해서 '추상메서드'를 가지고 있다면 이를 확실히 알게 해주기 위해 '추상 클래스'라고 표기해주는 것이다.

- 당연히 미완성된 것으로 인스턴스를 생성할 수는 없다. 추상 클래스는 상속을 통해서 자손클래스에서 '완성' 된다.

 

추상 메서드(abstract method)

- 선언부만 작성되어 있는 것. 구현부는 X

- 예전에 공부했던 디자인 패턴의 팩토리 패턴이랑 비슷한데 조상 클래스에서는 제목만 정하고 내용은 자손 클래스에서 구현하는 것이다. 참고로 주석은 조상클래스에서 남겨 역할에 대한 설명은 해준다.

- 한가지 중요한 점은 여타 메서드처럼 '리턴타입 메서드이름(매개변수){ }' 의 형태가 아니라 'abstract 리턴타입 메서드이름( );' 이다. 세미콜론에 집중하자. 추상 메서드는 반드시 이것으로 끝을 맺는다. 참고로 내용없이 괄호만 있어도 이는 추상 메서드가 아닌 일반 메서드로 처리된다.

- 당연한 얘기지만 조상에게서 추상 메서드를 상속받았는데 이를 자손또한 구현하지 않았다. 그렇다면 자손 클래스도 추상 클래스가 되는 것이다.

- 생각보다 내용이 중요하다는 내 강박관념에 의해 선언부만 선언되있는게 뭐가 중요하다 느낄 수 있지만 사실 절반은 완성된 아주 중요한 형태이다. 명심하자.

작성

- 보통은 공통적으로 사용될 수 있는 클래스를 바로 작성해 상속 받기도 하지만 '기존의 클래스의 공통적인 부분을 뽑아서' 추상클래스를 만들어 상속받는 경우도 있다. 이것을 '추상화'라고 한다. 공통부분을 뽑아서 조상 클래스를 만드는 것이다.

- 추상화는 계층도에서 위로 올라가는 것. 구현화는 반대로 내려가면서 클래스의 기능을 점점 추가하고 구체화 하는 것.

- 근데 어차피 오버라이딩하는 경우에는 abstract를 붙이지 않아도 결과에 별 차이가 없을텐데 어째서 붙이는 것일까? 음... 약간 강요문 같은 것이다. 이 라벨이 붙어있으면 반드시 ~~하세요! 이런 느낌. abstract는 반드시 구현하세요!! 라는 느낌인 것이다.

 

인터페이스(interface)

- 인터페이스는 일종의 추상클래스이다. 추상클래스처럼 추상메서드를 가지고 있다. 그럼 차이가 무엇일까? 이 추상클래스보다 더 추상화가 높다는 것이다! 무슨 말이냐 한마디로 제대로 된게 없다는 것. 추상 클래스와 달리 몸통을 갖춘 일반 메서드와 멤버변수를 구성원으로 가질 수 없다. '오직 추상메서드와 상수만을 멤버로' 가진다.

- 그래서 인터페이스는 '기본 설계도'라고 한다.

작성

- 클래스와 작성방법은 똑같지만 키워드가 class가 아니다. 'interface 인터페이스이름' 이다. 클래스와 마찬가지로 접근제어자는 사용될 수 있다.(public, default)

- 상수와 추상 메서드만을 가지고 있다고 했는데 모든 멤버변수는 public static final이고 메서드는 public abstract이다. 참고로 둘다 이를 생략할 수 있다. 컴파일러가 자동으로 추가해준다.

- JDK 1.8부터는 static메서드와 default method의 추가를 허용한다. 다만 아직은 실무에서 그렇게 많이 쓰이진 않는다.

 

* default 메서드와 static 메서드

- static 메서드의 접근 제어자는 항상 public이다.

- dfault 메서드는 추상 메서드의 기본적인 구현을 제공하는 메서드다. 추상 메서드가 아니기 때문에 디폴트 메서드가 새로 추가되어도 이 인터페이스를 구현한 클래스에서는 변경이 일어나지 않는다. 키워드는 'default 리턴타입 메서드이름' 으로 정의되는데 역시나 public의 접근 제어자만 가능하고 생략이 가능하다. 약간의 규칙이 있다.

1. 여러 인터페이스의 디폴트 메서드 간의 충돌 - 인터페이스를 구현한 클래스에서 디폴트 메서드를 오버라이딩 한다.

2. 디폴트 메서드와 조상 클래스의 메서드 간의 충돌 - 조상 클래스의 메서드가 상속, 디폴트 메서드는 무시된다.

 

상속

- 인터페이스는 인터페이스로부터만 상속받을 수 있고, 클래스와 달리 다중상속이 가능하다. 상속 키워드는 마찬가지로 extends이다.

구현

- 당연히 인스턴스 생성은 되지 않는다. 마찬가지로 인터페이스도 추상메서드의 몸통을 만들어주는 클래스를 작성해야하는데 추상클래스처럼 인터페이스를 상속받는 클래스가 구현해주어야한다. 다만! 추상클래스는 extends의 상속을 통해 구현했지만 인터페이스는 말그대로 구현한다는 의미로 implements를 사용해 구현하는 클래스를 작성한다. 햇갈려선 안되는게 상속도 받을 수 있다. 그런데 인터페이스를 구체적으로 구현해주는 클래스는 implements를 붙여 구분해주는 것이다. 참고로 동시에 상속, 구현이 된다.

- 인터페이스 이름으로 able이 많이 쓰이는 편이다. ~을 할 수 있다는 의미를 담아 인터페이스의 기능적 느낌을 담고 있다.

- 인터페이서도 조금은 다른의미의 조상이 될 수 있다. 그렇기 때문에 instanceof 연산자에서 구현관계인것도 true가 나오는 것이다.

 

인터페이스를 이용한 다중상속??

- 자바에서는 단점을 숨기기위해 인터페이스를 통한 다중상속을 지원한다고 한다. 한마디로 그냥 블랙기업식 보여주기. 우리 회사는 복지가 좋아요~ (복지 사실상 못씀) 이랑 같은 개념이랄까... 일단은 알아만 두자.

- 상수는 static으로만 정의되기 때문에 조상과 자손 사이에서 멤버변수의 충돌은 거의 없다. 추상 메서드 같은 경우에는 당연히 구현 내용이 없으므로 마찬가지로 이름이 같더라도 그냥 조상 클래스 쪽의 메서드를 상속받으면 되므로 문제가 없다.

- 그럼 다중상속을 도대체 왜하는거지? 그렇다 그렇기 때문에 다른 방법을 사용한다. 두 조상 클래스 중에서 비중이 높은 쪽을 선택하고 다른 한쪽은 클래스 내부에 멤버로 '포함'시키는 방식으로 처리하거나 + 어느 한쪽의 필요한 부분만 뽑아서 인터페이스로 만들어 구현하는 것이다.

public class Computer { // 대충 Computer에 대한 클래스 내용}

public class Monitor {
   void printing { // 대충 화면을 프린팅한다는 함수}
   void sound { // 모니터에서 소리나옴}
   void light { // 모니터에서 빛나옴}
}

public interface Demonitor { // monitor의 메서드와 일치하는 추상메서드를 갖는 인터페이스 작성
   public void printing();
   public void sound();
   public void light();
}

public class game extends Computer implements Demonitor {
   Monitor m = new Monitor();
   
   public void printing(){
      m.printing(); // 코드를 작성하지 않고 Monitor인스턴스의 메서드 호출
   }
   public void sound(){
      m.sound();
   }
   public void light(){
      m.light();
   }
} // 이중상속 구현

 

인터페이스를 이용한 다형성

- 인터페이스또한 이를 구현한 클래스의 조상이라고 했다. 그러므로 인터페이스 타입의 참조변수로 자손 클래스의 인스턴스를 참조할 수 있다. 당연히 인터페이스 타입으로의 형변환도 가능하다.

- 그런데... 미완성 설계도에 불과한 이게 도대체 어떻게 쓸모가 있을까? 사실 쓸모없다. 메서드의 매개변수로 쓰여야 그때서야 빛을 바란다. 매개변수로서의 인터페이스 타입이 갖는 의미도 메서드 호출 시 해당 인터페이스를 구현한 클래스의 인스턴스를 매개변수로 제공해야 한다는 것이다. 위에서 조상타입을 매개변수로 하면 그 자손타입들을 모두 매개변수로 사용할 수 있다고 하였는데 인터페이스는 인스턴스를 생성하지 못하니... 사실상 그냥 나를 구현한 자손을 호출하라는 의미이다.

- 그럼 리턴타입이 인터페이스 타입이라면? 마찬가지다. 메서드가 인터페이스를 구현한 클래스의 인스턴스를 반환하라는 것이다.

 

인터페이스의 장점

1. 개발시간을 단축시킬 수 있다.

2. 표준화가 가능하다.

3. 서로 관계없는 클래스들에게 관계를 맺어줄 수 있다 - 이거 중요하다! 좋은 능력이다.

4. 독립적인 프로그래밍이 가능하다.

- 자세한 설명은 390p에 정리되어 있다.

 

- 3번을 조금 더 자세히보자. 예를 들어서 어떤 메서드가 불특정 여러 클래스에서 공통적으로 사용된다고 하자. 이 메서드를 당연히 조상클래스로 추상화시켜 상속받는다는 생각을 하게될 것이다. 그런데 만약 이들이 하나의 조상을 상속한 자손들이라면? 그리고 이 조상을 건드려선 안된다면? 그럼 문제가 어려워진다. 이때 나오는 것이 바로 인터페이스이다. 공통된 메서드를 가지고 있는 인터페이스를 정의하고 이를 사용하는 여러 클래스에서 모두 이 인터페이스를 구현하는 것이다. 그럼 같은 인터페이스를 구현했다는 클래스 사이의 '공통점'이 생긴다. 만약에 이 인터페이스의 이름이 able이고 공통된 메서드의 매개변수로 able타입을 사용하면 어떻게 될까? 미리 말했던 대로 인터페이스를 구현한 클래스들의 타입을 모두 사용할 수 있게 된다. 한마디로

interface able {}

class A implements able{}
class B ...
class C ...

void method(able e){ // 공통사용 메서드}

method가 A, B, C를 호출해 사용할 수 있는 것이다.

 

- 조금 더 살펴보자. 만약에 B와 C에 '새로운 메서드'를 추가하려면 어떻게 해야하는가? 마찬가지다 새로운 인터페이스를 정의하고 이 안에 추상메서드로 '새로운 메서드'를 넣고 이 구현을 B와 C에서 하면 된다. 사실 각자에 따로 적어도 되지만 이러면 공통점도 생기고 코드 중복을 줄일 수 있다.

 

아직도 인터페이스를 잘 모르겠다. 인터페이스의 이해

- 먼저 두가지를 염두하자.

1. 클래스를 사용하는 쪽(user)와 제공하는 쪽(provider)가 있다.

2. 메서드를 사용하는 user에서는 사용하려는 메서드(provider)의 선언부만 알면된다. 내용물은 몰라도 된다.

- 클래스 A(user)가 클래스 B(provider)의 인스턴스를 생성하고 메서드를 호출한다고 해보자. 두 클래스는 직접적인 관계다. 그렇다면 A를 작성하기 위해서는 B가 이미 작성되어 있다는 것이다. B가 변경되면 이를 사용하면 A도 변경된다는 의미이다. 그렇다면 무조건 같이 변경되어야 한다는 점은 단점이다. 인터페이스는 이 단점을 해결할 수 있는 것이다. A가 B를 직접 호출하지 않고 인터페이스를 매개체로 클레스 A가 인터페이스를 통해서 클래스 B의 메서드에 접근 하도록 하면, 클래스 B에 변경사항이 생기거나 클래스 B와 같은 기능의 다른 클래스로 대체되어도 클래스 A는 전혀 영향을 받지 않게 하는 것이 가능하다.

- 조금 더 천천히 과정을 보자

1. 클래스 B의 메서드를 추상메서드로 정의하는 인터페이스 I를 만든다.

2. 클래스 B가 이 인터페이스 I를 구현한다.(당연하지?)

3. 클래스 A는 B대신에 인터페이스를 사용해서 작성될 수 있다. ( void method(I i)로 하면 B의 인스턴스를 제공받는다. 하지만 직접적인 연관은 없다.)

 

내부 클래스(inner class)

- 말 그대로 클래스 내에 선언된다는 점을 제외하고는 일반적인 클래스와 다르지 않다. 사용빈도가 높지 않아서 기본 원리와 특징이 있다는 점만 알아두고 넘어가도 무방!

- 클래스 내에 선언되있으니 두 클래스는 서로 긴밀한 관계임을 알 수 있다. 당연히 그러면 두 클래스 간의 멤버들이 서로 쉽게 접근할 수 있다는 점과 외부에는 불필요한 클래스를 감춤으로써 코드의 복잡성을 줄일 수 있다(캡슐화)는 장점이 있다. 참고로 외부클래스는 내부클래스를 감싸고 있는 클래스를 말한다. 설마 모르진 않을...?

- 내부 클래스는 외부 클래스를 제외하고는 잘 쓰이지 않는다. 캡슐화되어있다고 보면 될 것 같다.

- 내부 클래스의 종류는 변수의 선언위치에 따라 다르다. 여기서 변수의 선언위치란건 통상 클래스에서 변수를 선언할 때의 그 위치를 말한다. 인스턴스 클래스, static 클래스, 지역 클래스, 익명 클래스 이렇게 4개가 있는데 이름 그대로 다뤄진다고 보면 될 것 같다. 자세한 것은 404p에 정리되어 있다. 익명 클래스는 처음 등장하는 개념이니 한번 더 살펴보자.

선언

- 앞서 말했듯이 선언위치에 따라 내부 클래스의 종류도 바뀌는데 신기하게도 선언위치의 변수와 동일한 유효범위와 접근성을 갖는다. 예를 들어 지역 클래스이면 지역변수와 동일한 유효범위와 접근성을 가지는 것이다.

내부 클래스의 제어자와 접근성

- 내부 클래스가 외부 클래스의 멤버와 같이 간주되고 인스턴스멤버와 static멤버 간의 규칙이 내부 클래스에도 똑같이 적용된다. 한마디로 그냥 클래스기 때문에 클래스에서 사용하는 제어자를 모두 사용할 수 있다. 다만 멤버 변수들이 사용하는 접근제어자들도 사용할 수 있는 것이다! (ex : private)

- 내부 클래스 중에서 static 클래스만 static 멤버를 가질 수 있다. 왠지는 모르겠다. 그렇게 알아두자. 다만 final과 static이 동시에 붙은 변수는 상수이므로 다른데에도 존재할 수 있다.

- 인스턴스멤버는 같은 클래스에 있는 인스턴스멤버와 static멤버 모두 직접 호출이 가능하다. static은 인스턴스 생성전까지 인스턴스 멤버를 호출할 수 없다. (나는 이미 이것을 알고 있다.) 마찬가지로 인스턴스클래스도 외부 클래스의 인스턴스멤버를 그냥 사용할 수 있지만 static 클래스는 외부클래스의 인스턴스를 객체 생성 전까지는 사용할 수 없다.

- static 클래스는 외부 클래스의 static 멤버만 사용할 수 있다.

- 지역 클래스는 외부 클래스의 인스턴스멤버와 static멤버를 모두 사용할 수 있다. 메서드에 정의된 지역 변수도 사용할 수 있다. 근데 이 지역변수는 final이 붙은 것만 가능하다. 사실 이 부분은 이해가 잘 안되는데 407p를 잘 살펴보자. 메서드가 수행을 마쳐서 지역변수가 소멸된 시점에도 지역 클래스의 인스턴스가 소멸된 지역변수를 참조하기 때문이다라고 적혀있는데... 뭔 소린지 모르겠다. 그럴수가있나??? 지역 클래스도 메서드가 끝나면 같이 소멸되는 거 아닌가?? 뭐 아닌가 보다 외우자 걍.

- JDK1.8부터는 지역 클래스가 접근하는 지역 변수의 final은 생략할 수 있게 되었다. 컴파일러가 자동으로 붙여준다.

 

* 컴파일 했을 때 생성되는 파일명은 '외부 클래스명$내부 클래스명.class' 이다. 지역 내부 클래스는 다른 메서드에 같은 이름의 내부 클래스가 존재할 수도 있어서 클래스명 앞에 숫자가 붙는다.

 

- 만약 내부 클래스와 외부 클래스에 선언된 변수가 이름이 같다면(멤버변수가 같다면) this.변수로 호출한 것은 외부클래스의 변수이다. 변수 앞에 'this.변수' 혹은 '외부클래스명.this.변수'로 구별한다.

 

익명 클래스

- 이름이 없다. 클래스의 선언과 객체의 생성을 동시에 한다. 단 한번만 사용되고 하나의 객체만 가지는 일회용 클래스이다.

- 당연히 생성자도 가질 수 없고, 조상클래스의 이름이나 구현하고자 하는 인터페이스의 이름으로 사용해서 정의하기 때문에 하나의 클래스로 상속받는 동시에 인터페이스를 구현하거나 둘 이상의 인터페이스를 구현할 수 없다. 오로지 단 하나의 클래스를 상속받거나 단 하나의 인터페이스만 구현하는 둘 중 하나이다.

- new 조상클래스이름( ){ // 멤버 선언 } / new 구현인터페이스이름( ){  // 멤버 선언 }

- 외부클래스명$숫자.class로 클래스 파일명이 결정된다.

 

*내부 클래스는 꼭 7-25번 이후의 문제 해설을 살펴보길 바람! 여기 적은 개념말고도 직접 코드로 보면 이해가 잘된다.

 

 

 

 

 

 

'Java 공부' 카테고리의 다른 글

Java 현황  (0) 2023.09.04
ch08 예외처리  (0) 2023.04.11
ch06 객체지향프로그래밍1  (0) 2023.03.12
ch05 배열  (0) 2023.02.26
ch04 조건문과 반복문  (1) 2023.02.19