[1/6] TIL - 클래스변수 문제 해결, 객체와 클래스, 생성자, 오버로딩, 접근제어자, getter와 setter, this, super, 상속, 오버라이딩

2025. 1. 6. 20:49개발 회고/TIL

😊오늘 배운 내용

자료구조(컬렉션 - 리스트,스택,Set,Map), 객체와 클래스, 객체 생성 방법, 필드와 메서드, 오버로딩, 메서드에서 기본형변수와 참조형 변수, 인스턴스멤버와 클래스멤버, 지역변수, final, 생성자(기본생성자, 필드초기화, 생성자오버로딩), this와 this(), 접근제어자와 getter, setter, 패키지와 import, 상속, 오버라이딩

 

[어떤 문제가 있었는지 + 어떻게 해결하였는지]

1.먼저 오늘 공부한 개념들을 정리해보았다. 

 

[ 자료구조(컬렉션 - 리스트,스택,Set,Map)]

컬렉션할 선언할 때 꼭 임포트 해주어야 함. 

//list
//순서가 있는 데이터의 집합 (중복허용)
//배열과 다른 점 -> 최초길이를 몰라도 만들 수 있음. 가변적임
//Array -> 정적배열
//List(==ArrayList) -> 동적배열(크기가 가변적으로 늘어남)
//생성 시점에 작은 연속된 공간을 요청해서 참조형 변수들을 담아놓는다
//더 큰 공간이 필요하면 더 큰 공간을 받아서 저장함

//꺽쇠안에는 래퍼클래스가 들어가아함
ArrayList<Integer> intList = new ArrayList<Integer>();

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

System.out.println(intList.get(2));

//2번째 인덱스에 있는 값을 15로 바꿈
intList.set(2,15);

intList.remove(2);

//list 전체 값 삭제
intList.clear();

//list 값 확인
System.out.println(intList.toString());

 

//링크드 리스트
//메모리에 남는 공간을 요청해서 여기저기 담아놓는다
//그래서 실제값이 있는 주소값으로 목록을 구성하고 저장하는 자료구조

//기본적 기능은 ArrayList와 동일
//링크드 리스트는 값을 여기저기 나누어 담아놓은 것을 화살표로 이어놓았기 때문에 값을 조회하는 속도가
//인덱스 방식보다 느림
//대신에 리스트 중간에 값을 추가하거나 삭제할 때 용이함

LinkedList<Integer> linkedList = new LinkedList<Integer>();
linkedList.add(1);
linkedList.add(2);
linkedList.add(3);

System.out.println(linkedList.get(0));
System.out.println(linkedList.toString());

//2번 인덱스에 4라는 값을 추가
linkedList.add(2,4);
System.out.println(linkedList.toString());

linkedList.set(0,30);
System.out.println(linkedList.toString());

linkedList.remove(2);
System.out.println(linkedList.toString());

linkedList.clear();
System.out.println(linkedList.toString());

 

//stack
//push, peek, pop
//최근 저장된 데이터를 나열하고 싶거나, 데이터의 중복처리를 막고 싶을 때

//선언 및 생성
Stack<Integer> intStack = new Stack<>();

intStack.push(10);
intStack.push(15);
intStack.push(1);

//다 지워질떄까지 출력
//isEmpty()는 비워져있니? 라는 뜻 -> 값이 있으면 false 없으면 true
while(!intStack.isEmpty()) {
    System.out.println(intStack.pop());
}

//다시 추가
intStack.push(10);
intStack.push(15);
intStack.push(1);

//peek
//값을 꺼내지않고 가장 위에 있는 element를 반환하는 함수
System.out.println(intStack.peek()); //1
System.out.println(intStack.size());

 

//Queue
//add, peek, poll
//큐는 생성자가 없는 인터페이스

//선언 및 생성
Queue<Integer> intQueue = new LinkedList<Integer>();

intQueue.add(10);
intQueue.add(20);
intQueue.add(30);

while (!intQueue.isEmpty()) {
    System.out.println(intQueue.poll());
}

//다시추가
intQueue.add(10);
intQueue.add(20);
intQueue.add(30);

//peek
System.out.println(intQueue.peek());
System.out.println(intQueue.size());

 

//set
//집합과 닮음
//순서가 없고 중복 없음!
//순서가 없는 대신 중복을 허용하지 않는 프로그램에서 사용하는 자료구조
//Set은 HashSet, TreeSet 등으로 응용해서 사용가능
//Set은 생성자가 없는 껍데기라서 바로 생성할 수 없음
//생성자가 존재하는 HashSet을 이용해서 Set 구현 가능

//선언 및 생성
HashSet<Integer> intHashSet = new HashSet<>();

intHashSet.add(10);
intHashSet.add(8);
intHashSet.add(24);
intHashSet.add(9);
intHashSet.add(24);
intHashSet.add(8);

for(Integer val : intHashSet) {
    System.out.println(val);
}

//contains
System.out.println(intHashSet.contains(10)); //true
System.out.println(intHashSet.contains(7)); //false

 

//Map
//키 밸류 쌍으로 구성되어 있음
//key라는 값으로 unique하게 보장되어야 함!
//HashMap , TreeMap으로 응용해서 사용 가능

//선언 및 생성
Map<String, Integer> intMap = new HashMap<>();

intMap.put("일", 1);
intMap.put("이", 2);
intMap.put("삼", 3);
intMap.put("사", 4);
intMap.put("일", 5);
intMap.put("일", 6);

//key값 전체 출력
//keySet() -> Map에서 key만 빼서 배열로 만드는 함수
for(String key: intMap.keySet()) {
    System.out.println(key);
}

//value값 전체 출력
//values() -> Map에서 value만 빼서 배열로 만드는 함수
for(Integer val: intMap.values()) {
    System.out.println(val);
}

//key를 가지고 value를 찾아오는 방법
System.out.println(intMap.get("삼")); //3

 

[객체와 클래스]

Car 클래스

public class Car {

    //필드

    //고유데이터
    String company;
    String model = "GV80";
    String color;
    double price;

    //상태데이터
    double speed;
    char gear;
    boolean lights = true;

    //객체데이터(부품)
    Tire tire = new Tire();
    Door door;
    Handle handle;


    //생성자
    //처음 객체가 생성될 때 어떤 로직 수행, 어떤 값이 필수인지 강제하는 것
    public Car() {
        //아무 것도 없는 상태 -> 기본생성자 (디폴트)
        System.out.println("생성자가 호출되었습니다. 객체가 생성됩니다.");
    }

    //메서드
    //input : kmh
    //output : speed
    double gasPedal(double kmh, char type) {
        changeGear(type);
        speed = kmh;
        return speed;
    };

    //input : 없음
    //output : speed
    double brakePedal() {
        speed = 0;
        return speed;
    };

    //input : gear (gear type)
    //output : gear
    char changeGear(char type) {
        gear = type;
        return gear;
    };

    //input : 없음
    //output : lights
    boolean onOffLight() {
        lights = !lights;
        return lights;
    };

    //input : 없음
    //output : 없음
    void horn() {
        System.out.println("빵애에요");
    };

    //자동차의 속도
    //가변길이 매개변수
    void carSpeeds(double ... speeds) {
        for(double v : speeds) {
            System.out.println("v = " + v);
        }
    }

    public static void main(String[] args) {

    }

}

 

Main 클래스

//객체생성
//new 키워드 이용
Car car1 = new Car();
Car car2 = new Car();

//car1이 가지고 있는 주소
System.out.println(car1); //week03.Car@b4c966a

//배열 생성
Car[] carArray = new Car[3];

car1.changeGear('P');
carArray[0] = car1;

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

 

[객체지향프로그래밍]

  • 캡슐화
    • 캡슐화란 속성(필드)와 행위(메서드)를 하나로 묶어 객체로 만든 후 실제 내부 구현 내용은 외부에서 알 수 없게 감추는 것을 의미합니다.
    • 외부 객체에서는 캡슐화된 객체의 내부 구조를 알 수 없기 때문에 노출시켜 준 필드 혹은 메서드를 통해 접근할 수 있습니다.
    • 필드와 메서드를 캡슐화하여 숨기는 이유는 외부 객체에서 해당 필드와 메서드를 잘못 사용하여 객체가 변화하지 않게 하는 데 있습니다.
    • Java에서는 캡슐화된 객체의 필드와 메서드를 노출시킬지 감출지 결정하기 위해 접근 제어자를 사용합니다.
  • 상속
    • 객체지향 프로그래밍에는 부모 객체와 자식 객체가 존재합니다.
    • 부모 객체는 가지고 있는 필드와 메서드를 자식 객체에 물려주어 자식 객체가 이를 사용할 수 있도록 만들 수 있습니다.
    • 위 같은 행위를 상속이라고 할 수 있는데 상속을 하는 이유는 다음과 같습니다.
      1. 각각의 객체들을 상속 관계로 묶음으로써 객체 간의 구조를 파악하기 쉬워집니다.
      2. 필드와 메서드를 변경하는 경우 부모 객체에 있는 것만 수정하게 되면 자식 객체 전부 반영이 되기 때문에 일관성을 유지하기 좋습니다.
      3. 자식 객체가 부모 객체의 필드와 메서드를 물려받아 사용할 수 있기 때문에 코드의 중복이 줄어들며 코드의 재사용성이 증가됩니다.
  • 다형성
    • 객체가 연산을 수행할 때 하나의 행위에 대해 각 객체가 가지고 있는 고유한 특성에 따라 다른 여러 가지 형태로 재구성되는 것을 의미합니다.
    • Car 클래스를 토대로 자동차 객체를 만들 때 A자동차 객체와 B자동차 객체의 경적 소리가 다르다면 ‘경적을 울리다’라는 행위 즉, horn(); 메서드의 구현을 다르게 재정의 하여 사용할 수 있습니다.
  • 추상화
    • 객체에서 공통된 부분들을 모아 상위 개념으로 새롭게 선언하는 것을 추상화라고 합니다.
    • 공통적이고 중요한 것들을 모아 객체를 모델링 합니다.
    • 현실 세계의 여러 종류의 자동차들이 공통적으로 가지고 있는 가속, 브레이크, 속도와 같은 것들을 모아 자동차라는 객체를 모델링 할 수 있습니다.

[클래스 생성]

  1. 만들려고 하는 설계도를 선언합니다.(클래스 선언)
  2. 객체가 가지고 있어야 할 속성(필드)을 정의합니다.
  3. 객체를 생성하는 방식을 정의합니다.(생성자)
  4. 객체가 가지고 있어야 할 행위(메서드)를 정의합니다.

[객체 생성]

객체 생성 연산자인 ‘new’를 사용하면 클래스로부터 객체를 생성할 수 있다. 

//car1이 가지고 있는 주소
System.out.println(car1); //week03.Car@b4c966a

 

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

 

[객체 배열]

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

//배열 생성
Car[] carArray = new Car[3];

car1.changeGear('P');
carArray[0] = car1;

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

 

[필드]

필드는 객체의 데이터를 저장하는 역할을 합니다. 객체의 필드는 크게 고유한 데이터, 상태 데이터, 객체 데이터로 분류할 수 있습니다.

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

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

  • 우리가 클래스에 필드를 정의하여 선언했다고 해서 바로 사용할 수 있는 것은 아닙니다.
  • 클래스는 설계도일 뿐 실제로 필드의 데이터를 가지고 있는 것은 객체입니다.
  • 따라서 객체를 생성한 후에 필드를 사용할 수 있습니다.

[메서드]

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

가변 길이의 매개변수도 선언할 수 있습니다.

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

double … speeds 이렇게 … 을 사용하면 아래처럼 매개값을 , 로 구분하여 개수 상관없이 전달 가능합니다.

carSpeeds(100, 80);

carSpeeds(110, 120, 150);

 

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

  • 필드와 마찬가지로 클래스의 메서드를 정의하여 선언했다고 해서 바로 사용할 수 있는 것은 아닙니다.
  • 클래스는 설계도일 뿐 메서드는 객체의 행위를 정의한 것입니다.
  • 따라서 객체를 생성한 후에 메서드를 사용할 수 있습니다.

외부 접근 -> 도트연산자 사용 ex) car.brakePedal();

내부 접근 -> 

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

 

[오버로딩]

오버로딩 은 함수가 하나의 기능만을 구현하는 것이 아니라 하나의 메서드 이름으로 여러 기능을 구현하도록 하는 Java의 기능입니다.

 

오버로딩의 조건

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

[메서드에서 기본형 변수와 참조형 변수]

  • 기본형 매개변수
    • 메서드를 호출할 때 전달할 매개값으로 지정한 값을 메서드의 매개변수에 복사해서 전달합니다.
  • 참조형 매개변수
    • 메서드를 호출할 때 전달할 매개값으로 지정한 값의 주소를 매개변수에 복사해서 전달합니다.
    • 매개변수를 참조형으로 선언하면 값이 저장된 곳의 원본 주소를 알 수 있기 때문에 값을 읽어 오는 것은 물론 값을 변경하는 것도 가능합니다.
    • 메서드의 매개변수뿐만 아니라 반환 타입도 참조형이 될 수 있습니다.
//Car 클래스의 메서드
double brakePedal(char type) {
        speed = 0;
        type = 'P'; // 정지 후 매개변수 type을 어떤 타입으로 전달 받았는지 상관없이 'P'로 고정시키기
        changeGear(type);
        return speed;
    }
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' 출력
//Tire 클래스
public class Tire {
    String company; // 타이어 회사
    public Tire() {}
}
//Car 클래스의 setTire 메서드
Tire setTire(Tire tireCompany) {
        tireCompany.company = "KIA"; // 금호 타이어를 전달 받았지만 강제로 KIA 타이어로 교체
        tire = tireCompany;
        return tire;
    }
// 참조형 매개변수
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" 출력

company가 금호인 tire이지만 setTire 함수에 의해서 KIA로 변경되었다. 

 

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

멤버 = 필드 + 메서드

인스턴스 멤버 = 인스턴스 필드 + 인스턴스 메서드

클래스 멤버 = 클래스 필드 + 클래스 메서드

인스턴스 멤버는 객체 생성 후에 사용할 수 있고 클래스 멤버는 객체 생성 없이도 사용할 수 있습니다.

 

지금까지 학습하면서 선언한 필드와 메서드는 전부 인스턴스 멤버였습니다.

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

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

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

클래스 멤버 선언

  • 필드와 메서드를 클래스 멤버로 만들기 위해서는 static 키워드를 사용하면 됩니다.
    • 일반적으로 인스턴스마다 모두 가지고 있을 필요 없는 공용적인 데이터를 저장하는 필드는 클래스 멤버로 선언하는 것이 좋습니다.
    • 또한 인스턴스 필드를 사용하지 않고 실행되는 메서드가 존재한다면 static 키워드를 사용하여 클래스 메서드로 선언하는 것이 좋습니다.
    ⚠️ 여기서 주의할 점이 있습니다!
    • 클래스 멤버로 선언된 메서드는 인스턴스 멤버를 사용할 수 없습니다.
    • 반대로 인스턴스 멤버로 선언된 메서드는 클래스 멤버를 사용할 수 있습니다.
    • 클래스 멤버는 객체 생성 없이 바로 사용 가능하기 때문에 객체가 생성되어야 존재할 수 있는 인스턴스 멤버를 사용할 수 없습니다.
static String company = "GENESIS"; // 자동차 회사 : GENESIS

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

모든 Car 클래스의 객체마다 company 인스턴스 필드를 가지고 있을 필요 없이 클래스 필드로 만들어 공유하게 만든다면 메모리를 효율적으로 사용할 수 있습니다.

 

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

클래스 멤버를 사용하려면 클래스의 이름과 함께 도트(.) 연산자를 사용한다.

 

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

 

[지역변수]

public class Main {

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

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

 

[final]

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

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

 

[상수]

상수의 특징은 값이 반드시 한 개이며 불변의 값을 의미합니다.

따라서 인스턴스마다 상수를 저장할 필요가 없습니다.

static final String COMPANY = "GENESIS"; //static 키워드 추가

일반적으로 모두 대문자로 적는 것이 관례이다.

 

[생성자]

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

  • 모든 클래스는 반드시 생성자가 하나 이상 존재합니다.
  • 만약 클래스에 생성자를 하나도 선언하지 않았다면 컴파일러는 기본 생성자를 바이트코드 파일에 자동으로 추가시켜줍니다. 따라서 이러한 경우는 기본 생성자 생략이 가능합니다.
  • 반대로 단 하나라도 생성자가 선언되어 있다면 컴파일러는 기본 생성자를 추가하지 않습니다.

생성자는 객체를 초기화하는 역할을 수행합니다.

  • 객체를 만들 때 인스턴스마다 다른 값을 가져야 한다면 생성자를 통해서 필드를 초기화할 수 있습니다.
    • 예를 들어 만들어지는 자동차마다 모델, 색상, 가격이 다르다면 생성자를 사용하여 필드의 값을 초기화하는 것이 좋습니다.
  • 반대로 인스턴스마다 동일한 데이터를 가지는 필드는 초기값을 대입하는 것이 좋습니다.
    • 예를 들어 자동차가 만들어질 때마다 기어의 상태를 ‘P’로 고정해야 한다면 초기값을 직접 대입하는 것이 좋습니다. 

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

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

 

[this와 this()]

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

  • 객체 내부 생성자 및 메서드에서 객체 내부 멤버에 접근하기 위해 사용될 수 있습니다.
  • 객체 내부 멤버에 접근할 때 this 키워드가 필수는 아니지만 상황에 따라 필수가 될 수 있습니다.
public Car(String model, String color, double price) {
    this.model = model;
    this.color = color;
    this.price = price;
}

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

Car returnInstance() {
    return 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;
}

이렇게 생성자를 선언한다고 했을 때 코드의 중복이 발생합니다.

this()를 이용해서 코드 중복을 처리할 수 있습니다.

public Car(String model) {
    this(model, "Blue", 50000000);
}

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

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

 

[접근 제어자]

멤버 또는 클래스에 사용, 외부에서 접근하지 못하도록 제한합니다.

클래스, 멤버 변수, 메서드, 생성자에 사용되고, 지정되어 있지 않다면 default입니다.

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

생성자의 접근 제어자

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

[getter 와 setter]

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

  • 이때 외부에서 필드에 직접 접근하는 것을 막기 위해 필드에 private, default 등의 접근 제어자를 사용할 수 있습니다.
  • 그렇다면 우리는 어떻게 객체의 private 필드를 읽어오거나 저장할 수 있을까요?
  • 우리는 Getter 와 Setter를 사용하여 이를 해결할 수 있습니다.

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

public String getModel() {
    return model;
}

public String getColor() {
    return color;
}

public double getPrice() {
    return price;
}

 

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

자동차 클래스의 필드에 이처럼 private 접근 제어자로 지정한 필드가 있을 때 Setter 메서드를 통해 값을 저장하거나 수정할 수 있습니다.

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

public void setColor(String color) {
    this.color = color;
}

public void setPrice(double price) {
    this.price = price;
}

 

[패키지와 import]

패키지 == 폴더

package oop.pk1;

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

public class Car {
    public void horn() {
        System.out.println("pk2 빵빵");
    }
}
//import를 안할 시
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 할 시
package oop.main;

import oop.pk1.Car;

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

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

 

[상속]

클래스 간의 상속은 extends 키워드를 사용하여 정의할 수 있습니다.

  • 우리는 상속의 개념을 확장의 개념으로 이해해야 합니다.
  • 자칫하여 부모 클래스, 자식 클래스라는 용어에 현혹되어 ‘부모가 자식보다 큰 사람이니까 부모 클래스도 마찬가지로 자식 클래스 보다 큰 범위겠지?’라고 생각하는 순간 헷갈리기 시작합니다…

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

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

  • 상속관계 : is - a (”~은 ~(이)다”)
  • 포함관계 : has - a (”~은 ~을(를) 가지고 있다”) 
  • 상속관계는 위에서 예제로 살펴본 스포츠카와 자동차도 있지만 한 가지 더 예를 들자면 고래와 포유류가 있습니다.
  • 위 공식에 대입해 보겠습니다.
    • 상속관계 : 고래는 포유류다 👍
    • 포함관계 : 고래는 포유류를 가지고 있다…? 🤔

포함관계는 우리가 일전에 봤던 자동차와 타이어, 차 문, 핸들과의 관계라고 볼 수 있습니다.

  • 한번 위 공식에 대입해 보겠습니다.
  • 자동차는 타이어를 가지고 있다. 👍
  • 자동차는 차 문을 가지고 있다. 👍
  • 자동차는 핸들을 가지고 있다. 👍

[단일상속과 다중상속]

Java는 다중 상속을 허용하지 않습니다…

final 키워드를 클래스와 메서드에 선언하면 어떻게 될까요?

public final class Car {}

...

public class SportsCar extends Car{} // 오류가 발생합니다.

클래스에 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 clone() : 해당 객체의 복제본을 생성하여 반환함.
  • boolean equals(Object object) : 해당 객체와 전달받은 객체가 같은지 여부를 반환함.
  • Class getClass() : 해당 객체의 클래스 타입을 반환함.
  • int hashCode() : 자바에서 객체를 식별하는 정수값인 해시 코드를 반환함.
  • String toString() : 해당 객체의 정보를 문자열로 반환함. & Object 클래스에서는 클래스이름 @해쉬코드값 리턴함.
  • … 

[오버라이딩]

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

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

}

 

//자식 클래스 SportsCar 클래스
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(); // 경적, 부모클래스의 horn()호출

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

        // 오버라이딩한 brakePedal(), horn() 메서드 호출
        sportsCar.brakePedal(); //부모클래스의 brakePedal()호출
        sportsCar.horn(); //자식클래스에서 오버라이딩한 horn()호출

    }
}

 

[super와 super()]

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

// 부모 클래스 Car
String model; // 자동차 모델
String color; // 자동차 색상
double price; // 자동차 가격
// 자식 클래스 SportsCar
String model = "Ferrari"; // 자동차 모델
String color = "Red"; // 자동차 색상
double price = 300000000; // 자동차 가격
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(…)는 부모 클래스의 생성자를 호출할 수 있는 키워드입니다.

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

 

 

2. 계산기 과제 트러블 슈팅

 

문제상황

App.java

Calculator2 doCalculate = new Calculator2();
doCalculate.calculate(firstNumber, secondNumber, operator);

 

Calculator2.java

private ArrayList<Integer> resultArray = new ArrayList<Integer>();

 

App에서 매번 계산기 객체를 생성해서 리스트에 저장하고 있었다. 그래서 리스트에 값이 쌓이지 않고 서로 객체마다 각자의 리스트에 각자 저장하고 있었다. 

나는 결과 값을 담을 리스트를 인스턴스 멤버로 저장하고 있었던 셈이다.

 

<인스턴스 멤버>

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

 

<클래스 멤버>

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

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

클래스 멤버 선언

  • 필드와 메서드를 클래스 멤버로 만들기 위해서는 static 키워드를 사용하면 됩니다.
    • 일반적으로 인스턴스마다 모두 가지고 있을 필요 없는 공용적인 데이터를 저장하는 필드는 클래스 멤버로 선언하는 것이 좋습니다.
    • 또한 인스턴스 필드를 사용하지 않고 실행되는 메서드가 존재한다면 static 키워드를 사용하여 클래스 메서드로 선언하는 것이 좋습니다.

해결과정

따라서 리스트 자료구조를 다음과 같이 변경하였다.

static private ArrayList<Integer> resultArray = new ArrayList<Integer>();

 

그리고 다음과 같은 메서드를 이용해 리스트에 값이 쌓이는지 확인하였다.

public void fullGetter() {
    System.out.println(resultArray.toString());
};

 

또한 결과 값 출력을 다음 메서드로 따로 구현하였다.

public void printResult() {
    System.out.println("result is " + resultArray.get(resultArray.size()-1));
}

 

그래서 메서드를 다음과 같이 정리하고,

//method
public void calculate(int firstNumber, int secondNumber, String operator) {
    Scanner sc = new Scanner(System.in);
    if(operator.equals("+")){
        this.result = firstNumber + secondNumber;
    } else if (operator.equals("-")) {
        this.result = firstNumber - secondNumber;
    } else if (operator.equals("*")) {
        this.result = firstNumber * secondNumber;
    } else if (operator.equals("/")) {
        if(secondNumber == 0){
            System.out.println("Division by zero is impossible");
            System.out.println("first number is: " + firstNumber);
            System.out.println("rewrite the second number: ");
            secondNumber = sc.nextInt();
        }
        this.result = firstNumber / secondNumber;

    }

    resultArray.add(this.result);

};

public int getter(int index) {
    return resultArray.get(index);
};

public void setter(int index, int value) {
    resultArray.set(index, value);
};

public void printResult() {
    System.out.println("result is " + resultArray.get(resultArray.size()-1));
}

public void removeResult() {
    resultArray.remove(resultArray.remove(0));
}

public void fullGetter() {
    System.out.println(resultArray.toString());
};


public static void main(String[] args) {



}

 

메인 클래스에서 다음과 같이 사용하였다. 

Calculator2 doCalculate = new Calculator2();
doCalculate.calculate(firstNumber, secondNumber, operator);
doCalculate.fullGetter();
doCalculate.printResult();
doCalculate.removeResult();

로그를 보면 fullGetter 함수를 이용해 현재 리스트에 어떻게 담겨있는지 확이하였고 printResult함수를 이용해 현재 리스트에서 가장 최신값을 출력하도록 하였다. 그 다음에는 removeResult를 이용해 0번째 인덱스에 있는 값을 삭제하도록 하였다. 왜냐하면 이 로직대로라면 항상 리스트에는 한개의 값만 담길 것이기 때문이다.(무조건 계산한번하면 리스트에 한번 담고 한번 삭제하기 때문) 사용자에게 값을 선택하여 삭제할 수 있도록 한다면,

리스트에 값이 계속 쌓이게 하여서 출력하는 함수를 fullGetter를 이용해 리스트에 담긴 값을 모두를 보여주게 한 다음 거기서 값을 선택해서 삭제할 수 있도록 구현할 수도 있다. 

 

혹시 몰라 내가 클래스멤버와 static 키워드에 이해하고 있는 부분이 맞는지 질문 하였다. 

각 계산기 객체가 생성될때마다 새로운 리스트를 생성하는게 아닌 공용적으로 데이터를 저장하기 위해서 static 키워드 사용 > 내가 이해한 static 개념이 맞는가? > 맞았다고 해주셨다.

 

[오늘 회고]

오늘 배운 부분이 중요한 부분이고 상당히 많은걸 배운것 같은데 그래도 전부 이해하고 넘어갈 수 있어서 다행이다. 특히 계산기 과제에서 클래스멤버 문제를 스스로 해결한 부분이 뿌듯했다.

 

오늘도 앉아서 공부하기 힘들었지만, 노력에는 지름길이 없다!