클래스 안에 변수를 static으로 선언하면 해당 변수는 클래스당 하나만 존재한다. 반면 각 객체에는 자체적인 인스턴스 변수의 사본이 들어 있다.
변경 가능한 정적 변수는 드물지만 정적 상수(static final 변수)는 자주 사용한다. 예를 들어 Math 클래스는 Math.PI로 정적 상수를 갖는다.
public class Math {
public static final double PI = 3.14124124123123123;
}
static 키워드가 없으면 PI는 Math 클래스의 인스턴스 변수가 된다. 즉, PI에 접근하려면 Math 클래스의 객체가 필요하며, 모든 Math 객체는 자체적으로 PI의 사본을 가지고 있어야한다.
다음 코드는 객체를 담는 static final 변수의 예다. 난수가 필요할 때마다 새 난수 발생기를 생성하는 방법은 쓸모가 없고 안전하지도 않으므로, 클래스의 모든 인스턴스에서 난수 발생기를 하나 공유하는 방법이 더 낫다.
public class Employee {
private static final Random generator = new Random();
private int id;
public Employee() {
id = 1 + generator.nextInt(1_000_000);
}
}
앞에서는 정적 변수를 선언하면서 초기화 했는데, 정적 변수의 초기화 작업이 추가로 필요할 때는 정적 초기화 블록(static initialization block) 안에 넣으면 된다. 정적 초기화는 클래스를 처음 로드할 때 일어난다. 인스턴스 변수와 마찬가지로 정적 변수를 명시적으로 다른 값으로 설정하지 않으면 0이나 false 또는 null이 된다. 모든 정적 변수 초기화의 정적 초기화 블록은 클래스 선언 안에 나타난 순서로 실행된다.
public class CreditCardForm {
private static final ArrayList<Integer> expirationYear = new ArrayList<>();
static {
// 다음 20개 연도를 배열 리스트에 추가한다.
int year = LocalDate.now().getYear();
for (int i = year; i<= year + 20; i++) {
expirationYear.add(i);
}
}
}
정적 메서드는 객체에 작동하지 않는 메서드다. 밑의 메서드는 작업을 할 때 Math의 인스턴스를 전혀 사용하지 않는다.
Math.pow(x, a);
위의 Math가 인스턴스를 만들지 않는 이유는 자바에서 기본 타입은 클래스가 아니기 때문에 double의 메서드가 될 수 없고, Math 클래스의 인스턴스 메서드로 만들 수 있었지만, 그랬다면 pow메서드를 호출하기 위해 매번 Math 객체를 생성해야 하기 때문이다. 다음의 코드는 표준 라이브러리에 있는 Random 클래스에 메서드를 추가할 순 없지만, 다음과 같이 정적 메서드를 만들어 목적을 이룰 수 있다.
public class RandomNumbers {
public static int nextInt(Random gen, int low, int high) {
return low + generator.nextInt(high - low + 1);
}
}
int dieToss = RandomNumbers.nextInt(gen, 1, 6);
의미있는 방법은 아니지만 객체로도 정적 메서드를 호출할 수 있다. 하지만, 정적 메서드에서 인스턴스 변수에 접근은 불가능하다. 인스턴스 변수는 각 객체에 사본으로 복사되는 변수들인데, 정적 메서드는 객체가 없기 때문이다. 대신 자신이 속한 클래스의 정벅 변수에 접근할 수 있다.
JNI는 자바 가상머신을 구동하는 ‘native’ 운영체제(윈도, 리눅스, MacOS) 에서 제공하는 API에 접근하는 메커니즘이다. 네이티브 메서드는 C/C++ 언어로 작성하며 주로 특정 운영체제 고유의 API를 활용하거나 기존 C/C++ 기반 모듈을 자바 프로그램과 연동 하려고 한다. 이외에도 빠른 계산이 필요한 수학 라이브러리 등에서 사용한다. JNI를 사용하면 자바의 이식성이 떨어질 수 있고 잘못 작성하면 오히려 순수 자바로 작성한 프로그램보다 느릴 수 있기 때문에 필요할때만 써야한다.
factory method는 클래스의 새 인스턴스를 반환하는 정적 메서드를 의미한다. 정적 메서는 흔히 팩토리 메서드를 만드는데 사용한다. 밑의 코드는 사용 예이다.
NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance();
NumberFormat percentFormatter = NumberFormat.getPercentInstance();
생성자 대신 팩터리 메서드를 사용하는 이유는 생성자를 구별하는 유일한 방법은 생성자의 매개변수 타입이고, 따라서 매개변수가 없는 생성자를 두개 이상 둘 수 없기 때문이다. factory method를 사용하면 불필요하게 새 객체를 생성하는 대신 공유 객체를 반환할 수도 있다. Collections.emptyList()를 호출하면 변경할 수 없는 빈 리스트(공유 객체)를 반환한다.
자바에서는 연관된 클래스들을 한 패키지 안에 넣는다. 패키지를 사용하면 작업을 조직화하고 다른 사람이 제공한 코드를 라이브러리와 분리하기 편하다. 표준 자바 라이브러리는 java.lang, java.util, java.math 등을 비롯해 수많은 패키지에 분산되어 있다.
패키지를 사용하는 것은 클래스 이름의 유일성을 보장하기 위해서이다. 다른 패키지에 동일한 이름의 클래스가 존재해도 충돌이 일어나지 않는다.
패키지 이름은 java.util.regex처럼 점(.)으로 구분된 식별자 목록이다. 패키지 이름이 유일함을 보장하려면 유일하다고 알려진 인터넷 도메인 이름을 뒤집어서 사용하는 것이 좋다. 이 규칙을 벗어나는 대표적인 예외로 패키지이름이 java 또는 javax로 시작하는 표준 자바 라이브러리다.
클래스를 패키지 안에 넣으려면 클래스 소스 파일의 첫 번째 문장으로 package 문을 추가해야한다. 밑과 같이 작성하면 Employee 클래스는 com.horstmann.corejava 패키지에 속하게 되고, 전체 이름은 com.horstmann.corejava.Employee가 된다.
클래스를 기본 패키지(이름 없는 패키지 default package)에 추가하려면 package 문을 작성하지 않으면 된다. 하지만 권장되는 방법은 아니다. 파일 시스템에서 클래스 파일을 읽어 올 때 경로 이름은 반드시 패키지 이름과 일치해야 되기 때문이다.
package com.horstmann.corejava;
public class Employee {
...
}
클래스 파일을 파일 시스템에 저장하는 대신에 JAR 파일이라는 아카이브 파일 한 개 이상에 넣을 수도 있다. JDK에 들어있는 jar 유틸리티로 이런 아카이브를 만들 수 있다. JAR 파일은 보통 라이브러리와 프로그램을 묶는 데 사용한다.
jar --create --verbose --file library.jar com/mycompany/*.class
//짧은 옵션
jar -c -v -f library.jar com/mycompany/*.class
//tar 형식 옵션을 사용해도 된다.
jar cvf library.jar com/mycompany/*.class
프로젝트에서 라이브러리 JAR 파일을 사용할 때는 Class Path를 지정해서 Jar 파일의 위치를 컴파일러와 가상 머신에 알려야한다. 클래스 패스는 다음 요소를 포함할 수 있다.
public이 붙은 기능은 모든 클래스에서 사용할 수 있고, private이 붙은 기능은 선언한 클래스 안에서만 사용이 가능하다. 둘다 붙히지 않으면, 해당 기능(즉 클래스, 메서드, 변수)을 같은 패키지에 속한 모든 메서드에서 접근할 수 있다.
import 문을 사용하면 전체 이름을 쓰지 않아도 클래스를 이용할 수 있다. import java.util.Random; 을 위에 작성한다면, java.util.Random 으로 사용했던 것을 Random으로 사용할 수 있다. import를 선언하고 싶지 않다면 전체 클래스 이름을 사용해도 된다.
import문은 소스파일에서 첫번째 클래스 선언보다는 위에, package 문보다는 아래에 두어야 한다. 와일드 카드를 사용하면 패키지에 들어있는 모든 클래스를 임포트할 수 있다. import java.util.*;
와일드 카드는 오직 클래스만 임포트 하며, 패키지는 임포트할 수 없다.
여러 패키지를 임포트할 대는 이름 충돌이 일어날 수 있는데, java.util과 java.sql에는 Date가 있고 두 패키지를 같이 임포트할 경우, 패키지 이름을 붙이지 않으면 컴파일 에러가 난다.(java.sql.Date date 이렇게) 이때는 밑의 코드와 같이 특정 클래스를 임포트해서 사용하면 된다.
import java.util.*;
import java.sql.*;
import java.util.*;
import java.sql.*;
import java.sql.Date;
이 지시문을 소스파일의 위쪽에 추가하면 클래스 이름을 접두어로 붙이지 않고도 Math클래스의 정적 메서드와 정적 변수를 사용할 수 있다. 원하는 정적 메서드나 정벅 변수만 임포트할 수도 있다.
import static java.lang.Math.sqrt;
import static java.lang.Math.PI;
r = sqrt(pow(x, 2) + pow(y, 2)); //즉, Math.sqrt와 Math.pow를 의미한다.
클래스를 다른 클래스 내부에 두는 방법을 중첩 클래스라고 한다. 중첩 클래스는 가시성을 제한하거나 Element, Node, Item 처럼 일반적인 이름을 쓰면서도 정돈된 상태를 유지할 때 유용하다.
static class는 nested class에서만 사용이 가능하다. 밑의 item 클래스는 Invoice 안에 비공개로 선언해서 오직 Invoice의 메서드에서만 접근이 가능하다. 그래서 여기서는 이 내부 클래스의 인스턴스 변수를 굳이 비공개로 만들지 않았다. 그리고 내부 객체를 생성하는 메서드를 만들었다.
public class Invoice {
private static class Item{
String description;
int quantity;
double unitPrice;
double price() { return quantity * unitPrice; }
}
private ArrayList<Item> items = new ArrayList<>();
public void addItem(String description, int quantity, double unitPrice) {
Item newItem = new Item();
newItem.description = description;
newItem.quantity = quantity;
newItem.unitPrice = unitPrice;
items.add(newItem);
}
}
클래스는 중첩 클래스를 공개로도 만들 수 있는데 공개로 만들 때는 일반적인 캡슐화 메커니즘을 사용한다. Invoce.Item을 사용하여 Item 객체를 생성할 수 있다. 위 둘의 차이는 근본적으로 없다. 클래스 중첩은 그저 Item 클래스가 청구서에 들어 있는 물품을 표현한다는 사실을 분명하게 할 뿐이다.
public class Invoice {
public static class Item{
private String description;
private int quantity;
private double unitPrice;
public Item(String description, int quantity, double unitPrice) {
this.description = description;
this.quantity = quantity;
this.unitPrice = unitPrice;
}
double price() { return quantity * unitPrice; }
}
private ArrayList<Item> items = new ArrayList<>();
public void add(Item item) { items.add(item);}
public static void main(String[] args){
Invoice.Item newItem = new Invoice.Item("Toaster", 2, 19.95);
myInvoice.add(newItem)
}
}
static을 붙이지 않은 클래스를 내부 클래스(inner class)라고 한다.
밑의 코드는 내부 클래스를 이용하여 프로그래밍한 것이다. 내부 클래스와 정적 중첩 클래스의 차이는 내부클래스의 각 객체는 외부 클래스의 객체에 대한 참조를 갖는다.
public class Network {
public class Member {
private String name;
private ArrayList<Member> friends;
public Member(String name) {
this.name = name;
friends = new ArrayList<>();
}
// 내부 클래스의 메서드는 외부 클래스(outer class)의 인스턴스 변수에 접근할 수 있다. members는 외부에 선언된 변수다. 이것이 내부 클래스를 정적 중첩 클래스와 구별시키는 요인인데, 내부 클래스의 각 객체는 외부 클래스의 객체에 대한 참조를 포함한다.
public void deactivate() {
System.out.println(this);
members.remove(this);
}
}
private ArrayList<Member> members = new ArrayList<>();
public Member enroll(String name) {
Member newMember = new Member(name);
members.add(newMember);
return newMember;
}
public static void main(String[] args) {
Network myFace = new Network();
Network.Member fred = myFace.enroll("fred");
for(Member member : myFace.members) {
System.out.println(member.name);
}
System.out.println();
fred.deactivate();
for(Member member : myFace.members) {
System.out.println(member.name);
}
}
}
중첩 클래스의 인스턴스가 감싸고 있는 클래스의 어느 인스턴스에 속하는지 알 필요가 없을 때 정적 중첩 클래스를 사용한다. 내부 클래스는 이 정보가 중요할 때만 사용한다.
또 내부 클래스는 외부 클래스의 인스턴스를 거쳐 외부 클래스의 메서드를 호출할 수 있다. 예를들어 외부 클래스에 회원을 탈퇴(enroll) 시킨는 메서드가 있다면, deactivate 메서든느 다음과 같이 탈퇴 메서드를 호출할 수 있다.
public class Network {
public class Member {
private String name;
private ArrayList<Member> friends;
public Member(String name) {
this.name = name;
friends = new ArrayList<>();
}
public void deactivate() {
System.out.println(this);
members.remove(this);
}
public void deactivateWithInnerclass() {
unenroll(this);
}
}
private ArrayList<Member> members = new ArrayList<>();
public Member enroll(String name) {
Member newMember = new Member(name);
members.add(newMember);
return newMember;
}
public void unenroll(Member m) {
members.remove(m);
}
public static void main(String[] args) {
Network myFace = new Network();
Network.Member fred = myFace.enroll("fred");
for(Member member : myFace.members) {
System.out.println(member.name);
}
System.out.println();
fred.deactivateWithInnerclass();
for(Member member : myFace.members) {
System.out.println(member.name);
}
}
}
OuterClass.this 표현식은 외부 클래스 참조를 나타낸다.
public void deactivate() {
Network.this.members.remove(this);
}
위의 코드에서 Network.this 문법은 필수가 아니며 그냥 members로만 참조해도 암묵적으로 외부 클래스 참조를 사용한다. 하지만 외부 클래스 참조가 명시적으로 필요할때가 있다. 밑의 코드를 보라
public class Network {
public class Memeber {
public boolean belongsTo(Network n) {
return Network.this == n;
}
}
}
내부 클래스 객체를 생성할 때 해당 객체는 자신을 생성한 외부 클래스 객체를 기억한다. 그렇기 때문에 외부 클래스의 어느 인스턴스로도 내부 클래스 생성자를 호출할 수 있다.
Member newMember = new Member(name);// 밑의 축약형.
Member newMember = this.new Member(name);
온라인 API 문서는 표준 자바 라이브러리의 소스 코드에 javadoc을 실행한 결과다. 소스코드에 구분자 /** 로 시작하는 주석을 추가하면 된다.
javadoc 유틸리티는 다음 정보를 추출한다.
주석은 설명할 기능 바로 위에 붙이며, //* 을 시작으로 */로 마친다. 문서화 주석(/** … */)에는 자유 형식 텍스트와 그 뒤에 태그들을 적으며 태그는 @author 이나 @param 처럼 @ 기호로 시작한다.
자유 형식 텍스트의 첫 번째 문장은 요약문이어야 한다. javadoc 유틸리티는 이 요약문들을 추출해서 자동으로 요약 페이지를 만든다. 밑은 태그 종류다.
클래스 주석은 반드시 클래스 선언 바로 앞에 붙여야 한다. @author와 @version태그로 저자와 버전을 문서화할 수 있다.
/**
<code>Invoice</code> 각 객체는 각 주문 항목에 해당하는
품목을 나열한 청구서를 표현한다.
@author 홍길동
@author 바니 러블
@version 1.1
*/
각 메서드 주석은 메서드 바로 앞에 붙인다.
/**
직원의 급여를 인상한다.
@param byPercent 급여 인상 백분율
@return 인상액
*/
public double raiseSalary(double byPercent) {
double raise = salary * byPercent / 100;
salary += raise;
return raise;
}
공개 변수(정적 상수를 의미)만 문서화 하면된다.
/**
연간 일수(윤년 제외)
*/
public static final int DAYS_PER_YEAR = 365;
모든 문서화 주석에 @since 태그로 해당 기능을 도입한 버전을 적을 수 있다.
@since version 1.7.1
@deprecated Use <code>setVisible(true)</code> instead
@deprecated 태그에는 해당 클래스, 메서드, 변수를 사용하지 말아야 한다는 주석을 추가한다. 에너테이션도 @Deprecated가 있지만, 대체 방법을 제안하는 메커니즘이 없으므로 비권장 항목에는 에터네이션과 자바독 주석을 모두 붙힌다.
@see와 @link 태그로 자바독 문서의 관련 부분이나 외부 문서에 하이퍼링크를 추가할 수 있다. @see reference 태그는 ‘참고(see also)‘섹션에 하이퍼링크를 추가한다. @태그는 클래스와 메서드에 모두 사용할 수 잇다. 밑의 첫번째가 가장 유용하며, 클래스나 메서드, 변수 이름을 제공하면 javadoc이 해당 문서의 하이퍼링크를 삽입한다.
@see com.horstman.corejava.Employee#raiseSalary(double)
패키지 이름은 생략이 가능하고, 패키지 이름과 클래스 이름을 둘 다 생략할수도 있다. 이렇게 하기 위해선 해당 기능이 현재 패키지나 클래스에 있어야 한다.
@see Leap years@see태그 뒤에 <문자가 오면 하이퍼링크를 의미한다. 원하는 어떤 URL이든 링크가 가능하다.
@see 태그 뒤에 ” 문자가 오면 다음과 같이 큰따옴표 안에 있는 텍스트 ‘참고’섹션에 표시된다. @see “Core Java for the Impatient”
클래스 주석, 메서드 주석, 변수 주석 /** … */ 로 구분해서 자바 소스 파일에 직접 넣는다. 하지만 패키지 주석을 만들려면 각 패키지 디렉터리에 파일을 따로 추가해야한다.
패키지 디렉터리에 자바 파일 package-info.java를 추가한다. 이 파일의 첫 부분에는 /**와 */로 구분한 자바독 주석이 있어야 하고, 그 뒤에는 패키지 문이 있어야 한다. 패키지 문 뒤에는 코드나 주석을 적지 말아야한다.
모듈을 문서화하려면 module-info.java 파일에 주석을 넣어야 한다. @moduleGraph 지시문을 사용하면 모듈 의존성 그래프(module dependency graph)를 넣을 수 잇다.