어노테이션은 Java5에 추가된 기능
어노테이션은 클래스나 메소드 위에 붙는다. @(at)기호로 이름이 시작한다.
어노테이션을 클래스나 메타코드에 붙인 후, 클래스가 컴파일되거나 실행될 때 어노테이션의 유무나 어노테이션에 설정된 값을 통하여 클래스가 좀 더 다르게 실행되게 할 수 있다. 이런 이유로 어노테이션을 일종의 설정파일처럼 설명하는 경우도 있다.
어노테이션은 자바가 기본으로 제공해주는 것도 있고, 사용자가 직접 만들 수도 있다. 사용자가 직접 작성하는 어노테이션을 Custom 어노테이션이라고 말한다.
커스텀 어노테이션을 이용하는 방법
패키지 익스플로러에서 [new - Annotation]을 이용하여 Count100이라는 어노테이션 생성
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface Count100 {
}
"hello"를 출력하는 hello()메소드를 가지는 MyHello라는 클래스를 작성
public class MyHello {
@Count100
public void hello(){
System.out.println("hello");
}
}
MyHello클래스를 이용하는 MyHelloExam클래스를 작성
import java.lang.reflect.Method;
public class MyHelloExam {
public static void main(String[] args) {
MyHello hello = new MyHello();
try{
Method method = hello.getClass().getDeclaredMethod("hello");
if(method.isAnnotationPresent(Count100.class)){
for(int i = 0; i < 100; i++){
hello.hello();
}
}else{
hello.hello();
}
}catch(Exception ex){
ex.printStackTrace();
}
}
}
쓰레드란 동시에 여러가지 작업을 동시에 수행할 수 있게하는 것
프로세스란 현재 실행되고 있는 프로그램을 말한다.
자바 프로그램은 JVM에 의해 실행된다. 이 JVM도 프로그램중에 하나이다.
운영체제 입장으로 보면 자바도 하나의 프로세스로 실행을 하는 것이다.
워드프로세서가 하나의 프로세스라면, 하나의 프로세스 안에서도 여러개의 흐름이 동작할 수 있다. 이것은 Thread라고 말을 한다.
자바 프로그램이 여러개의 작업을 동시에 하게 만들고 싶다면 Thread를 공부해야 한다.
자바에서 Thread를 만드는 방법은 크게 Thread 클래스를 상속받는 방법과 Runnable 인터페이스를 구현하는 방법이 있다.
java.lang.Thread클래스를 상속받는다. 그리고 Thread가 가지고 있는 run()메소드를 오버라이딩한다.
10번 반복하면서 str을 찍기
public class MyThread1 extends Thread {
String str;
public MyThread1(String str){
this.str = str;
}
public void run(){
for(int i = 0; i < 10; i ++){
System.out.print(str);
try {
//컴퓨터가 너무 빠르기 때문에 수행결과를 잘 확인 할 수 없어서 Thread.sleep() 메서드를 이용해서 조금씩
//쉬었다가 출력할 수 있게한다.
Thread.sleep((int)(Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Thread 클래스를 상속받은 MyThread1을 사용하는 클래스
public class ThreadExam1 {
public static void main(String[] args) {
// MyThread인스턴스를 2개 만듭니다.
MyThread1 t1 = new MyThread1("*");
MyThread1 t2 = new MyThread1("-");
t1.start();
t2.start();
System.out.print("!!!!!");
}
}
자바에서 Thread를 만드는 방법은 크게 Thread 클래스를 상속받는 방법과 Runnable 인터페이스를 구현하는 방법이 있다.
Runnable을 구현하는 이유: 자바는 단일 상속만 지원하기 때문에, Thread 클래스를 상속받지 않고도 Thread를 만들 수 있도록
Runable 인터페이스가 가지고 있는 run()메소드를 구현한다.
public class MyThread2 implements Runnable {
String str;
public MyThread2(String str){
this.str = str;
}
public void run(){
for(int i = 0; i < 10; i ++){
System.out.print(str);
try {
Thread.sleep((int)(Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Runable 인터페이스를 구현한 MyThread2를 사용하는 방법
public class ThreadExam2 {
public static void main(String[] args) {
MyThread2 r1 = new MyThread2("*");
MyThread2 r2 = new MyThread2("-");
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
System.out.print("!!!!!");
}
}
하나의 객체를 여러개의 Thread가 사용한다는 것을 의미
MusicBox라는 클래스가 있다고 가정하자. 해당 클래스는 3개의 메소드를 가지고 있다. 각각의 메소드는 1초 이하의 시간동안 10번 반복하면서, 어떤 음악을 출력한다. 이러한 MusicBox를 사용하는 MusicPlayer를 3명 만들어 보도록 한다.
MusicPlayer 3명은 하나의 MusicBox를 사용할 것이다. 이때 어떤 일이 발생하는지 살펴보도록 하자.
공유객체 MusicBox
public class MusicBox {
//신나는 음악!!! 이란 메시지가 1초이하로 쉬면서 10번 반복출력
public void playMusicA(){
for(int i = 0; i < 10; i ++){
System.out.println("신나는 음악!!!");
try {
Thread.sleep((int)(Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
} // for
} //playMusicA
//슬픈 음악!!!이란 메시지가 1초이하로 쉬면서 10번 반복출력
public void playMusicB(){
for(int i = 0; i < 10; i ++){
System.out.println("슬픈 음악!!!");
try {
Thread.sleep((int)(Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
} // for
} //playMusicB
//카페 음악!!! 이란 메시지가 1초이하로 쉬면서 10번 반복출력
public void playMusicC(){
for(int i = 0; i < 10; i ++){
System.out.println("카페 음악!!!");
try {
Thread.sleep((int)(Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
} // for
} //playMusicC
}
MusicBox를 가지는 Thread객체 MusicPlayer
public class MusicPlayer extends Thread{
int type;
MusicBox musicBox;
// 생성자로 부터 musicBox와 정수를 하나 받아들여서 필드를 초기화
public MusicPlayer(int type, MusicBox musicBox){
this.type = type;
this.musicBox = musicBox;
}
// type이 무엇이냐에 따라서 musicBox가 가지고 있는 메소드가 다르게 호출
public void run(){
switch(type){
case 1 : musicBox.playMusicA(); break;
case 2 : musicBox.playMusicB(); break;
case 3 : musicBox.playMusicC(); break;
}
}
}
MusicBox와 MusicPlayer를 이용하는 MusicBoxExam1 클래스
public class MusicBoxExam1 {
public static void main(String[] args) {
// MusicBox 인스턴스
MusicBox box = new MusicBox();
MusicPlayer kim = new MusicPlayer(1, box);
MusicPlayer lee = new MusicPlayer(2, box);
MusicPlayer kang = new MusicPlayer(3, box);
// MusicPlayer쓰레드를 실행합니다.
kim.start();
lee.start();
kang.start();
}
}
공유객체가 가진 메소드를 동시에 호출되지 않도록 하는 방법
public synchronized void playMusicA(){
for(int i = 0; i < 10; i ++){
System.out.println("신나는 음악!!!");
try {
Thread.sleep((int)(Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
} // for
} //playMusicA
public void playMusicB(){
for(int i = 0; i < 10; i ++){
synchronized(this){
System.out.println("슬픈 음악!!!");
}
try {
Thread.sleep((int)(Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
} // for
} //playMusicB
쓰레드가 3개가 있다면 JVM은 시간을 잘게 쪼갠 후에 한번은 쓰레드1을, 한번은 쓰레드 2를, 한번은 쓰레드 3을 실행한다. 이것이 빠르게 일어나다 보니 쓰레드가 모두 동작하는 것처럼 보이는 것이다.
쓰레드는 실행 가능 상태인 Runnable과 실행 상태인 Running 상태로 나뉜다.
실행되는 쓰레드 안에서 Thread.sleep()이나 Object가 가지고 있는 wait()메소드가 호출이 되면 쓰레드는 블록상태가 된다.
= Thread.sleep()은 특정시간이 지나면 자신 스스로 블록상태에서 빠져나와 Runnable이나 Running상태가 된다.
Object가 가지고 있는 wait()메소드는 다른 쓰레드가 notify()나 notifyAll()메소드를 호출하기 전에는 블록상태에서 해제되지 않는다.
wait()메소드는 호출이 되면 모니터링 락을 놓게 된다. 그래서 대기중인 다른 메소드가 실행한다.
쓰레드의 run메소드가 종료되면, 쓰레드는 종료된다. 즉 Dead상태가 된다.
Thread의 yield메소드가 호출되면 해당 쓰레드는 다른 쓰레드에게 자원을 양보하게 된다.
Thread가 가지고 있는 join메소드를 호출하게 되면 해당 쓰레드가 종료될 때까지 대기하게 된다.
join()메소드는 쓰레드가 멈출 때까지 기다리게 한다.
0.5초씩 쉬면서 숫자를 출력하는 MyThread5를 작성해보자.
public class MyThread5 extends Thread{
public void run(){
for(int i = 0; i < 5; i++){
System.out.println("MyThread5 : "+ i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} // run
}
해당 쓰레드를 실행하고, 해당쓰레드가 종료될 때까지 기다린 후, 내용을 출력하는 JoinExam 클래스
public class JoinExam {
public static void main(String[] args) {
MyThread5 thread = new MyThread5();
// Thread 시작
thread.start();
System.out.println("Thread가 종료될때까지 기다립니다.");
try {
// 해당 쓰레드가 멈출때까지 멈춤
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread가 종료되었습니다.");
}
}
실행결과
Thread가 종료될때까지 기다립니다.
MyThread5 : 0
MyThread5 : 1
MyThread5 : 2
MyThread5 : 3
MyThread5 : 4
Thread가 종료되었습니다.
wait와 notify는 동기화 된 블록안에서 사용해야 한다. wait를 만나게 되면 해당 쓰레드는 해당 객체의 모니터링 락에 대한 권한을 가지고 있다면 모니터링 락의 권한을 놓고 대기한다.
Thread를 상속받는 ThreadB클래스를 작성
public class ThreadB extends Thread{
// 해당 쓰레드가 실행되면 자기 자신의 모니터링 락을 획득
// 5번 반복하면서 0.5초씩 쉬면서 total에 값을 누적
// 그후에 notify()메소드를 호출하여 wiat하고 있는 쓰레드를 깨움
int total;
@Override
public void run(){
synchronized(this){
for(int i=0; i<5 ; i++){
System.out.println(i + "를 더합니다.");
total += i;
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
notify();
}
}
}
이번에는 ThreadB를 사용하며 wait하는 클래스 작성
public class ThreadA {
public static void main(String[] args){
// 앞에서 만든 쓰레드 B를 만든 후 start
// 해당 쓰레드가 실행되면, 해당 쓰레드는 run메소드 안에서 자신의 모니터링 락을 획득
ThreadB b = new ThreadB();
b.start();
// b에 대하여 동기화 블럭을 설정
// 만약 main쓰레드가 아래의 블록을 위의 Thread보다 먼저 실행되었다면 wait를 하게 되면서 모니터링 락을 놓고 대기
synchronized(b){
try{
// b.wait()메소드를 호출.
// 메인쓰레드는 정지
// ThreadB가 5번 값을 더한 후 notify를 호출하게 되면 wait에서 깨어남
System.out.println("b가 완료될때까지 기다립니다.");
b.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
//깨어난 후 결과를 출력
System.out.println("Total is: " + b.total);
}
}
}
실행결과
b가 완료될때까지 기다립니다.
0를 더합니다.
1를 더합니다.
2를 더합니다.
3를 더합니다.
4를 더합니다.
Total is: 10
데몬(Daemon)이란 보통 리눅스와 같은 유닉스 계열의 운영체제에서 백그라운드로 동작하는 프로그램을 말한다.
데몬쓰레드를 만드는 방법은 쓰레드에 데몬 설정을 하면 된다.
데몬쓰레드는 일반 쓰레드(main 등)가 모두 종료되면 강제적으로 종료되는 특징을 가지고 있다.
// Runnable을 구현하는 DaemonThread클래스를 작성
public class DaemonThread implements Runnable {
// 무한루프안에서 0.5초씩 쉬면서 데몬쓰레드가 실행중입니다를 출력하도록 run()메소드를 작성
@Override
public void run() {
while (true) {
System.out.println("데몬 쓰레드가 실행중입니다.");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
break; //Exception발생시 while 문 빠찌도록
}
}
}
public static void main(String[] args) {
// Runnable을 구현하는 DaemonThread를 실행하기 위하여 Thread 생성
Thread th = new Thread(new DaemonThread());
// 데몬쓰레드로 설정
th.setDaemon(true);
// 쓰레드를 실행
th.start();
// 메인 쓰레드가 1초뒤에 종료되도록 설정.
// 데몬쓰레드는 다른 쓰레드가 모두 종료되면 자동종료.
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("메인 쓰레드가 종료됩니다. ");
}
}
람다식은 다른말로 익명 메소드라고도 한다.
인터페이스 중에서 메소드를 하나만 가지고 있는 인터페이스를 함수형 인터페이스라고 한다.
쓰레드를 만들때 사용하는 Runnable 인터페이스의 경우 run()메소드를 하나만 가지고 있다.
Runnable을 이용하여 쓰레드를 만드는 방법
public class LambdaExam1 {
public static void main(String[] args) {
new Thread(new Runnable(){public void run(){
for(int i = 0; i < 10; i++){
System.out.println("hello");
}
}}).start();
}
}
쓰레드가 실행되면 쓰레드 생성자 안에 넣은 run()메소드가 실행된다.
자바는 메소드만 매개변수로 전달할 방법이 없다. 인스턴스만 전달 할 수 있다.
그렇기 때문에 run()메소드를 가지고 있는 Runnable객체를 만들어서 전달한다.
메소드만 전달할 수 있다면, 좀더 편리하게 프로그래밍할 수 있을텐데,자바는 메소드만 전달할 수 있는 방법은 없었기 기때문에 매번 객체를 생성해서 매개변수로 전달해야 했다. 이런 부분을 해결한 것이 람다표현식이다.
람다식을 이용해서 수정한 코드
public class LambdaExam1 {
public static void main(String[] args) {
new Thread(()->{
for(int i = 0; i < 10; i++){
System.out.println("hello");
}
}).start();
}
}
()->{ ..... } 부분이 람다식, 다른말로 익명 메소드
JVM은 Thread생성자를 보고 ()->{} 이 무엇인지 대상을 추론한다.
Thread생성자 api를 보면 Runnable인터페이스를 받아들이는 것을 알 수 있다.
(매개변수목록)->{실행문}
2개의 값을 비교하여 어떤 값이 더 큰지 구하는 compareTo()라는 메소드를 가지고 있는 Compare 인터페이스
public interface Compare{
public int compareTo(int value1, int value2);
}
Compare 인터페이스를 이용하는 클래스
public class CompareExam {
public static void exec(Compara compara){
int k = 10;
int m = 20;
int value = compara.compareTo(k, m);
System.out.println(value);
}
public static void main(String[] args) {
exec((i, j)->{
return i - j;
}); }
}
자바는 메소드만 인자로 전달하려면 반드시 객체를 만들어서 전달해야 했다. Java8에 람다식이 생기면서, 마치 함수만 전달하는 것처럼 간편하게 문법을 사용할 수 있게 된다.
자바는 객체 지향 프로그래밍 : 기능을 수행하긴 위해서는 객체를 만들고 그 객체 내부에 멤버 변수를 선언하고 기능을 수행하는 메서드를 구현
자바 8부터 함수형 프로그래밍 방식을 지원하고 이를 람다식이라 함
함수의 구현과 호출만으로 프로그래밍이 수행되는 방식
자료의 대상과 관계없이 동일한 연산을 수행
배열, 컬렉션을 대상으로 연산을 수행 함
일관성 있는 연산으로 자료의 처리를 쉽고 간단하게 함
자료 처리에 대한 추상화가 구현되었다고 함
한번 생성하고 사용한 스트림은 재사용 할 수 없음
자료에 대한 스트림을 생성하여 연산을 수행하면 스트림은 소모됨
다른 연산을 수행하기 위해서는 스트림을 다시 생성해야 함
스트림 연산은 기존 자료를 변경하지 않음
자료에 대한 스트림을 생성하면 스트림이 사용하는 메모리 공간은 별도로 생성되므로 연산이 수행돼도 기존 자료에 대한 변경은 발생하지 않음
스트림 연산은 중간 연산과 최종 연산으로 구분 됨
스트림에 대해 중간 연산은 여러 개의 연산이 적용될 수 있지만 최종 연산은 마지막에 한 번만 적용됨
최종연산이 호출되어야 중간 연산에 대한 수행이 이루어 지고 그 결과가 만들어짐
따라서 중간 연산에 대한 결과를 연산 중에 알수 없음
이를 '지연 연산'이라 함
스트림 생성하고 사용하기
정수 배열에 스트림 생성하여 연산을 수행 하는 예
public class IntArrayTest {
public static void main(String[] args) {
int[] arr = {1,2,3,4,5};
int sumVal = Arrays.stream(arr).sum();
long count = Arrays.stream(arr).count();
System.out.println(sumVal);
System.out.println(count);
}
}
중간 연산의 예 - filter(), map(), sorted() 등
조건에 맞는 요소를 추출(filter)하거나 요소를 변환 함(map)
최종 연산이 호출될 때 중간 연산이 수행되고 결과가 생성 됨
문자열 리스트에서 문자열의 길이가 5 이상인 요소만 출력하기
sList.stream().filter(s->s.length() >= 5).forEach(s->System.out.println(s));
filter()는 중간 연산이고, forEach()는 최종 연산임
customerList.stream().map(c->c.getName()).forEach(s->System.out.println(s));
map()은 중간 연산이고, forEach()는 최종 연산임
중간 연산과 최종 연산에 대한 구현은 람다식을 활용함
최종 연산의 예 - forEach(), count(), sum() 등
스트림이 관리하는 자료를 하나씩 소모해가며 연산이 수행 됨
최종 연산 후에 스트림은 더 이상 다른 연산을 적용할 수 없음
forEach() : 요소를 하나씩 꺼내 옴
count() : 요소의 개수
sum() : 요소들의 합
ArrayList 객체에 스트림 생성하고 사용하기
ArrayList에 문자열 자료(이름)을 넣고 이에 대한 여러 연산을 수행해보기
public class ArrayListStreamTest {
public static void main(String[] args) {
List<String> sList = new ArrayList<String>();
sList.add("Tomas");
sList.add("Edward");
sList.add("Jack");
Stream<String> stream = sList.stream();
stream.forEach(s->System.out.print(s + " "));
System.out.println();
sList.stream().sorted().forEach(s->System.out.print(s+ " "));
sList.stream().map(s->s.length()).forEach(n->System.out.println(n));
sList.stream().filter(s->s.length() >= 5).forEach(s->System.out.println(s));
}
}
새로운 연산을 수행하기 위해서는 기존의 스트림은 재사용할 수 없고 stream()메서드를 호출하여 스트림을 다시 생성해야 함
public class IntArrayStreamTest {
public static void main(String[] args) {
int[] arr = {1,2,3,4,5};
Arrays.stream(arr).forEach(n->System.out.print(n + "\t"));
System.out.println();
int sum = Arrays.stream(arr).sum();
System.out.println("sum: " + sum);
List<Integer> list = new ArrayList<Integer>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
int sum2 = list.stream().mapToInt(n->n.intValue()).sum();
System.out.println(sum2);
}
}
정의된 연산이 아닌 프로그래머가 직접 구현한 연산을 적용
T reduce(T identify, BinaryOperator<T> accumulator)
최종 연산으로 스트림의 요소를 소모하며 연산을 수행
배열의 모든 요소의 합을 구하는 reduce() 연산 구현 예
Arrays.stream(arr).reduce(0, (a,b)->a+b));
reduce() 메서드의 두 번째 요소로 전달되는 람다식에 따라 다양한 기능을 수행 할 수 있음
람다식을 직접 구현하거나 람다식이 긴 경우 BinaryOperator를 구현한 클래스를 사용 함
class CompareString implements BinaryOperator<String>{
@Override
public String apply(String s1, String s2) {
if (s1.getBytes().length >= s2.getBytes().length) return s1;
else return s2;
}
}
public class ReduceTest {
public static void main(String[] args) {
String[] greetings = {"안녕하세요~~~", "hello", "Good morning", "반갑습니다^^"};
System.out.println(Arrays.stream(greetings).reduce("", (s1, s2)->
{if (s1.getBytes().length >= s2.getBytes().length)
return s1;
else return s2;}));
String str = Arrays.stream(greetings).reduce(new CompareString()).get(); //BinaryOperator를 구현한 클래스 이용
System.out.println(str);
}
}