제 홈페이지의 모든 글은 anti-nhn license에 따릅니다.



Google I/O 2011: Java Puzzlers - Scraping the Bottom of the Barrel

이 글은 http://www.youtube.com/watch?v=wbp-3BJWsU8 의 내용을 한글로 정리한 겁니다. 코드는 동영상에 나와있는 것을 거의 그대로 사용합니다.

1. 거스름돈 계산

package googleio2011;

public class Ex1 {
public static void main(String[] args) {
System.out.println(2.00 - 1.10);
}
}

$1.1 짜리 물건을 사는데, $2 를 냈으면 거스름돈은 얼마인가를 묻는 코드입니다. 위 프로그램의 결과 값은 무엇일까요?

0.8999999999999999 이 나옵니다. java에서 double은 정확한 값을 제공하지 않습니다. 따라서 BigDecimal 클래스를 사용하는 게 좋습니다. 그러면 아래 코드의 결과는 어떻게 될까요?

package googleio2011;

import java.math.BigDecimal;

public class Ex1_1 {
public static void main(String[] args) {
BigDecimal payment = new BigDecimal(2.00);
BigDecimal cost = new BigDecimal(1.10);
System.out.println(payment.subtract(cost));
}
}

a> 0.9
b> 0.90
c> 0.8999999999999999
d> 닶 없음.






--------------------------------- 여기서 부터 답 ---------------------------------

정답은 d>이며, 0.899999999999999911182158029987476766109466552734375 이 나옵니다.

이 나옵니다. 여기서 문제는 BigDecimal payment = new BigDecimal(2.00); 에서 constructor의 인자가 2.00 으로 double 값이 들어간 것입니다. 

BigDecimal(double val) 은 double의 값을 BigDecimal로 표현하는 것 뿐이기 때문에 이미 double에서 값이 나가리난 겁니다.


* float 이나 double은 정확한 연산이 필요한 곳에는 쓰이면 안 됩니다.

* new BigDecimal(double)을 쓰지 말고, new BigDecimal(String) 으로 해야 정확한 값이 나옵니다.


2. 사이즈 문제


package googleio2011;

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

public class Size {
enum Sex {
MALE, FEMALE;
}
public static void main(String[] args) {
System.out.println(size(new HashMap<Sex, Sex>()));  -- line 14
System.out.println(size(new EnumMap<Sex, Sex>(Sex.class))); -- line 15
}
public static int size(Map<Sex, Sex> map) {
map.put(Sex.MALE, Sex.FEMALE);
map.put(Sex.FEMALE, Sex.MALE);
map.put(Sex.MALE, Sex.MALE);
map.put(Sex.FEMALE, Sex.FEMALE);
Set<Map.Entry<Sex, Sex>> set = new HashSet<Map.Entry<Sex, Sex>>(map.entrySet());  -- line 22
return set.size();
}
}

a> 2 1
b> 2 2
c> 4 4
d> 닶 없음






--------------------------------- 여기서 부터 답 ---------------------------------

정답은 a>

line 14의 HashMap의 경우는 2가 나오는 게 당연한데, line 15의 EnumMap의 경우 1이 나오는 게 이상합니다.

일단 line 22의 public HashSet(Collection<? extends E> c) 생성자를 까보면 

addAll(c);

와 같이 되어있습니다. 즉 addAll 함수를 호출하고 addAll 함수를 다시 까보면 아래와 같습니다.

    public boolean addAll(Collection<? extends E> c) {
boolean modified = false;
Iterator<? extends E> e = c.iterator();
while (e.hasNext()) {
   if (add(e.next()))
modified = true;
}
return modified;
    }

결국 iterator를 불러서 한놈씩 돌면서 HashSet에 박아주는 겁니다.


EnumMap.entrySet() 을 까보면 

    private class EntrySet extends AbstractSet<Map.Entry<K,V>> {
        public Iterator<Map.Entry<K,V>> iterator() {
            return new EntryIterator();
        }

위와 같이 되어있고 EnumMap.EntryIterator 를 까보면 


    private class EntryIterator extends EnumMapIterator<Map.Entry<K,V>>
        implements Map.Entry<K,V>
    {
        public Map.Entry<K,V> next() {
            if (!hasNext())
                throw new NoSuchElementException();
            lastReturnedIndex = index++;
            return this;
        }

와 같이 되어있습니다.

뭔소리냐 하면 보통 EntryIterator.next() 하면 Entry  가 새로 만들어져서 return 되는 게 일반적인 방식입니다. 그러나 EnumMap의 EntryIterator는 implements Map.Entry 를 하고 있으며, next() 메쏘드는 return this 로 하고 있습니다. 즉, Iterator 자체가 Entry역할까지 겸하며, Entry 자체는 1개만 만들어 지고, 그 Entry 안에서 Iterator의 next가 호출될 때마다 순서값만 변경을 시키며, Entry. getKey() 나 getValue()가 호출될 때는 순서값을 가지고 자신을 감싸고 있는 EnumMap에 가서 그 순서에 맞는 Key나 Value를 가져다가 return합니다. 이렇게 하면 외부에서는 계속 새로운 Entry를 뽑아내는 것 같은 모양새를 유지하면서 객체를 새로 생성하지 않을 수 있습니다.

IdenticalHashMap과 EnumMap이 이런 식으로 구현되어있고, ConcurrentHashMap도 예전에는 그랬었다고 합니다.

이 문제를 해결하려면 line  22 를 아래와 같이 바꿔서 iterator 돌면서 Entry를 새로 만들어내면 됩니다.


Set<Map.Entry<Sex, Sex>> set  = new HashSet<Map.Entry<Sex, Sex>>();
for(Map.Entry<Sex,Sex> e : map.entrySet())
set.add(new AbstractMap.SimpleImmutableEntry<Sex,Sex)(e));


3. Match Game


package googleio2011;

import java.util.regex.Pattern;

public class Match {
public static void main(String[] args) {
Pattern p = Pattern.compile("(aa|aab?)+");
int count = 0;
for(String s = ""; s.length() < 200 ; s+="a")
if (p.matcher(s).matches()) 
count++;
System.out.println(count);
}
}

정규식에 관련된 문제입니다.

a> 99
b> 1000
c> exception 발생
d> 닶 없음





--------------------------------- 여기서 부터 답 ---------------------------------

정답은 d> 연산 결과가 나오려면 10^15 년 걸린다고 합니다.

문제는 (aa|aab?)+ 라는 정규식 입니다. aa|aab? 는 aab? 와 같은 표현식입니다. 정규식에 대해 이해가 잘 안 가시면 요기를..

자바 뿐만 아니라 많은 언어에서 두가지 이상의 방법으로 동일한 것을 체크하는 경우 이런 식의 과부하가 발생한다고 합니다.


4. That Sinking Feeling


package googleio2011;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

abstract class Sink<T>{
abstract void add(T... elements);
void addUnlessNulll(T... elements){  //line 10
for (T element : elements) {
if (element != null) {
add(element);   // line 13
}
}
}
}

public class StringSink extends Sink<String>{
private final List<String> list = new ArrayList<String>();
void add(String... elements){  //line 21
list.addAll(Arrays.asList(elements));
}
public String toString(){
return list.toString();
}
public static void main(String[] args) {
Sink<String> ss = new StringSink();
ss.addUnlessNulll("null", null);  // line 29
System.out.println(ss);
}
}

a> [null]
b> [null, null]
c> NullPointerException
d> 닶 없음.





--------------------------------- 여기서 부터 답 ---------------------------------

정답은 d>이며 ClassCastException이 뜹니다.

에러 결과를 보면 아래와 같습니다.

Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
at googleio2011.StringSink.add(StringSink.java:1)
at googleio2011.Sink.addUnlessNulll(StringSink.java:13)
at googleio2011.StringSink.main(StringSink.java:29)

여기서 재미있는 부분은 error stack에서 1번째 줄에서 뭔가 나왔다는 겁니다. 1번째 줄은 패키지 선언입니다. stack trace에 들어갈 이유가 없는 곳이죠!

3가지 포인트가 있는데, 첫째는 Varargs ( T... 으로 표현된 부분) , 둘째는 erasure (generic에서 생기는 거. 자세한 건 요기), 세째는 bridge method 입니다.

line 29에서  Varargs 는 내부적으로 갯수가 변하는 변수로 처리하는 것이 아니라 Array로 처리한답니다. 따라서 line 29에서 String[] 이 생깁니다. 즉 new String[]{"null", null} 이 생성된다고 보시면 됩니다. 
line 10에서 T... 으로 받는 것은 사실은 그냥 Object[] 로 받는 것입니다. erasure 로 처리되는 거죠. 그래서 line 13에서 element는 String이 아니라 사실 Object입니다. 
그런데, Sink<T>에서 정의된 add method를 보면
abstract void add(T... elements) 라고 되어있습니다. 이것을 상속받아 구현한 method는 21번째 줄에서 void add(String ...)으로 되어있습니다. 
line 13에서는 line 29에서 String[]가 생성된 것과 마찬가지로 Object[]가 생성됩니다. 이게 결국 line 21의 method로 전달되어야 하는데 Object[]를 String[]으로 전달할 수는 없습니다. 이런 이유로 코드 상에서는 존재하지 않지만 java가 내부적으로 메쏘드를 생성하는데, 이게 bridge method입니다.

void add(Object[] args){   -- 여기가 실제로 abstract add(T... elements)를 override하는 부분이며,
add( (String[]) args); -- 여기서 실제로 존재하는 메쏘드인 void add(String ... element) 를 호출하도록 연결 시켜줍니다.
}

이 bridge method 안 쪽에서 Object[]를 String[]으로 바꾸려는 시도를 하게 된 것입니다. 실제로 존재하지 않는 코드이기 때문에 error stack에서는 line1 이라는 이상한 숫자가 찍히거구요. 

실제로 이클립스를 통해서 코드를 만들면 13번째 줄에서 "Type safety : A generic array of T is created for varargs parameter" 라는 warning이 뜹니다. 

이걸 해결하는 좋은 방법은 varargs와 array를 전부 collection으로 뜯어 고치는 것이랩니다.

수정된 코드는 아래와 같습니다.

abstract class Sink2<T>{
abstract void add(Collection<T> elements);
void addUnlessNulll(Collection<T>  elements){
for (T element : elements) {
if (element != null) {
add(Collections.singleton(element));
}
}
}
}

public class StringSink2 extends Sink2<String>{
private final List<String> list = new ArrayList<String>();
void add(Collection<String>  elements){
list.addAll(elements);
}
public String toString(){
return list.toString();
}
public static void main(String[] args) {
Sink2<String> ss = new StringSink2();
ss.addUnlessNulll(Arrays.asList( "null", null));
System.out.println(ss);
}
}


교훈 

1. varargs 는 쫌 조심해서 써라.
2. Generics랑 array는 별로 사이가 안 좋다. 따라서 generic이랑 varargs도 사이가 안 좋다. 
3. 긍까 array 같은 거 쓰지말고, collection 으로 가자. 특히 API 만들때는!!!
4. 컴파일러 말씀을 잘 듣자. 컴팔러가 괜히 지랄하는 게 아니다.!! 죽었다 깨어나도 확실하다 싶은 부분 에다가 @SuppressWarnings annotation을 쓰자. 



5. Glommer pile

package googleio2011;

import java.util.Arrays;
import java.util.Collection;
import java.util.List;

public class Glommer<T> {   // line 7
String glom(Collection<?> objs){ // line 8
String result = "";
for (Object o : objs) { //line 10
result += o;
}
return result;
}
int glom(List<Integer> ints){ // line 15
int result = 0;
for (int i : ints) { // line 17
result += i; 
}
return result;
}
public static void main(String[] args) {
List<String> strings = Arrays.asList("1", "2","3");
System.out.println(new Glommer().glom(strings));  // line 24
}
}

a> 6
b> 123
c> exception
d> 답 없음





--------------------------------- 여기서 부터 답 ---------------------------------

c 입니다. 앞의 문제와 비슷한 것입니다. line 24에서 generic type이 지정되지 않은 것이 문제 입니다. 에러는 아래와 같습니다.

Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
at googleio2011.Glommer.glom(Glommer.java:17)
at googleio2011.Glommer.main(Glommer.java:24)

일단 raw type이 들어오면 (즉, line 24에서 generic type을 지정하지 않으면 ) line 7, 8, 15, 23에 있는 generic parameter 들은 홀라당 날아갑니다. 

결국 glom(List ) 와 glom(Collection) 으로만 선택하려고 하면 List로 가는 게 맞죠. 따라서 glom(List<Integer ints) 안으로 List<String>이 들어가서 ClassCastException으로 이어지는 거죠.


그런데 잘 보면 line 7에서 지정한 T는 쓰지도 않습니다. 따라서 다음과 같은 해법들이 가능합니다.

1. line 24에서 new Glommer<아무거나>().glom(.... ) 으로 바꿔줍니다.
2. line 7에서 generic parameter를 날려 버립니다.
3. 2개의 glom method는 사실 아무런 멤버 변수에 접근하지 않습니다. 따라서 그냥 static method로 정의해 버립니다. 호출할 때도 객체 생성하지 말구요.

그래서 교훈은...

1. raw type을 쓰지 말자.
2. raw type 쓰면 generic type 정보는 홀라당 날아간다~
3. 컴팔러가 뭐라고 하는 건 다 이유가 있는 거다. 말 좀 잘 들어줘라.


6번 째꺼 있는데 별 내용 아닙니다. 대충 정리를 하자면.,,

첫째는 숫자 1과 영어 소문자 l 을 헤깔리게 써놓고 장난질하지 말자.. (컴팔러는 에러를 안 냅니다!!!! 그냥 long이려니 하고 말죠.)
둘짜는 숫자 01234 라고 쓰면 1234와 는 달리 8진수로 처리해서 10진수로 668이 된다는 얘깁니다. System.out.println(01234); 해보면 668찍힙니다.


by 삼실청년 | 2013/05/06 06:06 | 컴터질~ | 트랙백(1) | 덧글(2)

트랙백 주소 : http://iilii.egloos.com/tb/5742008
☞ 내 이글루에 이 글과 관련된 글 쓰기 (트랙백 보내기) [도움말]
Tracked from at 2014/03/11 00:42

제목 : buy garcinia cambogia
line4...more

Commented by ryukato at 2013/06/06 00:54
재밌고 유용한 퍼즐이내요. 잼있게 잘 봤습니다. ^^
Commented by 삼실청년 at 2013/07/07 07:02
원본 강의하신 분이 원체 유명하신 분이라 내용은 좋더라구요. 제가 뻘짓해서 원본의 가치를 떨어뜨리는 게 아닐까 걱정스럽습니다.

:         :

:

비공개 덧글

◀ 이전 페이지          다음 페이지 ▶