데이터 타입을 정리하면 다음과 같다.
- 열거 타입
/ - 배열 타입
참조타입 - 클래스(문자열 등등)
/ \ - 인터페이스
/
/
데이터 타입
\
\
\
기본 타입(primitive type)
- 정수 타입(byte, char, short, int, long)
- 실수 타입(float, double)
- 논리 타입(boolean)
기본 타입은 변수 자체에서 값을 저장하고 있지만, 참조타입(reference type)은 객체가 생성된 메모리를 참조하는 주소를 저장한다.
primitive 변수 --> 값
reference 변수 --> 객체 메모리 주소 --> 객체(값)
객체들이 생성되어 관리되는 메모리 영역을 '힙(heap)'이라고 한다. 힙(heap)안에 객체가 생성되고, 참조 변수는 이 heap에 있는 '객체의 메모리 주소'를 가져오는 것이다.
자바에서 사용하는 메모리 영역에 대해서 간단히 알아보자, java 명령어로 JVM이 구동되면 JVM은 운영체제에서 할당받은 메모리 영역(memory data area)를 다음과 같이 구분해서 사용한다.
1. Method area: byte code 파일 내용이 저장되는 영역으로 클래스별 상수, 정적 필드, 메서드 코드, 생성자 코드 등이 저장된다. 즉, 클래스 코드 자체를 담는다고 생각하면 된다.
2. Heap area: 객체가 생성되는 영역이다. 객체의 번지는 method area와 stack area의 상수와 변수에서 참조할 수 있다.
3. Stack area: method를 호출할 때마다 생성되는 frame이 저장되는 영역이다. 메서드가 끝나면 frame이 자종으로 제거되기 때문에, 이 안의 변수들은 사라진다. frame 내부에는 로컬 변수 스택이 있다. 여기에서 기본 타입과 참조 타입 변수가 생성되었다면 frame이 제거될 때 사라진다.
--------------------메모리 영역------------------
| ----------------메서드 영역----------------- |
| | -----class1----- ---class2----- | |
| | | 상수와 정적 필드| | 상수와 정적 필드| | |
| | | 메서드 코드 | | 메서드 코드 | | |
| | | 생성자 코드 | | 생성자 코드 | | |
| | ---------------- --------------- | |
| ----------------------------------------- |
| |
| ------------Haep 영역-------------------- |
| | 객체1, 객체2, 객체3, 객체4, ... 객체 n | |
| ---------------------------------------- |
| |
| -----Thread1---- -----Thread2---- |
| | local stack | | local stack | |
| ---------------- ---------------- |
----------------------------------------------
각 thread마다 stack 영역을 가지고 있다는 사실을 잊지 말도록 하자. heap과 method area는 thread와 별개로 존재한다.
참조 타입 변수의 ==
, !=
연산에 대해서 조심해야한다. 이는 메모리 주소를 비교하는 작업이고, 메모리 주소가 같다면 같은 객체를 참조하므로 같다고 할 수 있다.
//TIP To <b>Run</b> code, press <shortcut actionId="Run"/> or
// click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter.
public class Main {
public static void main(String[] args) {
Temp t1 =new Temp(10);
Temp t2 =new Temp(10);
Temp t3 = t2;
System.out.println(t1 == t2); // false
System.out.println(t2 == t3); // true
}
}
class Temp {
int age;
public Temp(int age) {
this.age = age;
}
}
t1
과 t2
는 같은 age
값을 가지고 있어도, 서로 다른 객체로 생성되었으므로 t1
과 t2
가 가리키는 메모리의 주소(레퍼런스)는 서로 다르다. 따라서, false
가 나오는 것이다.
반면에 t2
와 t3
는 같은 메모리 주소를 참조하기 때문에 서로 같은 값으로 나온다. 따라서, true
가 나오는 것이다.
참고로 참조 변수(레퍼런스 변수)는 default로 null
을 가리키고 있다. 따라서, null
을 가리키고 있는 참조 변수에게 특정 메서드나, 맴버 변수를 호출하면 NullPointerException
이 발생하게 되는 것이다.
자바의 문자열은 String
객체로 생성된다. 따라서 문자열 리터럴로 할당하는 방법 말고도 new
를 사용하여 할당하는 방법을 사용할 수도 있다.
String str1 = new String("hello world");
String str2 = "hello world";
사실 문자열 리터럴 역시도 String
클래스를 통해서 객체를 생성하는 것이다. 따라서 문자열의 비교는 사실 문자열의 값을 비교하는 것이 아니라, 문자열 객체의 메모리 주소 값을 비교하는 것이 된다.
String str1 = new String("hello world");
String str2 = "hello world";
System.out.println(str1 == str2); // false
따라서, 문자열의 값으로 비교하기 위해서는 문자열 객체의 메서드인 equals
로 비교해야 정확한 것이다.
String str1 = new String("hello world");
String str2 = "hello world";
System.out.println(str1.equals(str2)); // true
참고로 String
객체에 빈 문자열인 ""
을 대입할 수도 있다. 빈 문자열도 String
객체로 생성되기 때문에 변수가 빈 문자열을 참조하는 지 조사하려면 equals
로 ""
인지 아닌지 비교해야한다.
문자열에서 특정 위치의 문자를 얻고 싶다면 charAt
메서드를 사용하면 된다. 매개변수로 주어진 index값에 해당하는 문자를 반환한다.
String str2 = "hello world";
System.out.println(str2.charAt(4)); // o
문자열의 길이는 length()
메서드를 사용하면 된다.
String str2 = "hello world";
System.out.println(str2.length()); // 11
문자열의 특정 문자열을 다른 문자열로 대체하고 싶다면 replace
메서드를 사용하면 된다. 단, replace()
메서드의 경우, 기존 문자열을 바꾸는 것이 아니라, 기존 문자열을 복사하여 대체한 새로운 문자열을 생성한다.
String str2 = "hello world";
String replaced = str2.replace("hello", "good bye");
System.out.println(str2); // hello world
System.out.println(replaced); // good bye world
원본 문자열은 바뀌지 않고, 기존 문자열에서 일부 문자열들을 바꾼 결과가 replaced
에 들어간 것을 볼 수 있다.
이는 heap 영역에 str2
가 가리키는 문자열 객체를 수정한 것이 아니라, 아예 새로운 객체를 생성하여 값을 수정한 것으로 알 수 있다.
문자열 슬라이싱으로는 substring
메서드를 사용하면 된다. 사용 방법은 두 가지가 있다.
1. substring(int start)
: start
인덱스부터 마지막까지 슬라이싱한다.
2. substring(int start, int end)
: start
인덱스부터 end
까지 슬라이싱한다. 단, end
까지는 포함하지 않는다. 즉, 마지막 인덱스는 포함하지 않는다.
String str2 = "hello world";
System.out.println(str2.substring(6)); // world
System.out.println(str2.substring(0,6)); // hello
결과를 보면 알 수 있듯이 원본을 바꾸는 것이 아니라 새로운 값을 생성 및 할당해주는 것을 알 수 있다.
문자열에서 특정 문자열의 위치를 알고 싶다면 indexOf
메서드를 사용하면 된다. 이를 사용하면 해당하는 문자열이 시작하는 index를 반환한다.
String str2 = "hello world";
System.out.println(str2.indexOf("world")); // 6
System.out.println(str2.indexOf("what")); // -1
단, 위와 같이 해당하는 문자열이 없다면 -1
을 반환한다.
만약, 주어진 문자열이 단순히 포함되어있는 지만 알고싶다면 contains
메서드를 사용하면 된다.
String str2 = "hello world";
boolean res = str2.contains("hello");
System.out.println(res); // true
문자열을 특정 구분자(delimeter)를 기준으로 여러 개의 문자열로 분리하고 싶다면 split
메서드를 사용하면 된다.
String board = "hello world my name is gyu";
String[] arr = board.split(" ");
for (String v : arr) {
System.out.println(v);
}
//hello
//world
//my
//name
//is
//gyu
split(" "")
은 공백을 기준으로 문자열을 나누어, 나누어진 문자열을 문자열 배열에 넣으라는 것이다. 여기서 만약에 구분자가 ,
이라면 ","
을 넣어주면 된다.
마지막으로 문자열의 문자들을 순회하는 두 가지 방법들이 있는데, 다음과 같다.
String board = "hello world my name is gyu";
for(int i = 0; i < board.length(); i++) {
System.out.println(board.charAt(i));
}
for (char c : board.toCharArray()) {
System.out.println(c);
}
둘 다 같은 결과를 낼 것이다. 두 번째 방법은 toCharArray
메서를 사용하여 String
객체를 char[]
로 만들어 반환하는 것이다.
파이썬과 달리 java의 배열은 같은 타입의 값만 관리한다. 또한, list가 아니므로 길이를 동적으로 늘리거나 줄일 수 없다.
배열을 선언하는 방법은 다음과 같이 두가지 방법이 있다.
int[] arr1;
int arr2[];
관례상 타입 바로 다음에 쓰는 int[]
을 쓴다. 물론 arr2[]
로 써도 문제는 없지만 c-style이라고 혼난다.
c,c++에서는 int arr[]
이고 go는 var arr []int
이런 식인데, 자바는 int[] arr
이니까 헷갈려 죽겠다.
배열은 참조 변수이다. 따라서, 배열도 객체이므로 힙 영역에 생성되고 배열 변수는 힙 영역의 배열 주소를 저장한다. 참조할 배열이 없다면 배열 변수도 null
로 초기화 할 수 있다.
int[] arr1 = null;
System.out.println(arr1 == null); // true
배열의 초기화는 다음과 같다.
int[] arr1 = {1,2,3,4};
for (int v: arr1) {
System.out.print(v + " "); // 1 2 3 4
}
문제는 배열 변수를 선언한 뒤에 대입으로 {1,2,3,4}
를 쓸 수 없다.
int[] arr1;
arr1 = {1,2,3,4}; // java: illegal start of expression
하지만 다음과 같은 방법으로는 변수를 선언한 다음에 대입이 가능하다.
int[] arr1;
arr1 = new int[]{1,2,3,4};
for (int v: arr1) {
System.out.print(v + " "); // 1 2 3 4
}
솔직히 마음에 안드는 문법이다. 별애별 이상한 API 만들지 말고 이러한 기본 문법 문제나 해결해주었으면 좋겠다.
이는 아주 재미난 코드를 만들 수 있는데, 가령 특정 함수의 매개변수가 배열이라면 배열 리터널을 사용할 때 다음이 안된다는 것이다.
public class Main {
public static void main(String[] args) {
printItem({1,2,3,4});
}
public static void printItem(int[] items) {
for (int v: items) {
System.out.print(v + " "); // 1 2 3 4
}
}
}
위의 코드는 에러가 발생한다. printItem
의 매개변수인 int[] items
에 들어갈 변수가 {1,2,3,4}
인데, 이는 배열 변수인 items
에 대입이 불가능하기 때문이다.
따라서, 다음과 같이 대입해주어야 한다.
public class Main {
public static void main(String[] args) {
printItem(new int[]{1,2,3,4});
}
public static void printItem(int[] items) {
for (int v: items) {
System.out.print(v + " "); // 1 2 3 4
}
}
}
이건 가능하다.
배열을 초기화할 때는 위와 같이 바로 값을 대입할 수도 있지만, 어떠한 값을 넣어야 할지 모를때도 있다. 이때 사용하는 것이 바로 배열 길이를 전달하는 방법이다.
int[] arr = new int[10];
int[] arr2 = null;
arr2 = new int[20];
int 배열 길이 10짜리 객체를 생성하고 그 메모리 주소를 arr
에 전달한 것이다. 이는 변수 선언과 동시에 초기화가 이루어진 경우이다. 반면에, arr2
은 변수 선언만 하고 값을 null
로 초기화한 다음에, 대입을 통해서 길이 20짜리 배열을 할당해준 것이다.
참고로 빈 배열에 초기화되는 값들은 다음과 같다.
1. 실수는 0.0, 정수는 0이다.
2. boolean은 false
이다.
3. 참조 타입은 null
이다.
따라서, 조심해야할 것은 String
객체로 이루어진 배열인 String[]
에 대한 배열을 생성하면 default로 모든 String
값들이 null
이라는 것이다. ""
이 아니라는 점에 유념하자.
배열의 길이는 length
메서드를 사용하면 된다.
int[] arr = {1,2,3,4};
System.out.println(arr.length); // 4
2차원, 3차원 배열을 생성하는 방법은 매우 간단하다. []
만 더 추가해주면 된다.
int[][] arr;
2차원 배열 레퍼런스 변수인 arr
가 만들어진 것이다. 초기화하는 방법은 다음과 같다.
int[][] arr = {
{1,2,3},
{4,5,6},
};
for (int[] line: arr) {
for(int v: line) {
System.out.print(v+" "); // 1 2 3 4 5 6
}
}
다음과 같이 리터럴한 배열을 바로 생성할 수도 있지만, new
를 통해서 배열 객체를 생성하는 방법도 있다.
int[][] arr = new int[][] {
{1,2,3},
{4,5,6},
};
for (int[] line: arr) {
for(int v: line) {
System.out.print(v+" "); // 1 2 3 4 5 6
}
}
같은 결과를 내는 것을 볼 수 있다.
그냥 배열 리터럴보다 new int[][] {}
으로 생성하는 편이 초기화, 대입 모두 가능하므로 적극 사용하는 것이 좋을 것 같다.
구체적인 값을 넣지않고 길이만 넣는 방법도 있다.
int[][] arr = new int[3][4];
for (int[] line: arr) {
for(int v: line) {
System.out.print(v+" "); // 0 0 0 0 0 0 0 0 0 0 0 0
}
}
new int[3][4]
로 row는 3이고 column은 4인 배열을 생성하여 할당해준 것이다. 값은 default로 채워진 것이다.
여기서 알아두어야 할 것은 int[3][4]
가 하나의 객체가 아니라는 것이다. 이는 int[0]
, int[1]
, int[2]
에 해당하는 int[4]
짜리 배열 객체를 가리키는 것일 뿐이다.
int[0] --> int[4] (0x111...)
int[1] --> int[4] (0x114...)
int[2] --> int[4] (0x118...)
이렇게 된 것이다.
즉 참조 객체의 참조 객체라는 것이다.
배열은 한 번 생성한 길이를 변경하지 못한다. 더 많은 공간이 필요하다면 더 큰 길이의 배열을 새로 만들고, 이전 배열로부터 항목들을 복사해야한다.
이를 위해서 지원하는 함수가 바로 System.arraycopy()
메서드이다. 사용 방법은 다음과 같다.
System.arraycopy(원본, 원본 복사 시작 인덱스, 복사할 대상, 복사할 대상 시작 인덱스, '원본 복사 시작 인덱스'에서 몇 개까지 복사할 지);
가령 다음의 예를 보도록 하ㅏㅈ.
int[] src = new int[]{1,2,3,4,5};
int[] target = new int[10];
System.arraycopy(src, 0, target, 0, src.length);
for (int v: target) {
System.out.print(v + " "); // 1 2 3 4 5 0 0 0 0 0
}
다음은 src
의 0번째 인덱스부터 5개의 값을 target
의 0번째 인덱스부터 복사하라는 발이다.
단, 언제나 복사는 조심해야하는데, 위의 예제처럼 원시 타입을 복사하는 경우는 src
와 target
이 서로 영향을 주지 않지만, 만약 참조 타입(레퍼런스 타입)이라면 이야기가 달라진다. 이 경우 같은 객체를 보고 있으므로, src
와 target
중 하나라도 수정하면 이것이 영향을 미치지게 되는 것이다.
public class Main {
public static void main(String[] args) {
Temp[] src = new Temp[] {new Temp(1), new Temp(2)};
Temp[] target = new Temp[2];
System.arraycopy(src, 0, target, 0, src.length);
for (Temp v: target) {
System.out.print(v.a + " "); // 1 2
}
System.out.println();
target[1].a = 18;
System.out.println(src[1].a);
}
}
class Temp {
int a;
public Temp(int a) {
this.a = a;
}
}
src
의 값을 target
에 모두 복사시키고나서, target
에서 target[1]
에 해당하는 객체의 맴버 변수 a
값을 수정한 것이 src
에도 영향을 미치는 것이다. 이는 레퍼런스 변수를 복사할 경우, 객체를 가리키는 메모리 주소가 복사되기 때문이다.
Heap
src[0] == target[0] ---> Temp(1);
src[1] == target[1] ---> Temp(18);
이렇게 되었기 때문이다. 따라서, 레퍼런스 변수에 대한 복사는 언제나 조심해야한다.
열거 타입은 특정 도메인이 한정된 경우들로 이루어질 때, 이들을 열거형으로 나열할 수 있다. 가령 일주일의 경우 '월, 화, 수, 목, 금, 토, 일' 7가지로 경우로 제한된 겨웅 열거 타입을 쓸 수 있다.
일반적으로 열거 타입은 새로운 파일을 만들어서 사용한다.
public enum Week {
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY,
SUNDAY
}
enum
으로 선언하고 Week
안에 각 경우들을 열거해주면 된다. enum
안의 요소들은 대문자로 쓰고, 단어와 단어 사이는 _
로 연결하는 것이 관례이다.
참고로 enum
도 class와 같이 객체로 치부되기 때문에 Week
타입을 가진 변수는 레퍼런스 타입이다.
Week week = null;
week = Week.SUNDAY;
if (week == Week.SUNDAY) {
System.out.println("Oh yeah!"); // Oh yeah!
}
다음과 같이 week
은 Week
라는 enum
타입을 가지므로 레퍼런스 변수가 된다. 따라서, null
값을 가질 수 있다.
enum
타입의 값은 enum
타입 이름으로 직접 접근하면 된다. 여기서는 Week.SUNDAY
로 직접 접근했다.
재미난 것은 enum
의 요소들은 각 고유한 객체이기 때문에 수정할 수 없다.