...
클래스(class) 구성
자바에서 클래스(class)란 객체를 정의하는 틀 또는 설계도와 같은 의미로 사용된다.
자바에서는 이러한 설계도를 가지고, 여러 객체를 생성하여 사용하는 식으로 프로그래밍을 이어나간다.
클래스는 객체의 속성 변수를 나타내는 필드(field)와 객체의 함수를 나타내는 메소드(method)로 구성되어있다.
클래스, 객체, 인스턴스, 메서드, 필드 등 자바 객체 지향 프로그래밍을 배우다 보면 정말 많은 용어들이 나온다.
특히 클래스나, 객체, 인스턴스 부분은 비슷하면서도 구분이 되어 있어 많이 혼동하는 편이다.
누군가에게 설명하기 위해서는 용어를 정확히 구분해서 사용해야하기 때문에 확실히 정리하고 넘어가자.
- 클래스 : 객체 데이터를 만드는 템플릿(설계도)
- 객체 : 클래스(템플릿)과 new 연산자를 통해 만든 실 데이터가 들어있는 변수
- 인스턴스 : 어떤 객체가 어떤 클래스의 객체인지 관계를 설명할때 (객체 선언 == 클래스 인스턴스화)
- 클래스 필드 / 멤버 / 속성 : 클래스 안에 있는 변수를 지칭
- 메서드 : 클래스 안에 있는 함수(function)를 지칭
- 생성자 : 클래스로 객체를 만들때 각 객체의 멤버 데이터(변수)들의 값을 초기 생성 해줄수 있는 특수 메서드
객체(object) / 인스턴스(instance)
클래스가 어떤 데이터의 구조 설계도라면, 객체는 설계도를 이용해 찍어낸 실 데이터라고 보면 된다.
그리고 클래스에 의해서 만들어진 객체를 인스턴스 라고도 한다.
객체와 인스턴스는 뜻하는바가 비슷해서 혼동이 올 수 있는데, 이렇게 생각 해 보도록 하자.
Animal cat = new Animal() 이렇게 만들어진 cat은 객체이다. 그리고 cat이라는 객체는 Animal의 인스턴스(instance) 이다. 즉, 인스턴스라는 말은 특정 객체(cat)가 어떤 클래스(Animal)의 객체인지를 관계위주로 설명할 때 사용된다라고 보면 된다.
// 클래스
class Animal { ... }
public class Sample {
public static void main(String[] args) {
// 변수 cat은 객체
// 변수 catd은 Animal 클래스의 인스턴스
Animal cat = new Animal(); // 클래스라는 설계도를 통해 객체 데이터를 new 생성
}
}
필드(field)
클래스의 필드(field)란 클래스에 포함된 변수(variable)를 가리킨다. 클래스 멤버(member) 라고도 불리운다.
또한 클래스 필드는 선언된 위치와 선언자에 따라 다음과 같이 구분된다.
- 클래스 변수(static variable)
- 인스턴스 변수(instance variable)
- 지역 변수(local variable)
클래스 영역에 위치한 변수 중에서 static 키워드를 가지는 변수를 클래스 변수 라고 한다.
그리고 반대로 static 키워드를 가지지 않는 변수를 인스턴스 변수라고 한다.
또한, 메소드나 생성자 블록 내에 위치한 변수를 지역 변수라고 한다.
class Field {
static int classVar = 10; // 클래스/스태틱 변수 선언
int instanceVar = 20; // 인스턴스 변수 선언
int method() {
int localVar = 30; // 지역 변수 선언
return localVar;
}
}
public class Member01 {
public static void main(String[] args) {
System.out.println( Field.classVar ); // 클래스/스태틱 변수 참조
Field myField1 = new Field(); // 인스턴스 생성
System.out.println( myField1.instanceVar ); // 인스턴스 변수 참조
System.out.println( myField1.method() ); // 메서드안의 지역변수 출력
}
}
이렇게 선언된 위치에 따라 구분되는 변수는 생성 및 소멸 시기, 저장되는 메모리 공간과 사용 방법까지도 서로 다르다.
변수 | 생성 시기 | 소멸 시기 | 저장 메모리 |
클래스 변수 | 클래스가 메모리에 올라갈 때 | 프로그램이 종료될 때 | 메소드 영역 |
인스턴스 변수 | 인스턴스가 생성될 때 | 인스턴스가 소멸할 때 | 힙 영역 |
지역 변수 | 블록 내에서 변수의 선언문이 실행될 때 | 블록을 벗어날 때 | 스택 영역 |
클래스(static) 변수는 해당 클래스의 모든 인스턴스가 공유해야 하는 값을 유지하기 위해 사용된다.
인스턴스를 생성하지 않고도 바로 사용할 수 있다는 특징이 있다.
마치 특정 클래스의 전역 공유 변수처럼 이용된다고 보면 된다.
다음 코드를 보면 Field 클래스로 인스턴스 3개를 생성해 사용하고 있는데, 만일 이 3개의 인스턴스가 공통적으로 사용할 변수가 필요하면 클래스 변수를 이용하면 된다는 것을 볼 수 있다.
반면에 인스턴스 변수는 인스턴스마다 가져야 하는 개별적인 값을 유지하기 위해 사용된다고 보면 된다.
class Field {
static int classVar = 0; // 클래스/스태틱 변수 선언
int instanceVar; // 인스턴스 변수 선언
}
public class Member01 {
public static void main(String[] args) {
Field myField1 = new Field(); // 인스턴스 생성
Field myField2 = new Field(); // 인스턴스 생성
Field myField3 = new Field(); // 인스턴스 생성
// 인스턴스 변수는 각 객체마다 개별적으로 저장된다
myField1.instanceVar = 10;
myField1.instanceVar = 20;
myField1.instanceVar = 30;
System.out.println( myField1.instanceVar ); // 10
System.out.println( myField2.instanceVar ); // 20
System.out.println( myField3.instanceVar ); // 30
// 클래스(스태틱) 변수는 하나의 클래스의 값으로 공유되어 고정으로 저장된다
myField1.classVar = 100;
myField2.classVar = 200;
myField3.classVar = 300;
System.out.println( myField1.classVar ); // 300
System.out.println( myField2.classVar ); // 300
System.out.println( myField3.classVar ); // 300
/* !! 위의 코드는 실행은 되지만 컴파일 경고를 내줄 것이다. !! */
// 클래스 변수에 접근하려면 Field.classVar 식으로 클래스명으로 바로 접근 해야 된다
System.out.println( Field.classVar ); // 300
}
}
메서드(method)
메소드란 클래스 안에 있는 함수를 뜻한다.
자바 프로그래밍 언어는 소스 파일에 메인 함수만 있더라도 반드시 클래스를 정의해야 한다는 특성이 있다.
그래서 다른 프로그래밍 언어에서 사용하는 함수(function)이라는 개념을 자바에서는 메소드(method)로 표현한다.
함수나 메서드나 위치의 차이이지 하고자 하는 역할은 다르지않으니 크게 의미를 두지 않아도 된다.
메소드 정의
- 접근 제어자/지정자 : 해당 메소드에 접근할 수 있는 범위를 명시
- 반환 타입(return type) : 메소드가 모든 작업을 마치고 반환하는 데이터의 타입을 명시
- 메소드명 : 메소드를 호출하기 위한 이름을 명시
- 매개변수 목록(parameters) : 메소드 호출 시에 전달되는 인수의 값을 저장할 변수들을 명시
- 구현부 : 메소드의 고유 기능을 수행하는 명령문의 집합. 중괄호 { } 안에 표현됨
파라미터 / 아규먼트
메서드에는 메서드에 입력값을 설정해주는 변수 목록이 있는데, 메서드와 함수를 구분해서 말하듯이, 매개변수(parameter)와 인수(arguments)는 혼용해서 사용되는 헷갈리는 용어이므로 이번 시간에 잘 기억해 두자.
- 매개변수(파라미터) : 메소드를 정의할때 입력으로 전달된 값을 받는 변수를 의미
- 인수(아규먼트) : 메소드를 호출할 때 전달하는 입력값을 의미한다.
public class Sample {
public static int sum(int a, int b) { // 메소드를 정의하는 입력값 a, b 는 매개변수(parameter)
return a+b;
}
public static void main(String[] args) {
sample.sum(3, 4); // 메소드를 사용하는 입력값 3, 4는 인수(argument)
}
}
클래스 메소드 / 인스턴스 메소드
클래스 필드에는 클래스 변수, 인스턴스 변수가 있듯이 메소드에서도 두가지로 나뉜다.
static 키워드를 가지는 메소드를 클래스 메소드(static method) 라고 하며,
static 키워드를 가지지 않는 메소드는 인스턴스 메소드(instance method) 라고 한다.
클래스 메소드는 클래스 변수와 마찬가지로 인스턴스를 생성하지 않고도 바로 사용할 수 있다.
다만 주의할 점은 클래스 메소드는 static 메소드 내부에서 인스턴스 변수를 사용할 수 없다는 특징이 있다.
그러므로 메소드 내부에서 인스턴스 변수나 인스턴스 메소드를 사용하지 않는 메소드를 클래스 메소드로 정의하는 것이 일반적이다.
class Method {
static int x = 100, y = 200; // 클래스(static) 변수
int a = 10, b = 20; // 인스턴스 변수
int add() { // 인스턴스 메소드
return this.a + this.b; // 인스턴스 변수끼리 합
}
static int addStatic() { // 클래스(static) 메소드
return Method.x + Method.y; // 클래스 변수끼리 합
}
}
public class Member {
public static void main(String[] args) {
System.out.println(Method.addStatic()); // 클래스 메소드의 호출 : 300
Method myMethod = new Method(); // 인스턴스 생성
System.out.println(myMethod.add()); // 인스턴스 메소드의 호출 : 30
}
}
메서드 오버로딩(overloading)
메소드 오버로딩(overloading)이란 같은 이름의 메소드를 중복하여 정의해서 다양한 상황에서 사용하는 것을 의미한다.
원래는 한 클래스 내에 같은 이름의 메소드를 둘 이상 가질 수 없다. 하지만 매개변수의 개수나 타입을 다르게 하면, 하나의 이름으로 메소드를 작성할 수 있다.
즉, 메소드 오버로딩은 서로 다른 메서드 시그니처를 갖는 여러 메소드를 같은 이름으로 정의하는 것이라고 할 수 있다.
메소드 시그니처(method signature)
메소드 시그니처란 메소드의 선언부에 명시되는 매개변수의 리스트를 가리킨다.
즉, 만약 두 메소드가 매개변수의 개수와 타입, 그 순서까지 모두 같다면, 이 두 메소드의 시그니처는 같다고 할 수 있다.
정리하자면 메소드 오버로딩이란 동일한 메서드 이름을 가지면서 메소드 시그니처가 다른 메서드를 뜻하는 것이라 보면 된다.
메소드 오버로딩의 대표적인 예로는 System.out.println() 메소드를 들 수 있다.
println() 메소드를 사용할때 우리는 입력값(argument)에 정수, 실수, 문자열, 객체 등 다양한 입력값을 넣어 콘솔에 출력하 였다.
System.out.println(100); // 정수값을 넣어 메소드 호출
System.out.println(0.4532); // 실수값을 넣어 메소드 호출
System.out.println("안녕"); // 문자열값을 넣어 메소드 호출
System.out.println(true); // 불리언값을 넣어 메소드 호출
보통 함수의 매개변수를 정의 할때 타입을 정해서 만드는데 어떻게 이런 것이 가능한 것일까?
정답은 간단하다.
다양한 매개변수를 받는 동인한 메서드명을 여러개 정의하면 되는 것이다.
/* println 메소드 원형 */
public static void println() { ... }
public static void println(boolean x) { ... }
public static void println(char x) { ... }
public static void println(char[] x) { ... }
public static void println(double x) { ... }
public static void println(float x) { ... }
public static void println(int x) { ... }
public static void println(long x) { ... }
public static void println(Object x) { ... }
public static void println(String x) { ... }
이처럼 자바 컴파일러는 사용자가 오버로딩된 함수를 호출하면, 전달된 매개변수의 개수와 타입과 같은 시그니처를 가지는 메소드를 찾아 호출하게 된다.
다만 메소드 오버로딩을 구현하기 위해서는 다음과 같은 조건을 만족해야 한다.
- 메소드의 이름이 같아야 한다.
- 메소드의 시그니처, 즉 매개변수의 개수 또는 타입이 달라야 한다.
참고로 메소드 오버로딩은 반환 타입과는 관계가 없다.
만약 메소드의 시그니처는 같은데 반환 타입만이 다른 경우에는 오버로딩이 성립하지 않는다.
class Test {
// 메소드 원형
void display(int num1) {
System.out.println(num1);
}
// 메소드 오버로딩 : 매개변수 갯수가 다른 유형
void display(int num1, int num2) {
System.out.println(num1 * num2);
}
// 메소드 오버로딩 : 매개변수는 같지만, 매개변수 타입이 다른 유형
void display(int num1, double num2) {
System.out.println(num1 + num2);
}
// 매개변수는 같지만, 메서드 반환 타입이 다른 유형은 오버로딩이 되지 않는다.
// 따라서 정수 타입을 반환하는 메서드를 작성하고 싶다면 그냥 새로 메서드를 만들면 된다.
int display2(int num1, int num2) {
return num1 + num2;
}
}
public class Method {
public static void main(String[] args) {
Test myfunc = new Test();
myfunc.display(10);
myfunc.display(10, 20);
myfunc.display(10, 3.14);
}
}
물론 오버로딩 개념없이 그냥 메서드명을 다르게하여 각기 다른 메서드 형태로 정의해서 사용해도 차이가 없다.
그렇지만 위에서 예시를 든 println() 메서드 처럼, 하나의 메서드에 여러가지 타입 데이터를 넣어 다채롭게 결과를 반환한다면 매우 매력적인 프로그래밍이 될 것이다.
그리고 이것이 객체 지향 프로그래밍에서 자주 등장하는 '다형성(polymorphism)' 의 특징 중 하나이다.
생성자(constructor)
생성자란 객체가 생성될 때 동적으로 인스턴스 변수 초기화를 위해 실행되는 특수한 메소드를 지칭한다.
우리가 변수를 선언 할때 int a = 1 처럼 변수에 값을 초기화 하는 것처럼, 객체를 선언 할때 Car mycar = new Car(값1, 값2) 처럼 클래스에 입력값을 보내 객체의 값을 초기화 해줄 수 있다.
생성자는 특수한 메서드 형태라 여러가지 규칙이 존재한다.
- 생성자의 목적은 객체 초기화
- 생성자 이름은 클래스 이름과 반드시 동일
- 생성자는 new를 통해 객체를 생성할 때, 객체당 한 번 호출
- 생성자는 객체가 생성될 때 반드시 호출됨.
- 생성자는 리턴 타입을 지정할 수 없음
- 개발자가 생성자를 작성하지 않았으면 컴파일러가 자동으로 기본 생성자 삽입
- 생성자는 여러 개 작성 가능 (오버로딩)
class Car {
String modelName;
int modelYear;
String color;
int maxSpeed;
int currentSpeed;
// 생성자 (인스턴스 변수 값 초기화)
Car(String modelName, int modelYear, String color, int maxSpeed) {
this.modelName = modelName; // 메서드의 입력값으로 인스턴스 변수의 값을 지정
this.modelYear = modelYear;
this.color = color;
this.maxSpeed = maxSpeed;
this.currentSpeed = 0; // 입력값 없이 디폴트 초기화
}
String getModel() {
return this.modelYear + "년식 " + this.modelName + " " + this.color;
}
}
public class Main {
public static void main(String[] args) {
Car myCar1 = new Car("아반떼", 2016, "흰색", 250); // 생성자의 호출
Car myCar2 = new Car("제네시스", 2020, "검은색", 500); // 생성자의 호출
Car myCar3 = new Car("티코", 2003, "빨간색", 100); // 생성자의 호출
System.out.println(myCar1.getModel()); // 2016년식 아반떼 흰색
System.out.println(myCar2.getModel()); // 2020년식 제네시스 검은색
System.out.println(myCar3.getModel()); // 2003년식 티코 빨간색
}
}
this 참조 변수
위의 코드의 생성자 안을 보면 this 키워드를 볼수 있는데, 단어 그대로 '클래스 자기 자신' 을 뜻하는 키워드 이다.
this 참조 변수는 해당 인스턴스의 주소를 가리키고 있기 때문에 자기 자신에 접근이 가능한 것이다.
그리고 모든 인스턴스 메소드에는 this 참조 변수가 숨겨진 지역 변수로 존재하고 있어 사용이 가능한 것이다. (클래스 메서드에는 this 변수가 없기 때문에 사용이 불가능하다)
class Car {
private String modelName;
private int modelYear;
private String color;
private int maxSpeed;
private int currentSpeed;
Car(String modelName, int modelYear, String color, int maxSpeed) {
this.modelName = modelName;
this.modelYear = modelYear;
this.color = color;
this.maxSpeed = maxSpeed;
this.currentSpeed = 0;
}
...
}
만일 this 키워드를 붙이지 않아도 자동으로 클래스의 인스턴스 변수로 인식하지만, 위의 코드와 같이 매개변수와 변수 이름이 같을 경우 매개변수와 인스턴스 변수명을 구분하기위해서 붙여주어야 한다. 또한 객체 자신의 래퍼런스를 반환할때 유용히 쓰인다.
this() 메서드
this() 메소드는 같은 클래스의 다른 생성자를 호출할 때 사용하는 메서드이다.
주의할 점은 생성자 내부에서만 사용할 수 있다는 특징이 있다.
this() 메소드에 인수를 전달하면, 정의되어 있는 다른 생성자를 찾아 호출해 준다.
class Car {
private String modelName;
private int modelYear;
private String color;
private int maxSpeed;
private int currentSpeed;
Car(String modelName, int modelYear, String color, int maxSpeed) {
this.modelName = modelName;
this.modelYear = modelYear;
this.color = color;
this.maxSpeed = maxSpeed;
this.currentSpeed = 0;
}
Car() {
this("소나타", 2012, "검정색", 160); // 해당 아규먼트가 일치하는 다른 생성자를 호출함.
}
public String getModel() {
return this.modelYear + "년식 " + this.modelName + " " + this.color;
}
}
public class Main {
public static void main(String[] args) {
// 초기화 인수를 보내주지 않아 Car() 생성자가 호출되지만, 안에서 this() 메서드가 호출되어
// 결과적으로 Car(String modelName, int modelYear, String color, int maxSpeed) 생성자가 호출되는 결과를 얻는다.
Car tcpCar = new Car();
System.out.println(tcpCar.getModel()); // 2012년식 소나타 검정색
}
}
단, 한 생성자에서 다른 생성자를 호출할 때에는 반드시 해당 생성자의 첫 줄에서 this() 를 호출해야 한다.
생성자 오버로딩
생성자는 메소드의 종류중 하나이다.
따라서 메소드 오버로딩이 가능한 것 처럼, 생성자도 오버로딩이 가능하다.
생성자를 오버로딩 하게 되면, 하나의 클래스에 여러개의 초기화 입력 항목을 구현할 수 있어서 좀더 다채로운 객체를 만드는데 도움이 된다.
예를들어 학번, 이름, 주소 3개의 입력값을 받는 생성자를 이전에 만들었는데, 만일 학번이나 이름만 적어줘도 객체가 정상적으로 생성되게 하고 싶으면 생성자 오버로딩을 통해 매개변수가 다른 생성자를 여러개 구현하면 된다.
class Student {
public int studentID; //학번
public String studentName; //이름
public String address; //주소
public Student(int id, String name, String address) {
this.studentID = id;
this.studentName = name;
this.address = address;
}
public Student(int id, String name) {
this.studentID = id;
this.studentName = name;
this.address = "대한민국"; // 주소를 정하지 않으면 대한민국 으로 설정
}
public Student(String name) {
this.studentID = 0; // 학번을 정하지 않으면 0으로 설정
this.studentName = name;
this.address = "대한민국"; // 주소를 정하지 않으면 대한민국 으로 설정
}
public Student() {
// 아무것도 입력값이 없다면 디폴트로 설정
this.studentID = 0;
this.studentName = "이름없음";
this.address = "대한민국";
}
}
public class Main {
public static void main(String[] args) {
Student studentPark = new Student(20219712,"박혁거세", "미국"); // public Student(int id, String name, String address) 생성자 호출
Student studentKim = new Student(20127721, "김종국"); // public Student(int id, String name) 생성자 호출
Student studentJames = new Student("제임스"); // public Student(String name) 생성자 호출
Student studentJames = new Student(); // public Student() 생성자 호출
// 정의되어 있지 않은 오버로딩 생성자는 호출 할 수 없음
Student studentJeff = new Student(20221024); // 학번만 입력값으로 보내 초기화 - 불가능 !!
}
}
기본 생성자
자바의 모든 클래스에는 반드시 하나 이상의 생성자가 정의되어 있어야 한다.
하지만 생성자를 정의하지않고 클래스를 만들어 사용하면 이상하게 어떠한 오류가 나지 않는다.
class Car {
private String modelName = "소나타";
}
public class Method03 {
public static void main(String[] args) {
Car myCar = new Car(); // 오류 안남
}
}
왜냐하면 이것은 자바 컴파일러가 기본 생성자(default constructor)라는 것을 기본적으로 제공해 주기 때문이다.
기본 생성자는 매개변수를 하나도 가지지 않으며, 아무런 명령어도 포함하고 있지 않는다.
class Car {
private String modelName = "소나타";
private int modelYear = 2016;
private String color = "파란색";
/*
다음 기본 생성자가 생략되어 있음
Car() {
}
*/
public String getModel() {
return this.modelYear + "년식 " + this.color + " " + this.modelName;
}
}
public class Method03 {
public static void main(String[] args) {
Car myCar = new Car(); // 기본 생성자의 호출
System.out.println(myCar.getModel()); // 2016년식 파란색 소나타
}
}
만약 매개변수를 가지는 생성자를 개발자가 하나라도 정의했다면, 기본 생성자는 당연히 자동으로 추가되지 않는다.
기본 생성자는 어디까지나 개발의 편의성을 위해 존재하는 기능 정도로 이해하면 된다.
클래스 상속(extends)
자바에서의 상속이란, 연관있는 클래스에 대해 공통적인 구성 요소를 정의하고, 이를 대표하는 클래스를 정의하는 것을 말한다.
상속 기능을 이용하게 되면, 상위 클래스의 특징을 하위클래스에서 상속받아 코드의 중복 제거, 코드 재사용성 증대 효과도 누릴 수 있다.
즉, 자주 사용될 것이 예상되는 기능을 모아놓은 클래스를 한번 만들어 놓으면 편하게 재사용을 함으로써 효율화를 추구할 수 있는 것이다.
이때 기존에 정의되어 있던 클래스를 부모 클래스(parent class) 또는 상위 클래스(super class), 기초 클래스(base class)라고 한다. 그리고 상속을 통해 새롭게 작성되는 클래스를 자식 클래스(child class) 또는 하위 클래스(sub class), 파생 클래스(derived class) 라고 한다.
다음은 Dog, Cat, Lion 클래스의 공통된 속성들 teethCount, legCount, tailCount를 Animal 클래스로 하나로 묶어 상속(extends)를 통해 코드량을 줄인 것을 볼 수 있다.
class Dog {
int teethCount; // 중복된 속성들
int legCount; // 중복된 속성들
int tailCount; // 중복된 속성들
void bark();
}
class Cat {
int teethCount; // 중복된 속성들
int legCount; // 중복된 속성들
int tailCount; // 중복된 속성들
void meow();
}
class Lion {
int teethCount; // 중복된 속성들
int legCount; // 중복된 속성들
int tailCount; // 중복된 속성들
void roar();
}
class Animal {
int teethCount;
int legCount;
int tailCount;
}
class Dog extends Animal { // 상속을 통해 중복 코드를 제거
void bark();
}
class Cat extends Animal { // 상속을 통해 중복 코드를 제거
void meow();
}
class Lion extends Animal { // 상속을 통해 중복 코드를 제거
void roar();
}
이렇게 상속을 통해 클래스를 구현하면 다음과 같은 장점을 얻을수 있게 된다.
- 클래스 간의 관계 형성을 명시해 줌으로써 코드의 가독성을 높일 수 있다.
- 자주 사용하는 코드를 공통으로 사용하여 불필요한 중복을 제거하고 일관성을 유지할 수 있다.
- 공통으로 사용하는 코드만 수정하면 되므로 생산성을 높이고, 유지보수를 쉽게 만들어준다.
다만 클래스 상속의 구조적인 한계 때문에 단 하나의 클래스 상속(단일 상속)만 가능하다는 점은 유의하자.
만일 같은 메소드를 가진 여러개의 클래스를 상속받으면 어떤 클래스의 메소드를 사용할지 애매해지기 때문이다.
자바 외의 다른 객체지향 언어에서는 이를 우선순위를 통해 해결하기도 한다.
상속 클래스의 생성자
각 클래스마다 생성자는 꼭 하나 이상은 존재한다.
그럼 만일 상속된 클래스간의 생성자 호출은 어느 순서대로 이루어질까?
상속 관계에서 자식 클래스를 인스턴스화 하면 부모 클래스의 객체가 먼저 인스터스화가 진행되고, 자식 클래스 객체가 인스턴스화 한다.
super 키워드
상속 클래스의 생성자를 다룰때 반드시 등장하는 개념이 super 키워드 이다.
super 키워드는 부모 클래스로부터 상속받은 필드나 메소드를 자식 클래스에서 참조하는 데 사용하는 참조 변수이다.
위에서 인스턴스 변수의 이름과 지역 변수의 이름이 같을 경우 인스턴스 변수 앞에 this 키워드를 사용하여 구분할 수 있었다. 이와 마찬가지로 부모 클래스의 멤버와 자식 클래스의 멤버 이름이 같을 경우 super 키워드를 사용하여 구별할 수 있다.
class Parent {
int a = 10; // 부모 인스턴스 변수
}
class Child extends Parent {
int a = 20; // 자식 인스턴스 변수
void display() {
System.out.println(a); // 자식 인스턴스 변수 a 출력
System.out.println(this.a); // 자식 인스턴스 변수 a 출력
System.out.println(super.a); // 부모 인스턴스 변수 a 출력
}
}
public class Inheritance02 {
public static void main(String[] args) {
Child ch = new Child();
ch.display();
/*
실행 결과 :
20
20
10
*/
}
}
위의 코드처럼 같은 변수명이라도 this 키워드냐 super 키워드냐에 따라 호출하는 인스턴스 변수가 달라지게 된다.
super() 메서드
위에서 다뤄본 this() 메소드가 같은 클래스의 다른 생성자를 호출할 때 사용된다면, super() 메소드는 부모 클래스의 생성자를 호출할 때 사용된다고 보면 된다.
자바는 자식 클래스의 객체가 인스턴스화 될때 기본적으로 부모 클래스의 디폴트 생성자를 호출하도록 설정 되어 있다.
그리고 이 디폴트 생성자를 호출하는 것이 super() 메서드 이다.
우리가 상속 클래스를 정의할때 super() 메서드를 호출하지 않아도 자동으로 부모 생성자가 호출되었는데, 이는 원래 자식 클래스 생성자의 첫줄에서 super() 메서드가 항상 실행되도록 정해져 있지만 개발 편의성을 위해 생략이 되어도 자동으로 인식되기 때문이다.
class Parent {
int a;
int b;
}
class Child extends Parent {
int c;
Child() {
// super(); 가 생략되어 있다
c = 20;
}
void display() {
System.out.println(a);
System.out.println(b);
System.out.println(c);
}
}
public class Main {
public static void main(String[] args) {
Child ch = new Child();
ch.display();
}
}
이를 다르게 말하면 부모 클래스의 멤버를 초기화하기 위해서는 자식 클래스의 생성자에서 부모 클래스의 생성자를 직접 호출해야 된다는 말이다.
이 특징은 다음에서 다룰 생성자를 오버로딩 하거나 직접 개발자가 생성자를 정의했을 경우 어떻게 되는지 이해하기 위해 매우 중요한 요소이다.
만일 아래 코드와 같이 부모클래스에 직접 생성자를 정의해 추가했다면, 위에서 배운바와 같이 부모 클래스의 디폴트 생성자는 없어진다.
그리고 부모 디폴트 생성자가 사라진다는 말은 곧, super()가 동작하지 않는다는 말이고 이는 곧 컴파일 에러를 일으키게 된다.
public class Employee {
private String name;
// 생성자를 직접 지정 → 디폴트 생성자는 작동치 않음
public Employee(String name) {
this.name = name;
}
}
public class Developer extends Employee {
private double salary;
public Developer(String name) {
// 원래는 자식 생성자 가장 첫번째 행에 super() 기본 생성자가 실행된다.
// 하지만 부모 클래스에는 Employee(String name) 밖에 없기 때문에 super()는 실행이 안되고 결과적으로 컴파일 에러가 뜬다
}
public void setSalary(double salary) {
this.salary = salary;
}
public double getSalary() {
return salary;
}
}
따라서 자식 클래스 생성자에서 부모 클래스 생성자의 메서드 시그니처에 맞는 super(name) 를 직접 호출해야 된다.
public class Employee {
private String name;
// 생성자를 직접 지정 → 디폴트 생성자는 작동치 않음
public Employee(String name) {
this.name = name;
}
}
public class Developer extends Employee {
private double salary;
public Developer(String name) {
super(name); // 자식 생성자의 입력값 name을 받아 부모 생성자 public Employee(String name) 를 호출
}
public void setSalary(double salary) {
this.salary = salary;
}
public double getSalary() {
return salary;
}
}
정리하자면 부모 클래스와 자식 클래스가 서로 상속 관계를 맺고 new 키워드를 통해 자식 클래스를 초기화 할때, 반드시 자식 클래스 생성자 내에서 부모 클래스 생성자를 호출하는 메서드 super() 가 가장 먼저(첫번째 행에서) 실행 된다.
이는 원래 직접 명시해야 되지만 디폴트 생성자일 경우 개발 편의성을 위해 생략할 수 있도록 되어 있다. (오히려 편의성 때문에 햇깔리기 마련이다)
만일 직접 생성자를 정의하거나 생성자 오버라이딩이 되어 있을 경우 메서드 시그니처에 맞는 생성자를 super(name) 메서드로 직접 정의해서 호출해야 된다. (생략된 super() 는 디폴트 생성자만 호출하기 때문이다)
this() 를 이용해 다채로운 자기자신 생성자 호출을 할수 있었던 것 처럼, super() 를 이용해 다채로운 부모 생성자 호출이 가능해진다.
다만 this() 는 잘 사용되지 않는 편이지만 super()는 정말 빈번하게 사용되니까 반드시 완벽히 학습하도록 하자.
메서드 오버라이딩(overriding)
위에서 먼저 배운 메서드 오버로딩(overloading) 과 이번에 배울 메서드 오버라이딩(overriding)은 자바 객체 지향 프로그래밍에서의 다형성(polymorphism)을 대표하는 매우 중요한 요소이다.
- 오버로딩(Overloading) : 기존에 없던 새로운 메서드를 여러개 정의
- 오버라이딩(Overriding) : 상속 받은 메서드의 내용만 변경 하여 덮어씌움
구분 | 오버로딩 | 오버라이딩 |
메소드 이름 | 동일 | 동일 |
매개변수, 타입 | 다름 | 동일 |
리턴 타입 | 상관없음 | 동일 |
정리하자면 오버로딩(overloading)이란 서로 다른 시그니처를 갖는 동일한 이름의 여러 메소드를 여러개 정의하는 것이면, 오버라이딩(overriding)이란 상속 관계에 있는 부모 클래스에서 이미 정의된 메소드를 자식 클래스에서 같은 시그니쳐를 갖는 메소드로 재정의하는 것이다.
좀더 알기 쉬게 overloading 과 overriding 의 차이를 활과 화살 그림으로 비유해 보았다.
활을 클래스로 보고 화살을 클래스의 메서드로 비유해보면, overloading은 멀티샷 같이 여러 종류의 화살촉을 여러개 걸어 발사하는 것이지만, overriding은 기존의 화살촉에 또다른 화살촉을 끼어넣어 발사하는 것으로 보면 된다.
자바에서 메소드를 오버라이딩하기 위한 조건은 다음과 같다.
- 오버라이딩이란 메소드의 동작만을 재정의하는 것이므로, 메소드의 선언부는 기존 메소드와 완전히 같아야 한다. (리턱값, 매개변수 갯수 ..등)
- 이는 리턴 타입도 마찬가지인데, 다만 메소드의 반환 타입은 부모 클래스의 반환 타입으로 타입 변환할 수 있는 타입이라면 변경할 수 있다.
- 부모 클래스의 메소드보다 접근 제어자를 더 좁은 범위로 변경할 수 없다 (후술)
class Parent {
void display() {
System.out.println("부모 클래스의 display() 메소드입니다.");
}
}
class Child extends Parent {
int count;
// 오버라이딩(overriding)된 display 메소드
void display() {
count++; // 자식의 인스턴스 변수를 증가시키고
System.out.println("자식 클래스의 display() 메소드입니다."); // 출력 내용도 다르게 한다.
}
// 오버로딩(overloading)된 display() 메소드
void display(String str) {
System.out.println(str); // 문자열을 입력값으로 받으면 그대로 출력
}
void display(int c) {
this.count += c;
System.out.println(count); // 정수를 입력값으로 받으면 자식 인스턴스 변수를 더해주고 더한 값을 출력
}
void display(boolean b) {
if(b == true) {
super.display(); // 만약 true를 입력값으로 받으면 부모 클래스의 메서드를 출력
}
}
}
public class Main {
public static void main(String[] args) {
Child ch = new Child();
// 오버라이딩(overriding) 된 자식 메서드 출력
ch.display(); // "자식 클래스의 display() 메소드입니다."
// 오버로딩(overloading) 된 자식 메서드 출력
ch.display("Hello World"); // "Hello World" 출력
ch.display(900); // 901
ch.display(true); // "부모 클래스의 display() 메소드입니다." - 오버라이딩 되었던 부모 메서드를 출력
}
}
메서드 오버라이딩을 할때 바로 뒤에서 배울 접근제어자 지정에 대해 조심해야 할 점이 있다.
바로 부모 메서드에 정의된 접근제어자의 범위보다 낮은 범위의 제어자를 지정할수 없다는 것인데, 예를 들어 부모 메서드에protected void display()라고 정의되어있는걸 오버라이딩 했을때 같은protected나public으로 설정해야 오버라이딩이 된다. 만일private나default제어자로 오버라이딩을 하려고 하면 컴파일 에러가 생긴다.
패키지(package)
패키지란 관련 있는 클래스 파일(컴파일된 .class)을 저장하는 디렉터리를 칭한다.
문서 작업할때 문서 파일들을 폴더를 이용해 디렉토리 구분하여 내용을 정리했듯이 자바의 클래스 파일들을 개발하기 용이하게 구조적으로 정리하는 것으로 동일하게 생각하면 된다.
이처럼 소스 파일들을 잘 정리해 보기 좋아지는 장점도 있지만, 패키지를 사용하는 가장 큰 이유는 클래스 이름에 대한 유일성을 보장받을 수 있기 떄문이다.
우리는 개발을 하면서 여러가지 형태의 클래스를 만들어 클래스 이름을 명명하고 사용할텐데, 외부 라이브러리를 가져와 사용해야 할때 만일 라이브러리의 클래스명 명사가 겹치면 수정하느라 번거로울 수도 있다.
따라서 이를 구분시키기 위해 자바에서는 도메인(폴더 경로)를 이용해 같은 클래스 이름이라도 경로가 다르면 완전 다른 파일로 인식된다.
보통 자바스크립트와 같은 여타 언어 에서는 역슬래쉬(\) 나 슬래쉬(/)로 각 폴더 경로를 구분한다.
하지만 자바에서는 점(.)을 이용해 [상위패키지].[하위패키지].[클래스] 이런식으로 구분하는게 이것을 도메인 이라고 부른다.
예를들어 패키지명 com.oracle.util 은 폴더 파일 경로 com\oracle\util 와 같다.
패키지 이름으로는 숫자와 특수문자( _ 와 $ 제외 ) 사용이 불가능하며, 모두 소문자로 작성해야 한다.
package com.oracle.util; // 해당 클래스가 속해있는 패키지 선언하여 그 패키지에 들어있는 다른 클래스들도 따로 선언없이 바로 사용 가능
// 클래스파일명
public class Sample {
}
import 문
코드 상단에 패키지를 명시해 두면, 그 패키지 안의 모든 클래스 파일을 바로 사용할 수 있게 된다.
하지만 만일 다른 패키지(폴더)에 들어있는 클래스 파일을 가져와 사용해야 할 경우 어떻게 해야 할까?
이때는 import 키워드를 통해 다른 피키지에 위치하고 있는 클래스 파일을 불러와 사용해야 한다.
import 문은 자바 컴파일러에 코드에서 사용할 클래스의 패키지에 대한 정보를 미리 제공하는 역할을 한다.
import java.util.Date; // java.util 패키지(폴더) 에 있는 Date클래스 만 가져와 사용
import java.util.*; // java.util 패키지(폴더) 에 있는 클래스 소스 파일들을 모두 사용
여기서 주의할 점이 있는데, import 문을 선언할 때 와일드카드(*)를 사용하면 모든 클래스 파일들을 포함시켜 주지만, 해당 패키지에 포함된 다른 모든 하위 패키지의 클래스까지 포함해 주는 것은 아니다.
즉, java 패키지에 있는 모든 파일과 그 하위에 있는 java.util 과 java.awt 패키지 파일들도 모두 포함시키고 싶다면, 다음과 같이 한번만 import 해주는 것이 아니라,
import java.*;
하위패키지들을 직접 모두 명시해 주어야 한다.
import java.awt.*;
import java.util.*;
StringBuffer나 Integer 클래스 역시 java.lang 패키지에 포함되어 있는 클래스 파일이다.
따라서 원래대로라면 import로 java.lang 패키지를 불러와야 되지만 실제 코딩해보면 import 하지 않는데, 이는java.lang 패키지는 자바에서 가장 많이 사용되는 패키지이기 때문에 이 패키지에 대해서는 import 문을 사용하지 않아도 클래스 이름만으로 사용할 수 있도록 개발 편의성을 위해 생략해주는 기능을 지원하지 때문이다.
static import
import static 문을 사용하면 정적 메소드나 필드를 클래스명 없이 사용할 수 있다.
예를들어 우리가 자바를 코딩하는데 있어 정말 많이 사용하는 정적 메소드인 System.out.println() 을 static import로 가져오면 다음과 같이 out.println() 만으로 사용도 가능하다.
이 기법은 단위테스트 과정에서는 사용되기는 하지만, 일반적으로 많이 사용되지는 않지만 이러한 구문이 있다는 정도만 짚고 넘어가면 된다.
import static java.lang.System.out;
public class Test {
public static void main(String[] args){
out.println("hello world");
}
}
제어자 (modifier)
제어자란 클래스와 클래스 멤버의 선언 시 사용하여 부가적인 의미를 부여하는 키워드를 의미한다.
메인 메소드인 public static void main() 에서의 public 과 static 키워드가 바로 이것이다.
자바에서의 제어자는 크게 접근 제어자(access modifier)와 그외 기타 제어자로 구분된다.
- 접근 제어자 : public, protected, (default), private
- 그 외 : static, final, abstract, …
- 하나의 대상에 여러 제어자를 같이 사용 가능 (접근 제어자는 오직 하나)
기타 제어자의 경우 여러 키워드를 함께 사용할 수도 있지만, 접근 제어자를 두 개 이상 같이 사용할 수는 없다는 특징이 있다.
접근 제어자
객체 지향에서 정보 은닉(data hiding)이란 사용자가 굳이 알 필요가 없는 정보는 사용자로부터 숨겨야 한다는 개념이다.
만일 어떠한 변수로 프로그래밍 동작이 결정된다 라고 하면, 외부 사용자가 이 변수로 접근해서 값을 바꾼다는 행위를 철저히 봉쇄 해야 한다. 안그러면 프로그램이 망가질 수 있기 때문이다.
따라서 접근 제어자는 외부로부터 데이터의 보호가 필요하고 사용자가 사용하는데 필요 없는 로직들을 숨기기 위해서 사용된다고 보면된다. (이러한 것을 은닉화/캡슐화 라고 한다)
자바에서는 이러한 정보 은닉을 위해 접근 제어자(access modifier)라는 4가지 키워드 기능을 제공한다.
접근 제어자를 사용하면 클래스 외부에서의 직접적인 접근을 허용하지 않는 멤버를 설정하여 정보 은닉을 코드로 구성 할 수 있다.
- private : 같은 클래스 내에서만 접근 가능
- (default) : 같은 패키지 내에서만 접근이 가능
- protected : 같은 패키지 내에서, 그리고 다른 패키지의 자손클래스에서 접근이 가능하다.
- public : 접근 제한이 전혀 없다.
public 제어자
- 프로그램 어디에서나 접근이 가능하다.
public class Everywhere {
public String var = "누구든지 허용"; // public 필드
public String getVar() { // public 메소드
return this.var;
}
}
default 제어자
- 따로 접근 제어자를 명시하지 않을 경우 기본값
- 같은 패키지에 속하면 어디에든지 접근 가능하다.
- 하지만 다른 패키지에 있으면 접근이 불가능하다.
package house; // 패키지가 동일하지 않다.
public class HouseKim {
String lastname = "kim"; // lastname은 다른 패키지에서 상속하든 뭘하든 접근이 절대 불가능 하다
}
package truck; // 패키지가 동일하지 않다.
import house.HouseKim; // 다른 패키제 있는 클래스를 사용하기 위해 불러옴
public class TruckPark {
public static void main(String[] args) {
HouseKim kim = new HouseKim(); // import 했으니 선언은 문제 없음
System.out.println(kim.lastname); // ERROR - 그러나 HouseKim 클래스의 lastname 변수를 사용할 수 없다
}
}
protected 제어자
- default 와 public의 중간에 위치한 제어자라고 보면 된다.
- 기본적으로 다른 패키지에서는 default와 같이 접근 불가하지만, 상속(extends) 할 경우 접근이 가능하다.
- 클래스의 protected 멤버에 접근할 수 있는 영역은 다음과 같다.
- 이 멤버를 선언한 클래스의 멤버
- 이 멤버를 선언한 클래스가 속한 같은 패키지의 멤버
- 이 멤버를 선언한 클래스를 상속받은 자식 클래스(child class)의 멤버
package house; // 패키지가 동일하지 않다.
public class HouseKim {
protected String lastname = "kim"; // lastname은 누군가 HouseKim을 상속하면 접근이 가능하다
}
package truck; // 패키지가 동일하지 않다.
import house.HouseKim; // 다른 패키제 있는 클래스를 사용하기 위해 불러옴
public class TruckPark extends HouseKim {
public static void main(String[] args) {
TruckPark park = new TruckPark();
System.out.println(park.lastname); // kim
}
}
private 제어자
- private자가 선언된 클래스 멤버는 외부에 공개되지 않으며, 외부에서는 직접 접근할 수 없다. (완벽히 보호)
- 따라서 private 멤버는 클래스 내부의 세부적인 동작을 구현하는 데 사용된다.
- 대부분 자바 프로그래밍에서 권장되는 제어 기법은 클래스 멤벼 변수들은 private를 지정해서 접근을 제한하고, 메소드를 public으로 지정해, 오로지 메소드를 통해서만 해당 객체의 멤버를 수정 할 수 있도록 한다.
- 대표적으로 Getter / Setter 을 구현하는데 자주 이용된다
public class Car {
private int speed; // 오로지 Car 클래스 내부에서만 접근 가능
private boolean stop;
// 따라서 메서드를 통해 클래스의 private 멤버의 값을 변경하도록 설정
// Getter 메서드
public int getSpeed() {
return this.speed;
}
// Setter 메서드
public void setSpeed(int speed) {
// 만약 옳지 않는 입력값을 넣을 경우 메서드 내에서 한번 걸러지게 할 수 있어 좋다.
if(speed < 0) {
this.speed = 0;
} else {
this.speed = speed;
}
}
}
public class Main {
public static void main(String[] args) {
Car car = new Car();
car.setSpeed(-50); // 잘못된 속도 변경
System.out.println("현재 속도 : "+ car.getSpeed()); // 속도는 음수가 될수 없기에 방지책이 실행되어 0 출력
car.setSpeed(60); // 올바른 속도 변경
System.out.println("현재 속도 : "+ car.getSpeed()); // 60 출력
}
}
위와 같은 메서드를 통해 클래스 멤버를 조정하는 것을 캡슐화(Encapsulation) 라고 한다.
기타 제어자
Static 제어자
- static 멤버는 메모리 측면에서 메서드(스태틱) 영역에 생성.
반대로 인스턴스 멤버는 new 생성자를 통해 힙 영역에 생성. - 동일한 클래스의 모든 객체에 의해 공유
- static메서드 내에서는 this 사용 불가능. (기본적으로 this 키워드는 인스턴스 멤버에 대한 접근 이니까)
class Car {
// 클래스 필드(static 변수) : 한 클래스에서 공통적으로 사용 되는 변수
static int var;
// 클래스 메소드(static 메소드)
static void func() {
// static 메소드에서는 this 키워드 사용 불가능. (this는 인스턴스 멤버에 대한 접근 키워드 이니까)
}
}
Car.var; // 클래스명.멤버명 으로 바로 접근됨
Car.func();
final 제어자
- final 키워드는 '변경할 수 없다'는 의미로 사용된다.
- 필드에 final을 붙이면 값을 변경할 수 없는 상수(constant)가 된다.
- 메소드에 final을 붙이면 해당 메소드는 오버라이딩(overriding)을 통한 재정의를 할 수 없다.
- 클래스에 final을 붙이면 해당 클래스는 다른 클래스가 상속받을 수 없다.
class SharedClass {
public static final double PI = 3.14; // static 상수 필드 선언 (반드시 초기값 지정)
}
public class FinalFieldClass {
final int ROWS = 10; // 인스턴스 상수 필드 선언
void f() {
int [] intArray = new int[ROWS]; // 상수 활용
ROWS = 30; // 컴파일 오류 발생, final 필드 값을 변경할 수 없다.
}
}
public class SuperClass {
protected final int finalMethod() { // final 메서드선언
// ...
}
}
class SubClass extends SuperClass { // SubClass가 SuperClass 상속
protected int finalMethod() { // 컴파일 오류, 오버라이딩 불가
// ...
}
}
final class FinalClass { // final 클래스 선언
.....
}
class SubClass extends FinalClass { // 컴파일 오류. FinalClass 상속 불가
.....
}
제어자의 조합
자바 프로그래밍 하면서 필드나 메서드에 접근 제어자(public, private, private)와 기타 제어자(static, final, abstract)를 조합하여 한 대상에 기재 해 놓은 것을 본 적이 있을 것이다.
하지만 모든 경우가 제어자 사용이 가능한 것은 아니며, 코드 대상에 따라 사용할 수 있는 조합이 각각 다르다.
- 클래스에 final과 abstract는 함께 사용할 수 없다.
- 메소드에 static과 abstract는 함께 사용할 수 없다.
- 메소드에 private과 abstract는 함께 사용할 수 없다.
- 메소드에 private과 final은 함께 사용할 필요가 없다.
대상 | 함께 사용할 수 있는 제어자 |
클래스 | public, (default), final, abstract |
메소드 | 모든 접근 제어자, final, static, abstract |
필드 | 모든 접근 제어자, final, static |
지역 변수 | final |
초기화 블록 | static |
# 참고자료
https://wikidocs.net/book/31
http://www.tcpschool.com/java/intro
https://www.youtube.com/watch?v=QK-PEVI7eDk&list=PLOSNUO27qFbtjCw-YHcmtfZAkE79HZSOO&index=8
https://blog.naver.com/PostView.naver?blogId=heartflow89&logNo=220961166980&parentCategoryNo=&categoryNo=&viewDate=&isShowPopularPosts=false&from=postView
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.