Java/개념

참조타입

spring_sunshine 2023. 8. 9. 22:52

1. 참조타입과 참조변수

자바의 타입은 크게 기본타입과 참조타입으로 나뉜다.

기본타입: 정수, 실수, 문자, 논리 리터럴을 저장

참조타입: 객체의 번지를 참조하는 타입으로 배열, 열거, 클래스, 인터페이스를 말한다.

  • 번지를 통해 객체를 참조한다는 뜻에서 참조타입이라고 부른다.
  • 기본타입 변수는 스택 영역에 직접 값(실제값)을 가지고 있지만, 참조타입 변수는 스택 영역에 힙 영역의 객체 주소(메모리의 번지)를 가진다.
// 기본타입 변수
int age = 25;
double price = 100.5;

// 참조타입 변수
String name = "초코";
int[] scores = {10,20,30};

 

메모리 사용 영역

  • JVM이 사용하는 메모리 영역은 다음과 같은 세부 영역들로 구분된다.
    • 메소드 영역
      • JVM이 시작할 때 생성되고 모든 스레드가 공유하는 영역
      • 코드에서 사용되는 클래스별로 정적필드, 상수, 메소드 코드, 생성자 코드 등을 분류해서 저장한다.
    • 힙 영역
      • 객체와 배열이 생성되는 영역
      • 여기서 생성된 객체와 배열은 JVM 스택 영역의 변수나 다른 객체의 필드에서 참조한다.
      • 만약 참조하는 변수나 필드가 없다면 JVM은 이것을 쓰레기로 취급하고 Garbage Collector을 실행시켜 자동으로 제거
    • JVM 스택
      • 메소드를 호출할 때마다 프레임을 추가하고, 메소드가 종료되면 해당 프레임을 제거하는 동작 수행
      • 프레임 내부에는 로컬변수 스택이 있고, 기본타입 변수와 참조타입 변수가 추가되거나 제거 된다.
      • 스택 영역에 변수가 생성되는 시점은 초기화 될 때, 즉 최초로 변수에 값이 저장될 때이다.
      • 변수는 선언된 블록 안에서만 스택에 존재하고 블록을 벗어나면 스택에서 제거된다.
  • 기본타입 변수는 스택 영역에 직접 값을 가지고 있지만, 참조타입 변수는 스택 영역에 힙 영역의 객체 주소를 가진다.

 

참조변수의 ==, != 연산

  • 기본타입 변수의 ==, != 연산은 변수의 값이 같은지 여부를 알아보지만 참조타입 변수의 ==, != 연산은 동일한 객체를 참조하는지, 다른 객체를 참조하는지 알아본다.
  • 참조타입 변수의 값은 힙 영역의 객체 주소이므로 결국 ==, != 연산은 번지값을 비교하는 것이다. (동일한 번지값 → 동일한 객체 참조)

 

null과 NullPointerException

  • 참조변수는 힙 영역의 객체를 참조하지 않는다는 뜻으로 null을 가질 수 있다.
  • null도 초기값으로 사용되기 때문에 null로 초기화된 참조변수는 스택 영역에 생성된다.
  • 참조변수가 null 값을 갖는지 확인하려면 ==, != 연산을 수행하면 된다.
  • 참조변수를 사용하면서 가장 많이 발생하는 예외 중 하나로 NullPointerException이 있다. 이 예외는 참조변수를 잘못 사용하면 발생한다.
#1
int[] intArray = null;
intArray[0] = 10;
// NullPointerException 발생: intArray 변수가 참조하는 배열 객체가 없기 때문

#2
String str = null;
System.out.println("총 문자수:"+str.length());
// NullPointerException 발생: str 변수가 참조하는 String 객체가 없기 때문

NullPointerException 발생 시 해결 방법은 참조변수를 추적해서 객체를 참조하도록 수정하는 것이다.

 

String 타입

  • 문자열을 String 변수에 저장되는 것이 아니라, 문자열은 String 객체로 생성되고 변수는 그 String 객체를 참조하는 것이다.
  • String hobby = "자바"; 에서 hobby 변수에는 String 객체의 번지 값이 저장된다.
  • 자바는 문자열 리터럴이 동일하다면 String 객체를 공유하도록 되어있다.
  • String 변수는 참조타입이므로 초기값으로 null을 가질 수 있는데, 이는 String 변수가 참조하는 String 객체가 없다는 뜻이다.
String name1 = "초코";
String name2 = "초코";

 

new 연산자

  • 일반적으로 변수에 문자열을 저장할 때 문자열 리터럴을 사용하지만, new 연산자를 사용해서 직접 String 객체를 생성할 수도 있다.
  • new 연산자는 힙 영역에 새로운 객체를 만들 때 사용하는 연산자로 객체 생성 연산자라고 부른다.
String name1 = new String("초코");
String name2 = new String("초코");

 

문자열 리터럴로 생성하느냐 new 연산자로 생성하느냐에 따라 비교 연산자의 결과는 달라질 수 있다.

String name1 = "초코";
String name2 = "초코";
String name3 = new String("초코");​
  • name1과 name2은 동일한 문자열 리터럴로 생성된 객체를 참조하기 때문에 name1==name2의 결과는 true가 된다.
  • name3은 String 객체를 별도로 생성했기 때문에 name1==name3은 false가 된다.
  • 동일한 String 객체이건, 다른 String 객체이건 상관없이 내부 문자열을 비교하고 싶을 때는 String 객체의 equals() 메소드를 사용하면 된다. equals()는 원본 문자열과 매개값으로 주어진 비교 문자열이 동일한지 비교하고 true/false를 리턴한다.
boolean result = str1.equals(str2);

 

2. 배열

배열의 특징

  • 배열은 같은 타입의 데이터만 저장할 수 있다.
  • 한번 생성된 배열은 길이를 늘리거나 줄일 수 없다.

배열 선언은 다음과 같이 2가지 형식이 있다.

  1. 타입[] 변수;
  2. 타입 변수[];

 

배열 변수

  • 배열 변수는 참조변수에 속한다.
  • 배열도 객체이므로 힙 영역에 생성되고, 배열 변수는 힙 영역의 배열 객체를 참조하게 된다. 
  • 참조할 배열 객체가 없다면 배열 변수는 null 값으로 초기화될 수 있다.

배열 생성

배열 객체를 생성하려면 1) 값 목록을 이용하거나 2) new 연산자를 이용하는 방법이 있다.

 

1) 값 목록으로 배열 생성

  • 값 목록이 있으면 타입[] 변수 = {값0, 값1, 값2, ...}; 와 같이 간단하게 배열 객체를 생성할 수 있다.
  • 중괄호{}는 주어진 값들을 항목으로 갖는 배열 객체를 힙 영역에 생성하고, 배열 객체의 번지를 리턴한다.
  • 배열 변수는 리턴된 번지를 저장함으로써 참조가 이루어진다.
  • 값 목록으로 배열 객체를 생성할 때 주의할 점은, 배열 변수를 이미 선언한 후에는 다른 실행문에서 중괄호를 사용한 배열 생성이 허용되지 않는다는 점이다.
  • 배열 변수를 미리 선언한 후 값 목록들이 나중에 결정되는 상황이라면, 변수 = new 타입[] {값0, 값1, 값2, ...}; 와 같이 new 연산자를 사용하여 값 목록을 지정해주면 된다.
#1 배열 변수 미리 선언 후 다음 실행문에서 {}를 사용한 배열 생성 에러
String[] names;
names = {"가", "나", "다"}; // 컴파일 에러

#2 배열 변수를 미리 선언 후 값 목록이 나중에 결정
names = new String[] {"가", "나", "다"};
  • 메소드의 매개값이 배열인 경우도 아래와 같이 매개변수로 int[] 배열이 선언된 add() 메소드가 있을 경우, 값 목록으로 배열을 생성함과 동시에 add() 메소드의 매개값으로 사용하고자 한다면 반드시 new 연산자를 사용해야 한다.
int add(int[] scores){...}

int result = add({95,80,90}); // 컴파일 에러
int result = add(new int[] {95,80,90});

 

2) new 연산자로 배열 생성

  • 값 목록은 없지만, 향후 값들을 저장할 배열을 미리 만들고 싶다면 타입[] 변수 = new 타입[길이]; 로 배열 객체를 생성할 수 있다.
  • 이미 변수가 선언된 경우에도 new 연산자로 배열을 생성할 수 있다.
타입[] 변수 = null;
변수 = new 타입[길이];
  • new 연산자로 배열을 처음 생성한 경우 배열은 자동적으로 기본값(0)으로 초기화된다.
#1 int 배열: 기본값 0
int[] scores = new int[30];

#2 String 배열: 기본값 null
String[] names = new String[30];
  • 배열이 생성된 후 특정 인덱스 위치에 새로운 값을 저장하려면 대입 연산자를 사용하면 된다.
int[] scores = new int[3];
scores[0]=90;
scores[1]=85;
scores[2]=80;

 

배열 길이

  • 배열 길이는 배열에 저장할 수 있는 전체 항목의 개수를 말한다.
  • 배열 변수.length; 와 같이 배열 객체의 length 필드를 통해 배열의 길이를 얻을 수 있다.
  • length는 읽기 전용 필드이기 때문에 값을 바꿀 수 없다.
int[] scores = {83,90,87};
int sum=0;
// length는 배열전체를 loop할때 유용함
// 배열의 마지막 인덱스는 배열의 길이보다 1이 작다 (배열의 인덱스 범위:0~길이-1)
for(int i=0;i<scores.length;i++){
	sum+=scores[i];
}
System.out.println("총합:"+sum);

 

다차원 배열

int[][] scores = new int[2][3]; // 2*3 행렬 (세로*가로)

scores.length // 2
scores[0].length // 3
scores[1].length // 3
  • 이 코드는 메모리에 3개의 배열 객체를 생성한다.
    • int 타입 배열 A: length가 2, 배열 A의 scores[0]은 배열 B를 참조하고 scores[1]은 배열 C를 참조함
    • int 타입 배열 B: length가 3, scores[0].length임
    • int 타입 배열 C: length가 3, scores[1].length임

이런 형태의 배열에서 주의할 점은 배열의 정확한 길이를 알고 인덱스를 사용해야 한다는 것이다.

 

int[][] scores = {{95,80},{92,96}};

int score = scores[0][0]; // 95
int score = scores[1][1]; // 96

 

객체를 참조하는 배열

  • 참조타입(클래스, 인터페이스) 배열은 각 항목에 객체의 번지를 가지고 있다.
  • String은 클래스이므로 String[] 배열은 각 항목에 문자열이 아니라, String 객체의 번지를 가지고 있다.
String[] strArr = new String[3];
strArr[0] = "Java";
strArr[1] = "Java";
strArr[2] = new String("Java");

System.out.println(strArr[0]==strArr[1]); // true
System.out.println(strArr[0]==strArr[2]); // false
System.out.println(strArr[0].equals(strArr[2])); // true

 

배열 복사

  • 배열은 한번 생성하면 크기를 변경할 수 없기 때문에 더 많은 저장공간이 필요하면, 더 큰 배열을 새로 만들고 이전 배열로부터 항목의 값들을 복사해야 한다.
  • 배열 간의 항목 값들을 복사하려면 for문을 사용하거나 System.arraycopy() 메소드를 사용한다.
  • 참조타입 배열이 복사되면 복사되는 값이 객체의 번지이므로, 새 배열의 항목과 이전 배열의 항목이 참조하는 객체는 동일하다.

1. for문으로 배열 복사

int[] oldArr = {1,2,3};
int[] newArr = new int[5];

for(int i=0; i<oldArr.length; i++){
	newArr[i] = oldArr[i];
}
for(int i=0; i<newArr.length; i++){
	System.out.println(newArr[i]+","); // 1,2,3,0,0
}

2. System.arraycopy()

System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length);

  • Obejct src: 원본 배열
  • int srcPos: 원본 배열에서 복사할 항목의 시작 인덱스
  • Object dest: 새 배열
  • destPos: 새 배열에서 붙여넣을 시작 인덱스
  • length: 복사할 개수
String[] oldArr = {"java","array","copy"};
String[] newArr = new String[5];

System.arraycopy(oldArr, 0, newArr, 0, oldArr.length);
for(int i=0; i<newArr.length; i++){
	System.out.print(newArr[i]+","); // java,array,copy,null,null
}

 

향상된 for문

  • 향상된 for문은 반복 실행을 하기위해 루프 카운터 변수와 증감식을 사용하지 않는다.
  • for문의 괄호에는 배열에서 꺼낸 항목을 저장할 변수 선언, 콜론, 배열을 나란히 작성한다.
  • 배열 및 컬렉션 항목의 개수만큼 반복하고, 자동적으로 for문을 빠져나간다.
int[] scores = {95,71,84,93,87};
int sum = 0;
for(int score:scores){
	sum = sum+score;
}
System.out.println("점수 총합:"+sum);

 

3. 열거 타입

  • 열거타입은 한정된 값인 열거상수 중에서 하나의 상수를 저장하는 타입이다.
  • 다음은 7개의 값을 갖는 요일 데이터의 열거타입 Week을 선언한 것이다.
public enum Week{
	MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY
}
  • Week today; 와 같이 변수를 선언할 수도 있다.
  • today 변수에 저장할 수 있는 것은 Week에 선언된 7개의 열거상수 중 하나이다. ex) today = Week.FRIDAY;

 

열거타입 선언

  • 열거타입을 선언하기 위해서는 먼저 열거타입의 이름을 정하고, 해당 이름으로 소스파일(.java)을 생성해야 한다.
  • 열거타입의 이름은 보통 첫글자는 대문자, 나머지는 소문자로 구성한다.
// 열거타입 소스파일의 이름
Week.java
LoginResult.java

// 소스파일의 내용
public enum Week {MONDAY,TUESDAY,...}
public enum LoginResult {LOGIN_SUCCESS,LOGIN_FAILED}

 

열거타입 변수

  • 열거타입도 하나의 타입이므로 변수를 선언하고 사용해야 한다.
  • 열거타입 변수를 선언했다면 다음과 같이 열거상수를 저장할 수 있다. 열거상수는 단독사용이 안되고 반드시 '열거타입.열거상수' 형태로 사용된다.
  • 열거타입도 참조타입이기 때문에 변수에 null도 저장할 수 있다.
  • 참조타입의 변수는 객체를 참조하는 변수이고, 열거상수도 열거객체들로 생성된다.
  • 열거타입 변수 Week의 경우 MONDAY~SUNDAY까지의 열거상수가 총 7개의 Week 객체로 생성된다. 그리고 메소드 영역에 생성된 열거상수가 해당 Week 객체를 각각 참조하게 된다.
Week today = Week.SUNDAY;
  • 열거타입 변수 today는 스택 영역에 생성된다.
  • today에 저장되는 값은 Week.SUNDAY 열거상수가 참조하는 Week 객체의 번지이다.
  • 따라서 열거상수 Week.SUNDAY와 today 변수는 서로 같은 객체를 참조하게 된다.
Week week1 = Week.MONDAY;
Week week2 = Week.MONDAY;
System.out.println(week1==week2); // true