객체지향 프로그래밍 - 3

cutiepazzipozzi·2023년 4월 27일
0

지식스택

목록 보기
24/35
post-thumbnail

앞단원이 객체지향을 이해하기 위한 밑바탕이었다면, 7단원이야 말로 객체지향 프로그래밍의 특징을 깊이 있게 이해하는 단원인 거 같다(두근)

[상속]

= 기존의 클래스를 재사용하여 새로운 클래스를 작성

  • 적은 양의 코드로 새로운 클래스 작성 가능

  • 코드를 공통으로 관리할 수 있어 추가 및 변경이 용이
    => 재사용성 높이고, 코드의 중복 제거 => 유지보수 Good
    (앞선 단원에서의 가졌던 궁금증의 이유가 바로 풀려버리는,,)

  • extends 키워드와 함께 새로 만들 클래스와 기존의 클래스를 작성해주면 됨.
    ex. class Child extends Parent {}
    여기서는 Child가 새로운 클래스, Parent가 상속받고자 하는 클래스!

조상 클래스: 부모 클래스, 상위 클래스
자손 클래스: 자식 클래스, 하위 클래스

클래스간의 관계 - 포함관계

상속 이외에 클래스를 재사용 하는 방법이 있다. 이것이 바로 포함관계를 맺어주는 것!
포함 = 한 클래스의 멤버 변수로 다른 클래스를 선언하는 것

class Car {
	Engine e = new Engine();
    Door[] d =  new Door[4];
}
  • 클래스 작성 및 코드가 간결하다.
  • 단위 클래스 별로 코드가 작게 나뉘어 코드 관리에도 수월하다.
  • 다른 클래스를 작성하는데 재사용될 수 있다.

클래스간의 관계 결정하기

우리는 앞서 설명한 상속포함 중 어떤 관계를 맺어줄 것인지 결정해야 한다.

상속관계 = ~는 ~이다 (is-a)
포함관계 = ~는 ~을 가지고 있다 (has-a)

우리가 관계를 고민하는 두 클래스에 대해 윗 문장에 대입하여 생각해보면 된다.

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

class Circle {
	Point center; //Point 클래스를 `포함`하여 Circle 클래스 생성
    int r;
    
    Circle() {
    	this(new Point(0, 0), 100);
    }
    
    Circle(Point center, int r) {
    	this.center = center;
        this.r = r;
    }
}

단일 상속

다중 상속시 서로 다른 클래스로부터 상속받은 멤버 간의 이름이 같을 때 구별할 수 없기 때문에, 자바에서는 단일 상속을 원칙으로 한다.

물론 다중 상속의 장점을 가져갈 수 없으므로 불편한 점도 있겠지만, 클래스 간의 관계가 보다 명확해지고 코드를 더욱 신뢰할 수 있게 만들어준다는 큰 장점을 가지게 된다!

//상속과 포함 관계가 모두 적용된 클래스 예시
class Tv {
	boolean power;
    int channel;
    
    void power() {power != power;}
    void channelUp() {++channel;}
    void channelDown() {--channel;}
}

class VCR {
	boolean power;
    int counter = 0;
    
    void power() {power != power;}
    void play() {}
    void stop() {}
    void rew() {}
    void ff() {}
}

class TVCR extends Tv { //extends = 상속
	VCR vcr = new VCR(); //포함
    int counter = vcr.counter;
    
    void play() {vcr.play();}
    void stop() {vcr.stop();}
    void rew() {vcr.rew();}
    void ff() {vcr.ff();}
}

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

Object클래스는 모든 클래스 상속계층도의 제일 위에 위치하는 조상클래스이다. 다른 클래스로부터 상속 받지 않는 클래스가 있다면 자동적으로 Object 클래스를 상속받는다.
(ex. 우리가 String 문자열을 비교할 때 equals를 사용했던 것이나, 어떤 자료형을 String으로 바꿀 때 사용했던 toString 모두 Object 클래스 안에 있었기 때문에 바로 사용이 가능했던 것..!)

[오버라이딩]

= 조상 클래스로부터 상속받은 메서드의 내용을 변경하는 것
= 덮어씌우기

class Point {
	int x, y;
    String getLoc() {
    	return "x :"+x+", y :"+y;
    }
}
class Point3D extends Point {
	int z;
    String getLoc() {
    	return "x :"+x+", y :"+y+", z:"+z;
    }
}

오버라이딩의 조건

  1. 이름이 같아야 한다.
  2. 매개변수가 같아야 한다.
  3. 리턴타입이 같아야 한다.

추가 조건

  1. 접근 제어자는 조상 클래스의 메서드보다 더 좁은 범위로 변경할 수 X
  2. 조상 클래스의 메서드보다 많은 수의 예외를 선언할 수 X
    -> 이때 Exception을 예외로 던지면 모든 예외의 최고 조상이기 때문에 가장 많은 개수의 예외를 던지도록 선언한 것이므로 주의!!!
  3. 인스턴스 메서드를 static 메서드 or 반대로 변경할 수 X

오버라이딩 vs 오버로딩

오버라이딩 = 덮어쓰기 (기존에 있던 메서드 내용 변경)
오버로딩 = 생성 (기존에 없던 새로운 메서드 정의)

super

super는 자손 클래스에서 조상 클래스로부터 상속받은 멤버 참조에 사용되는 참조 변수이다.

  • this처럼 super도 상속받은 멤버와 본인 클래스의 멤버 이름이 같을 때 구별하기 위해 super를 써주면 된다.
  • superthis와 마찬가지로 인스턴스 메서드에서만 사용할 수 있다.
class Ex2 {
	public static void main(String[] args) {
    	Child c = new Child();
        c.method();
    }
}

class Parent {int x = 10;}

class Child extends Parent {
	int x = 20;
    void method() {
    	System.out.println("x="+x); 
        System.out.println("this.x="+this.x);
        System.out.println("super.x="+super.x);
    }
}
// 출력결과
//20 줄바꿈 20 줄바꿈 10
//super는 상속받은 부모 클래스의 멤버 참조에 사용되니까!!

super()

= 조상 클래스의 생성자

  • 자손 클래스의 생성자에서 조상 클래스의 생성자가 호출돼야 함
    (보통 첫 줄에서 미리 호출되어야 하는데 왜냐면 자손 클래스 안의 멤버가 조상 클래스의 멤버 변수를 언제 참조할 지 모르기 때문)
    ** 생성자가 정의되어 있는 클래스에는 컴파일러가 기본 생성자를 자동적으로 추가하지 않기 때문에 자손 클래스의 생성자에서 super()를 꼭 넣어줘야함
class Point {
	int x = 10;
    int y = 20;
    
    Point(int x, int y) {
    	super(); // 이렇게 되면 최상위 조상 클래스 Object를 불러옴
    	this.x = x;
        this.y = y;
    }
}

class Point3D extends Point {
	int z = 30;
    
    Point3D() {
    	this(100, 200, 300);
    }
    
    Point3D(int x, int y, int z) {
    	super(x, y);
        this.z = z;
    }
}
  1. 클래스 - 어떤 클래스의 인스턴스를 생성할 것?
  2. 생성자 - 선택한 클래스의 어떤 생성자를 이용해서 인스턴스를 생성할 것?

[package와 import]

패키지

= 클래스의 묶음 (클래스 or 인터페이스)
ex. String 클래스 -> java.lang.String
= java.lang 패키지에 속한 String 클래스

  • 서로 관련된 클래스들끼리 묶음으로써 클래스를 효율적으로 관리
  • 클래스가 물리적으로 하나의 파일(.class)인 것과 같이 패키지도 물리적으로 하나의 디렉토리
  • 패키지도 다른 패키지를 포함할 수 있다
    ex. .으로 구분되는 -> java . lang (java 패키지의 하위 패키지)
  • 모든 클래스는 반드시 하나의 패키지에 속해야 한다.
# 궁금해서 ChatGPT에게 물어본 반드시 패키지에 속해야 하는 이유
1. 이름 충돌 방지: 패키지는 클래스의 이름 충돌을 방지하는 데 사용됩니다. 서로 다른 패키지에 있는 두 개의 클래스가 동일한 이름을 가질 수 있습니다. 이렇게 하면 두 클래스를 구분할 수 있습니다.

2. 클래스의 가시성 제어: 패키지는 클래스의 가시성을 제어하는 데 사용됩니다. 클래스를 패키지 내부에 두면 다른 패키지에서 접근할 수 없습니다. 이는 객체 지향 프로그래밍의 캡슐화 개념과 일치합니다.

3. 코드 관리 및 유지 보수: 클래스를 패키지에 그룹화하면 코드 관리 및 유지 보수가 쉬워집니다. 특정 기능 또는 관련된 클래스를 포함하는 패키지를 만들고 그 패키지에 대한 문서를 작성하면 코드를 사용하는 다른 개발자들이 코드를 더 쉽게 이해할 수 있습니다.

패키지의 선언

= package 패키지명

  • 선언문은 소스파일에서 주석, 공백 제외 첫 번째 문장
  • 1소스파일 1선언
  • 모든 클래스는 하나의 패키지에 반드시 포함되어야 하나 패키지를 선언하지 않았을 때도 오류가 발생하지 않았다면 이는 자바 컴파일러에서 기본적으로 제공하는 이름없는 패키지 때문!
    (컴파일러에서 패키지가 없다면 이름없는 패키지에 포함시켜 컴파일)
    = 패키지를 지정하지 않은 클래스끼리는 같은 패키지에 속함

++ 실행시 java -d . 파일이름.java로 실행한다.
-d옵션은 소스파일에 지정된 경로를 통해 패키지의 위치를 찾아 클래스 파일을 생성한다. 만약 일치하는 디렉토리가 존재하지 않는다면 자동적으로 생성!

클래스 패스

= 컴파일러, JVM, .. 이 클래스의 위치를 찾는데 사용하는 경로
=> 내 컴퓨터 어디에서도 Java 라이브러리? 패키지?를 사용하기 위해

import문

= 사용하고자 하는 클래스의 패키지를 미리 명시하기 위해 사용
= 컴파일러에게 소스파일에 사용된 클래스의 패키지에 대한 정보를 제공
** 컴파일 시간을 조금 늘릴 뿐, 성능과는 아무런 관련 X !

선언

  1. package문 | 2. import문 | 3. 클래스 선언
    import 패키지명.클래스명 or import 패키지명.*
  • 이때 *을 사용하는 것이 하위 패키지의 클래스까지 포함하라는 말 X !!
  • 우리가 굳이 import문을 작성하지 않아도 적용되는 java.lang과 같은 패키지들은 빈번히 사용되는 중요한 패키지이므로 묵시적으로 선언된다.

[제어자]

= 클래스, 변수, or 메서드의 선언부에 사용되어 부가적인 의미 부여

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

야무지게 설명해놓은 static과 final ㅎㅎ
-> 옛날에 차이점이 궁금해서 정리해놓은 글이다! 뉘앙스를 이해할 수 있어서 위의 글을 미리 읽고 와도 좋을 것 같당.

static -> 공통적인

= 클래스의, 공통적인

  • 인스턴스 변수는 하나의 클래스로부터 생성되어도 다른 값을 유지하지만, 클래스 변수는 인스턴스에 관계없이 같은 값을 가짐!
    (클래스 변수는 모든 인스턴스가 공유하고 있기 때문)

  • static이 붙었다면 인스턴스를 생성하지 않고도 사용 가능!!
    -> 멤버변수, 메서드, 초기화 블럭 에서 사용

  • 인스턴스 멤버를 사용하지 않는 메서드가 있다면 static 메서드로 바꾸면 좋음
    (static 블럭은 클래스가 메모리에 로드될 때 한번만 수행되고, 주로 클래스 변수를 초기화하는데 사용됨)

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

  • 변수에 사용되면 값을 변경할 수 없는 상수, 메서드에 사용되면 오버라이딩을 할 수 없고, 클래스에 사용되면 자신을 확장하는 자손 클래스를 정의하지 못한다.
    ex. String, Math 클래스도 final 클래스이다..!
    -> 클래스, 메서드, 멤버변수, 지역변수 거의 모두 사용 가능!!

생성자를 이용한 final 멤버변수 초기화

= 각 인스턴스마다 final이 붙은 멤버변수가 다른 값을 갖게 할 수 있음!!
-> final변수는 상수이므로 일반적으로 선언+초기화를 한번에 하지만, 인스턴스 변수라면 생성자에서 초기화되도록 할 수 있다.

class Card {
	final int NUMBER;
    final String KIND;
    // 상수지만 여기서 값을 초기화 하지 않고
    static int width = 100;
    static int height = 250;

	Card(String kind, int num) {
    //매개변수로 넘겨받은 값으로 초기화한다!
    	KIND = kind;
        NUMBER = num;
    }
    
    Card() {
    	this("HEART", 1);
    }
}

abstract - 미완성

= 메서드의 선언부만 작성하고 실제 수행 내용은 구현하지 않은 추상메서드
-> 클래스, 메서드에서 사용!

abstract class AbstractTest {
	abstract void move();
}

접근 제어자

= 해당 멤버 or 클래스를 외부에서 접근하지 못하도록 제한

private: 같은 클래스 내에서만 사용
default: 같은 패키지 내에서만 사용
protected: 같은 패키지 내, 다른 패키지의 자손클래스에서 접근
public: 접근 제한 없다
<아래로 갈수록 접근 범위가 넓다>

접근 제어자를 이용한 캡슐화

  • 우리는 접근 제어자를 클래스 내부에 선언된 데이터를 보호 하기 위해 사용함
  • 클래스 내에서만 사용되는, 내부 작업을 위해 임시로 사용되는 멤버변수or 부분작업을 처리하기 위한 메서드 등의 멤버를 클래스 내부에 감추기 위해

= 데이터 감추기 = (객체지향) 캡슐화

public class Time {
	//아래의 멤버 변수들의 접근 제어자를 private로 하여 외부에서 
    //접근하지 못하도록!
	private int hour;
    private int minute;
    private int second;

	public int getHour() {return hour;}
    public void setHour(int hour) {
    	if(hour < 0 || hour > 23) return;
        this.hour = hour;
    }
    public int getMinute() {return minute;}
    public void setMinute(int minute) {
    	if(minute < 0 || minute > 59) return;
        this.minute = minute;
    }
    public int getSecond() {return second;}
    public void setSecond(int second) {
    	if(second < 0 || second > 59) return;
        this.second = second;
    }
}

++ 위의 예시에서 set메서드는 원하는 조건에 맞는 값을 때만 멤버변수에 값을 대입하도록 하였다.
++ 만약 상속을 고려한다면 private가 아닌 protected로 바꿔주는 것이 적절하다. private라면 자손 클래스에서도 접근이 불가능하기 때문이다!!

생성자의 접근 제어자

= 생성자에 접근 제어자를 사용하여 인스턴스의 생성을 제한한다.

class Singleton {
	private Singleton() {//..}
    //클래스 내부에서'만' 인스턴스 생성이 가능
    //(외부에서 생성자에 접근할 수가 없음
}

class Singleton {
	private static Singleton s = new Singleton();
    private Singleton() {}
    
    //여기서 사용하기 위해 위에서 미리 static으로 인스턴스 생성
    public static Singleton getInstance() {return s;}
}

그래서 실제로 사용할 때는
class SingletonTest {
	public static void main(String[] args) {
    	Singleton s1 = Singleton.getInstance();
    }
}

//생성자가 private라면 다른 클래스의 조상도 될 수 없기 때문에
//클래스 앞에 final을 추가하여 상속할 수 없음을 알리면 좋음
ex.
public final class Math {
	private Math() {//...}
}

제어자의 조합

  1. 메서드에 static과 abstract를 함께 사용할 수 없다.
    = static은 구현부가 있는 메서드에서만 사용할 수 있음
  2. 클래스에 abstract와 final을 동시에 사용할 수 없다.
    = 클래스에서의 final은 클래스를 확장할 수 없다는 의미,
    abstract는 상속을 통해 완성돼야 한다는 의미
    = it's 모순!
  3. abstract메서드의 접근 제어자가 private일 수 없다.
    = 자손 클래스에서 구현해주어야 하는데 private면 조상 클래스에 접근할 수 없음
  4. 메서드에 private와 final을 같이 사용할 필요는 없다.
    = 애초에 private라면 오버라이드가 불가하기 때문에 둘 중 하나만 사용해도 의미는 충분히 전달됨

참조

자바의 정석, 2nd Edition.

profile
노션에서 자라는 중 (●'◡'●)

0개의 댓글