기초단계/JAVA

2023.02.16-1 Java 복습

춘핑이 2023. 2. 16. 18:34

6. 클래스

6.1 객체 지향 프로그래밍

현실세계에서 어떤 제품을 만들때 부품을 먼저 만들고 부품들을 하나씩 조립해서 완성품을 만든다.
개발할때도 부품에 해당하는 객체들을 먼저만들고 이 객체들을 하나씩 조립해서 만드는 것을 객체 지향 프로그래밍이라고 한다.

6.1.1 객체란?

객체란 물리적으로 존재하거나 개념적인 것 중에서 다른 것과 식별이 가능한 것이다.
예를 들어 물리적으로 존재하는 자동차 자전거 책 사람은 물론 개념적인 학과 가으이 주문 등도 모두 객체가 될 수 있다.
객체는 속성과 동작으로 이루어진다. 사람은 이름 나이 등의 속성과 웃다 걷다 등의 동작이 있다.
자바는 이러한 속성을 '필드' 동작을 '메소드'라고 부른다.

현실 세계의 객체를 소프트웨어 객체로 설계하는 것을 객체 모델링 이라고 한다.
객체 모델링은 현실 세계의 대표 속성과 동작을 추려내어 소프트웨어 객체의 필드와 메소드로 정의하는 과정이라고 볼 수 있다.

6.1.2 객체의 상호작용

현실세계에서 일어나는 모든 현상은 객체와 객체간의 상호작용으로 이루어져잇다.
사람이 전자계산기의 기능을 이용하고 전자계산기는 계산결과를 사람에게 리턴한다.
객체지향 프로그램에서도 서로 상호작ㅇ요하면서 동작한다. 객체들 사이의 상호작용 수단은 메소드 이다.

6.1.3 객체간의 관계

객체는 단독으로 존재할 수 있지만 대부분 다른 객체와 관계를 맺고 있다.
관계의 종류에는 집합관계 사용관계 상속관계가 있다.
1.집합관계
완성품과 부품의 관계를 말한다. 예를들어 자동차는 엔진 타이어 핸들등으로 구성되므로 자동차와 부품들은 집합관계이다.
2.사용관계
다른 객체의 필드를 읽고 변경하거나 메소드를 호출하는 관계를 말한다. 예를들어 사람이 자동차에게 달린다. 멈춘다 등의 메소드를 호출하면 사람과 자동차의 관계가 사용관계라고 할 수 있다.
3.상속 관계
부모와 자식간의 관곌르 말한다. 자동차가 기계의 특징(필드 메소드)를 물려받느 다면 기계(부모) 자동차(자식)은 상속관계에 있다고 볼 수 있다.

6.1.4 객체지향프로그래밍의 특징

1.캡슐화
캡슐화란 객체의 데이터(필드), 동작(메소드)을 하나로 묶고 실제 구현 내용을 외부에서 감추는 것을 말한다.
외부 객체는 객체 내부의 구졸르 알지 못하며 객체가 노출해서제공하는 필드와 메소드만 이용할 수 있다.
필드와 메소드를 캡슐화 하는 것은 외부의 잘못된 사용으로 객체가 손상되지 않도록 하는데 있다.

2.상속
객체 지향 프로그래밍에서 부모역할의 상위 객체와 자식역할의 하위객체가있다.
부모 객체는 자기가 가지고 있는 필드와 메소드를 자식객체에게 물려주어 자식객체가 사용할 수 있도록한다.
상속을 하는 이유는 코드의 재사용성을 높이고 유지보수 시간을 최소화하기위해 사용한다

.

3.다형성
다형성이란 사용방법은 동일하지만 실행결과가 다양하게 나오는 성질을 말한다.
자동차 부품을 교환하면 성능이 다르게 나오듯이 프로그램을 구성하는 객체(부품)을 바꾸면 프로그램의 실행성능이 다르게 나올 수 있다.
다형성을 구현하기 위해서는 자동타입 변환과 재정의 기술이 필요하다. 이 기술들은 상속과 인터페이스 구현을 통해 얻어진다.

6.2 객체와 클래스

객체를 생성할때에는 설계도가 필요하다.
현실세계에서 자동차를 생성하려면 자동차의 설계도가 필요하듯이 객체 지향프로그래밍에서 객체를 생성하려면 설계도에 해당하는 클래스가 필요하다.
클래스로부터 생성된 객체를 해당 클래스의 인스턴스(instance)라고 부른다. 그리고 클래스로부터 객체를 만드는과정을 인스턴스화라고한다.
동일한 클래스로부터 여러개의 인스턴스를 만들 수 있는데 이것은 동일한 설계도로 여러 대의 자동차를 만드는 것과 동일하다.

6.3 클래스 선언

클래스 선언은 객체 생성을 위한 설계도를 작성하는 작업이다.
객체를 어떻게 생성하고(생성자) 객체가 가져야할 데이터(필드)가 무엇이고 객체의 동작(메소드)는 무엇인지를 정의하는 내용이 포함된다.

6.4 객체 생성과 클래스 변수

클래스로부터 객체를 생성하려면 객체 생성연산자인 new가 필요하다.
클래스 변수 = new 클래스()
new연산자 뒤에는 생성자 호출 코드가 오는데 클래스()형태를 가진다. new연산자는 객체를 생성시킨 후 객체의 주소를 리턴한다.

6.4.1 클래스의 두가지용도

클래스에는 두가지 용도가 있다.
1.라이브러리 클래스 : 실행할 수 없으며 다른 클래스에서 이용하는 클래스
2.실행 클래스 : 메소드를 가지고 있는 실행가능한 클래스

6.5 클래스의 구성멤버

클래스 선언에는 객체 초기화 역할을 담당하는 생성자
객체에 포함될 필드와 메소드를 선언하는 코드가 포함된다.
생성자 필드 메소드를 클래스 구성멤버라고 한다.

1.필드
필드는 객체의 데이터를 저장하는 역할을 한다.
2.생성자
생성자는 new연산자로 객체를 생성할때 객체의 초기화 역할을 담당한다.
선언형태는 메소드와 비슷하지만 리턴타입이 없고 이름은 클래스 이름이다.
3.메소드
메소드는 객체가 수행할 동작이다.
다른 언어에서는 함수라고 하기도 하는데 객체 내부의 함수는 메소드라과 부른다.
메소드는 객체와 객체간의 상호작용을 위해 호출된다.

6.6 필드 선언과 사용

필드는 객체의 데이터를 저장하는 역할을 한다. 객체의 데이터에는 고유데이터, 현재상태 데이터, 부품데이터가 있다.
자동차객체를 예로들면 제작회사, 모델, 색, 최고 속도는 고유데이터에 해당하고 현재속도 엔진회전수는 상태데이터에 해당한다.
차체 엔진 타이어는 부품에 해당한다.

.6.1 필드선언

필드를 선언하는 방법은 변수를 선언하는 방법과 동일하다. 단 반드시 클래스 블록에서 선언되어야 필드 선언이 된다.
타입 필드명 = [초기값];
필드와 (로컬)변수의 차이점
변수는 메소드블록에서 선언되며 생성자와 메소드 호출시에만 생성되고 사용된다.
필드는 클래스블록에서 선언되며 객체 내부에서 존재하고 객체 내 외부에서 사용가능하다.

타입은 필드에 저장할 데이터의 종류를 결정한다. 기본타입과 참조타입이 모두가능하다.
필드 명은 첫 문자를 소문자로 하되, 캐멀 스타일로 작성하는 것이 관례이다.
초기값을 제공하지 않을 경우 필드는 객체 생성 시 자동으로 기본값으로 초기화돤다.
정수타입 필드는 0, 실수타입 필드는 0.0, boolean은 false, 참조타입은 null로 초기화

6.6.2 필드사용

필드를 사용한다는 것은 필드 값을 읽고 변경하는 것을 말한다. 클래스에서 필드를 선언햇다고 해서 바로 사용할 수 있는 것은 아니다.
필드는 객체의 데이터이므로 객체가 존재하지 않으면 필드도 존재하지 않는다.
클래스로 부터 객체가 생성된 후에 필드를 사용할 수 있다.필드 객체 내부의 생성자와 메소드내부에서 사용할 수 잇고 외부에서 접근해서 사용할 수도 있다.
객체 내부에서는 단순히 필드명으로 읽고 변경할 수 있지만 외부객체에서는 참조변수외 도트(.)연산자를 이용해서 필드를 읽고 변경해야한다.
도트(.)는 객체 접근 연산자로 객체가 가지고 있는 필드나 메소드에 접근하고자 할때 참조변수 뒤에 붙인다.

public class Car {
    //필드선언
    String company = "현대자동차";
    String model = "그랜져";
    String color = "검정";
    int maxSpeed = 350;
    int speed;
}

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

        //Car객체의 필드값 읽기
        System.out.println("제작회사: " + myCar.company); //현대자동차
        System.out.println("모델명: " + myCar.model); //그랜져
        System.out.println("색깔: " + myCar.color); //검정
        System.out.println("최고속도: " + myCar.maxSpeed); //350
        System.out.println("현재속도: " + myCar.speed); //0

        //Car 객체의 필드값 변경
        myCar.speed = 60;
        System.out.println("현재속도: " + myCar.speed); //60
    }
}

6.7 생성자 선언과 호출

new 연산자는 객체를 생성한 후 연이어 생성자를 호출해서 객체를 초기화하는 역할을 한다.
객체 초기화란 필드 초기화를 하거나 메소드를 호출해서 객체를 사용할 준비를 하는 것을 말한다.
클래스 변수 = new 클래스();
생성자가 성공적으로 실행이 끝나면 new연산자는 객체의 주소를 리턴한다. 리턴된 주소는 클래스 변수에 대입되어 객체의 필드나 메소드를 접근할대 이용된다.

6.7.1 기본 생성자

모든 클래스는 생성자가 존재하며 하나 이상을 가질 수 있다. 클래스에 생성자 선언이 없으면 컴파일러는 기본 생성자를 자동으로 추기시킨다.
[public] 클래스() {}

6.7.2 생성자 선언

객체를 다양하게 초기화하기 위해 개발자는 생성자를 직접 선언할 수 있다.
클래스(매개변수...) {
//객체의 초기화코드
}

생성자는 메소드와 비슷한 모양을 가지고 있으나 리턴 타입이 없고 클래스 이름과 동일하다.
매개변수는 new연산자로 생성자를 호출할 때 매개값을 생성자 블록 내부로 전달하는 역할을 한다.
매개값을 순서대로 대입받기 위해서는 생성자에도 순서대로 작성해야한다.

public class Car {
    //생성자 선언
    Car(String model, String color, int maxSpeed) {
    }
}

public class CarExample {
    public static void main(String[] args) {
        Car myCar = new Car("그랜저", "검정", 250);
        //기본생성자는 호출 못함
    }
}

6.7.3 필드 초기화

객체마다 동일한 값을 가지고 있다면 필드 선언시 초기값을 대입하는 것이 좋고
객체마다 다른 값을 가져야한다면 생성자에서 필드를 초기화하는 것이 좋다.
예를들어 Korean클래스를 선언한다고 가정해보자.
한국인이므로 nation(국가)은 대한민국으로 동일한 값을 갖지만 name(이름) ssn(주민등록번호)는 한국인마다 다르므로 생성자에서 초기화하는 것이 좋다.

public class Korean {
    // 필드선언
    String nation = "대한민국";
    String name;
    String ssn;

    // 생성자 선언
    public Korean(String n, String s) {
        name = n;
        ssn = s;
    }
}

public class KoreanExample {
    public static void main(String[] args) {
        // Korean 객체 생성
        Korean k1 = new Korean("박자바", "011225-1234567");
        // Korean객체 데이터 읽기
        System.out.println("k1.nation : " + k1.nation); //대한민국
        System.out.println("k1.name : " + k1.name); //박자바
        System.out.println("k1.ssn : " + k1.ssn); //011225-1234567
        System.out.println();

        // Korean 객체 생성
        Korean k2 = new Korean("김자바", "930525-0654321");
        // 또 다른 Korean객체 데이터 읽기
        System.out.println("k2.nation : " + k2.nation); //대한민국
        System.out.println("k2.name : " + k2.name); //김자바
        System.out.println("k2.ssn : " + k2.ssn); //930525-0654321
        System.out.println();
    }
}

위 예제의 Kroean생성자를 보면 매개변수 이름으로 각각 n과 s를 사용했다.
매개변수의 이름이 너무 짧으면 가동성이 좋지 않기때문에 가능하면 초기화 시킬 필드명과 도일한 이름을 사용하는 것이 좋다.
그러면 매개변수 명이 필드명과 동일하기 때문에 필드임을 구분하기 위해 this키워드를 필드명 앞에 붙여줘야한다.
this는 현재 객체를 말하며 this.name은 현재 객체의 데이터(필드)로서의 name을 뜻한다.

public class Korean {
    // 필드선언
    String nation = "대한민국";
    String name;
    String ssn;

    // 생성자 선언
    public Korean(String name, String ssn) {
        this.name = name;
        this.ssn = ssn;
    }
}

6.7.4 생성자 오버로딩

매개값으로 객체의 필드를 다양하게 초기화할면 생성자 오버로딩이 필요하다.
생성자 오버로딩이란 매개변수를 달리하는 생성자를 여러개 선언하는 것을 말한다.

매개변수 타입과 개수 그리고 선언된 순서가 똑같을 경우 매개변수 이름만 바꾸는 것은 생성자 오버로딩이 아니다.
Car(String model, String color){}
Car(String color, String model){} //오버로딩이 아님, 컴파일 에러 발생

생성자 오버로딩되어 있을 경우 new연산자로 생성자를 호출할때 제공되는 매개값의 타입과 수에 따라 실행될 생성자가 결정된다.

public class Car {
    // 필드선언
    String company = "현대자동차";
    String model;
    String color;
    int maxSpeed;

    // 생성자 선언
    Car() {
    }

    Car(String model) {
        this.model = model;
    }

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

    Car(String model, String color, int maxSpeed) {
        this.model = model;
        this.color = color;
        this.maxSpeed = maxSpeed;
    }
}

public class CarExample {
    public static void main(String[] args) {
        Car car1 = new Car();
        System.out.println("car1.company : " + car1.company);
        System.out.println();

        Car car2 = new Car("자가용");
        System.out.println("car2.company : " + car2.company);
        System.out.println("car2.model : " + car2.model);
        System.out.println();

        Car car3 = new Car("자가용", "빨강");
        System.out.println("car3.company : " + car3.company);
        System.out.println("car3.model : " + car3.model);
        System.out.println("car3.color : " + car3.color);
        System.out.println();

        Car car4 = new Car("자가용", "빨강", 200);
        System.out.println("car4.company : " + car4.company);
        System.out.println("car4.model : " + car4.model);
        System.out.println("car4.color : " + car4.color);
        System.out.println("car4.maxSpeed : " + car4.maxSpeed);
        System.out.println();
    }
}

6.7.5 다른 생성자 호출

생성자 오버로딩이 많아질 경우 생성자 간의 중복된 코드가 발생할 수 있다.
매개변수의 수만 달리하고 필드 초기화 내용이 비슷한 생성자에서 이런 중복코드를 많이 볼 수 있다.
이 경우에는 공통코들르 한 생성자에만 집중적으로 작성하고 나머지는 this(...)를 사용하여 공통코드를 가지고 있는 생성자를 호출하는 방법으로 개선할 수있다.
this(매개값, ...)은 생성자의 첫 줄에 작성되며 다른 생성자를 호출하는 역할을 한다.
호출하고 싶은 생성자의 매개변수에 맞게 매개값을 제공하면 된다.
this()다음에는 추가적인 실행문을 작성할 수 있는데 호출되는 생성자의 실행이 끝나면 원래 생성자로 돌아와서 다음 실행문을 실행한다.

6.8 메소드 선언과 호출

메소드 선언은 객체의 동작을 실행 블록으로 정의하는 것을 말한다.
메소드 호출은 실행블록을 실제로 실행하는 것을 말한다.
메소드는 객체 내부에서도 호출되지만 다른 객체에서도 호출 될 수 있기때문에 객체간의 상호작용하는 방법을 정의하는 것이라고 볼 수 있다.

6.8.1 메소드선언

리턴타입 메소드명 (매개변수){실행블록}
1.리턴타입
리턴타입은 메소드가 실행 한후 호출한 곳으로 전달하는 결과 값의 타입을 말한다. 리턴갑이 없는 메소드는 void로 작성한다.
리턴타입이 있는 메소드는 실행블록 안에서 return문으로 리턴값을 반드시 지정해야한다.
2.메소드명
메소드명은 첫 문자를 소문자로 시작하고 캐멀스타일로 작성한다.
3.매개변수
매개변수는 메소드를 호출할때 전달한 매개값을 받기 위해 사용된다.
4.실행블록
메소드호출시 실행되는 부분이다.

public class Calculator {
    // 리턴값이 없는 메소드 선언
    void powerOn() {
        System.out.println("전원을 켭니다.");
    }

    // 리턴값이 없는 메소드 선언
    void powerOff() {
        System.out.println("전원을 끕니다.");
    }

    // 호출시 두 정수 값을 전달 받고
    // 호출한 곳으로 결과값 int를 리턴하는 메소드 선언
    int plus(int x, int y) {
        int result = x + y;
        return result; // 리턴값 지정
    }

    // 호출시 두 정수 값을 전달 받고
    // 호출한 곳으로 결과값 double 리턴하는 메소드 선언
    double divide(int x, int y) {
        double result = (double) x / (double) y;
        return result; // 리턴값 지정
    }
}

6.8.2 메소드 호출

메소드를 호출한다는 것은 메소드 블록을 실행하는 것을 말한다.
클래스에서 메소드를 선언했다고 해서 바로 호출할 수 있는 것은 아니다.
메소드는 객체의 동작이므로 객체가 존재하지 않으면 호출할 수 없다.
클래스로부터 객체가 생성된 후 메소드는 생성자와 다른 메소드 내부에서 호출될 수 있고 객체 외부에서도 호출될 수 있다.
메소드에 리턴값이 있을 경우에 리턴값을 변수에 저장할 수 있다.
변수타입은 메소드의 리턴타입과 동일하거나 자동타입변환될 수 있어야한다.

public class CalculatorExample {
    public static void main(String[] args) {
        // 객체생성
        Calculator myCalc = new Calculator();

        // 리턴값이 없는 powerOn() 메소드 호출
        myCalc.powerOn(); // 전원을 켭니다.

        // plus() 메소드호출 5,6 매개값제공 덧셈결과 리턴받아 변수에대입
        int result1 = myCalc.plus(5, 6);
        System.out.println("result1: " + result1); // 11

        int x = 10;
        int y = 4;
        // divie()메소드 호출 x,y를 매개값제공 리턴값을 대입
        double result2 = myCalc.divide(x, y);
        System.out.println("result2: " + result2); // 2.5

        // 리턴값이 없는 powerOff() 메소드 호출
        myCalc.powerOff(); // 전원을 끕니다.
    }
}

6.8.3 가변길이 매개변수

메소드를 호출할때 매개변수의 개수에 맞게 매개값을 제공해야한다.
만약 메소드가 가변길이 매개변수를 가지고 있다면 매개변수의 개수와 상관없이 매개값을 줄 수 있다.
가변길이 매개변수는 int sum(int ... values){}의 형식으로 선언한다.
매개값들은 자동으로 배열항목으로 변환되어 메소드에서 사용된다. 그렇기 때문에 메소드 호출시 직접 배열을 매개값으로 제공해도된다.

public class ComputerExample {
    public static void main(String[] args) {
        // 객체생성
        Computer myCom = new Computer();

        // sum() 메소드 호출 시 매개값 1,2,3제공
        int result1 = myCom.sum(1, 2, 3);
        System.out.println("result1: " + result1); //6

        // 1,2,3,4,5 매개값제공
        int result2 = myCom.sum(1, 2, 3, 4, 5);
        System.out.println("result2: " + result2); //15

        // 배열 제공
        int[] values = { 1, 2, 3, 4, 5 };
        int result3 = myCom.sum(values);
        System.out.println("result3: " + result3); //15

        //생성자에 직접 배열생성해서 제공
        int result4 = myCom.sum(new int[] { 1, 2, 3, 4, 5 });
        System.out.println("result4: " + result4); //15
    }
}

6.8.4 return문

return문은 메소드의 실행을 강제 종료하고 호출한 곳으로 돌아간다는 의미이다.
메소드 선언에 리턴타입이 있을 경우에 반드시 리턴값을 추가로 지정해야한다.
return문 이후에 실행문을 작성하면 Unreachable code라는 컴파일 에러가 발생한다.
return문 이후의 실행문은 결코 실행되지 않기 때문이다.

6.8.5 메소드 오버로딩

메소드 오버로딩은 메소드 이름은 같되 매개변수의 타입, 개수, 순서가 다른 메소드를 여러개 선언하는 것을 말한다.
생성자 오버로딩과 비슷하다.

public class CalculatorExample {
    public static void main(String[] args) {
        // 객체생성
        Calculator myCalcu = new Calculator();

        // 정사각형의 넓이 구하기
        double result1 = myCalcu.areaRectangle(10);

        // 직사각형의 넓이 구하기
        double result2 = myCalcu.areaRectangle(10, 20);

        System.out.println("정사각형 넓이=" + result1); //100.0
        System.out.println("직사각형 넓이=" + result2); //200.0
    }
}

6.9 인스턴스 멤버

필드와 메소드는 선언 방법에 따라 인스턴스 멤버와 정적 멤버로 분류할 수 있다.
인스턴스 멤버로 선언되면 객체 생성후 사용할 수 있고 정적 멤버로 선언되면 객체 생성 없이도 사용할 수 있다.
인스턴스(instance)멤버 | 객체에 소속된멤버(객체 생성해야하만 사용가능)
정적(static)멤버 | 클래스에 고정된 멤버(객체 없이도 사용할 수 있는 멤버)

6.9.1 인스턴스 멤버 선언 및 사용

인스턴스 멤버란 객체에 소속된 멤버를 말한다. 따라서 객체가 있어야만 사용할 수 있는 멤버다.
이전 장들에서 지금까지 선언한 필드와 메소드들이 인스턴스 멤버엿다.
참조변수로 접근해서 사용하는 것을 의미한다.
메소드는 코드의 덩어리이므로 객체마다 저장하면 중복 저장으로 인해 메모리 효율이 떨어진다.
메소드 코드는 메소드 영역에 두되 공유해서 사용하고 객체 없이는 사용하지 못하도록 제한을 걸어둔 것이다.

6.9.2 this키워드

객체 내부에서는 인스턴스 멤버에 접근하기 위해서 this를 사용할 수 있다.
우리가 자신을 '나'라고 하듯이 객체는 자신을 'this'라고 한다.
매개변수명이 인스턴스멤버인 필드명과 동일한 경우 인스턴스 필드임을 강조하고자할 때 this를 주로 사용한다.

6.10 정적 멤버

자바는 클래스 로더를 이용해서 클래스를 메소드 영역에 저장하고 사용한다.
정적멤버란 메소드 영역의 클래스에 고정적으로 위치하는 멤버를 말한다. 객체 생성할 필요없이 클래스를 통해 바로 사용이 가능하다.

6.10.1 정적멤버 선언

필드와 메소드는 모두 정적 멤버가 될 수 있다.
정적 필드와 정적 메소드로 선언하려면 static키워드를 추가하면된다.

객체마다 가지고 있을 필요성이 없는 공용적인 필드는 정적 필드로 선언하는 것이 좋다.
예를들어 원의 넓이나 둘레를 구할때 필요한 파이는 객체마다 가지고 잇을 필요가 없어서 정적필드로 선언하는 것이 좋다.

인스턴스 필드를 이용하지 않는 메소드는 정적 메소드로 선언하는 것이 좋다.
이전 예제에서 plus()메소드는 외부에서 주어진 매개값을 가지고 처리하므로 정적메소드를 선언하는 것이 좋다.

6.10.2 정적 멤버 사용

클래스가 메모리로 로딩되면 정적 멤버를 바로 사용할 수 있는데 클래스 이름과 함께 도트(.)연산자로 접근하면된다.
정적요소는 클래스 이름으로 접근하는 것이 정석이고 정적멤버를 참조변수로 접근햇을 경우 경고 표시를 낸다.

public class Calculator {
    static double pi = 3.14159;

    static int plus(int x, int y) {
        return x + y;
    }

    static int minus(int x, int y) {
        return x - y;
    }
}

public class CalculatorExample {
    public static void main(String[] args) {
        double result1 = 10 * 10 * Calculator.pi;
        int result2 = Calculator.plus(10, 5);
        int result3 = Calculator.minus(10, 5);

        System.out.println("result1 : " + result1); // 314.159
        System.out.println("result2 : " + result2); // 15
        System.out.println("result3 : " + result3); // 5
    }
}

6.10.3 정적 블록

정적 필드는 필드 선언과 동시에 초기값을 주는 것이 일반적이다.
하지만 복잡한 초기화 작업이 필요하다면 정적블록을 이용해야한다.
정적블록은 클래스가 메모리로 로딩될 때 자동으로 실행된다.
정적블록이 클래스 내부에 여러개가 선언되어있을 경우에는 선언된 순서대로 실행된다.

public class Television {
    static String company = "MyCompany";
    static String model = "LCD";
    static String info;

    static {
        info = company + "-" + model;
    }
}

public class TelevisionExample {
    public static void main(String[] args) {
        System.out.println(Television.info);//MyCompany-LCD
    }
}

6.10.4 인스턴스 멤버 사용 불가

정적메소드와 정적블록은 객체가 없어도 실행되기 때문에 내부에서 인스턴스 필드나 인스턴스 메소드를 사용할 수없다.
또한 객체 자신의 참조인 this도 사용할 수 없다.
정적메소드와 정적블록에서 인스턴스 멤버를 사용하고 싶다면 객체를 먼저 생성하고 참조변수로 접근해야한다.
main()메소드도 동일한 규칙이 적용된다. main()메소드도 정적메소드 이므로 객체 생성없이 인스턴스 필드와 인스턴스 메소드를 main()메소드에서 바로 사용할 수 없다.

public class Car {
    // 인스턴스 필드선언
    int speed;

    // 인스턴스 메소드선언
    void run() {
        System.out.println(speed + "으로 달립니다.");
    }

    static void simulate() {
        // 객체 생성
        Car myCar = new Car();
        // 인스턴스 멤버 사용
        myCar.speed = 200;
        myCar.run();
    }

    public static void main(String[] args) {
        // 정적 메소드 호출
        simulate(); // 다른 Class라면 Car.simulate();
        // 200으로 달립니다.

        // 객체 생성
        Car myCar = new Car();
        // 인스턴스 멤버 사용
        myCar.speed = 60;
        myCar.run();
        // 60으로 달립니다.
    }
}

6.11 final 필드와 상수

6.11.1 final 필드 선언

final은 최종적이라는 뜻을 가지고 있다.
final필드는 초기값이 저장되면 이것이 최정적인 값이되어 프로그램 실행 도중에 수정할 없다.
final필드에 포기값을 줄 수 있는 방법은 두가지 뿐이다.
1.필드 선언시에 초기값 대입
2.생성자에서 초기값 대입
고정된 값이라면 필드 선언시에 주는 것이 제일 간단하다.
하지만 복잡한 초기화 코드가 필요하거나 객체 생성시 외부에서 전달된 값으로 초기화 한다면 생성자에서 해야한다.

public class Korean {
    //인스턴스 final필드선언
    final String nation = "대한민국";
    final String ssn;

    //인스턴스 필드선언
    String name;

    //생성자 선언
    public Korean(String ssn, String name) {
        this.ssn = ssn;
        this.name = name;
    }
}

public class KoreanExample {
    public static void main(String[] args) {
        // 객체 생성 시 주민등록번호와 이름 전달
        Korean k1 = new Korean("123456-1234567", "김자바");

        // 필드값 읽기
        System.out.println(k1.nation);
        System.out.println(k1.ssn);
        System.out.println(k1.name);

        // Final필드는 값을 변경할 수 없음.
        // k1.nation = "USA";
        // k1.ssn = "123-12-1234";

        //비 final필드는 값 변경가능
        k1.name = "김자바";
    }
}

6.11.2 상수 선언

우리 주변에는 불변의 값이 잇다. 불변의 값은 수학에서 사용되는 원주율 파이나 지구의 무게 및 둘레 등이 해당된다.
이런 불변의 값을 저장하는 필드를 자바에서는 상수 라고 한다.
상수는 객체마다 저장할 필요가 없고 여러개의 값을 가져도 안되서 static이면서 final인 특성을 가져야한다.
상수 이름은 모두 대문자로 작성하는 것이 관례이다.
만약 서로 다른 단어가 혼합된 이름이라면 언더바(_)로 단어들을 연결한다.

public class Earth {
    // 상수선언및 초기화
    static final double EARTH_RADIUS = 6400;

    // 상수선언
    static final double EARTH_SURFACE_AREA;

    // 정적블록에서 상수 초기화
    static {
        EARTH_SURFACE_AREA = 4 * Math.PI * EARTH_RADIUS * EARTH_RADIUS;
        // Math.PI는 자바에서 제공하는 상수이다.
    }
}

public class EarthExample {
    public static void main(String[] args) {
        //상수읽기
        System.out.println("지구의 반지름: "  + Earth.EARTH_RADIUS + "km");
        System.out.println("지구의 표면적: "  + Earth.EARTH_SURFACE_AREA+ "km^2");
    }
}

6.12 패키지

우리는 지금까지 장별 ㅈ러별 에제 클래스를 패키지 안에 생성해서 관리했다.
자바의 패키지는 단순히 디렉토리만을 의미하지 않는다.
패키지는 클래스의 일부분이며 클래스를 식별하는 용도로 사용된다.

패키지는 주로 개발회사의 도메인 이름의 역순으로 만든다.
예를들어 mycompany.com회사의 패키지는 com.mycompany로 yourcompany.com은 com.yourcompany로 만든다.
이렇게 하면 두 회사에서 개발한 Car클래스가 있을 경우 다르게 관리 할 수 있다.
패키지는 상위 패키지와 하위패키지를 도트(.)으로 구분한다. 도트는 물리적으로 하위 디렉토리임을 뜻한다.

패키지는 클래스를 식별하는 용도로 사용되기 때문에 클래스의 전체이름에 포함된다.
com.mycompany.Car클래스와 com.yourcompany.Car는 다를 클래스이다.

패키지에 속한 바이트 코드파일(~.class)는 따로 떼어내어 다른 디렉토리로 이동할 수 없다.

6.12.1 패키지 선언

패키지 디렉토리는 클래스를 컴파일하는 과정에서 자동으로 생성된다.
컴파일러는 클래스의 패키지 선언을 보고 디렉토리를 자동 생성시킨다.
패키지 선언은 package키워드와 함께 패키지 이름을 기술한 것으로 항상 소스파일 최상단에 위치해야한다.

패키지이름은 모두 소문자로 작성하는 것이 관례이다.
패키지 이름이 서로 중복되지 않도록 회사도메인 이름의 역순으로 작성하고
마지막에는 프로젝트 이름을 붙여주는 것이 일반적이다.

이클립스에서는 패키지를 먼저 생성하고 클래스를 나중에 추가하는 방식을 사용한다.
소스팡리이 저장되면 이클립스는 자동적으로 컴파일해서 bin디렉토리에 패키지 디렉토리와 함께 바이트코드파일(.class)를 생성한다.
패키지선언이 없다면 default package에 포함시킨다.

6.12.2 import문

같은 패키지에 있는 클래스는 아무런 조건 없이 사용할 수 있지만 다른 패키지에 있는 클래스를 사용하려면
import문을 이용해서 어떤 패키지의 클래스를 사용하는지 명시해야한다.

import문은 패키지선언과 클래스 선언 사이에 작성된다.
import 키워드 뒤에는 사용하고자하는 클래스의 전체이름을 기술한다.
만약 동일한 패키지에 포함된 다수의 클래스를 사용해야한다면 클래스 이름을 생략하고 * 을 사용할 수 있다.
import문은 하위패키지를 포함하지 않는다.

서로다른 패키지에 동일한 클래스이름이 존재하는데 두패키지를 모두 import하고 사용하려면 정확히 어떤 패키지의 것을 사용하는지 명시해야한다.

package Ch06.sec12.hyndai;

import Ch06.sec12.hankook.SnowTire;
import Ch06.sec12.kumho.AllSeasonTire;

public class Car {

    Ch06.sec12.hankook.Tire tire1 = new Ch06.sec12.hankook.Tire();
    Ch06.sec12.kumho.Tire tire2 = new Ch06.sec12.kumho.Tire();

    SnowTire tire3 = new SnowTire();
    AllSeasonTire tire4 = new AllSeasonTire();
}

6.13 접근제한자

경우에 따라서 객체의 필드를 외부에서 변경하거나 메소드를 호출할 수없도록 막아야할 필요가 있다.
중요한 필드와 메소드가 외부로 노출되지 않도록 해 객체의 무결성을 유지하기 위해서 이다.

자바는 이런 기능을 구현하기 위해 접근제한자를 사용한다. 접근제한자는 public, protected, private 세가지가 있다.
접근제한자 | 제한대상 | 제한범위
public | 클래스 필드 생성자 메소드 | 없음
protected | 필드 생성자 메소드 | 같은패키지이거나 자식객체만 사용가능(상속)
(defalut) | 클래스 필드 생성자 메소드 | 같은 패키지
private | 필드 생성자 메소드 | 객체 내부

6.13.1 클래스의 접근 제한

클래스를 어디에서나 사용할 수 있는 것은 아니다. 클래스가 어떤 접근 제한을 갖느냐에 따라 사용가능 여부가 결정된다.
클래스를 선언할때 public 혹은 default접근제한을 붙일 수 있다.

6.13.2 생성자의 접근제한

생성자가 어떤 접근제한을 갖느냐에 따라 호출 가능 여부가 결정된다.

6.13.3 필드와 메소드의 접근제한

호출 여부를 결정할 수 있다.

6.14 Getter와 Setter

객체의 필드(데이터)를 외부에서 마음대로 읽고 변경할 경우 객체의 무결성(결점이 없는 성질)이 깨질 수 있다.
예를들어 자동차의 속력은 음수가 될 수 없는데 외부에서 음수로 변경하면 객체의 무결성이 깨진다.

이러한 문제점 때문에 객체 지향 프로그래밍에서는 직접적인 외부에서의 필드 접근을 막고 대신 메소드를 통해 필드에 접근하는 것을 선호한다.
그 이유는 메소드는 데이터를 검증해서 유효한 값만 필드에 저장할 수 있기 때문이다. 이러한 역할을 하는 메소드가 Setter이다.

외부에서 객체의 필드를 읽을 때에도 메소드가 필요한 경우가 있다.
필드값이 객체 외부에서 사용되기에 부적절한 경우 메소드로 적절한 값으로 변환해서 리턴할 수 있기 때문이다.
이러한 역할을 하는 메소드가 Getter이다.

필트 타입이 boolean일 경우 Getter는 get으로 시작하지 않고 is로 시작ㄴ하는 것이 관례이다.

public class Car {
    // 필드 선언
    private int speed;
    private boolean stop;

    // speed 필드의 Getter/Settert 선언
    public int getSpeed() {
        return speed;
    }

    public void setSpeed(int speed) {
        if (speed < 0) {
            this.speed = 0;
            return;
        } else {
            this.speed = speed;
        }

    }

    public boolean isStop() {
        return stop;
    }

    public void setStop(boolean stop) {
        this.stop = stop;
        if (stop == true) {
            this.speed = 0;
        }
    }
}

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

        //잘못된 속도 변경
        myCar.setSpeed(-50);
        System.out.println("현재속도: " + myCar.getSpeed()); //0

        //올바른 속도 변경
        myCar.setSpeed(60);
        System.out.println("현재속도: " + myCar.getSpeed()); //60

        //멈춤
        if(!myCar.isStop()) {
            myCar.setStop(true);
        }
        System.out.println("현재속도: " + myCar.getSpeed()); //0ㄴ
    }
}

6.15 싱글톤 패턴

애플리케이션 전체에서 단 한개의 객체만 생성해서 사용하고 싶다면 싱글톤 패턴을 적용할 수 있다.
싱글톤 패턴의 핵심은 생성자를 private 접근제한해서 외부에서 new 연산자로 생성자를 호출할 수 없도록 막는 것이다.

생성자를 호출 할 수 없으니 외부에서 마음대로 객체를 생성하는 것이 불가능해진다.
대신 싱글톤 패턴이 제공하는 정적 메소드를 통해 간접적으로 객체를 얻을 수 있다.

1.자신의 타입으로 정적필드를 선언하고 미리 객체를 생성해서 초기화시킨다.
그리고 private접근 제한자를 붙여 외부에서 정적 필드값을 변경하지 못하도록 막는다.
2.정적필드값을 리턴하는 getInstance() 정적 메소드를 public으로 선언한다.
외부에서 이 객체를 얻는 유일한 방법은 getInstace()메소드를 호출하는 것이다.
getInstace()메소드가 리턴하는 객체는 정적 필드가 참조하는 싱글톤 객체이다.

왜 쓰나?
먼저, 객체를 생성할 때마다 메모리 영역을 할당받아야 한다. 하지만 한번의 new를 통해 객체를 생성한다면 메모리 낭비를 방지할 수 있다.
또한 싱글톤으로 구현한 인스턴스는 '전역'이므로, 다른 클래스의 인스턴스들이 데이터를 공유하는 것이 가능한 장점이 있다.

많이 사용하는 경우가 언제인가요?
주로 공통된 객체를 여러개 생성해서 사용해야하는 상황

데이터베이스에서 커넥션풀, 스레드풀, 캐시, 로그 기록 객체 등
안드로이드 앱 : 각 액티비티 들이나, 클래스마다 주요 클래스들을 하나하나 전달하는게 번거롭기 때문에 싱글톤 클래스를 만들어 어디서든 접근하도록 설계

또한 인스턴스가 절대적으로 한 개만 존재하는 것을 보증하고 싶을 때 사용

단점
객체 지향 설계 원칙 중에 개방-폐쇄 원칙이란 것이 존재한다.
만약 싱글톤 인스턴스가 혼자 너무 많은 일을 하거나 많은 데이터를 공유시키면 다른 클래스들 간의 결합도가 높아지게 되는데 이때 개방-폐쇄 원칙이 위배된다.
결합도가 높아지게 되면 유지보수가 힘들고 테스트도 원활하게 진행할 수 없는 문제점이 발생한다.
멀티 스레드 환경에서 동기화 처리를 하지 않았을 때, 인스턴스가 2개가 생성되는 문제도 발생할 수 있다.
따라서 반드시 싱글톤이 필요한 상황이 아니면 쓰지말자. (설계 자체에서 싱글톤 활용을 원활하게 할 자신이 있으면 괜찮음)
https://gyoogle.dev/blog/design-pattern/Singleton%20Pattern.html참조

public class Singleton {
    // private 접근 권한을 갖는 정적필드 선언과 초기화
    private static Singleton singleton = new Singleton();

    // private 접근권한을 갖는 생성자 선언
    private Singleton() {
    }

    //public 접근 권한을 갖는 정적 메소드 선언
    public static Singleton getInstance() {
        return singleton;
    }
}

public class SingletonExample {
    public static void main(String[] args) {
        /*
        Singleton obj1 = new Singleton(); 컴파일에런
        Singleton obj2 = new Singleton(); 컴파일에러
        */

        //정적 메소드를 호출해서 싱글톤 객체를 얻음
        Singleton obj1 = Singleton.getInstance();
        Singleton obj2 = Singleton.getInstance();

        if (obj1 == obj2) {
            System.out.println("같은 Singleton객체입니다.");
        } else {
            System.out.println("다른 Singleton객체입니다.");
        }
        //같은 Singleton객체입니다.
    }
}

6.16 연습문제

계좌관리 시스템
public class Account {

    private String ano;
    private String owner;
    private int balance;

    public Account(String ano, String owner, int balance) {
        this.ano = ano;
        this.owner = owner;
        this.balance = balance;
    }

    public String getAno() {
        return ano;
    }

    public void setAno(String ano) {
        this.ano = ano;
    }

    public String getOwner() {
        return owner;
    }

    public void setOwner(String owner) {
        this.owner = owner;
    }

    public int getBalance() {
        return balance;
    }

    public void setBalance(int balance) {
        this.balance = balance;
    }
}

public class BankApplication {
    private static Account[] accountArray = new Account[100];

    public static void main(String[] args) {

        while (true) {

            int menu = mainmenu();

            switch (menu) {
            case 1:
                createAccount();
                break;
            case 2:
                AccountList();
                break;
            case 3:
                depoist();
                break;
            case 4:
                withdraw();
                break;
            case 5:
                quit();
                break;
            }
        }
    }

    public static int mainmenu() {
        Scanner sc = new Scanner(System.in);

        System.out.println("--------------------------------------------------");
        System.out.println("1.계좌생성 | 2.계좌목록 | 3.예금 | 4.출금 | 5.종료");
        System.out.println("--------------------------------------------------");
        System.out.print("선택> ");
        int menu = Integer.parseInt(sc.nextLine());

        return menu;
    }

    public static void createAccount() {
        Scanner sc = new Scanner(System.in);

        System.out.println("--------");
        System.out.println("계좌생성");
        System.out.println("--------");

        System.out.print("계좌번호: ");
        String ano = sc.nextLine();

        System.out.print("계좌주: ");
        String owner = sc.nextLine();

        System.out.print("초기입금액: ");
        int balance = Integer.parseInt(sc.nextLine());

        Account newAccount = new Account(ano, owner, balance);
        for (int i = 0; i < accountArray.length ; i++) {
            if(accountArray[i] == null) {
                 accountArray[i] = newAccount;
                 System.out.println("결과: 계좌가 생성되었습니다.");
                 break;
            }
        }
    }

    public static void AccountList() {
        System.out.println("--------");
        System.out.println("계좌목록");
        System.out.println("--------");
        for (int i = 0; i < accountArray.length; i++) {
            Account account = accountArray[i];
            if (account != null) {
                String ano = account.getAno();
                String owner = account.getOwner();
                int balance = account.getBalance();
                System.out.printf("%s     %s     %d\n", ano, owner, balance);
            }
        }
    }

    public static void depoist() {
        Scanner sc = new Scanner(System.in);
        System.out.println("--------");
        System.out.println("예금");
        System.out.println("--------");
        System.out.print("계좌번호: ");
        String ano = sc.nextLine();
        System.out.print("예금액: ");
        int balance = Integer.parseInt(sc.nextLine());

        Account account = findAno(ano);
        if (account == null) {
            System.out.println("계좌가 없습니다.");
            return;
        } else {
            account.setBalance(account.getBalance() + balance);
            System.out.println("예금되었습니다.");
        }
    }

    public static void withdraw() {
        Scanner sc = new Scanner(System.in);
        System.out.println("--------");
        System.out.println("출금");
        System.out.println("--------");
        System.out.print("계좌번호: ");
        String ano = sc.nextLine();
        System.out.print("출금액: ");
        int money = Integer.parseInt(sc.nextLine());

        Account account = findAno(ano);
        if (account == null) {
            System.out.println("계좌가 없습니다.");
            return;
        } else {
            int balance = account.getBalance() - money;
            if( balance < 0) {
                System.out.println("잔액이 부족합니다.");
                return;
            } else {
                account.setBalance(balance);
                System.out.println("출금되었습니다.");
            }
        }
    }
    public static void quit() {
        System.out.println("프로그램 종료");
        System.exit(0);
    }

    public static Account findAno(String ano) {
        Account account = null;
        for (int i = 0; i < accountArray.length; i++) {
            if (accountArray[i] != null) {
                String dbAno = accountArray[i].getAno();
                if (dbAno.equals(ano)) {
                    account = accountArray[i];
                    break;
                }
            }
        }
        return account;
    }
}

2023.02.16 후기

과거에는 이해가 안됫는데 이해가 간다.
특히 연습문제의 findAno를 이해 못햇엇는데 가능하다.
객체 배열로 배열을 만들어서 객체를 저장하는 것임을 알게 되었다.

jdbc에서 db에서 비밀번호를 얻어와서 비교하는데 그 대상이 배열이라는 차이점이 있을 뿐이다.
그리고 찾아서 그 객체를 반환하는 것이다.