클래스는 하나의 틀이고, 클래스를 통해 만든 것들이 '객체(instance)'이다. 객체는 속성과 동작으로 구성되는데, '속성'은 객체의 상태를 나타내고, '동작'은 객체의 행동을 나타내준다.
객체는 단독으로 존재할 수 있지만 객체들끼리의 관계를 통해서 더 다양한 현실 세계의 관계들을 표현할 수 있다. 객체 간의 관계의 종류는 '집합 관계', '사용 관계', '상송 관계'가 있다.
Title.java
라는 class file을 하나 만들어보자.
다음과 같이 하나의 file에 두 개의 java class를 만들 수 있다.
public class Title {
}
class SubTitle {
}
'public class'는 파일 이름과 동일한 class이름으로 정의해야하며, 하나만 존재할 수 있다. 즉, 파일 하나 당 하나의 public
class만 허용하고, 이 클래스는 파일 이름과 동일해야한다는 것이다.
Title title = new Title();
클래스 내부의 구성 요소는 크게 3가지로 구성된다. 하나는 객체의 상태를 나타내는 'field' 또는 '맴버 변수'이고 두번째는 객체의 동작을 나타내는 'method'이다. 마지막 하나는 객체의 초기 동작을 지시하는 생성자, 'constructor'이다.
public class Title {
// field, member
String titleName;
// 생성자, constructor
Title(String titleName) {
this.titleName = titleName;
}
// method
String getTitleName() {
return this.titleName;
}
}
참고로 this
는 객체 자기 자신을 말한다. 이는 객체 내부에서 객체 자기 자신이 가진 요소들에 접근할 때 사용한다.
new
연산자로 객체를 생성할 때 객체의 초기화 역할을 담당한다. 선언 형태는 메서드와 비슷하지만 리턴 타입도 없고 이름은 클래스 이름과 동일하다.참고로 field명과 method는 소문자로 카멜 케이스 작성이 관례이다.
모든 클래스는 생성자가 존재하며, 하나 이상을 가질 수 없다. 클래스에 생성자 선언이 없으면 다음과 같이 기본 생성자를 바이트 코드 파일에 자동으로 추가시킨다.
[public] 클래스명() {}
'클래스명'이 'public'으로 선언되면 'public'이 붙고, 아니면 안 붙는다.
참고로 생성자는 여러 생성자 함수들을 정의할 수 있다. 즉, 오버로딩이 가능하다는 것이다. 오버로딩이란 매개변수의 타입과 갯수가 다른 생성자 함수라면 여러 개를 선언할 수 있다는 것이다.
public class Title {
// field, member
String titleName;
// constructor1
Title() {
this.titleName = "title";
}
// constructor2
Title(String titleName) {
this.titleName = titleName;
}
// constructor3
Title(int titleName) {
this.titleName = String.valueOf(titleName);
}
// constructor4
Title(String titleName, String subTitle) {
this.titleName = titleName + " " + subTitle;
}
}
다음과 같이 여러 생성자를 생성할 수 있는데 매개변수의 갯수와 타입이 다르면 여러 생성자들을 생성할 수 있다. 이를 오버로딩이라고 한다.
생성자 오버로딩을 사용하다보면, 반복되는 코드들이 있는데 이를 this
를 사용해서 다음과 같이 최적화할 수 있다.
public class Title {
// field, member
String titleName;
String author;
// constructor2
Title(String titleName) {
this.titleName = titleName;
}
// constructor4
Title(String titleName, String author) {
this(titleName);
this.author = author;
}
}
this
를 통해서 다른 생성자를 호출할 수 있는 것이다. 이를 통해서 반복되는 코드를 효율적으로 관리할 수 있다.
메서드를 호출할 때에는 매개변수의 개수에 맞게 매개값을 제공해야하는데, 만약 메서드가 가변 길이 매개변수를 가지고 있다면 매개변수의 개수와 상관없이 매개값을 줄 수 있다. 가변길이 매개변수는 다음과 같이 선언한다.
int sum(int ...value) {
}
가변길이 매개변수는 메서드 호출 시 매개값을 쉼표로 구분해서 개수와 상관없이 제공할 수 있다.
int res = sum(1,2,3);
int res = sum(1,2,3,4,5);
가변 길이 매개값들은 자동으로 배열 항목으로 변환되어 메서드에서 사용되는 것이다. 따라서, 배열에서 사용하는 메서드와 사용 방식들이 똑같이 적용된다.
public class Computer {
int sum(int ...values) { // int[] values와 같다.
int sum = 0;
for (int v: values) {
sum += v;
}
return sum;
}
}
생성자와 마찬가지로 메서드도 오버로딩이 가능하다. 오버로딩은 매개변수의 갯수, 타입과 관련이 있는 것이지 리턴값과는 관련이 없다는 것에 유념하자.
field와 method는 선언 방법에 따라 인스턴스 맴버와 정적 맴버로 분류할 수 있다. 인스턴스 맴버로 선언되면 객체 생성 후 사용할 수 있고, 정적 맴버로 선언되면 객체 생성없이도 클래스를 통해 사용이 가능하다.
인스턴스 맴버는 객체에 소속된 맴버를 말한다. 따라서 객체가 있어야만 사용할 수 있다.
public class Car {
int gas;
}
class TestCar {
void test() {
Car car1 = new Car();
Car car2 = new Car();
car1.gas = 100;
car2.gas = 200;
}
}
car1
과 car2
는 서로 다른 객체로 gas
의 값도 서로 다른 값을 가진다. 이것이 객체에 소속된 인스턴스 맴버 변수이다.
자바는 클래스 로더(loader)를 이용해서 클래스를 메모리의 '메서드 영역'에 저장하고 사용한다. 정적(static) 맴버란 메서드 영역에 저장된 클래스에서 고정적으로 위치하는 맴버들을 말한다. 따라서, 정적으로 이미 만들어져있기 때문에 객체를 생성할 필요없이 클래스를 통해 바로 사용이 가능하다.
class file --bytecode 로딩--> class loader --bytecode 저장--> method 영역(static field, static method)
field와 method 모두 정적 맴버가 될 수 있다. 정적 필드와 정적 메서드로 선언하려면 다음과 같이 static
키워드를 추가하면 된다.
public class Car {
static int gas;
static void setGas(int gas) {
Car.gas = gas;
}
}
public class Main {
public static void main(String[] args) {
Car.setGas(10);
System.out.println(Car.gas); // 10
}
}
Car
클래스 객체를 생성하지 않고도 static field인 gas
와 static method인 setGas
에 접근할 수 있다.
static field와 static method는 다음과 같이 객체를 통해서 접근할 수도 있다.
public class Main {
public static void main(String[] args) {
Car car = new Car();
car.setGas(10);
System.out.println(car.gas); // 10
}
}
그러나, 이는 별로 좋지 못한 방법이므로 static field, static method는 반드시 클래스 이름을 통해서 접근하는 것이 좋다.
정적 field는 일반적으로 선언과 동시에 초기화를 한다.
public class Car {
static int capacity = 10;
static int gas = capacity / 2;
static void setGas(int gas) {
Car.gas = gas;
}
}
그렇지만, 상당히 긴 계산이 필요하다면 다음과 같이 static {}
block을 만들 수 있다. 단, 정적 영역이기 때문에 this
는 쓸 수 없다.
public class Car {
static int capacity = 10;
static int gas;
static {
String gasType = System.getenv("GAS_TYPE");
switch (gasType) {
case "TYPE1":
gas = 100 * capacity;
case "TYPE2":
gas = 200 * capacity;
default:
gas = 10 * capacity;
}
}
static void setGas(int gas) {
Car.gas = gas;
}
}
참고로 static
field들은 위에서부터 순서대로 평가된다고 생각하면 된다.
main
함수도 정적 메서드이기 때문에, 클래스 자체에 저장되고 실행되는 코드이다. 따라서, Main
클래스 내부의 맴버 변수를 선언해도 main
에서 접근하지 못한다.
인스턴스 필드와 정적 필드는 언제든지 값을 변경할 수 있다. 그러나, 경우에 따라서는 읽기 전용으로 만들고 싶을 때도 있다. 이때는 final
을 사용해야 한다.
final 타입 필드;
또는
final 타입 필드 = 초기값;
final
필드에 초기값을 줄 수 있는 방법은 다음 두 가지 방법 밖에 없다.
1. field 선언 시에 초기값 대입
2. 생성자에서 초기값 대입
계산이 필요없는 값이라면 선언 시에 바로 주는 것이 가장 좋다. 하지만 복잡한 초기화 코드가 필요하거나 객체 생성 시 외부에서 전달된 값을 의존하여 초기화되어야 한다면 생성자에서 해야한다. 만약 final
을 그 어디에서도 값을 넣어주지 않으면 컴파일 에러가 발생한다.
public class Car {
final int gas = 10;
final int capacity;
Car(int capacity) {
this.capacity = capacity;
}
}
gas
와 capacity
는 둘 다 final
이므로 선언과 동시에 초기화되어야하거나 생성자에서 계산되어야 한다.
이제 해당 맴버 변수들은 수정이 불가능하다.
final
은 객체에 속하는 맴버 변수로 쓰이기 때문에 객체가 있어야만 사용할 수 있다. 그런데, 선언과 동시에 초기화되는 값은 일반적으로 고정된 값으로 모든 객체들이 공유해서 쓰이는 요소이다. 따라서, 클래스에서 사용하는 정적 변수로 선언되는 것이 좋다. 이렇게 되면 static final
이 되며, 이는 마치 다른 언어에서 상수를 선언하는 const
와 일맥상통하다.
public class Car {
static final int gas = 10;
final int capacity;
Car(int capacity) {
this.capacity = capacity;
}
}
public class Main {
public static void main(String[] args) {
Car car = new Car(20);
System.out.println(Car.gas); // 10
System.out.println(car.capacity); // 20
}
}
static final
을 하나의 상수처럼 쓸 수 있다는 사실을 알도록 하자.
더불어 final
이 static
과 함께 쓰이면 두 가지 방법으로 대입이 가능하다는 사실을 알도록 하자.
1. 선언과 동시에 초기화
2. static block 사용
static block
사용은 다음과 같다.
public class Car {
// 정적 변수
static final int gas;
static {
String gasType = System.getenv("GAS_TYPE");
if (gasType.equals("TYPE1")) {
gas = 100;
} else {
gas = 10;
}
}
// 인스턴스 변수
final int capacity;
Car(int capacity) {
this.capacity = capacity;
}
}
자바의 패키지는 단순히 디렉토리만을 의미하지 않는다. 패키지는 클래스의 일부분이먀, 클래스를 식별하는 용도로 사용된다.
패키지는 주로 개발 회사의 도메인 이름의 역순으로 만든다. 가령, mycompany.com
회사의 패키지는 com.mycompany
로 만든다. 이렇게 하면 두 회사에서 개발한 Car
class가 있을 경우 다음과 같이 관리할 수 있다.
com
|
|------mycompany
| |
| ------ Car.class
|
|------yourcompany
| |
| ------ Car.class
패키지는 상위 패키지와 하위 패키지를 .
로 구분한다. .
는 물리적으로 하위 디렉토리임을 뜻한다. com.mycompany
에서 com
은 상위 디렉토리, mycompany
는 하위 디렉터리이다.
패키지는 클래스를 식별하는 용도이기 때문에 클래스의 전체 이름에 포함된다. 가령, Car
class의 경우 com.mycompany.Car
이 된다. 따라서, com.yourcompany.Car
과는 서로 다른 클래스임을 알 수 있다.
패키지에 속한 바이트코드 파일(~.class
)은 따로 떼어내서 다른 디렉토리로 이동시킬 수 없다.
패캐지 디렉터리는 클래스를 컴파일하는 과정에서 자동으로 생성된다. 컴파일러는 클래스의 패키지 선언을 보고 디렉터리를 자동 생성시킨다. 패키지 선언은 package
키워드와 함께 패키지 이름을 기술한 것으로 항상 소스 파일 최상단 위에 위치해야한다.
package 상위패키지.하위패키지;
public class 클래스명 {...}
패키지 이름은 모두 소문자로 작성하는 것이 관례이다. 패키지 이름을 적을 때 마지막은 프로젝트 이름을 붙여주는 것이 일반적이다.
coms.samsung.projectname
coms.lg.projectname
org.apache.projectname
같은 패키지에 있는 클래스는 아무런 조건없이 사용할 수 있지만, 다른 패키지에 있는 클래스를 사용하려면 import
문을 사용하여 어떤 패키지의 클래스를 사용하는 지 명시해야한다.
다음은 com.mycompany
패키지의 Car
클래스의 com.hankook
패캐지의 Tier
클래스를 사용하기 위해 import
문을 사용한 것이다.
package com.mycompany;
import com.hankook.Tire;
public class Car {
Tier tier = new Tier();
}
만역 동일한 패키지에 포함된 다수의 클래스를 사용해야한다면 클래스 이름을 생략하고 *
을 사용할 수 있다.
import com.hankook.*;
import
문은 하위 패키지를 포함하지 않는다. 따라서, com.hankook
패키지에 있는 클래스도 사용해야 하고, com.hankook.project
패키지에 있는 클래스도 사용해야 한다면 다음과 같이 두 개의 import
문이 필요하다.
import com.hankook.*;
import com.hankook.project.*;
단, 위와 같이 *
을 쓸 경우 두 패키지에 동일한 클래스 명이 있을 수 있다. 이때는 컴파일에러가 발생하므로 명확히 하나를 지정해주어야 한다.
경우에 따라서 객체의 필드를 외부에서 변경하거나 메서드를 호출할 수 없도록 막아야 할 필요가 있다. 중요한 필드와 메서드는 외부로 노출되지 않도록 해서 객체의 무결성을 유지하기 위함이다.
이러한 기능을 위해서 접근 제한자(access modifier)를 사용할 수 있다. 접근 제한자는 public
, protected
, private
, default
가 있다.
접근 제한이 강화되는 순서는 다음과 같다. 오른쪽으로 갈수록 더 접근 제한이 강해지는 것이다.
public < protected < default < private
접근 제한자 | 제한 대상 | 제한 범위 |
---|---|---|
public | 클래스, 필드, 생성자, 메서드 | 없음 |
protected | 필드, 생성자, 메서드 | 같은 패키지 이거나, 자식 객체만 사용 가능 |
default | 클래스, 필드, 생성자, 메서드 | 같은 패키지 |
private | 필드, 생성자, 메서드 | 객체 내부 |
protected
는 추후에 더 알아보도록 하고, public
과 default
, private
접근 제한자를 class
, 생성자
, field
, method` 순서로 어떻게 적용되는 지 알아보도록 하자.
제일 먼저 class
의 경우는 접근 제한자로 public
과 default
만 쓸 수 있다. private
클래스와 protected
클래스는 없다.
public
접근 제한자가 붙은 class
는 같은 패키지뿐만 아니라, 다른 패키지에서도 사용할 수 있다. 만약 어떠한 접근 제한자도 안붙이면 default
접근 제한자가 사용된다. 이 경우 클래스는 같은 패키지에서는 아무런 제한 없이 사용할 수 있지만, 다른 패키지에서는 사용할 수 없다.
package1
을 만들고 B.java
에 다음의 코드를 넣어보도록 하자.
package package1;
public class B {
A a; // 접근 가능
}
class A {
}
A
class는 default 접근자를 가지고 B
class는 public 접근자를 갖는다. 따라서, B
class는 다른 패키지에서도 접근 가능하지만, A
class는 B.java
가 있는 패키지 안에서만 접근 가능하고, 외부 패키지에서는 접근이 불가능하다.
import package1.B;
public class Main {
public static void main(String[] args) {
B b;
A a; // error
}
}
B
class는 가지고 올 수 있지만, A
는 못 가져온다.
생성자에도 접근 제한자를 두어서, 호출 가능 여부를 따질 수 있다. 생성자는 public
, default
, private
, protected
접근 제한자를 가질 수 있다. protected
는 추후에 알아보자.
package1
을 만들고, A.java
에 다음의 코드를 입력하도록 하자.
package package1;
public class A {
A a1 = new A(true);
A a2 = new A(1);
A a3 = new A("문자열");
public A(boolean b) {
}
A(int b) {
}
private A(String s) {
}
}
public
, default
, private
생성자 모두 클래스 내부에서는 호출이 가능한 것을 볼 수 있다. 그러나 동일한 패키지의 다른 클래스에서는 private
생성자는 호출이 불가능하다.
같은 pakcage1
에 B.java
파일을 만들고 다음의 코드를 입력하도록 하자.
package package1;
public class B {
A a1 = new A(true);
A a2 = new A(1);
A a3 = new A("문자열"); // error
}
new A("문자열");
은 호출이 안되는 것을 볼 수 있다. 이는 private
생성자의 경우 class 내부에서만 호출이 가능하기 때문이다. 반면에 public
과 default
생성자는 같은 패키지 내부에서는 호출이 가능하다.
그럼 이제 다른 패키지에서 A
class의 생성자를 호출해보도록 하자.
import package1.A;
public class Main {
public static void main(String[] args) {
A a1 = new A(true);
A a2 = new A(1); // error
A a3 = new A("문자열"); // error
}
}
public
생성자 이외의 default
, private
생성자는 호출이 불가능한 것을 볼 수 있다.
field와 method는 public
, default
, private
, protected
접근 제한자를 가질 수 있다. protected
는 추후에 알아보자.
package1
를 만들고 A.java
에 다음의 코드를 입력하자.
package package1;
public class A {
// public 접근 제한을 갖는 필드 선언
public int field1;
// default 접근 제한을 갖는 필드 선언
int field2;
// private 접근 제한을 갖는 필드 선언
private int field3;
public void exec() {
field1 = 1; // 접근 가능
field2 = 2; // 접근 가능
field3 = 3; // 접근 가능
method1(); // 접근 가능
method2(); // 접근 가능
method3(); // 접근 가능
}
// public 접근 제한을 갖는 메서드 선언
public void method1() {
}
// default 접근 제한을 갖는 메서드 선언
void method2() {
}
// private 접근 제한을 갖는 메서드 선언
private void method3() {
}
}
클래스 코드 내부에서는 public
, default
, private
접근 제한자로 만든 field
와 method
들에 대해서 접근이 가능한 것을 볼 수 있다.
반면에 같은 패키지의 다른 클래스인 B.java
에서는 private
접근 제한자가 붙은 field와 method에 대해서는 접근이 불가능하다.
package package1;
public class B {
public void method() {
A a = new A();
a.field1 = 1; // 접근 가능
a.field2 = 2; // 접근 가능
a.field3 = 3; // 접근 불가능
a.method1(); // 접근 가능
a.method2(); // 접근 가능
a.method3(); // 접근 불가능
}
}
다른 패키지에서는 public
빼고는 default
와 private
말고는 접근이 불가능하다.
import package1.A;
public class Main {
public static void main(String[] args) {
A a = new A();
a.field1 = 1; // 접근 가능
a.field2 = 2; // 접근 불가능
a.field3 = 3; // 접근 불가능
a.method1(); // 접근 가능
a.method2(); // 접근 불가능
a.method3(); // 접근 불가능
}
}
정리하자면 생성자와 field, method의 접근 제한자 허용 범위는 다음과 같다.
1. 클래스 코드 내부: public, default, private
2. 패키지 내부: public, default
3. 패키지 외부: public
클래스의 경우는 public과 default만 가능하므로 다음과 같다.
1. 패키지 내부: default
2. 패키지 외부: public
private
접근 제한자를 생성자에 사용하면 클래스 코드 내부에서만 사용할 수 있었다. 이는 클래스 코드 내부에 생성자를 private
하나만 두게 될 경우, 같은 패키지나 외부 패키지에서 해당 클래스의 생성자를 호출하지 못하여 해당 객체의 생성이 불가능하다.
application 전체에서 단 한 개의 객체만을 생성해서 사용하고 싶다면 싱글톤 패턴을 사용할 수 있다. 싱글톤 패턴은 생성자를 private
접근 제한자를 사용해서 new
연산자로 생성자를 호출할 수 없도록 하는 것이다.
생성자를 호출할 수 없으니 마음대로 객체를 생성할 수가 없다. 대신 싱글톤 패턴으로 정적 메서드를 통해서 객체를 얻도록 하는 것이다.
public class 클래스이름 {
private static 클래스이름 instance = new 클래스이름();
private 클래스이름() {
}
public static 클래스이름 getInstance() {
return instance;
}
}
클래스의 정적 맴버 변수로 해당 클래스 타입인 instance
를 갖도록 하는 것이다. 이 instance
는 private
생성자를 통해 초기에 생성되는 것이다. 해당 클래스는 private
생성자가 정의되어 있기 때문에 같은 패키지나 외부 패키지에서 생성이 불가능하다. 즉, 처음 자바 바이트코드가 로팅되고 해당 클래스를 메서드 영역에 적재할 때, 생성된 정적 instance
가 유일한 것이다.
이 instance
에 접근하기 위해서는 정적 메서드인 getInstance
로만 가능한 것이다.