상속

📌 상속의 사전적 정의는 부모가 자식에게 물려주는 행위를 말합니다.

  • 객체 지향 프로그램에서도 부모 클래스의 필드와 메서드를 자식 클래스에게 물려줄 수 있습니다.
  • 상속을 사용하면 적은 양의 코드로 새로운 클래스를 작성할 수도 있고 공통적인 코드를 관리하여 코드의 추가와 변경이 쉬워질 수도 있습니다.
  • 이러한 특성 때문에 상속을 사용하면 코드의 중복이 제거되고 재사용성이 크게 증가하여 생산성과 유지 보수성에 매우 유리해집니다. 

= 자바 객체 지향의 중요한 기능 중 하나로, 한번 변수나 메서드 선언하고 그걸 다른 이들이 빌려쓰는 형태로 만들어, 매번 변수, 메서드를 반복해서 선언하는 것을 방지한 방식이다.

 

https://wonkang.tistory.com/88

 

  • 위의 그림처럼 개발자, 댄서, 가수 모두 인간의 특징인 이름,나이,학습,일등의 특징을 가질 수 밖에 없다. 이 인간으로서의 특징을 개발자, 댄서, 가수에 각각 변수로 선언하면 같은 내용이 3배가 될 뿐이니, 상위 클래스(부모)를 생성하여 부모에서 인간의 특징을 정하고 다른 하위 클래스(자식)에게 정보를 공유하는게 효과적이다.

 

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

}

 

  • 상속은 위와 같이 선언하게 되는데, 잘 보면 extends라는 키워드가 존재한다.
  • 말 그대로 확장이란 의미인데, 상속이 이 확장의 개념과 동일하다

 

부모의 확장형 자식

 

  1. 부모 클래스에 새로운 필드와 메서드가 추가되면 자식 클래스는 이를 상속받아 사용할 수 있다.
  2. 자식 클래스에 새로운 필드와 메서드가 추가되어도 부모 클래스는 어떠한 영향도 받지 않는다.
  3. 따라서 자식 클래스의 멤버 개수는 부모 클래스보다 항상 같거나 많다.

‘부모가 자식보다 큰 사람이니까 부모 클래스도 마찬가지로 자식 클래스 보다 큰 범위겠지?’ 라고 생각하면 망한다. 여기서는 '청출어람'이라는 느낌으로 자식이 부모보다 더 많은 데이터를 가지고 있다. 단 위의 설명처럼 자식은 부모에게 영향을 주지 못하지만 부모는 자식에게 영향을 줄 수 있다.

 

< 1980년대 일반적인 가정을 생각하면 편하다 >

  • 1. 자식이 인터넷 세상에 태어나면서 자연스럽게 부모보다 많은 데이터를 가지게 되었다.
  • 2. 보통 부모님이 자식에게 끼치는 영향이 크다.
  • 3. 부모의 수보다 자식 수가 보통 많다.

 

<예제>

public class Car {

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

    double speed;  // 자동차 속도 , km/h
    char gear = 'P'; // 기어의 상태, P,R,N,D
    boolean lights; // 자동차 조명의 상태

    public String getModel() {
        return model;
    }

    public void setModel(String model) {
        this.model = model;
    }

    public double gasPedal(double kmh, char type) {
        changeGear(type);
        speed = kmh;
        return speed;
    }

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

    public char changeGear(char type) {
        gear = type;
        return gear;
    }

    public boolean onOffLights() {
        lights = !lights;
        return lights;
    }

    public void horn() {
        System.out.println("빵빵");
    }

}

public class SportsCar extends Car{
    String engine;
    public void booster() {
        System.out.println("엔진 " + engine + " 부앙~\n");
    }
}


public class Main {
    public static void main(String[] args) {
        // 부모 클래스 객체에서 자식 클래스 멤버 사용
        Car car = new Car();
        // car.engine = "Orion"; // 오류
        // car.booster(); // 오류

        // 자식 클래스 객체 생성
        SportsCar sportsCar = new SportsCar();
        sportsCar.engine = "Orion";
        sportsCar.booster();

        // 자식 클래스 객체에서 부모 클래스 멤버 사용
        sportsCar.company = "GENESIS";
        sportsCar.setModel("GV80");
        System.out.println("sportsCar.company = " + sportsCar.company);
        System.out.println("sportsCar.getModel() = " + sportsCar.getModel());
        System.out.println();

        sportsCar.horn();
        System.out.println(sportsCar.changeGear('D'));
    }
}

 

 

클래스 간의 관계

📌 클래스 간의 관계를 분석하여 관계 설정을 해줄 수 있다.

  • 상속관계 : is - a (”~은 ~(이)다”)
  • 포함관계 : has - a (”~은 ~을(를) 가지고 있다”) 
상속관계  포함관계
  • 상속관계 : 고래는 포유류다 👍
  • 포함관계 : 고래는 포유류를 가지고 있다…? 🤔
  • 자동차는 타이어를 가지고 있다. 👍
  • 자동차는 차 문을 가지고 있다. 👍
  • 자동차는 핸들을 가지고 있다. 👍

1. 포함(Composition)

포함은 "has-a" 관계를 표현합니다.
한 클래스가 다른 클래스를 속성(멤버 변수)으로 가지는 형태입니다.

특징

  • 유연성: 포함된 객체는 독립적이며, 필요에 따라 다른 객체로 교체할 수 있습니다.
  • 코드 재사용: 여러 클래스에서 포함된 클래스를 재사용할 수 있습니다.
  • 의존성 낮음: 포함된 객체의 내부 구현은 외부 클래스에 영향을 주지 않습니다.
class Circle{
	int x;
    int y;
    int r;
}

----------------------------

class Circle{
	Poiont c = new Point();
    int r;
}

class Point {
	int x;
    int y;
}

2. 상속(Inheritance)

상속은 "is-a" 관계를 표현합니다.
하위 클래스(Subclass)가 상위 클래스(Superclass)의 속성과 메서드물려받는 구조입니다.

특징

  • 코드 재사용: 공통 기능을 상위 클래스에서 정의하고, 하위 클래스에서 상속받아 사용.
  • 유연성 낮음: 상속받은 클래스는 상위 클래스와 강한 결합을 가지며, 부모 클래스의 변경이 하위 클래스에 영향을 줌.
  • 다형성 지원: 부모 타입의 참조로 자식 객체를 사용할 수 있음.

 

3. 포함 vs 상속의 차이점

구분 포함(Composition) 상속(Inheritance)
관계 "has-a" 관계 "is-a" 관계
결합도 낮음 (유연성 높음) 높음 (부모 클래스에 강하게 의존)
코드 재사용성 객체를 포함하여 재사용 가능 공통된 동작을 부모 클래스에서 정의
확장성 포함된 객체를 교체하여 기능 확장 가능 부모 클래스 설계 변경 시 제한적
다형성 지원 다형성 제공하지 않음 다형성(Polymorphism) 지원
목적 객체의 조립, 독립적인 관계 유지 객체의 계층 구조 정의, 공통 기능 상속

 

4. 포함과 상속의 사용 시점

포함(Composition)이 적합한 경우:

  1. 두 클래스가 명확한 "has-a" 관계일 때.
  2. 한 클래스가 다른 클래스의 특정 기능만 필요할 때.
  3. 코드 재사용과 독립성을 유지하면서 기능 확장이 필요할 때.
  4. 설계 변경 가능성이 높아 유연성이 중요한 경우.

상속(Inheritance)이 적합한 경우:

  1. 두 클래스가 명확한 "is-a" 관계일 때.
  2. 하위 클래스가 상위 클래스의 대체 가능성을 가져야 할 때(다형성).
  3. 공통 기능을 상위 클래스에 정의하고, 하위 클래스에서 추가적으로 확장할 때.

5. 실무에서의 선호

  • 포함이 더 자주 사용됩니다.
    이유: 유연성, 낮은 결합도, 유지보수 용이성 때문.
  • 상속은 사용이 간단하지만, 잘못된 설계로 인해 강한 결합이나 불필요한 상속 계층 구조를 초래할 수 있습니다.
    따라서, 상속보다는 포함을 우선적으로 고려하고, 정말 필요한 경우에만 상속을 사용합니다.

 

단일 상속과 다중 상속

📌 Java는 클래스의 다중 상속을 허용하지 않습니다…

  • 다중 상속을 허용하면 복잡한 소프트웨어의 기능을 구현할 때 여러 개의 클래스를 상속받아 쉽게 구현할 수 있다는 장점이 있는데.. 왜? 허용하지 않을까요?
  • 왜냐하면 다중 상속을 허용하면 클래스 간의 관계가 복잡해지는 문제가 생기기 때문입니다.
  • 만약 자식 클래스에서 상속받는 서로 다른 부모 클래스들이 같은 이름의 멤버를 가지고 있다면?
  • 자식 클래스에서는 이 멤버를 구별할 수 있는 방법이 없다는 문제가 생깁니다. 

= 자식이 부모를 여럿 골라도 되는데(?), 부모 입장에서 자식 2명이 이름이 같으면 구분을 못한다.

 

 

final 클래스와 final 메서드

📌 클래스에 final 키워드를 지정하여 선언하면 최종적인 클래스가 됨으로 더 이상 상속할 수 없는 클래스가 됩니다.

public class Car {
    public final void horn() {
        System.out.println("빵빵");
    }
}

...

public class SportsCar extends Car{
    public void horn() { // 오류가 발생합니다.
        super.horn();
    }
}

 

  • final은 마치 채권처럼 모든 것을 처음 상태로 동결한다. 채권이 구매 시의 세금,가격,이자로 모든 변수를 상수로 지정해버리는 것과 같다 = 절대로 무엇도 바꿀 수 없다. 그저 그 모양그대로 가져와서 써야한다

 

Object

📌 Object는 말 그대로 “객체”를 의미하는 단어이며 보통, Object 클래스를 의미합니다.

  • Object 클래스는 Java 내 모든 클래스들의 최상위 부모 클래스입니다.
  • 따라서, 모든 클래스는 Object의 메서드를 사용할 수 있습니다.
  • 또한 부모 클래스가 없는 자식 클래스는 컴파일러에 의해 자동으로 Object 클래스를 상속받게 됩니다.
  • Object 클래스는 프로그래머가 만드는 클래스가 아니라 자체적으로 제공되는 최상위 클래스이다.

object 클래스의 메서드

 

 


 

 

오버라이딩

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

  • 부모 클래스의 메서드를 그대로 사용 가능하지만 자식 클래스의 상황에 맞게 변경을 해야 하는 경우 오버라이딩을 사용합니다.
  • 오버라이딩을 하기 위해서는 아래 조건들을 만족해야 합니다.
  1. 선언부가 부모 클래스의 메서드와 일치해야 합니다.
  2. 접근 제어자를 부모 클래스의 메서드 보다 좁은 범위로 변경할 수 없습니다.
  3. 예외는 부모 클래스의 메서드 보다 많이 선언할 수 없습니다
public class Car {

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

    double speed;  // 자동차 속도 , km/h
    char gear = 'P'; // 기어의 상태, P,R,N,D
    boolean lights; // 자동차 조명의 상태

    public String getModel() {
        return model;
    }

    public void setModel(String model) {
        this.model = model;
    }

    public double gasPedal(double kmh, char type) {
        changeGear(type);
        speed = kmh;
        return speed;
    }

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

    public char changeGear(char type) {
        gear = type;
        return gear;
    }

    public boolean onOffLights() {
        lights = !lights;
        return lights;
    }

    public void horn() {
        System.out.println("빵빵");
    }

}

public class SportsCar extends Car{
    String engine;
    public void booster() {
        System.out.println("엔진 " + engine + " 부앙~\n");
    }

    public SportsCar(String engine) {
        this.engine = engine;
    }

    @Override
    public double brakePedal() {
        speed = 100;
        System.out.println("스포츠카에 브레이크란 없다");
        return speed;
    }

    @Override
    public void horn() {
        booster();
    }
}

public class Main {
    public static void main(String[] args) {
        // 부모 클래스 자동차 객체 생성
        Car car = new Car();
        car.horn(); // 경적

        System.out.println();
        // 자식 클래스 스포츠카 객체 생성
        SportsCar sportsCar = new SportsCar("Orion");

        // 오버라이딩한 brakePedal(), horn() 메서드 호출
        sportsCar.brakePedal();
        sportsCar.horn();

    }
}
  • 오버라이딩에서 자식 클래스에서 부모 클래스의 메서드를 바꾼다고 해서 부모 클래스의 메서드가 바뀌는 것은 아닙니다.
  • 자식 클래스에서 그 메서드를 새로운 방식으로 재정의하는 것입니다.  =  "엄마"를 자식이 "착한 엄마"라고 자신이 원하는(필요한) 대로 재해석한다는 말. 부모는 "엄마"라는건 바뀌지 않지만 자식에 한해서는 "착한 엄마"가 됨
  • 그래서 아래와 같은 답이 나온다.
더보기

빵빵

 

스포츠카에 브레이크란 없다

엔진 Orion 부앙~

 

  • 이름 비슷하지만 엄연히 오버로드와 오버라이딩은 다른 개념이다.
오버로드 같은 메서드 이름을 다른 매개변수를 사용하여 여러 번 정의하는 것입니다. 반환 타입은 동일하거나 달라질 수 있습니다.
오버라이딩 상속 관계에서 부모 클래스의 메서드를 자식 클래스에서 재정의하는 것입니다. 메서드 이름, 매개변수, 반환 타입이 부모 클래스와 정확히 일치해야 합니다.

 

 

 

super 와 super()

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

  • 객체 내부 생성자 및 메서드에서 부모 클래스의 멤버에 접근하기 위해 사용될 수 있습니다.
  • 자식 클래스 내부에서 선언한 멤버와 부모 클래스에서 상속받은 멤버와 이름이 같을 경우 이를 구분하기 위해 사용됩니다.
public void setCarInfo(String model, String color, double price) {
    super.model = model; // model은 부모 필드에 set
    super.color = color; // color는 부모 필드에 set
    this.price = price; // price는 자식 필드에 set
}
  • 말 그대로 자식 클래스에서 부모 클래스의 맴버에 접근하기 위해 필요한 기능
  • super = 부모 , this = 만들어질 객체
  • model, color는 부모 클래스의 매개변수가 바뀐다. price는 생성될 객체의 price값이 바뀐다.

 

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

  • 객체 내부 생성자 및 메서드에서 해당 객체의 부모 클래스의 생성자를 호출하기 위해 사용될 수 있습니다.
  • 자식 클래스의 객체가 생성될 때 부모 클래스들이 모두 합쳐져서 하나의 인스턴스가 생성됩니다.
  • 이때 부모 클래스의 멤버들의 초기화 작업이 먼저 수행이 되어야 합니다.
    • 따라서 자식 클래스의 생성자에서는 부모 클래스의 생성자가 호출됩니다.
    • 또한 부모 클래스의 생성자는 가장 첫 줄에서 호출이 되어야 합니다.
// 자식 클래스 SportsCar 생성자
public SportsCar(String model, String color, double price, String engine) {
     // this.engine = engine; // 오류 발생
    super(model, color, price);
    this.engine = engine;
}
  • 자식 클래스 객체를 생성할 때 생성자 매개변수에 매개값을 받아와 super(…)를 사용해 부모 생성자의 매개변수에 매개값을 전달하여 호출하면서 부모 클래스의 멤버를 먼저 초기화합니다.
  • 오버로딩된 부모 클래스의 생성자가 없다고 하더라도 부모 클래스의 기본 생성자를 호출해야 합니다.
    • 따라서 눈에 보이지는 않지만 컴파일러가 super();를 자식 클래스 생성자 첫 줄에 자동으로 추가해 줍니다.
  • 간단히 말해, 자식 클래스에서 객체를 생성하면
  • 위 처럼 자식이 부모 클래스의 필드, 메서드를 포함함으로 자식의 객체에도 부모의 필드, 메서드가 포함되어야한다.
  • 당연히 객체가 새로 생성되는 만큼 부모 필드도 초기화를 해줘야하는데
  • 위의 코드에서 super(model, color, price);로 초기화를 따로 해줬지만, 안써도 자동으로 컴파일러가 초기화 해준다는 말
  • 쉽게 말하면, 자식 클래스라는 붕어빵 틀(자식 클래스) 안에 부모 클래스 호두과자 틀(부모 클래스)이 또 있다면 당연히 붕어빵을 만들기 전에 붕어빵 틀만이 아니라 호두과자 틀도 청소해야한다.(초기화)

 


 

 

참조 변수의 타입 변환

자동 타입 변환

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

  • 자식 객체는 부모 객체의 멤버를 상속받기 때문에 부모와 동일하게 취급될 수 있습니다.
    • 예를 들어 포유류 클래스를 상속받은 고래 클래스가 있다면 포유류 고래 = 고래 객체; 가 성립될 수 있습니다.
    • 왜냐하면 고래 객체는 포유류의 특징인 모유 수유 행위를 가지고 있기 때문입니다.
    • 다만 주의할 점은 부모 타입 변수로 자식 객체의 멤버에 접근할 때는 부모 클래스에 선언된 즉, 상속받은 멤버만 접근할 수 있습니다. 
class Mammal {
    // 포유류는 새끼를 낳고 모유수유를 한다.
    public void feeding() {
        System.out.println("모유수유를 합니다.");
    }
}

class Whale extends Mammal {
    // 고래는 포유류 이면서 바다에 살며 수영이 가능하다.
    public void swimming() {
        System.out.println("수영하다.");
    }

    @Override
    public void feeding() {
        System.out.println("고래는 모유수유를 합니다.");
    }
}

public class Main {
    public static void main(String[] args) {
        // 고래는 포유류이기 때문에 포유류 타입으로 변환될 수 있습니다.
        Mammal mammal = new Whale();

        // 하지만 포유류 전부가 바다에 살고 수영을 할 수 있는 것은 아니기 때문에
        // 수영 하다 메서드는 실행 불가
        // 즉, 부모 클래스에 swimming이 선언되어있지 않아서 사용 불가능합니다.
        // mammal.swimming(); // 오류 발생

        // 반대로 모든 포유류가 전부 고래 처럼 수영이 가능한 것이 아니기 때문에 타입변환이 불가능합니다.
        // 즉, 부모타입의 객체는 자식타입의 변수로 변환될 수 없습니다.
        // Whale whale = new Mammal(); // 오류 발생

        mammal.feeding();
    }
}

 

  • Mammal mammal = new Whale();은 자식 클래스인 Whale의 객체를 만들었지만, 이를 부모 클래스 타입 변수인 mammal에 할당했다. 
  • 원래 안될 것 같은 이 행위가 되는 이유는, 자식이 부모의 일부분의 데이터를 공유하기 때문이다.
  • 즉 위 처럼 부모 클래스 Mammal이 가지고 있는 메서드나 필드가 아닌 요청은 오류가 나고 나머지는 가능한것

 

강제 타입 변환

🧩 자식 타입 변수 = (자식 타입) 부모 타입 객체;

  • 부모 타입 객체는 자식 타입 변수로 자동으로 타입 변환되지 않습니다.
  • 이럴 때는 (자식 타입) 즉, 타입 변환 연산자를 사용하여 강제로 자식 타입으로 변환할 수 있습니다.
class Animal {
    void makeSound() {
        System.out.println("Some sound");
    }
}

class Dog extends Animal {
    void bark() {
        System.out.println("Woof Woof!");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal animal = new Dog();  // 부모 타입 변수에 자식 타입 객체 할당
        animal.makeSound();  // Animal 클래스의 메서드 호출

        // 강제 타입 변환
        Dog dog = (Dog) animal;  // Animal 타입을 Dog 타입으로 강제 변환
        dog.bark();  // Dog 클래스의 메서드 호출
    }
}
더보기

설명:

  • Animal animal = new Dog();에서 자식 객체인 Dog를 부모 타입인 Animal 변수에 할당합니다.
  • animal.makeSound();는 부모 클래스인 Animal의 메서드를 호출합니다.
  • 그 후, animal 변수는 Dog 객체를 참조하고 있기 때문에, 강제 타입 변환을 통해 Animal 타입 변수 animal을 Dog 타입 변수 dog로 변환합니다.
  • 강제 타입 변환 (Dog) animal을 통해 이제 dog 변수는 Dog 클래스에서 정의한 bark() 메서드를 호출할 수 있게 됩니다.
  • "Woof Woof!"가 결과로 나옵니다.
  • 다만 무조건 강제 타입 변환을 할 수 있는 것은 아닙니다.
    • 자식 타입 객체가 부모 타입으로 자동 타입 변환된 후 다시 자식 타입으로 변환될 때만 강제 타입 변환이 가능합니다.
    • 부모 타입 변수로는 자식 타입 객체의 고유한 멤버를 사용할 수 없기 때문에 사용이 필요한 경우가 생겼을 때 강제 타입 변환을 사용합니다.
Mammal newMammal = new Mammal();
Whale newWhale = (Whale) newMammal; // ClassCastException 발생


public class Main {
    public static void main(String[] args) {
        // 자동 타입변환된 부모타입의 변수의 자식 객체
        Mammal mammal = new Whale();
        mammal.feeding();

        // 자식객체 고래의 수영 기능을 사용하고 싶다면
        // 다시 자식타입으로 강제 타입변환을 하면된다.
        Whale whale = (Whale) mammal;
        whale.swimming();

        Mammal newMammal = new Mammal();
        // Whale newWhale = (Whale) newMammal;
    }
}

 

  • Mammal mammal = new Whale();이걸로 자동 타입 변환 한 상태면, Whale whale = (Whale) mammal;로 다시 자식타입으로 강제로 되돌릴 수 있다.
  • 되돌리는게 된다는 것이기 때문에, 부모를 자식의 변수에 할당하는 것은 불가능하다.
  • 용암을 5칸(자식)짜리 틀을 들고왔는데, 3칸(부모)에 부워버릴 수 없다. 다 넘친ㄷ;;;
  • 그러니까 저 말은 3칸을 5칸으로 늘렸던 걸 다시 3칸으로 줄인다는 말이다.

 

다형성

🧩 다형성이란 ‘여러 가지 형태를 가질 수 있는 능력’을 의미합니다.

  • 예를 들어 자동차의 핸들을 교체하면 핸들링이 부드러워지고 바퀴를 교체하면 승차감이 좋아집니다.
  • 소프트웨어 또한 구성하고 있는 객체를 바꿨을 때 소프트웨어의 실행 성능 및 결과물이 다르게 나올 수 있습니다.
  • 일전에 배운 참조 변수 타입 변환을 활용해서 다형성을 구현할 수 있습니다. 
public Car(Tire tire) {
    this.tire = tire;
}

...

Car car1 = new Car(new KiaTire("KIA"));
Car car2 = new Car(new HankookTire("HANKOOK"));
  • 위에서 본것 처럼 객체도 된다. 지금 위 예시는 자식 객체를 선언하고 그 자식 객체를 부모 타입 객체로 다시 재 선언한 코드이다.

 

Tire getHankookTire() {
    return new HankookTire("HANKOOK");
}

Tire getKiaTire() {
    return new KiaTire("KIA");
}

...

Tire hankookTire = car1.getHankookTire();
KiaTire kiaTire = (KiaTire) car2.getKiaTire();
  • 심지어는 매개변수도 된다... 여기서도 마찬가지로 반환 타입이 부모 타이어이기 때문에 자식 타이어 객체들을 반환값으로 지정할 수 있다. 당연히 이 자동 타입 변환 후라면 다시 강제 타입 변환도 된다......

 

  • 아마 위의 설명만으로는 이걸 왜 하는지 이해가 안될거다. 간단히 설명하면
// Tire 클래스 (부모 클래스)
class Tire {
    String brand;

    public Tire(String brand) {
        this.brand = brand;
    }

    public void getTireInfo() {
        System.out.println("Tire Brand: " + brand);
    }
}

// KiaTire와 HankookTire는 Tire를 상속받는 자식 클래스들
class KiaTire extends Tire {
    public KiaTire(String brand) {
        super(brand);
    }
}

class HankookTire extends Tire {
    public HankookTire(String brand) {
        super(brand);
    }
}

// Car 클래스
class Car {
    Tire tire;

    public Car(Tire tire) {
        this.tire = tire;  // Tire 타입 객체를 받는다
    }

    public void showTireInfo() {
        tire.getTireInfo();  // Tire 객체의 메서드 호출
    }
}

public class Main {
    public static void main(String[] args) {
        // 다양한 Tire 타입을 받아 Car 객체 생성 가능
        Car car1 = new Car(new KiaTire("KIA"));
        Car car2 = new Car(new HankookTire("HANKOOK"));

        car1.showTireInfo();  // Tire Brand: KIA
        car2.showTireInfo();  // Tire Brand: HANKOOK
    }
}

왜 이렇게 해야 할까?

  • 자동차의 타이어는 여러 종류일 수 있습니다. KiaTire, HankookTire 등 다양한 브랜드의 타이어가 있을 수 있는데, Car 클래스는 이 타이어를 구체적인 종류와 관계없이 받아들일 수 있어야 합니다.
  • 타이어 브랜드가 늘어난다고 해서 Car 클래스를 매번 수정하는 것보다는, 부모 클래스인 Tire 타입으로 받아들여 자식 클래스에서 Tire 객체를 사용하게 하면, 확장성이 좋아지고 변경에 강한 코드가 됩니다.

  • 말이 어려운데, 자 여기서 Car를 만들고자 한다. 자동차는 타이어라는 자식 클래스를 포함한다.
  • 그런데 타이어에 A,B 종류가 있다면?
  • Car가 tire의 종류를 파악하려면 , car -> 요청 -> tire -> 요청 -> A의 과정을 거친다
  • 만약 tire가 A의 정보를 바로 접속해서 이미 들고 있다면? car -> 요청 -> tire(자식 클래스 A에서 정보 이미 받음)
  • 더 빠르다.
변형 X car -> 요청 -> tire -> 요청 -> A
변형 O  car -> 요청 -> tire(자식 클래스 A에서 정보 다이렉트로 받음)

 

(변형 없이 접근)

// 부모 클래스 : Tire
class Tire {
    String brand;

    public Tire(String brand) {
        this.brand = brand;
    }

    public void getTireInfo() {
        System.out.println("Tire Brand: " + brand);
    }
}

// 자식 클래스 : KiaTire
class KiaTire extends Tire {
    public KiaTire(String brand) {
        super(brand);
    }

    public void getKiaTireInfo() {
        System.out.println("Kia Tire Information: " + brand);
    }
}

// 자식 클래스 : HankookTire
class HankookTire extends Tire {
    public HankookTire(String brand) {
        super(brand);
    }

    public void getHankookTireInfo() {
        System.out.println("Hankook Tire Information: " + brand);
    }
}

// 자동차 클래스 : Car
class Car {
    Tire tire;

    public Car(Tire tire) {
        this.tire = tire;
    }

    // 변형 없이 타이어 종류에 맞는 메서드 직접 호출
    public void showTireInfo() {
        if (tire instanceof KiaTire) {
            KiaTire kiaTire = (KiaTire) tire;
            kiaTire.getKiaTireInfo();
        } else if (tire instanceof HankookTire) {
            HankookTire hankookTire = (HankookTire) tire;
            hankookTire.getHankookTireInfo();
        } else {
            tire.getTireInfo();
        }
    }
}

public class Main {
    public static void main(String[] args) {
        // 다양한 타이어 종류를 Car에 할당
        Car car1 = new Car(new KiaTire("KIA"));
        car1.showTireInfo();  // Kia Tire Information: KIA

        Car car2 = new Car(new HankookTire("HANKOOK"));
        car2.showTireInfo();  // Hankook Tire Information: HANKOOK
    }
}

 

(다형성 접근)

// 부모 클래스 : Tire
class Tire {
    String brand;

    public Tire(String brand) {
        this.brand = brand;
    }

    public void getTireInfo() {
        System.out.println("Tire Brand: " + brand);
    }
}

// 자식 클래스 : KiaTire
class KiaTire extends Tire {
    public KiaTire(String brand) {
        super(brand);
    }

    @Override
    public void getTireInfo() {
        System.out.println("Kia Tire Information: " + brand);
    }
}

// 자식 클래스 : HankookTire
class HankookTire extends Tire {
    public HankookTire(String brand) {
        super(brand);
    }

    @Override
    public void getTireInfo() {
        System.out.println("Hankook Tire Information: " + brand);
    }
}

// 자동차 클래스 : Car
class Car {
    Tire tire;

    public Car(Tire tire) {
        this.tire = tire;
    }

    // 다형성 활용, 부모 타입으로 자식 클래스 메서드를 호출
    public void showTireInfo() {
        tire.getTireInfo();  // Tire 타입으로 메서드 호출
    }
}

public class Main {
    public static void main(String[] args) {
        // 다양한 타이어 종류를 Car에 할당
        Car car1 = new Car(new KiaTire("KIA"));
        car1.showTireInfo();  // Kia Tire Information: KIA

        Car car2 = new Car(new HankookTire("HANKOOK"));
        car2.showTireInfo();  // Hankook Tire Information: HANKOOK
    }
}

 

  • 이게 더 효율적이다.... 정말 자바는 말 그대로 효율의 극한, 중복을 정말 정말 정말 극혐한다.....
  • 위 방식으로 굳이 새로운 정보가 필요할 때마다 클래스를 수정할 필요 없이 Main에서 다형성 코딩만 진행하면되고 관리 유지, 오류 방지에도 탁월하다.... (만든 사람이 진짜 변태인가 보다...)

 

instanceof

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

  • 이 명령어를 통해서 해당 객체가 내가 의도하는 클래스의 객체인지 확인할 수 있습니다.
  • {대상 객체} instance of {클래스 이름} 와 같은 형태로 사용하면 응답값은 boolean입니다. 
  • 다형성 설명 보면 알겠지만, 상당이 코드가 복잡해진다. 
  • 이로 인해 프로그래머가 코드를 짜면서도 이 객체(붕어빵)가 어디 클래스(설계도)를 바탕으로 만들어 진건지 헷갈리는 경우가 있는데
  • 이때 해당 객체가 어느 클래스의 객체인지 확인하기 위해 있는 기능이다.
// 다형성

class Parent { }
class Child extends Parent { }
class Brother extends Parent { }


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

				Parent pc = new Child();  // 다형성 허용 (자식 -> 부모)

        Parent p = new Parent();

        System.out.println(p instanceof Object); // true 출력
        System.out.println(p instanceof Parent); // true 출력
        System.out.println(p instanceof Child);  // false 출력

        Parent c = new Child();

        System.out.println(c instanceof Object); // true 출력
        System.out.println(c instanceof Parent); // true 출력
        System.out.println(c instanceof Child);  // true 출력

    }
}

 

 

 


 

 

추상 클래스

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

  • abstract 키워드를 사용하여 추상 클래스를 선언할 수 있습니다.
public abstract class 추상클래스명 {
		abstract 리턴타입 메서드이름(매개변수, ...);
}
  • 추상 클래스는 추상 메서드를 포함할 수 있습니다.
    • 추상 메서드가 없어도 추상 클래스로 선언할 수 있습니다.
  • 추상 클래스는 자식 클래스에 상속되어 자식 클래스에 의해서만 완성될 수 있습니다.
  • 추상 클래스는 여러 개의 자식 클래스들에서 공통적인 필드나 메서드를 추출해서 만들 수 있습니다.
  • 추상 메서드는 일반적인 메서드와는 다르게 블록{ }이 없습니다. 즉, 정의만 할 뿐, 실행 내용은 가지고 있지 않습니다.

 

추상 클래스는 왜 쓰는가?

  1. 공통된 기능을 구현: 추상 클래스는 여러 자식 클래스가 공통적으로 가지는 기능을 제공하는데 유용합니다. 예를 들어, 여러 종류의 자동차가 있지만, 모든 자동차는 움직이는 기능이 필요하고, 이 기능을 추상 클래스에 구현할 수 있습니다.
  2. 구체적인 구현과 공통의 계약을 결합: 추상 클래스는 구체적인 메서드 구현을 제공하고, 일부는 추상 메서드로 남겨두어 자식 클래스가 구현하도록 할 수 있습니다. 이를 통해, 자식 클래스는 기본 구현을 상속받고, 필요한 부분만 구체화합니다.
  3. 상태(필드) 공유: 추상 클래스는 **상태(필드)**를 가질 수 있기 때문에, 자식 클래스에서 공통적으로 사용하는 값을 선언하고, 이를 자식 클래스에서 상속받아 사용할 수 있습니다.
  4. 일관성 유지: 추상 클래스를 사용함으로써, 모든 자식 클래스가 공통적으로 특정 메서드를 구현하도록 강제할 수 있습니다. 이를 통해 일관된 동작을 보장할 수 있습니다.

 

추상 클래스 상속

public class 클래스명 extends 추상클래스명 {
		@Override
    public 리턴타입 메서드이름(매개변수, ...) {
		       // 실행문
    }
}

 

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

< 예제 >

public abstract class Car {
    String company; // 자동차 회사 : GENESIS
    String color; // 자동차 색상
    double speed;  // 자동차 속도 , km/h

    public double gasPedal(double kmh) {
        speed = kmh;
        return speed;
    }

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

    public abstract void horn();
}

public class BenzCar {
    String company; // 자동차 회사 : GENESIS
    String color; // 자동차 색상
    double speed;  // 자동차 속도 , km/h

    public double gasPedal(double kmh) {
        speed = kmh;
        return speed;
    }

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

    public void horn() {
        System.out.println("Benz 빵빵");
    }

}

public class AudiCar {
    String company; // 자동차 회사 : GENESIS
    String color; // 자동차 색상
    double speed;  // 자동차 속도 , km/h

    public double gasPedal(double kmh) {
        speed = kmh;
        return speed;
    }

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

    public void horn() {
        System.out.println("Audi 빵빵");
    }

}

public class ZenesisCar {
    String company; // 자동차 회사 : GENESIS
    String color; // 자동차 색상
    double speed;  // 자동차 속도 , km/h
    
    public double gasPedal(double kmh) {
        speed = kmh;
        return speed;
    }

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

    public void horn() {
        System.out.println("Zenesis 빵빵");
    }

}

public class Main {
    public static void main(String[] args) {
        Car car1 = new BenzCar();
        car1.horn();
        System.out.println();

        Car car2 = new AudiCar();
        car2.horn();
        System.out.println();

        Car car3 = new ZenesisCar();
        car3.horn();
    }
}
  • BenzCar, AudiCar, GenesisCar 는 horn() 메서드의 내용에 차이가 존재합니다.
  • 따라서 horn() 메서드를 추상 메서드로 선언하여 자식 클래스에서 재정의 될 수 있도록 합니다.
  • BenzCar, AudiCar, GenesisCar 는 horn() 메서드의 내용에 차이가 존재합니다.
  • 즉 위의 자식 클래스에서 horn()메서드만 내용이 제각각이라는거
  • 각자 메서드 선언을 하면 3번 해야하니
  • 추상 클래스를 선언해서 메서드 선언을 1번으로 줄이고
  • 자식들이 내용만 채우라는것....
  • 아래 참고...
public class BenzCar extends Car {

    @Override
    public void horn() {
        System.out.println("Benz 빵빵");
    }
}

public class AudiCar extends Car {

    @Override
    public void horn() {
        System.out.println("Audi 빵빵");
    }
}

public class AudiCar extends Car {

    @Override
    public void horn() {
        System.out.println("Audi 빵빵");
    }
}

public abstract class Car {
    String company; // 자동차 회사
    String color; // 자동차 색상
    double speed;  // 자동차 속도 , km/h

    public double gasPedal(double kmh) {
        speed = kmh;
        return speed;
    }

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

    public abstract void horn(); // 추상클래스~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
}

public class Main {
    public static void main(String[] args) {
        Car car1 = new BenzCar();
        car1.horn();
        System.out.println();

        Car car2 = new AudiCar();
        car2.horn();
        System.out.println();

        Car car3 = new ZenesisCar();
        car3.horn();
    }
}

 

 


<  일단 정리 >

 

  • 다형성은 데이터 접근이 많을 때 그를 단순화 하기 위해 사용 ( 부모가 자식에게 손자의 정보를 달라고 할때)
  • 추상 클래스는 여러 자식들 중 메서드가 다 다른 애가 있으면 그걸 추상 클래스의 메서드로 만들어서 자식들이 내용만 채우게 (추상 : 그리기 / 자식들 : 원, 삼각형 동그라미 등)
  • 다형성은 다양한 데이터나 객체를 하나의 부모 타입으로 처리해야 할 때 유용하게 사용됩니다. 예를 들어, 여러 자식 클래스의 객체들을 부모 클래스 타입으로 다루면서, 각 자식 클래스의 고유한 기능을 호출하는 상황에서 사용됩니다.
  • 추상 클래스는 여러 자식 클래스가 공통된 기능을 가지고 있어야 할 때 유용합니다. 부모 클래스에서 추상 메서드를 정의해 두고, 각 자식 클래스가 그 메서드를 어떻게 구현할지 결정하게 만들 수 있습니다. 즉, 자식들이 부모 클래스의 특정 기능을 "구현"하게 강제하는 경우입니다.

< 요약 >

  • 추상 클래스는 공통적인 기능을 정의하지만, 세부 구현은 자식 클래스에서 하도록 강제합니다.
  • 다형성은 부모 타입으로 여러 자식 객체를 다룰 수 있게 해주며, 각 자식 클래스의 고유 메서드를 호출할 수 있도록 합니다.
  • 오버라이딩은 부모 클래스에서 정의한 메서드를 자식 클래스에서 구체적으로 구현하여 각 자식 클래스에 맞는 동작을 하도록 만듭니다.

 

 

 

오늘의 문제

다음은 자동차와 관련된 클래스 구조입니다. Car 클래스를 부모 클래스로 하고, 이를 상속받은 Sedan과 SUV 클래스를 만들어보세요. 각 차종은 drive라는 메서드를 오버라이딩하여 다르게 동작해야 합니다. 이때 Car는 추상 클래스이며, drive 메서드는 추상 메서드로 정의되어야 합니다. 또한, 다형성을 활용하여 Car 타입의 배열을 만들어 다양한 차들을 처리하는 방법을 구현하세요.

요구사항

  1. Car 클래스는 추상 클래스이며, drive()라는 추상 메서드를 가지고 있습니다.
  2. Sedan 클래스와 SUV 클래스는 Car 클래스를 상속받으며, 각자의 drive() 메서드를 구현해야 합니다.
  3. Main 클래스에서 Car 타입의 배열을 이용해 여러 종류의 자동차 객체를 생성하고, 다형성을 이용해 각 차종의 drive() 메서드를 호출하는 코드 작성.
// 1. 부모 클래스 Car
abstract class Car {
    String model;
    
    public Car(String model) {
        this.model = model;
    }
    
    // 2. 추상 메서드 drive()
    public abstract void drive();
}

// 3. 자식 클래스 Sedan
class Sedan extends Car {
    
    public Sedan(String model) {
        super(model);
    }
    
    // 4. 오버라이딩된 drive() 메서드
    @Override
    public void drive() {
        System.out.println(model + " 세단은 부드럽게 달립니다.");
    }
}

// 5. 자식 클래스 SUV
class SUV extends Car {
    
    public SUV(String model) {
        super(model);
    }
    
    // 6. 오버라이딩된 drive() 메서드
    @Override
    public void drive() {
        System.out.println(model + " SUV는 험한 길도 잘 달립니다.");
    }
}

// 7. Main 클래스
public class Main {
    public static void main(String[] args) {
        // 8. Car 타입 배열 선언
        Car[] cars = new Car[2];
        
        // 9. 다양한 차종 생성
        cars[0] = new Sedan("아반떼");
        cars[1] = new SUV("투싼");
        
        // 10. 다형성 활용하여 각 차의 drive 메서드 호출
        for (Car car : cars) {
            car.drive(); // 각 차종의 구체적인 drive 메서드가 호출됨
        }
    }
}

 

 

더보기

해설

  1. 추상 클래스 Car: Car 클래스는 abstract로 선언되어 있으며, drive()라는 추상 메서드를 정의하고 있습니다. 추상 메서드는 자식 클래스에서 반드시 구현해야 하므로, Car 클래스는 직접 drive()를 구현하지 않고, 자식 클래스들이 이 메서드를 구현하도록 강제합니다.
  2. 자식 클래스 Sedan과 SUV: Sedan과 SUV는 Car 클래스를 상속받고, 각자의 drive() 메서드를 구현합니다. 이때 drive() 메서드는 Car 클래스에서 정의된 추상 메서드를 오버라이딩하여 각자의 방식으로 자동차가 달리는 방식을 다르게 구현합니다.
  3. 다형성: Main 클래스에서 Car 타입의 배열을 만들고, 그 배열에 Sedan과 SUV 객체를 각각 저장합니다. 이후 Car 타입으로 선언된 변수 car를 통해 Sedan과 SUV 객체를 다룰 수 있습니다. 이때 각 객체는 다형성에 의해 자신의 drive() 메서드를 호출합니다. 즉, Sedan과 SUV는 부모 클래스 Car의 타입을 가지지만, 각자 다른 구현을 가진 drive() 메서드를 호출하게 됩니다.
  4. 오버라이딩: Sedan과 SUV 클래스에서 Car 클래스의 drive() 메서드를 오버라이딩하여, 각자 다른 방식으로 자동차의 운전 기능을 구현합니다. 이 메서드는 부모 클래스의 메서드를 자식 클래스에서 수정한 것으로, 해당 자식 객체에 맞는 동작을 하게 됩니다.

 

<결과>

아반떼 세단은 부드럽게 달립니다.
투싼 SUV는 험한 길도 잘 달립니다.

 

'Back-End (Web) > JAVA' 카테고리의 다른 글

[JAVA] 오류 및 예외에 대한 이해  (1) 2024.11.12
[JAVA] 인터페이스  (2) 2024.11.12
[JAVA] package와 import  (0) 2024.11.12
[JAVA] 접근 제어자  (2) 2024.11.12
[JAVA] This와 This()  (0) 2024.11.12

package

📌 패키지(Package)는 Java에서 클래스를 그룹으로 묶어 관리하는 폴더 같은 개념이다. 관련된 클래스들을 함께 묶어서 하나의 패키지로 정의함으로써 코드의 구조를 더 체계적이고 조직적으로 유지할 수 있다. 

 

패키지의 장점

  1. 코드의 논리적 그룹화: 비슷한 기능이나 역할을 하는 클래스들을 하나의 패키지에 넣어 관리할 수 있습니다.
  2. 네임스페이스 제공: 서로 다른 패키지에 같은 이름의 클래스가 있어도 충돌이 발생하지 않습니다.
  3. 접근 제어 강화: 특정 클래스나 멤버가 패키지 내에서만 접근 가능하게 하여 클래스 간의 의존성을 조절할 수 있습니다.

패키지 선언

패키지는 package 키워드를 통해 클래스 파일의 최상단에 선언하며, 선언된 패키지 경로는 실제 파일 경로와 일치해야 합니다.

  • 패키지는 상위 패키지와 하위 패키지를 도트(.)로 구분합니다.
  • package 상위 패키지.하위 패키지; 이렇게 선언할 수 있습니다.
  • 예를 들어 oop.pk1이라는 패키지와 oop.pk2라는 패키지가 있다고 가정해 보겠습니다.
    • 두 패키지에 모두 Car 클래스가 존재한다면? 그리고 이를 사용하려고 한다면?
    • Java는 패키지의 경로를 통해 이를 구분합니다.
    • 아래 예제를 통해 확인해 보겠습니다. 

 

oop.pk1.Car 클래스

package oop.pk1;

public class Car {
    public void horn() {
        System.out.println("pk1 빵빵");
    }
}

 

oop.pk2.Car 클래스

package oop.pk2;

public class Car {
    public void horn() {
        System.out.println("pk2 빵빵");
    }
}

 

main 메서드

package oop.main;

public class Main {
    public static void main(String[] args) {
        oop.pk1.Car car = new oop.pk1.Car();
        car.horn(); // pk1 빵빵

        oop.pk2.Car car2 = new oop.pk2.Car();
        car2.horn(); // pk2 빵빵
    }
}

 

 

 

import

📌 Java에서 다른 패키지에 있는 클래스를 가져와서 사용할 수 있게 해주는 키워드입니다. 이를 통해 특정 패키지에 속한 클래스들을 코드에 포함하고, 해당 클래스의 메서드와 필드에 접근할 수 있습니다.

  • 위에서 살펴본 oop.pk1이라는 패키지와 oop.pk2라는 패키지를 import로 명시해 보겠습니다.
    • import oop.pk1.Car;, import oop.pk2.Car;
  • 클래스 이름을 생략하고 *를 사용하여 import oop.pk1.*; 이렇게 표현하면 oop.pk1 패키지 아래에 있는 모든 클래스를 사용할 수 있습니다.
  • 다만 서로 다른 패키지에 있는 같은 이름의 클래스를 동시에 사용하려면 해당 클래스에 패키지 명을 전부 명시해야 합니다.
  • 아래 예제를 통해 확인해 보겠습니다. 


oop.pk1.Car 클래스

package oop.pk1;

public class Car {
    public void horn() {
        System.out.println("pk1 빵빵");
    }
}

 

oop.pk2.Car 클래스

package oop.pk2;

public class Car {
    public void horn() {
        System.out.println("pk2 빵빵");
    }
}

 

main 메서드

package oop.main;

public class Main {
    public static void main(String[] args) {
        oop.pk1.Car car = new oop.pk1.Car();
        car.horn(); // pk1 빵빵

        oop.pk2.Car car2 = new oop.pk2.Car();
        car2.horn(); // pk2 빵빵
    }
}

 

 

위처럼

import 패키지명.클래스명;

이런 방식으로 하나씩 코드에 추가하는 방법도 있지만

import 패키지명.*;

이런 방식으로 패키지(폴더)내 모든 클래스를 가져올 수도 있다.

'Back-End (Web) > JAVA' 카테고리의 다른 글

[JAVA] 인터페이스  (2) 2024.11.12
[JAVA] 클래스 간의 관계와 상속  (0) 2024.11.12
[JAVA] 접근 제어자  (2) 2024.11.12
[JAVA] This와 This()  (0) 2024.11.12
[JAVA] 생성자  (1) 2024.11.12

접근 제어자

  • 클래스, 메서드, 변수 등의 접근 범위를 제한하는 키워드로, 외부에서 사용할 수 있는지 여부를 결정한다
  • 클래스, 맴버 변수, 메서드 생성자에 사용되고, 지정되어 있지 않으면 default로 지정된다.

1. public

  • 범위: 모든 클래스에서 접근 가능.
  • 사용처: 클래스, 메서드, 필드.
  • 설명: public으로 선언된 요소는 모든 클래스에서 접근할 수 있습니다. 프로젝트 내 어디서든지 사용 가능한 공개 멤버로 설정됩니다.

2. protected

  • 범위: 같은 패키지 내의 클래스나 자식 클래스에서 접근 가능.
  • 사용처: 메서드, 필드 (클래스에는 사용할 수 없음).
  • 설명: protected는 동일 패키지 내의 모든 클래스와 다른 패키지의 자식 클래스에서 접근할 수 있도록 허용합니다.

3. default (아무 접근 제어자도 명시하지 않은 경우)

  • 범위: 같은 패키지 내에서만 접근 가능.
  • 사용처: 클래스, 메서드, 필드.
  • 설명: 접근 제어자를 명시하지 않으면 패키지 전용(default) 접근 수준이 됩니다. 같은 패키지 내에서만 접근할 수 있으며, 패키지를 벗어나면 접근할 수 없습니다.

4. private

  • 범위: 같은 클래스 내에서만 접근 가능.
  • 사용처: 메서드, 필드 (클래스에는 사용할 수 없음).
  • 설명: private로 선언된 멤버는 해당 클래스 내에서만 접근할 수 있습니다. 외부에서는 접근할 수 없으므로 정보 은닉에 유용합니다.

 

< 접근 제어자 정리표 >

접근 제어자 같은 클래스 같은 패키지 (클래스 폴더) 자식 클래스 외부 클래스
public O O O O
protected O O O X
default O O X X
private O X X X

 

< 사용 가능한 접근 제어자  >

클래스 public, default
메서드 & 멤버 변수 public, protected, default, private
지역변수 없음

 

접근 제어자를 이용한 캡슐화 (은닉성)

  • 접근제어자는 클래스 내부에 선언된 데이터를 보호하기 위해서 사용합니다.
  • 유효한 값을 유지하도록, 함부로 변경하지 못하도록 접근을 제한하는 것이 필요합니다.

생성자의 접근 제어자

  • 생성자에 접근 제어자를 사용함으로 인스턴스의 생성을 제한할 수 있습니다.
  • 일반적으로 생성자의 접근 제어자는 클래스의 접근 제어자와 일치합니다.

 

 

 

+ 같은 패키지 내

src
├── packageA
│   └── Car.java
└── packageB
    └── Main.java

 

  • Car 클래스에 default 접근 제어자가 적용된 필드나 메서드는 packageA 내에서만 접근할 수 있습니다.
  • packageB에 있는 Main 클래스에서는 Car의 default 필드나 메서드에 접근할 수 없습니다.

 

'Back-End (Web) > JAVA' 카테고리의 다른 글

[JAVA] 클래스 간의 관계와 상속  (0) 2024.11.12
[JAVA] package와 import  (0) 2024.11.12
[JAVA] This와 This()  (0) 2024.11.12
[JAVA] 생성자  (1) 2024.11.12
[JAVA] 인스턴스 멤버와 클래스 멤버  (0) 2024.11.12

This

 

this는 객체 즉, 인스턴스 자신을 표현하는 키워드다.

 

이게 무슨 말인가 싶을건데, 아래의 예시를 보면 이해가 쉽다

매개변수 값이 변경된다

 

만약 이처럼 생성자를 선언하는데 매개변수명(생성자의 지역변수)객체의 필드명(클래스의 맴버변수)이 동일할 경우 오류가 발생하지는 않지만 생성자 블록 내부에서 해당 변수들은 객체의 필드가 아닌 가장 가까운 매개변수명을 가리키게 됨으로 자기 자신에게 값을 대입하는 상황이 되어 버린다.

this 키워드를 통해 변수명에 해당하는 객체의 필드에 접근하여 받아온 매개변수의 매개값을 객체의 필드에 대입하여 저장할 수 있다.

 

Car returnInstance() {
    return this;
}

또한 this는 인스턴스 자신을 뜻하기 때문에 객체의 메서드에서 리턴 타입이 인스턴스 자신의 클래스 타입이라면 this를 사용하여 인스턴스 자신의 주소를 반환할 수도 있습니다. (여기선 car1)

 

 

This()

this(…)는 객체 즉, 인스턴스 자신의 생성자를 호출하는 키워드다.

 

 

이건 또 뭔말인가 싶을건데, 아래의 설명을 확인해 보자

public Car(String model) {
    this.model = model;
    this.color = "Blue";
    this.price = 50000000;
}

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

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

//-----------------------------------------------------------------------

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 class Main {
    public static void main(String[] args) {
        Car ★car1★ = new Car("Sedan", "Red");  // 객체 car1 생성
    }
}

 

2개는 같은 코드인데, ★대로 this는 car1(인스턴스)을 말한다.

 

< 장점 >

  • 객체 내부 생성자 및 메서드에서 해당 객체의 생성자를 호출하기 위해 사용될 수 있습니다.
  • 생성자를 통해 객체의 필드를 초기화할 때 중복되는 코드를 줄여줄 수 있습니다.

 

< ⚠️ 주의  >

  • this() 키워드를 사용해서 다른 생성자를 호출할 때는 반드시 해당 생성자의 첫 줄에 작성되어야 한다.
public Car(String model) {
    System.out.println("model = " + model);
    this(model, "Blue", 50000000);
}
  • 이처럼 this() 키워드로 다른 생성자 호출 이전에 코드가 존재하면 오류가 발생한다.

 

 

public class Car {

    static final String COMPANY = "GENESIS"; // 자동차 회사 : GENESIS
    String model; // 자동차 모델
    String color; // 자동차 색상
    double price; // 자동차 가격

    double speed;  // 자동차 속도 , km/h
    char gear = 'P'; // 기어의 상태, P,R,N,D
    boolean lights; // 자동차 조명의 상태


    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;
    }

    double gasPedal(double kmh, char type) {
        changeGear(type);
        speed = kmh;
        return speed;
    }

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

    char changeGear(char type) {
        gear = type;
        return gear;
    }

    boolean onOffLights() {
        lights = !lights;
        return lights;
    }

    void horn() {
        System.out.println("빵빵");
    }

    Car returnInstance() {
        return this;
    }
}






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

        // 생성자 오버로딩을 통해 여러 상황에서 자동차 생산
        // 제네시스 자동차를 생산 : static final String COMPANY = "GENESIS"; 상수 고정
        // 모든 자동차는 생산시 기어의 최초 상태 'P' 로 고정 : char gear = 'P'; 직접 대입하여 초기화

        // 모델을 변경하면서 만들 수 있고 색상 : Blue, 가격 50000000 고정
        Car car1 = new Car("GV60");
        System.out.println("car1.model = " + car1.model);
        System.out.println("car1.color = " + car1.color);
        System.out.println("car1.price = " + car1.price);
        System.out.println("car1.gear = " + car1.gear + "\n");

        // 모델, 색상을 변경하면서 만들 수 있고 가격 100000000 고정
        Car car2 = new Car("GV70", "Red");
        System.out.println("car2.model = " + car2.model);
        System.out.println("car2.color = " + car2.color);
        System.out.println("car2.price = " + car2.price);
        System.out.println("car2.gear = " + car2.gear + "\n");

        // GV80 모델, 색상 Black, 가격 120000000 으로 완전하게 고정된 경우
        Car car3 = new Car("GV80", "Black", 120000000);
        System.out.println("car3.model = " + car3.model);
        System.out.println("car3.color = " + car3.color);
        System.out.println("car3.price = " + car3.price);
        System.out.println("car3.gear = " + car3.gear + "\n");

        // this 키워드를 통해 car3 인스턴스 자신을 반환 : car3.returnInstance() = 인스턴스의 주소
        System.out.println(car3.returnInstance().model); // car3의 model
        System.out.println(car3.returnInstance().color); // car3의 color
        System.out.println(car3.returnInstance().price); // car3의 price

    }
}

 

'Back-End (Web) > JAVA' 카테고리의 다른 글

[JAVA] package와 import  (0) 2024.11.12
[JAVA] 접근 제어자  (2) 2024.11.12
[JAVA] 생성자  (1) 2024.11.12
[JAVA] 인스턴스 멤버와 클래스 멤버  (0) 2024.11.12
[JAVA] 객체의 필드와 메서드  (0) 2024.11.12

생성자

public Car() {} // 선언

...

Car car = new Car(); // 호출
  • 생성자는 객체가 생성될 때 호출되며 객체를 초기화하는 역할을 수행합니다.
  • 생성자는 반환 타입이 없고 이름은 클래스의 이름과 동일합니다.
  • new 연산자에 의해 객체가 생성되면서 Car(); 즉, 생성자가 호출됩니다.
  • 위와 같이 ()만 존재하는 경우를 기본 생성자라한다. 
  • 만약 생성자를 하나도 생성하지 않으면 컴파일러가 자동으로 기본 생성자를 자동으로 추가한다.
public class Car {
		public Car(String model) {} // 생성자 선언
		// 생성자가 한개 이상 선언되었기 때문에 기본 생성자를 추가하지 않음.
}
  • 즉 이 경우는 컴파일러는 기본 생성자를 생성하지 않음 (public Car가 선언되어 있음으로)
public class Car {
		public Car() {} // 컴파일러가 추가시켜줌
}

class Car { // 클래스가 public임으로 여기도 접근제어자는 없지만 public으로 기본 배치된다.
		Car() {} // 컴파일러가 추가시켜줌
}

컴파일러에 의해 생성되는 기본 생성자는 해당 클래스의 접근 제어자(public, …)를 따른다.

 

 

필드 초기화

  • 위의 기본 생성자처럼 생성자를 자동으로 추가하는 것이 아닌 프로그래머가 직접 지정하는 방식
  • 인스턴스마다 동일한 데이터를 가지는 필드는 초기값을 대입하는 것이 좋다.
public Car(String modelName, String colorName, double priceValue) {
    model = modelName;
    color = colorName;
    price = priceValue;
}

...

Car car = new Car(); // 오류 발생
더보기

한 개 이상의 생성자가 존재하기 때문에 컴파일러가 자동으로 기본 생성자를 추가해 주지 않기 때문에 기본 생성자가 존재하지 않아 오류가 발생합니다.

기본 생성자 필드 초기화
인스턴스마다 다른 값을 가져야 한다면 생성자를 통해서 필드를 초기화할 수 있습니다.

예를 들어 만들어지는 자동차마다 모델, 색상, 가격이 다르다면 생성자를 사용하여 필드의 값을 초기화하는 것이 좋습니다.

= 계속 변경할거면 굳이 직접 정해줄 이유가 없으니
반대로 인스턴스마다 동일한 데이터를 가지는 필드는 초기값을 대입하는 것이 좋습니다.

예를 들어 자동차가 만들어질 때마다 기어의 상태를 ‘P’로 고정해야 한다면 초기값을 직접 대입하는 것이 좋습니다.

= 변동이 없다면 미리 한번만 지정하면 되니

 

 

생성자 오버로딩

 

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

  • 예를 들어 우리가 자동차를 생성할 때 모델, 색상, 가격이 다른 자동차를 여러 대 생성할 수도 있고 색상만 다른 자동차를 여러 대 생성할 수도 있기 때문에 오버로딩을 사용하면 이를 효율적으로 처리할 수 있다. 
public Car(String modelName, String colorName, double priceValue)
public Car(String colorName, String modelName, double priceValue)

이거 될까 안될까?

 

더보기
  • modelName과 colorName 매개변수의 위치가 다르기 때문에 가능할 것처럼 보이지만
  • String, String, double : 매개변수의 개수, 타입, 순서가 동일하기 때문에 중복이 불가능합니다.
public class Car {

    static final String COMPANY = "GENESIS"; // 자동차 회사 : GENESIS
    String model; // 자동차 모델
    String color; // 자동차 색상
    double price; // 자동차 가격

    double speed;  // 자동차 속도 , km/h
    char gear = 'P'; // 기어의 상태, P,R,N,D
    boolean lights; // 자동차 조명의 상태


    public Car(String modelName) {
        model = modelName;
    }

    public Car(String modelName, String colorName) {
        model = modelName;
        color = colorName;
    }

    public Car(String modelName, String colorName, double priceValue) {
        model = modelName;
        color = colorName;
        price = priceValue;
    }

    double gasPedal(double kmh, char type) {
        changeGear(type);
        speed = kmh;
        return speed;
    }

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

    char changeGear(char type) {
        gear = type;
        return gear;
    }

    boolean onOffLights() {
        lights = !lights;
        return lights;
    }

    void horn() {
        System.out.println("빵빵");
    }
}

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

        // 기본 생성자 호출 오류 확인
        // Car car1 = new Car(); // 오류 발생

        // 생성자 오버로딩을 통해 여러 상황에서 자동차 생산
        // 제네시스 자동차를 생산 : static final String COMPANY = "GENESIS"; 상수 고정
        // 모든 자동차는 생산시 기어의 최초 상태 'P' 로 고정 : char gear = 'P'; 직접 대입하여 초기화

        // GV60 모델만 기본으로 선택
        Car car2 = new Car("GV60");
        System.out.println("car2.model = " + car2.model);
        System.out.println("car2.gear = " + car2.gear + "\n");

        // GV70 모델, 색상 Blue 만 기본으로 선택
        Car car3 = new Car("GV70", "Blue");
        System.out.println("car3.model = " + car3.model);
        System.out.println("car3.color = " + car3.color);
        System.out.println("car3.gear = " + car3.gear + "\n");

        // GV80 모델, 색상 Black, 가격 50000000 으로 완전하게 고정된 경우
        Car car4 = new Car("GV80", "Black", 50000000);
        System.out.println("car4.model = " + car4.model);
        System.out.println("car4.color = " + car4.color);
        System.out.println("car4.price = " + car4.price);
        System.out.println("car4.gear = " + car4.gear + "\n");

    }
}

 

'Back-End (Web) > JAVA' 카테고리의 다른 글

[JAVA] 접근 제어자  (2) 2024.11.12
[JAVA] This와 This()  (0) 2024.11.12
[JAVA] 인스턴스 멤버와 클래스 멤버  (0) 2024.11.12
[JAVA] 객체의 필드와 메서드  (0) 2024.11.12
[JAVA] 클래스 설계와 객체 생성  (0) 2024.11.12
public class Car {
    // 인스턴스 멤버 (각 객체마다 고유)
    String company; // 자동차 회사
    String model;   // 자동차 모델
    String color;   // 자동차 색상
    double price;   // 자동차 가격
    double speed;   // 자동차 속도, km/h 단위
    char gear;      // 기어 상태, P (주차), R (후진), N (중립), D (주행)
    boolean lights; // 자동차 조명 상태, 켜짐(true) 또는 꺼짐(false)

    // ★클래스 멤버 (모든 객체가 공유하는 값)★
    static int carCount = 0; // 생성된 자동차 객체 수

    // 기본 생성자: 매개변수 없이 Car 객체를 생성
    public Car(String company, String model, String color, double price) {
        this.company = company;
        this.model = model;
        this.color = color;
        this.price = price;
        this.speed = 0;  // 기본 속도 0
        this.gear = 'P'; // 기본 기어 P (주차)
        this.lights = false; // 기본적으로 조명은 꺼짐
        carCount++;  // 객체 생성 시 carCount 증가
    }

    // gasPedal 메서드: 속도를 설정하는 메서드로 kmh 값을 speed 필드에 저장하고 반환
    public double gasPedal(double kmh) {
        speed = kmh; // 자동차의 현재 속도를 kmh 값으로 설정
        return speed; // 설정된 속도 반환
    }

    // brakePedal 메서드: 속도를 0으로 설정하는 메서드로, speed 필드를 0으로 설정하고 반환
    public double brakePedal() {
        speed = 0; // 속도를 0으로 설정하여 정지 상태로 만듦
        return speed; // 속도 0을 반환
    }

    // changeGear 메서드: 기어 상태를 변경하는 메서드로, type 값을 gear 필드에 저장하고 반환
    public char changeGear(char type) {
        gear = type; // 기어를 매개변수 type의 값으로 설정
        return gear; // 설정된 기어 상태 반환
    }

    // onOffLights 메서드: 조명 상태를 반전시키는 메서드로, 현재 lights 값을 반전시키고 반환
    public boolean onOffLights() {
        lights = !lights; // 현재 조명 상태를 반전(true -> false, false -> true)
        return lights; // 변경된 조명 상태 반환
    }

    // horn 메서드: 경적을 울리는 메서드로, 호출 시 "빵빵" 소리를 콘솔에 출력
    public void horn() {
        System.out.println("빵빵"); // 경적 소리 출력
    }

    // ★클래스 메서드 (static): carCount를 출력하는 메서드로, 모든 객체에서 공유★
    public static void showCarCount() {
        System.out.println("총 자동차 객체 수: " + carCount);
    }
}

public class Main {
    public static void main(String[] args) {
        // Car 객체 생성
        Car car1 = new Car("Hyundai", "Sonata", "Red", 25000);
        Car car2 = new Car("Tesla", "Model S", "Black", 80000);
        Car car3 = new Car("Toyota", "Corolla", "Blue", 22000);

        // 각 자동차 객체에서 메서드 호출
        System.out.println("Car1 Speed: " + car1.gasPedal(100));  // Car1의 속도 설정
        System.out.println("Car2 Speed after braking: " + car2.brakePedal()); // Car2의 속도 정지
        System.out.println("Car3 Gear: " + car3.changeGear('D'));  // Car3의 기어 변경

        // 클래스 메서드 호출
        Car.showCarCount();  // 총 자동차 객체 수 출력 (클래스 변수 carCount 사용)

        // 각 자동차의 경적 울리기
        car1.horn();  // "빵빵" 출력
        car2.horn();  // "빵빵" 출력
        car3.horn();  // "빵빵" 출력
    }
}

 

기본적으로 지금까지 사용한 필드와 메서드 전부 인스턴스 맴버였다. 보통 클래스 맴버는 static이라는 메서드를 포함하는데, 이는 특정 인스턴트가 아닌 클래스 자체에 고정적으로 위치시킨다는 의미이다.(위의 ★로 된 코드가 클래스 맴버이다.)

 

간단히 말하면, Static은 전역변수와 유사한 역활을 한다고 생각하면 편하다. 모든 클래스 내의 객체가 공통적으로 가져야하는 값들에 Static을 사용한다. 위의 예시 처럼 '생성된 자동차 수'는 클래스 내에서 한개만 존재하고 이를 모두가 공유하면 됨으로 클래스 맴버에 속한다.

 

 

인스턴스 맴버

  • 인스턴스 멤버는 객체를 생성해야 사용할 수 있다고 했습니다.
  • 또한 객체의 인스턴스 필드는 각각의 인스턴스마다 고유하게 값을 가질 수 있습니다.
  • 그렇다면 객체가 인스턴스화할 때마다 객체의 메서드들은 인스턴스에 포함되어 매번 생성이 될까요?
    • 그렇지 않습니다. 매번 저장한다면 중복 저장으로 인해 메모리 효율이 매우 떨어지기 때문에 메서드는 메서드 영역에 두고서 모든 인스턴스들이 공유해서 사용합니다.
    • 대신 무조건 객체를 생성 즉, 인스턴스를 통해서만 메서드가 사용될 수 있도록 제한을 걸어둔 것입니다.

 

클래스 멤버

클래스는 Java의 클래스 로더에 의해 메서드 영역에 저장되고 사용됩니다.

  • 이때 클래스 멤버란 메서드 영역의 클래스와 같은 위치에 고정적으로 위치하고 있는 멤버를 의미합니다.
  • 따라서 클래스 멤버는 객체의 생성 필요 없이 바로 사용이 가능합니다. 

여기서 중요한게

1. 인스턴스 맴버는 클래스 맴버를 사용 가능하다

2. 클래스 맴버는 인스턴스 맴버를 사용 불가능하다. 

 

이유는 단순히, 인스턴스 맴버는 각 개체마다 값이 달라질 수 있다보니, static으로 전 클래스에 고정되는 전역변수 형태의 클래스 맴버에는 사용 불가능, 간단히, 클래스 맴버는 어떠한 객체든 공통으로 값이 같아야하니 객체마다 값이 다를 수 있는 인스턴스 맴버는 사용이 불가능하다.


<예시>

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

String getCompany() {
    return "(주)" + company;
}

 

  • Car 클래스를 통해 제품을 만들 때 만들어지는 자동차들의 회사가 “GENESIS”로 고정되어 있다고 가정해 보겠습니다.
    • 그렇다면 모든 Car 클래스의 객체마다 company 인스턴스 필드를 가지고 있을 필요 없이 클래스 필드로 만들어 공유하게 만든다면 메모리를 효율적으로 사용할 수 있습니다.
    • 또한 인스턴스 메서드인 getCompany()는 클래스 필드인 company를 사용할 수 있습니다.
static String setCompany(String companyName) {
    // System.out.println("자동차 모델 확인: " + model); // 인스턴스 사용 불가
    company = companyName;
    return company;
}
  • 자동차 회사를 변경할 수 있는 setCompany(String companyName)를 클래스 메서드로 만들어서 사용할 수 있습니다.
    • 이때 인스턴스 필드인 model을 사용하려고 하면 오류가 발생합니다.

클래스 맴버 사용


 

Car car = new Car(); // 객체 생성

car.company = "Ferrari";
String companyName2 = car.setCompany("Lamborghini");
  • 참조형 변수를 사용하여 클래스 멤버에 접근은 가능하지만 추천하지 않습니다.
    • 클래스 이름으로 접근하는 것이 좋습니다.

+ car.company에서 .(닷)은 참조 연산자입니다. 이 연산자는 객체의 필드메서드에 접근할 때 사용됩니다. 다시 말해, .은 객체를 통해 해당 객체의 속성이나 메서드에 접근하는 역할을 합니다. = . 은 '참조할게요' 라는 의미( 그렇다고 해서 .이 붙으면 다 참조형 변수라는건 아니다..  )

 

public class Car {

    static String company = "GENESIS"; // 자동차 회사 : GENESIS
    String model; // 자동차 모델
    String color; // 자동차 색상
    double price; // 자동차 가격

    double speed;  // 자동차 속도 , km/h
    char gear; // 기어의 상태, P,R,N,D
    boolean lights; // 자동차 조명의 상태


    public Car() {} // 기본 생성자

    double gasPedal(double kmh, char type) {
        changeGear(type);
        speed = kmh;
        return speed;
    }

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

    char changeGear(char type) {
        gear = type;
        return gear;
    }

    boolean onOffLights() {
        lights = !lights;
        return lights;
    }

    void horn() {
        System.out.println("빵빵");
    }

    String getCompany() {
        return "(주)" + company;
    }

    static String setCompany(String companyName) {
        // System.out.println("자동차 모델 확인: " + model); // 인스턴스 필드 사용 불가
        company = companyName;
        return company;
    }
}


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

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

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

        car.company = "Ferrari";
        System.out.println(car.company + "\n");

        String companyName2 = car.setCompany("Lamborghini");
        System.out.println("companyName2 = " + companyName2);
    }
}

 

 + 이거 헷갈릴 수 있는데, Car.company는 클래스 이름을 사용하여 클래스의 company 이름을 수정한거고, car.company가 Car 객체를 만들고 car가 참조형 변수로서 Car객체의 company 이름을 변경한거다.

 

+ 그러니까 클래스의 내용을 수정한건 Audi고 ferrari는 클래스라는 설계도를 기반으로한 객체 Car의 내용을 수정한거다


지역 변수


  • 메서드 내부에 선언한 변수를 의미합니다.
  • 메서드가 실행될 때마다 독립적인 값을 저장하고 관리하게 됩니다.
  • 지역 변수는 메서드 내부에서 정의될 때 생성되어 메서드가 종료될 때까지만 유지됩니다.
public class Main {
    public static void main(String[] args) {
        Main main = new Main();

        // 메서드 호출 : main.getClass()
        System.out.println("main.getClass() = " + main.getNumber());
        System.out.println("main.getClass() = " + main.getNumber());
        System.out.println("main.getClass() = " + main.getNumber());
    }

    public int getNumber() {
        int number = 1; // 지역 변수
        number += 1;
        return number; // 메서드 종료되면 지역변수 제거됨
    }
}

// 출력
//main.getNumber() = 2
//main.getNumber() = 2
//main.getNumber() = 2

final 필드와 상수


final은 ‘최종적’ 이라는 의미입니다.

  • final 필드는 초기값이 저장되면 해당값을 프로그램이 실행하는 도중에는 절대로 수정할 수 없습니다.
  • 또한 final 필드는 반드시 초기값을 지정해야 합니다. 
  • 말 그대로 변경 불가 필드
final String company = "GENESIS";

...

Car car = new Car();
System.out.println(car.company);
  • 필드 타입 앞에 final 키워드를 추가하여 final 필드를 선언할 수 있습니다.
  • 사용방법은 일반적인 인스턴스 필드와 동일합니다. 다만 수정이 불가능합니다.
    • car.company = "Benz"; 이렇게 수정하려 하면 오류가 발생합니다.

상수

  • 상수의 특징은 값이 반드시 한 개이며 불변의 값을 의미합니다.
  • 따라서 인스턴스마다 상수를 저장할 필요가 없습니다.
  • 그냥 final이 붙은 변수는 상수가 된다.
static final String COMPANY = "GENESIS";

...

System.out.println(Car.COMPANY);
  • final 앞에 static 키워드를 추가하여 모든 인스턴스가 공유할 수 있는 값이 한 개이며 불변인 상수를 선언할 수 있습니다.
  • 사용방법은 일반적인 클래스 필드와 동일합니다. 다만 수정이 불가능합니다.
    • Car.COMPANY = "Benz"; 이렇게 수정하려 하면 오류가 발생합니다.
  • 일반적으로 상수는 대문자로 작성하는 것이 관례입니다.

+ static도 변수로 만들면 바꿀 수 있고 상수로 만들면 못 변경하기도 한다.

'Back-End (Web) > JAVA' 카테고리의 다른 글

[JAVA] This와 This()  (0) 2024.11.12
[JAVA] 생성자  (1) 2024.11.12
[JAVA] 객체의 필드와 메서드  (0) 2024.11.12
[JAVA] 클래스 설계와 객체 생성  (0) 2024.11.12
[JAVA] 객체지향 프로그래밍 & 다형성  (1) 2024.11.12

필드 = 객체의 속성

필드는 객체의 데이터를 저장하는 역할을 한다.

  • 객체의 필드는 크게 고유한 데이터, 상태 데이터, 객체 데이터로 분류할 수 있다.
  • 이처럼 자동차 객체는 4개의 고유한 데이터와 3개의 상태 데이터 그리고 3개의 객체 데이터를 가질 수 있다.
    • 우리가 처음 소프트웨어의 부품을 객체라 표현한다.
    • 이 3개의 객체 데이터를 자동차를 만들기 위한 부품 데이터라고 이해해도 좋다.
public class Car {

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

    double speed;  // 자동차 속도 , km/h
    char gear; // 기어의 상태, P,R,N,D
    boolean lights; // 자동차 조명의 상태

    Tire tire;
    Door door;
    Handle handle;

    public Car() {} // 기본 생성자

    double gasPedal(double kmh) {
        speed = kmh;
        return speed;
    }

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

    char changeGear(char type) {
        gear = type;
        return gear;
    }

    boolean onOffLights() {
        lights = !lights;
        return lights;
    }

		void horn() {
		    System.out.println("빵빵");
		}
}

 

우리가 정의하여 선언한 클래스의 필드들은 기본적으로 초기값을 제공하지 않을 경우 객체가 생성될 때 자동으로 기본값으로 초기화된다.

  • 초기값을 제공하는 방법은 ‘필드 타입 필드명 = 값;’ 이렇게 직접 초기화할 수 있다.
    • String model = "Gv80"; 

 

필드 사용방법

필드를 사용한다’라는 의미는 필드의 값을 변경하거나 읽는 것을 의미합니다.

  • 우리가 클래스에 필드를 정의하여 선언했다고 해서 바로 사용할 수 있는 것은 아닙니다.
  • 클래스는 설계도일 뿐 실제로 필드의 데이터를 가지고 있는 것은 객체입니다.
  • 따라서 객체를 생성한 후에 필드를 사용할 수 있습니다. 
  • 간단히, 클래스는 설계도이다. 붕어빵 틀 설계도에 속(데이터)를 넣을 수 없다. 붕어빵 틀(객체)에 속(데이터)를 넣어야한다.
  • 외부 접근
    • Car car = new Car();
      • 이렇게 객체를 생성했다면 우리는 참조 변수 car를 이용하여 외부에서 객체 내부의 필드에 접근하여 사용할 수 있습니다.
      • 이때 객체의 내부 필드에 접근하는 방법은 도트(.) 연산자를 사용하면 됩니다.
        • car.color = "blue";
  • 내부 접근
    • 도트 연산자를 사용하여 외부에서 객체 내부에 접근할 수 있을 뿐만 아니라 객체 내부 메서드에서도 내부 필드에 접근할 수 있습니다.
double brakePedal() {
    speed = 0;
    return speed;
}
  • 이처럼 brakePedal() 메서드 내부에서 객체의 필드 speed를 바로 호출해서 사용할 수 있습니다.

 

public class Car {

    String company; // 자동차 회사
    String model = "Gv80"; // 자동차 모델
    String color; // 자동차 색상
    double price; // 자동차 가격

    double speed;  // 자동차 속도 , km/h
    char gear; // 기어의 상태, P,R,N,D
    boolean lights = true; // 자동차 조명의 상태

    Tire tire = new Tire();
    Door door;
    Handle handle;

    public Car() {} // 기본 생성자

    double gasPedal(double kmh) {
        speed = kmh;
        return speed;
    }

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

    char changeGear(char type) {
        gear = type;
        return gear;
    }

    boolean onOffLights() {
        lights = !lights;
        return lights;
    }

		void horn() {
		    System.out.println("빵빵");
		}
}

//- model 필드 값에 “Gv80” 초기값을 주겠습니다.
//- lights 필드 값에 true 초기값을 주겠습니다.
//- tire 필드 값에 `new Tire()` 초기값을 주겠습니다.

 

main 메서드를 사용하여 테스트한 경우

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

        // 초기값과 기본값 확인하기

        System.out.println("car.model = " + car.model); // 초기값 "Gv80"이 출력됩니다.
        System.out.println("car.color = " + car.color); // 기본값 null이 출력됩니다.
        System.out.println();

        System.out.println("car.speed = " + car.speed); // 기본값 0.0이 출력됩니다.
        System.out.println("car.gear = " + car.gear);  // 기본값 \u0000(공백)이 출력됩니다.
        System.out.println("car.lights = " + car.lights); // 초기값 true가 출력됩니다.
        System.out.println();

        System.out.println("car.tire = " + car.tire); // 초기값 인스턴스의 주소가 출력됩니다.
        System.out.println("car.door = " + car.door); // 기본값 null이 출력됩니다.
        System.out.println();

        // 필드 사용

        car.color = "blue"; // 필드 color에 "blue" 데이터를 저장합니다.
        car.speed = 100;    // 필드 speed에 100 데이터를 저장합니다.
        car.lights = false; // 필드 lights에 false 데이터를 저장합니다.

        System.out.println("car.color = " + car.color); // 저장된 "blue" 데이터가 출력됩니다.
        System.out.println("car.speed = " + car.speed); // 저장된 100.0 데이터가 출력됩니다.
        System.out.println("car.lights = " + car.lights); // 저장된 false 데이터가 출력됩니다.

    }
}

 

 

 

 

메서드 = 객체의 행위

  • 특정 작업을 수행하는 코드 블록, 객체 지향 프로그래밍에서 메서드는 주로 클래스 내부에 정의되며, 해당 클래스의 객체에서 호출하여 사용할 수 있다. 메서드는 작업을 캡슐화하여 코드의 재사용성을 높이고, 유지보수와 확장성을 용이하게 한다.
<접근 제어자> <반환 타입> <메서드 이름>(<매개변수>) {
    // 메서드 본문 (실제 작업을 수행)
    return <값>;
}

 

반환(리턴) 타입

double brakePedal() {...} // double 타입 반환
char changeGear(char type) {...} // char 타입 반환
boolean onOffLights() {...} // boolean 타입 반환
void horn() {...} // 반환할 값 없음
  • 리턴 타입이란 메서드가 실행된 후 호출을 한 곳으로 값을 반환할 때 해당 값의 타입을 의미한다.
    • return 리턴 타입의 반환값;
    • 주의할 점은 메서드에 리턴 타입을 선언하여 반환할 값이 있다면 반드시 return 문으로 해당하는 리턴 타입의 반환값을 지정해야 한다.
  • 반환할 값이 없을 때는 리턴 타입에 void를 작성해야 한다.
    • 반환값이 없음으로 return문을 반드시 지정할 필요는 없습다.
    • 메서드는 실행할 때 return문을 만나면 그대로 종료하게 되는데 void 타입일 때 return; 이렇게 return문을 사용하여 원하는 지점에서 메서드를 종료할 수도 있다.

매개변수

double gasPedal(double kmh, char type) {
    speed = kmh;
    return speed;
}
  • 매개변수는 메서드를 호출할 때 메서드로 전달하려는 값을 받기 위해 사용되는 변수이다.
  • 위 gasPedal(double kmh, char type) 메서드의 매개변수는 double 타입의 kmh, char 타입의 type이다.
    • 해당 매개변수에 값을 전달하기 위해서는 순서와 타입에 맞춰 값을 넣어주면 된다.
    • gasPedal(100, 'D');
  • 전달하려는 값이 없다면 생략 가능하다.

 

가변길이 매개변수 

void carSpeeds(double ... speeds) {
    for (double v : speeds) {
        System.out.println("v = " + v);
    }
}
  • double … speeds 이렇게 … 을 사용하면 아래처럼 매개값을 , 로 구분하여 개수 상관없이 전달 가능하다.
  • carSpeeds(100, 80);
  • carSpeeds(110, 120, 150);

 

 

메서드 호출 방법

‘메서드를 호출한다’라는 의미는 메서드의 블록 내부에 작성된 코드를 실행한다는 의미입니다.

  • 필드와 마찬가지로 클래스의 메서드를 정의하여 선언했다고 해서 바로 사용할 수 있는 것은 아닙니다.
  • 클래스는 설계도일 뿐 메서드는 객체의 행위를 정의한 것입니다.
  • 따라서 객체를 생성한 후에 메서드를 사용할 수 있습니다. 
  • 외부 접근
    • Car car = new Car();
      • 이렇게 객체를 생성했다면 우리는 참조 변수 car를 이용하여 외부에서 객체 내부의 메서드에 접근하여 호출할 수 있습니다.
      • 이때 객체의 내부 메서드에 접근하는 방법은 도트(.) 연산자를 사용하면 됩니다.
        • car.brakePedal();
      • 또한 메서드가 매개변수를 가지고 있다면 반드시 호출할 때 매개변수의 순서와 타입에 맞게 매개값을 넣어줘야 합니다.
        • car.gasPedal(100, 'D');
  • 내부 접근
    • 도트 연산자를 사용하여 외부에서 객체 내부에 접근할 수 있을 뿐만 아니라 객체 내부 메서드에서도 내부 메서드에 접근하여 호출할 수 있습니다.
      • 아래처럼 gasPedal(double kmh, char type) 메서드 내부에서 해당 객체의 changeGear(type); 메서드를 호출할 수 있습니다.
double gasPedal(double kmh, char type) {
    changeGear(type);
    speed = kmh;
    return speed;
}
  • 반환 값 저장
    • 메서드의 리턴 타입을 선언하여 반환할 값이 있다면 변수를 사용하여 받아줄 수 있습니다.
      • 반드시 리턴 타입과 변수의 타입이 동일하거나 자동 타입 변환될 수 있어야 합니다.
    double speed = car.gasPedal(100, 'D');
    
    • double 타입의 변수 speed를 사용하여 double gasPedal(double kmh, char type) 메서드의 double 타입의 반환값을 받아 저장할 수 있습니다.
public class Car {

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

    double speed;  // 자동차 속도 , km/h
    char gear = 'P'; // 기어의 상태, P,R,N,D
    boolean lights; // 자동차 조명의 상태

    public Car() {} // 기본 생성자

    double gasPedal(double kmh, char type) {
        changeGear(type);
        speed = kmh;
        return speed;
    }

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

    char changeGear(char type) {
        gear = type;
        return gear;
    }

    boolean onOffLights() {
        lights = !lights;
        return lights;
    }

		void horn() {
		    System.out.println("빵빵");
		}

    void carSpeeds(double ... speeds) {
        for (double v : speeds) {
            System.out.println("v = " + v);
        }
    }
}

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

        // 메서드 호출 및 반환값 저장
        double speed = car.gasPedal(100, 'D');
        System.out.println("speed = " + speed);

        boolean lights = car.onOffLights();
        System.out.println("lights = " + lights);

        System.out.println();
        // gasPedal 메서드 내부에 호출된 changeGear(type); 메서드의 결과 확인
        // gear의 초기값은 'P'
        System.out.println("car.gear = " + car.gear); // 'D' 출력

        System.out.println();
        // 가변길이 매개변수 확인
        car.carSpeeds(100, 80);
        System.out.println();
        car.carSpeeds(110, 120, 150);
    }
}

 

 

오버로딩

  • 같은 이름의 메서드나 생성자 매개변수의 개수나 타입이 달라지도록 여러 번 정의하는 기법입니다. 오버로딩을 사용하면 같은 이름으로 다양한 작업을 처리할 수 있습니다.

오버로딩의 특징

  • 같은 이름의 메서드나 생성자지만 매개변수의 개수나 타입이 달라야 합니다.
  • 반환 타입은 오버로딩을 구별하는 기준이 되지 않습니다.
  • 컴파일 시점에 메서드 호출을 구분하기 때문에, 실행 시간에 결정되는 것이 아니라 호출 시점에서 적절한 메서드가 선택됩니다.

오버로딩의 조건

  • 메서드의 이름이 같고, 매개변수의 개수, 타입, 순서가 달라야 합니다.
  • '응답 값만' 다른 것은 오버로딩을 할 수 없습니다.
  • 접근 제어자만 다른 것도 오버로딩을 할 수 없습니다.
  • 결론, 오버로딩은 매개변수의 차이로만 구현할 수 있습니다.

오버로딩의 장점

  • 코드의 가독성 향상: 비슷한 작업을 수행하는 메서드를 같은 이름으로 처리할 수 있어 코드가 깔끔하고 이해하기 쉬워집니다.
  • 유지보수 용이성: 같은 이름의 메서드를 사용하여 여러 매개변수에 대한 처리 로직을 관리할 수 있어 유지보수가 용이합니다.

오버로딩 규칙

  1. 매개변수의 타입이 달라야 오버로딩으로 인식됩니다.
  2. 매개변수의 개수가 달라야 오버로딩으로 인식됩니다.
  3. 매개변수의 순서가 달라야 오버로딩으로 인식됩니다.
public class PrintStream extends FilterOutputStream
    implements Appendable, Closeable
{
			...
			
		public void println() {
        newLine();
    }

    public void println(boolean x) {
        if (getClass() == PrintStream.class) {
            writeln(String.valueOf(x));
        } else {
            synchronized (this) {
                print(x);
                newLine();
            }
        }
    }

    public void println(char x) {
        if (getClass() == PrintStream.class) {
            writeln(String.valueOf(x));
        } else {
            synchronized (this) {
                print(x);
                newLine();
            }
        }
    }

    public void println(int x) {
        if (getClass() == PrintStream.class) {
            writeln(String.valueOf(x));
        } else {
            synchronized (this) {
                print(x);
                newLine();
            }
        }
    }

    public void println(long x) {
        if (getClass() == PrintStream.class) {
            writeln(String.valueOf(x));
        } else {
            synchronized (this) {
                print(x);
                newLine();
            }
        }
    }

    public void println(float x) {
        if (getClass() == PrintStream.class) {
            writeln(String.valueOf(x));
        } else {
            synchronized (this) {
                print(x);
                newLine();
            }
        }
    }

    public void println(double x) {
        if (getClass() == PrintStream.class) {
            writeln(String.valueOf(x));
        } else {
            synchronized (this) {
                print(x);
                newLine();
            }
        }
    }

    public void println(char[] x) {
        if (getClass() == PrintStream.class) {
            writeln(x);
        } else {
            synchronized (this) {
                print(x);
                newLine();
            }
        }
    }

    public void println(String x) {
        if (getClass() == PrintStream.class) {
            writeln(String.valueOf(x));
        } else {
            synchronized (this) {
                print(x);
                newLine();
            }
        }
    }

    public void println(Object x) {
        String s = String.valueOf(x);
        if (getClass() == PrintStream.class) {
            // need to apply String.valueOf again since first invocation
            // might return null
            writeln(String.valueOf(s));
        } else {
            synchronized (this) {
                print(s);
                newLine();
            }
        }
    }


		  ...
}

 

 

기본형 & 참조형 매개변수

 

기본형 매개변수

  • 매개변수의 타입이 기본형일 때는 값 자체가 복사되어 넘어가기 때문에 매개값으로 지정된 변수의 원본 값이 변경되지 않습니다.
  • 메서드를 호출할 때 전달할 매개값으로 지정한 값을 메서드의 매개변수에 복사해서 전달합니다.

참조형 매개변수

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

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

    double speed;  // 자동차 속도 , km/h
    char gear; // 기어의 상태, P,R,N,D
    boolean lights; // 자동차 조명의 상태

    Tire tire;
    Door door = new Door();
    Handle handle = new Handle();

    public Car() {} // 기본 생성자

    double gasPedal(double kmh, char type) {
        changeGear(type);
        speed = kmh;
        return speed;
    }

    double brakePedal(char type) {
        speed = 0;
        type = 'P'; // 정지 후 매개변수 type을 어떤 타입으로 전달 받았는지 상관없이 'P'로 고정시키기
        changeGear(type);
        return speed;
    }

    char changeGear(char type) {
        gear = type;
        return gear;
    }

    boolean onOffLights() {
        lights = !lights;
        return lights;
    }

		void horn() {
		    System.out.println("빵빵");
		}

    Tire setTire(Tire tireCompany) {
        tireCompany.company = "KIA"; // 금호 타이어를 전달 받았지만 강제로 KIA 타이어로 교체
        tire = tireCompany;
        return tire;
    }
}
public class Tire {
    String company; // 타이어 회사
    public Tire() {}
}
public class Main {
    public static void main(String[] args) {
        Car car = new Car(); // 객체 생성

        // 기본형 매개변수
        char type = 'D';
        car.brakePedal(type);

        // 메서드 실행 완료 후 전달할 매개값으로 지정된 type 값 확인
        System.out.println("type = " + type); // 기존에 선언한 값 'D' 출력, 원본 값 변경되지 않음
        // 메서드 실행 완료 후 반환된 car 인스턴스의 gear 타입 확인
        System.out.println("gear = " + car.gear); // 객체 내부에서 type을 변경하여 수정했기 때문에 'P' 출력

        System.out.println();
        // 참조형 매개변수
        Tire tire = new Tire();
        tire.company = "금호"; // 금호 타이어 객체 생성

        // 차 객체의 타이어를 등록하는 메서드 호출한 후 반환값으로 차 객체의 타이어 객체 반환
        Tire carInstanceTire = car.setTire(tire);

        // 메서드 실행 완료 후 전달할 매개값으로 지정된 참조형 변수 tire의 company 값 확인
        System.out.println("tire.company = " + tire.company); // "KIA" 출력
        // 전달할 매개값으로 지정된 tire 인스턴스의 주소값이 전달되었기 때문에 호출된 메서드에 의해 값이 변경됨.

        // 메서드 실행 완료 후 반환된 car 인스턴스의 tire 객체 값이 반환되어 저장된 참조형 변수 carInstanceTire의 company 값 확인
        System.out.println("carInstanceTire.company = " + carInstanceTire.company); // "KIA" 출력
    }
}

 

클래스

클래스 = 설계도 이다. 더 간단히 설명하면 붕어빵 틀이라고 생각하면된다. 

 

클래스의 구성 요소

  1. 필드(Field): 클래스의 속성을 나타내며, 객체가 가져야 할 데이터나 상태를 저장합니다. 필드는 변수와 유사하지만 클래스 내에 정의되어 객체의 상태를 나타내는 데 사용됩니다.
  2. 메소드(Method): 클래스가 수행하는 행동이나 기능을 정의합니다. 객체가 수행할 수 있는 작업을 메소드로 정의하며, 필드를 조작하거나 특정 기능을 수행하는 역할을 합니다.
  3. 생성자(Constructor): 객체를 생성할 때 호출되는 특별한 메소드로, 클래스 이름과 동일한 이름을 가집니다. 생성자를 통해 객체의 초기 상태를 설정할 수 있습니다.
  4. 접근 제어자(Access Modifier): 클래스, 필드, 메소드에 접근할 수 있는 범위를 제한하는 키워드입니다. 주로 public, private, protected 등이 있으며, 접근 범위를 명시하여 데이터 보호와 캡슐화를 구현할 수 있습니다.

클래스 생성

 

메소드와 함수의 차이

  1. 메소드(Method):
    • 객체 지향 프로그래밍(OOP)에서 객체의 행위를 정의하는 코드 블록입니다.
    • 클래스 내부에 정의되며, 특정 객체의 상태(필드)에 접근하고 이를 조작하는 데 사용됩니다.
    • 메소드는 클래스에 종속적이기 때문에 객체를 통해 호출됩니다.
    • Java에서는 모든 함수가 클래스 안에 있어야 하므로, Java에서는 함수 대신 "메소드"라는 용어를 사용합니다.
  2. 함수(Function):
    • 함수는 독립적인 코드 블록으로, 특정 작업을 수행하기 위해 사용됩니다.
    • 객체 지향 언어가 아닌 프로그래밍 언어(예: C, JavaScript(ES6 이전))에서는 함수가 클래스에 소속되지 않고, 단독으로 존재할 수 있습니다.
    • 함수는 클래스나 객체에 종속되지 않고, 필요에 따라 어디서든 호출할 수 있는 독립적인 개념입니다.

즉, 모든 메소드는 함수이지만, 모든 함수가 메소드인 것은 아닙니다. 메소드는 객체에 속한 함수로 볼 수 있습니다.

public class Calculator {
    public int add(int a, int b) { // add는 Calculator 클래스의 메소드
        return a + b;
    }
}

public class Main {
    public static void main(String[] args) {
        Calculator calculator = new Calculator();
        System.out.println(calculator.add(5, 3));  // 8
    }
}

 

 

자동차 클래스 생성

 

public class Car {
    String company; // 자동차 회사
    String model; // 자동차 모델
    String color; // 자동차 색상
    double price; // 자동차 가격
    double speed;  // 자동차 속도, km/h 단위
    char gear; // 기어 상태, P (주차), R (후진), N (중립), D (주행)
    boolean lights; // 자동차 조명 상태, 켜짐(true) 또는 꺼짐(false)

    // 기본 생성자: 매개변수 없이 Car 객체를 생성
    public Car() {}

    // gasPedal 메서드: 속도를 설정하는 메서드로 kmh 값을 speed 필드에 저장하고 반환
    double gasPedal(double kmh) {
        speed = kmh; // 자동차의 현재 속도를 kmh 값으로 설정
        return speed; // 설정된 속도 반환
    }

    // brakePedal 메서드: 속도를 0으로 설정하는 메서드로, speed 필드를 0으로 설정하고 반환
    double brakePedal() {
        speed = 0; // 속도를 0으로 설정하여 정지 상태로 만듦
        return speed; // 속도 0을 반환
    }

    // changeGear 메서드: 기어 상태를 변경하는 메서드로, type 값을 gear 필드에 저장하고 반환
    char changeGear(char type) {
        gear = type; // 기어를 매개변수 type의 값으로 설정
        return gear; // 설정된 기어 상태 반환
    }

    // onOffLights 메서드: 조명 상태를 반전시키는 메서드로, 현재 lights 값을 반전시키고 반환
    boolean onOffLights() {
        lights = !lights; // 현재 조명 상태를 반전(true -> false, false -> true)
        return lights; // 변경된 조명 상태 반환
    }

    // horn 메서드: 경적을 울리는 메서드로, 호출 시 "빵빵" 소리를 콘솔에 출력
    void horn() {
        System.out.println("빵빵"); // 경적 소리 출력
    }
}

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

        // gasPedal 호출하여 속도 설정 및 출력
        System.out.println("Current Speed: " + car.gasPedal(80)); // 속도 80 km/h 설정

        // brakePedal 호출하여 속도 정지 상태로 변경 및 출력
        System.out.println("Current Speed after braking: " + car.brakePedal()); // 속도 0 km/h 설정

        // changeGear 호출하여 기어 상태 변경 및 출력
        System.out.println("Current Gear: " + car.changeGear('D')); // 기어를 D로 변경

        // onOffLights 호출하여 조명 상태 반전 및 출력
        System.out.println("Lights On/Off: " + car.onOffLights()); // 조명 상태 변경

        // horn 호출하여 경적 소리 출력
        car.horn(); // "빵빵" 출력
    }
}

 

객체 생성

new Car(); // Car클래스 객체 생성
  • 객체 생성 연산자인 ‘new’를 사용하면 클래스로부터 객체를 생성할 수 있습니다.
  • new 연산자 뒤에는 해당 클래스의 생성자 호출 코드를 작성합니다.
  • 형태가 Car();즉, 기본 생성자의 형태와 같기 때문에 new 연산자에 의해 객체가 생성되면서 기본 생성자가 호출됩니다.

 

참조형 변수

Car car1 = new Car(); // Car클래스의 객체인 car1 인스턴스 생성
Car car2 = new Car(); // Car클래스의 객체인 car2 인스턴스 생성
  • new 연산자를 통해서 객체가 생성되면 해당 인스턴스의 주소가 반환되기 때문에 해당 클래스의 참조형 변수를 사용하여 받아줄 수 있다.

 

객체 배열

  • 객체는 참조형 변수와 동일하게 취급되기 때문에 배열 또는 컬렉션에도 저장하여 관리할 수 있다.
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);
        }
    }
}

// 출력
//car.gear = P
//car.gear = N
//car.gear = D

< 해당 내용의 사진은 한빛출판사의 이미지를, 강의 내용은 내일 배움 캠프를 통해 서술했습니다. >

 

객체

객체란 세상에 존재하는 물체를 뜻하며 식별이 가능한 것을 말한다. 강하지, 반려동물, 자동차와 같이 물질적인 것 부터 개념적인 부분까지 식별이 가능하면 객체라 불릴 수 있다. 객체는 일정한 속성을 가지고 있으며 행동을 할 수 있다.

자동차의 속성과 행위

 

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


객체지향

객체 지향 프로그래밍은 컴퓨터 프로그램을 명령어의 목록으로 보는 시각에서 벗어나 여러 개의 독립된 단위, 즉 "객체"들의 모임으로 파악하고자 하는 것이다. 각각의 객체는 메시지 를 주고받고, 데이터를 처리할 수 있다. (협력)

 

• 객체 지향 프로그래밍은 프로그램을 유연하고 변경이 용이하게 만들기 때문에 대규모 소프 트웨어 개발에 많이 사용된다.

 

자바의 객체지향

위 처럼 사람, 자동차 2가지 객체를 만들고 그들의 특징을 엮어서 '자동차를 타고 이동하는 사람'을 만들어 낼 수 있다. 


다형성

https://velog.io/@lsj16632/%EB%8B%A4%ED%98%95%EC%84%B1%EC%9D%B4%EB%9E%80JAVA

 

다형성은 "하나의 인터페이스(또는 메서드)가 여러 가지 형태를 가질 수 있다"는 의미입니다. 이를 통해 코드를 더 유연하고 확장 가능하게 만들어 줍니다.

 

위의 예시처럼 자동차는 K3, 아반떼, 테슬라등 여러가지 형태를 가질 수 있다. 차라는 역활이 테슬라라는 구체적인 대상으로 구현된 것이다.

 

이렇게  역활구현으로 구분하면 세상이 단순해지고, 유연해지며 변경이 편리해진다. 

 

1. 실세계 비유

  • 운전자 - 자동차:
    운전자는 자동차의 내부 구조를 알 필요 없이 운전대, 브레이크, 가속기라는 역할(인터페이스)만 이용하면 된다.
    자동차 내부를 바꿔도 운전자는 영향을 받지 않는다.
  • 공연 무대:
    무대에서 배우(역할)가 어떤 사람으로 바뀌어도, 대본(인터페이스)에 따라 공연이 진행된다.
    배우가 바뀌더라도 공연은 유지된다.
  • 키보드, 마우스, USB 등 표준 인터페이스:
    다양한 키보드나 마우스가 연결되더라도 표준 인터페이스(USB)를 통해 동작한다.
    하드웨어의 내부 구현은 다를 수 있지만, 인터페이스는 일정하다.
  • 정렬 알고리즘:
    정렬을 수행하는 인터페이스를 두고, QuickSort, MergeSort, BubbleSort 등의 구현을 상황에 따라 바꿀 수 있다.
  • 할인 정책 로직:
    다양한 할인 정책(예: 정률 할인, 정액 할인)을 인터페이스로 정의하고 구현을 바꿔도 로직은 변경되지 않는다.

2. 역할과 구현 분리

  • 역할과 구현을 분리하면 다음과 같은 장점이 있다:
    • 클라이언트는 대상의 역할(인터페이스)만 알면 된다.
    • 구현 대상의 내부 구조를 몰라도 된다.
    • 내부 구조가 변경되어도 클라이언트에 영향을 주지 않는다.
    • 구현 대상을 아예 바꿔도 클라이언트가 영향을 받지 않는다.
  • 객체 설계 원칙:
    • 객체를 설계할 때 역할(인터페이스)을 먼저 정의하고, 이를 수행하는 구현 객체를 만든다.
    • 클라이언트와 서버는 서로 협력 관계를 가지며, 서로 역할에만 의존한다.

3. 자바 언어에서 다형성

  • 역할 = 인터페이스
    • 예: MemberRepository라는 인터페이스.
  • 구현 = 인터페이스를 구현한 클래스
    • 예: MemoryMemberRepository, JdbcMemberRepository.
  • 실행 시점에 객체 변경 가능:
  • public class MemberService { private MemberRepository memberRepository = new MemoryMemberRepository(); } // 구현 객체를 유연하게 변경 가능 public class MemberService { private MemberRepository memberRepository = new JdbcMemberRepository(); }
  • 다형성의 본질:
    • 실행 시점에 인터페이스를 구현한 객체를 유연하게 변경할 수 있다.
    • 클라이언트를 변경하지 않고 서버의 구현 기능을 변경할 수 있다.

4. 스프링과 객체지향

  • 스프링에서 다형성의 활용:
    • 제어의 역전(IoC): 객체의 생성과 생명주기를 스프링이 관리.
    • 의존관계 주입(DI): 필요한 객체를 주입받아 유연한 설계를 가능하게 함.
    • 역할과 구현을 분리하여, 객체를 마치 레고 블록처럼 조립 가능.
    • 공연 무대에서 배우를 교체하듯, 구현을 간단히 변경 가능.

5. 다형성의 장점

  1. 유연한 설계 가능.
  2. 변경에 대한 영향 최소화.
  3. 확장성 향상.
  4. 코드 재사용성 증가.
  5. 구현의 내부 구조를 은닉하여 클라이언트를 단순화.

6. 다형성의 한계

  • 역할(인터페이스) 자체가 변하면, 클라이언트와 서버 모두 큰 변경이 필요:
    • 예: 자동차 인터페이스를 비행기로 변경.
    • USB 인터페이스의 규격 변경.
  • 해결 방안:
    • 인터페이스를 안정적으로 설계하는 것이 중요.

7. 요약

  • 다형성은 객체 간 협력을 기반으로 역할과 구현을 분리해 유연하고 확장 가능한 설계를 가능하게 한다.
  • 스프링 프레임워크는 다형성을 극대화하여 객체 지향 설계를 돕는 강력한 도구를 제공한다.
    • IoC와 DI를 활용해 구현체를 쉽게 교체하거나 확장할 수 있다.
  • 인터페이스 설계의 안정성이 다형성 설계의 핵심이다.

 

객체 간의 협력

 

객체간의 상호작용을 말한다.

    • 사람이 자동차의 가속 페달을 밟으면 자동차는 이에 반응하여 속도를 올리며 앞으로 이동합니다.
    • 사람이 자동차의 브레이크 페달을 밟으면 자동차는 이에 반응하여 속도를 줄이며 정지합니다.

 

자바에서 객체는 메서드를 통해서 서로 상호작용하면서 데이터를 주고 받을 수 있다.

 

예를 들어 사람이 자동차 객체가 가지고 있는 가속페달 메서드gasPedal(50);을 호출하면 자동차는 이 메서드에 반응하여 속도 속성의 값을 50으로 수정시킨다. 그러면 사람 객체는 자동차 객체의 브레이크 페달 brakePedal();을 호출한다.

+ 여기서 50은 매개값, 파라미터 라고 불리며 메서드 호출시 데이터를 포함해 호출하게 해준다

 

또한 자동차 객체는 gasPedal(50); 메서드에서 속도를 바꾸는 작업을 수행한 후 사람 객체에게 실행 결과인 속도의 값을 반환할 수 있습니다. 이때 반환되는 값을 ‘리턴값’이라 표현한다.

 


객체 간의 관계

  • 객체간의 협력에도 종류가 있는데, 크게 사용관계, 포함관계, 상속관계가 존재한다.

 

사용 관계

  • 사람 객체는 자동차 객체를 사용한다.

 

포함 관계

  • 타이어 객체, 차 문 객체, 핸들 객체는 자동차 객체에 포함되어 있다.

 

상속 관계

 

  • 만약 공장에 자동차만 생산하는 게 아니라 기차도 생산한다고 가정해 보자.
  • 자동차와 기차 객체는 하나의 공통된 기계 시스템 객체를 토대로 만들어진다.
  • 그렇다면 자동차 객체와 기차 객체는 기계 시스템 객체를 상속받는 상속 관계가 된다.

자바는 효율성을 강조한 언어이다. 위의 blueprint를 Car, Train 객체에서 각각 1번씩 정의하는 방식보다는, 그 상위 객체인 MachineSystem에서 한번만 정의하고 하위 객체에게 주소를 넘겨주는게 메모리 효율성이 높다.

// 부모 클래스
class Animal {
    // 공통 메소드
    public void eat() {
        System.out.println("This animal eats food.");
    }
}

// 자식 클래스
class Dog extends Animal {
    // 자식 클래스에만 있는 메소드
    public void bark() {
        System.out.println("The dog barks.");
    }

    // 부모 클래스의 메소드 오버라이딩
    @Override
    public void eat() {
        System.out.println("The dog eats dog food.");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog();
        
        // 상속받은 메소드 사용
        dog.eat();   // The dog eats dog food.
        dog.bark();  // The dog barks.
    }
}

 

더보기

코드 설명

  1. Animal 클래스: eat() 메소드를 가지는 부모 클래스입니다.
  2. Dog 클래스: Animal 클래스를 상속받아 bark() 메소드를 추가했고, eat() 메소드를 오버라이딩하여 개에 맞는 동작을 정의했습니다.
  3. 오버라이딩: eat() 메소드를 자식 클래스에서 재정의하여 다형성을 구현했습니다.

캡슐화

  • 캡슐화란 속성(필드)와 행위(메서드)를 하나로 묶어 객체로 만든 후 실제 내부 구현 내용은 외부에서 알 수 없게 감추는 것을 의미합니다. (안전성 확보)
  • 외부 객체에서는 캡슐화된 객체의 내부 구조를 알 수 없기 때문에 노출시켜 준 필드 혹은 메서드를 통해 접근할 수 있습니다.
  • 필드와 메서드를 캡슐화하여 숨기는 이유는 외부 객체에서 해당 필드와 메서드를 잘못 사용하여 객체가 변화하지 않게 하는 데 있습니다.
  • Java에서는 캡슐화된 객체의 필드와 메서드를 노출시킬지 감출지 결정하기 위해 접근 제어자를 사용합니다.

캡슐화

 

단순히 설명하면, 알약 캡슐처럼 내용을 하나의 알약에 담는다. 다만 알약이 밖에서 볼때 어떤 성분이 있는지 확인할 수 없듯 기본적으로 캡슐화가 된 클래스의 내용은 확인할 수 없다. 하지만 프로그래머가 예외로 설정한 데이터, 메소드에는 접근할 수 있다.

 

public class User {
    // private 접근 제어자를 사용하여 필드를 외부에서 직접 접근하지 못하게 함
    private String name;
    private int age;

    // 생성자를 통해 객체 초기화
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // getter 메소드: 필드에 대한 접근을 제공
    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    // setter 메소드: 필드를 수정할 수 있게 제공
    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        if(age > 0) { // 유효성 검사를 통해 데이터 보호
            this.age = age;
        } else {
            System.out.println("Age must be greater than 0.");
        }
    }
}

 

 

위의 private는 '개인적으로' 라는 의미로 캡슐만이 알 수 있고 외부에서 접근할 수 없는 데이터나 메소드를 만들어준다.

반대로 public은 '공공적으로' 라는 의미로 외부에서 접근이 가능하고 수정까지도 할 수 있게 해준다.


 

상속

  • 객체지향 프로그래밍에는 부모 객체와 자식 객체가 존재합니다.
  • 부모 객체는 가지고 있는 필드와 메서드를 자식 객체에 물려주어 자식 객체가 이를 사용할 수 있도록 만들 수 있습니다.

상속

 

  • 위에서 설명했던 대로, 효율성을 추구하는 자바라는 언어 특성상 굳이 곂치는 내용을 여러번 정의하는 것을 꺼린다. MachineSystem의 자식인 Car, Train에서 bluePrint를 정의하지 않고 부모인 MachineSystem에서 데이터를 상속받아 사용한다.

상속의 특징

  1. 코드 재사용성: 기존에 작성한 클래스의 코드와 기능을 자식 클래스에서 재사용할 수 있습니다. 이는 중복 코드를 줄이고 코드 유지보수를 쉽게 해줍니다.
  2. 계층 구조 생성: 부모-자식 관계의 계층 구조를 만들어 시스템을 구조적으로 관리할 수 있습니다. 이를 통해 클래스 간의 관계를 명확히 할 수 있습니다.
  3. 확장성: 새로운 기능이 필요할 때 기존 클래스에 추가하기보다 자식 클래스를 만들어 확장할 수 있습니다. 이렇게 하면 기존 클래스는 변경하지 않고 기능을 확장할 수 있어 코드의 안정성이 높아집니다.
  4. 다형성(Polymorphism): 상속을 통해 생성된 자식 클래스는 부모 클래스의 메소드를 자신의 방식대로 재정의(오버라이딩)할 수 있습니다. 이를 통해 부모 클래스를 참조하는 변수로 여러 자식 클래스의 인스턴스를 다룰 수 있습니다.

상속의 장점

  1. 코드 중복 감소: 자식 클래스가 부모 클래스의 속성과 메소드를 상속받아 사용할 수 있으므로 중복 코드를 줄일 수 있습니다.
  2. 유지보수 용이: 공통 기능을 부모 클래스에서 관리하기 때문에 수정 사항이 있을 때 모든 자식 클래스에 일관되게 반영할 수 있습니다. 예를 들어, 부모 클래스의 메소드를 수정하면 자식 클래스에서도 그 변화가 반영됩니다.
  3. 모듈화: 관련 있는 기능을 부모 클래스에 모아 두고, 세부적인 기능은 자식 클래스에 추가하여 코드가 모듈화되어 구조적인 프로그램을 작성할 수 있습니다.
  4. 다형성: 상속을 통해 다형성을 구현하여 코드의 유연성과 확장성을 높일 수 있습니다. 이를 통해 동일한 부모 클래스를 참조하는 변수가 다양한 형태로 동작할 수 있습니다.

 


추상화

  • 객체에서 공통된 부분들을 모아 상위 개념으로 새롭게 선언하는 것을 추상화라고 합니다
  • 자식들 중 공통된 특징(데이터, 메소드)이 있다면 부모에 선언하는 방식 
    https://velog.velcdn.com/images/dev-mage/post/55d11b06-01b6-42a6-b982-a97489ecbcc6/image.png

1-1이 자동차고, 그 하위가 페라리, 포터, 벤츠라고 하면 자식들은 바퀴가 4개인 특징이 있다. 이런 공통적인 특징이 있다면 1-1인 자동차에서 바퀴가 4개이다를 정의하고 아래 자식들에게 뿌려주면 된다. 이런 역전 방식을 추상화라한다.

 

추상화의 특징

  1. 중요한 정보만 노출: 추상화는 클래스나 객체의 중요한 속성과 동작을 중심으로 표현하여, 불필요한 세부 사항을 감춥니다. 사용자는 인터페이스나 메소드의 기본 동작 방식만 알면 되며, 내부 동작 원리를 몰라도 됩니다.
  2. 추상 클래스와 인터페이스 사용: 추상화는 주로 abstract 키워드로 선언된 추상 클래스나 인터페이스를 통해 구현됩니다. 추상 클래스는 공통 동작을 정의하고, 인터페이스는 반드시 구현해야 할 메소드를 선언함으로써 각기 다른 클래스가 공통된 동작을 갖도록 합니다.
  3. 구현의 강제성: 추상 클래스나 인터페이스를 사용하면, 상속받는 클래스가 특정 메소드를 반드시 구현하도록 강제할 수 있습니다. 이를 통해 코드의 일관성과 안정성을 높일 수 있습니다.
  4. 유연한 확장 가능성: 추상화를 사용하면 클래스 간의 결합도를 낮출 수 있어, 코드의 유연성과 확장 가능성이 높아집니다. 시스템이 확장되거나 변경되어도 기존 코드를 최소한으로 수정하면서 기능을 추가할 수 있습니다.

추상화의 장점

  1. 코드의 단순화: 불필요한 세부 사항을 숨기고 중요한 정보만 제공하여 코드의 복잡성을 줄이고 이해하기 쉽게 만듭니다. 사용자 입장에서 복잡한 내부 로직을 알 필요 없이 필요한 기능만 사용할 수 있습니다.
  2. 유지보수성 향상: 인터페이스나 추상 클래스는 공통된 작업 방식을 정의하고 구현체는 이를 따르기 때문에, 유지보수가 쉬워집니다. 새로운 기능을 추가하거나 수정할 때 전체 시스템에 미치는 영향을 줄일 수 있습니다.
  3. 모듈화 및 확장성: 추상화를 통해 각 클래스가 독립적으로 동작하면서도 동일한 인터페이스나 추상 클래스의 규칙을 따르기 때문에, 시스템의 모듈화가 용이해집니다. 이는 새로운 기능을 추가하거나 기존 코드를 수정할 때 유연성을 제공합니다.
  4. 다형성 지원: 추상 클래스와 인터페이스를 통해 다양한 객체들이 동일한 인터페이스를 따르도록 할 수 있어 다형성(Polymorphism)을 구현하기 쉽습니다. 예를 들어, 동일한 메소드를 다양한 방식으로 구현하여 객체마다 다른 동작을 수행하게 할 수 있습니다.
// 추상 클래스
abstract class Shape {
    // 추상 메소드 - 구체적인 구현은 자식 클래스에서 정의
    public abstract double calculateArea();

    // 공통 메소드
    public void display() {
        System.out.println("This is a shape.");
    }
}

// 구체 클래스 - Circle
class Circle extends Shape {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    // 추상 메소드 구현
    @Override
    public double calculateArea() {
        return Math.PI * radius * radius;
    }
}

// 구체 클래스 - Rectangle
class Rectangle extends Shape {
    private double width;
    private double height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    // 추상 메소드 구현
    @Override
    public double calculateArea() {
        return width * height;
    }
}

public class Main {
    public static void main(String[] args) {
        Shape circle = new Circle(5);
        Shape rectangle = new Rectangle(4, 6);

        circle.display();  // This is a shape.
        System.out.println("Circle Area: " + circle.calculateArea());  // Circle Area: 78.5398...

        rectangle.display();  // This is a shape.
        System.out.println("Rectangle Area: " + rectangle.calculateArea());  // Rectangle Area: 24.0
    }
}
더보기

코드 설명

  1. 추상 클래스 Shape: Shape 클래스는 calculateArea()라는 추상 메소드를 선언했습니다. 이 메소드는 구체 클래스에서 정의되어야 합니다.
  2. 구체 클래스 Circle과 Rectangle: Shape 클래스를 상속받아 calculateArea() 메소드를 각각 원과 사각형에 맞게 구현했습니다.
  3. 추상화의 효과: Shape 클래스를 상속받은 모든 도형 클래스는 calculateArea() 메소드를 반드시 구현하도록 강제됩니다. 이를 통해 각 도형 클래스가 calculateArea() 메소드를 통해 면적을 계산할 수 있다는 점에서 일관된 사용법을 제공하게 됩니다.

객체와 클래스

  • 우리는 객체를 생성하기 위해서 설계도가 필요합니다.
  • 현실 세계에서는 자동차를 만들기 위해 자동차 설계도를 토대로 자동차를 생산합니다.
  • 마찬가지로 소프트웨어에서도 객체를 만들기 위해서는 설계도에 해당하는 클래스가 필요합니다.
  • 이때 클래스를 토대로 생성된 객체를 해당 클래스의 ‘인스턴스’라고 부르며 이 과정을 ‘인스턴스화’라고 부릅니다.
  • 동일한 클래스로 여러 개의 인스턴스를 만들 수 있습니다.
  • 이때 객체와 인스턴스는 거의 비슷한 표현이지만 자세하게 구분해 보자면 아래와 같습니다. 

자동차 클래스를 통해 만들어진 하나의 자동차를 인스턴스라고 부르며 이러한 여러 개의 인스턴스들을 크게 통틀어서 자동차 객체라고 표현할 수 있다.

 

class Car {
    String model;
    String color;
    
    // 기본 생성자
    public Car(String model, String color) {
        this.model = model;
        this.color = color;
    }
    
    // gasPedal 메서드: 속도를 설정하는 메서드로 kmh 값을 speed 필드에 저장하고 반환
    public double gasPedal(double kmh) {
        speed = kmh; // 자동차의 현재 속도를 kmh 값으로 설정
        return speed; // 설정된 속도 반환
    }
}

public class Main {
    public static void main(String[] args) {
        // 인스턴스화 과정
        Car car1 = new Car("Sedan", "Red"); // Car 클래스로부터 car1 객체 생성
        Car car2 = new Car("SUV", "Blue");  // Car 클래스로부터 car2 객체 생성
        
        System.out.println(car1.model);  // Sedan 출력
        System.out.println(car2.model);  // SUV 출력
    }
}

 

슬슬 헷갈릴 건데 간단히 하면

인스턴스(객체) car1, car2
인스턴스화 new
메서드 public double gasPedal(double kmh)
클래스 class Car
생성자 public Car(String model, String color)    /     생성자는 메서드와 다르게 클래스와 이름이 같다

 

Car car1 = new Car("Sedan", "Red");는 Car 클래스를 사용하여 "Sedan""Red"라는 속성값을 가지는 새로운 Car 객체를 생성하고, 그 객체를 car1이라는 참조형 변수에 참조하도록 하는 코드

'Back-End (Web) > JAVA' 카테고리의 다른 글

[JAVA] 객체의 필드와 메서드  (0) 2024.11.12
[JAVA] 클래스 설계와 객체 생성  (0) 2024.11.12
[JAVA] 참조형 자료구조 정리(LIST / STACK / QUEUE / SET / MAP)  (0) 2024.11.11
[JAVA] 배열  (0) 2024.11.11
[JAVA] 반복문  (0) 2024.11.11

참조형 변수는 왜 쓸까?

변수 선언에서 보통 참조형 변수가 사람의 머리를 가장 잘 쪼개주는데, 그냥 상자에 물건을 담는게 아니라 물건을 HEAP이라는 상자에 담고 STACK에는 HEAP이라는 상자의 위치(주소)를 저장해 버린다..

 

참조형 변수(배열)

 

사실 생각해보면 이상하다. 그냥 값만 저장하면 1개만 저장하면 되는데, 뭐하러 주소와 값을 같이 저장해서 메모리 공간을 2배로 차지하는 것일까.. 하지만 여기에는 중요한 이유가 있다. 

 

왜 이런 일이 발생했는지 간단히 설명해 보자면 '객체 지향 프로그래밍과 효율적인 메모리 관리를 위해서' 이다. 예를 들어보자

 

1. 효율적인 메모리 관리를 위해

String str = "Hello";

 

여기서 str 변수는 "Hello"라는 값을 직접 저장하는 것이 아니라, "Hello"라는 문자열이 메모리 어딘가에 저장된 주소를 저장한다. 만약 "Hello"라는 문자열을 직접 값으로 저장하려면, "Hello"라는 데이터를 여러 번 복사해야 하는 번거로움이 발생할 수 있는 반면, 참조형 변수는 이 주소만 저장하면 되므로 메모리 절약이 가능하다.

 

= 재활용이 가능하다. hello라는 변수를 5번 선언해 메모리 5칸 먹을 바엔, 5개의 변수에 주소 1개, 값 1개만 할당하면 메모리 2칸만 사용한다는 의미이다.

 

2. 객체 지향 프로그래밍(OOP)의 핵심

이게 무슨 말인지를 알기 위해서는 Java의 핵심인 객체 지향이 뭔지 알아야한다.

 

객체 지향에서 객체속성(데이터)과 행동(기능)을 가지고 있는 구분 가능한 세트를 말한다.

객체 지향

 

예를 들어, 강아지라는 객체는 이름, 나이라는 속성을 가지고, 짖기, 산책하기와 같은 행동을 할 수 있다. 이러한 특징의 세트를 가진 존재를 객체라고 정의한다.

 

Person person1 = new Person("Alice", 30);
Person person2 = person1; // person2는 person1의 주소를 가리킴
person2.setName("Bob");  // person1과 person2 모두 "Bob"을 가리키게 됨

위 코드에서 사람이라는 객체는 (앨리스, 30)이라는 속성을 가지고 있다. 그리고 그 중 이름을 bob으로 수정하는 코드이다.

 

만약에 위와 같이 객체의 내용을 바꿔야할 때 마다. 객체를 복사해야한다면 어떨까?

1. 객체를 복사한다
2. 복사된 객체의 정보를 저장한다
3. 원본 객체를 지운다.

 

이 과정을 거친다. 이 과정은 메모리 사용량을 늘리고, 복잡한 코드를 작성하게 만든다. 차라

1. 객체 중 이름의 변수 내용을 수정한다
2. 수정한 내용을 저장한다.

 

이게 더 편하고 유용하다는 것.

 

 

정말 간단하게 이해하자면

상황 a 아파트에 배달을 해야하는데, 아파트의 이름이 b로 바뀌어야한다.
객체 지향 배달원에게 아파트이름이 a->b로 바뀌었다고 알려준다
절차적 프로그래밍 아파트 b를 새로 만들고 아파트a를 부순다.

 

아무리 그래도 아파트를 부수고 다시 건축하는 방식보단 주소를 변경해주는 것이 여러 의미로 이로울 것이다;;

 

결론적으로 위와 같은 객체 지향적 프로그래밍과 효율적인 메모리 관리를 위해 참조형 변수를 사용하게 되었다는 것

 

 

Java의 Collection

  • 데이터를 저장하고 다룰 수 있는 다양한 방법을 제공하는 인터페이스클래스들의 집합으로, 자바에서 Collection Framework는 데이터를 효율적으로 다룰 수 있도록 도와주는 핵심적인 역할을 하며, 여러 가지 자료구조와 알고리즘을 제공한다.
  • 대표적으로 List, Set, Queue, Map등 다양한 종류가 있다.
  • Collection은 기본형 변수가 아닌 참조형 변수를 저장한다.

Collection

 

List

  • 순서가 있는 데이터의 집합 (데이터 중복 허용) 으로 배열과 유사하다
    ArrayList

ArrayList

  • 배열은( Array ) 타입과 저장량을 지정하면 변경이 안되며 저장된 내용도 수정이 불가능하다(기본형 변수)
  • 리스트( ArrayList )의 경우 타입, 저장량, 수정 모두 관계없이 동적으로 늘어나고 자유롭게 수정이 가능하다.(참조형 변수) 다만,  배열(Array)처럼 일렬로 데이터를 저장하고 조회하여 순번 값(인덱스)로 값을 하나씩 조회할 수 있다.
// ArrayList 
// (사용하기 위해선 import java.util.ArrayList; 를 추가해야합니다.)
import java.util.ArrayList;

public class Main {

	public static void main(String[] args) {
		ArrayList<Integer> intList = new ArrayList<Integer>(); // 선언 및 생성
		
		intList.add(1);
		intList.add(2);
		intList.add(3);
		
		System.out.println(intList.get(0)); // 1 출력
		System.out.println(intList.get(1)); // 2 출력
		System.out.println(intList.get(2)); // 3 출력
		System.out.println(intList.toString()); // [1,2,3] 출력
		
		intList.set(1, 10); // 1번순번의 값을 10으로 수정합니다.
		System.out.println(intList.get(1)); // 10 출력
		
		
		intList.remove(1); // 1번순번의 값을 삭제합니다.
		System.out.println(intList.toString()); // [1,3] 출력
		
		intList.clear(); // 전체 값을 삭제합니다.
		System.out.println(intList.toString()); // [] 출력
	}
}
더보기

기능

  • 선언 : ArrayList<Integer> intList 형태로 선언합니다.
  • 생성 : new ArrayList<Integer>(); 형태로 생성합니다.
  • 초기화 : 사이즈를 지정하는 것이 없기 때문에 초기화가 필요 없습니다.
  • 값 추가 : intList.add({추가할 값}) 형태로 값을 추가합니다.
  • 값 수정 : intList.set({수정할 순번}, {수정할 값}) 형태로 값을 수정합니다.
  • 값 삭제 : intList.remove({삭제할 순번}) 형태로 값을 삭제합니다.
  • 전체 출력 : intList.toString() 형태로 전체 값을 대괄호[]로 묶어서 출력합니다.
  • 전체 제거 : intList.clear() 형태로 전체 값을 삭제합니다.

 

LinkedList

LinkedList

  • 메모리에 남는 공간을 요청해서 여기저기 나누어서 실제 값을 담아 놓고, 실제 값이 있는 주소값으로 목록을 구성하고 저장합니다. (남는 공간 아무대나 저장하고 데이터 위치를 주소로 찾는다)
  • 기본적인 기능은 ArrayList 와 동일하지만 LinkedList는 값을 나누어 담기 때문에 모든 값을 조회하는 속도가 느립니다. 대신에, 값을 중간에 추가하거나 삭제할 때는 속도가 빠릅니다.
  • 중간에 값을 추가하는 기능이 있습니다. (여기선 속도 빠르다. 순서 지정 없이 아무대나 막 저장해서)
// LinkedList 
// (사용하기 위해선 import java.util.LinkedList; 를 추가해야합니다.)
import java.util.LinkedList;

public class Main {

	public static void main(String[] args) {
		LinkedList<Integer> linkedList = new LinkedList<>(); // 선언 및 생성

		linkedList.add(1);
		linkedList.add(2);
		linkedList.add(3);

		System.out.println(linkedList.get(0)); // 1 출력
		System.out.println(linkedList.get(1)); // 2 출력
		System.out.println(linkedList.get(2)); // 3 출력
		System.out.println(linkedList.toString()); // [1,2,3] 출력 (속도 느림)

		linkedList.add(2, 4); // 2번 순번에 4 값을 추가합니다.
		System.out.println(linkedList); // [1,2,4,3] 출력

		linkedList.set(1, 10); // 1번순번의 값을 10으로 수정합니다.
		System.out.println(linkedList.get(1)); // 10 출력

		linkedList.remove(1); // 1번순번의 값을 삭제합니다.
		System.out.println(linkedList); // [1,4,3] 출력

		linkedList.clear(); // 전체 값을 삭제합니다.
		System.out.println(linkedList); // [] 출력
	}
}
더보기

기능

  • 선언 : LinkedList<Integer> linkedList 형태로 선언합니다.
  • 생성 : new LinkedList<Integer>(); 형태로 생성합니다.
  • 초기화 : 사이즈를 지정하는 것이 없기 때문에 초기화가 필요 없습니다.
  • 값 추가 : linkedList.add({추가할 값}) 형태로 값을 추가합니다.
  • 값 중간에 추가 : linkedList.add({추가할 순번}, {추가할 값}) 형태로 값을 중간에 추가합니다.
  • 값 수정 : linkedList.set({수정할 순번}, {수정할 값}) 형태로 값을 수정합니다.
  • 값 삭제 : linkedList.remove({삭제할 순번}) 형태로 값을 삭제합니다.
  • 전체 출력 : linkedList.toString() 형태로 전체 값을 대괄호[]로 묶어서 출력합니다.
  • 전체 제거 : linkedList.clear() 형태로 전체 값을 삭제합니다.

 

stack 

  • 수직으로 쌓아놓고 넣었다가 빼서 조회하는 형식으로 데이터를 관리하는 방식
  • 이걸 “나중에 들어간 것이 가장 먼저 나온다(Last-In-First-out)” 성질을 가졌다고 표현한다.
  • 넣는 기능(push()) 과 조회(peek()), 꺼내는(pop()) 기능만 존재한다.

Stack

 

스택을 사용하는 이유:
  1. 간단한 메모리 관리
    스택은 후입선출(LIFO) 구조로 동작하는데, 이는 함수 호출이나 작업을 관리하는 데 유용합니다. 프로그램에서 함수가 호출될 때마다 그 함수의 실행 정보를 스택에 저장하고, 함수가 끝나면 그 정보를 스택에서 꺼내는 방식으로 동작합니다. 이를 통해 함수의 호출 및 반환을 효율적으로 관리할 수 있습니다.
  2. 재귀 알고리즘 처리
    재귀 함수는 자기 자신을 호출하는 방식으로 동작합니다. 재귀 함수 호출 시, 각 호출의 상태를 스택에 저장하며, 함수 호출이 끝나면 스택에서 해당 상태를 꺼내어 돌아가게 됩니다. 스택은 재귀 함수의 동작을 간단하게 처리할 수 있게 도와줍니다.
  3. 작업의 순서 보장
    스택은 마지막에 추가된 데이터를 먼저 꺼내기 때문에, 작업의 순서를 보장하는 데 유용합니다. 예를 들어, 웹 브라우저의 뒤로 가기 버튼은 이전 페이지의 URL을 스택에 저장하고, 뒤로 가기 버튼을 클릭하면 스택에서 URL을 꺼내서 이전 페이지로 돌아갑니다.
  4. Undo/Redo 기능
    스택은 되돌리기(Undo)와 다시 하기(Redo) 기능을 구현하는 데 매우 유용합니다. 예를 들어, 문서 편집 프로그램에서 사용자가 어떤 작업을 실행할 때마다 그 작업을 스택에 저장하고, Undo 버튼을 클릭하면 마지막 작업을 꺼내서 이전 상태로 되돌릴 수 있습니다. 다시 하기 기능은 Undo 스택을 이용해 처리할 수 있습니다.
  5. 괄호 짝 맞추기
    컴파일러나 코드 분석기에서 스택을 사용하여 괄호의 짝을 맞추는 작업을 처리합니다. 괄호가 열릴 때마다 스택에 저장하고, 괄호가 닫힐 때마다 스택에서 꺼내는 방식으로 짝을 맞출 수 있습니다.

위의 기능을 많이 이야기 하지만, 가장 중요한 점은 최근 저장된 데이터를 나열하고 싶거나 데이터의 중복 처리를 막고 싶을 때 사용한다.

 

// Stack 
// (사용하기 위해선 import java.util.Stack; 를 추가해야합니다.)
import java.util.Stack;

public class Main {

	public static void main(String[] args) {
		Stack<Integer> intStack = new Stack<Integer>(); // 선언 및 생성
		
		intStack.push(1);
		intStack.push(2);
		intStack.push(3);

		while (!intStack.isEmpty()) { // 다 지워질때까지 출력
		    System.out.println(intStack.pop()); // 3,2,1 출력
		}

		// 다시 추가
		intStack.push(1);
		intStack.push(2);
		intStack.push(3);
		
		// peek()
		System.out.println(intStack.peek()); // 3 출력
		System.out.println(intStack.size()); // 3 출력 (peek() 할때 삭제 안됬음)
		
		// pop()
		System.out.println(intStack.pop()); // 3 출력
		System.out.println(intStack.size()); // 2 출력 (pop() 할때 삭제 됬음)		
		
		System.out.println(intStack.pop()); // 2 출력
		System.out.println(intStack.size()); // 1 출력 (pop() 할때 삭제 됬음)		

		while (!intStack.isEmpty()) { // 다 지워질때까지 출력
		    System.out.println(intStack.pop()); // 1 출력 (마지막 남은거 하나)
		}
	}
}
더보기

기능

  • 선언 : Stack<Integer> intStack 형태로 선언합니다.
  • 생성 : new Stack<Integer>(); 형태로 생성합니다.
  • 추가 : intStack.push({추가할 값}) 형태로 값을 추가합니다.
  • 조회 : intStack.peek() 형태로 맨 위값을 조회합니다.
  • 꺼내기 : intStack.pop() 형태로 맨 위값을 꺼냅니다. (꺼내고 나면 삭제됨)

Queue

  • 빨대처럼 한쪽에서 데이터를 넣고 반대쪽에서 데이터를 뺄 수 있는 집합이다.
  • 편의점 알바에서 많이 듣게 되는 선입선출(Fist In First Out)을 성질로 갖고 있는 Collection
  • 넣는 기능(add()) 과 조회(peek()), 꺼내는(poll()) 기능만 존재한다.

 

  • Queue는 생성자가 없는 껍데기라서 바로 생성할 수는 없습니다. (껍데기 = 인터페이스)
  • 생성자가 존재하는 클래스인 LinkedList를 사용하여 Queue를 생성해서 받을 수 있습니다.
// LinkedList 를 생성하면 Queue 기능을 할 수 있습니다. (Queue 가 부모/ LinkedList 가 자식이기 떄문)
Queue<Integer> intQueue = new LinkedList<Integer>();

 

이걸 이해하기 위해서는 인터페이스와 생성자, 오버로딩의 개념을 알 필요가 있다.

 

인터페이스 (Interface) = 설계도

  • 자바에서 클래스가 구현해야 하는 계약을 정의하는 구조로 쉽게 말해, 인터페이스는 "어떤 메서드들이 있어야 한다"고 약속만 정의하고, 그 구현 내용은 다른 클래스에서 작성합니다.
  • 말 그대로 클래스의 설계도로, 아래의 Animal 처럼 Animal 인터페이스에 포함될 클래스는 반드시 sound라는 메서드를 포함 시켜 주세요라는 설계도를 말한다.
// Animal 인터페이스
interface Animal {
    void sound();  // sound 메서드만 선언, 구현은 강제
}

// Dog 클래스는 Animal 인터페이스를 구현해야 하므로 sound 메서드를 반드시 구현해야 한다.
class Dog implements Animal {
    @Override
    public void sound() {
        System.out.println("Bark");
    }
}

// Cat 클래스도 Animal 인터페이스를 구현해야 하므로 sound 메서드를 반드시 구현해야 한다.
class Cat implements Animal {
    @Override
    public void sound() {
        System.out.println("Meow");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal dog = new Dog();
        Animal cat = new Cat();
        
        dog.sound();  // Bark
        cat.sound();  // Meow
    }
}

 

생성자 (Constructor)

  • 객체가 생성될 때 초기화 작업을 담당하는 특별한 메서드입니다. 자바에서 생성자는 클래스 이름과 동일하고, 반환 값이 없습니다(즉, void도 사용하지 않습니다). 생성자는 객체가 생성될 때 자동으로 호출되며, 클래스의 속성(멤버 변수)을 초기화하는 데 사용됩니다.
  • 말이 어려운데, 객체의 기본 초기값을 설정하여 객체를 추가로 생성해도 값이 미리 지정해 둔 값으로 초기화 될 수 있도록 한 메소드

생성자의 특징:

  1. 클래스 이름과 동일한 이름을 가집니다.
  2. 반환 타입이 없습니다 (반환 타입으로 void도 사용하지 않습니다 = 반환이 필요 없다).
  3. 자동 호출됩니다: 객체가 생성될 때마다 생성자가 자동으로 실행됩니다. (미리 지정해 두었으니..)
  4. 생성자는 오버로딩할 수 있습니다: 같은 클래스 내에서 여러 개의 생성자를 정의할 수 있습니다. 이때, 매개변수의 개수나 타입을 달리하여 구별합니다.

기본 생성자 (Default Constructor)

  • 생성자를 명시적으로 정의하지 않으면, 자바 컴파일러가 기본 생성자(default constructor)를 자동으로 제공합니다.
  • 기본 생성자는 매개변수가 없고, 객체를 생성할 때 기본적인 초기화 작업을 합니다.
class Person {
    String name;
    int age;

    // 기본 생성자
    public Person() {
        name = "Unknown";
        age = 0;
    }

    // 매개변수가 있는 생성자 (이름만 받는 생성자)
    public Person(String name) {
        this.name = name;
        this.age = 0;  // 나이는 기본값 0
    }

    // 매개변수가 있는 생성자 (이름과 나이 받는 생성자)
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 메서드
    public void introduce() {
        System.out.println("이름: " + name + ", 나이: " + age);
    }
}

public class Main {
    public static void main(String[] args) {
        // 기본 생성자 사용
        Person person1 = new Person();
        person1.introduce();  // 이름: Unknown, 나이: 0

        // 이름만 받는 생성자 사용
        Person person2 = new Person("Alice");
        person2.introduce();  // 이름: Alice, 나이: 0

        // 이름과 나이를 받는 생성자 사용
        Person person3 = new Person("Bob", 25);
        person3.introduce();  // 이름: Bob, 나이: 25
    }
}

 

 

오버로딩

  • 같은 이름의 메서드나 생성자 매개변수의 개수나 타입이 달라지도록 여러 번 정의하는 기법입니다. 오버로딩을 사용하면 같은 이름으로 다양한 작업을 처리할 수 있습니다.

오버로딩의 특징

  1. 같은 이름의 메서드나 생성자지만 매개변수의 개수나 타입이 달라야 합니다.
  2. 반환 타입은 오버로딩을 구별하는 기준이 되지 않습니다.
  3. 컴파일 시점에 메서드 호출을 구분하기 때문에, 실행 시간에 결정되는 것이 아니라 호출 시점에서 적절한 메서드가 선택됩니다.

오버로딩의 장점

  • 코드의 가독성 향상: 비슷한 작업을 수행하는 메서드를 같은 이름으로 처리할 수 있어 코드가 깔끔하고 이해하기 쉬워집니다.
  • 유지보수 용이성: 같은 이름의 메서드를 사용하여 여러 매개변수에 대한 처리 로직을 관리할 수 있어 유지보수가 용이합니다.

오버로딩 규칙

  1. 매개변수의 타입이 달라야 오버로딩으로 인식됩니다.
  2. 매개변수의 개수가 달라야 오버로딩으로 인식됩니다.
  3. 매개변수의 순서가 달라야 오버로딩으로 인식됩니다.
class Person {
    String name;
    int age;

    // 기본 생성자
    public Person() {
        name = "Unknown";
        age = 0;
    }

    // 매개변수가 있는 생성자 (이름만 받는 생성자)
    public Person(String name) {
        this.name = name;
        this.age = 0;  // 나이는 기본값 0
    }

    // 매개변수가 있는 생성자 (이름과 나이 받는 생성자)
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 메서드
    public void introduce() {
        System.out.println("이름: " + name + ", 나이: " + age);
    }
}

public class Main {
    public static void main(String[] args) {
        // 기본 생성자 사용
        Person person1 = new Person();
        person1.introduce();  // 이름: Unknown, 나이: 0

        // 이름만 받는 생성자 사용
        Person person2 = new Person("Alice");
        person2.introduce();  // 이름: Alice, 나이: 0

        // 이름과 나이를 받는 생성자 사용
        Person person3 = new Person("Bob", 25);
        person3.introduce();  // 이름: Bob, 나이: 25
    }
}

 

< 출력 결과 >

이름: Unknown, 나이: 0
이름: Alice, 나이: 0
이름: Bob, 나이: 25
더보기

이해를 위한 오버로딩의 어원
**오버로딩(Overloading)**이라는 용어는 **'과잉(Over)'**과 **'적재(Loading)'**의 결합으로, 프로그래밍에서 특정 작업이나 기능이 여러 방식으로 "과잉"으로 제공된다는 개념을 나타냅니다.

 

즉, **오버로딩(Overloading)**은 하나의 메소드나 생성자가 여러 형태로 존재한다는 개념입니다. 메소드 이름은 같지만, 매개변수의 수나 타입에 따라 서로 다른 구현이 가능하다는 뜻입니다. 이 개념은 하나의 이름으로 여러 기능을 제공하는 것으로, "과잉 적재"의 개념을 표현하고 있습니다.

 

< 위 예시의 해석입니다. >

오버로딩된 생성자들
1. **기본 생성자** (`Person()`):
   - 매개변수가 없는 생성자입니다.
   - `name`을 `"Unknown"`으로, `age`를 `0`으로 초기화합니다.

2. **이름만 받는 생성자** (`Person(String name)`):
   - 하나의 매개변수(`String name`)만 받는 생성자입니다.
   - `name`을 매개변수로 받아 초기화하고, `age`는 기본값인 `0`으로 초기화합니다.

3. **이름과 나이를 받는 생성자** (`Person(String name, int age)`):
   - 두 개의 매개변수(`String name`, `int age`)를 받는 생성자입니다.
   - `name`과 `age`를 매개변수로 받아 초기화합니다.

오버로딩 설명
**생성자 오버로딩**은 **같은 이름**의 생성자를 **매개변수의 개수나 타입이 다르게 정의**하여, 객체를 생성할 때 다양한 방법으로 초기화를 할 수 있게 해주는 기능입니다. 위 코드에서 `Person` 클래스의 생성자는 **세 가지 형태로 오버로딩**되어 있으며, 각기 다른 방식으로 객체를 초기화할 수 있습니다.

어떻게 오버로딩이 이루어졌는지

1. **기본 생성자** (`Person()`):
   - `Person person1 = new Person();`  
     이 코드에서는 매개변수가 없는 기본 생성자가 호출됩니다. 그래서 `name`은 `"Unknown"`, `age`는 `0`으로 초기화됩니다.

2. **이름만 받는 생성자** (`Person(String name)`):
   - `Person person2 = new Person("Alice");`  
     이 코드에서는 이름만 받는 생성자가 호출됩니다. `name`은 `"Alice"`, `age`는 기본값인 `0`으로 초기화됩니다.

3. **이름과 나이를 받는 생성자** (`Person(String name, int age)`):
   - `Person person3 = new Person("Bob", 25);`  
     이 코드에서는 이름과 나이를 받는 생성자가 호출됩니다. `name`은 `"Bob"`, `age`는 `25`로 초기화됩니다.

생성자 오버로딩의 장점:
- **유연성**: 같은 클래스의 객체를 다양한 방법으로 생성할 수 있습니다. 예를 들어, `Person` 클래스의 객체를 `name`만 받거나, `name`과 `age`를 함께 받거나, 기본값을 사용하여 객체를 생성할 수 있습니다.
- **코드의 가독성 향상**: 사용자가 원하는 방식으로 객체를 생성할 수 있기 때문에, 코드가 더 직관적이고 가독성이 좋습니다.

오버로딩의 규칙:
- **매개변수의 개수**가 다르면 오버로딩된 생성자로 인식됩니다.
- **매개변수의 타입**이 다르면 오버로딩된 생성자로 인식됩니다.
- **매개변수의 순서**가 다르면 오버로딩된 생성자로 인식됩니다.

 

 

자 이제 다시한번 Queue에 관한 설명을 읽어보자

  • Queue는 생성자가 없는 껍데기라서 바로 생성할 수는 없습니다. (껍데기 = 인터페이스)
  • 생성자가 존재하는 클래스인 LinkedList를 사용하여 Queue를 생성해서 받을 수 있습니다.

= 설계도를 생성하고 그 안에 값을 지정해 주는 생성자가 포함된 클래스를 통해 Queue를 생성

= Queue<String> queue = new LinkedList<>();

 

= Queue라는 설계도(인터페이스)를 바탕으로 LinkedList 메소드를 사용하여 큐를 초기화하여 String 타입을 처리하는 queue큐를 생성해라

 

= Queue라는 설계도(건설업장의 규칙 = 먼저 온 사람이 먼저 일한다 fist in first out )를 바탕으로 LinkedList 메소드(도구 = 곡괭이를 쓸지 드릴을 사용할지 [배열 기반의 Queue , 우선순위 Queue와 같은 방식들])를 사용하여 큐를 초기화하여 String 타입을 처리하는 queue큐를 생성해라

 

// Queue 
// (사용하기 위해선 java.util.LinkedList; 와 import java.util.Queue; 를 추가해야합니다.)
import java.util.LinkedList;
import java.util.Queue;

public class Main {

    public static void main(String[] args) {
        // Queue 인터페이스를 구현한 LinkedList 클래스를 사용
        Queue<String> queue = new LinkedList<>();

        // 큐에 데이터 삽입 (enqueuing)
        queue.offer("Apple");
        queue.offer("Banana");
        queue.offer("Cherry");

        // 큐에서 데이터 꺼내기 (dequeuing)
        System.out.println("First item: " + queue.poll()); // Apple
        System.out.println("Second item: " + queue.poll()); // Banana

        // 큐에 또 추가
        queue.offer("Grapes");

        // 큐에서 데이터 꺼내기
        System.out.println("Third item: " + queue.poll()); // Cherry
        System.out.println("Fourth item: " + queue.poll()); // Grapes

        // 큐가 비었는지 확인
        if (queue.isEmpty()) {
            System.out.println("큐가 비었습니다.");
	}
}
더보기

기능

  • 선언 : Queue<Integer> intQueue 형태로 선언합니다.
  • 생성 : new LinkedList<Integer>(); 형태로 생성합니다.
  • 추가 : intQueue.add({추가할 값}) 형태로 값을 맨 위에 추가합니다.
  • 조회 : intQueue.peek() 형태로 맨 아래 값을 조회합니다.
  • 꺼내기 : intQueue.poll() 형태로 맨 아래 값을 꺼냅니다. (꺼내고 나면 삭제됨)

 

SET

  • 순서가 없는 데이터의 집합 (데이터 중복 허용 안 함) - 순서 없고 중복 없는 배열
  • 여기도 Queue와 같이 인터페이스로 생성자가 존재하는 클래스인 HashSet, Set, TreeSet등을 생성해서 만들 수 있다.

  • HashSet : 가장 빠르며 순서를 전혀 예측할 수 없음
  • TreeSet : 정렬된 순서대로 보관하며 정렬 방법을 지정할 수 있음
  • LinkedHashSet : 추가된 순서, 또는 가장 최근에 접근한 순서대로 접근 가능
    즉, 보통 HashSet 을 쓰는데 순서 보장이 필요하면 LinkedHashSet 을 주로 사용한다.

// Set 
// (사용하기 위해선 import java.util.Set; 와 java.util.HashSet; 를 추가해야합니다.)
import java.util.HashSet;
import java.util.Set;

public class Main {

	public static void main(String[] args) {
		Set<Integer> intSet = new HashSet<Integer>(); // 선언 및 생성

		intSet.add(1);
		intSet.add(2);
		intSet.add(3);
		intSet.add(3); // 중복된 값은 덮어씁니다.
		intSet.add(3); // 중복된 값은 덮어씁니다.

		for (Integer value : intSet) {
			System.out.println(value); // 1,2,3 출력
		}

		// contains()
		System.out.println(intSet.contains(2)); // true 출력
		System.out.println(intSet.contains(4)); // false 출력

		// remove()
		intSet.remove(3); // 3 삭제

		for (Integer value : intSet) {
			System.out.println(value); // 1,2 출력
		}
	}
}
더보기

기능

  • 선언 : Set<Integer> intSet 형태로 선언합니다.
  • 생성 : new HashSet<Integer>(); 형태로 생성합니다.
  • 추가 : intSet.add({추가할 값}) 형태로 값을 맨 위에 추가합니다.
  • 삭제 : intSet.remove({삭제할 값}) 형태로 삭제할 값을 직접 지정합니다.
  • 포함 확인 : intSet.contains({포함 확인 할 값}) 형태로 해당 값이 포함되어있는지 boolean 값으로 응답받습니다.

 

Map

  • key-value 구조로 구성된 데이터를 저장한다.
  • 시간복잡도가 1 / 한번에 정보의 위치를 찾을 수 있는 자료구조형태
  • 꼭 주소가 포함되어야 한다는건 아니다 / 근데 보통 구현하면 주소로 구현한다.
  • key는 사실 인덱스와 비슷한 역활을 한다. 즉 절대로 중복되서는 안된다. (주소가 중복되면??)
  • 여기도 HashSet, TreeSet 등으로 응용하여 사용할 수 있다. 즉 여기도 인터페이스


  • HashMap : 중복을 허용하지 않고 순서를 보장하지 않음, 키와 값으로 null이 허용
  • TreeMap : key 값을 기준으로 정렬을 할 수 있습니다. 다만, 저장 시 정렬(오름차순)을 하기 때문에 저장시간이 다소 오래 걸림 
// Map 
// (사용하기 위해선 import java.util.Map; 를 추가해야합니다.)
import java.util.Map;

public class Main {

	public static void main(String[] args) {
		Map<String, Integer> intMap = new HashMap<>(); // 선언 및 생성

		//          키 , 값
		intMap.put("일", 11);
		intMap.put("이", 12);
		intMap.put("삼", 13);
		intMap.put("삼", 14); // 중복 Key값은 덮어씁니다.
		intMap.put("삼", 15); // 중복 Key값은 덮어씁니다.

		// key 값 전체 출력
		for (String key : intMap.keySet()) {
			System.out.println(key); // 일,이,삼 출력
		}

		// value 값 전체 출력
		for (Integer key : intMap.values()) {
			System.out.println(key); // 11,12,15 출력
		}

		// get()
		System.out.println(intMap.get("삼")); // 15 출력
	}
}
더보기

기능

  • 선언 : Map<String, Integer> intMap 형태로 Key타입과 Value타입을 지정해서 선언합니다.
  • 생성 : new HashMap<>(); 형태로 생성합니다.
  • 추가 : intMap.put({추가할 Key값},{추가할 Value값}) 형태로 Key에 Value값을 추가합니다.
  • 조회 : intMap.get({조회할 Key값}) 형태로 Key에 있는 Value값을 조회합니다.
  • 전체 key 조회 : intMap.keySet() 형태로 전체 key 값들을 조회합니다.
  • 전체 value 조회 : intMap.values() 형태로 전체 value 값들을 조회합니다.
  • 삭제 : intMap.remove({삭제할 Key값}) 형태로 Key에 있는 Value값을 삭제합니다.

 

배열의 길이 파악

1. length

  • arrays(int[], double[], String[])
  • length는 배열의 길이를 조회해 줍니다.
int[] numbers = {1, 2, 3, 4, 5};
System.out.println("배열의 길이: " + numbers.length);  // 출력: 배열의 길이: 5

2. length()

  • String related Object(String, StringBuilder etc)
  • length()는 문자열의 길이를 조회해 줍니다. (ex. “ABCD”.length() == 4)
String text = "Hello, World!";
System.out.println("문자열의 길이: " + text.length());  // 출력: 문자열의 길이: 13

3. size()

  • Collection Object(ArrayList, Set etc)
  • size()는 컬렉션 타입 목록의 길이를 조회해 줍니다.
ArrayList<String> list = new ArrayList<>();
list.add("apple");
list.add("banana");
list.add("cherry");
System.out.println("리스트의 크기: " + list.size());  // 출력: 리스트의 크기: 3

 

 

뭔가 많고 CS 지식을 추가로 확보해야하는 내용이었다. 추후 CS관련된 내용도 정리할 필요가 있어보인다....

 

오늘 요약...

import java.util.*;

public class DataStructureExample {
    public static void main(String[] args) {
        // 1. List 사용: 순서가 있는 데이터 저장, 중복 허용
        List<String> fruits = new ArrayList<>();
        fruits.add("Apple");
        fruits.add("Banana");
        fruits.add("Cherry");
        fruits.add("Apple");  // 중복 허용

        System.out.println("List (Fruits): " + fruits);

        // 2. Stack 사용: LIFO 구조로 데이터 처리
        Stack<String> bookStack = new Stack<>();
        bookStack.push("Book 1");
        bookStack.push("Book 2");
        bookStack.push("Book 3");

        System.out.println("Stack (Books - Last item): " + bookStack.peek());
        System.out.println("Stack (Pop Book): " + bookStack.pop());  // 마지막으로 들어온 데이터 제거
        System.out.println("Stack after pop: " + bookStack);

        // 3. Queue 사용: FIFO 구조로 데이터 처리
        Queue<String> lineQueue = new LinkedList<>();
        lineQueue.offer("Person 1");
        lineQueue.offer("Person 2");
        lineQueue.offer("Person 3");

        System.out.println("Queue (First in line): " + lineQueue.peek());
        System.out.println("Queue (Remove from line): " + lineQueue.poll());  // 처음으로 들어온 데이터 제거
        System.out.println("Queue after poll: " + lineQueue);

        // 4. Set 사용: 중복 없는 데이터 저장
        Set<String> uniqueNames = new HashSet<>();
        uniqueNames.add("Alice");
        uniqueNames.add("Bob");
        uniqueNames.add("Alice");  // 중복 추가 시도 (저장되지 않음)

        System.out.println("Set (Unique Names): " + uniqueNames);

        // 5. Map 사용: 키-값 쌍으로 데이터 저장
        Map<String, Integer> ageMap = new HashMap<>();
        ageMap.put("Alice", 30);
        ageMap.put("Bob", 25);
        ageMap.put("Charlie", 35);

        System.out.println("Map (Ages): " + ageMap);
        System.out.println("Age of Alice: " + ageMap.get("Alice"));  // 특정 키를 이용해 값 가져오기
    }
}

 

 

가비지 컬렉터

자바 프로그램에서 더 이상 사용되지 않는 객체들을 자동으로 메모리에서 해제하는 기능을 담당합니다. 즉, 자바에서는 메모리 관리가 자동으로 이루어지며, 개발자가 명시적으로 메모리를 해제할 필요가 없습니다. 이는 자바의 중요한 특징 중 하나로, 메모리 누수를 방지하고 효율적인 메모리 관리를 가능하게 합니다.

1. 가비지 컬렉터의 역할

가비지 컬렉터는 메모리에서 더 이상 필요하지 않은 객체를 찾아서 자동으로 삭제합니다. 메모리에서 객체를 삭제하는 과정을 **"가비지 수집"**이라고 부릅니다.

2. 가비지 컬렉터가 작동하는 방식

가비지 컬렉터는 객체가 더 이상 참조되지 않거나 접근할 수 없는 상태일 때 그 객체를 가비지로 간주하여 수집합니다. 즉, 객체가 "reachable" 하지 않게 되면 가비지 컬렉터가 이를 회수하고 메모리를 확보합니다.

3. 참조와 가비지 컬렉터

  • 참조(Reference): 객체를 가리키는 포인터나 주소입니다. 예를 들어, Object obj = new Object();에서 obj는 그 객체를 가리키는 참조입니다.
  • 참조 끊김: 객체가 더 이상 참조되지 않으면, 이 객체는 가비지 컬렉터에 의해 수거될 수 있는 상태가 됩니다.
public class GarbageCollectorExample {
    public static void main(String[] args) {
        Object obj1 = new Object(); // obj1이 새 객체를 참조
        Object obj2 = obj1;         // obj2도 동일 객체를 참조
        obj1 = null;                // obj1 참조를 null로 설정, obj2는 여전히 객체를 참조
        obj2 = null;                // obj2도 null로 설정, 이제 객체는 참조되지 않음
        // 이제 객체는 가비지 컬렉터가 회수할 수 있습니다.
    }
}

위 코드에서 obj1과 obj2가 모두 null로 설정되면, 그 객체는 더 이상 참조되지 않으므로 가비지 컬렉터가 이를 회수합니다.

 

4. 가비지 컬렉션의 과정

가비지 컬렉션은 다음과 같은 주요 단계를 거칩니다:

  1. 마킹(Marking):
    • 먼저, **활성 객체(참조되는 객체)**를 찾습니다. 이를 위해 **루트 객체(Root objects)**에서부터 시작해 모든 참조 가능한 객체를 추적합니다.
    • 루트 객체는 스택, 레지스터, static 변수 등에서 참조되는 객체입니다.
  2. 스위핑(Sweeping):
    • 마킹 과정에서 찾지 못한 객체들은 더 이상 사용되지 않으므로 삭제됩니다. 이 단계에서 가비지가 수집됩니다.
  3. 압축(Compaction) (일부 GC에서만 수행):
    • 수거된 가비지 객체들이 있을 경우, 남은 객체들을 메모리의 앞쪽에 압축하여 빈 공간을 없앱니다.
    • 이 과정은 메모리의 단편화(fragmentation) = 연속된 메모리 공간이 부족해져서 메모리를 효율적으로 활용할 수 없는 상태 문제를 해결하는 데 도움을 줍니다.

 

5. 자바에서의 가비지 컬렉션 종류

자바의 JVM(Java Virtual Machine)은 여러 가지 가비지 컬렉터를 지원합니다. 각 가비지 컬렉터는 서로 다른 방식으로 메모리를 관리하므로, 성능과 효율성에 차이가 있습니다.

대표적인 가비지 컬렉터 종류:

  1. Serial GC:
    • 단일 스레드로 작업을 처리하는 간단한 가비지 컬렉터입니다. 메모리 할당과 가비지 수집을 하나의 스레드에서 처리하므로, 멀티코어 환경에서는 효율성이 떨어질 수 있습니다.
    • JVM 옵션: -XX:+UseSerialGC
  2. Parallel GC (Throughput Collector):
    • 여러 스레드를 사용하여 가비지 수집을 병렬로 처리하는 방식입니다. 성능을 중시하며, 대용량 애플리케이션에서 유리합니다.
    • JVM 옵션: -XX:+UseParallelGC
  3. CMS (Concurrent Mark-Sweep) GC:
    • 병렬 및 동시 작업을 통해 짧은 가비지 수집 시간을 목표로 합니다. 특히, 애플리케이션의 일시적인 멈춤 시간을 최소화하려는 목적으로 사용됩니다.
    • JVM 옵션: -XX:+UseConcMarkSweepGC
  4. G1 GC (Garbage First GC):
    • 고급 가비지 컬렉터로, 애플리케이션의 예측 가능한 지연 시간을 보장하고자 설계되었습니다. 작은 단위로 메모리를 수집하고, 각 단위에 대해 우선순위를 두어 수집합니다.
    • JVM 옵션: -XX:+UseG1GC
  5. ZGC (Z Garbage Collector):
    • Low-latency를 제공하는 새로운 가비지 컬렉터로, 애플리케이션의 일시적인 멈춤 시간을 매우 짧게 유지합니다. 큰 힙(heap) 메모리에서 높은 효율성을 보입니다.
    • JVM 옵션: -XX:+UseZGC

6. 가비지 컬렉터와 성능

  • 가비지 컬렉션은 메모리 회수를 자동화하여 프로그램을 편리하게 만들지만, GC 작업실행 도중 애플리케이션을 멈추게 할 수 있기 때문에 성능에 영향을 미칠 수 있습니다. 이 현상을 Stop-the-world라고 합니다.
  • 가비지 컬렉션이 자주 발생하거나 긴 시간이 걸리면, 애플리케이션의 응답성이 저하될 수 있습니다.

7. GC를 강제로 호출

자바에서는 System.gc() 메서드를 사용하여 가비지 컬렉션을 강제로 요청할 수 있습니다. 그러나 이 방법은 권장되지 않으며, JVM이 가비지 컬렉션을 최적화하여 자동으로 처리하는 것이 훨씬 효율적입니다.

System.gc();  // 가비지 컬렉션 강제 호출

 

8. 정리

  • 가비지 컬렉터는 자바 프로그램의 메모리 관리를 자동으로 처리하여, 사용하지 않는 객체를 메모리에서 해제하는 역할을 합니다.
  • 가비지 컬렉션이 어떻게 작동하는지, 그 종류와 성능에 대한 이해는 애플리케이션 성능 최적화 및 안정성 유지에 매우 중요합니다.

 

 

 

누가 이딴거 만들었어....

 

 

 

오늘의 문제...

import java.util.LinkedList;
import java.util.Queue;

class Customer {
    public void purchaseTicket() {
        System.out.println("Customer purchases ticket");
    }
}

class RegularCustomer extends Customer {
    @Override
    public void purchaseTicket() {
        System.out.println("Regular customer purchases ticket");
    }
}

class VipCustomer extends Customer {
    @Override
    public void purchaseTicket() {
        System.out.println("VIP customer purchases ticket with priority");
    }
}

public class TicketQueue {
    public static void main(String[] args) {
        // Queue 생성
        Queue<Customer> ticketQueue = new LinkedList<>();

        // Queue에 고객 추가 (RegularCustomer와 VipCustomer)
        ticketQueue.add(new RegularCustomer());
        ticketQueue.add(new VipCustomer());
        ticketQueue.add(new RegularCustomer());
        ticketQueue.add(new VipCustomer());

        // 각 고객이 차례로 티켓을 구매함
        while (!ticketQueue.isEmpty()) {
            Customer customer = ticketQueue.poll(); // Queue에서 다음 고객을 가져옴
            customer.purchaseTicket(); // 오버라이딩된 메서드 호출
        }
    }
}
더보기

<해답>

Regular customer purchases ticket
VIP customer purchases ticket with priority
Regular customer purchases ticket
VIP customer purchases ticket with priority

 

'Back-End (Web) > JAVA' 카테고리의 다른 글

[JAVA] 클래스 설계와 객체 생성  (0) 2024.11.12
[JAVA] 객체지향 프로그래밍 & 다형성  (1) 2024.11.12
[JAVA] 배열  (0) 2024.11.11
[JAVA] 반복문  (0) 2024.11.11
[JAVA] 조건문  (0) 2024.11.11

+ Recent posts