객체 지향 프로그래밍에서는 객체가 속한 클래스에 객체의 동작을 정의하고, 객체를 사용해 작업을 수행한다. 자바는 모든 메서드를 클래스 안에 선언하며, primitive 타입을 제외한 모든 값이 객체다.
객체는 각각이 자신만의 state가 존재하며, 이 state는 메서드를 호출해서 얻는 결과에 영향을 준다.
다른 사람이 구현한 객체의 메서드를 호출할 때는 내부에서 무슨 일이 일어나는지 몰라도 된다. 이 원칙을 encapsulation이라고 하며, 이는 객체지향 프로그래밍의 핵심 개념이다.
다른 사람에게 객체를 제공하여 작업을 공유하기 위해 자바에선 class를 사용하면 된다. class는 같은 동작을 사용하는 객체를 생성하고 사용하는 메커니즘이다. 다른 프로그래머가 지식을 활용할 수 있게 하려면 클래스를 만들어 제공하며, 이외에도 프로그램을 일관되게 구성할 수 있다는 장점이 있다.
날짜를 표현하는데 LocalDate란 클래스를 사용하며, LocalDate date = LocalDate.of(year, month, 1); 를 이용하여 객체를 얻을 수 있다.
getDayOfWeek() 메서드는 날짜가 속한 요일을 알려주며, 이 메서드는 또다른 클래스인 DayOfWeek()의 객체를 반환한다. 요일의 숫자값을 알려주는 메서드는 getValue() 메서드 이다. 월요일(1),화요일(2) ~ 일요일(7)
plusDays 메서드는 두 가지 방법으로 구현하는데, 하나는 date 객체의 상태를 변경하고 아무것도 반환하지 않는 방법과, 또 다른 하나는 객체의 상태를 변강하지 않고 새로 생성된 LocaDate객체를 반환하는 방법이다.
첫번쨰 처럼 호출되는 객체를 변경하는 메서드를 변경자(mutator)라고 하며, 객체를 변경하지 않는 메서드를 접근자(accessor)라고 한다.
요즘은 컴퓨터에 CPU가 여러개 있어 동시 접근의 안정성은 중요한 문제인데, 이것을 해결하는 방법중 하나는 접근자 메서드만 제공해 immutable 객체로 만드는 것이다.
변수에 실제 객체(즉, 객체의 상태를 구성하는 비트들)를 담을 수 있다. 하지만 자바에서 변수에는 오직 객체 참조(reference)만 담을 수 있다. 실제 객체는 다른 곳에 있고, 참조는 실제 객체를 찾아내는 구현체 고유의 방법이다. C/C++에서는 포인터를 수정하고 포인터로 임의의 메로리 위치를 덮어쓸 수 있지만 자바 참조로는 특정 객체에만 접근할 수 있다.
밑의 코드는 friends의 객체의 참조를 복사하여 people에 넣고 있다. 그렇기 때문에 people을 변경하게 되면 원본의 reference가 가리키는 객체가 변경된다. 그러르모 friends 또한 변경된다. 객체를 공유하면 효율적이고 편리하지만(왜 ?), 어떤 참조로도 공유 객체를 변경할 수 잇다.
ArrayList<String> friends = new ArrayList<>();
ArrayList<String> people = friends;
people.add(Paul);
하지만 String이나 LocalDate 처럼 클래스에 변경자 메서드가 없다면 객체를 변경할 수 없으므로, 해당 객체에 대한 참조를 얼마든지 공유할 수 있다. 객체 변수를 특별한 값인 null로 설정해 변수가 어떤 객체도 참조하지 않게 할 수 있다. 하지만 예상 못한 null로 인해 NullPointerException이 일어날 수 있으므로 사용하는것이 권장되진 않는다.
밑의 코드에서 plusDays는 객체의 state를 변경하지 않고 변경된 새 객체를 반환해 준다. 밑의 코드 처럼 처음에 할당 했던 localDate는 어떻게 될까? 첫번째 객체의 참조는 더이상 필요 없으므로 가비지 컬렉터가 메모리를 정리해서 재사용할 수 있게 한다. 자바에서는 이 메모리 관리를 jvm이 해주기 때문에 메모리 할당 해제를 걱정하지 않아도 된다.
date = localDate.of(year, month, 1);
date = date.plusDays(1);
자바에서는 인스턴스 변수(instance variable)로 객체의 상태를 나타낸다. 자바에서는 인스턴스 변수를 보통 private으로 선언한다. private으로 선언하면 같은 클래스에 속한 메서드만 변수에 접근할 수 있다.
메서드를 선언할 때는 메서드 이름, 매개변수의 타입과 이름, 반환 타입을 지정해야 한다.
signature는 서명이라고 하며 메서드 이름, 매개변수, 반환 타입을 이야기한다.
대부분의 메서드는 public으로 선언한다. 때로는 헬퍼 메서드를 private으로 선언한다. 클래스 사용자와 관련이 없는 메서드는 (특히 세부 구현에 의존한다면) private으로 선언해야 한다.
메서드에서 값을 돌려줄 때는 return 키워드를 사용하며 메서드 선언은 클래스 선언 안에 넣어야 한다.
클래스의 인스턴스를 통해 사용되는 메서드를 instance method라고 한다. 자바에서 static으로 선언하지 않은 메서드는 모두 인스턴스 메서드다.
메서드를 호출시 메서드에는 메서드를 호출한 객체에 대한 참조와 매개변수가 전달된다. 메서드를 호출한 객체에 대한 참조를 수신자(receiver)라고 한다.
객체의 메서드를 호출할 때 해당 객체가 this로 설정된다. 지역 변수와 인스턴스 변수를 명확히 구별하기 위해 사용하기도 하지만 대부분 매개변수 이름을 인스턴스 변수와 다르게 지정하고 싶지 않을때 this 참조를 사용한다.
원한다면 this를 메서드의 매개변수로도 선언할 수 있다(생성자의 매개변수로는 선언할 수 없음.)
메서드에 객체를 전달하면 해당 메서드는 객체의 참조의 사본을 얻는다. 메서드는 이 참조로 매개변수 객체에 접근하거나 매개변수 객체를 변경할 수 있다.
밑의 코드를 boss.giveRandomRaise(fred); 로 호출한다면 fred를 e 매개변수로 복사하며, giveRandomRaise 메서드는 두 참조가 공유하는 객체를 변경한다.
public class EvilManager{
private Random generator;
public void giveRandomRaise(Employee e) {
double percentage = 10 * generator.nextGaussian();
e.raiseSalary(percentage);
}
}
하지만 자바에서는 기본 타입 매개변수를 업데이트 하는 메서드를 작성할 수 없다. boss.increaseRandomly(sales)를 호출하면 sales가 x로 복사되며, 다음으로 x를 증가시키지만 sales는 변하지 않는다. 이후 매개변수는 유효 범위를 벗어나고 증가 연산은 효력을 잃는다.
public void increaseRandomly(double x) {
double amount = x * generator.nextDouble();
x += amount;
}
위와 동일한 이유로 객체 참조를 다른 것으로 바꾸는 메서드도 작성할 수 없다. 밑의 코드를 boss.replaceWithZombie(fred); 처럼 실행하면, e는 다른 참조로 설정되며, 메서드가 끝날 때 e는 유효 범위를 벗어나며 fred는 어디서도 변경되지 않는다.
public class EvilManager {
public void replaceWithZombie(Employee e) {
e = new Employee("", 0);
}
}
생성자는 이름이 클래스와 같고, 반환 타입이 없다.
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
생성자는 public 접근이지만, private 생성자 역시 유용하다.
생성자는 new 연산자를 사용한 시점에 실행된다. new Employee(“James Bond”, 5000)
위의 표현식은 Employee 클래스의 객체를 할당한 후 생성자 바디를 호출한다. 그리고 생성자 바디는 생성자에 전달된 인수로 인스턴스 변수를 초기화한다. new 연산자는 생성된 객체의 참조를 반환한다. 보통 반환받은 참조를 변수에 저장하거나, 메서드에 매개변수로 전달한다.
Employee james = new Employee("James Bond", 50000);
ArrayList<Employee> staff = new ArrayList<>();
staff.add(new Employee("James Bond", 50000));
생성자는 두 가지 이상의 버전으로 제공할 수 있다. 생성자가 여러개라면 호출되는 생성자는 인수(매개변수)에 따라 결정된다. 이때 생성자가 overload(중복정의) 되었다고 한다.
class Employee{
public Employee(double salary) {
this.name = "";
this.salary = salary;
}
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
}
int, double, String 등을 매개변수로 받는 println 메서드는 오버로드된 메서드들을 갖고 있다.
또 다른 생성자에서 어느 한 생성자를 호출할 수 있는데, 호출하는 쪽 생성자 바디의 첫 번째 문장으로만 허용한다. 밑에서 this는 생성될 객체 참조가 아니다. this는 같은 클래스에 속한 다른 생성자를 호출할 때 사용하는 특수 문법이다.
public Employee(double salary) {
this("", salary); //Employee(String, double)을 호출
//이후에 다른 문장이 올 수 있다.
}
생성자 안에서 인스턴스 변수를 명시적으로 설정하지 않으면 자동으로 변수를 기본 값으로 설정한다. 숫자는 0, boolean 값은 false, 객체 참조는 null이 기본값이다.
인스턴스 변수는 지역 변수와 완전히 다르다. 지역 변수는 항상 명시적으로 초기화해야 한다는 점을 기억하자. instance 변수는 클래스안에 선언된 변수로 객체의 상태를 나타내는 것을 의미하며, 지역변수는 method에서 사용하는 변수를 지역변수라고 한다.
숫자는 보통 0으로 초기화하면 편하지만, 객체 참조를 기본 값 null로 초기화 할 경우 오류를 일으키는 원인이 된다. if(e.getName().equals(“James Bond”)); e를 null로 했을 경우 앞의 메서드는 null 포인터 예외가 일어난다.
이 초기화는 객체를 할당하고 나서 생성자가 실행되기 전에 일어난다.
public class Employee {
private String name = "";
}
인스턴스 변수를 선언할 때 초기화하는 방법 외에 클래스 선언 안에 임의의 초기화 블록(initialization block)을 넣는 방법도 있다.
public class Employee() {
private String name = "";
private int id;
private double salary;
//초기화 블록
{
Random generator = new Random();
id = 1 + generator.nextInt(1_000_000);
}
public Employee(String name, double salary){
...
}
}
초기화 코드는 자주 사용하지는 않는다. 대부분의 개발자는 긴 초기화 코드를 헬퍼 메서드 안에 두고, 생성자에서 헬퍼 메서드를 호출한다.
인스턴스 변수 초기화와 초기화 블록은 클래스 선언에 나타난 순서로 실행하며, 그다음에 생성자 바디를 실행한다.
인스턴스 변수를 final로 선언할 수 있다. 최종으로 선언한 변수는 생성자 실행이 끝나기 전에 초기화해야한다.
변경 가능한 객체를 가리키는 참조에 사용하면 final 제어자는 그저 참조 자체가 절대로 변하지 않는 사실만을 나타내며 객체의 내용을 변경하는 것은 완전히 합법적이다.
final Employee employee1 = new Employee();
employee1.salary = 1.1;
employee1 = new Employee(); //불가능
인수 없는 생성자는 기본 값으로 상태를 설정한 객체를 생성한다.
클래스에서 생성자가 없으면 자동으로 아무 작업도 하지 않는 인수 없는 생성자를 받는다. 모든 인스턴스 변수는 명시적인 초기화 없이 기본 값으로 남는다. 그래서 모든 클래스에는 생성자가 적어도 하나는 있다.
클래스에 생성자가 이미 있으면 인수 없는 생성자를 자동으로 받지 않는다. 생성자가 있는데도 인수가 없는 생성자가 필요하면 직접 작성해야한다.