본문 바로가기

복습

[Java 복습] 상속, 캡슐화, 다형성

클래스의 관계 

1. 상속 관계
2. 포함 관계

 

상속


기존의 클래스로 새로운 클래스를 작성하는 것( 코드의 재사용)
두 클래스를 부모와 자식으로 관계를 맺어주는 것

class Point {
    int x;
    int y;
}
class Point3D {
    int x;
    int y;
    int z;
}

class Point3D extend Point{
    int z;
}

 

 

포함 관계

  • 포함이란: 클래스의 멤버로 참조변수를 선언하는 것
  • 작은 단위의 클래스를 만들고, 이 들을 조합해서 클래스를 만든다.

 

클래스 간의 관계 결정하기

  • 상속관계: ~은 ~이다.(is -a) <- 꼭 필요할 때만(제약이 많기 때문)
  • 포함관계: ~은 ~을 가지고 있다(has - a) <- 90%

 

 

관계를 설정하기 힘들 땐 문장에 대입해보자!

  • 상속관계: 원(Circle)은 점(Point)이다.
  • 포함관계: 원은 점을 가지고 있다.
class Circle {
    int x;
    int y;
    int r;
}
class Circle {
    Point c = new Point(); // Circle이 Point를 포함한다.
    int r;
}

 

 

단일 상속

  • Java는 단일 상속만을 허용한다. (C++은 다중 상속 가능하다고 함.)
  • 비중이 높은 클래스 하나만 상속관계로, 나머지는 포함관계로 한다. (다중상속 효과)

 

다중 상속의 문제점

두 부모의 메서드의 이름이 같을 경우 어떤 것을 상속받을 지 모호함(충돌 문제)

 

Object 클래스 - 모든 클래스의 조상

  • 부모가 없는 클래스는 자동적으로 Object 클래스를 상속받게 된다.(컴파일러가 자동 추가)
  • 모든 클래스는 Object 클래스에 정의된 11개의 메서드를 상속받는다.
  • toString( ), equals(Object obj), hasCode( ), ...

 

 

메서드 오버라이딩

  • 상속받은 조상의 메서드를 자신에 맞게 변경하는 것
  • 선언부는 변경 불가, 구현부만 변경 가능
  • 부모가 없는 클래스는 Object를 자동으로 상속하므로 toString( ) 메서드를 오버라이딩 할 수 있다.
  • 팁: toString( )을 오버라이딩하면 참조변수만 println(참조 변수) 해도 내가 원하는 형식로 나옴
  • 이유: println(참조변수.toString( )) => println( ) 메서드에 toString( ) 로직이 있기 때문에!

 

참조변수 super

  • 객체 자신을 가리키는 참조변수, 인스턴스 메서드(생성자) 내에만 존재
  • 조상의 멤버를 자신의 멤버와 구별할 때 사용
  • 중복되지 않는 경우에는 this.x와 super.x가 같은 변수를 가르킴
public class Parent {
    int x = 10; // super.x
}

class Child extends Parent {
    int x = 20; // this.x

    void method() {
        System.out.println("x = " + x); // 가까운 쪽: this.x
        System.out.println("this.x = " + this.x);
        System.out.println("super.x = " + super.x);
    }
}
public class ChildMain {
    public static void main(String[] args) {
        Child child = new Child();
        child.method();
    }
}
x = 20
this.x = 20
super.x = 10

 

super( )- 조상의 생성자

  • 조상의 생성자를 호출할 때 사용
  • 조상의 멤버는 조상의 생성자를 호출해서 초기화
  • 조상의 생성자와 초기화 블럭은 상속이 안됨.
  • 오류는 아니지만 자손의 생성자는 자신이 선언것 만 초기화 해야함.
class Point {
    int x, y;
    
    Point(int x, int y) {
    this.x = x;
    this.y = y;
}
class Point3D extend Point{
    int z;
    
    Point3D(int x, int y, int z) {
        this.x = x; // 조상의 멤버를 초기화
        this.y = y; // 조상의 멤버를 초기화
        this.z = z;
    }
}

class Point3D extend Point{
    int z;
    
    Point3D(int x, int y, int z) {
        super(x, y);
        this.z = z;
    }
}

 

 

super( ) 조상의 생성자 규칙

  • ★ ★ ★ 어떤 생성자든!!!! 생성자의 첫줄에 반드시 생성자를 호출해야 한다. ( super(); or this(); ) ★ ★ ★
  • 그렇지 않으면 컴파일러가 생성자의 첫줄에 super( ); 삽입(Object 생성자 호출)

 

 

패키지

  • 서로 관련된 클래스의 묶음 -Java8 기준 4000개의 클래스
  • 클래스는 클래스파일, 패키지는 폴더, 하위 패키지는 하위 폴더
  • 클래스의 실제 이름(full name)은 패키지를 포함(java.lang.String)
  • rt.jar는 클래스들을 압축한 파일(자바9 부터 사라지고 module로 바뀜)

 

 

패키지의 선언

  • 패키지는 소스파일의 첫 번째 문장으로 단 한번 선언
  • 같은 소스 파일의 클래스들은 모두 같은 패키지에 속하게 된다.
  • 패키지 선언이 없으면 이름없는(unnamed) 패키지에 속하게 된다.

 

 

import 문

  • 클래스를 사용할 때 패키지 이름을 생략할 수 있다. (원래는 패키지명까지 적어줘야 한다.)
  • 컴파일러에게 클래스가 속한 패키지를 알려준다.
  • java.lang 패키지의 클래스는 import하지 않고도 사용할 수 있다.
  • (*)은 이 패키지에 있는 모든 클래스를 import 해준다.
  • import문은 컴파일 시에 처리되므로 프로그램의 성능에 영향없다. (* 찍어도 성능 이슈 없음. 하지만 명확도는 줄어듬.)
  • 이름이 같은 패키지의 경우에는 앞에 패키지명을 적어주자
  • String, Object, System, Thread ...
import java.util.Date;

class ImportTest{
    java.util.Date today = new java.util.Date();
    Date today = new Date();
}

 

 

 

static import 문

import static java.lang.Integer.*; // Integer클래스의 모든 static 메서드
import static java.lang.Math.random.*; // Math.random()만 괄호 안붙임.
import static java.lang.System.out.*; // System.out을 out만으로 참조가능

System.out.println(Math.random()); //=> out.println(random());

 

 

static import 쓰는 이유?

클래스 이름 붙이면 코드가 길어지기 때문에 쓰는 경우가 있지만 명확도를 위해 꼭 필요할 때만 쓰자

 

 

제어자

  • 클래스와 클래스의 멤버(멤버 변수, 메서드)에 부가적인 의미 부여
  • 하나의 대상에 여러 제어자를 같이 사용가능(접근 제어자는 한번만)
  • 관례적으로 접근제어자를 제일 왼쪽에 쓴다.

 

 

제어자의 종류

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

 

 

 

static - 클래스의, 공통적인

 

제어자: static

  • 대상: 멤버 변수
    • 모든 인스턴스에 공통적으로 사용되는 클래스 변수가 된다.
    • 클래스 변수는 인스턴스를 생성하지 않고도 사용 가능하다. (객체 없이)
    • 클래스가 메모리에 로드될 때 생성된다.
  • 대상: 메서드
    • 인스턴스를 생성하지 않고도 호출이 가능한 static 메서드가 된다.
    • static 메서드 내에서는 인스턴스 멤버들을 직접 사용할 수 없다.

 

 

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

 

제어자: final

  • 대상: 클래스
    • 변경될 수 없는 클래스, 확장될 수 없는 클래스가 된다. (조상이 될 수 없는 클래스)
    • 그래서 final로 지정된 클래스는 다른 클래스의 조상이 될 수 없다.
    • 예시: String(상속해서 부모 클래스의 멤버에 접근할 수 있는 보안문제 때문에), Math
  • 대상: 메서드
    • 변경될 수 없는 메서드, final로 지정된 메서드는 오버라이딩을 통해 재정의 될 수 없다.
  • 대상: 멤버변수, 지역변수
    • 변수 앞에 final이 붙으면, 값을 변경할 수 없는 상수가 된다. (값을 변경할 수 없는 멤버, 지역변수)

 

 

abstract - 추상의, 미완성의

 

제어자: abstract

  • 대상: 클래스
    • 클래스 내에 추상 메서드가 선언되어 있음을 의미한다.
    • 추상 메서드를 가지고 있으면 무조건 추상 클래스(미완성 설계도)
    • 객체(제품)을 생성하지 못한다.
    • 객체를 생성하려면 상속을 통해서 완성을 시켜줘야 한다.
  • 대상: 메서드
    • 선언부만 작성하고 구현부는 작성하지 않는 추상 메서드임을 알린다.
abstract class AbstractTest { // 추상 클래스(추상 메서드를 포함한 클래스)
    abstract void move(); // 추상 메서드(구현부{ }가 없는 메서드)
}

 

 

 

접근제어자

  • private: 같은 클래스 내에서만 접근이 가능하다.
  • (default): 같은 패키지 내에서만 접근이 가능하다.
  • protected: 같은 패키지 내에서, 그리고 다른 패키지의 자손 클래스에서 접근이 가능하다.
  • public: 접근 제한이 전혀 없다.
제어자 같은 클래스 같은 패키지 자손 클래스
(다른 패키지)
전체
public O O O O
protected O O O  
(default) O O    
private O      

 

  • 클래스 앞에 가능한 접근제어자: public, (default)
  • 멤버 앞에 가능한 접근제어자: public, protected, (default), private

 

 

캡술화와 접근 제어자

 

접근 제어자를 사용하는 이유?

  • 외부로부터 데이터를 보호하기 위해서
  • 외부에는 불필요한, 내부적으로만 사용되는, 부분을 감추기 위해서
  • 외부에서 객체를 생성해 멤버변수에 직접 가능하지 못하도록 접근제어자 private를 쓴다.
  • 외부에서 public 메서드를 이용해 간접 접근을 허용할 수 있다.

 

내가 생각한 캡슐화란?

외부에서 객체를 생성해 데이터(멤버변수)를 조작하는 것을 막고,

굳이 외부에 노출시킬 필요없는 내부검증 로직 접근제어자 private으로 막는 것

접근할 수 있는 범위를 최소화 시켜놓고, 필요할 때 확대하면 된다.

 

 

 

 다형성 

여러 가지 형태를 가질 수 있는 능력

조상 타입 참조 변수로 자손 타입 객체를 다루는 것(부모는 자식을 담을 수 있다.)

자손 타입의 참조 변수로 조상 타입의 객체를 가리킬 수 없다.

Tv t = new Tv(); // 부모(멤버 개수 5개)
SmartTv s = new SmartTv(); // 자식(멤버 개수 7개) -> 자신의 멤버는 2개를 가지고 있음.

Tv t = new SmartTv(); // 타입 불일치 OK! 가능!
SmartTv s = new TV(); // 오류!

 

 

 

객체와 참조변수의 타입이 일치할 때와 일치하지 않을 때의 차이

SmartTv s = new SmartTv(); // 참조 변수와 인스턴스의 타입이 일치
Tv t = new SmartTv(); // 조상 타입 참조변수로 자손 타입 인스턴스 참조

 

첫번째 경우 SmartTv 리모콘(객체)은 버튼(맴버) 7개를 모두 사용할 수 있다. (모든 기능 사용가능)

두번째 경우 Tv 리모콘(객체)에는 버튼(멤버)가 5개 밖에 없다.

실제기능(멤버)은 7개를 가지고 있지만 Tv 리모콘은 5개 밖에 쓰지 못한다.

멤버는 7개를 가지고 있지만, 실제 멤버는 5개밖에 사용하지 못한다.(일부만 사용 가능)

 

 

자손 타입의 참조변수로 조상 타입의 객체를 가리킬 수 없다.

Tv t = new SmartTv(); // 허용 OK
SmartTv s = new TV(); // 에러! 허용 안됨

 

실제 가지고 있는 버튼(멤버) 개수(7개)보다 버튼(멤버 5개)이 적으면 안됨! => 동작을 안하기 때문에(없는거 호출했잖아!)

버튼이 있는데 기능이 안된다? => 안됨.

기능은 있는데 버튼이 없다? => 허용 OK

 

중간 정리

 

Q. 참조변수의 타입은 인스턴스의 타입과 반드시 일치해야 하나요?

A. No! 일치하는 것이 보통이지만 부모의 타입으로 자식의 객체를 생성할 수 있습니다.

 

Q. 참조변수가 조상타입일 때와 자손타입일 때의 차이?

A. 참조 변수로 사용할 수 있는 멤버의 갯수가 달라집니다. (7개 -> 5개)

 

Q. 자손 타입의 참조변수로 조상 타입의 객체를 가르킬 수 있나요?

A. 허용되지 않습니다.(부모는 자식을 담을 수 있지만, 자식은 부모를 담을 수 없다.)

 

 

참조 변수의 형변환

사용할 수 있는 멤버의 갯수를 조절하는 것

조상 자손 관계의 참조변수는 서로 형변환 가능!!

리모콘을 바꿔주는 것

class Car{
    String color;
    int door;
    void drive(){}
    void stop(){}
}
class FireEngine extends Car {
    void water(){}
    }
class Ambulance extends Car {}
FireEngine f = new FireEngine();
Car c = (Car)f; // Ok. 조상인 Car 타입으로 형변환(생략가능)
FireEngine f2 = (FireEngine)c; // OK. 자손인 FireEngine 타입으로 형변환(생략불가)

Ambulance a = (Ambulance)f; // 에러. 상속관계가 아닌 클래스 간의 형변환 불가, 형제 관계 그딴거 없음
Car car = null;
FireEngine fe = new FireEngine(); // 쓸 수 있는 멤버 5개
FireEngine fe2 = null;

fe.water(); 
car = fe; // car = (Car)fe; 자동 형변환 쓸 수 있는 멤버 4개
// car.water(); // Car 타입의 참조변수인 car로는 water() 사용불가
fe2 = (FireEngine)car; // 자손타입 <- 조상타입. 형변환 생략 불가, 쓸 수 있는 멤버 5개
fe2.water();

 

이렇게 리모콘을 바꿀 때마다 사용할 수 있는 멤버 개수가 줄었다 늘었다 한다.

(이걸로 업캐스팅, 다운캐스팅을 구분하나?)

Car c = new Car();
FireEngine fe = (FireEngine)c; // java.lang.ClassCastException(형변환 에러)
fe.water(); // 컴파일 오케이

 

될것 같지만 안된다!!!! (실체 객체의 멤버 개수인 4개를 넘으면 안된다.)

   실체 객체의 멤버의 갯수가 중요하다! (이것을 넘지 않으면 줄이고 늘리는 것은 상관없음)★  

 

 

instanceof 연산자

참조변수의 형변환 가능여부 확인에 사용, 가능하면 true 반환

형변환 전에 반드시 연산자로 확인 후 변환하는 습관을 기르자!

void doWork(Car c) { // doWork(new Car(), new FireEngine(), new Ambulance()) 모두 가능
                     // doWork(new FireEngine()); 은 Car c = new FireEngine(), doWork(c)와 동일
    if (c instanceof FireEngine) { // 1. 형변환이 가능한지 확인
        FireEngine fe = (FireEngine)c; // 2. 형변환
        fe.water();
        ...
    }
}

 

형변환을 하는 이유는 인스턴스의 원래 기능을 모두 사용하기 위해서!

Car 타입의 리모콘인 c로는 water( )를 호출할 수 없기 때문에 리모콘을 FireEngine타입으로 바꿔서 water( ) 호출.

FireEngine fe = new FireEngine();
System.out.println(fe instanceof Object); // true
System.out.println(fe instanceof Car); // true
System.out.println(fe instanceof FireEngine); // true

 

조상과 자기 자신의 형변환을 허용한다.

 

중간 정리

 

Q. 참조변수의 형변환은 왜 하나요?

A. 참조변수(리모콘)을 변경함으로써 사용할 수 있는 멤버의 갯수를 조절하기 위해서

(객체도 그대로, 주소값도 그대로 이다)

 

Q. instanceof 연산자는 언제 사용하나요?

A. 참조변수를 형변환하기 전에 형변환 가능여부를 확일할 때

 

 

 

그럼 이렇게 기능이 줄어드는데 어떻게 장점이 될 수 있을까??

tv리모콘으로 스마트 티비를 다룬다? => 어떤 장점이 있을까(2가지 장점)이 있다!!

'복습' 카테고리의 다른 글

[Java 복습] 예외처리  (1) 2024.05.02
[Java 복습] 내부 클래스  (0) 2024.05.02
[Java 복습] 인터페이스의 장점  (0) 2024.05.02
[Java 복습] 다형성의 장점  (0) 2024.04.30
[Java 복습] 클래스와 메서드  (0) 2024.04.28