Chapter 8. 예외처리(Exception Handling)
1. 예외처리(exception handling)
1.1 프로그램 오류
프로그램이 실행 중 어떤 원인에 의해서 오작동을 하거나 비정상적으로 종료되는 경우가 있다. 이러한 결과를 초래하는 원인을 프로그램 에러 또는 오류라고 한다.
이를 발생 시점에 따라 컴파일 에러(compile-time error)와 런타임 에러(runtime error)로 나눌 수 있으며, 이 외에도 논리적 에러(logical error)가 존재한다.
▼ 에러의 종류
컴파일 에러(compile-tme error) : 컴파일 시에 발생하는 에러
런타임 에러(runtime error) : 실행 시에 발생하는 에러
논리적 에러(logical error) : 실행은 되지만, 의도와 다르게 동작하는 것. (ex. 창고의 재고가 음수가 됨 등등...)
소스를 컴파일하면 컴파일러가 소스코드(*.java)에 대해 오타나 잘못된 구문, 자료형 체크 등의 기본적인 검사를 수행하여 오류가 있는지 알려준다. 컴파일을 마치면 클래스 파일(*.class)이 생성되고, 생성된 클래스 파일을 실행할 수 있게 되는 것이다.
런타임 에러를 방지하기 위해서는 프로그램의 실행 도중 발생할 수 있는 모든 경우의 수를 고려하여 이에 대한 대비를 하는 것이 필요하다. 자바에서는 실행 시(runtime) 발생할 수 있는 프로그램 오류를 '에러(error)'와 '예외(exception)', 두 가지로 구분한다.
▼ 런타임 에러의 종류
에러(error) : 프로그램 코드에 의해서 수습될 수 없는 심각한 오류 (ex. 메모리 부족(OutOfMemoryError), 스택오버플로우(StackOverFlowError) 등)
예외(exception) : 프로그램 코드에 의해서 수습될 수 있는 다소 미약한 오류
에러가 발생하면 프로그램의 비정상적 종료를 막을 수 없지만, 예외는 발생하더라도 프로그래머가 이에 대한 적절한 코드를 미리 작성해놓음으로써 프로그램의 비정상적인 종료를 막을 수 있다.
1.2 예외 클래스의 계층 구조
자바에서는 실행 시 발생할 수 있는 오류(Exception과 Error)를 클래스로 정의하였다. 모든 클래스의 조상은 Object 클래스이므로 Excpetion과 Error 클래스 역시 Object 클래스의 자손들이다.
모든 예외의 최고 조상은 Exception 클래스이며, 상속계층도를 Exception 클래스부터 도식화하면 아래와 같다.
위 그림에서 볼 수 있듯이 예외 클래스들은 다음과 같이 두 그룹으로 나눠질 수 있다.
① Exception 클래스와 그 자손들(RuntimeException과 그 자손들 제외. 위 그림의 하얀색 부분) = Exception 클래스들
: 사용자의 실수와 같은 외적인 요인에 의해 발생하는 예외
(ex. 존재하지 않는 파일의 이름을 입력(FileNotFoundException), 클래스의 이름을 잘못 기입(ClassNotFoundException), 입력된 데이터의 형식이 잘못됨(DataFormatException)
② RuntimeException 클래스와 그 자손들(위 그림의 하늘색 부분) = RuntimeException 클래스들
: 프로그래머의 실수에 의해서 발생될 수 있는 예외. 자바의 프로그래밍 요소들과 관계가 깊다.
(ex. 배열의 범위를 벗어나기(ArrayIndexOutOfBoundsException), 값이 null인 참조변수의 멤버를 호출(NullPointerException), 클래스의 형변환을 잘못하기(ClasscastException), 정수를 0으로 나누기(ArithmeticException)
1.3 예외 처리하기 - try-catch 문
예외처리(exception handling)의
정의 : 프로그램 실행 시 발생할 수 있는 예외에 대비한 코드를 작성하는 것
목적 : 프로그램의 비정상 종료를 막고, 정상적인 실행상태를 유지하는 것
발생한 예외를 처리하지 못하면 프로그램은 비정상적으로 종료되며, 처리되지 못한 예외(uncaught exception)는 JVM의 '예외 처리기(UncaughtExceptionHandler)'가 받아서 예외의 원인을 화면에 출력한다.
▼ try-catch 문의 구조
try {
// 예외가 발생할 가능성이 있는 문장을 넣는다.
} catch (Exception1 e1) {
// Exception1이 발생했을 경우, 이를 처리하기 위한 문장을 적는다.
} catch (Exception2 e2) {
// Exception2가 발생했을 경우, 이를 처리하기 위한 문장을 적는다.
} catch (ExceptionN eN) {
// ExceptionN이 발생했을 경우, 이를 처리하기 위한 문장을 적는다.
}
하나의 try 블럭에는 여러 종류의 예외를 처리할 수 있도록 하나 이상의 catch 블럭이 올 수 있으며, 이 중 발생한 예외의 종류와 일치하는 단 한 개의 catch 블럭만 수행된다. 발생한 예외의 종류와 일치하는 catch 블럭이 없으면 예외는 처리되지 않는다.
여러 개의 catch 블럭 중 오류가 발생했을 때에는 단 하나만 실행되기 때문에 참조변수 e는 중복해서 사용해도 된다. 그러나 catch 블럭 안에 또 다른 try-catch 문이 포함될 경우에는 이름을 각자 다르게 해주어야 한다.
1.4 try-catch문에서의 흐름
▶ try 블럭 내에서 예외가 발생한 경우
- 발생한 예외와 일치하는 catch 블럭이 있는지 확인한다.
- 일치하는 catch 블럭을 찾게 되면, 그 catch 블럭 내의 문장들을 수행하고 전체 try-catch문을 빠져나가서 그 다음 문장을 계속해서 수행한다. 만약 일치하는 catch 블럭을 찾지 못하면, 예외는 처리되지 못한다.
▶ try 블럭 내에서 예외가 발생하지 않은경우
- catch 블럭을 거치지 않고 전체 try-catch문을 빠져나가서 수행을 계속한다.
1.5 예외의 발생과 catch 블럭
catch 블럭의 괄호( ) 안에는 처리하고자 하는 예외와 같은 타입의 참조변수 하나를 선언해준다. 예외가 발생하면 예외에 해당하는 클래스의 인스턴스가 만들어진다. 첫번째 catch 블럭부터 차례로 내려가면서 catch 블럭의 괄호( ) 내에 선언된 참조변수의 종류와 생성된 예외클래스의 인스턴스에 instanceof 연산자를 이용해서 검사하게 되는데, 검사결과가 true인 catch 블럭을 만날 때까지 찾는다. 검사 결과가 true인 catch 블럭을 찾으면 블럭 안에 있는 문장을 모두 수행한 후에 try-catch 문을 빠져나간다.
모든 예외 클래스는 Exception 클래스의 자손이므로, catch 블럭의 괄호( )안에 Exception 클래스 타입의 참조변수를 선언해 놓으면 어떤 종류의 예외가 발생하더라도 이 catch 블럭에 의해서 처리된다.
class ExceptionEx7 {
public static void main(String args[]) {
System.out.println(1);
System.out.println(2);
try {
System.out.println(3);
System.out.println(0/0); // ArithmeticException 발생
System.out.println(4);
} catch (ArithmeticException ae) {
if (ae instanceof ArithmeticException) {
System.out.println("true");
}
System.out.println("ArithmeticException");
} catch (Exception e) {
System.out.println("Exception");
} // try-catch의 끝
} // main의 끝
}
만약 위 코드와 같이 다른 예외의 Exception 클래스가 같이 있을 경우, catch 블럭은 위에서부터 차례로 검사하기 때문에 ArithmeticException을 제외한 예외를 catch(Exception e) 블럭에서 처리하게 된다.
예외가 발생했을 때 생성되는 예외 클래스의 인스턴스에는 발생한 예외에 대한 정보가 담겨있으며, getMessage()와 printStackTrace()를 통해서 이 정보들을 얻을 수 있다. catch 블럭의 괄호( )에 선언된 참조변수를 통해 이 인스턴스에 접근할 수 있으며, 선언된 catch 블럭 내에서만 사용 가능하다.
printStackTrace() : 예외발생 당시의 호출스택(Call Stack)에 있었던 메서드의 정보와 예외 메시지를 화면에 출력한다.
getMessage() : 발생한 예외클래스의 인스턴스에 저장된 메시지를 얻을 수 있다.
class ExceptionEx8 {
public static void main(String args[]) {
System.out.println(1);
System.out.println(2);
try {
System.out.println(3);
System.out.println(0/0); // ArithmeticException 발생
System.out.println(4); // 예외 발생으로 인해 실행되지 않는다.
} catch (ArithmeticException ae) {
ae.printStackTrace();
System.out.println("예외메시지 : " + ae.getMessage());
}
} // try-catch의 끝
System.out.println(6);
} // main의 끝
}
/* 실행 결과
1
2
3
java.lang.ArithmeticException: / by zero
at ExceptionEx8.main(ExceptionnEx8.java:7)
예외 메시지 : / by zero
6
*/
JDK 1.7부터 여러 catch 블럭을 '|' 기호를 이용해서 하나의 catch 블럭으로 합칠 수 있게 되었으며, 이를 '멀티 catch블럭'이라 한다. 그러나 만약 '|' 기호로 연결된 예외 클래스가 조상과 자손의 관계(ex. Exception 클래스와 다른 예외 클래스들)에 있다면 컴파일 에러가 발생한다.
▼ 멀티 catch 블럭의 사용
// 기존의 코드
try {
...
} catch (ExceptionA e) {
e.printStackTrace();
} catch (ExceptionB e2) {
e.printStackTrace();
}
// 멀티 catch블럭
try {
...
} catch (ExceptionA | ExceptionB e) {
e.printStackTrace();
}
또한 멀티 catch 블럭 내에서는 여러 예외를 처리하기 때문에 실제로 어떤 예외가 발생한 것인지 알기 어렵다. 따라서 이 경우 참조변수 e로 멀티 catch 블럭에 '|' 기호로 연결된 예외 클래스의 공통 분모인 조상 예외 클래스에 선언된 멤버만 사용할 수 있다. 필요하다면 아래와 같이 instanceof를 사용하여 어떤 예외가 발생한 것인지 확인하고 개별로 처리할 수 있지만, 이 경우에는 굳이 멀티 catch블럭을 사용할 필요가 없다.
try {
...
} catch (ExceptionA | ExceptionB e) {
e.methodA(); // 에러. ExceptionA에 선언된 methodA()는 호출불가
if (e instanceof ExceptionA) {
ExceptionA e1 = (ExceptionA)e;
e1.methodA(); // OK. ExceptionA에 선언된 메서드 호출 가능
} else { // if (e instanceof ExceptionB)
...
}
e.printStackTrace();
}
마지막으로 멀티 catch블럭에서 선언된 참조변수 e는 상수이므로 값을 변경할 수 없다는 제약이 있는데, 이것은 여러 catch 블럭이 하나의 참조변수를 공유하기 때문에 생기는 제약이므로 실제로 값을 바꿀 일은 없을 것이다.
1.6 예외 발생시키기
키워드 throw를 사용해서 프로그래머가 고의로 예외를 발생시킬 수 있으며, 방법은 아래의 순서를 따르면 된다.
- 먼저, 연산자 new를 이용해서 발생시키려는 예외 클래스의 객체를 만든 다음
Exception e = new Exception("고의로 발생시켰음"); - 키워드 throw를 이용해서 예외를 발생시킨다.
throw e;
class ExceptionEx9 {
public static void main(String args[]) {
try {
Exception e = new Exception("고의로 발생시켰음.");
throw e; // 예외를 발생시킴
throw new Exception("고의로 발생시켰음."); // 위의 두 줄을 한 줄로 줄여쓰기 가능
} catch (Exception e) {
System.out.println("에러 메시지 : " + e.getMessage());
e.printStackTrace();
}
System.out.println("프로그램이 정상 종료 되었음.");
}
}
/* 실행 결과
에러 미시지 : 고의로 발생시켰음.
java.lang.Exception : 고의로 발생시켰음.
at ExceptionEx9.main(ExceptionEx9.java:4)
프로그램이 정상 종료 되었음.
*/
Exception 인스턴스를 생성할 때, 생성자에 String을 넣어주면, 이 String이 Exception 인스턴스에 메시지로 저장된다. 이 메시지는 getMessage()를 이용해서 얻을 수 있다.
만약 throw 키워드를 이용해 예외를 발생시켰는데 try-catch 문을 이용하여 예외처리를 하지 않았다면, 컴파일 단계에서 'unreported exception; must be caught or declared to be thrown'이라는 오류가 발생한다.
다만, 일반적인 Exception이 아닌 RuntimeException을 발생시킬 경우에는 컴파일이 가능한데, 컴파일에서는 오류 메시지를 출력하지 않지만 실행하면 오류 메시지를 출력한다. RuntimeException같은 경우는 프로그래머의 실수로 일어나는 것이기 때문에 예외처리를 강제하지 않는다. 만약 RuntimeException 클래스들에 속하는 예외가 발생할 가능성이 있는 코드에도 예외처리를 필수로 해야한다면, 참조변수와 배열이 사용되는 모든 곳에 예외처리를 해주어야 한다.
컴파일러가 예외 처리를 확인하지 않는 RuntimeException 클래스들은 'unchecked 예외'라 부르고, 예외처리를 확인하는 Exception 클래스들은 'checked 예외'라고 부른다.
1.7 메서드에 예외 선언하기
예외를 처리하는 방법에는 try-catch문 외에도 예외를 메서드에 선언하는 방법이 있다. 메서드에 예외를 처리하려면 선언부에 throws를 사용해서 메서드 내에서 발생할 수 있는 예외를 적어주면 된다. 만약 예외가 여러 개일 경우에는 쉼표(,)로 구분한다. 또한 이곳에도 마찬가지로 모든 예외를 의미하는, 예외의 최고 조상 클래스인 Exception 클래스를 선언하는 것도 가능하다.
▼ 메서드에 예외 선언하기
void method() throws Exception1, Exception2, ... ExceptionN {
// 메서드의 내용
}
메서드의 선언부에 예외를 선언함으로써 메서드를 사용하려는 사람이 메서드의 선언부를 보았을 때, 이 메서드를 사용하기 위해서는 어떠한 예외들이 처리되어져야 하는지 쉽게 알 수 있다.
즉, 위의 예시 코드는 method()를 사용하기 위해서는 Exception1, Exception2, ... ExceptionN이라는 예외에 대해 예외처리가 이루어져야 한다는 것을 명시적으로 나타낸다. 명시적으로 작성하는 코드는 대부분 checked 예외이며, RuntimeException에 대해서는 보통 명시적으로 적지 않는다.
throws 키워드가 있는 메서드를 다른 메서드가 호출한 경우, 이러한 예외처리를 또다시 해당 메서드의 throws에 넣어 다른 메서드에게 전달할 수 있다. 이렇게 전달한 예외가 main에서도 예외처리 되지 않으면 프로그램에 에러가 발생하고 종료된다.
class ExceptionEx12 {
public static void main (String args[]) throws Exception{
method1(); // 같은 클래스 내의 static 멤버이므로 객체 생성 없이 호출 가능
} // main 메서드의 끝
static void method1() throws Exception {
method2();
} // method1의 끝
staic void method2() throws Exception {
throw new Exception();
} // method2의 끝
}
/* 실행 결과
java.lang.Exception
at ExceptionEx12.method2(ExceptionEx12.java:11)
at ExceptionEx12.method1(ExceptionEx12.java:7)
at ExceptionEx12.main(ExceptionEx12.java:3)
*/
위의 실행 결과를 보면 프로그램의 실행 도중 java.lang.Exception이 발생하여 비정상적으로 종료했다는 것을 알 수 있다. 예외가 발생했을 때 호출 스택(call stack)의 내용을 알 수 있다. 위의 결과로부터 알 수 있는 사실은 다음과 같다.
- 예외가 발생했을 때, 모두 3개의 메서드(main, method1, method2)가 호출스택에 있었으며,
- 예외가 발생한 곳은 제일 윗줄에 있는 method2()라는 것과
- main 메서드가 method1()을, 그리고 method1()은 method2()를 호출했다는 것을 알 수 있다.
위의 예제에서 method2()에서 'throw new Exception();' 문장에서 예외가 발생했는데 try-catch문으로 예외 처리를 해주지 않아 강제적으로 종료되며 자신을 호출한 method1()에게 예외를 넘겨주었다. 그리고 method1()은 마찬가지로 예외처리를 해주지 않아 강제적으로 종료되며 자신을 호출한 main()에게 예외를 넘겨주었으나 마찬가지로 예외처리가 되지 않아 main 메서드가 강제로 종료되며 프로그램이 비정상적으로 종료된 것이다.
예외를 처리하는 방법은 예외가 발생하는 throws를 쓰지 않고 함수 내부에서 try-catch문을 써서 예외를 처리하는 방법과, 함수에 throws를 작성하고 해당 함수를 호출하는 다른 함수에서 try-catch문으로 예외를 처리하는 방법이 있다.
교재 p.431-433에는 이러한 예외 처리를 이용해 파일명을 사용자가 넘겨주면 해당 파일명으로 파일을 생성하는 코드 예제가 있다. 이 코드에서는 사용자가 아무것도 넘겨주지 않았을 때 해당 예외를 어떻게 처리할 것인지에 대해 다룬다.
1.8 finally블럭
finally 블럭은 예외의 발생 여부에 상관 없이 실행되어야 할 코드를 포함시킬 목적으로 사용된다. try-catch문의 끝에 선택적으로 덧붙여 사용할 수 있으며, try-catch-finally의 순서로 구성된다.
▼ finally 블럭의 사용
try {
// 예외가 발생할 가능성이 있는 문장들을 넣는다.
} catch (Exception1 e1) {
// 예외 처리를 위한 문장을 적는다.
} finally {
// 예외의 발생 여부에 관계 없이 항상 수행되어야 하는 문장들을 넣는다.
// finally 블럭은 try-catch문의 맨 마지막에 위치해야 한다.
}
예외가 발생한 경우에는 try → catch → finally의 순서로 실행되고, 예외가 발생하지 않은 경우에는 try → finally의 순서로 실행된다.
만약 아래 코드와 같이 try블럭에서 return이 발생하는 경우에는 finally의 문장들이 먼저 실행된 후에, 현재 실행중인 메서드를 종료한다. 이와 마찬가지로 catch 블럭의 문장 수행 중에 return문을 만나도 finally블럭의 문장들은 수행된다.
▼ 수행 순서
try → (오류가 발생했을 경우 catch) → return문을 만나면 실행하지 않고 finally 블럭 실행 → return 수행
1.9 자동 자원 반환 - try-with-resources문
JDK 1.7부터 try-with-resources문이라는 try-catch문의 변형이 새로 추가되었다. 이 구문은 주로 '15장 입출력(I/O)'과 관련된 클래스를 사용할 때 유용하므로, 일단 있다는 것 정도만 알아두자.
주로 입출력에 사용도는 클래스 중에서는 사용한 후에 꼭 닫아줘야 하는 것들이 있다. 그래야 사용한 자원(resources)이 반환되기 때문이다.
// 기존의 try-catch문
try {
fis = new FileInsputStream("score.dat");
dis = new DataInputStream(fis);
} catch (IOException ie) {
ie.printStackTrace();
} finally {
try {
if(dis != null)
dis.close();
} catch (IOException ie) {
ie.printStackTrace();
}
}
// try-with-resources문
try (FileInputStream fis = new FileInputStream("score.dat");
DataInputStream dis = new DataInputStream(fis)) {
// 괄호( ) 안에 두 문장 이상 넣을 경우 ';'로 구분한다.
while (true) {
score = dis.readInt();
System.out.println(score);
sum += score;
}
} catch (EOFException e) {
System.out.println("점수의 총합은 " + sum + "입니다.");
} catch (IOException ie) {
ie.printStackTrace();
}
try-with-resources문은 try문에 괄호( )를 추가하고, 괄호 안에 객체를 생성하는 문장을 넣음으로써 작동한다. 이 경우 괄호( )안에 있는 객체는 따로 close()를 호출하지 않아도 try 블럭을 벗어나는 순간 자동으로 close()가 호출된다. 그 이후 catch 또는 finally 블럭이 실행된다. try 블럭의 괄호( )안에 객체가 아닌 변수를 선언하는 것도 가능하며, 이 경우 선언된 변수는 try 블럭 내에서만 사용할 수 있다.
그러나 try-with-resources문에서 자동호출하는 close()에서도 예외가 발생할 수 있는데, 이 경우 만약 close()외에 다른 곳에서도 예외가 발생했을 경우, 두 개의 예외가 동시에 발생할 수 없기 때문에 먼저 발생한 오류가 출력되고 다음에 발생한 오류는 '억제된(suppressed)'이라는 의미의 머리말과 함께 출력된다. 억제된 예외에 대한 정보는 실제로 발생한 예외에 저장된다.
Throwable에는 억제된 예외와 관련된 다음과 같은 메서드가 정의되어 있다.
void addSuppressed(Throwable exception) : 억제된 예외를 추가
Throwable[] getSuppressed() : 억제된 예외(배열)를 반환
1.10 사용자정의 예외 만들기
기존의 정의된 예외 클래스 외에 필요에 따라 예외를 작성할 경우에는 Exception 클래스나 RuntimeException을 상속받아 클래스를 만들거나, 알맞은 예외 클래스를 선택하면 된다.
▼ 사용자 정의의 예외 생성
class MyException extends Exception {
MyException(String msg) { // 문자열을 매개변수로 받는 생성자
super(msg); // 조상인 Exception 클래스의 생성자를 호출
}
}
위 코드는 Exception 클래스를 상속받아 MyException 클래스를 생성했다. 위 코드에서는 String값을 생성 시에 받아 메시지로 저장했지만, 그 외에도 에러코드 값을 저장하는 변수 등을 클래스의 멤버로 추가할 수 있다. 교재 p.440-441에 나와있는 코드와 같이 여러 개의 예외 클래스를 작성하여 조건에 맞게 throw 키워드를 통해 그때그때 알맞는 예외를 반환할 수 있다.
기존에는 checked 예외만을 상속받아 예외 클래스를 작성하는 경우도 많았지만, 요즘에는 예외처리를 선택적으로 할 수 있는 unchecked 예외를 상속받아 예외 클래스를 작성하기도 한다.
1.11 예외 되던지기(exception re-throwing)
한 메서드에서 발생할 수 있는 예외가 여럿인 경우, 몇 개는 try-catch문을 통해서 메서드 내에서 자체적으로 처리하고, 그 나머지는 선언부에 지정하여 호출한 메서드에서 처리하도록 함으로써, 양쪽에서 나눠서 처리되도록 할 수 있다.
또한 단 하나의 예외에 대해서도 예외가 발생한 메서드와 호출한 메서드, 양쪽에서 처리하도록 할 수 있다. 이것은 예외를 처리한 후에 인위적으로 다시 예외를 발생시키는 것으로, 이것을 예외 되던지기(exception re-throwing)라고 한다.
먼저 try-catch 문을 통해 예외를 처리하고, catch블럭에서 throw를 통해 다시 예외를 발생시켜 메서드에 예외를 전달하는 것이다. 이 방법은 하나의 예외에 대해서 예외가 발생한 메서드와 이를 호출한 메서드 양쪽 모두에서 처리해줘야 할 작업이 있을 때 사용된다. 이 때 주의할 점은 예외가 발생할 메서드에서는 try-catch문을 사용해서 예외처리를 해줌과 동시에 메서드의 선언부에 throws를 이용해 발생할 예외를 적어줘야 한다.
만약 반환값이 있는 return문의 경우, catch 블럭에도 return문이 있어야 한다. 예외가 발생했을 경우에도 값을 반환해야 하기 때문이다. 단, throws를 통해 예외를 던질 경우에는 return문을 작성하지 않아도 괜찮다.
1.12 연결된 예외(chained exception)
한 예외가 다른 예외를 발생시킬 수도 있다. 예를 들어 예외 A가 예외 B를 발생시켰다면, A를 B의 원인 예외(cause exception)라고 한다.
try {
startInstall(); // SpaceException 발생
copyFiles();
} catch (SpaceException e) {
InstallException ie = new InstallException("설치중 예외 발생"); // 예외 생성
ie.initCause(e); // InstallException의 원인 예외를 SpaceException으로 지정
thorw ie; // InstallException을 발생시킨다.
} catch (MemoryException me) {
...
위 코드는 SpaceException을 원인 예외로 하는 InstallException을 발생시키는 코드다. 먼저 InstallException을 생성한 후에, initCause()로 SpaceException을 InstallException의 원인 예외로 지정한다. 그리고 'throw'로 이 예외를 던진다.
initCause()는 Exception의 조상 클래스인 Throwable 클래스에 정의되어 있기 때문에 모든 예외에서 사용 가능하다.
Throwable initCause(Throwable cause) : 지정한 예외를 원인 예외로 등록
Throwable getCause() : 원인 예외를 반환
발생한 예외를 원인 예외로 등록하여 다시 예외를 발생시키는 것은, 여러가지 예외를 하나의 큰 분류의 예외로 묶어서 다루기 위함이다. 또다른 이유는 checked 예외를 unchecked 예외로 바꿀 수 있도록 하기 위해서다. 아래 코드는 그 예시다.
// MemoryException은 Exception의 자손 클래스이므로 반드시 예외처리가 필요
static void startInstall() throws SpaceException, MemoryException {
if(!enoughSpace())
throw new SpaceException("설치할 공간이 부족합니다.");
if(!enoughtMemory())
throw new MemoryException("메모리가 부족합니다.");
}
// MemoryException을 RuntimeException으로 감싸 예외처리 제외
static void startInstall() throws SpaceException {
if (!enoughtSpace())
throw new SpaceException("설치할 공간이 부족합니다.");
if (!enoughMemory())
throw new RuntimeException(new MemoryException("메모리가 부족합니다."));
}
위 코드의 하단에서 MemoryException을 RuntimeException으로 감싸 throws에서 MemoryException이 빠진 것을 확인할 수 있다.
Chapter 9. java.lang 패키지와 유용한 클래스
1. java.lang패키지
java.lang 패키지는 자바프로그래밍에 가장 기본이 되는 클래스들을 포함하고 있다. 그렇기 때문에 java.lang 패키지의 클래스들은 import문이 없어도 사용할 수 있다. 그동안 String 클래스나 System 클래스를 import문 없이 사용할 수 있었던 이유가 바로 java.lang 패키지에 속한 클래스들이기 때문이다.
아래에서 소개할 클래스들은 java.lang의 패키지의 여러 클래스들 중에서도 자주 사용되는 클래스들이다.
1.1 Object 클래스
Object 클래스는 모든 클래스들의 최고 조상이기 때문에, Object 클래스의 멤버들은 모든 클래스에서 사용 가능하다.
▼ Object 클래스의 메서드
Object 클래스의 메서드 | 설명 |
protected Object clone() | 객체 자신의 복사본을 반환한다. |
public boolean equals(Object obj) | 객체 자신과 객체 obj가 같은 객체인지 알려준다. (같으면 true) |
protected void finalize() | 객체가 소멸될 때 가비지 컬렉터에 의해 자동적으로 호출된다. 이때 수행되어야 하는 코드가 있을 때 오버라이딩한다. (거의 사용안함) |
public Class getClass() | 객체 자신의 클래스 정보를 담고 있는 Class 인스턴스를 반환한다. |
pubic int hashCode() | 객체 자신의 해시코드를 반환한다. |
public String toString() | 객체 자신의 정보를 문자열로 반환한다. |
public void notify() | 객체 자신을 사용하려고 기다리는 쓰레드를 하나만 깨운다. |
public void notifyAll() | 객체 자신을 사용하려고 기다리는 모든 쓰레드를 깨운다. |
public void wait() public void wait(long timeout) public void wait(long time, int nanos) |
다른 쓰레드가 notify()나 notifyAll()을 호출할 때까지 현재 쓰레드를 무한히 또는 지정된 시간(timeout, nanos)동안 기다리게 한다. (timeout은 천 분의 1초, nanos는 10^9분의 1초) |
Object 클래스는 멤버변수가 없고, 오로지 11개의 메서드만 가지고 있다.
1.2 String 클래스
1.3 StringBuffer 클래스와 StringBuilder 클래스
1.4 Math 클래스
1.5 래퍼(wrapper) 클래스
객체 지향 개념에서 모든 것은 객체로 다루어져야 한다. 그러나 자바에서는 8개의 기본형을 객체로 다루지 않는다. 그러나 때로는 기본형(primitive type) 변수도 어쩔 수 없이 객체로 다뤄야 하는 경우가 있다. 예를 들어 매개변수로 객체를 요구할 때, 기본형 값이 아닌 객체로 저장해야 할 때, 객체 간의 비교가 필요할 때 등이 있다.
이때 사용하는 것이 래퍼(wrapper) 클래스이다. 8개의 기본형을 대표하는 8개의 래퍼 클래스가 있는데, 이 클래스들을 이용하면 기본형 값을 객체로 다룰 수 있다.
▼ 래퍼 클래스의 생성자
기본형 | 래퍼 클래스 | 생성자 | 활용 예 |
boolean | Boolean | Boolean (boolean value) Boolean (String s) |
Boolean b = new Boolean(true); Boolean b2 = new Boolean("true"); |
char | Character | Character (char value) | Character c = new Character('a'); |
byte | Byte | Byte (byte value) Byte (String s) |
Byte b = new Byte(10); Byte b2 = new Byte("10"); |
short | Short | Short (short value) Short (String s) |
Short s = new Short(10); Short s2 = new Short("10"); |
int | Integer | Integer (int value) Integer (String s) |
Integer i = new Integer(100); Integer i2 = new Integer("100"); |
long | Long | Long (long value) Long (String s) |
Long l = new Long(100); Long l2 = new Long("100"); |
float | Float | Float (float value) Float (double value) Float (String s) |
Float f = new Float(1.0f); Float f2 = new Float(1.0); (1.0d double형) Float f3 = new Float("1.0f"); |
double | Double | Double (double value) Double (String s) |
Double d = new Double(1.0); Double d2 = new Double("1.0"); |
래퍼 클래스의 생성자는 매개변수로 문자열이나 각 자료형의 값들을 인자로 받는다. 이때 주의해야 할 것은 생성자의 매개변수로 문자열을 제공할 때, 각 자료형에 알맞는 문자열을 사용해야 한다는 것이다.
(ex. new Integer("1.0"); → NumberFormatException 발생)
래퍼 클래스들은 모두 equals()가 오버라이딩 되어 있어서 주소값이 아닌 객체가 가지고 있는 값을 비교한다. 그래서 실행결과를 보면 equals()를 이용한 두 Integer 객체의 비교 결과가 true라는 것을 알 수 있다. 오토박싱이 된다 해도 Integer 객체에 비교연산자를 사용할 수 없다. 대신 compareTo()를 제공한다.
그리고 toString()도 오버라이딩 되어 있어서 객체가 가지고 있는 값을 문자열로 변환하여 반환한다. 이 외에도 래퍼 클래스들은 MAX_VALUE, MIN_VALUE, SIZE, BYTES, TYPE 등의 static 상수를 공통적으로 가지고 있다.
Number 클래스는 추상 클래스로 내부적으로 숫자를 멤버변수로 갖는 래퍼 클래스들의 조상이다. 기본형 중에서도 숫자와 관련된 래퍼 클래스들은 모두 Number의 자손이다. (ex. Byte, Short, Integer, Long, Float, Double)
그 외에도 Number 클래스의 자손으로 BigInteger, BigDecimal이 있는데 BigInteger은 long으로 다룰 수 없는 큰 범위의 정수를, BigDecimal은 double로도 다룰 수 없는 큰 범위의 부동 소수점수를 처리하기 위한 것으로 연산자의 역할을 대신하는 메서드를 제공한다.
int i = new Integer("100").intValue(); // floatValue(), longValue(), ...
int i2 = Integer.parseInt("100"); // 주로 이 방법을 많이 사용
int i3 = Integer.valueOf("100");
위 코드는 문자열을 숫자로 변환하는 다양한 방법을 나타낸다. 문자열을 숫자로 변환할 때에는 아래의 방법 중 하나를 선택해서 사용하면 된다.
그 외에도 아래의 표는 래퍼 클래스의 '타입.parse타입(String s)' 형식의 메서드와 '타입.valueOf()' 메서드를 정리한 것이다. 둘 다 문자를 숫자로 바꿔주는 일을 하지만, 전자는 반환값이 기본형(primitive type)이고 후자는 반환값이 래퍼 클래스 타입이다.
▼ 문자열을 기본형 또는 래퍼 클래스로 변환하는 방법
문자열 → 기본형 | 문자열 → 래퍼 클래스 |
byte b = Byte.parseByte("100"); short s = Short.parseShort("100"); int i = Integer.parseInt("100"); long l = Long.parseLong("100"); float f = Float.parseFloat("3.14"); double d = Double.parseDouble("3.14"); |
Byte b = Byte.valueOf("100"); Short s = Short.valueOf("100"); Integer i = Integer.valueOf("100"); Long l = Long.valueOf("100"); Float f = Float.valueOf("3.14"); Double d = Double.valueOf("3.14"); |
JDK1.5부터 도입된 오토박싱(autoboxing)이라는 기능 때문에 반환값이 기본형일 때와 래퍼 클래스일때의 차이가 없어졌다. 그래서 그냥 구별없이 valueOf를 써도 되지만, 성능은 valueOf가 좀 더 느리다.
문자열이 10진수가 아닌 다른 진법(radix)의 숫자일 때도 변환이 가능하도록 아래와 같은 메서드가 제공된다. 단, 아래 메서드에서 진법을 생략하면 무조건 10진수로 간주한다.
static int parseInt(String s, int radix) // 문자열 s를 radix 진법으로 인식
static Integer valueOf(String s, int radix)
JDK1.5 이전에는 기본형과 참조형 간의 연산이 불가능했기 때문에, 래퍼 클래스로 기본형을 객체로 만들어서 연산해야 했다. 그러나 지금은 컴파일러가 자동으로 변환하는 코드를 넣어주기 때문에 연산이 가능하다. 아래 코드의 경우, 컴파일러가 Integer 객체를 int 타입의 값으로 변환해주는 intValue()를 추가해준다.
컴파일 전의 코드 | 컴파일 후의 코드 |
int i = 5; Integer iObj = new Integer(7); int sum = i + iObj; |
int i = 5; Integer iObj = new Integer(7); int sum = i + iObj.intValue(); |
이 외에도 내부적으로 객체 배열을 가지고 있는 Vector 클래스나 ArrayList 클래스에 기본형 값을 저장해야할 때나 형변환이 필요할 때도 컴파일러가 자동적으로 코드를 추가해준다. 기본형 값을 래퍼 클래스의 객체로 자동 변환 해주는 것을 '오토박싱(autoboxing)'이라고 하고, 반대로 변환하는 것을 '언박싱(unboxing)'이라고 한다.
아래 코드에서 볼 수 있듯이 ArrayList에 숫자를 저장하거나 꺼낼 때, 기본형 값을 래퍼 클래스의 객체로 변환하지 않아도 되므로 편리하다. 이때 여기서 <Integer>은 지네릭스(Generics)라고 하는 것인데, 12장에서 자세히 다룬다.
ArrayList<Integer> list = new AraryList<Integer>();
list.add(10); // 오토박싱. 10 -> new Integer(10)
int value = list.get(0) // 언박싱. new Integer(10) -> 10
그러나 사실 이 기능은 자바의 원칙이 바뀐 것이 아니라 컴파일러가 제공하는 편리한 기능일 뿐이다.
▼ 컴파일러 오토박싱에 의해 변경된 코드 비교
컴파일 전의 코드 | 컴파일 후의 코드 |
Interger intg = (Integer) i; Object obj = (Object)i; Long lng = 100L; |
Interger intg = Integer.valueOf(i); Object obj = (Object)Integer.valueOf(i); Long lng = new Long(100L); |
2. 유용한 클래스
java.util 패키지에는 많은 수의 클래스가 있지만 실제 자주 사용되는 중요한 클래스들을 소개하고자 한다.
2.1 java.util.Objects 클래스
Object 클래스의 보조 클래스로 Math 클래스처럼 모든 메서드가 'static'이다. 객체의 비교나 널 체크(null check)에 유용하다.
isNull()은 해당 객체가 널인지 확인해서 null이면 true를 반환하고 아니면 false를 반환한다.
nonNull()은 isNull()과 정반대의 일을 한다. 즉, !Object.isNull(obj)와 같다.
static boolean isNull(Object obj)
static boolean nonNull(Object obj)
requireNonNull()은 해당 객체가 널이 아니어야 하는 경우에 사용한다. 만약 객체가 널이면, NullPointerException을 발생시킨다. 두 번째 매개변수로 지정하는 문자열은 예외의 메시지가 된다.
// requireNonNull()을 쓰지 않는 경우
void setName(String name) {
if(name == null)
throw new NullPointException("name must not be null.");
this.name = name;
}
// requireNonNull()을 쓰는 경우
void setName(String name) {
this.name = Objects.requireNonNull(name, "name must not be null.");
}
Object 클래스에는 두 객체의 등가비교를 위한 equals()만 있고, 대소비교를 위한 compare()가 없다. 그래서 Objects에는 compare()가 추가되었다. 해당 메서드는 두 비교대상이 같으면 0, 크면 양수, 작으면 음수를 반환한다. 해당 함수에 있는 Comparator은 두 객체를 비교하는 비교 기준으로, 11장에서 자세히 배우게 된다.
static int compare(Object a, Object b, Comparator c)
물론 Objects에도 equals()가 있는데, 이때 메서드는 Object클래스에 있는 equals()와 다르게 null 검사를 하지 않아도 된다는 장점이 있다. equals()의 내부에서 검사를 하기 때문이다. 해당 메서드는 a와 b가 모두 널인 경우에는 참을 반환한다는 점을 빼고는 특별한 것이 없다.
Objects에는 deepEquals()라는 메서드가 있는데, 해당 메서드는 객체를 재귀적으로 비교하기 때문에 다차원 배열의 비교도 가능하다. 원소가 동일한 다차원 배열을 비교하면 equals()에서는 false를 반환하지만 deepEquals()에서는 true를 반환한다.
toString()도 equals()처럼 내부적으로 널 검사를 한다는 것 빼고는 특별한 것이 없다. 아래 코드의 두번째 메서드는 o가 널일 때, 대신 사용할 값을 지정할 수 있어 유용하다.
static String toString(Object o, String nullDefault)
마지막으로 hashCode() 역시 내부적으로 널 검사를 한 후에 Object 클래스의 hashCode()를 호출할 뿐이다. 단, 널일 때는 0을 반환한다. 보통은 클래스에 선언된 인스턴스의 변수들의 hashCode()를 조합해서 반환하도록, hashCode()를 오버라이딩 하는데, 그 대신 매개변수의 타입이 가변인자인 메서드를 사용하면 편리하다. hashCode()에 관한 내용 역시 11장에서 설명한다.
static import문을 사용하더라도 Object 클래스의 메서드와 이름이 같은 java.util.Objects의 메서드들과 충돌이 발생한다. 컴파일러가 구별을 못하기 때문에 java.util.Objects의 메서드를 사용할 때에는 앞에 Objects.를 붙여 사용해주어야 한다.
2.2 java.util.Random 클래스
난수를 얻는 방법은 Math.random()이 있지만 Random 클래스를 사용해서도 난수를 얻을 수 있다. 사실 Math.random()은 내부적으로 Random 클래스의 인스턴스를 생성해서 사용하기 때문에 둘 중에서 편한 것을 사용하면 된다. 아래의 두 문장은 동일하다.
double randNum = math.random();
double randNum = new Random().nextDouble(); // 위의 문장과 동일
Math.random()과 Random의 가장 큰 차이는 종자값(seed)의 설정 유무다. 동일한 종자값을 가진 Random 인스턴스들은 항상 같은 난수를 같은 순서대로 반환한다.
생성자 Random()은 종자값을 System.currentTimeMillis()로 하기 때문에 실행할 때마다 얻는 난수가 달라진다.
▼ Random의 생성자와 메서드
메서드 | 설명 |
Random() | 현재 시간(System.currentTimeMillis())를 종자값(seed)으로 이용하는 Random 인스턴스를 생성한다. |
Random(long seed) | 매개변수 seed를 종자값으로 하는 Random 인스턴스를 생성한다. |
boolean nextBoolean() | boolean 타입의 난수를 반환한다. |
void nextBytes(byte[] bytes) | bytes 배열에 byte타입의 난수를 채워서 반환한다. |
double nextDouble() | double 타입의 난수를 반환한다. (0.0 <= x < 1.0) |
float nextFloat() | float 타입의 난수를 반환한다. (0.0 <= x < 1.0) |
double nextGaussian() | 평균은 0.0이고 표준편차는 1.0인 가우시안(Gaussian) 분포에 따른 double형 난수를 반환한다. |
int nextInt() | int 타입의 난수를 반환한다. (int의 범위) |
int nextInt(int n) | 0 ~ n의 범위에 있는 int값을 반환한다. (n은 범위에 포함되지 않음) |
long nextLong() | long타입의 난수를 반환한다. (long의 범위) |
void setSeed(long seed) | 종자값을 주어진 값(seed)으로 변경한다. |
이 외에도 JDK1.8부터 새로 추가된 스트림(Stream)과 관련된 메서드들이 있는데, 이에 대한 설명은 14장 람다와 스트림에서 한다.
2.3 정규식(Regular Expression) - java.util.regex 패키지
정규식이란 텍스트 데이터 중에서 원하는 조건(패턴, pattern)과 일치하는 문자열을 찾아내기 위해 사용하는 것으로 미리 정의된 기호와 문자를 이용해서 작성한 문자열을 말한다. 원래 Unix에서 사용했으며 Perl의 강력한 기능이었지만, 요즘은 JAVA를 비롯한 다양한 곳에서 사용한다.
Java API 문서에서 java.util.regex.Pattern을 찾아보면 정규식에 사용되는 기호와 작성방법이 모두 설명되어 있다.
import java.util.regex.*;
class RegularEx1 {
pulic static void main(String args[]) {
String[] data = {"bat", "baby", "bonus", "cA", "ca", "co", "c.",
"c0", "car", "combat", "count", "date", "disc"};
Pattern p = Pattern.complie("c[a-z]*"); // c로 시작하는 소문자영단어
for(int i = 0; i < data.length; i++) {
Matcher m = p.matcher(data[i]);
if(m.matches())
System.out.print(data[i] + ",");
}
}
}
위 코드는 정규식 패키지 사용의 예시다. Pattern은 정규식을 정의하는데 사용되고 Matcher는 정규식(패턴)을 데이터와 비교하는 역할을 한다. 정규식을 정의하고 데이터를 비교하는 과정을 단계별로 설명하면 다음과 같다.
- 정규식을 매개변수로 Pattern 클래스의 static메서드인 Pattern compile(String regex)을 호출하여 Pattern 인스턴스를 얻는다.
Pattern p = Pattern.compile("c[a-z]*"); - 정규식으로 비교할 대상을 매개변수로 Pattern 클래스의 Matcher matcher(CharSequence input)를 호출해서 Matcher 인스턴스를 얻는다.
Matcher m = p.matcher(data[i]); - Matcher 인스턴스에서 boolean matches()를 호출해서 정규식에 부합하는지 확인한다.
if(m.matches())
▼ 정규식 패턴
정규식 패턴 | 설명 | 결과 |
c[a-z]* | c로 시작하는 영단어 | c, car, co, car, combat, count, |
c[a-z] | c로 시작하는 두 자리 영단어 | ca, co, |
c[a-zA-Z] | c로 시작하는 두 자리 영단어(a~z 또는 A~Z, 즉 대소문자 구분안함) | cA, ca, co, |
c[a-zA-Z0-9] c\w |
c로 시작하고 숫자와 영어로 조합된 두 글자 | cA, ca, co, c0, |
.* | 모든 문자열 | bat, baby, bonus, c, cA, ca, co, c., c0, c#, car, combat, count, date, disc |
c. | c로 시작하는 두 자리 문자열 | cA, ca, co, c., c0, c#, |
c.* | c로 시작하는 모든 문자열(기호 포함) | cA, ca, co, c., c0, c#, car, combat, count, |
c\. | c.와 일치하는 문자열 '.'은 패턴 작성에 포함되는 문자이므로 escape문자인 '\'를 사용해야 한다. (\.) |
c., |
c\d c[0-9] |
c와 숫자로 구성된 두 자리 문자열 | c0, |
c.*t | c로 시작하고 t로 끝나는 모든 문자열 | combat, count, |
[b|c].* [bc].* [b-c].* |
b 또는 c로 시작하는 모든 문자열 | bat, baby, bonus, c, cA, ca, co, c., c0, c#, car, combat, count, |
[^b|c].* [^bc].* [^b-c].* |
b 또는 c로 시작하지 않는 문자열 | date, disc, |
.*a.* | a를 포함하는 모든 문자열 * : 0 또는 그 이상의 문자 |
bat, baby, ca, car, combat, date, |
.*a.*+ | a를 포함하는 모든 문자열 + : 1 또는 그 이상의 문자. '+'는 '*'과는 달리 반드시 하나 이상의 문자가 있어야 하므로 a로 끝나는 단어는 포함되지 않는다. |
bat, baby, car, combat, date, |
[b|c].{2} | b 또는 c로 시작하는 세 자리 문자열. (b 또는 c 다음에 두 자리이므로 모두 세자리) | bat, car, |
0\\d{1, 2} | 0으로 시작하는 최소 2자리 최대 3자리 숫자(0포함) | |
\\d{3, 4} | 최소 3자리 최대 4자리의 숫자 | |
\\d{4} | 4자리의 숫자 |
matches 외에도 find를 이용해 패턴을 찾을 수 있는데, 아래 코드를 보자.
import java.util.regex.*;
class RegularEx3 {
public static void main(String args[]) {
String source = "HP:011-1111-1111, HOME:02-999-9999";
String pattern = "(0\\d{1, 2}) - (\\d{3, 4}) - (\\d{4})";
Pattern p = Pattern.compile(pattern);
Matcher m = p.matcher(source);
int i = 0;
while (m.find()) {
System.out.println(++i + ":" + m.group() + "->" + m.group(1)
+ ", " + m.group(2) + ", " + m.group(3));
}
}
}
위 예제에서는 괄호( )를 이용해 정규식의 일부를 묶어 그룹화(grouping) 하였다. 그룹화된 부분은 하나의 단위로 묶이는 셈이 되어서 한 번 또는 그 이상을 반복하는 '+'나 '*'가 뒤에 오면 그룹화된 부분이 적용 대상이 된다. 위 예제에서 pattern을 보면 세 개의 정규식을 각각 괄호로 묶어 3개의 그룹으로 나누었다. group(n)은 n번째 그룹을 반환하며, group()이나 group(0)은 전체를 나누지 않은 문자열이다.
find()는 주어진 소스 내에서 패턴과 일치하는 부분을 찾으면 true, 아니면 false를 반환한다. 패턴을 찾은 후 다시 find를 호출하면 그 다음부터 다시 패턴 매칭을 시작한다.
Matcher의 find()로 정규식과 일치하는 부분을 찾으면 start()와 end()로 그 위치를 찾을 수 있고, appendReplacement(StringBuffer sb, String replacement)를 이용해서 원하는 문자열(replacement)로 치환할 수 있다. 동작의 순서는 아래와 같다.
2.4 java.util.Scanner 클래스
Scanner는 화면, 파일, 문자열과 같은 입력소스로부터 문자데이터를 읽어오는데 도움을 주는 클래스다. String, File, InputStream, Readable, ReadableByteChannel, Path로 시작하는 생성자들 역시 지원하며, 정규식 표현을 이용한 라인 단위의 검색 및 구분자(delimiter)에도 정규식 표현을 사용할 수 있어 복잡한 형태의 구분자 처리도 지원한다. Scanner 클래스는 JDK1.5부터 추각되었다. JDK1.6부터는 화면 입출력만 전문적으로 담당하는 java.io.Console이 새로 추가되었다. 그러나 Console은 이클립스와 같은 IDE에서 잘 동작하지 않는다.
입력받는 값에 따라 nextLine() 대신 nextInt() 또는 nextLong()과 같은 메서드를 사용할 수 있다.
▼ 데이터 형식에 따른 메서드
데이터 형식 | 메서드 |
boolean | nextBoolean() |
byte | nextByte() |
short | nextShort() |
int | nextInt() |
long | nextLong() |
double | nextDouble() |
float | nextFloat() |
string | nextLine |
nextLine()은 라인 단위로 입력을 받을 수 있으며 입력 받은 내용을 공백을 구분자로 나눠서 출력한다. 이때 공백이 여러개일 경우에는 정규식을 이용하여 split(" +")과 같이 표기함으로써 나눌 수 있다.
Scanner을 이용해 new File() 메서드를 이용하면 파일을 열어 읽는 것도 가능해진다.
2.5 java.util.StringTokenizer 클래스
StringTokenizer는 긴 문자열을 지정된 구분자(delimiter)를 기준으로 토큰(token)이라는 여러 개의 문자열로 잘라내는 데 사용된다. StringTokenizer 외에도 String의 split(String regex)이나 Scanner의 useDelimiter(String pattern)을 사용할 수도 있지만, 이 두 가지 방법은 정규식 표현을 사용해야 하므로 정규식 표현에 익숙하지 않다면 StringTokenizer을 쓰는 것이 쉽다.
▼ StringTokenizer의 생성자와 메서드
생성자 / 메서드 | 설명 |
StringTokenizer (String str, String delim) | 문자열(str)을 지정한 구분(delim)로 나누는 StringTokenizer를 생성한다.(구분자는 토큰으로 간주되지 않ㅇ늠) |
StringTokenizer (String str, String delim, boolean returnDelims) | 문자열(str)을 지정된 구분자(delim)로 나누는 StringTokenizer를 생성한다. returnDelims의 값을 true로 하면 구분자도 토큰으로 간주된다. |
int countTokens() | 전체 토큰의 수를 반환한다. |
boolean hasMoreTokens | 토큰이 남아있는지 알려준다. |
String nextToken() | 다음 토큰을 반환한다. |
아래와 같이 StringTokenizer (String str, String delim, boolean returnDelims)에서 returnDelims의 값을 true로 한다면 구분자도 토큰으로 간주된다.
import java.util.*;
class StringTokenizerEx2 {
public static void main(String args[]){
String expression = "x=100*(200+300)/2";
StringTokenizer st = new StringTokenizer(expression, "+-*/=()", true);
while (st.hasMoreTokens()) {
System.out.println(st.nextToken());
}
}
}
/* 실행 결과
x
=
100
*
(
200
+
300
)
/
2*/
또한 위 코드에서는 여러 개의 문자들을 구분자로 지정해두었는데, "+-*/=()" 전체가 하나의 구분자가 아닌 각각의 구분자다. 또한 문자열이 여러 개의 구분자로 되어있을 경우, 먼저 하나의 구분자로 각각 구분한 다음 구분된 데이터 내에서 다시 구분자로 구분하는 방법이 있다.
2.6 java.math.BigInteger 클래스
가장 큰 정수형 타입인 long으로 표현할 수 있는 값은 10진수로 19자리 정도다. 과학적 계산에서 이보다 더 큰 값을 다루기 위해 사용하는 것이 BigInteger 클래스다. 해당 클래스는 내부적으로 int 배열을 사용해서 값을 다루기 때문에 long보다 더 큰 값을 다룰 수 있다. 대신 성능은 long보다 떨어진다.
final int signum; // 부호. 1(양수), 0, -1(음수) 셋 중의 하나
final int[] mag; // 값(magnitude)
위 코드에서 알 수 있듯이, BigInteger은 String처럼 불변(immutable)이다. 그리고 모든 정수형이 그렇듯이 '2의 보수'의 형태로 표현한다. 위의 코드에서 볼 수 있듯 부호와 값을 따로 저장하기에 만약 부호가 음수인 경우 2의 보수법에 맞게 mag의 값을 변환해서 처리한다.
▼BigInteger의 생성
BigIntegar val;
val = new BigInteger("12345678901234567890"); // 문자열로 생성
val = new BigInteger("FFFF", 16); // n진수(radix)의 문자열로 생성
val = BigInteger.valueOf(1234567890L); // 숫자로 생성
BigInteger을 문자열, 또는 byte배열로 변환하는 메서드는 다음과 같다. 또한 BigInteger도 Number로부터 상속받은 기본형으로 변환하는 메서드들을 가지고 있다. 정수형으로 변환하는 메서드 중에서 이름 끝에 'Exact'가 붙은 것들은 변환한 결과가 변환한 타입의 범위에 속하지 않으면 AtrithmeticException을 발생시킨다.
▼ BigInteger의 변환 메서드
// BigInteger을 문자열, byte배열로 변환
String toString() // 문자열로 변환
String toString(int radix) // 지정된 진법(radix)의 문자열로 변환
byte[] toByteArray() // byte배열로 변환
// BigInteger을 기본형으로 변환
int intValue()
long logValue()
float floatValue()
double doubleValue()
// Exact가 붙은 변환 메서드
byte byteValueExact()
int intValueExact()
long longValueExact()
BigInteger에는 정수형에 사용할 수 있는 모든 연산자와 수학적인 계산을 쉽게 해주는 메서드들이 정의되어 있다. 또한 비트 연산을 하는 메서드 역시 정의되어 있다.
▼ BigInteger의 연산 메서드
// 정수형 연산
BigInteger add(BigInteger val) // 덧셈(this + val)
BigInteger subtract(BigInteger val) // 뺄셈(this - val)
BigInteger multiply(BigInteger val) // 곱셈(this * val)
BigInteger divide(BigInteger val) // 나눗셈(this / val)
BigInteger remainder(BigInteger val) // 나머지(this % val)
// 비트 연산
int bitCount() // 2진수로 표현했을 때, 1의 개수(음수는 0의 개수)를 반환
int bitLength() // 2진수로 표현했을 때, 값을 표현하는데 필요한 bit수
boolean testBit(int n) // 우측에서 n+1번째 비트가 1이면 true, 0이면 false
BigInteger setBit(int n) // 우측에서 n+1번째 비트를 1로 변경
BigInteger clearBit(int n) // 우측에서 n+1번째 비트를 0으로 변경
BigInteger flipBit(int n) // 우측에서 n+1번째 비트를 전환(1->0, 0->1)
2.7 java.math.BigDecimal 클래스
BigDecimal은 정수를 이용해서 실수를 표현하며, 정수 * (10 ^ -scale)로 계산한다. BigDecimal 또한 BigInteger을 사용한다. 구조는 아래와 같다.
private final BigInteger intVal; // 정수(unscaled value)
private final int score; // 지수(scale)
private transient int precision // 정밀도(precision) - 정수의 자릿수
BigDecimal에서 123.45는 12345 * (10^-2)로 표현할 수 있으며, 이 경우 BigDecimal에 저장되면 intVal은 12345, scale의 값은 2가 된다.
BigDecimal은 기본형으로 표현하는 한계가 있기 때문에 일반적으로 문자열로 표현한다.
▼BigDecimal의 생성
BigDecimal val;
val = new BigDecimal("123.4567890"); // 문자열로 생성
val = new BigDecimal(123.456); // double타입의 리터럴로 생성
val = new BigDecimal(123456); // int, long타입의 리터럴로 생성
val = BigDecimal.valueOf(123.456); // 생성자 대신 valueOf(double) 사용
val = BigDecimal.valueOf(123456); // 생성자 대신 valueOf(int) 사용
이때 한 가지 주의할 점은, double타입의 값을 매개변수로 갖는 생성자를 사용하면 오차가 발생할 수 있다.
▼ BigDecimal의 변환 메서드
// BigDecimal을 문자열로 변환
String toPlainString() // 어떤 경우에도 다른 기호 없이 숫자로만 표현
String toString() // 필요하면 지수형태로 표현할 수 있음
// BigDecimal을 기본형으로 변환
int intValue()
long longValue()
double doubleValue()
float floatValue()
// Exact가 붙은 변환 메서드
byte byteValueExact()
short shortValueExact()
int intValueExact()
long longValueExact()
BigInteger toBigIntegerExact()
BigDecimal에는 실수형에 사용할 수 있는 모든 연산자와 수학적인 계산을 쉽게 해주는 메서드들이 정의되어 있다. 이때 BigDecimal은 불변이므로 반환타입이 BigDecimal인 경우 새로운 인스턴스가 반환된다.
▼ BigDecimal의 연산 메서드
BigDecimal add(BigDecimal val) // 덧셈(this + val)
BigDecimal subtract(BigDecimal val) // 뺄셈(this - val)
BigDecimal multiply(BigDecimal val) // 곱셈(this * val)
BigDecimal divide(BigDecimal val) // 나눗셈(this / val)
BigDecimal remainder(BigDecimal val) // 나머지(this % val)
// value, scale, precision
BigDecimal bd1 = new BigDecimal("123.456"); // 123456, 3, 6
BigDecimal bd2 = new BigDecimal("1.0"); // 10, 1, 2
BigDecimal bd3 = bd1.multiply(bd2); // 1234560, 4, 7
이때, 연산결과의 정수, 지수, 정밀도는 타입에 따라 달라질 수도 있다. 곱셈에서는 두 피연산자의 scale을 더하고, 나눗셈에서는 뺀다. 덧셈과 뺄셈에서는 둘 중에서 자리수가 높은 쪽으로 맞추기 위해 두 scale중에서 큰 쪽이 결과가 된다.
또한 BigDecimal에는 나눗셈을 처리하는데 있어 어떻게 반올림(roundingMode) 처리할 것인가, 몇 번째 자리(scale)에서 반올림할 것인지를 지정할 수 있다.
▼ 열거형 RoundingMode에 정의된 상수
상수 | 설명 |
CEIING | 올림 |
FLOOR | 내림 |
UP | 양수일 때는 올림, 음수일 때는 내림 |
DOWN | 양수일 때는 내림, 음수일 때는 올림(UP과 반대) |
HALF_UP | 반올림(5 이상 올림, 5 미만 버림) |
HALF_EVEN | 반올림(반올림 자리의 값이 짝수면 HALF_DOWN, 홀수면 HALF_UP) |
HALF_DOWN | 반올림(6 이상 올림, 6 미만 버림) |
UNNECESSARY | 나눗셈의 결과가 딱 떨어지는 수가 아니면, ArithmeticException 발생 |
java.math.MathContext 클래스는 반올림 모드와 정밀도(precision)를 하나로 묶어 놓은 것이다. 한 가지 주의할 점은 divide()에서는 scale이 소수점 이하의 자리수를 의미하는데, MathContext에서는 precision이 정수와 소수점 이하를 모두 포함한 자리수를 의미한다.
BigDecimal bd1 = new BigDecimal("123.456");
BigDecimal bd2 = new BigDecimal("1.0");
System.out.println(bd1.divide(bd2, 2, HALF_UP)); // 123.46
System.out.println(bd1.divide(bd2, newMathContext(2, HALF_UP))); // 1.2E+2
BigDecimal을 10으로 곱하거나 나누는 대신 scale의 값을 변경함으로써 같은 결과를 얻을 수 있다. BigDecimal의 scale을 변경하려면, setScale()을 이용하면 된다.
'JAVA' 카테고리의 다른 글
Chapter 7 요약 (0) | 2022.09.01 |
---|---|
Chapter 6 요약 (0) | 2022.08.26 |
Chapter 4, 5 요약 (0) | 2022.08.19 |
Chapter 1, 2, 3 요약 (0) | 2022.08.12 |