[220122] java PushbackReader
Table of Contents
1 동기
clojure
의 read-string
은 내부적으로 java.io.PushbackReader
를 사용한다. 이것이 무엇을 하는지 알아보는 시간을 가지자.
2 도큐먼트
https://docs.oracle.com/javase/7/docs/api/java/io/PushbackReader.html
java.io.PushbackReader
는 말그대로 PushBack이 가능한 Reader이다. 읽은 값을 다시 맨 뒤에 붙여서 다음에 또 읽을 수도 있고, 그대로 스킵할 수도 있다.
관련 메서드만 보면 대충 어떤 일을 하는 지 알 수 있다.
read()
: 하나의 문자를 읽는다.unread(inc )
: 문자하나를 푸시백 버퍼 앞에 복사하여 푸시백을 수행한다.skip(long n)
: 문자들을 스킵한다.
3 사용법
3.1 Without Pushback
import java.io.CharArrayReader; import java.io.IOException; import java.io.PushbackReader; public class PushBackReaderTest { public static void main(String[] args) throws IOException { char buf[] = {'a', 'b', 'c', 'd'}; PushbackReader pr = new PushbackReader(new CharArrayReader(buf)); while(true) { int c = pr.read(); if (c == -1) break; System.out.println((char) c); } } } // returns // a // b // c // d
이제 pushback을 수행해보자.
3.2 With Pushback
import java.io.CharArrayReader; import java.io.IOException; import java.io.PushbackReader; public class PushBackReaderTest { public static void main(String[] args) throws IOException { char buf[] = {'a', 'b', 'c', 'd'}; PushbackReader pr = new PushbackReader(new CharArrayReader(buf)); while(true) { int c = pr.read(); if (c == -1) break; if ('a' == (char) c) { pr.unread('A'); } System.out.println((char) c); } } } // returns // a // A // b // c // d
4 clojure는 어디서 PushBackReader를 사용하는가?
clojure에는 EdnReader.java
에서 readString
메소드를 보자.
static public Object readString(String s, IPersistentMap opts){ PushbackReader r = new PushbackReader(new java.io.StringReader(s)); return read(r, opts); } static public Object read(PushbackReader r, IPersistentMap opts){ return read(r,!opts.containsKey(EOF),opts.valAt(EOF),false,opts); }
그리고 이 두 함수는 아래 아주 긴 read
함수로 수렴한다.
static public Object read(PushbackReader r, boolean eofIsError, Object eofValue, boolean isRecursive, Object opts) { try { for(; ;) { // while true 라고 보면됨 int ch = read1(r); // whitespace 제거 while(isWhitespace(ch)) ch = read1(r); if(ch == -1) { if(eofIsError) throw Util.runtimeException("EOF while reading"); return eofValue; } if(Character.isDigit(ch)) { Object n = readNumber(r, (char) ch); if(RT.suppressRead()) return null; return n; } IFn macroFn = getMacro(ch); if(macroFn != null) { Object ret = macroFn.invoke(r, (char) ch, opts); if(RT.suppressRead()) return null; //no op macros return the reader if(ret == r) continue; return ret; } if(ch == '+' || ch == '-') { // 잠깐 다음 토큰을 가져와본다. int ch2 = read1(r); // 그 값이 숫자라면? 숫자가 두자리수가 있을 수 있으니 // 1. 일단 pushback을 한다. // 2. readNumber로 숫자를 읽는다. if(Character.isDigit(ch2)) { unread(r, ch2); // 여기서 pushback을 사용한다. 숫자인 경우 pushback을 한다. Object n = readNumber(r, (char) ch); // ch는 부호로 쓰일 것이다. if(RT.suppressRead()) return null; // 다 읽으면 그것은 숫자이므로 그대로 리턴한다. // +123 , -443 return n; } // 숫자가 아니면 숫자가 아니므로 다시 읽은것을 돌려놓는다. unread(r, ch2); } String token = readToken(r, (char) ch, true); if(RT.suppressRead()) return null; return interpretToken(token); } } catch(Exception e) { if(isRecursive || !(r instanceof LineNumberingPushbackReader)) throw Util.sneakyThrow(e); LineNumberingPushbackReader rdr = (LineNumberingPushbackReader) r; //throw Util.runtimeException(String.format("ReaderError:(%d,1) %s", rdr.getLineNumber(), e.getMessage()), e); throw new ReaderException(rdr.getLineNumber(), rdr.getColumnNumber(), e); } }
여기서 우리가 눈여겨 볼 코드는 (위에 적어놓기도 했지만) 숫자를 읽는 코드다.
if(ch == '+' || ch == '-') { // 잠깐 다음 토큰을 가져와본다. int ch2 = read1(r); // 그 값이 숫자라면? 숫자가 두자리수가 있을 수 있으니 // 1. 일단 pushback을 한다. // 2. readNumber로 숫자를 읽는다. if(Character.isDigit(ch2)) { unread(r, ch2); // 여기서 pushback을 사용한다. 숫자인 경우 pushback을 한다. Object n = readNumber(r, (char) ch); // ch는 부호로 쓰일 것이다. if(RT.suppressRead()) return null; // 다 읽으면 그것은 숫자이므로 그대로 리턴한다. // +123 , -443 return n; } unread(r, ch2); }
나는 분명 readNumber
에서 사용하는 ch
는 부호로 쓰일 것으로 예상했다. 아직 이 글을 쓰는 순간에도 해당 함수를 들여다보지 않았다.
이제 들여다보자.
static private Object readNumber(PushbackReader r, char initch) { StringBuilder sb = new StringBuilder(); sb.append(initch); for(; ;) { int ch = read1(r); if(ch == -1 || isWhitespace(ch) || isMacro(ch)) { unread(r, ch); break; } sb.append((char) ch); } String s = sb.toString(); Object n = matchNumber(s); if(n == null) throw new NumberFormatException("Invalid number: " + s); return n; }
역시 맨 처음에 들어갈 부호로써 +,- 사용되는 것을 알 수 있다. sb.append(initch)
5 RT.java 의 readString
REPL에서 read-string
을 해보았을 것이다. 단순 문자열을 클로저의 세계로 인도해주는 녀석이 RT#readString
이다.
static public Object readString(String s, Object opts) { PushbackReader r = new PushbackReader(new StringReader(s)); return LispReader.read(r, opts); }
한번 진짜인지 봐볼까? github으로 clojure 소스코드를 받아서 다음처럼 바꿔보았다.
static public Object readString(String s, Object opts) { System.out.println("하하하 여기다!!"); PushbackReader r = new PushbackReader(new StringReader(s)); return LispReader.read(r, opts); }
실행해보자. clojure package에 main.java
를 실행하라.
public static void main(String[] args) { RT.init(); REQUIRE.invoke(CLOJURE_MAIN); MAIN.applyTo(RT.seq(args)); }
이후 생성된 REPL에 read-string
을 수행하자.
user=> (read-string "A") 하하하 여기다!! A
6 EdnReader.java
EdnReader에도 있다. 한번 테스트 해보자.
static public Object readString(String s, IPersistentMap opts){ System.out.println("HI THIS IS Edn/READSTRING"); PushbackReader r = new PushbackReader(new java.io.StringReader(s)); return read(r, opts); }
repl에서 다음처럼 사용하면 된다.
user=> (require '[clojure.edn :a edn]) user=> (edn/read-string "A") HI THIS IS READSTRING A