[Java] 자바 문법 종합반 3주차

Yuri·2025년 1월 2일

Java

목록 보기
11/13
post-thumbnail

설계도(클래스)

객체지향 프로그래밍

📌 Java → '현실세계' 와 닮아있다.
필요한 부품(객체)들을 만들고 하나씩 조립해서 하나의 완성된 프로그램을 만드는 기법

객체

  • 객체는 세상에 존재하는 물체를 뜻하며 식별이 가능한 것

    • 물리적으로 존재하는 것 (자동차, 도서관, 계산기 ...)
    • 개념적으로 존재하는 것 (강의, 배달주문)
  • 객체는 속성행위로 구성이 되어있다.

  • 예) 자동차

    • 회사, 모델, 색상, 가격, 속도 (속성 = 필드)
    • 가속, 브레이크, 기어변속, 경적 (행위 = 메서드)
  • UML(Unified Modeling Language) 객체 다이어그램

    → 이처럼 현실 세계에 있는 객체를 소프트웨어의 객체로 설계하는 것을 '객체 모델링'이라고 부른다.

  • 객체 간의 협력
    사람이라는 객체와 자동차라는 객체는 서로 행위를 통해 상호작용을 하며 협력할 수 있습니다.

- 사람이 자동차의 가속 페달을 밟으면 자동차는 속도를 올리며 앞으로 이동한다.(gasPedal(50);)
- 사람이 자동차의 브레이크 페달을 밟으면 자동차는 속도를 줄이며 정지한다.(brakePedal();)

💡 사람은 자동차의 가속, 감속의 정확한 작동 원리까지 알 필요가 없다!

  • 또한 소프트웨어의 객체들은 메서드를 통해 데이터를 주고 받을 수도 있다.

  • 사람 객체는 메서드를 호출할 때 () 안에 데이터를 넣어 호출할 수 있는데 이때 이 괄호 안에 넣는 데이터를 '파라미터' 혹은 '매개값'이라고 표현한다.

  • 또한 자동차 객체는 속도를 바꾸는 작업을 수행한 후 사람 객체에게 실행 결과인 속도의 값을 반환할 수 있다. 이때 반환되는 값을 '리턴값'이라고 표현한다.
    (⇒ 자동차는 '리턴값'을 반환하지만 그것을 활용할지는 받는 쪽에서 선택할 수 있다.)

  • 객체 간의 관계

    • 사람↔자동차 : 별개의 개념, '사용 관계'
    • 자동차 객체를 뜯어본다면? 포함 관계
      자동차←(타이어/문/핸들): 자동차는 타이어와 문과 핸들을 부품으로 필요로 한다.
    • 좀 더 큰 개념으로 생각하면? 상속 관계
      이동수단←자동차←전기차: 하위 단계로 갈수록 구체화된다.

객체지향 프로그래밍의 특징
캡슐화, 상속, 다형성, 추상화

💊 캡슐화 (Encapsulation)

속성(필드)와 행위(메서드)를 하나로 묶어 객체로 만든 후 실제 내부 구현 내용은 외부에서 알 수 없게 감추는 것을 의미한다.

  • 외부 객체는 캡슐화된 객체의 내부 구조를 알 수 없기 때문에 노출된 필드나 메서드를 통해 접근할 수 있다.
  • 필드와 메서드를 캡슐화 하여 숨기는 이유는 외부 객체에서 해당 필드와 메서드를 잘못 사용하여 객체가 변화하지 않게 하는데 있습니다.
  • Java에서 캡슐화된 객체의 필드와 메서드를 노출 시킬지 감출 지 결정하기 위해 접근 제어자를 사용한다.

💰 상속

부모 객체자식 객체가 존재

  • 부모 객체는 가지고 있는 필드와 메서드를 자식 객체에 물려주어 자식 객체가 이를 사용할 수 있도록 만들 수 있다.
  • 자식 객체는 부모가 물려준 필드와 메서드 중 필요한 것만 선택하여 가공하여 사용할 수 있다 (overriding)

상속을 하는 이유

1. 각각의 객체를 상속 관계로 묶음으로써 객체 간의 구조 파악이 쉬움
2. 필드와 메서드를 변경하는 경우 부모 객체에 있는 것만 수정하면 자식 객체 전부 반영되기 때문에 일관성 유지에 용이
3. 자식 객체가 부모 객체의 필드와 메서드를 물려받아 사용할 수 있기 때문에 코드의 중복이 줄어들며 코드의 재사용성이 증가됨

🍞 다형성

객체가 연산을 수행할 때 하나의 행위에 대해 각 객체가 가지고 있는 고유한 특성에 따라 다른 여러가지 형태로 재구성 되는 것을 의미

동물은 모두 소리를 낸다. 하지만, 각 객체의 고유한 특성에 따라 강아지는 멍멍, 고양이는 야옹, 소는 음메하고 소리를 낸다.

💭 추상화

객체에서 공통된 부분들을 모아 상위 개념으로 새롭게 선언하는 것을 추상화라고 한다.

  • 공통적이고 중요한 것들을 모아 객체를 모델링한다.
  • 각 객체가 공통적으로 가지고 있는 속성, 행위를 모아 추상 객체를 모델링 할 수 있다.

객체와 클래스

  • 객체를 생성하기 위해서 '설계도' 가 필요하다
  • 자동차를 만들기 위해 자동차 설계도를 토대로 자동차를 생산한다. (예: 캐스퍼, 소나타, 아반떼…)
  • 소프트웨어에서도 객체를 만들기 위해서는 설계도에 해당하는 클래스가 필요하다.

클래스를 기반으로 생성(인스턴스화)된 것을 객체(인스턴스)라고 부른다.

클래스 설계

클래스의 구성은 필드, 생성자, 메서드가 있다.

  • 클래스 만들기 4 STEP
1. 만들려고 하는 설계도를 선언한다. (클래스 선언)
2. 객체가 가지고 있어야 할 속성(필드)를 정의한다.
3. 객체를 생성하는 방식을 정의한다(생성자)
4. 객체가 가지고 있어야 할 행위(메서드)를 정의한다.
  • 필드(field): 객체의 속성
  • 생성자(constructor): 객체의 생성방식. 처음 객체가 생성될 때 어떤 로직을 수행해야 하며, 어떤 값이 필수로 들어와야 하는지 정의
    - 기본 생성자: 파라미터가 아무것도 없는(필수값이 없는) 생성자
    • 클래스 내 생성자를 하나도 선언하지 않으면 기본 생성자를 컴파일러가 바이트코드 파일에 자동으로 추가한다. 필수값이 존재하지 않고 생성되며 어떠한 로직을 수행하지 않는다.
    • 생성자를 작성하면 기본 생성자는 자동으로 존재하지 않게 된다.
  • 메서드(method): 객체의 행위
package week03;

public class Car {
    // 필드
    String company; // 회사
    String model; // 모델
    String color; // 색상
    double price; // 가격
    double speed; // 속도
    char gear; // 기어 상태
    boolean lights; // 조명 상태

    // 생성자(Constructor): 클래스명과 동일
    public Car() {

    }

    // 메서드

    // gasPedal: kmh 을 입력받아 속도를 kmh 만큼 변속한다.
    double gasPedal(double kmh) {
        speed = kmh;
        return speed;
    }

    // brakePedal: 속도를 0으로 감속한다.
    double brakePedal() {
        speed = 0;
        return speed;
    }

    // changeGear: type을 입력받아 기어 상태를 type으로 변경한다.
    char changeGear(char type) {
        gear = type;
        return gear;
    }

    // onOffLights: 조명 상태를 켜거나 끈다.
    boolean onOffLights() {
        lights = !lights;
        return lights;
    }

    // horn: 경적을 울린다.
    void horn() {
        System.out.println("빵빵");
    }

}

객체 생성과 참조형 변수

이제 Car 클래스로 객체를 생성해보자.

객체 생성

new Car(); // Car 클래스 객체 생성
  • 객체 생성 연산자인 'new'를 사용하여 클래스로부터 객체를 생성할 수 있다.
  • new 연산자에 의해 객체가 생성되며 기본 생성자가 호출된다.

참조형 변수

Car car1 = new Car(); // car1 인스턴스
Car car2 = new Car(); // car2 인스턴스
System.out.println(car1);
System.out.println(car2);


car1, car2 인스턴스를 콘솔에 출력하면 참조형 변수와 같이 주소가 반환되어 해당 클래스의 참조형 변수를 사용하여 받아줄 수 있다.

😮 클래스는 참조형 데이터 타입으로 생각

주소값을 대입한 참조형 변수에 .(dot)을 사용하면 공개된(public) 클래스 내에 선언한 필드와 메서드에 접근하여 사용할 수 있다.

객체 배열

객체는 참조형 변수와 동일하게 취급되기 때문에 배열 또는 컬렉션에도 저장하여 관리할 수 있다.

package week03;

public class Main {
    public static void main(String[] args) {
        Car[] carArray = new Car[3];
        Car car1 = new Car();
        car1.changeGear('P');
        carArray[0] = car1;

        Car car2 = new Car();
        car2.changeGear('N');
        carArray[1] = car2;

        Car car3 = new Car();
        car3.changeGear('D');
        carArray[2] = car3;

        for (Car car : carArray) {
            System.out.println("car.gear = " + car.gear);
        }
    }

}

클래스가 참조형 데이터 타입이면, 클래스를 객체의 필드로 가질 수 있을까? 가질 수 있다!
자동차의 속성으로 바퀴, 문, 핸들을 가질 수 있다.
바퀴, 문, 핸들은 int도, String도 아니다 → Tire, Door, Handle 이라는 타입을 만들자.

public class Car {
    // 필드
    // 1) 고유 데이터 영역
    String company; // 회사
    String model; // 모델
    String color; // 색상

    // 2) 상태 데이터 영역
    double price; // 가격
    double speed; // 속도
    char gear; // 기어 상태
    boolean lights; // 조명 상태

    // 3) 객체 데이터 영역
    Tire tire;
    Door door;
    Handle handle;
    
    // ...
}

객체: 필드

필드는 객체의 데이터를 저장하는 역할

  • 필드의 초기값과 초기화
    필드를 선언만 하고 초기값을 할당하지 않을 경우 객체가 생성될 때 자동으로 기본값(default)으로 초기화된다.

  • 데이터 타입기본값
    byte0
    char\u0000 (공백)
    short0
    int0
    long0L
    float0.0F
    double0.0
    booleanfalse
    배열null
    클래스null
    인터페이스null
  • '필드를 사용한다'라는 의미는 필드의 값을 변경하거나 읽는 것을 의미한다.

    • 클래스에 필드를 정의하여 선언했다고 해서 바로 사용할 수 있는 것은 아니다.
    • 클래스를 인스턴스화해야만 비로소 필드를 사용할 수 있게 된다.
  • 외부 접근
    객체를 생성(인스턴스화)했다면 참조변수를 이용하여 외부에서 객체 내부에 필드에 접근하여 사용
    → 필드 접근 방법: .(dot)연산자

    ⚠️ 하지만, 이렇게 외부에서 클래스 내부의 필드를 직접적으로 접근하여 값을 호출하거나 변경하는 것은 캡슐화가 되어있지 않다는 것을 의미하므로 보통 외부에서 필드에 접근하지 않도록 한다.

  • 내부 접근
    클래스 내의 메서드에서 동일 클래스 내부 필드에 접근할 수 있다.

double speed; // 속도

double brakePedal() {
    speed = 0;
    return speed;
}

메서드 brakePedal()은 필드인 speed를 호출하여 사용

초기값과 기본값 확인

package week03;

public class Main2 {
    public static void main(String[] args) {
        Car car = new Car(); // 객체 생성

        // 초기값과 기본값 확인: 초기값을 준 것은 그 값이 들어가고, 아닌 값은 default value가 set
        System.out.println("car.model = " + car.model);
        System.out.println("car.color = " + car.color);
        System.out.println();

        System.out.println("car.speed = " + car.speed);
        System.out.println("car.gear = " + car.gear);
        System.out.println("car.lights = " + car.lights);
        System.out.println();

        System.out.println("car.tire = " + car.tire);
        System.out.println("car.door = " + car.door);
        System.out.println();
        
        // 필드 사용
        car.color = "blue";
        car.speed = 100;
        car.lights = false;

        System.out.println("car.color = " + car.color);
        System.out.println("car.speed = " + car.speed);
        System.out.println("car.lights = " + car.lights);
    }
}

객체: 메서드

메서드는 객체의 행위를 뜻하며 객체 간의 협력을 위해 사용됩니다.

메서드 선언

리턴타입 메서드명(매개변수, ...) {
	// 실행할 코드 작성
    // return 리턴 타입의 반환값;
}
  • 리턴 타입 (output)
    메서드가 실행된 후 호출을 한 곳으로 값을 반환할 때 해당 값의 타입
    - 반환할 값이 없을 때는 리턴 타입에 void를 작성

  • 매개변수 (input)
    메서드를 호출할 때 메서드로 전달하려는 값을 받기 위해 사용되는 변수

    • 해당 매개변수에 값을 전달하기 위해서는 순서와 타입에 맞춰 값을 넣어주면 됩니다. gasPedal(double kmh, char type)gasPedal(100, 'D');
    • 전달하려는 값이 없다면 생략 가능합니다.
  • 가변 길이의 매개변수도 선언할 수 있다.
    매개값을 , 로 구분하여 개수 상관 없이 전달 가능하다. (이 때, 가변 매개변수는 배열[array]이다.)

▶ Car.java

	// 가변길이 매개변수 선언 -> 개수 상관 X
    void carSpeeds(double ... speeds) {
        for (double v : speeds) {
            System.out.println("v = " + v);
        }
    }

▶ Main3.java

package week03;

public class Main3 {
    public static void main(String[] args) {
        Car car = new Car();
        car.carSpeeds(100, 80);
        System.out.println("-----");
        car.carSpeeds(10, 30, 50, 80);
        System.out.println("-----");
        car.carSpeeds(110, 120, 150);
    }
}

메서드 호출방법

호출: '메서드명(매개 변수)'

  • '메서드를 호출한다' : 메서드의 블록 내부에 작성된 코드를 실행한다.

  • 객체를 생성(인스턴스화)했다면 메서드를 사용할 수 있다.

  • 필드와 마찬가지로 외부 접근과 내부 접근이 가능하다.

  • 외부 접근
    참조변수를 이용하여 외부에서 객체 내부 메서드에 접근하여 사용
    → 메서드 접근 방법: .(dot)연산자

이 때, 반드시 매개변수의 순서와 타입에 맞게 매개값을 넣어줘야 한다. (다른 메서드로 인식)

  • 내부 접근
    객체 내부 메세드에서도 내부 메서드에 접근하여 호출할 수 있다.
double gasPedal(double kmh, char type) {
    changeGear(type);
    speed = kmh;
    return speed;
}
  • 반환 값 저장
    메서드의 리턴 타입을 선언하여 반환할 값이 있다면 변수를 사용하여 받아줄 수 있다.
    • 반드시 리턴 타입과 변수의 타입이 동일하거나 자동 형변환이 가능해야 한다.
		System.out.println("페달 밟기 전 car.gear = " + car.gear);
        double speed = car.gasPedal(100, 'D');
        System.out.println("speed = " + speed);

        boolean lights = car.onOffLights();
        System.out.println("lights = " + lights);
        
        System.out.println("페달 밟기 후 car.gear = " + car.gear);

메서드 오버로딩

오버로딩(Overloading = 과적)은 하나의 메서드 이름으로 여러 기능을 구현하도록 하는 Java의 기능이다.

한 클래스 내에 이미 사용하려는 이름과 같은 이름을 가진 메서드가 있더라도, 매개변수의 개수 또는 타입, 순서가 다르면 동일한 이름을 사용해서 메서드를 정의할 수 있다.

  • 메서드의 이름이 같고, 매개변수의 개수, 타입, 순서가 달라야 한다.
  • 리턴 타입, 접근 제어자만 다른 것은 오버로딩 할 수 없다.

* 메서드 오버로딩 장점:

1. 메서드 이름 하나로 상황에 따른 동작을 개별로 정의할 수 있다. 
- println()의 매개변수로 int, double, String 등이 가능
2. 메서드 이름을 절약할 수 있다.
- 같은 기능을 수행하는 println() → printInt(), printDouble() 등으로 이름 짓지 않고 하나의 메서드명으로 정의 가능 

기본형 & 참조형 매개변수

  • 기본형 매개변수
    매개변수의 타입이 기본형일 때는 값 자체가 복사되어 넘어가기 때문에 매개값으로 지정된 변수의 원본 값이 변경되지 않는다.

  • 참조형 매개변수

    • 매개변수를 참조형으로 선언하면 값이 저장된 곳의 원본 주소를 알 수 있기 때문에 값을 읽어오는 것은 물론 값을 변경하는 것도 가능하다.
    • 메서드의 매개변수뿐만 아니라 반환 타입도 참조형이 될 수 있다.

⭐️ 자바는 항상 변수의 값을 복사해서 대입한다.

  • 기본형이면 변수에 들어있는 사용하는 값을 복사해서 대입
  • 참조형이면 변수에 들어있는 참조값(주소)를 복사해서 대입
    → 참조형 변수를 매개변수로 사용하면 해당 주소를 찾아가 원본의 값을 변경한다.

인스턴스 멤버와 클래스 멤버

📌 멤버 = 필드 + 메서드

인스턴스 멤버: static이 붙지 않은 멤버 변수

인스턴스 멤버는 객체 생성 후에 사용할 수 있다.

  • 객체의 인스턴스 필드는 각각의 인스턴스마다 고유하게 값을 가질 수 있다.
  • 객체가 인스턴스화할 때마다 객체의 메서드들은 인스턴스에 포함되어 매번 생성이 된다면 중복 저장으로 메모리 효율이 매우 떨어지기 때문에 메서드 영역에 두고 공유해서 사용한다.
  • 대신, 메서드 영역에 있더라도 인스턴스를 통해서 메서드가 사용될 수 있도록 제한되어 있다.

클래스 멤버: static이 붙은 멤버 변수

클래스 멤버는 객체 생성 없이도 사용할 수 있다.

  • 클래스는 Class Loader에 의해 메서드 영역에 저장되고 사용된다.
  • 클래스 멤버는 객체의 생성 없이 바로 사용이 가능하다.
  • 여러 곳에서 공유하는 목적으로 사용된다.

▶︎ 클래스 멤버 선언
필드와 메서드를 클래스 멤버로 만들기 위해서는 static 키워드를 사용하면 된다.

static String company = "GENESIS"; // 자동차 회사 : GENESIS

String getCompany() {
    return "(주)" + company; 
}
  • 일반적으로 인스턴스마다 모두 가지고 있을 필요 없는 공용적인 데이터를 저장하는 필드는 클래스 멤버로 선언한다.
  • 또한, 인스턴스 필드를 사용하지 않고 실행되는 메서드가 존재한다면 static 키워드를 사용하여 클래스 메서드로 선언하는 것이 좋다.

⚠️ 클래스 멤버로 선언된 메서드는 인스턴스 멤버를 사용할 수 없다.

  • 클래스 멤버는 객체 생성 없이 바로 사용 가능하기 때문에 객체가 생성되어야 존재할 수 있는 인스턴스 멤버를 사용할 수 없다.

  • 참조형 변수를 사용하여 클래스 멤버에 접근은 가능하지만 추천하지 않는다.→ 마치 인스턴스 변수에 접근하는 것으로 오해할 수 있다.

✏️ IntelliJ IDE 에서 instance reference를 통해 정적 변수에 접근하는 것을 경고함

인스턴스 멤버 & 클래스 멤버 예제

package week03.staticFolder;

public class Car {

    static String company = "GENESIS"; // 자동차 회사 : GENESIS

	// 클래스 메서드 안에서 클래스 필드인 company의 이름을 변경
    static String setCompany(String companyName) {
        // System.out.println("자동차 모델 확인: " + model); // 인스턴스 필드 사용 불가
        company = companyName;
        return company;
    }
}
package week03.staticFolder;

public class Main {
    public static void main(String[] args) {
        // 클래스 필드 company 확인: Car.java 에서 선언
        System.out.println(Car.company + "\n");
        // 클래스 필드 변경 및 확인: 클래스명.필드명
        Car.company = "Audi";
        System.out.println(Car.company + "\n");

        // 클래스 메서드 호출: 클래스명.메서드명
        String companyName = Car.setCompany("Benz"); // Audi -> Benz
        System.out.println("companyName = " + companyName);

        System.out.println();
        // 참조형 변수 사용
        Car car = new Car(); // 객체 생성

        car.company = "Ferrari"; // 인스턴스 필드
        System.out.println(car.company + "\n");

		// 클래스 메서드의 리턴값으로 클래스 필드 company 를 반환함
        String companyName2 = car.setCompany("Lamborghini"); 
        System.out.println("companyName2 = " + companyName2);
    }
}

지역변수 및 상수

지역변수

package week03.sample;

public class Main {

    public static void main(String[] args) {
        Main main = new Main();
        System.out.println(main.getNumber());
        System.out.println(main.getNumber());
    }

    // 메서드
    public int getNumber() {
        // [지역변수]
        // 해당 메서드가 실행될 때 마다 독립적인 값을 저장하고 관리한다.
        // 이 지역변수는 메서드 내부에서 정의될 때 생성된다.
        // 이 메서드가 종료될 때 소멸된다.
        int number = 1;
        number += 1; // number = number + 1;
        return number;
    }
}

number = number + 1의 변경상태는 getNumber() 메서드 호출이 끝나면 사라진다.

final 필드

final 이라는 키워드가 붙으면 더는 값을 변경할 수 없다.

  • 최초 한번만 값 할당 가능, 이후 변경 시 컴파일 오류 발생
  • 참고로, final은 class, method를 포함한 여러 곳에 사용 가능하다.

상수 (static final)

단 하나만 존재하는 변하지 않는 고정된 값. 즉, 인스턴스 마다 상수를 저장할 필요없이 static 영역에 하나를 선언하여 그 값을 모든 인스턴스가 공유하며 변경이 불가능하도록 선언한다.

  • JVM 상에서 하나만 존재하므로 중복과 메모리 비효율 문제를 해결할 수 있다.
  • 상수는 대문자를 사용하고 구분은 _(언더스코어)로 하는 것이 관례이다. → CONST_VALUE : 일반적인 변수와 구분이 쉽다.

생성자

생성자는 객체가 생성될 때 호출되며 객체를 초기화하는 역할을 수행

▶︎ 생성자 선언과 호출

  • 생성자는 반환 타입이 없고 이름은 클래스의 이름과 동일하다.
  • new 연산자에 의해 객체가 생성되면서 Car(); 즉, 생성자가 호출된다.

💡 Intellij Mac 자동 생성 단축키 : + n → Constructor

▶︎ 기본 생성자

기본 생성자는 선언할 때 괄호( ) 안에 아무것도 넣지 않는 생성자를 의미한다.

  • 모든 클래스는 반드시 생성자가 하나 이상 존재한다.
  • 클래스에 생성자를 하나도 선언하지 않았다면 컴파일러는 기본 생성자를 바이트코드 파일에 자동으로 추가시켜준다. 이러한 경우 기본 생성자는 생략이 가능하다.
  • 반대로 단 하나라도 생성자가 선언되어 있다면 컴파일러는 기본 생성자를 추가하지 않는다.
  • 컴파일러에 의해 생성되는 기본 생성자는 해당 클래스의 접근 제어자(public, …)를 따릅니다.
public class Car {
		public Car(String model) {} // 생성자 선언
		// 생성자가 한개 이상 선언되었기 때문에 기본 생성자를 추가하지 않음.
}

필드 초기화

생성자는 객체를 초기화하는 역할을 수행한다.
객체를 만들 때 인스턴스마다 다른 값을 가져야 한다면 생성자를 통해서 필드를 초기화할 수 있다.

  • 인스턴스마다 동일한 데이터를 가지는 필드는 필드에 초기값을 직접 대입한다.
  • 인스턴스마다 각각 다른 데이터를 가지는 필드는 생성자를 통해 매개변수값을 받아 초기화 시켜준다.
  • 객체를 생성할 때 필수값을 사용자에게 강제할 수 있다.

생성자 오버로딩

생성자를 초기화할 때 오버로딩을 적용할 수 있다.

public Car() {}
public Car(String color, double price) {}
public Car(String model, String color, double Price) {}

⚠️ 주의
오버로딩을 할 때 개수, 타입, 순서가 동일할 때 매개변수 명만 다르게 하는 경우 오버로딩 규칙에 위배되어 오류 발생

→ Car 생성자 (String, String, double)이 이미 정의되어 있습니다.

this와 this()

this

this는 객체 즉, 인스턴스 자신을 표현하는 키워드이다.
객체 내부 생성자 및 메서드에서 객체 내부 멤버에 접근하기 위해 사용한다.

  • 생성자를 선언하는데 매개변수명과 객체의 필드명이 동일할 경우, 생성자 블록 내부에서 가장 가까운 변수명(매개변수)를 가리키게 됨으로 객체의 필드와 매개변수명을 구분하기 위해 this를 사용한다.
  • 또한 객체 메서드에서 리턴타입이 인스턴스 자신의 클래스 타입이라면 this를 이용하여 인스턴스 자신의 주소를 반환할 수 있다.
Car returnInstance() {
	return this;
}

this()

this()는 인스턴스 자신의 생성자를 호출하는 키워드이다.

  • 객체 내부 생성자 및 메서드에서 해당 객체의 생성자를 호출하기 위해 사용될 수 있다.
  • 생성자를 통해 객체의 필드를 초기화할 때 중복되는 코드를 줄여줄 수 있다.
  • 단, this() 키워드를 사용해서 다른 생성자를 호출할 때는 반드시 해당 생성자의 첫 줄에 작성되어야 한다.
public Car(String model) {
    this(model, "Blue", 50000000);
}

public Car(String model, String color) {
    this(model, color, 100000000);
}

public Car(String model, String color, double price) {
    this.model = model;
    this.color = color;
    this.price = price;
}

접근 제어자

접근 제어자 : public, protected, default, private
멤버 또는 클래스에 사용, 외부에서 접근하지 못하도록 제한한다.

  • public : 접근 제한이 전혀 없습니다.
  • protected : 같은 패키지 내에서, 다른 패키지의 자손 클래스에서 접근이 가능합니다
  • default : 같은 패키지 내에서만 접근이 가능합니다.
  • private : 같은 클래스 내에서만 접근이 가능합니다.

▶︎ 사용 가능한 접근 제어자
클래스 : public, default
메서드, 멤버변수: public, protected, default, private
지역변수 : 없음

  • 접근 제어자를 이용한 캡슐화 (은닉성)
    • 접근제어자는 클래스 내부에 선언된 데이터를 보호하기 위해서 사용합니다.
    • 유효한 값을 유지하도록, 함부로 변경하지 못하도록 접근을 제한하는 것이 필요합니다.
  • 생성자의 접근 제어자
    • 생성자에 접근 제어자를 사용함으로 인스턴스의 생성을 제한할 수 있습니다.
    • 일반적으로 생성자의 접근 제어자는 클래스의 접근 제어자와 일치합니다.

Getter 와 Setter

객체의 무결성 즉, 변경이 없는 상태를 유지하기 위해 접근 제어자를 사용합니다.

  • 이때 외부에서 필드에 직접 접근하는 것을 막기 위해 필드에 private, default 등의 접근 제어자를 사용할 수 있습니다.

💡 Intellij Mac 자동 생성 단축키 : + n → Getter / Setter

▶︎ Getter
외부에서 객체의 private 한 필드를 읽을 필요가 있을 때 Getter 메서드를 사용합니다.

▶︎ Setter
외부에서 객체의 private 한 필드를 저장/수정할 필요가 있을 때 Setter 메서드를 사용합니다.

pakage와 import 이해하기

package

📌 패키지란 클래스의 일부분이면서 클래스를 식별해 주는 용도입니다.

  • 패키지는 상위 패키지와 하위 패키지를 도트(.)로 구분합니다.
  • package 상위 패키지.하위 패키지; 이렇게 선언할 수 있습니다.
  • 사용하는 클래스와 같은 패키지: 패키지 경로 생략 가능
  • 사용하는 클래스와 다른 패키지: 패키지 전체 경로를 포함해서 클래스를 적어주어야 한다

import

사용하는 클래스와 패키지가 다를 때 마다 전체 경로를 포함한 클래스를 적는 것은 불편 → import를 사용하면 된다.

  • 이름이 중복되는 경우 자주 사용하는 클래스를 import, 나머지를 패키지를 포함한 전체 경로를 적어주면 된다.

상속

클래스 간의 관계와 상속

부모 클래스의 필드와 메서드를 자식 클래스에게 물려주는 것

  • 상속을 사용하면 적은 양의 코드로 새로운 클래스를 작성할 수 있고 공통적인 코드를 관리하여 코드의 추가와 변경이 쉬워진다.
  • 상속을 사용하면 코드의 중복이 제거되고 재사용성이 크게 증가하여 생산성과 유지 보수성에 매우 유리해진다.

상속

public class 자식클래스 extends 부모클래스 {

}
  • extends = 확장
  1. 부모 클래스에 새로운 필드와 메서드가 추가되면 자식 클래스는 이를 상속받아 사용할 수 있다.
  2. 자식 클래스에 새로운 필드와 메서드가 추가되어도 부모 클래스는 어떠한 영향도 받지 않는다.
  3. 따라서 자식 클래스의 멤버 개수는 부모 클래스보다 항상 같거나 많다.
public class SportsCar extends Car{
    String engine;
    public void booster() {
        System.out.println("엔진 " + engine + " 부앙~\n");
    }
}

Car를 상속받은 SportsCar는 Car의 필드와 메서드를 사용할 수 있다.

클래스 간의 관계

상속관계 : ~은 ~ 이다.
포함관계 : ~은 ~ 을 가지고 있다.

단일 상속과 다중 상속

Java는 다중 상속을 허용하지 않습니다.
다중 상속을 허용하면 클래스 간의 관계가 복잡해지는 문제가 생기기 때문이다.

  • 만약, 자식 클래스에서 상속받는 서로 다른 부모 클래스들이 같은 이름의 멤버(필드, 메서드)를 가지고 있다면?
  • 자식 클래스에서는 이 멤버를 구별할 수 있는 방법이 없다는 문제가 생긴다.

▶︎ final 클래스
final 클래스는 최종적인 클래스가 됨으로 더 이상 상속할 수 없는 클래스가 된다.

→ Cannot inherit from final : final 클래스는 상속받을 수 없다.

▶︎ final 메서드
final 메서드는 자식 클래스를 생성 후 호출은 할 수 있지만 자식 클래스에서 오버라이딩은 불가능하다. (final로 선언되어 변경할 수 없다.)

오버라이딩

부모 클래스로부터 상속받은 메서드의 내용을 재정의 하는 것을 오버라이딩이라고 합니다.

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

@Overriding

“@Overriding” 애노테이션은 상위 클래스의 메서드를 오버라이드하는 것임을 나타낸다.

이름 그대로 오버라이딩한 메서드 위에 이 애노테이션을 붙여야 한다.

컴파일러는 이 애노테이션을 보고 메서드가 정확히 오버라이드 되었는지 확인한다. 오버라이딩 조건(메서드 이름이 일치하지 않는 경우 등)을 만족하지 않았으면 컴파일 에러를 발생시킨다.

실수로 오버라이딩을 못하는 경우를 방지해준다.

super, super()

super

📌 super는 부모 클래스의 멤버를 참조할 수 있는 키워드입니다.

  • 객체 내부 생성자 및 메서드에서 부모 클래스의 멤버에 접근하기 위해 사용
  • 자식 클래스 내부에서 선언한 멤버와 부모 클래스에서 상속받은 멤버와 이름이 같을 경우 이를 구분하기 위해 사용됩니다.

▶︎ Car :부모 클래스

String model; // 자동차 모델
String color; // 자동차 색상
double price; // 자동차 가격

▶︎ SuperCar : 자식 클래스

String model = "Ferrari"; // 자동차 모델
String color = "Red"; // 자동차 색상
double price = 300000000; // 자동차 가격

public void setCarInfo(String model, String color, double price) {
        super.model = model;
        super.color = color;
        this.price = price;
    }

▶︎ Main : 실행

// setCarInfo 메서드 호출해서 부모 및 자식 필드 값 저장
sportsCar.setCarInfo("GV80", "Black", 50000000);

// 결과 확인을 위해 자식 클래스 필드 model, color 확인 & 부모 클래스 메서드인 getModel(), getColor() 호출
// 자식 클래스 필드 값은 변화 없음.
System.out.println("sportsCar.model = " + sportsCar.model); // Ferrari
System.out.println("sportsCar.color = " + sportsCar.color); // Red
// this.price = price; 결과 확인
System.out.println("sportsCar.price = " + sportsCar.price); // 5.0E7

super()

super(…)는 부모 클래스의 생성자를 호출할 수 있는 키워드입니다.

  • 자식 클래스의 객체가 생성될 때 부모 클래스들이 모두 합쳐져서 하나의 인스턴스가 생성
  • 이때 부모 클래스의 멤버들의 초기화 작업이 먼저 수행
    • 따라서 자식 클래스의 생성자에서는 부모 클래스의 생성자가 호출됩니다.
    • 또한 부모 클래스의 생성자는 가장 첫 줄에서 호출이 되어야 합니다.

⚠️ 부모 클래스의 생성자를 자식 클래스의 생성자에서 호출하지 않으면 에러 발생

▶︎ Car 생성자

// 부모 클래스 Car 생성자
public Car(String model, String color, double price) {
    this.model = model;
    this.color = color;
    this.price = price;
}

▶︎ SportsCar 생성자

// 자식 클래스 SportsCar 생성자
public SportsCar(String model, String color, double price, String engine) {
     // this.engine = engine; // 오류 발생
    super(model, color, price);
    this.engine = engine;
}

다형성

다형성이란 ‘여러 가지 형태를 가질 수 있는 능력’을 의미합니다.
참조 변수 타입 변환을 활용해서 다형성을 구현할 수 있습니다.

참조 변수의 타입 변환

  • 자동 타입 변환
    부모 타입 변수 = 자식 타입 객체; 는 자동으로 부모 타입으로 변환이 일어납니다.

    • 주의할 점은 부모 타입 변수로 자식 객체의 멤버에 접근할 때는 부모 클래스에 선언된 즉, 상속받은 멤버만 접근할 수 있습니다.
  • 강제 타입 변환
    📌 자식 타입 변수 = (자식 타입) 부모 타입 객체;

    • 부모 타입 객체는 자식 타입 변수로 자동으로 타입 변환되지 않습니다.
    • 다운캐스팅: 이럴 때는 (자식 타입) 즉, 타입 변환 연산자를 사용하여 강제로 자식 타입으로 변환할 수 있습니다.

⚠️ 다만 무조건 강제 타입 변환을 할 수 있는 것은 아닙니다.
→ 자식 타입 객체가 부모 타입으로 자동 타입 변환된 후 다시 자식 타입으로 변환될 때만 강제 타입 변환이 가능합니다.
: 즉, 생성 시점에 자식 타입이 인스턴스에 같이 생성되어 있어야 형 변환이 가능함.
: 컴파일 오류로 잡히지 않기 때문에 개발자의 주의를 요구한다.

Mammal newMammal = new Mammal();
Whale newWhale = (Whale) newMammal; // ClassCastException 발생

instanceof

다형성 기능으로 인해 해당 클래스 객체의 원래 클래스명을 체크하는 것이 필요한데 이때 사용할 수 있는 연산자가 instanceof 입니다.

  • 해당 객체가 내가 의도하는 클래스의 객체인지 확인할 수 있습니다.
  • {대상 객체} instance of {클래스 이름} 와 같은 형태로 사용하면 응답값은 boolean입니다.
    → {대상 객체} 가 {클래스 이름(참조 타입)}을 인스턴스에 가지고 있니?

추상 클래스

추상 클래스는 미완성된 설계도입니다.

추상 클래스 선언

abstract 키워드를 사용하여 추상 클래스를 선언할 수 있습니다.

public abstract class 추상클래스명 {

}
  • 추상 클래스는 추상 메서드를 포함할 수 있습니다.
  • 추상 메서드가 없어도 추상 클래스로 선언할 수 있습니다.
  • 추상 클래스는 자식 클래스에 상속되어 자식 클래스에 의해서만 완성될 수 있습니다.
  • 추상 클래스는 인스턴스로 생성할 수 없습니다.
  • 추상 메서드를 하나라도 가지고 있다면 추상 클래스로 선언되어야 한다.

추상 메서드

public abstract class 추상클래스명 {
	abstract 리턴타입 메서드이름(매개변수, ...);
}

상속받은 클래스에서 추상 클래스의 추상 메서드는 반드시 오버라이딩 되어야 합니다.

  • 자식 클래스에서 실수로 메서드를 오버라이딩 하지 않을 문제를 근본적으로 방지한다.

인터페이스

인터페이스의 역할

상속 관계가 없는 다른 클래스들이 서로 동일한 행위 즉, 메서드를 구현해야 할 때 인터페이스는 구현 클래스들의 동일한 사용 방법과 행위를 보장한다.

인터페이스 선언 및 구성

  • 인터페이스의 필드는 모두 public static final이어야 한다.
  • 인터페이스의 메서드는 모두 public abstract 이다. → 모두 다 구현해야 함
public interface 인터페이스명 { 
	public static final char A = 'A';
    static char B = 'B';
    final char C = 'C';
    char D = 'D';

    void turnOn(); // public abstract void turnOn();
}

인터페이스 구현

public class 클래스명 implements 인터페이스명 { 
	// 추상 메서드 오버라이딩
	@Override
	public 리턴타입 메서드이름(매개변수, ...) {
		// 실행문
	}
}

인터페이스 상속

인터페이스 간의 상속이 가능하다.

  • 인터페이스 간의 상속은 implements 가 아니라 extends 키워드를 사용한다.
  • 인터페이스 클래스와는 다르게 다중 상속이 가능하다.
package week03.interfaceExample;

public class Main extends D implements C {

    @Override
    public void a() {
        System.out.println("A");
    }

    @Override
    public void b() {
        System.out.println("B");
    }

    @Override
    void d() {
        super.d();
    }

    public static void main(String[] args) {
        Main main = new Main();
        main.a();
        main.b();
        main.d();
    }
}

interface A {
    void a();
}
interface B {
    void b();
}
interface C extends A, B { }

class D {
    void d() {
        System.out.println("D");
    }
}

디폴트 메서드와 static 메서드

디폴트 메서드

디폴트 메서드는 추상 메서드의 기본적인 구현을 제공하는 메서드입니다.

🤨 하지만, 인터페이스의 목적이 인터페이스를 구현(implements) 하는 곳에서 반드시 오버라이딩하여 기능을 완성시키도록 제약을 주는 것으로 default 메서드는 예외적으로 특별한 경우에만 사용해야 한다.

  • 메서드 앞에 default 키워드를 붙이며 블럭{ }이 존재해야 합니다.
  • default 메서드 역시 접근 제어자가 public이며 생략이 가능합니다.
  • 추상 메서드가 아니기 때문에 인터페이스의 구현체들에서 필수로 재정의 할 필요는 없습니다.
package week03.methodExample;

public class Main implements A {

    @Override
    public void a() {
        System.out.println("A");
    }


    public static void main(String[] args) {
        Main main = new Main();
        main.a();

        // 디폴트 메서드 재정의 없이 바로 사용가능합니다.
        main.aa();
    }
}

interface A {
    void a();
    default void aa() {
        System.out.println("AA");
    }
}

static 메서드

📌 인터페이스에서 static 메서드 선언이 가능합니다.

말하자면, 인터페이스에서 모든 인스턴스가 공통으로 쓸 static 메서드를 선언한 것과 동일하다.
  • static의 특성 그대로 인터페이스의 static 메서드 또한 객체 없이 호출이 가능합니다.
  • 선언하는 방법과 호출하는 방법은 클래스의 static 메서드와 동일합니다.
public class Main implements A {

    @Override
    public void a() {
        System.out.println("A");
    }

    public static void main(String[] args) {
        Main main = new Main();
        main.a();
        main.aa();
        System.out.println();

        // static 메서드 aaa() 호출
        A.aaa();
    }
}

interface A {
    void a();
    default void aa() {
        System.out.println("AA");
    }
    static void aaa() {
        System.out.println("static method");
    }
}

다형성

인터페이스의 타입변환(자동 타입 변환)

public class Main {
    public static void main(String[] args) {
        
        // A 인터페이스에 구현체 B 대입
        A a1 = new B();
        
        // A 인터페이스에 구편체 B를 상속받은 C 대입
        A a2 = new C();
        
    }
}

interface A { }
class B implements A {}
class C extends B {}

인터페이스의 타입변환(강제 타입 변환)

public class Main {
    public static void main(String[] args) {

        // A 인터페이스에 구현체 B 대입
        A a1 = new B();
        a1.a();
        // a1.b(); // 불가능

        System.out.println("\nB 강제 타입변환");
        B b = (B) a1;
        b.a();
        b.b(); // 강제 타입변환으로 사용 가능
        System.out.println();

        // A 인터페이스에 구편체 B를 상속받은 C 대입
        A a2 = new C();
        a2.a();
        //a2.b(); // 불가능
        //a2.c(); // 불가능

        System.out.println("\nC 강제 타입변환");
        C c = (C) a2;
        c.a();
        c.b(); // 강제 타입변환으로 사용 가능
        c.c(); // 강제 타입변환으로 사용 가능
 

    }
}

interface A {
    void a();
}
class B implements A {
    @Override
    public void a() {
        System.out.println("B.a()");
    }

    public void b() {
        System.out.println("B.b()");
    }
}
class C extends B {
    public void c() {
        System.out.println("C.c()");
    }
}
  • 추상 클래스, 인터페이스 다형성 예시

profile
안녕하세요 :)

0개의 댓글