2. 오브젝트 지향의 프로그래밍 방식이라는 것은
목적성을 위주로 하는 것인데,
예를 들어 내가 모든 것을 하는 것이 아니라, 그걸 할 줄 아는 사람에게 일을 시켜서
그 목적을 달성한다는 의미와 일맥상통하지 않는가 싶다.
그러니까 컴퓨터에서의 오브젝트 프로그래밍이라는 건 미리 정해져 있는 오브젝트들 (목적성을 가진
콤포넌트들)에게 일을 시켜서 일을 처리한다는 의미가 아닌가 싶다.
쉽게 얘기하면 Class는 type 선언이며, Object는 실제 변수이다.
실제 변수란 각각의 이름을 가질 수 있으며, Memory상에 자리 잡는 것을 말한다.
2 Java Program 만들기.
Java Programming의 개념과, 여러가지 기술에 대해서 알아본다.
2.1 객체지향 프로그래밍 기본.
여러분은 다음과 같은 소리를 많이 들어봤을 것입니다.
그러면 어떤것이 객체 지향적인 기법인가?
객체 지향이란 개념은 설명하기도 힘들고, 쉽게 감이 오지도 않는다. 단지 시간이 지남에 따라서 서서히 이해되는 것이다. 많은 회사에서는 자기들 제품이 객체지향적이라고 선전을 하기 때문에 고객 입장에서는 혼돈만 있을 뿐이다.
그러나, 객체지향의 남용과, 과다 선전에도 불구하고 현제 많은 회사에서는 차츰 객체 지향이라는 것의 의미를 잘 파악할려고 하고 있고, 하나씩 객체지향의 덕을 보고 있다.
2.1.1 객체란 무엇인가?
객체란 데이터와, 관계된 프로시저의 묶음이다. 간혹, 소프트웨어 객체는 실세계의 객체를 표현하는데 쓰이기도 한다.
객체라는 이름에서 아루 있듯이, 객체지향 기법을 알기위해서 가장 기본이 되는 것이 객체이다. 주위에서 실세계에 존재하는 객체는 많이 볼 수 있다. 강아지, 책상, 텔레비젼, 자전거.. 아주 많다.
실세계의 객체는 2가지 특성을 가지고 있는 것을 알 수가 있다. 상태(state)와 행동(behaviour)라는 특성을 가지고 있다. 예를 들어서, 강아지는 이름, 색, 품종, 배고픔.. 등 상태를 나타낼 수 있고 강아지는 멍멍 짖고, 물고, 달리는 행동울 한다. 또, 자전거는 기어가 있고, 패달이 있고, 두개의 바퀴가 있고, 기어를 바꾸고, 속도를 높이고, 정지를 하는 행동을 할 수 있다.
실세계의 객체와 마찬가지로 소프트웨어 객체도 상태(state)와 행동(behaviour) 를 각각 변수(variable)와 메소드(method)로 표현 할 수 있다.
정의 : 객체는 데이터와, 메소드의 소프트웨어 묶음이다.
( Objects are software bundles of data and related methods. )
소프트웨어 객체는 실세계 객체를 모델링 한 후에 생겨난 것이기 때문에 실세계의 모든 객체를 플포그래밍을 통해서 소프트웨어 객체로 표현 할 수 있다. 예를 들어서 강아지가 짖는 그래픽 에니메이션을 한다면 충분히 가능하다. 하지만, 약간의 구체 적인 작업이 필요한데, 일반적인 GUI에서 마우스나, 키보드가 눌러졌을때 객체가 어떠한 행동을 할 수 있도록 해주는 "event"를 처리해주어야 한다.
다음은 일반적인 소프트웨어를 표현한것이다.
소프트웨어 객체는 변수와, 메소드로 각각의 모든 상태와 행동을 표현한다. 다음은 자전거를 예로 들어 여러가지 상태와, 행동을 표현한 것이다.
소프트웨어 객체로서의 자전거는 정지를하고, 패달을 굴리고, 기어를 바꿀 수 있는 행동을 할 수있다. 즉, 패달을 굴리는 횟수를 늘림으로써 속도를 조절 할수 있고 기어를 조절하여 기어를 바꾼다. 하지만, 객체에 포함되어 있지 기능은 당연히 처리할 수가 없다. 예를 들어서, 강아지처럼.. 짓거나, 물어오는것 같은 기능은 그 기능에 해당한 변수와, 메소드가 없기 때문에 어떠한 행동도 할 수 없는것이다.
위의 다이어그램은 원자의 핵처럼 안쪽은 변수가 있고, 외부에 메소드가 둘러싸고 있다. 이렇게 내부에 있는 변수는 외부로 부터 보호되어 있는데 이것을 캡슐화(emcapsulation)이라고 한다. 일반적으로 캡슐화는 외부터 자세한 내부적인 행동을 숨긴다는 의미를 가진다. 예를 들어서 자전거의 기어를 바꿀려고 한다면, 굳이 기어아 어떻게 작용하는지 메커니즘까지는 알필요 없이, 레버를 어떻게 사용하는지 알기만 하면 된다. 또한 내부적인 기어의 메커니즘이 바뀌더라도, 레버로 기어를 바꾸는 전체적인 프로그램은 손을 대지 않아도 된다는 장점을 가지고 있다.
캡슐화의 장점. 캡슐화는 소프트웨어를 아주 깔금하고, 복잡하지 않게 만듬과 동시에 소프트웨어 개발자 입장에서는 다음과 같은 아주 강력한 잇점을 안겨준다.
* 모듈화(Modularity) :
하나의 객체에 대한 원시프로그램(source program)은 다른 객체의 원시프로그램에 영향을 주지않고 유지된다.
* 정보숨김(Information hiding) :
객체는 다른 객체가 외부에서 접촉할 수 있는 공개된 인터페이스 (public interface)가 제공됨과 동시에, 외부에 아무 영향을 미치지 않고 자신만의 정보를 처리할 수 있는(private, protected) 기능이 있다. 즉 기어를 사용하기 위해서 내부적인 기어 메커니즘을 알 필요는 없는것과 같은 것이다.
2.1.2 메시지란 무엇인가?
메시지는 객체사이에서 서로 통신할때 쓰인다.
객체는 혼자서는 별로 유용하지가 않다. 자전거에서도 마찬기지로, 하나의 기어로는 아무 일도 할수 없지만, 다른 기어나 패달과 연결되어, 더욱 복잡하고, 유용할 기능을 보일수 있는것이다.
다음 그림은 객체 A가 객체 B의 어떤 메소드의 기능을 필요로 할때 객체 A는 객체 B에게 메시지를 보내는 것이다.
가끔, 메시지를 받는 객체는 몇가지 정보를 필요로 할 경우가 있다. 예를 들어, 자전거에서 기어를 바꿀려고 할때 어떤기어를 선택할것인지에 대한 조그만 정보가 필요할 것이다. 이렇게 필요한 정보는 메시지의 파라미터(parameter)로 전송된다.
메시지에는 3가지 구성요소를 가지고 있다.
- 메시지를 받을 객체의 주소.
- 처리할 메소드 이름.
- 메소드에서 필요로하는 어떤 여분의 파라미터.
메시지의 잇점.
- 객체간의 상호접촉은 오직 메시지를 통해서 모든 기능을 제공할 수 있다.
- 메시지를 주고, 받는 객체는 한 프로세스내에 있을 필요가 없고, 같은 기계내에 있을 필요도 없다.
2.1.3 클래스란 무엇인가?
클래스란 특정 종류의 모든 객체들의 원형이다.
실세계에서는 같은형태의 많은 객체를 볼수 있을것이다. 예를 들어서 내가 가지고 있는 자전거도 실세계에 있는 자전거중의 하나이다. 즉, 객체지향적인 입장에서 내 자전거는 자전거라는 클래서의 하나의 인스턴스(instance)이다. 자전거는 기어, 바퀴를 가지고 있고, 정지하고 기어를 바꾸는 등 같은 행동을 한다.
자전거 공장에서는 하나의 자전거 설계도를 가지고 그것에 맞는 여러개의 자전거를 생산해낸다. 마찬가지로, 객체의 청사진을 클래스라고 한다.
정의 : 클래스는 특정 종류의 모든 객체들의 원형이다.
A class is a template or prototype that defines the variables and the methods common to all objects of a certain kind.
위의 일반적인 클래스는 다시 자전거 클래스로 표현할 수있다.
자전거 클래스를 만들고 나서는, 인스턴스를 생성(instanciate)해아만 사용할 수 있다. 클레스에서 인스턴스를 행성하면, 클래스에 생성된 변수는 드디어 메모리를 항당하게 된다. 그리고나서, 그 인스턴스의 메소드를 사용할 수 있는 것이다. 같은 클레스의 다른 인스턴스는 메소드를 공유한다.
객체의 의미...
일반적으로 객체의 의미와, 클래스의 의미는 혼돈되어 사용되고 있다. 실세계에서의 클레스와 객체는 자전거의 설계도가 자전거가 아니듯이 클래스는 결코 객체가 아니다. 하지만, 소프트웨어에서는 클래와 객체를 구분한다는것은 약간의 혼돈이 있다. 왜냐하면 소프트웨어 객체는 단지 실세계객체의 전자적인 표현이기 때문이다. 다라서, 보통 사람들은 객체를 클레스와 인스턴스를 동시에 부를때 사용한다.
위의 다이어 그램은 색이 없다. 그이유는 아직 생성이 되지 않았기 때문이다. 객체의 메소드나, 변수를 사용하기 위해서는 클레스로부터 인스턴스를 생성해야만 한다.
클래스의 장점
객체는 모듈화와, 정보를 감추는 장점을 가지고 있었다. 클래스는 재 사용할 수 있어서, 이식성도 뛰어나다. 예를 들어서 자전거공장은 자전거 설계도를 조금씩 고치고, 첨가하여 많은 자전거를 생산하는데에 다시 사용할 수 있는것이다.
2.1.4 상속이란 무엇인가?
클래스는 상태와, 행동을 상위클래스(superclass)에서 상속을 받는다. 상속은 소프트웨어 프로그램을 구성하고 제작하는데 있어 강력하고, 자연스러운 메커니즘을 제공한다.
일반적으로 객체는 클래스로 정의되어지므로 클레스를 통해서 그 객체를 알수 있다. 즉, 자전거라고 이야기를 하면, 핸들이 있고, 바퀴가 있고, 기어가있고, 패달이 있고, 쭈욱 굴러간다는것을 알아차리는것과 같다.
등산용 자전거, 경주용 자전거, 짐나르기용 자전거, 탠덤 자전거.. 모두 일반적인 자전거의 특성을 가지고 있으면서, 나름대로의 특징을 첨가 또는 개조한 것이다.
클래스로 표현하면, 다음의 그림처럼 bicycle class는 mountain bikes, race bikes, tandems의 superclass가 되고, mountain bikes, race bikes, tandems는 bicycle class의 subclass가 되는 것이다.
각각의 subclass는 superclass로 부터 state를 상속(inherit)받는다. Mountain bike, race bike, tandem은 진행의 보조, 속도와 같은 몇가지 state를 공유한다.
또한 각각의 subclass는 superclass의 method도 상속을 받는다. Mountain bike, race bike, tandem은 브레이크, 패달을 밟는 속도등 몇가지 behaviour를 공유한다.
그러나, subclass는 superclass로부터 제공되는 state, behaviour로 한정되는것만은 아니다. Subclass는 superclass로 부터 상속받은것에서 variable과, method를 더 첨가할 수 있다. 예를 들어, tandem은 두개의 좌석과 두개의 핸들대가 있고, mountain bike는 여러종류의 기어를 가질 수 있다.
또한, Subclass는 상속받은 method와, 상속받은 method를 위한 새로 특별히 사용할 method를 중복(override)할 수 있다. 예를 들어서, 만약 일반적인 bike와는 달리 특별한 기어를 가지고 있는 mountain bike에서, 이러한 새로운 기어를 사용하기 위한 "change gears"라는 새로운 method를 중복할 수 있다.
Class의 계층구조(inheritance tree, class hierarchy)는 한정되지 않고 필요에 따라서 깊이를 더해할 수 있다. Method와 variable은 아랫 단계로 가면서 상속된다. 계층구조가 아래로 내려갈 수록, 그기능은 특수화된다.
상속의 장점.
Subclass는 superclass로 부터 제공받은 기본적인 요소들을 특수하게 사용할 수 있다. 상속을 사용함으로써, 프로그래머는 superclass의 code를 여러번 재사용 할 수 있다.
프로그래머는 superclass를 generic(abstract class)으로 정의할 수 있다. Superclass의 기본적인것만 정의함으로써, class의 대부분의것은 수행하지 않고 일부만 수행하도록정의한다. 어떤 프로그래머는 정의되지 않은 부분에 특별한 subclass에 대해서 자세히 적어넣기도 한다.
2.1.5 그밖의 OOP의 정보.
An Introduction to Object-Oriented Programming
An introduction to the topic of object-oriented programming, as well as a comparison of C++, Objective C, SmallTalk, and Object Pascal. Written by Timothy Budd. Published by Addison-Wesley Publishing Company, Reading, Massachusetts.
Object-Oriented Technology: A Manager's Guide
A excellent discussion of object-oriented technology for the non-technical reader. Includes an assessment of the advantanges and disadvantages of this technology. Written by David A. Taylor, PhD. Published by Servio Corporation, Alameda, California, 1990.
NEXTSTEP Object-Oriented Programming and the Objective C Language
The book on Objective C. A good introduction to object-oriented programming concepts. Published by Addison-Wesley Publishing Company, Reading, Massachusetts, 1993.
2.2 Java 응용프로그램의 분석.
Java-compatible browser에서 돌아가는 Java program을 applets라고 하고 stand-alone Java program을 Java application이라고 한다. 즉, browser와는 독립적으로 돌아가는 프로그램을 말한다.
현재 시간과 날짜를 화면에 프린트해주는 조그만 Java Application의 전체적인 구문을 보면서 Java프로그램의 구조를 알아보겠다. 따라서 객체지향 프로그래밈의 2가지 기본 골격인 class와 object를 맛보게 될것이다.
2.2.1 'DateApp'와 'Hello World'와의 비교.
The DateApp Application
import java.util.Date; class DateApp { public static void main (String args[]) { Date today = new Date(); System.out.println(today); } }
The "Hello World" Application
class HelloWorldApp { public static void main (String args[]) { System.out.println("Hello World!"); } }
위 프로그램은 "Hello World" Application을 변형시킨 프로그램이다.
굵은 글씨로 표현된 부분이 서로 다른 부분이다. DateApp는 Date class에서 기능을 가져온다. Application이름은 'DateApp' vs 'HelloWorldApp'로 다르다. DateApp는 Date object를 생성하고 서로다른 출력을 한다.
'import'라는 부분은 java.util package에서 Date class를 가져와 사용한다는 의미이다. java.util은 다양한 여러가지 class를 모아놓은 package이다. Date class는 system과는 별도록 날짜를 처리할 수 있도록 만든 class이다.
2.2.2 클레스 정의하기.
Java language에서, 모든 function과 variable은 class(or object)안쪽부분에 정의를 한다. Java language는 전연변수나, 함수를 허용하지 않는다. 따라서 Java application은 class를 정의함으로서 시작된다.
import java.util.Date; class DateApp { public static void main(String args[]) { Date today = new Date(); System.out.println(today); } }
굵은 글씨가 DateApp application의 'class 정의 영역(class definition block)'이다. Java language의 기본 구조인 class는 data나 class의 instance의 behaviour을 표현해주는 template이다.
Class를 instantiate시키면 object를 생성시키는 것이다. Class나 object에 관련된 data를 variable이라고 하고, behaviour를 method라고 한다.
class name { . . . }
기본적인 class 정의의 형식은 위와 같다.
2.2.3 'main()'메소드.
Java application의 중추는 main() method이다. Java interpreter하에서 Java application을 수행시킬때, 실행 시켤려는 class의 이름을 명시해야 한다. 그러면 interpreter는 그 class의 main() method를 호출한다. main() method는 필요한 resource를 할당하고 다른 method를 실행시키는 등 program의 흐름을 컨트롤한다.
import java.util.Date; class DateApp { public static void main(String args[]) { Date today = new Date(); System.out.println(today); } }
위의 DateApp처럼 Java application는 꼭 main() method를 가지고 있어야만 한다. 그표현은 다음과 같다.
public static void main(String args[])
'public'은 main() method가 어떤 object에서든지 외부에서 호출 될 수 있음을 나타내는것이다. 'static'은 main() method가 static method임을 나타내는 것이고, void는 return value가 없다는 것을 나타낸다.
Java language의 main() method는 C/C++ 에서의 main()함수와 비슷하다. C/C++에서 runtime system은 program의 main() function을 호출함으로서 시작된다. main() function은 다른 function을 호출하게된다. 마찬가지로, Java language에서도, Java interpreter에서 class를 실행시킬때 runtime system은 class의 main() method를 호출함으로써 시작된다. 그리고 main() method는 다른 method를 호출한다.
만약 main() method가 없는 class를 Java interpreter로 실행 시킬려고 하면 에러 메시지를 호면에 보여주게 된다.
main() method의 arguments
main() method의 argument는 다음과 마찬가지로 String의 배열로 하나를 받는다.
public static void main(String args[])
이 String의 배열은 runtime system에서 application에 정보를 넘겨주기위한 수단으로 쓰인다. 배열에 있는 String값을 command line argument라고 부른다.
사용자는 command line argument 를 통해서 application에 정보를 전달할 수 있다. 다음 프로그램은 그 예를 보여주고 있다.
class Echo { public static void main (String args[]) { for (int i = 0; i < args.length; i++) System.out.println(args[i]); } }
DOS prompt상에서 다음과 같이 실행 시킬 수 있다.
C:\> java Echo Drink Hot Java Drink Hot Java
하지만 다음과 같이 입력하면 space를 무시하게 되므로 argument는 1개인 셈이다.
% java Echo "Drink Hot Java" Drink Hot Java
Unix에는 word arguments, arguments that require arguments, flags 3가지 종류의 command line argument가 있다. 그 3가지 argument를 다음의 예에서 처럼 Java에서 구현할 수 있다. 다음은 일반적인 command line 입력 형태는 다음과 같다.
usage: application_name [ optional_args ] required_args
다음은 프로그램과 사용법을 보여주고 있다.
usage: ParseCmdLine [-verbose] [-xn] [-output afile] filename
class ParseCmdLine { public static void main(String args[]) { int i = 0, j; String arg; char flag; boolean vflag = false; String outputfile = ""; while (i < args.length && args[i].startsWith("-")) { arg = args[i++]; // word argument // use this type of check for "wordy" arguments if (arg.equals("-verbose")) { System.out.println("verbose mode on"); vflag = true; } // use this type of check for arguments that require arguments else if (arg.equals("-output")) { if (i < args.length) outputfile = args[i++]; else System.err.println("-output requires a filename"); if (vflag) System.out.println("output file = " + outputfile); } // use this type of check for a series of flag arguments else { for (j = 1; j < arg.length(); j++) { flag = arg.charAt(j); switch (flag) { case 'x': if (vflag) System.out.println("Option x"); break; case 'n': if (vflag) System.out.println("Option n"); break; default: System.err.println("ParseCmdLine: illegal option " + flag); break; } } } } if (i == args.length) System.err.println("Usage: ParseCmdLine [-verbose] [-xn] [-output afile] filename"); else System.out.println("Success!"); } }
Java의 command line argument는 C/C++과는 다르다. C/C++은 argc와 argv를 넘겨주지만, Java는 args하나만을 넘겨준다.
다음은 C/C++의 argument형식과 Java의 argument형식과 비교하였다.
diff file1 file2
C/C++ argv의 첫번째 command line argumentsms diff이다.
java diff file1 file2
Java의 첫번째 command line argumentsms file1이다.
Runtime system은 Command line argument을 오직 Java application에만 넘겨준다. 대신 Java applet은 parameter를 사용하는데 그형식은 다르다.
2.2.4 Java 객체의 소개.
Java application의 주요 구성은 제공되는 object, class, method, 자신이 직접 작성한 Java language statement로 구성된다.
다음의 DateApp는 간단한 Java program으로, 간단하기 때문에 더이상의 class를 정의할 필요는 없지만, 대부분의 프로그램에서 좀더 복잡하고, 다른 class를 정의해야하는 프로그램을 작성해야 할것이다.
import java.util.Date; class DateApp { public static void main(String args[]) { Date today = new Date(); System.out.println(today); } }
2.2.4.1 객체의 선언과 초치화.
main() method의 첫번째 줄에서 today라는 object를 선언, 초기화, instantiate 시킨다. today의 constructor에서 현재 시간과 날짜로 초기화한다.
Object의 선언은 다음과 같다.
Date today;
그 일반적인 형태는 다음과 같다.
type name
new 연산자는 메모리를 할당하여 Object를 생성(instantiating)한다. Object의 초기화는 new 연산자가 생성되는 Object의 constructor method를 호출하면서 이루어진다. 따라서 constructor는 초기화를 해야 한다.
다음과 같이 argument가 없는 constructor를 default constructor라고 한다.
Date()
그러나, class는 여러개의 constructor를 가질 수 있다. 단 이름은 모두 같아야 하지만, argumnet의 type이나, 갯수는 서로 달라야한다. 예를 들면 다음과 같다.
Date(int year, int month, int day)
위의 것은 Date의 년, 월, 일을 초기화 시킬 수 있는 constructor이다.
2.2.4.2 인스턴스와 스터틱.
다음의 Java statement에서
System.out.println(today);
System.out은 System class의 out variable을 참조한다. C/C++의 struct의 element를 참조하듯이 Java에서도 '.'을 이용해서 class의 static variable이나 method를 참조 할 수 있다.
주의할점은, application에서 System class를 instantiate시키지 않고 out variable은 class로 부터 직접 참조 되지 않는다는것이다. 왜냐하면, out은 static variable로 선언 되어 있기 때문이다. 또한 static으로 선언된 method를 static method라고 부른다.
static으로 선언되지 않은 method와 variable을 instance method, instance variable이라고 한다. Instance variable이나 method를 사용하기 위해서는, 우선 class를 instanticate시키고, instance로부터 method와 variable을 얻어내야한다.
System의 out variable은 java.io package에 있는 PintStream class의 instance로써, standard output stream을 처리하는 Object이다.
System class가 application에 load될 때, System은 out을 생성시키고, 그밖의 다른 static variable도 생성한다. println()은 out의 instance method라고 부른다.
Static variable과 method를 class variable, class method라고 부른다. 왜냐하면, 각각의 class variable과 class method 오직 하나만 생성되어서 공유를 하지만, instance method와 instance vairable은 class의 instance마다 하나씩 각각 따로 생성된다.
2.2.5 응용프로그램 저장하기와 컴파일, 실행.
Java program은 ASCII 형식의 화일로 저장하면 된다. 확장자는 *.java로 한다.
Java compiler는 Java source code를 Java virgual machine을 위한 machine-level의 Java bytecode로 바꾸어준다. Java bytecode는 Java interpreter에 의해서 실행 된다.
Java source file을 compiler하면 확장자가 *.class의 화일을 생성한다. *.class의 화일 이름은 source code에 선언되어 있는 class의 이름을 이용하여 class의 갯수만큼 *.class화일이 생성된다.
Java interpreter로 main() method가 있는 *.class화일을 실행 시킨다.
2.3 Java언어의 구성요소들.
다음의 간단한 Java application을 한줄씩 보면서, Java language의 syntax와 semantics에 대해서 알아보겠다. 또한 몇가지 Java programming 환경에 대해서도 알아보겠다.
class Count { public static void main(String args[]) throws java.io.IOException { int count = 0; while (System.in.read() != -1) count++; System.out.println("Input has " + count + " chars."); } }
위프로그램은 입력을 받고 그 횟수, 즉 문자의 갯수를 보여주는 프로그램이다. (character counting program) 결과는 다음과 같다.
% java Count This is a test. Input has 16 chars.
개행문자(Enter key)까지 16번의 입력을 받았다.
2.3.1 간단한 클레스 정의하기.
class Count { public static void main(String args[]) throws java.io.IOException { int count = 0; while (System.in.read() != -1) count++; System.out.println("Input has " + count + " chars."); } }
Java language에서, 모든 method와 variable은 class안에 있어야만 한다. 그래서, 프로그램의 첫줄에 Count라는 class를 정의하고 있다. 그안에서는 character counting application에서 필요한 method와 variable을 정의한다. Count class에서는 main() method만을 정의하고 있다.
2.3.2 'main()' 메소드.
class Count { public static void main(String args[]) throws java.io.IOException { int count = 0; while (System.in.read() != -1) count++; System.out.println("Input has " + count + " chars."); } }
Java language에서 Java interpreter로 class를 실행 시킬때, runtime system은 class의 main() method를 호출하면서 시작한다. 그러면, main() method는 application을 실행시키는데 필요한 다른 method를 실행 시킨다.
2.3.3 예외상황의 소개.
class Count { public static void main(String args[]) throws java.io.IOException { int count = 0; while (System.in.read() != -1) count++; System.out.println("Input has " + count + " chars."); } }
다음에나오는 문장은 7을 0으로 나누는 수학적인 오류를 범하고 있다.
int x = 0; int y = 7; System.out.println("answer = " + y/x);
프로그램을 실행 하는동안에 위와 같은 오류가 발생될때, 즉 더이상 프로그램을 진행 할수 없는 상황이 발생 될때를 예외상황(exception) 이라고 한다.
여러가지 computer system의 예외상황 처리(handle exception)는 각각의 서로 다른 방법으로 처리하고있다.
Java language에서는 exception handler를 이용하여, 예외 상황을 catch, try할 수 있다.
Exception handler는 오류를 방지하기 위해서 try할 수 있고, 만약 돌이킬 수 없는 오류가 발생하면 exception handler는 사용자가 문제점을 해결 할 수 있도록 오류 메시지를 볼 수 있도록 한다.
Java language에서, 모든 method는 throw할 수 있도록 가능한한 모든 exception을 선언 해야한다. 위의 main() method에서는 예외상황을 처리하기 위해서(throw an exception) java.io.IOException을 호출 한다.
2.3.4 변수와 데이타형.
class Count { public static void main(String args[]) throws java.io.IOException { int count = 0; while (System.in.read() != -1) count++; System.out.println("Input has " + count + " chars."); } }
위의 프로그램에서는 지역 변수로 count 하나만 선언 하였다. 입력을 받을때마다 count값은 하나씩 증가를 하게 된다.
Java language에서 제공되는 data type은 다음과 같다.
Type Size/Format byte 8-bit two's complement short 16-bit two's complement int 32-bit two's complement long 64-bit two's complement float 32-bit IEEE 754 floating point double 64-bit IEEE 754 floating point char 16-bit Unicode character
프로그램에서 count는 정수형으로 선언되었다. Data type에 따라서 값의 한계점과, 연산자를 결정하게 된다. 예를 들면 정수형은 양수와 음수를 가질 수 있고, +, -와 같은 연산자를 이용하여 더하기, 빼기를 할 수 있는 것이다.
'= 0'은 variable을 0으로 초기화 시키는 것이다. 만약 변수를 초기화 시키지 않고 사용할려고 하면, compiler는 오류 메시지를 내보이게 된다.
Java language는 또한 array, strings, objects와 같은 여러가지 complex data type을 제공한다.
위와같은 이미 제공된 data type을 사용하는것 뿐만 아니라 'class', 'interface'를 이용하여 자신의 data type을 정의하여 사용할 수 있다. 알고보면 이미 Count라는 class를 정의하였지 않았는가?
2.3.5 흐름제어 문장.
class Count { public static void main(String args[]) throws java.io.IOException { int count = 0; while (System.in.read() != -1) count++; System.out.println("Input has " + count + " chars."); } }
character counting program에서 입력을 연속적으로 받기위한 loop를 while 문으로 표현하였다. while문의 형식은 다음과 같다.
while (expression) statement
이것은 expression이 참일 동안, statement를 실행 한다는 의미이다. read() method가 실행되어 return 되는 character값이 -1이 아니면 count를 하나 증가 시킨다.
그밖의 제어문은 다음과 같다.
Statement Keyword decision making if-else, switch loops for, while, do-while exceptions try-catch-throw miscellaneous break, continue, label:, return
Java language를 goto를 예약어로 정의하고 있지만, goto문을 제공하지는 않는다.
2.3.6 표준입력.
class Count { public static void main(String args[]) throws java.io.IOException { int count = 0; while (System.in.read() != -1) count++; System.out.println("Input has " + count + " chars."); } }
System은 java.lang package의 member이고, 표준 입력과 출력, 배열복사, 현재 시간과 날짜를 얻는 방법, 환경변수를 얻어내는것 같은 system function을 제공한다. 이때 while loop가 상용되었따.
System class의 모든 method와 variable은 class method이고 class variable이다.
System.in은 표준 입력(standard input stream)을 처리한다. Java language의 표준 입력(standard input stream)은 C library의 개념과 비슷한 것이다. Stream은 character의 flowing buffer로써, 보통 standard input stream은 keyboard로부터 character를 읽어들인다. standard input stream은 text-based application에서 사용자로부터 입력을 받는 기능을 한다.
read() method는 System.in에서 제공되어 하나의 character를 읽어 읽은 character를 return해준다. 만약 character가 없으면 -1을 return 한다.
Program에서 standard input stream으로 부터 값을 읽을때, program은 block되어 사용자가 값을 쳐넣을 동안 기다린다. 프로그램은 다음과 같은, 사용자가 입력을 마쳤다는 값을 넣을때까지 계속 기다린다.
- UNIX
- control-D character 를 사용한다. control-D character 문자는
^D
처럼 나타탄다..% java Count This is test. ^D Input has 16 chars.
- DOS shell (Windows 95/NT)
- control-Z character 를 사용한다. control-Z character 문자는
^Z
처럼 나타난다..C:\> java Count This is test. ^Z Input has 17 chars.
주의할 점은, UNIX에서는 '\n'이지만, DOS에서는 '\r\n'이다.
2.3.7 연산자.
class Count { public static void main(String args[]) throws java.io.IOException { int count = 0; while (System.in.read() != -1) count++; System.out.println("Input has " + count + " chars."); } }
character counting program에서는 [], =, !=, ++, + 와같은 몇가지 연산자를 사용하고 있다. 다른 프로그래밍 언어와 마찬가지로 Java language도 arithmetic, logical, relational, string이라는 4개의 연산자 category를 가지고 있다. 몇가지 예외를 제외하고 대체로 Java의 연산자는 다음과 같이 연산자가 피연산자 중간에 오는 infix notation을 사용하고 있다.
op1 operator op2
Arithmetic Operators.
Java language는 더하기, 빼기, 곱하기, 나누기등 여러가지 수학적연산자를 제공하고 있다.
count++는 값을 하나 증가시키는 short cut operator ++를 사용하고 있다.
Relational Opertors.
두개의 값을 비교하는 연산자다.
!= 는 두개의 값이 같지 않을때 참값을 return한다.
Logical Operators.
두개의 값을 비교하여 boolean logic opertion을 수행하는 연산자다. &&, ||는 boolean and, boolean or연산자이다.
프로그래머는 복합적인 조건을 표현할때 logical operator를 사용한다. 다음은 배열의 index가 두 한계값사이에 있도록 조건을 나타내는 것이다.
if (0 < index && index < NUM_ENTRIES)
String Operators.
Java language는 +연산자를 문자열까지 확장하여 문자열을 뒤에 덛붙이는 기능을 한다. 다음은 문자열 "Imput has", count의 값, 문자열 " chars."를 연결 시키는 문장이다.
System.out.println("Input has " + count + " chars.");
연산자 우선순위.
Java language는 다음과 같은 복합적인 연산표현을 할 수 있다.
x * y * z
위의 예는 연산 순서가 별의미가 없다.
그러나 다음의 표현은 순서에 따라 서로 다른 값이 나타난다.
x * y / 100
따라서 프로그래머는 'x * (y / 100)' 처럼 괄호를 써서 명시를 해야 한다.
2.3.8 표준출력.
class Count { public static void main(String args[]) throws java.io.IOException { int count = 0; while (System.in.read() != -1) count++; System.out.println("Input has " + count + " chars."); } }
System class는 이미 standard input stream에서 알아보았다. character counting program에서는 출력을 위해서 System class를 다시 사용한다. println()과 print()의 차이는 print()는 '\n'를 출력하지 않으므로 개행 하지 않는다는 점이다.
System.in은 표준 출력(standard output stream)을 처리한다. Java language의 표준 출력(standard output stream)은 C library의 개념과 비슷한 것이다. Stream은 character의 flowing buffer로써, 보통 standard output stream은 가지고 있는 값을 화면에 출력한다.
standard input stream은 text-based application에서 사용자로부터 입력을 받는 기능을 한다.
2.3.9 문자형.
class Count { public static void main(String args[]) throws java.io.IOException { int count = 0; while (System.in.read() != -1) count++; System.out.println("Input has " + count + " chars."); } }
C/C++에서, string은 단지 NULL로 끝나는 character의 배열이었다. 그러나, Java language에서는 String class의 object-instance이다. System과 마찬가지로, String class는 java.lang package의 member이다.
character counting program에서 String은 두번 쓰이는데, 첫번째는 main() method에서 쓰였다.
String args[]
위 code에서는 args라는 이름으로 String object 배열을 선언하고 있다. 대괄호가 비어있는것은 compilationtime에 배열의 길이를 알수 없다는 의미이다.
Compiler는 큰따옴표로 싸여진 문자열(literal string)을 만날때마다 String object을 할당한다. 따라서, 위 프로그램에서 두개의 문자열 "Input has", " chars."은 내부적으로 String object로 변환된다.
Java language에서는 편리상 + 연산자를 이용하여 다음과 같이 문자열을 연결 시킨다.
"Input has " + count + " chars."
"Input has "와 " chars."는 String으로 연결 될 수 있지만, 중간에 있는 count는 실제로는 integer지만, 그값을 문자열로 변환하여 처리한다.
2.3.10 응용프로그램 실행시키기.
2.4 'String' 클레스과 'StringBuffer' 클레스.
String, StringBuffer class를 이용하여 character data를 어떻게 처리하는지 알아보겠다. 또한 내부적으로 compiler가 어떻게 처리하는 지도 알아보겠다.
class ReverseString { public static String reverseIt(String source) { int i, len = source.length(); StringBuffer dest = new StringBuffer(len); for (i = (len - 1); i >= 0; i--) { dest.append(source.charAt(i)); } return dest.toString(); } }
위의 예어서 reverseIt() method는 String과 StringBuffer class 둘다 사용하여 string의 character의 순서를 역으로 바꾼다. 만약 word의 list를 가지고 있다면 reverseIt() method를 이용하여 덧붙여가며 sorting하는 프로그램을 구현 할 수 있다.
reverseIt() method는 순서를 역으로 바꿀 String type의 soruce라는 이름의 argument를 받아들인다. dest라는 soruce와 길이가 같은 StringBuffer를 생성한후, 역으로 바꾸기 위해서 loop에 들어간다.
여기에서 가장 중요한 점은 서로의 유용한 method를 사용하기 위해서 type를 서로 바꾸어가며 처리한다는 것이다.
2.4.1 왜 문자열을 위한 두가지 클레스를 쓰는가?
Java에서는 constant string을 위해서 String class를제공하고, string을 조작하기 위해서 StringBuffer class를 제공한다.
class ReverseString { public static String reverseIt(String source) { int i, len = source.length(); StringBuffer dest = new StringBuffer(len); for (i = (len - 1); i >= 0; i--) { dest.append(source.charAt(i)); } return dest.toString(); } }
string의 값을 바꾸고 싶지 않을때 constant string을 위한 String class를 사용한다. 예를 들어서 string data를 method에 전달해줄때, method가 string을 어떠한 방법으로도 바꾸지 않기를 바란다면, String을 사용해야 한다. 일반적으로, String은 method에 값을 전달할때, method에서 값을 return해줄때 사용한다. reverseIt() method에서는 argument롸 return값으로 String을 사용한다.
string의 character값을 알고 있고 그 값을 바꿀 필요가 있따면 non-constant string을 위한 StringBuffer class를 사용한다. 일반적으로 reverseIt() method처럼 character data를 생성, 초기화할때 StringBuffer를 사용한다.
String은 constant이기 때문에 StringBuffer보다 작고, 공유할 수 있다.
2.4.2 'String'과 'StringBuffer'생성하기.
StringBuffer dest = new StringBuffer(len);
StringBuffer를 새로 생성할때 선언, 초기화, instantiation의 3단계를 거친다. 이것은 다른모든 object를 생성할때도 마찬가지이다.
class ReverseString { public static String reverseIt(String source) { int i, len = source.length(); StringBuffer dest = new StringBuffer(len); for (i = (len - 1); i >= 0; i--) { dest.append(source.charAt(i)); } return dest.toString(); } }
dest는 초기화하기위한 constructor method에서 StringBuffer의 크기를 나타내는 integer 값이 필요하다.
StringBuffer(int length)
만약 크기를 정해주지 않으면, 다음번에 크기가 주어질때까지 내버려둔다. 그러나, 다음에 다시 메모리를 할당하지 않도록 처음에 크기를 정해주는 것이 더 좋다.
2.4.3 접근 메소드.
reverseIt() method는 source의 정보를 가져오기 위해서 2개의 charAt()과 length() accessor method를 사용하고 있다. String과 StringBuffer는 substring을 찾거나, 특정 character의 위치를 알아내는 몇가지 accessor method를 제공한다.
class ReverseString { public static String reverseIt(String source) { int i, len = source.length(); StringBuffer dest = new StringBuffer(len); for (i = (len - 1); i >= 0; i--) { dest.append(source.charAt(i)); } return dest.toString(); } }
Object의 instance variable은 외부의 다른 object로 부터 값을 변경하지 못하도록, object의 내부에 가려져 있다. Object의 instance는, 오직 잘 작성된 exception을 가지고 있는 object의 method를 통해서만 값을 접근, 변형 할 수 있다. Object의 data를 캡슐화 함으로써 외부로부터의 잘못된 상황을 방지하기위해 내부적인 정보를 외부로부터 숨긴다. 캡슐화는 객체지향 프로그래밍 기법의 기본이다.
Object로부터 정보를 얻어내기 위한 method를 accessor method라고 한다. reverseit() method는 soruce stirng의 정보를 가져오기 위해서 두개의 String의 accessor method를 사용하고 있다.
첫번째 String의 length() accesor method는 String source의 길이를 알아내는 기능을 한다.
int len = source.length();
두번째, charAt() accessor method는 parameter로 넘겨온 위치의 charcter의 값을 return해준다.
source.charAt(i)
2.4.3.1 다른 접근 메소드.
length(), charAt() 이외에 String은 몇가지 다른 accessor method를 제공한다. StringBuffer도 마찬가지로 비슷한 accessor method를 제공한다.
String은 특정 문자열의 character를 찾아 index를 넘겨주는 indexOf(), lastIndexOf()를 제공한다. indexOf()는 문자열의 처음 앞부분에서 부터 찾고, lastIndexOf()는 뒤에서 부터 찾는다.
indexOf(), lastIndexOf() method는 string의 substring을 찾아주는 substring()과함께 문자열을 서로 연결할때 많이 쓰인다. 다음의 class는 lastIndexOf()와 substring()을 사용하여 filename의 각각의 서로다른 부분을 떼어내는 기능을 수행한다.
다음 method는 error checking을 하지 않고, argument는 full directory path와 filename과 extention을 가지고 있는것으로 가정한다.
class Filename { String fullpath; char pathseparator; Filename(String str, char sep) { fullpath = str; pathseparator = sep; } String extension() { int dot = fullpath.lastIndexOf('.'); return fullpath.substring(dot + 1); } String filename() { int dot = fullpath.lastIndexOf('.'); int sep = fullpath.lastIndexOf(pathseparator); return fullpath.substring(sep + 1, dot); } String path() { int sep = fullpath.lastIndexOf(pathseparator); return fullpath.substring(0, sep); } }
extention() method는 lastIndexOf()를 사용하여 뒤에서 부터 '.'을 찾아내어 확장자의 시작 부분을 찾아낸 후, substring()을 이용하여 화일의 확장자를 알아낸다.
위 프로그램은 '.'을 포함한 확장자를 가지고 있는 filename이 argument로 넘겨온다는 것을 가정하고 있으며, 만약 '.'이 존재하지 않으면 lastIndexOf()는 -1을 return 하게 되고, substring() method는 오류상황에서 "string index out of range exception"을 수행하게 된다.
위의 예제어서는 한가지 version의 indexOf()와 lastIndexOf() method를 사용하고 있지만, 그밖의 다양한 기능을 수행하는 다른 version의 indexOf()와 lastIndexOf() method가 있다.
- returns the index of the first (last) occurrence of the specified character
- returns the index of the first (last) occurrence of the specified character, searching forward (backward) from the specified index
- returns the index of the first (last) occurrence of the specified String.
- returns the index of the first (last) occurrence of the specified String, searching forward (backward) from the specified index
String과 마찬가지로, StringBuffer도 length()와 charAt() accessor method를 제공한다. 또한, capacity()라는 method를 제공한다. capacity() method는 문자열의 길이를 알려주는 length()와는 달리, StringBuffer를 위해서 할당된 현재 메모리 크기를 return 해준다.
class ReverseString { public static String reverseIt(String source) { int i, len = source.length(); StringBuffer dest = new StringBuffer(len); for (i = (len - 1); i >= 0; i--) { dest.append(source.charAt(i)); } return dest.toString(); } }
위의 프로그램에서 바뀐것 없다. Loop를 돌면서 StringBuffer의 크기는 증가된다.
2.4.4 'StringBuffer' 변경하기.
reverseIt() method에서 StringBuffer의 append() method는 dest에 character를 덧붙힌다. StringBuffer는 그밖에 특정 위치에 character를 buffer에 첨가하는 기능을 가진 method도 제공한다.
class ReverseString { public static String reverseIt(String source) { int i, len = source.length(); StringBuffer dest = new StringBuffer(len); for (i = (len - 1); i >= 0; i--) { dest.append(source.charAt(i)); } return dest.toString(); } }
append() method에서 현재의 buffer크기보다 더큰 크기를 요구되면, 메모리를 다시 할당받아 StringBuffer의 크기를 변경한다. 메모리 할당은 많은 노력을 필요로 하는 작업이기 때문에, 처음에 StringBuffer의 크기를 충분히 적당한 많큼 할당하여 메모리를 할당하는 횟수를 줄임으로써 프로그램의 효율을 높힐 수 있다. 위의 예어서 처럼 dest의 크기를 soruce의 크기와 같게 함으로써 메모리 할당의 횟수를 1번만으로 만족하다.
위의 append()는 StringBuffer의 맨 끝부분에 characer를 첨가한다. 그밖에 float, int, boolean과 같은 여러가지 type의 data를 첨가 할 수도 있다. 이때 이러한 data type은 string의 형태로 type conversion이루어진후 뒤에 덛붙히게 된다.
그밖에 StringBuffer의 중간에 data를 삽입하고 싶을 경우가 있다. 이때 StringBuffer의 insert() method를 사용한다. 다음은 StringBuffer의 중간에 string을 삽입하는 예이다.
StringBuffer sb = new StringBuffer("Drink Java!"); sb.insert(6, "Hot "); System.out.println(sb.toString());
위 프로그램의 간단한 결과는 다음과 같다.
Drink Hot Java!
위의 예에서 볼 수 있듯이, insert()를 사용하기 위해서는, index를 명시해야 한다. "Hot "이 들어갈 자리는 "Java"의 'J'가 있는 자리이다. 따라서, string의 index는 0에서 부터 시작하고 'J'의 위치는 6이 된다. 마찬가지로, append()에서 맨 뒤에 character를 덛붙힌다는것은 문자열의 길이를 index로 하여 그곳에 덛붙힌다는것과 같은 의미이다.
그밖에 StringBuffer의 특정 위치에 charcter값을 변경하는 setCharAt()을 제공한다.
2.4.5 객체를 문자열로 변환.
reverseIt()에서 StringBuffer는 String으로 변경되어 return된다. 이렇게, String의 vlaueOf() method를 이용하여 여러가지 다른 type은 String으로 변경될 수있다.
class ReverseString { public static String reverseIt(String source) { int i, len = source.length(); StringBuffer dest = new StringBuffer(len); for (i = (len - 1); i >= 0; i--) { dest.append(source.charAt(i)); } return dest.toString(); } }
종종 object는 method가 오직 String 값을 처리할 수 있도록 String으로 변환할 필요가 있다. 예를 들어서 System.out.printIn()은 StringBuffer를 허용하지 않으므로, StringBuffer는 String으로 변환할 필요가 있다. 위의 예에서 reverseIt() method는 return할 때 StringBuffer를 String으로 변환하기 위해서 StringBuffer의 toString() method를 사용하였다.
return dest.toString();
java.lang의 몇개의 class에서는 Character, Integer, Boolean과 같은 "type wripper" class를 포함한 toString()을 제공한다. Object class는 Object를 String으로 변환하기 위해서 toString()을 제공한다. Object의 subclass를 작성할때, toString()은 subclass에서 다른 형태의 일을 하는 method로 다시 정의(override) 할 수 있다.
또한 편의상, String class는 다른 type의 variable을 String으로 바꾸어주는 static method로 valueOf()를 제공한다. 예를 들어서, 다음은 pi 의 값을 프린트해준다.
System.out.println(String.valueOf(Math.PI));
2.4.6 객체를 숫자로 변환.
Integer, Float, Double, Long class는 그 숫자값을 String으로 변환 할 수 있다.
String class자체 에서는 String을 실수나 정수 또는 다른 형태의 숫자로 변환 해주는 method를 제공하지 않는다.
그러나, 4개의 "type wrapper" class(Integer, Double, Float, Long)는 String을 각각의 숫자 값으로 바꾸어주는 valueOf()라는 method를 제공한다. 다음은 Float class의 valueOf() method의 예를 보여주고 있다.
String piStr = "3.14159"; Float pi = Float.valueOf(piStr);
2.4.7 문자열과 Java 컴파일러.
String과 StringBuffer는 그밖의 덛붙히기, 잘라내기, 교환하기, 비교하기, 대/소문자로 변환하기 등 string data를 처리하기 위한 많고도 유용한 method를 제공하고 있다.
마지막으로, Java compiler가 literal string을 내부적으로 어떻게 String, StringBuffer로 변환하는지 알아본다.
Java language에서 다음처럼 C/C++에서 쓰는것과 마찬가지로 큰따옴표로 둘러싸인 liternal 문자열을 작성한다.
"Hello World!"
literal string은 String이 들어갈 수 있는곳은 어디에나 쓸 수 있다. 다음은 System.out.println()이 String대신 literal string을 사용하는 예이다.
System.out.println("And might I add that you look lovely today.");
또한 literal string에 바로 String method를 사용할 수 도 있다.
int len = "Goodbye Cruel World".length();
compiler는 literal string을 마주칠때 마다 자동으로 String object를 새롭게 생성하기 때문에, literal string은 String을 초기화 시키는데도 쓰인다.
String s = "Hola Mundo";
String s = new String("Hola Mundo");
위의 것과 아래의 것과 같지만, 위의것이 더욱 효율적이다. 왜나하면, compiler는 "Hello World"가 나타날때 한번, new String()이 나타날 때 한번, 두번 String을 생성하기 때문이다.
Java language에서, String을 연결 할때 '+'를 사용할 수 있다.
String cat = "cat"; System.out.println("con" + cat + "enation");
위의 것은 보기좋고 현란하기 까지 한데, String은 변경할 수 없는 object이다. 따라서, compiler는 내부적으로 다음과 같은 code로 변환하여 실행 한다.
String cat = "cat"; System.out.println(new StringBuffer.append("con").append(cat).append("enation"));
2.5 명령어 Argument.
Java application에서 command line argument를 어떻게 처리하는지 알아본다. Java interperter는 단지 command line argumnet를 java application에 전달 해줄 뿐이다. applet에서는 대신 parameter를 사용한다.
Java application은에서는 command line으로부터 여러개의 argument를 전해받아 처리할 수 있다. Command line argument는 사용자로부터 application의 연산의 방법에 영향을 줄 수 있다.
Application을 실행 시킬때, application name 다음에 command line argument를 입력 할 수 있다. 예를 들어서, Sort라는 application을 가지고 있다고 할때, file의 sorting할려는 file 이름을 지정할 수 있다.
C:\> java Sort ListOfFriends
다음 프로그램은 command line argument를 받아서 하나씩 출력하는 예제이다.
class Echo { public static void main (String args[]) { for (int i = 0; i < args.length; i++) System.out.println(args[i]); } }
DOS prompt상에서 다음과 같이 실행 시킬 수 있다.
C:\> java Echo Drink Hot Java Drink Hot Java
2.5.1 공백분자는 명령어 Argument를 구분한다.
하지만 다음과 같이 입력하면 space를 무시하게 되므로 argument는 1개인 셈이다.
% java Echo "Drink Hot Java" Drink Hot Java
2.5.2 편리성.
Unix에는 word arguments, arguments that require arguments, flags 3가지 종류의 command line argument가 있다. 그 3가지 argument를 다음의 예에서 처럼 Java에서 구현할 수 있다. 다음은 아래와 같은 사용법을 가지는 프로그램이다.
usage: application_name [ optional_args ] required_args
다음은 word argument를 처리하는 예이다.
if (argument.equals("-verbose")) vflag = true;
다음은 Argument를 필요로 하는 Argument를 처리하는 방법이다. 즉, Argument 다음에 또다른 Argument를 꼭 필요로 한는것이다.
if (argument.equals("-output")) { if (nextarg < args.length) outputfile = args[nextarg++]; else System.err.println("-output requires a filename"); }
다음을 command line에서 Flag를 지정하는 형식과, Flag를 처리하는 방법이다.
-x -n or -n -x
위의 것은 사용자의 편리성을 위해서 다음과 같은 형식도 허용하면 좋다.
-nx or -xn
이것을 처리하기 위한 예이다.
for (j = 1; j < arg.length(); j++) { flag = arg.charAt(j); switch (flag) { case 'x': if (vflag) System.out.println("Option x"); break; case 'n': if (vflag) System.out.println("Option n"); break; default: System.err.println("ParseCmdLine: illegal option " + flag); break; } }
2.5.3 명령어 Argument 처리.
class ParseCmdLine { public static void main(String args[]) { int i = 0, j; String arg; char flag; boolean vflag = false; String outputfile = ""; while (i < args.length && args[i].startsWith("-")) { arg = args[i++]; // word argument // use this type of check for "wordy" arguments if (arg.equals("-verbose")) { System.out.println("verbose mode on"); vflag = true; } // use this type of check for arguments that require arguments else if (arg.equals("-output")) { if (i < args.length) outputfile = args[i++]; else System.err.println("-output requires a filename"); if (vflag) System.out.println("output file = " + outputfile); } // use this type of check for a series of flag arguments else { for (j = 1; j < arg.length(); j++) { flag = arg.charAt(j); switch (flag) { case 'x': if (vflag) System.out.println("Option x"); break; case 'n': if (vflag) System.out.println("Option n"); break; default: System.err.println("ParseCmdLine: illegal option " + flag); break; } } } } if (i == args.length) System.err.println("Usage: ParseCmdLine [-verbose] [-xn] [-output afile] filename"); else System.out.println("Success!"); } }
Java의 command line argument는 C/C++과는 다른다. C/C++은 argc와 argv를 넘겨주지만, Java는 args하나만을 넘겨준다.
다음은 C/C++의 argument형식과 Java의 argument형식과 비교하였다.
diff file1 file2
C/C++ argv의 첫번째 command line argumentsms diff이다.
java diff file1 file2
Java의 첫번째 command line argumentsms file1이다.
Runtime system은 Command line argument을 오직 Java application에만 넘겨준다. 대신 Java applet은 parameter를 사용하는데 그형식은 다르다.
2.6 Thread 처리.
이번에는 thread를 이용하여 동시에 여러가지 일을 수행 하는 Java application 또는 applet을 살펴보겠다.
왜? 또는 언제 thread를 사용하는가?
Thread와 thread group을 어떻게 처리하고, deadlock이나 race condition을 어떻게 피하는지에 대해서 알아본다.
2.6.1 Thread란 무엇인가?
Thread는 일반적으로 process에 의해서 control되는 execution context 또는 lightweight process라고 부른다.
모든 프로그래머는 순차적인 프로그램에 익숙해있다.
아마 당신은 "Hello World!"라고 출력해주는 프로그램이나, 이름, 번호를 sort해주는 프로그램을 작성해본 경험이 있을것이다. 그러나 이렇 프로그램은 시작해서 하나씩 수행해나가며 끝을 보는 순차적인 프로그램이다.
Thread도 순차적인 프로그램과 유사하다.
하나의 thread프로그램 또한 시작과 끝까지 하나씩 수행해나가는 순차적인 프로그램과 같다. 그러나, thread 자체는 프로그램은 아니다. 즉, 혼자 수행할 수 있는 단위가 아니라 프로그램 안에서 수행되는 것이다.
Definition: A thread is a single sequential flow of control within a program.
하나의 thread에서는 새로운 개념이 없다. 진정한 thread는 하나의 순차적인 thread가 아니라, 프로그램내에서 동시에 여러가지 일을 수행하는 multiple thread를 사용하는 것이다.
HotJava browser에서는 화면 scroll을 하는 동안에 image를 load시키고, 한쪽에서는 animation을 하고 있고, 다른 한쪽에서는 sound가 나온다. 배경화면을 읽는동안 새로운 page를 download할 수 있다. 이것은 HotJava browser가 multithreaded application이기 때문이다.
2.6.2 간단한 Thread 예제.
다음은 두개의 thread를 생성하여 독립적으로 수행하는 간단한 thread 프로그램이다.
class TwoThreadsTest { public static void main (String args[]) { new SimpleThread("Jamaica").start(); new SimpleThread("Fiji").start(); } } class SimpleThread extends Thread { public SimpleThread(String str) { super(str); } public void run() { for (int i = 0; i < 10; i++) { System.out.println(i + " " + getName()); try { sleep((int)(Math.random() * 1000)); } catch (InterruptedException e) {} } System.out.println("DONE! " + getName()); } }
SimpleThread는 java.lang package에서 제공되는 Thread class의 subclass이다.
SimpleThread class의 첫번째 method는 constructor이다. Constructor에서는 단지 argument를 String으로 받아 들인다. 넘겨온 argument는 프로그램의 의미적으로 볼때 thread의 이름으로 쓰인다.
그다음 method는 run()이다. run() method는 어떤 thread에서도 핵심이 되는 method이다.
여기에서는 단지 for loop를 돌면서 반복횟수와 thread의 이름을 출력하고, 0~1초동안 임의로 쉰다음에 다시 loop를 돈다. loop가 끝나면 "DONE!"이란 메시지를 남기고 수행을 마친다.
TwoThradsTest class는 main() method를 제공하고 있다. 여기에서는 "Jamaica"라는 이름과 "Fiji"라는 이름의 thread를 각각생성한다.
main() method는 각각의 thread의 start() method를 호출하게 됨으로써, 각각의 thread는 작업을 수행하게 된다. 다음은 위 프로그램의 실행 결과이다.
0 Jamaica 0 Fiji 1 Fiji 1 Jamaica 2 Jamaica 2 Fiji 3 Fiji 3 Jamaica 4 Jamaica 4 Fiji 5 Jamaica 5 Fiji 6 Fiji 6 Jamaica 7 Jamaica 7 Fiji 8 Fiji 9 Fiji 8 Jamaica DONE! Fiji 9 Jamaica DONE! Jamaica
이와같이 출력은 고정되어있지 않다. 그이유는 두개의 SimpleThread thread가 동시에 실행되기 때문이다.
run() method이외에 start(), sleep() method에대해서는 다음장에서 알아보기로 한다.
2.6.3 Thread의 속성값.
Thread를 이용한 효율적이고 오류가 없는 프로그램을 작성하기 위해서는 thread의 다양한 부분과, Java runtime system에 대해서 알 필요가 있다. Thread의 body를 어떻게 정의해야 하고, thread의 life-cycle과, runtime system이 어떻게 thread를 scheduling하는지에 대해서, thread group, daemon thread에 대해서대해서도 알아야 한다.
Java thread의 body는 java.lang package에 있는 Thread class에 의해서 run() method를 정의할 수 있고, 또는 Runnable object로 생성하는 두가지 방법이 있다.
2.6.3.1 Thread의 Body.
Thread의 모든 행위는 thread의 body에있는 run() method에 의해서 행해진다. Thread class의 subclass를 정의함으로써 run() method를 override 할 수 있다. Thread가 생성되어 초기화가 된후에, runtime system은 run() method를 호출한다. run() method는 생성된 thread가 해야할 일을 전담하여 수행한다.
보통 thread의 run() method는 loop를 돈다. 예를 들어서 Animation은 loop를 돌면서 연속적으로 image를 보여준다. 가끔 thread의 run() method는 오랜시간동안 연산을 할 경우도 있다. 예를 들어서 sound나 JPEG image를 보여주거나 download 하는 경우를 들 수 있다.
다음은 run() method를 정의하는 2가지 방법이다.
2.6.3.1.1 - 간단한 thread 예제.
java.lang package에 있는 Thread class의 run() method를 override 한다. 다음 이전에 2.6.2에서 정의했던 예제이다.
class SimpleThread extends Thread { public SimpleThread(String str) { super(str); } public void run() { for (int i = 0; i < 10; i++) { System.out.println(i + " " + getName()); try { sleep((int)(Math.random() * 1000)); } catch (InterruptedException e) {} } System.out.println("DONE! " + getName()); } }
2.6.3.1.2 Clock Applet.
다음은 Runnable interface를 제공하는 class이다. 이것 또한 java.lang package에 정의되어 있다.
이방법은 thread를 instantiate 시킬때 이 새로운 thread의 handle을 Runnable class의 instance에 넘겨준다. Runnable class는 이 thread에 run() method를 제공해준다. 다음 clock applet은 thread의 run() method를 지원하는 Runnable interface를 이용한 방법이다.
import java.awt.Graphics; import java.util.Date; public class Clock extends java.applet.Applet implements Runnable { Thread clockThread; public void start() { if (clockThread == null) { clockThread = new Thread(this, "Clock"); clockThread.start(); } } public void run() { while (clockThread != null) { repaint(); try { clockThread.sleep(1000); } catch (InterruptedException e){ } } } public void paint(Graphics g) { Date now = new Date(); g.drawString(now.getHours() + ":" + now.getMinutes() + ":" + now.getSeconds(), 5, 10); } public void stop() { clockThread.stop(); clockThread = null; } }
Clock class는 Applet class으로부터 파생되었다. 그러나 Clock applet은 실행하는 동안에 화면을 연속적으로 갱신하는데 따로 시간이 걸리지 않게 하기 위해서 thread를 이용한다. 또한, Java language는 다중 상속(muliple-inheritance)를 지원하지 않기 때문에 Applet과, Thread class를 동시에 상속할 수 없다. 따라서, Clock class는 thead의 기능 지원하기 위해서 Runnable interface를 사용하고 있다.
Applet자체가 thread가 아니고, Java 호환 browser나, appletviewer에서 자동으로 thread를 생성해주는 것이 아니므로, applet이 thread가 필요하면 따로 thread를 생성해야 한다. Clock applet에서는 자주 화면을 갱신하기 위해서 하나의 thread가 필요한데, 그이유는 화면갱신을 위해서 따로 시간이 걸리지 않게 하기 위해서다.
Runnable interface를 사용하는 방법은 다음과 같다.
class Clock extends Applet implements Runnable {
Runnable interface에서는 argument와 return값이 없는 run() method를 정의한다. Clock class에서는 Runnable interface를 처리하기 위해서, run() method를 정의해야 한다.
run() method를알아보기 전에 먼저 몇가지 필요한 것들을 알아본다.
Applet에서 applet을 load해서 실행할때 제일먼저 start() method가 실행된다.
public void start() { if (clockThread == null) { clockThread = new Thread(this, "Clock"); clockThread.start(); } }
Clock applet에서 clockThread라는 thread를 가지고 있는데, start() method에서는 제일먼저 clockThread가 null 인지 검사한다. 만약 clockThread가 null이면, applet은 새로운 thread를 생성된다. null이 아니면, applet이 이미 실행되고 있다는것이다. 다음은 thread를 새롭게 생성하는 방법이다.
clockThread = new Thread(this, "Clock");
여기에서 첫번째 argument로 this를 넘겨 받았다. 이것은 Runnable interface에서 생성된 thread의 target을 나타내는 것이다. 즉, 이렇게 함으로써 Thread constructor는 target Runnable object의 run() method를 얻어내는 것이다. 두번째 argument는 단지 thread의 이름이다.
만약, Clock applet이 실행되고 있는 page를 그만두고 떠나게 되면 applet이 실행되고 있는 application은 applet의 stop() method를 호출하게 되고, thread는 null이 된다. 다음의 예제에서는 시간을 갱신하는 작업을 그만두게 된다.
public void stop() { clockThread.stop(); clockThread = null; }
만약 Clock applet이 있는 page가 다시 참조되면, start() method를 다시 호출되고, 새로운 thread를 생성하고 함으로써 시계가 돌아가게 된다.
Clock class의 핵심 부분인 run() method는 다음과 같다.
public void run() { while (clockThread != null) { repaint(); try { clockThread.sleep(1000); } catch (InterruptedException e){ } } }
앞에서 살펴본것과 같이 applet이 stop되면, applet은 clockThread를 stop시키고 clockThread를 null로 만든다. 이때 run() method는 clockThread가 null이 아닐때만 loop를 돈다. Loop안에서는, applet은 화면을 repaint하며 thread를 1초동안 쉬게 한다. Applet의 repaint() method는 applet의 화면을 직접적으로 출력시켜주는 paint() method를 호출한다. Clock applet의 paint() method는 현재 시간을 얻어내어 화면에 출력해준다.
public void paint(Graphics g) { Date now = new Date(); g.drawString(now.getHours() + ":" + now.getMinutes() + ":" + now.getSeconds(), 5, 10); }
만약 class가 Thread가 아니 다른 class로 부터 파생되었다면, 일반적으로 Runnable을 사용한다.
2.6.3.2 Thread의 상태, 단계.
Thread는 몇가지 상태를 가지는데, thread는 running, sleep, dead 등 각 상태마다 해야 하는 일이 다르다.
다음 diagram은 thread의 각 단계와 수행되는 method를 보여주고 있다.
다음 문장은 새로운 thread를 생성한다. 그러나, start하지는 않고 다이어그램의 "New Thread"상태에 놓이게 된다.
Thread myThread = new MyThreadClass();
Thread가 "New Thread"상태에 놓이게 되면, 이것은 비어있는 Thread object이다. 아직 system resoruce를 항당하지 않았다. 그러므로, 이 상태에서는 thread를 오직 start, stop시킬 수만 있다.
다음의 2개의 문장을 보기로 한다.
Thread myThread = new MyThreadClass(); myThread.start();
start() method는 thread를 run시키기 위해서 system resource를 할당하고, scheduling을 하며, run() method를 호출 한다. 이러한 상태는 "Running"이라기 보다는 "Runnable" 상태라고 한다. 왜냐하면, 실지로 thread가 아직 실행되는것이 아니기 때문이다. 대부분의 computer는 동시에 thread가 "Runnable" 하는것은 불가능하기 때문에 Java runtime system은 processor는 공유하기 위해서 "Runnable"한 모든 thread를 scheduling해야한다. 이때 thread는 우선순위가 있는데 이것은 다음에 알아보기로 한다.
다음의 4가지 상황이 일어날때 thread는 "Not Runnable"상태에 놓이게 된다.
- someone calls its
suspend()
method - someone calls its
sleep()
method - the thread uses its
wait()
method to wait on a condition variable - the thread is blocking on I/O.
예를 들어서 다음의 굵은 글씨를 볼 수 있다.
Thread myThread = new MyThreadClass(); myThread.start(); try { myThread.sleep(10000); } catch (InterruptedException e){ }
위의 예는 thread를 10초동안 잠재우는 것이다. 이 10초동안은 processor를 상용할 수 있지만, myThread는 실행되지 않는다. 10초가 지나면 myThread는 다시 실행할 수 있도록 "Runnable"상태에 놓인다.
"Not Runnable"상태에 들어가기전에 thread가 다시 "Runnable"상태로 되돌아가기 위해서 escape route가 있다. 예를 들어서, thread가 sleep하기 전에 "Runnable" 상태로 돌아오기 위해서 몇초동안 쉴것인지 명시를 하는것이다. 다음은 "Not Runnable"상태로 들어가기전에 "Runnable"상태로 돌아오기 위한 escape route방법을 소개한다.
- If a thread has been put to sleep, then the specified number of milliseconds must elapse.
- If a thread has been suspended, then someone must call its
resume()
method. - If a thread is waiting on a condition variable, whatever object owns the variable must relinquish it by calling either
notify()
ornotifyAll()
. - If a thread is blocked on I/O, then the specified I/O command must complete.
Thread는 run() method를 마치고 자연스럽게 죽을 수 있다.
public void run() { int i = 0; while (i < 100) { i++; System.out.println("i = " + i); } }
또는, stop() method를 호출함으로써 thread를 죽일 수 있다.
Thread myThread = new MyThreadClass(); myThread.start(); try { Thread.currentThread().sleep(10000); } catch (InterruptedException e){ } myThread.stop();
Thread의 method를 호출했는데, thread가 method를 실행시키기위한 state로 전환이 되지 않으면, runtime system은 IllegalThreadStateExecption을 실행시킨다. 예를 들어서, "Runnable"하지 않은 thread의 suspend()를 호출하면 IllegalThreadStateException이 실행된다.
Thread의 method를 정의할때는 exception이나 catch, 또는 uncaught된 exception을 처리하기 위한 method를 정의해야 한다.
Thread의 state를 알아보기 위한 Thread class의 programming interface로 isAlive() method가 제공된다. isActive() method는 thread가 시작해서 stop되지 않았으면 true를 return한다. 그러므로, 만약 isActive() method가 false를 return하면, thread는 "New Thread"나 "Dead"상태에 있음을 알 수 있고, true를 return하면 thread는 "Runnable"이나 "Not Runnable"상태에 있음을 알 수 있다. 즉, 우리는 "New Thread"과 "Dead"상태를 굳이 구별할 필요없고, "Runnable"과 "Not Runnable"상태를 구별할 필요가 없다.
2.6.3.3 Thread의 우선순위.
Thread는 concurrent하게 실행된다는 것을 알고 있다. 즉, 대부분의 컴퓨터는 일반적으로 CUP 하나를 가지고 있으므로, cuncurrency에서도 사실상 한번에 오직 하나의 thread만이 실행된다. 이렇게, 하나의 CUP에서 여러개의 thread를 실행 시키기 위해서 scheduling이라는것을 한다. Java runtime system에서는 일반적으로 잘알려진 scheduling algorithm으로 fixed priority scheduling을 사용하고 있다.
Java thread가 생성될때, 그 thread는 자신을 생성한 thread의 우선순위를 상속 받는다. 또한, thread가 생성된 후에는 setPriority() method를 이용하여 thread의 우선순위를 정할 수 있다. Thread의 우선순위는 MIN_PRIORITY와 MAX_PRIORITY중간의 값을 가질 수 있다.
여러개의 thead가 존재할때 runtime system은 우선순위가 높은 thread를 선택하여 "Runnable"상태로 전환하여 실행 시킨다. 우선 순위가 높은 thread가 yeild, stop될때 또는 "Not Runnable"상태로 될때만, 우선순위가 낮은 thread가 실행 된다. 만약 우선순위가 같은 CUP를 기다리는 thread 2개가 존재하면 round-robin 방법으로 선택한다.
Java runtime system의 thread scheduling algorithm은 또한 preemptive이다. 즉, 현재 "Runable"한 thead보다 우선순위가 높은 thread가 기다리고 있다면, runtime system은 새로운 우선순위가 높은 thread를 선택하여 실행 시킨다. 이때, 이런 새로운 우선순위가 높은 thread는 다른 thread를 점유(preempt) 했다고 한다.
Java runtime system의 thread scheduling방법은 다음의 법칙으로 요약 할 수 있다.
Rule: At any given time, the highest priority runnable thread is running.
import java.awt.*; public class RaceApplet extends java.applet.Applet implements Runnable { final static int NUMRUNNERS = 2; final static int SPACING = 20; Runner runners[] = new Runner[NUMRUNNERS]; Thread updateThread; public void init() { String raceType = getParameter("type"); for (int i = 0; i < NUMRUNNERS; i++) { runners[i] = new Runner(); if (raceType.compareTo("unfair") == 0) runners[i].setPriority(i+1); else runners[i].setPriority(2); } if (updateThread == null) { updateThread = new Thread(this, "Thread Race"); updateThread.setPriority(NUMRUNNERS+1); } } public boolean mouseDown(java.awt.Event evt, int x, int y) { if (!updateThread.isAlive()) updateThread.start(); for (int i = 0; i < NUMRUNNERS; i++) { if (!runners[i].isAlive()) runners[i].start(); } return true; } public void paint(Graphics g) { g.setColor(Color.lightGray); g.fillRect(0, 0, size().width, size().height); g.setColor(Color.black); for (int i = 0; i < NUMRUNNERS; i++) { int pri = runners[i].getPriority(); g.drawString(new Integer(pri).toString(), 0, (i+1)*SPACING); } update(g); } public void update(Graphics g) { for (int i = 0; i < NUMRUNNERS; i++) { g.drawLine(SPACING, (i+1)*SPACING, SPACING + (runners[i].tick)/1000, (i+1)*SPACING); } } public void run() { while (updateThread != null) { repaint(); try { updateThread.sleep(10); } catch (InterruptedException e) { } } } public void stop() { for (int i = 0; i < NUMRUNNERS; i++) { if (runners[i].isAlive()) { runners[i].stop(); runners[i] = null; } } if (updateThread.isAlive()) { updateThread.stop(); updateThread = null; } } }
위의 예제는 우선순위가 서로 다른 "runner" thread가 animate하는 applet이다. Applet에 Mouse로 click하면, 두개의 runner가 시작된다. 첫번째 runner는 "1"이라는 이름을 가지고 있고, 우선순위가 1이다. 두번째 runner는 "2"라는 이름을 가지고 있고, 우선 순위가 2이다.
아래 를 마우스로 click하면 runner applet이 실행 됩니다.
다음은 두 runner의 run() method이다.
public int tick = 1; public void run() { while (tick < 400000) { tick++; } }
run() method는 단지 1 ~ 400,000가지 센다. Instance variable tick은 public이다. 왜냐하면, applet에서 이 값을 참조하기 때문이다.
두개의 runner thread이외에, applet은 화면에 선을 그려주는 3번째 thread를 하나 더 가지고 있다. 선을 그리는 thread의 run() method는 무한 loop를 돌고 있다. ; 두개의 runner의 tick의 값을 계산하여 화면에 길이만큼 선을 긋고 10 millisecond 동안 sleep한다. 이 thread의 우선순위는 3으로써 다른 2개의 runner보다 더 높다. 그러므로 drawing thread가 10 milliseconds 지나서 깨어나면, 이 때 우선순위가 높은 runner가 실행되고 화면에 선을 긋는다.
알다시피 이것은 정당한 경주가 되지 못한다. 왜냐하면, 어느 runner하나가 다른 runner보다 우선권이 더 높기 때문이다. 매번 우선순위가 낮은 runner는 우선순위가 높은 runner에게 CPU를 뺏긴다.
다음은 우선순위가 서로 같은 runner applet이다. 마우스로 click하면 경주가 시작된다.
이번에는 두 thread(runner)가 같은 우선권을 가지고있기 때문에, scheduler는 round-robin 기법으로 차례대로 실행시킨다.
어떤 system에서는 우선순위가 같은 "Runnable" thread가 여러개 있을때 서로 경쟁을 하면서 일을 수행한다. 다음은 우선순위가 같은 두개의 thread를 생성하여 실행 시키는 프로그램이다.
class SelfishRunner extends Thread { public int tick = 1; public int num; SelfishRunner(int num) { this.num = num; } public void run() { while (tick < 400000) { tick++; if ((tick % 50000) == 0) { System.out.println("Thread #" + num + ", tick = " + tick); } } } } class RaceTest { final static int NUMRUNNERS = 2; public static void main(String args[]) { SelfishRunner runners[] = new SelfishRunner[NUMRUNNERS]; for (int i = 0; i < NUMRUNNERS; i++) { runners[i] = new SelfishRunner(i); runners[i].setPriority(2); } for (int i = 0; i < NUMRUNNERS; i++) { runners[i].start(); } } }
시분할(Time-Slicing) system에서는 다음과 같은 결과를 보여준다.
Thread #1, tick = 50000 Thread #0, tick = 50000 Thread #0, tick = 100000 Thread #1, tick = 100000 Thread #1, tick = 150000 Thread #1, tick = 200000 Thread #0, tick = 150000 Thread #0, tick = 200000 Thread #1, tick = 250000 Thread #0, tick = 250000 Thread #0, tick = 300000 Thread #1, tick = 300000 Thread #1, tick = 350000 Thread #0, tick = 350000 Thread #0, tick = 400000 Thread #1, tick = 400000
하지만, non-time-sliced system에서는 다음과 같은 결과가 나온다.
Thread #0, tick = 50000 Thread #0, tick = 100000 Thread #0, tick = 150000 Thread #0, tick = 200000 Thread #0, tick = 250000 Thread #0, tick = 300000 Thread #0, tick = 350000 Thread #0, tick = 400000 Thread #1, tick = 50000 Thread #1, tick = 100000 Thread #1, tick = 150000 Thread #1, tick = 200000 Thread #1, tick = 250000 Thread #1, tick = 300000 Thread #1, tick = 350000 Thread #1, tick = 400000
위와같은 이유는, time-sliced system에서는 time slot과 우선순위에 따라서 CPU를 공유한다. 즉, 정해진 시간동안 번갈아가며 실행되고, 우선순위가 높은 것은 CPU를 점유할 수 있도록 되어있다. Thread가 어떻게 schedule될지 모름으로, 실행 결과의 순서는 예측 할 수 없게 된다.
하지만, non-time-sliced system에서는 처음 우선순위가 높은 thread를 선택하여 thread자신이 스스로 sleeping, yielding, finlish하여 CPU를 내놓거나, 우선순위가 높은 것이 CPU를 점유할때 까지 계속 실행 된다.
다음 예는 yield() method를 이용하여 자발적으로 CPU를 양보한다. 만약 "Runnable" state에 있는 thread가 없으면, yield는 무시된다.
class PoliteRunner extends Thread { public int tick = 1; public int num; PoliteRunner(int num) { this.num = num; } public void run() { while (tick < 400000) { tick++; if ((tick % 50000) == 0) { System.out.println("Thread #" + num + ", tick = " + tick); yield(); } } } } class RaceTest2 { final static int NUMRUNNERS = 2; public static void main(String args[]) { PoliteRunner runners[] = new PoliteRunner[NUMRUNNERS]; for (int i = 0; i < NUMRUNNERS; i++) { runners[i] = new PoliteRunner(i); runners[i].setPriority(2); } for (int i = 0; i < NUMRUNNERS; i++) { runners[i].start(); } } }
요약
- 대부분의 system은 CPU를 하나만 가지고 있기 때무에, 여러개의 thread는 CPU를 공유하며 사용한다. 그렇게 하기위해서는 scheduling을 하는데, Java runtime system은 간단하고 강력한 fixed priority scheduling을 사용한다.
- 각각의 Java thread는 숫자로된 우선순위를 가지는데, MIN_PRIORITY와 MAX_PRIORITY중간의 값을 가진다. 여러개의 실행 가능한 thread가 실행하기를 준비하고 있을때, 우선순위가 높은 therad가 선택되어 실행 된다. 만약, 어떠한 이유로 실행을 중단하게 될때만, 우선순위가 낮은 thread가 실행된다.
- CPU의 scheduling은 preemptive이다. 만약 우선순위가 높은 thread가 실행하기를 기다리며 준비하고 있다면, 우선순위가 높은 thread는 즉시 schedule된다.
- Java runtime system에서 현재 실행 중인 thread와 우선순위가 같은 thread가 실행을 준비하고 있다면 nonpreempive할 수 있다. 즉, Java runtime은 time-slice 하지 않는다. 그러나, system에서는 Java의 Thread class를 수행함으로써 time slicing을 제공할 수 있다. Time-slicing에 의존하는 program을 제작하지 않는것이 좋다.
- 또한, 언제 어디서나 yield() method를 호출하여 실행을 포기할 수가 있다. Thread는 우선순위가 같은 thread에게 CPU를 양보할 수 있고, 우선순위가 낮은 thread는 무시한다.
2.6.3.4 Daemon Thread.
모든 Java thread는 daemon thread가 될 수 있다. Daemon thread는 같은 process안에서 다른 thread나 object를 service해주는 daemon thread이다. 예를 들어서, HotJava browser는 image를 필요로하는 thread나 object에게 filesystem이나 network으로부터 image를 읽어주는 기능을 하는 "Background Image Reader"라는 daemon thread를 가지고 있다.
Daemon thread는 전형적으로 같은 application에 있는 object를 위해서 service를 제공하는 독립적인 thread이다. Daemon thread를 위한 run() method는 전형적으로 service의 요청을 기다리며 무한 loop를 수행한다.
Process에 오직 daemon thread 혼자만 남아있을때, interpreter는 exit한다. 즉 이때는, service를 요청하는 thread가 더이상 존재하지 않고, daemon thread만이 존재할때를 말한다.
true를 parameter로 하여 setDaemon() method를 호출하면, thread는 daemon thread가 된다. isDaemon() method를 사용하여 thread가 daemon thread인지 알아볼 수 있다.
2.6.3.5 Thread Group.
2.6.4 Multithreaded Progmrams.
가끔씩, thread는 data를 공유할 때가 있다. 예를 들어서, 어떤 thread가 file에 data를 쓰고, 같은 시간에 다른 thread는 같은 file에서 data를 읽어온다고 가정해보자. 이때, 정보를 공유하는데 올바른 결과를 얻기 위해서 thread는 동기화(synchronize)되어야 한다.
두개의 thread가 서로 어떤한 조건을 서로에게 요구하며 기다리면서 반면에 조건이 만족하지 않기 때문에 아무일도 하지 않는데, 다시 이것은 서로의 조건을 충족시켜주지 못해주는 상황이 발생하게 되는데, 이러한 상태를 deadlock이라고 하며, 다음에 만찬중인 철학자(dining philosopher)를 예로들어 설명하며, deadlock을 피하는 방법을 보이겠다.
Java monitor는 re-entrant이다.
2.6.4.1 동기화.
동시에(concurrently) 실행되는 thread는 data를 공유하기 위해서 서로 다른 thread의 상태(state)와 행동(activities)을 고려햐야하는 재미있는 상황 이 자주 발행한다. 일반적으로 생산자/소비자(Procuder/Consumer)의 예를 많이들어 설명하듯이 이곳에서도 그러한 방식으로 설명하겠다.
예를 들어서, Java application에서 file에 data를 write하는 thread(producer)가 있고, file에서 data를 읽어 오는 thread(consumer)가 있다고 가정하자. 또는, keyboard에서 key 입력을 기다릴때 producer 가 key event를 queue에 쓰고, consumer는 같은 queue에서 event를 읽어오는 경우가 있다.
이 모든 예는 common resource를 공유하는 concurrent thread이다. 그리고, 이 thread는 common resource를 공유하기 때문에, 어떠한 방법으로든 동기화 되어야 한다.
Producer/Consumer Example
다음은 0~9가지 세면서 "CubbyHole" object에 값을 저장하고, 값을 출력하는 Producer이다. 그리고, 0~100 millisecond동안 불규칙하게 sleep한다.
class Producer extends Thread { private CubbyHole cubbyhole; private int number; public Producer(CubbyHole c, int number) { cubbyhole = c; this.number = number; } public void run() { for (int i = 0; i < 10; i++) { cubbyhole.put(i); System.out.println("Producer #" + this.number + " put: " + i); try { sleep((int)(Math.random() * 100)); } catch (InterruptedException e) { } } } }
다음은 CubbyHole에서 모든 integer를 소모하는 Consumer이다.
class Consumer extends Thread { private CubbyHole cubbyhole; private int number; public Consumer(CubbyHole c, int number) { cubbyhole = c; this.number = number; } public void run() { int value = 0; for (int i = 0; i < 10; i++) { value = cubbyhole.get(); System.out.println("Consumer #" + this.number + " got: " + value); } } }
위의 예에서, Producer와 Consumer는 "CubbyHole" object를 통해서 data를 공유한다. 그러나, 위의 예에서 Producer가 값을 쓰고, Consumer가 값을 하나 읽는 부분을 명시한 부분은, CubbyHole에서 get(), put() method를 사용하여 내부적으로 표현되었다. 그러나, 주위할점은 이 두 thread의 동기화에 관한 부분은 없다.
그러면 발생되는 첫번째 문제를 고려해본다. Producer가 Consumer보다 빨라서 Producer가 2개를 처리했는데, Cunsumer는 하나밖에 처리하지 못해서, Consumer는 하나를 건너뛰게 되었다. 따라서 다음과 같은 결과가 나올 수 있다.
. . . Consumer #1 got: 3 Producer #1 put: 4 Producer #1 put: 5 Consumer #1 got: 5 . . .
또다른 문제점이 발생할 수 있다. Consumer가 Producer보다 빨라서 Cunsumer는 같은 값을 두번 읽어갔다. 따라서 다음과 같은 결과가 나올 수 있다.
. . . Producer #1 put: 4 Consumer #1 got: 4 Consumer #1 got: 4 Producer #1 put: 5 . . .
위의 두경우는 모두 잘못되었다. 우리는 Producer가 정확히 하나를 write하면 Consumer는 정확히 하나를 읽어가기를 바란다. 위와 같이, 하나의 object를 사용하여 여러개의 thread가 동시에 비동기적(asynchronously)으로 실행되어 잘못된 결과를 초래하는것을 "race condition"이라고 한다.
위와 Producer/Consumer예제에서 race condition을 방지하기 위해, Producer와 Consumer는 동기화(synchronized)가 되어야 한다. 또한, CubbyHole과 같은 object는 동시에 두개의 thread에 의해서 동기화 되면서 접근되는데, 이러한 object를 "condition variable"이라고 한다. Java language는 'monitors'를 이용하여 condition variable을 이용하는 thread를 동기화 시킨다.
Monitors
Java language와 runtime system은 약자로 C.A.R기능을 해주는 'monitors'를 이용하여 thread synchronization을 제공한다. 일반적으로, monitor는 특별한 data item(condition variable)과, data에 lock을 거는 function과 관련이 있다. Thread가 어떤 data item을 위한 monitor를 갖고 있을때, 다른 thread는 data를 처리하는 부분은 lock되어서 접근하지 못한다.
같은 data를 접근하는 concurrent thread를 서로 분리 하기 위한 program내부의 code segment를 임계영역(critical section)이라고 한다. Java language에서는, 'synchronized' keyword를 사용하여 critical section을 선언할 수 있다.
일반적으로, Java program의 critical section은 method이다. 동기화 할수 있도록 조그만 code segment를 시하는것이다. 그러나, 이것은 객체지향 기법에 어긋나는 것이고, code를 debugging하고 유지/보수 하는데 어려움을 준다. Java programming 전체적인 형식에 따라서, 'synchronized'는 method level에서만 사용하는것이 좋다.
Java language에서 유일한 monitor기법은 'synchronized'를 사용한 방법이다. 다음은 위의 Producer/Consumer가 상요한 CubbyHole class이다. put() method는 CubbyHole의 값을 바꾸고, get() method는 값을 읽어온다.
class CubbyHole { private int seq; private boolean available = false; public synchronized int get() { while (available == false) { try { wait(); } catch (InterruptedException e) { } } available = false; return seq; } public synchronized void put(int value) { seq = value; available = true; notify(); } }
또한, CubbyHole은 두개의 private variable을 가지고 있다.; seq는 값을 가지고 있고, 'available'은 CubbyHole의 값이 회수될 수 있는지를 나타내는 boolean 값을 가지고 있다. available이 false이면 Producer는 CubbyHole에 새로운 값을 넣어야 하고, 이때 Consummer는 아직 그 값을 소모 할 수 없다. Consumer는 오직 available이 true일때 값을 소모 할 수 있다.
CubbyHole은 synchronized method를 가지고 있기 때문에, Java language는 Producer와, Consumer의 예를 포함한, 각각의 CubbyHole instance에 오직 하나의 monitor만을 부여한다. Producer가 CubbyHole의 pub() method를 호출 할 때마다, Producer는 CubbyHole로 부터 monitor를 얻어내야만 한다.
public synchronized void put(int value) { // monitor has been acquired by the Producer seq = value; available = true; notify(); // monitor is released by the Producer }
put() method가 return되면, Produer는 monitor를 내놓음으로써 CubbyHole을 lock을 풀어준다.
마찬가지로, Consumer가 CubbyHole의 get() method를 호출 할때마다, Consumer는 CubbyHole로 부터 monitor를 얻어내야만 한다.
public synchronized int get() { // monitor has been acquired by the Consumer while (available == false) { try { wait(); } catch (InterruptedException e) { } } available = false; return seq; // monitor is released by the Consumer }
Java runtime system은 monitor를 얻고, 내놓는것을 자동으로 처리한다. 이것은 수행중의 thread간의 race condition을 방지하며, 정확한 data를 보장해준다.
그러나, CybbyHole의 put(), get() method에 따른 잠재적인 문제점이 있다. get() method를 실행시킬때, 만약 CubbyHole의 값이 없다면, Consumer는 Producer가 새로운 값을 써넣을 때까지 기다릴것이다.여기에서 문제가 생기는데, 만약 CubbyHole이 monitor를 쥐고 있다면, Producer는 어떻게 새로운 값을 써넣을 수 있을것인가?
Java language designer는 이것 역시 생각하고 있다. Thread가 wait() method를 실행 시키면, monitor는 자동으로 내놓게 된다. 그리고, wait() method를 exit하면, 다시 monitor를 얻게 된다. 이것은 Producer가 monitor를 얻을 수 있는 기회를 제공하고 CubbyHole에 새로운 값을 넣을수 있도록 한다.
The Main Program
다음은 CubbyHole object, Producer, Consumer를 생성하고, Producer와 Cosumer를 실행 시키는 간단한 Java application이다.
class ProducerConsumerTest { public static void main(String args[]) { CubbyHole c = new CubbyHole(); Producer p1 = new Producer(c, 1); Consumer c1 = new Consumer(c, 1); p1.start(); c1.start(); } }
The Output
다음은 ProducerConsumerTest의 출력이다.
Producer #1 put: 0 Consumer #1 got: 0 Producer #1 put: 1 Consumer #1 got: 1 Producer #1 put: 2 Consumer #1 got: 2 Producer #1 put: 3 Consumer #1 got: 3 Producer #1 put: 4 Consumer #1 got: 4 Producer #1 put: 5 Consumer #1 got: 5 Producer #1 put: 6 Consumer #1 got: 6 Producer #1 put: 7 Consumer #1 got: 7 Producer #1 put: 8 Consumer #1 got: 8 Producer #1 put: 9 Consumer #1 got: 9
위의 CubbyHole에서 굵은 글씨를 제거하고 다시 compile하고, 실행 시켜보면 Producer와 Consumer thread의 동기화에 전혀 신경을 쓰지 않기 때문에, 당연히 오류가 생긴다.
2.6.4.2 Deadlock.
2.6.4.3 Re-entrant Monitors.
2.6.5 요약.
위에서는 여러가지 많은 thread의 상요법과, 다양한 내용에 대해서 알아보았다. Thread는 Java develpment environment에서 제공되기 때문에, 일반적인 모습과는 혼돈이 있을 수 있다. 다음은 Java therad를 좀더 잘 알수 있도록, Java에서 제공되는 다양한 class, method와 언어적인 측면에 대해서 요약해보겠다.
package Support of Thread
java.lang.Thread
Java development environment에서, thread는 java.lang의 Thread class에서 파생된 object이다. Thread class는 Java thread로 수행된다. 자신만의 Runnable interface나 thread를 제공하기 위해서 Thread class의 subclass를 제작할 수 있다.
java.lang.Runnable
Java language library는 또한 어떠한 object건 간에 run() method를 사용할 수 있도록 Thread를 위한 Runnable interface를 제공한다.
java.lang.Object
가장 기본이 되는 Object class는 condition variable에 따른 wait(), notify(), notifyAll() 와같은 synchronized method를 제공한다.
java.lang.ThreadGroup
모든 thread는 일반적으로 관련된 thread들 끼리, thread group을 형성한다. java.lang package에 있는 ThreadGroup class는 이러한 thread group을 형성할 수 있도록 처리해준다.
java.lang.Death
Thread는 일반적으로 ThreadDeath object에 의해서 kill 된다. 이렇게 ThradDeath가 catch되어서 실질적으로 죽을 수 있는 thread는 드물다.
2.7 오류 처리와 예외 상황.
2.8 입출력 Stream.
댓글