객체(Object)란 물리적으로 존재하거나 개념적인 것 중에서 다른 것과 식별 가능한 것을 말한다. 예를들어 물리적으로 존재하는 자동차, 자전거, 책, 사람은 물론 개념적인 학과, 강의, 주문 등도 모두 객체가 될 수 있다.
객체는 속성과 동작으로 구성된다. 사람은 이름, 나이 등의 속성과 웃다, 걷다 등의 동작이 있고, 자동차는 색상, 모델명 등의 속성과 달린다, 멈춘다 등의 동작이 있다. 자바는 이러한 속성과 동작을 각각 필드(field)와 메소드(method)라고 부른다.
※ 객체는 속성(데이터)과 동작(코드)라고 할 수 있다.

현실 세계의 객체를 소프트웨어 객체로 설계하는 것을 객체 모델링(Object modeling)이라고 한다. 객체 모델링은 현실 세계 객체의 대표속성과 동작을 추려내어 소프트웨어 객체의 필드와 메소드로 정의하는 과정이라고 볼 수 있다.
현실 세계에서 일어나는 모든 현상은 객체와 객체 간의 상호작용으로 이루어져 있다. 예를 들어 사람은 전자계산기의 기능을 이용하고, 전자계산기는 계산 결과를 사람에게 리턴하는 상호 작용을 한다.
사람 = 더하기 기능 이용
계산기 = 결과 리턴
객체 지향 프로그램에서도 객체들은 다른 객체와 서로 상호작용하면서 동작한다. 객체들 사이의 상호작용 수단은 메소드이다. 객체가 다른 객체의 기능을 이용할 때 이 메소드를 호출한다.

메소드 호출은 다음과 같은 형태를 가지고 있다.
메소드(매개값1, 매개값2, ''');
메소드 호출을 통해 객체들은 데이터를 서로 주고받는다. 메소드 이름과 함께 전달하고자 하는 데이터를 괄호() 안에 기술하는데, 이러한 데이터를 매개값이라고 한다. 매개값은 메소드가 실행할 때 필요한 값이다. 리턴값은 메소드의 실행의 결과이며, 호출한 곳으로 돌려주는 값이다.

메소드의 리턴값은 다음과 같이 호출한 곳에서 변수로 대입 받아 사용한다.

객체는 단독으로 존재할 수 있지만 대부분 다른 객체와 관계를 맺고 있다. 관계의 종류에는 집합 관계, 사용 관계, 상속 관계가 있다.

다른 객체의 필드를 읽고 변경하거나 메소드를 호출하는 관계를 말한다. 예를 들어 사람이 자동차에게 달린다, 멈춘다 등의 메소드를 호출하면 사람과 자동차는 사용 관계라고 볼 수 있다.
부모와 자식 관계를 말한다. 자동차가 기계의 특징(필드, 메소드)을 물려받는다면 기계(부모)와 자동차(자식)는 상속 관계에 있다고 볼 수 있다.
객체 지향 프로그램의 특징은 캡슐화, 상속, 다형성이다. 이 특징들은 자바 언어를 학습하면서 자연스럽게 알게 되는데, 여기서는 개념만 간단히 살펴보기로 하자.
캡슐화(Encapsulation)란 객체의 데이터(필드), 동작(메소드)을 하나로 묶고 실제 구현 내용을 외부에 감추는 것을 말한다. 외부 객체는 객체 내부의 구조를 알지 못하며 객체가 노출해서 제공하는 필드와 메소드만 이용할 수 있다.

필드와 메소드를 캡슐화하여 보호하는 이유는 외부의 잘못된 사용으로 인해 객체가 손상되지 않도록 하는 데 있다. 자바 언어는 캡슐화된 멤버를 노출시킬 것인지 숨길 것인지를 결정하기 위해 접근 제한자(Access Modifier)를 사용한다.
객체 지향 프로그래밍에서는 부모 역할의 상위 객체와 자식 역할의 하위 객체가 있다. 부모 객체는 자기가 가지고 있는 필드와 메소드를 자식 객체에게 물려주어 자식 객체가 사용할 수 있도록 한다. 이것이 상속(Inheritance)이다. 상속을 하는 이유는 다음과 같다.
잘 개발된 부모 객체의 필드와 메소드를 자식이 그대로 사용할 수 있어 자식 객체에서 중복 코딩을 하지 않아도 된다.
부모 객체의 필드와 메소드를 수정하며 모든 자식 객체들은 수정된 필드와 메소드를 사용할 수 있다.

다형성(Polymorphism)이란 사용 방법은 동일하지만 실행 결과가 다양하게 나오는 성질을 말한다. 자동차의 부품을 교환하면 성능이 다르게 나오듯이 프로그램을 구성하는 객체(부품)를 바꾸면 프로그램 실행 성능이 다르게 나올 수 있다.
다형성을 구현하기 위해서는 자동 타입 변환과 재정의 기술이 필요하다. 이 기술들은 상속과 인터페이스 구현을 통해 얻어진다.
객체를 생성할 때에는 설계도가 필요하다. 현실 세계에서 자동차를 생성하려면 자동차의 설계도가 필요하듯이, 객체 지향 프로그래밍에서도 객체를 생성하려면 설계도에 해당하는 클래스(Class)가 필요 하다.
클래스로부터 생성된 객체를 해당 클래스의 인스턴스(Instance)라고 부른다. 그리고 클래스로부터 객체를 만드는 과정을 인스턴스화라고 한다. 동일한 클래스로부터 여러 개의 인스턴스를 만들 수 있는데, 이것은 동일한 설계도로 여러 대의 자동차를 만드는 것과 동일하다.
클래스 선언은 객체 생성을 위한 설계도를 작성하는 작업이다. 어떻게 객체를 생성(생성자)하고, 객체가 가져야 할 데이터(필드)가 무엇이고, 객체의 동작(메소드)은 무엇인지를 정의하는 내용이 포함된다. 클래스 선언은 소스 파일명과 동일하게 다음과 같이 작성한다.
//클래스 선언
public class 클래스명{
}
public class는 공개 클래스를 선언한다는 뜻이다. 클래스명은 첫 문자를 대문자로 하고 캐멀 스타일로 작성한다.
숫자를 포함해도 되지만 첫 문자는 숫자가 될 수 없고, 특수 문자 중 $, _를 포함할수 있다.
다음과 같이 복수 개의 클래스 선언을 포함할 수 있다.
package ch06.sec03;
public class SportsCar{
}
class Tire {
}
※ 복수 개의 클래스 선언이 포함된 소스 파일을 컴파일하면 바이트코드 파일(.class)은 클래스 선언 수 만큼 생긴다.
하나의 소스파일에 복수 개의 클래스를 선언할 때 주의할 점은 소스 파일명과 동일한 클래스만 공개 클래스(Public Class)로 선언할 수 있다.
공개 클래스 : 어느 위치에 있든지 패키지와 상관없이 사용할 수 있는 클래스. public은 접근 제한자 중 하나
Tire 클래스도 공개 클래스로 선언하고 싶다면 .java소스 파일을 별도로 생성해야 한다. 그렇기 때문에 특별한 이유가 없다면 소스 파일 하나당 클래스 하나를 선언하는 것이 좋다.
클래스로부터 객체를 생성하려면 객체 생성 연산자인 new가 필요하다.
new 클래스()
new 연산자 뒤에는 생성자 호출 코드가 오는데, 클래스() 형태를 가진다. new연산자는 객체를 생성시킨 후 객체의 주소를 리턴하기 때문에 클래스 변수에 다음과 같이 대입할 수 있다.
클래스 변수 = new 클래스();

클래스 변수가 생성된 객체를 참조하는 모양
package ch01.sec01;
public calss Student{
}
package ch01.sec01;
public calss StudentExample {
Student s1 = new Student();
System.out.println("s1 변수가 Student 객체를 참조합니다");
Student s2 = new Student();
System.out.println("s2 변수가 또 다른 Student 객체를 참조합니다");
}
실행결과
s1 변수가 Student 객체를 참조합니다
s2 변수가 또 다른 Student 객체를 참조합니다

클래스의 두 가지 용도
클래스에는 다음 두 가지 용도가 있다.
- 라이브러리(libary) 클래스 : 실행할 수 없으며 다른 클래스에서 이용하는 클래스
- 실행 클래스 : main() 메소드를 가지고 있는 실행 가능한 클래스
앞의 예제에서 Student는 라이브러리 클래스이고 StudentExample은 실행 클래스라고 볼 수 있다. 일반적으로 자바 프로그램은 하나의 실행 클래스와 여러 개의 라이브러리 클래스들로 구성된다. 실행 클래스는 실행하면서 라이브러리 클래스를 내부에서 이용한다.
클래스 선언에는 객체 초기화 역할을 담당하는 생성자와 객체에 포함될 필드와 메소드를 선언하는 코드가 포함된다. 그래서 생성자, 필드, 메소드를 클래스 구성 멤버라고 한다. 다음은 각 클래스 구성 멤버의 선언 형태이다.
public class ClassName{
//필드 선언 //필드 : 객체의 데이터가 저장되는 곳
int fieldName;
//생성자 선언 // 생성자 : 객체 생성 시 초기화 역할 담당
ClassName() { ... }
//메소드 선언 // 메소드 : 객체의 동작으로 호출 시 실행하는 블록
int methodName() {...}
}
필드(Field)는 객체의 데이터를 저장하는 역할을 한다. 객체의 데이터에는 고유 데이터, 현재 상태 데이터, 부품 데이터가 있다.
자동차 객체를 예로 들면 제작회사, 모델, 색상, 최고 속도는 고유 데이터에 해당하고, 현재 속도, 엔진 회전 수는 상태 데이터에 해당한다. 그리고 차체, 엔진, 타이어는 부품에 해당한다.

필드를 선언하는 방법은 변수를 선언하는 방법과 동일하다. 단, 반드시 클래스 블록에서 선언되어야만 필드 선언이 된다.
타입 필드명 [ = 초기값 ] ;
(로컬)변수는 생성자와 메소드 블록에서 선언되며 생성자와 메소드 호출 시에만 생성되고 사용된다. 필드는 클래스 블록에서 선언되며, 객체 내부에서 존재하고 객체 내/외부에서 사용 가능하다.
| 구분 | 필드 | (로컬)변수 |
|---|---|---|
| 선언 위치 | 클래스 선언 블록 | 생성자,메소드 선언블록 |
| 존재 위치 | 객체 내부에 존재 | 생성자, 메소드 호출 시에만 존재 |
| 사용 위치 | 객체 내/외부 어디든 사용 | 생성자, 메소드 블록 내부에서만 사용 |
타입은 필드에 저장할 데이터의 종류를 결정한다. 기본타입(byte, short, int, long, float, double, boolean)과 참조 타입(배열, 클래스, 인터페이스)이 모두 가능하다. 필드명은 첫 문자를 소문자로 하되, 캐멀 스타일로 작성하는 것이 관례.
public class Car{
String model = "그랜저"; //고유 데이터 필드
int speed = 300; //상태 데이터 필드
boolean start = true; //상태 데이터 필드
Tire tire = new Tire(); //부품 객체 필드
}
초기값을 제공하지 않을 경우 필드는 객체 생성 시 자동으로 기본값으로 초기화된다.

정수 타입 필드는 0, 실수 타입 필드는 0.0, 그리고 boolean 필드는 false로 초기화 되는 것을 볼 수 있다. 참조 타입은 객체를 참조하고 있지 않은 상태인 null로 초기화가된다.
public class Car {
String model; //null
int speed; //0
boolean start //false
Tire tire; //null
}
//Car.java
package ch01.sec01.exam01;
public class Car {
//필드선언
String model;
boolean start;
int speed;
}
//CarExample.java
package ch01.sec01.exam01;
public class CarExample {
public static void main(String[] args) {
//Car 객체 생성
Car myCar = new Car();
//Car 객체의 필드값 읽기
System.out.println("모델명 : " + myCar.model);
System.out.println("시동여부 : " + myCar.start);
System.out.println("현재속도 : " + myCar.speed);
}
}
실행결과
모델명 : null
시동여부 : false
현재속도 : 0
필드를 사용한다는 것은 필드값을 읽고 변경하는 것을 말한다. 클래스에서 필드를 선언했다고 해서 바로 사용할 수 있는 것은 아니다. 필드는 객체의 데이터이므로 객체가 존재하지 않으면 필드도 존재하지 않는다.
클래스로부터 개게가 생성된 후에 필드를 사용할 수 있다. 필드는 객체 내부의 생성자와 메소드 내부에서 사용할 수 있고, 객체 외부에서도 접근해서 사용할 수 있다.
객체 내부에서는 단순히 필드명으로 읽고 변경할 수 있지만 외부 객체에서는 참조 변수와 도트.연산자를 이용해서 필드를 읽고 변경해야 한다. 도트.는 객체 접근 연산자로, 객체가 가지고 있는 필드나 메소드에 접근하고자 할 때 참조 변수 뒤에 붙인다.
//Car.java
package ch01.sec01.exam01
public class Car{
//필드선언
String company = "현대자동차";
String model = "그렌저";
String color = "검정";
int maxSpeed = 350;
int speed;
}
package ch01.sec01.exam01;
public class CarExample {
public static void main(String[] args){
//Car 객체 생성
Car myCar = new Car();
//Car 객체의 필드값 읽기
System.out.println("제작회사: " + myCar.company);
System.out.println("모델명: " + myCar.model);
System.out.println("색깔: " + myCar.color);
System.out.println("최고속도: " + myCar.maxSpeed);
System.out.println("현재속도: " + myCar.speed);
//Car객체의 필드값 변경
myCar.speed = 60;
System.out.println("수정된 속도: " + myCar.speed);
}
}
실행결과
제작회사: 현대자동차
모델명: 그렌저
색깔: 검정
최고속도: 350
현재속도: 0
수정된 속도: 60
new 연산자는 객체를 생성한 후 연이어 생성자(Constructor)를 호출해서 객체를 초기화하는 역할을 한다. 객체 초기화란 필드 초기화를 하거나 메소드를호출해서 객체를 사용할 준비를 하는 것을 말한다.
클래스 변수 = new 클래스();
//생성자 호출
생성자가 성공적으로 실행이 끝나면 new 연산자는 객체의 주소를 리턴한다. 리턴된 주소는 클래스 변수에 대입되어 객체의 필드나 메소드에 접근할 때 이용된다.
모든 클래스는 생성자가 존재하며, 하나 이상을 가질 수 있다. 클래스에 생성자 선언이 없으면 컴파일러는 다음과 같은 기본 생성자(Default Constructor)를 바이트코드 파일에 자동으로 추가시킨다.
[public] 클래스() { }
클래스가 public class로 선언되면 기본 생성자도 public이 붙지만, 클래스가 public 없이 class로만 선언되면 기본 생성자에도 public이 붙지 않는다 예를 들어 Car 클래스를 설계할 때 생성자를 생략하면 기본 생성자가 다음과 같이 생성된다.
//소스파일(Car.java)
public class Car{
}
//바이트코드 파일(Car.class)
public class Car{
public Car() { } //자동 추가
}
그렇기 때문에 다음과 같이 new연산자 뒤에 기본 생성자를 호출할 수 있다.
Car myCar = new Car(); // 기본 생성자 호출
그러나 개발자가 명시적으로 선언한 생성자가 있다면 컴파일러는 기본 생성자를 추가하지않는다.
개발자가 생성자를 선언하는 이유는 객체를 다양하게 초기화하기 위해서이다.
객체를 다양하게 초기화하기 위해 개발자는 생성자를 다음과 같이 직접 선언할 수 있다.
클래스(매개변수,...){
//객체의 초기화 코드
}
//전체가 생성자 블록
생성자는 메소드와 비슷한 모양을 가지고 있으나, 리턴 타입이 없고 클래스 이름과 동일하다. 매개변수는 new 연산자로 생상자를 호출할 때 매개값을 생성자 블록 내부로 전달하는 역할을 한다. 예를 들어 다음과 같이 Car 생성자를 호출할 때 3개의 매개값을 블록 내부로 전달한다고 가정해보자.
Car myCar = new Car("그랜저", "검정", 300);
3개의 매개값을 순서대로 매개변수로 대입 받기 위해서는 다음과 같이 생성자가 선언되어야 한다.
public class Car {
//생성자 선언
Car(String model, String color, int maxSpeed) { ... }
}
매개변수의 타입은 매개값의 종류에 맞게 작성하면 된다.
//Car.java
package ch01.sec01.exam01;
public class Car {
Car(String model, String color, int maxSpeed){
// 대입 대입 대입
}
}
//CarExample.java
package ch01.sec01.exam01;
public class CarCarExample {
public static void main(String[] args){
Car myCar = new Car("그랜저", "검정", 250);
//Car myCar = new Car(); //기본 생성자 호출 못함
}
}
객체마다 동일한 값을 갖고 있다면 필드 선언 시 초기값을 대입하는 것이 좋고, 객체마다 다른 값을 가져야 한다면 생성자에서 필드를 초기화하는 것이 좋다.
예를 들어 Korean 클래스를 선언한다고 가정해보자. 한국인이므로 nation(국가)은 대한민국으로 동일한 값을 가지지만, name(이름)과 ssn(주민등록번호)는 한국인마다 다르므로 생성자에서 초기화하는 것이 좋다.
public class Korean{
//필드 선언
String nation = "대한민국";
String name;
String ssn;
//생성자 선언
public Korean(String n, Stirng s){
name = n;
ssn = s;
}
}
생성자의 매개값은 new연산자로 생성자를 호출할 때 주어진다. k1과 k2가 참조하는 객체는 주어진 매가값으로, name과 ssn필드가 각각 초기화 된다.
Korean k1 = new Korean("박자바","0123-12312");
Korean k2 = new Korean("김자바","0623-18762");
//Korean.java
package ch01.sec01.exam01;
public class Korean {
//필드 선언
String nation = "대한민국";
String name;
String ssn;
//생성자 선언
public Korean(String n, Stirng s){
name = n;
ssn = s;
}
}
package ch01.sec01.exam01;
public class KoreanExample {
public static void main(String[] args){
//Korean 객체 생성
Korean k1 = new Korean("박자바","0123-12312");
//Korean 객체의 필드값 읽기
System.out.println("k1.nation : " + k1.nation);
System.out.println("k1.name : " + k1.name);
System.out.println("k1.ssn : " + k1.ssn);
System.out.println();
//또 다른 Korean객체 생성
Korean k2 = new Korean("김자바","0623-18762");
//또 다른 Korean 객체 데이터 읽기
System.out.println("k2.nation : " + k2.nation);
System.out.println("k2.name : " + k2.name);
System.out.println("k2.ssn : " + k2.ssn);
}
}
실행된 결과
k1.nation : 대한민국
k1.name : 박자바
k1.ssn : 0123-12312
k2.nation : 대한민국
k2.name : 김자바
k2.ssn : 0623-18762
Korean 생성자를 보면 매개변수 이름으로 각각n과 s를 사용했다. 매개변수의 이름이 너무 짧으면 코드 가독성이 좋지 않기 때문에 가능하면 초기화시킬 필드명과 동일한 이름을 사용하는 것이 좋다.
public Korean(String n, Stirng s){
this.name = name;
this.ssn = ssn;
}
위와 같은 경우에는 매개변수명이 필드명과 동일하기 때문에 필드임을 구분하기 위해 this 키워드를 필드명 앞에 붙여주었다. this는 현재 객체를 말하며, this.name은 현재 객체의 데이터(필드)로서의 name을 뜻한다.
//Korean.java
package ch01.sec01.exam01;
public class Korean {
//필드 선언
String nation = "대한민국";
String name;
String ssn;
//생성자 선언
public Korean(String n, Stirng s){
this.name = name;
this.ssn = ssn;
}
}
이클립스는 필드의 색깔을 파란색, 매개변수의 색깔은 갈색으로 보여주기 때문에 코드를 보면 필드와 매개변수를 쉽게 구별할 수 있다.
매개 값으로 객체의 필드를 다양하게 초기화하려면 생성자 오버로딩이 필요하다. 생성자 오버로딩이란 매개변수를 달리하는 생성자를 여러 개 선언하는 것을 말한다. 다음은Car클래스에서 생성자를 오버로딩한 에이다.
public class Car{
Car() {..}
Car(String model) {}
Car(String model, String color) {}
Car(String model, String color, int maxSpeed) {}
//생성자 오버로딩
//매개변수의 타입, 개수 , 순서가 다르게 여러 개의 생성자 선언
매개변수의 타입과 개수 그리고 선언된 순서가 똑같을 경우 매개변수 이름만 바꾸는 것은 생성자 오버로딩이 아니다. 바로 다음과 같은 경우이다.
Car(String model, String color) {}
Car(String color, String model) {} // 오버로딩이 아님, 컴파일 에러 발생
생성자가 오버로딩되어 있을 경우, new 연산자로 생성자를 호출할 때 제공되는 매개값의 타입과 수에 따라 실행될 생성자가 결정된다.
Car car1 = new Car(); //-> Car() {..}
Car car2 = new Car("그렌저"); //-> Car(String model) {..}
Car car3 = new Car("그렌저","흰색"); //-> Car(String model, String color) {..}
Car car4 = new Car("그렌저","흰색",300); //-> Car(String model, String color, int maxSpeed) {..}
생성자 오버로딩이 많아질 경우 생성자 간의 중복된 코드가 발생할 수 있다. 매개변수의 수만 달리하고 필드 초기화 내용이 비슷한 생성자에서 이러한 중복 코드를 많이 볼 수 있다.
Car(String model){
this.model = model; //중복코드
this.color = "은색"; //중복코드
this.maxSpeed = 250; //중복코드
}
Car(String model, String color){
this.model = model; //중복코드
this.color = "은색"; //중복코드
this.maxSpeed = 250; //중복코드
}
Car(String model, String color, int maxSpeed){
this.model = model; //중복코드
this.color = "은색"; //중복코드
this.maxSpeed = 250; //중복코드
}
이 경우에는 공통 코드를 한 생성자에만 집중적으로 작성하고, 나머지 생성자는 this()를 사용하여 공통 코드를 가지고 있는 생성자를 호출하는 방법으로 개선할 수 있다.
Car(String model){// 맨 아래꺼에 호출
this(model, "은색", 250);
}
Car(String model, String color){ // 맨아래로 호출
this(model, color, 250);
]
Car(String model, String color, int maxSpeed){ // 공통 초기화 코드
this.model = model; //중복코드
this.color = color; //중복코드
this.maxSpeed = maxSpeed; //중복코드
}
this(매개값)는 생성자의 첫 줄에 작성되며 다른 생성자를 호출하는 역할을 한다. 호출하고 싶은 생성자의 매개변수에 맞게 매개값을 제공하면 된다. this() 다음에는 추가적인 실행문을 작성할 수 있다.