물리적으로 존재하거나 추상적으로 생각할 수 있는 것 중에서 자신의 속성을 가지고 있으면서 식별 가능한 것(자동차, 자전거, 책, 사람 등등)
객체는 속성과 동작으로 구성되어 있다. 자바는 이 속성과 동작을 각각 필드와 메소드라고 부른다.
(예시로, 사람은 이름, 나이 등의 속성과 웃다, 걷다 등의 동작이 있다)
현실 세계의 객체를 소프트웨어 객체로 설계하는 것을 객체 모델링이라고 한다.
객체는 개별적으로 사용될 수 있지만, 대부분 다른 객체와 관계를 맺고 있다.
객체의 상호작용은 객체 간의 메소드 호출을 의미하며 매개값과 리턴값을 통해 데이터를 주고받는다.
객체는 설계도를 바탕으로 만든다. 메모리에서 사용하고 싶은 객체가 있다면 우선 설계도로 해당 객체를 만드는 작업이 필요하다.
자바에서는 설계도가 바로 클래스이다.
클래스에는 객체를 생성하기 위한 필드와 메소드가 정의되어 있다. 클래스로부터 만들어진 객체를 해당 클래스의 인스턴스라고 한다.
하나의 클래스로 여러 개의 인스턴스를 만들 수 있는데,
이것은 동일한 설계도로부터 여러 대의 자동차를 만드는 것과 동일하다.
객체 지향 프로그래밍 개발은 세 단계가 있다.
클래스 이름을 정했다면 '클래스 이름.java'로 소스 파일을 생성해야 한다.
일반적으로 소스 파일당 하나의 클래스를 선언한다. 하지만 2개 이상의 클래스 선언도 가능하다.
2개 이상의 클래스가 선언된 소스 파일을 컴파일하면 바이트 코드 파일(.class)은 클래스를 선언한 개수만큼 생긴다. 결국 소스 파일은 클래스 선언을 담고 있는 저장 단위일 뿐, 클래스 자체가 아니다.
위 코드를 컴파일하면 Asdf.class와 Qwer.class가 각각 생성된다.
Student.java
package sec01.exam01;
public class Student{
}
위의 코드처럼 클래스를 선언한 다음 컴파일을 했다면 객체를 생성한 설계도가 만들어진 셈이다. 클래스로부터 객체를 생성하려면 아래과 같이 new 연산자를 사용하면 된다.
StudentExample.java
package sec01.exam01;
public class StudentExample {
public static void main(String[] args) {
Student s1 = new Student();
System.out.println("s1 변수가 Student 객체를 참조합니다.");
Student s1 = new Student();
System.out.println("s2 변수가 또 다른 Student 객체를 참조합니다.");
}
}
new는 클래스로부터 객체를 생성시키는 연산자이다. new 연산자 뒤에는 생성자가 오는데 생성자는 클래스() 형태를 가지고 있다. new 연산자로 생성된 객체는 메모리 힙 영역에 생성된다.
new 연산자는 힙 영역에 객체를 생성시킨 후 객체의 번지를 리턴하도록 되어 있다.
클래스는 두 가지 용도가 있다.
하나는 라이브러리용이고 다른 하나는 실행용이다.
라이브러리 클래스는 다른 클래스에서 이용할 목적으로 설계된다. 프로그램 전체에서 사용되는 클래스가 100개라면 99개는 라이브러리 클래스이고 단 하나가 실행 클래스이다. 실행 클래스는 프로그램의 실행 진입점인 main() 메소드를 제공하는 역할을 한다.
package sec03.exam01;
public class Asdf {
// 필드 : 객체의 데이터가 저장되는 곳
int fieldname;
// 생성자 : 객체 생성 시 초기화 역할 담당
Classname() { ... }
// 메소드 : 객체의 동작에 해당하는 실행 블록
void methodName() { ... }
}
생성자 선언
package sec03.exam01;
public class Car {
Car(String color,int cc) {}
}
생성자를 호출해서 객체 생성
package sec03.exam01;
public class CarExample {
public static void main(String[] args) {
Car myCar = new Car("검정",3000);
}
}
클래스로부터 객체가 생성될 때 필드는 기본 초기값으로 자동 설정된다. 만약 다른 값으로 초기화 하고 싶다면 아래처럼 필드를 선언할 때 초기값을 주거나 생성자에서 초기값을 주는 방법이 있다.
package sec03.exam01;
public class Korean {
//필드
String nation = "대한민국";
String name;
String ssn;
//생성자
public Korean(String n, String s) {
name=n;
ssn=s;
}
}
this 는 객체 자신의 참조인데, 객체가 객체 자신을 this 라고 한다.
'this.필드'는 this라는 참조 변수로 필드를 사용하는 것과 동일하다.
this 를 이용하여 위의 생성자를 수정하면 다음과 같다.
package sec03.exam01;
public class Korean {
//필드
String nation = "대한민국";
String name;
String ssn;
//생성자
public Korean(String n, String s) {
this.name=n;
this.ssn=s;
}
}
객체의 필드는 하나가 아니라 여러 개가 있고, 이 필드들을 모두 생성자에서 초기화한다면 생성자의 매개 변수 수는 객체의 필드 수만큼 선언되어야 한다. 그러나 실제로는 중요한 몇 개의 필드만 매개 변수를 통해 초기화되고 나머지 필드들은 필드 선어 시에 초기화하거나 생성자 내부에서 임의의 값 또는 계산된 값으로 초기화한다.
외부에서 제공되는 다양한 데이터들을 이용해서 객체를 초기화하려면 생성자도 다양화될 필요가 있다. Car 객체를 생성할 때 외부에서 제공되는 데이터가 없다면 기본 생성자로 Car 객체를 생성해야 하고, 외부에서 model 데이터가 제공되거나 model과 color가 제공될 경우에도 Car 객체를 생성할 수 있어야 한다. 생성자가 하나뿐이라면 이러한 요구 조건을 수용할 수 없다. 자바는 다양한 방법으로 객체를 생성할 수 있도록 생성자 오버로딩을 제공한다. 생성자 오버로딩이란 매개 변수를 달리하는 생성자를 여러 개 선언하는 것을 말한다.
package sec03.exam03;
public class Car {
//필드
String company = "현대자동차"
String model;
String color;
int maxSpeed;
//생성자
Car() {
}
Car(String model) {
this.model = model;
}
Car(String model, String color) {
this.model = model;
this.color = color;
}
Car(Stirng model, String color, int maxSpeed) {
this.model = model;
this.color = color;
this.maxSpeed = maxSpeed;
}
}
메소드는 객체의 동작에 해당하는 중괄호 블록을 말한다. 메소드를 호출하면 중괄호 블록에 있는 모든 코드들이 일괄적으로 실행된다.
메소드 선언은 선언부와 실행 블록으로 구성된다. 메소드 선언부를 메소드 시그니처라고 한다.
package sec04.exam01;
public class Calculator {
void powerOn() {
System.out.println("전원을 켭니다.");
}
int plus(int x, int y) {
int result = x + y;
return result
}
}
package sec04.exam01;
public class CalculatorExample {
public static void main(String[] args) {
Calculator myCalc = new Calculator();
myCalc.powerOn();
int result1 = myCalc.plus(5,6);
}
}
package sec04.exam06;
public class Calculator {
double areaRectangle(double width) {
return width * width;
}
double areaRectangle(double width, double height) {
return width * height;
}
}
위의 클래스 오버로딩과 유사하다
클래스로부터 객체(인스턴스)는 하나가 아니라 여러 개가 만들어질 수 있다. 이 경우 클래스 멤버들을 객체마다 가지고 있을 필요가 있을까?
예를 들어, 객체마다 필드값이 달라야 한다면 해당 필드는 객체마다 가지고 있는 것이 맞다.
하지만 객체의 필드값이 모두 같아야 한다면 이 필드를 객체마다 가질 필요가 있을까?
만약 객체마다 갖고 있다면 메모리 낭비가 되며, 모든 객체의 필드값을 같게 맞추는 추가적인 작업이 필요할 수도 있다. 오히려 이런 필드는 한 곳에 위치시키고 객체들이 공유하는 것이 좋다.
자바는 이런 경우를 위해 클래스 멤버를 인스턴스 멤버와 정적 멤버로 구분해서 선언할 수 있도록 하고 있다. 인스턴스 멤버는 객체마다 가지고 있는 멤버를 말하고, 정적 멤버는 클래스에 위치시키고 객체들이 공유하는 멤버를 말한다.
인스턴스 멤버란 객체를 생성한 후 사용할 수 있는 필드와 메소드를 말한다. 이들을 각각 인스턴스 필드, 인스턴스 메소드라고 부른다. 인스턴스 필드와 메소드는 객체에 소속된 멤버이기 때문에 객체 없이는 사용할 수 없다.
다음은 Car 클래스에 인스턴스 필드 gas 와 인스턴스 메소드 setSpeed() 를 선언한 모습이다.
public class Car {
int gas;
void setSpeed(int speed) { ... }
}
gas 필드와 setSpeed() 메소드는 인스턴스 멤버이다.
외부에서 사용하기 위해서는 우선 Car 객체(인스턴스)를 생성하고 참조변수로 접근하자.
정적 멤버는 클래스에 고정된 멤버로써 객체를 생성하지 않고 사용할 수 있는 필드와 메소드를 말한다.
정적 필드와 정적 메소드를 선언하려면 필드와 메소드 선언 시 static 키워드를 추가적으로 붙이면 된다. 다음은 정적 필드와 정적 메소드를 선언하는 방법이다.
public class 클래스 {
//정적 필드
static 타입 필드
//정적 메소드
static 타입 메소드() { ... }
}
정적 필드와 정적 메소드는 클래스에 고정된 멤버이므로 클래스 로더가 클래스(바이트 코드)를 로딩해서 메소드 메모리 영역에 적재할 때 클래스별로 관리된다. 따라서 클래스의 로딩이 끝나면 바로 사용할 수 있다.
필드를 선언할 때는 인스턴스 필드로 선언할 것인가 아니면 정적 필드로 선언할 것인가의 판단 기준이 필요하다. 객체마다 가지고 있어야 할 데이터라면 인스턴스 필드로 선언하고, 객체마다 가지고 있을 필요가 없는 공용 데이터라면 정적 필드로 선언하는 것이 좋다.
객체가 없어도 실행된다는 특징 때문에 정적 메소드를 선언할 때는 이들 내부에 인스턴스 필드나 인스턴스 메소드를 사용할 수 없다. 또한 객체 자신의 참조인 this 키워드도 사용이 불가능하다.
public class ClassName {
// 인스턴스 필드와 메소드
int field1;
void method1() { ... }
//정적 필드와 메소드
static int field2;
static void method2() { ... }
//정적 메소드
static void method3() {
this.field1 = 10 // X 컴파일 에러
this.method1(); // X 컴파일 에러
field2 = 10;
method2();
}
}
전체 프로그램에서 단 하나의 객체만 만들도록 보장해야 하는 경우가 있다.
단 하나만 생성된다고 해서 이 객체를 싱글톤이라고 한다.
싱글톤을 만들려면 클래스 외부에서 new 연산자로 생성자를 호출할 수 없도록 막아야 한다. 생성자를 호출한 만큼 객체가 생성되기 때문이다. 생성자를 외부에서 호출할 수 없도록 하려면 생성자 앞에 private 접근 제한자를 붙여주면 된다.
그리고 자신의 타입인 정적 필드를 하나 선언하고 자신의 객체를 생성해 초기화한다. 참고로 클래스 내부에서는 new 연산자로 생성자 호출이 가능하다. 정적 필드도 private 접근 제한자를 붙여 외부에서 필드값을 변경하지 못하도록 막는다. 대신 외부에서 호출할 수 있는 정적 메소드인 getInstance()를 선언하고 정적 필드에서 참조하고 있는 자신의 객체를 리턴해 준다.
싱글톤을 만드는 코드 :
public class 클래스 {
//정적 필드
private static 클래스 singleton = new 클래스();
//생성자
private 클래스() { }
//정적 메소드
static 클래스 getInstance() {
return singleton;
}
}
외부에서 객체를 얻는 유일한 방법은 getInstance() 메소드를 호출하는 방법이다. getInstance() 메소드는 단 하나의 객체만 리턴하기 때문에 아래 코드에서 변수1과 변수2는 동일한 객체를 참조한다.
클래스 변수1 = 클래스.getInstance();
클래스 변수2 = 클래스.getInstance();
예제 :
//싱글톤
package sec05.exam04;
public class Singleton {
private static Singleton singleton = new Singleton();
private Singleton() { }
static Singleton getInstance() {
return singleton;
}
}
// 싱글톤 객체
package sec05.exam04;
public static void main(String[] args) {
singleton obj1 = Singleton.getInstance();
singleton obj2 = Singleton.getInstance();
}
final 필드는 초기값이 저장되면 이것이 최종적인 값이 되어서 프로그램 실행 도중에 수정할 수 없다는 것이다.
선언은 다음과 같이 한다.
final 타입 필드
상수는 static 이면서 final 이어야 한다. 상수는 다음과 같이 선언한다.
static final 타입 상수 = 초기값;
패키지의 물리적인 형태는 파일 시스템의 폴더이다. 패키지는 단순히 파일 시스템의 폴더 기능만 하는 것이 아니라 클래스의 일부분으로, 클래스를 유일하게 만들어주는 식별자 역할을 한다. 클래스 이름이 동일하더라도 패키지가 다르면 다른 클래스로 인식한다. 클래스의 전체 이름은 '패키지 이름 + 클래스 이름'인데 패키지가 상하위로 구분되어 있다면 도트(.)를 사용해서 다음과 같이 표현한다.
상위패키지.하위패키지.클래스
사용하고자 하는 클래스 또는 인터페이스가 다른 패키지에 소속되어 있다면 import문으로 해당 패키지의 클래스 또는 인터페이스를 가져와 사용할 것임을 컴파일러에게 알려줘야 한다.
import 상위패키지.하위패키지.클래스이름;
import 상위패키지.하위패키지.*;
말 그대로 접근을 제한하기 위해 사용한다.
접근 제한자는 세 가지 종류가 있다.
public 접근 제한자 : 외부 클래스가 자유롭게 사용할 수 있도록 한다.
protected 접근 제한자 : 같은 패키지 또는 자식 클래스에서 사용할 수 있도록 한다.
private 접근 제한자 : 외부에서 사용할 수 없도록 한다.
위의 세가지 제한자가 적용되지 않으면 default 접근 제한을 가진다.
default 접근 제한 : 같은 패키지에 소속된 클래스에서만 사용할 수 있다.
일반적으로 객체 지향 프로그래밍에서는 객체의 필드를 객체 외부에서 직접적으로 접근하는 것을 막는다. 그 이유는 외부에서 마음대로 변경할 경우 객체의 무결성이 깨질 수 있기 때문이다.
매개값을 검증해서 유효한 값만 객체의 필드로 저장하는 역할을 하는 메소드가 Setter이다.
필드값을 가공한 후 외부로 전달하는 메소드는 Getter이다.
private boolean stop;
//Getter
public boolean isStop() {
return stop;
}
//Setter
public void setStop(boolean stop) {
this.stop = stop;
}