『자바의 신 3판』 을 읽고 내용 정리 및 공부한 내용을 정리한 글입니다.
서적: 자바의 신 3판 구입처
기본 자료형 8개를 제외한 나머지 타입은 모두 참조 자료형(Reference type)이다.
참조 자료형은 new를 사용하여 객체를 사용한다고 했는데, new 뒤에 나오는 것이 바로 생성자다.
TestClass obj = new TestClass();
자바의 생성자는 자바 클랙스의 객체(또는 인스턴스)를 생성하기 위해서 존재한다.
메소드와 선언 방식이 비슷한데, 선언부에 리턴 타입이 없고 메소드 이름 대신 클래스 이름과 동일하기 이름을 지정한다.
리턴 타입이 없는 이유는 클래스의 객체이기 때문이며, 클래스와 이름이 동일해야 컴파일러가 생성자인지 알 수 있기 때문이다.
💡 생성자를 선언할 때는 가독성을 위해 메소드들 보다 위에, 가장 윗부분에 선언하는 것이 좋다.
💡 생성자가 없더라도 객체를 얻을 수 있는 클래스도 있다.
자바는 생성자를 만들지 않아도 자동으로 만들어지는 기본 생성자가 있다. 그러나 기본 생성자는 다른 생성자가 있으면 자동으로 만들어지지 않는다.
그러므로, 기본 생성자와 다른 생성자를 같이 쓰고 싶다면 아래처럼 사용하면 된다.
public class TestClass {
public TestClass() {
}
public TestClass(int data) {
}
}
자바는 클래스의 객체를 보다 간편하게 만들기 위해서 여러 가지 매개 변수를 갖는 여러 생성자를 가질 수 있다. 생성자의 개수는 몇 개가 되어도 상관없다.
회원 클래스의 객체를 생성할 때, 이름, 전화번호, 이메일 중 일부만 알고 있을 수도 있다. 전부 알 수도 있고, 전부 모를 수도 있다.
이런 경우에 여러 개의 생성자를 DTO를 사용하면 모든 케이스에 대해 객체를 생성할 수 있다.
DTO는 Data Transfer Object의 약자로, 어떤 속성을 갖는 클래스를 만들고 그 속성들을 쉽게 전달하기 위한 자바의 패턴이다.
비슷한 클래스로 VO가 있다. VO는 Value Object로, DTO와 형태는 동일하지만 VO는 데이터를 담아 두기 위한 목적으로 사용되며 DTO는 다른 서버로 데이터를 전달하기 위한 것이 주 목적이다.
어떻게 보면 DTO가 VO를 포함한다고도 볼 수 있기 때문에 대부분 DTO라는 명칭을 선호한다.
💡 패턴
비슷한 기능을 하는 규칙에 하나의 이름을 정해 놓은 것이다.
자바의 메소드를 선언할 때 리턴 타입은 한 가지만 선언할 수 있다. 이때 DTO를 사용하면 타입이 다른 여러 개의 데이터(복합적인 데이터)를 전달할 수 있다는 장점이 있다.
public class MemberDTO {
public String name;
public String phone;
public String email;
public int age;
}
public MemberDTO methodName() {
MemberDTO member = new MemberDTO();
return member;
}
이렇게 사용하면 4개의 데이터를 리턴할 수 있다.
매개 변수를 케이스에 따라 받을 수 있도록 생성자를 여러 개 선언하면, 호출 시 아래처럼 호출할 수 있다.
public class MemberDTO {
public String name;
public String phone;
public String email;
public MemberDTO () {}
public MemberDTO (String name) {}
public MemberDTO (String name, phone) {}
public MemberDTO (String name, phone, email) {}
...
}
public MemberDTO methodName() {
MemberDTO member1 = new MemberDTO();
MemberDTO member2 = new MemberDTO("name");
MemberDTO member3 = new MemberDTO("name", "phone");
MemberDTO member4 = new MemberDTO("name", "phone", "email");
...
return member;
}
이렇게 생성자를 모두 활용하여 객체를 생성할 수 있다. 각 생성자로 만든 객체들은 서로 다른 속성값들을 갖고 있다.
객체의 변수와 매개 변수의 이름이 동일할 때, 인스턴스의 변수를 구분하기 위해 사용하는 예약어이다. 생성자와 메소드 안에서 사용할 수 있다.
이 this 예약어를 사용하는 것은 매개 변수 안에 있는 것이 아닌, “이 객체의 변수” 라고 명시적으로 지정해주는 것이다.
메소드에서도 this를 지정할 수 있는데, 이는 자바의 상속을 배운 후 알아본다.
public class MemberDTO {
public String name;
public String phone;
public String email;
public MemberDTO (String name) {
this.name = name;
}
}
생성자처럼 메소드도 이름은 같게 하고 매개 변수들을 서로 다르게 하여 선언할 수 있다.
중요한 것은 매개 변수의 이름이 아니라 매개 변수의 타입이다. 타입이 다르면 다른 메소드로 생각하지만, 타입이 같고 변수 이름이 같으면 같은 메소드로 인식한다.
public class ReferenceOberloading {
// main 생략
...
public void print(int data) {}
public void print(String data) {}
public void print(int data, String str) {}
public void print(String str, int data) {}
// 이 경우엔 타입과 개수가 같으므로 오류가 난다.
public void print(int data2) {}
}
오버로딩의 예시로는 System.out.println
와 System.out.printf
가 있다. println
의 경우, 사용할 때 int를 줘도 되고, long을 줘도 되고, String을 줘도 된다.
이렇게 메소드 오버로딩은 “같은 역할을 하는 메소드는 같은 메소드 이름을 가져야 한다” 는 모토로 사용한다.
자바에서 메소드가 종료되는 조건은 다음과 같다.
그동안 void
를 많이 썼는데, “이 메소드는 아무것도 돌려주지 않습니다.”라는 의미이다. 이렇게 선언했을 경우엔 모든 문장이 실행되면 메소드가 종료된다.
앞서말했듯 자바에서는 모든 기본 자료형과 참조 자료형 중 하나만 리턴 타입으로 넘겨줄 수 있다.
이때 값을 리턴하기 위해서 return 예약어를 사용한다. 리턴 타입이 void가 아니라면, 반드시 해당 리턴 타입을 return 해줘야 한다.
return type;
물론 void일 경우에도 더 이상 메소드를 실행하고 싶지 않을 경우에 return 을 사용해 종료할 수 있다. 이 경우에는 타입 없이 리턴 문장을 사용한다.
return ;
만약, 리턴 타입 뒤의 다른 코드가 있다면 “unreachable statement”라는 메시지와 함께 컴파일 에러가 발생한다. 즉, 리턴 문자 이후에는 어떤 문장도 있으면 안된다.
if문에서 return을 하는 상황에서는 예외이다. 조건에 맞을 경우에 return이 실행되지만, 조건이 맞지 않을 경우에는 실행되지 않기 때문이다. 때문에 이 경우에는 리턴 문장이 하나의 메소드 내에 두 개 이상 있어야 한다.
boolean flg = true;
if (flg) {
return "true";
} else {
return "false";
}
else 문을 생략하고 사용해도 상관없다.
static으로 선언된 메소드는 객체를 생성하지 않아도 호출할 수 있다. 하지만 static 메소드는 클래스 변수만 사용할 수 있다는 단점이 있다.
대표적인 예로 System.out.println
메소드가 있다.
error: non-static variable name cannot be referenced from a static context
왜냐하면 클래스 변수가 되면 모든 객체에서 하나의 값을 바라보기 때문이다. 때문에 두 개의 객체를 만들고, 값을 변경하면 두 객체 모두 변경된 값을 바라본다.
public class TestClass {
static String name;
public TestClass(String name) {
this.name = name;
}
}
public static void main(String[] args) {
TestClass test1 = new TestClass("default");
System.out.println(test1.name); // default 출력
TestClass test2 = new TestClass("change");
System.out.println(test1.name); // change 출력
}
객체를 여러 개를 생성할 때, 한 번만 호출되어야 하는 코드가 있다면 static 블록을 사용한다. 이 static 블록은 객체가 생성되기 전에 한 번만 호출되고, 그 이후에는 호출할 수가 없다.
클래스 내에 선언되어 있어야 하며, 메소드 내에서는 선언할 수가 없다. 또한, 내부에서는 static한 것들만 호출할 수 있다.
static {
// 딱 한번만 수행되는 코드
}
static 블록은 여러 개를 선언할 수 있으며, 선언된 순서대로 블록들이 차례로 호출되기 때문에 선언되어 있는 순서가 매우 중요하다.
클래스를 초기화할 때 꼭 수행되어야 하는 작업이 있을 경우 유용하게 사용될 수 있다. 생성자가 불리지 않더라도 클래스의 대한 참조가 발생하면 호출된다.
기본 자료형은 무조건 "Pass by Value"로 데이터를 전달한다. 그리고 참조 자료형은 “Pass by Reference”로 데이터를 전달한다
Pass by Value 는 “값을 전달한다”는 말이다. 메소드의 매개 변수로 넘길 때에는 원래 값은 놔두고, 전달되는 값이 진짜인 것처럼 보이게 한다. 그래서, 매개 변수를 받은 메소드에서 그 값을 수정하더라도 원래의 값은 변하지 않는다.
참조 자료형들도 호출된 메소드에서 다른 객체로 대체하여 처리하면 기존 값은 바뀌지 않는다.
public void callPassByValue() {
int a = 10;
String b = "b";
System.out.println(a, b); // 10, b
passByValue(a,b);
System.out.println(a, b); // 10, b
}
public void passByValue(int a, String b) {
a = 20;
b = "z"; // b = new String("z")와 같은 의미
System.out.println(a, b); // 20, z
}
String을 “”로 전달했을 경우에는 new String(””)처럼, new를 사용하여 객체를 생성한 것과 같으므로 원래의 값이 변경되지 않는다.
하지만 Pass by Reference로 값이 전달되면 호출한 메소드의 데이터에도 영향이 있다.
만약 매개 변수로 받은 참조 자료형 안에 있는 객체를 변경할 경우 호출한 참조 자료형 안에 있는 객체는 호출된 메소드에서 변경한 대로 데이터가 바뀐다.
이와 같이 값이 아니라 객체에 대한 참조가 넘어가는 것을 Pass by Reference라고 한다.
필요에 따라서 매개 변수 수를 정하기 애매한 경우 “타입…변수명”으로 선언해주면, 해당 변수는 배열로 인식하고 받아올 수 있다.
하나의 메소드에서는 한 번만 사용 가능하고, 여러 매개 변수가 있다면 가장 마지막에 선언해야 한다.
public void arbirary(int ... numbers) {
// numbers는 배열
}
public void arbirary(String message, int ... numbers) {
// numbers는 배열
}
이 매개변수를 사용한 예로는 System.out.printf
가 있다.
printf(String format, Object... args)
Object는 다음 장에서 알아보고, printf를 사용하기 위한 규칙은 부록 Formatter에서 확인하면 된다. 또한 20장에서 printf() 메소드에 대한 설명이 추가되어 있다.
Me: X
Me: 자바가 자동으로 기본 생성자를 만들어주지 않기 때문에 오류(error)가 발생한다.
Me: X
Me: this
Me: return
Me: void
Me: 객체 생성 없이 사용할 수 있다.
Me: 오버로딩
Me: Pass by value
Me: Pass by reference
Me: …
💡 책에 있는 내용이 아닙니다.
책을 읽으며 설명이 더 필요하거나, 추가로 궁금한 점에 대해 질문 형식으로 작성 후, 답을 구해보고 있습니다.
참고한 사이트나 영상은 [출처]로 달아두었으며, 오류 지적은 언제나 환영합니다.
만들 수 있다. 보통은 정적 메소드나 필드를 모아놓고 사용하는 유틸리티 클래스의 경우, 객체를 만들 목적의 클래스가 아니므로 객체 생성을 막기 위해 만든다고 한다.
생성자가 없으면 기본 생성자가 만들어지기 때문에, private으로 생성자를 만들어서 외부에서 객체 생성을 못하게 막아버린다.
그러면 하위 클래스 상속도 불가능하고 외부에서 호출도 불가능하다.
💡 뒷장에 나오는 enum 클래스가 private 생성자를 갖는다.
접근 제어자와 반환 자료형은 오버로딩과 상관 없다. 즉, 메소드의 이름만 동일하다는 조건 하에, 매개 변수의 타입과 개수를 다르게 할 때 오버로딩한다고 한다.
public class ReferenceOverloading {
public static void main(String[] args) {
ReferenceOverloading reference = new ReferenceOverloading();
}
public void print(String data) {
}
// 변수 이름이 다르더라도, 타입이 같아서는 안된다.
public void print(String test) {
}
}
이건 뒤에 다시 나오는 개념이다. 배운 내용을 복습할 겸 다시 정리해본다.
Java는 값을 복사하여 전달한다. 이건 참조 자료형도 마찬가지이다. 다만, 이것은 전달되는 값이 무엇을 담고 있느냐가 다른 것이다.
기본 자료형과 참조 자료형은 생성 및 초기화 하면, Stack 메모리에 값이 저장된다. 이때 저장되는 값은 다음과 같다.
즉, 참조 자료형을 매개 변수로 넘길 때는 메모리의 주소라는 값이 넘어간다. 그러면 실제 주소값을 넘기는 것과는 무엇이 다른 걸까?
C 언어에는 포인터라는 것이 존재한다. 이 포인터는 실제로 값이 저장된 주소에 직접적으로 접근하여 조회 및 변경, 삭제가 가능하다.
객체로 따지자면, 매개 변수로 객체를 넘겨 받은 후 new 연산자로 재정의하면 실제 객체가 변경되는 것이다.
그러나, Java에서는 실제 메모리에 직접 접근하는 것을 제한하고 있다. 즉, 복사된 주소 값을 통해 내부 멤버 변수의 값을 조작하는 것은 가능하나, 실제 객체는 변경할 수 없다.
다음과 같은 포인터 클래스를 만들자. 클래스는 생성자를 통해 값을 입력할 수도 있고, 직접 val 값을 변경할 수도 있다.
```java
public class Pointer {
public int val;
Pointer(int newVal) {
this.val = newVal;
}
}
```
그리고 아래와 같은 클래스를 만든 후 실행해보자.
만약 Pass by Reference 라면, 마지막에 실행된 값은 10이어야 한다.
```java
public class PointerMain {
public static void main(String[] args) {
PointerMain pm = new PointerMain();
Pointer org = new Pointer(5);
System.out.println("1. Before Origin value = " + org.val);
pm.newPointer(org);
System.out.println("4. After Origin value = " + org.val);
}
public void newPointer(Pointer arg) {
arg.val = 15;
System.out.println("2. Change value = " + arg.val);
arg = new Pointer(10);
System.out.println("3. Chnage Object = " + arg.val);
}
}
```
💡 참고한 사이트에서 그림과 함께 설명을 잘해둬서 이 사이트의 글을 읽어보는 걸 추천한다.
둘의 차이는 저장되는 저장공간(메모리)의 차이이다.
Java의 String은 new 생성자를 이용해 인스턴스를 생성한 뒤, heap에서 메모리 관리가 이루어진다는 사실은 같지만, 다른 참조형들과는 다르게 변하지 않는다는 특징을 가지고 있다.
한 번 저장된 String 객체의 값은 변하지 않는다. 값을 새로 넣을 경우에는 새로운 String 객체를 새로 생성하고 그 인스턴스를 참조하는 것이다.
이처럼 String 객체의 연산이 이루어질 때마다 새로운 객체를 생성하는 것은 메모리 관리 측면에서 비효율적이다.
이러한 이유로 만들어진 메모리 영역이 Heap 안에 있는 String Constant Pool이다. 여기에는 기존에 만들어진 문자열 값이 저장되어 있고, 리터럴(literal, “”로 생성한 것)로 생성된 같은 값을 가지는 객체는 같은 레퍼런스를 가지게 된다.
코드를 짜서 직접 출력해봤다.
public class MethodVarargs {
public static void main(String[] args) {
MethodVarargs varargs = new MethodVarargs();
System.out.println("First:");
varargs.calculateNumbers();
System.out.println("Second:");
varargs.calculateNumbers(1);
System.out.println("Third:");
varargs.calculateNumbers(1, 2, 3, 4, 5);
System.out.println("Forth:");
// int[] array ; // 오류 발생
int[] array = new int[5] ; // 출력하기 위해 주석 및 변경
varargs.calculateNumbers(array);
System.out.println("fifth:");
varargs.calculateNumbers(new int[5]);
System.out.println("sixth:");
varargs.calculateNumbers(new int[]{1, 2, 3, 4, 5});
}
public void calculateNumbers(int...numbers) {
System.out.println(numbers + ", " + numbers.length);
for(int i=0; i<numbers.length; i++) {
System.out.println(numbers[i]);
}
}
}
초기화하지 않은 상태였던 4번 빼고는 모두 성공했다.
[아이템 4] 인스턴스화를 막으려거든 private 생성자를 사용하라
[Java] Pass By Value와 Pass By Reference의 차이 및 이해 (3/3)