프로그램을 만들다보면 수많은 오류가 생기는데 오류가 발생하는 이유는 프로그램이 정상적으로 작동하게 하기 위한 자바의 배려이다. 오류가 날때 그에 맞는 적절한 처리를 하는 경우도 있고 무시할 수도 있다. 이를 예외처리라 한다.
import java.io.BufferedReader;
import java.io.FileReader;
public class Sample {
public static void main(String[] args) {
BufferedReader bufferedReader = new BufferedReader(new FileReader("없는파일"));
bufferedReader.readLine();
bufferedReader.close();
}
}
존재하지 않는 파일을 불러올 경우에 오류가 발생한다.
public class Sample {
public static void main(String[] args) {
int a = 4 / 0;
}
}
계산할 수 없는 값을 계산하려고 하면 오류가 발생한다.
public class Sample {
public static void main(String[] args) {
int[] a = {1,2,3};
System.out.println(a[3]);
}
}
배열에 존재하지 않는 인덱스를 불러오려고 할때 오류가 발생한다.
try {
시도할 문장
...
} catch (exception 종류) {
예외 발생 시 처리 방법
} catch () {
}...
int c;
try {
c = 4 / 0;
} catch (ArithmeticException e) {
c = -1;
}
try문에 수행할 문장들이 있을 때 예외가 생기는 문장이 생기면 그 뒤의 문장은 수행되지 않고 곧바로 해당 catch문을 수행하게 된다. 혹시 예외가 생긴 문장 뒤에 꼭 수행해야만 하는 문장이 있다면 이럴 때 finally 문을 사용하게 된다.
public class Sample {
public void ShouldBeRun() {
System.out.println("hi, there");
}
public static void main(String[] args) {
Sample sample = new Sample();
int c;
try {
c = 4 / 0;
sample.ShouldBeRun(); // 수행 안됨.
} catch (ArithmeticException e) {
c = -1;
}
}
}
sample.ShouldBeRun 문을 수행하기 위해서 finally문을 이용하면
public class Sample {
public void ShouldBeRun() {
System.out.println("hi, there");
}
public static void main(String[] args) {
Sample sample = new Sample();
int c;
try {
c = 4 / 0;
} catch (ArithmeticException e) {
c = -1;
} finally {
sample.ShouldBeRun();
}
}
}
hi, there // 출력
Exception에는 크게 Checked Exception(RuntimeException)과 Unchecked Exception(Exception)이 있다. Checked Exception은 컴파일 단계에서 확인이 가능한 예외이며 try,catch문으로 꼭 예외처리를 해주어야 한다. Unchecked Exception은 실행 단계에서 확인이 가능한 예외이며 예외처리를 강제하지 않는다.(무슨 내용인지 100% 이해하지는 못했다.) 닉네임을 출력해주는 프로그램으로 예를 들면
public class Sample {
public void sayNick(String nick) {
if ("fool".equals(nick)) {
return;
}
System.out.println("당신의 별명은 " + nick + " 입니다.");
}
public static void main(String[] args) {
Sample sample = new Sample();
sample.sayNick("fool");
sample.sayNick("genious");
}
}
class FoolException extends RuntimeException {
}
public class Sample {
public void sayNick(String nick) {
if ("fool".equals(nick)) {
throw new FoolException();
}
System.out.println("당신의 별명은 " + nick + " 입니다.");
}
public static void main(String[] args) {
Sample sample = new Sample();
sample.sayNick("fool");
sample.sayNick("genious");
}
}
class FoolException extends Exception {
}
public class Sample {
public void sayNick(String nick) {
try {
if ("fool".equals(nick)) {
throw new FoolException();
}
System.out.println("당신의 별명은 " + nick + " 입니다.");
} catch (FoolException e) {
System.err.println("FoolException이 발생하였습니다.");
}
}
public static void main(String[] args) {
Sample sample = new Sample();
sample.sayNick("fool");
sample.sayNick("genious");
}
}
FoolException이 발생하였습니다.
당신의 별명은 genious입니다.
이번엔 sayNick 메소드에서 예외처리를 하는 것이 아닌 sayNick을 호출할 곳(main 메소드)에서 예외처리를 해보자
class FoolException extends Exception {
}
public class Sample {
public void sayNick(String nick) throws FoolException {
if ("fool".equals(nick)) {
throw new FoolException();
}
System.out.println("당신의 별명은 " + nick + " 입니다.");
}
public static void main(String[] args) {
Sample sample = new Sample();
try {
sample.sayNick("fool");
sample.sayNick("genious"); // 수행 안됨
} catch (FoolException e) {
System.err.println("FoolException 발생");
}
}
}
sayNick 메소드 옆에 FoolException을 던져서 sayNick 메소드를 호출할 곳에서 예외처리를 하게끔 했다. 즉, main메소드에서 예외처리를 해야하므로 try,catch 문을 이용하였다.
sayNick 메소드에서 예외처리를 했던 것과 가장 큰 차이점은 sample.sayNick("genious")문이 수행되지 않는 다는 점이다. 왜냐하면 sample.sayNick("fool")문에서 이미 예외가 발생되어 catch문으로 넘어가기 때문이다.
트랜잭션이란 하나의 작업단위를 뜻한다. 쇼핑몰에서 상품발송하는 것을 수도코드(일반적인 언어로 코드를 흉내내어 알고리즘을 대략적으로 모델링한 코드)를 보며 예로 들어 설명하면
상품발송() {
포장();
영수증발행();
발송();
}
포장() {
...
}
영수증발행() {
...
}
발송() {
...
}
상품발송을 위해서는 포장,영수증발행,발송 모두를 만족해야지만 상품 발송을 한다고 하자. 하나라도 예외가 발생하면 상품발송이 롤백(roll-back,모두 취소하고 되돌아간다는 의미)이 된다고 하면, 예외처리를 상품발송에 해야할까? 포장,영수증발행,발송에 각각 해야할까? 후자를 선택하면 어떤건 만족하는데 어떤건 예외가 발생하는 경우가 생길 수 있다.
상품발송() {
try {
포장();
영수증발행();
발송();
} catch (예외) {
모두 취소
}
}
포장() throw 예외{
...
}
영수증발행() throw 예외{
...
}
발송() throw 예외{
...
}
메모리를 할당받아 실행중인 프로그램을 프로세스(Process)라 한다. 하나의 프로세스 내부에서 독립적으로 실행되는 하나의 작업단위를 쓰레드라고 한다. JVM에 의해 하나의 프로세스가 발생하고, main 메소드 안의 실행문들이 하나의 쓰레드이다. main 메소드 이외의 또 다른 쓰레드를 만들기 위해서는 두가지 방법이 있는데 하나는 Thread 클래스를 상속(extends)하는 방법이고 또 하나는 Runnable 인터페이스를 구현(implements)하는 방법이다. 첫번째 예로 하나의 쓰레드만 실행하는 코드를 구현해보면
public class Sample extends Thread {
public void run() {
System.out.println("hello");
}
public static void main(String[] args) {
Sample sample = new Sample();
sample.start();
}
}
public class Sample extends Thread {
int seq;
public Sample(int seq) {
this.seq = seq;
}
public void run() {
System.out.println(this.seq + " Thread start.");
try {
Thread.sleep(500);
} catch (Exception e) {
}
System.out.println(this.seq + " Thread end.");
}
public static void main(String[] args) {
for (int i=0; i<10; i++) {
Thread t = new Sample(i);
t.start();
}
System.out.println("main end");
}
}
전체적인 코드의 내용은 원하는 문장을 10번 출력하고 조금있다가 다시 출력하는 코드이다.
seq를 객체변수로 선언하고 Sample 클래스의 생성자를 만들어서 객체를 생성할때마다 순번을 붙여주도록 했다. run 메소드에는 thread 시작을 알리는 문구를 10번 출력하고, 0.5초 뒤에 끝을 알리는 문구를 10번 출력하도록 했다. main 메소드에서 쓰레드를 실행시키는 방법은 Thread 자료형의 Sample 클래스 객체를 10번 생성해서 각각의 객체로 run 메소드를 호출하게끔 했다. 그 후에 메인메소드를 종료하는 문구를 출력하도록 했다.
출력 결과물에서 알 수 있는 첫번째는 쓰레드가 출력되는 순서가 랜덤이라는 것을 알 수 있다. 두번째는 쓰레드가 끝나기도 전에 main 메소드가 종료된 것을 알 수 있다. 두번째 문제를 해결하기 위해 join이라는 키워드를 사용한다.
모든 쓰레드가 종료된 후에 main 메소드를 종료시키기 위해 join 메소드를 사용해 보겠다.
import java.util.ArrayList;
public class Sample extends Thread {
int seq;
public Sample(int seq) {
this.seq = seq;
}
public void run() {
System.out.println(this.seq + " Thread start.");
try {
Thread.sleep(500);
} catch (Exception e) {
}
System.out.println(this.seq + " Thread end.");
}
public static void main(String[] args) {
ArrayList<Thread> threads = new ArrayList<>();
for (int i=0; i<10; i++) {
Thread t = new Sample(i);
t.start();
threads.add(t);
}
for (int i=0; i<threads.size(); i++) {
Thread t = threads.get(i);
try {
t.join();
} catch (Exception e) {}
}
System.out.println("main end");
}
}
main 메소드 이외의 또다른 쓰레들를 만들기 위한 방법으로 쓰레드를 상속하는 방법 외에 Runnable 인터페이스를 implements 하는 방법이 있다고 했다. 쓰레드 클래스를 상속하는 방법과 가장 큰 차이점은 쓰레드 클래스는 상속을 하면 이중상속이 안되는 규칙 탓에 다른 클래스를 상속하는 것이 어려워진다. 하지만 Runnable은 인터페이스이므로 다른 클래스 상속을 할 수 있다.
import java.util.ArrayList;
public class Sample implements Runnable {
int seq;
public Sample(int seq) {
this.seq = seq;
}
public void run() {
System.out.println(this.seq + " Thread start.");
try {
Thread.sleep(500);
} catch (Exception e) {
}
System.out.println(this.seq + " Thread end.");
}
public static void main(String[] args) {
ArrayList<Thread> threads = new ArrayList<>();
for (int i=0; i<10; i++) {
Thread t = new Thread(new Sample(i));
t.start();
threads.add(t);
}
for (int i=0; i<threads.size(); i++) {
Thread t = threads.get(i);
try {
t.join();
} catch (Exception e) {}
}
System.out.println("main end");
}
}
Thread t - new Tread(new Sample(i));
로 변경하였다. 쓰레드의 생성자로 Runnable 인터페이스의 객체(Sample가 Runnable을 implement했으므로 Sample의 객체)를 입력인자로 사용할 수 있는데 이 방법을 이용한 것이다. 쓰레드를 상속했을 때와 동일한 결과를 내지만 이중상속이 안되는걸 감안하면 Runnable을 implement한 것이 다소 유연한 코드라고 할 수 있다.
자바8부터 함수형 프로그래밍을 지원하기 위해 람다와 스트림을 도입했다. 람다와 스트림을 이용하면 코드를 더욱 간결하고 가독성을 좋게 한다는 장점이 있다.
람다는 익명 함수(Anonymous Function)을 의미한다. 말그대로 이름이 없는 함수이다. 클래스를 생성하여 함수를 만들었던 기존의 방법에서 단 몇줄의 코드로 함수의 이름없이 코드를 짤 수 있다. 계산기를 예로 들면
interface Cal {
int sum(int a, int b);
}
class MyCal implements Cal {
public int sum(int a, int b) {
return a+b;
}
}
public class Sample {
public static void main(String[] args) {
MyCal myCal = new MyCal();
System.out.println(myCal.sum(3,4));
}
}
interface Cal {
int sum(int a, int b);
}
public class Sample {
public static void main(String[] args) {
Cal cal = (int a, int b) -> a+b; // 람다 함수
System.out.println(cal.sum(3,4));
}
}
Cal cal = (a, b) -> a+b;
Cal cal = Integer::sum;
import java.util.function.BiFunction;
public class Sample {
public static void main(String[] args) {
BiFunction<Integer,Integer,Integer> biFunction = (a,b) -> a+b;
System.out.println(biFunction.apply(3,4));
}
}
다음 코드는 BiFuction이라는 자바에 내장되어있는 인터페이스를 임포트해서 코드를 짠 것이다. BiFuction의 객체를 생성할 때 꺽쇠에는 람다함수의 입력항목과 리턴값의 자료형을 표기한다. 그리고 BuFunction 인터페이스 내에 더하는 메소드인 apply 메소드를 이용해 결과를 출력한다.
입력항목과 리턴값의 자료형이 모두 동일하므로 BynaryOperator 인터페이스를 불러와서 자료형을 한번만 쓰도록 할 수도 있다.
import java.util.function.BinaryOperator;
public class Sample {
public static void main(String[] args) {
BinaryOperator<Integer> binaryOperator = (a, b) -> a+b;
System.out.println(binaryOperator.apply(3,4));
}
}
@FunctionalInterface
interface Cal {
int sum(int a, int b);
int min(int c, int d); // 컴파일 에러
}
스트림이란 데이터가 물결(stream)처럼 흘러가면서 필터링 과정을 통해 여러번 변경되는 코드를 뜻한다. 예를 들어 배열에 여러 수 중에서 짝수만을 뽑아서, 중복되는 원소를 제거하고, 역순으로 재배열하는 코드를 짠다고 해보자.
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
public class Sample {
public static void main(String[] args) {
int[] data = {5,6,4,2,3,1,1,2,2,4,8};
// 1. 짝수만 뽑기
ArrayList<Integer> even = new ArrayList<>();
for (int i : data) {
if (i %2 == 0) {
even.add(i);
}
}
// 2. 중복 제거
HashSet<Integer> duplicates = new HashSet<>(odd);
// 3. 역순
ArrayList<Integer> reverse = new ArrayList<>(duplicates);
reverse.sort(Comparator.reverseOrder());
// 4. ArrayList -> int[]
int[] result = new int[reverse.size()];
for (int i=0; i< reverse.size(); i++) {
result[i] = reverse.get(i);
}
}
}
import java.util.Arrays;
import java.util.Comparator;
public class Sample {
public static void main(String[] args) {
int[] data = {5,6,4,2,3,1,1,2,2,4,8};
int[] result = Arrays.stream(data) // IntStream 생성
.boxed() // IntStream -> Stream<Integer>
.filter((a) -> a % 2 == 0) // 짝수만 뽑기
.distinct() // 중복 제거
.sorted(Comparator.reverseOrder()) // 역순
.mapToInt(Integer::intValue) // Stream<Integer> -> IntStream
.toArray() // IntStream -> int[]
;
}
}