7. 객체지향 프로그래밍 (2)

KOO HEESEUNG·2021년 9월 27일
0

Java의 정석

목록 보기
2/8
post-thumbnail

1. 상속

정의 및 특징

기존 클래스를 재사용하여 새로운 클래스를 만드는 것. 두 클래스를 부모-자식관계로 짝지어주는 것.

extends 라는 키워드를 사용함.

class Child extends Parents {}
  1. 자식 클래스는 부모 클래스의 모든 멤버를 상속받는다.
  2. 자식 클래스의 멤버 개수는 항상 부모 클래스의 멤버 개수보다 많거나 같다.
  3. 자식 클래스의 변경은 부모 클래스에 영향을 미치지 않는다.

포함관계

클래스의 멤버로 다른 클래스 타입의 참조변수를 선언하는 것.

class Circle {
  Point p = new Point(); // 포함
  int r;
}

class Point {
  int x;
  int y;
}

상속과 그 구조가 다르다. 상속과 포함 모두 Circle 클래스를 생성하여 Point 객체의 변수를 사용할 수 있지만, 포함관계일 경우, Point의 멤버를 사용하기 위해 아래 예시처럼 Point의 참조변수 p를 거쳐야 한다.

Circle c = new Circle();
c.p.x = 3;
c.p.y = 4;
c.r = 1;

A와 B 두 클래스간 관계 결정할 때는 "A는 B이다."(상속) 혹은 "A는 B를 가지고 있다."(포함) 라고 문장을 만들어서 관계가 보다 명확한 쪽을 선택한다.

단일상속

Java는 클래스의 상속을 1개까지만 가능하도록 제한하고 있다. 다중상속을 허용할 경우, 다른 클래스 내에 같은 이름이면서 다른 내용의 메서드로 인한 충돌문제 등이 일어날 우려가 있기 때문이다.

Java에서 다중상속처럼 여러 클래스와 관계를 맺어주고 싶다면 비중이 높은 클래스를 상속하고, 다른 클래스들은 포함을 사용하면 된다.

Object 클래스

모든 클래스의 최고 조상.

  1. 부모 클래스가 없을 경우, 컴파일러가 자동으로 Object 클래스를 상속해준다.
  2. 모든 클래스는 Object 클래스의 멤버들을 사용할 수 있다.(toString(), equals() 등... )

2. 오버라이딩

상속받은 조상의 메서드를 자신에 맞게 변경하는 것.

조건

  1. 조상 클래스의 메서드와 선언부가 일치해야 한다.
  2. 접근 제어자를 조상 클래스의 메서드보다 좁은 범위로 설정할 수 없다.
  3. 예외처리는 조상 클래스의 메서드보다 많이 선언할 수 없다.

오버로딩? 오버라이딩?

오버로딩과 오버라이딩은 이름만 비슷할 뿐 근본적으로 아무 관련이 없다.

오버로딩 : 메서드명만 같은 새로운 메서드를 만드는 것

오버라이딩 : 기존의 메서드를 상속받아 내용을 자신에 맞게 변경하는 것


3. super / super()

참조변수 super

조상의 멤버와 자손의 멤버를 구분하기 위한 참조변수.

생성자 super()

조상 클래스의 생성자.

조상의 멤버는 조상의 생성자 super()를 사용하여 초기화하는 것이 바람직하다.

생성자의 첫 줄에 반드시 생성자를 호출해야 한다. 생성자를 호출하지 않을 경우, 컴파일러가 자동으로 super();를 삽입하며, 이때 조상 클래스에 기본 생성자가 없으면 컴파일 에러가 발생한다.

class Point {
  int x; int y;
  
  Point(int x, int y) {
    this.x = x;
    this.y = y;
  }
}

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

4. 패키지

패키지

서로 관련된 클래스들의 묶음.

클래스는 클래스 파일(*.class), 패키지는 폴더.

클래스의 실제 이름은 패키지명.클래스명(ex> java.lang.String.class)

패키지 선언은 소스 코드 맨 첫줄에 딱 한번만 선언한다.

같은 소스파일의 클래스들은 모두 같은 패키지에 속하고, 선언이 없으면 이름없는 패키지에 속한다.

클래스 패스(classpath)

클래스 파일(*.class)의 위치를 알려주는 경로.

환경변수 CLASSPATH로 관리하며, 경로간 구분자는 세미콜론(;)을 사용한다.

import문

다른 패키지의 클래스를 사용하려면 패키지명이 포함된 클래스 이름을 사용하는데, import문을 사용하여 해당 클래스의 패키지를 명시하면 패키지명을 생략할 수 있다.

import 패키지명.클래스명;
// 또는
import 패키지명.*; // 모든 클래스를 의미

그럼 왜 String과 같은 일부 클래스들은 import하지 않고도 사용이 가능할까?

👉 java.lang패키지(기본 패키지)의 클래스는 import하지 않고도 사용이 가능하기 때문.

static import문

import문에 static을 붙이면 해당 클래스의 static 멤버들을 호출할 때 클래스 이름 생략이 가능하다.

예)

import static java.lang.System.out;

class Example {
  public static void main(String[] args) {
    out.println("TEST"); // = System.out.println("TEST");
  }
}

5. 제어자(modifier)

제어자

클래스와 클래스 멤버에 부가적인 의미를 부여. 영어의 형용사와 같은 역할.

접근제어자와 그 외의 제어자로 분류.

접근 제어자그 외
public, protected, (default), privatestatic, final, abstract 등...

하나의 대상에 여러 제어자를 붙일 수 있음. 그러나 접근 제어자는 4개 중 단 1개만 사용 가능.(범위에 대한 것이기 때문)

제어자의 순서는 자유롭지만, 관례적으로 접근 제어자를 제일 왼쪽에 작성.

1. static

의미 : 클래스의, 공통적인

붙일 수 있는 곳 : 멤버변수, 메서드, 초기화 블럭
image

2. final

의미 : 마지막의, 변경될 수 없는

붙일 수 있는 곳 : 클래스, 메서드, 멤버변수, 지역변수
image

3. abstract

의미 : 추상적인

붙일 수 있는 곳 : 클래스, 메서드
image

접근 제어자

클래스 또는 멤버의 외부 접근 가능 범위를 설정한다.

public > protected > (default) > private 순으로 범위가 넓다.

접근 제어자범위
private같은 클래스 내에서만 접근 가능
(default)같은 패키지 내에서만 접근 가능
protected같은 패키지 + 다른 패키지 자손 클래스에서 접근 가능
public접근 제한 없음

캡슐화 ⭐️

접근 제어자를 사용하는 이유는 외부로부터 데이터를 보호하기 위함이다.

예를 들어, Time이라는 클래스에 hour, minute, second라는 변수가 있다고 할 때, 해당 변수들을 public으로 둔다면 외부로부터 범위에 어긋나는 값을 지정할 수 있기 때문에 해당 변수들을 private으로 두어 외부 접근을 막고, 메서드를 사용한 간접 접근만을 허용함으로써 데이터를 유효한 값으로 유지한다.

이를 캡슐화라고 하며, 캡슐화를 통해 데이터가 유효한 값을 유지할 수 있다.

예)

public class Time {
  private int hour;  // private을 통해 직접 접근을 막는다.
  private int minute;
  private int second;
  
  public int setHour(int hour) {
    if (hour < 0 || hour > 23) return; // hour의 범위를 설정
    this.hour = hour;
  }
}

6. 다형성 ⭐️

다형성

조상 타입의 참조변수로 자손 타입의 객체를 다루는 것.

조상, 자손 관계의 참조변수는 서로 형변환이 가능하다. 참조변수의 형변환을 통해 참조변수가 다룰 수 있는 멤버의 개수를 조절할 수 있다. 참조변수의 타입이 생성한 객체의 조상 타입이라면, 조상이 갖고 있는 멤버만큼만 다룰 수 있는 것.

instanceof

참조변수를 형변환할 때에는 해당 타입으로의 형변환 가능여부를 반드시 확인한 후에 형변환해야 한다. 이때 사용하는 것이 instanceof 연산자이다.

장점

1. 다형적 매개변수

​ 참조형 매개변수는 메서드 호출시 자신과 같은 타입 또는 자손 타입의 인스턴스를 넘겨줄 수 있다.

​ 아래와 같은 경우, 다형성에 의해 메서드 buy에서 매개변수 p에는 Product 타입을 상속받은 Tv와 Computer를 받을 수 있다.

class Product {
  int price;
}
class Tv extends Product {}
class Computer extends Product {}

class Buyer {
  int money = 1000;
  
  void buy(Product p) {
    money -= p.price;
  }
}

2. 여러 종류의 객체를 배열로 다루기

​ 조상 타입의 배열에 자손 타입의 객체를 담을 수 있다.


7. 추상 클래스/인터페이스

추상 클래스

추상 메서드를 갖고 있는 클래스. 미완성 설계도.

제어자 abstract를 붙이며, 추상 클래스는 다른 클래스에서 상속받아 모든 추상 메서드의 구현부를 작성하여 완성하지 않는 이상 객체를 생성할 수 없다.

추상 메서드

구현부가 작성되지 않은 메서드. 제어자 abstract를 붙인다.

abstract void method();

인터페이스

추상 메서드의 집합. 멤버변수를 가질 수 없고, 오직 추상 메서드만 존재할 수 있다.

class 대신 interface라는 키워드를 사용한다.

인터페이스의 모든 멤버변수는 public static final 이고, 모든 메서드는 public abstract 이기 때문에 해당 제어자들을 일부 또는 전부 생략할 수 있다.

추상 클래스 vs 인터페이스?

추상 클래스는 '추상 메서드를 갖고 있는 클래스'로, 멤버변수나 생성자 등을 가질 수 있지만, 인터페이스는 '추상 메서드의 집합'으로, 오직 추상 메서드만 가질 수 있다.
( JDK 1.8부터 인터페이스에 상수나 디폴트 메서드, static 메서드를 추가할 수 있게 되었지만, 이는 핵심이 아님. )

인터페이스의 구현

인터페이스의 조상은 인터페이스만 가능하기 때문에 Object가 최고조상이 아니다.

인터페이스 또한 다른 클래스들처럼 상속이 가능하며, 인터페이스의 상속은 implements라는 키워드를 사용하고, 구현이라는 표현을 사용한다.

인터페이스는 다중 상속이 가능하다. 메서드의 구현부가 전혀 없는 추상 메서드의 집합이기 때문에, 충돌이 발생하더라도 문제가 되지 않는다.

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

인터페이스 타입의 매개변수는 해당 인터페이스를 구현한 클래스의 객체만 받을 수 있다.

리턴타입이 인터페이스일 경우, 해당 인터페이스를 구현한 클래스의 객체를 반환한다.

장점

인터페이스는 두 객체의 중간역할을 한다. 선언과 구현을 분리시킬 수 있어 유연한 코드가 되고, 변경에 유리해진다.

  1. 개발 시간을 단축할 수 있다.
  2. 변경에 유리한 유연한 설계가 가능하다.
  3. 표준화가 가능하다.(ex> JDBC)
  4. 서로 관계없는 클래스들간에 관계를 맺어줄 수 있다.

디폴트 메서드

interface Example {
  void abstractMethod();
  default void defaultMethod() {} // 디폴트 메서드
}

앞에 키워드로 default를 붙이고, 구현부({})가 존재해야 한다.

JDK 1.8부터 인터페이스에 추가할 수 있게 되었다. 기존에 인터페이스에 추상 메서드를 추가할 경우, 해당 인터페이스를 구현한 클래스들에 일일이 구현해주어야 하기 때문에 추상 메서드를 추가하기가 어려웠던 문제를 해결하기 위함이다.

인터페이스간 디폴트 메서드가 충돌하는 경우, 디폴트 메서드를 오버라이딩하여 해결한다.

디폴트 메서드와 조상 클래스 메서드가 충돌하는 경우, 조상 클래스 메서드가 우선하고, 디폴트 메서드는 무시된다.

👉 그냥 필요한 쪽의 메서드와 같은 내용으로 오버라이딩하는 것이 편리


8. 내부 클래스

내부 클래스

클래스 안의 클래스.

특정 클래스에서만 사용될 경우, 해당 클래스의 내부에 넣어주어 코드의 복잡성을 줄일 수 있다.(캡슐화)

내부 클래스에서는 외부 클래스 객체 생성 없이도 외부 클래스의 멤버들을 이용 가능하다.

class Outer { // 외부 클래스
  ...
  class Inner { // 내부 클래스
    ...
  }
}

내부 클래스 종류는 인스턴스 클래스, static 클래스, 지역 클래스, 익명 클래스가 있으며, 인스턴스 클래스, static 클래스, 지역 클래스는 6장의 변수의 종류와 유효범위가 동일하다.

원래 클래스 앞에는 접근 제어자로 (default)와 public 밖에 사용할 수 없지만, 내부 클래스는 변수에 사용 가능한 것과 동일하게 모든 접근 제어자를 사용 가능하다.

class Outer {
  class InstanceInner {} // 인스턴스 내부 클래스
  static class StaticInner {} // static 내부 클래스
  
  void myMethod() {
    class LocalInner {} // 지역 내부 클래스
  }
}

static 내부 클래스에만 static 멤버를 정의할 수 있지만(final static은 상수이므로 다른 내부 클래스도 허용), 외부 클래스의 인스턴스 멤버에는 접근할 수 없다.

익명 클래스

이름 없는 일회용 클래스. 생성과 정의를 동시에 한다.

이름이 없기 때문에 조상클래스나 구현한 인터페이스 이름을 사용한다.

new 조상클래스이름(or 구현한 인터페이스 이름) {
  ...
}

0개의 댓글