변수는 클래스변수, 인스턴스변수, 지역변수 세 종류이다.
변수의 종류를 결정짓는 요소는 '변수의 선언된 위치'이다.
멤벼변수를 제외한 나머지 변수들은 모두 지역변수이며, 멤버변수 중 static이 붙은 것은 클래스변수, 붙지 않은 것은 인스턴스 변수이다.
아래의 iv와 cv는 클래스 영역에 선언되어 있으므로 멤버변수이다.
그 중 cv는 키워드 static과 함께 선언되어 있으므로 클래스 변수이며, iv는 인스턴스변수이다.
lv는 메서드인 method()의 내부에 있으므로 지역변수이다.
class Variables {
int iv; // 인스턴스 변수
static int cv; // 클래스변수(static변수, 공유변수)
void method() {
int lv = 0; // 지역변수
}
}
→ 클래스 영역에 선언되며, 클래스의 인스턴스를 생성할 때 만들어짐.
독립적인 저장공간을 가지므로 서로 다른 값을 가질 수 있다.
→ 인스턴스변수 앞에 static을 붙이기만 하면 된다.
모든 인스턴스가 공통된 저장공간(변수)을 공유하게 된다.
인스턴스를 생성하지 않고도 바로 사용할 수 있으며, '클래스이름.클래스변수'와 같은 형식으로 사용
ex) 'Variables.cv'
클래스가 메모리에 로딩될 때 생성되어 프로그램이 종료될 때 까지 유지되며,
public을 앞에 붙이면 같은 프로그램 내에서 접근 가능한 '전역변수'의 성격을 가짐.
→ 메서드 내에 선언되어 메서드 내에서만 사용가능하며, 메서드가 종료되면 자동소멸.
클래스 변수와 인스턴스변수의 차이를 이해하기 위해 카드를 클래스로 정의해보자
class Card {
String kind; //무늬(인스턴스 변수)
int number; //숫자(인스턴스 변수)
static int width = 100; //폭(클래스변수)
static int height = 250;//높이(클래스변수)
}
자신만의 무늬(kind)와 숫자(number)을 유지해야 하므로 인스턴스변수로,
각 카드의 폭(width)와 높이(height)는 모든 인스턴스가 공통적으로 같은 값을 지녀야 하므로 클래스변수로 선언.
class CardTest{
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 + ")" );
System.out.println("c1의 width와 height를 각각 50, 80으로 변경합니다.");
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와 height는 인스턴스를 생성하지 않고도 '클래스이름.클래스변수'와 같은 방식으로 사용 가능.
"인스턴스변수는 인스턴스가 생성될 때 마다 생성되므로 인스턴스마다 각기 다른 값을 유지할 수 있지만,
클래스 변수는 모든 인스턴스가 하나의 저장공간을 공유하므로, 항상 공통된 값을 갖는다."
'메서드'는 특정 작업을 수행하는 일련의 문장들을 하나로 묶은 것이다.
→ 한번 만들어 놓은 메서드는 몇 번이고 호출 가능, 다른 프로그램에서도 사용 가능
→ 반복되는 문장들을 묶어서 하나의 메서드로 작성해 놓으면, 메서드를 호출하는 것으로 대체 가능.
→ 문장들을 작업단위로 나눠서 여러 개의 메서드에 담아 프로그램의 구조를 단순화
메서드는 '선언부'와 '구현부'로 이루어져 있다.
→ '메서드 이름', '매개변수 선언', '반환타입'으로 구성되어 있다.
→ 메서드가 작업을 수행하는데 필요한 값들을 제공받기 위한 것.
→ 변수의 타입이 같아도 변수 타입 생략 불가.
→ 결과인 반환값의 타입을 적는다.
→ 반환값이 없는 경우에는 반환타입으로 'void'를 적어야 한다.
→ 메서드의 반환타입이 'void'가 아닌 경우, 구현부{} 안에 'return 반환값;'이 반드시 포함되야함.
→ 이 문장은 반환값을 호출할 메서드로 전달하는데, 이 값의 타입은 반환타입과 일치하거나,
적어도 자동 형변환이 가능한 것이어야 한다.
int add(int x, int y) {
int result = x + y;
return result; // 반환값을 반환한다.
}
// 반환값(result)와 메서드(add)의 타입(int)가 일치한다.
→ 서로 다른 메서드라면 같은 이름의 변수를 선언해도 오케이.
메서드이름(값1, 값2...); //메서드를 호출하는 방법
print99danAll(); //void print99danAll()을 호출
int result = add(3,5); // int add(int x, int y)를 호출하고, 그 결과를 result에 저장
→ 인자의 개수와 순서는 호출된 메서드에 선언된 매개변수와 일치해야 한다.
→ 인자의 타입은 매개변수의 타입과 일치하거나 자동 형변환이 가능한 것이어야 한다.
→ 같은 클래스 내의 메서드끼리는 참조변수를 사용하지 않고도 서로 호출이 가능하지만,
static메서드는 같은 클래스 내의 인스턴스 메서드를 호출할 수 없다.
MyMath mm = new MyMath(); // 먼저 인스턴스를 생성.
long value = mm.add(1L, 2L); // 메서드를 호출한다.
long add(long a, long b){
long result = a + b;
return result;
}
반환값의 유무에 관계없이 모든 메서드에는 return문이 있어야한다.
그런데도 반환타입이 void인 경우, return문 없이도 문제가 없던 이유는 컴파일러가 자동적으로 return을 추가해주었기 때문.
→ return문의 반환값으로는 수식이 올수도 있다. 이 경우에는 수식을 계산한 결과가 반환된다.
→ 타입만 맞으면 어떤 값도 매개변수로 넘어올 수 있기 때문에 매개변수의 값이 적절한 것인지 확인해야 한다.
→ 클래스가 사용되면, JVM은 해당 클래스의 클래스파일을 읽어서 정보를 이곳에 저장한다.
이 때, 그 클래스의 클래스변수(cv)도 이 영역에 함께 생성된다.
→ 인스턴스가 생성되는 공간. 즉, 인스턴스변수(iv)들이 생성되는 공간이다.
→ 메서드가 호출되면 수행에 필요한 만큼의 메모리를 스택에 할당받는다.
→ 수행을 마치고 나면 메모리를 반환하고 제거됨.
→ 제일 위에 있는 메서드가 실행 중인 메서드.
→ 아래에 있는 메서드가 바로 위의 메서드를 호출한 메서드이다.
기본형 매개변수 : 변수의 값을 읽기만 할 수 있다.(read only)
참조형 매개변수 : 변수의 값을 읽고 변경할 수 있다.(read & write)
예제 6-9/PrimitiveParmaEx.java
class Data { int x; }
class PrimitiveParamEx {
public static void main(String[] args) {
Data d = new Data();
d.x = 10;
System.out.println("main() : x = " + d.x);
change(d.x);
System.out.println("After change(d.x)");
System.out.println("main() : x = " + d.x);
}
static void change(int x) { // 기본형 매개변수
x = 1000;
System.out.println("change() : x = " + x);
}
}
→ 'd.x'의 값이 변경된 것이 아니라, change메서드의 매개변수 x값이 변경된 것이다.
이처럼 기본형 매개변수는 변수에 저장된 값을 읽을 수만 있을 뿐 변경할 수는 없다.
class Data { int x; }
class ReferenceParamEx {
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);
}
static void change(Data d) { // 참조형 매개변수
d.x = 1000;
System.out.println("change() : x = " + d.x);
}
}
이전과는 달리 change메서드를 호출한 후에 d.x의 값이 변경되었다.
change메서드의 매개변수가 참조형이라서 '값이 저장된 주소'를 change메서드에 넘겨주었기 때문에
값을 읽는것뿐만 아니라 변경하는 것도 가능하다.
change메서드가 호출되면서 참조변수 d의 값(주소)이 매개변수 d에 복사됨
이제 매개변수 d에 저장된 주소값으로 x에 접근이 가능
change메서드에서 매개변수 d로 x의 값을 1000으로 변경
change메서드가 종료되면서 매개변수 d는 스택에서 제거됨
class ReturnTest {
public static void main(String[] args) {
ReturnTest r = new ReturnTest();
int result = r.add(3,5);
System.out.println(result);
int[] result2 = {0}; // 배열을 생성하고 result2[0]의 값을 0으로 초기화
r.add(3,5,result2); // 배열을 add메서드의 매개변수로 전달
System.out.println(result2[0]);
}
int add(int a, int b) {
return a + b;
}
void add(int a, int b, int[] result) {
result[0] = a + b; // 매개변수로 넘겨받은 배열에 연산결과를 저장
}
}
반환값이 있는 메서드를 반환값이 없는 메서드로 바꾸는 방법이다.
int add(int a, int b){
return a+b;
}
→
void add(int a, int b, int[] result){
result[0] = a+b;
}
반환타입도 참조형이 될 수 있다.
모든 참조형 타입의 값은 '객체의 주소'이다.
class Data { int x; }
class ReferenceReturnEx {
public static void main(String[] args)
{
Data d = new Data3();
d.x = 10;
Data d2 = copy(d);
System.out.println("d.x ="+d.x);
System.out.println("d2.x="+d2.x);
}
static **Data** copy(Data d) {
**Data** **tmp** = new Data3(); // 새로운 객체 tmp를 생성한다.
tmp.x = d.x; // d.x의 값을 tmp.x에 복사한다.
return **tmp**; // 복사한 객체의 주소를 반환한다.
}
}
반환하는 값이 Data객체의 주소이므로 반환 타입이 'Data'인것이다.
"반환타입이 '참조형'이라는 것은 메서드가 '객체의 주소'를 반환한다는 것을 의미한다."
void method() {
method(); //재귀호출. 메서드 자신을 호출한다.
}
무한반복문이 조건문과 함께 사용되어야 하는 것처럼, 재귀호출도 조건문이 필수적으로 따라다닌다.
매개변수 복사, 종료 후 복귀할 주소저장 등 추가로 필요한게 많아서 반복문보다 수행시간이 더 오래 걸린다.
그럼에도 사용하는 이유는 '논리적 간결함' 때문이다.
알아보기 쉽게 작성하는 것이 논리적 오류가 발생할 확률도 줄어들고 나중에 수정하기도 좋다.
하지만 비효율적이므로, "재귀호출이 주는 간결함" > "재귀호출에 드는 비용" 이 충분히 큰 경우에만 사용해야함.
재귀호출의 대표로 팩토리얼이 있다. 아래의 코드로 살펴보자.
class FactorialTest {
public static void main(String args[]) {
int result = factorial(4); // int result = FactorialTest.factorial(4);
System.out.println(result);
}
static int factorial(int n) {
if(n==1) return 1;
return n * factorial(n-1); //다시 메서드 자신을 호출한다.
}
}
factorial 메서드가 static메서드이므로 인스턴스를 생성하지 않고 직접 호출할 수 있다.
main메서드와 같은 클래스에 있기 때문에 클래스 이름 생략 가능
FactorialTest.factorial(4) → factorial(4)
"클래스의 멤버변수 중 모든 인스턴스에 공통된 값을 유지해야하는 것이 있는지 살펴보고 있으면, static을 붙여준다."
"작성한 메서드 중에서 인스턴스 변수나 인스턴스 메서드를 사용하지 않는 메서드에 static을 붙일 것을 고려한다."
클래스멤버가 인스턴스 멤버를 참조 또는 호출하는 경우에는 인스턴스를 생성해야 한다.
→ 그 이유는 인스턴스 멤버가 존재하는 시점에 클래스 멤버는 항상 존재하지만, 클래스 멤버가 존재하는 심점에 인스턴스 멤버가 존재하지 않을 수 도 있기 때문이다.
예제를 통해 확인해보자
class MemberCall {
int iv = 10;
static int cv = 20;
int iv2 = cv;
//static int cv2 = iv; //에러. 클래스변수는 인스턴스변수를 사용할 수 없음.
static int cv2 = new MemberCall().iv; // 이처럼 객체를 생성해야 가능.
static void staticMethod1() {
System.out.println(cv);
// System.out.println(iv); //에러. 클래스메서드에서는 인스턴스 변수 사용 불가
MemberCall c = new MemberCall();
System.out.println(c.iv); // 객체를 생성한 후에야 인스턴스 변수 참조가능.
}
void instanceMethod1() {
System.out.println(cv);
System.out.println(iv); //인스턴스메서드에서는 인스턴스변수 바로 사용 가능.
}
static void staticMethod2() {
staticMethod1();
// instanceMethod1(); // 에러. 클래스메서드에서는 인스턴스메서드 호출 불가.
MemberCall c = new MemberCall();
c.instanceMethod1(); // 인스턴스를 생성한 후에야 호출할 수 있음.
}
void instanceMethod2() { // 인스턴스메서드에서는 인스턴스메서드와 클래스메서드
staticMethod1(); // 모두 인스턴스 생성없이 바로 사용 가능
instanceMethod1();
}
}
"인스턴스멤버간의 호출에는 아무런 문제가 없다. 하나의 인스턴스멤버가 존재한다는 것은 인스턴스가 이미 생성되어있다는 것을 의미하며, 즉 다른 인스턴스멤버들도 모두 존재하기 때문이다.