객체는 속성과 기능으로 이루어져 있으며 속성에는 변수가 기능에는 메서드가 포함된다. 자동차의 크기나 길이 등을 속성이라 할 수 있고 가속, 브레이크를 기능이라 할 수 있다.
변수는 선언되는 위치에 따라 멤버변수와 지역변수로 분류된다.
멤버변수란 클래스 영역에서 선언된 변수이고 static이 붙으면 클래스 변수, 없으면 인스턴스 변수라 한다.
지역변수란 클래스 외의 영역, 메서드 영역에 있는 변수를 지역변수라고 한다.
class Example{
int a = 3;
static int b = 4;
void variable(){
int c = 5;
}
이 클래스에서 a는 인스턴스 변수, b는 클래스 변수, c가 지역변수이다.
각 변수의 특징을 알아보자
사용하기 위해서는 우선 인스턴스를 생성해야 한다. 인스턴스 생성 시 생성되며 인스턴스를 생성하지 않으면 사용이 불가능하다.
인스턴스는 독립적인 저장공간을 가지므로 서로 다른 값을 가질 수 있다. 예를 들어, 클래스에서 만든 인스턴스1에는 a가 1이고 인스턴스2에서는 a가 3이 될 수 있다.
Heap 영역에 인스턴스가 저장되기 때문에 이런 기능이 가능한 것이다.
선언시 변수 앞에 static을 붙여서 선언하다. 클래스 변수는 모든 인스턴스가 공통된 저장공간을 공유하기 때문에 서로 다른 값을 가질 수 없다. 인스턴스 변수와 다른 점은 인스턴스를 생성하지 않고 사용이 가능한 점이다. 클래스가 메모리에 로딩될 때 생성되며 Heap 영역이 아닌 Stack 영역에 저장된다. "클래스명.변수명"으로 바로 사용이 가능하다.
메서드 내에서만 사용 가능하며 메서드가 종료되면 소멸되어 사용이 불가하다.
메서드란 특정 작업을 수행하는 일련의 문장들을 하나로 묶은 것으로 함수와 유사하다.
왜 메서드를 사용하는가?
1) 높은 재사용성
한 번 만들어놓은 메서드는 언제는 호출이 가능하다
2) 중복된 코드 제거
반복되는 문장들을 묶어서 하나의 메서드로 작성해두면 코드의 양이 줄어 오류 발생 가능성이 감소한다
3) 프로그램 구조화
작업단위로 나누어서 메서드를 만들면 main 메서드가 간단해진다
public static void main(String args[]){
int[] numArr = new int[10];
initArr(numArr);
printArr(numArr);
sortArr(numArr);
main 메서드는 프로그램 전체 흐름이 한 눈에 들어올 정도로 단순화하는 것이 좋다
반환타입 메서드명 (타입 변수명) //선언부
{ 메서드 호출시 수행될 코드 } //구현부
기본적인 메서드 구조는 위와 같고 모든 메서드에는 적어도 하나의 return문이 있어야 한다
int method1(int a, int b){
return a + b;
}
반환타입이 void인 경우 return문이 없어도 아무런 문제가 없었던 이유는 컴파일러가 자동으로 return을 추가해주었기 때문이다
void method2(){
return;
}
메서드가 호출 시 호출스택에 메모리가 할당된다. JVM은 용도에 따라 크게 3가지 영역으로 나누어 메모리를 관리하는데 메서드 영역, 힙 영역, 호출스택 영역이 있다.
Method Area
클래스 데이터, 클래스 변수가 저장
Heap Area
인스턴스가 모두 이곳에 저장된다. 인스턴스 변수가 생성되는 공간이다
Call Stack Area
메모리가 호출되면 메모리 공간을 제공하고 메모리가 작업을 마치면 할당되어 있던 공간은 반환되어 지워진다(지역변수도 마찬가지)
메서드를 호출할 때 매개변수의 타입이 기본형일 경우 기본형 값이 복사되지만, 참조형일 경우 인스턴의 주소가 복사된다
이게 무슨 의미냐면 메서드의 매개변수로 기본형을 선언하면 값을 읽는 것은 가능하지만 변경은 불가능하다
class Data{
int x;
}
class PrimitiveParam{
public static void main(String args[]){
Data d = new Data();
d.x = 10;
change(d.x);
System.out.println(d.x);
}
static void change(int x){
x = 100;
}
}
위의 코드를 실행하면 100이 아닌 10이 출력이 된다. 메서드가 종료되면서 매개변수 x가 Call Stack에서 제거되었기 때문이다.
그렇다면 값을 변경한채로 유지하려면 어떻게 해야할까?
매개변수의 타입을 참조형으로 선언하면 된다
class Data{
int x;
}
class PrimitiveParam{
public static void main(String args[]){
Data d = new Data();
d.x = 10;
change(d);
System.out.println(d.x);
}
static void change(Data d){
d.x = 100;
}
}
참조형으로 매개변수로 받으면 인스턴스의 주소값을 받기 때문에 메서드가 종료되어 Call Stack에서 매개변수가 제거되어도 변경된 결과가 유지된다
그렇다면 매개변수가 배열일 경우에는 어떨까?
배열도 참조형이기 때문에 메서드를 이용하여 값을 변경하면 원래 배열에도 영향을 미칠 수 있다
public class ReferenceParamEX {
public static void main(String[] args) {
int[] numArr = new int[10];
for(int i = 0; i < numArr.length; i++) {
numArr[i] = (int)(Math.random() * 10);
}
for(int i = 0; i < numArr.length; i++) {
System.out.print(numArr[i]);
}
System.out.println();
change(numArr);
for(int i = 0; i < numArr.length; i++) {
System.out.print(numArr[i]);
}
}
static void change(int[] numArr) {
for(int i = 0; i < numArr.length - 1; i++) {
for(int j = 0; j < numArr.length - 1 - i; j++) {
if(numArr[j] > numArr[j+1]) {
int temp = numArr[j];
numArr[j] = numArr[j+1];
numArr[j+1] = temp;
}
}
}
} // 버블정렬 해주는 메서드
}
여기서 처음에 numArr 배열에 랜덤으로 10개의 수를 대입하여 출력한 것과 버블정렬을 해주는 change 매서드에 입력 후 출력한 결과를 비교해보면 값이 다른 것을 알 수 있다
정리하자면, 메서드의 입력매개변수로 참조형을 선언하면 값을 읽거나 변경이 가능하다
만약, 메서드 내에서 객체를 생성하고 이를 main 메서드에서 사용하려면 어떻게 해야할까?
class Data{
int x;
}
public class ReferenceReturnType {
public static void main(String[] args) {
Data d = new Data();
d.x = 10;
copy(d);
System.out.println(d.x);
System.out.println(tmp.x); // 컴파일 에러 발생
}
static void copy(Data d) {
Data tmp = new Data();
tmp.x = 20;
}
}
copy 메서드에서 Data 클래스의 인스턴스 tmp를 생성한 후 인스턴스 변수에 20을 대입하였다. main 메서드에서는 tmp 인스턴스의 변수값을 호출하려고 했지만 컴파일 오류가 발생하였다
왜 이런 일이 발생하냐면 copy 메서드가 종료되면서 Call Stack에서 사라졌기 때문이다. 이를 해결하려면 copy 메서드를 return 타입이 참조형인 메서드로 바꿔줘야한다
class Data{
int x;
}
public class Review {
public static void main(String[] args) {
Data d = new Data();
d.x = 10;
Data d2 = copy(d);
System.out.println(d.x);
System.out.println(d2.x);
}
static Data copy(Data d) {
Data tmp = new Data();
tmp.x = 20;
return tmp;
}
}
이렇게 return 타입을 참조형으로 바꾸어주면 주소값을 반환하게 되고 그 주소값을 d2가 받게 되어 main 메서드에서도 사용이 가능하다.
재귀호출이란 메서드의 구현부에서 자기 자신인 메서드를 호출하는 것을 말한다.
하지만, 자기 자신을 호출하게 되면 호출된 자신도 다시 자신을 호출하는 루프에 빠지게 된다. 따라서, 조건문이 필수적으로 따라다녀야한다.
void method(){
method();
} // 무한루프 발생
void method2(int n){
if(n == 0)
return; // n의 값이 0일 때 메소드를 종료한다
System.out.println(n);
method(--n); // 재귀호출
메서드를 호출하는 것은 반복문보다 더 많은 과정을 거치지 때문에 재귀호출의 수행시간이 반복문보다 오래걸린다. 그럼 왜 사용할까? 논리적 간결함 때문에 그렇다. 반복문과 조건문을 여러 가지 섞는 것보다 재귀호출을 사용하는 것이 단순한 구조로 만들기 쉽다. 하지만, 컴파일 시간이 오래걸리기 때문에 논리적 간결함의 이득이 수행시간보다 더 클 경우에 사용하는 것이 좋다.
예시로 작성한 코드에는 허점이 존재하는데 n이 음수이거나 int에서 가장 큰 100,000 이상일 경우 Stack Overflow Error가 발생한다. 사용자가 알아서 잘 입력하겠지라는 믿음보다는 어떤 값이 입력되어도 에러없이 처리되는 코드를 작성할 수 있어야 한다
클래스 메서드는 메서드 앞에 static이 붙어 클래스명.메서드명으로 객체 생성 없이 호출 가능한 메서드이다. 같은 클래스 내에서는 클래스명 없이 호출 가능하다.
인스턴스 메서드는 객체 생성이 필수적이며 참조변수명.메서드명으로 호출한다.
class InstanceMethod{
int a = 10; // 인스턴스 변수
void InstanceMethod() {
System.out.println("인스턴스 메서드");
}
static void StaticMethod() {
System.out.println("클래스 메서드");
}
}
public class Review {
public static void main(String[] args) {
InstanceMethod IM = new InstanceMethod();
IM.InstanceMethod();
InstanceMethod.StaticMethod();
}
}
InstanceMethod가 인스턴스 메서드, StaticMethod가 클래스 메서드이다. 그렇다면 언제 인스턴스 메서드를 사용하고 언제 클래스 메서드를 사용할까?
인스턴스 변수는 객체를 생성해야만 만들어지므로 인스턴스 변수를 필요로 하는 경우 인스턴스 메서드를 사용한다.
반면, 객체 생성이 필요없는 클래스 메서드는 객체 생성이 필요한 인스턴스 변수와 관계없는 메서드로서 사용한다.
다음 예시를 보자
class MethodClass{
int a = 10; // 인스턴스 변수
int b = 20;
int InstanceMethod() {
return a + b;
}
static int StaticMethod(int a, int b) {
return a + b;
}
}
public MethodTest {
public static void main(String[] args) {
InstanceMethod IM = new InstanceMethod();
int result1, result2;
result1 = IM.InstanceMethod();
result2 = InstanceMethod.StaticMethod(1,2);
System.out.println(result1);
System.out.println(result2);
}
}
InstanceMethod는 인스턴스 변수인 a,b만으로 작업이 가능하기 때문에 매개변수를 필요로 하지 않는다.
하지만, StaticMethod는 인스턴스 변수 없이 매개변수만으로 작업을 수행하기 때문에 static을 붙여서 클래스 메서드로 선언하였다.
만약, InstanceMethod에 매개변수를 넣었으면 매개변수로 작업을 수행하였을 것이다. 하지만, 굳이 인스턴스 메서드로 하는 것보다는 클래스 변수로 작업을 수행하는 것이 더 효율적이다.
클래스 설계 시 다음 사항들을 지켜야 한다
모든 인스턴스에 공통으로 사용되는 것에는 static을 붙여야 한다.
예를 들어, 어느 한 클래스에 대하여 인스턴스 a,b를 생성할 경우 a와 b는 서로 독립적이기 때문에 각 인스턴스 변수는 다른 값을 가진다. 하지만, 공통된 특성을 가져야 할 경우에는 서로 같은 값이어야 하므로 static을 사용하면 Method Area에 저장되어 항상 같은 값을 사용할 수 있다.
클래스 메서드는 인스턴스 변수를 사용할 수 없다. 앞에서도 설명했지만 인스턴스 변수는 객체 생성이 필요하기 때문에 객체 생성이 필요없는 클래스 메서드가 호출되었을 때 인스턴스가 존재하지 않을 수 있다. 그래서 클래스 메서드에서 인스턴스 변수 사용을 금지한다. 반대의 경우는 가능하다(인스턴스 메서드에서 클래스 변수 사용)
메서드 내에서 인스턴스 변수를 사용하지 않는다면 static을 붙이는 것이 좋다. 인스턴스 메서드는 객체 생성이 필요하고 실행 시 호출되어야 할 메서드를 찾는 과정이 추가적으로 필요하기 때문에 시간이 더 걸린다.
위와 비슷한 맥락의 내용이다. 같은 클래스에 속한 멤버들 간에는 별도의 인스턴스를 생성하지 않고도 서로 참조/호출이 가능하다. 단, 클래스 멤버가 인스턴스 멤버를 참조/호출 시에는 객체 생성이 필수다.
클래스 멤버가 존재하는 시점에 인스턴스 멤버가 존재하지 않을 수 있기 때문이다.
class Test{
void instanceMethod(){}
static void staticMethod(){}
int iv; //인스턴스 변수
static int cv; //클래스 변수
//인스턴스 메서드
void example(){
instanceMethod(); //가능
staticMethod(); // 가능
System.out.println(iv); //가능
System.out.println(cv); //가능
}
//클래스 메서드
static void example2(){
instanceMethod(); // 불가
staticMethod(); // 가능
System.out.println(iv); //불가
System.out.println(cv); //가능
}
하지만, 클래스 메서드에서 인스턴스 멤버를 사용하는 방법이 꼭 없는 것은 아니다.
메서드를 포함하고 있는 클래스의 인스턴스를 생성하여 호출하는 방법을 사용할 수 있다.
다음 예시를 보자.
class outer{
int iv = 3;
static void staticMethod(){
outer o = new outer();
System.out.println(o.iv);
}
}
그럼 이렇게도 생각해 볼 수 있다.
클래스 메서드에서 클래스의 인스턴스를 생성하고 인스턴스 변수를 호출해서 초기화하면 인스턴스 변수가 변경될 수 있나?
하지만, 이때 초기화 되는 것은 outer 클래스의 여러 인스턴스 중 하나의 인스턴스 변수뿐이다.