반응형

Using Nashorn


Nashorn은 자바 프로그램에서 사용할 수 있는 자바스크립트 엔진이다. Nashorn은 커맨드라인에서 사용할 수 있는데 jjs라는 커맨드로 실행시킬 수 있다.


$ jjs

jjs> print('Hello World');


이번 튜토리얼은 자바코드에서 nashorn을 사용하는 것에 포커스를 맞춘다. 다음은 간단한 HelloWorld 예제이다.


자바에서 자바스크립트를 수행하기 위해서 nashorn 스크립트 엔진을 먼저 생성해야 한다.

ScriptEngine scriptEngine = new ScriptEngineManager().getEngineByName("nashorn");
scriptEngine.eval("print('Hello world');");


자바 스크립트 코드는 위와 같이 직접 문자열로 전달할 수도 있고 FileReader를 통해 js 스크립트 파일을 전달 할 수 있다.

ScriptEngine scriptEngine = new ScriptEngineManager().getEngineByName("nashorn");
scriptEngine.eval(new FileReader("test.js"));


Nashorn 스크립트는 ECMAScript 5.1 기반이지만 곧 ECMAScript 6을 지원할 것이다.


Nashorn의 현재 전략은 ECMAScript 스펙을 따르는 것이다. 


Nashorn의 다양한 언어와 API 확장은 ECMAScript 표준에 정의되어 있다. 그러나 먼저 자바와 자바스크립트간의 코드가 어떻게 동작하는지를 살펴보자.


Invoking Javascript Functions from Java


나스호른에서는 자바코드로부터 스크립트 파일에 정의된 자바스크립트 함수를 직접 호출할 수 있다. 우리는 자바 오브젝트를 함수의 인자로 전달할 수 있고 함수의 호출결과를 전달 받을 수 있다.


다음은 자바에서 호출할 자바스크립트 함수의 예이다.

var func1 = function(name) {
print("Hi there from javascript," + name);
return "greetings from javscript";
}

var func2 = function(object) {
print("JS class definition : " + Object.prototype.toString().call(object));
}


먼저 함수를 호출하기 위해서는 자바 스크립트 엔진을 invocable로 캐스팅해야된다. Invocale 인터페이스는 NashornScriptEngine에 구현되어 있으며 주어진 이름의 자바스크립트 함수를 호출할 수 있는 invokeFunction 함수가 정의되어 있다.

ScriptEngine scriptEngine = new ScriptEngineManager().getEngineByName("nashorn");
scriptEngine.eval(new FileReader(filePath));

Invocable invocable = (Invocable)scriptEngine;

Object result = invocable.invokeFunction("func1", "Peter Parker");
System.out.println(result);
System.out.println(result.getClass());


// Hi there from javascript,Peter Parker

// greetings from javscript

// class java.lang.String


코드 실행의 결과는 콘솔에 세줄로 출력된다. 


이제 두번째 함수를 자바오브젝트를 전달해서 호출해보자.

invocable.invokeFunction("func2", new Date());

// JS class definition : [object java.util.Date]


invocable.invokeFunction("func2", LocalDateTime.now());

// JS class definition : [object java.time.LocalDateTime]


invocable.invokeFunction("func2", new Person());

// JS class definition : [object com.kakao.warriv.process.application.mapping.NashornTest$Person]


자바 오프젝트의 어떤 타입 정보도 손실없이 자바스크립트에 전달된 것을 알 수 있다. 스크립트나 네이티브하게 JVM에서 실행되기 때문에 우리는 자바 API 또는 외부 라이브러리를 완전히 나스호른에서 이용할 수 있다.


Invoking Java Methods from Javascript


자바스크립트로부터 자바 메소드를 호출하는 것도 매우 쉽다. 먼저 static한 자바 메소드를 하나 정의해보자.

public static String func1(String name) {
System.out.format("Hi there from Java, %s\n", name);
return "greetings from java";
}

자바 클래스들은 Java.type API로 자바스크립트에서 호출될 수 있다. 이것은 자바 코드의 import와 비슷하다. 자바 타입에 정적으로 fun1이 정의되어 있기 때문에 우리는 인스턴스를 먼저 생성할 필요가 없다.

var NashornTest = Java.type("mapping.NashornTest");
var result = NashornTest.func1("John Doe");
print(result);


// Hi there from Java, John Doe

// greetings from java


나스호른에서 자바 메소드를 호출했을때 자바오브젝트 - 네이티브 자바스크립트 타입간의 어떻게 타입 컨버젼을 처리할까? 다음의 간단한 예제를 따라해보자.


다음은 단순히 메소드 파라미터의 클래스를 출력하는 자바 메소드이다.

public static void func2(Object object) {
System.out.println(object.getClass());
}

타입 컨버젼을 이해하기 위해 다음과 같은 자바스크립트 코드들을 실행해보자.


MyJavaClass.fun2(123);

// class java.lang.Integer


MyJavaClass.fun2(49.99);

// class java.lang.Double


MyJavaClass.fun2(true);

// class java.lang.Boolean


MyJavaClass.fun2("hi there")

// class java.lang.String


MyJavaClass.fun2(new Number(23));

// class jdk.nashorn.internal.objects.NativeNumber


MyJavaClass.fun2(new Date());

// class jdk.nashorn.internal.objects.NativeDate


MyJavaClass.fun2(new RegExp());

// class jdk.nashorn.internal.objects.NativeRegExp


MyJavaClass.fun2({foo: 'bar'});

// class jdk.nashorn.internal.scripts.JO4


프리미티브 자바스크립트 타입은 적절한 자바 랩퍼 클래스로 컨버팅된다. 대신에 네이티브 자바스크립트 오브젝트는 인터널 어댑터 클래스로 표현된다. jdk.nashorn.internal 클래스들의 목적이 변경을 위한 것임을 명심하자. 그래서 클라이언트 코드에 이러한 클래스에 대항하는 프로그램을 하지 않아야 한다.



ScriptObjectMirror


네이티브 자바스크립트 오브젝트를 자바에 전달할때 ScriptObjectMirror 클래스를 활용해 내부의 자바스크립트 오브젝트를 표현한다. ScriptObjectMirror 맵 인터페이스를 구현하고 있으며 내부에 jdk.nashorn.api를 패키징하고 있다. 이 패키지의 클래스들은 클라이언트 코드에서 사용될 수 있다.


다음 샘플은 오브젝트 파라미터 타입을 ScriptObjectMirror로 변경한다.. 그래서 전달된 자바스크립트 오브젝트로부터 몇가지 정보를 추출할 수 있다.

public static void func3(ScriptObjectMirror mirror) {
System.out.println(mirror.getClassName() + ":" + Arrays.toString(mirror.getOwnKeys(true)));
}


오브젝트 hash가 이 메소드에 전달되었을때 해당 프로퍼티들을 자바에서 접근할 수 있다.

NashornTest.func3({foo : "foo", bar : "bar"})


// Object: [foo, bar]


우리는 또한 자바에서 자바스크립트의 멤버 함수를 호출할 수 있다. 먼저 firstName과 lastName 프로퍼티를 가진 Person 자바스크립트 타입을 정의해보자.

function Person(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;

this.getFullName = function() {
return this.firstName + " " + this.lastName;
}
}


자바 스크립트 메소드 getFullName은 ScriptObjectMirror callMember() 메소드에 의해 호출할 수 있다.

public static void func4(ScriptObjectMirror person) {
System.out.println("Full Name is: " + person.callMember("getFullName"));
}


새로운 인물을 자바 메소드에 전달한 다음 그 결과를 콘솔에서 확인해보자.

var person1 = new Person("Peter", "Parker");
NashornTest.func4(person1);


// Full Name is: Peter Parker


Language Extensions


나스호른은 ECMAScript 표준에서 제공하는 다양한 언어와 API 확장판을 지원한다.


Typed Arrays


네이티브 자바스크립트 배열은 untyped이다. 나사호른은 자바스크립트에서 typed 자바 배열을 가능하게 한다.

var IntArray = Java.type("int[]");

var array = new IntArray(5);

array[0] = 5;
array[1] = 4;
array[2] = 3;
array[3] = 2;
array[4] = 1;

try {
array[5] = 23;
} catch (e) {
print(e.message); // Array index out of range: 5
}

array[0] = "17";
print(array[0]); // 17

array[0] = "wrong type";
print(array[0]); // 0

array[0] = "17.3";
print(array[0]); // 17

int[] 배열은 실제 자바 int 배열처럼 동작한다. 그러나 추가적으로 나스호른은 묵시적은 타입 변경을 수행한다. 문자열은 꽤 핸디가 있는 자동 변환이 일어난다.


Collections and For Each


배열대신에 자바 컬렉션을 사용할 수 있다. 먼저 Java.type을 통해 새로운 인스턴스를 생성하자.

var ArrayList = Java.type("java.util.ArrayList");
var list = new ArrayList();

list.add("a");
list.add("b");
list.add("c");

for each (var el in list) print(el);

// a

// b

// c


컬렉션이나 배열을 탐색하기 위해 나스호른은  for each이 제공한다. 이것은 자바의 foreach같이 수행된다.


여기 또 다른 foreach 예제이다. HashMap을 사용해보자.

var map = new java.util.HashMap();
map.put("foo", "var1");
map.put("bar", "var2");

for each (var e in map.keySet()) print(e); // foo, bar
for each (var e in map.values()) print(e); // var1, var2


Lambda expressions and Streams


모든 사람은 람다와 스트림을 사랑한다. 그래서 나스호른은 지원한다. 비록 ECMAScript의 람다와 스트림은 자바 8보다 부족하지만 우리는 람다를 사용할 수 있다.

var list2 = new java.util.ArrayList();
list2.add("ddd2");
list2.add("aaa2");
list2.add("bbb1");
list2.add("aaa1");
list2.add("bbb3");
list2.add("ccc");
list2.add("bbb2");
list2.add("ddd1");

list2.stream()
.filter(function(el) {
return el.startsWith("aaa");
})
.sorted()
.forEach(function(el) {
print(el);
});

// aaa1, aaa2


Extending classes


자바 타입은 단순히 Java.extend 확장으로 확장될 수 있다. 다음 예제를 보면 스크립트에서 멀티 스레드 코드조차 실행할 수 있음을 알 수 있다.

var Runnabe = Java.type("java.lang.Runnable");

var Printer = Java.extend(Runnabe, {
run : function() {
print("printed from a seperate thread.")
}
});

var Thread = Java.type("java.lang.Thread");
new Thread(new Printer()).start();

new Thread(function() {
print("printed from another therad.");
}).start();

// printed from a separate thread

// printed from another thread


Parameter overloading


메소드와 함수는 point notation 또는 square braces notation에 의해 호출될 수 있다.

var System = Java.type('java.lang.System');
System.out.println(10); // 10
System.out["println"](11.0); // 11.0
System.out["println(double)"](12); // 12.0


옵션 파라미터 println(doublue)을 전달함으로써 오버로드된 정확한 메소드를 호출할 수 있다.


Java Beans


명시적인 getter나 setter를 호출하는 대신에 프로퍼키의 이름으로 자바 빈의 프로퍼티를 접근할 수 있다.

var Date = Java.type("java.util.Date");
var date = new Date();
date.year += 1900;
print(date.year); // 2016


Function Literals


단순한 한 라인짜리 함수는 블레이스를 생략할 수 있다.

function sqr(x) x * x;
print(sqr(3)); // 9


Binding properties


두 다른 오브젝트 프로퍼티들은 서로 바운드될 수 있다.

var o1 = {};
var o2 = { foo: 'bar'};

Object.bindProperties(o1, o2);

print(o1.foo); // bar
o1.foo = 'BAM';
print(o2.foo); // BAM


Trimming strings


문자열을 트림할 수 있다.

print("   hehe".trimLeft());            // hehe
print("hehe ".trimRight() + "he"); // hehehe


Whereis


이 경우 어느 위치에 실행을 하는지 알 수 있다.

print(__FILE__, __LINE__, __DIR__);


Import Scopes


때때로 자바 패키지의 한꺼번에 임포트하는 것은 유용하다. 이런 경우 우리는 JavaImporter 클래스를 이용할 수 있다. 

var imports = new JavaImporter(java.io, java.lang);
with (imports) {
var file = new File(__FILE__);
System.out.println(file.getAbsolutePath());
// /path/to/my/script.js
}


Convert arrays


java.util과 같이 몇가지 패키지는 Java.type 이나 JavaImporter를 사용하지 않고 직접 참조할 수 있다.

var list = new java.util.ArrayList();
list.add("s1");
list.add("s2");
list.add("s3");


다음 코드의 자바의 리스트를 네이티비 자바스크립트 오브젝트 배열로 변경하는 것이다.

var jsArray = Java.from(list);
print(jsArray); // s1,s2,s3
print(Object.prototype.toString.call(jsArray)); // [object Array]


또 다른 방식은 다음과 같다.

var javaArray = Java.to([3, 5, 7, 11], "int[]");


Calling Super


자바스크립트에서 오버라이드된 멤버를 접근하는 것은 일반적으로 어색하다. ECMAScript에서는 자바처럼 super가 지원되지 않기 때문이다. 나스호른에서는 이러한 사항을 지원한다.


먼저 자바 코드에 슈퍼타입을 정의해보자.

public class SuperRunner implements Runnable {
@Override
public void run() {
System.out.println("super run");
}
}


다음은 자바스크립트에서 SuperRunner를 오버라이드할 것이다.

var SuperRunner = Java.type("mapping.SuperRunner");
var Runner = Java.extend(SuperRunner);

var runner = new Runner() {
run: function() {
Java.super(runner).run();
print('on my run');
}
}
runner.run();

// super run

// on my run


Java.super 확장을 이용해서 SuperRunner.run()의 슈퍼 메소드를 호출할 수 있다.


Loading scripts


추가적인 스크립트를 로딩하는 것은 매우 쉽다. 우리는 load함수를 통해 로컬 또는 리모트 스크립트를 로딩할 수 있다.


Underscore.js는 많은 웹프론트에서 사용된다. 나스호른에서 Underscore를 사용해보자.

load('http://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.6.0/underscore-min.js');

var odds = _.filter([1, 2, 3, 4, 5, 6], function (num) {
return num % 2 == 1;
});

print(odds); // 1, 3, 5


외부 스크립트도 같은 자바스크립트 컨텍스트에서 평가된다. 그래서 우리는 underscore의 변수들을 직접 접근할 수 있다. 스크립트 로딩을 잠재적으로 우리의 코드에 영향을 줄 수 있다. 각 변수명이 오버래핑될 수 있기 때문이다.


이러한 문제는 로딩 스크립트 파일을 새로운 글로벌 컨텍스트로 전달하면 된다.

loadWithNewGlobal('script.js');


여기까지다.


이 가이드가 나스호른 자바스크립트 엔진을 여행하는데 도움이 되었으면 좋겠다. 


Keep on coding!


출처 : http://winterbe.com/posts/2014/04/05/java8-nashorn-tutorial/

반응형

'JAVA > Pleasure' 카테고리의 다른 글

Java - Stream  (0) 2016.01.14
Java - Lamda expression  (1) 2016.01.13
NIO란?  (0) 2015.04.17
Stack과 heap의 차이점  (0) 2015.01.12
반응형

How streams work


스트림은 엘리먼트의 처리순서를 표현하고 이러한 엘리먼트들에 다른 종류의 연산을 제공한다.

List<String> myList = Arrays.asList("a1", "a2", "b1", "c2", "c1");

myList
.stream()
.filter(source -> source.startsWith("c"))
.map(String::toUpperCase)
.sorted()
.forEach(System.out::println);

스트림 연산은 intemediate 또는 terminal이다. Intermediate 연산은 stream을 리턴하여 세미콜론 없이 메소드 체이닝형식으로 사용할 수 있다. Terminal 연산은 void이거나 stream이 아닌 값을 리턴한다. 위의 예에서 filter, map, sorted는 intermediate 연산인 반면 forEach는 terminal 연산이다. 스트림에서 사용할 수 있는 모든 연산이 궁금하다면 javadoc을 살펴보도록 하자.


대부분의 스트림 연산은 람다 표현식을 파라미터로 받을 수 있는데 이는 functional interface로 연산의 동작을 명시한다. 대부분의 연산은 non-interfering 하며 stateless해야 한다. 이것은 무슨 의미인가?


함수가 간섭받지 않아야 한다는 의미는 스트림에 주어진 데이터소스를 변경할 수 없다는 것이다. 위의 예에서 보이듯 컬렉션의 엘리먼트를 추가하거나 삭제하는 람다식을 제공할 수 없다는 것이다.


또한 함수가 상태가 없어야 한다. 위 예제에서는 변경가능한 변수가 없거나 실행중에 변경할 수 있는 외부 스코프의 상태가 없다는 의미를 한다.


Different kind of streams


스트림은 다양한 데이터 소스 특히 컬렉션으로 부터 생성될 수 있다. List나 Set은 새로운 메소드 stream과 parallelStream을 제공한다. 병렬 스트림은 멀티 스레드에서 해당 오퍼레이션을 실행할 수 있는 능력을 가지고 있는데 이러한 내용은 뒷부분에서 다루도록 하겠다.

Arrays.asList("a1", "a2", "a3")
.stream()
.findFirst()
.ifPresent(System.out::println);

list의 stream 메소드를 호출함으로써 정상적인 오브젝트 스트림을 전달받을 수 있다. 그러나 스트림을 만들기 위해서 반드시 컬렉션을 생성해야 하는 것은 아닌다. 다음 코드를 보자.

Stream.of("a1", "a2", "a3")
.findFirst()
.ifPresent(System.out::println);

Stream.of()를 이용해서 오브젝트 레퍼런스의 묶음으로부터 스트림을 생성할 수 있다.


게다가 자바 8에는 기본형 int, long, double을 위한 특별한 종류의 스트림이 제공되는데 IntStream, LongStream 그리고 DoubleStream이다.


IntStream은 일반적인 for-loop을 IntStream.range()로 대체 할 수 있다.


IntStream.range(0, 4)
.forEach(System.out::println);


프리미티브 스트림은 일반적인 스트림과 같지만 다음과 같은 차이 점이 있다.

프리미티브 스트림은 특별한 람다 표현식을 사용할 수 있다. 예를 들어 Function 대신에 IntFunction을 Predicate 대신에 IntPredicate를 사용할 수 있다. 그리고 프리미티브 스트림에서는 추가적인 터미널 어그리게이트 오퍼레이션을 지원하는데 sum과 average이다. 

Arrays.stream(new int[] {1, 2, 3})
.map(n -> 2 * n + 1)
.average()
.ifPresent(System.out::println);

때떄로 일반적인 스트림을 기본형 스트림으로 변경하는 것은 편리하다. 이러한 목적으로 오브젝트 스트림은 특별한 매핑 오브젝트 오퍼레이션인 mapToInt, mapToLong과 mapToDouble을 제공한다.

Arrays.asList("a1", "a2", "a3").stream()
.map(s -> s.substring(1))
.mapToInt(Integer::parseInt)
.max()
.ifPresent(System.out::println);

기본형 스트림은 또한 mapToObj를 통해 객체 스트림으로 변경할 수 있다.

IntStream.range(1,4)
.mapToObj(n -> "a" + n)
.forEach(System.out::println);


Processing Order


이제 우리는 스트림을 어떻게 만들고 또 어떻게 동작하는지에 대해 배웠다. 이제 스트림 오퍼레이션이 어떻게 처리되는지 더욱 자세히 살펴보자.


인터미디어트 오퍼레이션의 중요한 특징은 laziness이다. 터미널 오퍼레이션이 없는 예제를 살펴보자.

Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> {
System.out.println(s);
return true;
});


위의 코드를 실행하면 아무런 것도 콘솔에 출력되지 않는 것을 알 수 있다. 이유는 인터미디어트 오퍼레이션은 터미널 오퍼레이션이 존재할 때 실행되기 때문이다.


이제 위의 예제에 forEach 터미널 오퍼레이션을 추가해보자. 

Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> {
System.out.println("filter : " + s);
return true;
})
.forEach(s -> System.out.println("forEach : " + s));


filter : d2

forEach : d2

filter : a2

forEach : a2

filter : b1

forEach : b1

filter : b3

forEach : b3

filter : c

forEach : c


결과의 순서에 놀랄것이다. 원시적인 접근은 스트림에서 모든 오퍼레이션은 스트림의 모든 엘리먼트는 수평적으로 실행된다는 것이다. 하지만 대신에 각 엘리먼트는 수직적으로 체인으로 이동했다. 첫번째 문자열 d2는 필터에 전달된 다음 forEach에 전달되고, 그런 다음에서 a2가 처리되었다.


이러한 특성은 각 엘리먼트에 대한 실제적인 오퍼레이션의 수를 줄일수 있다. 다음을 살펴보자.

Stream.of("d2", "a2", "b1", "b3", "c")
.map(s -> {
System.out.println("map : " + s);
return s.toUpperCase();
})
.anyMatch(s -> {
System.out.println("anyMatch : " + s);
return s.startsWith("A");
});


map : d2

anyMatch : D2

map : a2

anyMatch : A2


anyMacth 연산이 true를 리턴하자마자 predicate가 주어진 입력 엘레먼트에 적용된다. 이것은 두번째 element "A2"가 전달되었을 때이다. 스트림 체인은 버티컬하게 수행되기 때문에 이 경우 맵은 단지 두번만 실행된다. 그래서 스트림은 모든 엘레먼트를 매핑하는 대신에 최소한의 매핑만 수행하게 된다.


Why order matters


다음 예제는 두개의 인터미디어트 연산 map과 filter 그리고 forEach 터미널 오퍼레이션으로 구성되어 있다. 

Stream.of("d2", "a2", "b1", "b3", "c")
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
})
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("A");
})
.forEach(s -> System.out.println("forEach: " + s));

map: d2

filter: D2

map: a2

filter: A2

forEach: A2

map: b1

filter: B1

map: b3

filter: B3

map: c

filter: C



모든 문자열에 대해서 map과 filter가 수행되었으며 forEach는 한번 호출된 것을 확인할 수 있다.


만약 수행횟수를 줄이고 싶다면 필터를 체인의 시작으로 옮기면 된다.

Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("a");
})
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
})
.forEach(s -> System.out.println("forEach: " + s));

filter: d2

filter: a2

map: a2

forEach: A2

filter: b1

filter: b3

filter: c



이제 맵은 한번만 수행될 것이며 파이프라인의 성능은 입력 엘레먼트 수에 비례하여 훨씬 빨라질 것이다. 복잡한 체인 연산을 수행할 시 명심해두자.


이제 위의 예제를 확장해보자. 정렬을 추가하였다.

Stream.of("d2", "a2", "b1", "b3", "c")
.sorted((s1, s2) -> {
System.out.printf("sort : %s, %s\n", s1, s2);
return s1.compareTo(s2);
})
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("a");
})
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
})
.forEach(s -> System.out.println("forEach: " + s));

정렬은 특별한 종류의 인터미디어트 연산이다. 정렬동안 state를 유지해야 함으로 이러한 연산을 stateful operation이라고 부른다.


이러한 예제의 결과로 다음과 같은 콘솔 출력을 나타나는 것을 확인할 수 있다.


sort:    a2; d2

sort:    b1; a2

sort:    b1; d2

sort:    b1; a2

sort:    b3; b1

sort:    b3; d2

sort:    c; b3

sort:    c; d2

filter:  a2

map:     a2

forEach: A2

filter:  b1

filter:  b3

filter:  c

filter:  d2



첫번째 정렬 오퍼레이션은 전체 입력 컬렉션에 대해서 수행된다. 다시 말하자면 sorted는 horizontablly하게 실행된다. 그래서 이 경우 다수의 조합에 따라 8번 호출된다.


다시 한번 정렬 체인의 성능을 옵티마이즈 해보자.

Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("a");
})
.sorted((s1, s2) -> {
System.out.printf("sort : %s, %s\n", s1, s2);
return s1.compareTo(s2);
})
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
})
.forEach(s -> System.out.println("forEach: " + s));

filter: d2

filter: a2

filter: b1

filter: b3

filter: c

map: a2

forEach: A2


이 예제는 sorted가 호출되지 않는다. 이유는 필터가 입력 엘레먼트를 하나의 엘레먼트로 줄였기 때문이다. 성능은 굉장히 개선될 것이다.


Reusing Streams


자바 8 스트림은 재활용할 수 없다. 어떤 터미널 오퍼레이션이 호출되면 스트림은 닫힌다.

Stream<String> stream =
Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> s.startsWith("a"));

stream.anyMatch(s -> true);
stream.noneMatch(s -> true);


같은 스트림의 결과로 anyMatch를 호출한 후에 다시 nonMatch를 호출하면 오류가 발생한다.


Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed

at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229)

at java.util.stream.ReferencePipeline.noneMatch(ReferencePipeline.java:459)

at com.kakao.chub.stream.TestStream.main(TestStream.java:15)

at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)

at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)

at java.lang.reflect.Method.invoke(Method.java:483)

at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)


이러한 한계를 극복하기 위해 모든 터미널 오퍼레이션을 위한 새로운 스트림을 생성해야 한다.

그러나 우리는 stream supplier를 만들어서 이미 세팅된 인터미디어트 연산을 새로운 스트림으로 만들 수 있다.

Supplier<Stream<String>> supplier =
() -> Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> s.startsWith("a"));

supplier.get().anyMatch(s -> true);
supplier.get().noneMatch(s -> true);

각 get 메소드를 호출할 때마다 새로운 스트림이 생성되고 원하는 터미널 연산에 전달 할 수 있다.


Advanced Operations


스트림은 많은 수의 다른 오퍼레이션들을 제공한다. 우리는 이미 filter, map과 같은 매우 중요한 스트림 연산에 대해서 배웠다. 이제 더 복잡한 오퍼레이션 collect, flatMap 및 reduce에 대해서 살펴보자.


이번 섹션의 대부분의 코드 샘플은 사람에 대한 목록을 이용해서 진행해보겠다.

public class Person {
private String name;
private int age;

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

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}    

List<Person> persons = Arrays.asList(
new Person("Max", 18),
new Person("Peter", 23),
new Person("Pamela", 23),
new Person("David", 12)
);


Collect


Collect는 명백하게 유용한 터미널 오퍼레이션이다. 스트림의 각 엘리먼트를 다른 종류의 결과로 만들기 위해 트랜스폼하기 위해 사용된다. Collect는 4개의 다른 오퍼레이션으로 구성된 Collector를 적용할 수 있다. : supplier, accumulator, combiner 그리고 finisher. 처음에는 매우 복잡하게 들릴지 모르나 자바 8의 지원하는 매우 좋은 기능중의 하나이다. 


매우 단순한 유스케이스로 시작해보자.

List<Person> filteredPersons = persons.stream()
.filter(person -> person.name.startsWith("P"))
.collect(Collectors.toList());

System.out.println(filteredPersons);

[Person{name='Peter', age=23}, Person{name='Pamela', age=23}]


스트림의 엘레먼트를 리스트로 구성하는 것은 매우 쉽다는 것을 알 수 있다. list 대신에 set으로 구성하고 싶다면 Collectors.toSet()을 사용하면 된다.


다음 예제는 나이로 모든 사람들을 그룹핑하는 예제이다.

Map<Integer, List<Person>> personsByAge = persons.stream()
.collect(Collectors.groupingBy(person -> person.age));

personsByAge.forEach((age, personGroup) -> {
System.out.println(String.format("age : %d, persons : %s", age, personGroup));
});

age : 18, persons : [Person{name='Max', age=18}]

age : 23, persons : [Person{name='Peter', age=23}, Person{name='Pamela', age=23}]

age : 12, persons : [Person{name='David', age=12}]


Collectors는 정말 다양하다. 또한 스트림에 각 엘레먼트에 대한 어그리게이션 연산도 수행할 수 있는데 예를 들면 평균도 낼 수 있다.

Double average = persons.stream()
.collect(Collectors.averagingInt(person -> person.age));

System.out.println(average);


만약 더 포괄적인 통계에 관심이 있다면 summarizing collector는 빌트인 summary statistics 오브젝트를 반환한다. 이를 통해 우리는 쉽게 최소, 최대 평균, 카운트, 합계를 구할 수 있다.

IntSummaryStatistics statistics = persons.stream()
.collect(Collectors.summarizingInt(person -> person.age));

System.out.println(statistics);

IntSummaryStatistics{count=4, sum=76, min=12, average=19.000000, max=23}


다음 예제는 모든 사람을 하나의 문자열로 조인하는 것이다.

String phrase = persons.stream()
.filter(person -> person.age > 18)
.map(person -> person.name)
.collect(Collectors.joining(" and ", "In Germany ", " are of legal age."));

System.out.println(phrase);

In Germany Peter and Pamela are of legal age.


조인 컬렉터에는 구분자와 옵션으로 prefix와 suffix를 적용할 수 있다.


스트림 엘레먼트를 맵으로 변경하기 위해서 우리는 키와 값을 명시해줘야 한다. 키는 유니크해야 하며 그렇지 않을 경우 IllegalStateException이 발생한다. 또한 오류 발생시 머지를 하기 위해 추가적인 연산을 전달할 수 있다.

Map<Integer, String> map = persons.stream()
.collect(
Collectors.toMap(
person -> person.age,
person -> person.name,
(name1, name2) -> name1 + ":" + name2
));

System.out.println(map);

{18=Max, 23=Peter:Pamela, 12=David}


이제 우리는 매우 강력한 빌트인 컬렉터에 대해서 알게 되었다. 이제 우리 자신만의 컬렉터를 만들어 보자. 우리는 | 파이프 문자열로 구분된 대문자로된 하나의 문자열로 만들어 보고 싶다. 우리는 Collector.of를 통해 새로운 컬렉터를 만들 수 있다. 컬렉터에 전달할 수 있는 재료는 다음과 같다. : a supplier, an accumulator, a combiner and a finisher.

Collector<Person, StringJoiner, String> personCollector =
Collector.of(
() -> new StringJoiner(" | "),
((stringJoiner, person) -> stringJoiner.add(person.name.toUpperCase())),
((stringJoiner1, stringJoiner2) -> stringJoiner1.merge(stringJoiner2)),
StringJoiner::toString
);

String names = persons.stream().collect(personCollector);

System.out.println(names);

MAX | PETER | PAMELA | DAVID


자바에서 문자열은 immutable 하기 때문에 StringJoiner와 같은 헬퍼 클래스가 필요하다. supplier는 적당한 구분자와 StringJoiner을 초기화하고, accumulator는 StringJoiner에 각 사람의 대문자 이름을 추가하기 위해 사용된다. combiner는 어떻게 두개의 StringJoiner를 하나로 머지할지를 알고 있다. 마지막 스텝은 finisher는 StringJoiner로부터 원하는 문자열을 만들어 낸다.


FlatMap


우리는 map 오퍼레이션을 통해 스트림의 객체가 어떻게 다른 타입의 객체로 변경할 수 있는지에 대해서 배웠다. Map은 한계가 있는데 모든 오브젝트가 정확히 하나의 다른 오브젝트에 매핑되어야 한다는 것이다. 그러나 만약 하나의 오브젝트가 다른 둘이상의 오브젝트로 변경하기를 원한다면 어떻게 할 것인가? 이러한 경우를 위해 flatMap이 있다.


FlatMap은 스트림의 각 엘레먼트를 다른 오브젝트의 스트림으로 변경한다. 그래서 각 오브젝트는 0,1 또는 둘 이상의 다른 오브젝트로 변경될 수 있다. 이러한 스트림들은 flatMap 오퍼레이션에서 리턴된 스트림에 포함된다.


FlatMap의 동작을 살펴보기 전에 우리는 적절한 type hierarchy가 필요하다.

public class Foo {
String name;

List<Bar> bars = new ArrayList<>();

Foo(String name) {
this.name = name;
}
}
public class Bar {
String name;

Bar(String name) {
this.name = name;
}
}


다음은 스트림에 가진 지식을 이용하여 두개의 오브젝트를 초기화해보자.


List<Foo> foos = new ArrayList<>();


// create foos

IntStream

    .range(1, 4)

    .forEach(i -> foos.add(new Foo("Foo" + i)));


// create bars

foos.forEach(f ->

    IntStream

        .range(1, 4)

        .forEach(i -> f.bars.add(new Bar("Bar" + i + " <- " + f.name))));


이제 우리는 3개의 bar로 구성된 3개의 foo리스트를 가지게 되었다.


FlatMap에는 스트림을 리턴하는 함수를 적용할 수 있다. 그리서 각 foo의 bar 객체를 접근하기 위해 적당한 함수를 전달해보자.


foos.stream()
.flatMap(foo -> foo.bars.stream())
.forEach(bar -> System.out.println(bar.name));

Bar1<-Foo1

Bar2<-Foo1

Bar3<-Foo1

Bar1<-Foo2

Bar2<-Foo2

Bar3<-Foo2

Bar1<-Foo3

Bar2<-Foo3

Bar3<-Foo3


보시다시피 3개의 foo 오브젝트 스트림은 7개의 바 오브젝트 스트림으로 변경되었다.


결론적으로 위의 코드 예제는 스트림 단일 파이프라인을 단순화 할수도 있다.

IntStream.range(1,4)
.mapToObj(i -> new Foo("Foo" + i))
.peek(foo -> IntStream.range(1,4).forEach(i ->new Bar("Bar" + i + "<-" + foo.name)))
.flatMap(foo -> foo.bars.stream())
.forEach(bar -> System.out.println(bar.name));


FlatMap은 자바 8에서 소개된 Optional 클래스에서도 매우 유용하다. Optionals flatMap 연산은 다른 타입의 optional 객체를 리턴한다. 그래서 다루기 힘든 null 체크를 방지하기 위해 유용하게 사용할 수 있다.


다음과 같은 계층적인 구조를 생각해보자.

public class Outer {
Nested nested;
}
public class Nested {
Inner inner;
}
public class Inner {
String foo;
}


NullPointerException을 방지 하기 위해 outer 인스턴스에 inner foo 문자열에 널체크를 한다면 다음과 같이 될 것이다.

Outer outer = new Outer();

if (outer != null && outer.nested != null && outer.nested.inner != null) {
System.out.println(outer.nested.inner.foo);
}


같은 동작을 optionals flatMap을 사용하면 다음과 같다.


Optional.of(outer)
.flatMap(o -> Optional.ofNullable(o.nested))
.flatMap(n -> Optional.ofNullable(n.inner))
.flatMap(i -> Optional.ofNullable(i.foo))
.ifPresent(System.out::println);

각 flatMap 호출은 원하는 객체를 래핑한 Optional이 리턴된다. Optional에는 원하는 오브젝트가 있거나 null이 들어있을 것이다.


Reduce


Reduce 오퍼레이션은 스트림의 모든 엘레민트들을 조합하여 하나의 결과로 리턴한다. 자바 8에서는 3가지 reduce 오퍼레이션을 제공하는데 첫번째는 스트림의 엘레먼트를 스트림이 존재하는 하나의 엘레먼트로 변경하는 것이다. 앞의 예제에서 어떻게 가장 나이가 많은 사람을 추출하는지 알아보자.

persons.stream()
.reduce(((person1, person2) -> person1.age > person2.age ? person1 : person2))
.ifPresent(System.out::println);


리듀스 메소드는 BinaryOperator accumulator 함수를 적용할 수 있다. 실제로 BiFunction은 같은 타입간의 연산이며 이번 케이스는 Person이다. BiFunction이 Function과 다르게 두개의 인자를 적용한다. 이번 예제에서는 두 사람의 나이를 비교해서 나이가 가장 많은 사람을 리턴한다.


두번째 reduce 메소드는 id값과 BinaryOperator acculmulator를 적용할 수 있다. 이 메소드는 새로운 Person객체를 만드는데 이 객체는 스트림에 존재하는 모든 사람의 이름과 나이를 조합한 결과이다.


Person total = persons.stream()
.reduce(new Person("Total", 0), (person1, person2) -> {
person1.age += person2.age;
person1.name += person2.name;
return person1;
});

System.out.println(total);


Person{name='TotalMaxPeterPamelaDavid', age=76}


세번째 리듀스 메소드는 세 개의 파라미터를 적용할 수 있다 : id값, BiFunction accumulator 그리고 BinaryOperator의 combiner 함수이다. id값은 Person 타입으로 한정되지 않으므로, 모든 사람의 나이의 합을 내는 것은 아래와 같이 구현하는 것이 가능하다.


Integer total = persons.stream()
.reduce(0, (sum, person1) -> sum += person1.age, (sum1, sum2) -> sum1 + sum2);

System.out.println(total);


결과로 76이 리턴되는 것을 알 수 있다. 하지만 내부적으로 어떤일이 벌어질까? 위의 코드의 몇가지 디버그 코드를 입력해보자.

Integer total = persons.stream()
.reduce(0, (sum, person1) -> {
System.out.println(String.format("accumulator. sum=%d, person=%s", sum, person1));
return sum += person1.age;
}, (sum1, sum2) -> {
System.out.println(String.format("combiner. sum1=%d, sum2=%d", sum1, sum2));
return sum1 + sum2;
});

accumulator. sum=0, person=Person{name='Max', age=18}

accumulator. sum=18, person=Person{name='Peter', age=23}

accumulator. sum=41, person=Person{name='Pamela', age=23}

accumulator. sum=64, person=Person{name='David', age=12}


보시다시피 accumulator 함수는 모든 경우 실행되는 것을 알 수 있다. 처음 호출이 되면 id 값은 0이며 나머지 세 스텝이 호출되면 마지막 스텝에서는 나이가 증가하여 76이 됨을 알 수 있다.


그런데 combiner는 호출되지 않았다. 같은 스트림을 병렬로 돌려보자.

Integer total = persons.parallelStream()
.reduce(0, (sum, person1) -> {
System.out.println(String.format("accumulator. sum=%d, person=%s", sum, person1));
return sum += person1.age;
}, (sum1, sum2) -> {
System.out.println(String.format("combiner. sum1=%d, sum2=%d", sum1, sum2));
return sum1 + sum2;
});

accumulator. sum=0, person=Person{name='Pamela', age=23}

accumulator. sum=0, person=Person{name='David', age=12}

accumulator. sum=0, person=Person{name='Max', age=18}

accumulator. sum=0, person=Person{name='Peter', age=23}

combiner. sum1=23, sum2=12

combiner. sum1=18, sum2=23

combiner. sum1=41, sum2=35


이 스트림을 병렬로 실행하면 전체적인 실행이 달라지는 것을 알 수 있다. 그리고 병렬로 accumulator가 실행되기 때문에 combiner는 각 분리 계산된 값을 합계를 내기 위해 필요하다.


Parallel Streams


스트림은 대용량처리시 실행성능을 증가하기 위해 병렬로 수행될 수 있다. 병렬 스트림은 ForkJoinPool.commonPool 메소드를 이용하여 ForkJoinPool을 이용한다. 스레드풀의 사이즈는 5개까지 사용할 수 있으며 이것은 물리적인 CPU의 코어수에 비례한다.

ForkJoinPool forkJoinPool = ForkJoinPool.commonPool();

System.out.println(forkJoinPool.getParallelism());


나의 머신에서는 디폴트는 7이었다. 이 값은 다음과 같은 JVM 파라미터에 의해 증가하거나 줄어들수 있다.

-Djava.util.concurrent.ForkJoinPool.common.parallelism=5


컬렉션은 parallelStream이라는 메소드를 지원하는데 이는 엘레먼트의 스트림의 병렬 스트림을 생성하기 위해 사용된다. 대안으로는 인터미디어트 메소드 parallel을 호출할 수있는데 이는 직렬 스트림을 병렬 스트림으로 변경한다.


이해를 돕기 위해서 현재 스레드를 출력해보자.

Arrays.asList("a1", "a2", "b1", "c2", "c1")
.parallelStream()
.filter(s -> {
System.out.println(String.format("Current Thread : %s, filter : %s", Thread.currentThread(), s));
return true;
})
.map(s -> {
System.out.println(String.format("Current Thread : %s, map : %s", Thread.currentThread(), s));
return s.toUpperCase();
})
.forEach(s -> {
System.out.println(String.format("Current Thread : %s, forEach : %s", Thread.currentThread(), s));
});


Current Thread : Thread[main,5,main], filter : b1

Current Thread : Thread[ForkJoinPool.commonPool-worker-2,5,main], filter : c1

Current Thread : Thread[ForkJoinPool.commonPool-worker-4,5,main], filter : c2

Current Thread : Thread[main,5,main], map : b1

Current Thread : Thread[ForkJoinPool.commonPool-worker-4,5,main], map : c2

Current Thread : Thread[ForkJoinPool.commonPool-worker-3,5,main], filter : a1

Current Thread : Thread[ForkJoinPool.commonPool-worker-1,5,main], filter : a2

Current Thread : Thread[main,5,main], forEach : B1

Current Thread : Thread[ForkJoinPool.commonPool-worker-3,5,main], map : a1

Current Thread : Thread[ForkJoinPool.commonPool-worker-1,5,main], map : a2

Current Thread : Thread[ForkJoinPool.commonPool-worker-4,5,main], forEach : C2

Current Thread : Thread[ForkJoinPool.commonPool-worker-3,5,main], forEach : A1

Current Thread : Thread[ForkJoinPool.commonPool-worker-2,5,main], map : c1

Current Thread : Thread[ForkJoinPool.commonPool-worker-1,5,main], forEach : A2

Current Thread : Thread[ForkJoinPool.commonPool-worker-2,5,main], forEach : C1


위와 같이 각 스트림 오퍼레이션은 common ForkJoinPool을 이용하는 것을 알 수 있다. 


위의 예제에 추가적으로 소팅 작업을 수행해보자.

Arrays.asList("a1", "a2", "b1", "c2", "c1")
.parallelStream()
.filter(s -> {
System.out.println(String.format("Current Thread : %s, filter : %s", Thread.currentThread(), s));
return true;
})
.map(s -> {
System.out.println(String.format("Current Thread : %s, map : %s", Thread.currentThread(), s));
return s.toUpperCase();
})
.sorted((s1, s2) -> {
System.out.println(String.format("Current Thread : %s, sorted : %s, %s", Thread.currentThread(), s1, s2));
return s1.compareTo(s2);
})
.forEach(s -> {
System.out.println(String.format("Current Thread : %s, forEach : %s", Thread.currentThread(), s));
});

Current Thread : Thread[ForkJoinPool.commonPool-worker-1,5,main], filter : a2

Current Thread : Thread[ForkJoinPool.commonPool-worker-4,5,main], filter : c2

Current Thread : Thread[ForkJoinPool.commonPool-worker-2,5,main], filter : c1

Current Thread : Thread[main,5,main], filter : b1

Current Thread : Thread[ForkJoinPool.commonPool-worker-4,5,main], map : c2

Current Thread : Thread[ForkJoinPool.commonPool-worker-3,5,main], filter : a1

Current Thread : Thread[ForkJoinPool.commonPool-worker-2,5,main], map : c1

Current Thread : Thread[main,5,main], map : b1

Current Thread : Thread[ForkJoinPool.commonPool-worker-3,5,main], map : a1

Current Thread : Thread[ForkJoinPool.commonPool-worker-1,5,main], map : a2

Current Thread : Thread[main,5,main], sorted : A2, A1

Current Thread : Thread[main,5,main], sorted : B1, A2

Current Thread : Thread[main,5,main], sorted : C2, B1

Current Thread : Thread[main,5,main], sorted : C1, C2

Current Thread : Thread[main,5,main], sorted : C1, B1

Current Thread : Thread[main,5,main], sorted : C1, C2

Current Thread : Thread[main,5,main], forEach : B1

Current Thread : Thread[ForkJoinPool.commonPool-worker-2,5,main], forEach : C1

Current Thread : Thread[ForkJoinPool.commonPool-worker-3,5,main], forEach : A1

Current Thread : Thread[ForkJoinPool.commonPool-worker-5,5,main], forEach : A2

Current Thread : Thread[ForkJoinPool.commonPool-worker-1,5,main], forEach : C2


정렬작업은 메인 스레드에서 직렬로 실행된 것 처럼 보인다. 실제로 병렬 스트림에서는 Arrays.parallelSort()를 사용할 수 있다. 


만약 명시된 배열의 길이가 minimum granularity보다 작다면 적당한 Arrays.sort 메소드를 사용할 수 있다

마지막 섹션에 reduce메소드로 돌아가자. 우리는 이미 직렬 스트림에서 combiner 메소드를 실행해보았다. 이제 어떤 스레드가 실제적으로 참여하는지를 살펴보자.


List<Person> persons = Arrays.asList(

    new Person("Max", 18),

    new Person("Peter", 23),

    new Person("Pamela", 23),

    new Person("David", 12));


Integer total = persons.parallelStream()
.reduce(0, (sum, person1) -> {
System.out.println(String.format("accumulator. sum=%d, person=%s [%s]", sum, person1, Thread.currentThread().getName()));
return sum += person1.age;
}, (sum1, sum2) -> {
System.out.println(String.format("combiner. sum1=%d, sum2=%d [%s]", sum1, sum2, Thread.currentThread().getName()));
return sum1 + sum2;
});

System.out.println(total);

accumulator. sum=0, person=Person{name='Pamela', age=23} [main]

accumulator. sum=0, person=Person{name='Max', age=18} [ForkJoinPool.commonPool-worker-3]

accumulator. sum=0, person=Person{name='Peter', age=23} [ForkJoinPool.commonPool-worker-1]

accumulator. sum=0, person=Person{name='David', age=12} [ForkJoinPool.commonPool-worker-2]

combiner. sum1=23, sum2=12 [ForkJoinPool.commonPool-worker-2]

combiner. sum1=18, sum2=23 [ForkJoinPool.commonPool-worker-1]

combiner. sum1=41, sum2=35 [ForkJoinPool.commonPool-worker-1]

76


요약하자면 병렬 스트림은 대용량 처리에 좋은 성능 개선을 가질 수 있다. 그러나 명심해야 할 것은 reduce나 collect(예를 들어 combiner) 오퍼레이션은 추가적은 컴퓨팅 자원을 사용한다는 것이다.


더욱이 모든 병렬 스트림은 JVM 공통 ForkJoinPool을 사용한다. 그래서 무거운 병렬 스트림작업 떄문에 우리의 어플리케이션은 잠재적으로 더 느려질 수도 있다.


Happy coding!



출처 : http://winterbe.com/posts/2014/07/31/java8-stream-tutorial-examples/

반응형

'JAVA > Pleasure' 카테고리의 다른 글

Java - Nashorn  (1) 2016.03.10
Java - Lamda expression  (1) 2016.01.13
NIO란?  (0) 2015.04.17
Stack과 heap의 차이점  (0) 2015.01.12
반응형

Java 8의 가장 주요한 변화는 더 빠르고, 명확하게 코딩할 수 있는 펑션 프로그래밍을 수행할 수 있게 되었다는 것이다.


자바는 1990년대에 객체지향 프로그래밍 언어로서 설계되었다. 그 당시 객체지향 프로그래밍은 소프트웨어 개발의 주요한 패러다임이였다. 객체지향 프로그래밍 훨씬 전에 Lisp 이나 Scheme 같은 함수형 프로그래밍 언어가 존재하였지만 학술적 영역외에서는 별다른 빛을 보지 못하였다. 최근 함수형 프로그래밍의 중요설이 대두되고 있는데 그 이유는 동시성과 이벤트 처리 프로그래밍에 적합히기 때문이다. 이 의미는 객체지향이 나쁘다는 것이 아니라, 객체지향과 함수형 프로그래밍을 적절히 사용하는 것이 좋은 전략이라는 것이다. 이것은 동시성에 관심이 없더라도 충분히 유의미하다. 예를 들어 컬렉션 라이브러리는 강력한 API를 제공하는데 이러한 함수형 표현은 훨씬 효율적인 문법을 가지고 있다.


자바8의 주요한 진화는 객체지향 기반에 함수형 프로그래밍을 추가하였다는 것이다. 이 글은 기본적인 문법을 설명하고 몇몇의 주요한 컨텍스트들을 어떻게 활용하는지에 대해 살펴본다. 주요한 점은 다음과 같다.


  • Lamda 표현식은 파라미터가 있는 코드 블럭이다.
  • 나중에 실행되어지는 코드 블럭을 원한다면 Lamda 표현식을 사용하자
  • Lamda 표현식은 함수형 인터페이스로 전환할 수 있다.
  • Lamda 표현식은 enclosing scope으로 부터 효율적으로 final 변수에 접근할 수 있다.
  • 메소드와 생성자 참조는 메소드와 생성자의 언급없이 호출할 수 있다.
  • 기본 메소드와 스태틱 메소드를 실제 구현을 제공하여 인터페이스에 추가할 수있다. 
  • 다수의 인터페이스의 기본 메소드 사이에 충돌은 해결해야 한다.


Why Lambdas? 


람다 표현식은 코드 블럭이다. 그래서 나중에 실행될 수 있고 한번 실행될 수 있고 여러번 실행될 수도 있다. 문법을 살펴보기 전에 자바에서 사용했던 코드 블럭을 살펴보자. 분리된 스레드에서 작업이 수행되기 원할때 우리는 작업을 Runnable의 run 스레드에 넣는다. 다음과 같이

public class Worker implements Runnable {
@Override
public void run() {
for (int i = 0 ; i < 1000 ; i++) {
doWork();
}
}

private void doWork() {

}
}

그런 다음 이 코드를 실행하기 위해 Worker 클래스의 인스턴스를 만든다. 그리고 스레드 풀에 인스턴스를 서브밋 해야 한다.

public static void main(String[] args) {
Worker worker = new Worker();
new Thread(worker).start();
}

중요한 포인트는 run 메소드에 코드가 포함되어 있으면 다른 스레드에서 실행되기를 원한다는 것이다. 커스텀 comparator에 의한 sorting을 고려해보자. 만약 기본적인 딕셔너리 순서 대신에 길이로 정렬을 하고 싶다면 다음과 같이 코딩할 수 있다.

public class LenghComparator implements Comparator<String> {
@Override
public int compare(String string1, String string2) {
return Integer.compare(string1.length(), string2.length());
}
}


또 다른 예는 디퍼드 실행이다. 버튼 콜백을 생각해보자. 당신은 listener 인터페이스를 구현한 클래스의 메소드에 콜백 액션을 집어넣을 것이다. 그리고 인스턴스를 생성하고 버튼의 인스턴스에 등록할 것 이다. 이러한 경우 자주 많은 프로그래머들들이 "anonymous instance of anonymous class" 문법을 사용한다.

Button button = new Button();

button.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
System.out.println("This button is clicked.");
}
});


위와 같은 모든 예제에서 우리는 똑같은 접근방법을 알 수 있다. 코드블럭이 누군가에게 전달되었다는 것이다. - 스레드 풀, 소트 메소드 또는 버튼. 코드는 나중에 호출될 것이다.


지금까지 자바에서 코드 블럭을 누군가에게 전달하는 것은 쉽지 않은 일이었다. 단순히 코드 블럭을 전달할 수 있는 방법이 없었다. 자바는 객체지향 언어이다. 그래서 클래스를 생성해야 하고 거기에다가 원하는 코드를 메소드로 작성해야 한다. 다른 언어에서는 코드 블럭을 직접적으로 전달하는 것이 가능했다. 다음 섹션에서는 자바 8에서 코드 블럭을 어떻게 동작하게 할 수 있는지 살펴보겠다.


The Syntax of Lambda Expressions


정렬 예제를 다시 한번 살펴보자. 우리는 하나의 문자열의 길이가 다른 것보다 짧은지를 체크하는 코드를 전달했다.

Integer.compare(string1.length(), string2.length());

string1과 string2는 무엇인가? 둘다 문자열이다. 자바는 strong typed 언어이다. 다음과 같이 코딩할 수 있다.

(String string1, String string2) -> Integer.compare(string1.length(), string2.length())

첫번째 람다 표현식이다. 이러한 표현식은 단순한 코드 블럭이다. 어떤 변수를 명시함으로써 코드를 전달시킬 수 있다.


자바에서 람다 표현식의 하나의 형태를 살펴보았다. 파라미터, 화살표 -> 그리고 표현식이다.  만약 코드가 하나의 표현식으로 안된다면 블럭 {}을 사용할 수 있다. 다음과 같은 예이다.

(String string1, String string2) -> {
if (string1.length() < string2.length())
return -1;
else if (string1.length() > string2.length())
return 1;
else
return 0;
});


만약 람다표현식에 파라미터가 필요없다면 빈괄호로 두면 된다.

() -> {
for (int i = 0; i < 1000 ; i++) {
doWork();
}
}


만약 파라미터 타입을 추론할 수 있다면 생략할 수 있다.

Comparator<String> comparator = (string1, string2) -> Integer.compare(string1.length(), string2.length());

여기서 컴파일러는 string1과 string2는 문자열로 추론할 수 잇다. 때문에 람다 표현식은 string comparator에 할당된다. 


만약 메소드가 하나의 파라미터이고 추론될 수 있다면 괄호도 생략할 수 있다.

EventHandler<ActionEvent> handler = (event) -> System.out.println("This button is clicked.");

그리고 final이나 어노테이션도 같이 붙여서 전달할 수 있다. 


그리고 람다표현식의 결과 타입을 명시할 필요가 없다. 이것은 항상 컨텍스트에 의해 추론될 수 있기 때문이다. 예를 들어 아래 표현식에서는 리턴타입이 int라는 것을 추론할 수 있다.

(String string1, String string2) -> Integer.compare(string1.length(), string2.length())


Functional Interfaces


논의한바와 같이 자바에서는 매우 많은 인터페이스들이 존재한다. 이러한 인터페이스들은 코드 블럭을 캡슐화하는데 Runnable 또는 Comparator가 그 예이다. 람다는 이러한 인터페이스에 적합하다.


하나의 추상화 메소드를 가진 인터페이스들은 람다표현식으로 채울수 있다. 이러한 인터페이스를 functional interface라고 부른다.


당신은 왜 functional interface가 하나의 추상화 메소드를 가져야 하는지 궁금할 것이다. 인터페이스는 모든 메소드는 abstract가 아닌가? 사실 Object 클래스의 toString 또는 clone 과 같은 메소드들은 항상 재정의할 수 있으며 이러한 메소드들은 abstract 메소드로 정의되지 않았다. 더 중요한 점은 자바 8에서 인터페이스는 abstract 메소드가 아닌 메소드들을 정의할 수 있다.


Functional interface에 대한 논의를 설명하기 위해서 Arrays.sort 메소드를 살펴보자. 두번째 파라미터로 Comparator의 인스턴스를 필요로 하고 인터페스는 하나의 메소드들 가지고 있다. 간단하게 람다로 채운다면 다음과 같다.

Arrays.sort(strings, (string1, string2) -> Integer.compare(string1.length(), string2.length()));

위의 내용을 살펴보면 Arrays.sort 메소드는 Comparator<String>을 구현한 어떤 클래스의 객체를 전달받아야 한다. 그러나 람다로 표현했을때 이러한 객체와 클래스의 관리는 구현과 완전히 독립적이 되며, 전통적인 inner 클래스 방식보다 훨씬 더 효율적이다. 람다는 객체가 아니라 function으로 생각하는 것이 맞다. 그리고 람다는 functional interface로 전달될 수 있다.


인터페이스의 이러한 변화는 람다를 더 받아들이기 쉽게 만든다. 문법은 짧고 단순하다.

button.setOnAction(event -> System.out.println("This button is clicked."));

얼마나 읽기 쉽고 좋은가?


사실 functional interface로 전환이 자바에서 람다를 써야할 유일한 이유이다. 다른 언어에서는 function 리터럴을 제공한다. 자바에서는 람다 표현식을 오브젝트의 변수로 할당할 필요가 없으며, 이는 오브젝트가 아니라 functional interface이기 때문이다. 


자바 API는 몇몇의 제너릭 functional interface를 정의하고 있다. BiFunction<T,U,R> 이 하나의 예인데 이것은 파라미터 타입 T, U 그리고 리턴타입이 R이라고 서술하고 있다. string 비교 람다를 해당 타입에 저장할 수 있다.

BiFunction<String,String,Integer> function = (string1, string2) -> Integer.compare(string1.length(), string2.length());

하지만 이러한 것은 정렬에 도움이 되지 못한다. 이유는 Arrays.sort 메소드가 BiFunction을 원하지 않기 때문이다. 만약 이전에 functional 프로그래밍에 경험이 있다면 궁금중이 생길 것이다. 그러나 자바 프로그래머에게는 매우 자연스러운 일이다. Comparator와 같은 인터페이스는 특정한 목적을 가지고 있으며, 단지 파라미터와 리턴타입을 메소드와 함께 제공하는 것이 아니다. 자바8에서는 조미료가 생겼다.


java.util.function의 인터페이스는 몇몇 자바 8 API들에서 사용되고 있다. 그러나 명심해라. 람다 표현식은 functional interface로 잘 변경할 수 있다. 또한 어떤 functional interface에 @FunctionInterface 어노테이션으로 태그할 수 있다. 이것은 두가지 이점이 있다. 컴파일러는 어노테이션된 엔티티가 single abstract method를 가진 인터페이스인지 확인한다. 그리고 javadoc 페이지에 당신의 인터페이스가 functional interface이라고 표시된다. 어노테이션을 꼭 사용할 필요는 없다. 그러나 @FunctionInterface 어노테이션을 붙이는 것은 좋은 아이디어이다.


마지막으로 람다를 functional interface의 인스턴스로 전환했을때 checked exception에 주의하자. 만약 람다가 checked exception을 발생한다면 exception은 타겟 인터페이스의 abstract 메소드에 정의해야 한다. 다음 예제는 에러를 유발할 것이다.

Runnable sleeper = () -> {
System.out.println("zzz");
Thread.sleep(1000);
};

이유는 Runnble의 run메소드가 exception을 throw하지 않아서 이러한 할당은 옳지 않다. 에러를 수정하기 위해 두가지 선택을 해야하는데 람다에서 exception을 처리하던지 람다가 할당되는 인터페이스의 메소드가 exception을 throw 해야 한다.


Method References


때때로 메소드만 전달했을 경우에도 정확한 동작을 수행할 수 있을 경우가 있다. 예를 들어 버튼이 클릭되었을때 이벤트를 출력하는 코드를 본다면 다음과 같다.

button.setOnAction(event -> System.out.println(event));

그러나 위와 같은 코드의 더욱 단순해질수 있는데 그것은 event 자체가 추론될 수 있기 때문이다.

button.setOnAction(System.out::println);


System.out::println 이라는 표현식이 method reference이며 이것은 x -> System.out.println(x)라는 람다 표현식과 동일하다.


또 다른 예제는 문자열을 대소문자 구분없이 정렬을 하고 싶을때의 예제이다.

Arrays.sort(strings, String::compareToIgnoreCase);

이러한 예제들을 봤을때, :: 연산자는 오브젝트나 클래스와 메소드의 구분자임을 알 수 있다.


  • object::instanceMethod

  • Class::staticMethod

  • Class::instanceMethod


첫번째 두가지 케이스에서는 메소드 레퍼런스는 파라미터가 전달된 람다 표현식과 같다. 이미 언급한 바와 같이 System.out::println은  x -> System.out.println(x)과 동일하다. 비슷하게 Math::pow는 (x,y) -> Math.pow(x,y)와 같다. 세번째 케이스는 첫번째 파라미터가 메소드의 타겟이 되어야 한다. 즉  String::compareToIgnoreCase는 (x, y) -> x.compareToIgnoreCase(y)과 동일하다.


같은 이름의 다양하게 메소드가 오버로드외어 있다면 컴파일러는 context에 따라서 메소드를 찾아낼려고 노력할 것이다. 예를 들어 Math.max 메소드의 두가지 버젼이 있고 하나는 integer고 또 다른 하나는 double에 대한 것이다. 이럴 경우 컴파일러는 functional interface의 파라미터에 따라서 Math::max를 선택할려고 할 것이다. 


우리는 또한 메소드 레퍼런스의 파라미터를 확보할 수 있다. 예를 들어 this:equals는 x -> this.equals(x)와 동일하다. 이것은 super를 사용할떄도 동일하다. 

public class Greeter {
public void greet() {
System.out.println("Hello");
}

public class CocurrentGreeter extends Greeter {
public void greet() {
new Thread(super::greet).start();
}
}
}

스레드가 시작될때 Runnable이 실행되고 super::greet가 실행된다.


Constructor References


Constructor references는 메소드 레퍼런스와 같다. 다른 점이 있다면 메소드의 이름이 new라는 것이다. 예를 들어 Button:new는 Button 생성자에 대한 레퍼런스이다. 어떤 생성자가 선택되느냐? 이것 역시 context에 따른 것이다. 문자열 리스트를 가지고 있다고 가정해보자. 그리고 버튼의 배열로 변경하는데 각 문자열들을 생성자를 호출할때 사용한다고 가정해보자.

List<String> labels = new ArrayList<>();
Stream<Button> stream = labels.stream().map(Button::new);
List<Button> buttons = stream.collect(Collectors.toList());

stream, map, collect 메소드의 자세한 언급은 이 글에서 하지 않겠다.


우리는 생성자 레퍼런스를 배열 타입과 함께 구성할 수도 있다. 예를 들어 int[]::new는 하나의 파라미터에 대한 생성자 레퍼런스이며 그것은 array의 길이이다. 이것은 x->new int[x]와 동일하다.


Array 생성자는 자바의 한계를 극복하는데 유용하다. array 생성자에는 제너릭을 사용할 없다. 이러한 문제는 라이브러리 개발자를 곤혹스럽게 만든다. 예를들어 버튼의 배열을 원할때 Stream 인터페이스의 toArray함수는 Object 배열을 리턴한다.

Object[] objects = stream.toArray();


불행하게도 우리가 원하는것은 오브젝트 배열이 아니라 Button의 배열이다. 해결책은 아래와 같다.

Button[] buttons = stream.toArray(Button[]::new);


Variable Scope


종종 우리는 근접한 메소드나 클래스의 변수를 람다 표현식에서 접근하고 싶을 경우가 있다.

public static void repeatMessage(String text, int count) {
Runnable r = () -> {
for (int i = 0 ; i < count ; i++) {
System.out.println(text);
Thread.yield();
}
};

new Thread(r).start();
}

람다 표현식 내에서 count와 text변수를 사용한 것을 알 수 있다. 이러한 변수들은 람다 표현식에서 정의하지 않은 것임에 주의하자. 


어떠한 일이 발생할지 이해하기 위해 람다 표현식에 대한 이해를 다시 정리할 필요가 있다. 람다 표현식은 세가지 요소를 가지고 있다.


  • A block of code

  • Parameters

  • Values for the free variables; 즉 변수인데 파라미터가 아니며 코드내에서 정의되지 않은 것


우리의 예제에서는 람다 표현식은 두가지의 외부 변수를 가지고 있다. - text, count. 람다 표현식을 나타내는 데이터 구조는 이러한 변수를 값으로 저장해야 한다. 우리의 경우 "Hello"와 1000이다. 우리는 이러한 값을 람다 표현식에 저장한다고 말한다. 


외부 변수의 값을 사용하는 코드 블럭의 기술적인 의미는 클로져이다. 만약 누군가가 그들의 언어에 클로져가 있다고 자랑스러워한다면 자바도 있다고 말하라. 자바에서 람다 표현식은 클로져이다. 사실 inner 클래스 자체가 크로져일수도 있다. 


보시다시피 람다 표현식에서는 근접한 스코프의 변수의 값을 저장할 수 있다. 자바에서는 잘 정의된 중요한 제약조건이 있다. 람다 표현식에서 참조한 외부 변수는 변경될 수 없다.

public static void repeatMessage(String text, int count) {
Runnable r = () -> {
while(count > 0) {
System.out.println(text);
Thread.yield();
count--;
}
};

new Thread(r).start();
}

위와 같은 경우는 허용되지 않는다. 람다 표현식에서 외부 변수의 변경은 스레드 세이프하지 않다. 

int matches = 0;

for (Path p : files) {
new Thread(() -> {
if (p != null)
matches++;
});
}

이와 같은 코드는 합법적이나 매우 매우 나쁘다.  matches++는 atomic이 아니기 때문에 다수의 스레드에서는 동시에 값을 증가 시키면 무슨일이 발생할지 모른다.


내부 클래스는 외부 스코프로부터 온 값을 사용할 수 있다. 자바 8 이전에 내부 클래스는 final 로컬변수만 접근할 수 있었는데 이러한 규칙은 람다 표현식을 위해 완화되었다. 


이상하게 들릴지 모르지만 공유된 객체를 변경하는 것도 가능하다. 

List<Path> matches = new ArrayList<>();

for (Path p : files)
new Thread(() -> {
if (p != null)
matches.add(p);
});

이 경우 matches는 항상 같은 ArrayList 객체를 참조한다. 하지만 오브젝트는 변경되고, 이것은 스레드 세이프하지 않다. 다수의 스레드에서 add를 수행한다면 결과는 예측할 수 없다.


동시성에서 카운트하고 수집하는 메커니즘은 존재한다. 그러나 우리의 상황에서는 스레드 세이프한 카운터나 컬렉션을 사용할 수 있을 것이다.


람다 표현식는 몸체는 인접한 블럭과 동일한 스코프이다. 람다에서 파라미터와 같은 이름의 로컬 변수가 존재한다면 이는 허용되지 않을 것이다.

Path first = Paths.get("/usr/bin");

Comparator<String> comparator = (first, second) -> Integer.compare(first.length(), second.length());

메소드내에서 동일한 이름의 두개의 로컬 변수는 가질 수 없다. 그러므로 람다 표현식 또한 그러한 변수들을 사용할 수 없다. 람다 표현식에서 this 키워드를 사용한다면 메소드의 this 파라미터를 언급해야 된다.

public class Application {
public void doWork() {
Runnable runnable = () -> System.out.println(this.toString());
}
}

표현식에서 this.toString()을 호출하는데 이것은 Application 객체의 toString 메소드를 호출하게 된다. 절대 Runnable 인터페이스가 아니다. 이러한 사용은 람다 표현식에서 당연한 것이다. 


Default Methods


많은 프로그래밍 언어에서 그들의 컬렉션 라이브러리에 함수형 표현이 포함되어 있다. 이러한 경우 종종 루프보다 더 짧고 이해하기 쉬운 코드로 작성할 수 있다.

for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}

더 좋은 방법이 존재하는데 forEach는 각 엘레먼트에 함수를 적용할 수 있다.

list.forEach(System.out::println);

자바 컬렉션 라이브러리는 매우 오랜 시간전에 디자인되었고, 몇몇의 문제를 가지고 있다. 만약 Collection 인터페이스가 forEach와 같은 새로운 메소드를 가지게 된다면 모든 Collection 인터페이스를 구현한 프로그래밍에서 해당 메소드를 상속해야 하는데 자바에서 단순히 받아드릴 수는 없을 것이다.


그래서 자바 디자이너들은 이러한 문제를 풀기 위해 default 메소드를 고안하데 되었다. 이러한 메소드들은 기존 존재하는 인터페이스에 안전하게 추가될 수 있게 되었다. 이러한 연유로 자바 8에서는 forEach가 Iterable 인터페이스에 추가될 수 있었다.

public interface Person {
long getId();
default String getName() {
return "John Q. Public";
}
}

인터페이스는 두개의 메소드를 가지고 있다. getId라는 추상 메소드와 getName이라고 하는 디폴트 메소드이다. Person 인터페이스를 추상 메소드는 당연히 구현해야 하며 getName은 그대로 유지할 수도 오버라이드 할 수도 있게 되었다.


디폴트 메소드는 인터페이스나 추상 클래스의 마직막에 추가 되었다. 이제 우리는 인터페이스에 메소드를 구현할 수 있게 되었다.


하나의 인터페이스 추가된 똑같은 메소드가 수퍼클래스나 또 다른 인터페이스에 있다면 어떤 일이 벌어질까? Scala나 C++에서는 이런 애매모호함을 해결하기 위해 복잡한 룰을 가지고 있는데 다행히도 자바에서도 좀더 심플하다.


1. Superclasses win. 만약 수퍼클래스가 구현메소드를 제공한다면 동일한 메소드 시그니처를 가진 default 메소드는 무시된다.

2. Interfaces clash. 만약 수퍼 인터페이스가 default 메소드를 제공하고, 또다른 인터페이스에서 같은 메소드 시그니처에 메소드를 제공한다면 그때는 반드시 구현 클래스에서메소드를 오버라이딩해야 한다.


두번째 룰을 살펴보자.

public interface Named {
default String getName() {
return getClass().getName() + "_" + hashCode();
}
}
public class Student implements Person, Named {
@Override
public long getId() {
return 0;
}

@Override
public String getName() {
return Person.super.getName();
}
}

위와 같이 Person과 Named를 구현한 Student클래스에서는 getName 메소드를 오버라이드 해야한다.

public interface Named {
String getName();
}

위와 같이 default 메소드로 지정되어 있지 않다고 가정해보자.


Student 클래스는 Person인터페이스로부터 default 메소드를 상속받을수 있지 않느냐? 이것은 타당한것 처럼 보인다. 하지만 자바 디자이너들은 일관성을 선호하는 쪽으로 선택했다. 적어도 하나의 인터페이스가 구현을 하라고 메소드를 제공하면 컴파일러는 에러를 리포팅하고 프로그래머는 반드시 애매모호함을 해소해야 한다.solve the ambiguity.


자바8 이전에는 default 메소드를 제공하지 않았기 대문에 이러한 출동은 존재하지 않았다. 구현 클래스는 두가지 선택이 존재한다. 메소드를 구현하거나 구현하지 않는 것이다. 이 경우 클래스는 abstract가 되어야 한다.


지금까지 두개의 인터페이스간의 충돌에 대해 논의를 하였다. 이제 수퍼클래스와 인터페이스 간의 참조를 살펴보자.

public class Student extends Person implements Named {

}

이 경우 수퍼클래스에서 메소드가 제공되었기 때문에 인터페이스의 메소드는 무시된다. 이것은 "class wins" 룰이다. 이는 자바 7에서 확정되었다.


Static Methods in Interfaces


자바 8에서는 인터페이스에 스태틱 메소드를 추가할 수 있다. 원래 기술적인 이슈는 없었다. 추상화명세로 인터페이스의 정신에 반하는 것으로 보였기 때문이다.


이제부터 companion 클래스들을 위한 static 메소드를 추가할 수 있다. 


Path 인터페이스를 살펴보자. 여기는 팩토리 메소드들이 있는데 문자열들로 path를 생성할 수 있다.

public interface Path {
public static Path get(String first, String... more) {
return FileSystems.getDefault().getPath(first, more);
}
...
}

자바 8에서는 상당히 많은 static 메소드가 인터페이스에 추가되었다. 


출처 : http://www.drdobbs.com/jvm/lambda-expressions-in-java-8/240166764?pgno=1

반응형

'JAVA > Pleasure' 카테고리의 다른 글

Java - Nashorn  (1) 2016.03.10
Java - Stream  (0) 2016.01.14
NIO란?  (0) 2015.04.17
Stack과 heap의 차이점  (0) 2015.01.12
반응형

NIO (New Input/Output) vs IO (Input/Output) and NIO.2 in Java


Non-blocking I/O (일반적으로 NIO라고 불리우고 때때로 New I/O라고도 함)은 강력한 I/O 오퍼레이션 기능을 제공하는 자바 프로그래밍 API이다.  NIO는 J2SE 1.4에서 표준I/O를 보완하기 위해서 소개되었다.  또 NIO는 확장되어 자바7에 NIO2라고 불리는 새로운 파일시스템 API를 제공한다. 


What Is Input/Output


I/O는 소스로부터 데이터를 읽어서 목적지에 데이터를 읽는 것이다. 데이터는 입력 소스로부터 읽고 출력 목적지로 쓰여진다. 예를 들어 당신이 키보드가 표준 입력으로 동작하면 데이터를 읽어 당신의 프로그램에 쓰여지는 것이다. 표준출력으로 텍스트를 출력하기 위해 System.out.println()를 사용해 본적이 있을 것이다.  당신은 아무 지식없이 I/O를 수행한 것이다.


JDK에 java.io 및 java.nio 패키지에는 I/O를 처리하는 자바클래스들이 포함되어 있다. java.io 패키지는 I/O를 수행하기 위한 굉장히 많은 클래스들을 가지고 있다. 자바 I/O를 배우는 것은 약간 복잡하다. 클래스가 증가하는 수가 관리할 수 없는 지경에 이르는 것을 class explosion이라고 하며, java.io 패키지는 class explosion의 좋은 예제이다.


Input/Output Streams


Stream의 사전적의미는 "무엇인가의 끊기지 않는 흐름"이다. Java I/O에서 스트림의 의미는 데이터의 순차적인 흐름이다. 스트림의 데이터는 바이트, 문자, 객체일 수 있다.


강은 물의 스트림이다. 물은 소스로부터 목적지까지 끊기지않고 흐른다. 비슷하게 Java I/O는 데이터의 흐름이다. Data source로 알려진 소스로부터 data sink로 불리우는 목적지까지 흐른다. 


데이터는 데이터 소스로 부터 자바 프로그램으로 읽어지고, 자바 프로그램은 데이터 싱크로 데이터를 쓴다. 데이터 소스와 자바 프로그램 연결한 스트림을 input stream이라고 한다.  자바 프로그램과 데이터 싱크를 연결한 스트림을 output stream이라고 한다. 


자연에서 예를 들자면 스트림은 소스와 목적지를 물의 흐름으로 연결하지만 자바에서는 자바 프로그램이 input stream과 output stream의 사이에 존재한다. Input 스트림을 통한 데이터 소스로 부터 데이터의 흐름은 자바 프로그램으로 전달되고, 자바 프로그램으로부터 데이터 흐름은 output stream을 통해 데이터 싱크로 전달된다. 다시 말하면 자바 프로그램은 input 스트림으로 데이터를 읽고 output 스트림으로 데이터를 쓴다. 




How we work with input/output stream


데이터 소스로부터 자바 프로그램으로 데이터를 읽기 위해 다음과 같은 단계를 수행한다.


  • 데이터 소스를 지정한다.. 데이터 소스는 파일, 문자열, 배열, 네트워크 연결 등일 수 있다. 
  • 데이터 소스를 사용해 Input 스트림을 생성한다.
  • Input 스트림을 통해 데이터를 읽는다. 일반적으로 입력 스트림으로부터 모든 데이터를 읽을때까지 루프를 수행한다. 입력 스트림의 메소드들은 입력 스트림의 끝이 감지되었을 때, 특정한 문자를 리턴한다.
  • Input 스트림을 닫는다. Input 스트림을 연산을 위해 자신을 연다. 그래서 Input 스트림에서는 스트림을 여는 과장이 묵시적으로 존재한다. 하지만 데이터를 다 읽었으면 input 스트림을 닫아줘야 한다.


자바 프로그램으로 데이터싱크로 데이터를 쓰기 위해선 다음과 같은 단계를 수행한다.


  • 데이터 싱크를 지정한다. 데이터가 쓰여질 목적지를 명시한다. 파일, 문자열, 배열, 네트워크 연결 등일 수 있다.
  • 데이터 싱크를 사용하여 output 스트림을 생성한다.
  • 데이터를 output 스트림으로 쓴다. 
  • Output 스트림을 닫는다. 


Input/output 스트림 클래스는 데코레이터 패턴을 기반으로 한다.


What Is NIO?


스트림 기반 I/O에서는 데이터 소스와 데이터 싱크, 자바 프로그램간의 데이터 이동을 위해서 스트림을 사용한다. 자바 프로그램은 스트림으로부터 데이터를 읽거나 쓴다. 이러한 접근은 I/O 수행을 느리게 만든다. NIO는 스트림 기반 I/O는 느린 속도 문제를 해결한다. NIO에서는 I/O처리를 위해 채널과 버퍼를 사용한다. 채널은 stream과 비슷하다. 채널은 데이터 소스/싱크 및 자바 프로그램간의 연결을 대표한다. 


채널과 스트림사이에는 한가지 차이점이 존재한다.


스트림은 단방향 데이터 전송을 위해 사용된다. 즉, 입력 스트림은 단지 데이터 소스로부터 자바 프로그램으로 데이터를 전송하고, 출력 스트림은 자바 프로그램으로부터 데이터 싱크로 데이터를 전송한다. 하지만, 채널은 양방향 전송 능력을 가지고 있다.


채널은 데이터 읽을 때뿐만 아니라 쓸 때에도 데이터 전송을 위해 사용할 수 있다. 요구에 따라 read-write 채널을 설정할 수 있고, read-only, write-only 채널을 설정할 수 있다.



스트림 기반 I/O에서 데이터 전송의 기본 단위는 byte이다. 그러나 채널기반 NIO에서는 데이터 전송의 기본 단위는 buffer이다. 버퍼는 bounded data container이다. 즉 버퍼는 정해진 용량을 가지고 있다. 스트림 기반 I/O에서는 데이터를 stream에 직접 쓰지만 채널기반 I/O에서는 데이터를 버퍼에 넣는다. 버퍼를 채널로 전달하면, 데이터 싱크로 데이터를 쓸 수 있다. 비슷하게 데이터 소스로부터 데이터을 읽기를 원한다면 채널로 버퍼를 전덩하면 된다. 채널은 데이터 소스로부터 데이터를 읽어서 버퍼로 전달한다. 우리는 버퍼로 부터 데이터를 읽을 수 있다. 위의 다이어그램은 채널, 버퍼, 데이터소스, 데이터싱크, 자바 프로그램 간의 상호 작용을 도식화 한것이다.


New Input/Output 2 (NIO.2)


자바 7에서는 NIO2를 소개하였다. 오리지널 File I/O의 부족함을 보충하기 위해 많은 기능을 제공한다. 이들은 java.nio.file, java.nio.file.attribute 그리고 java.nio.file.spi 패키지에서 찾아 볼 수 있다.


NIO2에서는 모든 파일 시스템을 동일한 방식으로 처리한다. 파일 시스템은 NIO2를 확장해서 제공한다. 또한 파일 시스템에 대한 기본구현을 활용할 수 있고, 파일 시스템 각각 구현체를 사용할 수 있다. 


모든 파일 시스템의 기본 파일 오퍼레이션(copt, move, delete)를 제공한다. 또한 move 오퍼레이션을 제공한다. 또한 향상된 예외 처리를 제공하며, 심볼릭 링크를 제공한다. 


파일이 추가되거나 서브디렉토리가 생기는 것과 같은 디렉토리에 발생하는 이벤트를 처리할 수 있는 서비스를 만들 수 있으며, 파일 트리를 탐색할 수 있는 API도 제공된다.


네트워크 소켓에 대한 비동기 I/O도 제공되며, DatagramChannel을 이용한 멀티캐스팅도 지원한다.


출처 : http://java-latte.blogspot.kr/


반응형

'JAVA > Pleasure' 카테고리의 다른 글

Java - Nashorn  (1) 2016.03.10
Java - Stream  (0) 2016.01.14
Java - Lamda expression  (1) 2016.01.13
Stack과 heap의 차이점  (0) 2015.01.12
반응형

자바를 배우기 시작하는 사람이나 다른 언어를 사용하는 사람들에게서 stack과 heap 메모리의 차이를 묻는 것은 일반적이다. Stack과 heap 이라는 두 단어를 누구나 들어봤지만, 명확하게 설명하는 것은 어려운을 흔히 겪는다. 이런 상황은 stack과 heap의 연관관계에 대한 오해에서 비롯된다. 더 혼란스럽게도 stack은 jata.util 패키지에 있는 LIFO순서로 데이터를 저장하는 데이터 구조체이기도 하다.일반적으로 stack과 heap은 메모리의 부분이다. 프로그램은 다른 목적으로 stack과 heap을 사용하며 할당한다. 자바 프로그램은 "java"라는 명으로 실행되는 프로세스의 JVM 상에서 실행된다. 자바 또한 stack과 heap을 다른 목적으로 이용한다.

  

자바에서 stack과 heap의 몇몇 차이점은 다음과 같다.


1) stack과 heap의 주요한 차이는 stack의 지역 변수와 함수 호출시 사용된 변수를 저장하는데 사용되는 반면에 heap은 자바 객체를 저장하기 위해 사용된다. 객체가 코드의 어디에서 생성되는지 중요하지 않다. 즉, 멤버 변수인지 지역 변수인지 클래스 변수인지와 상관없이 항상 자바의 heap 영역에 생성이 된다.


2) 자바는 각 스레드마다 stack 영역을 가지고 있다. (-Xss JVM 파라미터로 지정 가능) 비슷하게 자바 프로그램은 실행된 JVM에 할당된 heap영역을 가지고 있다. ( -Xms -Xmx JVM 파라미터로 지정 가능)


3) 함수 호출이나 지역 변수를 저장하다가 stack에 더 이상 저장할 공간이 없을 경우, JVM은 StackOverFlowError를 발생시키고, 객체를 생성하다가 heap에 더이상 저장할 공간이 없을 경우 OutOfMemoryError를 JVM이 발생시킨다.


4) 재귀를 사용하면 stack 메모리를 빠르게 채울 수 있다. stack과 heap의 또 다른 차이점은 일반적으로 자바에서 stack은 heap보다 용량이 훨씬 작다.


5) stack에 저장된 변수는 자신의 thread 에서만 접근 할 수 있다. 모든 스레드에서 접근을 하고 싶다면 heap에 저장되는 객체를 만들어야 한다. 다시 말하자면 stack은 자바 스레드의 개별적인 메모리이며, heap은 모든 스레드가 공유하는 메모리이다.


여기까지가 자바에서의 stack과 heap의 차이점이다. stack과 heap이 어떻게 동작하는지 알기 전에 어떤 종류의 변수들에 의해 사용되는지에 대한 이해가 중요하다. 


출처: http://javarevisited.blogspot.com

반응형

'JAVA > Pleasure' 카테고리의 다른 글

Java - Nashorn  (1) 2016.03.10
Java - Stream  (0) 2016.01.14
Java - Lamda expression  (1) 2016.01.13
NIO란?  (0) 2015.04.17
반응형

JMX란?

JMX는 java management extension의 약자이다. JDK 1.5부터 기본적으로 탑재되어 제공되고 있다. Application 관리를 위한 다양한 기능을 제공할 목적으로 시작되었다.
Non-Java resources와 하드웨어에 대해 wrapping한 인터페이스를 제공하며, API를 외부로 노출해 application 설정 및 통계데이터를 수집할수도 있다.

JMX 용어

Manageable Resource : 관리대상의 되는 리소스
MBean : Managed bean의 약자이며, Manageble Resource에 대한 접근 및 조작에 대한 interface를 제공한다.
MBean Server : MBean을 관리하는 Java Class
JMX Agent : Mbean 관리를 위한 서비스를 제공하는 Java Process. 다양한 Protocol Adapter와 Connector를 제공한다.
Management Application : JMX 활용하여 만들어진 Application 관리를 담당하는 Application
Notification : MBean에 의해 발생한 event, alert, information을 Wrapping한 Java Object
Instrumentation : MBean에 의해 관리되는 리소스들

JMX 아키텍처

 Level Description 
Instrumentation
MBean에 의해 관리되는 리소스들(applications, devices, services). Mbean은 management inteface 노출하여 원격 관리 및 모니터링을 위해 JMX Agent에 전달한다.
Agent
JMX Agent의 가장 중요한 요소는 MBean들이 등록되는 핵심 object인 MBean Server이다. JMAX는 또한 Mbean들을 조작할 수 있는 서비스들을 포함하고 있다.
Remote Management
JMX Agent는 Protocol Adapter와 Connetor을 통해 외부 원격 management application에 접근을 제공한다.

반응형

+ Recent posts