객제치향 언어
1980년대 초 소프트웨어의 위기
- 빠른 변화를 못쫓아감
해결책으로 객체지향 언어를 도입 (절차적 -> 객체지향)
코드의 재사용성이 높고 유지보수가 용이 (중복코드 제거)
객체지향 언어 = 프로그래밍 언어 + 객체지향 개념(규칙)
객체지향 언어 핵심개념 ( Object-Oriented-Programming)
1. 캡슐화
2. 상속
3. 추상화
4. 다형성
객체지향 개념은 어떻게 공부해야 하나요?
객체지향 언어 = 프로그래밍 언어 + 객체지향개념(규칙) -> 외울것.
외우라는 말이 무책임하다고 할 수 있지만, 규칙을 알아야 게임도 즐기는 것처럼 코딩도 할 수 있다.
자바의 정석은 최소한의 핵심을 정리한것,,
자바의 정석을 공부한 뒤에는 JSP, Spring 으로 실습위주로 가야한다.
클래스와 객체
클래스의 정의 : 객체를 정의해 놓은 것
클래스의 용도 : 객체를 생성하는데 사용
설계도(클래스)와 제품(객체)
클래스 | 객체 |
제품 설계도 | 제품 |
TV 설계도 | TV |
붕어빵 기계 | 붕어빵 |
객체의 구성요소 - 속성과 기능
실제세계를 어떻게하면 컴퓨터로 구현할 수 있을까?
객체 = 속성(변수) + 기능(메서드)
ex) TV
속성 | 크기, 길이, 높이, 색상, 볼륨, 채널 등 |
기능 | 켜기, 끄기, 볼륨 높이기, 볼륨 낮추기, 채널 변경하기 등 |
객체 : 모든 인스턴스를 대표하는 일반적 용어 (=인스턴스)
인스턴스 : 특정 클래스로부터 생성된 객체 예) Tv인스턴스
== 같은 용어라고 생각하면 된다.
클래스 -----------> 인스턴스(객체)
인스턴스화
* 클래스를 만드는 이유? 인스턴스화 해서 객체를 만들어서 사용하려고! (잊지말자!)
Q: 클래스가 왜 필요한가?
-> 객체를 생성하기 위해
Q: 객체가 왜 필요한가?
-> 객체를 사용하기 위해
Q: 객체를 사용하다는 의미?
-> 객체가 가지고 있는 속성과 기능을 사용한다.
객체의 생성과 사용
1. 객체의 생성
클래스명 변수명; // 클래스의 객체를 참조하기 위한 참조변수 선언
변수명 = new 클래스명(); // 클래스의 객체를 생성 후, 객체의 주소를 참조변수에 저장
Tv t; // Tv클래스 타입의 참조변수 t를 선언
t= new Tv(); // Tv인스턴스를 생성한 후, 생성된 Tv인스턴스의 주소를 t에 저장
2. 객체의 사용
- 객체가 가지고 있는 속성(변수)와 기능(메서드)를 사용하는 것.
t.channel = 7; // Tv인스턴스의 멤버변수 channel의 값을 7로 한다.
t.channelDown(); // Tv인스턴스의 메서드 channelDown()을 호출한다.
System.out.println("현재 채널은 " + t.channel + " 입니다.");
Tv t; // 변수선언과 동일하다. 타입 변수명
t = new Tv(); // Tv인스턴스를 생성한 후, 생성된 Tv인스턴스의 주소 저장
객체마다 별도의 저장공간이 생긴다.
객체 배열
객체 역시 배열로 다루는 것이 가능하며, 많은 수의 객체를 다뤄야할 때, 배열로 다루면 편리하다.
Tv tv1, tv2, tv3; -----> Tv[] tvArr = new Tv[3];
객체 배열을 생성하는 것 -> 객체를 다루기 위한 참조 변수들이 만들어진 것, 아직 객체는 저장되지 않은 상태
그렇기 때문에 반드시! 객체를 생성해서 개체 배열의 요소에 저장해야 한다.
Tv[] tvArr = new Tv[3];
// 객체를 생성해서 배열의 각 요소에 저장
tvArr[0] = new Tv();
tvArr[1] = new Tv();
tvArr[2] = new Tv();
// 배열의 초기화 블럭
Tv[] tvArr = {new Tv(), new Tv(), new Tv()};
// 객체의 수가 많은 경우
Tv[] tvArr = new Tv[100];
for (int i = 0; i < tvArr.length; i++) {
tvArr[i] = new Tv();
}
public class Q12 {
// 단어 맞추기
public static void main(String[] args) {
Tv[] tvArr = new Tv[3]; // 길이가 3인 Tv객체 뱌열
// Tv객체를 생성해서 Tv객체 배열의 각 요소에 저장
for (int i = 0; i < tvArr.length; i++) {
tvArr[i] = new Tv();
tvArr[i].channel = i + 10; // tvArr[i]의 channel에 i+10을 저장
}
for (int i = 0; i < tvArr.length; i++) {
tvArr[i].channelUp(); // tvArr[i]의 메서드를 호출, 채널이 1증가
System.out.printf("tvArr[%d].channel=%d\n", i, tvArr[i].channel);
}
}
}
class Tv {
String color;
boolean power;
int channel;
void power() {
power = !power;
}
void channelUp() {
++channel;
}
void channelDown() {
--channel;
}
}
클래스의 또 다른 정의
클래스 : 객체를 생성하기 위한 틀(설계도) 이며, 속성과 기능으로 정의되어 있다.
1. 클래스 - 데이터와 함수의 결합
프로그래밍 관점에서 데이터 저장형태의 발전 과정
변수 -> 배열 -> 구조체 -> 클래스
1. 변수
- 1개의 데이터를 저장할 수 있는 공간
2. 배열
- 같은 종류의 여러 데이털르 하나의 집합으로 저장할 수 있는 공간
3. 구조체
- 서로 관련된 여러 데이터를 종류에 관계없이 하나의 집합으로 저장할 수 있는 공간
4. 클래스
- 데이터와 함수의 결합 (구조체 + 함수)
객체지향에서는 구조체와 함수(메서드)를 클래스를통해 관리하게 했다.
2. 클래스 - 사용자정의 타입 (user-defined type)
프로그래밍언어에서 제공하는 자료형(primitive type)외에 프로그래머가 서로 관련된 변수들을 묶어서 자신만의 타입으로 새로 추가하는 것을 '사용자정의 타입'이라고 한다.
비객체지향적 코드 | 객체지향적 코드 |
int hour1, hour2, hour3; int minute1, minute2, minute3; float second1, second2, second3; |
Time t1 = new Time(); Time t2 = new Time(); Time t3 = new Time(); |
int[] hour = new int[3]; int[] minute = new int[3]; float[] second = new float[3]; |
Time[] t= new Time[3]; t[0] = new Time(); t[1] = new Time(); t[2] = new Time(); |
변수와 메서드
변수의 종류 - 클래스 변수, 인스턴스 변수, 지역변수
변수의 선언된 위치에 따라 종류가 결정된다.
멤버변수를 제외한 나머지 변수는 모두 지역변수, 멤버변수 중 static이 붙은 변수는 클래스변수, 붙지 않은 것은 인스턴스변수이다.
변수의 종류 | 선언위치 | 생성시기 |
클래스변수 (class variable) |
클래스 영역 | 클래스가 메모리에 올라갈 때 |
인스턴스변수 (instance variable) |
인스턴스가 생성되었을 때 | |
지역변수 (local variable) |
클래스 영역 이외의 영역 (메서드, 생성자, 초기화 블럭 내부) |
변수 선언문이 수행되었을 때 |
1. 인스턴스 변수
- 인스턴스를 생성할 때 만들어진다.
- 인스턴스가 생성된 상태에서 변수의 값을 읽어 오거나 저장할 수 있다.
- 인스턴스는 독립적인 저장공간을 가지므로 서로 다른 값을 가질 수 있다.
2. 클래스 변수
- static이 붙은 변수이다.
- 클래스변수는 모든 인스턴스가 공통된 저장공간을 공유하게 된다.
- 한 클래스의 모든 인스턴스들이 공통적인 값을 유지해야하는 속성의 경우, 클래스변수로 선언해야 한다.
- 인스턴스를 생성하지 않고도 언제든지 바로 사용할 수 있다.
- 클래스가 메모리에 '로딩'될 때 생성되어 프로그램이 종료될 때 까지 유지한다.
3. 지역변수
- 메서드 내에에서만 사용 가능하며, 메서드가 종료되면 소멸되어 사용할 수 없다.
클래스변수와 인스턴스변수
public class Q13 {
// 단어 맞추기
public static void main(String[] args) {
System.out.println("Card.width = " + Card.width);
System.out.println("Card.height = " + Card.height);
Card c1 = new Card();
c1.kind = "Heart";
c1.number = 7;
Card c2 = new Card();
c2.kind = "Spade";
c2.number = 4;
System.out.println("c1은 " + c1.kind + ", " + c1.number + "이며, 크기는 (" + c1.width + ", " +
c1.height + ")");
System.out.println("c2은 " + c2.kind + ", " + c2.number + "이며, 크기는 (" + c2.width + ", " +
c2.height + ")");
c1.width = 50;
c1.height = 80;
System.out.println("c1은 " + c1.kind + ", " + c1.number + "이며, 크기는 (" + c1.width + ", " +
c1.height + ")");
System.out.println("c2은 " + c2.kind + ", " + c2.number + "이며, 크기는 (" + c2.width + ", " +
c2.height + ")");
}
}
class Card {
String kind;
int number;
static int width = 100;
static int height = 250;
}
Card.width, c1.width, c2.width는 모두 같은 저장공간을 참조하므로 항상 같은 값을 갖는다.
메서드
메서드
- 특정 작업을 수행하는 일련의 문장들을 하나로 묶은 것이다.
- 메서드가 작업을 수행하는데 필요한 값만 넣고 원하는 결과를 얻으면 될 뿐, 이 메서드가 내부적으로 어떤 과정을 거쳐 결과를 만들어내는지 전혀 몰라도 된다.
메서드를 사용하는 이유
1. 높은 재사용성
- 한번 만들어 놓은 메서드는 몇 번이고 호출할 수 있으며, 다른 프로그램에서도 사용이 가능하다.
2. 중복된 코드의 제거
- 같은 내용의 문장들이 여러 곳에서 반복해서 나타나곤 한다. 이렇게 반복되는 문장들을 묶어서 하나의 메서드로 작성해 놓으면 반복되는 문장들 대신 메서드를 호출하는 한 문장으로 대체할 수 있다.
3. 프로그램의 구조화
- 길어야 100줄 정도 밖에 안되는 작은 프로그램의 경우 이렇게 해도 별 문제가 없지만, 몇 천-만 줄되는 프로그램을 작성할때는 문장들을 작업단위로 나눠서 여러 개의 메서드에 담아 프로그램의 구조를 단순화 시키는 것이 필수적이다.
메서드의 선언과 구현
메서드는 '선언부'와 '구현부' 로 이루어져 있다.
반환타입 메서드이름 (타입 변수명, 타입 변수명, ....) // 선언부
{
// 메서드 호출시 수행될 코드 // 구현부
}
메서드의 호출
메서드를 호출하면 메서드 구현부{}의 문장들이 수행된다.
메서드이름(값1, 값2, ...); // 메서드를 호출하는 방법
메서드를 호출할 때 괄호()안에 지정해준 값들을 '인자(argument)' 또는 '인수'라고 하는데, 인자의 개수와 순서는 호출된 메서드에 선언된 매개변수와 일치해야 한다. 그리고 인자는 메서드가 호출되면서 매개변수에 대입되므로, 인자의 타입은 배개변수의 타입과 일치하거나 자동 형변환이 가능한 것이어야 한다.
public class Q14 {
// 단어 맞추기
public static void main(String[] args) {
MyMath mm = new MyMath();
long result1 = mm.add(5L, 3L);
long result2 = mm.subtract(5L, 3L);
long result3 = mm.multiply(5L, 3L);
double result4 = mm.divide(5L, 3L);
System.out.println("add(5L, 3L) = " + result1);
System.out.println("subtract(5L, 3L) = " + result2);
System.out.println("multiply(5L, 3L) = " + result3);
System.out.println("divide(5L, 3L) = " + result4);
}
}
class MyMath {
long add(long a, long b) {
long result = a+b;
return result;
// return a+b; // 위의 두줄을 이와 같이 한 줄로 간단히 할 수 있다.
}
long subtract(long a, long b) {
return a-b;
}
long multiply(long a, long b) {
return a*b;
}
double divide(double a, double b) {
return a/b;
}
}
return문
return문
- 현재 실행중인 메서드를 종료하고 호출한 메서드로 되돌아간다.
- 반환값이 있을 때만 return문을 썼지만, 원래는 반환값의 유무에 관계없이 모든 메서드에는 적어도 하나의 return문이 있어야 한다.
- 반환타입이 void인 경우, return;을 자동적으로 추가해준다.
void printGugudan(int dan) {
return; // 반환 타입이 void이므로 생략가능. 컴파일러가 자동추가
}
실제로 자동으로 return이 만들어질까?
원본 코드
void print() {
System.out.println("출력문");
}
바이트코드
* 자바 바이트코드 확인하는 방법 (https://ecsimsw.tistory.com/entry/%EC%9E%84%EC%8B%9C%EC%A0%80%EC%9E%A5)
실제로 코드에는 작성하지 않은 retrun이 컴파일되어 바이트코드로 생성되어있다.
매개변수의 유효성 검사
메서드의 구현부{}를 작성할 때, 제일 먼저 해야 하는 일이 매개변수의 값이 적절한 것인지 확인하는 것이다.
메서드를 작성하는 사람은 '호출하는 쪽에서 알아서 적절한 값을 넘겨주겠지.'라는 생각을 절대로 가져서는 안 된다.
( 클라이언트를 무조건적으로 신뢰할 수 없다. 클라이언트는 올바른 값을 입력했음에도 중간에 해커가 다른 값으로 바꿀수도 있기 때문 )
JVM의 메모리구조
JVM은 운영체제(OS)로부터 프로그램을 수행하는데 필요한 메모리를 할당받고 JVM은 이 메모리를 용도에 따라 여러 영역으로 나누어 관리한다.
1. 메서드 영역(method area)
- 프로그램 실행 중 어떤 클래스가 사용되면, JVM은 해당 클래스의 클리스파일(*.class)를 읽어서 분석하여 클래스에 대한 정보(클래스 데이터)를 이곳에 저장한다. 이 때, 그 클래스의 클래스변수(class variable)도 이 영역에 함께 생성된다.
2. 힙(Heap)
- 인스턴스가 생성되는 공간, 프로그램 실행 중 생성되는 인스턴스는 모두 이곳에 생성된다.
3. 호출스택(call stack 또는 excution stack)
- 호출스택은 메서드의 작업에 필요한 메모리 공간을 제공한다. 메서드가 호출되면, 호출스택에 호출된 메서드를 위한 메모리가 할당되며, 이 메모리는 메서드가 작업을 수행하는 동안 지역변수(매개변수)들과 연산의 중간결과 등을 저장하는데 사용되낟. 그리고 메서드가 작업을 마치면 할당되었던 메모리공간은 반환되어 비워진다.
- 메서드가 호출되면 수행에 필요한 만큼의 메모리를 스택에 할당받는다.
- 메서드가 수행을 마치고나면 사용했던 메모리를 반환하고 스택에서 제거된다.
- 호출스택의 제일 위에 있는 메서드가 현재 실행 중인 메서드이다.
- 아래에 있는 메서드가 바로 위의 메서드를 호출한 메서드이다.
public class CallStackText {
public static void main(String[] args) {
firstmethod(); // static 메서드는 객체 생성없이 호출이가능하다.
}
private static void firstmethod() {
secondMethod();
}
private static void secondMethod() {
System.out.println("secondMethod()");
}
}
기본형 매개변수와 참조형 매개변수
자바에서는 메서드를 호출할 때 매개변수로 지정한 값을 메서드의 매개변수에 복사해서 넘겨준다.
매개변수의 타입이 기본형(primitive type)일 때는 기본형 값이 복사되겠지만
참조형(reference type)이면 인스턴스의 주소가 복사된다.
메서드의 매개변수를 기본형으로 선언하면 단순히 저장된 값만 얻지만, 참조형으로 선언하면 값이 저장된 곳의 주소를 알 수 있기 때문에 값을 읽어 온느 것은 물론 값을 변경하는 것도 가능하다.
기본형 매개변수 - 변수의 값을 읽기만 할 수 있다. (read only)
참조형 매개변수 - 변수의 값을 읽고 변경할 수 있다. (read & write)
class Data {
int x;
}
public class PrimitiveParamEx {
public static void main(String[] args) {
Data d = new Data();
d.x = 10;
System.out.println("main() : x = " + d.x);
change(d);
System.out.println("After Change(d)");
System.out.println("main() : x = " + d.x);
}
private static void change(Data d) {
d.x = 1000;
System.out.println("change() : x = " + d.x);
}
}
public class ReferenceParamEx2 {
public static void main(String[] args) {
int[] x = { 10 }; // 크기가 1인 배열. x[0] = 10;
System.out.println("main() : x = " + x[0]);
change(x);
System.out.println("After change(x)");
System.out.println("main() : x = " + x[0]);
}
private static void change(int[] x) {
x[0] = 1000;
System.out.println("change() : x = " + x[0]);
}
}
- 배열도 객체와 마찬가지로 참조변수를 통해 데이터가 저장된 공간에 접근이 된다.
참조형 반환타입
매개변수뿐만 아니라 반환타입도 참조형이 될 수있다. 반환타입이 참조형이라는 것은 반환하는 값의 타입이 참조형이라는 얘기이다. 모든 참조형 타입의 값은 '객체의 주소'이므로 그저 정수값이 반환되는 것일 뿐 특별할 것이 없다.
static Data copy(Data d) {
Data tmp = new Data(); // 새로운 객체 tmp를 생성한다.
tmp.x = d.x; // d.x의 값을 tmp.x에 복사한다.
return tmp; // 복사한 객체의 주소를 반환한다.
}
copy() 메서드는 새로운 객체를 생성한 다음에, 매개변수로 넘겨받은 객체에 저장된 값을 복사해서 반환한다.
반환하는 값이 Data객체의 주소이므로 반환 타입이 'Data'인 것이다.
재귀호출(recursive call)
메서드의 내부에서 메서드 자신을 다시 호출하는 것을 '재귀호출(recursive call)'이라 하고, 재귀호출을 하는 메서드를 '재귀 메서드'라 한다.
void method() {
method(); // 재귀호출, 메서드 자신을 호출한다.
}
위의 코드처럼 오로지 재귀호출뿐이면, 무한히 자기자신을 호출하기 때문에 무한반복에 빠지게 된다.
무한반복문이 조건문과 함께 사용되어야 하는 것처럼, 재귀호출도 조건문이 필수적으로 따라다닌다.
void method(int n) {
if (n==0)
return; // n의 값이 0일 때, 메서드를 종료한다.
System.out.println(n);
method(--n); // 재귀호출
}
왜 재귀호출을 사용할까?
- 재귀호출이 주는 논리적 간결함 때문
- 몇 겹의 반복문과 조건문으로 복잡하게 작성된 코드가 재귀호출로 작성하면 보다 단순한 구조로 바뀔 수도 있다.
- 너무 반복문이 복잡하면 재귀호출로 간단히 할 수 없는지 고민해볼 필요가 있다.
- 재귀호출은 비효율적이므로 재귀호출에 드는 비용보다 재귀호출의 간결함이 주는 이득이 충분히 큰 경우에만 사용해야 한다.
- 대표적인 재귀호출의 예) 팩토리얼
public class FactorialTest {
public static void main(String[] args) {
int result = factorial(4); // int result = FactorialTest.factorial(4);
System.out.println(result);
}
private static int factorial(int n) {
int result = 0;
if ( n == 1 ) {
result = 1;
} else {
result = n * factorial(n-1);
}
return result;
}
}
클래스 메서드(static 메서드)와 인스턴스 메서드
1. 클래스를 설계할 때, 멤버변수 중 모든 인스턴스에 공통적으로 사용해야하는 것에 static을 붙인다.
- 생성된 각 인스턴스는 서로 독립적이기 때문에 각 인스턴스의 변수(iv)는 서로 다른 값을 유지한다. 그러나 모든 인스턴스에서 같은 값이 유지되어야 하는 변수는 static을 붙여서 클래스변수로 정의해야 한다.
2. 클래스 변수(static변수)는 인스턴스를 생성하지 않아도 사용할 수 있다.
- static이 붙은 변수(클래스변수)는 클래스가 메모리에 올라갈 때 이미 자동적으로 생성되기 때문이다.
3. 클래스 메서드(static메서드)는 인스턴스 변수를 사용할 수 없다.
- 클래스 메서드는 인스턴스 생성 없이 호출가능하므로 클래스 메서드가 호출되었을 때 인스턴스가 존재하지 않을 수도 있다. 그래서 클래스 메서드에서 인스턴스변수의 사용을 금지하다.
4. 메서드 내에서 인스턴스 변수를 사용하지 않는다면, static을 붙이는 것을 고려한다.
- 메서드의 작업내용 중에서 인스턴스변수를 필요로 한다면, static을 붙일 수 없다. 반대로 인스턴스변수를 필요로 하지 않는다면 static을 붙이자. 메서드 호출시간이 짧아지므로 성능이 향상된다.
- 클래스의 멤버변수 중 모든 인스턴스에 공통된 값을 유지해야하는 것이 있는지 살펴보고 있으면, static을 붙여준다.
- 작성한 메서드 중에서 인스턴스 변수나 인스턴스 메서드를 사용하지 않는 메서드에 static을 붙일 것을 고려한다.
클래스 멤버와 인스턴스 멤버간의 참조와 호출
인스턴스 멤버가 존재하는 시점에 클래스 멤버는 항상 존재하지만, 클래스멤버가 존재하는 시점에 인스턴스 멤버가 존재하지 않을 수도 있기 때문이다.
public class TestClass {
public static void main(String[] args) {
}
void instanceMethod() {}
static void staticMethod() {}
void instanceMethod2() {
instanceMethod();
staticMethod();
}
static void staticMethod2() {
instanceMethod(); // 인스턴스 메서드는 호출할 수 없다.
staticMethod();
}
}
위의 코드는 같은 클래스 내의 인스턴스 메서드와 static메서드 간의 호출에 대해서 설명하고 있다. 같은 클래스 내의 메서드는 서로 객체의 생성이나 참조변수 없이 직접 호출이 가능하지만 static메서드는 인스턴스 메서드를 호출할 수 없다.
오버로딩(overloading)
오버로딩이란?
- 메서드도 변수와 마찬가지로 같은 클래스 내에서 서로 구별될 수 있어야 하기 때문에 각기 다른 이름을 가져야 한다.
자바에서는 한 클래스 내에 이미 사용하려는 이름과 같은 이름을 가진 메서드가 있더라도!
- 매개변수의 개수, 매개변수의 타입이 다르면 같은 이름을 사용해서 메서드를 정의할 수 있다.
오버로딩의 조건
- 같은 이름의 메서드를 정의한다고 해서 무조건 오버로딩인 것은 아니다.
- 오버로딩이 성립하기 위해서는 다음과 같은 조건을 만족해야한다.
1. 메서드 이름이 같아야 한다.
2. 매개변수의 개수 또는 타입이 달라야 한다.
반환 타입은 오버로딩을 구현하는데 아무런 영향을 주지 못한다.
오버로딩의 예
println메서드를 호출할 때 매개변수로 지정하는 값의 타입에 따라서 호출되는 println메서드가 달라진다.
PrintStream클래스에는 어떤 종류의 매개변수를 지정해도 출력할 수 있도록 아래와 같이 10개의 오버로딩된 println메서드를 정의해놓고 있다.
void println()
void println(boolean n)
void println(char x)
void println(char[] x)
void println(double x)
void println(float x)
void println(int x)
void println(long x)
void println(Object x)
println메서드를 호출할 때 매개변수로 넘겨주는 값의 타입에 따라서 위의 오버로딩된 메서드들 중의 하나가 선택되어 실행되는 것이다.
오버로딩의 장점
그렇다면 오버로딩을 구현함으로써 얻는 이득은 무엇인가?
- 메서드도 변수처럼 단지 이름만으로 구별된다면, 한 클래스내의 모든 메서드들은 이름이 달라야 한다.
그렇다면, 이전에 예로 들었던 10가지의 println메서드들은 각기 다른 이름을 가져야 한다.
void println()
void printlnBoolean(boolean x)
void printlnChar(char x)
모두 근본적으로는 같은 기능을 하는 메서드들이지만, 서로 다른 이름을 가져야 하기 때문에 메서드를 작성하는 쪽에서는 이름을 짓기도 어렵고, 메서드를 사용하는 쪽에서는 이름을 일일이 구분해서 기억해야하기 때문에 서로 부담이 된다.
하지만 오버로딩을 통해 여러 메서드들이 println이라는 하나의 이름으로 정의될 수 있다면, println이라는 이름만 기억하면 되므로 기억하기도 쉽고 이름도 짧게 할 수 있어서 오류의 가능성을 많이 줄일 수 있다.
가변인자(varagrs)와 오버로딩
기존에는 메서드의 매개변수 개수가 고정적이었으나 JDK1.5부터 동적으로 지정해 줄 수 있게 되었으며, 이 기능을 '가변인자(variable arguments)'라고 한다.
가변인자는 '타입... 변수명'과 같은 형식으로 선언하며, PrintStream클래스의 printf()가 대표적인 예이다.
public PrintStream printf(String format, Object... args) { ... }
- 가변인자는 내부적으로 배열을 이용하는 것이다.
- 그래서 가변인자가 선언된 메서드를 호출할 때마다 배열이 새로 생성된다.
- 가변인자가 편리하지만, 이런 비효율이 숨어있으므로 꼭 필요한 경우에만 가변인자를 사용하자.
생성자(Constructor)
생성자란?
- 생성자는 인스턴스가 생성될 때 호출되는 '인스턴스 초기화 메서드'이다.
- 인스턴스 변수의 초기화 작업에 주로 사용된다.
1. 생성자의 이름은 클래스의 이름과 같아야 한다.
2. 생성자는 리턴 값이 없다.
3. 생성자도 오버로딩이 가능하다.
연산자 new가 인스턴스를 생성하는 것이지 생성자가 인스턴스를 생성하는 것이 아니다.
1. 연산자 new에 의해서 메모리(heap)에 Card클래스의 인스턴스가 생성된다.
2. 생성자 Card()가 호출되어 수행된다.
3. 연산자 new의 결과로, 생성된 Card인스턴스의 주소가 반환되어 참조변수 c에 저장된다.
기본 생성자(default constructor)
모든 클래스에는 반드시 하나 이상의 생성자가 정의되어 있어야 한다.
그러나 지금까지 클래스에 생성자를 정의하지 않고도 인스턴스를 생성할 수 있었던 이유는 컴파일러가 제공하는 '기본 생성자(default constructor)' 덕분이었다.
컴파일 할 때, 소스파일(*.java)의 클래스에 생성자가 하나도 정의되지 않은 경우 컴파일러는 자동적으로 아래와 같은 내용의 기본 생성자를 추가하여 컴파일 한다.
기본 생성자가 컴파일러에 의해서 추가되는 경우는 클래스에 정의된 생성자가 하나도 없을 때 뿐이다.
매개변수가 있는 생성자
생성자도 메서드처럼 매개변수를 선언하여 호출 시 값을 넘겨받아서 인스턴스의 초기화 작업에 사용할 수 있다.
인스턴스마다 각기 다른 값으로 초기화되어야하는 경우가 많기 때문에 매개변수를 사용한 초기화는 매우 유용하다.
public class Car {
String color;
String gearType;
int door;
Car() {} // 생성자
Car(String c, String g, int d) {
color = c;
gearType = g;
door = d;
}
}
Car인스턴스를 생성할 때, 생성자 Car()를 사용한다면, 인스턴스를 생성한 다음에 인스턴스변수들을 따로 초기화해주어야 하지만, 매개변수가 있는 생성자 Car(String color, String gearType, int door)를 사용한다면 인스턴스를 생성하는 동시에 원하는 값으로 초기화를 할 수 있게 된다.
클래스를 작성할 때 다양한 생성자를 제공함으로써 인스턴스 생성 후에 별도로 초기화를 하지 않아도 되도록 하는 것이 바람직하다.
생성자에서 다른 생성자 호출하기 - this(), this
- 생성자의 이름으로 클래스이름 대신 this를 사용한다.
- 한 생성자에서 다른 생성자를 호출할 때는 반드시 첫 줄에서만 호출이 가능하다.
this는 참조변수로 인스턴스 자신을 가리킨다. 참조변수를 통해 인스턴스의 멤버에 접근할 수 있는 것처럼, 'this' 역시 사용할 수 없다. 왜냐하면, static메서드는 인스턴스를 생성하지 않고도 호출될 수 있으므로 static메서드가 호출된 시점에 인스턴스가 존재하지 않을 수도 있기 때문이다.
this - 인스턴스 자신을 가리키는 참조변수, 인스턴스의 주소가 저장되어 있다.
모든 인스턴스메서드에 지역변수로 숨겨진 채로 존재한다.
this(), this(매개변수) - 생성자, 같은 클래스의 다른 생성자를 호출할 때 사용한다.
변수의 초기화
변수를 선언하고 처음으로 값을 저장하는 것을 '변수의 초기화'라고 한다.
변수의 초기화는 경우에 따라서 필수적이기도 하고 선택적이기도 하지만, 가능하면 선언과 동시에 적절한 값으로 초기화 하는것이 바람직하다.
멤버변수는 초기화를 하지 않아도 자동적으로 변수의 자료형에 맞는 기본값으로 초기화가 이루어지므로 초기화하지 않고 사용해도 되지만, 지역변수는 사용하기 전에 반드시 초기화해야 한다.
멤버변수(클래스변수와 인스턴스변수)와 배열의 초기화는 선택적이지만, 지역변수의 초기화는 필수적이다.
자료형 | 기본값 |
boolean | false |
char | '\u000' |
byte, short, int | 0 |
long | 0L |
float | 0.0f |
double | 0.0d 도는 0.0 |
참조형 변수 | null |
멤버변수 초기화 방법
1. 명시적 초기화
2. 생성자
3. 초기화 블럭
- 인스턴스 초기화 블럭 : 인스턴스변수를 초기화 하는데 사용.
- 클래스 초기화 블럭 : 클래스변수를 초기화 하는데 사용
명시적 초기화
변수를 선언과 동시에 초기화하는 것을 명시적 초기화라고 한다.
가장 기본적이면서도 간단한 초기화 방법이므로 여러 초기화 방법 중에서 가장 우선적으로 고려되어야 한다.
class Car {
int door = 4; // 기본형(primitive type) 변수의 초기화
Engine e = new Engine(); // 참조형(reference type) 변수의 초기화
}
보다 복잡한 초기화 작업 또는 공통적인 초기화 작업이 필요할 때는 '초기화 블럭' 또는 생성자를 사용해야 한다.
초기화 블럭
클래스 초기화 블럭 - 클래스변수의 복잡한 초기화에 사용된다.
인스턴스 초기화 블럭 - 인스턴스변수의 복잡한 초기화에 사용된다.
초기화 블럭을 작성하려, 인스턴스 초기화 블럭은 단순히 클래스 내에 블럭{}만들고 그안에 코드를 작성
클래스 초기화 블럭은 인스턴스 초기화 블럭앞에 단순히 static을 덧붙이기만 하면된다.
초기화 블럭 내에는 메서드 내에서와 같이 조건문, 반복문, 예외처리구문 등을 자유롭게 사용할 수 있다.
초기화 작업이 복잡하여 명시적 초기화만으로는 부족한 경우 초기화 블럭을 사용한다.
클래스 초기화 블럭
- 클래스가 메모리에 처음 로딩될 때 한번만 수행되며, 인스턴스 초기화 블럭은 생성자와 같이 인스턴스를 생성할 때 마다 수행된다.
- 생성자보다 인스턴스 초기화 블럭이 먼저 수행된다.
* 클래스가 처음 로딩될 때 클래스변수들이 자동적으로 메모리에 만들어지고, 곧바로 클래스 초기화블럭이 클래스변수들을 초기화하게 되는 것이다.
인스턴스 초기화 블럭
- 모든 생성자에서 공통으로 수행돼야 한느 코드를 넣는데 사용한다.
예를들면, 클래스의 모든 생성자에 공통ㅇ으로 수행되어야 한느 문장들이 있을때, 이 문장들을 각 생성자마다 써주기 보다는 인스턴스 블럭에 넣어주면 코드가 보다 간결해진다.
즉, 재사용성을 높이고 중복을 제거하는 것, 이것이 바로 객체지향프로그래밍이 추구하는 궁극적인 목표이다.
멤버변수의 초기화 시기와 순서 (305p 도표 확인!)
클래스변수의 초기화시점 : 클래스가 처음 로딩될 때 단 한번 초기화 된다.
인스턴스변수의 초기화시점 : 인스턴스가 생성될 때마다 각 인스턴스별로 초기화가 이루어진다.
클래스변수의 초기화순서 : 기본값 -> 명시적초기화 -> 클래스 초기화 블럭
인스턴스변수의 초기화순서 : 기본값 -> 명시적초기화 -> 인스턴스 초기화 블럭 -> 생성자
프로그램 실행도중 클래스에 대한 정보가 요구될 때, 클래스는 메모리에 로딩된다.
예를들면, 클래스 멤버를 사용했을 때, 인스턴스를 생성할 때 등이 이에 해당한다.
하지만, 해당 클래스가 이미 메모리에 로딩되어 있다면, 또다시 로딩하지 않는다
* 클래스의 로딩 시기는 JVM의 종류에 따라 다르다. 클래스가 필요할 때 바로 메모리에 로딩하도록 설계되어 있기도 하며, 실행효율을 높이기 위해서 사용될 클래스들을 프로그램이 시작될 때 미리 로딩하도록 되어있는 것도 있다.
( JVM의 종류는 ? )
public class DocumentTest {
public static void main(String[] args) {
Document d1 = new Document();
Document d2 = new Document("자바.txt");
Document d3 = new Document();
Document d4 = new Document();
}
}
class Document {
static int count = 0;
String name;
Document() {
this("제목없음" + ++count);
}
Document(String name) {
this.name = name;
System.out.println("문서 " + this.name + "가 생성되었습니다.");
}
}
워드프로세서나 문서편집기에 이와 유사한 코드가 사용된다.
문서를 생성할 때, 문서의 이름을 지정하면 그 이름의 문서가 생성되지만, 문서의 이름을 지정하지 않으면 프로그램이 일정한 규칙을 적용해서 자동으로 이름을 결정한다.
'자바의정석' 카테고리의 다른 글
자바의정석 - 제네릭 (0) | 2021.12.29 |
---|---|
자바의 정석 - 컬렉션 프레임워크(Collections Framework) (0) | 2021.12.26 |
자바의정석 - 예외처리(Excepption handling) (0) | 2021.12.22 |
자바의정석 - 객체지향개념2 (0) | 2021.12.19 |
자바의 정석 - 배열(Array) (0) | 2021.12.14 |