[TIL] 220929 - 자바 : 객체지향프로그래밍, 객체, 상속, 다형성

yujamint·2022년 9월 29일
0
post-custom-banner

객체지향 3대 특징

  • 캡슐화 (encapsulation) / 정보은닉 : 외부에서 객체의 데이터 일부를 접근하지 못하게 한다.
  • 상속 (inheritance) : 부모-자식 관계를 맺어 코드의 중복을 줄인다 → 재사용성 향상
  • 다형성 (polymorphism) : 객체의 데이터 타입이 다양하게 올 수 있는 성질

객체는 필드(데이터)와 데이터를 기능하게 하는 메서드(함수)를 가진다.

Stduent 객체를 설계해보자.

  • 필드 - 이름 / 학번 / 학기 / 생년월일 / 수강과목 등
  • 메서드 - 이름 변경 함수 / 학번 변경 함수 / 학기 변경 함수 / 수강과목 철회 함수 등

학생마다 가지고 있는 데이터는 다를 수 있다.

클래스 : 객체가 가질 수 있는 필드와 메서드는 어떤 게 있다라고 정의해놓은 설계도

객체 : 설계도(클래스)를 통해 실제 행동하는 대상

객체 관계

  • is-a 관계 : 상속(inheritance)
    • SportCar is a Car (O) → SportCar 클래스가 Car 클래스를 상속받았다.
    • SportCar is a Person (X) → SportCar 클래스가 Person 클래스를 상속받지 않았다.
  • has-a 관계 : 연관
    • Circle has a Point (O) : Circle 객체를 만들기 위해서는 Point 객체가 필요하다.
    • Point 객체가 Circle 객체의 필드에 속한다.
  • use-a 관계 : 의존
    • Person uses a Car (O) : Person 객체의 특정 메소드를 사용하는 데에 Car 객체가 인자로 필요하다

객체 == 인스턴스??

객체 = new 사용자(); → 객체가 (new 연산자를 통해) 인스턴스화 되었다. → 객체가 메모리를 사용하고 있다.

객체는 필드로 가지고 있는 자료형의 메모리를 합친만큼의 메모리를 할당하고 있다.

  • 필드 : String name, int age → 8byte + 4byte = 12byte의 메모리 할당

생성자

객체 생성과 동시에 원하는 값으로 초기화할 수 있는 메소드

  • 객체의 필드를 모두 채우지 못 할 수도 있기 때문에 오버로딩을 통해 생성자를 다양하게 생성하는 것이 좋다

생성자 이름은 클래스 이름과 동일 Car car = new Car();

객체 비교

  1. Object.hashcode()

    객체는 각각의 고유한 id(해시코드)를 가진다. → 해시코드가 다르면 다른 객체이다.

    • 두 객체를 비교할 때 해시코드를 먼저 비교하면 빠른 연산이 가능하다.

    해시코드 또한 유효한 범위가 있기 때문에, 서로 다른 두 객체가 같은 해시코드를 가지게 되는 해시코드의 충돌이 발생할 수 있다.

  2. Object.equals()

    그렇기 때문에, 해시코드가 같더라도 즉시 같은 객체라고 판단하지 않고, Object.equals() 메서드를 통해 다시 한 번 비교한다.

    equals() 메서드에서는 객체의 내용물을 모두 비교하며 같은 객체인지 판단한다. → 내용물(필드)의 값이 모두 같으면 같은 객체이다.

    1. 객체의 클래스 비교
    2. 객체가 가지고 있는 int값 비교
    3. 객체가 가지고 있는 String값 비교

hashcode를 먼저 비교함으로써 equals() 메서드가 내용물까지 비교하는 과정을 생략할 수 있다.

String의 경우 두 객체를 비교할 때 값이 같으면 String Pool에서 같은 주소를 가지기 때문에 ‘==’을 통해 비교해도 올바른 결과값이 나오긴 한다.

객체 정렬

객체에 담긴 필드는 여러개가 있는데 이 중에서 어떤 것을 기준으로 정렬할지 정해야 된다.

→ Comparable 인터페이스의 compareTo 메서드를 구현하여 정렬 기준을 정한다.

o1.compareTo(o2)

  • o1 < o2 : 음수 반환
  • o1 == o2 : 0 반환
  • o1 > o2 : 양수 반환

양수 반환시 비교대상과 순서 바꾼다.

객체 복사

주소값만 복사하는 Shallow Copy를 통해 원본의 데이터가 수정되는 것을 주의!!

참고 : https://velog.io/@db_jam/Java-얕은-복사Shallow-Copy와-깊은-복사Deep-Copy

접근 제어자

  • private : 같은 클래스 멤버에서만 접근 가능
  • default : private + 같은 package 내에서 접근 가능
  • protected : default + 다른 package의 자식 클래스 멤버에서 접근 가능
  • public : 모든 곳에서 접근 가능

setter/getter

객체에 접근할 때, 필드를 숨기고 메소드를 통해서만 접근 가능하도록 한다.

  • 메소드 내부에서는 정보손상이나 유효하지 않은 데이터를 막는 로직을 구현할 수 있다.
    • if 또는 regex 통해
  • 메소드를 한 번 거쳐서 유효한 데이터만 필드에 등록될 수 있도록 함

→ 객체 모듈화 가능

→ 코드 이식성 좋아진다.

정적 필드와 메소드

클래스 멤버를 정적 필드로 선언하면 클래스 로딩시 같이 로딩되어 메모리 공통영역에 존재하게 된다. → 객체 생성 필요 없다.

선언 방법 - static 으로 선언, 접근제어자 static 타입필드 = 초기값;

static 변수를 선언해놓으면, 이는 메모리 공통영역에 한 번만 load 된다

→ 인스턴스를 생성할 때, 해당 변수에 대한 메모리를 할당할 필요가 없다.

→ 어떤 인스턴스든 해당 메모리에 접근할 수 있다.

—>메모리 관리에 용이하다!

static 필드를 반환하는 메소드 또한 static으로 선언해줘야 한다.

class Person {
	private static String name;

	// 생성자 생략..
	
	public static String getName(Person person){
		return person.name;
	} 
}

학생의 totalNum을 필드로 두고 싶을 때, 학생 하나하나가 이 정보를 필드로 가지고 있을 필요가 없다 → 정적 필드로 두고 모든 인스턴스가 접근할 수 있도록 한다.

클래스 로더에 의해 메인함수가 먼저 메모리에 올라가야 하기 때문에 메인함수는 public static void로 선언되어있는 것을 알 수 있다.

클래스로더 (클래스 / 정적변수 / 정적메소드 / 메인함수) → 함수가 스택에 쌓임 → 스택 해제 → 클래스로더 해제

클래스 로더 : 어떤 메모리를 먼저 할당해야 될지 정하는 역할을 한다.

싱글톤 패턴

객체를 하나만 두겠다. == 객체를 여러개 생성하지 않겠다.

목적

  • 전체 프로그램에서 하나의 객체만 보장해야하는 경우
  • 데이터베이스에 접근하는 객체 설계하는 경우 → 데이터베이스에 접근하는 객체는 여러개면 안 된다!

구현방법 1 - 동기화 문제 해결

멀티스레드 환경에서는 다른 스레드에서 객체를 생성하여 객체가 여러개 생성되지 않도록 주의해야 된다.

  • getInstance() 메서드를 사용해 이미 객체가 존재한다면, 해당 객체를 사용할 수 있도록 한다.
    • 객체가 존재하지 않는다면 생성
  • new 연산자를 통해 객체가 생성되는 것을 막기 위해 생성자의 접근제어자를 private으로 만든다.

이러한 방법도 JVM에서는 멀티스레드 환경에서 여러 스레들들 왔다갔다하면서 실행시키기 때문에 동시에 객체를 생성하는 상황이 발생할 수 있다.

어떻게 해결할까?

synchronized 키워드 → 한 스레드가 해당 작업 진행 중일 때는 다른 스레드가 같은 작업을 진행하지 못하도록 한다.

  • 속도가 매우 느려진다는 단점이 있다.

구현방법 2 - 처음부터 인스턴스 생성

인스턴스를 필요할 때 생성하지 않고, 처음 클래스 로딩시에 생성한다.

구현방법 3 - DCL(Double-Checking Locking)

volatile 키워드 → 매번 Read할 때마다 cache에 저장된 값이 아닌, 메인 메모리에서 읽고 변수 값을 Write 할 때마다 메인 메모리에 쓴다.

어노테이션

메타데이터(metadata) → 컴파일 과정과 실행 과정에서 코드를 어떻게 컴파일하고 처리할 것인지 알려주는 정보

상속 (inheritance)

부모가 가진 필드를 자식에게 물려주는 것

  • 부모 클래스 : super class
  • 자식 클래스 : sub class

자바 계층 구조 최상위에 java.lang.Object 클래스가 존재한다 → 자바에서 클래스를 생성하면 자동으로 Object 클래스를 상속받는다

→ 클래스를 생성하면 Object 클래스의 toString() / equals() 메소드를 사용할 수 있는 이유

장점

  • 코드의 중복을 줄이고 객체 간의 관계를 지향하기 위함
  • 코드의 수정 최소화

특징

  • 자바에서는 다중 상속 불가능(부모가 한 명만 가능)

오버라이드

부모로부터 상속한 메소드를 자식 클래스에 맞는 메소드로 재정의하는 것 ≠ 오버로드

규칙

  • 부모 메소드와 동일한 리턴타입 / 메소드 이름 / 매개변수를 가져야 함
  • 접근 제한을 더 강하게 오버라이딩할 수 없음
    • public > protected > default > private
  • 새로운 예외를 throws 할 수 없음

@Override 어노테이션 사용하면 부모에 있는 메소드인지 checking 할 수 있음

→ 부모에 있는 메소드가 아니면 컴파일 오류 발생

상속이 안 되는 경우

  • 부모 클래스의 private으로 정의된 필드,메소드
  • 부모 클래스 자체가 final로 정의된 경우
  • 부모 클래스 메소드가 final로 정의된 경우 재정의 불가능

다형성 (polymorphism)

Circle 클래스가 Shape 클래스를 상속받았을 때,

Shape shape = new Circle(); 은 가능하지만, Circle circle = new Shape(); 는 컴파일 오류가 발생한다.

전자의 경우, 실제 메모리는 Circle을 기준으로 할당받기 때문에 shape의 필드를 모두 상속받은 Shape의 필드에 접근할 수 있지만, 후자의 경우에는 실제 메모리를 Shape를 기준으로 할당받기 때문에 circle의 필드에 접근할 수 없을 수도 있다.

  • 전자의 경우는 자동 형 변환이 일어나는 것이다.

Shape shape = new Circle();

Shape 클래스에 draw() 메소드가 정의되어 있고, Circle 클래스가 draw() 메소드를 오버라이드 했을 떄, shpae.draw()를 호출하면 Circle이 오버라이드한 draw()가 호출된다.

→ 자바에서 가상 함수 매핑 테이블을 참조하며 동적 바인딩을 수행하게 되는데, 가상 함수 매핑 테이블에 자식이 오버라이드한 메서드가 있다면, 해당 메서드를 호출해준다.

자바에서는 모든 함수가 가상 함수로 되어있는데, 런타임 중에 가상 함수 매핑 테이블을 보고 어떤 함수를 호출할지 결정한다.

  • 자식이 메서드를 오버라이드하면 해당 함수를 새로운 주소값으로 메서드를 저장한다.

메서드를 생성하면 메모리의 Code 영역에 주소가 저장되고, 메서드의 주소가 가상 함수 매핑 테이블에서 관리되며 런타임 중에 각상 함수 매핑 테이블을 참조해서 어떤 함수 호출할지 결정한다.

추상 클래스

객체를 만들기에는 너무 추상적인 클래스 == 구체화할 수 없는 클래스

ex) Shape 클래스 : 단순히 Shape라는 정보만 가지고는 도형을 그릴 수 없다.

↔ 실체 클래스 : 실제 인스턴스가 생성될 수 있는, 구체화할 수 있는 클래스

객체로 생성하지도 못 하는 추상 클래스를 왜 만들까??

  • 자식 클래스들을 관리하기 위함
  • 자식들의 공통적인 특성을 추출해서 선언시킨다.
  • 서로 다른 클래스의 자식들을 하나의 객체변수로 관리할 수 있다.
    Shape[] shapes = {new Circle(), new Triangle(), new Rectangle()};
  • Circle, Triangle, Rectangle은 모두 추상 클래스인 Shape를 상속받은 클래스이다.

특징

  • 추상 메서드가 하나라도 존재하면, 추상 클래스여야 한다.
  • 추상 클래스를 상속받은 클래스는 추상 클래스의 추상 메서드를 구현(재정의)해야 된다.
    • 재정의하지 않으면 컴파일 오류
  • 추상 클래스는 new를 통해서 객체를 생성할 수 없다.
    • 익명 객체를 통해서 생성할 수는 있다. (new를 통해 객체 생성시 추상 메서드 정의) 이때 정의한 필드와 메서드는 일회성이다. (이후에 또 사용하려면 또 이렇게 정의하면서 생성해야됨)
      Shape trapezoid = new Shape() {
            Line[] lines;
      
            public Line[] getLines() {
                return lines;
            }
      
            public void setLines(Line[] lines) {
                this.lines = lines;
            }
      
            @Override
            public void draw() {
                System.out.println("trapezoid draw ...");
            }
      
            @Override
            public String toString() {
                return "$classname{" +
                        "lines=" + Arrays.toString(lines) +
                        ", centerPoint=" + centerPoint +
                        '}';
            }
        };

단점

  • 추상 클래스를 상속받은 자식 클래스들은 추상 메서드를 강제로 구현해야만 하기 때문에 확장성이 좋지 않다.

인터페이스

클래스를 상속 계층도는 아니지만 해당 객체가 특정 작업이 가능한가?에 따라 나눌 수 있다.

인터페이스 정의

  • 인터페이스의 접근제어자는 기본값이 public이다!
  • 상수 // 이 상수는 필드의 개념이 아니라, 메서드를 구현하기 위해서만 사용된다.
    • 인터페이스 필드 기본값 : static final
      • int a = 0;public static final int a = 0; 자동으로 변환
  • 추상 메서드
    • 인터페이스 메소드 기본값 : abstract
      • void renew();public abstract void renew(); 자동으로 변환
    • 구현 클래스에서 무조건 재정의 해야함
  • 디폴트 메서드
    • 구현 클래스에서 재정의 해도 되고, 하지 않아도 된다.
    • 해당 인터페이스를 구현하는 클래스에서 재정의하지 않으면 디폴트로 정의한 메소드 사용한다
    • 개방-폐쇄 원칙을 따라 만든 것이라고 볼 수 있다.
      • 개방-폐쇄 원칙 : 확장에 대해서는 개방(OPEN)되어 있고, 수정에 대해서는 폐쇄(CLOSE)되어 있어야 한다.
  • 정적 메서드
    • 구현 클래스에서 재정의 불가

상속과 같은 상속 계층도는 아니지만, 구현 클래스를 인터페이스에 담아 객체를 생성할 수 있다.

DriveLicenseAble driveLicenseAble = new UnivStudent();

인터페이스에 추상 메서드가 없다면 직접 new 해서 객체를 생성할 수 있을까?

  • 불가능하다 !
    → 개념 자체가 추상 메서드를 구현하기 위한 것이기 때문이다.
  • 객체를 생성하며 추상 메서드를 구현하면 가능하긴 하다. (익명 객체)

특징

  • 하나의 클래스에서 여러 인터페이스 구현 가능
  • 인터페이스끼리 상속 가능 (extends )
    • 인터페이스는 다중 상속이 가능하다.
profile
개발 기록
post-custom-banner

0개의 댓글