728x90

1편에 이어지는 글이다. (↓1편 링크)

https://breakcoding.tistory.com/113

 

[Java] 컬렉션 프레임워크 (List, Set, Queue, Map) 1

데이터의 모임(컬렉션)을 다룰 때 배열을 사용하면 중간에 끼워넣기에도 불편하고 크기를 늘릴 수도 없어 여러 가지로 불편한 점이 많다. 그래서 더 편리한 자료구조를 만든 것이 컬렉션이다. 컬렉션과 컬렉션 프..

breakcoding.tistory.com

지난 포스팅의 내용을 간단히 정리하자면 컬렉션 프레임워크는 인터페이스와 클래스로 구성되어 있는데 Collection, List, Queue, Set, Map 이 5가지가 인터페이스이다. (이 인터페이스 5가지는 꼭 알아두자.)

List 인터페이스의 ArrayList는 자주 사용하므로 꼭 알아놔야 하고 LinkedList의 특이한 점은 List의 구현 클래스이자 Queue의 구현 클래스라는 거. Vector는 ArrayList와 똑같은데 Vector는 동기화를 지원하고 ArrayList는 동기화를 지원한다는 거, 따라서 동기화가 필요한 멀티스레딩이 아니라면 ArrayList를 쓰는 게 낫다. 멀티스레딩이 아닌데 굳이 Vector를 쓰면 performance가 더 떨어진다.

Set은 순서가 중요하지 않고 동일한 데이터가 없는 자료구조이다. Set에는 HashSet과 TreeSet이 있다는 것 정도는 알아두자. 굳이 HashSet과 TreeSet의 차이점을 말하자면 HashMap은 동기화를 지원하지 않고 Hashtable은 동기화를 지원한다. 그리고 HashMap은 key 값이든 value 값이든 null을 사용할 수 있는데 Hashtable은 null의 사용이 불가능하다.

 

List는 인터페이스이기 때문에 객체를 생성하지 못한다. 따라서 자식 클래스인 ArrayList 등을 사용해야 하는데 선언할 때에는

List<Integer> list = new ArrayList<>();

ArrayList가 아니라 이렇게 List 타입으로 많이 선언하는 것 같다. 그리고 나서 객체를 생성할 때에 구현 클래스로 생성하는 것이다. 대부분 이렇게 많이 쓰므로 그냥 관습에 따르는 것이 좋다.

 

List와 Set은 서로 반대되는 성질을 가지고 있다.

List는 순서가 있고 중복될 수 있는 반면 Set은 순서가 없고 중복될 수 없다.

 

Collection 인터페이스에서는 여러가지 메소드로 다양한 기능들을 제공한다. (지난 포스팅에 메소드들을 정리해놓았다.) 하지만 배열은 그렇지 않다. 메소드가 없다. 즉 기능이 별로 없다. 배열의 크기를 알고 싶으면 배열이름.length()가 아니라 배열이름.length이다. 메소드가 아니다. 하지만 String은 문자열의 길이를 알고 싶으면 문자열.length()이고 컬렉션도 컬렉션에 저장된 원소의 개수를 알고 싶다면 컬렉션객체.size()이다. 이렇게 크기를 알아내는 방법만 봐도 기능이 많은 String과 Collection은 메소드로 크기를 알아낸다.

따라서 가능하면 기초형보다는 참조형이 좋다. 사실 배열도 참조형인데 기초형의 데이터를 여러개 모아놓은 정도이기 때문에 기능이 별로 없다.

컬렉션은 제네릭 타입이기 때문에 <> 사이에 타입을 써주는 것이 좋다. <> 사이에는 int, double, boolean과 같은 기초타입은 쓰면 안 되고 참조 타입을 써줘야 한다. 기초타입을 쓰고 싶다면 Integer, Double, Boolean과 같은 Wrapper 클래스로 써줘야 한다. 그래도 자동 형변환이 되어 오류가 안 난다.

 

이제 본격적으로 Map을 배워보자. 일단 기본적인 Map의 메소드들부터 살펴보자.

void clear() 맵에 있는 모든 엔트리를 삭제한다.
boolean containsKey(Object key) key라는 키를 가진 엔트리가 있는지 없는지를 반환한다.
boolean containsValue(Object value) value라는 값을 가진 엔트리가 있는지 없는지 반환한다.
Set> entrySet() 맵에 있는 모든 엔트리를 원소로 하는 Set 객체를 만든다.
V get(Object key) key에 해당하는 value를 반환한다.
boolean isEmpty() 맵 객체가 비어있는지 아닌지를 반환한다.
Set keySet() key값들을 모아 Set 객체를 만든다.
V put(K key, V value) key-value 엔트리를 맵 객체에 추가한다.
V remove(Object key) key에 해당하는 엔트리를 삭제한다.
int size() 맵 객체에 저장된 엔트리의 개수를 반환한다.
Collection values() 맵에 저장된 value들을 모아 컬렉션 객체를 만든다.

Map 인터페이스는 키와 값을 쌍으로 저장한다. 메소드만 봐도 containsKey(), containsValue()라는 메소드가 있다.

Map에 저장된 객체 중에 해당 key 또는 value가 있는지를 알아낼 수 있다.

get 메소드는 key를 주면 value가 나오는 메소드이다. key가 유일하기 때문에 key로 접근해서 value를 얻어올 수 있다.

isEmpty()는 굳이 외우지 않아도 당연히 알테고 put 메소드는 Collection으로 따지면 add와 같은 메소드이다. Map 객체가 타입이고 맵 객체 이름이 map이라면 map.put("one", 1); 이렇게 하면 one을 key로, 1을 value로 하는 하나의 쌍이 Map에 추가된다.

 

Map은 key와 value 이렇게 두 가지 데이터를 하나의 쌍으로 저장하기 때문에 Collection과는 조금 다르다. 하지만 Map 타입을 Collection으로 바꿔야 할 수도 있다. Map을 Collection처럼 이용하고 싶다면 3가지 방법이 있다.

keySet()

keySet()을 이용하면 결과는 Set이 나온다. 맵이 Map<K, V> 였다면 Set은 Set<K>가 반환타입으로 나온다.

맵의 key값들을 모으니 당연히 Set의 원소들의 타입은 Map의 key 타입이 나올 것이다.

즉, Map에서 key 타입이 String이었다면 keySet()의 반환타입은 Set<String>이라는 것이다.

Map에서 key 값들은 유일하기 때문에 중복된 값이 없는 자료구조인 Set이 딱이다.

다음 코드는 Map 객체에서 key값들만 뽑아내서 Set에 저장하는 코드이다.

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

public class CollectionTest {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        map.put("김철수", 15);
        map.put("김영희", 15);
        map.put("홍길동", 40);
        System.out.println(map);
        Set<String> names = map.keySet();
        System.out.println(names);
    }
}

Map 객체에 데이터를 추가하려면 put() 메소드를 쓰면 된다. 첫 번째 원소는 key, 두 번째 원소는 value.

위 코드의 실행 결과

Map에서의 key값이었던 이름들을 모아서 Set으로 잘 만들어진 것을 볼 수 있다.sfdasfafasfaf

2. values()

key값이 아니라 value 값들만 모아서 Collection으로 만들고 싶다면 values() 메소드를 사용하면 된다.

Map에서 key값과 달리 value 값들은 중복이 있을 수도 있고 순서가 중요할 수도 있으므로 Set으로는 못 만든다. 그리고 사용자가 Queue로 바꾸고 싶은지 Set으로 바꾸고 싶은지 List로 만들고 싶은지 모르기 때문에 일단 가장 최상위 클래스인 Collection 타입으로 반환한다. 그 후에 사용자가 원하는 타입으로 바꾸면 된다. ArrayList로 바꾸고 싶으면 ArrayList로 바꿀 수 있다. (그 방법은 뒤에서 설명할 것이다.)

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

public class CollectionTest {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        map.put("김철수", 15);
        map.put("김영희", 15);
        map.put("홍길동", 40);
        System.out.println(map);
        Collection<Integer> ages = map.values();
        System.out.println(ages);
    }
}

위 코드의 실행 결과

여기에서 영희와 철수의 나이는 모두 15살인데 정보가 손실되지 않고 15가 두 개 다 들어간 것을 볼 수 있다.

Map에서 value 값들은 중복될 수 있다는 것을 잊지말자.

3. entrySet()

entrySet()은 Map의 key-value 한 쌍을 하나의 원소로 보는 Collection으로 바꿔준다. 이는 Map과 다르게 하나의 원소 자체가 pair이다. Map.Entry 이렇게 쓴 것에서 알 수 있듯이 Entry는 Map 인터페이스의 내부 인터페이스이다. (내부인터페이스 또는 내부클래스는 외부인터페이스.내부인터페이스 이렇게 쓴다.)

entrySet()은 Map을 key와 value를 합쳐서 한 개의 데이터로 사용하는 Collection으로 바꾸겠다는 것이다.

반환타입은 Set<Map.Entry<K, V>>이다. 왜 Set일까? value는 중복이 있을 수 있지만 key값과 value값을 합치면 유일하다.

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

public class CollectionTest {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        map.put("김철수", 15);
        map.put("김영희", 15);
        map.put("홍길동", 40);
        System.out.println(map);
        Collection<Map.Entry<String, Integer>> nameAndAges = map.entrySet();
        System.out.println(nameAndAges);
    }
}

위 코드의 실행 결과

위의 출력 결과는 {}인 것을 봐서 Map이고 아래는 []인 것을 봐서 Collection인 것을 알 수 있다. Map을 Collection으로 바꾼 것이다.

728x90
728x90
실행 ctrl + shift + F10
메소드 매개변수 보기 인자가 들어갈 괄호 안에 마우스 커서 놓고 ctrl+p
메인 메소드 만들기 psvm+Enter
마우스 커서 위치와 상관 없이 개행 shift+Enter
자동완성 ctrl+Enter
해당 이름을 가진 변수 전체를 다른 이름으로 바꾸기 바꾸고 싶은 변수 이름을 더블클릭 또는 드래그 해서 선택한 후 shift + F6 누르고 원하는 변수 입력 후 Enter
//으로 주석처리 및 //으로 된 주석처리 해제 주석처리 또는 해제하고 싶은 행들을 선택한 후 ctr+/
/**/으로 주석처리 및 /**/으로 된 주석처리 해제 주석처리 또는 해제하고 싶은 부분을 선택한 후 ctrl+shift+/
코드 정렬 ctrl + alt + l
코드 완성 ctrl+shift+Enter
728x90
728x90

LocalDate 객체로 오늘 날짜를 뽑은 다음에 그것을 문자열로 바꾸려면 이렇게 하면 된다.

LocalDate date = LocalDate.now(); //오늘 날짜 LocalDate 객체 생성
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String today = date.format(dateTimeFormatter); //LocalDate 객체를 String 객체로 바꿈

이렇게 LocalDate를 String으로 바꿔서 today라는 문자열을 DB에 저장했다고 하자.

나중에 DB에서 그 문자열로 된 날짜를 읽어들였는데 이 날이 무슨 요일인지 알고 싶으면 어떻게 하면 될까?

일단 문자열을 Date타입으로 바꾸자.

DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
Date d = dateFormat.parse(today); //String이었던 today를 Date로 바꿈

이렇게 DateFormat과 parse() 메소드만 있으면 문자열을 Date 객체로 바꿀 수 있다.

그 후 이 Date 타입의 객체 d를 LocalDate 타입으로 바꿔준다.

LocalDate localDate = d.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); //Date 객체 d를 LocalDate 객체로 바꿈

Date 객체.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); 해주면 LocalDate 객체를 반환한다.

위의 두 줄을 한 줄로

LocalDate localDate = dateFormat.parse(today).toInstant().atZone(ZoneId.systemDefault()).toLocalDate();

이렇게 할 수도 있다.

 

그러고나면 localDate 객체에서 요일을 뽑아낼 수 있다.

DayOfWeek day = localDate.getDayOfWeek();
if(day == DayOfWeek.MONDAY) {
	System.out.println(today +"는 월요일입니다.");
}
else if (day == DayOfWeek.TUESDAY) {
	System.out.println(today +"는 화요일입니다.");
}
else if (day == DayOfWeek.WEDNESDAY) {
	System.out.println(today +"는 수요일입니다.");
}
else if (day == DayOfWeek.THURSDAY) {
	System.out.println(today +"는 목요일입니다.");
}
else if (day == DayOfWeek.FRIDAY) {
	System.out.println(today +"는 금요일입니다.");
}
else if (day == DayOfWeek.SATURDAY) {
	System.out.println(today +"는 토요일입니다.");
}
else {
	System.out.println(today +"는 일요일입니다.");
}

parse() 메소드를 사용할 때에는 ParseException이 발생하기 때문에 반드시 예외처리를 해주어야 한다.

예외처리를 해주지 않으면 이렇게 컴파일 에러가 발생한다. 이 포스팅은 예외처리에 관한 포스팅이 아니기 때문에 그냥 main 메소드에서 throw 해주었다.

전체 코드이다.

public class DateTest {
    public static void main(String[] args) throws ParseException{
        LocalDate date = LocalDate.now(); //오늘 날짜 LocalDate 객체 생성
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        String today = date.format(dateTimeFormatter); //LocalDate 객체를 String 객체로 바꿈
        DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        Date d = dateFormat.parse(today); //String이었던 today를 Date로 바꿈
        LocalDate localDate = d.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); //Date를 LocalDate로
        DayOfWeek day = localDate.getDayOfWeek();
        if (day == DayOfWeek.MONDAY) {
            System.out.println(today + "는 월요일입니다.");
        } else if (day == DayOfWeek.TUESDAY) {
            System.out.println(today + "는 화요일입니다.");
        } else if (day == DayOfWeek.WEDNESDAY) {
            System.out.println(today + "는 수요일입니다.");
        } else if (day == DayOfWeek.THURSDAY) {
            System.out.println(today + "는 목요일입니다.");
        } else if (day == DayOfWeek.FRIDAY) {
            System.out.println(today + "는 금요일입니다.");
        } else if (day == DayOfWeek.SATURDAY) {
            System.out.println(today + "는 토요일입니다.");
        } else {
            System.out.println(today + "는 일요일입니다.");
        }
    }
}

이 코드를 실행하면 다음과 같이 오늘의 요일을 출력하는 것을 볼 수 있다.

 

728x90
728x90

데이터의 집합(컬렉션)을 다룰 때 배열을 사용하면 중간에 끼워넣기에도 불편하고 크기를 늘릴 수도 없어 여러 가지로 불편한 점이 많다. 그래서 더 편리한 자료구조를 만든 것이 컬렉션이다. 컬렉션과 컬렉션 프레임워크는 혼용해서 많이 쓰기 때문에 용어에 집착할 필요는 없다.

컬렉션 프레임워크는 우리가 자료구조 시간에 배운 그 자료구조들을 쉽게 이용할 수 있도록 제공해준다.

JDK 1.2까지는 제네릭 타입이 없었는데 JDK 5부터는 제네릭을 지원하므로 더 안전하게 사용 가능하다. (JDK 1.5를 JDK 5라고도 함)

컬렉션은 데이터의 집합을 말하는데 특징에 따라 두 가지 종류로 나눌 수 있다.

하나는 동일한 데이터의 집합인 Collection

또 하나는 두 가지 종류(key, value)의 데이터로 이루어진 Map이다.

이렇게 Map은 Collection과 특징이 좀 다르지만 일반적으로 넓은 의미의 컬렉션인 Collection+Map의 의미로 많이 쓴다.

Collection은 인터페이스이다. Collection 인터페이스의 자식 인터페이스로 List, Queue, Set이 있다.

일단 List, Queue, Set의 간단한 특징부터 말하자면 List는 선형적인 자료구조, Queue는 선입선출 자료구조, Set은 순서가 없고 중복이 없는 자료구조이다.

이렇게 List, Queue, Set은 Collection의 자식 인터페이스이고 Map은 Collection의 자식 인터페이스가 아니다.

Map은 key와 value를 하나의 쌍으로 저장하는 자료구조로, key 값은 중복될 수 없다. 따라서 key 값은 유일하다.

이 인터페이스 5개 (Collection, List, Queue, Set, Map)는 꼭 알아두자.

 

일단 컬렉션 인터페이스와 클래스의 구조부터 살펴보자. 분홍색은 인터페이스, 하늘색은 클래스이다.

 

 

 

List의 구현 클래스 중에서 가장 자주 쓰이는 것은 ArrayList이다. Vector는 ArrayList와 동일한데 동기화를 지원한다. 따라서 멀티스레딩, 다중화에서 사용한다. 멀티스레딩을 하는게 아니라면 굳이 더 느린 Vector를 쓸 필요는 없다.

List의 구현 클래스들을 보다보면 특이한 것을 발견했을 것이다. LinkedList는 List의 구현 클래스이자 Queue의 구현 클래스이다. 클래스를 다중 상속하는 것은 안 되지만 여러 인터페이스를 구현하는 것은 가능하다. LinkedList는 List 인터페이스도 구현하고 Queue 인터페이스도 구현한 것이다. LinkedList의 자식 클래스로 Stack이 있다.

Set의 구현 클래스로는 HashSet과 TreeMap이 있는데 TreeMap은 Map이 아니라 Set이라는 거 기억하자. 헷갈리기 쉽다.

Map에는 HashMap을 자주 사용한다. Hashtable도 종종 쓰는데 t가 소문자라는 거 기억하자. (교수님께서는 아마 만든 사람이 실수한 것 같다고 하셨다)

 

이제 정말 메소드들을 배워보자.

다음은 Collection 인터페이스가 제공하는 메소드이다.

boolean add(E e)객체 e를 컬렉션에 추가한다.
void clear컬렉션 객체에 있는 모든 객체들을 삭제한다.
boolean contains(Object o)객체 o가 컬렉션에 있는지 없는지 반환한다.
boolean isEmpty()컬렉션이 비어있는지 아닌지 반환한다.
boolean remove(Object o)객체 o를 제거하고 제거했는지 아닌지 반환한다.
int size()컬렉션에 있는 객체의 개수를 반환한다.
T[] toArray(T[] a)컬렉션을 배열로 만들어서 반환한다.

헷갈리지 말아야 할 것은 컬렉션 객체의 크기(길이)를 알고 싶다면 length()가 아니라 size()라는 것이다.

문자열은 length(), 배열은 length, 컬렉션은 size()라는 거 잊지 말자.

Collection이 최상위 인터페이스이므로 이 메소드들은 하위 인터페이스, 클래스도 모두 가지고 있는 공통의 메소드이다.

굳이 컬렉션을 더 불편한 배열로 바꿀 일은 거의 없지만 그래도 배열로 바꾸는 방법은 알아야 하므로 toArray 메소드는 알아두자.

다음은 Map 인터페이스가 제공하는 메소드이다.

void clear()맵에 있는 모든 엔트리를 삭제한다.
boolean containsKey(Object key)key라는 키를 가진 엔트리가 있는지 없는지를 반환한다.
boolean containsValue(Object value)value라는 값을 가진 엔트리가 있는지 없는지 반환한다.
Set<Map.Entry<K, V>> entrySet()맵에 있는 모든 엔트리를 원소로 하는 Set 객체를 만든다.
V get(Object key)key에 해당하는 value를 반환한다.
boolean isEmpty()맵 객체가 비어있는지 아닌지를 반환한다.
Set<K> keySet()key값들을 모아 Set 객체를 만든다.
V put(K key, V value)key-value 엔트리를 맵 객체에 추가한다.
V remove(Object key)key에 해당하는 엔트리를 삭제한다.
int size()맵 객체에 저장된 엔트리의 개수를 반환한다.
Collection<V> values()맵에 저장된 value들을 모아 컬렉션 객체를 만든다.

Map 인터페이스는 키와 값을 쌍으로 저장한다. 메소드만 봐도 containsKey(), containsValue()라는 메소드가 있다.

Map에 저장된 객체 중에 해당 key 또는 value를 가지고 있는지를 알아낼 수 있다.

get 메소드는 key를 주면 value가 나오는 메소드이다. key가 유일하기 때문에 key로 접근해서 value를 얻어올 수 있다.

isEmpty()는 굳이 외우지 않아도 당연히 알테고 put 메소드는 Collection으로 따지면 add와 같은 메소드이다. Map 객체가 <String, Integer> 타입이고 맵 객체 이름이 map이라면 map.put("one", 1); 이렇게 하면 one을 key로, 1을 value로 하는 하나의 쌍이 Map에 추가된다.

여기에서 keySet()과 values() 메소드는 꼭 알아둬야 한다. Map에 있는 데이터들을 Collection 타입으로 바꾸어야 하는 상황이 있을 수도 있다. 그런데key와 value로 이루어져있는데 Map에는 데이터 한 개에 2개의 값이 들어있으므로 key와 value를 따로 따로 가져와야 한다. key들을 가져와서 Collection으로 만드는 게 keySet() 메소드이고 value들을 가져와서 Collection으로 만드는 것은 values() 메소드이다.

그런데 key들을 모아서 컬렉션으로 만드는데 keySet() 메소드의 반환타입은 왜 하필 Set일까?

Map의 특징 중 하나, key값들끼리는 중복이 없으므로 Set의 특징인 중복이 없는 것과 일치한다. 따라서 key값들을 모으면 저장할 자료구조는 Set이 딱이다. keySet() 메소드의 반환타입은 Set<K>인데 K는 key를 말한다. Map 원소들 중 key의 타입이었던 그 타입이 Set의 원소들의 타입이 되는 것이다.

반면 values() 메소드는 반환 타입이 Collection<V>이다. value 값들을 모아서 컬렉션으로 만드는데 value는 중복되어도 되기 때문에 중복 데이터가 있을 수 있다. 따라서 Set으로는 못 바꾸고 Collection으로 바꾸는 것이다. 물론 Set도 Collection이다. (Collection의 자식인터페이스)

 

사실 여기까지 봤을 때에는 무슨 소리인지 잘 모를 수도 있지만 이에 해당하는 예제 코드들을 보면 이해가 갈 것이다. 예제는 다음 글에 이어가겠다.

728x90
728x90

우선 입출력 스트림의 특징부터 알아보자.

1. 입출력 스트림은 선입선출 구조이다. 따라서 순차적(단방향)으로만 접근이 가능하다.

사실 양방향으로 다 되는 게 있긴 하다. (임의 접근 파일 스트림)

그래도 스트림은 기본적으로 순차적이다.

2. 입출력 스트림은 객체로 구성되어 있다. (자바에서는 기초형을 제외하고는 전부 다 객체이다.)

3.출력스트림과 입력스트림은 서로 연결해서 파이프라인 방식으로 만들 수 있다.

4. 지연 가능성이 있다. 출력을 하는데 CPU 속도와 프린터의 속도가 다를 수도 있으니까.

 

입출력 스트림은 다음과 같은 구조를 가지고 있다.

입출력 스트림은 크게 바이트 스트림과 문자 스트림으로 나눌 수 있다.

바이트 스트림은 일반적인 이진 데이터 파일을 처리할 때 사용한다. 이진스트림이라고도 한다.

문자 스트림은 한글, 영어 등 언어로 되어 있는 파일을 처리할 때 유용하다. 동영상, 이미지 등을 처리할 때에는 적합하지 않다.

 

InputStream, OutputStream, Reader, Writer 4개의 클래스는 모두 추상클래스이다.

왜 추상클래스일까?

입력 스트림의 경우 입력이 키보드에서 될 수도 있고 마우스에서 입력될 수도 있고

출력 스트림의 경우 모니터에 출력할 수도 있고 프린터에 출력할 수도 있고 네트워크를 통해서 다른 곳으로 출력할 수도 있고 다 다르다.

그렇기 때문에 입출력 메소드인 read(), write() 메소드를 구현할 수가 없다.

따라서 4개의 추상클래스를 만들어 놓고 사용할 때에는 구현된 자식 클래스를 이용한다.

 

입출력 스트림의 사용 과정은 다음과 같다.

1. 스트림을 열고

2. 스트림으로 처리하고

3. 스트림을 닫는다.

마지막에는 close() 메소드로 반드시 스트림을 닫아줘야 한다.

스트림을 여는 open()이라는 메소드는 없다. 스트림 객체를 생성하는 것 자체가 스트림을 여는 것이다.

 

먼저 바이트 스트림부터 살펴보자.

 

살펴보기 전에 각 클래스의 특징부터 설명하자면

바이트 스트림은 이미지나 동영상을 처리할 때 적합하다. 바이트 스트림은 모두 InputStream, OutputStream의 자식클래스들이다.

InputStream에서는 FileInputStream, DataInputStream, BufferedInputStream을 자주 사용한다.

파일을 다룰 때에는 FileInputStream을, 데이터 자체를 다루고 싶을 때에는 DataInputStream을 사용하면 된다.

데이터 자체를 다룬다는 말은 메모리에 저장되어 있는 그 데이터를 그대로 다루겠다는 것이다.  

만약에 int 타입의 1이라는 데이터가 있다면 00000000000000000000000000000001로 다루는 것을 말한다.

BufferedStream은 말 그대로 버퍼를 이용하는 것이다. 속도가 차이나기 때문에 버퍼에 잠시 보관하는 것이다.

이 중에서도 우리는 주로 FileInputStream과 BufferedInputStream을 자주 사용한다.

 

OutputStream에서는 FileOutputStream, BufferedOutputStream, PrintStream을 자주 쓰는데 PrintStream은 다른 것들과 특징이 조금 달라서 기억해 둘 필요가 있다. 그 특징은 뒤에서 배울 것이다.

 

이제 정말 바이트 스트림을 알아보자

 

InputStream과 OutputStream은 모든 자식 바이트 스트림에서 공통으로 사용하는 메소드(read(), write() 등)를 포함하는 바이트 스트림의 최상위 클래스이다.

InputStream 클래스에는 read(), OutputStream 클래스에는 write()라는 추상메소드가 있다. 앞에서 말했지만 이 두 메소드는 구현을 할 수가 없다.

 

 

InputStream (Java SE 10 & JDK 10 )

Reads all bytes from this input stream and writes the bytes to the given output stream in the order that they are read. On return, this input stream will be at end of stream. This method does not close either stream. This method may block indefinitely read

docs.oracle.com

오라클 홈페이지에서 Java API를 보면 InputStream은 추상클래스이고 Closeable의 구현 클래스인 것을 알 수 있다.

Closeable이라는 것은 자원을 close해야 한다는 것이다. Closeable의 자식클래스로는 AutoCloseable이 있다.

 

 

 

 

OutputStream (Java SE 10 & JDK 10 )

Flushes this output stream and forces any buffered output bytes to be written out. The general contract of flush is that calling it is an indication that, if any bytes previously written have been buffered by the implementation of the output stream, such b

docs.oracle.com

OutputStream도 마찬가지로 추상클래스이고 Closeable의 구현 클래스인 것을 볼 수 있다. 한 가지 다른 것은 Flushable의 구현 클래스라는 것이다. 뒤에서 배우겠지만 출력 스트림은 flush를 해줘야 한다.

 

InputStream의 주요 메소드

int available() 읽을 수 있는 바이트의 개수 반환.
void close() 입력 스트림을 닫는다.
abstract int read() 한 바이트를 읽고 그 읽은 것을 int로 리턴한다.
int read(byte[] b) 1바이트씩 읽어 배열에 집어넣고 몇 바이트 읽었는지 반환

데이터 읽을 게 있는지 없는지 체크하려면 available() 메소드를 사용한다. 헷갈릴 수도 있는데 이 메소드는 boolean 타입이 아니다. 기억해 두자.

InputStream객체.available() 해보고 0보다 클 경우에만 읽으면 된다.

read() 메소드가 특이한 것은 1byte를 읽어서 int 타입으로 반환한다는 것이다. int는 4바이트이다. 즉, 앞쪽 3바이트는 사용을 안 한다는 것이다. read() 메소드는 1바이트를 읽어서 읽은 내용을 반환하는데 int 타입으로 반환한다는 것 잊지 말자.

read() 메소드는 만약 더 이상 읽을 것이 없으면 -1을 반환한다. -1을 반환하기 위해서 read() 메소드의 반환 타입이 int 타입인 것이다.

 

OutputStream의 주요 메소드

void close() 출력 스트림을 닫는다.
void flush() 버퍼를 비운다.
abstract void write(int b) b를 바이트로 변환해서 1바이트를 쓴다.
void write(byte[] b) 바이트 배열 b를 쓴다.

flush()라는 것은 InputStream에는 없고 출력 스트림에만 있는 메소드이다. flush는 뭘까?

대부분의 운영체제나 JVM은 read(입력), write(출력)를 효율적으로 하기 위해서 버퍼를 사용한다.

컴퓨터가 처리하는 속도와 출력장치가 출력하는 속도를 비교하면 출력장치가 출력하는 속도가 훨씬 느리다.

예를 들어 출력 장치가 모니터라고 하자.

컴퓨터의 처리와 모니터가 출력하는 것은 속도 차이가 나기 때문에 출력을 효율적으로 하기 위해서 일단 버퍼에 써서 모아놨다가 나중에 한꺼번에 출력(write)을 한다.

System.in, System.out, System.err은 각각 표준 입력, 표준 출력, 표준 오류 장치이다. 표준 장치들은 시스템에서 바로 쓰기 때문에 효율적으로 사용하는 것이 좋으므로 버퍼를 사용한다. 따라서 내가 System.out.write(b) 하면 모니터에 바로 b가 나타나는 것이 아니라 버퍼에 모아둔다.

그 이후에 계속 써도 일단 버퍼에 쓴다. 그러고 나서 버퍼가 꽉 차면 그제서야 모니터에 출력한다.

그런데 나는 아직 버퍼가 꽉 차지는 않았지만 지금 바로 실제로 모니터에 출력을 하고 싶다면 그 때 사용하는 것이 flush 메소드이다. 그러면 버퍼에 있는 내용을 비우고 모니터에 바로 출력한다.

그런데 close() 메소드에는 flush()의 기능이 있어서 close()를 하면 자동으로 flush()가 된다. 더 이상 이 스트림 객체를 안 쓴다는 것이므로 끝내기 전에 버퍼에 있던 내용들은 출력하고 끝내야 하기 때문이다.

write() 메소드는 read() 메소드와 마찬가지로 1byte를 쓰지만 매개변수는 int 타입이다. 인자로 int 타입을 받아서 byte 타입으로 변환해서 1바이트를 쓴다.

 

import java.io.IOException;

public class IOStreamDemo {
    public static void main(String[] args) throws IOException {
        int b, len = 0;
        int ba[] = new int[100];
        System.out.println("---입력 스트림---");
        while ((b = System.in.read()) != '\n') {
            System.out.printf("%c(%d)", (char)b, b);
            ba[len++] = b;
        }
        System.out.println("\n\n---출력 스트림---");
        for(int i = 0; i < len; i++)
            System.out.write(ba[i]);
        System.out.flush();
    }
}

public static void main(String[] args) 뒤에 throws IOException이라고 예외 처리를 한 것을 볼 수 있는데

입출력 장치를 이용할 때에는 거의 IOException이 일어나기 때문에 반드시 예외 처리를 해 줘야 한다.

예외처리를 해주지 않으면 다음과 같이 컴파일 에러가 난다. 따라서 예외 처리는 선택이 아니라 필수이다.

예외처리를 해주지 않으면 컴파일 에러가 남

이 글은 예외처리 글이 아니라서 예외처리를 자세히 설명하면 너무 산으로 갈 것 같아서 예외처리는 따로 설명을 하겠다.

아무튼 입출력을 할 때에는 무조건 IOException에 대한 예외 처리를 해줘야 한다는 것이다.

 

이 코드에서는 InputStream, OutputStream 객체를 따로 만들지는 않았고 표준 입력 장치인 System.in과 표준 출력 장치인 System.out을 사용했다. 키보드로부터 입력을 받고 모니터에 출력을 하겠다는 것이다.

while문을 돌면서 키보드로부터 1바이트를 읽어서 ba 배열에 저장한다.

여기에서 주목해야 할 것은 1바이트를 읽어서 b에 저장하는데 b는 int 타입이라는 것이다.

잊지 말자 read() 메소드는 1바이트를 읽어서 int를 리턴한다.

그렇게 읽은 것은 ba 배열에 저장하고 나중에 write 메소드로 모니터에 출력한다.

마지막에 System.out.flush()를 해준다. flush()를 해주지 않으면 모니터에 출력이 안 될 수도 있다.

 

실행 결과는 다음과 같다.

위 코드의 실행 결과

728x90
728x90

스트림의 장점은 별도의 저장공간(변수)이 필요없이 사용 가능하다는 점이다.

하지만 스트림의 단점은 저장해두지 않았기 때문에 한 번 사용하면 재사용을 할 수 없다.

스트림은 스트림 데이터와 스트림 연산으로 이루어져있다.

스트림 데이터는 연속된 데이터이면 된다. 데이터의 집합체라면 전부 스트림 데이터로 만들 수 있다.

스트림의 특징: 조립성, 병렬화, 선언형

선언형이라는 것은 내가 구현할 필요가 없이 선언만 하면 된다는 것이다.

스트림 연산은 메소드의 인자가 전부 다 람다식 또는 메소드 참조이다.

스트림이 뭔지 아직 감이 안 올텐데 스트림 데이터가 들어오면 내가 필요한 것만 빼낼 수도 있고 몇 개 skip할 수도 있고 개수를 count할 수도 있고 평균도 낼 수 있고 매핑도 할 수 있고 집계도 할 수 있고 많은 것을 할 수 있다.

스트림 연산은 how 방식이 아니라 what 방식이다. 어떻게(how) 코딩할까 복잡하고 디테일한 것은 신경쓰지 않고 무슨(what)동작을 수행시킬지만 명시하면 된다. 디테일한 것들은 전부 구현체에 맡겨버리는 것이다.

 

컬렉션 vs 스트림

1. 처리 방식

스트림은 처리방식이 스트리밍이고 컬렉션은 처리방식이 다운로드 방식이다.

따라서 컬렉션은 한 번 만들어 놓으면 재사용할 수 있다. (물론 컬렉션 객체에서 iterator를 뽑아내면 iterator는 한 번 밖에 사용 못 한다.)

따라서 컬렉션은 저장공간이 필요하고 스트림 방식은 저장공간이 필요없다.

 

2. 반복 방식

컬렉션은 전부 외부 방식(Iterator 방식)이다. 요즘은 for문 대신 for~each문을 많이 사용하는데 for~each문은 iterator을 뽑아서 사용하지는 않지만 자세히 생각하면 궁극적으로는 외부 iteration(외부반복)이다. 바깥에서 누군가가 건드려줘야 한다.

하지만 스트림은 iterator가 필요 없다. 내부에서 다 알아서 해 준다.

 

3. 코드 구현

컬렉션은 명령형, 스트림은 선언형이다. 명령형이 훨씬 더 어렵다.

 

4. 원본 데이터 변경 여부

컬렉션은 원본데이터가 변경되지만 스트림은 원본데이터는 변경하지 않고 소비만 한다. 한 번 쓰고 끝나는거.

 

여기서도 느끼는 인생의 진리는 하나를 얻으면 하나를 포기할 수밖에 없다.

컬렉션은 다운로드를 해야하므로 저장공간이 크게 필요하지만 재사용이 가능하고

스트림은 시간에 따라 흘러가는 것이므로 별도의 저장공간이 필요하지 않지만 재사용이 불가능하다.

 

예제로 살펴보자.

 

예제1

다음 두 개의 코드는 똑같은 일을 수행한다.

랜덤으로 20개의 정수를 만들어서 리스트에 추가한 뒤 그 중에서 10보다 큰 수만 뽑아서 출력하는 일을 수행한다.

public class Stream1Demo {
    public static void main(String[] args) {
        List list = new ArrayList<>();
        List gt10 = new ArrayList<>();
        Random r = new Random();
        for(int i = 0; i < 20; i++) {
            list.add(r.nextInt(30)); //30 미만의 정수 20개를 list에 추가
        }
        for (int i : list)
            gt10.add(i);
        Collections.sort(gt10);
        System.out.println(gt10);
    }
}
public class Stream1Demo {
    public static void main(String[] args) {
        List list = new ArrayList<>();
        Random r = new Random();
        for(int i = 0; i < 20; i++) {
            list.add(r.nextInt(30)); //30 미만의 정수 20개를 list에 추가
        }
        list.stream().filter(i -> i > 10).sorted().forEach(x -> System.out.print(x + " "));
    }
}

그러니까 이 4줄과

for (int i : list)
	gt10.add(i);
Collections.sort(gt10);
System.out.println(gt10);

이 1줄은

list.stream().filter(i -> i > 10).sorted().forEach(x -> System.out.print(x + " "));

똑같은 일을 한다.

 

심지어 스트림을 사용한 코드는 gt10이라는 저장공간이 따로 필요하지 않으므로 gt10 리스트를 선언하는 부분도 필요 없으니까 코드가 4줄 더 짧은 것이다. 스트림을 사용하면 코드도 짧아지고 저장공간도 덜 든다는 것을 알 수 있다.

 

예제2

아래의 코드는 int 배열에서 5보다 큰 수들만 더해서 출력하는 것이다.

public class Stream2Demo {
    public static void main(String[] args) {
        int[] ia = {1, 6, 3, 9, 5, 4, 2};
        IntStream is = Arrays.stream(ia); //배열을 스트림으로 만들 때에는 Arrays.stream(배열) 이렇게 사용
        int sum = is.filter(i -> i > 5).sum();
        System.out.println(sum);
    }
}

실행결과는 아래와 같다.

배열을 스트림 객체로 만들고 싶을 때에는 Arrays 클래스의 static 메소드 stream(배열)를 사용하면 스트림 객체를 만들 수 있다. Arrays.stream(배열)의 인자로 int 타입의 배열이 들어가면 IntStream이 나오고 double 타입의 배열이 들어가면 DoubleStream이, long 타입의 배열이 들어가면 LongStream이 만들어진다. 이 외의 타입은 제네릭 타입의 Stream<T> 객체를 반환한다.

여기에 들어가면 Arrays 클래스의 메소드들을 볼 수 있다.

 

Arrays (Java SE 10 & JDK 10 )

Compares two int arrays lexicographically over the specified ranges. If the two arrays, over the specified ranges, share a common prefix then the lexicographic comparison is the result of comparing two elements, as if by Integer.compare(int, int), at a rel

docs.oracle.com

왠만하면 그냥 Stream보다는 IntStream 같이 어느 타입에 특화된 스트림을 사용하는 것이 좋다. IntStream에는 그냥 Stream에는 없는 int에 특화된, 정수이기 때문에 가능한, 편리한 메소드들이 있다.

728x90
728x90

Comparator 인터페이스의 유용한 정적 메소드 (static method)를 소개하고자 한다.

 

우선 comparing이라는 메소드의 인자는 Function 타입이다. 따라서 주로 람다식이나 메소드참조를 이용한다.

static 메소드는 메소드 이름 앞에 클래스 이름(또는 인터페이스 이름)까지 붙여서 외우는 것이 좋다.

Comparator.comparing(Function 객체) 이렇게 통째로 기억해 두자.

 

그리고 thenComparing이라는 메소드도 있다.

이름만 들어도 짐작이 가겠지만 어떤 기준으로 정렬한 후 그래도 똑같다면 이 기준으로 정렬하라는 것이다.

이 메소드의 인자는 Function 객체를 넣어도 되고 Comparator 객체를 넣어도 된다. 따라서 이 메소드도 인자로 람다식이나 메소드 참조가 주로 들어간다.

thenComparing 메소드는 이 뒤에도 계속 thenComparing(기준).thenComparing(기준) 이런 식으로 메소드 체이닝이 가능하다. thenComparing 메소드의 반환타입도 Comparator이기 때문이다.

import java.util.Arrays;
import java.util.Comparator;

public class PersonTest {
    public static void main(String[] args) {
        Person[] list = {new Person("홍길동", 30), new Person("황진이", 20), new Person("이몽룡", 30)};
        Arrays.sort(list, Comparator.comparing(Person::getAge).thenComparing(Person::getName));
        for (Person p : list) {
            System.out.println(p);
        }
    }
}
class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

나이순으로 정렬하되 나이가 같은 경우 이름순으로 정렬된 것을 볼 수 있다.

728x90
728x90

C/C++에는 함수포인터라는 개념이 있어 함수를 다른 함수로 전달하고 싶을 때에는 함수 포인터를 사용하면 된다.

그런데 자바는 C/C++보다 더 객체지향적인 언어이기 때문에 메소드(C/C++로 따지면 함수)는 무조건 클래스의 내부에 있어야 한다.

따라서 메소드를 전달하고 싶을 때도 (행위를 전달하고 싶을 때도) 메소드만 전달할 수는 없고 클래스를 만들어 그 안에 메소드를 정의하고 그 클래스의 객체를 생성해서 넘겨줘야 한다.

 

람다식은 객체를 생성하는 방법 중 하나이다. 람다식을 사용하면 객체가 생성된다.

 

자바에는 인터페이스라는 개념이 있다. 인터페이스는 추상클래스의 극단적인 형태라고 보면 된다.

인터페이스는 구현되어 있는 메소드가 없고 모두 추상메소드로만 이루어져 있다. (JDK 8부터 default 메소드와 static 메소드, JDK 9부터 private 메소드는 예외적으로 구현메소드가 가능하다)

인터페이스를 구현하는 클래스는 그 추상메소드를 반드시 구현해야 한다.

 

아래의 코드를 보면 speak()라는 아직 구현되지 않은 추상메소드가 있기 때문에 Person 객체를 생성할 수 없다고 컴파일 에러가 난다.

따라서 Person 타입의 객체를 생성하기 위해서는 speak 메소드를 구현해줄 필요가 있다.

 

그 방법으로 그 인터페이스를 구현하는 MyPerson이라는 클래스를 새로 만들고 그 클래스를 이용해 새로운 객체를 만드는 방법이 있다.

 

아래의 코드가 그 방법을 사용한 것이다.

interface Person{
    void speak();
}

public class PersonTest {
    public static void main(String[] args) {
        Person p = new MyPerson();
        p.speak();
    }
}
class MyPerson implements Person {
    public void speak(){
        System.out.println("어쩌구저쩌구");
    }
}

Person 인터페이스를 구현하는 MyPerson이라는 클래스를 만들어서 speak() 메소드를 구현해주었다.

그러나 이것은 너무 비효율적이고 번거롭다. 딱 한 번만 쓰고 말 객체인데도 그 객체를 만들기 위해서 Person 인터페이스를 구현하는 MyPerson이라는 클래스를 새로 만들었다. 이러한 비효율적인 문제는 무명객체를 만들어 해결할 수 있다.

 

new 인터페이스나 추상클래스(){정의되지 않은 메소드}; 

예를 들어 new Person(){메소드};

이렇게 그 중괄호 안에 구현되지 않은 메소드를 정의해주는 것이 무명객체를 생성하는 방법이다.

interface Person{
    void speak();
}

public class PersonTest {
    public static void main(String[] args) {
        Person p = new Person() {
            @Override
            public void speak() {
                System.out.println("어쩌구저쩌구");
            }
        };
        p.speak();
    }
}

이렇게 말이다.

 

무명객체 안에서 메소드를 오버라이딩하기 위해서는 메소드의 시그니쳐(매개변수 개수, 매개변수 타입, 메소드 이름)와 반환타입이 인터페이스에 정의된 메소드 헤더와 일치해야 한다. 참고로 인터페이스의 추상메소드는 앞에 public이라고 써있지 않아도 무조건 public이다. 따라서 public void speak(){} 하고 중괄호 사이에 speak 메소드가 수행할 코드를 적어주는 것이다.

 

하지만 무명객체를 선언하는 것도 번거롭고 코드가 꽤 길다. 단 한 번만 사용할 객체라 클래스의 이름은 선언하지 않았지만 클래스를 정의하는 것과 똑같이 메소드의 반환타입, 매개변수, 메소드 이름도 적어야 하고, 메소드 내용을 직접 정의해줘야 한다. 정말 꼭 필요한 것은 메소드 내부의 코드인데 말이다.

 

그래서 나온 것이 람다식이다. 람다식을 쓴다는 것은 인터페이스를 구현한 객체를 만드는 것이다. 예를 들어 Person이라는 클래스가 있을 때

new Person(); 한 것과 똑같이 객체를 하나 선언하는 것이다. 다만 그 객체는 이름이 없는 무명객체일 뿐이고 객체를 생성할 때 new를 사용하지 않았을 뿐이다.

 

람다식을 사용할 수 있는 조건은 제한적이다.

일단 람다식은 구현하는 인터페이스에 추상메소드가 딱 1개 있을 때에만 가능하다.

추상 클래스에 추상메소드가 딱 1개 있을 때도 안 된다. 꼭 인터페이스에 추상메소드가 1개 있어야 한다.

추상메소드가 1개만 있는 인터페이스를 함수형 인터페이스라고 한다.

람다식을 사용한다는 것은 구현되지 않은 메소드를 정의해주는 것이다. 그리고 그 결과로 객체가 생성된다.

어느 한 클래스의 인스턴스 메소드가 하나라도 구현되지 않았으면 객체를 생성할 수가 없다.

즉, 인터페이스는 객체 생성이 불가능하다.

그래서 그 메소드 내부를 구현해줘야 객체를 생성할 수 있는데 람다식은 편리성을 위해 어느 메소드를 구현하는지 메소드 이름을 쓰지 않고 그 메소드 내부 코드만 적어주는 방법이다.

그런데 아직 구현되지 않은 메소드가 여러 개라면 이 코드가 어느 메소드를 구현하는 코드인지 알 수가 없다.

따라서 인터페이스에 추상메소드가 딱 1개 있을 때에만 람다식을 쓸 수 있다. 만약 구현되지 않은 메소드가 2개 이상이라면 위의 두 가지 방식(익명 객체 생성, 구현 클래스 만들어서 객체 생성) 중 하나를 사용해야 한다.

 

다음과 같이 Person이라는 인터페이스가 있고 그 안에 speak()라는 추상메소드가 있다. Person 객체를 생성해서 그 객체가 speak라는 행위를 하도록 하고 싶을 때

interface Person{
    void speak();
}

public class PersonTest {
    public static void main(String[] args) {
        Person p1 = () -> System.out.println("어쩌구저쩌구");
        Person p2 = () -> System.out.println("배고프다");
        p1.speak();
        p2.speak();
    }
}

이렇게 람다식을 이용해서 간단히 Person 객체를 생성할 수 있다. 내가 원하는 코드로 speak() 메소드의 내부를 구현해주는 것이다.

 

 

 

따라서 람다식을 이용해서 speak 메소드의 내부를 구현해주는 것이다.

람다식 규칙은

(매개변수)->{메소드 내부 코드;}

인데 speak() 메소드의 매개변수는 없으므로 () 이렇게 빈 괄호를 쳐주는 것이다. 그리고 p1은 speak 메소드를 실행했을 때에 "어쩌구저쩌구"을 출력하고 싶고 p2는 "배고프다"를 출력하고 싶으므로 그렇게 speak 메소드를 정의한 것이다.

그렇게 speak 메소드를 정의한 후에 객체.speak()로 speak 메소드를 실행하는 것이다.

실행한 결과이다.

 

 

이렇게 우리가 speak 메소드를 정의해준대로 실행하는 것을 알 수 있다.

 

람다식에서 메소드 내부 코드를 감싸는 중괄호는 실행문이 한 문장일 때에는 생략 가능하다. 그리고 한 문장일 경우 세미콜론도 생략 가능하다.

위의 코드에서 speak 메소드의 내부 코드는 한 줄이므로 중괄호와 세미콜론을 생략했다. 끝에 있는 세미콜론은 System.out.println("어쩌구저쩌구") 문장의 종결을 의미하는 세미콜론이 아니라 Person p1 = new Person() 뒤에 찍는 그 세미콜론과 같다.

 

또한 메소드의 매개변수가 1개라면 인자를 감싸는 소괄호도 생략 가능하다.

interface ChangeNumber {
    int increase(int n);
}
public class LambdaTest {
    public static void main(String[] args) {
        ChangeNumber c = a -> a + 10;
        printChangeNumber(c, 7);
        printChangeNumber(c, 9);
        printChangeNumber(x -> x + 5, 10); //5를 증가하도록 하는 ChangeNumber 객체를 넘겨줌
    }
    static void printChangeNumber(ChangeNumber changeNumber, int num) {
        System.out.println(changeNumber.increase(num));
    }
}

printChangeNumber() 메소드를 총 3번 호출했고 첫 번째, 두 번째는 10씩 증가시켰고 세 번째 수는 5 증가시켰다.

ChangeNumber 인터페이스의 increase 메소드의 매개변수가 n 한 개이기 때문에 (a) -> a + 10 하지 않고 a -> a + 10 이렇게 괄호를 생략할 수 있다. 그리고 실행문이 한 개이기 때문에 a -> {a + b;} 이렇게 중괄호를 생략할 수 있다.

처음에는 ChangeNumber c = a -> a + 10; 이렇게 인터페이스 객체를 c에 저장해주었고

마지막에는 x -> x + 5라는 ChangeNumber 객체를 바로 printChangeNumber() 메소드의 인자로 넘겨주었다.

람다식에서 쓰는 매개변수의 이름은 상관이 없다. a를 써도 되고 x를 써도 되고 number -> number + 10 이렇게 해도 된다.

만약 람다식을 쓰지 않았더라면

interface ChangeNumber {
    int increase(int n);
}
public class LambdaTest {
    public static void main(String[] args) {
        ChangeNumber c = new ChangeNumber() {
            @Override
            public int increase(int n) {
                return n + 10;
            }
        };
        printChangeNumber(c, 7);
        printChangeNumber(c, 9);
        printChangeNumber(new ChangeNumber() {
            @Override
            public int increase(int n) {
                return n + 5;
            }
        }, 10); //10을 5 만큼 증가하도록 하는 ChangeNumber 객체를 넘겨줌
    }
    static void printChangeNumber(ChangeNumber changeNumber, int num) {
        System.out.println(changeNumber.increase(num));
    }
}

코드가 이렇게 길어졌을 것이다. 이 코드는 람다식을 쓴 위의 코드와 정확히 똑같은 코드이다. 그런데 훨씬 길고 복잡하다. 람다식을 쓰면 이렇게 불필요한 코드를 줄일 수 있다.

 

 

그러면 우리가 () -> System.out.println("배고프다")를 쳤을 때 그게 Person 객체를 생성하고 싶은 것인지 어떻게 아는 걸까? 매개변수가 없는 추상메소드를 가지는 또 다른 인터페이스가 있으면 저게 어느 인터페이스를 구현하는 객체인지 어떻게 아는 걸까?

우리가 a -> a + 10 했을 때 그게 ChangeNumber 객체를 생성하고 싶은건지 어떻게 아는 것일까?

() -> System.out.println("배고프다"), a -> a + 10 앞에 각각 Person p, ChangeNumber c 이렇게 타입을 선언했기 때문이다. 따라서 Person 타입의 p라는 변수를 선언함으로 이 람다식이 Person 타입의 객체를 생성한다는 것을 추론할 수 있고 ChangeNumber 타입의 c라는 변수를 선언함으로써 ChangeNumber 타입의 객체를 생성한다는 것을 추론할 수 있다.

만약에 Person p = ()->System.out.println("어쩌구저쩌구");에서 앞에 Person p =을 붙이지 않는다면 다음과 같이 컴파일 에러가 발생한다. 타입을 전혀 추론할 수 없기 때문이다.

Person p = ()-&amp;gt;System.out.println("어쩌구저쩌구");로 하지 않았을 때

 

그러면 변수 선언이 아니라 람다식을 메소드의 인자로 쓸 경우에는 타입을 적어주지 않았는데 어떻게 타입을 추론하는 것일까?

메소드는 이미 정의되어 있고 메소드에는 매개변수의 타입까지 정의되어 있으므로 인자로 넣었을 때 그 메소드의 매개변수 타입으로 추론할 수 있다.

personSpeak 메소드를 정의할 때 personSpeak 메소드의 매개변수는 한 개이고 그 타입은 Person 타입이라는 것을 8~10줄에 정의해놓았다. 따라서 메인메소드에서 personSpeak 메소드를 호출해서 사용할 때에 인자로 람다식을 넣어줘도 그 람다식이 Person 타입의 객체라는 것을 추론할 수 있다.

다시 한 번 말하지만 람다식을 사용한다는 것은 객체를 생성하는 것이다. 무명객체를 생성해서 personSpeak 메소드의 인자로 넘기는 것이다. 그러니까 6번째 줄의 ()->System.out.println("안녕)"은  Person 타입의 객체를 생성한 것이다.

 

사실 이렇게 우리가 정의한 인터페이스 객체를 만드는 경우보다는 Java에 원래 있는 인터페이스를 우리가 구현하는 경우가 대부분이다. 정렬을 할 때에 비교기준을 정의해줄 때 사용하는 Comparator, 스트림에서의 인자인 Predicate, Consumer, Supplier, Operator 등은 모두 추상메소드가 1개인 함수형 인터페이스이다. 따라서 람다식을 매우 유용하게 이용할 수 있다. 이는 다음 포스팅에서 자세히 설명하겠다.

 

728x90

'Java' 카테고리의 다른 글

[Java] 입출력 스트림  (0) 2020.01.29
[Java] Stream (스트림)  (0) 2020.01.21
[Java] Comparator 인터페이스  (0) 2020.01.21
[Java] String, StringBuilder, StringBuffer의 차이점  (0) 2019.08.30
[Java] 자바(Java) 언어의 특징  (0) 2019.08.28
728x90

자바에 문자열을 담을 수 있는 클래스로 String, StringBuilder, StringBuffer라는 클래스들이 있는데 약간씩 차이가 있다.

일단 가장 큰 차이점은 String은 문자열을 변경할 수 없지만 StringBuilder, StringBuffer는 문자열을 변경 가능한 variable 클래스이다.

 

String 클래스는 final 클래스이다.

클래스가 final이라는 말은 String 클래스를 상속받아 파생클래스를 만들 수가 없다는 것이다.

 

또한 String 클래스는 Comparable 인터페이스를 구현(implement)한 클래스이다. Comparable 인터페이스의 구현 클래스는 compareTo라는 메소드를 반드시 구현해야 하므로 String 클래스 안에는 compareTo 메소드가 있다.

두 객체를 비교하기 위해서는 compareTo 메소드가 있어야 한다.

 

예를 들어

class Person {

}
public class PersonTest {
    public static void main(String args[]){
        Person p1 = new Person();
        Person p2 = new Person();
    }
}

 

이렇게 선언을 하고 p1과 p2를 비교하는 것은 불가능하다. 비교 기준이 없기 때문이다.

만약에 p1 객체와 p2 객체를 비교하고 싶다면

 

class Person implements Comparable {
    int age;
    String name;

    public Person(int age, String name) {
        this.age = age;
        this.name = name;
    }

    @Override
    public int compareTo(Person p) {
        return age - p.age;
    }

    @Override
    public String toString() {
        return name + "(" + age + ")";
    }
}
public class PersonTest {
    public static void main(String[] args) {
        Person p1 = new Person(15, "철수");
        Person p2 = new Person(12, "영희");
        Person p3 = new Person(17, "순이");
        ArrayList people = new ArrayList<>();
        people.add(p1);
        people.add(p2);
        people.add(p3);
        Collections.sort(people);
        System.out.println(people);
    }
}

이렇게 Comparable 인터페이스를 상속받아 compareTo 메소드를 통해 비교기준을 제시해야 한다.

이 코드에서는 나이를 비교 기준으로 제시하였다.

 

이 코드를 실행한 결과는 다음과 같다.

 

compareTo 메소드를 나이 순으로 정의하고 정렬한 결과

 

String 클래스는 compareTo 메소드가 구현되어 있으므로 비교 기준이 설정되어 있다. 그 비교 기준은 알파벳 순서 오름차순이고 소문자보다 대문자가 우선이다. 예를 들어 C, c, d, D가 있고 이것을 정렬한다면 C, D, c, d가 된다. String 클래스는 final 클래스이기 때문에 이러한 비교기준을 변경할 수 없다.

 

String 객체는 정적인 문자열이다. 즉, 변경 불가능한 고정 문자열이라는 뜻이다.

'String 객체도 + 연산자 써서 문자열 변경 가능하던데?' 할 수 있다. 하지만

String s = "a"; //①

이렇게 해 놓고

s = s + "b"; //②

하면 ①에서의 객체 s와 ②에서의 객체 s는 전혀 다른 객체이다. ①에서의 그 객체에 "b"를 붙여서 "ab"가 되는 것이 아니라 ①에서의 "a" 문자열 객체는 가비지가 되어버리고 "ab"라는 새로운 객체를 만들어 s라는 변수가 새로 만들어진 객체 "ab"를 가리키도록 하는 것이다. 그러나 StringBuffer와 StringBuilder는 그렇지 않다.

 

StringBuilder와 StringBuffer는

StringBuffer sb = "a"; //또는 StringBuilder sb = "a" //③

이렇게 해 놓고

sb.append("b"); //④

해도 ③에서의 객체와 ④에서의 객체는 같은 객체이고 그 객체에 "b"라는 문자열을 append 한 것 뿐이다. 객체를 새로 만드는 과정이 이루어지지 않는다. (참고로 String은 고정문자열이니 당연히 append 메소드가 없고 StringBuilder와 StringBuffer는 가변 문자열이므로 append 메소드가 있다. 대신 + 연산자로 문자열을 붙이는 것은 불가능하다.)

 

따라서 문자열이 자주 바뀌어야 한다면 String보다는 StringBuilder나 StringBuffer를 쓰는게 시간적으로나 메모리 공간적으로나 훨씬 효율적이다.

 

시간적으로 StringBuilder/StringBuffer가 더 효율적이라는 사실은 실제로 이 코드를 돌려보면 알 수 있다.


public class StringTest {
    public static void main(String[] args) {
        String s = "a";
        long before = System.nanoTime();
        appendString(s);
        long after = System.nanoTime();
        System.out.println("String을 썼을 때에 걸린 밀리초:     " + (after - before));
        StringBuilder sb = new StringBuilder("a");
        before = System.nanoTime();
        appendStringBuilder(sb);
        after = System.nanoTime();
        System.out.println("StringBuffer를 썼을 때에 걸린 밀리초: " + (after - before));
    }
    static void appendString(String s){
        for(int i = 0; i < 1000; i++){
            s = s + "a";
        }
    }
    static void appendStringBuilder(StringBuilder s){
        for(int i = 0; i < 1000; i++){
            s.append("a");
        }
    }
}

 

위의 코드를 돌린 결과는 아래와 같다.

String을 사용했을 때와 StringBuilder를 사용했을 때 걸리는 시간

 

String을 사용했을 때, StringBuilder를 사용했을 때보다 약 500배 정도 더 걸리는 것을 볼 수 있다.

 

다만 StringBuilder와 StringBuffer의 차이점은 StringBuffer는 동기화 기능이 있어서 멀티 스레딩을 해야 할 때 사용한다. 따라서 멀티 스레딩을 쓰지 않는다면 StringBuilder를 사용하는 것이 일반적이다.

 

 

 

728x90
728x90

-플랫폼 독립적이다 (Platform Independent)

 

플랫폼이란? 운영체제 + 하드웨어를 말함

플랫폼 독립적이라는 것의 의미는 한 번 짠 그 코드는 이 컴퓨터에서도 돌아가고 운영체제와 CPU와 전혀 상관없이 다른 컴퓨터에서도 똑같이 잘 돌아간다는 것을 의미한다. CPU와 운영체제가 서로 다른 컴퓨터에서도 말이다.

 

따라서 플랫폼 종속적인 프로그래밍 언어들은 운영체제에 따라, 하드웨어(CPU)에 따라서는 그 코드가 돌아가지 않을 수도 있다는 불편함이 있다. 이것을 이식성(portability)이 낮다고 말한다. (이식성이 높은 프로그램은 이 컴퓨터에서도, 저 컴퓨터에서도 잘 돌아가는 것을 말한다.)

 

①하드웨어에 따라 왜 다르냐?

 

하드웨어 아키텍처마다 사용하는 기계어 종류가 다름. ex) 인텔 CPU와 AMD CPU는 사용하는 기계어가 다름.

그런데 실행코드는 기계어로 되어 있음. 기계(CPU)에게 일을 시켜야 하는데 CPU는 기계어 밖에 못 알아들으니까.

따라서 당연히  다를 수밖에 없음

 

②운영체제에 따라 왜 다르냐?

 

 ②-⑴운영체제가 사용하는 API 형식이 다름.

 API(Application Programming Interface)란? 응용프로그램이 운영체제의 기능을 사용하고 싶을 때 (운영체제에게 일을 시키고 싶을 때) 프로그래밍 언어를 통해서 운영체제에게 특정 기능을 요청할 수 있는 함수라고 할 수 있음.

 응용프로그램도 운영체제가 제공하는 API 함수를 이용해서 간접적으로 하드웨어에 접근할 수 있는거임. 운영체제만이  하드웨어를 직접 제어하기 때문.

 

따라서 컴퓨터 하나는 운영체제가 리눅스이고 또 다른 하나는 윈도우즈라면 당연히 운영체제가 제공하는 API 형식이 다르므로 프로그램에서 사용하는 함수 이름이 달라지고 (운영체제가 달라도 API 함수 이름이 같으면 상관없겠지만.) 코드도 달라질 수밖에 없다.

 

 ②-⑵운영체제마다 메모리를 관리하는 기법이 다름.

 프로그램을 실행하려면 운영체제는 메모리(메인메모리, RAM)를 사용하게 되는데 운영체제마다 메모리를 관리하는 기법이 다르다.

 

이러한 이유들 때문에 대부분의 프로그래밍 언어들은 플랫폼 종속적인 것이다. 그러나 자바는 그렇지 않다.

 

자바 프로그램을 자바 컴파일러가 컴파일 하면 바이트코드가 만들어지는데 이 바이트코드는 자바 플랫폼(JVM)에서 돌아간다.

여기서 헷갈리지 말아야 할 것은 '자바 플랫폼'은 플랫폼 '종속적'이다. 따라서 실제로 오라클 홈페이지에서 JDK를 다운받을 때에 mac OS용, Windows용, Linux용이 따로 있다. 하지만 이렇게 플랫폼 종속적인 JVM만 설치되어 있으면 자바 프로그램은 플랫폼 독립적이다. (JVM도 사실상 운영체제라고 볼 수 있다. 메모리를 관리하는 능력이 있기 때문) Windows용이든 mac OS용이든 Linux용이든 자바 플랫폼 위에서는 똑같은 코드가 멀쩡히 다 돌아갈 수 있는 것이다.

 

 

-C/C++ vs Java

①실행 환경

C/C++ 프로그램을 작성하여 컴파일러를 통해서 목적코드가 만들어지면 목적코드 안에 printf나 cout 같은 함수가 있을 수 있다. 그런데 이러한 함수는 내가 만든 함수가 아니다. 따라서 이 함수 호출 부분을 라이브러리 안에 있는 함수와 연결시켜줘야 하는데 그것을 링크(link)라고 한다. C/C++의 경우 프로그램 실행 전에 링크를 하므로 정적인 링킹(static linkng)이다. 그 후 운영체제 위에서 프로그램이 돌아갈 때 printf나 cout 같은 함수들이 링크되어있기 때문에 그 함수가 있는 라이브러리가 프로그램 안에 포함되어 있는 것이다. 따라서 자연히 프로그램의 크기도 커지게 된다.

 

그러나 자바는 다르다. 자바는 정적인 링킹이 아니라 동적인 로딩을 한다.

자바는 자바 컴파일러가 바이트코드로 만들면(컴파일 방식) 링크 과정 없이 그 바이트 코드를 JVM에서 바로 실행한다. (인터프리터 방식으로) 그 프로그램을 실행하는 동안에 printf 메소드가 필요하다면 JVM의 클래스로더가 그 때 링크를 시켜주는 것이다. 필요할 때에 가져오고 반납하고 하는 것이다. 실행 도중에. 그렇다고 자바 프로그램의 크기가 작은 것은 아니다. 방식이 달라서 크기 비교는 어렵다.

 

 

②메모리 관리 기법

C/C++에서는 응용프로그램에서 메모리가 필요하면 함수를 사용해 메모리를 직접 할당할 수 있다. C에서의 malloc 함수, C++에서의 new 함수가 그것이다. 운영체제의 도움을 받아서 프로그램에서 필요한 만큼의 메모리를 할당받는 것이다.

참고로 운영체제의 가장 큰 역할은 자원 관리자(resource manager)이다. CPU, 메모리 등의 자원을 관리하는 것이다.

따라서 C 프로그램에서 이만큼의 메모리가 필요하니까 메모리를 할당해달라고 운영체제에게 요청을 하면 운영체제가 메모리에 여유가 있는지 확인하고 여유가 있다면 그만큼의 메모리를 할당해준다. 그러면 그 할당 받은 메모리를 사용해서 프로그램이 실행되는 것이다. 또한 그렇게 할당받은 메모리를 다 사용했다면 C에서는 free 함수, C++에서는 delete 함수를 호출하여 운영체제에게 자원을 반납해야 한다.

 

그러나 자바는 그렇지 않다. 자바 프로그램은 JVM 위에서 돌아가는데 JVM은 운영체제로부터 메모리를 미리 할당받아 놓고 자기가 관리를 한다. 따라서 자바에서 new 함수를 호출하면 운영체제의 도움이 아니라 JVM의 도움으로 메모리를 할당받아서 사용하는 것이다. 그런데 여기서 또 하나 C/C++과 다른 점이 있다. 

 

자바에는 new 함수는 있지만 free(또는 delete) 함수는 없다는 것이다. 자바에는 가비지 컬렉터(Garbage Collector)가 있기 때문에 알아서 다 쓰고 난 메모리 자원을 수거한다. 따라서 자바에서는 메모리 누수 걱정을 하지 않아도 된다는 장점이 있다. 하지만 가비지 컬렉터가 가비지를 모으려고 돌아다니면 그 때 프로그램의 속도가 느려질 수 있다는 단점도 있다. 그래서 JDK 11부터는 속도를 개선시키기 위해서 ZGC(A Scalable Low-Latency Garbage Collector)라는 새로운 가비지 컬렉터를 사용한다. (약자가 왜 ZGC냐면 처음에는 Zero Latency Garbage Collector로 하려고 했으나 현실적으로 Zero Latency는 불가능하기 때문에 약자는 ZGC이지만 풀이는 다르다고 한다.)

 

728x90

+ Recent posts