Chap 6. 클래스
1. 객체 지향 프로그래밍
- 소프트웨어를 개발할 때 부품에 해당되는 객체를 먼저 만들고, 객체를 하나씩 조립해서 완성된 프로그램을 만드는 기법을 객체 지향 프로그래밍이라고 한다.
- 객체 지향 프로그래밍은 컴퓨터 프로그램을 명령어의 목록으로 보는 시각에서 벗어나 여러 개의 독립된 단위, 즉 "객체"들의 모임으로 파악하고자 하는 것이다. 각각의 객체는 메시지를 주고받고, 데이터를 처리할 수 있다.
1) 객체
- 물리적으로 존재하거나 추상적으로 생각할 수 있는 것 중에서 자신의 속성을 가지고 있으면서 식별 가능한 것을 말한다.
- 객체는 속성과 동작으로 구성되어 있으며, 자바에서 이를 각각 필드와 메소드로 부른다.
- 객체들은 각각 독립적으로 존재하고, 다른 객체와 서로 상호작용하면서 동작한다. 객체들 간의 상호작용 수단은 메소드이다.
2) 객체 간의 관계
- 종류에는 집합 관계, 사용 관계, 상속 관계가 있다.
- 집합 관계에 있는 객체는 하는 부품이고 완성품에 해당된다. 자동차와 엔진, 타이어 등의 관계로 볼 수 있다.
- 사용 관계는 객체간의 상호작용을 말한다. 사람과 자동차의 관계로 볼 수 있다.
- 상속 관계는 상위(부모) 객체를 기반으로 하위(자식) 객체를 생성하는 관례를 말한다. 기계와 자동차의 관계로 볼 수 있다.
3) 객체와 클래스
- 객체를 만들기 위해서는 설계도가 필요하다. 자바에서는 설계도가 바로 클래스이다.
- 클래스로부터 만들어진 객체는 인스턴스라고 부른다.
4) 클래스 선언
- 소스 파일 당 두개 이상의 클래스 선언이 가능하지만 일반적으로 하나의 클래스를 선언한다.
- 두개 이상의 클래스가 선언된 소스 파일을 컴파일하면 바이트 코드 파일은 클래스의 개수만큼 생성된다. 소스 파일은 클래스 선언을 담고 있는 저장 단위일 뿐이다.
- public 접근 제한자는 파일 이름과 동일한 클래스 선언에만 붙일 수 있다.
5) 객체 생성과 클래스 변수
- 클래스로부터 객체를 생성하려면 new 연산자를 사용하면 된다.
- new 연산자는 힙 영역에 객체를 생성시킨 후 객체의 번지를 리턴한다. 이 주소를 참조 타입인 클래스 변수에 저장해두면 변수를 통해 객체를 사용할 수 있다.
new 클래스();
6) 클래스의 용도
- 클래스는 두가지 용도가 있다. 하나는 라이브러리용, 다른 하나는 실행용이다.
- 크래브러리 클래스는 다른 클래스에서 이용할 목적으로 생성된다.
- 클래스가 100라면, 이 중에서 단 하나만 실행 클래스이다.
7) 클래스의 구성 멤버
- 필드 : 객체의 고유 데이터, 부품 객체, 상태 정보를 저장하는 곳이다. 변수와 비슷하지만 변수는 생성자와 메소드 내에서만 사용되고, 필드는 생성자와 메소드 전체에서 사용되며 객체가 소멸되지 않는 한 객체와 함께 존재한다.
- 생성자 : new 연산자로 호출되는 특별한 중괄호 {} 블록이다. 역할은 객체 생성 시 초기화를 담당한다.
- 메소드는 객체의 동작에 해당되는 중괄호 {} 블록을 말한다. 필드를 읽고 수정하는 역할도 하지만, 다른 객체를 생성해서 다양한 기능을 수행하기도 한다. 메소드는 객체 간의 데이터를 전달하는 수단이다.
2. 필드
- 객체의 고유 데이터, 객체가 가져야할 부품, 객체의 현재 상태 데이터를 저장하는 곳이다.
- 클래스 중괄호 {} 블록 어디서든 존재할 수 있지만, 생성자와 메소드 중괄호 {} 블록 내부에는 선언될 수 없다.
- 타입은 기본 타입과 참조 타입 모두 올 수 있다.
- 초기값이 지정되지 않은 필드는 객체 생성 시 자동으로 기본 초기값으로 설정된다.
1) 필드 사용
- 클래스 내부에서 사용할 경우 : 변수를 사용하듯이 단순히 필드 이름으로 읽고 변경한다.
- 클래스 외부에서 사용할 경우 : 객체를 생성한 뒤 필드를 사용해야한다.
Car myCar = new Car();
myCar.speed = 60;
3. 생성자
- new 연산자로 클래스로부터 객체를 생성할 때 호출되어 객체의 초기화를 담당한다.
- 객체 초기화란 필드를 초기화하거나 메소드를 호출해서 객체를 사용할 준비를 하는 것을 말한다.
- new 연산자에 의해 생성자가 성공적으로 실행되면 힙 영역에서 객체가 생성되고 객체의 번지가 리턴된다. 리턴된 객체의 번지는 클래스 변수에 저장된다.
1) 기본 생성자
- 모든 클래스에는 생성자가 하나 이상 반드시 존재한다.
- 생성자 선언을 생략했다면 컴파일러는 중괄호 {} 블록 내용이 비어있는 기본 생성자를 바이트 코드에 자동 추가한다.
- 클래스가 public class로 선언되면 기본 생성자에서도 public이 붙지만, 클래스가 public 없이 선언된다면 기본 생성자에도 public이 붙지 않는다.
- 클래스에 명시적으로 선언한 생산자가 1개 이상 존재한다면, 컴파일러는 기본 생성자를 추가하지 않는다.
- 객체를 다양한 값으로 초기화하기 위해 명시적으로 생성자를 선언한다.
2) 필드 초기화
- 필드를 기본 초기값이 아닌, 다른 값으로 초기화하는 방법은 두가지가 있다.
- 필드를 선언할 때 초기값을 주는 방법 : 생성되는 객체들은 모두 같은 값을 가진다.
- 생성자에서 초기값을 주는 방법 : 매개값을 전달 받아서 초기화를 한다. 통상적으로 생성자의 매개변수는 필드와 동일한 이름으로 명명하기 때문에 this.를 이용하여 필드를 호출할 수 있다.
public Korean(String name, String ssn) {
this.name = name;
this.ssn = ssn;
3) 생성자 오버로딩
- 매개 변수를 달리하는 생성자를 여러개 선언하는 것
public class {
Car() {...}
Car(String model) {...}
Car(String model, String color) {...}
Car(String model, String color, int maxSpeed) {...}
}
- 매개 변수의 타입과 개수 그리고 선언된 순서가 똑같을 경우 매개 변수 이름만 바꾸는 것은 생성자 오버로딩이 아니다.
다른 생성자 호출 : this()
- 생성자 오버로딩이 많아질 경우 생성자 간의 코드 중복을 최소화하기 위해 사용한다.
- 반드시 생성자의 첫 줄에서만 허용한다.
public class Person {
String name;
String ssn;
Person() {
this("최준영", "213123-123123");
}
Person(String name, string ssn) {
this.name = name;
this.ssn = ssn;
}
}
4. 메소드
1) 메소드 선언
- 메소드 선언부를 메소드 시그너처라고한다.
- 메소드 선언은 선언부(리턴 타입, 메소드 이름, 매개 변수 선언), 메소드 실행 블록으로 구성된다.
- 리턴타입은 리턴값의 타입을 말하며, 리턴값이란 메소드를 실행한 후의 결과값을 말한다. 리턴값이 없을 경우 void로 선언한다.
- 매개변수는 메소드가 실행할 때 필요한 데이터를 외부로부터 받기위해 사용한다. 없어도 된다.
2) 매개 변수의 개수를 모를 경우
- 매개 변수를 배열 타입으로 선언한다.
int num1(int[] value) {}
int[] values = { 1, 2, 3};
int result = sum1(values);
or
int result = sum1(new int[] {1, 2, 3, 4, 5});
- 메소드의 매개 변수를 ...를 사용해서 선언한다. 호출시 넘겨준 값의 수에 따라 자동으로 배열이 생성되고 매개값으로 사용된다.
int sum2(int ...values) {}
int result = sum2(1, 2, 3);
int result = sum2(1, 2, 3, 4, 5);
3) 리턴문
- 메소드 선언에 리턴 타입이 있는 메소드는 반드시 리턴문을 사용해야한다.
- int plus() 메소드 선언 시 byte나 short로 리턴해도 상관없다.
- 리턴값이 없는 메소드는 리턴 타입으로 void를 사용한다. 해당 메소드에서도 return은 사용 가능하며, 메소드를 강제 종료시키는 역할을 한다.
4) 메소드 호출
- 클래스 내부에서 다른 메소드에서 호출할 경우에는 단순한 메소드 이름으로 호출하면 된다.
- 클래스 외부에서 호출할 경우에는 우선 클래스로부터 객체를 생성한 뒤 참조 변수를 이용해서 메소드를 호출해야 한다.
5) 메소드 오버로딩
- 클래스 내에 같은 이름의 메소드를 여러 개 선언하는 것을 말한다.
- 매개값을 다양하게 받아 처리하기 위해 사용한다.
- 오버로딩된 메소드를 호출할 경우 JVM은 매개값의 타입을 보고 메소드를 선택한다. 또한, 매개 타입이 일치하지 않을 경우 자동 타입 변환이 가능한지 검사한다.
- 리턴 타입만 다르고 매개 변수가 동일하다면 오버로딩이 아니다.
- 매개 변수의 타입과 개수, 순서가 똑같을 경우 매개 변수 이름이 다르더라도 오버로딩이 아니다.
5. 인스턴스 멤버와 정적 멤버
- 객체마다 필드값이 다르다면 해당 필드는 객체마다 가지고 있는 것이 맞다. 하지만 객체의 필드값이 모두 같다면 해당 필드는 한 곳에 위치시키고 객체들이 공유하는 것이 효과적이다.
- 인스턴스 멤버는 개체마다 가지고 있는 멤버를 말하고, 정적 멤버는 클래스에 위치시키고 객체들이 공유하는 멤버를 말한다.
1) 인스턴스 멤버
- 객체(인스턴스)를 생성한 후 사용할 수 있는 필드와 메소드를 말한다.
- 객체없이는 사용할 수 없다.
- 인스턴스 필드는 객체마다 따로 존재하고, 인스턴스 메소드는 메소드 영역에 저장되고 공유된다.
- 메소드는 코드 블록이므로 객체마다 도일한 코드 블록을 가지고 있을 필요가 없기 때문이다.
- 메모리 블록 내부에 인스턴스 필드 등이 사용되는 경우가 있기 때문에 '인스턴스' 메소드라고 부른다.
2) 정적 멤버와 static
- 클래스에 고정된 멤버로서 객체를 생성하지 않고 사용할 수 있는 필드와 메소드를 말한다.
- 클래스에 고정된 멤버이므로 클래스 로더가 클래스(바이트 코드)를 로딩해서 메소드 메모리 영역에 적재할 때 클래스 별로 관리된다. 따라서 클래스 로딩이 끝나면 바로 사용 가능하다.
- 일반적으로 클래스이름.필드나 클래스이름.메소드(매개값, ...)으로 접근한다.
- 정적 메소드 내부에 인스턴스 필드나 인스턴스 메소드를 사용할 수 없다.
- 객체 자신의 참조인 this 키워드 사용이 불가능하다.
- 정적 메소드에서 인스턴스 멤버를 사용하려면, 객체를 먼저 생성하고 참조변수로 접근한다.
- main 메소드도 정적 변수이다.
3) 싱글톤
- 단 하나의 객체만 만들도록 보장해야할 때 사용한다.
- 외부에서 new 연산자로 사용할 수 없게 생성자 앞에 private를 붙여준다.
- 자신의 타입인 정적 필드 하나를 선언하여 자신의 객체를 생성해 초기화한다.
- getInstance()를 선언하여 외부에서 호출할 수 있게 한다.
public class 클래스 {
private static 클래스 singleton = new 클래스();
private 클래스() {}
static 클래스 getInstance() {
return singleton;
}
}
4) final 필드와 상수
final 필드
- 초기값이 저장되면 프로그램 실행 도중에 수정할 수 없다.
- 초기값은 줄 수 있는 방법은 다음의 두가지 뿐이다.
- 필드 선언시 초기값을 준다.
- 생성자에서 초기값을 준다.
- 초기화하지 않으면 컴파일 에러가 발생한다.
상수(static final)
- 객체마다 저장할 필요가 없는 공용성을 띄기 때문에 static final을 붙여준다.
- 즉, 객체마다 존재하지 않고 클래스에만 존재하며 한번 초기값이 저장되면 변경할 수 없다.
6. 패키지와 접근 제한자
- 패키지의 물리적인 형태는 파일 시스템의 폴더이다.
- 단순한 폴더 기능 뿐만 아니라 클래스를 유일하게 만들어주는 식별자 역할도 한다. 클래스 이름이 동일하더라도 패키지가 다르면 다른 클래스로 인식한다.
- 상위패키지.하위패키지.클래스
- 패키지는 클래스의 일부이다. 그 이유는 클래스만 따로 복사해서 다른 곳으로 이동하면 클래스를 사용할 수 없기 때문이다.
1) import 문
- 사용하고자 하는 클래스 또는 인터페이스가 다른 패키지에 소속되어 있다면, import문으로 해당 패키지의 클래스 또는 인터페이스를 가져와 사용할 것임을 컴파일러에게 알려줘야한다.
import 상위패키지.하위패키지.클래스이름;
or import 상위패키지.하위패키지.*;
- 주의할 점은 패키지 전체 이름으로 패키지를 식별하기 때문에 com.hankook과 com.hankook.project를 서로 다른 패키지로 인식한다.
2) 접근 제한자
- 클래스 및 인터페이스 그리고 이들이 가지고 있는 멤버의 접근을 제한하기 위해 사용한다.
- public, protected, private, default가 있다.
- public 접근 제한자 : 외부 클래스가 자유롭게 사용할 수 있다.
- protected 접근 제한자 : 같은 패키지 또는 자식 클래스에서 사용할 수 있다.
- private 접근 제한자 : 외부에서 사용 불가능하다.
- default 접근 제한자 : 같은 패키지에 소속된 클래스에서만 사용할 수 있다. 접근 제한자를 명시하지 않으면 자동으로 적용된다.
- 생성자, 필드, 메소드에도 명시할 수 있다.
3) 클래스의 접근 제한
- 클래스는 public이나 default 접근 제한을 가진다.
- 클래스를 선언할 때 public을 생략했다면 default 접근 제한을 갖게 된다.
4) 생성자의 접근 제한
- 객체를 생성하기 위해서는 new 연산자로 생성자를 호출한다. 생성자가 어떤 접근 제한을 갖느냐에 따라 호출 가능 여부가 결정된다.
- public, protected, default, private 접근 제한을 가진다.
- 클래스에 생성자를 선언하지 않으면, 컴파일러에 의해 자동으로 기본 생성자가 추가된다. 이때 기본 생성자의 접근 제한은 클래스의 접근 제한과 동일하다.
- protected 필드, 메소드, 생성자 모두 다른 패키지의 자식 클래스에서 접근 가능하다. 여기서 생성자는 new 연산자를 사용하여 직접 호출할 수 없고, 자식 생성자에서 super()로 호출할 수 있다.
5) 필드와 메소드의 접근 제한
- 해당 필드와 메소드를 클래스 내부에서만 사용할 것인지, 패키지 내에서만 사용할 것인지, 아니면 다른 패키지에서도 사용할 수 있도록 할 것인지를 결정해야한다.
- public, protected, default, private 접근 제한을 가진다.
6) getter와 setter 메소드
- 객체의 무결성이 깨질 위험이 있기 때문에 일반적으로 객체의 필드를 객체 외부에서 직접적으로 접근하는 것을 막기위해 사용한다.
- 객체 지향 프로그래밍에서는 메소드를 통해서 필드를 변경하는 방법을 선호한다. 그 이유는 메소드는 매개값을 검증해서 유효한 값만 객체의 필드로 저장할 수 있기 때문이다.
- 필드를 private로 선언한 후 setter와 getter 메소드를 작성해서 안전하게 변경/사용하는 것이 좋다.
Chap 7. 상속
1. 상속
- 상속은 이미 잘 개발된 클래스를 재사용해서 새로운 클래스를 만들기 때문에 중복되는 코드를 줄여준다.
- 부모 클래스의 수정으로 모든 자식 클래스들도 수정되는 효과를 가져오기 때문에 유지 보수 시간을 최소화 할 수 있다.
1) 특징
- 여러개의 부모 클래스를 상속할 수 없다.
- 부모 클래스에서 private 접근 제한을 갖는 필드와 메소드는 상속 대상에서 제외된다.
- 부모 클래스와 자식 클래스가 다른 패키지에 존재한다면 default 접근 제한을 갖는 필드와 메소드도 상속 대상에서 제외된다.
2) 부모 생성자 호출
- 자식 객체를 생성하면, 부모 객체가 먼저 생성되고 그 다음에 자식 객체가 생성된다.
- 자식 클래스의 생성자가 명시적으로 선언되지 않았다면, 컴파일러는 다음과 같은 기본 생성자를 생성한다.
- 부모 클래스에 매개 변수가 있는 생성자만 있다면, 에러가 발생한다.
public 자식클래스명() {
super();
}
- protected 클래스는 new 연산자로 생성자를 호출할 수 없고, 자식 생성자에서 super()로 A생성자를 호출할 수 있다.
3) 메소드 재정의
- 상속된 메소드를 자식 클래스에서 수정해서 사용할 수 있다.
메소드 재정의 방법
- 부모의 메소드와 동일한 시그니처(리턴 타입, 메소드 이름, 매개 변수 목록)를 가져야 한다.
- 접근 제한을 더 강하게 재정의할 수 없다. 반대는 가능하다.
- 새로운 예외(Exception)를 throws할 수 없다.
- 메소드가 재정의되었다면 부모 객체의 메소드는 숨겨지기 때문에, 자식 객체에서 메소드를 호출하면 재정의된 자식 메소드가 호출된다.
부모 메소드 호출
- 자식 클래스 내부에서 재정의된 부모 클래스의 메소드를 호출하려면
super.부모메소드();
를 사용하면 된다.
4) final 클래스와 final 메소드
- 클래스에 final을 붙이면 상속할 수 없는 클래스가 된다. ex) String 클래스
- 메소드에 final을 붙이면 재정의할 수 없다.
2. 타입 변환과 다형성
- 다양한 객체를 이용해서 다양한 실행결과가 나오도록 하는 성질이다.
- 메소드 재정의 + 타입 변환 -> 다형성
1) 자동 타입 변환
- 자식은 부모 타입으로 자동 타입 변환이 가능하다.
- 부모 타입으로 자동 타입 변환된 이후에는 부모 클래스에 선언된 필드와 메소드만 접근이 가능하다.
- 메소드가 자식 클래스에서 재정의되었다면 자식 클래스의 메소드가 대신 호출된다.
2) 매개 변수의 다형성
- 매개 변수의 타입이 클래스일 경우, 해당 클래스의 객체뿐만 아니라 자식 객체까지도 매개값으로 사용할 수 있다.
3) 강제 타입 변환
- 부모 타입을 자식 타입으로 변환하는 것이다.
- 자식 타입이 부모타입으로 자동 변환 후 다시 자식타입으로 변환할 때 강제 타입 변환을 사용할 수 있다.
- 처음부터 부모 타입으로 생성된 객체는 자식 타입으로 변환할 수 없기 때문에 instanceof 연산자로 해당 객체가 어떤 클래스의 인스턴스인지 확인 한다.
boolean result = 좌항(객체) instanceof 우항(타입)
3. 추상 클래스
- 객체를 직접 생성할 수 있는 클래스를 실체 클래스라 한다면, 이 클래스들의 공통적인 특성을 추출해서 선언한 클래스를 추상 클래스라 한다.
1) 추상 클래스의 용도
- 공통된 필드와 메소드의 이름을 통일할 목적
- 실체 클래스를 작성할 때 시간 절약 : 일반적으로 개발 프로젝트에서 설계자가 추상 클래스로 설계 규격을 만든 후 추상 클래스를 상속해서 구체적인 클래스를 만들도록 요청한다.
2) 추상 클래스 선언
- 클래스 선언에 abstract 키워드를 붙여야한다.
- abstract를 붙이면 new 연산자를 이용해서 객체를 만들지 못하고, 상속을 통해 자식 클래스만 만들 수 있다.
- 추상 클래스도 필드, 생성자, 메소드 선언을 할 수 있다.
- new 연산자로 직접 생성자 호출을 못하기 때문에 super(...)를 호출해서 추상 클래스 객체를 생성하므로 생성자가 반드시 있어야한다.
3) 추상 메소드와 재정의
- 추상 메소드는 abstract 키워드와 함께 메소드의 선언부만 있고 메소드의 실행 내용인 중괄호 {}가 없는 메소드를 말한다.
- 추상 클래스 설계시 하위 클래스가 반드시 실행 내용을 채우도록 강제하고 싶은 메소드가 있을 때 사용한다.
[public | protected] abstract 리턴타임 메소드이름(매개변수, ...);