리스트와 배열이 순차적으로(sequentially) 해당 요소 값을 구하는 반면, 맵은 key를 통해 value를 얻는 방식이다. Map인터페이스를 구현한 자료형에는 HashMap, LinkedHashMap, TreeHashMap이 있다. 자바의 맵 중 가장 기본적인 HashMap에 대해 알아보자.
① put
② get
③ containsKey
④ remove
⑤ size
⑥ keyset
맵의 모든 key를 반환한다. 이 때 모아진 key는 Set 자료형으로 반환된다.
key만 따로 모아 리스트로 관리하고 싶다면, Set자료형을 List형으로 바꾸어 사용하면 된다. Set자료형에 대해서는 아래에서 다루겠다.
Set인터페이스를 구현한 자료형에는 HashSet, TreeSet, LinkedHashSet이 있다. 이 중 HashSet에 대해 알아보기로 하자. 먼저 예시를 보자.
위의 예시에는 이상한 점이 두 가지 있다. "l" 문자가 하나 없어졌다는 것과 순서가 뒤죽박죽 되었다는 것이다. 그 이유는 Set자료형이 중복을 허용하지 않고, 순서가 없는(Unordered) 자료형이기 때문이다. 당연히 리스트나 배열에서 지원하는 인덱싱 연산도 지원하지 않는다. 이러한 점은 맵과 유사하다. 집합 자료형은 주로 중복을 제거하기 위해 사용된다.
아래와 같이 s1, s2집합을 생성하고 두 집합 간의 연산을 수행해보자. 참고로, 제네릭스에는 원시자료형을 쓸 수 없기 때문에 Wrapper클래스를 사용해주어야 한다.
① 교집합
② 합집합
③ 차집합
① add
② remove
콘솔 출력은 이미 잘 알고 있기 때문에(System.out.println) 콘솔 입력에 대해서만 다루도록 하겠다. 사용자가 입력한 문자열을 얻기 위해서는 System.in을 이용해야 한다. 여기서 System.in은 InputStream의 객체이다.
InputStream의 read메서드는 1byte만을 읽을 수 있기 때문에 몇 글자를 입력하든 한 글자만 인식한다. 더구나 a를 입력하고 결과를 출력하면 뜬금없이 a에 해당하는 아스키코드 값인 97이 나온다. 이는 자바에서 문자열을 유니코드 즉, 2바이트로 해석하기 때문이다. 문자열을 1바이트로 해석하려다 보니 어쩔 수 없이 대응되는 아스키코드 값이 출력된 것이다. 문자도 하나밖에 입력 받을 수 없고, 문자로 출력도 안 되다 보니 실용성이 낮다.
사실 문자를 여러 개 입력 받으려면 위의 예시에서 int a를 byte[] a로 바꿔주면 된다.
하지만, 여전히 문자로 출력되지 않고 아스키코드 값이 출력된다. 이를 해결하기 위해서 InputStreamReader를 쓸 수 있다. InputStreamReader 객체를 생성하기 위해선 생성자 입력으로 InputStream객체가 필요하다.
이제는 byte배열이 아닌 char배열을 사용할 수 있게 되었다. 당연히 abc를 입력하면 abc가 출력된다. 하지만 여전히 아쉬운 부분은 고정적으로 3byte 스트림만 읽을 수 있다는 것이다. 사용자가 엔터키를 입력할 때까지 사용자의 입력을 받아들이기 위해 BufferReader를 사용할 수 있다.
BufferReader를 사용하려면 InputStreamReader객체가 필요하다. 즉, BufferReader는 생성자 입력으로 InputStreamReader 객체를 받고, InputStreamReader는 생성자 입력으로 InputStream 객체를 받는 것이다. 물론 이를 한 줄로 쓰는 방법도 있다. 둘 중 선호하는 방법을 사용하라.
헷갈림 방지를 위해 보충 설명하면, InputStream은 byte를 읽고, InputStreamReader는 character를 읽고, BufferedReader는 String을 읽는다. BufferReader에서 문장을 입력 받는 메서드는 readLine()으로 띄어쓰기를 포함한 문자열을 원하는 대로 입력 받을 수 있다.
단, String타입으로만 입력 받을 수 있기 때문에 정수나 실수형으로 변환이 필요하다면, Wrapper클래스를 사용하여 형변환해야 한다. 한 가지 더 중요한 사항은 문자열을 입력 받을 때는 반드시 예외처리를 해주어야 한다는 것이다. 예외처리를 하지 않으면, 컴파일 에러가 난다. 이는 IOException이 CheckedException 즉, 컴파일에러를 일으키는 ExceptionClass이기 때문이다.
우리는 throws 키워드를 통해 함수를 호출한 쪽에 예외처리를 떠넘길 수 있다고 배웠다. 위 예시와 같이 main 함수에 throws 키워드를 사용하면 main함수를 호출하는 JVM이 그 책임을 떠맡게 되는데, 이는 예외를 잘 처리한 것은 아니다. JVM이 예외를 떠맡게 되면, 콘솔에 예외가 발생한 이유를 출력하는 것으로 예외를 처리한다. 에러 메시지는 사용자가 읽기에 불편하기 때문에 개발자가 직접 예외를 처리하는 것이 좋다.
단순히 사용자 입력을 받는 것 치곤 너무 복잡하고 어렵다고 느낀다면 다음의 간단한 방법을 사용할 수도 있다. 바로 Scanner클래스를 이용하는 것이다.
Scanner클래스도 생성자의 입력으로 InputStream의 객체인 System.in을 필요로 한다. Scanner의 내장메서드에는 단어 하나를 입력 받는 next(), 정수를 입력 받는 nextInt(), 문자열을 입력 받는 nextLine()이 있다. String만 받을 수 있는 BufferReader 와 달리 다양한 타입을 받을 수 있고 사용법도 훨씬 간단하다. 다만, 실행시간 차이가 두 배 가까이 난다고 한다. 되도록이면 BufferReader를 쓰는게 효율적이겠으나, 간단한 프로그램이라면 코드의 가독성을 높이기 위해 Scanner를 사용할 수 있다.
① FileOutputStream
이번에는 파일에 내용을 적어보자.
② FileWriter
④ 파일에 내용 추가하기
FileInputStream클래스를 이용하여 파일을 읽을 수 있다. 다만 byte배열을 이용하여 파일을 일기 때문에 길이를 지정해주어야 한다는 단점이 있다.
b의 값을 출력할 때 문자열로 변환하여 출력하기 위해 즉시 반환 객체의 생성자 입력으로 b를 넣어주었다. 고정 길이를 지정해야 하고, String으로 변환도 해야 하는 문제를 해결하기 위해 BufferReader와 FileReader를 사용할 수 있다.
BufferReader 의 내장메서드인 readLine은 더 이상 읽을 Line이 없으면 null을 반환하기 때문에 이를 이용하면 길이를 지정해줄 필요가 없어진다.