Java 이론 정리(중급)

김범수·2023년 7월 13일
0

문법

목록 보기
3/3
post-thumbnail

시작하며

기초에 이어서 중급도 정리하고자 한다.
강의를 참고하여 작성할 계획이고, 새롭게 알게된 개념이나 알아야 하는 것들은 후에 다시 메모할 계획이다.

Object Class

Object 클래스는 모든 클래스의 최상위 클래스다. 아무것도 상속받지 않으면 모두 이 클래스를 상속받는다.

Method

가장 많이 사용되는 Method는 다음과 같다.
해당 Method들은 반드시 Override를 한 뒤 사용해야 한다.
1. equals: 객체가 가진 값을 비교할 때 사용
2. toString: 객체가 가진 값을 문자열로 반환
3. hashCode: 객체의 Hash 코드를 구해 값을 반환, 특히 자료구조에서 많이 사용된다.

예시

public class Student{
  String name;
  String number;
  int birthYear;
  
  Student(String name, String number, int birthYear){
    this.name = name;
    this.number = number;
    this.birthYear = birthYear;
  
  public static void main(String[] args){
    Student s1 = new Student("홍길동", "1234", 1995);
    Student s2 = new Student("홍길동", "1234", 1995);
    
    if(!s1.equals(s2)) System.out.println("s1 != s2");
   
    System.out.println(s1.hashCode());
    System.out.println(s2.hashCode());
}

이런 hashCode()나 equals()를 사용할 때에는 Override를 통해 새롭게 구현할 수 있다.

@Override
public int hashCode(){
  final int prime = 31;
  int result = 1;
  result = prime * result + ((number == null) ? 0 | number.hashCode());
  return result;
}

@Override
public boolean equals(Object obj){
  if (this == obj) return true;
  if (obj == null) return false;
  if (getClass() != obj.getClass()) return false;
  Student other = (Student) obj;
  if (number == null) {
    if (other.number != null) return false;
  } else if (!number.equals(other.number)) return false;

  return true;
}

// 위 코드를 Override한 뒤 출력하면 결과가 달라진다.

@Override
public String toString(){
  return "Student [name=" + name + ", number=" + number + ", birthYear=" + brithYear + "]";
}

// 이때 toString()을 만들었다면, 다음 부분이 달라진다.
System.out.println(s1.toString());
System.out.println(s1);
// 두 개의 결과가 같은 값이 나옴. 이유는 내부적으로 toString()을 통해 값을 출력하기 때문이다.
// toString()을 만들지 않았다면, 객체 주소를 나타낸다.

java.lang Package

java.lang 패키지는 클래스를 import를 하지 않고도 사용할 수 있다.
java.lang패키지에는 기본형 타입을 객체로 변환시킬 때 사용하는 Wrapper클래스가 있다.

Wrapper

Wrapper class는 실제로 존재하는 것은 아니고 기본형 Type을 Boolean, Byte, Short, Integer, Long, Float, Double 클래스로 나타낼 수 있게 하는것이다.

Object

Object도 java.lang에 존재한다.

String

  • String
  • StringBuffer
  • StringBuilder
    문자열과 관련돤 3개의 Class도 모두 java.lang 패키지에 있다.

other Class

  • System Class
  • Math Class

이외에도 Thread와 관련된 중요 클래스와 다른 클래스 및 인터페이스가 java.lang에 포함되어 있다.

Auto Boxing

int i = 5;
Integer i2 = new Integer(5);

Integer i3 = 5; // 이렇게 넣으면 기본 데이터 타입 5가 들어가서 옳지 않은 방법이지만, Auto Boxing을 통해 들어가진다.
  • Auto Boxing(오토박싱): Integer i3 = 5;처럼 숫자는 원래 기본형이지만 자동으로 Integer형태로 변환된다.
  • Auto unboxing(오토언박싱): int i5 = i2; Integer 객체 타입의 값을 기본형 int로 자동으로 변환되어 값을 할당한다.

이러한 오토박싱, 오토언박싱은 JAVA 5부터 지원한다. 이 때 내부적으로 Wrapper Class들이 사용된다.

public class WrapperExam {
	public static void main(String[] args) {	
		int i = 5; 
		Integer i2 = new Integer(5);	
		Integer i3 = 5;     //오토박싱
		int i4 = i2.intValue();
		int i5 = i2;       //오토언박싱
	}
}

Integer Class의 Field 및 Method
https://docs.oracle.com/javase/7/docs/api/java/lang/Integer.html

StringBuffer

String은 불변 클래스였지만, StringBuffer는 자기 자신이 변하는 Class다.

StringBuffer sb = new StringBuffer();
sb.append("hello"); // 문자열 추가 메소드
sb.append(" ");
sb.append("world);

String str = sb.toString(); // toString() 메소드를 통해 값을 반환

StringBuffer가 가지고 있는 메소드들은 대부분 자기 자신인 this를 반환한다.

StringBuffer sb2 = new StringBuffer();
StringBuffer sb3 = sb2.append("hello");
if(sb2 == sb3){ // true
	System.out.println("sb2 == sb3");
}

위 결과가 true가 나올 수 있는 것은 자신의 객체를 바꿔나가기 때문이다.
자기 자신의 메소드를 호출하여 자신의 값을 바꿔나가는 것을 메소드체이닝 이라고 한다.

메소드 체인 방식으로 사용할 수도 있다.

String str2 = new StringBuffer().append("hello").append(" ").append("world").toString();
System.out.println(str2);

이런식으로 사용한다면 5줄로 작성했던 코드를 한 줄로 수정할 수 있게된다.

String Class의 문제점

String 클래스는 문자열을 다룰때 사용하는 클래스이며, 불변 클래스다.

String str1 = "Hello World";  // Hello World
String str2 = str1.substring(5); //  World
String str3 = str1 + str2; // Hello World World

이런식으로 한다면 문제가 없어 보인다.
그렇지만 내부적으로는 다음과 같은 동작을 한다.
String str4 = new StringBuffer().append(str1).append(str2).toString();
이런식으로 한다면 str3과 str4가 같은 문자열을 가지게 된다.

이렇게 문자열을 더한다면 내부적으로는 이런 코드가 실행이되며, 이러한 문제로 인해서 문자열을 반복문 안에서 더하는 것은 성능상 문제가 생길 수 있다는 문제점이 있다.

  • 좋지않은 코드 예시
String str5="";
for(int i = 0; i < 100; i++){
  str5 += "*";
}
System.out.println(str5);
  • 개선된 코드 예시
StringBuffer sb = new StringBuffer();
for(int i = 0; i < 100; i++){
  sb.append("*");
}
String str6 = sb.toString();
System.out.println(str6);

위 두 코드는 결과는 똑같지만, 성능상으로 차이가 발생한다.

  • 성능 차이 비교 코드
public class StringBufferPerformanceTest{
    public static void main(String[] args){
        // (1) +연산: 10,000개의 "*"
        //시작시간
        long startTime1 = System.currentTimeMillis();
        String str="";
        for(int i=0;i<10000;i++) str=str+"*";
        
        //종료시간
        long endTime1 = System.currentTimeMillis();
        
        // (2) StringBuffer 이용
        //시작시간
        long startTime2 = System.currentTimeMillis();
        StringBuffer sb = new StringBuffer();
        for(int i=0;i<10000;i++) sb.append("*");
        
        //종료시간
        long endTime2 = System.currentTimeMillis();
        
        // 방법(1), 방법(2) 비교
        long duration1 = endTime1-startTime1;
        long duration2 = endTime2-startTime2;
        
        System.out.println("String +연산: " + duration1);
        System.out.println("StringBuffer append(): " + duration2);
    }
}

Math

Math클래스는 이름에서 알 수 있는 수학 계산과 관련된 Class다.
절대값(abs), cos, sin, tan, random 등을 구할 수 있다.
해당 Math 클래스는 생성자가 private으로 되어 있어, new 연산자를 통한 객체를 생성할 수 없다.
객체를 생성할 수 없어도 모든 메소드와 속성이 static으로 정의되어 있어 객체를 생성하지 않고도 사용할 수 있다.

public class MathExam {
	public static void main(String[] args) {
		int value1 = Math.max(5, 20);
		int value2 = Math.min(5, -5);
		int value3 = Math.abs(-10);
		double value4 = Math.random(); // 0 ~ 1.0(이상, 미만)의 랜덤 값
		double value5 = Math.sqrt(25);  // 제곱근
        
        System.out.println("2의 10승 = " + Math.pow(2, 10));
        System.out.println("16의 1/2승 = : " + Math.pow(16, 0.5));
        System.out.println("log200 = " + Math.log10(200));
	}
}

이외에도 많은 Math Class에 Method가 있다. (아래는 관련 링크)
https://docs.oracle.com/javase/7/docs/api/java/lang/Math.html

java.util Package

유용한 클래스들을 많이 가지고 있는 패키지다.

해당 Documentation에 보면 다음과 같은 것이 있다.

이런 것은 자바 1.2부터 존재해왔다는 의미이다.
Constructors나 Method 등이 있는데 용도에 맞게 사용하면 되고, 만약 deprecated라는 용어가 적혀있으면, 해당 기능은 더 이상 지원하지 않으니 사용하지 않는 것이 좋다는 의미라 한다.

Collection FrameWork

자료구조와 관련 Collection Framework이다.
자료구조: 자료를 저장할 수 있는 구조

가장 기본이 되는 Interface는 Collection이다.

Collection Interface에는 위와 같은 Method 등이 있다.
Collection은 저장된 자료 순서를 기억하지 못하는 특성을 지녔다. 그래서 Iterator라는 Interface를 반환하여, hasNext()와 next() 메소드를 통해 하나씩 값을 접근할 수 있다.

Collection Interface를 상속받는 자료구조로는 Set, List 등이 존재한다.

Map이라는 자료구조도 따로 존재한다. 이는 Collection을 상속받지 않고 Set을 의존하고 있는 형태이다.

각각의 형태를 간략하게 적자면 다음과 같다.

  1. Set
    • Collection인터페이스를 상속받는다.
    • Set인터페이스가 가지고 있는 add메소드는 같은 자료가 있으면 false, 없으면 true를 반환하는 add메소드를 가지고 있다.
  2. List
    • Set인터페이스와 마찬가지로 Collection인터페이스를 상속받고 있다.
    • List는 순서를 기억하고 있기 때문에 0번째 1번째 n번째의 자료를 꺼낼 수 있는 get(int)메소드를 가지고 있다.
  3. Map
    • 저장할 때 put()메소드를 이용하여 key와 value를 함께 저장한다.
    • 원하는 값을 꺼낼때는 key를 매개변수로 받아들이는 get()메소드를 이용하여 값을 꺼낸다.
    • Map에 저장되어 있는 모든 Key들은 중복된 값을 가지면 안된다.
    • Key의 이런 특징 때문에 Map은 자신이 가지고 있는 모든 Key들에 대한 정보를 읽어들일 수 있는 Set을 반환하는 keySet()메소드를 가지고 있다.

Generic

Java5에는 Generic이라는 문법이 사용됨으로써 인스턴스를 만들때 사용하는 타입을 지정하는 문법이 추가됐다.

일반적으로 여러 개의 값을 가지는 Class를 만들 때는 다음과 같이 선언한 뒤 사용할 것이다.

과거

public class Box {
	private Object obj;
	
    public void setObj(Object obj){
		this.obj = obj;
	}
    
	public Object getObj(){
		return obj;
	}
}

public class BoxExam {
	public static void main(String[] args) {
		Box box = new Box();
		box.setObj(new Object());
		Object obj = box.getObj();
		box.setObj("hello");
		String str = (String)box.getObj();
		System.out.println(str);
		box.setObj(1);
		int value = (int)box.getObj();
		System.out.println(value);
	}
}

이렇게 Java의 다형성 특성을 활용하여, Object를 통해 Object의 후손을 객체형변환으로 사용할 수 있을 것이다.
그렇지만, Generic이 추가된 문법의 경우 인스턴스의 Type을 지정하는 문법이 추가됐다.

변경

public class Box<E> {
    private E obj;
    public void setObj(E obj){
        this.obj = obj;
    } 
    public E getObj(){
        return obj;
    }
}

위 코드를 보면 Object로 선언된 Class들이 E라는 값으로 변경됐다. Box에서의 E는 가상의 클래스를 나타내는 것으로, 실제로 존재하는 클래스가 아니다.

해당 Element는 다음과 같이 클래스를 넣어줄 수 있다.

public class BoxExam {
	public static void main(String[] args) {
		Box<Object> box = new Box<>();
		box.setObj(new Object());
		Object obj = box.getObj();
		Box<String> box2 = new Box<>();
		box2.setObj("hello");
		String str = box2.getObj();
		System.out.println(str);
		Box<Integer> box3 = new Box<>();
		box3.setObj(1);
		int value = (int)box3.getObj();
		System.out.println(value);
	}
}

<> 안에 참조타입을 넣는다. 각각 Object, String, Integer에 맞게 다음과 같이 들어간다.
1. Object를 사용하는 Box를 인스턴스로 만들겠다는 의미
2. String을 사용하는 Box인스턴스를 만들겠다는 의미
3. Integer를 사용하는 Box인스턴스를 만든다는 의미

Generic을 사용함으로써 선언할 때는 가상의 타입으로 선언하고, 사용시에는 구체적인 타입을 설정함으로써 다양한 Type의 Class를 이용하는 Class를 만들 수 있다.

위 이유로 인해, Generic은 자료구조를 저장하는 Collection FrameWork와 밀접한 관련이 있다.
(다양한 자료가 들어오는 자료구조)

Type은 E로 고정된 것이 아닌 의미에 따라 다르게 사용할 수 있으며, 꼭 아래있는 값이 아니어도 괜찮다.

TypeDescriptions
<T>Type
<E>Element
<K>Key
<V>Value
<N>Number

Set

Set은 중복이 없고 순서도 없는 자료구조.
Java에서 구현한 Class로는 HashSet과 TreeSet이 있다.

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class SetExam {
	public static void main(String[] args) {
		Set<String> set1 = new HashSet<>();
		boolean flag1 = set1.add("kim");
		boolean flag2 = set1.add("lee");
		boolean flag3 = set1.add("kim");
		System.out.println(set1.size());   //저장된 크기를 출력 -> 2
		System.out.println(flag1);  //true
		System.out.println(flag2);  //true
		System.out.println(flag3);  //false
		Iterator<String> iter = set1.iterator();
        /*
        for(String str : set){
            System.out.println(str);
        } 이런식으로 set을 for each로 꺼낼 수도 있다. */ 
		while (iter.hasNext()) {   // 꺼낼 것이 있다면 true          
			String str = iter.next(); // next()메소드는 하나를 꺼낸 뒤, 하나를 꺼내면 자동으로 다음것을 참조
			System.out.println(str);
		}
	}
}

List

List는 데이터의 중복이 있을 수 있고, 순서도 있다.
배열과 비슷한 형태의 자료구조이며, 둘의 차이점은 다음과 같다.

  • 배열: 한번 생성하면 크기 변경 불가
  • 리스트: 저장공간이 필요에 따라 자유
import java.util.ArrayList; // 구현한 Class
import java.util.List; // Interface

public class ListExam {
	public static void main(String[] args) {
		List<String> list = new ArrayList<>();
		list.add("kim");
		list.add("lee");
		list.add("kim");
		System.out.println(list.size()); // 출력 -> 3
        
		for(int i = 0; i < list.size(); i++){
			String str = list.get(i);
			System.out.println(str);
		}
	}   
}

ArrayList를 가장 많이 사용하며, 이외에도 LinkedList 등 다른 형태가 존재한다.

Map

Map Interface를 구현하는 Class.
Map은 key와 value를 쌍으로 저장하는 자료구조로 키는 중복될 수 없고, 값은 중복될 수 있다.
이는 HashTable을 활용한 자료구조이다.

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;   

public class MapExam {  
	public static void main(String[] args) {
		// Key, Value가 모두 String 타입
		Map<String, String> map = new HashMap<>();
    	
    	// 값 삽입
		map.put("001", "kim");
		map.put("002", "lee");
		map.put("003", "choi");
		// 같은 key가 2개 있을 수 없습니다. 첫번째로 저장했던 001, kim은 001, kang으로 바뀐다.
		map.put("001", "kang");
		System.out.println(map.size()); // 출력 -> 3
		
    	// 값 가져오기
    	System.out.println(map.get("001"));
		System.out.println(map.get("002"));
		System.out.println(map.get("003"));
	
		// map에 저장된 모든 key들을 Set자료구조로 꺼낸다.
		Set<String> keys = map.keySet();
		Iterator<String> iter = keys.iterator();
		while (iter.hasNext()) {
			String key = iter.next();
			// key에 해당하는 value를 꺼냅니다.
			String value = map.get(key);
			// key와 value를 출력합니다.
			System.out.println(key + " : " + value);
		}
	}
}

Date

Date Class는 날짜와 시간을 구하기 위한 클래스로, JDK 1.0때 만들어진 최초 클래스다.
그렇기에 해당 Class는 오래된 만큼 지역화가 되어있지 않다. 한국과 미국 등이 시간이 다른 그런 부분들에 대한 것이 고려된 것이 없다.

이런 Date Class 단점을 극복하기 위한 Class가 Calendar 클래스다.
해당 Calendar Class는 JDK1.1에 만들어졌다.

Date Class에는 실제로 Deprecated된 메소드가 많다.

Date date = new Date();

System.out.println(date.toString());

// java.util.SimpleDateFormat 을 통해 원하는 형태로 출력
// yyyy는 년, MM은 월, dd는 일을 표현한다.
// hh는 시간, mm은 분, ss는 초를 표현하며 a는 오전/오후 를 표현, zzz는 TimeZone으로 한국은 KST이다.
SimpleDateFormat ft =  new SimpleDateFormat ("yyyy.MM.dd 'at' hh:mm:ss a zzz"); System.out.println(ft.format(date));

// Long 값
System.out.println(date.getTime());
// System이 가지고 있는 currentTimeMillis()메소드를 이용해도 된다.
long today = System.currentTimeMillis();
System.out.println(today);

Calendar

Date의 단점을 해결하고 등장한 Calendar 클래스
Calendar Class는 추상클래스(Abstract Class)다.
추상 클래스에 있는 추상 메소드는 반드시 구현해야한다.
마찬가지로, Calendar Class로 객체(인스턴스)를 생성하려면 getInstance()를 사용해야한다.
getInstance() 메소드를 호출하면 내부적으로 java.util.GrgorianCalendar라는 인스턴스를 만들어서 return 한다.
Calendar cal = Calendar.getInstance();
GregorianCalendar는 Calendar의 자식 클래스다.

구현

Calendar cal = Calendar.getInstance();

int yyyy = cal.get(Calendar.YEAR);
int month = cal.get(Calendar.MONTH) + 1; // 월은 0부터 시작
int date = cal.get(Calendar.DATE);
// cal.get(Calendar.HOUR_OF_DAY); // 00-12
int hour = cal.get(Calendar.HOUR_OF_DAY); // 00-23
int minute = cal.get(Calendar.MINUTE);

Calendar는 현재 날짜와 시간에 대한 정보를 가지고 있어, get메소드에 어떤 Calendar 상수를 넣어주느냐에 따라서 다른 값이 나오게 된다.

만약 현재 Calendar에 5시간을 더하고 싶다거나 5시간 전 시간을 구하고 싶다면 다음과 같이 사용한다.

cal.add(Calendar.HOUR, 5);
cal.add(Calendar.HOUR, -5);

구현2

import java.util.Calendar;

public class CalendarExam{
    public String hundredDaysAfter(){
        //오늘부터 100일 뒤의 날짜를 "2016년1월1일"의 형식으로 return하세요.
        Calendar cal = Calendar.getInstance();
        cal.add(Calendar.DATE, 100);
        int yyyy = cal.get(Calendar.YEAR);
        int month = cal.get(Calendar.MONTH) + 1;
        int date = cal.get(Calendar.DATE);
        return yyyy + "년" + month + "월" + date + "일";
    }
}

java.time Package

오랜 시간동안 Date와 Time API는 부족한 기능 지원을 포함한 여러문제가 있었다.
직관적이지 않은 몇가지 문제점이 있었다.
그래서 나온 Joda-Time이라는 라이브러리를 사용하기도 했다.

  • Joda-Time: 자바의 Date와 Time을 대신 할 수 있는 클래스를 제공하는 라이브러리

그래서 Java SE 8부터 새롭게 디자인한 Date, Time Api를 제공하기 시작했다.

사용

새로운 API의 핵심 클래스는 오브젝트를 생성하기 위해 다양한 factory 메서드를 사용한다.

오브젝트 자기 자신의 특정 요소를 가지고 오브젝트를 생성할 경우 of 메서드를 호출하면 되고, 다른 타입으로 변경할 경우에는 from 메서드를 호출하는 방식

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Month;

LocalDateTime timePoint = LocalDateTime.now(); // 현재의 날짜와 시간

// 2012년 12월 12일의 시간에 대한 정보를 가지는 LocalDate객체를 만드는 방법  
LocalDate ld1 = LocalDate.of(2012, Month.DECEMBER, 12); // 2012-12-12 from values
// 17시 18분에 대한 LocalTime객체를 구한다.
LocalTime lt1 = LocalTime.of(17, 18); // 17:18 (17시 18분)the train I took home today
// 10시 15분 30초라는 문자열에 대한 LocalTime객체를 구한다.
LocalTime lt2 = LocalTime.parse("10:15:30"); // From a String

LocalDate theDate = timePoint.toLocalDate();
System.out.println(ld1);
System.out.println(lt1);
System.out.println(lt2);
System.out.println(theDate);

Month month = timePoint.getMonth();
System.out.println(month); // JANUARY 형태로 출력

int day = timePoint.getDayOfMonth();
int hour = timePoint.getHour();
int minute = timePoint.getMinute();
int second = timePoint.getSecond();
// 달을 숫자로 출력한다 1월도 1부터 시작하는 것을 알 수 있습니다. 
System.out.println(month.getValue() + "/" + day + "  " + hour + ":" + minute + ":" + second);
- 출력 결과

2012-12-12
17:18
10:15:30
2023-07-15
JULY
7/15 12:21:8

자바 IO

자바 IO 패키지
Input/Output -> 입력/출력

Java Program이 동작하는데 들어오는 InputData와 밖으로 나가는 OutputData가 있는데 이러한 입출력과 관련된 Class와 Interface가 있다.

IO는 Byte와 Char 단위 입출력 클래스로 나뉘게 된다.

이렇게 각각 InputStream이나 Reader등 아래있는 4가지 추상클래스를 상속받아 만들어지게 된다.
각각의 클래스에 따라 4가지 추상클래스를 받아들이는 생성자에 따라 다르게 된다.

  • 파일: FileInputStream, FileOutputStream, FileReader, FileWriter
  • 배열: ByteArrayInputStream, ByteArrayOutputStream, CharReader, CharWriter

이런식으로 Input과 Output에 대한 대상을 지정할 수 있는 IO 클래슫르을 장식대상 클래스라고 한다.

DataInputStream, DataOutputStream, BufferedReader, PrintWriter 등

Decorator Pattern

자바 IO는 Decorator Pattern으로 만들어져 있다.
하나의 클래스를 마치 장식하는 것처럼 생성자에서 감싸서 새로운 기능들을 계속 추가할 수 있도록 클래스를 만드는 방식

IO는 그래서 클래스가 하나만 그냥 생성돼서 사용되는 것이 아니라, 여러개 클래스를 생성했는데 생성자 안에다가 또 넣고 넣는 방식으로 사용하는 경우가 많다.

그래서 어렵다고 올 수 있어 연습이 필요한 부분이다.

간략하게 정리하자면 결국 Java IO는 장식대상 클래스와 장식하는 클래스로 나뉘어져서, 여러 개의 클래스를 생성자안에 생성자를 넣는 그런 방식으로 만드는 것

Byte 단위 입출력

Byte단위 입출력 클래스는 클래스의 이름이 InputStream이나 OutputStream으로 끝난다.

파일

파일로부터 1byte씩 읽어들여 파일에 1byte씩 저장하는 프로그램

public class ByteIOExam1 {
	public static void main(String[] args){     
		FileInputStream fis = null; // 파일 읽기 객체
		FileOutputStream fos = null; // 파일 쓰기 객체
		try {
        	fis = new FileInputStream("패키지경로/java 이름"); //src/java10/exam/ByteExam1.java
			fos = new FileOutputStream("byte.txt"); // 기본 경로는 현재 경로
			int readData = -1; 
			while((readData = fis.read())!= -1){
				fos.write(readData); // 1byte 단위로 읽어서 쓰는 동작 / 끝은 -1
			}           
		} catch (Exception e) {
			e.printStackTrace();
		}finally{
			try {
				fos.close(); // 반드시 닫아야한다. 해당 메소드도 IO Exception을 발생
			} catch (IOException e) {
				e.printStackTrace();
			}
			try {
				fis.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}

개선

기존 코드는 1Byte씩 읽어서 바로바로 썼지만, 이번에는 512byte씩 읽어서 파일에 512byte씩 저장하는 프로그램

public class ByteIOExam1 {
	public static void main(String[] args){     
		//메소드가 시작된 시간
		long startTime = System.currentTimeMillis();        
		FileInputStream fis = null; 
		FileOutputStream fos = null;        
		try {
			fis = new FileInputStream("src/javaIO/exam/ByteExam1.java");
			fos = new FileOutputStream("byte.txt");
			int readCount = -1; 
			byte[] buffer = new byte[512];
			while((readCount = fis.read(buffer))!= -1){
				fos.write(buffer,0,readCount);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}finally{
			try {
				fos.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
			try {
				fis.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		long endTime = System.currentTimeMillis();
        // 소요 시간
		System.out.println(endTime-startTime); 
	}
}

개선된 코드처럼 byte의 단위를 크게한다면, 수행시간이 훨씬 빨라진다.

OS에서는 기본적으로 512byte 단위로 잡기때문에 512의 배수를 활용하는 것이 좋다고 한다.

DataOutputStream

try-with-resources

Java IO 객체는 인스턴스 사용이 종료되면 close() 메소드를 호출해야했다. 그런데, Exception이 발견되지 않았다면 자동으로 close()가 되게 하는 방법이 있다.
try-with-resources

구현

  • DataOutputStream : 다양한 타입으로 저장이 가능하다.
    • wirteInt() - 정수값으로 저장
    • wirteBoolean() - boolean값으로 저장
    • writeDouble() - double 값으로 저장
import java.io.DataOutputStream;
import java.io.FileOutputStream;

public static void main(String[] args) {
	try(
    	DataOutputStream out = new DataOutputStream(new FileOutputStream("data.txt"));
    	){
		out.writeInt(100);
		out.writeBoolean(true);
		out.writeDouble(50.5);
	}catch (Exception e) {
		e.printStackTrace();
	}
} 

위에서 사용한 것처럼 try()와 같이 괄호로 IO 객체를 선언하여 사용하면 된다.

DataOutputStream도 위에서 선언한 것처럼 DataOutPutStream을 사용하기 위해서는 혼자 생성할 수 없고, 다른 OutputStream을 넣어야 한다. 해당 부분을 FileOutputStream을 경로를 지정하여 생성한 뒤 넣은 것.
(해당 부분이 Decorator Pattern을 나타낸 부분인듯 하다.)

  • 결과

data.txt로 가면 다음과 같이 출력이 나온다.
□□□d□@I@□□□□

이런식으로 저장된 파일이 Data로 적었기 때문에 제대로된 값을 못가져온다.

DataInputStream

DataOutputStream으로 얻은 결과로 나타났던 data.txt 파일이 이상하게 저장이 되어 있었다.
이렇게 저장한 데이터를 화면에 읽어낼 수 있는 Class를 작성해보겠다.

구현

  • DataInputStream : 양한 타입의 데이터를 읽어낼 수 있는 DataInputStream
    • readInt() -정수를 읽어들이는 메소드
    • readBoolean() - boolean 값을 읽어들이는 메소드
    • readDouble() - douboe 값을 읽어들이는 메소드
import java.io.DataInputStream;
import java.io.FileInputStream;

public static void main(String[] args) {
	try(
		DataInputStream out = new DataInputStream(new FileInputStream("data.dat"));
		){
		int i = out.readInt();          
		boolean b = out.readBoolean();      
		double d = out.readDouble();
		System.out.println(i);
		System.out.println(b);
		System.out.println(d);
	}catch(Exception ex){
		ex.printStackTrace();
	}
}
  • 결과

100
true
50.5

이렇게 File의 내용을 눈으로 볼 수 없지만, Data Type 자체로 저장했다가 해당 Data Type으로 불러서 볼 수 있다.

이렇듯 IO Class는 다양하게 Class를 조합해서 사용한다.

Character 단위 입출력(콘솔)

char 단위 입출력 클래스 이름은 Reader나 Writer로 끝이난다.
char 단위 입출력 클래스를 이용해서 키보드로부터 한 줄 입력 받아서 콘솔에 출력하는 코드

  • System.in - 키보드를 의미(InputStream)
  • BufferedReader - 한줄씩 입력 받기 위한 클래스
  • BufferedReader 클래스의 생성자는 InputStream을 입력받는 생성자가 없다.
  • System.in은 InputStream 타입이므로 BufferedReader의 생성자에 바로 들어갈 수 없으므로 InputStreamReader Class를 이용해야 한다.

정리하자면 BufferedReaderReader 객체만 받을 수 있지만, System.in 이 Type에 맞지 않아서 InputStreamReader를 통해 생성한다.

아래 예제는 키보드로부터 입력을 받아서 콘솔에 출력하는 예제다.

import java.io.BufferedReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter; 

public class CharIOExam01 {
	public static void main(String[] args) {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		
        //키보드로 입력받은 문자열을 저장하기 위해 line변수를 선언               
		String line = null;    
		try {
			line = br.readLine()
		} catch (IOException e) {
			e.printStackTrace();
		}
        
		System.out.println(line);
	}
}

br.readLine();IOException이 발생할 수 있는 Method라서 해당 부분에 대한 예외 처리를 해줘야한다.

Character 단위 입출력(파일)

Reader, Writer 객체를 이용해서 File로부터 값을 입력받아 File로 출력하는 예제

  • File에서 값을 입력받기 위해서는 FileReader 클래스가 필요하다.
  • 한 줄 읽어 들이기 위해서 BufferedReader 클래스를 이용해야 한다. (readLine() 사용, 없으면 return null)
  • 파일에 쓰게하기 위해 FileWriter Class 이용
  • 편리하게 출력을 위한 PrintWriter Class 이용
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter; 

public class CharIOExam02 {
	public static void main(String[] args) {
		BufferedReader br = null; 
		PrintWriter pw = null;
		try{        
			br = new BufferedReader(new FileReader("src/javaIO/exam/CharIOExam02.java"));
            // System.out 의 Type이 PrintWriter 객체
			pw = new PrintWriter(new FileWriter("test.txt"));
			String line = null;
			while((line = br.readLine())!= null){
				pw.println(line);
			}
		}catch(Exception e){
			e.printStackTrace();
		}finally {
			pw.close();
			try {
				br.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}
  • new FileReader는 파일리더가 생성되면서 FileNotFoundException을 발생시킬 수 있기 때문에 예외 처리를 해야한다.
  • System.out 의 Type이 PrintWriter 객체이다.
  • IO는 열었으면 닫아줘야한다. 닫지않는다면, 제대로 .txt를 작성하지 않을 수 있다.

어노테이션(Annotation)

어노테이션은 Java5에 추가된 기능이다.

  • 클래스나 메소드 위에 붙여 사용 ex)@Override
  • 소스코드에 메타코드(추가정보)를 주는 것

어노테이션을 통해 클래스가 컴파일되거나 실행될 때 어노테이션의 유무나 어노테이션에 설정된 값을 통해 클래스가 좀 더 다르게 실행되게 할 수 있다.

이러한 이유로 어노테이션을 일정의 설정파일처럼 설명하는 경우도 있다.

어노테이션은 Custom이 가능하여 2개로 나뉜다.

  • Java 기본 제공 어노테이션
  • Custom 어노테이션

Custom 어노테이션

  1. 어노테이션 정의
  2. 클래스에서 어노테이션 사용
  3. 어노테이션을 이용하여 실행

1. 어노테이션 정의

// Annotation 정의
@Retention(RetentionPolicy.RUNTIME)
public @interface Count100 {

}
  • @interface를 통해 정의한다.
  • @Retention(RetentionPolicy.RUNTIME)은 JVM 실행시에 Custom Annotation을 감지할 수 있도록 지정하는 어노테이션이다.

2. 어노테이션 사용

public class MyHello {
	@Count100
	public void hello(){
		System.out.println("hello");
	}
}

3. 어노테이션 실행

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(); // 어노테이션이 적용 됐다면 100번 호출
					}
				}else{
					hello.hello(); // 적용 안됐다면 한 번
				}
			}catch(Exception ex){
			ex.printStackTrace();
		}       
	}
}
  • MyHello를 정의하는 Main Class
  • method에 대한 정보를 얻는Method 객체, hello.getClass().getDeclaredMethod()를 통해 Method 정보를 얻어온다.
  • 단, getClass()라는 method는 Object가 가지고 있는 method인데, 해당 method는 해당 Instance를 만들 때 사용한 클래스 정보를 return한다.
  • getDeclaredMethod("hello");는 해당 클래스에 정보로부터 "hello"라는 이름의 method에 대한 정보를 구하란 의미
  • 위 getDeclaredMethod는 NoSuchMethodException이 발생할 수 있어서 예외 처리를 해줘야한다.
  • method에 대한 정보를 얻은 뒤에 isAnnotationPresent() method를 통해 특정 Annotationc이 method에 적용됐는지 알 수 있다. (적용됐다면, true 아니라면, false)

사실 어노테이션을 직접 만드는 경우는 많지 않다.

자바의 경력이 많이 쌓였을 때나 가능한 얘기이고, 대부분은 누군가가 제공하는 Annotation이 가지고 있는 설명서를 잘 보고 사용하는 것이 중요하다.

Thread

운영 체제(Operating System)란 컴퓨터의 하드웨어를 사용하게 해주는 프로그램이다.
현시대 컴퓨터는 여러개의 작업을 동시에 할 수 있다. 유튜브보기, 게임하기, 음악 듣기 등
작업관리자에 보면 Process(실행된 프로그램)를 볼 수 있다.

JVM도 결국은 운영체제 입장에서 프로그램중 하나이다. 운영체제 입장으로 봤을 때 자바도 하나의 프로세스로 실행하고 있고, 이러한 하나의 프로세스 안에서도 여러개의 흐름이 동작할 수 있고, 이러한 것이 Thread다.

즉, 자바 프로그램에서 여러 작업을 동시에 진행하고 싶다면 Thread를 공부해야 한다.

Thread 만들기

자바에서 Thread를 만드는 방법은 크게 2가지가 있다.
1. Thread Class를 상속받아 만드는 방법
2. Runnable 인터페이스를 구현하는 방법

extend Thread

java.lang.Thread Class를 상속받은 뒤, Thread가 가지고 있는 run() 메소드를 Overriding한다. (다른 흐름의 main method라고 생각하면 될 듯하다)

public class MyThread1 extends Thread {
	String str;
    
    public MyThread1(String str){ // 생성자
		this.str = str;
	}
    
    @Override
	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를 통해서 10번 출력하는 프로그램이다.
  • 이때 너무 빠른 속도로 출력되는 것을 방지하고자, Thread.sleep() method를 사용한다. (ms 단위로 적어서 동작해도 가능하다.)
  • Thread를 동작하면서 발생하는 InterruptedException으로 인해 예외 처리가 필요하다.
  • 호출 시에는 아래 코드와 같이 run()이 아닌 start() method를 호출한다.
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 실행시에는 run() method를 호출하는 것이아니라, start() method를 써야만 Thread가 실행되면서 프로젝트의 흐름이 원래의 main method와 thread의 method 흐름으로 나뉘게 된다.
  • 출력 결과를 확인해보면, main thread의 흐름이 종료됐더라도, 모든 thread가 끝났을 때 프로그램이 종료된다.

implements Runnable

두 번째 방법이었던 Runnable Interface를 구현하는 방법이다.

Runnable 인터페이스에는 run() method가 존재하여, 이를 구현한다.

public class MyThread2 implements Runnable {
	String str;

	public MyThread2(String str){
		this.str = str;
	}

	@Override
	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();
			}
		} 
	} 
}
  • 위에서 만든 Method는 Thread를 상속받은게 아니기 때문에 Thread가 아니다.
  • 따라서 Thread를 생성하고, 해당 생성자에 MyThread2를 넣어서 Thread를 생성해야 한다.
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를 생성하여, 해당 Thread에 Class를 넣어서 만들었다.
  • 실행은 마찬가지로 start() method를 통해서 실행하고, 결과는 같게 나온다.

이미 다른 Class를 상속받는 Class인 경우 Interface의 형태인 Runnable Interface를 사용하고, 그렇지 않은 경우 Thread를 바로 상속받는 형태로 사용하면 될 것 같다.

Thread 공유 객체

각각의 Thread는 다르게 실행하고 있지만, 하나의 프로그램에서 공유해서 사용하는 자원이 있을 것이다.

집에 가족 컴퓨터가 있을 때, 아빠 나 누나 누가 쓸지 이러한 것이 공유 객체다.

예제

공유 객체 실습을 위해 MusicBox라는 클래스가 있다고 가정했을 때, 이 MusicBox를 사용하는 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();
			}
		}
	}

	//슬픈 음악!!!이란 메시지가 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();
			}
		}
	}

	//카페 음악!!! 이란 메시지가 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();
			}
		}
	}
}
  • MusicBox를 사용하는 Thread 객체 MusicPlayer
public class MusicPlayer extends Thread{
	int type;
	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;
		}
	}       
}
  • Thread와 공유 객체 MusicBox를 사용하는 Main Class
public class Main {
	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();           
	}   
}

쓰레드와 공유객체를 이렇게 사용할 수 있다.

동기화 메소드와 동기화 블록

위 공유객체를 실습해봤는데, 공유 객체를 동시에 호출하면서 문제가 발생할 수 있다. 이러한 문제점을 해결하기 위한 문법이 있다.

공유객체가 가진 메소드를 동시에 호출되지 않도록 하는 방법

  • Method앞에 synchronized를 붙인다.
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();
		}
	}
}
  • synchronized를 붙여서 실행하면, 메소드 하나가 모두 실행된 후에 다음 메소드가 실행

  • 여러 개의 Thread들이 공유객체 메소드를 사용할 때 메소드의 synchronized가 붙어있다면, 먼저 호출한 메소드가 객체의 사용권을 가지게 되고, 이를 Monitoring Lock을 얻는다 한다.

  • 해당 Monitoring Lock은 실행 종료 또는 wait() 같은 method를 만나기전까지 유지돼, 다른 쓰레드들은 대기한다.

  • 물론, synchronized가 붙지않은 메소드는 다른 쓰레드들과 상관없이 실행될 수 있다.
    이는 다른 쓰레드가 너무 오래기다리는 것을 막기 위해 필요한 method에만 synchronized블록을 사용한다.

  • synchronized block 예시

for(int i = 0; i < 10; i ++){
	synchronized (this){
		System.out.println("신나는 음악!!!");
    }
		
    try {
		Thread.sleep((int)(Math.random() * 1000));
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
}

쓰레드와 상태제어

쓰레드가 3개 있다면 JVM은 시간을 잘게 쪼갠 후 쓰레드 1, 쓰레드 2, 쓰레드 3을 실행한다.
Process를 Time을 통해 분할한 방식으로 한다.

그렇기에 Thread는 실행가능상태(Runnable)과 실행상태인 Running상태로 나뉘어, 왔다갔다 한다.

쓰레드가 sleep()이나 wait() 메소드가 호출된다면 쓰레드는 Blocked 상태가 된다. 이외에도 suspend()도 있지만, Deprecated되어 사용하지 않는게 좋다.

또한 Thread를 종료시킬(Dead 상태) 목적으로 만든 method인 stop()도 버그가 존재하여 Deprecated됐다.

Blocked -> Runnable 상태로 전환하는 resume() method도 지원이 종료됐다.

sleep() method는 특정 시간이 지나면 Runnable 또는 Running 상태로 변하게 된다. Object가 가지고 있는 wait() 메소드는 notify()notifyAll() method를 호출해야만 상태가 변하게 된다.

run() 종료시에는 쓰레드는 종료되며, Dead 상태가 된다.
이외에도 다른 쓰레드에게 자원을 양보하는 yeild() method와 쓰레드가 종료될 때까지 대기하게 만드는 join() method 등이 있다.

쓰레드 상태제어

join() Method

상태제어중에서도 우선 join() 메소드를 알아보겠다. join() method는 쓰레드가 멈출때까지 기다리게 하는 method다.

기존에 run() method를 통해서 실행하고, 중간에 쓰레드를 기다리게 하려고 Thread.sleep() method를 사용했었다.
이로인해서 main Thread가 코드를 동작할 때 다른 Thread의 흐름과는 별개로 동작하였는데, join() method를 활용하면, 다른 Thread의 동작이 끝날때까지 대기한다.

코드 예시는 다음과 같다.

MyThread5 thread = new MyThread5();
System.out.println("Thread가 종료될때까지 기다립니다.");
thread.start();
System.out.println("Thread가 종료되었습니다."); 
  • 결과: 기존

시작
종료!
thread : 1
thread : 2
thread : 3
thread : 4
thread : 5

join() 사용

MyThread5 thread = new MyThread5();
// Thread 시작 
thread.start(); 

System.out.println("Thread가 종료될때까지 기다립니다.");

try {
	// 해당 쓰레드가 멈출때까지 멈춤
	thread.join();
} catch (InterruptedException e) {
	e.printStackTrace();
}
System.out.println("Thread가 종료되었습니다."); 
  • 결과: join()

시작
thread : 1
thread : 2
thread : 3
thread : 4
thread : 5
종료!

wait(), notify() Method

wait()notify()는 동기화된 블록안에서 사용해야 한다.
wait을 만난 Thread는 해당 객체의 모니터링 락에 대한 권한을 가지고 있다면, 이 권한을 놓고 대기하게 된다.

public class ThreadB extends Thread{
// 쓰레드 실행 시 자기 자신의 모니터링 락을 획득
// 5번 반복, 0.5초씩 쉬면서 total에 값을 누적
// 그 후 notify()메소드를 호출하여 wait하고 있는 쓰레드를 깨움 
	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();
		}
	}
}
  • 동기화 조건을 맞추기 위해 synchronized() block으로 감싼다.
  • for문의 모든 동작이 끝나면 notify() method를 호출한다.

위 동작에서는 Thread를 기다리게 하는 wait()이 존재하지 않는다. 따라서 해당 부분을 만들겠다.

public class ThreadA {
	public static void main(String[] args){
		// 쓰레드 B를 start, run() method 안에서 자신의 Monitoring Rock 획득
		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);
		}
	}
}
  • wait() Method를 통해서 Main Thread가 동작을 멈췄다.
  • Main Thread는 다른 Thread인 ThreadB() Class에 run() Method에 있는 notify() Method가 호출되면서 wait() 상태에서 벗어날 수 있다.

b가 완료될때까지 기다립니다.
0를 더합니다.
1를 더합니다.
2를 더합니다.
3를 더합니다.
4를 더합니다.
Total is: 10

Daemon

데몬이란 리눅스, 유닉스 계열의 OS에서 백그라운드로 동작하는 프로그램이다. (윈도우 계열에서는 Service라고 보통 부른다)

자바에서도 Daemon Thread라고해서 자바에서 데몬과 유사하게 동작하는 쓰레드가 있다.
이것을 사용하기 위해서는 Java에서 Daemon을 설정해주면 된다.

사용되는 용도는 자바 프로그램을 만들 때 백그라운드에서 특별한 처리를 할 때 사용된다.

  • 주기적으로 자동 저장
  • 에디터를 만들 때 맞춤법 검사를 주기적으로 한다던가

이런 용도로 사용된다.

Daemon Thread는 일반 쓰레드(main 등)가 모두 종료되면 강제적으로 종료되는 특징이 있다.

구현

// Runnable로 구현
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) {
		Thread th = new Thread(new DaemonThread());  // Runnable을 구현하는 DaemonThread를 실행하기 위한 Thread 생성
		th.setDaemon(true);  		// 데몬쓰레드 설정
		th.start();          		// 쓰레드 실행
        
		try {
			Thread.sleep(1000);  		// 메인 쓰레드가 1초뒤에 종료되도록 설정

		} catch (InterruptedException e) {
			e.printStackTrace();
		}   
        
        // 데몬쓰레드는 다른 쓰레드가 모두 종료되면 자동종료
		System.out.println("메인 쓰레드가 종료됩니다. ");    
	}   
}

Lambda

Java 8에서 새로 추가된 lambda식

Interface에서 method를 하나만 가지고 있는 것을 함수형 인터페이스라고 한다.
단일 추상 메서드(SAM, Single Abstract Method)
예를 들어 java.lang package에 존재하는 Runnable Interface는 run() method만 존재했다.

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();
	}   
}

위 코드처럼 Runnalbe Interface를 생성하여, 메소드를 바로 구현할 수 있다.
이때 자바는 메소드만 매개변수로 전달할 방법이 없다. 인스턴스만 전달 할 수 있다. 그렇기 때문에 Runnable이라는 객체를 만들어서 Method를 전달한 코드이다.

만약, 메소드를 전달할 수 있다면, 좀 더 편리하게 프로그래밍 할 수 있지 않을까? 에서 이를 해결한 것이 람다표현식이다.

람다표현식은 () -> {} 이러한 키워드로 사용이 된다.

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 Interface를 받아들이는 것을 알 수 있다.
  • JVM은 Thread 생성자가 Runnable 인터페이스를 구현한 것이 와야 하는 것을 알게 되고, 람다식을 Runnable을 구현하는 객체로 자동으로 만들어 매개변수로 넣어준다.

람다식 기본문법

(매개변수목록) -> {실행문}의 형태로 사용된다.

비교적 간단한 문법으로 보이는데, 사용되는 예시와 함께 알아보겠다.
Compare Interface에는 2개의 값을 비교하여, 어떤 값이 더 큰지 구하는 compareTo()라는 method를 가지고 있다.

2개의 값을 받아들인 후, 정수를 반환하는 메소드이다.

public interface Compare{
    public int compareTo(int value1, int value2);
}

이를 이용하는 클래스로 실제 동작 확인

public class CompareExam {      
	public static void exec(Compare compare){
		int k = 10;
		int m = 20;
		int value = compare.compareTo(k, m);
		System.out.println(value);
	}

	public static void main(String[] args) {    
		exec((i, j)->{
			return i - j;
		}); 
	}
}
  • JVM은 exec() method를 찾은 뒤 매개변수 2개를 받은 Interface의 Method가 무엇인지 찾아볼 것이다.

정리하자면, main Method에서 exec()를 호출하면서 Argument로 Method의 표현식을 전달한다. Parameter로는Compare 인터페이스를 받아서, 이 인터페이스의 compareTo() method의 구현 방법을 전달 받는다.

다른 예시

프로그래머스 마지막 강의에 있는 실습 문제를 통한 예제로 복습을 해보겠다.

  • Car Class
public class Car{
    public String name;
    public int capacity;  
    public int price;
    public int age;
    
    public Car(String name, int capacity, int price, int age){
        this.name = name;
        this.capacity = capacity;
        this.price = price;
        this.age = age;
    }
    
    public String toString(){
        return name;
    }
}
  • Car 구현 (Main Class)
import java.util.*;

public class CarExam{
    public static void main(String[] args){
        //Car객체를 만들어서 cars에 넣는다.
        List<Car> cars = new ArrayList<>();
        cars.add( new Car("작은차",2,800,3) );
        cars.add( new Car("봉고차",12,1500,8) );
        cars.add( new Car("중간차",5,2200,0) );
        cars.add( new Car("비싼차",5,3500,1) );
        
        printCarCheaperThan(cars, 2000); // 2000 보다 작은 가격의 차 출력
    }
    
    // static인 main에서 호출하여, static으로 선언
    public static void printCarCheaperThan(List<Car> cars, int price){
        for(Car car : cars){
            if(car.price < price){
                System.out.println(car);
            }
        }
    }
}

이런식으로 코드를 구현할 수 있을텐데, 위 예시를 람다로 변경한 예제를 보겠다.

우선 람다로 변경하기 전, 함수형 인터페이스를 통해 method를 구현하는 클래스를 만들고 해당 클래스를 생성하는 생성자를 매개변수로 넘기는 코드이다.

import java.util.*;
public class CarExam{
    public static void main(String[] args){
        List<Car> cars = new ArrayList<>();
        cars.add( new Car("작은차",2,800,3) );
        cars.add( new Car("봉고차",12,1500,8) );
        cars.add( new Car("중간차",5,2200,0) );
        cars.add( new Car("비싼차",5,3500,1) );
        
        printCar(cars, new CheckCarForBigAndNotExpensive());
    }
    
    public static void printCar(List<Car> cars, CheckCar tester){
        for(Car car : cars){
            if (tester.test(car)) {
                System.out.println(car);
            }
        }
    }
    
    interface CheckCar{
        boolean test(Car car);
    }
    
    //내부클래스를 만들어서 사용
    static class CheckCarForBigAndNotExpensive implements CheckCar{
        public boolean test(Car car){
            return car.capacity >= 4 && car.price < 2500;
        }
    }
}
  • 4명 이상이 탈 수 있고, 가격이 2500이하인 차를 검색하는 method

다음은 익명클래스를 사용하는 단계입니다.

import java.util.*;
public class CarExam{
    public static void main(String[] args){
        List<Car> cars = new ArrayList<>();
        cars.add( new Car("작은차",2,800,3) );
        cars.add( new Car("봉고차",12,1500,8) );
        cars.add( new Car("중간차",5,2200,0) );
        cars.add( new Car("비싼차",5,3500,1) );
        
        printCar(cars, 
            //인터페이스 CheckCar를 구현하는 익명클래스를 만든다.
            new CheckCar(){
                public boolean test(Car car){
                    return car.capacity >= 4 && car.price < 2500;
                }
            });
    }
    
    public static void printCar(List<Car> cars, CheckCar tester){
        for(Car car : cars){
            if (tester.test(car)) {
                System.out.println(car);
            }
        }
    }
    
    interface CheckCar{
        boolean test(Car car);
    }  
}

이렇듯 익명클래스를 만들어서 바로 method를 구현하면, 코드가 좀 더 간결해지는 것을 확인할 수 있다.

마지막으로 같은 조건의 람다식을 활용한 코드이다.

import java.util.*;
public class CarExam{
    public static void main(String[] args){
        List<Car> cars = new ArrayList<>();
        cars.add( new Car("작은차",2,800,3) );
        cars.add( new Car("봉고차",12,1500,8) );
        cars.add( new Car("중간차",5,2200,0) );
        cars.add( new Car("비싼차",5,3500,1) );
        
        CarExam carExam = new CarExam();
        // 인터페이스 CheckCar의 test메소드에 대응하는 람다를 만듭니다.
        carExam.printCar(cars, (Car car) -> { return car.capacity >= 4 && car.price < 2500; }
        );
    }
    
    public void printCar(List<Car> cars, CheckCar tester){
        for(Car car : cars){
            if (tester.test(car)) {
                System.out.println(car);
            }
        }
    }
    
    interface CheckCar{
        boolean test(Car car);
    }  
}

이런식으로 람다를 사용할 수 있으며, 코드가 많이 단축되는 것을 확인할 수 있다.
Collections.sort(list, (o1, o2) -> {}); 의 형태 등 코딩테스트 및 실제 개발에서도 많이 쓰이는 람다식인 만큼 해당 부분에 대한 공부는 이후에도 더욱 필요할 것이라 생각한다.

profile
새싹

0개의 댓글